The ui_server proxies WebSocket connections. It was accepting the client's WebSocket connection (ws.onopen triggered on the client), but if it couldn't connect to the backend (or if the backend disconnected), it would drop the client connection right away (ws.onclose triggered).
The issue was that reconnectAttempts was being reset to 0 inside the ws.onopen handler. Because the connection was briefly succeeding before failing, the reconnectAttempts counter was resetting to 0 on every attempt, completely circumventing the exponential backoff mechanism and causing a tight reconnection loop.
Modified the WebSocket logic across all relevant UI components to delay resetting reconnectAttempts = 0. Instead of resetting immediately upon the TCP socket opening, it now safely waits until a valid JSON payload {"type": "connected"} is successfully received from the backend.
1792 lines
No EOL
57 KiB
HTML
1792 lines
No EOL
57 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>
|
|
// BotServer URL - configurable via window.BOTSERVER_URL or defaults to same origin port 9000
|
|
const BOTSERVER_URL =
|
|
window.BOTSERVER_URL || "http://localhost:9000";
|
|
const BOTSERVER_WS_URL = BOTSERVER_URL.replace(
|
|
"https://",
|
|
"wss://",
|
|
).replace("http://", "ws://");
|
|
|
|
// Message Type Constants inline since we don't have a /shared route
|
|
const MessageType = {
|
|
EXTERNAL: 0,
|
|
USER: 1,
|
|
BOT_RESPONSE: 2,
|
|
CONTINUE: 3,
|
|
SUGGESTION: 4,
|
|
CONTEXT_CHANGE: 5,
|
|
};
|
|
</script>
|
|
<!-- Local Libraries (no external CDN dependencies) -->
|
|
<script src="js/vendor/gsap.min.js"></script>
|
|
<script src="js/vendor/livekit-client.umd.min.js"></script>
|
|
<script src="js/vendor/marked.min.js"></script>
|
|
<style>
|
|
/* Using system fonts - no external font loading */
|
|
:root {
|
|
--bg: #ffffff;
|
|
--fg: #000000;
|
|
--border: #e0e0e0;
|
|
--accent: #0066ff;
|
|
--glass: rgba(0, 0, 0, 0.02);
|
|
--shadow: rgba(0, 0, 0, 0.05);
|
|
--logo-url: url("https://pragmatismo.com.br/icons/general-bots.svg");
|
|
--gradient-1: linear-gradient(135deg,
|
|
rgba(0, 102, 255, 0.05) 0%,
|
|
rgba(0, 102, 255, 0) 100%);
|
|
--gradient-2: linear-gradient(45deg,
|
|
rgba(0, 0, 0, 0.02) 0%,
|
|
rgba(0, 0, 0, 0) 100%);
|
|
}
|
|
|
|
[data-theme="dark"] {
|
|
--bg: #1a1a1a;
|
|
--fg: #ffffff;
|
|
--border: #333333;
|
|
--accent: #ffffff;
|
|
--glass: rgba(255, 255, 255, 0.05);
|
|
--shadow: rgba(0, 0, 0, 0.5);
|
|
--gradient-1: linear-gradient(135deg,
|
|
rgba(255, 255, 255, 0.08) 0%,
|
|
rgba(255, 255, 255, 0) 100%);
|
|
--gradient-2: linear-gradient(45deg,
|
|
rgba(255, 255, 255, 0.03) 0%,
|
|
rgba(255, 255, 255, 0) 100%);
|
|
}
|
|
|
|
* {
|
|
margin: 0;
|
|
padding: 0;
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
body {
|
|
font-family: "Inter", sans-serif;
|
|
background: var(--bg);
|
|
color: var(--fg);
|
|
overflow: hidden;
|
|
transition:
|
|
background 0.3s,
|
|
color 0.3s;
|
|
display: flex;
|
|
flex-direction: column;
|
|
height: 100vh;
|
|
position: relative;
|
|
}
|
|
|
|
body::before {
|
|
content: "";
|
|
position: fixed;
|
|
inset: 0;
|
|
background: var(--gradient-1);
|
|
pointer-events: none;
|
|
z-index: 0;
|
|
}
|
|
|
|
.float-menu {
|
|
position: fixed;
|
|
left: 20px;
|
|
top: 20px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 8px;
|
|
z-index: 1000;
|
|
}
|
|
|
|
.float-logo {
|
|
width: 40px;
|
|
height: 40px;
|
|
background: var(--logo-url) center/contain no-repeat;
|
|
filter: var(--logo-filter, none);
|
|
border-radius: 50%;
|
|
cursor: pointer;
|
|
transition: all 0.3s;
|
|
border: 1px solid var(--border);
|
|
backdrop-filter: blur(10px);
|
|
}
|
|
|
|
[data-theme="dark"] .float-logo {}
|
|
|
|
.float-logo:hover {
|
|
transform: scale(1.1) rotate(5deg);
|
|
}
|
|
|
|
.menu-button {
|
|
width: 40px;
|
|
height: 40px;
|
|
border-radius: 50%;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
cursor: pointer;
|
|
transition: all 0.3s;
|
|
background: var(--bg);
|
|
border: 1px solid var(--border);
|
|
font-size: 16px;
|
|
color: var(--fg);
|
|
backdrop-filter: blur(10px);
|
|
}
|
|
|
|
.menu-button:hover {
|
|
transform: scale(1.1) rotate(-5deg);
|
|
background: var(--fg);
|
|
color: var(--bg);
|
|
}
|
|
|
|
.sidebar {
|
|
position: fixed;
|
|
left: -320px;
|
|
top: 0;
|
|
width: 320px;
|
|
height: 100vh;
|
|
background: var(--bg);
|
|
border-right: 1px solid var(--border);
|
|
transition: left 0.4s cubic-bezier(0.4, 0, 0.2, 1);
|
|
z-index: 999;
|
|
overflow-y: auto;
|
|
padding: 20px;
|
|
backdrop-filter: blur(20px);
|
|
box-shadow: 4px 0 20px var(--shadow);
|
|
}
|
|
|
|
.sidebar.open {
|
|
left: 0;
|
|
}
|
|
|
|
.sidebar-header {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 12px;
|
|
margin-bottom: 30px;
|
|
padding-top: 10px;
|
|
}
|
|
|
|
.sidebar-logo {
|
|
width: 32px;
|
|
height: 32px;
|
|
background: var(--logo-url) center/contain no-repeat;
|
|
filter: var(--logo-filter, none);
|
|
}
|
|
|
|
[data-theme="dark"] .sidebar-logo {}
|
|
|
|
.sidebar-title {
|
|
font-size: 16px;
|
|
font-weight: 500;
|
|
}
|
|
|
|
.sidebar-button {
|
|
width: 100%;
|
|
padding: 12px 16px;
|
|
border-radius: 12px;
|
|
cursor: pointer;
|
|
transition: all 0.3s;
|
|
font-weight: 500;
|
|
font-size: 14px;
|
|
margin-bottom: 8px;
|
|
background: var(--glass);
|
|
border: 1px solid var(--border);
|
|
color: var(--fg);
|
|
text-align: left;
|
|
}
|
|
|
|
.sidebar-button:hover {
|
|
background: var(--fg);
|
|
color: var(--bg);
|
|
transform: translateX(4px) scale(1.02);
|
|
}
|
|
|
|
.history-section {
|
|
margin-top: 20px;
|
|
}
|
|
|
|
.history-title {
|
|
font-size: 12px;
|
|
opacity: 0.5;
|
|
margin-bottom: 12px;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.5px;
|
|
}
|
|
|
|
.history-item {
|
|
padding: 10px 14px;
|
|
margin-bottom: 6px;
|
|
border-radius: 10px;
|
|
cursor: pointer;
|
|
transition: all 0.3s;
|
|
font-size: 13px;
|
|
border: 1px solid transparent;
|
|
}
|
|
|
|
.history-item:hover {
|
|
background: var(--fg);
|
|
color: var(--bg);
|
|
transform: translateX(4px) scale(1.02);
|
|
}
|
|
|
|
#messages {
|
|
flex: 1;
|
|
overflow-y: auto;
|
|
padding: 20px 20px 140px;
|
|
max-width: 680px;
|
|
margin: 0 auto;
|
|
width: 100%;
|
|
position: relative;
|
|
z-index: 1;
|
|
}
|
|
|
|
.message-container {
|
|
margin-bottom: 24px;
|
|
opacity: 0;
|
|
transform: translateY(10px);
|
|
}
|
|
|
|
.user-message {
|
|
display: flex;
|
|
justify-content: flex-end;
|
|
margin-bottom: 8px;
|
|
}
|
|
|
|
.user-message-content {
|
|
background: var(--fg);
|
|
color: var(--bg);
|
|
border-radius: 18px;
|
|
padding: 12px 18px;
|
|
max-width: 80%;
|
|
font-size: 14px;
|
|
line-height: 1.5;
|
|
box-shadow: 0 2px 8px var(--shadow);
|
|
position: relative;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.user-message-content::before {
|
|
content: "";
|
|
position: absolute;
|
|
inset: 0;
|
|
background: var(--gradient-2);
|
|
opacity: 0.3;
|
|
pointer-events: none;
|
|
}
|
|
|
|
.assistant-message {
|
|
display: flex;
|
|
gap: 8px;
|
|
align-items: flex-start;
|
|
}
|
|
|
|
.assistant-avatar {
|
|
width: 24px;
|
|
height: 24px;
|
|
border-radius: 50%;
|
|
background: var(--logo-url) center/contain no-repeat;
|
|
flex-shrink: 0;
|
|
margin-top: 2px;
|
|
filter: var(--logo-filter, none);
|
|
}
|
|
|
|
[data-theme="dark"] .assistant-avatar {}
|
|
|
|
.assistant-message-content {
|
|
flex: 1;
|
|
font-size: 14px;
|
|
line-height: 1.7;
|
|
background: var(--glass);
|
|
border-radius: 18px;
|
|
padding: 12px 18px;
|
|
border: 1px solid var(--border);
|
|
box-shadow: 0 2px 8px var(--shadow);
|
|
position: relative;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.assistant-message-content::before {
|
|
content: "";
|
|
position: absolute;
|
|
inset: 0;
|
|
background: var(--gradient-1);
|
|
opacity: 0.5;
|
|
pointer-events: none;
|
|
}
|
|
|
|
.thinking-indicator {
|
|
display: flex;
|
|
gap: 8px;
|
|
align-items: center;
|
|
font-size: 13px;
|
|
opacity: 0.4;
|
|
}
|
|
|
|
.typing-dots {
|
|
display: flex;
|
|
gap: 4px;
|
|
}
|
|
|
|
.typing-dot {
|
|
width: 4px;
|
|
height: 4px;
|
|
background: var(--fg);
|
|
border-radius: 50%;
|
|
animation: bounce 1.4s infinite;
|
|
}
|
|
|
|
.typing-dot:nth-child(1) {
|
|
animation-delay: -0.32s;
|
|
}
|
|
|
|
.typing-dot:nth-child(2) {
|
|
animation-delay: -0.16s;
|
|
}
|
|
|
|
@keyframes bounce {
|
|
|
|
0%,
|
|
80%,
|
|
100% {
|
|
transform: scale(0);
|
|
opacity: 0.3;
|
|
}
|
|
|
|
40% {
|
|
transform: scale(1);
|
|
opacity: 1;
|
|
}
|
|
}
|
|
|
|
footer {
|
|
position: fixed;
|
|
bottom: 0;
|
|
left: 0;
|
|
right: 0;
|
|
background: var(--bg);
|
|
border-top: 1px solid var(--border);
|
|
padding: 12px;
|
|
z-index: 100;
|
|
transition: all 0.3s;
|
|
backdrop-filter: blur(20px);
|
|
}
|
|
|
|
.suggestions-container {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 4px;
|
|
margin-bottom: 8px;
|
|
justify-content: center;
|
|
max-width: 680px;
|
|
margin: 0 auto 8px;
|
|
}
|
|
|
|
.suggestion-button {
|
|
padding: 6px 12px;
|
|
border-radius: 12px;
|
|
cursor: pointer;
|
|
font-size: 11px;
|
|
font-weight: 400;
|
|
transition: all 0.2s;
|
|
background: var(--glass);
|
|
border: 1px solid var(--border);
|
|
color: var(--fg);
|
|
}
|
|
|
|
.suggestion-button:hover {
|
|
background: var(--fg);
|
|
color: var(--bg);
|
|
transform: scale(1.05);
|
|
}
|
|
|
|
.input-container {
|
|
display: flex;
|
|
gap: 6px;
|
|
max-width: 680px;
|
|
margin: 0 auto;
|
|
align-items: center;
|
|
}
|
|
|
|
#messageInput {
|
|
flex: 1;
|
|
border-radius: 20px;
|
|
padding: 10px 16px;
|
|
font-size: 14px;
|
|
font-family: "Inter", sans-serif;
|
|
outline: none;
|
|
transition: all 0.3s;
|
|
background: var(--glass);
|
|
border: 1px solid var(--border);
|
|
color: var(--fg);
|
|
backdrop-filter: blur(10px);
|
|
}
|
|
|
|
#messageInput:focus {
|
|
border-color: var(--accent);
|
|
box-shadow: 0 0 0 3px rgba(0, 102, 255, 0.1);
|
|
}
|
|
|
|
#messageInput::placeholder {
|
|
opacity: 0.3;
|
|
}
|
|
|
|
#sendBtn,
|
|
#voiceBtn {
|
|
width: 36px;
|
|
height: 36px;
|
|
border-radius: 18px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
cursor: pointer;
|
|
transition: all 0.2s;
|
|
border: none;
|
|
background: var(--fg);
|
|
color: var(--bg);
|
|
font-size: 16px;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
#sendBtn:hover,
|
|
#voiceBtn:hover {
|
|
transform: scale(1.08) rotate(5deg);
|
|
}
|
|
|
|
#sendBtn:active,
|
|
#voiceBtn:active {
|
|
transform: scale(0.95);
|
|
}
|
|
|
|
#voiceBtn.recording {
|
|
animation: pulse 1.5s infinite;
|
|
}
|
|
|
|
@keyframes pulse {
|
|
|
|
0%,
|
|
100% {
|
|
opacity: 1;
|
|
transform: scale(1);
|
|
}
|
|
|
|
50% {
|
|
opacity: 0.6;
|
|
transform: scale(1.1);
|
|
}
|
|
}
|
|
|
|
.flash-overlay {
|
|
position: fixed;
|
|
inset: 0;
|
|
background: var(--fg);
|
|
opacity: 0;
|
|
pointer-events: none;
|
|
z-index: 9999;
|
|
}
|
|
|
|
.scroll-to-bottom {
|
|
position: fixed;
|
|
bottom: 80px;
|
|
right: 20px;
|
|
width: 40px;
|
|
height: 40px;
|
|
background: var(--fg);
|
|
border: 1px solid var(--border);
|
|
border-radius: 50%;
|
|
color: var(--bg);
|
|
font-size: 18px;
|
|
cursor: pointer;
|
|
display: none;
|
|
align-items: center;
|
|
justify-content: center;
|
|
transition: all 0.3s;
|
|
z-index: 90;
|
|
}
|
|
|
|
.scroll-to-bottom.visible {
|
|
display: flex;
|
|
}
|
|
|
|
.scroll-to-bottom:hover {
|
|
transform: scale(1.1) rotate(180deg);
|
|
}
|
|
|
|
.warning-message {
|
|
border-radius: 12px;
|
|
padding: 12px 16px;
|
|
margin-bottom: 18px;
|
|
opacity: 0.6;
|
|
background: var(--glass);
|
|
border: 1px solid var(--border);
|
|
font-size: 13px;
|
|
}
|
|
|
|
.continue-button {
|
|
display: inline-block;
|
|
border-radius: 10px;
|
|
padding: 8px 16px;
|
|
font-weight: 500;
|
|
cursor: pointer;
|
|
margin-top: 10px;
|
|
transition: all 0.3s;
|
|
font-size: 13px;
|
|
background: var(--glass);
|
|
border: 1px solid var(--border);
|
|
}
|
|
|
|
.continue-button:hover {
|
|
background: var(--fg);
|
|
color: var(--bg);
|
|
transform: translateY(-2px);
|
|
}
|
|
|
|
.context-indicator {
|
|
position: fixed;
|
|
bottom: 130px;
|
|
right: 20px;
|
|
width: 120px;
|
|
border-radius: 12px;
|
|
padding: 10px;
|
|
font-size: 10px;
|
|
text-align: center;
|
|
z-index: 90;
|
|
background: var(--bg);
|
|
border: 1px solid var(--border);
|
|
display: none;
|
|
backdrop-filter: blur(10px);
|
|
}
|
|
|
|
.context-indicator.visible {
|
|
display: block;
|
|
}
|
|
|
|
.context-progress {
|
|
height: 3px;
|
|
background: var(--glass);
|
|
border-radius: 2px;
|
|
margin-top: 6px;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.context-progress-bar {
|
|
height: 100%;
|
|
background: var(--accent);
|
|
border-radius: 2px;
|
|
transition: width 0.3s;
|
|
}
|
|
|
|
.connection-status {
|
|
position: fixed;
|
|
top: 20px;
|
|
right: 20px;
|
|
width: 8px;
|
|
height: 8px;
|
|
border-radius: 50%;
|
|
z-index: 1000;
|
|
transition: all 0.3s;
|
|
}
|
|
|
|
.connection-status.connecting {
|
|
background: var(--fg);
|
|
opacity: 0.3;
|
|
animation: ping 1.5s infinite;
|
|
}
|
|
|
|
.connection-status.connected {
|
|
background: var(--accent);
|
|
opacity: 0.8;
|
|
}
|
|
|
|
.connection-status.disconnected {
|
|
background: var(--fg);
|
|
opacity: 0.2;
|
|
}
|
|
|
|
@keyframes ping {
|
|
|
|
0%,
|
|
100% {
|
|
opacity: 0.3;
|
|
transform: scale(0.8);
|
|
}
|
|
|
|
50% {
|
|
opacity: 0.8;
|
|
transform: scale(1.2);
|
|
}
|
|
}
|
|
|
|
.markdown-content p {
|
|
margin-bottom: 12px;
|
|
line-height: 1.7;
|
|
}
|
|
|
|
.markdown-content ul,
|
|
.markdown-content ol {
|
|
margin-bottom: 12px;
|
|
padding-left: 20px;
|
|
}
|
|
|
|
.markdown-content li {
|
|
margin-bottom: 4px;
|
|
}
|
|
|
|
.markdown-content code {
|
|
background: var(--glass);
|
|
padding: 2px 6px;
|
|
border-radius: 4px;
|
|
font-family: monospace;
|
|
font-size: 13px;
|
|
}
|
|
|
|
.markdown-content pre {
|
|
border-radius: 8px;
|
|
padding: 12px;
|
|
overflow-x: auto;
|
|
margin-bottom: 12px;
|
|
background: var(--glass);
|
|
border: 1px solid var(--border);
|
|
}
|
|
|
|
.markdown-content pre code {
|
|
background: none;
|
|
padding: 0;
|
|
}
|
|
|
|
.markdown-content h1,
|
|
.markdown-content h2,
|
|
.markdown-content h3 {
|
|
margin-top: 16px;
|
|
margin-bottom: 8px;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.markdown-content h1 {
|
|
font-size: 20px;
|
|
}
|
|
|
|
.markdown-content h2 {
|
|
font-size: 18px;
|
|
}
|
|
|
|
.markdown-content h3 {
|
|
font-size: 16px;
|
|
}
|
|
|
|
.markdown-content table {
|
|
width: 100%;
|
|
border-collapse: collapse;
|
|
margin-bottom: 14px;
|
|
}
|
|
|
|
.markdown-content table th,
|
|
.markdown-content table td {
|
|
padding: 8px;
|
|
text-align: left;
|
|
border: 1px solid var(--border);
|
|
}
|
|
|
|
.markdown-content table th {
|
|
font-weight: 600;
|
|
background: var(--glass);
|
|
}
|
|
|
|
.markdown-content blockquote {
|
|
border-left: 2px solid var(--accent);
|
|
padding-left: 14px;
|
|
margin: 12px 0;
|
|
opacity: 0.7;
|
|
font-style: italic;
|
|
}
|
|
|
|
.markdown-content a {
|
|
color: var(--accent);
|
|
text-decoration: none;
|
|
transition: all 0.3s;
|
|
}
|
|
|
|
.markdown-content a:hover {
|
|
opacity: 0.7;
|
|
text-decoration: underline;
|
|
}
|
|
|
|
::-webkit-scrollbar {
|
|
width: 6px;
|
|
}
|
|
|
|
::-webkit-scrollbar-track {
|
|
background: transparent;
|
|
}
|
|
|
|
::-webkit-scrollbar-thumb {
|
|
background: var(--border);
|
|
border-radius: 3px;
|
|
}
|
|
|
|
::-webkit-scrollbar-thumb:hover {
|
|
background: var(--fg);
|
|
opacity: 0.3;
|
|
}
|
|
|
|
@media (max-width: 768px) {
|
|
.sidebar {
|
|
width: 100%;
|
|
left: -100%;
|
|
}
|
|
|
|
#messages {
|
|
padding: 20px 16px 140px;
|
|
}
|
|
|
|
.float-menu {
|
|
left: 12px;
|
|
top: 12px;
|
|
}
|
|
|
|
.float-logo,
|
|
.menu-button {
|
|
width: 36px;
|
|
height: 36px;
|
|
font-size: 14px;
|
|
}
|
|
|
|
.scroll-to-bottom {
|
|
width: 36px;
|
|
height: 36px;
|
|
bottom: 70px;
|
|
right: 12px;
|
|
}
|
|
|
|
.context-indicator {
|
|
bottom: 120px;
|
|
right: 12px;
|
|
width: 100px;
|
|
}
|
|
}
|
|
</style>
|
|
</head>
|
|
|
|
<body>
|
|
<div class="connection-status connecting" id="connectionStatus"></div>
|
|
<div class="flash-overlay" id="flashOverlay"></div>
|
|
|
|
<div class="sidebar" id="sidebar">
|
|
<div class="sidebar-header">
|
|
<div class="sidebar-logo"></div>
|
|
<div class="sidebar-title" id="sidebarTitle">General Bots</div>
|
|
</div>
|
|
<button class="sidebar-button" id="voiceToggle" onclick="toggleVoiceMode()">
|
|
🎤 Voice Mode
|
|
</button>
|
|
<div class="history-section">
|
|
<div class="history-title">History</div>
|
|
<div id="history"></div>
|
|
</div>
|
|
</div>
|
|
<main id="messages"></main>
|
|
<footer>
|
|
<div class="suggestions-container" id="suggestions"></div>
|
|
<div class="input-container">
|
|
<input id="messageInput" type="text" placeholder="Message..." autofocus />
|
|
<button id="voiceBtn" title="Voice">🎤</button>
|
|
<button id="sendBtn" title="Send">↑</button>
|
|
</div>
|
|
</footer>
|
|
<button class="scroll-to-bottom" id="scrollToBottom">↓</button>
|
|
<div class="context-indicator" id="contextIndicator">
|
|
<div>Context</div>
|
|
<div id="contextPercentage">0%</div>
|
|
<div class="context-progress">
|
|
<div class="context-progress-bar" id="contextProgressBar" style="width: 0%"></div>
|
|
</div>
|
|
</div>
|
|
<script>
|
|
let ws = null,
|
|
currentSessionId = null,
|
|
currentUserId = null,
|
|
currentBotId = "default_bot",
|
|
isStreaming = false,
|
|
voiceRoom = null,
|
|
isVoiceMode = false,
|
|
mediaRecorder = null,
|
|
audioChunks = [],
|
|
streamingMessageId = null,
|
|
isThinking = false,
|
|
currentStreamingContent = "",
|
|
hasReceivedInitialMessage = false,
|
|
reconnectAttempts = 0,
|
|
reconnectTimeout = null,
|
|
thinkingTimeout = null,
|
|
currentTheme = "auto",
|
|
themeColor1 = null,
|
|
themeColor2 = null,
|
|
customLogoUrl = null,
|
|
contextUsage = 0,
|
|
isUserScrolling = false,
|
|
autoScrollEnabled = true,
|
|
isContextChange = false;
|
|
const maxReconnectAttempts = 5;
|
|
let messagesDiv = document.getElementById("messages");
|
|
let input = document.getElementById("messageInput");
|
|
let sendBtn = document.getElementById("sendBtn");
|
|
let voiceBtn = document.getElementById("voiceBtn");
|
|
|
|
// Debug element initialization
|
|
console.log("Element initialization:");
|
|
console.log("messagesDiv:", messagesDiv);
|
|
console.log("input:", input);
|
|
console.log("sendBtn:", sendBtn);
|
|
console.log("voiceBtn:", voiceBtn);
|
|
|
|
const connectionStatus =
|
|
document.getElementById("connectionStatus"),
|
|
flashOverlay = document.getElementById("flashOverlay"),
|
|
suggestionsContainer = document.getElementById("suggestions"),
|
|
sidebar = document.getElementById("sidebar"),
|
|
|
|
scrollToBottomBtn = document.getElementById("scrollToBottom"),
|
|
contextIndicator = document.getElementById("contextIndicator"),
|
|
contextPercentage =
|
|
document.getElementById("contextPercentage"),
|
|
contextProgressBar =
|
|
document.getElementById("contextProgressBar"),
|
|
sidebarTitle = document.getElementById("sidebarTitle");
|
|
marked.setOptions({ breaks: true, gfm: true });
|
|
|
|
|
|
|
|
function toggleSidebar() {
|
|
sidebar.classList.toggle("open");
|
|
}
|
|
|
|
function toggleTheme() {
|
|
const themes = ["auto", "dark", "light"];
|
|
const savedTheme = localStorage.getItem("gb-theme") || "auto";
|
|
const idx = themes.indexOf(savedTheme);
|
|
const newTheme = themes[(idx + 1) % themes.length];
|
|
localStorage.setItem("gb-theme", newTheme);
|
|
currentTheme = newTheme;
|
|
applyTheme();
|
|
updateThemeButton();
|
|
}
|
|
|
|
function updateThemeButton() {
|
|
// Theme button removed for non-logged users
|
|
}
|
|
|
|
function applyTheme() {
|
|
const prefersDark = window.matchMedia(
|
|
"(prefers-color-scheme: dark)",
|
|
).matches;
|
|
let theme = currentTheme;
|
|
if (theme === "auto") {
|
|
theme = prefersDark ? "dark" : "light";
|
|
}
|
|
document.documentElement.setAttribute("data-theme", theme);
|
|
if (themeColor1 && themeColor2) {
|
|
const root = document.documentElement;
|
|
root.style.setProperty(
|
|
"--bg",
|
|
theme === "dark" ? themeColor2 : themeColor1,
|
|
);
|
|
root.style.setProperty(
|
|
"--fg",
|
|
theme === "dark" ? themeColor1 : themeColor2,
|
|
);
|
|
}
|
|
if (customLogoUrl) {
|
|
document.documentElement.style.setProperty(
|
|
"--logo-url",
|
|
`url('${customLogoUrl}')`,
|
|
);
|
|
}
|
|
}
|
|
|
|
window.addEventListener("load", function () {
|
|
const savedTheme = localStorage.getItem("gb-theme") || "auto";
|
|
currentTheme = savedTheme;
|
|
applyTheme();
|
|
updateThemeButton();
|
|
window
|
|
.matchMedia("(prefers-color-scheme: dark)")
|
|
.addEventListener("change", () => {
|
|
if (currentTheme === "auto") {
|
|
applyTheme();
|
|
}
|
|
});
|
|
input.focus();
|
|
});
|
|
|
|
|
|
|
|
document.addEventListener("click", function (e) {
|
|
if (
|
|
sidebar.classList.contains("open") &&
|
|
!sidebar.contains(e.target)
|
|
) {
|
|
sidebar.classList.remove("open");
|
|
}
|
|
});
|
|
|
|
messagesDiv.addEventListener("scroll", function () {
|
|
const isAtBottom =
|
|
messagesDiv.scrollHeight - messagesDiv.scrollTop <=
|
|
messagesDiv.clientHeight + 100;
|
|
if (!isAtBottom) {
|
|
isUserScrolling = true;
|
|
scrollToBottomBtn.classList.add("visible");
|
|
} else {
|
|
isUserScrolling = false;
|
|
scrollToBottomBtn.classList.remove("visible");
|
|
}
|
|
});
|
|
|
|
scrollToBottomBtn.addEventListener("click", function () {
|
|
scrollToBottom();
|
|
});
|
|
|
|
function updateContextUsage(u) {
|
|
contextUsage = u;
|
|
const p = Math.min(100, Math.round(u * 100));
|
|
contextPercentage.textContent = `${p}%`;
|
|
contextProgressBar.style.width = `${p}%`;
|
|
contextIndicator.classList.remove("visible");
|
|
}
|
|
|
|
function flashScreen() {
|
|
gsap.to(flashOverlay, {
|
|
opacity: 0.15,
|
|
duration: 0.1,
|
|
onComplete: () => {
|
|
gsap.to(flashOverlay, { opacity: 0, duration: 0.2 });
|
|
},
|
|
});
|
|
}
|
|
|
|
function updateConnectionStatus(s) {
|
|
connectionStatus.className = `connection-status ${s}`;
|
|
}
|
|
|
|
function getWebSocketUrl() {
|
|
const s = currentSessionId || crypto.randomUUID(),
|
|
u = currentUserId || crypto.randomUUID();
|
|
return `${BOTSERVER_WS_URL}/ws?session_id=${s}&user_id=${u}`;
|
|
}
|
|
|
|
async function initializeAuth() {
|
|
try {
|
|
console.log("Starting auth initialization...");
|
|
updateConnectionStatus("connecting");
|
|
const p = window.location.pathname
|
|
.split("/")
|
|
.filter((s) => s),
|
|
b = p.length > 0 ? p[0] : "default";
|
|
console.log("Bot name:", b);
|
|
const r = await fetch(
|
|
`${BOTSERVER_URL}/api/auth?bot_name=${encodeURIComponent(b)}`,
|
|
),
|
|
a = await r.json();
|
|
console.log("Auth response:", a);
|
|
currentUserId = a.user_id;
|
|
currentSessionId = a.session_id;
|
|
currentBotId = a.bot_id || "default_bot";
|
|
console.log(
|
|
"Auth initialized - User:",
|
|
currentUserId,
|
|
"Session:",
|
|
currentSessionId,
|
|
"Bot:",
|
|
currentBotId,
|
|
);
|
|
connectWebSocket();
|
|
loadSessions();
|
|
} catch (e) {
|
|
console.error("Failed to initialize auth:", e);
|
|
updateConnectionStatus("disconnected");
|
|
setTimeout(initializeAuth, 3000);
|
|
}
|
|
}
|
|
|
|
async function loadSessions() {
|
|
try {
|
|
const r = await fetch(`${BOTSERVER_URL}/api/sessions`),
|
|
s = await r.json(),
|
|
h = document.getElementById("history");
|
|
h.innerHTML = "";
|
|
s.forEach((session) => {
|
|
const item = document.createElement("div");
|
|
item.className = "history-item";
|
|
item.textContent =
|
|
session.title ||
|
|
`Session ${session.session_id.substring(0, 8)}`;
|
|
item.onclick = () => switchSession(session.session_id);
|
|
h.appendChild(item);
|
|
});
|
|
} catch (e) {
|
|
console.error("Failed to load sessions:", e);
|
|
}
|
|
}
|
|
|
|
async function createNewSession() {
|
|
try {
|
|
const r = await fetch(`${BOTSERVER_URL}/api/sessions`, {
|
|
method: "POST",
|
|
}),
|
|
s = await r.json();
|
|
currentSessionId = s.session_id;
|
|
hasReceivedInitialMessage = false;
|
|
connectWebSocket();
|
|
loadSessions();
|
|
messagesDiv.innerHTML = "";
|
|
clearSuggestions();
|
|
updateContextUsage(0);
|
|
if (isVoiceMode) {
|
|
await stopVoiceSession();
|
|
isVoiceMode = false;
|
|
const v = document.getElementById("voiceToggle");
|
|
v.textContent = "🎤 Voice Mode";
|
|
voiceBtn.classList.remove("recording");
|
|
}
|
|
} catch (e) {
|
|
console.error("Failed to create session:", e);
|
|
}
|
|
}
|
|
|
|
function switchSession(s) {
|
|
currentSessionId = s;
|
|
hasReceivedInitialMessage = false;
|
|
loadSessionHistory(s);
|
|
connectWebSocket();
|
|
if (isVoiceMode) {
|
|
startVoiceSession();
|
|
}
|
|
sidebar.classList.remove("open");
|
|
}
|
|
|
|
async function loadSessionHistory(s) {
|
|
try {
|
|
const r = await fetch(`${BOTSERVER_URL}/api/sessions/${s}`),
|
|
h = await r.json(),
|
|
m = document.getElementById("messages");
|
|
m.innerHTML = "";
|
|
if (h.length === 0) {
|
|
updateContextUsage(0);
|
|
} else {
|
|
h.forEach(([role, content]) => {
|
|
addMessage(role, content, false);
|
|
});
|
|
updateContextUsage(h.length / 20);
|
|
}
|
|
} catch (e) {
|
|
console.error("Failed to load session history:", e);
|
|
}
|
|
}
|
|
|
|
function connectWebSocket() {
|
|
if (ws) {
|
|
ws.close();
|
|
}
|
|
clearTimeout(reconnectTimeout);
|
|
const u = getWebSocketUrl();
|
|
ws = new WebSocket(u);
|
|
ws.onmessage = function (e) {
|
|
console.log("WebSocket message received:", e.data);
|
|
try {
|
|
if (!e.data || e.data.trim() === "") {
|
|
console.warn("Empty WebSocket message received");
|
|
return;
|
|
}
|
|
const r = JSON.parse(e.data);
|
|
if (r.type === "connected") {
|
|
console.log("WebSocket welcome message:", r);
|
|
reconnectAttempts = 0;
|
|
return;
|
|
}
|
|
if (r.bot_id) {
|
|
currentBotId = r.bot_id;
|
|
}
|
|
// BOT_RESPONSE type is used for both regular streaming content and special event messages
|
|
// Event messages have JSON-encoded content with 'event' and 'data' properties
|
|
// Regular messages have plain text content that should be displayed directly
|
|
if (r.message_type === MessageType.BOT_RESPONSE) {
|
|
// Check if content looks like JSON (starts with { or [)
|
|
const contentTrimmed = r.content.trim();
|
|
if (
|
|
contentTrimmed.startsWith("{") ||
|
|
contentTrimmed.startsWith("[")
|
|
) {
|
|
try {
|
|
const d = JSON.parse(r.content);
|
|
if (d.event && d.data) {
|
|
// This is an event message
|
|
handleEvent(d.event, d.data);
|
|
return;
|
|
}
|
|
} catch (parseErr) {
|
|
// Not a valid event message, treat as regular content
|
|
console.debug(
|
|
"Content is not an event message, processing as regular message",
|
|
);
|
|
}
|
|
}
|
|
// Process as regular message content
|
|
processMessageContent(r);
|
|
return;
|
|
}
|
|
if (r.message_type === MessageType.CONTEXT_CHANGE) {
|
|
isContextChange = true;
|
|
return;
|
|
}
|
|
processMessageContent(r);
|
|
} catch (err) {
|
|
console.error(
|
|
"WebSocket message parse error:",
|
|
err,
|
|
"Raw data:",
|
|
e.data,
|
|
);
|
|
}
|
|
};
|
|
ws.onopen = function () {
|
|
console.log(
|
|
"Connected to WebSocket, readyState:",
|
|
ws.readyState,
|
|
);
|
|
updateConnectionStatus("connected");
|
|
hasReceivedInitialMessage = false;
|
|
};
|
|
ws.onclose = function (e) {
|
|
console.log("WebSocket disconnected:", e.code, e.reason);
|
|
updateConnectionStatus("disconnected");
|
|
if (isStreaming) {
|
|
showContinueButton();
|
|
}
|
|
if (reconnectAttempts < maxReconnectAttempts) {
|
|
reconnectAttempts++;
|
|
const d = Math.min(1000 * reconnectAttempts, 10000);
|
|
reconnectTimeout = setTimeout(() => {
|
|
updateConnectionStatus("connecting");
|
|
connectWebSocket();
|
|
}, d);
|
|
} else {
|
|
updateConnectionStatus("disconnected");
|
|
}
|
|
};
|
|
ws.onerror = function (e) {
|
|
console.error("WebSocket error:", e);
|
|
updateConnectionStatus("disconnected");
|
|
};
|
|
}
|
|
|
|
function processMessageContent(r) {
|
|
if (isContextChange) {
|
|
isContextChange = false;
|
|
return;
|
|
}
|
|
if (r.context_usage !== undefined) {
|
|
updateContextUsage(r.context_usage);
|
|
}
|
|
if (r.suggestions && r.suggestions.length > 0) {
|
|
handleSuggestions(r.suggestions);
|
|
}
|
|
if (r.is_complete) {
|
|
if (isStreaming) {
|
|
finalizeStreamingMessage();
|
|
isStreaming = false;
|
|
streamingMessageId = null;
|
|
currentStreamingContent = "";
|
|
} else {
|
|
if (r.content && r.content.trim() !== "") {
|
|
addMessage("assistant", r.content, false);
|
|
}
|
|
}
|
|
} else {
|
|
if (!isStreaming) {
|
|
isStreaming = true;
|
|
streamingMessageId = "streaming-" + Date.now();
|
|
currentStreamingContent = r.content || "";
|
|
addMessage(
|
|
"assistant",
|
|
currentStreamingContent,
|
|
true,
|
|
streamingMessageId,
|
|
);
|
|
} else {
|
|
currentStreamingContent += r.content || "";
|
|
updateStreamingMessage(currentStreamingContent);
|
|
}
|
|
}
|
|
}
|
|
|
|
function handleEvent(t, d) {
|
|
console.log("Event received:", t, d);
|
|
switch (t) {
|
|
case "thinking_start":
|
|
showThinkingIndicator();
|
|
break;
|
|
case "thinking_end":
|
|
hideThinkingIndicator();
|
|
break;
|
|
case "warn":
|
|
showWarning(d.message);
|
|
break;
|
|
case "context_usage":
|
|
updateContextUsage(d.usage);
|
|
break;
|
|
case "change_theme":
|
|
if (d.color1) themeColor1 = d.color1;
|
|
if (d.color2) themeColor2 = d.color2;
|
|
if (d.logo_url) customLogoUrl = d.logo_url;
|
|
if (d.title) document.title = d.title;
|
|
if (d.logo_text) {
|
|
sidebarTitle.textContent = d.logo_text;
|
|
}
|
|
applyTheme();
|
|
break;
|
|
}
|
|
}
|
|
|
|
function showThinkingIndicator() {
|
|
if (isThinking) return;
|
|
const t = document.createElement("div");
|
|
t.id = "thinking-indicator";
|
|
t.className = "message-container";
|
|
t.innerHTML = `<div class="assistant-message"><div class="assistant-avatar"></div><div class="thinking-indicator"><div class="typing-dots"><div class="typing-dot"></div><div class="typing-dot"></div><div class="typing-dot"></div></div></div></div>`;
|
|
messagesDiv.appendChild(t);
|
|
gsap.to(t, {
|
|
opacity: 1,
|
|
y: 0,
|
|
duration: 0.3,
|
|
ease: "power2.out",
|
|
});
|
|
if (!isUserScrolling) {
|
|
scrollToBottom();
|
|
}
|
|
thinkingTimeout = setTimeout(() => {
|
|
if (isThinking) {
|
|
hideThinkingIndicator();
|
|
showWarning(
|
|
"O servidor pode estar ocupado. A resposta está demorando demais.",
|
|
);
|
|
}
|
|
}, 60000);
|
|
isThinking = true;
|
|
}
|
|
|
|
function hideThinkingIndicator() {
|
|
if (!isThinking) return;
|
|
const t = document.getElementById("thinking-indicator");
|
|
if (t) {
|
|
gsap.to(t, {
|
|
opacity: 0,
|
|
duration: 0.2,
|
|
onComplete: () => {
|
|
if (t.parentNode) {
|
|
t.remove();
|
|
}
|
|
},
|
|
});
|
|
}
|
|
if (thinkingTimeout) {
|
|
clearTimeout(thinkingTimeout);
|
|
thinkingTimeout = null;
|
|
}
|
|
isThinking = false;
|
|
}
|
|
|
|
function showWarning(m) {
|
|
const w = document.createElement("div");
|
|
w.className = "warning-message";
|
|
w.innerHTML = `⚠️ ${m}`;
|
|
messagesDiv.appendChild(w);
|
|
gsap.from(w, {
|
|
opacity: 0,
|
|
y: 20,
|
|
duration: 0.4,
|
|
ease: "power2.out",
|
|
});
|
|
if (!isUserScrolling) {
|
|
scrollToBottom();
|
|
}
|
|
setTimeout(() => {
|
|
if (w.parentNode) {
|
|
gsap.to(w, {
|
|
opacity: 0,
|
|
duration: 0.3,
|
|
onComplete: () => w.remove(),
|
|
});
|
|
}
|
|
}, 5000);
|
|
}
|
|
|
|
function showContinueButton() {
|
|
const c = document.createElement("div");
|
|
c.className = "message-container";
|
|
c.innerHTML = `<div class="assistant-message"><div class="assistant-avatar"></div><div class="assistant-message-content"><p>A conexão foi interrompida. Clique em "Continuar" para tentar recuperar a resposta.</p><button class="continue-button" onclick="continueInterruptedResponse()">Continuar</button></div></div>`;
|
|
messagesDiv.appendChild(c);
|
|
gsap.to(c, {
|
|
opacity: 1,
|
|
y: 0,
|
|
duration: 0.5,
|
|
ease: "power2.out",
|
|
});
|
|
if (!isUserScrolling) {
|
|
scrollToBottom();
|
|
}
|
|
}
|
|
|
|
function continueInterruptedResponse() {
|
|
if (!ws || ws.readyState !== WebSocket.OPEN) {
|
|
connectWebSocket();
|
|
}
|
|
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
const d = {
|
|
bot_id: "default_bot",
|
|
user_id: currentUserId,
|
|
session_id: currentSessionId,
|
|
channel: "web",
|
|
content: "continue",
|
|
message_type: MessageType.CONTINUE,
|
|
media_url: null,
|
|
timestamp: new Date().toISOString(),
|
|
};
|
|
ws.send(JSON.stringify(d));
|
|
}
|
|
document.querySelectorAll(".continue-button").forEach((b) => {
|
|
b.parentElement.parentElement.parentElement.remove();
|
|
});
|
|
}
|
|
|
|
function addMessage(
|
|
role,
|
|
content,
|
|
streaming = false,
|
|
msgId = null,
|
|
) {
|
|
const m = document.createElement("div");
|
|
m.className = "message-container";
|
|
if (role === "user") {
|
|
m.innerHTML = `<div class="user-message"><div class="user-message-content">${escapeHtml(content)}</div></div>`;
|
|
updateContextUsage(contextUsage + 0.05);
|
|
} else if (role === "assistant") {
|
|
m.innerHTML = `<div class="assistant-message"><div class="assistant-avatar"></div><div class="assistant-message-content markdown-content" id="${msgId || ""}">${streaming ? "" : marked.parse(content)}</div></div>`;
|
|
updateContextUsage(contextUsage + 0.03);
|
|
} else if (role === "voice") {
|
|
m.innerHTML = `<div class="assistant-message"><div class="assistant-avatar">🎤</div><div class="assistant-message-content">${content}</div></div>`;
|
|
} else {
|
|
m.innerHTML = `<div class="assistant-message"><div class="assistant-avatar"></div><div class="assistant-message-content">${content}</div></div>`;
|
|
}
|
|
messagesDiv.appendChild(m);
|
|
gsap.to(m, {
|
|
opacity: 1,
|
|
y: 0,
|
|
duration: 0.5,
|
|
ease: "power2.out",
|
|
});
|
|
if (!isUserScrolling) {
|
|
scrollToBottom();
|
|
}
|
|
}
|
|
|
|
function updateStreamingMessage(c) {
|
|
const m = document.getElementById(streamingMessageId);
|
|
if (m) {
|
|
m.innerHTML = marked.parse(c);
|
|
if (!isUserScrolling) {
|
|
scrollToBottom();
|
|
}
|
|
}
|
|
}
|
|
|
|
function finalizeStreamingMessage() {
|
|
const m = document.getElementById(streamingMessageId);
|
|
if (m) {
|
|
m.innerHTML = marked.parse(currentStreamingContent);
|
|
m.removeAttribute("id");
|
|
if (!isUserScrolling) {
|
|
scrollToBottom();
|
|
}
|
|
}
|
|
}
|
|
|
|
function escapeHtml(t) {
|
|
const d = document.createElement("div");
|
|
d.textContent = t;
|
|
return d.innerHTML;
|
|
}
|
|
|
|
function clearSuggestions() {
|
|
suggestionsContainer.innerHTML = "";
|
|
}
|
|
|
|
function handleSuggestions(s) {
|
|
const uniqueSuggestions = s.filter(
|
|
(v, i, a) =>
|
|
i ===
|
|
a.findIndex(
|
|
(t) => t.text === v.text && t.context === v.context,
|
|
),
|
|
);
|
|
suggestionsContainer.innerHTML = "";
|
|
uniqueSuggestions.forEach((v) => {
|
|
const b = document.createElement("button");
|
|
b.textContent = v.text;
|
|
b.className = "suggestion-button";
|
|
b.onclick = () => {
|
|
setContext(v.context);
|
|
input.value = "";
|
|
};
|
|
suggestionsContainer.appendChild(b);
|
|
});
|
|
}
|
|
|
|
let pendingContextChange = null;
|
|
|
|
async function setContext(c) {
|
|
try {
|
|
const t = event?.target?.textContent || c;
|
|
addMessage("user", t);
|
|
const i = document.getElementById("messageInput");
|
|
if (i) {
|
|
i.value = "";
|
|
}
|
|
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
pendingContextChange = new Promise((r) => {
|
|
const h = (e) => {
|
|
const d = JSON.parse(e.data);
|
|
if (
|
|
d.message_type ===
|
|
MessageType.CONTEXT_CHANGE &&
|
|
d.context_name === c
|
|
) {
|
|
ws.removeEventListener("message", h);
|
|
r();
|
|
}
|
|
};
|
|
ws.addEventListener("message", h);
|
|
const s = {
|
|
bot_id: currentBotId,
|
|
user_id: currentUserId,
|
|
session_id: currentSessionId,
|
|
channel: "web",
|
|
content: t,
|
|
message_type: MessageType.SUGGESTION,
|
|
is_suggestion: true,
|
|
context_name: c,
|
|
timestamp: new Date().toISOString(),
|
|
};
|
|
ws.send(JSON.stringify(s));
|
|
});
|
|
await pendingContextChange;
|
|
const x = document.getElementById("contextIndicator");
|
|
if (x) {
|
|
document.getElementById(
|
|
"contextPercentage",
|
|
).textContent = c;
|
|
}
|
|
} else {
|
|
console.warn(
|
|
"WebSocket não está conectado. Tentando reconectar...",
|
|
);
|
|
connectWebSocket();
|
|
}
|
|
} catch (err) {
|
|
console.error("Failed to set context:", err);
|
|
}
|
|
}
|
|
|
|
async function sendMessage() {
|
|
console.log("=== sendMessage called ===");
|
|
console.log("input element:", input);
|
|
console.log(
|
|
"input.value:",
|
|
input ? input.value : "input is null",
|
|
);
|
|
|
|
if (pendingContextChange) {
|
|
await pendingContextChange;
|
|
pendingContextChange = null;
|
|
}
|
|
|
|
if (!input) {
|
|
console.error("Input element is null!");
|
|
return;
|
|
}
|
|
|
|
const m = input.value.trim();
|
|
console.log(
|
|
"Attempting to send message:",
|
|
m,
|
|
"WS state:",
|
|
ws ? ws.readyState : "no ws",
|
|
"WebSocket.OPEN value:",
|
|
WebSocket.OPEN,
|
|
);
|
|
|
|
if (!m) {
|
|
console.log("Message is empty, not sending");
|
|
return;
|
|
}
|
|
|
|
if (!ws || ws.readyState !== WebSocket.OPEN) {
|
|
console.log(
|
|
"WebSocket not connected, attempting reconnect",
|
|
);
|
|
if (!ws || ws.readyState !== WebSocket.OPEN) {
|
|
showWarning(
|
|
"Conexão não disponível. Tentando reconectar...",
|
|
);
|
|
connectWebSocket();
|
|
}
|
|
return;
|
|
}
|
|
if (isThinking) {
|
|
hideThinkingIndicator();
|
|
}
|
|
|
|
console.log("Adding message to UI");
|
|
addMessage("user", m);
|
|
|
|
console.log("Building message data object");
|
|
console.log("currentBotId:", currentBotId);
|
|
console.log("currentUserId:", currentUserId);
|
|
console.log("currentSessionId:", currentSessionId);
|
|
|
|
const d = {
|
|
bot_id: currentBotId || "default_bot",
|
|
user_id: currentUserId,
|
|
session_id: currentSessionId,
|
|
channel: "web",
|
|
content: m,
|
|
message_type: MessageType.USER,
|
|
media_url: null,
|
|
timestamp: new Date().toISOString(),
|
|
};
|
|
console.log("Message data object:", JSON.stringify(d, null, 2));
|
|
|
|
try {
|
|
const messageString = JSON.stringify(d);
|
|
console.log("Stringified message:", messageString);
|
|
console.log("About to call ws.send()");
|
|
ws.send(messageString);
|
|
console.log("ws.send() completed successfully");
|
|
} catch (error) {
|
|
console.error("Error sending message:", error);
|
|
console.error("Error stack:", error.stack);
|
|
}
|
|
|
|
console.log("Clearing input field");
|
|
input.value = "";
|
|
input.focus();
|
|
console.log("=== sendMessage completed ===");
|
|
}
|
|
|
|
async function toggleVoiceMode() {
|
|
isVoiceMode = !isVoiceMode;
|
|
const v = document.getElementById("voiceToggle");
|
|
if (isVoiceMode) {
|
|
v.textContent = "🔴 Stop Voice";
|
|
v.classList.add("recording");
|
|
await startVoiceSession();
|
|
} else {
|
|
v.textContent = "🎤 Voice Mode";
|
|
v.classList.remove("recording");
|
|
await stopVoiceSession();
|
|
}
|
|
}
|
|
|
|
async function startVoiceSession() {
|
|
if (!currentSessionId) return;
|
|
try {
|
|
const r = await fetch(`${BOTSERVER_URL}/api/voice/start`, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({
|
|
session_id: currentSessionId,
|
|
user_id: currentUserId,
|
|
}),
|
|
}),
|
|
d = await r.json();
|
|
if (d.token) {
|
|
await connectToVoiceRoom(d.token);
|
|
startVoiceRecording();
|
|
}
|
|
} catch (e) {
|
|
console.error("Failed to start voice session:", e);
|
|
showWarning("Falha ao iniciar modo de voz");
|
|
}
|
|
}
|
|
|
|
async function stopVoiceSession() {
|
|
if (!currentSessionId) return;
|
|
try {
|
|
await fetch(`${BOTSERVER_URL}/api/voice/stop`, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ session_id: currentSessionId }),
|
|
});
|
|
if (voiceRoom) {
|
|
voiceRoom.disconnect();
|
|
voiceRoom = null;
|
|
}
|
|
if (mediaRecorder && mediaRecorder.state === "recording") {
|
|
mediaRecorder.stop();
|
|
}
|
|
} catch (e) {
|
|
console.error("Failed to stop voice session:", e);
|
|
}
|
|
}
|
|
|
|
async function connectToVoiceRoom(t) {
|
|
try {
|
|
const r = new LiveKitClient.Room(),
|
|
u = `${BOTSERVER_WS_URL}/voice`;
|
|
await r.connect(u, t);
|
|
|
|
voiceRoom = r;
|
|
r.on("dataReceived", (d) => {
|
|
const dc = new TextDecoder(),
|
|
m = dc.decode(d);
|
|
try {
|
|
const j = JSON.parse(m);
|
|
if (j.type === "voice_response") {
|
|
addMessage("assistant", j.text);
|
|
}
|
|
} catch (e) {
|
|
console.log("Voice data:", m);
|
|
}
|
|
});
|
|
const l = await LiveKitClient.createLocalTracks({
|
|
audio: true,
|
|
video: false,
|
|
});
|
|
for (const k of l) {
|
|
await r.localParticipant.publishTrack(k);
|
|
}
|
|
} catch (e) {
|
|
console.error("Failed to connect to voice room:", e);
|
|
showWarning("Falha na conexão de voz");
|
|
}
|
|
}
|
|
|
|
function startVoiceRecording() {
|
|
if (!navigator.mediaDevices) {
|
|
console.log("Media devices not supported");
|
|
return;
|
|
}
|
|
navigator.mediaDevices
|
|
.getUserMedia({ audio: true })
|
|
.then((s) => {
|
|
mediaRecorder = new MediaRecorder(s);
|
|
audioChunks = [];
|
|
mediaRecorder.ondataavailable = (e) => {
|
|
audioChunks.push(e.data);
|
|
};
|
|
mediaRecorder.onstop = () => {
|
|
const a = new Blob(audioChunks, {
|
|
type: "audio/wav",
|
|
});
|
|
simulateVoiceTranscription();
|
|
};
|
|
mediaRecorder.start();
|
|
setTimeout(() => {
|
|
if (
|
|
mediaRecorder &&
|
|
mediaRecorder.state === "recording"
|
|
) {
|
|
mediaRecorder.stop();
|
|
setTimeout(() => {
|
|
if (isVoiceMode) {
|
|
startVoiceRecording();
|
|
}
|
|
}, 1000);
|
|
}
|
|
}, 5000);
|
|
})
|
|
.catch((e) => {
|
|
console.error("Error accessing microphone:", e);
|
|
showWarning("Erro ao acessar microfone");
|
|
});
|
|
}
|
|
|
|
function simulateVoiceTranscription() {
|
|
const p = [
|
|
"Olá, como posso ajudá-lo hoje?",
|
|
"Entendo o que você está dizendo",
|
|
"Esse é um ponto interessante",
|
|
"Deixe-me pensar sobre isso",
|
|
"Posso ajudá-lo com isso",
|
|
"O que você gostaria de saber?",
|
|
"Isso parece ótimo",
|
|
"Estou ouvindo sua voz",
|
|
],
|
|
r = p[Math.floor(Math.random() * p.length)];
|
|
if (voiceRoom) {
|
|
const m = {
|
|
type: "voice_input",
|
|
content: r,
|
|
timestamp: new Date().toISOString(),
|
|
};
|
|
voiceRoom.localParticipant.publishData(
|
|
new TextEncoder().encode(JSON.stringify(m)),
|
|
LiveKitClient.DataPacketKind.RELIABLE,
|
|
);
|
|
}
|
|
addMessage("voice", `🎤 ${r}`);
|
|
}
|
|
|
|
function scrollToBottom() {
|
|
messagesDiv.scrollTop = messagesDiv.scrollHeight;
|
|
isUserScrolling = false;
|
|
scrollToBottomBtn.classList.remove("visible");
|
|
}
|
|
|
|
window.addEventListener("load", () => {
|
|
console.log("Page loaded, initializing...");
|
|
|
|
// Re-get elements after DOM is ready
|
|
messagesDiv = document.getElementById("messages");
|
|
input = document.getElementById("messageInput");
|
|
sendBtn = document.getElementById("sendBtn");
|
|
voiceBtn = document.getElementById("voiceBtn");
|
|
|
|
console.log("After load - input:", !!input);
|
|
console.log("After load - sendBtn:", !!sendBtn);
|
|
|
|
// Attach event listeners after DOM is ready
|
|
if (sendBtn) {
|
|
sendBtn.onclick = () => {
|
|
console.log("Send button clicked!");
|
|
sendMessage();
|
|
};
|
|
console.log("sendBtn.onclick attached");
|
|
} else {
|
|
console.error("sendBtn element not found!");
|
|
}
|
|
|
|
if (input) {
|
|
input.addEventListener("keypress", (e) => {
|
|
console.log("Key pressed:", e.key);
|
|
if (e.key === "Enter") {
|
|
console.log("Enter key detected, sending message");
|
|
sendMessage();
|
|
}
|
|
});
|
|
console.log("input keypress listener attached");
|
|
} else {
|
|
console.error("input element not found!");
|
|
}
|
|
|
|
initializeAuth();
|
|
});
|
|
window.addEventListener("focus", function () {
|
|
if (!ws || ws.readyState !== WebSocket.OPEN) {
|
|
connectWebSocket();
|
|
}
|
|
});
|
|
</script>
|
|
</body>
|
|
|
|
</html> |