body::before{content:'';position:fixed;inset:0;background:radial-gradient(circle at 20% 30%,rgba(255,255,255,0.06),transparent 60%),radial-gradient(circle at 80% 70%,rgba(255,255,255,0.04),transparent 60%);pointer-events:none;z-index:0}
[data-theme="light"] body::before{background:radial-gradient(circle at 20% 30%,rgba(0,0,0,0.03),transparent 60%),radial-gradient(circle at 80% 70%,rgba(0,0,0,0.02),transparent 60%)}
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,lastMessageLength=0,contextUsage=0,isUserScrolling=false,autoScrollEnabled=true,currentTheme='dark',isContextChange=false;
function toggleSidebar(){document.getElementById("sidebar").classList.toggle("open")}
function toggleTheme(){const t=['auto','dark','light'],s=localStorage.getItem('gb-theme')||'auto',i=t.indexOf(s),n=t[(i+1)%t.length];localStorage.setItem('gb-theme',n);if(n==='auto'){const p=window.matchMedia('(prefers-color-scheme: dark)').matches;currentTheme=p?'dark':'light';themeToggle.textContent='⚙️'}else{currentTheme=n;themeToggle.textContent=n==='dark'?'🌙':'☀️'}document.documentElement.setAttribute('data-theme',currentTheme)}
function updateConnectionStatus(s){connectionStatus.className=`connection-status ${s}`}
function getWebSocketUrl(){const p=window.location.protocol==="https:"?"wss:":"ws:",s=currentSessionId||crypto.randomUUID(),u=currentUserId||crypto.randomUUID();return`${p}//${window.location.host}/ws?session_id=${s}&user_id=${u}`}
function updateContextUsage(u){contextUsage=u;const p=Math.min(100,Math.round(u*100));contextPercentage.textContent=`${p}%`;contextProgressBar.style.width=`${p}%`;if(p>=50){}else{contextIndicator.style.display="none"}}
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(`/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("/api/sessions"),s=await r.json(),h=document.getElementById("history");h.innerHTML=""}catch(e){console.error("Failed to load sessions:",e)}}
async function createNewSession(){try{const r=await fetch("/api/sessions",{method:"POST"}),s=await r.json();currentSessionId=s.session_id;hasReceivedInitialMessage=false;connectWebSocket();loadSessions();messagesDiv.innerHTML=`<divid="emptyState"><divclass="empty-icon glass"><imgsrc="https://pragmatismo.com.br/gb-logo.png"alt="GB"/></div><h2class="empty-title">Bem-vindo ao General Bots</h2><pclass="empty-subtitle">Seu assistente de IA avançado</p></div>`;clearSuggestions();updateContextUsage(0);if(isVoiceMode){await stopVoiceSession();isVoiceMode=false;const v=document.getElementById("voiceToggle");v.textContent="🎤 Modo Voz";v.classList.remove("recording");document.getElementById("voiceStatus").style.display="none"}if(window.innerWidth<=768){document.getElementById("sidebar").classList.remove("open")}}catch(e){console.error("Failed to create session:",e)}}
function switchSession(s){currentSessionId=s;hasReceivedInitialMessage=false;loadSessionHistory(s);connectWebSocket();if(isVoiceMode){startVoiceSession()}if(window.innerWidth<=768){document.getElementById("sidebar").classList.remove("open")}}
async function loadSessionHistory(s){try{const r=await fetch("/api/sessions/"+s),h=await r.json(),m=document.getElementById("messages");m.innerHTML="";if(h.length===0){m.innerHTML=`<divid="emptyState"><divclass="empty-icon glass"><imgsrc="https://pragmatismo.com.br/gb-logo.png"alt="GB"/></div><h2class="empty-title">Bem-vindo ao General Bots</h2><pclass="empty-subtitle">Seu assistente de IA avançado</p></div>`;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<maxReconnectAttempts){reconnectAttempts++;constd=Math.min(1000*reconnectAttempts,10000);console.log(`Reconnectingin${d}ms...(attempt${reconnectAttempts})`);reconnectTimeout=setTimeout(()=>{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}const e=document.getElementById("emptyState");if(e){e.remove()}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}}
function showThinkingIndicator(){if(isThinking)return;const e=document.getElementById("emptyState");if(e)e.remove();const t=document.createElement("div");t.id="thinking-indicator";t.className="message-container";t.innerHTML=`<divclass="assistant-message"><divclass="assistant-avatar glass"><imgsrc="https://pragmatismo.com.br/gb-logo.png"alt="GB"/></div><divclass="thinking-indicator"><divclass="typing-dots"><divclass="typing-dot"></div><divclass="typing-dot"></div><divclass="typing-dot"></div></div><span>Pensando...</span></div></div>`;messagesDiv.appendChild(t);gsap.to(t,{opacity:1,y:0,duration:.4,ease:"power2.out"});if(!isUserScrolling){scrollToBottom()}else{showScrollToBottomButton()}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 glass";w.innerHTML=`⚠️ ${m}`;messagesDiv.appendChild(w);gsap.from(w,{opacity:0,y:20,duration:.4,ease:"power2.out"});if(!isUserScrolling){scrollToBottom()}else{showScrollToBottomButton()}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=`<divclass="assistant-message"><divclass="assistant-avatar glass"><imgsrc="https://pragmatismo.com.br/gb-logo.png"alt="GB"/></div><divclass="assistant-message-content glass"><p>A conexão foi interrompida. Clique em "Continuar" para tentar recuperar a resposta.</p><buttonclass="continue-button glass"onclick="continueInterruptedResponse()">Continuar</button></div></div>`;messagesDiv.appendChild(c);gsap.to(c,{opacity:1,y:0,duration:.5,ease:"power2.out"});if(!isUserScrolling){scrollToBottom()}else{showScrollToBottomButton()}}
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 updateStreamingMessage(c){const m=document.getElementById(streamingMessageId);if(m){m.innerHTML=marked.parse(c);if(!isUserScrolling){scrollToBottom()}else{showScrollToBottomButton()}}}
function finalizeStreamingMessage(){const m=document.getElementById(streamingMessageId);if(m){m.innerHTML=marked.parse(currentStreamingContent);m.removeAttribute("id");if(!isUserScrolling){scrollToBottom()}else{showScrollToBottomButton()}}}
function escapeHtml(t){const d=document.createElement("div");d.textContent=t;return d.innerHTML}
function clearSuggestions(){const f=document.querySelector('footer'),c=f.querySelector('.suggestions-container');if(c){c.innerHTML=''}}
function handleSuggestions(s){const f=document.querySelector('footer');let c=f.querySelector('.suggestions-container');if(!c){c=document.createElement('div');c.className='suggestions-container';f.insertBefore(c,f.firstChild)}c.innerHTML='';const u=s.filter((v,i,a)=>i===a.findIndex(t=>t.text===v.text&&t.context===v.context));u.forEach(v=>{const b=document.createElement('button');b.textContent=v.text;b.className='suggestion-button glass';b.onclick=()=>{setContext(v.context);input.value=''};c.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()}
async function toggleVoiceMode(){isVoiceMode=!isVoiceMode;const v=document.getElementById("voiceToggle"),s=document.getElementById("voiceStatus");if(isVoiceMode){v.textContent="🔴 Parar Voz";v.classList.add("recording");s.style.display="block";await startVoiceSession()}else{v.textContent="🎤 Modo Voz";v.classList.remove("recording");s.style.display="none";await stopVoiceSession()}if(window.innerWidth<=768){document.getElementById("sidebar").classList.remove("open")}}
async function startVoiceSession(){if(!currentSessionId)return;try{const r=await fetch("/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("/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=window.location.protocol==="https:"?"wss:":"ws:",u=`${p}//${window.location.host}/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}`)}