redact: UX overhaul — engine simplification, credits, spinner, save-to-docs, badges

- Remove GPU/regex engine options; keep only azure_mini (1 credit) and azure_full (2 credits)
- Variable credit cost: engine-aware pre-check and charge in api/redact.php; PricingCatalog base = 1
- Fix ATTORNEY not preserved when keepOfficials=true: add to LLM prompt, generic-tag, pseudonym regexes
- Replace Azure credits hint with per-engine credit cost text (all 4 languages)
- Single-file upload only (was: up to 5); simplify status messages
- Clear previous redaction output and show pulsing spinner when a new run starts
- Add "Save to My Docs" button in redact output panel (corpus-save.js path)
- corpus-save.js: capture source_doc_ids from button dataset, pass in POST payload
- api/save-to-corpus.php: accept source_doc_ids, store first as source_url=corpus-doc:{id}
- doc-picker.js: show "✂ Redacted" badge for documents saved from the redact tool
- CSS: .redact-working spinner, doc-item__badge--redact pill styles

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-25 08:18:51 +02:00
parent a821d39dcd
commit 56cd87dd7b
10 changed files with 117 additions and 50 deletions
+18 -24
View File
@@ -8,9 +8,7 @@ const REDACT_I18N = {
redactEngine: 'Engine',
redactEngineAzureMini: 'Azure gpt-4o-mini',
redactEngineAzureFull: 'Azure gpt-4o',
redactEngineGpu: 'GPU (cuttlefish)',
redactEngineRegex: 'Regex only',
redactEngineHint: 'Azure engines use your BNL Azure credits. GPU runs the local LiteLLM proxy. Regex-only is instant and free but finds no names or organisations.',
redactEngineHint: 'gpt-4o-mini: 1 credit — fast, handles most documents well. gpt-4o: 2 credits — higher accuracy for complex or multi-person cases.',
redactMode: 'Mode',
redactModeStandard: 'Standard',
redactModeStrict: 'Strict',
@@ -42,7 +40,7 @@ const REDACT_I18N = {
redactAliasAdd: 'Add',
redactAliasHint: 'Replace a specific name with a custom bracketed label, e.g. "David Jr" → [Junior].',
redactUploadAria: 'File upload',
redactUploadDrop: 'Drop up to 5 files here, or',
redactUploadDrop: 'Drop one file here, or',
redactUploadBrowse: 'browse',
redactUploadHint: 'text extracted in memory, never stored',
redactUploadClear: '× Clear',
@@ -63,9 +61,7 @@ const REDACT_I18N = {
redactEngine: 'Motor',
redactEngineAzureMini: 'Azure gpt-4o-mini',
redactEngineAzureFull: 'Azure gpt-4o',
redactEngineGpu: 'GPU (cuttlefish)',
redactEngineRegex: 'Kun regex',
redactEngineHint: 'Azure-motorer bruker BNL Azure-kreditter. GPU kjører lokal LiteLLM-proxy. Kun regex er øyeblikkelig og gratis, men finner ingen navn eller organisasjoner.',
redactEngineHint: 'gpt-4o-mini: 1 kreditt — rask, håndterer de fleste dokumenter godt. gpt-4o: 2 kreditter — høyere nøyaktighet for komplekse eller flerpersonssaker.',
redactMode: 'Modus',
redactModeStandard: 'Standard',
redactModeStrict: 'Strikt',
@@ -97,7 +93,7 @@ const REDACT_I18N = {
redactAliasAdd: 'Legg til',
redactAliasHint: 'Erstatt et spesifikt navn med en egendefinert merkelapp, f.eks. «David Jr» → [Junior].',
redactUploadAria: 'Filopplasting',
redactUploadDrop: 'Slipp opptil 5 filer her, eller',
redactUploadDrop: 'Slipp én fil her, eller',
redactUploadBrowse: 'bla',
redactUploadHint: 'tekst hentes i minnet, lagres aldri',
redactUploadClear: '× Tøm',
@@ -118,9 +114,7 @@ const REDACT_I18N = {
redactEngine: 'Рушій',
redactEngineAzureMini: 'Azure gpt-4o-mini',
redactEngineAzureFull: 'Azure gpt-4o',
redactEngineGpu: 'GPU (cuttlefish)',
redactEngineRegex: 'Лише регулярні вирази',
redactEngineHint: 'Рушії Azure використовують кредити BNL Azure. GPU запускає локальний проксі LiteLLM. Лише regex — миттєво і безкоштовно, але не знаходить імен або організацій.',
redactEngineHint: 'gpt-4o-mini: 1 кредит — швидко, добре обробляє більшість документів. gpt-4o: 2 кредити — вища точність для складних або багатоособових справ.',
redactMode: 'Режим',
redactModeStandard: 'Стандартний',
redactModeStrict: 'Суворий',
@@ -152,7 +146,7 @@ const REDACT_I18N = {
redactAliasAdd: 'Додати',
redactAliasHint: 'Замініть конкретне ім\'я на власну мітку, напр. «David Jr» → [Junior].',
redactUploadAria: 'Завантаження файлів',
redactUploadDrop: 'Перетягніть до 5 файлів сюди, або',
redactUploadDrop: 'Перетягніть один файл сюди, або',
redactUploadBrowse: 'огляд',
redactUploadHint: 'текст обробляється в пам\'яті, ніколи не зберігається',
redactUploadClear: '× Очистити',
@@ -173,9 +167,7 @@ const REDACT_I18N = {
redactEngine: 'Silnik',
redactEngineAzureMini: 'Azure gpt-4o-mini',
redactEngineAzureFull: 'Azure gpt-4o',
redactEngineGpu: 'GPU (cuttlefish)',
redactEngineRegex: 'Tylko regex',
redactEngineHint: 'Silniki Azure używają kredytów Azure BNL. GPU korzysta z lokalnego proxy LiteLLM. Tylko regex jest natychmiastowy i bezpłatny, ale nie znajdzie imion ani organizacji.',
redactEngineHint: 'gpt-4o-mini: 1 kredyt — szybko, dobrze radzi sobie z większością dokumentów. gpt-4o: 2 kredyty — wyższa dokładność dla złożonych lub wieloosobowych spraw.',
redactMode: 'Tryb',
redactModeStandard: 'Standardowy',
redactModeStrict: 'Ścisły',
@@ -207,7 +199,7 @@ const REDACT_I18N = {
redactAliasAdd: 'Dodaj',
redactAliasHint: 'Zastąp konkretną nazwę własną etykietą, np. «David Jr» → [Junior].',
redactUploadAria: 'Przesyłanie pliku',
redactUploadDrop: 'Upuść do 5 plików tutaj lub',
redactUploadDrop: 'Upuść jeden plik tutaj lub',
redactUploadBrowse: 'przeglądaj',
redactUploadHint: 'tekst wyodrębniany w pamięci, nigdy nie przechowywany',
redactUploadClear: '× Wyczyść',
@@ -1126,6 +1118,9 @@ async function runTool(event) {
lastToolPayload = { ...payload };
setBusy(true);
if (state.activeTool === 'redact') {
els.results.innerHTML = '<div class="redact-working" role="status" aria-live="polite"><span class="redact-working__spinner" aria-hidden="true"></span><p>Redacting document…</p></div>';
}
renderTrace([
{ label: 'Query interpretation', detail: 'Preparing request.', status: 'running' },
]);
@@ -1218,7 +1213,7 @@ function setupUpload() {
async function handleFiles(fileList) {
const allowed = ['pdf', 'docx', 'txt'];
const files = Array.from(fileList).slice(0, 5);
const files = Array.from(fileList).slice(0, 1);
for (const file of files) {
const ext = file.name.split('.').pop().toLowerCase();
@@ -1228,7 +1223,7 @@ async function handleFiles(fileList) {
}
}
els.status.textContent = files.length === 1 ? `Extracting ${files[0].name}` : `Extracting ${files.length} files…`;
els.status.textContent = `Extracting ${files[0].name}`;
setBusy(true);
const parts = [];
@@ -1256,9 +1251,7 @@ async function handleFiles(fileList) {
if (data.truncated) anyTruncated = true;
}
const combined = parts.length === 1
? parts[0].text
: parts.map((p) => `--- Document: ${p.filename} ---\n\n${p.text}`).join('\n\n');
const combined = parts[0].text;
const MAX_COMBINED = 128000;
const combinedTruncated = combined.length > MAX_COMBINED;
@@ -1271,9 +1264,7 @@ async function handleFiles(fileList) {
els.uploadFileInfo.classList.remove('is-hidden');
const truncNote = (anyTruncated || combinedTruncated) ? ' — truncated to 128000 char limit' : '';
els.status.textContent = parts.length === 1
? `Extracted ${totalChars.toLocaleString()} chars from ${parts[0].filename}${truncNote}.`
: `Extracted ${totalChars.toLocaleString()} chars total from ${parts.length} files${truncNote}.`;
els.status.textContent = `Extracted ${totalChars.toLocaleString()} chars from ${parts[0].filename}${truncNote}.`;
} catch (err) {
els.status.textContent = err.message;
resetUpload();
@@ -1560,10 +1551,13 @@ function renderMainFinding(data) {
? `<button type="button" class="upgrade-engine-btn" id="rerunBetterBtn">Re-run with gpt-4o for higher accuracy →</button>`
: '';
const sourceDocIds = lastToolPayload?.doc_ids?.join(',') || '';
const suggestedTitle = `Redacted document — ${new Date().toLocaleDateString()}`;
const dlRow = `<div class="redact-downloads">
<button type="button" class="redact-dl-btn" id="rdlCopy">${t('redactCopy')}</button>
<button type="button" class="redact-dl-btn" id="rdlTxt">${t('redactDownloadTxt')}</button>
<button type="button" class="redact-dl-btn" id="rdlDocx">${t('redactDownloadDocx')}</button>
<button type="button" class="redact-dl-btn js-save-corpus" data-content-id="redactOutputPre" data-tool="redact" data-suggested-title="${escapeHtml(suggestedTitle)}" data-source-doc-ids="${escapeHtml(sourceDocIds)}">Save to My Docs</button>
</div>`;
return `${viewToggle}<pre class="redacted-output" id="redactOutputPre">${highlightRedactedText(lastRedactedText)}</pre>${inventoryHtml}${upgradeBtn}${dlRow}`;