diff --git a/unredact/static/dom.js b/unredact/static/dom.js
index 1317b95..eead44c 100644
--- a/unredact/static/dom.js
+++ b/unredact/static/dom.js
@@ -39,6 +39,11 @@ export const solveAccept = document.getElementById("solve-accept");
export const solveStatus = document.getElementById("solve-status");
export const solveResults = document.getElementById("solve-results");
export const solveLoadMore = /** @type {HTMLButtonElement} */ (document.getElementById("solve-load-more"));
+export const solveValidate = /** @type {HTMLButtonElement} */ (document.getElementById("solve-validate"));
+export const validatePanel = document.getElementById("validate-panel");
+export const validateLeft = /** @type {HTMLInputElement} */ (document.getElementById("validate-left"));
+export const validateRight = /** @type {HTMLInputElement} */ (document.getElementById("validate-right"));
+export const validateRun = /** @type {HTMLButtonElement} */ (document.getElementById("validate-run"));
export const fontToolbar = document.getElementById("font-toolbar");
export const fontSelect = /** @type {HTMLSelectElement} */ (document.getElementById("font-select"));
export const sizeSlider = /** @type {HTMLInputElement} */ (document.getElementById("size-slider"));
diff --git a/unredact/static/index.html b/unredact/static/index.html
index f3d5c50..dc06528 100644
--- a/unredact/static/index.html
+++ b/unredact/static/index.html
@@ -150,8 +150,14 @@
+
+
+
+
+
+
diff --git a/unredact/static/solver.js b/unredact/static/solver.js
index 4740e6b..de75cf8 100644
--- a/unredact/static/solver.js
+++ b/unredact/static/solver.js
@@ -6,7 +6,9 @@ import {
solveCharset, solveTolerance, solveMode, solveFilter,
solveKnownStart, solveKnownEnd, solvePlural, solveVocab,
solveResults, solveStatus, solveStart, solveStop,
- solveAccept, solveLoadMore, redactionMarker, escapeHtml,
+ solveAccept, solveLoadMore, solveValidate,
+ validatePanel, validateLeft, validateRight, validateRun,
+ redactionMarker, escapeHtml,
} from './dom.js';
import { renderCanvas } from './canvas.js';
import { matchAssociates, tierBadgeClass, tierLabel, isVictimMatch, showAssocDetail } from './associates.js';
@@ -45,6 +47,8 @@ export function startSolve() {
solveStart.hidden = true;
solveStop.hidden = false;
solveAccept.hidden = true;
+ solveValidate.hidden = true;
+ validatePanel.hidden = true;
const body = {
font_id: fontId,
@@ -210,6 +214,9 @@ function handleSolveEvent(data, redactionId) {
solveStart.hidden = false;
solveStop.hidden = true;
activeEventSource = null;
+ if (totalFound > 0) {
+ solveValidate.hidden = false;
+ }
}
}
@@ -278,9 +285,88 @@ async function loadMore() {
}
}
+function showValidatePanel() {
+ const id = state.activeRedaction;
+ if (!id) return;
+ const r = state.redactions[id];
+ if (!r || !r.analysis) return;
+
+ const a = r.analysis;
+ const o = r.overrides || {};
+ const leftText = o.leftText ?? (a.segments.length > 0 ? a.segments[0].text : "");
+ const rightText = o.rightText ?? (a.segments.length > 1 ? a.segments[1].text : "");
+
+ validateLeft.value = leftText;
+ validateRight.value = rightText;
+ validatePanel.hidden = false;
+}
+
+async function runValidation() {
+ if (!currentSolveId) return;
+ validateRun.disabled = true;
+ validateRun.textContent = "Validating...";
+ solveStatus.textContent = `Validating ${totalFound} results...`;
+
+ try {
+ const resp = await fetch(`/api/solve/${currentSolveId}/validate`, {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({
+ left_context: validateLeft.value,
+ right_context: validateRight.value,
+ }),
+ });
+ if (!resp.ok) throw new Error("Validation failed");
+ const data = await resp.json();
+
+ // Replace results with scored list
+ solveResults.innerHTML = "";
+ displayedCount = 0;
+ solveLoadMore.hidden = true;
+
+ const redactionId = state.activeRedaction;
+ for (const item of data.results) {
+ renderScoredResult(item, redactionId);
+ }
+
+ solveStatus.textContent = `Validated. ${data.total} results scored.`;
+ validatePanel.hidden = true;
+ } catch (err) {
+ solveStatus.textContent = "Validation error: " + err.message;
+ } finally {
+ validateRun.disabled = false;
+ validateRun.textContent = "Run";
+ }
+}
+
+function renderScoredResult(data, redactionId) {
+ handleSolveEvent({ status: "match", ...data }, redactionId);
+
+ if (data.llm_score !== undefined) {
+ const results = solveResults.children;
+ for (const el of results) {
+ const textEl = el.querySelector(".result-text");
+ if (textEl && textEl.textContent === data.text && !el.querySelector(".llm-score")) {
+ const badge = document.createElement("span");
+ badge.className = "llm-score";
+ const score = data.llm_score;
+ if (score >= 70) badge.classList.add("score-high");
+ else if (score >= 30) badge.classList.add("score-mid");
+ else badge.classList.add("score-low");
+ badge.textContent = String(score);
+ badge.title = "LLM contextual fit score";
+ el.prepend(badge);
+ break;
+ }
+ }
+ }
+}
+
/** Set up solver button listeners. Call once from main.js. */
export function initSolver() {
solveStart.addEventListener("click", startSolve);
solveStop.addEventListener("click", stopSolve);
solveLoadMore.addEventListener("click", loadMore);
+ solveValidate.addEventListener("click", showValidatePanel);
+ validateRun.addEventListener("click", runValidation);
}
diff --git a/unredact/static/style.css b/unredact/static/style.css
index 0a4ee27..b719f66 100644
--- a/unredact/static/style.css
+++ b/unredact/static/style.css
@@ -827,6 +827,65 @@ main {
to { opacity: 0; }
}
+/* LLM validation score badges */
+.llm-score {
+ flex-shrink: 0;
+ padding: 2px 6px;
+ border-radius: 3px;
+ font-size: 0.75rem;
+ font-weight: bold;
+ margin-right: 4px;
+}
+
+.llm-score.score-high {
+ background: #2d8a4e;
+ color: #fff;
+}
+
+.llm-score.score-mid {
+ background: var(--yellow);
+ color: #000;
+}
+
+.llm-score.score-low {
+ background: var(--red);
+ color: #fff;
+}
+
+/* Validate panel */
+#validate-panel {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 8px;
+ padding: 8px;
+ background: rgba(255, 255, 255, 0.04);
+ border-radius: 4px;
+ margin-top: 4px;
+}
+
+#validate-panel label {
+ display: flex;
+ flex-direction: column;
+ flex: 1;
+ min-width: 120px;
+ font-size: 0.75rem;
+ color: var(--text-dim);
+}
+
+#validate-panel input {
+ background: var(--bg-input);
+ border: 1px solid var(--border-subtle);
+ color: var(--text);
+ padding: 4px 8px;
+ border-radius: 3px;
+ font-size: 0.85rem;
+ margin-top: 2px;
+}
+
+#validate-panel .solve-btn {
+ align-self: flex-end;
+}
+
/* ── Mobile (≤768px) ── */
@media (max-width: 768px) {