Added interaction count tracking for sessions with Redis or in-memory fallback. Implemented conversation history replacement functionality to compact and update message history. The changes include: - New AtomicUsize counter in SessionManager for interaction tracking - increment_and_get_interaction_count method with Redis support - replace_conversation_history to update and compact message history - Maintains existing functionality while adding new features
204 lines
No EOL
30 KiB
HTML
204 lines
No EOL
30 KiB
HTML
<!doctype html>
|
|
<html lang="pt-br">
|
|
<head>
|
|
<meta charset="utf-8"/>
|
|
<title>General Bots</title>
|
|
<meta name="viewport" content="width=device-width,initial-scale=1.0"/>
|
|
<script src="https://cdn.tailwindcss.com"></script>
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/gsap.min.js"></script>
|
|
<script src="https://cdn.jsdelivr.net/npm/livekit-client/dist/livekit-client.umd.min.js"></script>
|
|
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
|
<style>
|
|
@import url("https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;600&display=swap");
|
|
:root{--bg:#0a0a0a;--fg:#ffffff;--glass:rgba(255,255,255,0.03);--glass-border:rgba(255,255,255,0.08);--accent:#ffffff;--shadow:rgba(0,0,0,0.4)}
|
|
[data-theme="light"]{--bg:#f5f5f5;--fg:#0a0a0a;--glass:rgba(255,255,255,0.4);--glass-border:rgba(0,0,0,0.06);--accent:#0a0a0a;--shadow:rgba(0,0,0,0.1)}
|
|
*{margin:0;padding:0;box-sizing:border-box}
|
|
body{font-family:"Inter",sans-serif;background:var(--bg);color:var(--fg);overflow:hidden;transition:all .3s}
|
|
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%)}
|
|
.glass{background:linear-gradient(135deg,var(--glass) 0%,rgba(255,255,255,0.01) 100%);backdrop-filter:blur(20px) saturate(180%);border:1px solid var(--glass-border);box-shadow:0 8px 32px var(--shadow),inset 0 1px 0 rgba(255,255,255,0.1)}
|
|
[data-theme="light"] .glass{background:linear-gradient(135deg,var(--glass) 0%,rgba(255,255,255,0.6) 100%);box-shadow:0 8px 32px var(--shadow),inset 0 1px 0 rgba(255,255,255,0.8)}
|
|
.sidebar{position:fixed;left:-320px;top:0;width:320px;height:100vh;transition:left .4s cubic-bezier(.4,0,.2,1);z-index:100;overflow-y:auto;padding:20px}
|
|
.sidebar.open{left:0}
|
|
.sidebar-toggle,.theme-toggle{position:fixed;top:20px;z-index:101;width:44px;height:44px;display:flex;align-items:center;justify-content:center;border-radius:12px;cursor:pointer;transition:all .3s;font-size:18px}
|
|
.sidebar-toggle{left:20px}
|
|
.sidebar-toggle:hover,.theme-toggle:hover{transform:scale(1.08);background:var(--accent);color:var(--bg)}
|
|
.theme-toggle{right:80px}
|
|
.new-chat,.voice-toggle{width:100%;padding:14px;border-radius:12px;cursor:pointer;transition:all .3s;font-weight:600;font-size:14px;margin-bottom:12px}
|
|
.new-chat{margin-top:70px}
|
|
.voice-toggle.recording{animation:pulse 2s infinite}
|
|
@keyframes pulse{0%,100%{box-shadow:0 0 0 0 rgba(255,255,255,0.4)}50%{box-shadow:0 0 20px 8px rgba(255,255,255,0)}}
|
|
.history-item{padding:12px;margin-bottom:8px;border-radius:10px;cursor:pointer;transition:all .3s;font-size:13px}
|
|
.history-item:hover,.new-chat:hover,.voice-toggle:hover{background:var(--accent);color:var(--bg);transform:translateX(4px)}
|
|
.main{margin-left:0;width:100%;height:100vh;display:flex;flex-direction:column;transition:margin-left .4s;position:relative;z-index:2}
|
|
.sidebar.open~.main{margin-left:320px;width:calc(100% - 320px)}
|
|
header{padding:18px 40px 18px 80px;display:flex;align-items:center;justify-content:space-between}
|
|
.logo{display:flex;align-items:center;gap:14px}
|
|
.logo-icon{width:44px;height:44px;border-radius:12px;display:flex;align-items:center;justify-content:center;font-size:24px;font-weight:700;box-shadow:0 4px 16px var(--shadow);overflow:hidden}
|
|
.logo-icon img{width:100%;height:100%;object-fit:contain;padding:6px;filter:invert(1)}
|
|
[data-theme="light"] .logo-icon img{filter:invert(0)}
|
|
.logo-text{font-size:24px;font-weight:700}
|
|
#messages{flex:1;overflow-y:auto;padding:30px 20px;max-width:1000px;margin:0 auto;width:100%}
|
|
#emptyState{text-align:center;padding-top:100px;animation:fadeIn .8s}
|
|
@keyframes fadeIn{from{opacity:0;transform:translateY(20px)}to{opacity:1;transform:translateY(0)}}
|
|
.empty-icon{width:90px;height:90px;background:var(--accent);border-radius:24px;display:inline-flex;align-items:center;justify-content:center;font-size:48px;margin-bottom:20px;animation:float 3s ease-in-out infinite;overflow:hidden}
|
|
.empty-icon img{width:100%;height:100%;object-fit:contain;padding:12px;filter:invert(1)}
|
|
[data-theme="light"] .empty-icon img{filter:invert(0)}
|
|
@keyframes float{0%,100%{transform:translateY(0)}50%{transform:translateY(-8px)}}
|
|
.empty-title{font-size:36px;font-weight:700;margin-bottom:10px}
|
|
.empty-subtitle{color:var(--fg);opacity:.5;font-size:16px;font-weight:300}
|
|
.message-container{margin-bottom:20px;opacity:0;transform:translateY(15px)}
|
|
.user-message{display:flex;justify-content:flex-end}
|
|
.user-message-content{border-radius:18px 18px 4px 18px;padding:14px 18px;max-width:70%}
|
|
.assistant-message{display:flex;gap:14px;align-items:flex-start}
|
|
.assistant-avatar{width:40px;height:40px;border-radius:10px;display:flex;align-items:center;justify-content:center;font-size:20px;flex-shrink:0;box-shadow:0 4px 16px var(--shadow);overflow:hidden}
|
|
.assistant-avatar img{width:100%;height:100%;object-fit:contain;padding:6px;filter:invert(1)}
|
|
[data-theme="light"] .assistant-avatar img{filter:invert(0)}
|
|
.assistant-message-content{border-radius:18px 18px 18px 4px;padding:14px 18px;flex:1;line-height:1.7}
|
|
.thinking-indicator{display:flex;gap:14px;align-items:center;padding:14px 18px;opacity:.6}
|
|
.typing-dots{display:flex;gap:6px}
|
|
.typing-dot{width:8px;height:8px;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:.5}40%{transform:scale(1);opacity:1}}
|
|
footer{padding:20px 40px}
|
|
.suggestions-container{display:flex;flex-wrap:wrap;gap:6px;margin-bottom:14px;justify-content:center}
|
|
.suggestion-button{padding:8px 16px;border-radius:20px;cursor:pointer;font-size:12px;font-weight:500;transition:all .3s}
|
|
.suggestion-button:hover{background:var(--accent);color:var(--bg);transform:translateY(-2px)}
|
|
.input-container{display:flex;gap:10px;max-width:1000px;margin:0 auto;align-items:center}
|
|
#messageInput{flex:1;border-radius:24px;padding:14px 22px;font-size:14px;font-family:"Inter",sans-serif;outline:none;transition:all .3s}
|
|
#messageInput:focus{box-shadow:0 0 0 2px var(--accent)}
|
|
#messageInput::placeholder{opacity:.4}
|
|
#sendBtn,#newChatBtn{border-radius:24px;padding:14px 28px;font-weight:600;font-size:14px;cursor:pointer;transition:all .3s;border:none}
|
|
#sendBtn{background:var(--accent);color:var(--bg);box-shadow:0 4px 16px var(--shadow)}
|
|
#sendBtn:hover{transform:translateY(-2px);box-shadow:0 6px 24px var(--shadow)}
|
|
#newChatBtn{background:transparent}
|
|
#newChatBtn:hover{background:var(--accent);color:var(--bg)}
|
|
.voice-status{padding:14px 20px;text-align:center;opacity:.6;font-weight:600}
|
|
.warning-message{border-radius:12px;padding:14px 18px;margin-bottom:18px;opacity:.6}
|
|
.connection-status{position:fixed;top:20px;right:20px;width:10px;height:10px;border-radius:50%;z-index:1000;transition:all .3s}
|
|
.connection-status.connecting{background:var(--fg);opacity:.5;animation:ping 1.5s infinite}
|
|
.connection-status.connected{background:var(--accent);box-shadow:0 0 16px var(--accent);animation:ping2 2s infinite}
|
|
.connection-status.disconnected{background:var(--fg);opacity:.3}
|
|
@keyframes ping{0%,100%{opacity:.6;transform:scale(.8)}50%{opacity:1;transform:scale(1.2)}}
|
|
@keyframes ping2{0%,100%{opacity:.8}50%{opacity:1;transform:scale(1.2)}}
|
|
.markdown-content h1,.markdown-content h2,.markdown-content h3{margin-top:20px;margin-bottom:10px;font-weight:600}
|
|
.markdown-content h1{font-size:24px}
|
|
.markdown-content h2{font-size:20px}
|
|
.markdown-content h3{font-size:18px}
|
|
.markdown-content p{margin-bottom:14px;line-height:1.8}
|
|
.markdown-content ul,.markdown-content ol{margin-bottom:14px;padding-left:24px}
|
|
.markdown-content li{margin-bottom:6px}
|
|
.markdown-content code{background:var(--glass);padding:2px 8px;border-radius:6px;font-family:"JetBrains Mono",monospace;font-size:13px}
|
|
.markdown-content pre{border-radius:12px;padding:18px;overflow-x:auto;margin-bottom:14px}
|
|
.markdown-content pre code{background:none;padding:0}
|
|
.markdown-content table{width:100%;border-collapse:collapse;margin-bottom:14px}
|
|
.markdown-content table th,.markdown-content table td{padding:10px;text-align:left}
|
|
.markdown-content table th{font-weight:600}
|
|
.markdown-content blockquote{border-left:3px solid var(--accent);padding-left:18px;margin:14px 0;opacity:.6;font-style:italic}
|
|
.markdown-content a{color:var(--fg);text-decoration:underline;transition:all .3s}
|
|
.markdown-content a:hover{opacity:.7}
|
|
::-webkit-scrollbar{width:8px}
|
|
::-webkit-scrollbar-track{background:transparent}
|
|
::-webkit-scrollbar-thumb{background:var(--glass-border);border-radius:4px}
|
|
::-webkit-scrollbar-thumb:hover{background:var(--fg);opacity:.3}
|
|
.scroll-to-bottom{position:absolute;bottom:20px;right:20px;width:44px;height:44px;background:var(--accent);border:none;border-radius:50%;color:var(--bg);font-size:18px;cursor:pointer;display:flex;align-items:center;justify-content:center;box-shadow:0 4px 16px var(--shadow);transition:all .3s;z-index:10}
|
|
.scroll-to-bottom:hover{transform:scale(1.1)}
|
|
.continue-button{display:inline-block;border-radius:10px;padding:10px 20px;font-weight:600;cursor:pointer;margin-top:10px;transition:all .3s;font-size:13px}
|
|
.continue-button:hover{background:var(--accent);color:var(--bg);transform:translateY(-2px)}
|
|
.context-indicator{position:fixed;bottom:100px;right:20px;width:130px;border-radius:12px;padding:12px;font-size:11px;text-align:center;z-index:100;box-shadow:0 4px 16px var(--shadow)}
|
|
.context-progress{height:4px;background:var(--glass);border-radius:2px;margin-top:8px;overflow:hidden}
|
|
.context-progress-bar{height:100%;background:var(--accent);border-radius:2px;transition:width .3s}
|
|
@media(max-width:768px){.sidebar{width:100%;left:-100%}.sidebar.open~.main{margin-left:0;width:100%}header{padding:14px 20px 14px 70px}.logo-text{font-size:20px}.logo-icon{width:38px;height:38px;font-size:20px}#newChatBtn{padding:10px 18px;font-size:13px}.input-container{padding:0 10px}#messageInput{padding:12px 18px}#sendBtn{padding:12px 24px}.user-message-content,.assistant-message-content{max-width:85%}.empty-title{font-size:28px}.empty-icon{width:70px;height:70px;font-size:36px}.scroll-to-bottom,.sidebar-toggle,.theme-toggle{width:40px;height:40px;font-size:16px}.context-indicator{bottom:80px;right:18px;width:110px}.theme-toggle{right:70px}}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="connection-status connecting" id="connectionStatus"></div>
|
|
<button class="sidebar-toggle glass" onclick="toggleSidebar()">☰</button>
|
|
<button class="theme-toggle glass" id="themeToggle" onclick="toggleTheme()">⚙️</button>
|
|
<div class="sidebar glass" id="sidebar">
|
|
<button class="new-chat glass" onclick="createNewSession()">+ Novo Chat</button>
|
|
<button class="voice-toggle glass" id="voiceToggle" onclick="toggleVoiceMode()">🎤 Modo Voz</button>
|
|
<div class="history" id="history"></div>
|
|
</div>
|
|
<div class="main">
|
|
<header class="glass">
|
|
<div class="logo">
|
|
<div class="logo-icon glass"><img src="https://pragmatismo.com.br/gb-logo.png" alt="GB"/></div>
|
|
<h1 class="logo-text">General Bots</h1>
|
|
</div>
|
|
<button class="glass" id="newChatBtn">Novo Chat</button>
|
|
</header>
|
|
<div class="voice-status glass" id="voiceStatus" style="display:none">🎤 Ouvindo... Fale agora</div>
|
|
<main id="messages">
|
|
<div id="emptyState">
|
|
<div class="empty-icon glass"><img src="https://pragmatismo.com.br/gb-logo.png" alt="GB"/></div>
|
|
<h2 class="empty-title">Bem-vindo ao General Bots</h2>
|
|
<p class="empty-subtitle">Seu assistente de IA avançado</p>
|
|
</div>
|
|
</main>
|
|
<footer class="glass">
|
|
<div class="input-container">
|
|
<input class="glass" id="messageInput" type="text" placeholder="Fale com General Bots..." autofocus/>
|
|
<button id="sendBtn">Enviar</button>
|
|
</div>
|
|
</footer>
|
|
</div>
|
|
<button class="scroll-to-bottom" id="scrollToBottom" style="display:none">↓</button>
|
|
<div class="context-indicator glass" id="contextIndicator" style="display:none">
|
|
<div>Contexto</div>
|
|
<div id="contextPercentage">0%</div>
|
|
<div class="context-progress"><div class="context-progress-bar" id="contextProgressBar" style="width:0%"></div></div>
|
|
</div>
|
|
<script>
|
|
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;
|
|
const maxReconnectAttempts=5,messagesDiv=document.getElementById("messages"),input=document.getElementById("messageInput"),sendBtn=document.getElementById("sendBtn"),newChatBtn=document.getElementById("newChatBtn"),connectionStatus=document.getElementById("connectionStatus"),scrollToBottomBtn=document.getElementById("scrollToBottom"),contextIndicator=document.getElementById("contextIndicator"),contextPercentage=document.getElementById("contextPercentage"),contextProgressBar=document.getElementById("contextProgressBar"),themeToggle=document.getElementById("themeToggle");
|
|
marked.setOptions({breaks:true,gfm:true});
|
|
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}`}
|
|
window.addEventListener("load",function(){const s=localStorage.getItem('gb-theme'),p=window.matchMedia('(prefers-color-scheme: dark)').matches;if(s==='auto'||!s){currentTheme=p?'dark':'light';localStorage.setItem('gb-theme','auto')}else{currentTheme=s}document.documentElement.setAttribute('data-theme',currentTheme);themeToggle.textContent=s==='auto'||!s?'⚙️':(currentTheme==='dark'?'🌙':'☀️');window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change',e=>{if(localStorage.getItem('gb-theme')==='auto'){currentTheme=e.matches?'dark':'light';document.documentElement.setAttribute('data-theme',currentTheme)}});input.focus()});
|
|
document.addEventListener("click",function(e){const s=document.getElementById("sidebar"),t=document.querySelector(".sidebar-toggle");if(window.innerWidth<=768&&s.classList.contains("open")&&!s.contains(e.target)&&!t.contains(e.target)){s.classList.remove("open")}});
|
|
messagesDiv.addEventListener("scroll",function(){const a=messagesDiv.scrollHeight-messagesDiv.scrollTop<=messagesDiv.clientHeight+100;if(!a){isUserScrolling=true;showScrollToBottomButton()}else{isUserScrolling=false;hideScrollToBottomButton()}});
|
|
function scrollToBottom(){messagesDiv.scrollTop=messagesDiv.scrollHeight;isUserScrolling=false;hideScrollToBottomButton()}
|
|
function showScrollToBottomButton(){scrollToBottomBtn.style.display="flex"}
|
|
function hideScrollToBottomButton(){scrollToBottomBtn.style.display="none"}
|
|
scrollToBottomBtn.addEventListener("click",scrollToBottom);
|
|
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=`<div id="emptyState"><div class="empty-icon glass"><img src="https://pragmatismo.com.br/gb-logo.png" alt="GB"/></div><h2 class="empty-title">Bem-vindo ao General Bots</h2><p class="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=`<div id="emptyState"><div class="empty-icon glass"><img src="https://pragmatismo.com.br/gb-logo.png" alt="GB"/></div><h2 class="empty-title">Bem-vindo ao General Bots</h2><p class="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++;const d=Math.min(1000*reconnectAttempts,10000);console.log(`Reconnecting in ${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=`<div class="assistant-message"><div class="assistant-avatar glass"><img src="https://pragmatismo.com.br/gb-logo.png" alt="GB"/></div><div class="thinking-indicator"><div class="typing-dots"><div class="typing-dot"></div><div class="typing-dot"></div><div class="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=`<div class="assistant-message"><div class="assistant-avatar glass"><img src="https://pragmatismo.com.br/gb-logo.png" alt="GB"/></div><div class="assistant-message-content glass"><p>A conexão foi interrompida. Clique em "Continuar" para tentar recuperar a resposta.</p><button class="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 addMessage(role,content,streaming=false,msgId=null){const e=document.getElementById("emptyState");if(e){gsap.to(e,{opacity:0,y:-20,duration:.3,onComplete:()=>e.remove()})}const m=document.createElement("div");m.className="message-container";if(role==="user"){m.innerHTML=`<div class="user-message"><div class="user-message-content glass">${escapeHtml(content)}</div></div>`;updateContextUsage(contextUsage+.05)}else if(role==="assistant"){m.innerHTML=`<div class="assistant-message"><div class="assistant-avatar glass"><img src="https://pragmatismo.com.br/gb-logo.png" alt="GB"/></div><div class="assistant-message-content glass markdown-content" id="${msgId||""}">${streaming?"":marked.parse(content)}</div></div>`;updateContextUsage(contextUsage+.03)}else if(role==="voice"){m.innerHTML=`<div class="assistant-message"><div class="assistant-avatar glass">🎤</div><div class="assistant-message-content glass">${content}</div></div>`}else{m.innerHTML=`<div class="assistant-message"><div class="assistant-avatar glass"><img src="https://pragmatismo.com.br/gb-logo.png" alt="GB"/></div><div class="assistant-message-content glass">${content}</div></div>`}messagesDiv.appendChild(m);gsap.to(m,{opacity:1,y:0,duration:.5,ease:"power2.out"});if(!isUserScrolling){scrollToBottom()}else{showScrollToBottomButton()}}
|
|
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()}
|
|
sendBtn.onclick=sendMessage;
|
|
input.addEventListener("keypress",e=>{if(e.key==="Enter")sendMessage()});
|
|
newChatBtn.onclick=()=>createNewSession();
|
|
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}`)}
|
|
window.addEventListener("load",initializeAuth);
|
|
window.addEventListener("focus",function(){if(!ws||ws.readyState!==WebSocket.OPEN){connectWebSocket()}});
|
|
</script>
|
|
</body>
|
|
</html> |