From f9a1e3a8c07898fabdff42e5e731da6f66d17c33 Mon Sep 17 00:00:00 2001 From: "Rodrigo Rodriguez (Pragmatismo)" Date: Mon, 17 Nov 2025 10:00:12 -0300 Subject: [PATCH] refactor(web): consolidate routing and expose auth handler - Add `*.log` to `.gitignore` to exclude log files from version control. - Change `auth_handler` to `pub` in `src/auth/mod.rs` to make the endpoint publicly accessible. - Remove unused `bot_index` import and route; replace direct service registration with `web_server::configure_app` in `src/main.rs`. - Refactor `src/web_server/mod.rs`: - Remove the `bot_index` handler. - Introduce `serve_html` helper for loading HTML pages. - Simplify static file serving by configuring separate routes for JS and CSS assets. - Centralize all route and static file configuration in `configure_app`. - Clean up related imports and improve error handling for missing pages. --- .gitignore | 1 + src/auth/mod.rs | 2 +- src/main.rs | 5 +- src/web_server/mod.rs | 74 ++- web/desktop/chat/chat.html | 128 +--- web/desktop/chat/chat.js | 1267 +++++++++++++++++++----------------- web/desktop/index.html | 69 +- 7 files changed, 772 insertions(+), 774 deletions(-) diff --git a/.gitignore b/.gitignore index 4ac04969..6354f50f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +*.log target* .env *.env diff --git a/src/auth/mod.rs b/src/auth/mod.rs index cef00d2d..47d50438 100644 --- a/src/auth/mod.rs +++ b/src/auth/mod.rs @@ -11,7 +11,7 @@ impl AuthService { } } #[actix_web::get("/api/auth")] -async fn auth_handler( +pub async fn auth_handler( _req: HttpRequest, data: web::Data, web::Query(params): web::Query>, diff --git a/src/main.rs b/src/main.rs index 4708b65c..c5f0cfdb 100644 --- a/src/main.rs +++ b/src/main.rs @@ -46,7 +46,6 @@ use crate::session::{create_session, get_session_history, get_sessions, start_se use crate::shared::state::AppState; use crate::shared::utils::create_conn; use crate::shared::utils::create_s3_operator; -use crate::web_server::{bot_index}; #[derive(Debug, Clone)] pub enum BootstrapProgress { StartingBootstrap, @@ -306,7 +305,6 @@ async fn main() -> std::io::Result<()> { .wrap(Logger::default()) .wrap(Logger::new("HTTP REQUEST: %a %{User-Agent}i")) .app_data(web::Data::from(app_state_clone)) - .configure(web_server::configure_app) .service(auth_handler) .service(create_session) .service(get_session_history) @@ -332,7 +330,8 @@ async fn main() -> std::io::Result<()> { .service(save_draft) .service(save_click); } - app = app.service(bot_index); + app = app.configure(web_server::configure_app); + app }) .workers(worker_count) diff --git a/src/web_server/mod.rs b/src/web_server/mod.rs index bc3a09e0..7e17f93d 100644 --- a/src/web_server/mod.rs +++ b/src/web_server/mod.rs @@ -14,32 +14,19 @@ async fn index() -> Result { } } -#[actix_web::get("/{botname}")] -async fn bot_index(req: HttpRequest) -> Result { - let botname = req.match_info().query("botname"); - debug!("Serving bot interface for: {}", botname); - match fs::read_to_string("web/desktop/index.html") { +async fn serve_html(path: &str) -> Result { + match fs::read_to_string(format!("web/desktop/{}", path)) { Ok(html) => Ok(HttpResponse::Ok().content_type("text/html").body(html)), Err(e) => { - error!("Failed to load index page for bot {}: {}", botname, e); - Ok(HttpResponse::InternalServerError().body("Failed to load index page")) + error!("Failed to load page {}: {}", path, e); + Ok(HttpResponse::InternalServerError().body("Failed to load page")) } } } pub fn configure_app(cfg: &mut actix_web::web::ServiceConfig) { let static_path = Path::new("./web/desktop"); - - // Serve all static files from desktop directory - cfg.service( - Files::new("/", static_path) - .index_file("index.html") - .prefer_utf8(true) - .use_last_modified(true) - .use_etag(true) - .show_files_listing() - ); - + // Serve all JS files cfg.service( Files::new("/js", static_path.join("js")) @@ -47,18 +34,43 @@ pub fn configure_app(cfg: &mut actix_web::web::ServiceConfig) { .use_last_modified(true) .use_etag(true) ); - - // Serve all component directories - ["drive", "tasks", "mail"].iter().for_each(|dir| { - cfg.service( - Files::new(&format!("/{}", dir), static_path.join(dir)) - .prefer_utf8(true) - .use_last_modified(true) - .use_etag(true) - ); - }); - - // Serve index routes + + // Serve CSS files + cfg.service( + Files::new("/css", static_path.join("css")) + .prefer_utf8(true) + .use_last_modified(true) + .use_etag(true) + ); + + cfg.service( + Files::new("/drive", static_path.join("drive")) + .prefer_utf8(true) + .use_last_modified(true) + .use_etag(true) + ); + + + cfg.service( + Files::new("/chat", static_path.join("chat")) + .prefer_utf8(true) + .use_last_modified(true) + .use_etag(true) + ); + + cfg.service( + Files::new("/mail", static_path.join("mail")) + .prefer_utf8(true) + .use_last_modified(true) + .use_etag(true) + ); + + cfg.service( + Files::new("/tasks", static_path.join("tasks")) + .prefer_utf8(true) + .use_last_modified(true) + .use_etag(true) + ); cfg.service(index); - cfg.service(bot_index); + } diff --git a/web/desktop/chat/chat.html b/web/desktop/chat/chat.html index b2027e63..03d1ab39 100644 --- a/web/desktop/chat/chat.html +++ b/web/desktop/chat/chat.html @@ -1,111 +1,23 @@ - - - - -General Bots Chat - - - - - - - -
-
-
- - -
- - -
-
-
-

