CaffeinatedCoding commited on
Commit
2512b85
·
verified ·
1 Parent(s): 0bbd462

Update frontend/app.js

Browse files
Files changed (1) hide show
  1. frontend/app.js +216 -131
frontend/app.js CHANGED
@@ -1,53 +1,116 @@
1
- // Set to "" for HuggingFace (same origin), or "http://localhost:8000" for local dev
2
- const API_BASE = "";
3
 
 
 
 
4
  let isLoading = false;
5
 
6
- // Auto-resize textarea
7
- const input = document.getElementById("query-input");
8
- input.addEventListener("input", () => {
9
- input.style.height = "auto";
10
- input.style.height = Math.min(input.scrollHeight, 120) + "px";
 
 
 
11
  });
12
 
13
- // Submit on Enter (Shift+Enter for newline)
14
- input.addEventListener("keydown", (e) => {
15
- if (e.key === "Enter" && !e.shiftKey) {
16
- e.preventDefault();
17
- submitQuery();
18
- }
19
  });
20
 
21
- function fillQuery(card) {
22
- input.value = card.textContent;
23
- input.dispatchEvent(new Event("input"));
24
- input.focus();
25
  }
26
 
27
- async function submitQuery() {
28
- const query = input.value.trim();
29
- if (!query || isLoading) return;
30
- if (query.length < 10) { alert("Query too short — minimum 10 characters"); return; }
31
- if (query.length > 1000) { alert("Query too long — maximum 1000 characters"); return; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
32
 
33
- // Switch from welcome to chat
34
- document.getElementById("welcome-screen").classList.add("hidden");
35
- document.getElementById("chat-area").classList.remove("hidden");
 
 
 
 
 
 
 
 
 
 
 
 
36
 
37
- // Clear input
38
- input.value = "";
39
- input.style.height = "auto";
 
 
 
 
 
 
 
 
40
 
41
- // Add to history
42
- addToHistory(query);
 
 
 
 
 
 
 
 
 
 
 
43
 
44
- // Add user bubble
45
- appendMessage("user", query);
46
 
47
- // Add loading bubble
48
- const loadingId = appendLoading();
49
 
50
- // Disable button
 
51
  setLoading(true);
52
 
53
  try {
@@ -58,159 +121,181 @@ async function submitQuery() {
58
  });
59
 
60
  const data = await res.json();
61
- removeLoading(loadingId);
62
 
63
  if (!res.ok) {
64
- appendError(data.detail || "Something went wrong. Please try again.");
 
 
65
  } else {
66
- appendAnswer(data);
 
67
  }
 
68
  } catch (err) {
69
- removeLoading(loadingId);
70
- appendError("Could not reach the server. Please check your connection.");
 
 
71
  }
72
 
73
  setLoading(false);
74
- scrollToBottom();
75
  }
76
 
77
- function appendMessage(type, text) {
78
- const messages = document.getElementById("messages");
79
- const div = document.createElement("div");
80
- div.className = `message message-${type}`;
81
- div.innerHTML = `<div class="bubble-${type}">${escapeHtml(text)}</div>`;
82
- messages.appendChild(div);
83
- scrollToBottom();
84
  }
85
 
86
- function appendAnswer(data) {
87
- const messages = document.getElementById("messages");
88
  const div = document.createElement("div");
89
- div.className = "message message-ai";
 
 
 
 
90
 
91
- const verified = data.verification_status === "verified";
92
- const badge = verified
93
- ? `<span class="verification-badge badge-verified">✓ Verified</span>`
94
- : `<span class="verification-badge badge-unverified">⚠ Unverified</span>`;
95
 
96
  const truncNote = data.truncated
97
- ? `<div style="font-size:11px;color:#9aa3b2;margin-top:8px;">Only 3 of 5 retrieved documents used due to context length.</div>`
98
  : "";
99
 
100
- // Build sources HTML
101
- let sourcesHtml = "";
102
- if (data.sources && data.sources.length > 0) {
103
- const sourceItems = data.sources.map(s => {
104
- const meta = s.meta || {};
105
- const id = meta.judgment_id || "Unknown";
106
- const year = meta.year || "";
107
- const excerpt = (s.text || "").substring(0, 200) + "...";
108
- return `
109
- <div class="source-card">
110
- <div class="source-meta">${escapeHtml(id)}${year ? " · " + year : ""}</div>
111
- <div class="source-excerpt">${escapeHtml(excerpt)}</div>
112
- </div>`;
113
- }).join("");
114
-
115
- sourcesHtml = `
116
- <div class="sources-section">
117
- <button class="sources-toggle" onclick="toggleSources(this)">▶ Sources (${data.sources.length})</button>
118
- <div class="sources-list hidden">${sourceItems}</div>
119
- </div>`;
120
- }
121
 
 
 
122
  div.innerHTML = `
123
  <div class="bubble-ai">
124
- <div>${formatAnswer(data.answer)}</div>
125
- ${badge}
126
  ${truncNote}
127
- ${sourcesHtml}
 
 
 
 
128
  </div>`;
129
-
130
- messages.appendChild(div);
131
  }
