CaffeinatedCoding commited on
Commit
0da4cfe
·
verified ·
1 Parent(s): 789b00f

Upload folder using huggingface_hub

Browse files
Files changed (1) hide show
  1. frontend/app.js +67 -336
frontend/app.js CHANGED
@@ -1,352 +1,83 @@
1
- // ── Config ──────────────────────────────────────────────────────
2
- const API_BASE = ""; // "" = same origin (HF Spaces). "http://localhost:8000" for local dev.
3
-
4
- // ── State ───────────────────────────────────────────────────────
5
- let sessions = []; // [{id, title, messages:[]}]
6
- let activeSessionId = null;
7
- let isLoading = false;
8
-
9
- // ── Init ────────────────────────────────────────────────────────
10
- const textarea = document.getElementById("query-input");
11
- const sendBtn = document.getElementById("send-btn");
12
- const msgsList = document.getElementById("messages-list");
13
-
14
- textarea.addEventListener("input", () => {
15
- textarea.style.height = "auto";
16
- textarea.style.height = Math.min(textarea.scrollHeight, 140) + "px";
17
- });
18
-
19
- textarea.addEventListener("keydown", e => {
20
- if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); submitQuery(); }
21
- });
22
-
23
- // ── Screen switching ─────────────────────────────────────────────
24
- function showScreen(name) {
25
- document.querySelectorAll(".screen").forEach(s => s.classList.remove("active"));
26
- document.getElementById("screen-" + name).classList.add("active");
27
- }
28
-
29
- // ── Session management ───────────────────────────────────────────
30
- function createSession(firstQuery) {
31
- const id = Date.now();
32
- const title = firstQuery.length > 45
33
- ? firstQuery.substring(0, 45) + "…"
34
- : firstQuery;
35
- const session = { id, title, messages: [] };
36
- sessions.unshift(session);
37
- activeSessionId = id;
38
- renderSessionsList();
39
- return session;
40
- }
41
-
42
- function getActiveSession() {
43
- return sessions.find(s => s.id === activeSessionId);
44
- }
45
-
46
- function switchSession(id) {
47
- activeSessionId = id;
48
- renderSessionsList();
49
- renderMessages();
50
- showScreen("chat");
51
- const session = getActiveSession();
52
- document.getElementById("topbar-title").textContent = session.title;
53
- }
54
-
55
- function newChat() {
56
- activeSessionId = null;
57
- msgsList.innerHTML = "";
58
- showScreen("welcome");
59
- document.getElementById("topbar-title").textContent = "New Research Session";
60
- renderSessionsList();
61
- textarea.focus();
62
- }
63
-
64
- function renderSessionsList() {
65
- const list = document.getElementById("sessions-list");
66
- if (sessions.length === 0) {
67
- list.innerHTML = '<div class="sessions-empty">No sessions yet</div>';
68
- return;
69
- }
70
- list.innerHTML = sessions.map(s => `
71
- <div class="session-item ${s.id === activeSessionId ? "active" : ""}"
72
- onclick="switchSession(${s.id})">
73
- <div class="session-dot"></div>
74
- <span class="session-label">${escHtml(s.title)}</span>
75
- <span class="session-count">${s.messages.filter(m => m.role === "ai").length}</span>
76
- </div>
77
- `).join("");
78
- }
79
-
80
- function renderMessages() {
81
- const session = getActiveSession();
82
- if (!session) return;
83
- msgsList.innerHTML = "";
84
- session.messages.forEach(msg => {
85
- if (msg.role === "user") appendUserBubble(msg.text, false);
86
- else if (msg.role === "ai") appendAIBubble(msg.data, false);
87
- else if (msg.role === "error") appendErrorBubble(msg.text, false);
88
- });
89
- scrollBottom();
90
- }
91
-
92
- // ── Submit ───────────────────────────────────────────────────────
93
- async function submitQuery() {
94
- const query = textarea.value.trim();
95
- if (!query || isLoading) return;
96
- if (query.length < 10) { showToast("Query too short — minimum 10 characters."); return; }
97
- if (query.length > 1000) { showToast("Query too long — maximum 1000 characters."); return; }
98
-
99
- // First message in this chat — create session and switch screen
100
- if (!activeSessionId) {
101
- createSession(query);
102
- showScreen("chat");
103
- document.getElementById("topbar-title").textContent = getActiveSession().title;
104
- }
105
-
106
- // Save user message to session
107
- getActiveSession().messages.push({ role: "user", text: query });
108
-
109
- textarea.value = "";
110
- textarea.style.height = "auto";
111
-
112
- appendUserBubble(query);
113
- const loaderId = appendLoader();
114
- setLoading(true);
115
-
116
- try {
117
- const res = await fetch(`${API_BASE}/query`, {
118
- method: "POST",
119
- headers: { "Content-Type": "application/json" },
120
- body: JSON.stringify({ query })
121
- });
122
-
123
- const data = await res.json();
124
- removeLoader(loaderId);
125
 
126
- if (!res.ok) {
127
- const msg = data.detail || "Something went wrong. Please try again.";
128
- getActiveSession().messages.push({ role: "error", text: msg });
129
- appendErrorBubble(msg);
130
- } else {
131
- getActiveSession().messages.push({ role: "ai", data });
132
- appendAIBubble(data);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
133
  }
134
 
135
- } catch (err) {
136
- removeLoader(loaderId);
137
- const msg = "Could not reach the server. The Space may be waking up — try again in 30 seconds.";
138
- getActiveSession().messages.push({ role: "error", text: msg });
139
- appendErrorBubble(msg);
140
- }
141
-
142
- setLoading(false);
143
- scrollBottom();
144
- }
145
-
146
- function usesuggestion(el) {
147
- textarea.value = el.textContent;
148
- textarea.dispatchEvent(new Event("input"));
149
- submitQuery();
150
- }
151
-
152
- // ── Bubble renderers ─────────────────────────────────────────────
153
- function appendUserBubble(text, scroll = true) {
154
- const div = document.createElement("div");
155
- div.className = "msg msg-user";
156
- div.innerHTML = `<div class="bubble-user">${escHtml(text)}</div>`;
157
- msgsList.appendChild(div);
158
- if (scroll) scrollBottom();
159
- }
160
-
161
- function appendAIBubble(data, scroll = true) {
162
- const verified = data.verification_status === true || data.verification_status === "verified";
163
- const badgeClass = verified ? "verified" : "unverified";
164
- const badgeText = verified ? "✓ Verified" : "⚠ Unverified";
165
-
166
- const truncNote = data.truncated
167
- ? `<div class="truncated-note">3 of 5 retrieved documents used — context limit reached.</div>`
168
- : "";
169
-
170
- const sourceCount = (data.sources || []).length;
171
- const sourcesBtn = sourceCount > 0
172
- ? `<button class="sources-btn" onclick='openSources(${escAttr(JSON.stringify(data.sources))})'>
173
- 📄 ${sourceCount} Source${sourceCount > 1 ? "s" : ""}
174
- </button>`
175
- : "";
176
-
177
- const latency = data.latency_ms
178
- ? `<span class="latency-label">${Math.round(data.latency_ms)}ms</span>`
179
- : "";
180
-
181
- const div = document.createElement("div");
182
- div.className = "msg msg-ai";
183
- div.innerHTML = `
184
- <div class="bubble-ai">
185
- <div class="bubble-answer">${formatAnswer(data.answer)}</div>
186
- ${truncNote}
187
- <div class="bubble-meta">
188
- <span class="verify-badge ${badgeClass}">${badgeText}</span>
189
- ${sourcesBtn}
190
- ${latency}
191
- </div>
192
- </div>`;
193
- msgsList.appendChild(div);
194
- if (scroll) scrollBottom();
195
- }
196
-
197
- function appendErrorBubble(text, scroll = true) {
198
- const div = document.createElement("div");
199
- div.className = "msg msg-ai";
200
- div.innerHTML = `<div class="bubble-error">⚠ ${escHtml(text)}</div>`;
201
- msgsList.appendChild(div);
202
- if (scroll) scrollBottom();
203
- }
204
-
205
- function appendLoader() {
206
- const id = "loader-" + Date.now();
207
- const div = document.createElement("div");
208
- div.id = id;
209
- div.className = "msg msg-ai";
210
- div.innerHTML = `
211
- <div class="bubble-ai bubble-loading">
212
- <div class="dots"><span></span><span></span><span></span></div>
213
- Searching judgments…
214
- </div>`;
215
- msgsList.appendChild(div);
216
- scrollBottom();
217
- return id;
218
- }
219
-
220
- function removeLoader(id) {
221
- const el = document.getElementById(id);
222
- if (el) el.remove();
223
- }
224
 
