function chatApp() { // Core state variables (shared via closure) let ws = null, pendingContextChange = null, o; ((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; // DOM references (cached for performance) let messagesDiv, messageInputEl, sendBtn, voiceBtn, connectionStatus, flashOverlay, suggestionsContainer, floatLogo, sidebar, themeBtn, scrollToBottomBtn, contextIndicator, contextPercentage, contextProgressBar, sidebarTitle; marked.setOptions({ breaks: true, gfm: true }); return { // ---------------------------------------------------------------------- // UI state (mirrors the structure used in driveApp) // ---------------------------------------------------------------------- current: "All Chats", search: "", selectedChat: null, navItems: [ { name: "All Chats", icon: "💬" }, { name: "Direct", icon: "👤" }, { name: "Groups", icon: "👥" }, { name: "Archived", icon: "🗄" }, ], 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", }, ], get filteredChats() { return this.chats.filter((chat) => chat.name.toLowerCase().includes(this.search.toLowerCase()), ); }, // ---------------------------------------------------------------------- // UI helpers (formerly standalone functions) // ---------------------------------------------------------------------- toggleSidebar() { sidebar.classList.toggle("open"); }, 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; this.applyTheme(); this.updateThemeButton(); }, 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}')`, ); } }, // ---------------------------------------------------------------------- // Lifecycle / event handlers // ---------------------------------------------------------------------- init() { window.addEventListener("load", () => { // Assign DOM elements after the document is ready messagesDiv = document.getElementById("messages"); messageInputEl = 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"); // Theme initialization and focus const savedTheme = localStorage.getItem("gb-theme") || "auto"; currentTheme = savedTheme; this.applyTheme(); window .matchMedia("(prefers-color-scheme: dark)") .addEventListener("change", () => { if (currentTheme === "auto") { this.applyTheme(); } }); if (messageInputEl) { messageInputEl.focus(); } // UI event listeners document.addEventListener("click", (e) => {}); messagesDiv.addEventListener("scroll", () => { 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", () => { this.scrollToBottom(); }); sendBtn.onclick = () => this.sendMessage(); messageInputEl.addEventListener("keypress", (e) => { if (e.key === "Enter") this.sendMessage(); }); window.addEventListener("focus", () => { if (!ws || ws.readyState !== WebSocket.OPEN) { this.connectWebSocket(); } }); // Start authentication flow this.initializeAuth(); }); }, updateContextUsage(u) { contextUsage = u; const p = Math.min(100, Math.round(u * 100)); contextPercentage.textContent = `${p}%`; contextProgressBar.style.width = `${p}%`; contextIndicator.classList.remove("visible"); }, flashScreen() { gsap.to(flashOverlay, { opacity: 0.15, duration: 0.1, onComplete: () => { gsap.to(flashOverlay, { opacity: 0, duration: 0.2 }); }, }); }, updateConnectionStatus(s) { connectionStatus.className = `connection-status ${s}`; }, getWebSocketUrl() { const p = "ws:", s = currentSessionId || crypto.randomUUID(), u = currentUserId || crypto.randomUUID(); return `${p}//localhost:8080/ws?session_id=${s}&user_id=${u}`; }, async initializeAuth() { try { this.updateConnectionStatus("connecting"); const p = window.location.pathname.split("/").filter((s) => s); const b = p.length > 0 ? p[0] : "default"; const r = await fetch( `http://localhost:8080/api/auth?bot_name=${encodeURIComponent(b)}`, ); const a = await r.json(); currentUserId = a.user_id; currentSessionId = a.session_id; this.connectWebSocket(); } catch (e) { console.error("Failed to initialize auth:", e); this.updateConnectionStatus("disconnected"); setTimeout(() => this.initializeAuth(), 3000); } }, async loadSessions() { try { const r = await fetch("http://localhost:8080/api/sessions"); const s = await r.json(); const 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 = () => this.switchSession(session.session_id); h.appendChild(item); }); } catch (e) { console.error("Failed to load sessions:", e); } }, async createNewSession() { try { const r = await fetch("http://localhost:8080/api/sessions", { method: "POST", }); const s = await r.json(); currentSessionId = s.session_id; hasReceivedInitialMessage = false; this.connectWebSocket(); this.loadSessions(); messagesDiv.innerHTML = ""; this.clearSuggestions(); this.updateContextUsage(0); if (isVoiceMode) { await this.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); } }, switchSession(s) { currentSessionId = s; hasReceivedInitialMessage = false; this.connectWebSocket(); if (isVoiceMode) { this.startVoiceSession(); } sidebar.classList.remove("open"); }, connectWebSocket() { if (ws) { ws.close(); } clearTimeout(reconnectTimeout); const u = this.getWebSocketUrl(); ws = new WebSocket(u); ws.onmessage = (e) => { const r = JSON.parse(e.data); if (r.bot_id) { currentBotId = r.bot_id; } // Message type 2 is a bot response (not an event) // Message type 5 is context change if (r.message_type === 5) { isContextChange = true; return; } // Check if this is a special event message (has event field) if (r.event) { this.handleEvent(r.event, r.data || {}); return; } this.processMessageContent(r); }; ws.onopen = () => { console.log("Connected to WebSocket"); this.updateConnectionStatus("connected"); reconnectAttempts = 0; hasReceivedInitialMessage = false; }; ws.onclose = (e) => { console.log("WebSocket disconnected:", e.code, e.reason); this.updateConnectionStatus("disconnected"); if (isStreaming) { this.showContinueButton(); } if (reconnectAttempts < maxReconnectAttempts) { reconnectAttempts++; const d = Math.min(1000 * reconnectAttempts, 10000); reconnectTimeout = setTimeout(() => { this.updateConnectionStatus("connecting"); this.connectWebSocket(); }, d); } else { this.updateConnectionStatus("disconnected"); } }; ws.onerror = (e) => { console.error("WebSocket error:", e); this.updateConnectionStatus("disconnected"); }; }, processMessageContent(r) { if (isContextChange) { isContextChange = false; return; } if (r.context_usage !== undefined) { this.updateContextUsage(r.context_usage); } if (r.suggestions && r.suggestions.length > 0) { this.handleSuggestions(r.suggestions); } if (r.is_complete) { if (isStreaming) { this.finalizeStreamingMessage(); isStreaming = false; streamingMessageId = null; currentStreamingContent = ""; } else { this.addMessage("assistant", r.content, false); } } else { if (!isStreaming) { isStreaming = true; streamingMessageId = "streaming-" + Date.now(); currentStreamingContent = r.content || ""; this.addMessage( "assistant", currentStreamingContent, true, streamingMessageId, ); } else { currentStreamingContent += r.content || ""; this.updateStreamingMessage(currentStreamingContent); } } }, handleEvent(t, d) { console.log("Event received:", t, d); switch (t) { case "thinking_start": this.showThinkingIndicator(); break; case "thinking_end": this.hideThinkingIndicator(); break; case "warn": this.showWarning(d.message); break; case "context_usage": this.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; } this.applyTheme(); break; } }, 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: 0.3, ease: "power2.out" }); if (!isUserScrolling) { this.scrollToBottom(); } thinkingTimeout = setTimeout(() => { if (isThinking) { this.hideThinkingIndicator(); this.showWarning( "O servidor pode estar ocupado. A resposta está demorando demais.", ); } }, 60000); isThinking = true; }, hideThinkingIndicator() { if (!isThinking) return; const t = document.getElementById("thinking-indicator"); if (t) { gsap.to(t, { opacity: 0, duration: 0.2, onComplete: () => { if (t.parentNode) { t.remove(); } }, }); } if (thinkingTimeout) { clearTimeout(thinkingTimeout); thinkingTimeout = null; } isThinking = false; }, 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: 0.4, ease: "power2.out" }); if (!isUserScrolling) { this.scrollToBottom(); } setTimeout(() => { if (w.parentNode) { gsap.to(w, { opacity: 0, duration: 0.3, onComplete: () => w.remove(), }); } }, 5000); }, showContinueButton() { const c = document.createElement("div"); c.className = "message-container"; c.innerHTML = ``; messagesDiv.appendChild(c); gsap.to(c, { opacity: 1, y: 0, duration: 0.5, ease: "power2.out" }); if (!isUserScrolling) { this.scrollToBottom(); } }, continueInterruptedResponse() { if (!ws || ws.readyState !== WebSocket.OPEN) { this.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(); }); }, addMessage(role, content, streaming = false, msgId = null) { const m = document.createElement("div"); m.className = "message-container"; if (role === "user") { m.innerHTML = ``; this.updateContextUsage(contextUsage + 0.05); } else if (role === "assistant") { m.innerHTML = ``; this.updateContextUsage(contextUsage + 0.03); } else if (role === "voice") { m.innerHTML = ``; } else { m.innerHTML = ``; } messagesDiv.appendChild(m); gsap.to(m, { opacity: 1, y: 0, duration: 0.5, ease: "power2.out" }); if (!isUserScrolling) { this.scrollToBottom(); } }, updateStreamingMessage(c) { const m = document.getElementById(streamingMessageId); if (m) { m.innerHTML = marked.parse(c); if (!isUserScrolling) { this.scrollToBottom(); } } }, finalizeStreamingMessage() { const m = document.getElementById(streamingMessageId); if (m) { m.innerHTML = marked.parse(currentStreamingContent); m.removeAttribute("id"); if (!isUserScrolling) { this.scrollToBottom(); } } }, escapeHtml(t) { const d = document.createElement("div"); d.textContent = t; return d.innerHTML; }, clearSuggestions() { suggestionsContainer.innerHTML = ""; }, 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 = () => { this.setContext(v.context); messageInputEl.value = ""; }; suggestionsContainer.appendChild(b); }); }, async setContext(c) { try { const t = event?.target?.textContent || c; this.addMessage("user", t); messageInputEl.value = ""; messageInputEl.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..."); this.connectWebSocket(); } } catch (err) { console.error("Failed to set context:", err); } }, async sendMessage() { if (pendingContextChange) { await pendingContextChange; pendingContextChange = null; } const m = messageInputEl.value.trim(); if (!m || !ws || ws.readyState !== WebSocket.OPEN) { if (!ws || ws.readyState !== WebSocket.OPEN) { this.showWarning("Conexão não disponível. Tentando reconectar..."); this.connectWebSocket(); } return; } if (isThinking) { this.hideThinkingIndicator(); } this.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)); messageInputEl.value = ""; messageInputEl.focus(); }, async toggleVoiceMode() { isVoiceMode = !isVoiceMode; const v = document.getElementById("voiceToggle"); if (isVoiceMode) { v.textContent = "🔴 Stop Voice"; v.classList.add("recording"); await this.startVoiceSession(); } else { v.textContent = "🎤 Voice Mode"; v.classList.remove("recording"); await this.stopVoiceSession(); } }, async 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, }), }); const d = await r.json(); if (d.token) { await this.connectToVoiceRoom(d.token); this.startVoiceRecording(); } } catch (e) { console.error("Failed to start voice session:", e); this.showWarning("Falha ao iniciar modo de voz"); } }, async 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 connectToVoiceRoom(t) { try { const r = new LiveKitClient.Room(); const 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") { this.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); this.showWarning("Falha na conexão de voz"); } }, 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" }); this.simulateVoiceTranscription(); }; mediaRecorder.start(); setTimeout(() => { if (mediaRecorder && mediaRecorder.state === "recording") { mediaRecorder.stop(); setTimeout(() => { if (isVoiceMode) { this.startVoiceRecording(); } }, 1000); } }, 5000); }) .catch((e) => { console.error("Error accessing microphone:", e); this.showWarning("Erro ao acessar microfone"); }); }, 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", ]; const 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, ); } this.addMessage("voice", `🎤 ${r}`); }, scrollToBottom() { messagesDiv.scrollTop = messagesDiv.scrollHeight; isUserScrolling = false; scrollToBottomBtn.classList.remove("visible"); }, }; } // Initialize the app chatApp().init();