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) {