Refactor TALK delivery and streaming pipelines
This commit is contained in:
parent
e146add4b2
commit
382a01658d
4 changed files with 1178 additions and 828 deletions
|
|
@ -94,32 +94,19 @@ pub async fn execute_talk(
|
||||||
};
|
};
|
||||||
let user_id = user_session.id.to_string();
|
let user_id = user_session.id.to_string();
|
||||||
let response_clone = response.clone();
|
let response_clone = response.clone();
|
||||||
match state.response_channels.try_lock() {
|
|
||||||
Ok(response_channels) => {
|
// Use web adapter which handles the connection properly
|
||||||
if let Some(tx) = response_channels.get(&user_id) {
|
let web_adapter = Arc::clone(&state.web_adapter);
|
||||||
if let Err(e) = tx.try_send(response_clone) {
|
tokio::spawn(async move {
|
||||||
error!("Failed to send TALK message via WebSocket: {}", e);
|
if let Err(e) = web_adapter
|
||||||
} else {
|
.send_message_to_session(&user_id, response_clone)
|
||||||
trace!("TALK message sent via WebSocket");
|
.await
|
||||||
}
|
{
|
||||||
} else {
|
error!("Failed to send TALK message via web adapter: {}", e);
|
||||||
let web_adapter = Arc::clone(&state.web_adapter);
|
} else {
|
||||||
tokio::spawn(async move {
|
trace!("TALK message sent via web adapter");
|
||||||
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)
|
Ok(response)
|
||||||
}
|
}
|
||||||
pub fn talk_keyword(state: Arc<AppState>, user: UserSession, engine: &mut Engine) {
|
pub fn talk_keyword(state: Arc<AppState>, user: UserSession, engine: &mut Engine) {
|
||||||
|
|
|
||||||
250
src/bot/mod.rs
250
src/bot/mod.rs
|
|
@ -67,13 +67,156 @@ impl BotOrchestrator {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Placeholder for stream_response used by UI
|
// Stream response to user via LLM
|
||||||
pub async fn stream_response(
|
pub async fn stream_response(
|
||||||
&self,
|
&self,
|
||||||
_user_message: UserMessage,
|
message: UserMessage,
|
||||||
_response_tx: mpsc::Sender<BotResponse>,
|
response_tx: mpsc::Sender<BotResponse>,
|
||||||
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
) -> 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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -146,6 +289,12 @@ async fn handle_websocket(
|
||||||
.add_connection(session_id.to_string(), tx.clone())
|
.add_connection(session_id.to_string(), tx.clone())
|
||||||
.await;
|
.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!(
|
info!(
|
||||||
"WebSocket connected for session: {}, user: {}",
|
"WebSocket connected for session: {}, user: {}",
|
||||||
session_id, user_id
|
session_id, user_id
|
||||||
|
|
@ -232,19 +381,16 @@ async fn handle_websocket(
|
||||||
session_id, user_msg.content
|
session_id, user_msg.content
|
||||||
);
|
);
|
||||||
// Process the message through the bot system
|
// Process the message through the bot system
|
||||||
let state_for_task = state_clone.clone();
|
if let Err(e) = process_user_message(
|
||||||
tokio::spawn(async move {
|
state_clone.clone(),
|
||||||
if let Err(e) = process_user_message(
|
session_id,
|
||||||
state_for_task,
|
user_id,
|
||||||
session_id,
|
user_msg,
|
||||||
user_id,
|
)
|
||||||
user_msg,
|
.await
|
||||||
)
|
{
|
||||||
.await
|
error!("Error processing user message: {}", e);
|
||||||
{
|
}
|
||||||
error!("Error processing user message: {}", e);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!(
|
error!(
|
||||||
|
|
@ -288,6 +434,12 @@ async fn handle_websocket(
|
||||||
.remove_connection(&session_id.to_string())
|
.remove_connection(&session_id.to_string())
|
||||||
.await;
|
.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);
|
info!("WebSocket disconnected for session: {}", session_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -303,64 +455,20 @@ async fn process_user_message(
|
||||||
user_id, session_id, user_msg.content
|
user_id, session_id, user_msg.content
|
||||||
);
|
);
|
||||||
|
|
||||||
// Get the session from the session manager
|
// Get the response channel for this session
|
||||||
let session = {
|
let tx = {
|
||||||
let mut sm = state.session_manager.lock().await;
|
let channels = state.response_channels.lock().await;
|
||||||
sm.get_session_by_id(session_id)
|
channels.get(&session_id.to_string()).cloned()
|
||||||
.map_err(|e| format!("Session error: {}", e))?
|
|
||||||
.ok_or("Session not found")?
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let content = user_msg.content.clone();
|
if let Some(response_tx) = tx {
|
||||||
let bot_id = session.bot_id;
|
// Use BotOrchestrator to stream the response
|
||||||
|
let orchestrator = BotOrchestrator::new(state.clone());
|
||||||
info!("Sending message to LLM for processing");
|
if let Err(e) = orchestrator.stream_response(user_msg, response_tx).await {
|
||||||
|
error!("Failed to stream response: {}", e);
|
||||||
// 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
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
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 {
|
} else {
|
||||||
info!("Response sent successfully to session {}", session_id);
|
error!("No response channel found for session {}", session_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,36 +1,50 @@
|
||||||
function chatApp() {
|
function chatApp() {
|
||||||
|
|
||||||
// Core state variables (shared via closure)
|
// Core state variables (shared via closure)
|
||||||
let ws = null,
|
let ws = null,
|
||||||
pendingContextChange = null,o
|
pendingContextChange = null,
|
||||||
currentSessionId = null,
|
o;
|
||||||
currentUserId = null,
|
((currentSessionId = null),
|
||||||
currentBotId = "default_bot",
|
(currentUserId = null),
|
||||||
isStreaming = false,
|
(currentBotId = "default_bot"),
|
||||||
voiceRoom = null,
|
(isStreaming = false),
|
||||||
isVoiceMode = false,
|
(voiceRoom = null),
|
||||||
mediaRecorder = null,
|
(isVoiceMode = false),
|
||||||
audioChunks = [],
|
(mediaRecorder = null),
|
||||||
streamingMessageId = null,
|
(audioChunks = []),
|
||||||
isThinking = false,
|
(streamingMessageId = null),
|
||||||
currentStreamingContent = "",
|
(isThinking = false),
|
||||||
hasReceivedInitialMessage = false,
|
(currentStreamingContent = ""),
|
||||||
reconnectAttempts = 0,
|
(hasReceivedInitialMessage = false),
|
||||||
reconnectTimeout = null,
|
(reconnectAttempts = 0),
|
||||||
thinkingTimeout = null,
|
(reconnectTimeout = null),
|
||||||
currentTheme = 'auto',
|
(thinkingTimeout = null),
|
||||||
themeColor1 = null,
|
(currentTheme = "auto"),
|
||||||
themeColor2 = null,
|
(themeColor1 = null),
|
||||||
customLogoUrl = null,
|
(themeColor2 = null),
|
||||||
contextUsage = 0,
|
(customLogoUrl = null),
|
||||||
isUserScrolling = false,
|
(contextUsage = 0),
|
||||||
autoScrollEnabled = true,
|
(isUserScrolling = false),
|
||||||
isContextChange = false;
|
(autoScrollEnabled = true),
|
||||||
|
(isContextChange = false));
|
||||||
|
|
||||||
const maxReconnectAttempts = 5;
|
const maxReconnectAttempts = 5;
|
||||||
|
|
||||||
// DOM references (cached for performance)
|
// 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 });
|
marked.setOptions({ breaks: true, gfm: true });
|
||||||
|
|
||||||
|
|
@ -38,25 +52,60 @@ function chatApp() {
|
||||||
// ----------------------------------------------------------------------
|
// ----------------------------------------------------------------------
|
||||||
// UI state (mirrors the structure used in driveApp)
|
// UI state (mirrors the structure used in driveApp)
|
||||||
// ----------------------------------------------------------------------
|
// ----------------------------------------------------------------------
|
||||||
current: 'All Chats',
|
current: "All Chats",
|
||||||
search: '',
|
search: "",
|
||||||
selectedChat: null,
|
selectedChat: null,
|
||||||
navItems: [
|
navItems: [
|
||||||
{ name: 'All Chats', icon: '💬' },
|
{ name: "All Chats", icon: "💬" },
|
||||||
{ name: 'Direct', icon: '👤' },
|
{ name: "Direct", icon: "👤" },
|
||||||
{ name: 'Groups', icon: '👥' },
|
{ name: "Groups", icon: "👥" },
|
||||||
{ name: 'Archived', icon: '🗄' }
|
{ name: "Archived", icon: "🗄" },
|
||||||
],
|
],
|
||||||
chats: [
|
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: 1,
|
||||||
{ id: 3, name: 'Team Stand‑up', icon: '🗣️', lastMessage: 'Done with the UI updates.', time: '2 hrs ago', status: 'Active' },
|
name: "General Bot Support",
|
||||||
{ id: 4, name: 'Random Chat', icon: '🎲', lastMessage: 'Did you see the game last night?', time: '5 hrs ago', status: 'Idle' },
|
icon: "🤖",
|
||||||
{ id: 5, name: 'Support Ticket #1234', icon: '🛠️', lastMessage: 'Issue resolved, closing ticket.', time: '3 days ago', status: 'Closed' }
|
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 Stand‑up",
|
||||||
|
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() {
|
get filteredChats() {
|
||||||
return this.chats.filter(chat =>
|
return this.chats.filter((chat) =>
|
||||||
chat.name.toLowerCase().includes(this.search.toLowerCase())
|
chat.name.toLowerCase().includes(this.search.toLowerCase()),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -64,34 +113,45 @@ function chatApp() {
|
||||||
// UI helpers (formerly standalone functions)
|
// UI helpers (formerly standalone functions)
|
||||||
// ----------------------------------------------------------------------
|
// ----------------------------------------------------------------------
|
||||||
toggleSidebar() {
|
toggleSidebar() {
|
||||||
sidebar.classList.toggle('open');
|
sidebar.classList.toggle("open");
|
||||||
},
|
},
|
||||||
|
|
||||||
toggleTheme() {
|
toggleTheme() {
|
||||||
const themes = ['auto', 'dark', 'light'];
|
const themes = ["auto", "dark", "light"];
|
||||||
const savedTheme = localStorage.getItem('gb-theme') || 'auto';
|
const savedTheme = localStorage.getItem("gb-theme") || "auto";
|
||||||
const idx = themes.indexOf(savedTheme);
|
const idx = themes.indexOf(savedTheme);
|
||||||
const newTheme = themes[(idx + 1) % themes.length];
|
const newTheme = themes[(idx + 1) % themes.length];
|
||||||
localStorage.setItem('gb-theme', newTheme);
|
localStorage.setItem("gb-theme", newTheme);
|
||||||
currentTheme = newTheme;
|
currentTheme = newTheme;
|
||||||
this.applyTheme();
|
this.applyTheme();
|
||||||
this.updateThemeButton();
|
this.updateThemeButton();
|
||||||
},
|
},
|
||||||
|
|
||||||
applyTheme() {
|
applyTheme() {
|
||||||
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
const prefersDark = window.matchMedia(
|
||||||
|
"(prefers-color-scheme: dark)",
|
||||||
|
).matches;
|
||||||
let theme = currentTheme;
|
let theme = currentTheme;
|
||||||
if (theme === 'auto') {
|
if (theme === "auto") {
|
||||||
theme = prefersDark ? 'dark' : 'light';
|
theme = prefersDark ? "dark" : "light";
|
||||||
}
|
}
|
||||||
document.documentElement.setAttribute('data-theme', theme);
|
document.documentElement.setAttribute("data-theme", theme);
|
||||||
if (themeColor1 && themeColor2) {
|
if (themeColor1 && themeColor2) {
|
||||||
const root = document.documentElement;
|
const root = document.documentElement;
|
||||||
root.style.setProperty('--bg', theme === 'dark' ? themeColor2 : themeColor1);
|
root.style.setProperty(
|
||||||
root.style.setProperty('--fg', theme === 'dark' ? themeColor1 : themeColor2);
|
"--bg",
|
||||||
|
theme === "dark" ? themeColor2 : themeColor1,
|
||||||
|
);
|
||||||
|
root.style.setProperty(
|
||||||
|
"--fg",
|
||||||
|
theme === "dark" ? themeColor1 : themeColor2,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if (customLogoUrl) {
|
if (customLogoUrl) {
|
||||||
document.documentElement.style.setProperty('--logo-url', `url('${customLogoUrl}')`);
|
document.documentElement.style.setProperty(
|
||||||
|
"--logo-url",
|
||||||
|
`url('${customLogoUrl}')`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -99,10 +159,10 @@ function chatApp() {
|
||||||
// Lifecycle / event handlers
|
// Lifecycle / event handlers
|
||||||
// ----------------------------------------------------------------------
|
// ----------------------------------------------------------------------
|
||||||
init() {
|
init() {
|
||||||
window.addEventListener('load', () => {
|
window.addEventListener("load", () => {
|
||||||
// Assign DOM elements after the document is ready
|
// Assign DOM elements after the document is ready
|
||||||
messagesDiv = document.getElementById("messages");
|
messagesDiv = document.getElementById("messages");
|
||||||
|
|
||||||
messageInputEl = document.getElementById("messageInput");
|
messageInputEl = document.getElementById("messageInput");
|
||||||
sendBtn = document.getElementById("sendBtn");
|
sendBtn = document.getElementById("sendBtn");
|
||||||
voiceBtn = document.getElementById("voiceBtn");
|
voiceBtn = document.getElementById("voiceBtn");
|
||||||
|
|
@ -119,40 +179,44 @@ function chatApp() {
|
||||||
sidebarTitle = document.getElementById("sidebarTitle");
|
sidebarTitle = document.getElementById("sidebarTitle");
|
||||||
|
|
||||||
// Theme initialization and focus
|
// Theme initialization and focus
|
||||||
const savedTheme = localStorage.getItem('gb-theme') || 'auto';
|
const savedTheme = localStorage.getItem("gb-theme") || "auto";
|
||||||
currentTheme = savedTheme;
|
currentTheme = savedTheme;
|
||||||
this.applyTheme();
|
this.applyTheme();
|
||||||
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => {
|
window
|
||||||
if (currentTheme === 'auto') {
|
.matchMedia("(prefers-color-scheme: dark)")
|
||||||
this.applyTheme();
|
.addEventListener("change", () => {
|
||||||
}
|
if (currentTheme === "auto") {
|
||||||
});
|
this.applyTheme();
|
||||||
|
}
|
||||||
|
});
|
||||||
if (messageInputEl) {
|
if (messageInputEl) {
|
||||||
messageInputEl.focus();
|
messageInputEl.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
// UI event listeners
|
// UI event listeners
|
||||||
document.addEventListener('click', (e) => {
|
document.addEventListener("click", (e) => {});
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
messagesDiv.addEventListener('scroll', () => {
|
messagesDiv.addEventListener("scroll", () => {
|
||||||
const isAtBottom = messagesDiv.scrollHeight - messagesDiv.scrollTop <= messagesDiv.clientHeight + 100;
|
const isAtBottom =
|
||||||
|
messagesDiv.scrollHeight - messagesDiv.scrollTop <=
|
||||||
|
messagesDiv.clientHeight + 100;
|
||||||
if (!isAtBottom) {
|
if (!isAtBottom) {
|
||||||
isUserScrolling = true;
|
isUserScrolling = true;
|
||||||
scrollToBottomBtn.classList.add('visible');
|
scrollToBottomBtn.classList.add("visible");
|
||||||
} else {
|
} else {
|
||||||
isUserScrolling = false;
|
isUserScrolling = false;
|
||||||
scrollToBottomBtn.classList.remove('visible');
|
scrollToBottomBtn.classList.remove("visible");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
scrollToBottomBtn.addEventListener('click', () => {
|
scrollToBottomBtn.addEventListener("click", () => {
|
||||||
this.scrollToBottom();
|
this.scrollToBottom();
|
||||||
});
|
});
|
||||||
|
|
||||||
sendBtn.onclick = () => this.sendMessage();
|
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", () => {
|
window.addEventListener("focus", () => {
|
||||||
if (!ws || ws.readyState !== WebSocket.OPEN) {
|
if (!ws || ws.readyState !== WebSocket.OPEN) {
|
||||||
this.connectWebSocket();
|
this.connectWebSocket();
|
||||||
|
|
@ -169,13 +233,17 @@ function chatApp() {
|
||||||
const p = Math.min(100, Math.round(u * 100));
|
const p = Math.min(100, Math.round(u * 100));
|
||||||
contextPercentage.textContent = `${p}%`;
|
contextPercentage.textContent = `${p}%`;
|
||||||
contextProgressBar.style.width = `${p}%`;
|
contextProgressBar.style.width = `${p}%`;
|
||||||
contextIndicator.classList.remove('visible');
|
contextIndicator.classList.remove("visible");
|
||||||
},
|
},
|
||||||
|
|
||||||
flashScreen() {
|
flashScreen() {
|
||||||
gsap.to(flashOverlay, { opacity: 0.15, duration: 0.1, onComplete: () => {
|
gsap.to(flashOverlay, {
|
||||||
gsap.to(flashOverlay, { opacity: 0, duration: 0.2 });
|
opacity: 0.15,
|
||||||
} });
|
duration: 0.1,
|
||||||
|
onComplete: () => {
|
||||||
|
gsap.to(flashOverlay, { opacity: 0, duration: 0.2 });
|
||||||
|
},
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
updateConnectionStatus(s) {
|
updateConnectionStatus(s) {
|
||||||
|
|
@ -183,16 +251,20 @@ function chatApp() {
|
||||||
},
|
},
|
||||||
|
|
||||||
getWebSocketUrl() {
|
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}`;
|
return `${p}//localhost:8080/ws?session_id=${s}&user_id=${u}`;
|
||||||
},
|
},
|
||||||
|
|
||||||
async initializeAuth() {
|
async initializeAuth() {
|
||||||
try {
|
try {
|
||||||
this.updateConnectionStatus("connecting");
|
this.updateConnectionStatus("connecting");
|
||||||
const p = window.location.pathname.split('/').filter(s => s);
|
const p = window.location.pathname.split("/").filter((s) => s);
|
||||||
const b = p.length > 0 ? p[0] : 'default';
|
const b = p.length > 0 ? p[0] : "default";
|
||||||
const r = await fetch(`http://localhost:8080/api/auth?bot_name=${encodeURIComponent(b)}`);
|
const r = await fetch(
|
||||||
|
`http://localhost:8080/api/auth?bot_name=${encodeURIComponent(b)}`,
|
||||||
|
);
|
||||||
const a = await r.json();
|
const a = await r.json();
|
||||||
currentUserId = a.user_id;
|
currentUserId = a.user_id;
|
||||||
currentSessionId = a.session_id;
|
currentSessionId = a.session_id;
|
||||||
|
|
@ -210,10 +282,11 @@ function chatApp() {
|
||||||
const s = await r.json();
|
const s = await r.json();
|
||||||
const h = document.getElementById("history");
|
const h = document.getElementById("history");
|
||||||
h.innerHTML = "";
|
h.innerHTML = "";
|
||||||
s.forEach(session => {
|
s.forEach((session) => {
|
||||||
const item = document.createElement('div');
|
const item = document.createElement("div");
|
||||||
item.className = 'history-item';
|
item.className = "history-item";
|
||||||
item.textContent = session.title || `Session ${session.session_id.substring(0, 8)}`;
|
item.textContent =
|
||||||
|
session.title || `Session ${session.session_id.substring(0, 8)}`;
|
||||||
item.onclick = () => this.switchSession(session.session_id);
|
item.onclick = () => this.switchSession(session.session_id);
|
||||||
h.appendChild(item);
|
h.appendChild(item);
|
||||||
});
|
});
|
||||||
|
|
@ -224,7 +297,9 @@ function chatApp() {
|
||||||
|
|
||||||
async createNewSession() {
|
async createNewSession() {
|
||||||
try {
|
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();
|
const s = await r.json();
|
||||||
currentSessionId = s.session_id;
|
currentSessionId = s.session_id;
|
||||||
hasReceivedInitialMessage = false;
|
hasReceivedInitialMessage = false;
|
||||||
|
|
@ -252,10 +327,9 @@ function chatApp() {
|
||||||
if (isVoiceMode) {
|
if (isVoiceMode) {
|
||||||
this.startVoiceSession();
|
this.startVoiceSession();
|
||||||
}
|
}
|
||||||
sidebar.classList.remove('open');
|
sidebar.classList.remove("open");
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
connectWebSocket() {
|
connectWebSocket() {
|
||||||
if (ws) {
|
if (ws) {
|
||||||
ws.close();
|
ws.close();
|
||||||
|
|
@ -268,15 +342,17 @@ function chatApp() {
|
||||||
if (r.bot_id) {
|
if (r.bot_id) {
|
||||||
currentBotId = r.bot_id;
|
currentBotId = r.bot_id;
|
||||||
}
|
}
|
||||||
if (r.message_type === 2) {
|
// Message type 2 is a bot response (not an event)
|
||||||
const d = JSON.parse(r.content);
|
// Message type 5 is context change
|
||||||
this.handleEvent(d.event, d.data);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (r.message_type === 5) {
|
if (r.message_type === 5) {
|
||||||
isContextChange = true;
|
isContextChange = true;
|
||||||
return;
|
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);
|
this.processMessageContent(r);
|
||||||
};
|
};
|
||||||
ws.onopen = () => {
|
ws.onopen = () => {
|
||||||
|
|
@ -333,7 +409,12 @@ function chatApp() {
|
||||||
isStreaming = true;
|
isStreaming = true;
|
||||||
streamingMessageId = "streaming-" + Date.now();
|
streamingMessageId = "streaming-" + Date.now();
|
||||||
currentStreamingContent = r.content || "";
|
currentStreamingContent = r.content || "";
|
||||||
this.addMessage("assistant", currentStreamingContent, true, streamingMessageId);
|
this.addMessage(
|
||||||
|
"assistant",
|
||||||
|
currentStreamingContent,
|
||||||
|
true,
|
||||||
|
streamingMessageId,
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
currentStreamingContent += r.content || "";
|
currentStreamingContent += r.content || "";
|
||||||
this.updateStreamingMessage(currentStreamingContent);
|
this.updateStreamingMessage(currentStreamingContent);
|
||||||
|
|
@ -376,14 +457,16 @@ function chatApp() {
|
||||||
t.className = "message-container";
|
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>`;
|
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);
|
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) {
|
if (!isUserScrolling) {
|
||||||
this.scrollToBottom();
|
this.scrollToBottom();
|
||||||
}
|
}
|
||||||
thinkingTimeout = setTimeout(() => {
|
thinkingTimeout = setTimeout(() => {
|
||||||
if (isThinking) {
|
if (isThinking) {
|
||||||
this.hideThinkingIndicator();
|
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);
|
}, 60000);
|
||||||
isThinking = true;
|
isThinking = true;
|
||||||
|
|
@ -393,7 +476,15 @@ function chatApp() {
|
||||||
if (!isThinking) return;
|
if (!isThinking) return;
|
||||||
const t = document.getElementById("thinking-indicator");
|
const t = document.getElementById("thinking-indicator");
|
||||||
if (t) {
|
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) {
|
if (thinkingTimeout) {
|
||||||
clearTimeout(thinkingTimeout);
|
clearTimeout(thinkingTimeout);
|
||||||
|
|
@ -407,13 +498,17 @@ function chatApp() {
|
||||||
w.className = "warning-message";
|
w.className = "warning-message";
|
||||||
w.innerHTML = `⚠️ ${m}`;
|
w.innerHTML = `⚠️ ${m}`;
|
||||||
messagesDiv.appendChild(w);
|
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) {
|
if (!isUserScrolling) {
|
||||||
this.scrollToBottom();
|
this.scrollToBottom();
|
||||||
}
|
}
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (w.parentNode) {
|
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);
|
}, 5000);
|
||||||
},
|
},
|
||||||
|
|
@ -423,7 +518,7 @@ function chatApp() {
|
||||||
c.className = "message-container";
|
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>`;
|
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);
|
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) {
|
if (!isUserScrolling) {
|
||||||
this.scrollToBottom();
|
this.scrollToBottom();
|
||||||
}
|
}
|
||||||
|
|
@ -442,11 +537,13 @@ function chatApp() {
|
||||||
content: "continue",
|
content: "continue",
|
||||||
message_type: 3,
|
message_type: 3,
|
||||||
media_url: null,
|
media_url: null,
|
||||||
timestamp: new Date().toISOString()
|
timestamp: new Date().toISOString(),
|
||||||
};
|
};
|
||||||
ws.send(JSON.stringify(d));
|
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) {
|
addMessage(role, content, streaming = false, msgId = null) {
|
||||||
|
|
@ -454,17 +551,17 @@ function chatApp() {
|
||||||
m.className = "message-container";
|
m.className = "message-container";
|
||||||
if (role === "user") {
|
if (role === "user") {
|
||||||
m.innerHTML = `<div class="user-message"><div class="user-message-content">${this.escapeHtml(content)}</div></div>`;
|
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") {
|
} 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>`;
|
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") {
|
} else if (role === "voice") {
|
||||||
m.innerHTML = `<div class="assistant-message"><div class="assistant-avatar">🎤</div><div class="assistant-message-content">${content}</div></div>`;
|
m.innerHTML = `<div class="assistant-message"><div class="assistant-avatar">🎤</div><div class="assistant-message-content">${content}</div></div>`;
|
||||||
} else {
|
} else {
|
||||||
m.innerHTML = `<div class="assistant-message"><div class="assistant-avatar"></div><div class="assistant-message-content">${content}</div></div>`;
|
m.innerHTML = `<div class="assistant-message"><div class="assistant-avatar"></div><div class="assistant-message-content">${content}</div></div>`;
|
||||||
}
|
}
|
||||||
messagesDiv.appendChild(m);
|
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) {
|
if (!isUserScrolling) {
|
||||||
this.scrollToBottom();
|
this.scrollToBottom();
|
||||||
}
|
}
|
||||||
|
|
@ -498,17 +595,24 @@ function chatApp() {
|
||||||
},
|
},
|
||||||
|
|
||||||
clearSuggestions() {
|
clearSuggestions() {
|
||||||
suggestionsContainer.innerHTML = '';
|
suggestionsContainer.innerHTML = "";
|
||||||
},
|
},
|
||||||
|
|
||||||
handleSuggestions(s) {
|
handleSuggestions(s) {
|
||||||
const uniqueSuggestions = s.filter((v, i, a) => i === a.findIndex(t => t.text === v.text && t.context === v.context));
|
const uniqueSuggestions = s.filter(
|
||||||
suggestionsContainer.innerHTML = '';
|
(v, i, a) =>
|
||||||
uniqueSuggestions.forEach(v => {
|
i ===
|
||||||
const b = document.createElement('button');
|
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.textContent = v.text;
|
||||||
b.className = 'suggestion-button';
|
b.className = "suggestion-button";
|
||||||
b.onclick = () => { this.setContext(v.context); messageInputEl.value = ''; };
|
b.onclick = () => {
|
||||||
|
this.setContext(v.context);
|
||||||
|
messageInputEl.value = "";
|
||||||
|
};
|
||||||
suggestionsContainer.appendChild(b);
|
suggestionsContainer.appendChild(b);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
@ -517,30 +621,42 @@ function chatApp() {
|
||||||
try {
|
try {
|
||||||
const t = event?.target?.textContent || c;
|
const t = event?.target?.textContent || c;
|
||||||
this.addMessage("user", t);
|
this.addMessage("user", t);
|
||||||
messageInputEl.value = '';
|
messageInputEl.value = "";
|
||||||
messageInputEl.value = '';
|
messageInputEl.value = "";
|
||||||
if (ws && ws.readyState === WebSocket.OPEN) {
|
if (ws && ws.readyState === WebSocket.OPEN) {
|
||||||
pendingContextChange = new Promise(r => {
|
pendingContextChange = new Promise((r) => {
|
||||||
const h = e => {
|
const h = (e) => {
|
||||||
const d = JSON.parse(e.data);
|
const d = JSON.parse(e.data);
|
||||||
if (d.message_type === 5 && d.context_name === c) {
|
if (d.message_type === 5 && d.context_name === c) {
|
||||||
ws.removeEventListener('message', h);
|
ws.removeEventListener("message", h);
|
||||||
r();
|
r();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
ws.addEventListener('message', h);
|
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() };
|
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));
|
ws.send(JSON.stringify(s));
|
||||||
});
|
});
|
||||||
await pendingContextChange;
|
await pendingContextChange;
|
||||||
const x = document.getElementById('contextIndicator');
|
const x = document.getElementById("contextIndicator");
|
||||||
if (x) { document.getElementById('contextPercentage').textContent = c; }
|
if (x) {
|
||||||
|
document.getElementById("contextPercentage").textContent = c;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
console.warn("WebSocket não está conectado. Tentando reconectar...");
|
console.warn("WebSocket não está conectado. Tentando reconectar...");
|
||||||
this.connectWebSocket();
|
this.connectWebSocket();
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} 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.hideThinkingIndicator();
|
||||||
}
|
}
|
||||||
this.addMessage("user", m);
|
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));
|
ws.send(JSON.stringify(d));
|
||||||
messageInputEl.value = "";
|
messageInputEl.value = "";
|
||||||
messageInputEl.focus();
|
messageInputEl.focus();
|
||||||
|
|
@ -587,7 +712,10 @@ messageInputEl.value = '';
|
||||||
const r = await fetch("http://localhost:8080/api/voice/start", {
|
const r = await fetch("http://localhost:8080/api/voice/start", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json" },
|
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();
|
const d = await r.json();
|
||||||
if (d.token) {
|
if (d.token) {
|
||||||
|
|
@ -606,7 +734,7 @@ messageInputEl.value = '';
|
||||||
await fetch("http://localhost:8080/api/voice/stop", {
|
await fetch("http://localhost:8080/api/voice/stop", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify({ session_id: currentSessionId })
|
body: JSON.stringify({ session_id: currentSessionId }),
|
||||||
});
|
});
|
||||||
if (voiceRoom) {
|
if (voiceRoom) {
|
||||||
voiceRoom.disconnect();
|
voiceRoom.disconnect();
|
||||||
|
|
@ -623,11 +751,13 @@ messageInputEl.value = '';
|
||||||
async connectToVoiceRoom(t) {
|
async connectToVoiceRoom(t) {
|
||||||
try {
|
try {
|
||||||
const r = new LiveKitClient.Room();
|
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);
|
await r.connect(u, t);
|
||||||
voiceRoom = r;
|
voiceRoom = r;
|
||||||
r.on("dataReceived", d => {
|
r.on("dataReceived", (d) => {
|
||||||
const dc = new TextDecoder(), m = dc.decode(d);
|
const dc = new TextDecoder(),
|
||||||
|
m = dc.decode(d);
|
||||||
try {
|
try {
|
||||||
const j = JSON.parse(m);
|
const j = JSON.parse(m);
|
||||||
if (j.type === "voice_response") {
|
if (j.type === "voice_response") {
|
||||||
|
|
@ -637,7 +767,10 @@ messageInputEl.value = '';
|
||||||
console.log("Voice data:", m);
|
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) {
|
for (const k of l) {
|
||||||
await r.localParticipant.publishTrack(k);
|
await r.localParticipant.publishTrack(k);
|
||||||
}
|
}
|
||||||
|
|
@ -652,34 +785,58 @@ messageInputEl.value = '';
|
||||||
console.log("Media devices not supported");
|
console.log("Media devices not supported");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
navigator.mediaDevices.getUserMedia({ audio: true }).then(s => {
|
navigator.mediaDevices
|
||||||
mediaRecorder = new MediaRecorder(s);
|
.getUserMedia({ audio: true })
|
||||||
audioChunks = [];
|
.then((s) => {
|
||||||
mediaRecorder.ondataavailable = e => { audioChunks.push(e.data); };
|
mediaRecorder = new MediaRecorder(s);
|
||||||
mediaRecorder.onstop = () => { const a = new Blob(audioChunks, { type: "audio/wav" }); this.simulateVoiceTranscription(); };
|
audioChunks = [];
|
||||||
mediaRecorder.start();
|
mediaRecorder.ondataavailable = (e) => {
|
||||||
setTimeout(() => {
|
audioChunks.push(e.data);
|
||||||
if (mediaRecorder && mediaRecorder.state === "recording") {
|
};
|
||||||
mediaRecorder.stop();
|
mediaRecorder.onstop = () => {
|
||||||
setTimeout(() => {
|
const a = new Blob(audioChunks, { type: "audio/wav" });
|
||||||
if (isVoiceMode) {
|
this.simulateVoiceTranscription();
|
||||||
this.startVoiceRecording();
|
};
|
||||||
}
|
mediaRecorder.start();
|
||||||
}, 1000);
|
setTimeout(() => {
|
||||||
}
|
if (mediaRecorder && mediaRecorder.state === "recording") {
|
||||||
}, 5000);
|
mediaRecorder.stop();
|
||||||
}).catch(e => {
|
setTimeout(() => {
|
||||||
console.error("Error accessing microphone:", e);
|
if (isVoiceMode) {
|
||||||
this.showWarning("Erro ao acessar microfone");
|
this.startVoiceRecording();
|
||||||
});
|
}
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
}, 5000);
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
console.error("Error accessing microphone:", e);
|
||||||
|
this.showWarning("Erro ao acessar microfone");
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
simulateVoiceTranscription() {
|
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)];
|
const r = p[Math.floor(Math.random() * p.length)];
|
||||||
if (voiceRoom) {
|
if (voiceRoom) {
|
||||||
const m = { type: "voice_input", content: r, timestamp: new Date().toISOString() };
|
const m = {
|
||||||
voiceRoom.localParticipant.publishData(new TextEncoder().encode(JSON.stringify(m)), LiveKitClient.DataPacketKind.RELIABLE);
|
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}`);
|
this.addMessage("voice", `🎤 ${r}`);
|
||||||
},
|
},
|
||||||
|
|
@ -687,8 +844,8 @@ messageInputEl.value = '';
|
||||||
scrollToBottom() {
|
scrollToBottom() {
|
||||||
messagesDiv.scrollTop = messagesDiv.scrollHeight;
|
messagesDiv.scrollTop = messagesDiv.scrollHeight;
|
||||||
isUserScrolling = false;
|
isUserScrolling = false;
|
||||||
scrollToBottomBtn.classList.remove('visible');
|
scrollToBottomBtn.classList.remove("visible");
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue