Replace magic numbers with MessageType constants

This commit replaces all hardcoded message type integers (0, 1, 2, 3, 4,
5) with named constants from a new MessageType module, improving code
readability and maintainability across the codebase.
This commit is contained in:
Rodrigo Rodriguez (Pragmatismo) 2025-11-28 18:15:09 -03:00
parent 2dca1664dd
commit 752d455312
9 changed files with 302 additions and 157 deletions

View file

@ -1,3 +1,4 @@
use crate::shared::message_types::MessageType;
use crate::shared::models::{BotResponse, UserSession};
use crate::shared::state::AppState;
use log::{error, trace};
@ -84,7 +85,7 @@ pub async fn execute_talk(
session_id: user_session.id.to_string(),
channel: "web".to_string(),
content: message,
message_type: 1,
message_type: MessageType::USER,
stream_token: None,
is_complete: true,
suggestions,

View file

@ -1,6 +1,7 @@
use crate::core::bot::channels::{
instagram::InstagramAdapter, teams::TeamsAdapter, whatsapp::WhatsAppAdapter, ChannelAdapter,
};
use crate::shared::message_types::MessageType;
use crate::shared::models::UserSession;
use crate::shared::state::AppState;
use log::{error, trace};
@ -200,7 +201,7 @@ async fn send_message_to_recipient(
user_id: recipient_id.clone(),
channel: "whatsapp".to_string(),
content: message.to_string(),
message_type: 0,
message_type: MessageType::EXTERNAL,
stream_token: None,
is_complete: true,
suggestions: vec![],
@ -218,7 +219,7 @@ async fn send_message_to_recipient(
user_id: recipient_id.clone(),
channel: "instagram".to_string(),
content: message.to_string(),
message_type: 0,
message_type: MessageType::EXTERNAL,
stream_token: None,
is_complete: true,
suggestions: vec![],
@ -236,7 +237,7 @@ async fn send_message_to_recipient(
user_id: recipient_id.clone(),
channel: "teams".to_string(),
content: message.to_string(),
message_type: 0,
message_type: MessageType::EXTERNAL,
stream_token: None,
is_complete: true,
suggestions: vec![],
@ -588,7 +589,7 @@ async fn send_web_message(
session_id: session_id.to_string(),
channel: "web".to_string(),
content: message.to_string(),
message_type: 1,
message_type: MessageType::USER,
stream_token: None,
is_complete: true,
suggestions: Vec::new(),

View file

@ -1,113 +1,114 @@
use crate::shared::message_types::MessageType;
use crate::shared::models::BotResponse;
use crate::shared::state::AppState;
use color_eyre::Result;
use std::sync::Arc;
use crate::shared::state::AppState;
use crate::shared::models::BotResponse;
use tokio::sync::mpsc;
use uuid::Uuid;
pub struct ChatPanel {
pub messages: Vec<String>,
pub input_buffer: String,
pub session_id: Uuid,
pub user_id: Uuid,
pub response_rx: Option<mpsc::Receiver<BotResponse>>,
pub messages: Vec<String>,
pub input_buffer: String,
pub session_id: Uuid,
pub user_id: Uuid,
pub response_rx: Option<mpsc::Receiver<BotResponse>>,
}
impl ChatPanel {
pub fn new(_app_state: Arc<AppState>) -> Self {
Self {
messages: vec!["Welcome to General Bots Console Chat!".to_string()],
input_buffer: String::new(),
session_id: Uuid::new_v4(),
user_id: Uuid::new_v4(),
response_rx: None,
}
}
pub fn add_char(&mut self, c: char) {
self.input_buffer.push(c);
}
pub fn backspace(&mut self) {
self.input_buffer.pop();
}
pub async fn send_message(&mut self, bot_name: &str, app_state: &Arc<AppState>) -> Result<()> {
if self.input_buffer.trim().is_empty() {
return Ok(());
}
let message = self.input_buffer.clone();
self.messages.push(format!("You: {}", message));
self.input_buffer.clear();
let bot_id = self.get_bot_id(bot_name, app_state).await?;
let user_message = crate::shared::models::UserMessage {
bot_id: bot_id.to_string(),
user_id: self.user_id.to_string(),
session_id: self.session_id.to_string(),
channel: "console".to_string(),
content: message,
message_type: 1,
media_url: None,
timestamp: chrono::Utc::now(),
context_name: None,
};
let (tx, rx) = mpsc::channel::<BotResponse>(100);
self.response_rx = Some(rx);
let orchestrator = crate::bot::BotOrchestrator::new(app_state.clone());
let _ = orchestrator.stream_response(user_message, tx).await;
Ok(())
}
pub async fn poll_response(&mut self, _bot_name: &str) -> Result<()> {
if let Some(rx) = &mut self.response_rx {
while let Ok(response) = rx.try_recv() {
if !response.content.is_empty() && !response.is_complete {
if let Some(last_msg) = self.messages.last_mut() {
if last_msg.starts_with("Bot: ") {
last_msg.push_str(&response.content);
} else {
self.messages.push(format!("Bot: {}", response.content));
}
} else {
self.messages.push(format!("Bot: {}", response.content));
}
}
if response.is_complete && response.content.is_empty() {
break;
}
}
}
Ok(())
}
async fn get_bot_id(&self, bot_name: &str, app_state: &Arc<AppState>) -> Result<Uuid> {
use crate::shared::models::schema::bots::dsl::*;
use diesel::prelude::*;
let mut conn = app_state.conn.get().unwrap();
let bot_id = bots
.filter(name.eq(bot_name))
.select(id)
.first::<Uuid>(&mut *conn)?;
Ok(bot_id)
}
pub fn render(&self) -> String {
let mut lines = Vec::new();
lines.push("╔═══════════════════════════════════════╗".to_string());
lines.push("║ CONVERSATION ║".to_string());
lines.push("╚═══════════════════════════════════════╝".to_string());
lines.push("".to_string());
let visible_start = if self.messages.len() > 15 {
self.messages.len() - 15
} else {
0
};
for msg in &self.messages[visible_start..] {
if msg.starts_with("You: ") {
lines.push(format!(" {}", msg));
} else if msg.starts_with("Bot: ") {
lines.push(format!(" {}", msg));
} else {
lines.push(format!(" {}", msg));
}
}
lines.push("".to_string());
lines.push("─────────────────────────────────────────".to_string());
lines.push(format!(" > {}_", self.input_buffer));
lines.push("".to_string());
lines.push(" Enter: Send | Tab: Switch Panel".to_string());
lines.join("\n")
}
pub fn new(_app_state: Arc<AppState>) -> Self {
Self {
messages: vec!["Welcome to General Bots Console Chat!".to_string()],
input_buffer: String::new(),
session_id: Uuid::new_v4(),
user_id: Uuid::new_v4(),
response_rx: None,
}
}
pub fn add_char(&mut self, c: char) {
self.input_buffer.push(c);
}
pub fn backspace(&mut self) {
self.input_buffer.pop();
}
pub async fn send_message(&mut self, bot_name: &str, app_state: &Arc<AppState>) -> Result<()> {
if self.input_buffer.trim().is_empty() {
return Ok(());
}
let message = self.input_buffer.clone();
self.messages.push(format!("You: {}", message));
self.input_buffer.clear();
let bot_id = self.get_bot_id(bot_name, app_state).await?;
let user_message = crate::shared::models::UserMessage {
bot_id: bot_id.to_string(),
user_id: self.user_id.to_string(),
session_id: self.session_id.to_string(),
channel: "console".to_string(),
content: message,
message_type: MessageType::USER,
media_url: None,
timestamp: chrono::Utc::now(),
context_name: None,
};
let (tx, rx) = mpsc::channel::<BotResponse>(100);
self.response_rx = Some(rx);
let orchestrator = crate::bot::BotOrchestrator::new(app_state.clone());
let _ = orchestrator.stream_response(user_message, tx).await;
Ok(())
}
pub async fn poll_response(&mut self, _bot_name: &str) -> Result<()> {
if let Some(rx) = &mut self.response_rx {
while let Ok(response) = rx.try_recv() {
if !response.content.is_empty() && !response.is_complete {
if let Some(last_msg) = self.messages.last_mut() {
if last_msg.starts_with("Bot: ") {
last_msg.push_str(&response.content);
} else {
self.messages.push(format!("Bot: {}", response.content));
}
} else {
self.messages.push(format!("Bot: {}", response.content));
}
}
if response.is_complete && response.content.is_empty() {
break;
}
}
}
Ok(())
}
async fn get_bot_id(&self, bot_name: &str, app_state: &Arc<AppState>) -> Result<Uuid> {
use crate::shared::models::schema::bots::dsl::*;
use diesel::prelude::*;
let mut conn = app_state.conn.get().unwrap();
let bot_id = bots
.filter(name.eq(bot_name))
.select(id)
.first::<Uuid>(&mut *conn)?;
Ok(bot_id)
}
pub fn render(&self) -> String {
let mut lines = Vec::new();
lines.push("╔═══════════════════════════════════════╗".to_string());
lines.push("║ CONVERSATION ║".to_string());
lines.push("╚═══════════════════════════════════════╝".to_string());
lines.push("".to_string());
let visible_start = if self.messages.len() > 15 {
self.messages.len() - 15
} else {
0
};
for msg in &self.messages[visible_start..] {
if msg.starts_with("You: ") {
lines.push(format!(" {}", msg));
} else if msg.starts_with("Bot: ") {
lines.push(format!(" {}", msg));
} else {
lines.push(format!(" {}", msg));
}
}
lines.push("".to_string());
lines.push("─────────────────────────────────────────".to_string());
lines.push(format!(" > {}_", self.input_buffer));
lines.push("".to_string());
lines.push(" Enter: Send | Tab: Switch Panel".to_string());
lines.join("\n")
}
}

View file

@ -7,6 +7,7 @@ use crate::llm::llm_models;
use crate::llm::OpenAIClient;
#[cfg(feature = "nvidia")]
use crate::nvidia::get_system_metrics;
use crate::shared::message_types::MessageType;
use crate::shared::models::{BotResponse, UserMessage, UserSession};
use crate::shared::state::AppState;
use axum::extract::ws::{Message, WebSocket};
@ -209,7 +210,7 @@ impl BotOrchestrator {
session_id: message.session_id.clone(),
channel: message.channel.clone(),
content: processed,
message_type: 2,
message_type: MessageType::BOT_RESPONSE,
stream_token: None,
is_complete: false,
suggestions: Vec::new(),
@ -246,7 +247,7 @@ impl BotOrchestrator {
session_id: message.session_id.clone(),
channel: message.channel.clone(),
content: processed,
message_type: 2,
message_type: MessageType::BOT_RESPONSE,
stream_token: None,
is_complete: false,
suggestions: Vec::new(),
@ -281,7 +282,7 @@ impl BotOrchestrator {
session_id: message.session_id.clone(),
channel: message.channel.clone(),
content: chunk,
message_type: 2,
message_type: MessageType::BOT_RESPONSE,
stream_token: None,
is_complete: false,
suggestions: Vec::new(),
@ -314,7 +315,7 @@ impl BotOrchestrator {
session_id: message.session_id,
channel: message.channel,
content: full_response,
message_type: 2,
message_type: MessageType::BOT_RESPONSE,
stream_token: None,
is_complete: true,
suggestions: Vec::new(),

View file

@ -11,6 +11,7 @@
//! - Storage abstraction for S3-compatible backends
//! - URL processing and validation
use crate::shared::message_types::MessageType;
use crate::shared::models::{BotResponse, UserMessage};
use anyhow::Result;
use async_trait::async_trait;
@ -154,7 +155,7 @@ impl MultimediaHandler for DefaultMultimediaHandler {
session_id: session_id.to_string(),
channel: "multimedia".to_string(),
content,
message_type: 0,
message_type: MessageType::EXTERNAL,
stream_token: None,
is_complete: true,
suggestions: Vec::new(),
@ -182,7 +183,7 @@ impl MultimediaHandler for DefaultMultimediaHandler {
session_id: session_id.to_string(),
channel: "multimedia".to_string(),
content: response_content,
message_type: 0,
message_type: MessageType::EXTERNAL,
stream_token: None,
is_complete: true,
suggestions: Vec::new(),
@ -215,7 +216,7 @@ impl MultimediaHandler for DefaultMultimediaHandler {
session_id: session_id.to_string(),
channel: "multimedia".to_string(),
content: response_content,
message_type: 0,
message_type: MessageType::EXTERNAL,
stream_token: None,
is_complete: true,
suggestions: Vec::new(),
@ -248,7 +249,7 @@ impl MultimediaHandler for DefaultMultimediaHandler {
session_id: session_id.to_string(),
channel: "multimedia".to_string(),
content: response_content,
message_type: 0,
message_type: MessageType::EXTERNAL,
stream_token: None,
is_complete: true,
suggestions: Vec::new(),
@ -277,7 +278,7 @@ impl MultimediaHandler for DefaultMultimediaHandler {
session_id: session_id.to_string(),
channel: "multimedia".to_string(),
content: response_content,
message_type: 0,
message_type: MessageType::EXTERNAL,
stream_token: None,
is_complete: true,
suggestions: Vec::new(),
@ -294,7 +295,7 @@ impl MultimediaHandler for DefaultMultimediaHandler {
session_id: session_id.to_string(),
channel: "multimedia".to_string(),
content: "Message received and processing...".to_string(),
message_type: 0,
message_type: MessageType::EXTERNAL,
stream_token: None,
is_complete: true,
suggestions: Vec::new(),

View file

@ -1,5 +1,6 @@
pub mod admin;
pub mod analytics;
pub mod message_types;
pub mod models;
pub mod state;
pub mod utils;

View file

@ -1,3 +1,4 @@
use crate::shared::message_types::MessageType;
use chrono::{DateTime, Utc};
use diesel::prelude::*;
use serde::{Deserialize, Serialize};
@ -51,7 +52,7 @@ pub struct UserMessage {
pub session_id: String,
pub channel: String,
pub content: String,
pub message_type: i32,
pub message_type: MessageType,
pub media_url: Option<String>,
pub timestamp: DateTime<Utc>,
pub context_name: Option<String>,
@ -68,7 +69,7 @@ pub struct BotResponse {
pub session_id: String,
pub channel: String,
pub content: String,
pub message_type: i32,
pub message_type: MessageType,
pub stream_token: Option<String>,
pub is_complete: bool,
pub suggestions: Vec<Suggestion>,
@ -90,7 +91,7 @@ impl BotResponse {
session_id: session_id.to_string(),
channel,
content,
message_type: 2,
message_type: MessageType::BOT_RESPONSE,
stream_token: None,
is_complete: true,
suggestions: Vec::new(),

View file

@ -3,6 +3,7 @@
use crate::basic::compiler::BasicCompiler;
use crate::config::ConfigManager;
use crate::core::kb::KnowledgeBaseManager;
use crate::shared::message_types::MessageType;
use crate::shared::state::AppState;
use aws_sdk_s3::Client;
use log::{debug, error, info};
@ -345,7 +346,7 @@ impl DriveMonitor {
session_id: session_id.clone(),
channel: "web".to_string(),
content: serde_json::to_string(&theme_data)?,
message_type: 2,
message_type: MessageType::BOT_RESPONSE,
stream_token: None,
is_complete: true,
suggestions: Vec::new(),

View file

@ -4,6 +4,17 @@
<meta charset="utf-8" />
<title>General Bots</title>
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
<script>
// Message Type Constants inline since we don't have a /shared route
const MessageType = {
EXTERNAL: 0,
USER: 1,
BOT_RESPONSE: 2,
CONTINUE: 3,
SUGGESTION: 4,
CONTEXT_CHANGE: 5,
};
</script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/gsap.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/livekit-client/dist/livekit-client.umd.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
@ -733,12 +744,21 @@
isUserScrolling = false,
autoScrollEnabled = true,
isContextChange = false;
const maxReconnectAttempts = 5,
messagesDiv = document.getElementById("messages"),
input = document.getElementById("messageInput"),
sendBtn = document.getElementById("sendBtn"),
voiceBtn = document.getElementById("voiceBtn"),
connectionStatus = document.getElementById("connectionStatus"),
const maxReconnectAttempts = 5;
let messagesDiv = document.getElementById("messages");
let input = document.getElementById("messageInput");
let sendBtn = document.getElementById("sendBtn");
let voiceBtn = document.getElementById("voiceBtn");
// Debug element initialization
console.log("Element initialization:");
console.log("messagesDiv:", messagesDiv);
console.log("input:", input);
console.log("sendBtn:", sendBtn);
console.log("voiceBtn:", voiceBtn);
const connectionStatus =
document.getElementById("connectionStatus"),
flashOverlay = document.getElementById("flashOverlay"),
suggestionsContainer = document.getElementById("suggestions"),
floatLogo = document.getElementById("floatLogo"),
@ -878,17 +898,29 @@
async function initializeAuth() {
try {
console.log("Starting auth initialization...");
updateConnectionStatus("connecting");
const p = window.location.pathname
.split("/")
.filter((s) => s),
b = p.length > 0 ? p[0] : "default",
r = await fetch(
`http://localhost:8080/api/auth?bot_name=${encodeURIComponent(b)}`,
b = p.length > 0 ? p[0] : "default";
console.log("Bot name:", b);
const r = await fetch(
`/api/auth?bot_name=${encodeURIComponent(b)}`,
),
a = await r.json();
console.log("Auth response:", a);
currentUserId = a.user_id;
currentSessionId = a.session_id;
currentBotId = a.bot_id || "default_bot";
console.log(
"Auth initialized - User:",
currentUserId,
"Session:",
currentSessionId,
"Bot:",
currentBotId,
);
connectWebSocket();
loadSessions();
} catch (e) {
@ -984,6 +1016,7 @@
const u = getWebSocketUrl();
ws = new WebSocket(u);
ws.onmessage = function (e) {
console.log("WebSocket message received:", e.data);
try {
if (!e.data || e.data.trim() === "") {
console.warn("Empty WebSocket message received");
@ -997,19 +1030,35 @@
if (r.bot_id) {
currentBotId = r.bot_id;
}
if (r.message_type === 2) {
try {
const d = JSON.parse(r.content);
handleEvent(d.event, d.data);
} catch (parseErr) {
console.error(
"Failed to parse event content:",
parseErr,
);
// BOT_RESPONSE type is used for both regular streaming content and special event messages
// Event messages have JSON-encoded content with 'event' and 'data' properties
// Regular messages have plain text content that should be displayed directly
if (r.message_type === MessageType.BOT_RESPONSE) {
// Check if content looks like JSON (starts with { or [)
const contentTrimmed = r.content.trim();
if (
contentTrimmed.startsWith("{") ||
contentTrimmed.startsWith("[")
) {
try {
const d = JSON.parse(r.content);
if (d.event && d.data) {
// This is an event message
handleEvent(d.event, d.data);
return;
}
} catch (parseErr) {
// Not a valid event message, treat as regular content
console.debug(
"Content is not an event message, processing as regular message",
);
}
}
// Process as regular message content
processMessageContent(r);
return;
}
if (r.message_type === 5) {
if (r.message_type === MessageType.CONTEXT_CHANGE) {
isContextChange = true;
return;
}
@ -1024,7 +1073,10 @@
}
};
ws.onopen = function () {
console.log("Connected to WebSocket");
console.log(
"Connected to WebSocket, readyState:",
ws.readyState,
);
updateConnectionStatus("connected");
reconnectAttempts = 0;
hasReceivedInitialMessage = false;
@ -1218,7 +1270,7 @@
session_id: currentSessionId,
channel: "web",
content: "continue",
message_type: 3,
message_type: MessageType.CONTINUE,
media_url: null,
timestamp: new Date().toISOString(),
};
@ -1327,7 +1379,8 @@
const h = (e) => {
const d = JSON.parse(e.data);
if (
d.message_type === 5 &&
d.message_type ===
MessageType.CONTEXT_CHANGE &&
d.context_name === c
) {
ws.removeEventListener("message", h);
@ -1341,7 +1394,7 @@
session_id: currentSessionId,
channel: "web",
content: t,
message_type: 4,
message_type: MessageType.SUGGESTION,
is_suggestion: true,
context_name: c,
timestamp: new Date().toISOString(),
@ -1367,12 +1420,42 @@
}
async function sendMessage() {
console.log("=== sendMessage called ===");
console.log("input element:", input);
console.log(
"input.value:",
input ? input.value : "input is null",
);
if (pendingContextChange) {
await pendingContextChange;
pendingContextChange = null;
}
if (!input) {
console.error("Input element is null!");
return;
}
const m = input.value.trim();
if (!m || !ws || ws.readyState !== WebSocket.OPEN) {
console.log(
"Attempting to send message:",
m,
"WS state:",
ws ? ws.readyState : "no ws",
"WebSocket.OPEN value:",
WebSocket.OPEN,
);
if (!m) {
console.log("Message is empty, not sending");
return;
}
if (!ws || ws.readyState !== WebSocket.OPEN) {
console.log(
"WebSocket not connected, attempting reconnect",
);
if (!ws || ws.readyState !== WebSocket.OPEN) {
showWarning(
"Conexão não disponível. Tentando reconectar...",
@ -1384,27 +1467,44 @@
if (isThinking) {
hideThinkingIndicator();
}
console.log("Adding message to UI");
addMessage("user", m);
console.log("Building message data object");
console.log("currentBotId:", currentBotId);
console.log("currentUserId:", currentUserId);
console.log("currentSessionId:", currentSessionId);
const d = {
bot_id: currentBotId,
bot_id: currentBotId || "default_bot",
user_id: currentUserId,
session_id: currentSessionId,
channel: "web",
content: m,
message_type: 1,
message_type: MessageType.USER,
media_url: null,
timestamp: new Date().toISOString(),
};
ws.send(JSON.stringify(d));
console.log("Message data object:", JSON.stringify(d, null, 2));
try {
const messageString = JSON.stringify(d);
console.log("Stringified message:", messageString);
console.log("About to call ws.send()");
ws.send(messageString);
console.log("ws.send() completed successfully");
} catch (error) {
console.error("Error sending message:", error);
console.error("Error stack:", error.stack);
}
console.log("Clearing input field");
input.value = "";
input.focus();
console.log("=== sendMessage completed ===");
}
sendBtn.onclick = sendMessage;
input.addEventListener("keypress", (e) => {
if (e.key === "Enter") sendMessage();
});
async function toggleVoiceMode() {
isVoiceMode = !isVoiceMode;
const v = document.getElementById("voiceToggle");
@ -1569,7 +1669,44 @@
scrollToBottomBtn.classList.remove("visible");
}
window.addEventListener("load", initializeAuth);
window.addEventListener("load", () => {
console.log("Page loaded, initializing...");
// Re-get elements after DOM is ready
messagesDiv = document.getElementById("messages");
input = document.getElementById("messageInput");
sendBtn = document.getElementById("sendBtn");
voiceBtn = document.getElementById("voiceBtn");
console.log("After load - input:", !!input);
console.log("After load - sendBtn:", !!sendBtn);
// Attach event listeners after DOM is ready
if (sendBtn) {
sendBtn.onclick = () => {
console.log("Send button clicked!");
sendMessage();
};
console.log("sendBtn.onclick attached");
} else {
console.error("sendBtn element not found!");
}
if (input) {
input.addEventListener("keypress", (e) => {
console.log("Key pressed:", e.key);
if (e.key === "Enter") {
console.log("Enter key detected, sending message");
sendMessage();
}
});
console.log("input keypress listener attached");
} else {
console.error("input element not found!");
}
initializeAuth();
});
window.addEventListener("focus", function () {
if (!ws || ws.readyState !== WebSocket.OPEN) {
connectWebSocket();