sameerbanchhor-work's picture
download
raw
38.8 kB
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, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;");
}
// 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, '&quot;').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, '&quot;').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.