From 59b39ff85b4f0aa52ca35b361e58cfbc3bd66515 Mon Sep 17 00:00:00 2001 From: davegilligan Date: Mon, 18 May 2026 08:22:41 +0200 Subject: [PATCH] feat(redact): tag highlighting, inventory panel, before/after toggle, gpt-4o upgrade MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - CSS: colour-coded [TAG] spans by entity type (person=pink, org=blue, place=green, date=amber, id=purple) - Inventory panel: collapsible list showing tag → original text mappings with occurrence counts, sourced from new redaction_map API response key - Before/after toggle: Redacted / Original view-switch buttons wired to lastOriginalText captured at submission time - One-click gpt-4o upgrade button when mini or GPU engine was used - Backend: redaction_map built from applied LLM entities (tag → originals + occurrence count via substr_count on final text) - renderResults now calls setupRedactViewToggle() after DOM is written Co-Authored-By: Claude Sonnet 4.6 --- assets/css/tools.css | 109 ++++++++++++++++++++++++++++++++++++++++ assets/js/tools.js | 2 + includes/LegalTools.php | 21 ++++++++ 3 files changed, 132 insertions(+) diff --git a/assets/css/tools.css b/assets/css/tools.css index 2a8ea7a..01fb4b9 100644 --- a/assets/css/tools.css +++ b/assets/css/tools.css @@ -897,6 +897,115 @@ p { line-height: 1.55; } +/* ── Redact: tag highlighting ── */ +.redact-tag { + display: inline; + border-radius: 4px; + padding: 1px 4px; + font-weight: 600; + font-size: 0.88em; +} +.redact-tag--person { background: #fce7f3; color: #9d174d; } +.redact-tag--org { background: #dbeafe; color: #1e40af; } +.redact-tag--place { background: #dcfce7; color: #166534; } +.redact-tag--date { background: #fef3c7; color: #92400e; } +.redact-tag--id { background: #ede9fe; color: #5b21b6; } + +/* ── Redact: view toggle ── */ +.redact-view-toggle { + display: flex; + gap: 6px; + margin-bottom: 8px; +} +.view-btn { + padding: 4px 14px; + border-radius: 999px; + border: 1px solid var(--line); + background: transparent; + color: var(--muted); + font-size: 0.82em; + cursor: pointer; + transition: background 0.15s, color 0.15s; +} +.view-btn.is-active { + background: var(--ink); + color: #fff; + border-color: var(--ink); +} + +/* ── Redact: inventory panel ── */ +.redact-inventory { + margin: 10px 0 6px; + border: 1px solid var(--line); + border-radius: 8px; + overflow: hidden; +} +.redact-inventory summary { + padding: 8px 12px; + cursor: pointer; + font-size: 0.85em; + font-weight: 600; + color: var(--muted); + background: #fafafa; + list-style: none; + display: flex; + align-items: center; + gap: 8px; +} +.redact-inventory summary::-webkit-details-marker { display: none; } +.inv-badge { + background: var(--line); + color: var(--muted); + border-radius: 999px; + padding: 1px 7px; + font-size: 0.8em; + font-weight: 700; +} +.inv-list { + list-style: none; + padding: 8px 12px; + margin: 0; + display: flex; + flex-direction: column; + gap: 6px; + background: #fff; +} +.inv-list li { + display: flex; + align-items: baseline; + gap: 6px; + font-size: 0.84em; + flex-wrap: wrap; +} +.inv-originals { color: var(--muted); } +.inv-originals em { font-style: normal; color: var(--ink); font-weight: 500; } +.inv-count { + margin-left: auto; + color: var(--muted); + font-size: 0.82em; + white-space: nowrap; +} + +/* ── Redact: upgrade engine button ── */ +.upgrade-engine-btn { + display: block; + margin: 10px 0 4px; + background: none; + border: 1px dashed var(--line); + border-radius: 8px; + padding: 8px 14px; + font-size: 0.83em; + color: var(--muted); + cursor: pointer; + width: 100%; + text-align: left; + transition: border-color 0.15s, color 0.15s; +} +.upgrade-engine-btn:hover { + border-color: var(--ink); + color: var(--ink); +} + .pill-list { display: flex; flex-wrap: wrap; diff --git a/assets/js/tools.js b/assets/js/tools.js index a1c04bd..d0744af 100644 --- a/assets/js/tools.js +++ b/assets/js/tools.js @@ -1317,6 +1317,8 @@ function renderResults(data) { els.results.innerHTML = sections.join(''); setupFeedbackWidget(data.tool || state.activeTool); + if (data.tool === 'redact') setupRedactViewToggle(); + const sortDoc = document.getElementById('sortDocOrder'); const sortChr = document.getElementById('sortChronological'); if (sortDoc && sortChr) { diff --git a/includes/LegalTools.php b/includes/LegalTools.php index ecef6e7..a413f23 100644 --- a/includes/LegalTools.php +++ b/includes/LegalTools.php @@ -512,6 +512,7 @@ PROMPT; $finalRedacted = $preRedacted; $pass2Counts = []; $llmDeployment = null; + $redactionMap = []; $llmResult = $this->llmRedactionPass( $preRedacted, $language, $aliases, $engine, @@ -527,6 +528,7 @@ PROMPT; $entities = $llmResult['entities'] ?? []; $llmDeployment = $llmResult['deployment'] ?? null; $applied = 0; + $redactionMap = []; foreach ($entities as $entity) { if (!is_array($entity)) { @@ -549,14 +551,32 @@ PROMPT; $finalRedacted = $replaced; $pass2Counts[$type] = ($pass2Counts[$type] ?? 0) + 1; $applied++; + if (!isset($redactionMap[$tag])) { + $redactionMap[$tag] = ['originals' => [], 'type' => $type]; + } + if (!in_array($original, $redactionMap[$tag]['originals'], true)) { + $redactionMap[$tag]['originals'][] = $original; + } } elseif (str_contains($finalRedacted, $original)) { // Fallback for names adjacent to punctuation or non-word characters $finalRedacted = str_replace($original, $tag, $finalRedacted); $pass2Counts[$type] = ($pass2Counts[$type] ?? 0) + 1; $applied++; + if (!isset($redactionMap[$tag])) { + $redactionMap[$tag] = ['originals' => [], 'type' => $type]; + } + if (!in_array($original, $redactionMap[$tag]['originals'], true)) { + $redactionMap[$tag]['originals'][] = $original; + } } } + // Add occurrence counts by scanning the final text + foreach ($redactionMap as $tag => &$entry) { + $entry['occurrences'] = substr_count($finalRedacted, $tag); + } + unset($entry); + $pass2Detail = $applied > 0 ? "{$applied} additional: " . implode(', ', array_map(fn($k, $v) => "{$k}: {$v}", array_keys($pass2Counts), $pass2Counts)) : 'no additional entities found'; @@ -592,6 +612,7 @@ PROMPT; 'redacted_text' => $finalRedacted, 'detected_entity_categories' => $categories, 'entity_counts' => $allCounts, + 'redaction_map' => $redactionMap, 'evidence_trail' => [['title' => 'Pasted text', 'excerpt' => 'Processed in-memory only; not stored.']], 'what_remains_uncertain' => ['Human review is still recommended for contextual identification.'], 'next_practical_step' => 'Review the output and rerun in strict mode if the text will be shared broadly.',