Buckets:
| import './theme.js'; | |
| import './style.css'; | |
| // ========================================================================== | |
| // STATE MANAGEMENT & CONSTANTS | |
| // ========================================================================== | |
| let currentMode = "sql"; | |
| let chatHistory = []; | |
| let chatSessions = []; | |
| let activeSessionId = null; | |
| const SESSIONS_STORAGE_KEY = "durg_university_chat_sessions"; | |
| // Dynamic model configurations | |
| let currentLLMModel = localStorage.getItem("durg_university_llm_model") || "gemini-3.1-flash-lite"; | |
| let currentThinkingLevel = localStorage.getItem("durg_university_thinking_level") || "MINIMAL"; | |
| let currentStreamMode = localStorage.getItem("durg_university_stream_mode") || "stream"; | |
| // Select DOM Elements | |
| const apiInput = document.getElementById("api-endpoint"); | |
| const apiSecretKeyInput = document.getElementById("api-secret-key"); | |
| const apiBadge = document.getElementById("api-status-badge"); | |
| const chatInput = document.getElementById("chat-input"); | |
| const chatMessages = document.getElementById("chat-messages"); | |
| const sendChatBtn = document.getElementById("send-chat-btn"); | |
| const statusIndicator = document.getElementById("system-status-indicator"); | |
| const progressBar = document.getElementById("progress-bar"); | |
| // Health check interval handler | |
| let healthCheckTimer = null; | |
| // Initialize app when DOM is fully loaded | |
| document.addEventListener("DOMContentLoaded", () => { | |
| // Load saved values on DOM load | |
| const savedEndpoint = localStorage.getItem("api-endpoint"); | |
| if (savedEndpoint) { | |
| apiInput.value = savedEndpoint; | |
| } | |
| const savedSecretKey = localStorage.getItem("api-secret-key") || ""; | |
| if (apiSecretKeyInput) { | |
| apiSecretKeyInput.value = savedSecretKey; | |
| } | |
| // Set LLM model dropdown state | |
| const modelSelect = document.getElementById("llm-model-select"); | |
| if (modelSelect) { | |
| modelSelect.value = currentLLMModel; | |
| } | |
| // Set thinking level dropdown state | |
| const thinkingSelect = document.getElementById("thinking-level-select"); | |
| if (thinkingSelect) { | |
| thinkingSelect.value = currentThinkingLevel; | |
| } | |
| // Set stream mode dropdown state | |
| const streamSelect = document.getElementById("stream-mode-select"); | |
| if (streamSelect) { | |
| streamSelect.value = currentStreamMode; | |
| } | |
| // On desktop viewports, have the right panel open by default | |
| if (window.innerWidth > 1024) { | |
| const panel = document.querySelector(".right-panel"); | |
| if (panel) { | |
| panel.classList.add("open"); | |
| } | |
| } | |
| // On desktop/tablet viewports, have the left sidebar open by default | |
| if (window.innerWidth > 768) { | |
| const sidebar = document.querySelector(".sidebar-left"); | |
| if (sidebar) { | |
| sidebar.classList.add("open"); | |
| } | |
| } | |
| // Perform initial health check | |
| checkAPIHealth(); | |
| // Automatically trigger health check when input changes | |
| apiInput.addEventListener("change", () => { | |
| localStorage.setItem("api-endpoint", apiInput.value.trim()); | |
| showToast("Reconnecting to API endpoint..."); | |
| checkAPIHealth(); | |
| }); | |
| if (apiSecretKeyInput) { | |
| apiSecretKeyInput.addEventListener("change", () => { | |
| localStorage.setItem("api-secret-key", apiSecretKeyInput.value.trim()); | |
| showToast("Updated API Secret Key..."); | |
| checkAPIHealth(); | |
| }); | |
| } | |
| // Start periodic health check polling (every 12 seconds) | |
| healthCheckTimer = setInterval(checkAPIHealth, 12000); | |
| // Initial sizing for textarea auto-expand | |
| chatInput.addEventListener("input", autoGrowTextarea); | |
| // Load saved chat sessions | |
| loadSessionsFromStorage(); | |
| renderSessions(); | |
| }); | |
| // Auto-grow chat textarea during typing | |
| function autoGrowTextarea() { | |
| this.style.height = "auto"; | |
| this.style.height = (this.scrollHeight) + "px"; | |
| } | |
| // ========================================================================== | |
| // UI HELPER FUNCTIONS | |
| // ========================================================================== | |
| // Toggle Theme Mode (Light / Dark) | |
| function toggleTheme() { | |
| const body = document.body; | |
| const currentTheme = body.getAttribute("data-theme"); | |
| const nextTheme = currentTheme === "dark" ? "light" : "dark"; | |
| body.setAttribute("data-theme", nextTheme); | |
| // Reapply current color preset variables corresponding to the new theme mode | |
| if (typeof applyThemePreset === "function" && typeof getCurrentPresetKey === "function") { | |
| applyThemePreset(getCurrentPresetKey()); | |
| } | |
| showToast(`Switched to ${nextTheme} mode`); | |
| } | |
| // Toggle Left Sidebar (Drawer menu on mobile) | |
| function toggleLeftSidebar() { | |
| const sidebar = document.querySelector(".sidebar-left"); | |
| const backdrop = document.querySelector(".sidebar-backdrop"); | |
| sidebar.classList.toggle("open"); | |
| backdrop.classList.toggle("open"); | |
| } | |
| // Toggle Right Panel (SQL Workbench & Vector explorer) | |
| function toggleRightPanel() { | |
| const panel = document.querySelector(".right-panel"); | |
| panel.classList.toggle("open"); | |
| } | |
| // Select query execution mode (SQL, Analyst, Scientist, MoE Experts) | |
| function selectMode(mode, quiet = false) { | |
| currentMode = mode; | |
| // Toggle active state in sidebar pills | |
| const pills = document.querySelectorAll(".sidebar-mode-list .mode-pill"); | |
| pills.forEach(pill => { | |
| const isActive = pill.getAttribute("onclick").includes(`'${mode}'`); | |
| pill.classList.toggle("active", isActive); | |
| }); | |
| if (!quiet) { | |
| showToast(`Switched to ${mode.toUpperCase()} Mode`); | |
| } | |
| } | |
| // Update bottom system status indicators | |
| function setStatus(type, text) { | |
| statusIndicator.className = ""; // Reset classes | |
| statusIndicator.classList.add(`status-${type}`); | |
| statusIndicator.textContent = text; | |
| } | |
| // Trigger top indicator progress bar loading | |
| function setProgressIndeterminate(loading) { | |
| if (loading) { | |
| progressBar.classList.add("indeterminate"); | |
| progressBar.style.width = "100%"; | |
| } else { | |
| progressBar.classList.remove("indeterminate"); | |
| progressBar.style.width = "0%"; | |
| } | |
| } | |
| // Spawn toast notifications | |
| function showToast(message) { | |
| const container = document.getElementById("toast-container"); | |
| const toast = document.createElement("div"); | |
| toast.className = "toast"; | |
| toast.innerHTML = `<i class="fa-solid fa-circle-info" style="color:var(--accent-blue)"></i> <span>${message}</span>`; | |
| container.appendChild(toast); | |
| // Fade out and remove toast after 3 seconds | |
| setTimeout(() => { | |
| toast.style.transition = "opacity 0.5s ease"; | |
| toast.style.opacity = "0"; | |
| setTimeout(() => toast.remove(), 500); | |
| }, 3000); | |
| } | |
| // Escapes special characters to render HTML cleanly as code text | |
| function escapeHtml(text) { | |
| if (!text) return ""; | |
| return String(text) | |
| .replace(/&/g, "&") | |
| .replace(/</g, "<") | |
| .replace(/>/g, ">") | |
| .replace(/"/g, """) | |
| .replace(/'/g, "'"); | |
| } | |
| // Hide welcome hero on initial chat query submission | |
| function hideWelcome() { | |
| const welcome = document.getElementById("welcome-hero"); | |
| if (welcome) welcome.style.display = "none"; | |
| } | |
| // Add message bubble to the messages panel | |
| function appendMessage(sender, contentHtml, isTyping = false) { | |
| const bubble = document.createElement("div"); | |
| bubble.className = `message-bubble ${sender}`; | |
| const id = "bubble-" + Math.random().toString(36).substr(2, 9); | |
| bubble.id = id; | |
| const avatarIcon = sender === "user" | |
| ? '<i class="fa-solid fa-user"></i>' | |
| : '<img src="assets/robot-icon.png" alt="Bot Avatar" class="bot-avatar-img">'; | |
| bubble.innerHTML = ` | |
| <div class="avatar">${avatarIcon}</div> | |
| <div class="bubble-content">${contentHtml}</div> | |
| `; | |
| chatMessages.appendChild(bubble); | |
| chatMessages.scrollTop = chatMessages.scrollHeight; | |
| return id; | |
| } | |
| // Timeline toggle collapsibles | |
| function toggleTimeline(headerElement) { | |
| const body = headerElement.nextElementSibling; | |
| const icon = headerElement.querySelector(".toggle-icon"); | |
| const isOpen = body.style.display !== "none"; | |
| if (isOpen) { | |
| body.style.display = "none"; | |
| icon.className = "fa-solid fa-chevron-down toggle-icon"; | |
| } else { | |
| body.style.display = "flex"; | |
| icon.className = "fa-solid fa-chevron-up toggle-icon"; | |
| } | |
| } | |
| // ========================================================================== | |
| // CHAT SESSION HISTORY MANAGEMENT | |
| // ========================================================================== | |
| function loadSessionsFromStorage() { | |
| try { | |
| const data = localStorage.getItem(SESSIONS_STORAGE_KEY); | |
| chatSessions = data ? JSON.parse(data) : []; | |
| } catch (e) { | |
| console.error("Error loading sessions:", e); | |
| chatSessions = []; | |
| } | |
| } | |
| function saveSessionsToStorage() { | |
| try { | |
| localStorage.setItem(SESSIONS_STORAGE_KEY, JSON.stringify(chatSessions)); | |
| } catch (e) { | |
| console.error("Error saving sessions:", e); | |
| } | |
| } | |
| function renderSessions() { | |
| const listContainer = document.getElementById("chat-sessions-list"); | |
| if (!listContainer) return; | |
| if (chatSessions.length === 0) { | |
| listContainer.innerHTML = ` | |
| <div style="font-size:0.75rem; color:var(--text-muted); text-align:center; padding:1.25rem 0; font-style:italic;"> | |
| No conversations yet | |
| </div> | |
| `; | |
| return; | |
| } | |
| listContainer.innerHTML = ""; | |
| chatSessions.forEach(session => { | |
| const item = document.createElement("div"); | |
| item.className = `chat-session-item ${session.id === activeSessionId ? 'active' : ''}`; | |
| item.setAttribute("data-id", session.id); | |
| item.onclick = () => loadSession(session.id); | |
| item.innerHTML = ` | |
| <div class="chat-session-left"> | |
| <i class="fa-regular fa-comment chat-session-icon"></i> | |
| <span class="chat-session-title" id="title-${session.id}">${escapeHtml(session.title)}</span> | |
| </div> | |
| <div class="chat-session-actions"> | |
| <button class="chat-session-btn rename-btn" onclick="startRenameSession('${session.id}', event)" title="Rename Session"> | |
| <i class="fa-solid fa-pen"></i> | |
| </button> | |
| <button class="chat-session-btn delete-btn" onclick="deleteSession('${session.id}', event)" title="Delete Session"> | |
| <i class="fa-solid fa-trash"></i> | |
| </button> | |
| </div> | |
| `; | |
| listContainer.appendChild(item); | |
| }); | |
| } | |
| async function generateAndSetSessionTitle(sessionId, message) { | |
| const baseUrl = getBaseUrl(); | |
| try { | |
| const res = await fetch(`${baseUrl}/api/title`, { | |
| method: "POST", | |
| headers: getHeaders({ "Content-Type": "application/json" }), | |
| body: JSON.stringify({ | |
| message: message, | |
| model_name: currentLLMModel | |
| }) | |
| }); | |
| if (res.ok) { | |
| const data = await res.json(); | |
| const session = chatSessions.find(s => s.id === sessionId); | |
| if (session && data.title) { | |
| session.title = data.title; | |
| saveSessionsToStorage(); | |
| // Re-render only if title element is not currently an input | |
| const titleSpan = document.getElementById(`title-${sessionId}`); | |
| if (titleSpan && titleSpan.tagName.toLowerCase() !== 'input') { | |
| renderSessions(); | |
| } | |
| } | |
| } | |
| } catch (err) { | |
| console.error("Failed to generate title:", err); | |
| } | |
| } | |
| function loadSession(sessionId) { | |
| const session = chatSessions.find(s => s.id === sessionId); | |
| if (!session) return; | |
| activeSessionId = sessionId; | |
| chatHistory = [...session.history]; | |
| // Set current mode and select without triggering toast | |
| selectMode(session.mode, true); | |
| // Clear chatMessages container and hide welcome | |
| hideWelcome(); | |
| chatMessages.innerHTML = ""; | |
| // Render all messages from history | |
| chatHistory.forEach(msg => { | |
| const formattedHtml = msg.role === "user" | |
| ? escapeHtml(msg.content) | |
| : marked.parse(msg.content); | |
| appendMessage(msg.role === "user" ? "user" : "bot", formattedHtml); | |
| }); | |
| renderSessions(); | |
| showToast("Loaded conversation"); | |
| } | |
| function startRenameSession(sessionId, event) { | |
| if (event) event.stopPropagation(); | |
| const titleSpan = document.getElementById(`title-${sessionId}`); | |
| if (!titleSpan) return; | |
| if (titleSpan.tagName.toLowerCase() === 'input') return; | |
| const currentTitle = titleSpan.textContent; | |
| const input = document.createElement('input'); | |
| input.type = 'text'; | |
| input.className = 'chat-session-input'; | |
| input.value = currentTitle; | |
| input.addEventListener('click', (e) => e.stopPropagation()); | |
| const finishRename = () => { | |
| const val = input.value.trim(); | |
| if (val && val !== currentTitle) { | |
| const session = chatSessions.find(s => s.id === sessionId); | |
| if (session) { | |
| session.title = val; | |
| saveSessionsToStorage(); | |
| showToast("Conversation renamed"); | |
| } | |
| } | |
| renderSessions(); | |
| }; | |
| input.addEventListener('keydown', (e) => { | |
| if (e.key === 'Enter') { | |
| e.preventDefault(); | |
| finishRename(); | |
| } else if (e.key === 'Escape') { | |
| e.preventDefault(); | |
| renderSessions(); | |
| } | |
| }); | |
| input.addEventListener('blur', finishRename); | |
| titleSpan.parentNode.replaceChild(input, titleSpan); | |
| input.focus(); | |
| input.select(); | |
| } | |
| function deleteSession(sessionId, event) { | |
| if (event) event.stopPropagation(); | |
| chatSessions = chatSessions.filter(s => s.id !== sessionId); | |
| saveSessionsToStorage(); | |
| if (activeSessionId === sessionId) { | |
| // Reset view silently | |
| activeSessionId = null; | |
| chatHistory = []; | |
| chatMessages.innerHTML = ` | |
| <div class="welcome-hero" id="welcome-hero"> | |
| <img src="assets/robot-icon.png" alt="Robot Icon" class="welcome-hero-logo"> | |
| <h3>Durg University Results Copilot</h3> | |
| <p>Ask anything about student marks, result distributions, failures, pass percentages, college-level statistics, or individual roll number reports.</p> | |
| <div class="suggestions-grid"> | |
| <div class="suggestion-card" onclick="useSuggestion(this)"> | |
| <div class="card-icon"><i class="fa-solid fa-chart-line"></i></div> | |
| <div class="card-text">Show BCA average marks by subject</div> | |
| </div> | |
| <div class="suggestion-card" onclick="useSuggestion(this)"> | |
| <div class="card-icon"><i class="fa-solid fa-percent"></i></div> | |
| <div class="card-text">Compare B.Sc and BA pass rates</div> | |
| </div> | |
| <div class="suggestion-card" onclick="useSuggestion(this)"> | |
| <div class="card-icon"><i class="fa-solid fa-award"></i></div> | |
| <div class="card-text">Who scored highest in BCom Session 2023?</div> | |
| </div> | |
| </div> | |
| </div> | |
| `; | |
| setStatus("ready", "READY · CONVERSATION DELETED"); | |
| renderSessions(); | |
| } else { | |
| renderSessions(); | |
| } | |
| showToast("Conversation deleted"); | |
| } | |
| // ========================================================================== | |
| // API CLIENT IMPLEMENTATION | |
| // ========================================================================== | |
| // Get sanitized endpoint from config box | |
| function getBaseUrl() { | |
| let url = apiInput.value.trim(); | |
| if (url.endsWith("/")) { | |
| url = url.slice(0, -1); | |
| } | |
| return url; | |
| } | |
| // Generate headers including authorization secret key | |
| function getHeaders(extraHeaders = {}) { | |
| const headers = { ...extraHeaders }; | |
| const secretKey = apiSecretKeyInput ? apiSecretKeyInput.value.trim() : ""; | |
| if (secretKey) { | |
| headers["X-Secret-Key"] = secretKey; | |
| } | |
| return headers; | |
| } | |
| // Check if API endpoint is online and pull DB metrics | |
| async function checkAPIHealth() { | |
| const baseUrl = getBaseUrl(); | |
| const statusLabel = apiBadge.querySelector(".status-label"); | |
| try { | |
| const res = await fetch(`${baseUrl}/api/status`, { | |
| method: "GET", | |
| headers: getHeaders({ "Accept": "application/json" }) | |
| }); | |
| if (res.ok) { | |
| const data = await res.json(); | |
| apiBadge.className = "api-badge online"; | |
| statusLabel.textContent = "Online"; | |
| // Populates database stats panel | |
| if (data.db_stats) { | |
| document.getElementById("db-stat-students").textContent = data.db_stats.duckdb_students.toLocaleString(); | |
| document.getElementById("db-stat-attempts").textContent = data.db_stats.duckdb_attempts.toLocaleString(); | |
| document.getElementById("db-stat-marks").textContent = data.db_stats.duckdb_marks.toLocaleString(); | |
| document.getElementById("db-stat-colleges").textContent = data.db_stats.duckdb_colleges.toLocaleString(); | |
| } | |
| if (data.server_time) { | |
| document.getElementById("server-time-meta").textContent = data.server_time.split("T")[1].substring(0, 5); | |
| } | |
| } else { | |
| throw new Error(); | |
| } | |
| } catch (err) { | |
| apiBadge.className = "api-badge offline"; | |
| statusLabel.textContent = "Offline"; | |
| } | |
| } | |
| // Send chat message payload to API and process responses | |
| async function sendChatMessage() { | |
| const message = chatInput.value.trim(); | |
| if (!message) return; | |
| // Initialize session if none is active | |
| if (activeSessionId === null) { | |
| activeSessionId = "session_" + Date.now(); | |
| const newSession = { | |
| id: activeSessionId, | |
| title: message.length > 25 ? message.substring(0, 25) + "..." : message, | |
| history: [], | |
| mode: currentMode, | |
| timestamp: Date.now() | |
| }; | |
| chatSessions.unshift(newSession); | |
| saveSessionsToStorage(); | |
| renderSessions(); | |
| // Trigger asynchronous LLM title generation | |
| generateAndSetSessionTitle(activeSessionId, message); | |
| } | |
| // Reset layout elements | |
| chatInput.value = ""; | |
| chatInput.style.height = "auto"; | |
| hideWelcome(); | |
| // Render User message | |
| appendMessage("user", escapeHtml(message)); | |
| // Append loading typing bubble for Bot | |
| const loadingId = appendMessage("bot", ` | |
| <div class="typing-indicator"> | |
| <div class="typing-dot"></div> | |
| <div class="typing-dot"></div> | |
| <div class="typing-dot"></div> | |
| </div> | |
| `); | |
| setStatus("running", "COPILOT PROCESSING QUERY..."); | |
| setProgressIndeterminate(true); | |
| const baseUrl = getBaseUrl(); | |
| if (currentStreamMode === "stream") { | |
| try { | |
| const res = await fetch(`${baseUrl}/api/chat/stream`, { | |
| method: "POST", | |
| headers: getHeaders({ | |
| "Content-Type": "application/json" | |
| }), | |
| body: JSON.stringify({ | |
| message: message, | |
| history: chatHistory, | |
| mode: currentMode, | |
| model_name: currentLLMModel, | |
| thinking_level: currentThinkingLevel | |
| }) | |
| }); | |
| if (!res.ok) { | |
| throw new Error(`HTTP Error: ${res.status} ${res.statusText}`); | |
| } | |
| // Remove typing indicator bubble | |
| document.getElementById(loadingId).remove(); | |
| // Create initial bot bubble with empty inner containers | |
| const botBubbleId = appendMessage("bot", ` | |
| <div class="bot-bubble-inner"> | |
| <div class="streaming-steps" style="display: none; margin-bottom: 0.75rem;"></div> | |
| <div class="thinking-block" style="display: none;"> | |
| <div class="thinking-block-title"><i class="fa-solid fa-brain"></i> Thinking Process...</div> | |
| <div class="thinking-text" style="white-space: pre-wrap;"></div> | |
| </div> | |
| <div class="response-text-container"></div> | |
| <div class="response-metadata" style="display: none; margin-top: 0.75rem;"></div> | |
| </div> | |
| `); | |
| const bubbleEl = document.getElementById(botBubbleId); | |
| const stepsDiv = bubbleEl.querySelector(".streaming-steps"); | |
| const thinkingDiv = bubbleEl.querySelector(".thinking-block"); | |
| const thinkingTextDiv = bubbleEl.querySelector(".thinking-text"); | |
| const responseDiv = bubbleEl.querySelector(".response-text-container"); | |
| const metaDiv = bubbleEl.querySelector(".response-metadata"); | |
| const reader = res.body.getReader(); | |
| const decoder = new TextDecoder("utf-8"); | |
| let buffer = ""; | |
| let accumulatedResponse = ""; | |
| let accumulatedThoughts = ""; | |
| let steps = []; | |
| let sqlQuery = ""; | |
| let vectorQuery = ""; | |
| let queryType = ""; | |
| while (true) { | |
| const { done, value } = await reader.read(); | |
| if (done) break; | |
| buffer += decoder.decode(value, { stream: true }); | |
| const lines = buffer.split("\n"); | |
| buffer = lines.pop(); // Keep partial line in buffer | |
| for (const line of lines) { | |
| const cleanLine = line.trim(); | |
| if (!cleanLine.startsWith("data: ")) continue; | |
| try { | |
| const jsonStr = cleanLine.substring(6); | |
| const chunk = JSON.parse(jsonStr); | |
| if (chunk.type === "step") { | |
| stepsDiv.style.display = "block"; | |
| const newStep = chunk.step; | |
| const existingStepIdx = steps.findIndex(s => s.agent === newStep.agent && s.action === newStep.action); | |
| if (existingStepIdx !== -1) { | |
| steps[existingStepIdx] = newStep; | |
| } else { | |
| steps.push(newStep); | |
| } | |
| // Re-render steps timeline | |
| let stepsHtml = ` | |
| <div class="agent-steps-timeline"> | |
| <div class="timeline-header" onclick="toggleTimeline(this)"> | |
| <span><i class="fa-solid fa-code-fork"></i> Agent Execution Timeline (${steps.length} Steps)</span> | |
| <i class="fa-solid fa-chevron-up toggle-icon"></i> | |
| </div> | |
| <div class="timeline-body" style="display: flex; flex-direction: column;"> | |
| `; | |
| steps.forEach(step => { | |
| let badgeIcon = "fa-check"; | |
| if (step.status === "error") badgeIcon = "fa-xmark"; | |
| else if (step.status === "running") badgeIcon = "fa-spinner fa-spin"; | |
| stepsHtml += ` | |
| <div class="timeline-step"> | |
| <div class="timeline-badge ${step.status}"> | |
| <i class="fa-solid ${badgeIcon}"></i> | |
| </div> | |
| <div class="timeline-content"> | |
| <div class="timeline-agent">${step.agent}</div> | |
| <div class="timeline-action">${step.action}</div> | |
| ${step.details ? `<div class="timeline-details">${escapeHtml(step.details)}</div>` : ''} | |
| </div> | |
| </div> | |
| `; | |
| }); | |
| stepsHtml += `</div></div>`; | |
| stepsDiv.innerHTML = stepsHtml; | |
| } | |
| else if (chunk.type === "thought") { | |
| thinkingDiv.style.display = "block"; | |
| accumulatedThoughts += chunk.text; | |
| thinkingTextDiv.textContent = accumulatedThoughts; | |
| } | |
| else if (chunk.type === "text") { | |
| accumulatedResponse += chunk.text; | |
| responseDiv.innerHTML = marked.parse(accumulatedResponse); | |
| } | |
| else if (chunk.type === "done") { | |
| sqlQuery = chunk.sql_query || ""; | |
| vectorQuery = chunk.vector_query || ""; | |
| queryType = chunk.query_type || ""; | |
| // Auto-complete steps on done | |
| steps.forEach(s => { | |
| if (s.status === "running") s.status = "success"; | |
| }); | |
| } | |
| } catch (e) { | |
| console.error("Failed to parse SSE line:", cleanLine, e); | |
| } | |
| } | |
| } | |
| // Once complete, toggle timeline to collapsed by default | |
| const timelineHeader = stepsDiv.querySelector(".timeline-header"); | |
| if (timelineHeader) { | |
| const timelineBody = stepsDiv.querySelector(".timeline-body"); | |
| const icon = stepsDiv.querySelector(".toggle-icon"); | |
| if (timelineBody && timelineBody.style.display !== "none") { | |
| timelineBody.style.display = "none"; | |
| icon.className = "fa-solid fa-chevron-down toggle-icon"; | |
| } | |
| } | |
| // Mark thinking block as finished | |
| const thoughtTitle = thinkingDiv.querySelector(".thinking-block-title"); | |
| if (thoughtTitle) { | |
| thoughtTitle.innerHTML = `<i class="fa-solid fa-brain"></i> Thinking Process`; | |
| } | |
| // Render final metadata | |
| let metaHtml = `<div class="metadata-tags">`; | |
| metaHtml += `<span class="meta-tag"><i class="fa-solid fa-route"></i> Mode: ${(queryType || currentMode).toUpperCase()}</span>`; | |
| if (sqlQuery) { | |
| const escapedSql = sqlQuery.replace(/\\/g, '\\\\').replace(/'/g, "\\'").replace(/"/g, '"').replace(/\n/g, '\\n'); | |
| metaHtml += `<span class="meta-tag sql" onclick="navigator.clipboard.writeText('${escapedSql}'); showToast('SQL copied to clipboard!')"><i class="fa-solid fa-copy"></i> Copy SQL Query</span>`; | |
| } | |
| if (vectorQuery) { | |
| metaHtml += `<span class="meta-tag"><i class="fa-solid fa-spider"></i> Vector retrieval matched</span>`; | |
| } | |
| metaHtml += `</div>`; | |
| metaDiv.innerHTML = metaHtml; | |
| metaDiv.style.display = "block"; | |
| // Push to chat history | |
| chatHistory.push({ role: "user", content: message }); | |
| chatHistory.push({ role: "assistant", content: accumulatedResponse }); | |
| if (chatHistory.length > 16) { | |
| chatHistory = chatHistory.slice(-16); | |
| } | |
| // Save session | |
| const session = chatSessions.find(s => s.id === activeSessionId); | |
| if (session) { | |
| session.history = [...chatHistory]; | |
| session.mode = currentMode; | |
| session.timestamp = Date.now(); | |
| saveSessionsToStorage(); | |
| } | |
| setStatus("ready", "READY · RESPONSE STREAM COMPLETE"); | |
| setProgressIndeterminate(false); | |
| showToast("Response complete!"); | |
| } catch (err) { | |
| console.error(err); | |
| const el = document.getElementById(loadingId); | |
| if (el) el.remove(); | |
| appendMessage("bot", `<div style="color:var(--accent-rose)"><i class="fa-solid fa-triangle-exclamation"></i> Error: ${escapeHtml(err.message)}</div>`); | |
| setStatus("error", "COPILOT ERROR: " + err.message.toUpperCase()); | |
| setProgressIndeterminate(false); | |
| showToast("Error processing streaming query!"); | |
| } | |
| } else { | |
| try { | |
| const res = await fetch(`${baseUrl}/api/chat`, { | |
| method: "POST", | |
| headers: getHeaders({ | |
| "Content-Type": "application/json" | |
| }), | |
| body: JSON.stringify({ | |
| message: message, | |
| history: chatHistory, | |
| mode: currentMode, | |
| model_name: currentLLMModel, | |
| thinking_level: currentThinkingLevel | |
| }) | |
| }); | |
| if (!res.ok) { | |
| throw new Error(`HTTP Error: ${res.status} ${res.statusText}`); | |
| } | |
| const data = await res.json(); | |
| // Remove typing indicator bubble | |
| document.getElementById(loadingId).remove(); | |
| // Convert Bot markdown response to HTML using Marked.js | |
| const formattedHtml = marked.parse(data.response); | |
| // Build Agent Steps Timeline | |
| let stepsHtml = ""; | |
| if (data.steps && data.steps.length > 0) { | |
| stepsHtml += ` | |
| <div class="agent-steps-timeline"> | |
| <div class="timeline-header" onclick="toggleTimeline(this)"> | |
| <span><i class="fa-solid fa-code-fork"></i> Agent Execution Timeline (${data.steps.length} Steps)</span> | |
| <i class="fa-solid fa-chevron-down toggle-icon"></i> | |
| </div> | |
| <div class="timeline-body" style="display: none;"> | |
| `; | |
| data.steps.forEach(step => { | |
| let badgeIcon = "fa-check"; | |
| if (step.status === "error") badgeIcon = "fa-xmark"; | |
| else if (step.status === "running") badgeIcon = "fa-spinner fa-spin"; | |
| stepsHtml += ` | |
| <div class="timeline-step"> | |
| <div class="timeline-badge ${step.status}"> | |
| <i class="fa-solid ${badgeIcon}"></i> | |
| </div> | |
| <div class="timeline-content"> | |
| <div class="timeline-agent">${step.agent}</div> | |
| <div class="timeline-action">${step.action}</div> | |
| ${step.details ? `<div class="timeline-details">${escapeHtml(step.details)}</div>` : ''} | |
| </div> | |
| </div> | |
| `; | |
| }); | |
| stepsHtml += `</div></div>`; | |
| } | |
| // Build Metadata elements (Query mode + runnable SQL buttons) | |
| let metaHtml = `<div class="metadata-tags">`; | |
| metaHtml += `<span class="meta-tag"><i class="fa-solid fa-route"></i> Mode: ${(data.query_type || currentMode).toUpperCase()}</span>`; | |
| if (data.sql_query) { | |
| // Escape quotes inside function arguments string to copy nicely | |
| const escapedSql = data.sql_query.replace(/\\/g, '\\\\').replace(/'/g, "\\'").replace(/"/g, '"').replace(/\n/g, '\\n'); | |
| metaHtml += `<span class="meta-tag sql" onclick="navigator.clipboard.writeText('${escapedSql}'); showToast('SQL copied to clipboard!')"><i class="fa-solid fa-copy"></i> Copy SQL Query</span>`; | |
| } | |
| if (data.vector_query) { | |
| metaHtml += `<span class="meta-tag"><i class="fa-solid fa-spider"></i> Vector retrieval matched</span>`; | |
| } | |
| metaHtml += `</div>`; | |
| // Render Bot response | |
| appendMessage("bot", formattedHtml + stepsHtml + metaHtml); | |
| // Update chat history memory | |
| chatHistory.push({ role: "user", content: message }); | |
| chatHistory.push({ role: "assistant", content: data.response }); | |
| // Limit memory size to last 8 turns (16 messages) to avoid token overflow | |
| if (chatHistory.length > 16) { | |
| chatHistory = chatHistory.slice(-16); | |
| } | |
| // Save session history | |
| const session = chatSessions.find(s => s.id === activeSessionId); | |
| if (session) { | |
| session.history = [...chatHistory]; | |
| session.mode = currentMode; | |
| session.timestamp = Date.now(); | |
| saveSessionsToStorage(); | |
| } | |
| setStatus("ready", "READY · RESPONSE RECEIVED"); | |
| setProgressIndeterminate(false); | |
| showToast("Response received!"); | |
| } catch (err) { | |
| console.error(err); | |
| document.getElementById(loadingId).remove(); | |
| appendMessage("bot", `<div style="color:var(--accent-rose)"><i class="fa-solid fa-triangle-exclamation"></i> Error: ${escapeHtml(err.message)}</div>`); | |
| setStatus("error", "COPILOT ERROR: " + err.message.toUpperCase()); | |
| setProgressIndeterminate(false); | |
| showToast("Error processing query!"); | |
| } | |
| } | |
| } | |
| // Trigger chat submit on pressing Enter (without Shift) | |
| function handleChatSubmit(event) { | |
| if (event.key === "Enter" && !event.shiftKey) { | |
| event.preventDefault(); | |
| sendChatMessage(); | |
| } | |
| } | |
| // Suggestion cards integration | |
| function useSuggestion(cardElement) { | |
| const text = cardElement.querySelector(".card-text").textContent; | |
| chatInput.value = text; | |
| chatInput.style.height = "auto"; | |
| sendChatMessage(); | |
| } | |
| // Start a fresh new chat session | |
| function startNewChat() { | |
| activeSessionId = null; | |
| chatHistory = []; | |
| chatMessages.innerHTML = ` | |
| <div class="welcome-hero" id="welcome-hero"> | |
| <img src="assets/robot-icon.png" alt="Robot Icon" class="welcome-hero-logo"> | |
| <h3>Durg University Results Copilot</h3> | |
| <p>Ask anything about student marks, result distributions, failures, pass percentages, college-level statistics, or individual roll number reports.</p> | |
| <div class="suggestions-grid"> | |
| <div class="suggestion-card" onclick="useSuggestion(this)"> | |
| <div class="card-icon"><i class="fa-solid fa-chart-line"></i></div> | |
| <div class="card-text">Show BCA average marks by subject</div> | |
| </div> | |
| <div class="suggestion-card" onclick="useSuggestion(this)"> | |
| <div class="card-icon"><i class="fa-solid fa-percent"></i></div> | |
| <div class="card-text">Compare B.Sc and BA pass rates</div> | |
| </div> | |
| <div class="suggestion-card" onclick="useSuggestion(this)"> | |
| <div class="card-icon"><i class="fa-solid fa-award"></i></div> | |
| <div class="card-text">Who scored highest in BCom Session 2023?</div> | |
| </div> | |
| </div> | |
| </div> | |
| `; | |
| renderSessions(); | |
| setStatus("ready", "READY · NEW CONVERSATION STARTED"); | |
| showToast("Chat reset complete"); | |
| } | |
| // Export the active chat conversation history to a JSON file | |
| function exportActiveChat() { | |
| if (chatHistory.length === 0) { | |
| showToast("No active conversation to export!"); | |
| return; | |
| } | |
| const session = chatSessions.find(s => s.id === activeSessionId) || { | |
| id: "temp_" + Date.now(), | |
| title: "Unsaved Conversation", | |
| mode: currentMode, | |
| history: chatHistory | |
| }; | |
| const exportData = { | |
| sessionId: session.id, | |
| title: session.title, | |
| mode: session.mode, | |
| history: chatHistory, | |
| exportedAt: new Date().toISOString() | |
| }; | |
| const dataStr = "data:text/json;charset=utf-8," + encodeURIComponent(JSON.stringify(exportData, null, 2)); | |
| const downloadAnchor = document.createElement('a'); | |
| downloadAnchor.setAttribute("href", dataStr); | |
| const cleanTitle = session.title.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '') || 'conversation'; | |
| const filename = `durg-rag-chat-${cleanTitle}-${Date.now()}.json`; | |
| downloadAnchor.setAttribute("download", filename); | |
| document.body.appendChild(downloadAnchor); | |
| downloadAnchor.click(); | |
| downloadAnchor.remove(); | |
| showToast("Chat exported successfully!"); | |
| } | |
| function changeLLMModel(val) { | |
| currentLLMModel = val; | |
| localStorage.setItem("durg_university_llm_model", val); | |
| showToast(`Model set to ${val}`); | |
| } | |
| function changeThinkingLevel(val) { | |
| currentThinkingLevel = val; | |
| localStorage.setItem("durg_university_thinking_level", val); | |
| showToast(`Thinking level set to ${val}`); | |
| } | |
| function changeStreamMode(val) { | |
| currentStreamMode = val; | |
| localStorage.setItem("durg_university_stream_mode", val); | |
| showToast(`Response mode set to ${val.toUpperCase()}`); | |
| } | |
| // Export/bind to window scope for compatibility with ES Modules in Vite | |
| window.toggleTheme = toggleTheme; | |
| window.toggleLeftSidebar = toggleLeftSidebar; | |
| window.toggleRightPanel = toggleRightPanel; | |
| window.selectMode = selectMode; | |
| window.sendChatMessage = sendChatMessage; | |
| window.handleChatSubmit = handleChatSubmit; | |
| window.useSuggestion = useSuggestion; | |
| window.startNewChat = startNewChat; | |
| window.toggleTimeline = toggleTimeline; | |
| window.showToast = showToast; | |
| window.startRenameSession = startRenameSession; | |
| window.deleteSession = deleteSession; | |
| window.loadSession = loadSession; | |
| window.exportActiveChat = exportActiveChat; | |
| window.changeLLMModel = changeLLMModel; | |
| window.changeThinkingLevel = changeThinkingLevel; | |
| window.changeStreamMode = changeStreamMode; | |
Xet Storage Details
- Size:
- 38.8 kB
- Xet hash:
- 659cd2a87f87b0cad98aada9f4c7d5c718c13511ba44f6382c3deb0d423b07a2
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.