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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
💬
+
Select a chat to view details
+
+
+
+
+
+
+
+
+
+
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=``;
+ updateContextUsage(contextUsage+.05);
+ }else if(role==="assistant"){
+ m.innerHTML=`${streaming?"":marked.parse(content)}
`;
+ updateContextUsage(contextUsage+.03);
+ }else if(role==="voice"){
+ m.innerHTML=``;
+ }else{
+ m.innerHTML=``;
+ }
+ 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 @@