2025-11-16 22:53:51 -03:00
function chatApp ( ) {
2025-11-17 10:00:12 -03:00
// Core state variables (shared via closure)
let ws = null ,
2025-11-20 13:40:40 -03:00
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 ) ) ;
2025-11-17 10:00:12 -03:00
const maxReconnectAttempts = 5 ;
// DOM references (cached for performance)
2025-11-20 13:40:40 -03:00
let messagesDiv ,
messageInputEl ,
sendBtn ,
voiceBtn ,
connectionStatus ,
flashOverlay ,
suggestionsContainer ,
floatLogo ,
sidebar ,
themeBtn ,
scrollToBottomBtn ,
contextIndicator ,
contextPercentage ,
contextProgressBar ,
sidebarTitle ;
2025-11-17 10:00:12 -03:00
marked . setOptions ( { breaks : true , gfm : true } ) ;
2025-11-16 22:53:51 -03:00
return {
2025-11-17 10:00:12 -03:00
// ----------------------------------------------------------------------
// UI state (mirrors the structure used in driveApp)
// ----------------------------------------------------------------------
2025-11-20 13:40:40 -03:00
current : "All Chats" ,
search : "" ,
2025-11-16 22:53:51 -03:00
selectedChat : null ,
navItems : [
2025-11-20 13:40:40 -03:00
{ name : "All Chats" , icon : "💬" } ,
{ name : "Direct" , icon : "👤" } ,
{ name : "Groups" , icon : "👥" } ,
{ name : "Archived" , icon : "🗄" } ,
2025-11-16 22:53:51 -03:00
] ,
chats : [
2025-11-20 13:40:40 -03:00
{
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" ,
} ,
2025-11-16 22:53:51 -03:00
] ,
get filteredChats ( ) {
2025-11-20 13:40:40 -03:00
return this . chats . filter ( ( chat ) =>
chat . name . toLowerCase ( ) . includes ( this . search . toLowerCase ( ) ) ,
2025-11-16 22:53:51 -03:00
) ;
2025-11-17 10:00:12 -03:00
} ,
// ----------------------------------------------------------------------
// UI helpers (formerly standalone functions)
// ----------------------------------------------------------------------
toggleSidebar ( ) {
2025-11-20 13:40:40 -03:00
sidebar . classList . toggle ( "open" ) ;
2025-11-17 10:00:12 -03:00
} ,
toggleTheme ( ) {
2025-11-20 13:40:40 -03:00
const themes = [ "auto" , "dark" , "light" ] ;
const savedTheme = localStorage . getItem ( "gb-theme" ) || "auto" ;
2025-11-17 10:00:12 -03:00
const idx = themes . indexOf ( savedTheme ) ;
const newTheme = themes [ ( idx + 1 ) % themes . length ] ;
2025-11-20 13:40:40 -03:00
localStorage . setItem ( "gb-theme" , newTheme ) ;
2025-11-17 10:00:12 -03:00
currentTheme = newTheme ;
this . applyTheme ( ) ;
this . updateThemeButton ( ) ;
} ,
applyTheme ( ) {
2025-11-20 13:40:40 -03:00
const prefersDark = window . matchMedia (
"(prefers-color-scheme: dark)" ,
) . matches ;
2025-11-17 10:00:12 -03:00
let theme = currentTheme ;
2025-11-20 13:40:40 -03:00
if ( theme === "auto" ) {
theme = prefersDark ? "dark" : "light" ;
2025-11-17 10:00:12 -03:00
}
2025-11-20 13:40:40 -03:00
document . documentElement . setAttribute ( "data-theme" , theme ) ;
2025-11-17 10:00:12 -03:00
if ( themeColor1 && themeColor2 ) {
const root = document . documentElement ;
2025-11-20 13:40:40 -03:00
root . style . setProperty (
"--bg" ,
theme === "dark" ? themeColor2 : themeColor1 ,
) ;
root . style . setProperty (
"--fg" ,
theme === "dark" ? themeColor1 : themeColor2 ,
) ;
2025-11-17 10:00:12 -03:00
}
if ( customLogoUrl ) {
2025-11-20 13:40:40 -03:00
document . documentElement . style . setProperty (
"--logo-url" ,
` url(' ${ customLogoUrl } ') ` ,
) ;
2025-11-17 10:00:12 -03:00
}
} ,
// ----------------------------------------------------------------------
// Lifecycle / event handlers
// ----------------------------------------------------------------------
init ( ) {
2025-11-20 13:40:40 -03:00
window . addEventListener ( "load" , ( ) => {
2025-11-17 10:00:12 -03:00
// Assign DOM elements after the document is ready
messagesDiv = document . getElementById ( "messages" ) ;
2025-11-20 13:40:40 -03:00
2025-11-17 12:11:13 -03:00
messageInputEl = document . getElementById ( "messageInput" ) ;
2025-11-17 10:00:12 -03:00
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
2025-11-20 13:40:40 -03:00
const savedTheme = localStorage . getItem ( "gb-theme" ) || "auto" ;
2025-11-17 10:00:12 -03:00
currentTheme = savedTheme ;
this . applyTheme ( ) ;
2025-11-20 13:40:40 -03:00
window
. matchMedia ( "(prefers-color-scheme: dark)" )
. addEventListener ( "change" , ( ) => {
if ( currentTheme === "auto" ) {
this . applyTheme ( ) ;
}
} ) ;
2025-11-17 12:16:53 -03:00
if ( messageInputEl ) {
messageInputEl . focus ( ) ;
}
2025-11-16 22:53:51 -03:00
2025-11-17 10:00:12 -03:00
// UI event listeners
2025-11-20 13:40:40 -03:00
document . addEventListener ( "click" , ( e ) => { } ) ;
2025-11-17 10:00:12 -03:00
2025-11-20 13:40:40 -03:00
messagesDiv . addEventListener ( "scroll" , ( ) => {
const isAtBottom =
messagesDiv . scrollHeight - messagesDiv . scrollTop <=
messagesDiv . clientHeight + 100 ;
2025-11-17 10:00:12 -03:00
if ( ! isAtBottom ) {
isUserScrolling = true ;
2025-11-20 13:40:40 -03:00
scrollToBottomBtn . classList . add ( "visible" ) ;
2025-11-17 10:00:12 -03:00
} else {
isUserScrolling = false ;
2025-11-20 13:40:40 -03:00
scrollToBottomBtn . classList . remove ( "visible" ) ;
2025-11-17 10:00:12 -03:00
}
} ) ;
2025-11-16 22:53:51 -03:00
2025-11-20 13:40:40 -03:00
scrollToBottomBtn . addEventListener ( "click" , ( ) => {
2025-11-17 10:00:12 -03:00
this . scrollToBottom ( ) ;
} ) ;
2025-11-16 22:53:51 -03:00
2025-11-17 10:00:12 -03:00
sendBtn . onclick = ( ) => this . sendMessage ( ) ;
2025-11-20 13:40:40 -03:00
messageInputEl . addEventListener ( "keypress" , ( e ) => {
if ( e . key === "Enter" ) this . sendMessage ( ) ;
} ) ;
2025-11-17 10:00:12 -03:00
window . addEventListener ( "focus" , ( ) => {
if ( ! ws || ws . readyState !== WebSocket . OPEN ) {
this . connectWebSocket ( ) ;
}
} ) ;
2025-11-16 22:53:51 -03:00
2025-11-17 10:00:12 -03:00
// Start authentication flow
this . initializeAuth ( ) ;
2025-11-16 22:53:51 -03:00
} ) ;
2025-11-17 10:00:12 -03:00
} ,
updateContextUsage ( u ) {
contextUsage = u ;
const p = Math . min ( 100 , Math . round ( u * 100 ) ) ;
contextPercentage . textContent = ` ${ p } % ` ;
contextProgressBar . style . width = ` ${ p } % ` ;
2025-11-20 13:40:40 -03:00
contextIndicator . classList . remove ( "visible" ) ;
2025-11-17 10:00:12 -03:00
} ,
flashScreen ( ) {
2025-11-20 13:40:40 -03:00
gsap . to ( flashOverlay , {
opacity : 0.15 ,
duration : 0.1 ,
onComplete : ( ) => {
gsap . to ( flashOverlay , { opacity : 0 , duration : 0.2 } ) ;
} ,
} ) ;
2025-11-17 10:00:12 -03:00
} ,
updateConnectionStatus ( s ) {
connectionStatus . className = ` connection-status ${ s } ` ;
} ,
getWebSocketUrl ( ) {
2025-11-20 13:40:40 -03:00
const p = "ws:" ,
s = currentSessionId || crypto . randomUUID ( ) ,
u = currentUserId || crypto . randomUUID ( ) ;
2025-11-17 10:00:12 -03:00
return ` ${ p } //localhost:8080/ws?session_id= ${ s } &user_id= ${ u } ` ;
} ,
async initializeAuth ( ) {
try {
this . updateConnectionStatus ( "connecting" ) ;
2025-11-20 13:40:40 -03:00
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 ) } ` ,
) ;
2025-11-17 10:00:12 -03:00
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 = "" ;
2025-11-20 13:40:40 -03:00
s . forEach ( ( session ) => {
const item = document . createElement ( "div" ) ;
item . className = "history-item" ;
item . textContent =
session . title || ` Session ${ session . session _id . substring ( 0 , 8 ) } ` ;
2025-11-17 10:00:12 -03:00
item . onclick = ( ) => this . switchSession ( session . session _id ) ;
h . appendChild ( item ) ;
} ) ;
} catch ( e ) {
console . error ( "Failed to load sessions:" , e ) ;
}
} ,
async createNewSession ( ) {
try {
2025-11-20 13:40:40 -03:00
const r = await fetch ( "http://localhost:8080/api/sessions" , {
method : "POST" ,
} ) ;
2025-11-17 10:00:12 -03:00
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 ( ) ;
}
2025-11-20 13:40:40 -03:00
sidebar . classList . remove ( "open" ) ;
2025-11-17 10:00:12 -03:00
} ,
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 ;
}
2025-11-20 13:40:40 -03:00
// Message type 2 is a bot response (not an event)
// Message type 5 is context change
2025-11-17 10:00:12 -03:00
if ( r . message _type === 5 ) {
isContextChange = true ;
return ;
}
2025-11-20 13:40:40 -03:00
// Check if this is a special event message (has event field)
if ( r . event ) {
this . handleEvent ( r . event , r . data || { } ) ;
return ;
}
2025-11-17 10:00:12 -03:00
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 || "" ;
2025-11-20 13:40:40 -03:00
this . addMessage (
"assistant" ,
currentStreamingContent ,
true ,
streamingMessageId ,
) ;
2025-11-17 10:00:12 -03:00
} 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 ;
2025-11-16 22:53:51 -03:00
}
2025-11-17 10:00:12 -03:00
this . applyTheme ( ) ;
break ;
}
} ,
showThinkingIndicator ( ) {
if ( isThinking ) return ;
const t = document . createElement ( "div" ) ;
t . id = "thinking-indicator" ;
t . className = "message-container" ;
t . innerHTML = ` <div class="assistant-message"><div class="assistant-avatar"></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></div></div> ` ;
messagesDiv . appendChild ( t ) ;
2025-11-20 13:40:40 -03:00
gsap . to ( t , { opacity : 1 , y : 0 , duration : 0.3 , ease : "power2.out" } ) ;
2025-11-17 10:00:12 -03:00
if ( ! isUserScrolling ) {
this . scrollToBottom ( ) ;
}
thinkingTimeout = setTimeout ( ( ) => {
if ( isThinking ) {
this . hideThinkingIndicator ( ) ;
2025-11-20 13:40:40 -03:00
this . showWarning (
"O servidor pode estar ocupado. A resposta está demorando demais." ,
) ;
2025-11-17 10:00:12 -03:00
}
} , 60000 ) ;
isThinking = true ;
} ,
hideThinkingIndicator ( ) {
if ( ! isThinking ) return ;
const t = document . getElementById ( "thinking-indicator" ) ;
if ( t ) {
2025-11-20 13:40:40 -03:00
gsap . to ( t , {
opacity : 0 ,
duration : 0.2 ,
onComplete : ( ) => {
if ( t . parentNode ) {
t . remove ( ) ;
}
} ,
} ) ;
2025-11-17 10:00:12 -03:00
}
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 ) ;
2025-11-20 13:40:40 -03:00
gsap . from ( w , { opacity : 0 , y : 20 , duration : 0.4 , ease : "power2.out" } ) ;
2025-11-17 10:00:12 -03:00
if ( ! isUserScrolling ) {
this . scrollToBottom ( ) ;
}
setTimeout ( ( ) => {
if ( w . parentNode ) {
2025-11-20 13:40:40 -03:00
gsap . to ( w , {
opacity : 0 ,
duration : 0.3 ,
onComplete : ( ) => w . remove ( ) ,
} ) ;
2025-11-17 10:00:12 -03:00
}
} , 5000 ) ;
} ,
showContinueButton ( ) {
const c = document . createElement ( "div" ) ;
c . className = "message-container" ;
c . innerHTML = ` <div class="assistant-message"><div class="assistant-avatar"></div><div class="assistant-message-content"><p>A conexão foi interrompida. Clique em "Continuar" para tentar recuperar a resposta.</p><button class="continue-button" onclick="this.parentElement.parentElement.parentElement.remove();">Continuar</button></div></div> ` ;
messagesDiv . appendChild ( c ) ;
2025-11-20 13:40:40 -03:00
gsap . to ( c , { opacity : 1 , y : 0 , duration : 0.5 , ease : "power2.out" } ) ;
2025-11-17 10:00:12 -03:00
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 ,
2025-11-20 13:40:40 -03:00
timestamp : new Date ( ) . toISOString ( ) ,
2025-11-16 22:53:51 -03:00
} ;
2025-11-17 10:00:12 -03:00
ws . send ( JSON . stringify ( d ) ) ;
}
2025-11-20 13:40:40 -03:00
document . querySelectorAll ( ".continue-button" ) . forEach ( ( b ) => {
b . parentElement . parentElement . parentElement . remove ( ) ;
} ) ;
2025-11-17 10:00:12 -03:00
} ,
addMessage ( role , content , streaming = false , msgId = null ) {
const m = document . createElement ( "div" ) ;
m . className = "message-container" ;
if ( role === "user" ) {
m . innerHTML = ` <div class="user-message"><div class="user-message-content"> ${ this . escapeHtml ( content ) } </div></div> ` ;
2025-11-20 13:40:40 -03:00
this . updateContextUsage ( contextUsage + 0.05 ) ;
2025-11-17 10:00:12 -03:00
} else if ( role === "assistant" ) {
m . innerHTML = ` <div class="assistant-message"><div class="assistant-avatar"></div><div class="assistant-message-content markdown-content" id=" ${ msgId || "" } "> ${ streaming ? "" : marked . parse ( content ) } </div></div> ` ;
2025-11-20 13:40:40 -03:00
this . updateContextUsage ( contextUsage + 0.03 ) ;
2025-11-17 10:00:12 -03:00
} else if ( role === "voice" ) {
m . innerHTML = ` <div class="assistant-message"><div class="assistant-avatar">🎤</div><div class="assistant-message-content"> ${ content } </div></div> ` ;
} else {
m . innerHTML = ` <div class="assistant-message"><div class="assistant-avatar"></div><div class="assistant-message-content"> ${ content } </div></div> ` ;
}
messagesDiv . appendChild ( m ) ;
2025-11-20 13:40:40 -03:00
gsap . to ( m , { opacity : 1 , y : 0 , duration : 0.5 , ease : "power2.out" } ) ;
2025-11-17 10:00:12 -03:00
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 ( ) {
2025-11-20 13:40:40 -03:00
suggestionsContainer . innerHTML = "" ;
2025-11-17 10:00:12 -03:00
} ,
handleSuggestions ( s ) {
2025-11-20 13:40:40 -03:00
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" ) ;
2025-11-17 10:00:12 -03:00
b . textContent = v . text ;
2025-11-20 13:40:40 -03:00
b . className = "suggestion-button" ;
b . onclick = ( ) => {
this . setContext ( v . context ) ;
messageInputEl . value = "" ;
} ;
2025-11-17 10:00:12 -03:00
suggestionsContainer . appendChild ( b ) ;
2025-11-16 22:53:51 -03:00
} ) ;
2025-11-17 10:00:12 -03:00
} ,
async setContext ( c ) {
try {
const t = event ? . target ? . textContent || c ;
this . addMessage ( "user" , t ) ;
2025-11-20 13:40:40 -03:00
messageInputEl . value = "" ;
messageInputEl . value = "" ;
2025-11-17 10:00:12 -03:00
if ( ws && ws . readyState === WebSocket . OPEN ) {
2025-11-20 13:40:40 -03:00
pendingContextChange = new Promise ( ( r ) => {
const h = ( e ) => {
2025-11-17 10:00:12 -03:00
const d = JSON . parse ( e . data ) ;
if ( d . message _type === 5 && d . context _name === c ) {
2025-11-20 13:40:40 -03:00
ws . removeEventListener ( "message" , h ) ;
2025-11-17 10:00:12 -03:00
r ( ) ;
}
} ;
2025-11-20 13:40:40 -03:00
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 ( ) ,
} ;
2025-11-17 10:00:12 -03:00
ws . send ( JSON . stringify ( s ) ) ;
} ) ;
await pendingContextChange ;
2025-11-20 13:40:40 -03:00
const x = document . getElementById ( "contextIndicator" ) ;
if ( x ) {
document . getElementById ( "contextPercentage" ) . textContent = c ;
}
2025-11-17 10:00:12 -03:00
} else {
console . warn ( "WebSocket não está conectado. Tentando reconectar..." ) ;
this . connectWebSocket ( ) ;
2025-11-16 22:53:51 -03:00
}
2025-11-17 10:00:12 -03:00
} catch ( err ) {
2025-11-20 13:40:40 -03:00
console . error ( "Failed to set context:" , err ) ;
2025-11-16 22:53:51 -03:00
}
2025-11-17 10:00:12 -03:00
} ,
2025-11-16 22:53:51 -03:00
2025-11-17 10:00:12 -03:00
async sendMessage ( ) {
if ( pendingContextChange ) {
await pendingContextChange ;
pendingContextChange = null ;
}
2025-11-17 12:11:13 -03:00
const m = messageInputEl . value . trim ( ) ;
2025-11-17 10:00:12 -03:00
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 ) ;
2025-11-20 13:40:40 -03:00
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 ( ) ,
} ;
2025-11-17 10:00:12 -03:00
ws . send ( JSON . stringify ( d ) ) ;
2025-11-17 12:11:13 -03:00
messageInputEl . value = "" ;
messageInputEl . focus ( ) ;
2025-11-17 10:00:12 -03:00
} ,
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" } ,
2025-11-20 13:40:40 -03:00
body : JSON . stringify ( {
session _id : currentSessionId ,
user _id : currentUserId ,
} ) ,
2025-11-17 10:00:12 -03:00
} ) ;
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" } ,
2025-11-20 13:40:40 -03:00
body : JSON . stringify ( { session _id : currentSessionId } ) ,
2025-11-17 10:00:12 -03:00
} ) ;
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 ( ) ;
2025-11-20 13:40:40 -03:00
const p = "ws:" ,
u = ` ${ p } //localhost:8080/voice ` ;
2025-11-17 10:00:12 -03:00
await r . connect ( u , t ) ;
voiceRoom = r ;
2025-11-20 13:40:40 -03:00
r . on ( "dataReceived" , ( d ) => {
const dc = new TextDecoder ( ) ,
m = dc . decode ( d ) ;
2025-11-17 10:00:12 -03:00
try {
const j = JSON . parse ( m ) ;
if ( j . type === "voice_response" ) {
this . addMessage ( "assistant" , j . text ) ;
}
} catch ( e ) {
console . log ( "Voice data:" , m ) ;
2025-11-16 22:53:51 -03:00
}
2025-11-17 10:00:12 -03:00
} ) ;
2025-11-20 13:40:40 -03:00
const l = await LiveKitClient . createLocalTracks ( {
audio : true ,
video : false ,
} ) ;
2025-11-17 10:00:12 -03:00
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" ) ;
2025-11-16 22:53:51 -03:00
}
2025-11-17 10:00:12 -03:00
} ,
2025-11-16 22:53:51 -03:00
2025-11-17 10:00:12 -03:00
startVoiceRecording ( ) {
if ( ! navigator . mediaDevices ) {
console . log ( "Media devices not supported" ) ;
return ;
}
2025-11-20 13:40:40 -03:00
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" ) ;
} ) ;
2025-11-17 10:00:12 -03:00
} ,
simulateVoiceTranscription ( ) {
2025-11-20 13:40:40 -03:00
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" ,
] ;
2025-11-17 10:00:12 -03:00
const r = p [ Math . floor ( Math . random ( ) * p . length ) ] ;
if ( voiceRoom ) {
2025-11-20 13:40:40 -03:00
const m = {
type : "voice_input" ,
content : r ,
timestamp : new Date ( ) . toISOString ( ) ,
} ;
voiceRoom . localParticipant . publishData (
new TextEncoder ( ) . encode ( JSON . stringify ( m ) ) ,
LiveKitClient . DataPacketKind . RELIABLE ,
) ;
2025-11-17 10:00:12 -03:00
}
this . addMessage ( "voice" , ` 🎤 ${ r } ` ) ;
} ,
2025-11-16 22:53:51 -03:00
2025-11-17 10:00:12 -03:00
scrollToBottom ( ) {
messagesDiv . scrollTop = messagesDiv . scrollHeight ;
isUserScrolling = false ;
2025-11-20 13:40:40 -03:00
scrollToBottomBtn . classList . remove ( "visible" ) ;
} ,
2025-11-17 10:00:12 -03:00
} ;
2025-11-16 22:53:51 -03:00
}
2025-11-17 10:00:12 -03:00
// Initialize the app
chatApp ( ) . init ( ) ;