Added new configuration options for theme colors (green, yellow) and a custom logo URL to enhance branding and visual customization in announcement templates.
1218 lines
No EOL
28 KiB
HTML
1218 lines
No EOL
28 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');
|
|
}
|
|
[data-theme="dark"]{
|
|
--bg:#727171;
|
|
--fg:#ffffff;
|
|
--border:#a3a0a0;
|
|
--accent:#ffffff;
|
|
--glass:rgba(255,255,255,0.02);
|
|
--shadow:rgba(0,0,0,0.3);
|
|
}
|
|
*{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;
|
|
}
|
|
.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);
|
|
}
|
|
[data-theme="dark"] .float-logo{
|
|
}
|
|
.float-logo:hover{
|
|
transform:scale(1.1);
|
|
}
|
|
.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);
|
|
}
|
|
.menu-button:hover{
|
|
transform:scale(1.1);
|
|
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;
|
|
}
|
|
.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);
|
|
}
|
|
.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);
|
|
}
|
|
#messages{
|
|
flex:1;
|
|
overflow-y:auto;
|
|
padding:20px 20px 140px;
|
|
max-width:680px;
|
|
margin:0 auto;
|
|
width:100%;
|
|
}
|
|
.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:10px 16px;
|
|
max-width:80%;
|
|
font-size:14px;
|
|
line-height:1.5;
|
|
}
|
|
.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;
|
|
}
|
|
.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;
|
|
}
|
|
.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:4px 10px;
|
|
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.02);
|
|
}
|
|
.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);
|
|
}
|
|
#messageInput:focus{
|
|
border-color:var(--accent);
|
|
}
|
|
#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);
|
|
}
|
|
#sendBtn:active,#voiceBtn:active{
|
|
transform:scale(0.95);
|
|
}
|
|
#voiceBtn.recording{
|
|
animation:pulse 1.5s infinite;
|
|
}
|
|
@keyframes pulse{
|
|
0%,100%{opacity:1}
|
|
50%{opacity:0.6}
|
|
}
|
|
.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);
|
|
}
|
|
.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;
|
|
}
|
|
.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>
|
|
</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});
|
|
|
|
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();
|
|
});
|
|
|
|
floatLogo.addEventListener('click',toggleSidebar);
|
|
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}%`;
|
|
if(p>=50){
|
|
contextIndicator.classList.add('visible');
|
|
}else{
|
|
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=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}`;
|
|
}
|
|
|
|
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="";
|
|
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("/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("/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("/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}`);
|
|
}
|
|
|
|
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> |