920 lines
32 KiB
HTML
920 lines
32 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="UTF-8" />
|
||
<title>Lyra Core Chat</title>
|
||
<link rel="stylesheet" href="style.css" />
|
||
<!-- PWA -->
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
||
<meta name="mobile-web-app-capable" content="yes" />
|
||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
||
<link rel="manifest" href="manifest.json" />
|
||
|
||
</head>
|
||
<body>
|
||
<!-- Mobile Menu Overlay -->
|
||
<div class="mobile-menu-overlay" id="mobileMenuOverlay"></div>
|
||
|
||
<!-- Mobile Slide-out Menu -->
|
||
<div class="mobile-menu" id="mobileMenu">
|
||
<div class="mobile-menu-section">
|
||
<h4>Mode</h4>
|
||
<select id="mobileMode">
|
||
<option value="standard">Standard</option>
|
||
<option value="cortex">Cortex</option>
|
||
</select>
|
||
</div>
|
||
|
||
<div class="mobile-menu-section">
|
||
<h4>Session</h4>
|
||
<select id="mobileSessions"></select>
|
||
<button id="mobileNewSessionBtn">➕ New Session</button>
|
||
<button id="mobileRenameSessionBtn">✏️ Rename Session</button>
|
||
</div>
|
||
|
||
<div class="mobile-menu-section">
|
||
<h4>Actions</h4>
|
||
<button id="mobileThinkingStreamBtn">🧠 Show Work</button>
|
||
<button id="mobileSettingsBtn">⚙ Settings</button>
|
||
<button id="mobileToggleThemeBtn">🌙 Toggle Theme</button>
|
||
<button id="mobileForceReloadBtn">🔄 Force Reload</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="chat">
|
||
<!-- Mode selector -->
|
||
<div id="model-select">
|
||
<!-- Hamburger menu (mobile only) -->
|
||
<button class="hamburger-menu" id="hamburgerMenu" aria-label="Menu">
|
||
<span></span>
|
||
<span></span>
|
||
<span></span>
|
||
</button>
|
||
<label for="mode">Mode:</label>
|
||
<select id="mode">
|
||
<option value="standard">Standard</option>
|
||
<option value="cortex">Cortex</option>
|
||
</select>
|
||
<button id="settingsBtn" style="margin-left: auto;">⚙ Settings</button>
|
||
<div id="theme-toggle">
|
||
<button id="toggleThemeBtn">🌙 Dark Mode</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Session selector -->
|
||
<div id="session-select">
|
||
<label for="sessions">Session:</label>
|
||
<select id="sessions"></select>
|
||
<button id="newSessionBtn">➕ New</button>
|
||
<button id="renameSessionBtn">✏️ Rename</button>
|
||
<button id="thinkingStreamBtn" title="Show thinking stream panel">🧠 Show Work</button>
|
||
</div>
|
||
|
||
<!-- Status -->
|
||
<div id="status">
|
||
<span id="status-dot"></span>
|
||
<span id="status-text">Checking Relay...</span>
|
||
</div>
|
||
|
||
<!-- Chat messages -->
|
||
<div id="messages"></div>
|
||
|
||
<!-- Thinking Stream Panel (collapsible) -->
|
||
<div id="thinkingPanel" class="thinking-panel collapsed">
|
||
<div class="thinking-header" id="thinkingHeader">
|
||
<span>🧠 Thinking Stream</span>
|
||
<div class="thinking-controls">
|
||
<span class="thinking-status-dot" id="thinkingStatusDot"></span>
|
||
<button class="thinking-clear-btn" id="thinkingClearBtn" title="Clear events">🗑️</button>
|
||
<button class="thinking-toggle-btn" id="thinkingToggleBtn">▼</button>
|
||
</div>
|
||
</div>
|
||
<div class="thinking-content" id="thinkingContent">
|
||
<div class="thinking-empty" id="thinkingEmpty">
|
||
<div class="thinking-empty-icon">🤔</div>
|
||
<p>Waiting for thinking events...</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Input box -->
|
||
<div id="input">
|
||
<input id="userInput" type="text" placeholder="Type a message..." autofocus />
|
||
<button id="sendBtn">Send</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Settings Modal (outside chat container) -->
|
||
<div id="settingsModal" class="modal">
|
||
<div class="modal-overlay"></div>
|
||
<div class="modal-content">
|
||
<div class="modal-header">
|
||
<h3>Settings</h3>
|
||
<button id="closeModalBtn" class="close-btn">✕</button>
|
||
</div>
|
||
<div class="modal-body">
|
||
<div class="settings-section">
|
||
<h4>Standard Mode Backend</h4>
|
||
<p class="settings-desc">Select which LLM backend to use for Standard Mode:</p>
|
||
<div class="radio-group">
|
||
<label class="radio-label">
|
||
<input type="radio" name="backend" value="SECONDARY" checked>
|
||
<span>SECONDARY - Ollama/Qwen (3090)</span>
|
||
<small>Fast, local, good for general chat</small>
|
||
</label>
|
||
<label class="radio-label">
|
||
<input type="radio" name="backend" value="OPENAI">
|
||
<span>OPENAI - GPT-4o-mini</span>
|
||
<small>Cloud-based, high quality (costs money)</small>
|
||
</label>
|
||
<label class="radio-label">
|
||
<input type="radio" name="backend" value="custom">
|
||
<span>Custom Backend</span>
|
||
<input type="text" id="customBackend" placeholder="e.g., PRIMARY, FALLBACK" />
|
||
</label>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="settings-section" style="margin-top: 24px;">
|
||
<h4>Session Management</h4>
|
||
<p class="settings-desc">Manage your saved chat sessions:</p>
|
||
<div id="sessionList" class="session-list">
|
||
<p style="color: var(--text-fade); font-size: 0.85rem;">Loading sessions...</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="modal-footer">
|
||
<button id="saveSettingsBtn" class="primary-btn">Save</button>
|
||
<button id="cancelSettingsBtn">Cancel</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
const RELAY_BASE = "http://10.0.0.41:7078";
|
||
const API_URL = `${RELAY_BASE}/v1/chat/completions`;
|
||
|
||
function generateSessionId() {
|
||
return "sess-" + Math.random().toString(36).substring(2, 10);
|
||
}
|
||
|
||
let history = [];
|
||
let currentSession = localStorage.getItem("currentSession") || null;
|
||
let sessions = []; // Now loaded from server
|
||
|
||
async function loadSessionsFromServer() {
|
||
try {
|
||
const resp = await fetch(`${RELAY_BASE}/sessions`);
|
||
const serverSessions = await resp.json();
|
||
sessions = serverSessions;
|
||
return sessions;
|
||
} catch (e) {
|
||
console.error("Failed to load sessions from server:", e);
|
||
return [];
|
||
}
|
||
}
|
||
|
||
async function renderSessions() {
|
||
const select = document.getElementById("sessions");
|
||
const mobileSelect = document.getElementById("mobileSessions");
|
||
select.innerHTML = "";
|
||
mobileSelect.innerHTML = "";
|
||
|
||
sessions.forEach(s => {
|
||
const opt = document.createElement("option");
|
||
opt.value = s.id;
|
||
opt.textContent = s.name || s.id;
|
||
if (s.id === currentSession) opt.selected = true;
|
||
select.appendChild(opt);
|
||
|
||
// Clone for mobile menu
|
||
const mobileOpt = opt.cloneNode(true);
|
||
mobileSelect.appendChild(mobileOpt);
|
||
});
|
||
}
|
||
|
||
function getSessionName(id) {
|
||
const s = sessions.find(s => s.id === id);
|
||
return s ? (s.name || s.id) : id;
|
||
}
|
||
|
||
async function saveSessionMetadata(sessionId, name) {
|
||
try {
|
||
await fetch(`${RELAY_BASE}/sessions/${sessionId}/metadata`, {
|
||
method: "PATCH",
|
||
headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify({ name })
|
||
});
|
||
return true;
|
||
} catch (e) {
|
||
console.error("Failed to save session metadata:", e);
|
||
return false;
|
||
}
|
||
}
|
||
|
||
async function loadSession(id) {
|
||
try {
|
||
const res = await fetch(`${RELAY_BASE}/sessions/${id}`);
|
||
const data = await res.json();
|
||
history = Array.isArray(data) ? data : [];
|
||
const messagesEl = document.getElementById("messages");
|
||
messagesEl.innerHTML = "";
|
||
history.forEach(m => addMessage(m.role, m.content));
|
||
addMessage("system", `📂 Loaded session: ${getSessionName(id)} — ${history.length} message(s)`);
|
||
} catch (e) {
|
||
addMessage("system", `Failed to load session: ${e.message}`);
|
||
}
|
||
}
|
||
|
||
async function saveSession() {
|
||
if (!currentSession) return;
|
||
try {
|
||
await fetch(`${RELAY_BASE}/sessions/${currentSession}`, {
|
||
method: "POST",
|
||
headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify(history)
|
||
});
|
||
} catch (e) {
|
||
addMessage("system", `Failed to save session: ${e.message}`);
|
||
}
|
||
}
|
||
|
||
async function sendMessage() {
|
||
const inputEl = document.getElementById("userInput");
|
||
const msg = inputEl.value.trim();
|
||
if (!msg) return;
|
||
inputEl.value = "";
|
||
|
||
addMessage("user", msg);
|
||
history.push({ role: "user", content: msg });
|
||
await saveSession(); // ✅ persist both user + assistant messages
|
||
|
||
|
||
const mode = document.getElementById("mode").value;
|
||
|
||
// make sure we always include a stable user_id
|
||
let userId = localStorage.getItem("userId");
|
||
if (!userId) {
|
||
userId = "brian"; // use whatever ID you seeded Mem0 with
|
||
localStorage.setItem("userId", userId);
|
||
}
|
||
|
||
// Get backend preference for Standard Mode
|
||
let backend = null;
|
||
if (mode === "standard") {
|
||
backend = localStorage.getItem("standardModeBackend") || "SECONDARY";
|
||
}
|
||
|
||
const body = {
|
||
mode: mode,
|
||
messages: history,
|
||
sessionId: currentSession
|
||
};
|
||
|
||
// Only add backend if in standard mode
|
||
if (backend) {
|
||
body.backend = backend;
|
||
}
|
||
|
||
try {
|
||
const resp = await fetch(API_URL, {
|
||
method: "POST",
|
||
headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify(body)
|
||
});
|
||
|
||
const data = await resp.json();
|
||
const reply = data.choices?.[0]?.message?.content || "(no reply)";
|
||
addMessage("assistant", reply);
|
||
history.push({ role: "assistant", content: reply });
|
||
await saveSession();
|
||
} catch (err) {
|
||
addMessage("system", "Error: " + err.message);
|
||
}
|
||
}
|
||
|
||
function addMessage(role, text) {
|
||
const messagesEl = document.getElementById("messages");
|
||
|
||
const msgDiv = document.createElement("div");
|
||
msgDiv.className = `msg ${role}`;
|
||
msgDiv.textContent = text;
|
||
messagesEl.appendChild(msgDiv);
|
||
|
||
// only auto-scroll if user is near bottom
|
||
const threshold = 120;
|
||
const isNearBottom = messagesEl.scrollHeight - messagesEl.scrollTop - messagesEl.clientHeight < threshold;
|
||
if (isNearBottom) {
|
||
messagesEl.scrollTo({ top: messagesEl.scrollHeight, behavior: "smooth" });
|
||
}
|
||
}
|
||
|
||
|
||
async function checkHealth() {
|
||
try {
|
||
const resp = await fetch(API_URL.replace("/v1/chat/completions", "/_health"));
|
||
if (resp.ok) {
|
||
document.getElementById("status-dot").className = "dot ok";
|
||
document.getElementById("status-text").textContent = "Relay Online";
|
||
} else {
|
||
throw new Error("Bad status");
|
||
}
|
||
} catch (err) {
|
||
document.getElementById("status-dot").className = "dot fail";
|
||
document.getElementById("status-text").textContent = "Relay Offline";
|
||
}
|
||
}
|
||
|
||
document.addEventListener("DOMContentLoaded", () => {
|
||
// Mobile Menu Toggle
|
||
const hamburgerMenu = document.getElementById("hamburgerMenu");
|
||
const mobileMenu = document.getElementById("mobileMenu");
|
||
const mobileMenuOverlay = document.getElementById("mobileMenuOverlay");
|
||
|
||
function toggleMobileMenu() {
|
||
mobileMenu.classList.toggle("open");
|
||
mobileMenuOverlay.classList.toggle("show");
|
||
hamburgerMenu.classList.toggle("active");
|
||
}
|
||
|
||
function closeMobileMenu() {
|
||
mobileMenu.classList.remove("open");
|
||
mobileMenuOverlay.classList.remove("show");
|
||
hamburgerMenu.classList.remove("active");
|
||
}
|
||
|
||
hamburgerMenu.addEventListener("click", toggleMobileMenu);
|
||
mobileMenuOverlay.addEventListener("click", closeMobileMenu);
|
||
|
||
// Sync mobile menu controls with desktop
|
||
const mobileMode = document.getElementById("mobileMode");
|
||
const desktopMode = document.getElementById("mode");
|
||
|
||
// Sync mode selection
|
||
mobileMode.addEventListener("change", (e) => {
|
||
desktopMode.value = e.target.value;
|
||
desktopMode.dispatchEvent(new Event("change"));
|
||
});
|
||
|
||
desktopMode.addEventListener("change", (e) => {
|
||
mobileMode.value = e.target.value;
|
||
});
|
||
|
||
// Mobile theme toggle
|
||
document.getElementById("mobileToggleThemeBtn").addEventListener("click", () => {
|
||
document.getElementById("toggleThemeBtn").click();
|
||
updateMobileThemeButton();
|
||
});
|
||
|
||
function updateMobileThemeButton() {
|
||
const isDark = document.body.classList.contains("dark");
|
||
document.getElementById("mobileToggleThemeBtn").textContent = isDark ? "☀️ Light Mode" : "🌙 Dark Mode";
|
||
}
|
||
|
||
// Mobile settings button
|
||
document.getElementById("mobileSettingsBtn").addEventListener("click", () => {
|
||
closeMobileMenu();
|
||
document.getElementById("settingsBtn").click();
|
||
});
|
||
|
||
// Mobile thinking stream button
|
||
document.getElementById("mobileThinkingStreamBtn").addEventListener("click", () => {
|
||
closeMobileMenu();
|
||
document.getElementById("thinkingStreamBtn").click();
|
||
});
|
||
|
||
// Mobile new session button
|
||
document.getElementById("mobileNewSessionBtn").addEventListener("click", () => {
|
||
closeMobileMenu();
|
||
document.getElementById("newSessionBtn").click();
|
||
});
|
||
|
||
// Mobile rename session button
|
||
document.getElementById("mobileRenameSessionBtn").addEventListener("click", () => {
|
||
closeMobileMenu();
|
||
document.getElementById("renameSessionBtn").click();
|
||
});
|
||
|
||
// Sync mobile session selector with desktop
|
||
document.getElementById("mobileSessions").addEventListener("change", async (e) => {
|
||
closeMobileMenu();
|
||
const desktopSessions = document.getElementById("sessions");
|
||
desktopSessions.value = e.target.value;
|
||
desktopSessions.dispatchEvent(new Event("change"));
|
||
});
|
||
|
||
// Mobile force reload button
|
||
document.getElementById("mobileForceReloadBtn").addEventListener("click", async () => {
|
||
if (confirm("Force reload the app? This will clear cache and reload.")) {
|
||
// Clear all caches if available
|
||
if ('caches' in window) {
|
||
const cacheNames = await caches.keys();
|
||
await Promise.all(cacheNames.map(name => caches.delete(name)));
|
||
}
|
||
|
||
// Force reload from server (bypass cache)
|
||
window.location.reload(true);
|
||
}
|
||
});
|
||
|
||
// Dark mode toggle - defaults to dark
|
||
const btn = document.getElementById("toggleThemeBtn");
|
||
|
||
// Set dark mode by default if no preference saved
|
||
const savedTheme = localStorage.getItem("theme");
|
||
if (!savedTheme || savedTheme === "dark") {
|
||
document.body.classList.add("dark");
|
||
btn.textContent = "☀️ Light Mode";
|
||
localStorage.setItem("theme", "dark");
|
||
} else {
|
||
btn.textContent = "🌙 Dark Mode";
|
||
}
|
||
|
||
btn.addEventListener("click", () => {
|
||
document.body.classList.toggle("dark");
|
||
const isDark = document.body.classList.contains("dark");
|
||
btn.textContent = isDark ? "☀️ Light Mode" : "🌙 Dark Mode";
|
||
localStorage.setItem("theme", isDark ? "dark" : "light");
|
||
updateMobileThemeButton();
|
||
});
|
||
|
||
// Initialize mobile theme button
|
||
updateMobileThemeButton();
|
||
|
||
// Sessions - Load from server
|
||
(async () => {
|
||
await loadSessionsFromServer();
|
||
await renderSessions();
|
||
|
||
// Ensure we have at least one session
|
||
if (sessions.length === 0) {
|
||
const id = generateSessionId();
|
||
const name = "default";
|
||
currentSession = id;
|
||
history = [];
|
||
await saveSession(); // Create empty session on server
|
||
await saveSessionMetadata(id, name);
|
||
await loadSessionsFromServer();
|
||
await renderSessions();
|
||
localStorage.setItem("currentSession", currentSession);
|
||
} else {
|
||
// If no current session or current session doesn't exist, use first one
|
||
if (!currentSession || !sessions.find(s => s.id === currentSession)) {
|
||
currentSession = sessions[0].id;
|
||
localStorage.setItem("currentSession", currentSession);
|
||
}
|
||
}
|
||
|
||
// Load current session history
|
||
if (currentSession) {
|
||
await loadSession(currentSession);
|
||
}
|
||
})();
|
||
|
||
// Switch session
|
||
document.getElementById("sessions").addEventListener("change", async e => {
|
||
currentSession = e.target.value;
|
||
history = [];
|
||
localStorage.setItem("currentSession", currentSession);
|
||
addMessage("system", `Switched to session: ${getSessionName(currentSession)}`);
|
||
await loadSession(currentSession);
|
||
});
|
||
|
||
// Create new session
|
||
document.getElementById("newSessionBtn").addEventListener("click", async () => {
|
||
const name = prompt("Enter new session name:");
|
||
if (!name) return;
|
||
const id = generateSessionId();
|
||
currentSession = id;
|
||
history = [];
|
||
localStorage.setItem("currentSession", currentSession);
|
||
|
||
// Create session on server
|
||
await saveSession();
|
||
await saveSessionMetadata(id, name);
|
||
await loadSessionsFromServer();
|
||
await renderSessions();
|
||
|
||
addMessage("system", `Created session: ${name}`);
|
||
});
|
||
|
||
// Rename session
|
||
document.getElementById("renameSessionBtn").addEventListener("click", async () => {
|
||
const session = sessions.find(s => s.id === currentSession);
|
||
if (!session) return;
|
||
const newName = prompt("Rename session:", session.name || currentSession);
|
||
if (!newName) return;
|
||
|
||
// Update metadata on server
|
||
await saveSessionMetadata(currentSession, newName);
|
||
await loadSessionsFromServer();
|
||
await renderSessions();
|
||
|
||
addMessage("system", `Session renamed to: ${newName}`);
|
||
});
|
||
|
||
// Thinking Stream button
|
||
document.getElementById("thinkingStreamBtn").addEventListener("click", () => {
|
||
if (!currentSession) {
|
||
alert("Please select a session first");
|
||
return;
|
||
}
|
||
|
||
// Open thinking stream in new window
|
||
const streamUrl = `http://10.0.0.41:8081/thinking-stream.html?session=${currentSession}`;
|
||
const windowFeatures = "width=600,height=800,menubar=no,toolbar=no,location=no,status=no";
|
||
window.open(streamUrl, `thinking_${currentSession}`, windowFeatures);
|
||
|
||
addMessage("system", "🧠 Opened thinking stream in new window");
|
||
});
|
||
|
||
|
||
// Settings Modal
|
||
const settingsModal = document.getElementById("settingsModal");
|
||
const settingsBtn = document.getElementById("settingsBtn");
|
||
const closeModalBtn = document.getElementById("closeModalBtn");
|
||
const saveSettingsBtn = document.getElementById("saveSettingsBtn");
|
||
const cancelSettingsBtn = document.getElementById("cancelSettingsBtn");
|
||
const modalOverlay = document.querySelector(".modal-overlay");
|
||
|
||
// Load saved backend preference
|
||
const savedBackend = localStorage.getItem("standardModeBackend") || "SECONDARY";
|
||
|
||
// Set initial radio button state
|
||
const backendRadios = document.querySelectorAll('input[name="backend"]');
|
||
let isCustomBackend = !["SECONDARY", "OPENAI"].includes(savedBackend);
|
||
|
||
if (isCustomBackend) {
|
||
document.querySelector('input[name="backend"][value="custom"]').checked = true;
|
||
document.getElementById("customBackend").value = savedBackend;
|
||
} else {
|
||
document.querySelector(`input[name="backend"][value="${savedBackend}"]`).checked = true;
|
||
}
|
||
|
||
// Session management functions
|
||
async function loadSessionList() {
|
||
try {
|
||
// Reload from server to get latest
|
||
await loadSessionsFromServer();
|
||
|
||
const sessionListEl = document.getElementById("sessionList");
|
||
if (sessions.length === 0) {
|
||
sessionListEl.innerHTML = '<p style="color: var(--text-fade); font-size: 0.85rem;">No saved sessions found</p>';
|
||
return;
|
||
}
|
||
|
||
sessionListEl.innerHTML = "";
|
||
sessions.forEach(sess => {
|
||
const sessionItem = document.createElement("div");
|
||
sessionItem.className = "session-item";
|
||
|
||
const sessionInfo = document.createElement("div");
|
||
sessionInfo.className = "session-info";
|
||
|
||
const sessionName = sess.name || sess.id;
|
||
const lastModified = new Date(sess.lastModified).toLocaleString();
|
||
|
||
sessionInfo.innerHTML = `
|
||
<strong>${sessionName}</strong>
|
||
<small>${sess.messageCount} messages • ${lastModified}</small>
|
||
`;
|
||
|
||
const deleteBtn = document.createElement("button");
|
||
deleteBtn.className = "session-delete-btn";
|
||
deleteBtn.textContent = "🗑️";
|
||
deleteBtn.title = "Delete session";
|
||
deleteBtn.onclick = async () => {
|
||
if (!confirm(`Delete session "${sessionName}"?`)) return;
|
||
|
||
try {
|
||
await fetch(`${RELAY_BASE}/sessions/${sess.id}`, { method: "DELETE" });
|
||
|
||
// Reload sessions from server
|
||
await loadSessionsFromServer();
|
||
|
||
// If we deleted the current session, switch to another or create new
|
||
if (currentSession === sess.id) {
|
||
if (sessions.length > 0) {
|
||
currentSession = sessions[0].id;
|
||
localStorage.setItem("currentSession", currentSession);
|
||
history = [];
|
||
await loadSession(currentSession);
|
||
} else {
|
||
const id = generateSessionId();
|
||
const name = "default";
|
||
currentSession = id;
|
||
localStorage.setItem("currentSession", currentSession);
|
||
history = [];
|
||
await saveSession();
|
||
await saveSessionMetadata(id, name);
|
||
await loadSessionsFromServer();
|
||
}
|
||
}
|
||
|
||
// Refresh both the dropdown and the settings list
|
||
await renderSessions();
|
||
await loadSessionList();
|
||
|
||
addMessage("system", `Deleted session: ${sessionName}`);
|
||
} catch (e) {
|
||
alert("Failed to delete session: " + e.message);
|
||
}
|
||
};
|
||
|
||
sessionItem.appendChild(sessionInfo);
|
||
sessionItem.appendChild(deleteBtn);
|
||
sessionListEl.appendChild(sessionItem);
|
||
});
|
||
} catch (e) {
|
||
const sessionListEl = document.getElementById("sessionList");
|
||
sessionListEl.innerHTML = '<p style="color: #ff3333; font-size: 0.85rem;">Failed to load sessions</p>';
|
||
}
|
||
}
|
||
|
||
// Show modal and load session list
|
||
settingsBtn.addEventListener("click", () => {
|
||
settingsModal.classList.add("show");
|
||
loadSessionList(); // Refresh session list when opening settings
|
||
});
|
||
|
||
// Hide modal functions
|
||
const hideModal = () => {
|
||
settingsModal.classList.remove("show");
|
||
};
|
||
|
||
closeModalBtn.addEventListener("click", hideModal);
|
||
cancelSettingsBtn.addEventListener("click", hideModal);
|
||
modalOverlay.addEventListener("click", hideModal);
|
||
|
||
// ESC key to close
|
||
document.addEventListener("keydown", (e) => {
|
||
if (e.key === "Escape" && settingsModal.classList.contains("show")) {
|
||
hideModal();
|
||
}
|
||
});
|
||
|
||
// Save settings
|
||
saveSettingsBtn.addEventListener("click", () => {
|
||
const selectedRadio = document.querySelector('input[name="backend"]:checked');
|
||
let backendValue;
|
||
|
||
if (selectedRadio.value === "custom") {
|
||
backendValue = document.getElementById("customBackend").value.trim().toUpperCase();
|
||
if (!backendValue) {
|
||
alert("Please enter a custom backend name");
|
||
return;
|
||
}
|
||
} else {
|
||
backendValue = selectedRadio.value;
|
||
}
|
||
|
||
localStorage.setItem("standardModeBackend", backendValue);
|
||
addMessage("system", `Backend changed to: ${backendValue}`);
|
||
hideModal();
|
||
});
|
||
|
||
// Health check
|
||
checkHealth();
|
||
setInterval(checkHealth, 10000);
|
||
|
||
// Input events
|
||
document.getElementById("sendBtn").addEventListener("click", sendMessage);
|
||
document.getElementById("userInput").addEventListener("keypress", e => {
|
||
if (e.key === "Enter") sendMessage();
|
||
});
|
||
|
||
// ========== THINKING STREAM INTEGRATION ==========
|
||
const thinkingPanel = document.getElementById("thinkingPanel");
|
||
const thinkingHeader = document.getElementById("thinkingHeader");
|
||
const thinkingToggleBtn = document.getElementById("thinkingToggleBtn");
|
||
const thinkingClearBtn = document.getElementById("thinkingClearBtn");
|
||
const thinkingContent = document.getElementById("thinkingContent");
|
||
const thinkingStatusDot = document.getElementById("thinkingStatusDot");
|
||
const thinkingEmpty = document.getElementById("thinkingEmpty");
|
||
|
||
let thinkingEventSource = null;
|
||
let thinkingEventCount = 0;
|
||
const CORTEX_BASE = "http://10.0.0.41:7081";
|
||
|
||
// Load thinking panel state from localStorage
|
||
const isPanelCollapsed = localStorage.getItem("thinkingPanelCollapsed") === "true";
|
||
if (!isPanelCollapsed) {
|
||
thinkingPanel.classList.remove("collapsed");
|
||
}
|
||
|
||
// Toggle thinking panel
|
||
thinkingHeader.addEventListener("click", (e) => {
|
||
if (e.target === thinkingClearBtn) return; // Don't toggle if clicking clear
|
||
thinkingPanel.classList.toggle("collapsed");
|
||
localStorage.setItem("thinkingPanelCollapsed", thinkingPanel.classList.contains("collapsed"));
|
||
});
|
||
|
||
// Clear thinking events
|
||
thinkingClearBtn.addEventListener("click", (e) => {
|
||
e.stopPropagation();
|
||
clearThinkingEvents();
|
||
});
|
||
|
||
function clearThinkingEvents() {
|
||
thinkingContent.innerHTML = '';
|
||
thinkingContent.appendChild(thinkingEmpty);
|
||
thinkingEventCount = 0;
|
||
// Clear from localStorage
|
||
if (currentSession) {
|
||
localStorage.removeItem(`thinkingEvents_${currentSession}`);
|
||
}
|
||
}
|
||
|
||
function connectThinkingStream() {
|
||
if (!currentSession) return;
|
||
|
||
// Close existing connection
|
||
if (thinkingEventSource) {
|
||
thinkingEventSource.close();
|
||
}
|
||
|
||
// Load persisted events
|
||
loadThinkingEvents();
|
||
|
||
const url = `${CORTEX_BASE}/stream/thinking/${currentSession}`;
|
||
console.log('Connecting thinking stream:', url);
|
||
|
||
thinkingEventSource = new EventSource(url);
|
||
|
||
thinkingEventSource.onopen = () => {
|
||
console.log('Thinking stream connected');
|
||
thinkingStatusDot.className = 'thinking-status-dot connected';
|
||
};
|
||
|
||
thinkingEventSource.onmessage = (event) => {
|
||
try {
|
||
const data = JSON.parse(event.data);
|
||
addThinkingEvent(data);
|
||
saveThinkingEvent(data); // Persist event
|
||
} catch (e) {
|
||
console.error('Failed to parse thinking event:', e);
|
||
}
|
||
};
|
||
|
||
thinkingEventSource.onerror = (error) => {
|
||
console.error('Thinking stream error:', error);
|
||
thinkingStatusDot.className = 'thinking-status-dot disconnected';
|
||
|
||
// Retry connection after 2 seconds
|
||
setTimeout(() => {
|
||
if (thinkingEventSource && thinkingEventSource.readyState === EventSource.CLOSED) {
|
||
console.log('Reconnecting thinking stream...');
|
||
connectThinkingStream();
|
||
}
|
||
}, 2000);
|
||
};
|
||
}
|
||
|
||
function addThinkingEvent(event) {
|
||
// Remove empty state if present
|
||
if (thinkingEventCount === 0 && thinkingEmpty.parentNode) {
|
||
thinkingContent.removeChild(thinkingEmpty);
|
||
}
|
||
|
||
const eventDiv = document.createElement('div');
|
||
eventDiv.className = `thinking-event thinking-event-${event.type}`;
|
||
|
||
let icon = '';
|
||
let message = '';
|
||
let details = '';
|
||
|
||
switch (event.type) {
|
||
case 'connected':
|
||
icon = '✓';
|
||
message = 'Stream connected';
|
||
details = `Session: ${event.session_id}`;
|
||
break;
|
||
|
||
case 'thinking':
|
||
icon = '🤔';
|
||
message = event.data.message;
|
||
break;
|
||
|
||
case 'tool_call':
|
||
icon = '🔧';
|
||
message = event.data.message;
|
||
if (event.data.args) {
|
||
details = JSON.stringify(event.data.args, null, 2);
|
||
}
|
||
break;
|
||
|
||
case 'tool_result':
|
||
icon = '📊';
|
||
message = event.data.message;
|
||
if (event.data.result && event.data.result.stdout) {
|
||
details = `stdout: ${event.data.result.stdout}`;
|
||
}
|
||
break;
|
||
|
||
case 'done':
|
||
icon = '✅';
|
||
message = event.data.message;
|
||
if (event.data.final_answer) {
|
||
details = event.data.final_answer;
|
||
}
|
||
break;
|
||
|
||
case 'error':
|
||
icon = '❌';
|
||
message = event.data.message;
|
||
break;
|
||
|
||
default:
|
||
icon = '•';
|
||
message = JSON.stringify(event.data);
|
||
}
|
||
|
||
eventDiv.innerHTML = `
|
||
<span class="thinking-event-icon">${icon}</span>
|
||
<span>${message}</span>
|
||
${details ? `<div class="thinking-event-details">${details}</div>` : ''}
|
||
`;
|
||
|
||
thinkingContent.appendChild(eventDiv);
|
||
thinkingContent.scrollTop = thinkingContent.scrollHeight;
|
||
thinkingEventCount++;
|
||
}
|
||
|
||
// Persist thinking events to localStorage
|
||
function saveThinkingEvent(event) {
|
||
if (!currentSession) return;
|
||
|
||
const key = `thinkingEvents_${currentSession}`;
|
||
let events = JSON.parse(localStorage.getItem(key) || '[]');
|
||
|
||
// Keep only last 50 events to avoid bloating localStorage
|
||
if (events.length >= 50) {
|
||
events = events.slice(-49);
|
||
}
|
||
|
||
events.push({
|
||
...event,
|
||
timestamp: Date.now()
|
||
});
|
||
|
||
localStorage.setItem(key, JSON.stringify(events));
|
||
}
|
||
|
||
// Load persisted thinking events
|
||
function loadThinkingEvents() {
|
||
if (!currentSession) return;
|
||
|
||
const key = `thinkingEvents_${currentSession}`;
|
||
const events = JSON.parse(localStorage.getItem(key) || '[]');
|
||
|
||
// Clear current display
|
||
thinkingContent.innerHTML = '';
|
||
thinkingEventCount = 0;
|
||
|
||
// Replay events
|
||
events.forEach(event => addThinkingEvent(event));
|
||
|
||
// Show empty state if no events
|
||
if (events.length === 0) {
|
||
thinkingContent.appendChild(thinkingEmpty);
|
||
}
|
||
}
|
||
|
||
// Update the old thinking stream button to toggle panel instead
|
||
document.getElementById("thinkingStreamBtn").addEventListener("click", () => {
|
||
thinkingPanel.classList.remove("collapsed");
|
||
localStorage.setItem("thinkingPanelCollapsed", "false");
|
||
});
|
||
|
||
// Mobile thinking stream button
|
||
document.getElementById("mobileThinkingStreamBtn").addEventListener("click", () => {
|
||
closeMobileMenu();
|
||
thinkingPanel.classList.remove("collapsed");
|
||
localStorage.setItem("thinkingPanelCollapsed", "false");
|
||
});
|
||
|
||
// Connect thinking stream when session loads
|
||
if (currentSession) {
|
||
connectThinkingStream();
|
||
}
|
||
|
||
// Reconnect thinking stream when session changes
|
||
const originalSessionChange = document.getElementById("sessions").onchange;
|
||
document.getElementById("sessions").addEventListener("change", () => {
|
||
setTimeout(() => {
|
||
connectThinkingStream();
|
||
}, 500); // Wait for session to load
|
||
});
|
||
|
||
// Cleanup on page unload
|
||
window.addEventListener('beforeunload', () => {
|
||
if (thinkingEventSource) {
|
||
thinkingEventSource.close();
|
||
}
|
||
});
|
||
});
|
||
</script>
|
||
</body>
|
||
</html>
|