botserver/web/html/index.html
Rodrigo Rodriguez (Pragmatismo) b5e1501454 feat: add trace logging, refactor bot streaming, add config fallback
- Added `trace!` logging in `bot_memory.rs` to record retrieved memory values for easier debugging.
- Refactored `BotOrchestrator` in `bot/mod.rs`:
  - Removed duplicate session save block and consolidated message persistence.
  - Replaced low‑level LLM streaming with a structured `UserMessage` and `stream_response` workflow, improving error handling and readability.
- Updated configuration loading in `config/mod.rs`:
  - Imported `get_default_bot` and enhanced `get_config` to fall back to the default bot configuration when the primary query fails.
  - Established a fresh DB connection for the fallback path to avoid borrowing issues.
2025-11-03 10:13:39 -03:00

2018 lines
No EOL
72 KiB
HTML

<!doctype html>
<html lang="pt-br">
<head>
<meta charset="utf-8" />
<title>General Bots</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/gsap.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/livekit-client/dist/livekit-client.umd.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<style>
@import url("https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;600&display=swap");
:root {
--primary: #ffffff;
--primary-glow: rgba(255, 255, 255, 0.2);
--secondary: #e0e0e0;
--accent: #ffffff;
--bg-dark: #000000;
--bg-card: rgba(20, 20, 20, 0.8);
--text-primary: #ffffff;
--text-secondary: #888888;
--border-color: rgba(255, 255, 255, 0.1);
}
[data-theme="light"] {
--primary: #000000;
--primary-glow: rgba(0, 0, 0, 0.1);
--secondary: #333333;
--accent: #000000;
--bg-dark: #ffffff;
--bg-card: rgba(250, 250, 250, 0.9);
--text-primary: #000000;
--text-secondary: #666666;
--border-color: rgba(0, 0, 0, 0.1);
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: "Space Grotesk", sans-serif;
background: var(--bg-dark);
color: var(--text-primary);
overflow: hidden;
position: relative;
transition: background 0.3s ease, color 0.3s ease;
}
body::before {
content: '';
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: radial-gradient(circle at 30% 40%, rgba(255, 255, 255, 0.03) 0%, transparent 50%);
pointer-events: none;
z-index: 0;
}
[data-theme="light"] body::before {
background: radial-gradient(circle at 30% 40%, rgba(0, 0, 0, 0.02) 0%, transparent 50%);
}
.grain-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 400 400' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noiseFilter'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noiseFilter)' opacity='0.03'/%3E%3C/svg%3E");
pointer-events: none;
z-index: 1;
opacity: 0.4;
}
[data-theme="light"] .grain-overlay {
opacity: 0.1;
}
.sidebar {
position: fixed;
left: -320px;
top: 0;
width: 320px;
height: 100vh;
background: var(--bg-card);
backdrop-filter: blur(40px);
border-right: 1px solid var(--border-color);
transition: left 0.4s cubic-bezier(0.4, 0, 0.2, 1);
z-index: 100;
overflow-y: auto;
padding: 24px;
box-shadow: 10px 0 50px rgba(0, 0, 0, 0.3);
}
[data-theme="light"] .sidebar {
box-shadow: 10px 0 30px rgba(0, 0, 0, 0.05);
}
.sidebar.open {
left: 0;
}
.sidebar-toggle {
position: fixed;
left: 24px;
top: 24px;
z-index: 101;
background: var(--bg-card);
border: 1px solid var(--border-color);
color: var(--primary);
padding: 12px;
border-radius: 12px;
cursor: pointer;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
font-size: 20px;
width: 48px;
height: 48px;
display: flex;
align-items: center;
justify-content: center;
backdrop-filter: blur(20px);
}
.sidebar-toggle:hover {
background: var(--text-primary);
color: var(--bg-dark);
transform: scale(1.05);
}
.new-chat {
width: 100%;
padding: 16px;
background: var(--bg-card);
border: 1px solid var(--border-color);
border-radius: 12px;
color: var(--primary);
cursor: pointer;
margin-top: 60px;
margin-bottom: 16px;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
font-weight: 600;
font-size: 15px;
backdrop-filter: blur(10px);
}
.new-chat:hover {
background: var(--text-primary);
color: var(--bg-dark);
transform: translateY(-2px);
}
.voice-toggle {
width: 100%;
padding: 16px;
background: var(--bg-card);
border: 1px solid var(--border-color);
border-radius: 12px;
color: var(--text-secondary);
cursor: pointer;
margin-bottom: 16px;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
font-weight: 600;
font-size: 15px;
backdrop-filter: blur(10px);
}
.voice-toggle:hover {
background: var(--text-primary);
color: var(--bg-dark);
transform: translateY(-2px);
}
.voice-toggle.recording {
background: rgba(100, 100, 100, 0.2);
border-color: var(--text-secondary);
animation: recordingPulse 2s infinite;
}
@keyframes recordingPulse {
0%, 100% { box-shadow: 0 0 0 0 rgba(255, 255, 255, 0.4); }
50% { box-shadow: 0 0 20px 10px rgba(255, 255, 255, 0); }
}
.history-item {
padding: 12px;
margin-bottom: 8px;
background: var(--bg-card);
border: 1px solid var(--border-color);
border-radius: 10px;
cursor: pointer;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
font-size: 14px;
word-wrap: break-word;
}
.history-item:hover {
background: var(--text-primary);
color: var(--bg-dark);
transform: translateX(4px);
}
.main {
margin-left: 0;
width: 100%;
height: 100vh;
display: flex;
flex-direction: column;
transition: margin-left 0.4s cubic-bezier(0.4, 0, 0.2, 1);
position: relative;
z-index: 2;
}
.sidebar.open ~ .main {
margin-left: 320px;
width: calc(100% - 320px);
}
header {
background: var(--bg-card);
backdrop-filter: blur(40px) saturate(180%);
border-bottom: 1px solid var(--border-color);
padding: 20px 40px 20px 90px;
display: flex;
align-items: center;
justify-content: space-between;
}
.logo {
display: flex;
align-items: center;
gap: 16px;
}
.logo-icon {
width: 48px;
height: 48px;
border-radius: 14px;
display: flex;
align-items: center;
justify-content: center;
font-size: 28px;
font-weight: 700;
color: var(--bg-dark);
box-shadow: 0 8px 32px var(--primary-glow);
position: relative;
overflow: hidden;
}
.logo-icon img {
width: 100%;
height: 100%;
object-fit: contain;
padding: 6px;
filter: invert(1);
}
[data-theme="light"] .logo-icon img {
}
.logo-text {
font-size: 28px;
font-weight: 700;
color: var(--text-primary);
}
#messages {
flex: 1;
overflow-y: auto;
padding: 40px 20px;
max-width: 1000px;
margin: 0 auto;
width: 100%;
position: relative;
}
#emptyState {
text-align: center;
padding-top: 120px;
animation: fadeInUp 0.8s ease;
}
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(30px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.empty-icon {
width: 100px;
height: 100px;
background: var(--primary);
border-radius: 28px;
display: inline-flex;
align-items: center;
justify-content: center;
font-size: 56px;
font-weight: 700;
color: var(--bg-dark);
margin-bottom: 24px;
position: relative;
animation: floatIcon 3s ease-in-out infinite;
overflow: hidden;
}
.empty-icon img {
width: 100%;
height: 100%;
object-fit: contain;
padding: 12px;
filter: invert(1);
}
[data-theme="light"] .empty-icon img {
filter: invert(0);
}
@keyframes floatIcon {
0%, 100% { transform: translateY(0px); }
50% { transform: translateY(-10px); }
}
.empty-title {
font-size: 40px;
font-weight: 700;
color: var(--text-primary);
margin-bottom: 12px;
}
.empty-subtitle {
color: var(--text-secondary);
font-size: 18px;
font-weight: 300;
}
.message-container {
margin-bottom: 24px;
opacity: 0;
transform: translateY(20px);
}
.user-message {
display: flex;
justify-content: flex-end;
}
.user-message-content {
background: var(--bg-card);
border: 1px solid var(--border-color);
border-radius: 20px 20px 4px 20px;
padding: 16px 20px;
max-width: 70%;
color: var(--text-primary);
font-weight: 400;
backdrop-filter: blur(10px);
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);
}
[data-theme="light"] .user-message-content {
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.05);
}
.assistant-message {
display: flex;
gap: 16px;
align-items: flex-start;
}
.assistant-avatar {
width: 44px;
height: 44px;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
font-size: 22px;
font-weight: 700;
color: var(--bg-dark);
flex-shrink: 0;
box-shadow: 0 8px 24px var(--primary-glow);
position: relative;
overflow: hidden;
}
.assistant-avatar img {
width: 100%;
height: 100%;
object-fit: contain;
padding: 6px;
filter: invert(1);
}
[data-theme="light"] .assistant-avatar img {
filter: invert(1);
}
.assistant-message-content {
background: var(--bg-card);
border: 1px solid var(--border-color);
border-radius: 20px 20px 20px 4px;
padding: 16px 20px;
flex: 1;
color: var(--text-primary);
line-height: 1.7;
backdrop-filter: blur(10px);
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);
}
[data-theme="light"] .assistant-message-content {
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.05);
}
.thinking-indicator {
display: flex;
gap: 16px;
align-items: center;
padding: 16px 20px;
color: var(--text-secondary);
}
.typing-dots {
display: flex;
gap: 8px;
}
.typing-dot {
width: 10px;
height: 10px;
background: var(--text-secondary);
border-radius: 50%;
animation: bounce 1.4s infinite ease-in-out;
}
.typing-dot:nth-child(1) { animation-delay: -0.32s; }
.typing-dot:nth-child(2) { animation-delay: -0.16s; }
@keyframes bounce {
0%, 80%, 100% { transform: scale(0); opacity: 0.5; }
40% { transform: scale(1); opacity: 1; }
}
footer {
background: var(--bg-card);
backdrop-filter: blur(40px) saturate(180%);
border-top: 1px solid var(--border-color);
padding: 24px 40px;
position: relative;
}
.suggestions-container {
display: flex;
flex-wrap: wrap;
gap: 6px;
margin-bottom: 16px;
justify-content: center;
}
.suggestion-button {
padding: 6px 12px;
background: var(--bg-card);
border: 1px solid var(--border-color);
color: var(--text-secondary);
border-radius: 16px;
cursor: pointer;
font-size: 11px;
font-weight: 500;
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
backdrop-filter: blur(10px);
}
.suggestion-button:hover {
background: var(--text-primary);
color: var(--bg-dark);
border-color: var(--text-primary);
transform: translateY(-1px);
}
.input-container {
display: flex;
gap: 12px;
max-width: 1000px;
margin: 0 auto;
position: relative;
}
#messageInput {
flex: 1;
background: var(--bg-card);
border: 1px solid var(--border-color);
border-radius: 16px;
padding: 16px 24px;
color: var(--text-primary);
font-size: 15px;
font-family: "Space Grotesk", sans-serif;
outline: none;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
backdrop-filter: blur(10px);
}
#messageInput:focus {
border-color: var(--text-primary);
box-shadow: 0 0 0 3px var(--primary-glow);
}
#messageInput::placeholder {
color: var(--text-secondary);
}
#sendBtn {
background: var(--primary);
border: none;
border-radius: 16px;
padding: 16px 32px;
color: var(--bg-dark);
font-weight: 600;
font-size: 15px;
cursor: pointer;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
box-shadow: 0 8px 24px var(--primary-glow);
position: relative;
overflow: hidden;
}
#sendBtn:hover {
transform: translateY(-2px);
box-shadow: 0 12px 36px var(--primary-glow);
}
#newChatBtn {
background: var(--bg-card);
border: 1px solid var(--border-color);
border-radius: 12px;
padding: 12px 24px;
color: var(--text-primary);
font-weight: 600;
font-size: 14px;
cursor: pointer;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
backdrop-filter: blur(10px);
}
#newChatBtn:hover {
background: var(--text-primary);
color: var(--bg-dark);
transform: translateY(-2px);
}
.voice-status {
background: var(--bg-card);
border-bottom: 1px solid var(--border-color);
padding: 16px 20px;
text-align: center;
color: var(--text-secondary);
font-weight: 600;
backdrop-filter: blur(20px);
}
.warning-message {
background: var(--bg-card);
border: 1px solid var(--border-color);
border-radius: 12px;
padding: 16px 20px;
margin-bottom: 20px;
color: var(--text-secondary);
backdrop-filter: blur(10px);
}
.connection-status {
position: fixed;
top: 24px;
right: 24px;
width: 12px;
height: 12px;
border-radius: 50%;
z-index: 1000;
transition: all 0.3s ease;
}
.connection-status.connecting {
background-color: var(--text-secondary);
animation: connectingPulse 1.5s infinite;
}
.connection-status.connected {
background-color: var(--primary);
box-shadow: 0 0 20px var(--primary-glow);
animation: connectedPulse 2s infinite;
}
.connection-status.disconnected {
background-color: var(--text-secondary);
opacity: 0.5;
}
@keyframes connectingPulse {
0%, 100% { opacity: 0.6; transform: scale(0.8); }
50% { opacity: 1; transform: scale(1.2); }
}
@keyframes connectedPulse {
0%, 100% { opacity: 0.8; transform: scale(1); }
50% { opacity: 1; transform: scale(1.3); }
}
.markdown-content h1,
.markdown-content h2,
.markdown-content h3 {
margin-top: 24px;
margin-bottom: 12px;
font-weight: 600;
color: var(--text-primary);
}
.markdown-content h1 { font-size: 28px; }
.markdown-content h2 { font-size: 24px; }
.markdown-content h3 { font-size: 20px; }
.markdown-content p {
margin-bottom: 16px;
line-height: 1.8;
}
.markdown-content ul,
.markdown-content ol {
margin-bottom: 16px;
padding-left: 28px;
}
.markdown-content li {
margin-bottom: 8px;
}
.markdown-content code {
background: var(--bg-card);
padding: 3px 8px;
border-radius: 6px;
font-family: "JetBrains Mono", monospace;
font-size: 14px;
color: var(--text-primary);
border: 1px solid var(--border-color);
}
.markdown-content pre {
background: var(--bg-card);
border: 1px solid var(--border-color);
border-radius: 12px;
padding: 20px;
overflow-x: auto;
margin-bottom: 16px;
backdrop-filter: blur(10px);
}
.markdown-content pre code {
background: none;
padding: 0;
border: none;
color: var(--text-primary);
}
.markdown-content table {
width: 100%;
border-collapse: collapse;
margin-bottom: 16px;
}
.markdown-content table th,
.markdown-content table td {
border: 1px solid var(--border-color);
padding: 12px;
text-align: left;
}
.markdown-content table th {
background: var(--bg-card);
font-weight: 600;
color: var(--text-primary);
}
.markdown-content blockquote {
border-left: 3px solid var(--text-primary);
padding-left: 20px;
margin: 16px 0;
color: var(--text-secondary);
font-style: italic;
}
.markdown-content strong {
font-weight: 600;
color: var(--text-primary);
}
.markdown-content em {
font-style: italic;
color: var(--text-secondary);
}
.markdown-content a {
color: var(--text-primary);
text-decoration: underline;
transition: all 0.3s;
}
.markdown-content a:hover {
opacity: 0.7;
}
::-webkit-scrollbar {
width: 10px;
}
::-webkit-scrollbar-track {
background: var(--bg-card);
}
::-webkit-scrollbar-thumb {
background: var(--border-color);
border-radius: 5px;
}
::-webkit-scrollbar-thumb:hover {
background: var(--text-secondary);
}
.scroll-to-bottom {
position: absolute;
bottom: 24px;
right: 24px;
width: 48px;
height: 48px;
background: var(--primary);
border: none;
border-radius: 50%;
color: var(--bg-dark);
font-size: 20px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 8px 24px var(--primary-glow);
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
z-index: 10;
}
.scroll-to-bottom:hover {
transform: scale(1.1);
}
.continue-button {
display: inline-block;
background: var(--bg-card);
border: 1px solid var(--border-color);
border-radius: 10px;
padding: 10px 20px;
color: var(--text-primary);
font-weight: 600;
cursor: pointer;
margin-top: 12px;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
font-size: 14px;
}
.continue-button:hover {
background: var(--text-primary);
color: var(--bg-dark);
transform: translateY(-2px);
}
.context-indicator {
position: fixed;
bottom: 110px;
right: 24px;
width: 140px;
background: var(--bg-card);
backdrop-filter: blur(20px);
border: 1px solid var(--border-color);
border-radius: 12px;
padding: 12px;
font-size: 12px;
text-align: center;
z-index: 100;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
}
[data-theme="light"] .context-indicator {
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.05);
}
.context-progress {
height: 6px;
background: var(--bg-card);
border-radius: 3px;
margin-top: 8px;
overflow: hidden;
}
.context-progress-bar {
height: 100%;
background: var(--text-primary);
border-radius: 3px;
transition: width 0.3s ease, background-color 0.3s ease;
}
.context-progress-bar.warning {
background: var(--text-secondary);
}
.context-progress-bar.danger {
background: var(--text-secondary);
}
.theme-toggle {
position: fixed;
top: 24px;
right: 160px;
z-index: 101;
background: var(--bg-card);
border: 1px solid var(--border-color);
color: var(--primary);
padding: 12px;
border-radius: 12px;
cursor: pointer;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
font-size: 20px;
width: 48px;
height: 48px;
display: flex;
align-items: center;
justify-content: center;
backdrop-filter: blur(20px);
}
.theme-toggle:hover {
background: var(--text-primary);
color: var(--bg-dark);
transform: scale(1.05);
}
@media (max-width: 768px) {
.sidebar {
width: 100%;
left: -100%;
}
.sidebar.open ~ .main {
margin-left: 0;
width: 100%;
}
header {
padding: 16px 20px 16px 70px;
}
.logo-text {
font-size: 22px;
}
.logo-icon {
width: 40px;
height: 40px;
font-size: 22px;
}
#newChatBtn {
padding: 10px 18px;
font-size: 13px;
}
.input-container {
padding: 0 10px;
}
#messageInput {
padding: 14px 20px;
}
#sendBtn {
padding: 14px 24px;
}
.user-message-content,
.assistant-message-content {
max-width: 85%;
}
.empty-title {
font-size: 32px;
}
.empty-icon {
width: 80px;
height: 80px;
font-size: 44px;
}
.scroll-to-bottom {
width: 44px;
height: 44px;
font-size: 18px;
}
.context-indicator {
bottom: 90px;
right: 20px;
width: 120px;
}
.theme-toggle {
top: 24px;
right: 60px;
width: 44px;
height: 44px;
}
}
@media (max-width: 480px) {
.sidebar-toggle {
left: 16px;
top: 16px;
width: 44px;
height: 44px;
}
header {
padding: 12px 16px 12px 60px;
}
.logo-text {
font-size: 18px;
}
#newChatBtn {
padding: 8px 14px;
font-size: 12px;
}
.user-message-content,
.assistant-message-content {
max-width: 90%;
}
.empty-title {
font-size: 28px;
}
.scroll-to-bottom {
width: 40px;
height: 40px;
font-size: 16px;
}
.theme-toggle {
top: 24px;
right: 60px;
width: 40px;
height: 40px;
}
}
</style>
</head>
<body>
<div class="grain-overlay"></div>
<div class="connection-status connecting" id="connectionStatus"></div>
<button class="sidebar-toggle" onclick="toggleSidebar()"></button>
<button class="theme-toggle" id="themeToggle" onclick="toggleTheme()">⚙️</button>
<div class="sidebar" id="sidebar">
<button class="new-chat" onclick="createNewSession()">
+ Novo Chat
</button>
<button
class="voice-toggle"
id="voiceToggle"
onclick="toggleVoiceMode()"
>
🎤 Modo Voz
</button>
<div class="history" id="history"></div>
</div>
<div class="main">
<header>
<div class="logo">
<div class="logo-icon">
<img src="https://pragmatismo.com.br/gb-logo.png" alt="General Bots Logo">
</div>
<h1 class="logo-text">General Bots</h1>
</div>
<button id="newChatBtn">Novo Chat</button>
</header>
<div class="voice-status" id="voiceStatus" style="display: none">
🎤 Ouvindo... Fale agora
</div>
<main id="messages">
<div id="emptyState">
<div class="empty-icon">
<img src="https://pragmatismo.com.br/gb-logo.png" alt="General Bots Logo">
</div>
<h2 class="empty-title">
Bem-vindo ao General Bots
</h2>
<p class="empty-subtitle">Seu assistente de IA avançado</p>
</div>
</main>
<footer>
<div class="input-container">
<input
id="messageInput"
type="text"
placeholder="Fale com General Bots..."
autofocus
/>
<button id="sendBtn">Enviar</button>
</div>
</footer>
</div>
<button
class="scroll-to-bottom"
id="scrollToBottom"
style="display: none"
>
</button>
<div
class="context-indicator"
id="contextIndicator"
style="display: none"
>
<div>Contexto</div>
<div id="contextPercentage">0%</div>
<div class="context-progress">
<div
class="context-progress-bar"
id="contextProgressBar"
style="width: 0%"
></div>
</div>
</div>
<script>
let ws = null;
let currentSessionId = null;
let currentUserId = null;
let currentBotId = "default_bot";
let isStreaming = false;
let voiceRoom = null;
let isVoiceMode = false;
let mediaRecorder = null;
let audioChunks = [];
let streamingMessageId = null;
let isThinking = false;
let currentStreamingContent = "";
let hasReceivedInitialMessage = false;
let reconnectAttempts = 0;
const maxReconnectAttempts = 5;
let reconnectTimeout = null;
let thinkingTimeout = null;
let lastMessageLength = 0;
let contextUsage = 0;
let isUserScrolling = false;
let autoScrollEnabled = true;
let currentTheme = 'dark';
let isContextChange = false;
const messagesDiv = document.getElementById("messages");
const input = document.getElementById("messageInput");
const sendBtn = document.getElementById("sendBtn");
const newChatBtn = document.getElementById("newChatBtn");
const connectionStatus = document.getElementById("connectionStatus");
const scrollToBottomBtn = document.getElementById("scrollToBottom");
const contextIndicator = document.getElementById("contextIndicator");
const contextPercentage = document.getElementById("contextPercentage");
const contextProgressBar = document.getElementById("contextProgressBar");
const themeToggle = document.getElementById("themeToggle");
marked.setOptions({
breaks: true,
gfm: true,
});
function toggleSidebar() {
document.getElementById("sidebar").classList.toggle("open");
}
function toggleTheme() {
const themeOptions = ['auto', 'dark', 'light'];
const savedTheme = localStorage.getItem('gb-theme') || 'auto';
const currentIndex = themeOptions.indexOf(savedTheme);
const nextTheme = themeOptions[(currentIndex + 1) % themeOptions.length];
localStorage.setItem('gb-theme', nextTheme);
if (nextTheme === 'auto') {
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
currentTheme = prefersDark ? 'dark' : 'light';
themeToggle.textContent = '⚙️';
} else {
currentTheme = nextTheme;
themeToggle.textContent = currentTheme === 'dark' ? '🌙' : '☀️';
}
document.documentElement.setAttribute('data-theme', currentTheme);
}
function updateConnectionStatus(status) {
connectionStatus.className = `connection-status ${status}`;
}
function getWebSocketUrl() {
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
const sessionId = currentSessionId || crypto.randomUUID();
const userId = currentUserId || crypto.randomUUID();
return `${protocol}//${window.location.host}/ws?session_id=${sessionId}&user_id=${userId}`;
}
window.addEventListener("load", function () {
// Auto-detect system theme preference
const savedTheme = localStorage.getItem('gb-theme');
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
if (savedTheme === 'auto' || !savedTheme) {
currentTheme = prefersDark ? 'dark' : 'light';
localStorage.setItem('gb-theme', 'auto');
} else {
currentTheme = savedTheme;
}
document.documentElement.setAttribute('data-theme', currentTheme);
themeToggle.textContent = savedTheme === 'auto' || !savedTheme ? '⚙️' : (currentTheme === 'dark' ? '🌙' : '☀️');
// Listen for system theme changes
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => {
if (localStorage.getItem('gb-theme') === 'auto') {
currentTheme = e.matches ? 'dark' : 'light';
document.documentElement.setAttribute('data-theme', currentTheme);
}
});
input.focus();
});
document.addEventListener("click", function (event) {
const sidebar = document.getElementById("sidebar");
const sidebarToggle = document.querySelector(".sidebar-toggle");
if (window.innerWidth <= 768 && sidebar.classList.contains("open") &&
!sidebar.contains(event.target) && !sidebarToggle.contains(event.target)) {
sidebar.classList.remove("open");
}
});
messagesDiv.addEventListener("scroll", function () {
const isAtBottom = messagesDiv.scrollHeight - messagesDiv.scrollTop <= messagesDiv.clientHeight + 100;
if (!isAtBottom) {
isUserScrolling = true;
showScrollToBottomButton();
} else {
isUserScrolling = false;
hideScrollToBottomButton();
}
});
function scrollToBottom() {
messagesDiv.scrollTop = messagesDiv.scrollHeight;
isUserScrolling = false;
hideScrollToBottomButton();
}
function showScrollToBottomButton() {
scrollToBottomBtn.style.display = "flex";
}
function hideScrollToBottomButton() {
scrollToBottomBtn.style.display = "none";
}
scrollToBottomBtn.addEventListener("click", scrollToBottom);
function updateContextUsage(usage) {
contextUsage = usage;
const percentage = Math.min(100, Math.round(usage * 100));
contextPercentage.textContent = `${percentage}%`;
contextProgressBar.style.width = `${percentage}%`;
if (percentage >= 90) {
contextProgressBar.className = "context-progress-bar danger";
} else if (percentage >= 70) {
contextProgressBar.className = "context-progress-bar warning";
} else {
contextProgressBar.className = "context-progress-bar";
}
if (percentage >= 50) {
//contextIndicator.style.display = "block";
} else {
contextIndicator.style.display = "none";
}
}
async function initializeAuth() {
try {
updateConnectionStatus("connecting");
const pathSegments = window.location.pathname.split('/').filter(s => s);
const botName = pathSegments.length > 0 ? pathSegments[0] : 'default';
const response = await fetch(`/api/auth?bot_name=${encodeURIComponent(botName)}`);
const authData = await response.json();
currentUserId = authData.user_id;
currentSessionId = authData.session_id;
connectWebSocket();
loadSessions();
} catch (error) {
console.error("Failed to initialize auth:", error);
updateConnectionStatus("disconnected");
setTimeout(initializeAuth, 3000);
}
}
async function loadSessions() {
try {
const response = await fetch("/api/sessions");
const sessions = await response.json();
const history = document.getElementById("history");
history.innerHTML = "";
} catch (error) {
console.error("Failed to load sessions:", error);
}
}
async function createNewSession() {
try {
const response = await fetch("/api/sessions", { method: "POST" });
const session = await response.json();
currentSessionId = session.session_id;
hasReceivedInitialMessage = false;
connectWebSocket();
loadSessions();
// Clear messages and show empty state
messagesDiv.innerHTML = `
<div id="emptyState">
<div class="empty-icon">
<img src="https://pragmatismo.com.br/gb-logo.png" alt="General Bots Logo">
</div>
<h2 class="empty-title">Bem-vindo ao General Bots</h2>
<p class="empty-subtitle">Seu assistente de IA avançado</p>
</div>
`;
// Clear suggestions
clearSuggestions();
// Reset context usage
updateContextUsage(0);
// Stop voice if active
if (isVoiceMode) {
await stopVoiceSession();
isVoiceMode = false;
const voiceToggle = document.getElementById("voiceToggle");
voiceToggle.textContent = "🎤 Modo Voz";
voiceToggle.classList.remove("recording");
const voiceStatus = document.getElementById("voiceStatus");
voiceStatus.style.display = "none";
}
if (window.innerWidth <= 768) {
document.getElementById("sidebar").classList.remove("open");
}
} catch (error) {
console.error("Failed to create session:", error);
}
}
function switchSession(sessionId) {
currentSessionId = sessionId;
hasReceivedInitialMessage = false;
loadSessionHistory(sessionId);
connectWebSocket();
if (isVoiceMode) {
startVoiceSession();
}
if (window.innerWidth <= 768) {
document.getElementById("sidebar").classList.remove("open");
}
}
async function loadSessionHistory(sessionId) {
try {
const response = await fetch("/api/sessions/" + sessionId);
const history = await response.json();
const messages = document.getElementById("messages");
messages.innerHTML = "";
if (history.length === 0) {
messages.innerHTML = `
<div id="emptyState">
<div class="empty-icon">
<img src="https://pragmatismo.com.br/gb-logo.png" alt="General Bots Logo">
</div>
<h2 class="empty-title">Bem-vindo ao General Bots</h2>
<p class="empty-subtitle">Seu assistente de IA avançado</p>
</div>
`;
updateContextUsage(0);
} else {
history.forEach(([role, content]) => {
addMessage(role, content, false);
});
updateContextUsage(history.length / 20);
}
} catch (error) {
console.error("Failed to load session history:", error);
}
}
function connectWebSocket() {
if (ws) {
ws.close();
}
clearTimeout(reconnectTimeout);
const wsUrl = getWebSocketUrl();
ws = new WebSocket(wsUrl);
ws.onmessage = function (event) {
const response = JSON.parse(event.data);
if (response.bot_id) {
currentBotId = response.bot_id;
}
if (response.message_type === 2) {
const eventData = JSON.parse(response.content);
handleEvent(eventData.event, eventData.data);
return;
}
// Check if this is a context change message and skip processing
if (response.message_type === 5) {
isContextChange = true;
return;
}
processMessageContent(response);
};
ws.onopen = function () {
console.log("Connected to WebSocket");
updateConnectionStatus("connected");
reconnectAttempts = 0;
hasReceivedInitialMessage = false;
};
ws.onclose = function (event) {
console.log("WebSocket disconnected:", event.code, event.reason);
updateConnectionStatus("disconnected");
if (isStreaming) {
showContinueButton();
}
if (reconnectAttempts < maxReconnectAttempts) {
reconnectAttempts++;
const delay = Math.min(1000 * reconnectAttempts, 10000);
console.log(`Reconnecting in ${delay}ms... (attempt ${reconnectAttempts})`);
reconnectTimeout = setTimeout(() => {
updateConnectionStatus("connecting");
connectWebSocket();
}, delay);
} else {
updateConnectionStatus("disconnected");
}
};
ws.onerror = function (error) {
console.error("WebSocket error:", error);
updateConnectionStatus("disconnected");
};
}
function processMessageContent(response) {
// Skip processing if this is a context change
if (isContextChange) {
isContextChange = false;
return;
}
const emptyState = document.getElementById("emptyState");
if (emptyState) {
emptyState.remove();
}
if (response.context_usage !== undefined) {
updateContextUsage(response.context_usage);
}
if (response.suggestions && response.suggestions.length > 0) {
handleSuggestions(response.suggestions);
}
if (response.is_complete) {
if (isStreaming) {
finalizeStreamingMessage();
isStreaming = false;
streamingMessageId = null;
currentStreamingContent = "";
} else {
addMessage("assistant", response.content, false);
}
} else {
if (!isStreaming) {
isStreaming = true;
streamingMessageId = "streaming-" + Date.now();
currentStreamingContent = response.content || "";
addMessage("assistant", currentStreamingContent, true, streamingMessageId);
} else {
currentStreamingContent += response.content || "";
updateStreamingMessage(currentStreamingContent);
}
}
}
function handleEvent(eventType, eventData) {
console.log("Event received:", eventType, eventData);
switch (eventType) {
case "thinking_start":
showThinkingIndicator();
break;
case "thinking_end":
hideThinkingIndicator();
break;
case "warn":
showWarning(eventData.message);
break;
case "context_usage":
updateContextUsage(eventData.usage);
break;
}
}
function showThinkingIndicator() {
if (isThinking) return;
const emptyState = document.getElementById("emptyState");
if (emptyState) emptyState.remove();
const thinkingDiv = document.createElement("div");
thinkingDiv.id = "thinking-indicator";
thinkingDiv.className = "message-container";
thinkingDiv.innerHTML = `
<div class="assistant-message">
<div class="assistant-avatar">
<img src="https://pragmatismo.com.br/gb-logo.png" alt="General Bots Logo">
</div>
<div class="thinking-indicator">
<div class="typing-dots">
<div class="typing-dot"></div>
<div class="typing-dot"></div>
<div class="typing-dot"></div>
</div>
<span>Pensando...</span>
</div>
</div>
`;
messagesDiv.appendChild(thinkingDiv);
gsap.to(thinkingDiv, {
opacity: 1,
y: 0,
duration: 0.4,
ease: "power2.out",
});
if (!isUserScrolling) {
scrollToBottom();
} else {
showScrollToBottomButton();
}
thinkingTimeout = setTimeout(() => {
if (isThinking) {
hideThinkingIndicator();
showWarning("O servidor pode estar ocupado. A resposta está demorando demais.");
}
}, 60000);
isThinking = true;
}
function hideThinkingIndicator() {
if (!isThinking) return;
const thinkingDiv = document.getElementById("thinking-indicator");
if (thinkingDiv) {
gsap.to(thinkingDiv, {
opacity: 0,
duration: 0.2,
onComplete: () => {
if (thinkingDiv.parentNode) {
thinkingDiv.remove();
}
},
});
}
if (thinkingTimeout) {
clearTimeout(thinkingTimeout);
thinkingTimeout = null;
}
isThinking = false;
}
function showWarning(message) {
const warningDiv = document.createElement("div");
warningDiv.className = "warning-message";
warningDiv.innerHTML = `⚠️ ${message}`;
messagesDiv.appendChild(warningDiv);
gsap.from(warningDiv, {
opacity: 0,
y: 20,
duration: 0.4,
ease: "power2.out",
});
if (!isUserScrolling) {
scrollToBottom();
} else {
showScrollToBottomButton();
}
setTimeout(() => {
if (warningDiv.parentNode) {
gsap.to(warningDiv, {
opacity: 0,
duration: 0.3,
onComplete: () => warningDiv.remove(),
});
}
}, 5000);
}
function showContinueButton() {
const continueDiv = document.createElement("div");
continueDiv.className = "message-container";
continueDiv.innerHTML = `
<div class="assistant-message">
<div class="assistant-avatar">
<img src="https://pragmatismo.com.br/gb-logo.png" alt="General Bots Logo">
</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(continueDiv);
gsap.to(continueDiv, {
opacity: 1,
y: 0,
duration: 0.5,
ease: "power2.out",
});
if (!isUserScrolling) {
scrollToBottom();
} else {
showScrollToBottomButton();
}
}
function continueInterruptedResponse() {
if (!ws || ws.readyState !== WebSocket.OPEN) {
connectWebSocket();
}
if (ws && ws.readyState === WebSocket.OPEN) {
const continueData = {
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(continueData));
}
const continueButtons = document.querySelectorAll(".continue-button");
continueButtons.forEach((button) => {
button.parentElement.parentElement.parentElement.remove();
});
}
function addMessage(role, content, streaming = false, msgId = null) {
const emptyState = document.getElementById("emptyState");
if (emptyState) {
gsap.to(emptyState, {
opacity: 0,
y: -20,
duration: 0.3,
onComplete: () => emptyState.remove(),
});
}
const msg = document.createElement("div");
msg.className = "message-container";
if (role === "user") {
msg.innerHTML = `
<div class="user-message">
<div class="user-message-content">${escapeHtml(content)}</div>
</div>
`;
updateContextUsage(contextUsage + 0.05);
} else if (role === "assistant") {
msg.innerHTML = `
<div class="assistant-message">
<div class="assistant-avatar">
<img src="https://pragmatismo.com.br/gb-logo.png" alt="General Bots Logo">
</div>
<div class="assistant-message-content markdown-content" id="${msgId || ""}">
${streaming ? "" : marked.parse(content)}
</div>
</div>
`;
updateContextUsage(contextUsage + 0.03);
} else if (role === "voice") {
msg.innerHTML = `
<div class="assistant-message">
<div class="assistant-avatar">🎤</div>
<div class="assistant-message-content">${content}</div>
</div>
`;
} else {
msg.innerHTML = `
<div class="assistant-message">
<div class="assistant-avatar">
<img src="https://pragmatismo.com.br/gb-logo.png" alt="General Bots Logo">
</div>
<div class="assistant-message-content">${content}</div>
</div>
`;
}
messagesDiv.appendChild(msg);
gsap.to(msg, {
opacity: 1,
y: 0,
duration: 0.5,
ease: "power2.out",
});
if (!isUserScrolling) {
scrollToBottom();
} else {
showScrollToBottomButton();
}
}
function updateStreamingMessage(content) {
const msgElement = document.getElementById(streamingMessageId);
if (msgElement) {
msgElement.innerHTML = marked.parse(content);
if (!isUserScrolling) {
scrollToBottom();
} else {
showScrollToBottomButton();
}
}
}
function finalizeStreamingMessage() {
const msgElement = document.getElementById(streamingMessageId);
if (msgElement) {
msgElement.innerHTML = marked.parse(currentStreamingContent);
msgElement.removeAttribute("id");
if (!isUserScrolling) {
scrollToBottom();
} else {
showScrollToBottomButton();
}
}
}
function escapeHtml(text) {
const div = document.createElement("div");
div.textContent = text;
return div.innerHTML;
}
function clearSuggestions() {
const footer = document.querySelector('footer');
const container = footer.querySelector('.suggestions-container');
if (container) {
container.innerHTML = '';
}
}
function handleSuggestions(suggestions) {
const footer = document.querySelector('footer');
let container = footer.querySelector('.suggestions-container');
if (!container) {
container = document.createElement('div');
container.className = 'suggestions-container';
footer.insertBefore(container, footer.firstChild);
}
// Clear existing suggestions before adding new ones
container.innerHTML = '';
suggestions.forEach(s => {
const btn = document.createElement('button');
btn.textContent = s.text;
btn.className = 'suggestion-button';
btn.onclick = () => {
setContext(s.context);
input.value = '';
};
container.appendChild(btn);
});
}
let pendingContextChange = null;
async function setContext(context) {
try {
const buttonText = event?.target?.textContent || context;
addMessage("user", buttonText);
const input = document.getElementById('messageInput');
if (input) {
input.value = '';
}
if (ws && ws.readyState === WebSocket.OPEN) {
pendingContextChange = new Promise((resolve) => {
const handler = (event) => {
const response = JSON.parse(event.data);
if (response.message_type === 5 &&
response.context_name === context) {
ws.removeEventListener('message', handler);
resolve();
}
};
ws.addEventListener('message', handler);
const suggestionEvent = {
bot_id: currentBotId,
user_id: currentUserId,
session_id: currentSessionId,
channel: "web",
content: buttonText,
message_type: 4,
is_suggestion: true,
context_name: context,
timestamp: new Date().toISOString()
};
ws.send(JSON.stringify(suggestionEvent));
});
await pendingContextChange;
const contextIndicator = document.getElementById('contextIndicator');
if (contextIndicator) {
//contextIndicator.style.display = 'block';
document.getElementById('contextPercentage').textContent = context;
}
} 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 message = input.value.trim();
if (!message || !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", message);
const messageData = {
bot_id: currentBotId,
user_id: currentUserId,
session_id: currentSessionId,
channel: "web",
content: message,
message_type: 1,
media_url: null,
timestamp: new Date().toISOString()
};
ws.send(JSON.stringify(messageData));
input.value = "";
input.focus();
}
sendBtn.onclick = sendMessage;
input.addEventListener("keypress", (e) => {
if (e.key === "Enter") sendMessage();
});
newChatBtn.onclick = () => createNewSession();
async function toggleVoiceMode() {
isVoiceMode = !isVoiceMode;
const voiceToggle = document.getElementById("voiceToggle");
const voiceStatus = document.getElementById("voiceStatus");
if (isVoiceMode) {
voiceToggle.textContent = "🔴 Parar Voz";
voiceToggle.classList.add("recording");
voiceStatus.style.display = "block";
await startVoiceSession();
} else {
voiceToggle.textContent = "🎤 Modo Voz";
voiceToggle.classList.remove("recording");
voiceStatus.style.display = "none";
await stopVoiceSession();
}
if (window.innerWidth <= 768) {
document.getElementById("sidebar").classList.remove("open");
}
}
async function startVoiceSession() {
if (!currentSessionId) return;
try {
const response = await fetch("/api/voice/start", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
session_id: currentSessionId,
user_id: currentUserId,
}),
});
const data = await response.json();
if (data.token) {
await connectToVoiceRoom(data.token);
startVoiceRecording();
}
} catch (error) {
console.error("Failed to start voice session:", error);
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 (error) {
console.error("Failed to stop voice session:", error);
}
}
async function connectToVoiceRoom(token) {
try {
const room = new LiveKitClient.Room();
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
const voiceUrl = `${protocol}//${window.location.host}/voice`;
await room.connect(voiceUrl, token);
voiceRoom = room;
room.on("dataReceived", (data) => {
const decoder = new TextDecoder();
const message = decoder.decode(data);
try {
const parsed = JSON.parse(message);
if (parsed.type === "voice_response") {
addMessage("assistant", parsed.text);
}
} catch (e) {
console.log("Voice data:", message);
}
});
const localTracks = await LiveKitClient.createLocalTracks({
audio: true,
video: false,
});
for (const track of localTracks) {
await room.localParticipant.publishTrack(track);
}
} catch (error) {
console.error("Failed to connect to voice room:", error);
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((stream) => {
mediaRecorder = new MediaRecorder(stream);
audioChunks = [];
mediaRecorder.ondataavailable = (event) => {
audioChunks.push(event.data);
};
mediaRecorder.onstop = () => {
const audioBlob = new Blob(audioChunks, { type: "audio/wav" });
simulateVoiceTranscription();
};
mediaRecorder.start();
setTimeout(() => {
if (mediaRecorder && mediaRecorder.state === "recording") {
mediaRecorder.stop();
setTimeout(() => {
if (isVoiceMode) {
startVoiceRecording();
}
}, 1000);
}
}, 5000);
})
.catch((error) => {
console.error("Error accessing microphone:", error);
showWarning("Erro ao acessar microfone");
});
}
function simulateVoiceTranscription() {
const phrases = [
"Olá, como posso ajudá-lo hoje?",
"Entendo o que você está dizendo",
"Esse é um ponto interessante",
"Deixe-me pensar sobre isso",
"Posso ajudá-lo com isso",
"O que você gostaria de saber?",
"Isso parece ótimo",
"Estou ouvindo sua voz",
];
const randomPhrase = phrases[Math.floor(Math.random() * phrases.length)];
if (voiceRoom) {
const message = {
type: "voice_input",
content: randomPhrase,
timestamp: new Date().toISOString(),
};
voiceRoom.localParticipant.publishData(
new TextEncoder().encode(JSON.stringify(message)),
LiveKitClient.DataPacketKind.RELIABLE,
);
}
addMessage("voice", `🎤 ${randomPhrase}`);
}
window.addEventListener("load", initializeAuth);
window.addEventListener("focus", function () {
if (!ws || ws.readyState !== WebSocket.OPEN) {
connectWebSocket();
}
});
</script>
</body>
</html>