From d429e785e8f5e7d4b63638517e969fae8b002a3d Mon Sep 17 00:00:00 2001 From: davegilligan Date: Fri, 15 May 2026 01:13:42 +0200 Subject: [PATCH] feat(feedback): thumbs up/down + missed-items widget across all tools New api/feedback.php stores rating + correction text to tool_feedback table in bnl_admin. renderFeedbackWidget() appended to all tool results (timeline, redact, transcribe, ask, summarize, search). Thumbs reveal a textarea for missed/wrong items on click; submit POSTs asynchronously. Engine from last run is stored alongside the rating. Co-Authored-By: Claude Sonnet 4.6 --- api/feedback.php | 42 +++++++++++++++++++ assets/css/tools.css | 98 ++++++++++++++++++++++++++++++++++++++++++++ assets/js/tools.js | 76 +++++++++++++++++++++++++++++++++- 3 files changed, 215 insertions(+), 1 deletion(-) create mode 100644 api/feedback.php diff --git a/api/feedback.php b/api/feedback.php new file mode 100644 index 0000000..ea6959a --- /dev/null +++ b/api/feedback.php @@ -0,0 +1,42 @@ +prepare( + 'INSERT INTO tool_feedback (session_id, tool, rating, missed_or_wrong, engine) + VALUES (?, ?, ?, ?, ?)' + ); + $stmt->execute([ + substr(session_id(), 0, 40) ?: null, + $tool, + $rating, + $missed !== '' ? $missed : null, + $engine !== '' ? $engine : null, + ]); +} catch (Throwable $e) { + error_log('tool_feedback insert failed: ' . $e->getMessage()); + dbnToolsAbort('Could not save feedback.', 500, 'db_error'); +} + +header('Content-Type: application/json'); +echo json_encode(['ok' => true]); diff --git a/assets/css/tools.css b/assets/css/tools.css index c1c5153..66d3c2d 100644 --- a/assets/css/tools.css +++ b/assets/css/tools.css @@ -1578,3 +1578,101 @@ p { opacity: 0.55; cursor: progress; } + +/* ── Feedback widget ──────────────────────────────────────────── */ +.feedback-widget { + margin-top: 1.5rem; + padding: 1rem 1.25rem; + border-top: 1px solid var(--line); +} + +.feedback-label { + font-size: 0.85rem; + color: var(--muted); + margin: 0 0 0.6rem; +} + +.feedback-btns { + display: flex; + gap: 0.5rem; + margin-bottom: 0.75rem; +} + +.feedback-thumb { + background: var(--bg); + border: 1px solid var(--line); + border-radius: 8px; + padding: 0.35rem 0.75rem; + font-size: 1.2rem; + cursor: pointer; + transition: background 0.12s, border-color 0.12s; + line-height: 1; +} + +.feedback-thumb:hover { background: var(--soft-teal); border-color: var(--teal); } +.feedback-thumb.is-active { background: var(--soft-teal); border-color: var(--teal); } + +.feedback-detail { + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.feedback-detail-label { + font-size: 0.83rem; + color: var(--ink); + font-weight: 500; +} + +.feedback-optional { + color: var(--muted); + font-weight: 400; +} + +.feedback-textarea { + width: 100%; + border: 1px solid var(--line); + border-radius: 6px; + padding: 0.5rem 0.75rem; + font-size: 0.85rem; + resize: vertical; + font-family: inherit; + color: var(--ink); + background: var(--panel); +} + +.feedback-textarea:focus { outline: 3px solid var(--teal); outline-offset: 1px; } + +.feedback-detail-footer { + display: flex; + align-items: center; + gap: 1rem; +} + +.feedback-submit-btn { + background: var(--teal); + color: #fff; + border: none; + border-radius: 6px; + padding: 0.4rem 1rem; + font-size: 0.83rem; + font-weight: 600; + cursor: pointer; + transition: background 0.12s; +} + +.feedback-submit-btn:hover { background: var(--teal-dark); } +.feedback-submit-btn:disabled { opacity: 0.55; cursor: progress; } + +.feedback-status { + font-size: 0.8rem; + color: var(--muted); + margin: 0; +} + +.feedback-thanks { + font-size: 0.85rem; + color: var(--teal-dark); + font-weight: 500; + margin: 0; +} diff --git a/assets/js/tools.js b/assets/js/tools.js index 7967700..c89a369 100644 --- a/assets/js/tools.js +++ b/assets/js/tools.js @@ -361,6 +361,7 @@ let lastTimelineEvents = []; let audioQueue = []; // [{file, status: 'pending'|'processing'|'done'|'error', result}] let lastTranscriptData = null; let lastRedactedText = null; +let lastRunEngine = null; const VOCAB_PRESETS = { barnerett: 'Barnevernet, Fylkesnemnda, barnevernloven, barneloven, barnets beste, samvær, foreldreansvar, omsorgsovertakelse, sakkyndig, advokat, prosessfullmektig, dommer, vitne, tolk, bistandsadvokat, fosterforeldre, fosterhjem, akuttvedtak, statsforvalter, Bufetat, saksbehandler, rettslig medhold, begjæring, samtykke, tilsynsfører', @@ -1272,6 +1273,8 @@ function currentRedactionRegion() { } function renderResults(data) { + lastRunEngine = data.trace_metadata?.deployment || null; + const sections = []; sections.push(sectionHtml('What We Found', renderMainFinding(data))); sections.push(sectionHtml('Evidence Trail', renderEvidence(data))); @@ -1282,7 +1285,9 @@ function renderResults(data) { sections.push(`

${escapeHtml(data.disclaimer)}

`); } + sections.push(renderFeedbackWidget()); els.results.innerHTML = sections.join(''); + setupFeedbackWidget(data.tool || state.activeTool); } function renderMainFinding(data) { @@ -1381,6 +1386,72 @@ function renderTimeline(events) { }).join('')}`; } +function renderFeedbackWidget() { + return ` + + `; +} + +function setupFeedbackWidget(tool) { + const widget = document.getElementById('feedbackWidget'); + const detail = document.getElementById('feedbackDetail'); + const submit = document.getElementById('feedbackSubmit'); + const status = document.getElementById('feedbackStatus'); + const missed = document.getElementById('feedbackMissed'); + if (!widget) return; + + let chosenRating = null; + + widget.querySelectorAll('.feedback-thumb').forEach((btn) => { + btn.addEventListener('click', () => { + chosenRating = btn.dataset.rating; + widget.querySelectorAll('.feedback-thumb').forEach((b) => b.classList.remove('is-active')); + btn.classList.add('is-active'); + detail.classList.remove('is-hidden'); + missed.focus(); + }); + }); + + submit.addEventListener('click', async () => { + if (!chosenRating) return; + submit.disabled = true; + status.textContent = 'Saving…'; + try { + const resp = await fetch('api/feedback.php', { + method: 'POST', + credentials: 'same-origin', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + tool, + rating: chosenRating, + missed_or_wrong: missed.value.trim(), + engine: lastRunEngine || '', + }), + }); + const data = await resp.json().catch(() => ({})); + if (!resp.ok || !data.ok) throw new Error(data.error?.message || 'Save failed'); + widget.innerHTML = ''; + } catch (err) { + status.textContent = err.message; + submit.disabled = false; + } + }); +} + function exportTimelineCSV(events) { const header = ['Date', 'Date Type', 'Actor', 'Event', 'Source Excerpt', 'Confidence']; const rows = events.map((ev) => [ @@ -1613,6 +1684,7 @@ function renderTranscriptResults(data) { ` : ''; + lastRunEngine = data.engine || null; els.results.innerHTML = `

Transcript

@@ -1623,7 +1695,9 @@ function renderTranscriptResults(data) { ${dlSrtVtt} -
`; + + ${renderFeedbackWidget()}`; + setupFeedbackWidget('transcribe'); const traceMeta = []; if (data.duration_sec) traceMeta.push({ label: `Duration: ${Math.round(data.duration_sec)}s`, detail: '', status: 'complete' });