132
 
133
- function appendError(msg) {
134
- const messages = document.getElementById("messages");
135
  const div = document.createElement("div");
136
- div.className = "message message-ai";
137
- div.innerHTML = `
138
- <div class="bubble-ai" style="border-left-color:#e05252; color:#e8a0a0;">
139
- ${escapeHtml(msg)}
140
- </div>`;
141
- messages.appendChild(div);
142
  }
143
 
144
- function appendLoading() {
145
- const messages = document.getElementById("messages");
146
- const id = "loading-" + Date.now();
147
  const div = document.createElement("div");
148
  div.id = id;
149
- div.className = "message message-ai";
150
  div.innerHTML = `
151
  <div class="bubble-ai bubble-loading">
152
- <div class="loading-dots">
153
- <span></span><span></span><span></span>
154
- </div>
155
- Searching judgments...
156
  </div>`;
157
- messages.appendChild(div);
158
- scrollToBottom();
159
  return id;
160
  }
161
 
162
- function removeLoading(id) {
163
  const el = document.getElementById(id);
164
  if (el) el.remove();
165
  }
166
 
167
- function toggleSources(btn) {
168
- const list = btn.nextElementSibling;
169
- const isHidden = list.classList.contains("hidden");
170
- list.classList.toggle("hidden");
171
- btn.textContent = (isHidden ? "" : "▶") + btn.textContent.slice(1);
172
- }
173
-
174
- function addToHistory(query) {
175
- const list = document.getElementById("history-list");
176
- const empty = list.querySelector(".history-empty");
177
- if (empty) empty.remove();
 
 
 
 
 
 
 
 
178
 
179
- const item = document.createElement("div");
180
- item.className = "history-item";
181
- item.title = query;
182
- item.textContent = query.length > 40 ? query.substring(0, 40) + "…" : query;
183
- list.insertBefore(item, list.firstChild);
184
 
185
- // Keep max 10 items
186
- const items = list.querySelectorAll(".history-item");
187
- if (items.length > 10) items[items.length - 1].remove();
 
 
188
  }
189
 
 
190
  function setLoading(state) {
191
  isLoading = state;
192
- const btn = document.getElementById("send-btn");
193
- btn.disabled = state;
194
- document.getElementById("send-icon").textContent = state ? "…" : "→";
 
 
 
 
 
 
 
195
  }
196
 
197
- function scrollToBottom() {
198
- const chat = document.getElementById("chat-area");
199
- chat.scrollTop = chat.scrollHeight;
200
  }
201
 
202
- function escapeHtml(text) {
203
- return String(text)
204
  .replace(/&/g, "&amp;")
205
  .replace(/</g, "&lt;")
206
  .replace(/>/g, "&gt;")
207
  .replace(/"/g, "&quot;");
208
  }
209
 
 
 
 
 
210
  function formatAnswer(text) {
211
- // Convert newlines to paragraphs
212
- return text
213
  .split(/\n\n+/)
214
- .map(p => `<p>${escapeHtml(p.trim())}</p>`)
215
  .join("");
 
 
 
 
 
216
  }
 
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 {
 
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
+ // Split double newlines into paragraphs, single newlines into line breaks
292
+ return (text || "")
293
  .split(/\n\n+/)
294
+ .map(para => `<p>${escHtml(para.trim()).replace(/\n/g, "<br>")}</p>`)
295
  .join("");
296
+ }
297
+
298
+ function showToast(msg) {
299
+ // Simple alert fallback — can be styled later
300
+ alert(msg);
301
  }