// ── Config ──────────────────────────────────────────────────────
const API_BASE = ""; // "" = same origin (HF Spaces). "http://localhost:8000" for local dev.
// ── State ───────────────────────────────────────────────────────
let sessions = []; // [{id, title, messages:[]}]
let activeSessionId = null;
let isLoading = false;
let sidebarCollapsed = localStorage.getItem("sidebarCollapsed") === "true";
// ── Init ────────────────────────────────────────────────────────
const textarea = document.getElementById("query-input");
const sendBtn = document.getElementById("send-btn");
const msgsList = document.getElementById("messages-list");
// Apply sidebar collapsed state on load
if (sidebarCollapsed) {
document.getElementById("sidebar").classList.add("collapsed");
}
textarea.addEventListener("input", () => {
textarea.style.height = "auto";
textarea.style.height = Math.min(textarea.scrollHeight, 140) + "px";
});
textarea.addEventListener("keydown", e => {
if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); submitQuery(); }
});
// ── Screen switching ─────────────────────────────────────────────
function showScreen(name) {
document.querySelectorAll(".screen").forEach(s => s.classList.remove("active"));
document.getElementById("screen-" + name).classList.add("active");
}
// ── Sidebar toggle ───────────────────────────────────────────────
function toggleSidebar() {
const sidebar = document.getElementById("sidebar");
sidebarCollapsed = !sidebarCollapsed;
sidebar.classList.toggle("collapsed");
localStorage.setItem("sidebarCollapsed", sidebarCollapsed);
}
// ── Session management ───────────────────────────────────────────
function createSession(firstQuery) {
const id = Date.now();
const title = firstQuery.length > 45
? firstQuery.substring(0, 45) + "…"
: firstQuery;
const session = { id, title, messages: [] };
sessions.unshift(session);
activeSessionId = id;
renderSessionsList();
return session;
}
function getActiveSession() {
return sessions.find(s => s.id === activeSessionId);
}
function switchSession(id) {
activeSessionId = id;
renderSessionsList();
renderMessages();
showScreen("chat");
const session = getActiveSession();
document.getElementById("topbar-title").textContent = session.title;
}
function newChat() {
activeSessionId = null;
msgsList.innerHTML = "";
showScreen("welcome");
document.getElementById("topbar-title").textContent = "New Research Session";
renderSessionsList();
textarea.focus();
}
function renderSessionsList() {
const list = document.getElementById("sessions-list");
if (sessions.length === 0) {
list.innerHTML = '
No sessions yet
';
return;
}
list.innerHTML = sessions.map(s => `
${escHtml(s.title)}
${s.messages.filter(m => m.role === "ai").length}
`).join("");
}
function renderMessages() {
const session = getActiveSession();
if (!session) return;
msgsList.innerHTML = "";
session.messages.forEach(msg => {
if (msg.role === "user") appendUserBubble(msg.text, false);
else if (msg.role === "ai") appendAIBubble(msg.data, false);
else if (msg.role === "error") appendErrorBubble(msg.text, false);
});
scrollBottom();
}
// ── Submit ───────────────────────────────────────────────────────
async function submitQuery() {
const query = textarea.value.trim();
if (!query || isLoading) return;
if (query.length < 10) { showToast("Query too short — minimum 10 characters."); return; }
if (query.length > 1000) { showToast("Query too long — maximum 1000 characters."); return; }
if (!activeSessionId) {
createSession(query);
showScreen("chat");
document.getElementById("topbar-title").textContent = getActiveSession().title;
}
getActiveSession().messages.push({ role: "user", text: query });
textarea.value = "";
textarea.style.height = "auto";
appendUserBubble(query);
const loaderId = appendLoader();
setLoading(true);
try {
const res = await fetch(`${API_BASE}/query`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
query,
session_id: activeSessionId ? String(activeSessionId) : "default"
})
});
const data = await res.json();
removeLoader(loaderId);
if (!res.ok) {
const msg = data.detail || "Something went wrong. Please try again.";
getActiveSession().messages.push({ role: "error", text: msg });
appendErrorBubble(msg);
} else {
getActiveSession().messages.push({ role: "ai", data });
appendAIBubble(data);
}
} catch (err) {
removeLoader(loaderId);
const msg = "Could not reach the server. The Space may be waking up — try again in 30 seconds.";
getActiveSession().messages.push({ role: "error", text: msg });
appendErrorBubble(msg);
}
setLoading(false);
scrollBottom();
}
function usesuggestion(el) {
textarea.value = el.textContent;
textarea.dispatchEvent(new Event("input"));
submitQuery();
}
// ── Bubble renderers ─────────────────────────────────────────────
function appendUserBubble(text, scroll = true) {
const div = document.createElement("div");
div.className = "msg msg-user";
div.innerHTML = `${escHtml(text)}
`;
msgsList.appendChild(div);
if (scroll) scrollBottom();
}
function appendAIBubble(data, scroll = true) {
const verified = data.verification_status === true || data.verification_status === "verified";
const badgeClass = verified ? "verified" : "unverified";
const badgeText = verified ? "✓ Verified" : "⚠ Unverified";
const truncNote = data.truncated
? `3 of 5 retrieved documents used — context limit reached.
`
: "";
const sourceCount = (data.sources || []).length;
const sourcesBtn = sourceCount > 0
? `
📄 ${sourceCount} Source${sourceCount > 1 ? "s" : ""}
`
: "";
const latency = data.latency_ms
? `${Math.round(data.latency_ms)}ms `
: "";
const div = document.createElement("div");
div.className = "msg msg-ai";
div.innerHTML = `
${formatAnswer(data.answer)}
${truncNote}
${badgeText}
${sourcesBtn}
${latency}
`;
msgsList.appendChild(div);
if (scroll) scrollBottom();
}
function appendErrorBubble(text, scroll = true) {
const div = document.createElement("div");
div.className = "msg msg-ai";
div.innerHTML = `⚠ ${escHtml(text)}
`;
msgsList.appendChild(div);
if (scroll) scrollBottom();
}
function appendLoader() {
const id = "loader-" + Date.now();
const div = document.createElement("div");
div.id = id;
div.className = "msg msg-ai";
div.innerHTML = `
`;
msgsList.appendChild(div);
scrollBottom();
return id;
}
function removeLoader(id) {
const el = document.getElementById(id);
if (el) el.remove();
}
// ── Sources panel ────────────────────────────────────────────────
function openSources(sources) {
const panel = document.getElementById("sources-panel");
const overlay = document.getElementById("sources-overlay");
const body = document.getElementById("sources-panel-body");
body.innerHTML = sources.map((s, i) => {
const meta = s.meta || {};
const id = meta.judgment_id || "Unknown";
const year = meta.year ? ` · ${meta.year}` : "";
const excerpt = (s.text || "").trim().substring(0, 400);
return `
${i + 1}
${escHtml(id)}
Supreme Court of India${year}
${escHtml(excerpt)}${s.text && s.text.length > 400 ? "…" : ""}
`;
}).join("");
panel.classList.add("open");
overlay.classList.add("open");
requestAnimationFrame(() => { panel.style.transform = "translateX(0)"; });
}
function closeSourcesPanel() {
const panel = document.getElementById("sources-panel");
const overlay = document.getElementById("sources-overlay");
panel.classList.remove("open");
overlay.classList.remove("open");
}
// ── Helpers ──────────────────────────────────────────────────────
function setLoading(state) {
isLoading = state;
sendBtn.disabled = state;
const pill = document.getElementById("status-pill");
const text = document.getElementById("status-text");
if (state) {
pill.classList.add("loading");
text.textContent = "Searching…";
} else {
pill.classList.remove("loading");
text.textContent = "Ready";
}
}
function scrollBottom() {
const c = document.querySelector(".messages-container");
if (c) c.scrollTop = c.scrollHeight;
}
function escHtml(str) {
return String(str || "")
.replace(/&/g, "&")
.replace(//g, ">")
.replace(/"/g, """);
}
function escAttr(str) {
return String(str || "").replace(/'/g, "'").replace(/"/g, """);
}
// ── Answer formatter ─────────────────────────────────────────────
function formatAnswer(text) {
if (!text) return "";
text = text.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
const lines = text.split('\n');
let html = '';
let inTable = false;
let tableHtml = '';
let inList = false;
let listType = '';
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
// Table row
if (line.trim().startsWith('|')) {
if (line.match(/^\|[\s\-|]+\|$/)) continue; // skip separator rows
if (!inTable) { tableHtml = ''; inTable = true; }
const cells = line.split('|').filter((c, idx, a) => idx > 0 && idx < a.length - 1);
tableHtml += '' + cells.map(c => `${inline(c.trim())} `).join('') + ' ';
continue;
} else if (inTable) {
html += tableHtml + '
';
tableHtml = ''; inTable = false;
}
// Numbered list
if (line.match(/^\d+\.\s+/)) {
if (!inList || listType !== 'ol') {
if (inList) html += `${listType}>`;
html += ''; inList = true; listType = 'ol';
}
html += `${inline(line.replace(/^\d+\.\s+/, ''))} `;
continue;
}
// Bullet list
if (line.match(/^[\*\-]\s+/)) {
if (!inList || listType !== 'ul') {
if (inList) html += `${listType}>`;
html += ''; inList = true; listType = 'ul';
}
html += `${inline(line.replace(/^[\*\-]\s+/, ''))} `;
continue;
}
// Close list on blank line
if (inList && line.trim() === '') {
html += `${listType}>`;
inList = false; listType = '';
}
// Headers
if (line.startsWith('### ')) { html += `${inline(line.slice(4))} `; continue; }
if (line.startsWith('## ')) { html += `${inline(line.slice(3))} `; continue; }
if (line.startsWith('# ')) { html += `${inline(line.slice(2))} `; continue; }
// Blank line
if (line.trim() === '') { html += ' '; continue; }
// Normal paragraph line
html += `${inline(line)}
`;
}
// Close any unclosed tags
if (inTable) html += tableHtml + '';
if (inList) html += `${listType}>`;
return html;
}
function inline(text) {
return String(text || '')
.replace(/&/g, '&')
.replace(//g, '>')
.replace(/\*\*(.+?)\*\*/g, '$1 ')
.replace(/\*(.+?)\*/g, '$1 ')
.replace(/`(.+?)`/g, '$1');
}
function showToast(msg) {
alert(msg);
}
// ── Analytics ────────────────────────────────────────────────────────
async function showAnalytics() {
showScreen("analytics");
document.getElementById("topbar-title").textContent = "System Analytics";
await loadAnalytics();
}
async function loadAnalytics() {
try {
const res = await fetch(`${API_BASE}/analytics`);
const data = await res.json();
if (data.total_queries === 0) {
document.getElementById("stat-total").textContent = "0";
document.getElementById("stat-verified").textContent = "—";
document.getElementById("stat-latency").textContent = "—";
document.getElementById("stat-ood").textContent = "—";
document.getElementById("stat-sources").textContent = "—";
document.getElementById("chart-stages").innerHTML = "No queries yet. Start asking questions.
";
document.getElementById("chart-entities").innerHTML = "No entity data yet.
";
document.getElementById("chart-latency").innerHTML = "No latency data yet.
";
return;
}
// Stat cards
document.getElementById("stat-total").textContent = data.total_queries;
document.getElementById("stat-verified").textContent = data.verified_ratio + "%";
document.getElementById("stat-latency").textContent = data.avg_latency_ms + "ms";
document.getElementById("stat-ood").textContent = data.out_of_domain_rate + "%";
document.getElementById("stat-sources").textContent = data.avg_sources;
// Stage distribution bar chart
renderBarChart("chart-stages", data.stage_distribution);
// Entity frequency bar chart
renderBarChart("chart-entities", data.entity_type_frequency);
// Latency sparkline
renderSparkline("chart-latency", data.recent_latencies);
} catch (err) {
document.getElementById("chart-stages").innerHTML = "Could not load analytics.
";
}
}
function renderBarChart(containerId, data) {
const container = document.getElementById(containerId);
if (!data || Object.keys(data).length === 0) {
container.innerHTML = "No data yet.
";
return;
}
const max = Math.max(...Object.values(data));
const html = Object.entries(data)
.sort((a, b) => b[1] - a[1])
.map(([label, value]) => `
${escHtml(label)}
${value}
`).join("");
container.innerHTML = `${html}
`;
}
function renderSparkline(containerId, latencies) {
const container = document.getElementById(containerId);
if (!latencies || latencies.length === 0) {
container.innerHTML = "No data yet.
";
return;
}
const max = Math.max(...latencies);
const min = Math.min(...latencies);
const range = max - min || 1;
const height = 60;
const width = 300;
const step = width / (latencies.length - 1 || 1);
const points = latencies.map((v, i) => {
const x = i * step;
const y = height - ((v - min) / range) * height;
return `${x},${y}`;
}).join(" ");
container.innerHTML = `
${Math.round(min)}ms min
${Math.round(max)}ms max
`;
}
function escHtml(text) {
const map = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' };
return String(text).replace(/[&<>"']/g, m => map[m]);
}