Download static/app.js from nvikou/corrective-rag-data-assistant: direct link, hf CLI and curl.
- Browser
- Download file 10.7 kB
-
https://huggingface.co/spaces/nvikou/corrective-rag-data-assistant/resolve/main/static/app.js
- Command line
-
hf download hf://spaces/nvikou/corrective-rag-data-assistant/static/app.js
-
curl -L -o app.js https://huggingface.co/spaces/nvikou/corrective-rag-data-assistant/resolve/main/static/app.js
10.7 kB
| const API_BASE = "/api"; | |
| const DECISION_HINTS = { | |
| CORRECT: "Contexte assez solide. Passages inutiles filtrés.", | |
| AMBIGUOUS: "Qualité moyenne. Reformulation et recherche complémentaire.", | |
| INCORRECT: "Contexte trop faible. SQL ou refus.", | |
| }; | |
| const METRIC_META = { | |
| context_precision: { | |
| label: "Précision retrieval", | |
| group: "retrieval", | |
| kind: "pct", | |
| higherIsBetter: true, | |
| }, | |
| context_recall: { | |
| label: "Rappel retrieval", | |
| group: "retrieval", | |
| kind: "pct", | |
| higherIsBetter: true, | |
| }, | |
| retrieval_hit_rate: { | |
| label: "Hit rate retrieval", | |
| group: "retrieval", | |
| kind: "pct", | |
| higherIsBetter: true, | |
| }, | |
| faithfulness: { | |
| label: "Fidélité", | |
| group: "quality", | |
| kind: "pct", | |
| higherIsBetter: true, | |
| }, | |
| answer_relevancy: { | |
| label: "Pertinence", | |
| group: "quality", | |
| kind: "pct", | |
| higherIsBetter: true, | |
| }, | |
| abstention_accuracy: { | |
| label: "Refus juste", | |
| group: "quality", | |
| kind: "pct", | |
| higherIsBetter: true, | |
| }, | |
| avg_latency_ms: { | |
| label: "Latence moyenne", | |
| group: "time", | |
| kind: "ms", | |
| higherIsBetter: false, | |
| }, | |
| sql_execution_accuracy: { | |
| label: "Exactitude SQL", | |
| group: "quality", | |
| kind: "pct", | |
| higherIsBetter: true, | |
| }, | |
| fallback_rate: { | |
| label: "Taux de correction", | |
| group: "quality", | |
| kind: "pct", | |
| higherIsBetter: null, | |
| }, | |
| }; | |
| const views = { | |
| chat: "view-chat", | |
| metrics: "view-metrics", | |
| about: "view-about", | |
| }; | |
| document.querySelectorAll(".nav-item").forEach((btn) => { | |
| btn.addEventListener("click", () => { | |
| document.querySelectorAll(".nav-item").forEach((b) => { | |
| b.classList.remove("active"); | |
| }); | |
| btn.classList.add("active"); | |
| Object.values(views).forEach((id) => { | |
| document.getElementById(id).classList.add("hidden"); | |
| }); | |
| document.getElementById(views[btn.dataset.view]).classList.remove("hidden"); | |
| if (btn.dataset.view === "metrics") loadMetrics(); | |
| }); | |
| }); | |
| async function checkHealth() { | |
| const dot = document.getElementById("status-dot"); | |
| const text = document.getElementById("status-text"); | |
| try { | |
| const res = await fetch(`${API_BASE}/health`); | |
| const data = await res.json(); | |
| if (data.status === "starting" || data.embedding_backend === "loading") { | |
| text.textContent = "Démarrage…"; | |
| setTimeout(checkHealth, 2000); | |
| return; | |
| } | |
| dot.classList.add("ok"); | |
| text.textContent = | |
| data.llm_provider === "mock" ? "Prêt (mock)" : "Prêt"; | |
| } catch (e) { | |
| dot.classList.add("err"); | |
| text.textContent = "Hors ligne"; | |
| } | |
| } | |
| checkHealth(); | |
| const messagesEl = document.getElementById("messages"); | |
| const form = document.getElementById("composer"); | |
| const input = document.getElementById("question-input"); | |
| const sendBtn = document.getElementById("send-btn"); | |
| function escapeHtml(value) { | |
| return String(value) | |
| .replaceAll("&", "&") | |
| .replaceAll("<", "<") | |
| .replaceAll(">", ">") | |
| .replaceAll('"', """); | |
| } | |
| function appendMessage(role, text, options = {}) { | |
| const wrapper = document.createElement("div"); | |
| const extras = []; | |
| if (options.isRefusal) extras.push("refusal"); | |
| if (options.thinking) extras.push("thinking"); | |
| wrapper.className = | |
| `message ${role}` + (extras.length ? " " + extras.join(" ") : ""); | |
| const bubble = document.createElement("div"); | |
| bubble.className = "bubble"; | |
| bubble.textContent = text; | |
| wrapper.appendChild(bubble); | |
| messagesEl.appendChild(wrapper); | |
| messagesEl.scrollTop = messagesEl.scrollHeight; | |
| return wrapper; | |
| } | |
| function renderGrades(grades) { | |
| const box = document.getElementById("grades-list"); | |
| box.innerHTML = ""; | |
| if (!grades || !grades.length) { | |
| box.innerHTML = '<p class="muted small">Aucun grade</p>'; | |
| return; | |
| } | |
| grades.slice(0, 5).forEach((g) => { | |
| const row = document.createElement("div"); | |
| row.className = "grade-row"; | |
| const doc = g.doc_id || g.chunk_id || "passage"; | |
| row.innerHTML = ` | |
| <div class="grade-doc">${escapeHtml(String(doc))}</div> | |
| <div class="grade-score">${g.score ?? "—"}/100</div> | |
| <div class="grade-meta">${escapeHtml(g.decision || "")}${ | |
| g.reason ? " · " + escapeHtml(g.reason) : "" | |
| }</div> | |
| `; | |
| box.appendChild(row); | |
| }); | |
| } | |
| function renderContext(data) { | |
| const badge = document.getElementById("decision-badge"); | |
| const hint = document.getElementById("decision-hint"); | |
| badge.textContent = data.decision || "—"; | |
| badge.className = `decision ${data.decision || ""}`; | |
| hint.textContent = DECISION_HINTS[data.decision] || ""; | |
| const sourceList = document.getElementById("source-list"); | |
| sourceList.innerHTML = ""; | |
| if (!data.sources || !data.sources.length) { | |
| sourceList.innerHTML = '<li class="muted">Aucune source</li>'; | |
| } else { | |
| data.sources.forEach((s) => { | |
| const li = document.createElement("li"); | |
| li.textContent = s; | |
| sourceList.appendChild(li); | |
| }); | |
| } | |
| renderGrades(data.grades || []); | |
| const traceList = document.getElementById("trace-list"); | |
| traceList.innerHTML = ""; | |
| const steps = data.trace || []; | |
| if (!steps.length) { | |
| traceList.innerHTML = | |
| '<li class="muted">En attente — posez une question</li>'; | |
| } else { | |
| steps.forEach((t, i) => { | |
| const li = document.createElement("li"); | |
| li.innerHTML = | |
| `<span class="step-idx">${i + 1}</span>` + | |
| `<span class="step-text">${escapeHtml(t)}</span>`; | |
| traceList.appendChild(li); | |
| }); | |
| } | |
| document.getElementById("diag-supported").textContent = data.supported | |
| ? "Oui" | |
| : "Non"; | |
| document.getElementById("diag-structured").textContent = | |
| data.used_structured_data ? "Oui" : "Non"; | |
| document.getElementById("diag-loops").textContent = data.loop_count; | |
| document.getElementById("diag-latency").textContent = | |
| `${data.latency_ms} ms`; | |
| } | |
| async function sendQuestion(question) { | |
| appendMessage("user", question); | |
| input.value = ""; | |
| sendBtn.disabled = true; | |
| const thinking = appendMessage("assistant", "Analyse…", { thinking: true }); | |
| try { | |
| const res = await fetch(`${API_BASE}/chat`, { | |
| method: "POST", | |
| headers: { "Content-Type": "application/json" }, | |
| body: JSON.stringify({ question }), | |
| }); | |
| const data = await res.json(); | |
| thinking.remove(); | |
| if (!res.ok) { | |
| const detail = | |
| typeof data.detail === "string" ? data.detail : "Erreur serveur."; | |
| appendMessage("assistant", detail, { isRefusal: true }); | |
| return; | |
| } | |
| const isRefusal = | |
| data.decision === "INCORRECT" && | |
| !data.used_structured_data && | |
| (!data.sources || data.sources.length === 0); | |
| appendMessage("assistant", data.answer, { isRefusal }); | |
| renderContext(data); | |
| } catch (e) { | |
| thinking.remove(); | |
| appendMessage("assistant", "Impossible de joindre le serveur.", { | |
| isRefusal: true, | |
| }); | |
| } finally { | |
| sendBtn.disabled = false; | |
| } | |
| } | |
| form.addEventListener("submit", (e) => { | |
| e.preventDefault(); | |
| const q = input.value.trim(); | |
| if (q) sendQuestion(q); | |
| }); | |
| document.querySelectorAll(".suggestion").forEach((chip) => { | |
| chip.addEventListener("click", () => sendQuestion(chip.dataset.q)); | |
| }); | |
| let metricsLoaded = false; | |
| function formatValue(key, value) { | |
| const meta = METRIC_META[key]; | |
| if (typeof value !== "number") return "—"; | |
| if (meta.kind === "ms") return `${Math.round(value)} ms`; | |
| return `${(value * 100).toFixed(1)} %`; | |
| } | |
| function formatDelta(key, classic, crag) { | |
| const meta = METRIC_META[key]; | |
| if (typeof classic !== "number" || typeof crag !== "number") return "—"; | |
| if (meta.kind === "ms") { | |
| const d = Math.round(crag - classic); | |
| const sign = d > 0 ? "+" : ""; | |
| return `${sign}${d} ms`; | |
| } | |
| const d = (crag - classic) * 100; | |
| const sign = d > 0 ? "+" : ""; | |
| return `${sign}${d.toFixed(1)} pts`; | |
| } | |
| function renderScoreCards(containerId, keys, crag, classic) { | |
| const grid = document.getElementById(containerId); | |
| grid.innerHTML = ""; | |
| keys.forEach((key) => { | |
| if (typeof crag[key] !== "number") return; | |
| const meta = METRIC_META[key]; | |
| const card = document.createElement("article"); | |
| card.className = "score-card"; | |
| const delta = | |
| typeof classic[key] === "number" | |
| ? `<div class="delta">${formatDelta(key, classic[key], crag[key])} vs classique</div>` | |
| : ""; | |
| card.innerHTML = ` | |
| <div class="label">${meta.label}</div> | |
| <div class="value">${formatValue(key, crag[key])}</div> | |
| ${delta} | |
| `; | |
| grid.appendChild(card); | |
| }); | |
| } | |
| async function loadMetrics() { | |
| if (metricsLoaded) return; | |
| const res = await fetch(`${API_BASE}/metrics`); | |
| const data = await res.json(); | |
| if (!data.available) { | |
| document.getElementById("metrics-empty").classList.remove("hidden"); | |
| document.getElementById("metrics-content").classList.add("hidden"); | |
| return; | |
| } | |
| metricsLoaded = true; | |
| document.getElementById("metrics-empty").classList.add("hidden"); | |
| document.getElementById("metrics-content").classList.remove("hidden"); | |
| const crag = data.corrective_rag || {}; | |
| const classic = data.rag_classique || {}; | |
| document.getElementById("metrics-n").textContent = | |
| `${data.n_questions ?? "—"} questions`; | |
| document.getElementById("metrics-generated").textContent = data.generated_at | |
| ? data.generated_at.replace("T", " ") | |
| : ""; | |
| renderScoreCards( | |
| "retrieval-grid", | |
| ["context_precision", "context_recall", "retrieval_hit_rate"], | |
| crag, | |
| classic, | |
| ); | |
| renderScoreCards( | |
| "quality-grid", | |
| ["faithfulness", "abstention_accuracy", "answer_relevancy"], | |
| crag, | |
| classic, | |
| ); | |
| renderScoreCards("time-grid", ["avg_latency_ms"], crag, classic); | |
| const tbody = document.getElementById("metrics-tbody"); | |
| tbody.innerHTML = ""; | |
| Object.keys(METRIC_META).forEach((key) => { | |
| if (typeof crag[key] !== "number" && typeof classic[key] !== "number") { | |
| return; | |
| } | |
| const tr = document.createElement("tr"); | |
| const delta = formatDelta(key, classic[key], crag[key]); | |
| const better = | |
| METRIC_META[key].higherIsBetter === true && | |
| typeof classic[key] === "number" && | |
| crag[key] > classic[key]; | |
| const worse = | |
| METRIC_META[key].higherIsBetter === true && | |
| typeof classic[key] === "number" && | |
| crag[key] < classic[key]; | |
| const cls = better ? "delta-up" : worse ? "delta-down" : ""; | |
| tr.innerHTML = ` | |
| <td>${METRIC_META[key].label}</td> | |
| <td class="num">${formatValue(key, classic[key])}</td> | |
| <td class="num strong">${formatValue(key, crag[key])}</td> | |
| <td class="num ${cls}">${delta}</td> | |
| `; | |
| tbody.appendChild(tr); | |
| }); | |
| document.getElementById("metrics-footnote").textContent = | |
| "Scores mock non représentatifs. Relancer evaluate.py avec HF_TOKEN."; | |
| } | |