feat(redact): tag highlighting, inventory panel, before/after toggle, gpt-4o upgrade
- 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 <noreply@anthropic.com>
This commit is contained in:
@@ -897,6 +897,115 @@ p {
|
|||||||
line-height: 1.55;
|
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 {
|
.pill-list {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
|||||||
@@ -1317,6 +1317,8 @@ function renderResults(data) {
|
|||||||
els.results.innerHTML = sections.join('');
|
els.results.innerHTML = sections.join('');
|
||||||
setupFeedbackWidget(data.tool || state.activeTool);
|
setupFeedbackWidget(data.tool || state.activeTool);
|
||||||
|
|
||||||
|
if (data.tool === 'redact') setupRedactViewToggle();
|
||||||
|
|
||||||
const sortDoc = document.getElementById('sortDocOrder');
|
const sortDoc = document.getElementById('sortDocOrder');
|
||||||
const sortChr = document.getElementById('sortChronological');
|
const sortChr = document.getElementById('sortChronological');
|
||||||
if (sortDoc && sortChr) {
|
if (sortDoc && sortChr) {
|
||||||
|
|||||||
@@ -512,6 +512,7 @@ PROMPT;
|
|||||||
$finalRedacted = $preRedacted;
|
$finalRedacted = $preRedacted;
|
||||||
$pass2Counts = [];
|
$pass2Counts = [];
|
||||||
$llmDeployment = null;
|
$llmDeployment = null;
|
||||||
|
$redactionMap = [];
|
||||||
|
|
||||||
$llmResult = $this->llmRedactionPass(
|
$llmResult = $this->llmRedactionPass(
|
||||||
$preRedacted, $language, $aliases, $engine,
|
$preRedacted, $language, $aliases, $engine,
|
||||||
@@ -527,6 +528,7 @@ PROMPT;
|
|||||||
$entities = $llmResult['entities'] ?? [];
|
$entities = $llmResult['entities'] ?? [];
|
||||||
$llmDeployment = $llmResult['deployment'] ?? null;
|
$llmDeployment = $llmResult['deployment'] ?? null;
|
||||||
$applied = 0;
|
$applied = 0;
|
||||||
|
$redactionMap = [];
|
||||||
|
|
||||||
foreach ($entities as $entity) {
|
foreach ($entities as $entity) {
|
||||||
if (!is_array($entity)) {
|
if (!is_array($entity)) {
|
||||||
@@ -549,14 +551,32 @@ PROMPT;
|
|||||||
$finalRedacted = $replaced;
|
$finalRedacted = $replaced;
|
||||||
$pass2Counts[$type] = ($pass2Counts[$type] ?? 0) + 1;
|
$pass2Counts[$type] = ($pass2Counts[$type] ?? 0) + 1;
|
||||||
$applied++;
|
$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)) {
|
} elseif (str_contains($finalRedacted, $original)) {
|
||||||
// Fallback for names adjacent to punctuation or non-word characters
|
// Fallback for names adjacent to punctuation or non-word characters
|
||||||
$finalRedacted = str_replace($original, $tag, $finalRedacted);
|
$finalRedacted = str_replace($original, $tag, $finalRedacted);
|
||||||
$pass2Counts[$type] = ($pass2Counts[$type] ?? 0) + 1;
|
$pass2Counts[$type] = ($pass2Counts[$type] ?? 0) + 1;
|
||||||
$applied++;
|
$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
|
$pass2Detail = $applied > 0
|
||||||
? "{$applied} additional: " . implode(', ', array_map(fn($k, $v) => "{$k}: {$v}", array_keys($pass2Counts), $pass2Counts))
|
? "{$applied} additional: " . implode(', ', array_map(fn($k, $v) => "{$k}: {$v}", array_keys($pass2Counts), $pass2Counts))
|
||||||
: 'no additional entities found';
|
: 'no additional entities found';
|
||||||
@@ -592,6 +612,7 @@ PROMPT;
|
|||||||
'redacted_text' => $finalRedacted,
|
'redacted_text' => $finalRedacted,
|
||||||
'detected_entity_categories' => $categories,
|
'detected_entity_categories' => $categories,
|
||||||
'entity_counts' => $allCounts,
|
'entity_counts' => $allCounts,
|
||||||
|
'redaction_map' => $redactionMap,
|
||||||
'evidence_trail' => [['title' => 'Pasted text', 'excerpt' => 'Processed in-memory only; not stored.']],
|
'evidence_trail' => [['title' => 'Pasted text', 'excerpt' => 'Processed in-memory only; not stored.']],
|
||||||
'what_remains_uncertain' => ['Human review is still recommended for contextual identification.'],
|
'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.',
|
'next_practical_step' => 'Review the output and rerun in strict mode if the text will be shared broadly.',
|
||||||
|
|||||||
Reference in New Issue
Block a user