- Added LLM server readiness check in AutomationService before starting tasks - Renamed `user` parameter to `user_session` in execute_talk for clarity - Updated BotResponse fields to use user_session data instead of hardcoded values - Improved Redis key generation in execute_talk to use user_session fields - Removed commented Redis code in set_current_context_keyword The changes ensure proper initialization of automation tasks by checking LLM server availability first, and improve code clarity by using more descriptive variable names for user session data.
1983 lines
No EOL
70 KiB
HTML
1983 lines
No EOL
70 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;
|
|
background: var(--primary);
|
|
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 {
|
|
filter: invert(0);
|
|
}
|
|
|
|
.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;
|
|
background: var(--primary);
|
|
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(0);
|
|
}
|
|
|
|
.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: 60px;
|
|
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: 16px;
|
|
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: 16px;
|
|
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';
|
|
|
|
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();
|
|
document.getElementById("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);
|
|
if (isVoiceMode) {
|
|
await startVoiceSession();
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
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) {
|
|
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 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);
|
|
}
|
|
|
|
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> |