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(); } });