General Bots Chat

+ +
+
+ +
+
+
+ + + +
+
+ +
+
Context
+
0%
+
+
+
- -
-
-
-

- -
-
- -
-
- -
- - -
-
- -
-
-
- - - -
-
- - - - +
\ No newline at end of file diff --git a/web/desktop/chat/chat.js b/web/desktop/chat/chat.js index b617f1de..8792fd97 100644 --- a/web/desktop/chat/chat.js +++ b/web/desktop/chat/chat.js @@ -1,19 +1,51 @@ function chatApp() { + + // Core state variables (shared via closure) + let ws = null, + 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, input, sendBtn, voiceBtn, connectionStatus, flashOverlay, suggestionsContainer, floatLogo, sidebar, themeBtn, scrollToBottomBtn, contextIndicator, contextPercentage, contextProgressBar, sidebarTitle; + + marked.setOptions({ breaks: true, gfm: true }); + return { - // Current navigation section (e.g., All Chats, Direct, Group) + // ---------------------------------------------------------------------- + // UI state (mirrors the structure used in driveApp) + // ---------------------------------------------------------------------- current: 'All Chats', - // Search term for filtering chats search: '', - // Currently selected chat object selectedChat: null, - // Navigation items similar to the Drive UI navItems: [ { name: 'All Chats', icon: '💬' }, { name: 'Direct', icon: '👤' }, { name: 'Groups', icon: '👥' }, { name: 'Archived', icon: '🗄' } ], - // Sample chat list – in a real app this would be fetched from a server 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' }, @@ -21,620 +53,661 @@ function chatApp() { { 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' } ], - // Computed property – filters chats based on the search term get filteredChats() { return this.chats.filter(chat => chat.name.toLowerCase().includes(this.search.toLowerCase()) ); - } - }; -} + }, -/* ----- Full application mechanics migrated from web/html/index.html ----- */ + // ---------------------------------------------------------------------- + // UI helpers (formerly standalone functions) + // ---------------------------------------------------------------------- + toggleSidebar() { + sidebar.classList.toggle('open'); + }, -let ws=null,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,messagesDiv=document.getElementById("messages"),input=document.getElementById("messageInput"),sendBtn=document.getElementById("sendBtn"),voiceBtn=document.getElementById("voiceBtn"),connectionStatus=document.getElementById("connectionStatus"),flashOverlay=document.getElementById("flashOverlay"),suggestionsContainer=document.getElementById("suggestions"),floatLogo=document.getElementById("floatLogo"),sidebar=document.getElementById("sidebar"),themeBtn=document.getElementById("themeBtn"),scrollToBottomBtn=document.getElementById("scrollToBottom"),contextIndicator=document.getElementById("contextIndicator"),contextPercentage=document.getElementById("contextPercentage"),contextProgressBar=document.getElementById("contextProgressBar"),sidebarTitle=document.getElementById("sidebarTitle"); -marked.setOptions({breaks:true,gfm:true}); + toggleTheme() { + 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); + currentTheme = newTheme; + this.applyTheme(); + this.updateThemeButton(); + }, -floatLogo.addEventListener('click',toggleSidebar); - -function toggleSidebar(){ - sidebar.classList.toggle('open'); -} - -function toggleTheme(){ - 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); - currentTheme=newTheme; - applyTheme(); - updateThemeButton(); -} - -function updateThemeButton(){ - const icons={'auto':'⚙','dark':'🌙','light':'☀️'}; - themeBtn.textContent=icons[currentTheme]||'⚙'; -} - -function applyTheme(){ - const prefersDark=window.matchMedia('(prefers-color-scheme: dark)').matches; - let theme=currentTheme; - if(theme==='auto'){ - theme=prefersDark?'dark':'light'; - } - 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); - } - if(customLogoUrl){ - document.documentElement.style.setProperty('--logo-url',`url('${customLogoUrl}')`); - } -} - -window.addEventListener("load",function(){ - const savedTheme=localStorage.getItem('gb-theme')||'auto'; - currentTheme=savedTheme; - applyTheme(); - updateThemeButton(); - window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change',()=>{ - if(currentTheme==='auto'){ - applyTheme(); - } - }); - input.focus(); -}); - -themeBtn.addEventListener('click',toggleTheme); - -document.addEventListener('click',function(e){ - if(sidebar.classList.contains('open')&&!sidebar.contains(e.target)&&!floatLogo.contains(e.target)){ - sidebar.classList.remove('open'); - } -}); - -messagesDiv.addEventListener('scroll',function(){ - const isAtBottom=messagesDiv.scrollHeight-messagesDiv.scrollTop<=messagesDiv.clientHeight+100; - if(!isAtBottom){ - isUserScrolling=true; - scrollToBottomBtn.classList.add('visible'); - }else{ - isUserScrolling=false; - scrollToBottomBtn.classList.remove('visible'); - } -}); - -scrollToBottomBtn.addEventListener('click',function(){ - scrollToBottom(); -}); - -function updateContextUsage(u){ - contextUsage=u; - const p=Math.min(100,Math.round(u*100)); - contextPercentage.textContent=`${p}%`; - contextProgressBar.style.width=`${p}%`; - contextIndicator.classList.remove('visible'); -} - -function flashScreen(){ - gsap.to(flashOverlay,{opacity:0.15,duration:0.1,onComplete:()=>{gsap.to(flashOverlay,{opacity:0,duration:0.2});}}); -} - -function updateConnectionStatus(s){ - connectionStatus.className=`connection-status ${s}`; -} - -function getWebSocketUrl(){ - const p="ws:",s=currentSessionId||crypto.randomUUID(),u=currentUserId||crypto.randomUUID(); - return`${p}//localhost:8080/ws?session_id=${s}&user_id=${u}`; -} - -async function initializeAuth(){ - try{ - 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)}`),a=await r.json(); - currentUserId=a.user_id; - currentSessionId=a.session_id; - connectWebSocket(); - loadSessions(); - }catch(e){ - console.error("Failed to initialize auth:",e); - updateConnectionStatus("disconnected"); - setTimeout(initializeAuth,3000); - } -} - -async function loadSessions(){ - try{ - const r=await fetch("http://localhost:8080/api/sessions"),s=await r.json(),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)}`; - item.onclick=()=>switchSession(session.session_id); - h.appendChild(item); - }); - }catch(e){ - console.error("Failed to load sessions:",e); - } -} - -async function createNewSession(){ - try{ - const r=await fetch("http://localhost:8080/api/sessions",{method:"POST"}),s=await r.json(); - currentSessionId=s.session_id; - hasReceivedInitialMessage=false; - connectWebSocket(); - loadSessions(); - messagesDiv.innerHTML=""; - clearSuggestions(); - updateContextUsage(0); - if(isVoiceMode){ - await stopVoiceSession(); - isVoiceMode=false; - const v=document.getElementById("voiceToggle"); - v.textContent="🎤 Voice Mode"; - voiceBtn.classList.remove("recording"); - } - }catch(e){ - console.error("Failed to create session:",e); - } -} - -function switchSession(s){ - currentSessionId=s; - hasReceivedInitialMessage=false; - loadSessionHistory(s); - connectWebSocket(); - if(isVoiceMode){ - startVoiceSession(); - } - sidebar.classList.remove('open'); -} - -async function loadSessionHistory(s){ - try{ - const r=await fetch("http://localhost:8080/api/sessions/"+s),h=await r.json(),m=document.getElementById("messages"); - m.innerHTML=""; - if(h.length===0){ - updateContextUsage(0); - }else{ - h.forEach(([role,content])=>{ - addMessage(role,content,false); - }); - updateContextUsage(h.length/20); - } - }catch(e){ - console.error("Failed to load session history:",e); - } -} - -function connectWebSocket(){ - if(ws){ - ws.close(); - } - clearTimeout(reconnectTimeout); - const u=getWebSocketUrl(); - ws=new WebSocket(u); - ws.onmessage=function(e){ - const r=JSON.parse(e.data); - if(r.bot_id){ - currentBotId=r.bot_id; - } - if(r.message_type===2){ - const d=JSON.parse(r.content); - handleEvent(d.event,d.data); - return; - } - if(r.message_type===5){ - isContextChange=true; - return; - } - processMessageContent(r); - }; - ws.onopen=function(){ - console.log("Connected to WebSocket"); - updateConnectionStatus("connected"); - reconnectAttempts=0; - hasReceivedInitialMessage=false; - }; - ws.onclose=function(e){ - console.log("WebSocket disconnected:",e.code,e.reason); - updateConnectionStatus("disconnected"); - if(isStreaming){ - showContinueButton(); - } - if(reconnectAttempts{ - updateConnectionStatus("connecting"); - connectWebSocket(); - },d); - }else{ - updateConnectionStatus("disconnected"); - } - }; - ws.onerror=function(e){ - console.error("WebSocket error:",e); - updateConnectionStatus("disconnected"); - }; -} - -function processMessageContent(r){ - if(isContextChange){ - isContextChange=false; - return; - } - if(r.context_usage!==undefined){ - updateContextUsage(r.context_usage); - } - if(r.suggestions&&r.suggestions.length>0){ - handleSuggestions(r.suggestions); - } - if(r.is_complete){ - if(isStreaming){ - finalizeStreamingMessage(); - isStreaming=false; - streamingMessageId=null; - currentStreamingContent=""; - }else{ - addMessage("assistant",r.content,false); - } - }else{ - if(!isStreaming){ - isStreaming=true; - streamingMessageId="streaming-"+Date.now(); - currentStreamingContent=r.content||""; - addMessage("assistant",currentStreamingContent,true,streamingMessageId); - }else{ - currentStreamingContent+=r.content||""; - updateStreamingMessage(currentStreamingContent); - } - } -} - -function handleEvent(t,d){ - console.log("Event received:",t,d); - switch(t){ - case"thinking_start": - showThinkingIndicator(); - break; - case"thinking_end": - hideThinkingIndicator(); - break; - case"warn": - showWarning(d.message); - break; - case"context_usage": - updateContextUsage(d.usage); - break; - case"change_theme": - if(d.color1)themeColor1=d.color1; - if(d.color2)themeColor2=d.color2; - if(d.logo_url)customLogoUrl=d.logo_url; - if(d.title)document.title=d.title; - if(d.logo_text){ - sidebarTitle.textContent=d.logo_text; + applyTheme() { + const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches; + let theme = currentTheme; + if (theme === 'auto') { + theme = prefersDark ? 'dark' : 'light'; } - applyTheme(); - break; - } -} + 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); + } + if (customLogoUrl) { + document.documentElement.style.setProperty('--logo-url', `url('${customLogoUrl}')`); + } + }, -function showThinkingIndicator(){ - if(isThinking)return; - const t=document.createElement("div"); - t.id="thinking-indicator"; - t.className="message-container"; - t.innerHTML=`
`; - messagesDiv.appendChild(t); - gsap.to(t,{opacity:1,y:0,duration:.3,ease:"power2.out"}); - if(!isUserScrolling){ - scrollToBottom(); - } - thinkingTimeout=setTimeout(()=>{ - if(isThinking){ - hideThinkingIndicator(); - showWarning("O servidor pode estar ocupado. A resposta está demorando demais."); - } - },60000); - isThinking=true; -} + // ---------------------------------------------------------------------- + // Lifecycle / event handlers + // ---------------------------------------------------------------------- + init() { + document.addEventListener('ready', () => { + // Assign DOM elements after the document is ready + messagesDiv = document.getElementById("messages"); + input = document.getElementById("messageInput"); + sendBtn = document.getElementById("sendBtn"); + voiceBtn = document.getElementById("voiceBtn"); + connectionStatus = document.getElementById("connectionStatus"); + flashOverlay = document.getElementById("flashOverlay"); + suggestionsContainer = document.getElementById("suggestions"); + floatLogo = document.getElementById("floatLogo"); + sidebar = document.getElementById("sidebar"); + themeBtn = document.getElementById("themeBtn"); + scrollToBottomBtn = document.getElementById("scrollToBottom"); + contextIndicator = document.getElementById("contextIndicator"); + contextPercentage = document.getElementById("contextPercentage"); + contextProgressBar = document.getElementById("contextProgressBar"); + sidebarTitle = document.getElementById("sidebarTitle"); -function hideThinkingIndicator(){ - if(!isThinking)return; - const t=document.getElementById("thinking-indicator"); - if(t){ - gsap.to(t,{opacity:0,duration:.2,onComplete:()=>{if(t.parentNode){t.remove();}}}); - } - if(thinkingTimeout){ - clearTimeout(thinkingTimeout); - thinkingTimeout=null; - } - isThinking=false; -} - -function showWarning(m){ - const w=document.createElement("div"); - w.className="warning-message"; - w.innerHTML=`⚠️ ${m}`; - messagesDiv.appendChild(w); - gsap.from(w,{opacity:0,y:20,duration:.4,ease:"power2.out"}); - if(!isUserScrolling){ - scrollToBottom(); - } - setTimeout(()=>{ - if(w.parentNode){ - gsap.to(w,{opacity:0,duration:.3,onComplete:()=>w.remove()}); - } - },5000); -} - -function showContinueButton(){ - const c=document.createElement("div"); - c.className="message-container"; - c.innerHTML=`

A conexão foi interrompida. Clique em "Continuar" para tentar recuperar a resposta.

`; - messagesDiv.appendChild(c); - gsap.to(c,{opacity:1,y:0,duration:.5,ease:"power2.out"}); - if(!isUserScrolling){ - scrollToBottom(); - } -} - -function continueInterruptedResponse(){ - if(!ws||ws.readyState!==WebSocket.OPEN){ - connectWebSocket(); - } - if(ws&&ws.readyState===WebSocket.OPEN){ - const d={bot_id:"default_bot",user_id:currentUserId,session_id:currentSessionId,channel:"web",content:"continue",message_type:3,media_url:null,timestamp:new Date().toISOString()}; - ws.send(JSON.stringify(d)); - } - document.querySelectorAll(".continue-button").forEach(b=>{b.parentElement.parentElement.parentElement.remove();}); -} - -function addMessage(role,content,streaming=false,msgId=null){ - const m=document.createElement("div"); - m.className="message-container"; - if(role==="user"){ - m.innerHTML=`
${escapeHtml(content)}
`; - updateContextUsage(contextUsage+.05); - }else if(role==="assistant"){ - m.innerHTML=`
${streaming?"":marked.parse(content)}
`; - updateContextUsage(contextUsage+.03); - }else if(role==="voice"){ - m.innerHTML=`
🎤
${content}
`; - }else{ - m.innerHTML=`
${content}
`; - } - messagesDiv.appendChild(m); - gsap.to(m,{opacity:1,y:0,duration:.5,ease:"power2.out"}); - if(!isUserScrolling){ - scrollToBottom(); - } -} - -function updateStreamingMessage(c){ - const m=document.getElementById(streamingMessageId); - if(m){ - m.innerHTML=marked.parse(c); - if(!isUserScrolling){ - scrollToBottom(); - } - } -} - -function finalizeStreamingMessage(){ - const m=document.getElementById(streamingMessageId); - if(m){ - m.innerHTML=marked.parse(currentStreamingContent); - m.removeAttribute("id"); - if(!isUserScrolling){ - scrollToBottom(); - } - } -} - -function escapeHtml(t){ - const d=document.createElement("div"); - d.textContent=t; - return d.innerHTML; -} - -function clearSuggestions(){ - suggestionsContainer.innerHTML=''; -} - -function 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'); - b.textContent=v.text; - b.className='suggestion-button'; - b.onclick=()=>{setContext(v.context);input.value='';}; - suggestionsContainer.appendChild(b); - }); -} - -let pendingContextChange=null; - -async function setContext(c){ - try{ - const t=event?.target?.textContent||c; - addMessage("user",t); - const i=document.getElementById('messageInput'); - if(i){i.value='';} - if(ws&&ws.readyState===WebSocket.OPEN){ - 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); - r(); + // Theme initialization and focus + const savedTheme = localStorage.getItem('gb-theme') || 'auto'; + currentTheme = savedTheme; + this.applyTheme(); + window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => { + if (currentTheme === 'auto') { + this.applyTheme(); } - }; - 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)); + }); + input.focus(); + + // UI event listeners + document.addEventListener('click', (e) => { + if (sidebar.classList.contains('open') && !sidebar.contains(e.target) && !floatLogo.contains(e.target)) { + sidebar.classList.remove('open'); + } + }); + + messagesDiv.addEventListener('scroll', () => { + const isAtBottom = messagesDiv.scrollHeight - messagesDiv.scrollTop <= messagesDiv.clientHeight + 100; + if (!isAtBottom) { + isUserScrolling = true; + scrollToBottomBtn.classList.add('visible'); + } else { + isUserScrolling = false; + scrollToBottomBtn.classList.remove('visible'); + } + }); + + scrollToBottomBtn.addEventListener('click', () => { + this.scrollToBottom(); + }); + + sendBtn.onclick = () => this.sendMessage(); + input.addEventListener("keypress", e => { if (e.key === "Enter") this.sendMessage(); }); + window.addEventListener("focus", () => { + if (!ws || ws.readyState !== WebSocket.OPEN) { + this.connectWebSocket(); + } + }); + + // Start authentication flow + this.initializeAuth(); }); - await pendingContextChange; - const x=document.getElementById('contextIndicator'); - if(x){document.getElementById('contextPercentage').textContent=c;} - }else{ - console.warn("WebSocket não está conectado. Tentando reconectar..."); - connectWebSocket(); - } - }catch(err){ - console.error('Failed to set context:',err); - } -} + }, -async function sendMessage(){ - if(pendingContextChange){ - await pendingContextChange; - pendingContextChange=null; - } - const m=input.value.trim(); - if(!m||!ws||ws.readyState!==WebSocket.OPEN){ - if(!ws||ws.readyState!==WebSocket.OPEN){ - showWarning("Conexão não disponível. Tentando reconectar..."); - connectWebSocket(); - } - return; - } - if(isThinking){ - hideThinkingIndicator(); - } - 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()}; - ws.send(JSON.stringify(d)); - input.value=""; - input.focus(); -} + updateContextUsage(u) { + contextUsage = u; + const p = Math.min(100, Math.round(u * 100)); + contextPercentage.textContent = `${p}%`; + contextProgressBar.style.width = `${p}%`; + contextIndicator.classList.remove('visible'); + }, -sendBtn.onclick=sendMessage; -input.addEventListener("keypress",e=>{if(e.key==="Enter")sendMessage();}); + flashScreen() { + gsap.to(flashOverlay, { opacity: 0.15, duration: 0.1, onComplete: () => { + gsap.to(flashOverlay, { opacity: 0, duration: 0.2 }); + } }); + }, -async function toggleVoiceMode(){ - isVoiceMode=!isVoiceMode; - const v=document.getElementById("voiceToggle"); - if(isVoiceMode){ - v.textContent="🔴 Stop Voice"; - v.classList.add("recording"); - await startVoiceSession(); - }else{ - v.textContent="🎤 Voice Mode"; - v.classList.remove("recording"); - await stopVoiceSession(); - } -} + updateConnectionStatus(s) { + connectionStatus.className = `connection-status ${s}`; + }, -async function startVoiceSession(){ - if(!currentSessionId)return; - try{ - 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})}),d=await r.json(); - if(d.token){ - await connectToVoiceRoom(d.token); - startVoiceRecording(); - } - }catch(e){ - console.error("Failed to start voice session:",e); - showWarning("Falha ao iniciar modo de voz"); - } -} + getWebSocketUrl() { + const p = "ws:", s = currentSessionId || crypto.randomUUID(), u = currentUserId || crypto.randomUUID(); + return `${p}//localhost:8080/ws?session_id=${s}&user_id=${u}`; + }, -async function stopVoiceSession(){ - if(!currentSessionId)return; - try{ - await fetch("http://localhost:8080/api/voice/stop",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({session_id:currentSessionId})}); - if(voiceRoom){ - voiceRoom.disconnect(); - voiceRoom=null; - } - if(mediaRecorder&&mediaRecorder.state==="recording"){ - mediaRecorder.stop(); - } - }catch(e){ - console.error("Failed to stop voice session:",e); - } -} + 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 a = await r.json(); + currentUserId = a.user_id; + currentSessionId = a.session_id; + this.connectWebSocket(); + this.loadSessions(); + } catch (e) { + console.error("Failed to initialize auth:", e); + this.updateConnectionStatus("disconnected"); + setTimeout(() => this.initializeAuth(), 3000); + } + }, -async function connectToVoiceRoom(t){ - try{ - const r=new LiveKitClient.Room(),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); - try{ - const j=JSON.parse(m); - if(j.type==="voice_response"){ - addMessage("assistant",j.text); + async loadSessions() { + try { + const r = await fetch("http://localhost:8080/api/sessions"); + 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)}`; + item.onclick = () => this.switchSession(session.session_id); + h.appendChild(item); + }); + } catch (e) { + console.error("Failed to load sessions:", e); + } + }, + + async createNewSession() { + try { + const r = await fetch("http://localhost:8080/api/sessions", { method: "POST" }); + const s = await r.json(); + currentSessionId = s.session_id; + hasReceivedInitialMessage = false; + this.connectWebSocket(); + this.loadSessions(); + messagesDiv.innerHTML = ""; + this.clearSuggestions(); + this.updateContextUsage(0); + if (isVoiceMode) { + await this.stopVoiceSession(); + isVoiceMode = false; + const v = document.getElementById("voiceToggle"); + v.textContent = "🎤 Voice Mode"; + voiceBtn.classList.remove("recording"); } - }catch(e){ - console.log("Voice data:",m); + } catch (e) { + console.error("Failed to create session:", e); } - }); - const l=await LiveKitClient.createLocalTracks({audio:true,video:false}); - for(const k of l){ - await r.localParticipant.publishTrack(k); - } - }catch(e){ - console.error("Failed to connect to voice room:",e); - showWarning("Falha na conexão de voz"); - } -} + }, -function startVoiceRecording(){ - if(!navigator.mediaDevices){ - 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"});simulateVoiceTranscription();}; - mediaRecorder.start(); - setTimeout(()=>{ - if(mediaRecorder&&mediaRecorder.state==="recording"){ - mediaRecorder.stop(); - setTimeout(()=>{ - if(isVoiceMode){ - startVoiceRecording(); + switchSession(s) { + currentSessionId = s; + hasReceivedInitialMessage = false; + this.loadSessionHistory(s); + this.connectWebSocket(); + if (isVoiceMode) { + this.startVoiceSession(); + } + sidebar.classList.remove('open'); + }, + + async loadSessionHistory(s) { + try { + const r = await fetch(`http://localhost:8080/api/sessions/${s}`); + const h = await r.json(); + const m = document.getElementById("messages"); + m.innerHTML = ""; + if (h.length === 0) { + this.updateContextUsage(0); + } else { + h.forEach(([role, content]) => { + this.addMessage(role, content, false); + }); + this.updateContextUsage(h.length / 20); + } + } catch (e) { + console.error("Failed to load session history:", e); + } + }, + + connectWebSocket() { + if (ws) { + ws.close(); + } + clearTimeout(reconnectTimeout); + const u = this.getWebSocketUrl(); + ws = new WebSocket(u); + ws.onmessage = (e) => { + const r = JSON.parse(e.data); + 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; + } + if (r.message_type === 5) { + isContextChange = true; + return; + } + this.processMessageContent(r); + }; + ws.onopen = () => { + console.log("Connected to WebSocket"); + this.updateConnectionStatus("connected"); + reconnectAttempts = 0; + hasReceivedInitialMessage = false; + }; + ws.onclose = (e) => { + console.log("WebSocket disconnected:", e.code, e.reason); + this.updateConnectionStatus("disconnected"); + if (isStreaming) { + this.showContinueButton(); + } + if (reconnectAttempts < maxReconnectAttempts) { + reconnectAttempts++; + const d = Math.min(1000 * reconnectAttempts, 10000); + reconnectTimeout = setTimeout(() => { + this.updateConnectionStatus("connecting"); + this.connectWebSocket(); + }, d); + } else { + this.updateConnectionStatus("disconnected"); + } + }; + ws.onerror = (e) => { + console.error("WebSocket error:", e); + this.updateConnectionStatus("disconnected"); + }; + }, + + processMessageContent(r) { + if (isContextChange) { + isContextChange = false; + return; + } + if (r.context_usage !== undefined) { + this.updateContextUsage(r.context_usage); + } + if (r.suggestions && r.suggestions.length > 0) { + this.handleSuggestions(r.suggestions); + } + if (r.is_complete) { + if (isStreaming) { + this.finalizeStreamingMessage(); + isStreaming = false; + streamingMessageId = null; + currentStreamingContent = ""; + } else { + this.addMessage("assistant", r.content, false); + } + } else { + if (!isStreaming) { + isStreaming = true; + streamingMessageId = "streaming-" + Date.now(); + currentStreamingContent = r.content || ""; + this.addMessage("assistant", currentStreamingContent, true, streamingMessageId); + } else { + currentStreamingContent += r.content || ""; + this.updateStreamingMessage(currentStreamingContent); + } + } + }, + + handleEvent(t, d) { + console.log("Event received:", t, d); + switch (t) { + case "thinking_start": + this.showThinkingIndicator(); + break; + case "thinking_end": + this.hideThinkingIndicator(); + break; + case "warn": + this.showWarning(d.message); + break; + case "context_usage": + this.updateContextUsage(d.usage); + break; + case "change_theme": + if (d.color1) themeColor1 = d.color1; + if (d.color2) themeColor2 = d.color2; + if (d.logo_url) customLogoUrl = d.logo_url; + if (d.title) document.title = d.title; + if (d.logo_text) { + sidebarTitle.textContent = d.logo_text; } - },1000); + this.applyTheme(); + break; } - },5000); - }).catch(e=>{ - console.error("Error accessing microphone:",e); - showWarning("Erro ao acessar microfone"); - }); + }, + + showThinkingIndicator() { + if (isThinking) return; + const t = document.createElement("div"); + t.id = "thinking-indicator"; + t.className = "message-container"; + t.innerHTML = `
`; + messagesDiv.appendChild(t); + gsap.to(t, { opacity: 1, y: 0, duration: .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."); + } + }, 60000); + isThinking = true; + }, + + hideThinkingIndicator() { + if (!isThinking) return; + const t = document.getElementById("thinking-indicator"); + if (t) { + gsap.to(t, { opacity: 0, duration: .2, onComplete: () => { if (t.parentNode) { t.remove(); } } }); + } + if (thinkingTimeout) { + clearTimeout(thinkingTimeout); + thinkingTimeout = null; + } + isThinking = false; + }, + + showWarning(m) { + const w = document.createElement("div"); + w.className = "warning-message"; + w.innerHTML = `⚠️ ${m}`; + messagesDiv.appendChild(w); + gsap.from(w, { opacity: 0, y: 20, duration: .4, ease: "power2.out" }); + if (!isUserScrolling) { + this.scrollToBottom(); + } + setTimeout(() => { + if (w.parentNode) { + gsap.to(w, { opacity: 0, duration: .3, onComplete: () => w.remove() }); + } + }, 5000); + }, + + showContinueButton() { + const c = document.createElement("div"); + c.className = "message-container"; + c.innerHTML = `

A conexão foi interrompida. Clique em "Continuar" para tentar recuperar a resposta.

`; + messagesDiv.appendChild(c); + gsap.to(c, { opacity: 1, y: 0, duration: .5, ease: "power2.out" }); + if (!isUserScrolling) { + this.scrollToBottom(); + } + }, + + continueInterruptedResponse() { + if (!ws || ws.readyState !== WebSocket.OPEN) { + this.connectWebSocket(); + } + if (ws && ws.readyState === WebSocket.OPEN) { + const d = { + bot_id: "default_bot", + user_id: currentUserId, + session_id: currentSessionId, + channel: "web", + content: "continue", + message_type: 3, + media_url: null, + timestamp: new Date().toISOString() + }; + ws.send(JSON.stringify(d)); + } + document.querySelectorAll(".continue-button").forEach(b => { b.parentElement.parentElement.parentElement.remove(); }); + }, + + addMessage(role, content, streaming = false, msgId = null) { + const m = document.createElement("div"); + m.className = "message-container"; + if (role === "user") { + m.innerHTML = `
${this.escapeHtml(content)}
`; + this.updateContextUsage(contextUsage + .05); + } else if (role === "assistant") { + m.innerHTML = `
${streaming ? "" : marked.parse(content)}
`; + this.updateContextUsage(contextUsage + .03); + } else if (role === "voice") { + m.innerHTML = `
🎤
${content}
`; + } else { + m.innerHTML = `
${content}
`; + } + messagesDiv.appendChild(m); + gsap.to(m, { opacity: 1, y: 0, duration: .5, ease: "power2.out" }); + if (!isUserScrolling) { + this.scrollToBottom(); + } + }, + + updateStreamingMessage(c) { + const m = document.getElementById(streamingMessageId); + if (m) { + m.innerHTML = marked.parse(c); + if (!isUserScrolling) { + this.scrollToBottom(); + } + } + }, + + finalizeStreamingMessage() { + const m = document.getElementById(streamingMessageId); + if (m) { + m.innerHTML = marked.parse(currentStreamingContent); + m.removeAttribute("id"); + if (!isUserScrolling) { + this.scrollToBottom(); + } + } + }, + + escapeHtml(t) { + const d = document.createElement("div"); + d.textContent = t; + return d.innerHTML; + }, + + clearSuggestions() { + 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'); + b.textContent = v.text; + b.className = 'suggestion-button'; + b.onclick = () => { this.setContext(v.context); input.value = ''; }; + suggestionsContainer.appendChild(b); + }); + }, + + async setContext(c) { + try { + const t = event?.target?.textContent || c; + this.addMessage("user", t); + input.value = ''; + if (ws && ws.readyState === WebSocket.OPEN) { + 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); + 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.send(JSON.stringify(s)); + }); + await pendingContextChange; + 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); + } + }, + + async sendMessage() { + if (pendingContextChange) { + await pendingContextChange; + pendingContextChange = null; + } + const m = input.value.trim(); + if (!m || !ws || ws.readyState !== WebSocket.OPEN) { + if (!ws || ws.readyState !== WebSocket.OPEN) { + this.showWarning("Conexão não disponível. Tentando reconectar..."); + this.connectWebSocket(); + } + return; + } + if (isThinking) { + 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() }; + ws.send(JSON.stringify(d)); + input.value = ""; + input.focus(); + }, + + async toggleVoiceMode() { + isVoiceMode = !isVoiceMode; + const v = document.getElementById("voiceToggle"); + if (isVoiceMode) { + v.textContent = "🔴 Stop Voice"; + v.classList.add("recording"); + await this.startVoiceSession(); + } else { + v.textContent = "🎤 Voice Mode"; + v.classList.remove("recording"); + await this.stopVoiceSession(); + } + }, + + async startVoiceSession() { + if (!currentSessionId) return; + try { + 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 }) + }); + const d = await r.json(); + if (d.token) { + await this.connectToVoiceRoom(d.token); + this.startVoiceRecording(); + } + } catch (e) { + console.error("Failed to start voice session:", e); + this.showWarning("Falha ao iniciar modo de voz"); + } + }, + + async stopVoiceSession() { + if (!currentSessionId) return; + try { + await fetch("http://localhost:8080/api/voice/stop", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ session_id: currentSessionId }) + }); + if (voiceRoom) { + voiceRoom.disconnect(); + voiceRoom = null; + } + if (mediaRecorder && mediaRecorder.state === "recording") { + mediaRecorder.stop(); + } + } catch (e) { + console.error("Failed to stop voice session:", e); + } + }, + + async connectToVoiceRoom(t) { + try { + const r = new LiveKitClient.Room(); + 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); + try { + const j = JSON.parse(m); + if (j.type === "voice_response") { + this.addMessage("assistant", j.text); + } + } catch (e) { + console.log("Voice data:", m); + } + }); + const l = await LiveKitClient.createLocalTracks({ audio: true, video: false }); + for (const k of l) { + await r.localParticipant.publishTrack(k); + } + } catch (e) { + console.error("Failed to connect to voice room:", e); + this.showWarning("Falha na conexão de voz"); + } + }, + + startVoiceRecording() { + if (!navigator.mediaDevices) { + 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"); + }); + }, + + 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 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); + } + this.addMessage("voice", `🎤 ${r}`); + }, + + scrollToBottom() { + messagesDiv.scrollTop = messagesDiv.scrollHeight; + isUserScrolling = false; + scrollToBottomBtn.classList.remove('visible'); + } + }; } -function 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"],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); - } - addMessage("voice",`🎤 ${r}`); -} - -function scrollToBottom(){ - messagesDiv.scrollTop=messagesDiv.scrollHeight; - isUserScrolling=false; - scrollToBottomBtn.classList.remove('visible'); -} - -window.addEventListener("load",initializeAuth); -window.addEventListener("focus",function(){ - if(!ws||ws.readyState!==WebSocket.OPEN){ - connectWebSocket(); - } -}); +// Initialize the app +chatApp().init(); diff --git a/web/desktop/index.html b/web/desktop/index.html index d4e76821..3ea0a31f 100644 --- a/web/desktop/index.html +++ b/web/desktop/index.html @@ -1,42 +1,43 @@ + - - General Bots Desktop - - - + + General Bots + + + + + + + + - + + +
+ +
+ + + + + + + - - - - - - - - - - - - - - + + \ No newline at end of file