nvikou commited on
Commit
4bdc0c5
·
verified ·
1 Parent(s): 13dd3be

Product UI redesign (light) + disable API key for public demo

Browse files
Files changed (1) hide show
  1. static/app.js +67 -124
static/app.js CHANGED
@@ -1,55 +1,44 @@
1
  const API_BASE = "/api";
2
 
3
  const DECISION_HINTS = {
4
- CORRECT:
5
- "Contexte suffisamment pertinent. Les chunks parasites sont filtrés avant génération.",
6
- AMBIGUOUS:
7
- "Qualité intermédiaire. Réécriture de requête et recherche complémentaire.",
8
- INCORRECT:
9
- "Contexte faible. Le pipeline écarte les documents et tente une requête structurée ou un refus.",
10
  };
11
 
12
  const METRIC_META = {
13
  context_precision: {
14
- label: "Context precision",
15
- definition: "Part des passages retenus réellement utiles (vs annotation).",
16
  higherIsBetter: true,
17
  headline: false,
18
  },
19
  context_recall: {
20
- label: "Context recall",
21
- definition: "Part de l'information nécessaire effectivement récupérée.",
22
  higherIsBetter: true,
23
  headline: true,
24
  },
25
  faithfulness: {
26
- label: "Faithfulness",
27
- definition: "Réponse soutenue par les documents / données fournis.",
28
  higherIsBetter: true,
29
  headline: true,
30
  },
31
  answer_relevancy: {
32
- label: "Answer relevancy",
33
- definition: "Similarité texte question ↔ réponse.",
34
  higherIsBetter: true,
35
  headline: false,
36
  },
37
  abstention_accuracy: {
38
- label: "Abstention accuracy",
39
- definition: "Justesse du choix répondre vs refuser quand les preuves manquent.",
40
  higherIsBetter: true,
41
  headline: true,
42
  },
43
  sql_execution_accuracy: {
44
- label: "SQL execution accuracy",
45
- definition: "Routage et résultat SQL corrects pour les questions chiffrées.",
46
  higherIsBetter: true,
47
  headline: false,
48
  },
49
  fallback_rate: {
50
- label: "Correction rate",
51
- definition:
52
- "Fréquence des actions correctives (réécriture, requête structurée, re-recherche). Plus élevé = plus de corrections déclenchées.",
53
  higherIsBetter: null,
54
  headline: false,
55
  },
@@ -61,60 +50,60 @@ const HEADLINE_ORDER = [
61
  "context_recall",
62
  ];
63
 
64
- // ---------- Navigation ----------
65
  const views = {
66
  chat: "view-chat",
67
  metrics: "view-metrics",
68
  about: "view-about",
69
  };
70
 
71
- document.querySelectorAll(".nav-item").forEach((btn) => {
72
  btn.addEventListener("click", () => {
73
- document.querySelectorAll(".nav-item").forEach((b) => {
74
- b.classList.remove("active");
75
- });
76
  btn.classList.add("active");
77
  Object.values(views).forEach((id) => {
78
  document.getElementById(id).classList.add("hidden");
79
  });
80
  document.getElementById(views[btn.dataset.view]).classList.remove("hidden");
81
- if (btn.dataset.view === "metrics") {
82
- loadMetrics();
83
- }
84
  });
85
  });
86
 
87
- // ---------- Statut backend ----------
88
  async function checkHealth() {
89
  const dot = document.getElementById("status-dot");
90
  const text = document.getElementById("status-text");
91
- const modelChip = document.getElementById("model-chip");
92
  try {
93
  const res = await fetch(`${API_BASE}/health`);
94
  const data = await res.json();
95
  dot.classList.add("ok");
96
- text.textContent = `${data.chunks_indexed} segments indexés`;
97
- modelChip.textContent =
98
- data.model + (data.llm_provider === "mock" ? " (mock)" : "");
99
  } catch (e) {
100
  dot.classList.add("err");
101
- text.textContent = "Backend indisponible";
102
  }
103
  }
104
  checkHealth();
105
 
106
- // ---------- Chat ----------
107
  const messagesEl = document.getElementById("messages");
108
  const form = document.getElementById("composer");
109
  const input = document.getElementById("question-input");
110
  const sendBtn = document.getElementById("send-btn");
111
 
 
 
 
 
 
 
 
 
112
  function appendMessage(role, text, options = {}) {
113
  const wrapper = document.createElement("div");
114
  const extras = [];
115
  if (options.isRefusal) extras.push("refusal");
116
  if (options.thinking) extras.push("thinking");
117
- wrapper.className = `message ${role}${extras.length ? " " + extras.join(" ") : ""}`;
 
118
  const bubble = document.createElement("div");
119
  bubble.className = "bubble";
120
  bubble.textContent = text;
@@ -127,48 +116,33 @@ function appendMessage(role, text, options = {}) {
127
  function renderGrades(grades) {
128
  const box = document.getElementById("grades-list");
129
  box.innerHTML = "";
130
- if (!grades || grades.length === 0) {
131
- box.innerHTML = '<p class="empty">Aucun grade disponible.</p>';
132
- return;
133
- }
134
  grades.slice(0, 5).forEach((g) => {
135
  const row = document.createElement("div");
136
  row.className = "grade-row";
137
- const doc = g.doc_id || g.chunk_id || "chunk";
138
- const decision = g.decision || "—";
139
- const score = typeof g.score === "number" ? g.score : "—";
140
- const reason = g.reason || "";
141
  row.innerHTML = `
142
  <div class="grade-doc">${escapeHtml(String(doc))}</div>
143
- <div class="grade-score">${score}/100</div>
144
- <div class="grade-meta">${escapeHtml(decision)}${reason ? " · " + escapeHtml(reason) : ""}</div>
 
 
145
  `;
146
  box.appendChild(row);
147
  });
148
  }
149
 
150
- function escapeHtml(value) {
151
- return value
152
- .replaceAll("&", "&amp;")
153
- .replaceAll("<", "&lt;")
154
- .replaceAll(">", "&gt;")
155
- .replaceAll('"', "&quot;");
156
- }
157
-
158
  function renderContext(data) {
159
  const badge = document.getElementById("decision-badge");
160
  const hint = document.getElementById("decision-hint");
161
  badge.textContent = data.decision || "—";
162
- badge.className = `decision-badge ${data.decision || ""}`;
163
- hint.textContent =
164
- DECISION_HINTS[data.decision] ||
165
- "Décision du retrieval evaluator.";
166
 
167
  const sourceList = document.getElementById("source-list");
168
  sourceList.innerHTML = "";
169
- if (!data.sources || data.sources.length === 0) {
170
- sourceList.innerHTML =
171
- '<li class="empty">Aucune source (réponse structurée ou refus).</li>';
172
  } else {
173
  data.sources.forEach((s) => {
174
  const li = document.createElement("li");
@@ -195,16 +169,16 @@ function renderContext(data) {
195
  document.getElementById("diag-loops").textContent = data.loop_count;
196
  document.getElementById("diag-latency").textContent =
197
  `${data.latency_ms} ms`;
 
 
 
198
  }
199
 
200
  async function sendQuestion(question) {
201
  appendMessage("user", question);
202
  input.value = "";
203
  sendBtn.disabled = true;
204
-
205
- const thinking = appendMessage("assistant", "Analyse en cours…", {
206
- thinking: true,
207
- });
208
 
209
  try {
210
  const res = await fetch(`${API_BASE}/chat`, {
@@ -215,9 +189,11 @@ async function sendQuestion(question) {
215
  const data = await res.json();
216
  thinking.remove();
217
  if (!res.ok) {
218
- appendMessage("assistant", data.detail || "Erreur inconnue.", {
219
- isRefusal: true,
220
- });
 
 
221
  return;
222
  }
223
  const isRefusal =
@@ -228,7 +204,7 @@ async function sendQuestion(question) {
228
  renderContext(data);
229
  } catch (e) {
230
  thinking.remove();
231
- appendMessage("assistant", "Impossible de contacter le backend.", {
232
  isRefusal: true,
233
  });
234
  } finally {
@@ -242,11 +218,10 @@ form.addEventListener("submit", (e) => {
242
  if (q) sendQuestion(q);
243
  });
244
 
245
- document.querySelectorAll(".suggestion-chip").forEach((chip) => {
246
  chip.addEventListener("click", () => sendQuestion(chip.dataset.q));
247
  });
248
 
249
- // ---------- Métriques ----------
250
  let metricsLoaded = false;
251
 
252
  function pct(value) {
@@ -259,16 +234,6 @@ function formatPct(value) {
259
  return p === null ? "—" : `${p.toFixed(1)} %`;
260
  }
261
 
262
- function deltaClass(key, deltaPts) {
263
- const meta = METRIC_META[key];
264
- if (meta.higherIsBetter === null || Math.abs(deltaPts) < 0.05) {
265
- return "neutral";
266
- }
267
- const improved =
268
- meta.higherIsBetter === true ? deltaPts > 0 : deltaPts < 0;
269
- return improved ? "up" : "down";
270
- }
271
-
272
  function formatDelta(deltaPts) {
273
  const sign = deltaPts > 0 ? "+" : "";
274
  return `${sign}${deltaPts.toFixed(1)} pts`;
@@ -293,9 +258,9 @@ async function loadMetrics() {
293
  const classic = data.rag_classique || {};
294
 
295
  document.getElementById("metrics-n").textContent =
296
- `Jeu de test · n = ${data.n_questions ?? "—"}`;
297
  document.getElementById("metrics-generated").textContent = data.generated_at
298
- ? `Généré le ${data.generated_at.replace("T", " ")}`
299
  : "";
300
 
301
  const headlineGrid = document.getElementById("headline-grid");
@@ -306,20 +271,15 @@ async function loadMetrics() {
306
  const value = pct(crag[key]);
307
  const baseline = pct(classic[key]);
308
  const card = document.createElement("article");
309
- card.className = "headline-card";
310
- let deltaHtml = "";
311
  if (baseline !== null) {
312
- const delta = value - baseline;
313
- const cls = deltaClass(key, delta);
314
- deltaHtml = `
315
- <div class="headline-delta ${cls}">${formatDelta(delta)} vs classique</div>
316
- <div class="headline-note">RAG classique : ${baseline.toFixed(1)} %</div>
317
- `;
318
  }
319
  card.innerHTML = `
320
- <div class="headline-label">${meta.label}</div>
321
- <div class="headline-value">${value.toFixed(1)}%</div>
322
- ${deltaHtml}
323
  `;
324
  headlineGrid.appendChild(card);
325
  });
@@ -330,22 +290,16 @@ async function loadMetrics() {
330
  if (typeof crag[key] !== "number" && typeof classic[key] !== "number") {
331
  return;
332
  }
333
- const meta = METRIC_META[key];
334
  const cVal = pct(classic[key]);
335
  const rVal = pct(crag[key]);
336
- let deltaCell = "—";
337
- if (cVal !== null && rVal !== null) {
338
- const delta = rVal - cVal;
339
- const cls = deltaClass(key, delta);
340
- deltaCell = `<span class="delta-pill ${cls}">${formatDelta(delta)}</span>`;
341
- }
342
  const tr = document.createElement("tr");
343
  tr.innerHTML = `
344
- <td class="metric-name">${meta.label}</td>
345
- <td class="metric-def">${meta.definition}</td>
346
  <td class="num">${formatPct(classic[key])}</td>
347
  <td class="num">${formatPct(crag[key])}</td>
348
- <td class="num">${deltaCell}</td>
349
  `;
350
  tbody.appendChild(tr);
351
  });
@@ -354,31 +308,20 @@ async function loadMetrics() {
354
  rows.innerHTML = "";
355
  Object.keys(METRIC_META).forEach((key) => {
356
  if (typeof crag[key] !== "number") return;
357
- const meta = METRIC_META[key];
358
  const classicVal = pct(classic[key]) ?? 0;
359
  const cragVal = pct(crag[key]) ?? 0;
360
  const row = document.createElement("div");
 
361
  row.innerHTML = `
362
- <div class="compare-row-label">${meta.label}</div>
363
- <div class="compare-bars">
364
- <div class="compare-bar-track">
365
- <div class="compare-bar-fill classic" style="width:${Math.max(classicVal, 8)}%">
366
- ${classicVal.toFixed(1)}%
367
- </div>
368
- </div>
369
- <div class="compare-bar-track">
370
- <div class="compare-bar-fill crag" style="width:${Math.max(cragVal, 8)}%">
371
- ${cragVal.toFixed(1)}%
372
- </div>
373
- </div>
374
- </div>
375
  `;
376
  rows.appendChild(row);
377
  });
378
 
379
- const footnote = document.getElementById("metrics-footnote");
380
- footnote.textContent =
381
- "Les scores mock (LLM_PROVIDER=mock) sont structurellement valides " +
382
- "mais non représentatifs. Relancez eval/evaluate.py avec HF_TOKEN " +
383
- "pour des chiffres de production.";
384
  }
 
1
  const API_BASE = "/api";
2
 
3
  const DECISION_HINTS = {
4
+ CORRECT: "Contexte assez solide. Passages inutiles filtrés.",
5
+ AMBIGUOUS: "Qualité moyenne. Reformulation et recherche complémentaire.",
6
+ INCORRECT: "Contexte trop faible. SQL ou refus.",
 
 
 
7
  };
8
 
9
  const METRIC_META = {
10
  context_precision: {
11
+ label: "Précision du contexte",
 
12
  higherIsBetter: true,
13
  headline: false,
14
  },
15
  context_recall: {
16
+ label: "Couverture",
 
17
  higherIsBetter: true,
18
  headline: true,
19
  },
20
  faithfulness: {
21
+ label: "Fidélité",
 
22
  higherIsBetter: true,
23
  headline: true,
24
  },
25
  answer_relevancy: {
26
+ label: "Pertinence",
 
27
  higherIsBetter: true,
28
  headline: false,
29
  },
30
  abstention_accuracy: {
31
+ label: "Refus juste",
 
32
  higherIsBetter: true,
33
  headline: true,
34
  },
35
  sql_execution_accuracy: {
36
+ label: "Exactitude SQL",
 
37
  higherIsBetter: true,
38
  headline: false,
39
  },
40
  fallback_rate: {
41
+ label: "Taux de correction",
 
 
42
  higherIsBetter: null,
43
  headline: false,
44
  },
 
50
  "context_recall",
51
  ];
52
 
 
53
  const views = {
54
  chat: "view-chat",
55
  metrics: "view-metrics",
56
  about: "view-about",
57
  };
58
 
59
+ document.querySelectorAll(".tab").forEach((btn) => {
60
  btn.addEventListener("click", () => {
61
+ document.querySelectorAll(".tab").forEach((b) => b.classList.remove("active"));
 
 
62
  btn.classList.add("active");
63
  Object.values(views).forEach((id) => {
64
  document.getElementById(id).classList.add("hidden");
65
  });
66
  document.getElementById(views[btn.dataset.view]).classList.remove("hidden");
67
+ if (btn.dataset.view === "metrics") loadMetrics();
 
 
68
  });
69
  });
70
 
 
71
  async function checkHealth() {
72
  const dot = document.getElementById("status-dot");
73
  const text = document.getElementById("status-text");
 
74
  try {
75
  const res = await fetch(`${API_BASE}/health`);
76
  const data = await res.json();
77
  dot.classList.add("ok");
78
+ text.textContent = "Prêt";
79
+ if (data.llm_provider === "mock") text.textContent = "Prêt (mock)";
 
80
  } catch (e) {
81
  dot.classList.add("err");
82
+ text.textContent = "Hors ligne";
83
  }
84
  }
85
  checkHealth();
86
 
 
87
  const messagesEl = document.getElementById("messages");
88
  const form = document.getElementById("composer");
89
  const input = document.getElementById("question-input");
90
  const sendBtn = document.getElementById("send-btn");
91
 
92
+ function escapeHtml(value) {
93
+ return String(value)
94
+ .replaceAll("&", "&amp;")
95
+ .replaceAll("<", "&lt;")
96
+ .replaceAll(">", "&gt;")
97
+ .replaceAll('"', "&quot;");
98
+ }
99
+
100
  function appendMessage(role, text, options = {}) {
101
  const wrapper = document.createElement("div");
102
  const extras = [];
103
  if (options.isRefusal) extras.push("refusal");
104
  if (options.thinking) extras.push("thinking");
105
+ wrapper.className =
106
+ `message ${role}` + (extras.length ? " " + extras.join(" ") : "");
107
  const bubble = document.createElement("div");
108
  bubble.className = "bubble";
109
  bubble.textContent = text;
 
116
  function renderGrades(grades) {
117
  const box = document.getElementById("grades-list");
118
  box.innerHTML = "";
119
+ if (!grades || !grades.length) return;
 
 
 
120
  grades.slice(0, 5).forEach((g) => {
121
  const row = document.createElement("div");
122
  row.className = "grade-row";
123
+ const doc = g.doc_id || g.chunk_id || "passage";
 
 
 
124
  row.innerHTML = `
125
  <div class="grade-doc">${escapeHtml(String(doc))}</div>
126
+ <div class="grade-score">${g.score ?? "—"}/100</div>
127
+ <div class="grade-meta">${escapeHtml(g.decision || "")}${
128
+ g.reason ? " · " + escapeHtml(g.reason) : ""
129
+ }</div>
130
  `;
131
  box.appendChild(row);
132
  });
133
  }
134
 
 
 
 
 
 
 
 
 
135
  function renderContext(data) {
136
  const badge = document.getElementById("decision-badge");
137
  const hint = document.getElementById("decision-hint");
138
  badge.textContent = data.decision || "—";
139
+ badge.className = `decision ${data.decision || ""}`;
140
+ hint.textContent = DECISION_HINTS[data.decision] || "";
 
 
141
 
142
  const sourceList = document.getElementById("source-list");
143
  sourceList.innerHTML = "";
144
+ if (!data.sources || !data.sources.length) {
145
+ sourceList.innerHTML = '<li class="muted">Aucune source</li>';
 
146
  } else {
147
  data.sources.forEach((s) => {
148
  const li = document.createElement("li");
 
169
  document.getElementById("diag-loops").textContent = data.loop_count;
170
  document.getElementById("diag-latency").textContent =
171
  `${data.latency_ms} ms`;
172
+
173
+ const drawer = document.getElementById("trace-drawer");
174
+ if (drawer && !drawer.open) drawer.open = true;
175
  }
176
 
177
  async function sendQuestion(question) {
178
  appendMessage("user", question);
179
  input.value = "";
180
  sendBtn.disabled = true;
181
+ const thinking = appendMessage("assistant", "Analyse…", { thinking: true });
 
 
 
182
 
183
  try {
184
  const res = await fetch(`${API_BASE}/chat`, {
 
189
  const data = await res.json();
190
  thinking.remove();
191
  if (!res.ok) {
192
+ const detail =
193
+ typeof data.detail === "string"
194
+ ? data.detail
195
+ : "Erreur serveur.";
196
+ appendMessage("assistant", detail, { isRefusal: true });
197
  return;
198
  }
199
  const isRefusal =
 
204
  renderContext(data);
205
  } catch (e) {
206
  thinking.remove();
207
+ appendMessage("assistant", "Impossible de joindre le serveur.", {
208
  isRefusal: true,
209
  });
210
  } finally {
 
218
  if (q) sendQuestion(q);
219
  });
220
 
221
+ document.querySelectorAll(".suggestion").forEach((chip) => {
222
  chip.addEventListener("click", () => sendQuestion(chip.dataset.q));
223
  });
224
 
 
225
  let metricsLoaded = false;
226
 
227
  function pct(value) {
 
234
  return p === null ? "—" : `${p.toFixed(1)} %`;
235
  }
236
 
 
 
 
 
 
 
 
 
 
 
237
  function formatDelta(deltaPts) {
238
  const sign = deltaPts > 0 ? "+" : "";
239
  return `${sign}${deltaPts.toFixed(1)} pts`;
 
258
  const classic = data.rag_classique || {};
259
 
260
  document.getElementById("metrics-n").textContent =
261
+ `${data.n_questions ?? "—"} questions`;
262
  document.getElementById("metrics-generated").textContent = data.generated_at
263
+ ? data.generated_at.replace("T", " ")
264
  : "";
265
 
266
  const headlineGrid = document.getElementById("headline-grid");
 
271
  const value = pct(crag[key]);
272
  const baseline = pct(classic[key]);
273
  const card = document.createElement("article");
274
+ card.className = "score-card";
275
+ let delta = "";
276
  if (baseline !== null) {
277
+ delta = `<div class="delta">${formatDelta(value - baseline)} vs classique</div>`;
 
 
 
 
 
278
  }
279
  card.innerHTML = `
280
+ <div class="label">${meta.label}</div>
281
+ <div class="value">${value.toFixed(1)}%</div>
282
+ ${delta}
283
  `;
284
  headlineGrid.appendChild(card);
285
  });
 
290
  if (typeof crag[key] !== "number" && typeof classic[key] !== "number") {
291
  return;
292
  }
 
293
  const cVal = pct(classic[key]);
294
  const rVal = pct(crag[key]);
295
+ let delta = "—";
296
+ if (cVal !== null && rVal !== null) delta = formatDelta(rVal - cVal);
 
 
 
 
297
  const tr = document.createElement("tr");
298
  tr.innerHTML = `
299
+ <td>${METRIC_META[key].label}</td>
 
300
  <td class="num">${formatPct(classic[key])}</td>
301
  <td class="num">${formatPct(crag[key])}</td>
302
+ <td class="num">${delta}</td>
303
  `;
304
  tbody.appendChild(tr);
305
  });
 
308
  rows.innerHTML = "";
309
  Object.keys(METRIC_META).forEach((key) => {
310
  if (typeof crag[key] !== "number") return;
 
311
  const classicVal = pct(classic[key]) ?? 0;
312
  const cragVal = pct(crag[key]) ?? 0;
313
  const row = document.createElement("div");
314
+ row.className = "bar-item";
315
  row.innerHTML = `
316
+ <div class="name">${METRIC_META[key].label}</div>
317
+ <div class="bar-track"><div class="bar-fill classic" style="width:${Math.max(classicVal, 2)}%"></div></div>
318
+ <div class="bar-caption">Classique ${classicVal.toFixed(1)}%</div>
319
+ <div class="bar-track"><div class="bar-fill" style="width:${Math.max(cragVal, 2)}%"></div></div>
320
+ <div class="bar-caption">Corrective ${cragVal.toFixed(1)}%</div>
 
 
 
 
 
 
 
 
321
  `;
322
  rows.appendChild(row);
323
  });
324
 
325
+ document.getElementById("metrics-footnote").textContent =
326
+ "Scores mock non représentatifs. Relancer evaluate.py avec HF_TOKEN.";
 
 
 
327
  }