Refactor TALK delivery and streaming pipelines

This commit is contained in:
Rodrigo Rodriguez (Pragmatismo) 2025-11-20 13:40:40 -03:00
parent e146add4b2
commit 382a01658d
4 changed files with 1178 additions and 828 deletions

View file

@ -94,32 +94,19 @@ pub async fn execute_talk(
};
let user_id = user_session.id.to_string();
let response_clone = response.clone();
match state.response_channels.try_lock() {
Ok(response_channels) => {
if let Some(tx) = response_channels.get(&user_id) {
if let Err(e) = tx.try_send(response_clone) {
error!("Failed to send TALK message via WebSocket: {}", e);
} else {
trace!("TALK message sent via WebSocket");
}
} else {
let web_adapter = Arc::clone(&state.web_adapter);
tokio::spawn(async move {
if let Err(e) = web_adapter
.send_message_to_session(&user_id, response_clone)
.await
{
error!("Failed to send TALK message via web adapter: {}", e);
} else {
trace!("TALK message sent via web adapter");
}
});
}
// Use web adapter which handles the connection properly
let web_adapter = Arc::clone(&state.web_adapter);
tokio::spawn(async move {
if let Err(e) = web_adapter
.send_message_to_session(&user_id, response_clone)
.await
{
error!("Failed to send TALK message via web adapter: {}", e);
} else {
trace!("TALK message sent via web adapter");
}
Err(_) => {
error!("Failed to acquire lock on response_channels for TALK command");
}
}
});
Ok(response)
}
pub fn talk_keyword(state: Arc<AppState>, user: UserSession, engine: &mut Engine) {

View file

@ -67,13 +67,156 @@ impl BotOrchestrator {
Ok(())
}
// Placeholder for stream_response used by UI
// Stream response to user via LLM
pub async fn stream_response(
&self,
_user_message: UserMessage,
_response_tx: mpsc::Sender<BotResponse>,
message: UserMessage,
response_tx: mpsc::Sender<BotResponse>,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
// No-op placeholder
trace!(
"Streaming response for user: {}, session: {}",
message.user_id,
message.session_id
);
let user_id = Uuid::parse_str(&message.user_id)?;
let session_id = Uuid::parse_str(&message.session_id)?;
let bot_id = Uuid::parse_str(&message.bot_id).unwrap_or_default();
// All database operations in one blocking section
let (session, context_data, history, model, key) = {
let state_clone = self.state.clone();
tokio::task::spawn_blocking(
move || -> Result<_, Box<dyn std::error::Error + Send + Sync>> {
// Get session
let session = {
let mut sm = state_clone.session_manager.blocking_lock();
sm.get_session_by_id(session_id)?
}
.ok_or_else(|| "Session not found")?;
// Save user message
{
let mut sm = state_clone.session_manager.blocking_lock();
sm.save_message(session.id, user_id, 1, &message.content, 1)?;
}
// Get context and history
let context_data = {
let sm = state_clone.session_manager.blocking_lock();
let rt = tokio::runtime::Handle::current();
rt.block_on(async {
sm.get_session_context_data(&session.id, &session.user_id)
.await
})?
};
let history = {
let mut sm = state_clone.session_manager.blocking_lock();
sm.get_conversation_history(session.id, user_id)?
};
// Get model config
let config_manager = ConfigManager::new(state_clone.conn.clone());
let model = config_manager
.get_config(&bot_id, "llm-model", Some("gpt-3.5-turbo"))
.unwrap_or_else(|_| "gpt-3.5-turbo".to_string());
let key = config_manager
.get_config(&bot_id, "llm-key", Some(""))
.unwrap_or_default();
Ok((session, context_data, history, model, key))
},
)
.await??
};
// Build messages
let system_prompt = std::env::var("SYSTEM_PROMPT")
.unwrap_or_else(|_| "You are a helpful assistant.".to_string());
let messages = OpenAIClient::build_messages(&system_prompt, &context_data, &history);
// Stream from LLM
let (stream_tx, mut stream_rx) = mpsc::channel::<String>(100);
let llm = self.state.llm_provider.clone();
tokio::spawn(async move {
if let Err(e) = llm
.generate_stream("", &messages, stream_tx, &model, &key)
.await
{
error!("LLM streaming error: {}", e);
}
});
let mut full_response = String::new();
let mut chunk_count = 0;
while let Some(chunk) = stream_rx.recv().await {
chunk_count += 1;
info!("Received LLM chunk #{}: {:?}", chunk_count, chunk);
full_response.push_str(&chunk);
let response = BotResponse {
bot_id: message.bot_id.clone(),
user_id: message.user_id.clone(),
session_id: message.session_id.clone(),
channel: message.channel.clone(),
content: chunk,
message_type: 2,
stream_token: None,
is_complete: false,
suggestions: Vec::new(),
context_name: None,
context_length: 0,
context_max_length: 0,
};
info!("Sending streaming chunk to WebSocket");
if let Err(e) = response_tx.send(response).await {
error!("Failed to send streaming chunk: {}", e);
break;
}
}
info!(
"LLM streaming complete, received {} chunks, total length: {}",
chunk_count,
full_response.len()
);
// Send final complete response
let final_response = BotResponse {
bot_id: message.bot_id.clone(),
user_id: message.user_id.clone(),
session_id: message.session_id.clone(),
channel: message.channel.clone(),
content: full_response.clone(),
message_type: 2,
stream_token: None,
is_complete: true,
suggestions: Vec::new(),
context_name: None,
context_length: 0,
context_max_length: 0,
};
info!("Sending final complete response to WebSocket");
response_tx.send(final_response).await?;
info!("Final response sent successfully");
// Save bot response in blocking context
let state_for_save = self.state.clone();
let full_response_clone = full_response.clone();
tokio::task::spawn_blocking(
move || -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let mut sm = state_for_save.session_manager.blocking_lock();
sm.save_message(session.id, user_id, 2, &full_response_clone, 2)?;
Ok(())
},
)
.await??;
Ok(())
}
@ -146,6 +289,12 @@ async fn handle_websocket(
.add_connection(session_id.to_string(), tx.clone())
.await;
// Also register in response_channels for BotOrchestrator
{
let mut channels = state.response_channels.lock().await;
channels.insert(session_id.to_string(), tx.clone());
}
info!(
"WebSocket connected for session: {}, user: {}",
session_id, user_id
@ -232,19 +381,16 @@ async fn handle_websocket(
session_id, user_msg.content
);
// Process the message through the bot system
let state_for_task = state_clone.clone();
tokio::spawn(async move {
if let Err(e) = process_user_message(
state_for_task,
session_id,
user_id,
user_msg,
)
.await
{
error!("Error processing user message: {}", e);
}
});
if let Err(e) = process_user_message(
state_clone.clone(),
session_id,
user_id,
user_msg,
)
.await
{
error!("Error processing user message: {}", e);
}
}
Err(e) => {
error!(
@ -288,6 +434,12 @@ async fn handle_websocket(
.remove_connection(&session_id.to_string())
.await;
// Also remove from response_channels
{
let mut channels = state.response_channels.lock().await;
channels.remove(&session_id.to_string());
}
info!("WebSocket disconnected for session: {}", session_id);
}
@ -303,64 +455,20 @@ async fn process_user_message(
user_id, session_id, user_msg.content
);
// Get the session from the session manager
let session = {
let mut sm = state.session_manager.lock().await;
sm.get_session_by_id(session_id)
.map_err(|e| format!("Session error: {}", e))?
.ok_or("Session not found")?
// Get the response channel for this session
let tx = {
let channels = state.response_channels.lock().await;
channels.get(&session_id.to_string()).cloned()
};
let content = user_msg.content.clone();
let bot_id = session.bot_id;
info!("Sending message to LLM for processing");
// Call the LLM to generate a response
let messages = serde_json::json!([{"role": "user", "content": content}]);
let llm_response = match state
.llm_provider
.generate(&content, &messages, "gpt-3.5-turbo", "")
.await
{
Ok(response) => response,
Err(e) => {
error!("LLM generation failed: {}", e);
format!(
"I'm sorry, I encountered an error processing your message: {}",
e
)
if let Some(response_tx) = tx {
// Use BotOrchestrator to stream the response
let orchestrator = BotOrchestrator::new(state.clone());
if let Err(e) = orchestrator.stream_response(user_msg, response_tx).await {
error!("Failed to stream response: {}", e);
}
};
info!("LLM response received: {}", llm_response);
// Create and send the bot response
let response = BotResponse {
bot_id: bot_id.to_string(),
user_id: user_id.to_string(),
session_id: session_id.to_string(),
channel: "web".to_string(),
content: llm_response,
message_type: 2,
stream_token: None,
is_complete: true,
suggestions: Vec::new(),
context_name: None,
context_length: 0,
context_max_length: 0,
};
// Send response back through WebSocket
info!("Sending response to WebSocket session {}", session_id);
if let Err(e) = state
.web_adapter
.send_message_to_session(&session_id.to_string(), response)
.await
{
error!("Failed to send LLM response: {:?}", e);
} else {
info!("Response sent successfully to session {}", session_id);
error!("No response channel found for session {}", session_id);
}
Ok(())

File diff suppressed because it is too large Load diff

View file

@ -1,36 +1,50 @@
function chatApp() {
// Core state variables (shared via closure)
let ws = null,
pendingContextChange = null,o
currentSessionId = null,
currentUserId = null,
currentBotId = "default_bot",
isStreaming = false,
voiceRoom = null,
isVoiceMode = false,
mediaRecorder = null,
audioChunks = [],
streamingMessageId = null,
isThinking = false,
currentStreamingContent = "",
hasReceivedInitialMessage = false,
reconnectAttempts = 0,
reconnectTimeout = null,
thinkingTimeout = null,
currentTheme = 'auto',
themeColor1 = null,
themeColor2 = null,
customLogoUrl = null,
contextUsage = 0,
isUserScrolling = false,
autoScrollEnabled = true,
isContextChange = false;
pendingContextChange = null,
o;
((currentSessionId = null),
(currentUserId = null),
(currentBotId = "default_bot"),
(isStreaming = false),
(voiceRoom = null),
(isVoiceMode = false),
(mediaRecorder = null),
(audioChunks = []),
(streamingMessageId = null),
(isThinking = false),
(currentStreamingContent = ""),
(hasReceivedInitialMessage = false),
(reconnectAttempts = 0),
(reconnectTimeout = null),
(thinkingTimeout = null),
(currentTheme = "auto"),
(themeColor1 = null),
(themeColor2 = null),
(customLogoUrl = null),
(contextUsage = 0),
(isUserScrolling = false),
(autoScrollEnabled = true),
(isContextChange = false));
const maxReconnectAttempts = 5;
// DOM references (cached for performance)
let messagesDiv, messageInputEl, sendBtn, voiceBtn, connectionStatus, flashOverlay, suggestionsContainer, floatLogo, sidebar, themeBtn, scrollToBottomBtn, contextIndicator, contextPercentage, contextProgressBar, sidebarTitle;
let messagesDiv,
messageInputEl,
sendBtn,
voiceBtn,
connectionStatus,
flashOverlay,
suggestionsContainer,
floatLogo,
sidebar,
themeBtn,
scrollToBottomBtn,
contextIndicator,
contextPercentage,
contextProgressBar,
sidebarTitle;
marked.setOptions({ breaks: true, gfm: true });
@ -38,25 +52,60 @@ function chatApp() {
// ----------------------------------------------------------------------
// UI state (mirrors the structure used in driveApp)
// ----------------------------------------------------------------------
current: 'All Chats',
search: '',
current: "All Chats",
search: "",
selectedChat: null,
navItems: [
{ name: 'All Chats', icon: '💬' },
{ name: 'Direct', icon: '👤' },
{ name: 'Groups', icon: '👥' },
{ name: 'Archived', icon: '🗄' }
{ name: "All Chats", icon: "💬" },
{ name: "Direct", icon: "👤" },
{ name: "Groups", icon: "👥" },
{ name: "Archived", icon: "🗄" },
],
chats: [
{ id: 1, name: 'General Bot Support', icon: '🤖', lastMessage: 'How can I help you?', time: '10:15 AM', status: 'Online' },
{ id: 2, name: 'Project Alpha', icon: '🚀', lastMessage: 'Launch scheduled for tomorrow.', time: 'Yesterday', status: 'Active' },
{ id: 3, name: 'Team Standup', icon: '🗣️', lastMessage: 'Done with the UI updates.', time: '2 hrs ago', status: 'Active' },
{ id: 4, name: 'Random Chat', icon: '🎲', lastMessage: 'Did you see the game last night?', time: '5 hrs ago', status: 'Idle' },
{ id: 5, name: 'Support Ticket #1234', icon: '🛠️', lastMessage: 'Issue resolved, closing ticket.', time: '3 days ago', status: 'Closed' }
{
id: 1,
name: "General Bot Support",
icon: "🤖",
lastMessage: "How can I help you?",
time: "10:15 AM",
status: "Online",
},
{
id: 2,
name: "Project Alpha",
icon: "🚀",
lastMessage: "Launch scheduled for tomorrow.",
time: "Yesterday",
status: "Active",
},
{
id: 3,
name: "Team Standup",
icon: "🗣️",
lastMessage: "Done with the UI updates.",
time: "2 hrs ago",
status: "Active",
},
{
id: 4,
name: "Random Chat",
icon: "🎲",
lastMessage: "Did you see the game last night?",
time: "5 hrs ago",
status: "Idle",
},
{
id: 5,
name: "Support Ticket #1234",
icon: "🛠️",
lastMessage: "Issue resolved, closing ticket.",
time: "3 days ago",
status: "Closed",
},
],
get filteredChats() {
return this.chats.filter(chat =>
chat.name.toLowerCase().includes(this.search.toLowerCase())
return this.chats.filter((chat) =>
chat.name.toLowerCase().includes(this.search.toLowerCase()),
);
},
@ -64,34 +113,45 @@ function chatApp() {
// UI helpers (formerly standalone functions)
// ----------------------------------------------------------------------
toggleSidebar() {
sidebar.classList.toggle('open');
sidebar.classList.toggle("open");
},
toggleTheme() {
const themes = ['auto', 'dark', 'light'];
const savedTheme = localStorage.getItem('gb-theme') || 'auto';
const themes = ["auto", "dark", "light"];
const savedTheme = localStorage.getItem("gb-theme") || "auto";
const idx = themes.indexOf(savedTheme);
const newTheme = themes[(idx + 1) % themes.length];
localStorage.setItem('gb-theme', newTheme);
localStorage.setItem("gb-theme", newTheme);
currentTheme = newTheme;
this.applyTheme();
this.updateThemeButton();
},
applyTheme() {
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
const prefersDark = window.matchMedia(
"(prefers-color-scheme: dark)",
).matches;
let theme = currentTheme;
if (theme === 'auto') {
theme = prefersDark ? 'dark' : 'light';
if (theme === "auto") {
theme = prefersDark ? "dark" : "light";
}
document.documentElement.setAttribute('data-theme', theme);
document.documentElement.setAttribute("data-theme", theme);
if (themeColor1 && themeColor2) {
const root = document.documentElement;
root.style.setProperty('--bg', theme === 'dark' ? themeColor2 : themeColor1);
root.style.setProperty('--fg', theme === 'dark' ? themeColor1 : themeColor2);
root.style.setProperty(
"--bg",
theme === "dark" ? themeColor2 : themeColor1,
);
root.style.setProperty(
"--fg",
theme === "dark" ? themeColor1 : themeColor2,
);
}
if (customLogoUrl) {
document.documentElement.style.setProperty('--logo-url', `url('${customLogoUrl}')`);
document.documentElement.style.setProperty(
"--logo-url",
`url('${customLogoUrl}')`,
);
}
},
@ -99,7 +159,7 @@ function chatApp() {
// Lifecycle / event handlers
// ----------------------------------------------------------------------
init() {
window.addEventListener('load', () => {
window.addEventListener("load", () => {
// Assign DOM elements after the document is ready
messagesDiv = document.getElementById("messages");
@ -119,40 +179,44 @@ function chatApp() {
sidebarTitle = document.getElementById("sidebarTitle");
// Theme initialization and focus
const savedTheme = localStorage.getItem('gb-theme') || 'auto';
const savedTheme = localStorage.getItem("gb-theme") || "auto";
currentTheme = savedTheme;
this.applyTheme();
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => {
if (currentTheme === 'auto') {
this.applyTheme();
}
});
window
.matchMedia("(prefers-color-scheme: dark)")
.addEventListener("change", () => {
if (currentTheme === "auto") {
this.applyTheme();
}
});
if (messageInputEl) {
messageInputEl.focus();
}
// UI event listeners
document.addEventListener('click', (e) => {
document.addEventListener("click", (e) => {});
});
messagesDiv.addEventListener('scroll', () => {
const isAtBottom = messagesDiv.scrollHeight - messagesDiv.scrollTop <= messagesDiv.clientHeight + 100;
messagesDiv.addEventListener("scroll", () => {
const isAtBottom =
messagesDiv.scrollHeight - messagesDiv.scrollTop <=
messagesDiv.clientHeight + 100;
if (!isAtBottom) {
isUserScrolling = true;
scrollToBottomBtn.classList.add('visible');
scrollToBottomBtn.classList.add("visible");
} else {
isUserScrolling = false;
scrollToBottomBtn.classList.remove('visible');
scrollToBottomBtn.classList.remove("visible");
}
});
scrollToBottomBtn.addEventListener('click', () => {
scrollToBottomBtn.addEventListener("click", () => {
this.scrollToBottom();
});
sendBtn.onclick = () => this.sendMessage();
messageInputEl.addEventListener("keypress", e => { if (e.key === "Enter") this.sendMessage(); });
messageInputEl.addEventListener("keypress", (e) => {
if (e.key === "Enter") this.sendMessage();
});
window.addEventListener("focus", () => {
if (!ws || ws.readyState !== WebSocket.OPEN) {
this.connectWebSocket();
@ -169,13 +233,17 @@ function chatApp() {
const p = Math.min(100, Math.round(u * 100));
contextPercentage.textContent = `${p}%`;
contextProgressBar.style.width = `${p}%`;
contextIndicator.classList.remove('visible');
contextIndicator.classList.remove("visible");
},
flashScreen() {
gsap.to(flashOverlay, { opacity: 0.15, duration: 0.1, onComplete: () => {
gsap.to(flashOverlay, { opacity: 0, duration: 0.2 });
} });
gsap.to(flashOverlay, {
opacity: 0.15,
duration: 0.1,
onComplete: () => {
gsap.to(flashOverlay, { opacity: 0, duration: 0.2 });
},
});
},
updateConnectionStatus(s) {
@ -183,16 +251,20 @@ function chatApp() {
},
getWebSocketUrl() {
const p = "ws:", s = currentSessionId || crypto.randomUUID(), u = currentUserId || crypto.randomUUID();
const p = "ws:",
s = currentSessionId || crypto.randomUUID(),
u = currentUserId || crypto.randomUUID();
return `${p}//localhost:8080/ws?session_id=${s}&user_id=${u}`;
},
async initializeAuth() {
try {
this.updateConnectionStatus("connecting");
const p = window.location.pathname.split('/').filter(s => s);
const b = p.length > 0 ? p[0] : 'default';
const r = await fetch(`http://localhost:8080/api/auth?bot_name=${encodeURIComponent(b)}`);
const p = window.location.pathname.split("/").filter((s) => s);
const b = p.length > 0 ? p[0] : "default";
const r = await fetch(
`http://localhost:8080/api/auth?bot_name=${encodeURIComponent(b)}`,
);
const a = await r.json();
currentUserId = a.user_id;
currentSessionId = a.session_id;
@ -210,10 +282,11 @@ function chatApp() {
const s = await r.json();
const h = document.getElementById("history");
h.innerHTML = "";
s.forEach(session => {
const item = document.createElement('div');
item.className = 'history-item';
item.textContent = session.title || `Session ${session.session_id.substring(0, 8)}`;
s.forEach((session) => {
const item = document.createElement("div");
item.className = "history-item";
item.textContent =
session.title || `Session ${session.session_id.substring(0, 8)}`;
item.onclick = () => this.switchSession(session.session_id);
h.appendChild(item);
});
@ -224,7 +297,9 @@ function chatApp() {
async createNewSession() {
try {
const r = await fetch("http://localhost:8080/api/sessions", { method: "POST" });
const r = await fetch("http://localhost:8080/api/sessions", {
method: "POST",
});
const s = await r.json();
currentSessionId = s.session_id;
hasReceivedInitialMessage = false;
@ -252,10 +327,9 @@ function chatApp() {
if (isVoiceMode) {
this.startVoiceSession();
}
sidebar.classList.remove('open');
sidebar.classList.remove("open");
},
connectWebSocket() {
if (ws) {
ws.close();
@ -268,15 +342,17 @@ function chatApp() {
if (r.bot_id) {
currentBotId = r.bot_id;
}
if (r.message_type === 2) {
const d = JSON.parse(r.content);
this.handleEvent(d.event, d.data);
return;
}
// Message type 2 is a bot response (not an event)
// Message type 5 is context change
if (r.message_type === 5) {
isContextChange = true;
return;
}
// Check if this is a special event message (has event field)
if (r.event) {
this.handleEvent(r.event, r.data || {});
return;
}
this.processMessageContent(r);
};
ws.onopen = () => {
@ -333,7 +409,12 @@ function chatApp() {
isStreaming = true;
streamingMessageId = "streaming-" + Date.now();
currentStreamingContent = r.content || "";
this.addMessage("assistant", currentStreamingContent, true, streamingMessageId);
this.addMessage(
"assistant",
currentStreamingContent,
true,
streamingMessageId,
);
} else {
currentStreamingContent += r.content || "";
this.updateStreamingMessage(currentStreamingContent);
@ -376,14 +457,16 @@ function chatApp() {
t.className = "message-container";
t.innerHTML = `<div class="assistant-message"><div class="assistant-avatar"></div><div class="thinking-indicator"><div class="typing-dots"><div class="typing-dot"></div><div class="typing-dot"></div><div class="typing-dot"></div></div></div></div>`;
messagesDiv.appendChild(t);
gsap.to(t, { opacity: 1, y: 0, duration: .3, ease: "power2.out" });
gsap.to(t, { opacity: 1, y: 0, duration: 0.3, ease: "power2.out" });
if (!isUserScrolling) {
this.scrollToBottom();
}
thinkingTimeout = setTimeout(() => {
if (isThinking) {
this.hideThinkingIndicator();
this.showWarning("O servidor pode estar ocupado. A resposta está demorando demais.");
this.showWarning(
"O servidor pode estar ocupado. A resposta está demorando demais.",
);
}
}, 60000);
isThinking = true;
@ -393,7 +476,15 @@ function chatApp() {
if (!isThinking) return;
const t = document.getElementById("thinking-indicator");
if (t) {
gsap.to(t, { opacity: 0, duration: .2, onComplete: () => { if (t.parentNode) { t.remove(); } } });
gsap.to(t, {
opacity: 0,
duration: 0.2,
onComplete: () => {
if (t.parentNode) {
t.remove();
}
},
});
}
if (thinkingTimeout) {
clearTimeout(thinkingTimeout);
@ -407,13 +498,17 @@ function chatApp() {
w.className = "warning-message";
w.innerHTML = `⚠️ ${m}`;
messagesDiv.appendChild(w);
gsap.from(w, { opacity: 0, y: 20, duration: .4, ease: "power2.out" });
gsap.from(w, { opacity: 0, y: 20, duration: 0.4, ease: "power2.out" });
if (!isUserScrolling) {
this.scrollToBottom();
}
setTimeout(() => {
if (w.parentNode) {
gsap.to(w, { opacity: 0, duration: .3, onComplete: () => w.remove() });
gsap.to(w, {
opacity: 0,
duration: 0.3,
onComplete: () => w.remove(),
});
}
}, 5000);
},
@ -423,7 +518,7 @@ function chatApp() {
c.className = "message-container";
c.innerHTML = `<div class="assistant-message"><div class="assistant-avatar"></div><div class="assistant-message-content"><p>A conexão foi interrompida. Clique em "Continuar" para tentar recuperar a resposta.</p><button class="continue-button" onclick="this.parentElement.parentElement.parentElement.remove();">Continuar</button></div></div>`;
messagesDiv.appendChild(c);
gsap.to(c, { opacity: 1, y: 0, duration: .5, ease: "power2.out" });
gsap.to(c, { opacity: 1, y: 0, duration: 0.5, ease: "power2.out" });
if (!isUserScrolling) {
this.scrollToBottom();
}
@ -442,11 +537,13 @@ function chatApp() {
content: "continue",
message_type: 3,
media_url: null,
timestamp: new Date().toISOString()
timestamp: new Date().toISOString(),
};
ws.send(JSON.stringify(d));
}
document.querySelectorAll(".continue-button").forEach(b => { b.parentElement.parentElement.parentElement.remove(); });
document.querySelectorAll(".continue-button").forEach((b) => {
b.parentElement.parentElement.parentElement.remove();
});
},
addMessage(role, content, streaming = false, msgId = null) {
@ -454,17 +551,17 @@ function chatApp() {
m.className = "message-container";
if (role === "user") {
m.innerHTML = `<div class="user-message"><div class="user-message-content">${this.escapeHtml(content)}</div></div>`;
this.updateContextUsage(contextUsage + .05);
this.updateContextUsage(contextUsage + 0.05);
} else if (role === "assistant") {
m.innerHTML = `<div class="assistant-message"><div class="assistant-avatar"></div><div class="assistant-message-content markdown-content" id="${msgId || ""}">${streaming ? "" : marked.parse(content)}</div></div>`;
this.updateContextUsage(contextUsage + .03);
this.updateContextUsage(contextUsage + 0.03);
} else if (role === "voice") {
m.innerHTML = `<div class="assistant-message"><div class="assistant-avatar">🎤</div><div class="assistant-message-content">${content}</div></div>`;
} else {
m.innerHTML = `<div class="assistant-message"><div class="assistant-avatar"></div><div class="assistant-message-content">${content}</div></div>`;
}
messagesDiv.appendChild(m);
gsap.to(m, { opacity: 1, y: 0, duration: .5, ease: "power2.out" });
gsap.to(m, { opacity: 1, y: 0, duration: 0.5, ease: "power2.out" });
if (!isUserScrolling) {
this.scrollToBottom();
}
@ -498,17 +595,24 @@ function chatApp() {
},
clearSuggestions() {
suggestionsContainer.innerHTML = '';
suggestionsContainer.innerHTML = "";
},
handleSuggestions(s) {
const uniqueSuggestions = s.filter((v, i, a) => i === a.findIndex(t => t.text === v.text && t.context === v.context));
suggestionsContainer.innerHTML = '';
uniqueSuggestions.forEach(v => {
const b = document.createElement('button');
const uniqueSuggestions = s.filter(
(v, i, a) =>
i ===
a.findIndex((t) => t.text === v.text && t.context === v.context),
);
suggestionsContainer.innerHTML = "";
uniqueSuggestions.forEach((v) => {
const b = document.createElement("button");
b.textContent = v.text;
b.className = 'suggestion-button';
b.onclick = () => { this.setContext(v.context); messageInputEl.value = ''; };
b.className = "suggestion-button";
b.onclick = () => {
this.setContext(v.context);
messageInputEl.value = "";
};
suggestionsContainer.appendChild(b);
});
},
@ -517,30 +621,42 @@ function chatApp() {
try {
const t = event?.target?.textContent || c;
this.addMessage("user", t);
messageInputEl.value = '';
messageInputEl.value = '';
messageInputEl.value = "";
messageInputEl.value = "";
if (ws && ws.readyState === WebSocket.OPEN) {
pendingContextChange = new Promise(r => {
const h = e => {
pendingContextChange = new Promise((r) => {
const h = (e) => {
const d = JSON.parse(e.data);
if (d.message_type === 5 && d.context_name === c) {
ws.removeEventListener('message', h);
ws.removeEventListener("message", h);
r();
}
};
ws.addEventListener('message', h);
const s = { bot_id: currentBotId, user_id: currentUserId, session_id: currentSessionId, channel: "web", content: t, message_type: 4, is_suggestion: true, context_name: c, timestamp: new Date().toISOString() };
ws.addEventListener("message", h);
const s = {
bot_id: currentBotId,
user_id: currentUserId,
session_id: currentSessionId,
channel: "web",
content: t,
message_type: 4,
is_suggestion: true,
context_name: c,
timestamp: new Date().toISOString(),
};
ws.send(JSON.stringify(s));
});
await pendingContextChange;
const x = document.getElementById('contextIndicator');
if (x) { document.getElementById('contextPercentage').textContent = c; }
const x = document.getElementById("contextIndicator");
if (x) {
document.getElementById("contextPercentage").textContent = c;
}
} else {
console.warn("WebSocket não está conectado. Tentando reconectar...");
this.connectWebSocket();
}
} catch (err) {
console.error('Failed to set context:', err);
console.error("Failed to set context:", err);
}
},
@ -561,7 +677,16 @@ messageInputEl.value = '';
this.hideThinkingIndicator();
}
this.addMessage("user", m);
const d = { bot_id: currentBotId, user_id: currentUserId, session_id: currentSessionId, channel: "web", content: m, message_type: 1, media_url: null, timestamp: new Date().toISOString() };
const d = {
bot_id: currentBotId,
user_id: currentUserId,
session_id: currentSessionId,
channel: "web",
content: m,
message_type: 1,
media_url: null,
timestamp: new Date().toISOString(),
};
ws.send(JSON.stringify(d));
messageInputEl.value = "";
messageInputEl.focus();
@ -587,7 +712,10 @@ messageInputEl.value = '';
const r = await fetch("http://localhost:8080/api/voice/start", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ session_id: currentSessionId, user_id: currentUserId })
body: JSON.stringify({
session_id: currentSessionId,
user_id: currentUserId,
}),
});
const d = await r.json();
if (d.token) {
@ -606,7 +734,7 @@ messageInputEl.value = '';
await fetch("http://localhost:8080/api/voice/stop", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ session_id: currentSessionId })
body: JSON.stringify({ session_id: currentSessionId }),
});
if (voiceRoom) {
voiceRoom.disconnect();
@ -623,11 +751,13 @@ messageInputEl.value = '';
async connectToVoiceRoom(t) {
try {
const r = new LiveKitClient.Room();
const p = "ws:", u = `${p}//localhost:8080/voice`;
const p = "ws:",
u = `${p}//localhost:8080/voice`;
await r.connect(u, t);
voiceRoom = r;
r.on("dataReceived", d => {
const dc = new TextDecoder(), m = dc.decode(d);
r.on("dataReceived", (d) => {
const dc = new TextDecoder(),
m = dc.decode(d);
try {
const j = JSON.parse(m);
if (j.type === "voice_response") {
@ -637,7 +767,10 @@ messageInputEl.value = '';
console.log("Voice data:", m);
}
});
const l = await LiveKitClient.createLocalTracks({ audio: true, video: false });
const l = await LiveKitClient.createLocalTracks({
audio: true,
video: false,
});
for (const k of l) {
await r.localParticipant.publishTrack(k);
}
@ -652,34 +785,58 @@ messageInputEl.value = '';
console.log("Media devices not supported");
return;
}
navigator.mediaDevices.getUserMedia({ audio: true }).then(s => {
mediaRecorder = new MediaRecorder(s);
audioChunks = [];
mediaRecorder.ondataavailable = e => { audioChunks.push(e.data); };
mediaRecorder.onstop = () => { const a = new Blob(audioChunks, { type: "audio/wav" }); this.simulateVoiceTranscription(); };
mediaRecorder.start();
setTimeout(() => {
if (mediaRecorder && mediaRecorder.state === "recording") {
mediaRecorder.stop();
setTimeout(() => {
if (isVoiceMode) {
this.startVoiceRecording();
}
}, 1000);
}
}, 5000);
}).catch(e => {
console.error("Error accessing microphone:", e);
this.showWarning("Erro ao acessar microfone");
});
navigator.mediaDevices
.getUserMedia({ audio: true })
.then((s) => {
mediaRecorder = new MediaRecorder(s);
audioChunks = [];
mediaRecorder.ondataavailable = (e) => {
audioChunks.push(e.data);
};
mediaRecorder.onstop = () => {
const a = new Blob(audioChunks, { type: "audio/wav" });
this.simulateVoiceTranscription();
};
mediaRecorder.start();
setTimeout(() => {
if (mediaRecorder && mediaRecorder.state === "recording") {
mediaRecorder.stop();
setTimeout(() => {
if (isVoiceMode) {
this.startVoiceRecording();
}
}, 1000);
}
}, 5000);
})
.catch((e) => {
console.error("Error accessing microphone:", e);
this.showWarning("Erro ao acessar microfone");
});
},
simulateVoiceTranscription() {
const p = ["Olá, como posso ajudá-lo hoje?", "Entendo o que você está dizendo", "Esse é um ponto interessante", "Deixe-me pensar sobre isso", "Posso ajudá-lo com isso", "O que você gostaria de saber?", "Isso parece ótimo", "Estou ouvindo sua voz"];
const p = [
"Olá, como posso ajudá-lo hoje?",
"Entendo o que você está dizendo",
"Esse é um ponto interessante",
"Deixe-me pensar sobre isso",
"Posso ajudá-lo com isso",
"O que você gostaria de saber?",
"Isso parece ótimo",
"Estou ouvindo sua voz",
];
const r = p[Math.floor(Math.random() * p.length)];
if (voiceRoom) {
const m = { type: "voice_input", content: r, timestamp: new Date().toISOString() };
voiceRoom.localParticipant.publishData(new TextEncoder().encode(JSON.stringify(m)), LiveKitClient.DataPacketKind.RELIABLE);
const m = {
type: "voice_input",
content: r,
timestamp: new Date().toISOString(),
};
voiceRoom.localParticipant.publishData(
new TextEncoder().encode(JSON.stringify(m)),
LiveKitClient.DataPacketKind.RELIABLE,
);
}
this.addMessage("voice", `🎤 ${r}`);
},
@ -687,8 +844,8 @@ messageInputEl.value = '';
scrollToBottom() {
messagesDiv.scrollTop = messagesDiv.scrollHeight;
isUserScrolling = false;
scrollToBottomBtn.classList.remove('visible');
}
scrollToBottomBtn.classList.remove("visible");
},
};
}