- Added HTTP server with CORS support and various endpoints - Introduced http_tx/http_rx channels for HTTP server control - Cleaned up build.rs by removing commented code - Updated .gitignore to use *.rdb pattern instead of .rdb - Simplified capabilities.json to empty object - Improved UI initialization with better error handling - Reorganized module imports in main.rs - Added worker count configuration for HTTP server The changes introduce a new HTTP server capability while cleaning up and improving existing code structure. The HTTP server includes authentication, session management, and websocket support.
1268 lines
30 KiB
HTML
1268 lines
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://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&display=swap");
|
|
:root{
|
|
--bg:#ffffff;
|
|
--fg:#000000;
|
|
--border:#e0e0e0;
|
|
--accent:#0066ff;
|
|
--glass:rgba(0,0,0,0.02);
|
|
--shadow:rgba(0,0,0,0.05);
|
|
--logo-url:url('https://pragmatismo.com.br/icons/general-bots.svg');
|
|
--gradient-1:linear-gradient(135deg,rgba(0,102,255,0.05) 0%,rgba(0,102,255,0.0) 100%);
|
|
--gradient-2:linear-gradient(45deg,rgba(0,0,0,0.02) 0%,rgba(0,0,0,0.0) 100%);
|
|
}
|
|
[data-theme="dark"]{
|
|
--bg:#1a1a1a;
|
|
--fg:#ffffff;
|
|
--border:#333333;
|
|
--accent:#ffffff;
|
|
--glass:rgba(255,255,255,0.05);
|
|
--shadow:rgba(0,0,0,0.5);
|
|
--gradient-1:linear-gradient(135deg,rgba(255,255,255,0.08) 0%,rgba(255,255,255,0.0) 100%);
|
|
--gradient-2:linear-gradient(45deg,rgba(255,255,255,0.03) 0%,rgba(255,255,255,0.0) 100%);
|
|
}
|
|
*{margin:0;padding:0;box-sizing:border-box}
|
|
body{
|
|
font-family:"Inter",sans-serif;
|
|
background:var(--bg);
|
|
color:var(--fg);
|
|
overflow:hidden;
|
|
transition:background 0.3s, color 0.3s;
|
|
display:flex;
|
|
flex-direction:column;
|
|
height:100vh;
|
|
position:relative;
|
|
}
|
|
body::before{
|
|
content:'';
|
|
position:fixed;
|
|
inset:0;
|
|
background:var(--gradient-1);
|
|
pointer-events:none;
|
|
z-index:0;
|
|
}
|
|
.float-menu{
|
|
position:fixed;
|
|
left:20px;
|
|
top:20px;
|
|
display:flex;
|
|
flex-direction:column;
|
|
gap:8px;
|
|
z-index:1000;
|
|
}
|
|
.float-logo{
|
|
width:40px;
|
|
height:40px;
|
|
background:var(--logo-url) center/contain no-repeat;
|
|
filter:var(--logo-filter, none);
|
|
border-radius:50%;
|
|
cursor:pointer;
|
|
transition:all 0.3s;
|
|
border:1px solid var(--border);
|
|
backdrop-filter:blur(10px);
|
|
}
|
|
[data-theme="dark"] .float-logo{
|
|
}
|
|
.float-logo:hover{
|
|
transform:scale(1.1) rotate(5deg);
|
|
}
|
|
.menu-button{
|
|
width:40px;
|
|
height:40px;
|
|
border-radius:50%;
|
|
display:flex;
|
|
align-items:center;
|
|
justify-content:center;
|
|
cursor:pointer;
|
|
transition:all 0.3s;
|
|
background:var(--bg);
|
|
border:1px solid var(--border);
|
|
font-size:16px;
|
|
color:var(--fg);
|
|
backdrop-filter:blur(10px);
|
|
}
|
|
.menu-button:hover{
|
|
transform:scale(1.1) rotate(-5deg);
|
|
background:var(--fg);
|
|
color:var(--bg);
|
|
}
|
|
.sidebar{
|
|
position:fixed;
|
|
left:-320px;
|
|
top:0;
|
|
width:320px;
|
|
height:100vh;
|
|
background:var(--bg);
|
|
border-right:1px solid var(--border);
|
|
transition:left 0.4s cubic-bezier(0.4,0,0.2,1);
|
|
z-index:999;
|
|
overflow-y:auto;
|
|
padding:20px;
|
|
backdrop-filter:blur(20px);
|
|
box-shadow:4px 0 20px var(--shadow);
|
|
}
|
|
.sidebar.open{
|
|
left:0;
|
|
}
|
|
.sidebar-header{
|
|
display:flex;
|
|
align-items:center;
|
|
gap:12px;
|
|
margin-bottom:30px;
|
|
padding-top:10px;
|
|
}
|
|
.sidebar-logo{
|
|
width:32px;
|
|
height:32px;
|
|
background:var(--logo-url) center/contain no-repeat;
|
|
filter:var(--logo-filter, none);
|
|
}
|
|
[data-theme="dark"] .sidebar-logo{
|
|
}
|
|
.sidebar-title{
|
|
font-size:16px;
|
|
font-weight:500;
|
|
}
|
|
.sidebar-button{
|
|
width:100%;
|
|
padding:12px 16px;
|
|
border-radius:12px;
|
|
cursor:pointer;
|
|
transition:all 0.3s;
|
|
font-weight:500;
|
|
font-size:14px;
|
|
margin-bottom:8px;
|
|
background:var(--glass);
|
|
border:1px solid var(--border);
|
|
color:var(--fg);
|
|
text-align:left;
|
|
}
|
|
.sidebar-button:hover{
|
|
background:var(--fg);
|
|
color:var(--bg);
|
|
transform:translateX(4px) scale(1.02);
|
|
}
|
|
.history-section{
|
|
margin-top:20px;
|
|
}
|
|
.history-title{
|
|
font-size:12px;
|
|
opacity:0.5;
|
|
margin-bottom:12px;
|
|
text-transform:uppercase;
|
|
letter-spacing:0.5px;
|
|
}
|
|
.history-item{
|
|
padding:10px 14px;
|
|
margin-bottom:6px;
|
|
border-radius:10px;
|
|
cursor:pointer;
|
|
transition:all 0.3s;
|
|
font-size:13px;
|
|
border:1px solid transparent;
|
|
}
|
|
.history-item:hover{
|
|
background:var(--fg);
|
|
color:var(--bg);
|
|
transform:translateX(4px) scale(1.02);
|
|
}
|
|
#messages{
|
|
flex:1;
|
|
overflow-y:auto;
|
|
padding:20px 20px 140px;
|
|
max-width:680px;
|
|
margin:0 auto;
|
|
width:100%;
|
|
position:relative;
|
|
z-index:1;
|
|
}
|
|
.message-container{
|
|
margin-bottom:24px;
|
|
opacity:0;
|
|
transform:translateY(10px);
|
|
}
|
|
.user-message{
|
|
display:flex;
|
|
justify-content:flex-end;
|
|
margin-bottom:8px;
|
|
}
|
|
.user-message-content{
|
|
background:var(--fg);
|
|
color:var(--bg);
|
|
border-radius:18px;
|
|
padding:12px 18px;
|
|
max-width:80%;
|
|
font-size:14px;
|
|
line-height:1.5;
|
|
box-shadow:0 2px 8px var(--shadow);
|
|
position:relative;
|
|
overflow:hidden;
|
|
}
|
|
.user-message-content::before{
|
|
content:'';
|
|
position:absolute;
|
|
inset:0;
|
|
background:var(--gradient-2);
|
|
opacity:0.3;
|
|
pointer-events:none;
|
|
}
|
|
.assistant-message{
|
|
display:flex;
|
|
gap:8px;
|
|
align-items:flex-start;
|
|
}
|
|
.assistant-avatar{
|
|
width:24px;
|
|
height:24px;
|
|
border-radius:50%;
|
|
background:var(--logo-url) center/contain no-repeat;
|
|
flex-shrink:0;
|
|
margin-top:2px;
|
|
filter:var(--logo-filter, none);
|
|
}
|
|
[data-theme="dark"] .assistant-avatar{
|
|
}
|
|
.assistant-message-content{
|
|
flex:1;
|
|
font-size:14px;
|
|
line-height:1.7;
|
|
background:var(--glass);
|
|
border-radius:18px;
|
|
padding:12px 18px;
|
|
border:1px solid var(--border);
|
|
box-shadow:0 2px 8px var(--shadow);
|
|
position:relative;
|
|
overflow:hidden;
|
|
}
|
|
.assistant-message-content::before{
|
|
content:'';
|
|
position:absolute;
|
|
inset:0;
|
|
background:var(--gradient-1);
|
|
opacity:0.5;
|
|
pointer-events:none;
|
|
}
|
|
.thinking-indicator{
|
|
display:flex;
|
|
gap:8px;
|
|
align-items:center;
|
|
font-size:13px;
|
|
opacity:0.4;
|
|
}
|
|
.typing-dots{
|
|
display:flex;
|
|
gap:4px;
|
|
}
|
|
.typing-dot{
|
|
width:4px;
|
|
height:4px;
|
|
background:var(--fg);
|
|
border-radius:50%;
|
|
animation:bounce 1.4s infinite;
|
|
}
|
|
.typing-dot:nth-child(1){animation-delay:-.32s}
|
|
.typing-dot:nth-child(2){animation-delay:-.16s}
|
|
@keyframes bounce{
|
|
0%,80%,100%{transform:scale(0);opacity:.3}
|
|
40%{transform:scale(1);opacity:1}
|
|
}
|
|
footer{
|
|
position:fixed;
|
|
bottom:0;
|
|
left:0;
|
|
right:0;
|
|
background:var(--bg);
|
|
border-top:1px solid var(--border);
|
|
padding:12px;
|
|
z-index:100;
|
|
transition:all 0.3s;
|
|
backdrop-filter:blur(20px);
|
|
}
|
|
.suggestions-container{
|
|
display:flex;
|
|
flex-wrap:wrap;
|
|
gap:4px;
|
|
margin-bottom:8px;
|
|
justify-content:center;
|
|
max-width:680px;
|
|
margin:0 auto 8px;
|
|
}
|
|
.suggestion-button{
|
|
padding:6px 12px;
|
|
border-radius:12px;
|
|
cursor:pointer;
|
|
font-size:11px;
|
|
font-weight:400;
|
|
transition:all 0.2s;
|
|
background:var(--glass);
|
|
border:1px solid var(--border);
|
|
color:var(--fg);
|
|
}
|
|
.suggestion-button:hover{
|
|
background:var(--fg);
|
|
color:var(--bg);
|
|
transform:scale(1.05);
|
|
}
|
|
.input-container{
|
|
display:flex;
|
|
gap:6px;
|
|
max-width:680px;
|
|
margin:0 auto;
|
|
align-items:center;
|
|
}
|
|
#messageInput{
|
|
flex:1;
|
|
border-radius:20px;
|
|
padding:10px 16px;
|
|
font-size:14px;
|
|
font-family:"Inter",sans-serif;
|
|
outline:none;
|
|
transition:all 0.3s;
|
|
background:var(--glass);
|
|
border:1px solid var(--border);
|
|
color:var(--fg);
|
|
backdrop-filter:blur(10px);
|
|
}
|
|
#messageInput:focus{
|
|
border-color:var(--accent);
|
|
box-shadow:0 0 0 3px rgba(0,102,255,0.1);
|
|
}
|
|
#messageInput::placeholder{
|
|
opacity:0.3;
|
|
}
|
|
#sendBtn,#voiceBtn{
|
|
width:36px;
|
|
height:36px;
|
|
border-radius:18px;
|
|
display:flex;
|
|
align-items:center;
|
|
justify-content:center;
|
|
cursor:pointer;
|
|
transition:all 0.2s;
|
|
border:none;
|
|
background:var(--fg);
|
|
color:var(--bg);
|
|
font-size:16px;
|
|
flex-shrink:0;
|
|
}
|
|
#sendBtn:hover,#voiceBtn:hover{
|
|
transform:scale(1.08) rotate(5deg);
|
|
}
|
|
#sendBtn:active,#voiceBtn:active{
|
|
transform:scale(0.95);
|
|
}
|
|
#voiceBtn.recording{
|
|
animation:pulse 1.5s infinite;
|
|
}
|
|
@keyframes pulse{
|
|
0%,100%{opacity:1;transform:scale(1)}
|
|
50%{opacity:0.6;transform:scale(1.1)}
|
|
}
|
|
.flash-overlay{
|
|
position:fixed;
|
|
inset:0;
|
|
background:var(--fg);
|
|
opacity:0;
|
|
pointer-events:none;
|
|
z-index:9999;
|
|
}
|
|
.scroll-to-bottom{
|
|
position:fixed;
|
|
bottom:80px;
|
|
right:20px;
|
|
width:40px;
|
|
height:40px;
|
|
background:var(--fg);
|
|
border:1px solid var(--border);
|
|
border-radius:50%;
|
|
color:var(--bg);
|
|
font-size:18px;
|
|
cursor:pointer;
|
|
display:none;
|
|
align-items:center;
|
|
justify-content:center;
|
|
transition:all 0.3s;
|
|
z-index:90;
|
|
}
|
|
.scroll-to-bottom.visible{
|
|
display:flex;
|
|
}
|
|
.scroll-to-bottom:hover{
|
|
transform:scale(1.1) rotate(180deg);
|
|
}
|
|
.warning-message{
|
|
border-radius:12px;
|
|
padding:12px 16px;
|
|
margin-bottom:18px;
|
|
opacity:0.6;
|
|
background:var(--glass);
|
|
border:1px solid var(--border);
|
|
font-size:13px;
|
|
}
|
|
.continue-button{
|
|
display:inline-block;
|
|
border-radius:10px;
|
|
padding:8px 16px;
|
|
font-weight:500;
|
|
cursor:pointer;
|
|
margin-top:10px;
|
|
transition:all 0.3s;
|
|
font-size:13px;
|
|
background:var(--glass);
|
|
border:1px solid var(--border);
|
|
}
|
|
.continue-button:hover{
|
|
background:var(--fg);
|
|
color:var(--bg);
|
|
transform:translateY(-2px);
|
|
}
|
|
.context-indicator{
|
|
position:fixed;
|
|
bottom:130px;
|
|
right:20px;
|
|
width:120px;
|
|
border-radius:12px;
|
|
padding:10px;
|
|
font-size:10px;
|
|
text-align:center;
|
|
z-index:90;
|
|
background:var(--bg);
|
|
border:1px solid var(--border);
|
|
display:none;
|
|
backdrop-filter:blur(10px);
|
|
}
|
|
.context-indicator.visible{
|
|
display:block;
|
|
}
|
|
.context-progress{
|
|
height:3px;
|
|
background:var(--glass);
|
|
border-radius:2px;
|
|
margin-top:6px;
|
|
overflow:hidden;
|
|
}
|
|
.context-progress-bar{
|
|
height:100%;
|
|
background:var(--accent);
|
|
border-radius:2px;
|
|
transition:width 0.3s;
|
|
}
|
|
.connection-status{
|
|
position:fixed;
|
|
top:20px;
|
|
right:20px;
|
|
width:8px;
|
|
height:8px;
|
|
border-radius:50%;
|
|
z-index:1000;
|
|
transition:all 0.3s;
|
|
}
|
|
.connection-status.connecting{
|
|
background:var(--fg);
|
|
opacity:0.3;
|
|
animation:ping 1.5s infinite;
|
|
}
|
|
.connection-status.connected{
|
|
background:var(--accent);
|
|
opacity:0.8;
|
|
}
|
|
.connection-status.disconnected{
|
|
background:var(--fg);
|
|
opacity:0.2;
|
|
}
|
|
@keyframes ping{
|
|
0%,100%{opacity:0.3;transform:scale(0.8)}
|
|
50%{opacity:0.8;transform:scale(1.2)}
|
|
}
|
|
.markdown-content p{
|
|
margin-bottom:12px;
|
|
line-height:1.7;
|
|
}
|
|
.markdown-content ul,.markdown-content ol{
|
|
margin-bottom:12px;
|
|
padding-left:20px;
|
|
}
|
|
.markdown-content li{
|
|
margin-bottom:4px;
|
|
}
|
|
.markdown-content code{
|
|
background:var(--glass);
|
|
padding:2px 6px;
|
|
border-radius:4px;
|
|
font-family:monospace;
|
|
font-size:13px;
|
|
}
|
|
.markdown-content pre{
|
|
border-radius:8px;
|
|
padding:12px;
|
|
overflow-x:auto;
|
|
margin-bottom:12px;
|
|
background:var(--glass);
|
|
border:1px solid var(--border);
|
|
}
|
|
.markdown-content pre code{
|
|
background:none;
|
|
padding:0;
|
|
}
|
|
.markdown-content h1,.markdown-content h2,.markdown-content h3{
|
|
margin-top:16px;
|
|
margin-bottom:8px;
|
|
font-weight:600;
|
|
}
|
|
.markdown-content h1{font-size:20px}
|
|
.markdown-content h2{font-size:18px}
|
|
.markdown-content h3{font-size:16px}
|
|
.markdown-content table{
|
|
width:100%;
|
|
border-collapse:collapse;
|
|
margin-bottom:14px;
|
|
}
|
|
.markdown-content table th,.markdown-content table td{
|
|
padding:8px;
|
|
text-align:left;
|
|
border:1px solid var(--border);
|
|
}
|
|
.markdown-content table th{
|
|
font-weight:600;
|
|
background:var(--glass);
|
|
}
|
|
.markdown-content blockquote{
|
|
border-left:2px solid var(--accent);
|
|
padding-left:14px;
|
|
margin:12px 0;
|
|
opacity:0.7;
|
|
font-style:italic;
|
|
}
|
|
.markdown-content a{
|
|
color:var(--accent);
|
|
text-decoration:none;
|
|
transition:all 0.3s;
|
|
}
|
|
.markdown-content a:hover{
|
|
opacity:0.7;
|
|
text-decoration:underline;
|
|
}
|
|
::-webkit-scrollbar{
|
|
width:6px;
|
|
}
|
|
::-webkit-scrollbar-track{
|
|
background:transparent;
|
|
}
|
|
::-webkit-scrollbar-thumb{
|
|
background:var(--border);
|
|
border-radius:3px;
|
|
}
|
|
::-webkit-scrollbar-thumb:hover{
|
|
background:var(--fg);
|
|
opacity:0.3;
|
|
}
|
|
@media(max-width:768px){
|
|
.sidebar{
|
|
width:100%;
|
|
left:-100%;
|
|
}
|
|
#messages{
|
|
padding:20px 16px 140px;
|
|
}
|
|
.float-menu{
|
|
left:12px;
|
|
top:12px;
|
|
}
|
|
.float-logo,.menu-button{
|
|
width:36px;
|
|
height:36px;
|
|
font-size:14px;
|
|
}
|
|
.scroll-to-bottom{
|
|
width:36px;
|
|
height:36px;
|
|
bottom:70px;
|
|
right:12px;
|
|
}
|
|
.context-indicator{
|
|
bottom:120px;
|
|
right:12px;
|
|
width:100px;
|
|
}
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="connection-status connecting" id="connectionStatus"></div>
|
|
<div class="flash-overlay" id="flashOverlay"></div>
|
|
<div class="float-menu">
|
|
<div class="float-logo" id="floatLogo" title="Menu"></div>
|
|
<div class="menu-button" id="themeBtn" title="Theme">⚙</div>
|
|
</div>
|
|
<div class="sidebar" id="sidebar">
|
|
<div class="sidebar-header">
|
|
<div class="sidebar-logo"></div>
|
|
<div class="sidebar-title" id="sidebarTitle">General Bots</div>
|
|
</div>
|
|
<button class="sidebar-button" id="voiceToggle" onclick="toggleVoiceMode()">🎤 Voice Mode</button>
|
|
<div class="history-section">
|
|
<div class="history-title">History</div>
|
|
<div id="history"></div>
|
|
</div>
|
|
</div>
|
|
<main id="messages"></main>
|
|
<footer>
|
|
<div class="suggestions-container" id="suggestions"></div>
|
|
<div class="input-container">
|
|
<input id="messageInput" type="text" placeholder="Message..." autofocus/>
|
|
<button id="voiceBtn" title="Voice">🎤</button>
|
|
<button id="sendBtn" title="Send">↑</button>
|
|
</div>
|
|
</footer>
|
|
<button class="scroll-to-bottom" id="scrollToBottom">↓</button>
|
|
<div class="context-indicator" id="contextIndicator">
|
|
<div>Context</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,currentTheme='auto',themeColor1=null,themeColor2=null,customLogoUrl=null,contextUsage=0,isUserScrolling=false,autoScrollEnabled=true,isContextChange=false;
|
|
const maxReconnectAttempts=5,messagesDiv=document.getElementById("messages"),input=document.getElementById("messageInput"),sendBtn=document.getElementById("sendBtn"),voiceBtn=document.getElementById("voiceBtn"),connectionStatus=document.getElementById("connectionStatus"),flashOverlay=document.getElementById("flashOverlay"),suggestionsContainer=document.getElementById("suggestions"),floatLogo=document.getElementById("floatLogo"),sidebar=document.getElementById("sidebar"),themeBtn=document.getElementById("themeBtn"),scrollToBottomBtn=document.getElementById("scrollToBottom"),contextIndicator=document.getElementById("contextIndicator"),contextPercentage=document.getElementById("contextPercentage"),contextProgressBar=document.getElementById("contextProgressBar"),sidebarTitle=document.getElementById("sidebarTitle");
|
|
marked.setOptions({breaks:true,gfm:true});
|
|
|
|
floatLogo.addEventListener('click',toggleSidebar);
|
|
|
|
function toggleSidebar(){
|
|
sidebar.classList.toggle('open');
|
|
}
|
|
|
|
function toggleTheme(){
|
|
const themes=['auto','dark','light'];
|
|
const savedTheme=localStorage.getItem('gb-theme')||'auto';
|
|
const idx=themes.indexOf(savedTheme);
|
|
const newTheme=themes[(idx+1)%themes.length];
|
|
localStorage.setItem('gb-theme',newTheme);
|
|
currentTheme=newTheme;
|
|
applyTheme();
|
|
updateThemeButton();
|
|
}
|
|
|
|
function updateThemeButton(){
|
|
const icons={'auto':'⚙','dark':'🌙','light':'☀️'};
|
|
themeBtn.textContent=icons[currentTheme]||'⚙';
|
|
}
|
|
|
|
function applyTheme(){
|
|
const prefersDark=window.matchMedia('(prefers-color-scheme: dark)').matches;
|
|
let theme=currentTheme;
|
|
if(theme==='auto'){
|
|
theme=prefersDark?'dark':'light';
|
|
}
|
|
document.documentElement.setAttribute('data-theme',theme);
|
|
if(themeColor1&&themeColor2){
|
|
const root=document.documentElement;
|
|
root.style.setProperty('--bg',theme==='dark'?themeColor2:themeColor1);
|
|
root.style.setProperty('--fg',theme==='dark'?themeColor1:themeColor2);
|
|
}
|
|
if(customLogoUrl){
|
|
document.documentElement.style.setProperty('--logo-url',`url('${customLogoUrl}')`);
|
|
}
|
|
}
|
|
|
|
window.addEventListener("load",function(){
|
|
const savedTheme=localStorage.getItem('gb-theme')||'auto';
|
|
currentTheme=savedTheme;
|
|
applyTheme();
|
|
updateThemeButton();
|
|
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change',()=>{
|
|
if(currentTheme==='auto'){
|
|
applyTheme();
|
|
}
|
|
});
|
|
input.focus();
|
|
});
|
|
|
|
themeBtn.addEventListener('click',toggleTheme);
|
|
|
|
document.addEventListener('click',function(e){
|
|
if(sidebar.classList.contains('open')&&!sidebar.contains(e.target)&&!floatLogo.contains(e.target)){
|
|
sidebar.classList.remove('open');
|
|
}
|
|
});
|
|
|
|
messagesDiv.addEventListener('scroll',function(){
|
|
const isAtBottom=messagesDiv.scrollHeight-messagesDiv.scrollTop<=messagesDiv.clientHeight+100;
|
|
if(!isAtBottom){
|
|
isUserScrolling=true;
|
|
scrollToBottomBtn.classList.add('visible');
|
|
}else{
|
|
isUserScrolling=false;
|
|
scrollToBottomBtn.classList.remove('visible');
|
|
}
|
|
});
|
|
|
|
scrollToBottomBtn.addEventListener('click',function(){
|
|
scrollToBottom();
|
|
});
|
|
|
|
function updateContextUsage(u){
|
|
contextUsage=u;
|
|
const p=Math.min(100,Math.round(u*100));
|
|
contextPercentage.textContent=`${p}%`;
|
|
contextProgressBar.style.width=`${p}%`;
|
|
contextIndicator.classList.remove('visible');
|
|
}
|
|
|
|
function flashScreen(){
|
|
gsap.to(flashOverlay,{
|
|
opacity:0.15,
|
|
duration:0.1,
|
|
onComplete:()=>{
|
|
gsap.to(flashOverlay,{opacity:0,duration:0.2});
|
|
}
|
|
});
|
|
}
|
|
|
|
function updateConnectionStatus(s){
|
|
connectionStatus.className=`connection-status ${s}`;
|
|
}
|
|
|
|
function getWebSocketUrl(){
|
|
const p="ws:",s=currentSessionId||crypto.randomUUID(),u=currentUserId||crypto.randomUUID();
|
|
return`${p}//localhost:8080/ws?session_id=${s}&user_id=${u}`;
|
|
}
|
|
|
|
async function initializeAuth(){
|
|
try{
|
|
updateConnectionStatus("connecting");
|
|
const p=window.location.pathname.split('/').filter(s=>s),b=p.length>0?p[0]:'default',r=await fetch(`http://localhost:8080/api/auth?bot_name=${encodeURIComponent(b)}`),a=await r.json();
|
|
currentUserId=a.user_id;
|
|
currentSessionId=a.session_id;
|
|
connectWebSocket();
|
|
loadSessions();
|
|
}catch(e){
|
|
console.error("Failed to initialize auth:",e);
|
|
updateConnectionStatus("disconnected");
|
|
setTimeout(initializeAuth,3000);
|
|
}
|
|
}
|
|
|
|
async function loadSessions(){
|
|
try{
|
|
const r=await fetch("http://localhost:8080/api/sessions"),s=await r.json(),h=document.getElementById("history");
|
|
h.innerHTML="";
|
|
s.forEach(session=>{
|
|
const item=document.createElement('div');
|
|
item.className='history-item';
|
|
item.textContent=session.title||`Session ${session.session_id.substring(0,8)}`;
|
|
item.onclick=()=>switchSession(session.session_id);
|
|
h.appendChild(item);
|
|
});
|
|
}catch(e){
|
|
console.error("Failed to load sessions:",e);
|
|
}
|
|
}
|
|
|
|
async function createNewSession(){
|
|
try{
|
|
const r=await fetch("http://localhost:8080/api/sessions",{method:"POST"}),s=await r.json();
|
|
currentSessionId=s.session_id;
|
|
hasReceivedInitialMessage=false;
|
|
connectWebSocket();
|
|
loadSessions();
|
|
messagesDiv.innerHTML="";
|
|
clearSuggestions();
|
|
updateContextUsage(0);
|
|
if(isVoiceMode){
|
|
await stopVoiceSession();
|
|
isVoiceMode=false;
|
|
const v=document.getElementById("voiceToggle");
|
|
v.textContent="🎤 Voice Mode";
|
|
voiceBtn.classList.remove("recording");
|
|
}
|
|
}catch(e){
|
|
console.error("Failed to create session:",e);
|
|
}
|
|
}
|
|
|
|
function switchSession(s){
|
|
currentSessionId=s;
|
|
hasReceivedInitialMessage=false;
|
|
loadSessionHistory(s);
|
|
connectWebSocket();
|
|
if(isVoiceMode){
|
|
startVoiceSession();
|
|
}
|
|
sidebar.classList.remove('open');
|
|
}
|
|
|
|
async function loadSessionHistory(s){
|
|
try{
|
|
const r=await fetch("http://localhost:8080/api/sessions/"+s),h=await r.json(),m=document.getElementById("messages");
|
|
m.innerHTML="";
|
|
if(h.length===0){
|
|
updateContextUsage(0);
|
|
}else{
|
|
h.forEach(([role,content])=>{
|
|
addMessage(role,content,false);
|
|
});
|
|
updateContextUsage(h.length/20);
|
|
}
|
|
}catch(e){
|
|
console.error("Failed to load session history:",e);
|
|
}
|
|
}
|
|
|
|
function connectWebSocket(){
|
|
if(ws){
|
|
ws.close();
|
|
}
|
|
clearTimeout(reconnectTimeout);
|
|
const u=getWebSocketUrl();
|
|
ws=new WebSocket(u);
|
|
ws.onmessage=function(e){
|
|
const r=JSON.parse(e.data);
|
|
if(r.bot_id){
|
|
currentBotId=r.bot_id;
|
|
}
|
|
if(r.message_type===2){
|
|
const d=JSON.parse(r.content);
|
|
handleEvent(d.event,d.data);
|
|
return;
|
|
}
|
|
if(r.message_type===5){
|
|
isContextChange=true;
|
|
return;
|
|
}
|
|
processMessageContent(r);
|
|
};
|
|
ws.onopen=function(){
|
|
console.log("Connected to WebSocket");
|
|
updateConnectionStatus("connected");
|
|
reconnectAttempts=0;
|
|
hasReceivedInitialMessage=false;
|
|
};
|
|
ws.onclose=function(e){
|
|
console.log("WebSocket disconnected:",e.code,e.reason);
|
|
updateConnectionStatus("disconnected");
|
|
if(isStreaming){
|
|
showContinueButton();
|
|
}
|
|
if(reconnectAttempts<maxReconnectAttempts){
|
|
reconnectAttempts++;
|
|
const d=Math.min(1000*reconnectAttempts,10000);
|
|
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;
|
|
}
|
|
if(r.context_usage!==undefined){
|
|
updateContextUsage(r.context_usage);
|
|
}
|
|
if(r.suggestions&&r.suggestions.length>0){
|
|
handleSuggestions(r.suggestions);
|
|
}
|
|
if(r.is_complete){
|
|
if(isStreaming){
|
|
finalizeStreamingMessage();
|
|
isStreaming=false;
|
|
streamingMessageId=null;
|
|
currentStreamingContent="";
|
|
}else{
|
|
addMessage("assistant",r.content,false);
|
|
}
|
|
}else{
|
|
if(!isStreaming){
|
|
isStreaming=true;
|
|
streamingMessageId="streaming-"+Date.now();
|
|
currentStreamingContent=r.content||"";
|
|
addMessage("assistant",currentStreamingContent,true,streamingMessageId);
|
|
}else{
|
|
currentStreamingContent+=r.content||"";
|
|
updateStreamingMessage(currentStreamingContent);
|
|
}
|
|
}
|
|
}
|
|
|
|
function handleEvent(t,d){
|
|
console.log("Event received:",t,d);
|
|
switch(t){
|
|
case"thinking_start":
|
|
showThinkingIndicator();
|
|
break;
|
|
case"thinking_end":
|
|
hideThinkingIndicator();
|
|
break;
|
|
case"warn":
|
|
showWarning(d.message);
|
|
break;
|
|
case"context_usage":
|
|
updateContextUsage(d.usage);
|
|
break;
|
|
case"change_theme":
|
|
if(d.color1)themeColor1=d.color1;
|
|
if(d.color2)themeColor2=d.color2;
|
|
if(d.logo_url)customLogoUrl=d.logo_url;
|
|
if(d.title)document.title=d.title;
|
|
if(d.logo_text){
|
|
sidebarTitle.textContent=d.logo_text;
|
|
}
|
|
applyTheme();
|
|
break;
|
|
}
|
|
}
|
|
|
|
function showThinkingIndicator(){
|
|
if(isThinking)return;
|
|
const t=document.createElement("div");
|
|
t.id="thinking-indicator";
|
|
t.className="message-container";
|
|
t.innerHTML=`<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);
|
|
gsap.to(t,{opacity:1,y:0,duration:.3,ease:"power2.out"});
|
|
if(!isUserScrolling){
|
|
scrollToBottom();
|
|
}
|
|
thinkingTimeout=setTimeout(()=>{
|
|
if(isThinking){
|
|
hideThinkingIndicator();
|
|
showWarning("O servidor pode estar ocupado. A resposta está demorando demais.");
|
|
}
|
|
},60000);
|
|
isThinking=true;
|
|
}
|
|
|
|
function hideThinkingIndicator(){
|
|
if(!isThinking)return;
|
|
const t=document.getElementById("thinking-indicator");
|
|
if(t){
|
|
gsap.to(t,{opacity:0,duration:.2,onComplete:()=>{
|
|
if(t.parentNode){
|
|
t.remove();
|
|
}
|
|
}});
|
|
}
|
|
if(thinkingTimeout){
|
|
clearTimeout(thinkingTimeout);
|
|
thinkingTimeout=null;
|
|
}
|
|
isThinking=false;
|
|
}
|
|
|
|
function showWarning(m){
|
|
const w=document.createElement("div");
|
|
w.className="warning-message";
|
|
w.innerHTML=`⚠️ ${m}`;
|
|
messagesDiv.appendChild(w);
|
|
gsap.from(w,{opacity:0,y:20,duration:.4,ease:"power2.out"});
|
|
if(!isUserScrolling){
|
|
scrollToBottom();
|
|
}
|
|
setTimeout(()=>{
|
|
if(w.parentNode){
|
|
gsap.to(w,{opacity:0,duration:.3,onComplete:()=>w.remove()});
|
|
}
|
|
},5000);
|
|
}
|
|
|
|
function showContinueButton(){
|
|
const c=document.createElement("div");
|
|
c.className="message-container";
|
|
c.innerHTML=`<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="continueInterruptedResponse()">Continuar</button></div></div>`;
|
|
messagesDiv.appendChild(c);
|
|
gsap.to(c,{opacity:1,y:0,duration:.5,ease:"power2.out"});
|
|
if(!isUserScrolling){
|
|
scrollToBottom();
|
|
}
|
|
}
|
|
|
|
function continueInterruptedResponse(){
|
|
if(!ws||ws.readyState!==WebSocket.OPEN){
|
|
connectWebSocket();
|
|
}
|
|
if(ws&&ws.readyState===WebSocket.OPEN){
|
|
const d={bot_id:"default_bot",user_id:currentUserId,session_id:currentSessionId,channel:"web",content:"continue",message_type:3,media_url:null,timestamp:new Date().toISOString()};
|
|
ws.send(JSON.stringify(d));
|
|
}
|
|
document.querySelectorAll(".continue-button").forEach(b=>{
|
|
b.parentElement.parentElement.parentElement.remove();
|
|
});
|
|
}
|
|
|
|
function addMessage(role,content,streaming=false,msgId=null){
|
|
const m=document.createElement("div");
|
|
m.className="message-container";
|
|
if(role==="user"){
|
|
m.innerHTML=`<div class="user-message"><div class="user-message-content">${escapeHtml(content)}</div></div>`;
|
|
updateContextUsage(contextUsage+.05);
|
|
}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>`;
|
|
updateContextUsage(contextUsage+.03);
|
|
}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);
|
|
gsap.to(m,{opacity:1,y:0,duration:.5,ease:"power2.out"});
|
|
if(!isUserScrolling){
|
|
scrollToBottom();
|
|
}
|
|
}
|
|
|
|
function updateStreamingMessage(c){
|
|
const m=document.getElementById(streamingMessageId);
|
|
if(m){
|
|
m.innerHTML=marked.parse(c);
|
|
if(!isUserScrolling){
|
|
scrollToBottom();
|
|
}
|
|
}
|
|
}
|
|
|
|
function finalizeStreamingMessage(){
|
|
const m=document.getElementById(streamingMessageId);
|
|
if(m){
|
|
m.innerHTML=marked.parse(currentStreamingContent);
|
|
m.removeAttribute("id");
|
|
if(!isUserScrolling){
|
|
scrollToBottom();
|
|
}
|
|
}
|
|
}
|
|
|
|
function escapeHtml(t){
|
|
const d=document.createElement("div");
|
|
d.textContent=t;
|
|
return d.innerHTML;
|
|
}
|
|
|
|
function clearSuggestions(){
|
|
suggestionsContainer.innerHTML='';
|
|
}
|
|
|
|
function handleSuggestions(s){
|
|
const uniqueSuggestions=s.filter((v,i,a)=>i===a.findIndex(t=>t.text===v.text&&t.context===v.context));
|
|
suggestionsContainer.innerHTML='';
|
|
uniqueSuggestions.forEach(v=>{
|
|
const b=document.createElement('button');
|
|
b.textContent=v.text;
|
|
b.className='suggestion-button';
|
|
b.onclick=()=>{
|
|
setContext(v.context);
|
|
input.value='';
|
|
};
|
|
suggestionsContainer.appendChild(b);
|
|
});
|
|
}
|
|
|
|
let pendingContextChange=null;
|
|
|
|
async function setContext(c){
|
|
try{
|
|
const t=event?.target?.textContent||c;
|
|
addMessage("user",t);
|
|
const i=document.getElementById('messageInput');
|
|
if(i){
|
|
i.value='';
|
|
}
|
|
if(ws&&ws.readyState===WebSocket.OPEN){
|
|
pendingContextChange=new Promise(r=>{
|
|
const h=e=>{
|
|
const d=JSON.parse(e.data);
|
|
if(d.message_type===5&&d.context_name===c){
|
|
ws.removeEventListener('message',h);
|
|
r();
|
|
}
|
|
};
|
|
ws.addEventListener('message',h);
|
|
const s={bot_id:currentBotId,user_id:currentUserId,session_id:currentSessionId,channel:"web",content:t,message_type:4,is_suggestion:true,context_name:c,timestamp:new Date().toISOString()};
|
|
ws.send(JSON.stringify(s));
|
|
});
|
|
await pendingContextChange;
|
|
const x=document.getElementById('contextIndicator');
|
|
if(x){
|
|
document.getElementById('contextPercentage').textContent=c;
|
|
}
|
|
}else{
|
|
console.warn("WebSocket não está conectado. Tentando reconectar...");
|
|
connectWebSocket();
|
|
}
|
|
}catch(err){
|
|
console.error('Failed to set context:',err);
|
|
}
|
|
}
|
|
|
|
async function sendMessage(){
|
|
if(pendingContextChange){
|
|
await pendingContextChange;
|
|
pendingContextChange=null;
|
|
}
|
|
const m=input.value.trim();
|
|
if(!m||!ws||ws.readyState!==WebSocket.OPEN){
|
|
if(!ws||ws.readyState!==WebSocket.OPEN){
|
|
showWarning("Conexão não disponível. Tentando reconectar...");
|
|
connectWebSocket();
|
|
}
|
|
return;
|
|
}
|
|
if(isThinking){
|
|
hideThinkingIndicator();
|
|
}
|
|
addMessage("user",m);
|
|
const d={bot_id:currentBotId,user_id:currentUserId,session_id:currentSessionId,channel:"web",content:m,message_type:1,media_url:null,timestamp:new Date().toISOString()};
|
|
ws.send(JSON.stringify(d));
|
|
input.value="";
|
|
input.focus();
|
|
}
|
|
|
|
sendBtn.onclick=sendMessage;
|
|
input.addEventListener("keypress",e=>{
|
|
if(e.key==="Enter")sendMessage();
|
|
});
|
|
|
|
async function toggleVoiceMode(){
|
|
isVoiceMode=!isVoiceMode;
|
|
const v=document.getElementById("voiceToggle");
|
|
if(isVoiceMode){
|
|
v.textContent="🔴 Stop Voice";
|
|
v.classList.add("recording");
|
|
await startVoiceSession();
|
|
}else{
|
|
v.textContent="🎤 Voice Mode";
|
|
v.classList.remove("recording");
|
|
await stopVoiceSession();
|
|
}
|
|
}
|
|
|
|
async function startVoiceSession(){
|
|
if(!currentSessionId)return;
|
|
try{
|
|
const r=await fetch("http://localhost:8080/api/voice/start",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({session_id:currentSessionId,user_id:currentUserId})}),d=await r.json();
|
|
if(d.token){
|
|
await connectToVoiceRoom(d.token);
|
|
startVoiceRecording();
|
|
}
|
|
}catch(e){
|
|
console.error("Failed to start voice session:",e);
|
|
showWarning("Falha ao iniciar modo de voz");
|
|
}
|
|
}
|
|
|
|
async function stopVoiceSession(){
|
|
if(!currentSessionId)return;
|
|
try{
|
|
await fetch("http://localhost:8080/api/voice/stop",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({session_id:currentSessionId})});
|
|
if(voiceRoom){
|
|
voiceRoom.disconnect();
|
|
voiceRoom=null;
|
|
}
|
|
if(mediaRecorder&&mediaRecorder.state==="recording"){
|
|
mediaRecorder.stop();
|
|
}
|
|
}catch(e){
|
|
console.error("Failed to stop voice session:",e);
|
|
}
|
|
}
|
|
|
|
async function connectToVoiceRoom(t){
|
|
try{
|
|
const r=new LiveKitClient.Room(),p="ws:",u=`${p}//localhost:8080/voice`;
|
|
await r.connect(u,t);
|
|
|
|
voiceRoom=r;
|
|
r.on("dataReceived",d=>{
|
|
const dc=new TextDecoder(),m=dc.decode(d);
|
|
try{
|
|
const j=JSON.parse(m);
|
|
if(j.type==="voice_response"){
|
|
addMessage("assistant",j.text);
|
|
}
|
|
}catch(e){
|
|
console.log("Voice data:",m);
|
|
}
|
|
});
|
|
const l=await LiveKitClient.createLocalTracks({audio:true,video:false});
|
|
for(const k of l){
|
|
await r.localParticipant.publishTrack(k);
|
|
}
|
|
}catch(e){
|
|
console.error("Failed to connect to voice room:",e);
|
|
showWarning("Falha na conexão de voz");
|
|
}
|
|
}
|
|
|
|
function startVoiceRecording(){
|
|
if(!navigator.mediaDevices){
|
|
console.log("Media devices not supported");
|
|
return;
|
|
}
|
|
navigator.mediaDevices.getUserMedia({audio:true}).then(s=>{
|
|
mediaRecorder=new MediaRecorder(s);
|
|
audioChunks=[];
|
|
mediaRecorder.ondataavailable=e=>{
|
|
audioChunks.push(e.data);
|
|
};
|
|
mediaRecorder.onstop=()=>{
|
|
const a=new Blob(audioChunks,{type:"audio/wav"});
|
|
simulateVoiceTranscription();
|
|
};
|
|
mediaRecorder.start();
|
|
setTimeout(()=>{
|
|
if(mediaRecorder&&mediaRecorder.state==="recording"){
|
|
mediaRecorder.stop();
|
|
setTimeout(()=>{
|
|
if(isVoiceMode){
|
|
startVoiceRecording();
|
|
}
|
|
},1000);
|
|
}
|
|
},5000);
|
|
}).catch(e=>{
|
|
console.error("Error accessing microphone:",e);
|
|
showWarning("Erro ao acessar microfone");
|
|
});
|
|
}
|
|
|
|
function simulateVoiceTranscription(){
|
|
const p=["Olá, como posso ajudá-lo hoje?","Entendo o que você está dizendo","Esse é um ponto interessante","Deixe-me pensar sobre isso","Posso ajudá-lo com isso","O que você gostaria de saber?","Isso parece ótimo","Estou ouvindo sua voz"],r=p[Math.floor(Math.random()*p.length)];
|
|
if(voiceRoom){
|
|
const m={type:"voice_input",content:r,timestamp:new Date().toISOString()};
|
|
voiceRoom.localParticipant.publishData(new TextEncoder().encode(JSON.stringify(m)),LiveKitClient.DataPacketKind.RELIABLE);
|
|
}
|
|
addMessage("voice",`🎤 ${r}`);
|
|
}
|
|
|
|
function scrollToBottom(){
|
|
messagesDiv.scrollTop=messagesDiv.scrollHeight;
|
|
isUserScrolling=false;
|
|
scrollToBottomBtn.classList.remove('visible');
|
|
}
|
|
|
|
window.addEventListener("load",initializeAuth);
|
|
window.addEventListener("focus",function(){
|
|
if(!ws||ws.readyState!==WebSocket.OPEN){
|
|
connectWebSocket();
|
|
}
|
|
});
|
|
</script>
|
|
</body>
|
|
</html>
|