botserver/ui/minimal/index.html

1718 lines
64 KiB
HTML
Raw Normal View History

<!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>
// 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>
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/gsap.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/livekit-client/dist/livekit-client.umd.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<style>
@import url("https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500&display=swap");
:root {
--bg: #ffffff;
--fg: #000000;
--border: #e0e0e0;
--accent: #0066ff;
--glass: rgba(0, 0, 0, 0.02);
--shadow: rgba(0, 0, 0, 0.05);
--logo-url: url("https://pragmatismo.com.br/icons/general-bots.svg");
--gradient-1: linear-gradient(
135deg,
rgba(0, 102, 255, 0.05) 0%,
rgba(0, 102, 255, 0) 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="float-menu">
<div class="float-logo" id="floatLogo" title="Menu"></div>
<div class="menu-button" id="themeBtn" title="Theme"></div>
</div>
<div class="sidebar" id="sidebar">
<div class="sidebar-header">
<div class="sidebar-logo"></div>
<div class="sidebar-title" id="sidebarTitle">General Bots</div>
</div>
<button
class="sidebar-button"
id="voiceToggle"
onclick="toggleVoiceMode()"
>
🎤 Voice Mode
</button>
<div class="history-section">
<div class="history-title">History</div>
<div id="history"></div>
</div>
</div>
<main id="messages"></main>
<footer>
<div class="suggestions-container" id="suggestions"></div>
<div class="input-container">
<input
id="messageInput"
type="text"
placeholder="Message..."
autofocus
/>
<button id="voiceBtn" title="Voice">🎤</button>
<button id="sendBtn" title="Send"></button>
</div>
</footer>
<button class="scroll-to-bottom" id="scrollToBottom"></button>
<div class="context-indicator" id="contextIndicator">
<div>Context</div>
<div id="contextPercentage">0%</div>
<div class="context-progress">
<div
class="context-progress-bar"
id="contextProgressBar"
style="width: 0%"
></div>
</div>
</div>
<script>
let ws = null,
currentSessionId = null,
currentUserId = null,
currentBotId = "default_bot",
isStreaming = false,
voiceRoom = null,
isVoiceMode = false,
mediaRecorder = null,
audioChunks = [],
streamingMessageId = null,
isThinking = false,
currentStreamingContent = "",
hasReceivedInitialMessage = false,
reconnectAttempts = 0,
reconnectTimeout = null,
thinkingTimeout = null,
currentTheme = "auto",
themeColor1 = null,
themeColor2 = null,
customLogoUrl = null,
contextUsage = 0,
isUserScrolling = false,
autoScrollEnabled = true,
isContextChange = false;
const maxReconnectAttempts = 5;
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"),
floatLogo = document.getElementById("floatLogo"),
sidebar = document.getElementById("sidebar"),
themeBtn = document.getElementById("themeBtn"),
scrollToBottomBtn = document.getElementById("scrollToBottom"),
contextIndicator = document.getElementById("contextIndicator"),
contextPercentage =
document.getElementById("contextPercentage"),
contextProgressBar =
document.getElementById("contextProgressBar"),
sidebarTitle = document.getElementById("sidebarTitle");
marked.setOptions({ breaks: true, gfm: true });
floatLogo.addEventListener("click", toggleSidebar);
function toggleSidebar() {
sidebar.classList.toggle("open");
}
function toggleTheme() {
const themes = ["auto", "dark", "light"];
const savedTheme = localStorage.getItem("gb-theme") || "auto";
const idx = themes.indexOf(savedTheme);
const newTheme = themes[(idx + 1) % themes.length];
localStorage.setItem("gb-theme", newTheme);
currentTheme = newTheme;
applyTheme();
updateThemeButton();
}
function updateThemeButton() {
const icons = { auto: "⚙", dark: "🌙", light: "☀️" };
themeBtn.textContent = icons[currentTheme] || "⚙";
}
function applyTheme() {
const prefersDark = window.matchMedia(
"(prefers-color-scheme: dark)",
).matches;
let theme = currentTheme;
if (theme === "auto") {
theme = prefersDark ? "dark" : "light";
}
document.documentElement.setAttribute("data-theme", theme);
if (themeColor1 && themeColor2) {
const root = document.documentElement;
root.style.setProperty(
"--bg",
theme === "dark" ? themeColor2 : themeColor1,
);
root.style.setProperty(
"--fg",
theme === "dark" ? themeColor1 : themeColor2,
);
}
if (customLogoUrl) {
document.documentElement.style.setProperty(
"--logo-url",
`url('${customLogoUrl}')`,
);
}
}
window.addEventListener("load", function () {
const savedTheme = localStorage.getItem("gb-theme") || "auto";
currentTheme = savedTheme;
applyTheme();
updateThemeButton();
window
.matchMedia("(prefers-color-scheme: dark)")
.addEventListener("change", () => {
if (currentTheme === "auto") {
applyTheme();
}
});
input.focus();
});
themeBtn.addEventListener("click", toggleTheme);
document.addEventListener("click", function (e) {
if (
sidebar.classList.contains("open") &&
!sidebar.contains(e.target) &&
!floatLogo.contains(e.target)
) {
sidebar.classList.remove("open");
}
});
messagesDiv.addEventListener("scroll", function () {
const isAtBottom =
messagesDiv.scrollHeight - messagesDiv.scrollTop <=
messagesDiv.clientHeight + 100;
if (!isAtBottom) {
isUserScrolling = true;
scrollToBottomBtn.classList.add("visible");
} else {
isUserScrolling = false;
scrollToBottomBtn.classList.remove("visible");
}
});
scrollToBottomBtn.addEventListener("click", function () {
scrollToBottom();
});
function updateContextUsage(u) {
contextUsage = u;
const p = Math.min(100, Math.round(u * 100));
contextPercentage.textContent = `${p}%`;
contextProgressBar.style.width = `${p}%`;
contextIndicator.classList.remove("visible");
}
function flashScreen() {
gsap.to(flashOverlay, {
opacity: 0.15,
duration: 0.1,
onComplete: () => {
gsap.to(flashOverlay, { opacity: 0, duration: 0.2 });
},
});
}
function updateConnectionStatus(s) {
connectionStatus.className = `connection-status ${s}`;
}
function getWebSocketUrl() {
const p = "ws:",
s = currentSessionId || crypto.randomUUID(),
u = currentUserId || crypto.randomUUID();
return `${p}//localhost:8080/ws?session_id=${s}&user_id=${u}`;
}
async function initializeAuth() {
try {
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(
`/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("http://localhost:8080/api/sessions"),
s = await r.json(),
h = document.getElementById("history");
h.innerHTML = "";
s.forEach((session) => {
const item = document.createElement("div");
item.className = "history-item";
item.textContent =
session.title ||
`Session ${session.session_id.substring(0, 8)}`;
item.onclick = () => switchSession(session.session_id);
h.appendChild(item);
});
} catch (e) {
console.error("Failed to load sessions:", e);
}
}
async function createNewSession() {
try {
const r = await fetch(
"http://localhost:8080/api/sessions",
{ method: "POST" },
),
s = await r.json();
currentSessionId = s.session_id;
hasReceivedInitialMessage = false;
connectWebSocket();
loadSessions();
messagesDiv.innerHTML = "";
clearSuggestions();
updateContextUsage(0);
if (isVoiceMode) {
await stopVoiceSession();
isVoiceMode = false;
const v = document.getElementById("voiceToggle");
v.textContent = "🎤 Voice Mode";
voiceBtn.classList.remove("recording");
}
} catch (e) {
console.error("Failed to create session:", e);
}
}
function switchSession(s) {
currentSessionId = s;
hasReceivedInitialMessage = false;
loadSessionHistory(s);
connectWebSocket();
if (isVoiceMode) {
startVoiceSession();
}
sidebar.classList.remove("open");
}
async function loadSessionHistory(s) {
try {
const r = await fetch(
"http://localhost:8080/api/sessions/" + s,
),
h = await r.json(),
m = document.getElementById("messages");
m.innerHTML = "";
if (h.length === 0) {
updateContextUsage(0);
} else {
h.forEach(([role, content]) => {
addMessage(role, content, false);
});
updateContextUsage(h.length / 20);
}
} catch (e) {
console.error("Failed to load session history:", e);
}
}
function connectWebSocket() {
if (ws) {
ws.close();
}
clearTimeout(reconnectTimeout);
const u = getWebSocketUrl();
ws = new WebSocket(u);
ws.onmessage = function (e) {
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);
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");
reconnectAttempts = 0;
hasReceivedInitialMessage = false;
};
ws.onclose = function (e) {
console.log("WebSocket disconnected:", e.code, e.reason);
updateConnectionStatus("disconnected");
if (isStreaming) {
showContinueButton();
}
if (reconnectAttempts < maxReconnectAttempts) {
reconnectAttempts++;
const d = Math.min(1000 * reconnectAttempts, 10000);
reconnectTimeout = setTimeout(() => {
updateConnectionStatus("connecting");
connectWebSocket();
}, d);
} else {
updateConnectionStatus("disconnected");
}
};
ws.onerror = function (e) {
console.error("WebSocket error:", e);
updateConnectionStatus("disconnected");
};
}
function processMessageContent(r) {
if (isContextChange) {
isContextChange = false;
return;
}
if (r.context_usage !== undefined) {
updateContextUsage(r.context_usage);
}
if (r.suggestions && r.suggestions.length > 0) {
handleSuggestions(r.suggestions);
}
if (r.is_complete) {
if (isStreaming) {
finalizeStreamingMessage();
isStreaming = false;
streamingMessageId = null;
currentStreamingContent = "";
} else {
addMessage("assistant", r.content, false);
}
} else {
if (!isStreaming) {
isStreaming = true;
streamingMessageId = "streaming-" + Date.now();
currentStreamingContent = r.content || "";
addMessage(
"assistant",
currentStreamingContent,
true,
streamingMessageId,
);
} else {
currentStreamingContent += r.content || "";
updateStreamingMessage(currentStreamingContent);
}
}
}
function handleEvent(t, d) {
console.log("Event received:", t, d);
switch (t) {
case "thinking_start":
showThinkingIndicator();
break;
case "thinking_end":
hideThinkingIndicator();
break;
case "warn":
showWarning(d.message);
break;
case "context_usage":
updateContextUsage(d.usage);
break;
case "change_theme":
if (d.color1) themeColor1 = d.color1;
if (d.color2) themeColor2 = d.color2;
if (d.logo_url) customLogoUrl = d.logo_url;
if (d.title) document.title = d.title;
if (d.logo_text) {
sidebarTitle.textContent = d.logo_text;
}
applyTheme();
break;
}
}
function showThinkingIndicator() {
if (isThinking) return;
const t = document.createElement("div");
t.id = "thinking-indicator";
t.className = "message-container";
t.innerHTML = `<div class="assistant-message"><div class="assistant-avatar"></div><div class="thinking-indicator"><div class="typing-dots"><div class="typing-dot"></div><div class="typing-dot"></div><div class="typing-dot"></div></div></div></div>`;
messagesDiv.appendChild(t);
gsap.to(t, {
opacity: 1,
y: 0,
duration: 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(
"http://localhost:8080/api/voice/start",
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
session_id: currentSessionId,
user_id: currentUserId,
}),
},
),
d = await r.json();
if (d.token) {
await connectToVoiceRoom(d.token);
startVoiceRecording();
}
} catch (e) {
console.error("Failed to start voice session:", e);
showWarning("Falha ao iniciar modo de voz");
}
}
async function stopVoiceSession() {
if (!currentSessionId) return;
try {
await fetch("http://localhost:8080/api/voice/stop", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ session_id: currentSessionId }),
});
if (voiceRoom) {
voiceRoom.disconnect();
voiceRoom = null;
}
if (mediaRecorder && mediaRecorder.state === "recording") {
mediaRecorder.stop();
}
} catch (e) {
console.error("Failed to stop voice session:", e);
}
}
async function connectToVoiceRoom(t) {
try {
const r = new LiveKitClient.Room(),
p = "ws:",
u = `${p}//localhost:8080/voice`;
await r.connect(u, t);
voiceRoom = r;
r.on("dataReceived", (d) => {
const dc = new TextDecoder(),
m = dc.decode(d);
try {
const j = JSON.parse(m);
if (j.type === "voice_response") {
addMessage("assistant", j.text);
}
} catch (e) {
console.log("Voice data:", m);
}
});
const l = await LiveKitClient.createLocalTracks({
audio: true,
video: false,
});
for (const k of l) {
await r.localParticipant.publishTrack(k);
}
} catch (e) {
console.error("Failed to connect to voice room:", e);
showWarning("Falha na conexão de voz");
}
}
function startVoiceRecording() {
if (!navigator.mediaDevices) {
console.log("Media devices not supported");
return;
}
navigator.mediaDevices
.getUserMedia({ audio: true })
.then((s) => {
mediaRecorder = new MediaRecorder(s);
audioChunks = [];
mediaRecorder.ondataavailable = (e) => {
audioChunks.push(e.data);
};
mediaRecorder.onstop = () => {
const a = new Blob(audioChunks, {
type: "audio/wav",
});
simulateVoiceTranscription();
};
mediaRecorder.start();
setTimeout(() => {
if (
mediaRecorder &&
mediaRecorder.state === "recording"
) {
mediaRecorder.stop();
setTimeout(() => {
if (isVoiceMode) {
startVoiceRecording();
}
}, 1000);
}
}, 5000);
})
.catch((e) => {
console.error("Error accessing microphone:", e);
showWarning("Erro ao acessar microfone");
});
}
function simulateVoiceTranscription() {
const p = [
"Olá, como posso ajudá-lo hoje?",
"Entendo o que você está dizendo",
"Esse é um ponto interessante",
"Deixe-me pensar sobre isso",
"Posso ajudá-lo com isso",
"O que você gostaria de saber?",
"Isso parece ótimo",
"Estou ouvindo sua voz",
],
r = p[Math.floor(Math.random() * p.length)];
if (voiceRoom) {
const m = {
type: "voice_input",
content: r,
timestamp: new Date().toISOString(),
};
voiceRoom.localParticipant.publishData(
new TextEncoder().encode(JSON.stringify(m)),
LiveKitClient.DataPacketKind.RELIABLE,
);
}
addMessage("voice", `🎤 ${r}`);
}
function scrollToBottom() {
messagesDiv.scrollTop = messagesDiv.scrollHeight;
isUserScrolling = false;
scrollToBottomBtn.classList.remove("visible");
}
window.addEventListener("load", () => {
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>