From 017d4aecd02614eeb2cd3971b1a92cf25d8ab60a Mon Sep 17 00:00:00 2001 From: "Rodrigo Rodriguez (Pragmatismo)" Date: Sun, 16 Nov 2025 22:53:51 -0300 Subject: [PATCH] feat: add Chat navigation link and section mapping - Added a new "Chat" link in the desktop navigation bar with appropriate click handling and active state styling. - Updated the layout configuration to include the Chat section, mapping it to `chat/chat.html`. - Enables users to switch to the Chat interface directly from the main navigation. --- web/desktop/chat/chat.css | 586 +++++++++++++++++++++++++++++++++ web/desktop/chat/chat.html | 111 +++++++ web/desktop/chat/chat.js | 640 +++++++++++++++++++++++++++++++++++++ web/desktop/index.html | 3 + web/desktop/js/layout.js | 1 + 5 files changed, 1341 insertions(+) create mode 100644 web/desktop/chat/chat.css create mode 100644 web/desktop/chat/chat.html create mode 100644 web/desktop/chat/chat.js diff --git a/web/desktop/chat/chat.css b/web/desktop/chat/chat.css new file mode 100644 index 000000000..211160389 --- /dev/null +++ b/web/desktop/chat/chat.css @@ -0,0 +1,586 @@ +@import url("https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500&display=swap"); +:root{ +--bg:#ffffff; +--fg:#000000; +--border:#e0e0e0; +--accent:#0066ff; +--glass:rgba(0,0,0,0.02); +--shadow:rgba(0,0,0,0.05); +--logo-url:url('https://pragmatismo.com.br/icons/general-bots.svg'); +--gradient-1:linear-gradient(135deg,rgba(0,102,255,0.05) 0%,rgba(0,102,255,0.0) 100%); +--gradient-2:linear-gradient(45deg,rgba(0,0,0,0.02) 0%,rgba(0,0,0,0.0) 100%); +} +[data-theme="dark"]{ +--bg:#1a1a1a; +--fg:#ffffff; +--border:#333333; +--accent:#ffffff; +--glass:rgba(255,255,255,0.05); +--shadow:rgba(0,0,0,0.5); +--gradient-1:linear-gradient(135deg,rgba(255,255,255,0.08) 0%,rgba(255,255,255,0.0) 100%); +--gradient-2:linear-gradient(45deg,rgba(255,255,255,0.03) 0%,rgba(255,255,255,0.0) 100%); +} +*{margin:0;padding:0;box-sizing:border-box} +body{ +font-family:"Inter",sans-serif; +background:var(--bg); +color:var(--fg); +overflow:hidden; +transition:background 0.3s, color 0.3s; +display:flex; +flex-direction:column; +height:100vh; +position:relative; +} +body::before{ +content:''; +position:fixed; +inset:0; +background:var(--gradient-1); +pointer-events:none; +z-index:0; +} +.float-menu{ +position:fixed; +left:20px; +top:20px; +display:flex; +flex-direction:column; +gap:8px; +z-index:1000; +} +.float-logo{ +width:40px; +height:40px; +background:var(--logo-url) center/contain no-repeat; +filter:var(--logo-filter, none); +border-radius:50%; +cursor:pointer; +transition:all 0.3s; +border:1px solid var(--border); +backdrop-filter:blur(10px); +} +[data-theme="dark"] .float-logo{ +} +.float-logo:hover{ +transform:scale(1.1) rotate(5deg); +} +.menu-button{ +width:40px; +height:40px; +border-radius:50%; +display:flex; +align-items:center; +justify-content:center; +cursor:pointer; +transition:all 0.3s; +background:var(--bg); +border:1px solid var(--border); +font-size:16px; +color:var(--fg); +backdrop-filter:blur(10px); +} +.menu-button:hover{ +transform:scale(1.1) rotate(-5deg); +background:var(--fg); +color:var(--bg); +} +.sidebar{ +position:fixed; +left:-320px; +top:0; +width:320px; +height:100vh; +background:var(--bg); +border-right:1px solid var(--border); +transition:left 0.4s cubic-bezier(0.4,0,0.2,1); +z-index:999; +overflow-y:auto; +padding:20px; +backdrop-filter:blur(20px); +box-shadow:4px 0 20px var(--shadow); +} +.sidebar.open{ +left:0; +} +.sidebar-header{ +display:flex; +align-items:center; +gap:12px; +margin-bottom:30px; +padding-top:10px; +} +.sidebar-logo{ +width:32px; +height:32px; +background:var(--logo-url) center/contain no-repeat; +filter:var(--logo-filter, none); +} +[data-theme="dark"] .sidebar-logo{ +} +.sidebar-title{ +font-size:16px; +font-weight:500; +} +.sidebar-button{ +width:100%; +padding:12px 16px; +border-radius:12px; +cursor:pointer; +transition:all 0.3s; +font-weight:500; +font-size:14px; +margin-bottom:8px; +background:var(--glass); +border:1px solid var(--border); +color:var(--fg); +text-align:left; +} +.sidebar-button:hover{ +background:var(--fg); +color:var(--bg); +transform:translateX(4px) scale(1.02); +} +.history-section{ +margin-top:20px; +} +.history-title{ +font-size:12px; +opacity:0.5; +margin-bottom:12px; +text-transform:uppercase; +letter-spacing:0.5px; +} +.history-item{ +padding:10px 14px; +margin-bottom:6px; +border-radius:10px; +cursor:pointer; +transition:all 0.3s; +font-size:13px; +border:1px solid transparent; +} +.history-item:hover{ +background:var(--fg); +color:var(--bg); +transform:translateX(4px) scale(1.02); +} +#messages{ +flex:1; +overflow-y:auto; +padding:20px 20px 140px; +max-width:680px; +margin:0 auto; +width:100%; +position:relative; +z-index:1; +} +.message-container{ +margin-bottom:24px; +opacity:0; +transform:translateY(10px); +} +.user-message{ +display:flex; +justify-content:flex-end; +margin-bottom:8px; +} +.user-message-content{ +background:var(--fg); +color:var(--bg); +border-radius:18px; +padding:12px 18px; +max-width:80%; +font-size:14px; +line-height:1.5; +box-shadow:0 2px 8px var(--shadow); +position:relative; +overflow:hidden; +} +.user-message-content::before{ +content:''; +position:absolute; +inset:0; +background:var(--gradient-2); +opacity:0.3; +pointer-events:none; +} +.assistant-message{ +display:flex; +gap:8px; +align-items:flex-start; +} +.assistant-avatar{ +width:24px; +height:24px; +border-radius:50%; +background:var(--logo-url) center/contain no-repeat; +flex-shrink:0; +margin-top:2px; +filter:var(--logo-filter, none); +} +[data-theme="dark"] .assistant-avatar{ +} +.assistant-message-content{ +flex:1; +font-size:14px; +line-height:1.7; +background:var(--glass); +border-radius:18px; +padding:12px 18px; +border:1px solid var(--border); +box-shadow:0 2px 8px var(--shadow); +position:relative; +overflow:hidden; +} +.assistant-message-content::before{ +content:''; +position:absolute; +inset:0; +background:var(--gradient-1); +opacity:0.5; +pointer-events:none; +} +.thinking-indicator{ +display:flex; +gap:8px; +align-items:center; +font-size:13px; +opacity:0.4; +} +.typing-dots{ +display:flex; +gap:4px; +} +.typing-dot{ +width:4px; +height:4px; +background:var(--fg); +border-radius:50%; +animation:bounce 1.4s infinite; +} +.typing-dot:nth-child(1){animation-delay:-.32s} +.typing-dot:nth-child(2){animation-delay:-.16s} +@keyframes bounce{ +0%,80%,100%{transform:scale(0);opacity:.3} +40%{transform:scale(1);opacity:1} +} +footer{ +position:fixed; +bottom:0; +left:0; +right:0; +background:var(--bg); +border-top:1px solid var(--border); +padding:12px; +z-index:100; +transition:all 0.3s; +backdrop-filter:blur(20px); +} +.suggestions-container{ +display:flex; +flex-wrap:wrap; +gap:4px; +margin-bottom:8px; +justify-content:center; +max-width:680px; +margin:0 auto 8px; +} +.suggestion-button{ +padding:6px 12px; +border-radius:12px; +cursor:pointer; +font-size:11px; +font-weight:400; +transition:all 0.2s; +background:var(--glass); +border:1px solid var(--border); +color:var(--fg); +} +.suggestion-button:hover{ +background:var(--fg); +color:var(--bg); +transform:scale(1.05); +} +.input-container{ +display:flex; +gap:6px; +max-width:680px; +margin:0 auto; +align-items:center; +} +#messageInput{ +flex:1; +border-radius:20px; +padding:10px 16px; +font-size:14px; +font-family:"Inter",sans-serif; +outline:none; +transition:all 0.3s; +background:var(--glass); +border:1px solid var(--border); +color:var(--fg); +backdrop-filter:blur(10px); +} +#messageInput:focus{ +border-color:var(--accent); +box-shadow:0 0 0 3px rgba(0,102,255,0.1); +} +#messageInput::placeholder{ +opacity:0.3; +} +#sendBtn,#voiceBtn{ +width:36px; +height:36px; +border-radius:18px; +display:flex; +align-items:center; +justify-content:center; +cursor:pointer; +transition:all 0.2s; +border:none; +background:var(--fg); +color:var(--bg); +font-size:16px; +flex-shrink:0; +} +#sendBtn:hover,#voiceBtn:hover{ +transform:scale(1.08) rotate(5deg); +} +#sendBtn:active,#voiceBtn:active{ +transform:scale(0.95); +} +#voiceBtn.recording{ +animation:pulse 1.5s infinite; +} +@keyframes pulse{ +0%,100%{opacity:1;transform:scale(1)} +50%{opacity:0.6;transform:scale(1.1)} +} +.flash-overlay{ +position:fixed; +inset:0; +background:var(--fg); +opacity:0; +pointer-events:none; +z-index:9999; +} +.scroll-to-bottom{ +position:fixed; +bottom:80px; +right:20px; +width:40px; +height:40px; +background:var(--fg); +border:1px solid var(--border); +border-radius:50%; +color:var(--bg); +font-size:18px; +cursor:pointer; +display:none; +align-items:center; +justify-content:center; +transition:all 0.3s; +z-index:90; +} +.scroll-to-bottom.visible{ +display:flex; +} +.scroll-to-bottom:hover{ +transform:scale(1.1) rotate(180deg); +} +.warning-message{ +border-radius:12px; +padding:12px 16px; +margin-bottom:18px; +opacity:0.6; +background:var(--glass); +border:1px solid var(--border); +font-size:13px; +} +.continue-button{ +display:inline-block; +border-radius:10px; +padding:8px 16px; +font-weight:500; +cursor:pointer; +margin-top:10px; +transition:all 0.3s; +font-size:13px; +background:var(--glass); +border:1px solid var(--border); +} +.continue-button:hover{ +background:var(--fg); +color:var(--bg); +transform:translateY(-2px); +} +.context-indicator{ +position:fixed; +bottom:130px; +right:20px; +width:120px; +border-radius:12px; +padding:10px; +font-size:10px; +text-align:center; +z-index:90; +background:var(--bg); +border:1px solid var(--border); +display:none; +backdrop-filter:blur(10px); +} +.context-indicator.visible{ +display:block; +} +.context-progress{ +height:3px; +background:var(--glass); +border-radius:2px; +margin-top:6px; +overflow:hidden; +} +.context-progress-bar{ +height:100%; +background:var(--accent); +border-radius:2px; +transition:width 0.3s; +} +.connection-status{ +position:fixed; +top:20px; +right:20px; +width:8px; +height:8px; +border-radius:50%; +z-index:1000; +transition:all 0.3s; +} +.connection-status.connecting{ +background:var(--fg); +opacity:0.3; +animation:ping 1.5s infinite; +} +.connection-status.connected{ +background:var(--accent); +opacity:0.8; +} +.connection-status.disconnected{ +background:var(--fg); +opacity:0.2; +} +@keyframes ping{ +0%,100%{opacity:0.3;transform:scale(0.8)} +50%{opacity:0.8;transform:scale(1.2)} +} +.markdown-content p{ +margin-bottom:12px; +line-height:1.7; +} +.markdown-content ul,.markdown-content ol{ +margin-bottom:12px; +padding-left:20px; +} +.markdown-content li{ +margin-bottom:4px; +} +.markdown-content code{ +background:var(--glass); +padding:2px 6px; +border-radius:4px; +font-family:monospace; +font-size:13px; +} +.markdown-content pre{ +border-radius:8px; +padding:12px; +overflow-x:auto; +margin-bottom:12px; +background:var(--glass); +border:1px solid var(--border); +} +.markdown-content pre code{ +background:none; +padding:0; +} +.markdown-content h1,.markdown-content h2,.markdown-content h3{ +margin-top:16px; +margin-bottom:8px; +font-weight:600; +} +.markdown-content h1{font-size:20px} +.markdown-content h2{font-size:18px} +.markdown-content h3{font-size:16px} +.markdown-content table{ +width:100%; +border-collapse:collapse; +margin-bottom:14px; +} +.markdown-content table th,.markdown-content table td{ +padding:8px; +text-align:left; +border:1px solid var(--border); +} +.markdown-content table th{ +font-weight:600; +background:var(--glass); +} +.markdown-content blockquote{ +border-left:2px solid var(--accent); +padding-left:14px; +margin:12px 0; +opacity:0.7; +font-style:italic; +} +.markdown-content a{ +color:var(--accent); +text-decoration:none; +transition:all 0.3s; +} +.markdown-content a:hover{ +opacity:0.7; +text-decoration:underline; +} +::-webkit-scrollbar{ +width:6px; +} +::-webkit-scrollbar-track{ +background:transparent; +} +::-webkit-scrollbar-thumb{ +background:var(--border); +border-radius:3px; +} +::-webkit-scrollbar-thumb:hover{ +background:var(--fg); +opacity:0.3; +} +@media(max-width:768px){ +.sidebar{ +width:100%; +left:-100%; +} +#messages{ +padding:20px 16px 140px; +} +.float-menu{ +left:12px; +top:12px; +} +.float-logo,.menu-button{ +width:36px; +height:36px; +font-size:14px; +} +.scroll-to-bottom{ +width:36px; +height:36px; +bottom:70px; +right:12px; +} +.context-indicator{ +bottom:120px; +right:12px; +width:100px; +} +} diff --git a/web/desktop/chat/chat.html b/web/desktop/chat/chat.html new file mode 100644 index 000000000..b2027e633 --- /dev/null +++ b/web/desktop/chat/chat.html @@ -0,0 +1,111 @@ + + + + +General Bots Chat + + + + + + + +
+
+
+ + +
+ + + +
+
+
+

General Bots Chat

+
+ +
+ +
+
+

+ +
+
+ +
+
+ +
+ + +
+
+ + + + + + diff --git a/web/desktop/chat/chat.js b/web/desktop/chat/chat.js new file mode 100644 index 000000000..b617f1de3 --- /dev/null +++ b/web/desktop/chat/chat.js @@ -0,0 +1,640 @@ +function chatApp() { + return { + // Current navigation section (e.g., All Chats, Direct, Group) + 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' }, + { 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' } + ], + // 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 ----- */ + +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}); + +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(); + break; + } +} + +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; +} + +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(); + } + }; + 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..."); + 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(); +} + +sendBtn.onclick=sendMessage; +input.addEventListener("keypress",e=>{if(e.key==="Enter")sendMessage();}); + +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(); + } +} + +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"); + } +} + +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 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); + } + }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); + 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(); + } + },1000); + } + },5000); + }).catch(e=>{ + console.error("Error accessing microphone:",e); + showWarning("Erro ao acessar microfone"); + }); +} + +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(); + } +}); diff --git a/web/desktop/index.html b/web/desktop/index.html index 93ba1873c..d4e768219 100644 --- a/web/desktop/index.html +++ b/web/desktop/index.html @@ -10,6 +10,9 @@