225
- // ── Sources panel ────────────────────────────────────────────────
226
- function openSources(sources) {
227
- const panel = document.getElementById("sources-panel");
228
- const overlay = document.getElementById("sources-overlay");
229
- const body = document.getElementById("sources-panel-body");
 
 
 
 
230
 
231
- body.innerHTML = sources.map((s, i) => {
232
- const meta = s.meta || {};
233
- const id = meta.judgment_id || "Unknown";
234
- const year = meta.year ? ` · ${meta.year}` : "";
235
- const excerpt = (s.text || "").trim().substring(0, 400);
236
- return `
237
- <div class="source-card">
238
- <div class="source-num">${i + 1}</div>
239
- <div class="source-id">${escHtml(id)}</div>
240
- <div class="source-year">Supreme Court of India${year}</div>
241
- <div class="source-excerpt">${escHtml(excerpt)}${s.text && s.text.length > 400 ? "…" : ""}</div>
242
- </div>`;
243
- }).join("");
244
 
245
- panel.classList.add("open");
246
- overlay.classList.add("open");
247
- // Trigger CSS transition
248
- requestAnimationFrame(() => { panel.style.transform = "translateX(0)"; });
249
- }
250
 
251
- function closeSourcesPanel() {
252
- const panel = document.getElementById("sources-panel");
253
- const overlay = document.getElementById("sources-overlay");
254
- panel.classList.remove("open");
255
- overlay.classList.remove("open");
256
- }
257
 
258
- // ── Helpers ──────────────────────────────────────────────────────
259
- function setLoading(state) {
260
- isLoading = state;
261
- sendBtn.disabled = state;
262
- const pill = document.getElementById("status-pill");
263
- const text = document.getElementById("status-text");
264
- if (state) {
265
- pill.classList.add("loading");
266
- text.textContent = "Searching…";
267
- } else {
268
- pill.classList.remove("loading");
269
- text.textContent = "Ready";
270
  }
271
- }
272
 
273
- function scrollBottom() {
274
- const c = document.querySelector(".messages-container");
275
- if (c) c.scrollTop = c.scrollHeight;
276
- }
277
 
278
- function escHtml(str) {
279
- return String(str || "")
280
- .replace(/&/g, "&amp;")
281
- .replace(/</g, "&lt;")
282
- .replace(/>/g, "&gt;")
283
- .replace(/"/g, "&quot;");
284
  }
285
 
286
- function escAttr(str) {
287
- return String(str || "").replace(/'/g, "&#39;").replace(/"/g, "&quot;");
288
- }
289
-
290
- function formatAnswer(text) {
291
- if (!text) return "";
292
-
293
- let html = text
294
- // Headers
295
- .replace(/^### (.+)$/gm, '<h3>$1</h3>')
296
- .replace(/^## (.+)$/gm, '<h2>$1</h2>')
297
- .replace(/^# (.+)$/gm, '<h1>$1</h1>')
298
-
299
- // Bold and italic
300
  .replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
301
  .replace(/\*(.+?)\*/g, '<em>$1</em>')
302
-
303
- // Numbered lists
304
- .replace(/^\d+\.\s+(.+)$/gm, '<li class="numbered">$1</li>')
305
-
306
- // Bullet lists
307
- .replace(/^[-•]\s+(.+)$/gm, '<li class="bullet">$1</li>')
308
-
309
- // Tables — | col | col |
310
- .replace(/\|(.+)\|/g, (match) => {
311
- const cells = match.split('|').filter(c => c.trim());
312
- return '<tr>' + cells.map(c =>
313
- c.trim().match(/^[-]+$/)
314
- ? ''
315
- : `<td>${c.trim()}</td>`
316
- ).join('') + '</tr>';
317
- })
318
-
319
- // Inline code
320
- .replace(/`(.+?)`/g, '<code>$1</code>')
321
-
322
- // Double newline = paragraph break
323
- .replace(/\n\n/g, '</p><p>')
324
-
325
- // Single newline = line break
326
- .replace(/\n/g, '<br>');
327
-
328
- // Wrap consecutive <li class="numbered"> in <ol>
329
- html = html.replace(
330
- /(<li class="numbered">.*?<\/li>)+/gs,
331
- (match) => `<ol>${match.replace(/class="numbered"/g, '')}</ol>`
332
- );
333
-
334
- // Wrap consecutive <li class="bullet"> in <ul>
335
- html = html.replace(
336
- /(<li class="bullet">.*?<\/li>)+/gs,
337
- (match) => `<ul>${match.replace(/class="bullet"/g, '')}</ul>`
338
- );
339
-
340
- // Wrap table rows in <table>
341
- html = html.replace(
342
- /(<tr>.*?<\/tr>)+/gs,
343
- (match) => `<table class="answer-table">${match}</table>`
344
- );
345
-
346
- return `<p>${html}</p>`;
347
- }
348
-
349
- function showToast(msg) {
350
- // Simple alert fallback — can be styled later
351
- alert(msg);
352
  }
 
1
+ function formatAnswer(text) {
2
+ if (!text) return "";
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3
 
4
+ // Split into lines and process each
5
+ const lines = text.split('\n');
6
+ let html = '';
7
+ let inTable = false;
8
+ let tableHtml = '';
9
+ let inList = false;
10
+ let listType = '';
11
+
12
+ for (let i = 0; i < lines.length; i++) {
13
+ let line = lines[i];
14
+
15
+ // Table row
16
+ if (line.trim().startsWith('|')) {
17
+ if (line.match(/^\|[\s\-|]+\|$/)) continue; // skip separator rows
18
+ if (!inTable) { tableHtml = '<table class="answer-table">'; inTable = true; }
19
+ const cells = line.split('|').filter((c, i, a) => i > 0 && i < a.length - 1);
20
+ const isHeader = i === 0 || !lines[i-1]?.trim().startsWith('|');
21
+ const tag = isHeader ? 'th' : 'td';
22
+ tableHtml += '<tr>' + cells.map(c => `<${tag}>${inline(c.trim())}</${tag}>`).join('') + '</tr>';
23
+ continue;
24
+ } else if (inTable) {
25
+ html += tableHtml + '</table>';
26
+ tableHtml = '';
27
+ inTable = false;
28
  }
29
 
30
+ // Numbered list
31
+ if (line.match(/^\d+\.\s+/)) {
32
+ if (!inList || listType !== 'ol') {
33
+ if (inList) html += `</${listType}>`;
34
+ html += '<ol>'; inList = true; listType = 'ol';
35
+ }
36
+ html += `<li>${inline(line.replace(/^\d+\.\s+/, ''))}</li>`;
37
+ continue;
38
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
39
 
40
+ // Bullet list (* or -)
41
+ if (line.match(/^[\*\-]\s+/)) {
42
+ if (!inList || listType !== 'ul') {
43
+ if (inList) html += `</${listType}>`;
44
+ html += '<ul>'; inList = true; listType = 'ul';
45
+ }
46
+ html += `<li>${inline(line.replace(/^[\*\-]\s+/, ''))}</li>`;
47
+ continue;
48
+ }
49
 
50
+ // Close list if needed
51
+ if (inList && line.trim() === '') {
52
+ html += `</${listType}>`;
53
+ inList = false; listType = '';
54
+ }
 
 
 
 
 
 
 
 
55
 
56
+ // Headers
57
+ if (line.startsWith('### ')) { html += `<h3>${inline(line.slice(4))}</h3>`; continue; }
58
+ if (line.startsWith('## ')) { html += `<h2>${inline(line.slice(3))}</h2>`; continue; }
59
+ if (line.startsWith('# ')) { html += `<h1>${inline(line.slice(2))}</h1>`; continue; }
 
60
 
61
+ // Empty line = paragraph break
62
+ if (line.trim() === '') { html += '<br>'; continue; }
 
 
 
 
63
 
64
+ // Normal line
65
+ html += `<p>${inline(line)}</p>`;
 
 
 
 
 
 
 
 
 
 
66
  }
 
67
 
68
+ // Close any open tags
69
+ if (inTable) html += tableHtml + '</table>';
70
+ if (inList) html += `</${listType}>`;
 
71
 
72
+ return html;
 
 
 
 
 
73
  }
74
 
75
+ function inline(text) {
76
+ return text
77
+ .replace(/&/g, '&amp;')
78
+ .replace(/</g, '&lt;')
79
+ .replace(/>/g, '&gt;')
 
 
 
 
 
 
 
 
 
80
  .replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
81
  .replace(/\*(.+?)\*/g, '<em>$1</em>')
82
+ .replace(/`(.+?)`/g, '<code>$1</code>');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
83
  }