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:
+10
-3
@@ -5,8 +5,15 @@ require_once __DIR__ . '/../includes/LegalTools.php';
|
|||||||
|
|
||||||
dbnToolsRequireMethod('POST');
|
dbnToolsRequireMethod('POST');
|
||||||
dbnToolsRequireAuth();
|
dbnToolsRequireAuth();
|
||||||
$ftUid = dbnToolsFreeTierCheck('redact');
|
|
||||||
|
// Determine engine and its credit cost before the pre-flight credit check
|
||||||
$input = dbnToolsJsonInput(400000);
|
$input = dbnToolsJsonInput(400000);
|
||||||
|
$_validEngines = ['azure_mini', 'azure_full'];
|
||||||
|
$_engine = in_array((string)($input['engine'] ?? ''), $_validEngines, true)
|
||||||
|
? (string)$input['engine'] : 'azure_mini';
|
||||||
|
$_engineCredits = $_engine === 'azure_full' ? 2 : 1;
|
||||||
|
|
||||||
|
$ftUid = dbnToolsFreeTierCheckAmount('redact', $_engineCredits);
|
||||||
|
|
||||||
dbnToolsWithChargedTelemetry('redact', '', $ftUid, function () use ($input): array {
|
dbnToolsWithChargedTelemetry('redact', '', $ftUid, function () use ($input): array {
|
||||||
$text = dbnToolsInjectDocContent($input, dbnToolsString($input, 'text', 128000, false));
|
$text = dbnToolsInjectDocContent($input, dbnToolsString($input, 'text', 128000, false));
|
||||||
@@ -17,7 +24,7 @@ dbnToolsWithChargedTelemetry('redact', '', $ftUid, function () use ($input): arr
|
|||||||
$region = dbnToolsNormalizeRegion($input['region'] ?? 'nordic');
|
$region = dbnToolsNormalizeRegion($input['region'] ?? 'nordic');
|
||||||
$language = dbnToolsNormalizeLanguage($input['language'] ?? 'en');
|
$language = dbnToolsNormalizeLanguage($input['language'] ?? 'en');
|
||||||
|
|
||||||
$validEngines = ['azure_mini', 'azure_full', 'gpu', 'regex'];
|
$validEngines = ['azure_mini', 'azure_full'];
|
||||||
$engine = in_array((string)($input['engine'] ?? ''), $validEngines, true)
|
$engine = in_array((string)($input['engine'] ?? ''), $validEngines, true)
|
||||||
? (string)$input['engine']
|
? (string)$input['engine']
|
||||||
: 'azure_mini';
|
: 'azure_mini';
|
||||||
@@ -67,4 +74,4 @@ dbnToolsWithChargedTelemetry('redact', '', $ftUid, function () use ($input): arr
|
|||||||
$text, $mode, $region, $language, $aliases,
|
$text, $mode, $region, $language, $aliases,
|
||||||
$engine, $outputFormat, $keepOfficials, $exemptNames, $redactTypes
|
$engine, $outputFormat, $keepOfficials, $exemptNames, $redactTypes
|
||||||
);
|
);
|
||||||
});
|
}, $_engineCredits);
|
||||||
|
|||||||
+10
-3
@@ -35,6 +35,13 @@ $tags = json_encode(
|
|||||||
JSON_UNESCAPED_UNICODE
|
JSON_UNESCAPED_UNICODE
|
||||||
);
|
);
|
||||||
|
|
||||||
|
$rawSourceDocIds = $input['source_doc_ids'] ?? null;
|
||||||
|
$sourceDocIdArr = is_array($rawSourceDocIds)
|
||||||
|
? $rawSourceDocIds
|
||||||
|
: (is_string($rawSourceDocIds) ? array_filter(array_map('trim', explode(',', $rawSourceDocIds))) : []);
|
||||||
|
$firstSourceDocId = (int)(reset($sourceDocIdArr) ?: 0);
|
||||||
|
$sourceUrl = $firstSourceDocId > 0 ? "corpus-doc:{$firstSourceDocId}" : null;
|
||||||
|
|
||||||
if ($title === '') {
|
if ($title === '') {
|
||||||
dbnToolsError('title is required.', 400, 'bad_request');
|
dbnToolsError('title is required.', 400, 'bad_request');
|
||||||
}
|
}
|
||||||
@@ -71,10 +78,10 @@ $wordCount = str_word_count($content);
|
|||||||
$ins = $db->prepare("
|
$ins = $db->prepare("
|
||||||
INSERT INTO client_documents
|
INSERT INTO client_documents
|
||||||
(client_id, corpus_id, title, source_type, content, category,
|
(client_id, corpus_id, title, source_type, content, category,
|
||||||
tags, import_method, source_tool, word_count, status)
|
tags, import_method, source_tool, source_url, word_count, status)
|
||||||
VALUES (?, ?, ?, 'text', ?, 'tool-output', ?, 'tool_output', ?, ?, 'pending')
|
VALUES (?, ?, ?, 'text', ?, 'tool-output', ?, 'tool_output', ?, ?, ?, 'pending')
|
||||||
");
|
");
|
||||||
$ins->execute([$clientId, $corpusId, $title, $content, $tags, $sourceTool, $wordCount]);
|
$ins->execute([$clientId, $corpusId, $title, $content, $tags, $sourceTool, $sourceUrl, $wordCount]);
|
||||||
$docId = (int)$db->lastInsertId();
|
$docId = (int)$db->lastInsertId();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -270,3 +270,23 @@
|
|||||||
transition: border-color 0.15s, color 0.15s;
|
transition: border-color 0.15s, color 0.15s;
|
||||||
}
|
}
|
||||||
.audio-corpus-upload:hover { border-color: var(--dbn-accent, #00205B); color: var(--dbn-accent, #00205B); }
|
.audio-corpus-upload:hover { border-color: var(--dbn-accent, #00205B); color: var(--dbn-accent, #00205B); }
|
||||||
|
|
||||||
|
|
||||||
|
/* ── Redacted document badge ─────────────────────────────────────────────── */
|
||||||
|
|
||||||
|
.doc-item__badge {
|
||||||
|
display: inline-block;
|
||||||
|
margin-left: 0.45em;
|
||||||
|
padding: 0.1em 0.45em;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 0.72rem;
|
||||||
|
font-weight: 600;
|
||||||
|
vertical-align: middle;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.doc-item__badge--redact {
|
||||||
|
background: #ede8f7;
|
||||||
|
color: #5b35a8;
|
||||||
|
border: 1px solid rgba(91, 53, 168, 0.2);
|
||||||
|
}
|
||||||
|
|||||||
@@ -2266,6 +2266,38 @@ p {
|
|||||||
cursor: progress;
|
cursor: progress;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@keyframes redact-spin {
|
||||||
|
to { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.redact-working {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 32px 16px;
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #fbfcfe;
|
||||||
|
}
|
||||||
|
|
||||||
|
.redact-working p {
|
||||||
|
margin: 0;
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 0.92rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.redact-working__spinner {
|
||||||
|
display: block;
|
||||||
|
width: 22px;
|
||||||
|
height: 22px;
|
||||||
|
border: 3px solid var(--line);
|
||||||
|
border-top-color: var(--teal);
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: redact-spin 0.9s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
/* ── Feedback widget ──────────────────────────────────────────── */
|
/* ── Feedback widget ──────────────────────────────────────────── */
|
||||||
.feedback-widget {
|
.feedback-widget {
|
||||||
margin-top: 1.5rem;
|
margin-top: 1.5rem;
|
||||||
|
|||||||
+16
-10
@@ -41,6 +41,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function bodyFor(kind, payload) {
|
function bodyFor(kind, payload) {
|
||||||
|
const sourceDocIds = (payload.sourceDocIds || '').split(',').map(s => s.trim()).filter(Boolean);
|
||||||
if (window.DBN_DASHBOARD) {
|
if (window.DBN_DASHBOARD) {
|
||||||
return JSON.stringify({
|
return JSON.stringify({
|
||||||
title: payload.title,
|
title: payload.title,
|
||||||
@@ -48,6 +49,7 @@
|
|||||||
source_tool: payload.tool || 'dashboard-save',
|
source_tool: payload.tool || 'dashboard-save',
|
||||||
tags: payload.tags,
|
tags: payload.tags,
|
||||||
kind,
|
kind,
|
||||||
|
...(sourceDocIds.length ? { source_doc_ids: sourceDocIds } : {}),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return JSON.stringify({
|
return JSON.stringify({
|
||||||
@@ -55,6 +57,7 @@
|
|||||||
content: payload.content,
|
content: payload.content,
|
||||||
source_tool: payload.tool || '',
|
source_tool: payload.tool || '',
|
||||||
tags: payload.tags,
|
tags: payload.tags,
|
||||||
|
...(sourceDocIds.length ? { source_doc_ids: sourceDocIds } : {}),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,9 +77,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
_pendingBtn = btn;
|
_pendingBtn = btn;
|
||||||
dlg.dataset.pendingContent = content;
|
dlg.dataset.pendingContent = content;
|
||||||
dlg.dataset.pendingTool = btn.dataset.tool || '';
|
dlg.dataset.pendingTool = btn.dataset.tool || '';
|
||||||
dlg.dataset.pendingKind = 'tool_output';
|
dlg.dataset.pendingKind = 'tool_output';
|
||||||
|
dlg.dataset.pendingSourceDocIds = btn.dataset.sourceDocIds || '';
|
||||||
|
|
||||||
titleIn.value = btn.dataset.suggestedTitle || '';
|
titleIn.value = btn.dataset.suggestedTitle || '';
|
||||||
tagsIn.value = '';
|
tagsIn.value = '';
|
||||||
@@ -90,12 +94,13 @@
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
dlg.close();
|
dlg.close();
|
||||||
|
|
||||||
const btn = _pendingBtn;
|
const btn = _pendingBtn;
|
||||||
const content = dlg.dataset.pendingContent || '';
|
const content = dlg.dataset.pendingContent || '';
|
||||||
const tool = dlg.dataset.pendingTool || '';
|
const tool = dlg.dataset.pendingTool || '';
|
||||||
const kind = dlg.dataset.pendingKind || 'tool_output';
|
const kind = dlg.dataset.pendingKind || 'tool_output';
|
||||||
const title = titleIn.value.trim();
|
const sourceDocIds = dlg.dataset.pendingSourceDocIds || '';
|
||||||
const tags = tagsIn.value.trim();
|
const title = titleIn.value.trim();
|
||||||
|
const tags = tagsIn.value.trim();
|
||||||
|
|
||||||
if (!title || !content) return;
|
if (!title || !content) return;
|
||||||
|
|
||||||
@@ -109,7 +114,7 @@
|
|||||||
method: 'POST',
|
method: 'POST',
|
||||||
credentials: 'same-origin',
|
credentials: 'same-origin',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: bodyFor(kind, { title, content, tool, tags }),
|
body: bodyFor(kind, { title, content, tool, tags, sourceDocIds }),
|
||||||
});
|
});
|
||||||
const data = await resp.json().catch(() => ({}));
|
const data = await resp.json().catch(() => ({}));
|
||||||
|
|
||||||
@@ -146,6 +151,7 @@
|
|||||||
delete dlg.dataset.pendingContent;
|
delete dlg.dataset.pendingContent;
|
||||||
delete dlg.dataset.pendingTool;
|
delete dlg.dataset.pendingTool;
|
||||||
delete dlg.dataset.pendingKind;
|
delete dlg.dataset.pendingKind;
|
||||||
|
delete dlg.dataset.pendingSourceDocIds;
|
||||||
});
|
});
|
||||||
|
|
||||||
function showToast(msg, isError) {
|
function showToast(msg, isError) {
|
||||||
|
|||||||
@@ -97,10 +97,13 @@
|
|||||||
.toLocaleDateString(undefined, { dateStyle: 'medium' }));
|
.toLocaleDateString(undefined, { dateStyle: 'medium' }));
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
}
|
}
|
||||||
|
var redactBadge = doc.source_tool === 'redact'
|
||||||
|
? '<span class="doc-item__badge doc-item__badge--redact">✂ Redacted</span>'
|
||||||
|
: '';
|
||||||
return '<div class="doc-item' + (sel ? ' is-selected' : '') + '" data-id="' + id + '" role="option" aria-selected="' + sel + '">'
|
return '<div class="doc-item' + (sel ? ' is-selected' : '') + '" data-id="' + id + '" role="option" aria-selected="' + sel + '">'
|
||||||
+ '<input type="checkbox" ' + (sel ? 'checked' : '') + ' tabindex="-1" aria-hidden="true">'
|
+ '<input type="checkbox" ' + (sel ? 'checked' : '') + ' tabindex="-1" aria-hidden="true">'
|
||||||
+ '<div>'
|
+ '<div>'
|
||||||
+ '<div class="doc-item__title">' + esc(doc.title || 'Untitled') + '</div>'
|
+ '<div class="doc-item__title">' + esc(doc.title || 'Untitled') + redactBadge + '</div>'
|
||||||
+ (meta.length ? '<div class="doc-item__meta">' + esc(meta.join(' · ')) + '</div>' : '')
|
+ (meta.length ? '<div class="doc-item__meta">' + esc(meta.join(' · ')) + '</div>' : '')
|
||||||
+ '</div>'
|
+ '</div>'
|
||||||
+ '</div>';
|
+ '</div>';
|
||||||
|
|||||||
+18
-24
@@ -8,9 +8,7 @@ const REDACT_I18N = {
|
|||||||
redactEngine: 'Engine',
|
redactEngine: 'Engine',
|
||||||
redactEngineAzureMini: 'Azure gpt-4o-mini',
|
redactEngineAzureMini: 'Azure gpt-4o-mini',
|
||||||
redactEngineAzureFull: 'Azure gpt-4o',
|
redactEngineAzureFull: 'Azure gpt-4o',
|
||||||
redactEngineGpu: 'GPU (cuttlefish)',
|
redactEngineHint: 'gpt-4o-mini: 1 credit — fast, handles most documents well. gpt-4o: 2 credits — higher accuracy for complex or multi-person cases.',
|
||||||
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.',
|
|
||||||
redactMode: 'Mode',
|
redactMode: 'Mode',
|
||||||
redactModeStandard: 'Standard',
|
redactModeStandard: 'Standard',
|
||||||
redactModeStrict: 'Strict',
|
redactModeStrict: 'Strict',
|
||||||
@@ -42,7 +40,7 @@ const REDACT_I18N = {
|
|||||||
redactAliasAdd: 'Add',
|
redactAliasAdd: 'Add',
|
||||||
redactAliasHint: 'Replace a specific name with a custom bracketed label, e.g. "David Jr" → [Junior].',
|
redactAliasHint: 'Replace a specific name with a custom bracketed label, e.g. "David Jr" → [Junior].',
|
||||||
redactUploadAria: 'File upload',
|
redactUploadAria: 'File upload',
|
||||||
redactUploadDrop: 'Drop up to 5 files here, or',
|
redactUploadDrop: 'Drop one file here, or',
|
||||||
redactUploadBrowse: 'browse',
|
redactUploadBrowse: 'browse',
|
||||||
redactUploadHint: 'text extracted in memory, never stored',
|
redactUploadHint: 'text extracted in memory, never stored',
|
||||||
redactUploadClear: '× Clear',
|
redactUploadClear: '× Clear',
|
||||||
@@ -63,9 +61,7 @@ const REDACT_I18N = {
|
|||||||
redactEngine: 'Motor',
|
redactEngine: 'Motor',
|
||||||
redactEngineAzureMini: 'Azure gpt-4o-mini',
|
redactEngineAzureMini: 'Azure gpt-4o-mini',
|
||||||
redactEngineAzureFull: 'Azure gpt-4o',
|
redactEngineAzureFull: 'Azure gpt-4o',
|
||||||
redactEngineGpu: 'GPU (cuttlefish)',
|
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.',
|
||||||
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.',
|
|
||||||
redactMode: 'Modus',
|
redactMode: 'Modus',
|
||||||
redactModeStandard: 'Standard',
|
redactModeStandard: 'Standard',
|
||||||
redactModeStrict: 'Strikt',
|
redactModeStrict: 'Strikt',
|
||||||
@@ -97,7 +93,7 @@ const REDACT_I18N = {
|
|||||||
redactAliasAdd: 'Legg til',
|
redactAliasAdd: 'Legg til',
|
||||||
redactAliasHint: 'Erstatt et spesifikt navn med en egendefinert merkelapp, f.eks. «David Jr» → [Junior].',
|
redactAliasHint: 'Erstatt et spesifikt navn med en egendefinert merkelapp, f.eks. «David Jr» → [Junior].',
|
||||||
redactUploadAria: 'Filopplasting',
|
redactUploadAria: 'Filopplasting',
|
||||||
redactUploadDrop: 'Slipp opptil 5 filer her, eller',
|
redactUploadDrop: 'Slipp én fil her, eller',
|
||||||
redactUploadBrowse: 'bla',
|
redactUploadBrowse: 'bla',
|
||||||
redactUploadHint: 'tekst hentes i minnet, lagres aldri',
|
redactUploadHint: 'tekst hentes i minnet, lagres aldri',
|
||||||
redactUploadClear: '× Tøm',
|
redactUploadClear: '× Tøm',
|
||||||
@@ -118,9 +114,7 @@ const REDACT_I18N = {
|
|||||||
redactEngine: 'Рушій',
|
redactEngine: 'Рушій',
|
||||||
redactEngineAzureMini: 'Azure gpt-4o-mini',
|
redactEngineAzureMini: 'Azure gpt-4o-mini',
|
||||||
redactEngineAzureFull: 'Azure gpt-4o',
|
redactEngineAzureFull: 'Azure gpt-4o',
|
||||||
redactEngineGpu: 'GPU (cuttlefish)',
|
redactEngineHint: 'gpt-4o-mini: 1 кредит — швидко, добре обробляє більшість документів. gpt-4o: 2 кредити — вища точність для складних або багатоособових справ.',
|
||||||
redactEngineRegex: 'Лише регулярні вирази',
|
|
||||||
redactEngineHint: 'Рушії Azure використовують кредити BNL Azure. GPU запускає локальний проксі LiteLLM. Лише regex — миттєво і безкоштовно, але не знаходить імен або організацій.',
|
|
||||||
redactMode: 'Режим',
|
redactMode: 'Режим',
|
||||||
redactModeStandard: 'Стандартний',
|
redactModeStandard: 'Стандартний',
|
||||||
redactModeStrict: 'Суворий',
|
redactModeStrict: 'Суворий',
|
||||||
@@ -152,7 +146,7 @@ const REDACT_I18N = {
|
|||||||
redactAliasAdd: 'Додати',
|
redactAliasAdd: 'Додати',
|
||||||
redactAliasHint: 'Замініть конкретне ім\'я на власну мітку, напр. «David Jr» → [Junior].',
|
redactAliasHint: 'Замініть конкретне ім\'я на власну мітку, напр. «David Jr» → [Junior].',
|
||||||
redactUploadAria: 'Завантаження файлів',
|
redactUploadAria: 'Завантаження файлів',
|
||||||
redactUploadDrop: 'Перетягніть до 5 файлів сюди, або',
|
redactUploadDrop: 'Перетягніть один файл сюди, або',
|
||||||
redactUploadBrowse: 'огляд',
|
redactUploadBrowse: 'огляд',
|
||||||
redactUploadHint: 'текст обробляється в пам\'яті, ніколи не зберігається',
|
redactUploadHint: 'текст обробляється в пам\'яті, ніколи не зберігається',
|
||||||
redactUploadClear: '× Очистити',
|
redactUploadClear: '× Очистити',
|
||||||
@@ -173,9 +167,7 @@ const REDACT_I18N = {
|
|||||||
redactEngine: 'Silnik',
|
redactEngine: 'Silnik',
|
||||||
redactEngineAzureMini: 'Azure gpt-4o-mini',
|
redactEngineAzureMini: 'Azure gpt-4o-mini',
|
||||||
redactEngineAzureFull: 'Azure gpt-4o',
|
redactEngineAzureFull: 'Azure gpt-4o',
|
||||||
redactEngineGpu: 'GPU (cuttlefish)',
|
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.',
|
||||||
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.',
|
|
||||||
redactMode: 'Tryb',
|
redactMode: 'Tryb',
|
||||||
redactModeStandard: 'Standardowy',
|
redactModeStandard: 'Standardowy',
|
||||||
redactModeStrict: 'Ścisły',
|
redactModeStrict: 'Ścisły',
|
||||||
@@ -207,7 +199,7 @@ const REDACT_I18N = {
|
|||||||
redactAliasAdd: 'Dodaj',
|
redactAliasAdd: 'Dodaj',
|
||||||
redactAliasHint: 'Zastąp konkretną nazwę własną etykietą, np. «David Jr» → [Junior].',
|
redactAliasHint: 'Zastąp konkretną nazwę własną etykietą, np. «David Jr» → [Junior].',
|
||||||
redactUploadAria: 'Przesyłanie pliku',
|
redactUploadAria: 'Przesyłanie pliku',
|
||||||
redactUploadDrop: 'Upuść do 5 plików tutaj lub',
|
redactUploadDrop: 'Upuść jeden plik tutaj lub',
|
||||||
redactUploadBrowse: 'przeglądaj',
|
redactUploadBrowse: 'przeglądaj',
|
||||||
redactUploadHint: 'tekst wyodrębniany w pamięci, nigdy nie przechowywany',
|
redactUploadHint: 'tekst wyodrębniany w pamięci, nigdy nie przechowywany',
|
||||||
redactUploadClear: '× Wyczyść',
|
redactUploadClear: '× Wyczyść',
|
||||||
@@ -1126,6 +1118,9 @@ async function runTool(event) {
|
|||||||
lastToolPayload = { ...payload };
|
lastToolPayload = { ...payload };
|
||||||
|
|
||||||
setBusy(true);
|
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([
|
renderTrace([
|
||||||
{ label: 'Query interpretation', detail: 'Preparing request.', status: 'running' },
|
{ label: 'Query interpretation', detail: 'Preparing request.', status: 'running' },
|
||||||
]);
|
]);
|
||||||
@@ -1218,7 +1213,7 @@ function setupUpload() {
|
|||||||
|
|
||||||
async function handleFiles(fileList) {
|
async function handleFiles(fileList) {
|
||||||
const allowed = ['pdf', 'docx', 'txt'];
|
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) {
|
for (const file of files) {
|
||||||
const ext = file.name.split('.').pop().toLowerCase();
|
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);
|
setBusy(true);
|
||||||
|
|
||||||
const parts = [];
|
const parts = [];
|
||||||
@@ -1256,9 +1251,7 @@ async function handleFiles(fileList) {
|
|||||||
if (data.truncated) anyTruncated = true;
|
if (data.truncated) anyTruncated = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
const combined = parts.length === 1
|
const combined = parts[0].text;
|
||||||
? parts[0].text
|
|
||||||
: parts.map((p) => `--- Document: ${p.filename} ---\n\n${p.text}`).join('\n\n');
|
|
||||||
|
|
||||||
const MAX_COMBINED = 128000;
|
const MAX_COMBINED = 128000;
|
||||||
const combinedTruncated = combined.length > MAX_COMBINED;
|
const combinedTruncated = combined.length > MAX_COMBINED;
|
||||||
@@ -1271,9 +1264,7 @@ async function handleFiles(fileList) {
|
|||||||
els.uploadFileInfo.classList.remove('is-hidden');
|
els.uploadFileInfo.classList.remove('is-hidden');
|
||||||
|
|
||||||
const truncNote = (anyTruncated || combinedTruncated) ? ' — truncated to 128 000 char limit' : '';
|
const truncNote = (anyTruncated || combinedTruncated) ? ' — truncated to 128 000 char limit' : '';
|
||||||
els.status.textContent = parts.length === 1
|
els.status.textContent = `Extracted ${totalChars.toLocaleString()} chars from ${parts[0].filename}${truncNote}.`;
|
||||||
? `Extracted ${totalChars.toLocaleString()} chars from ${parts[0].filename}${truncNote}.`
|
|
||||||
: `Extracted ${totalChars.toLocaleString()} chars total from ${parts.length} files${truncNote}.`;
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
els.status.textContent = err.message;
|
els.status.textContent = err.message;
|
||||||
resetUpload();
|
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>`
|
? `<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">
|
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="rdlCopy">${t('redactCopy')}</button>
|
||||||
<button type="button" class="redact-dl-btn" id="rdlTxt">${t('redactDownloadTxt')}</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" 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>`;
|
</div>`;
|
||||||
|
|
||||||
return `${viewToggle}<pre class="redacted-output" id="redactOutputPre">${highlightRedactedText(lastRedactedText)}</pre>${inventoryHtml}${upgradeBtn}${dlRow}`;
|
return `${viewToggle}<pre class="redacted-output" id="redactOutputPre">${highlightRedactedText(lastRedactedText)}</pre>${inventoryHtml}${upgradeBtn}${dlRow}`;
|
||||||
|
|||||||
@@ -1120,7 +1120,7 @@ PROMPT;
|
|||||||
// Build officials note
|
// Build officials note
|
||||||
$officialsNote = '';
|
$officialsNote = '';
|
||||||
if ($keepOfficials) {
|
if ($keepOfficials) {
|
||||||
$officialsNote = "\n\nOFFICIALS — for persons identified as JUDGE, EXPERT_WITNESS, or CASEWORKER in an official capacity: do NOT replace their name with a plain bracket tag. Instead use the format [ROLE: Name], e.g. [JUDGE: Andersen] or [EXPERT_WITNESS: Dr. Larsen]. Their name must remain visible inside the tag.";
|
$officialsNote = "\n\nOFFICIALS — for persons identified as JUDGE, ATTORNEY, EXPERT_WITNESS, or CASEWORKER in an official capacity: do NOT replace their name with a plain bracket tag. Instead use the format [ROLE: Name], e.g. [JUDGE: Andersen], [ATTORNEY: Skretting] or [EXPERT_WITNESS: Dr. Larsen]. Their name must remain visible inside the tag.";
|
||||||
}
|
}
|
||||||
|
|
||||||
$allowedTypesNote = '';
|
$allowedTypesNote = '';
|
||||||
@@ -1363,7 +1363,7 @@ PROMPT;
|
|||||||
private function applyGenericTags(string $text): string
|
private function applyGenericTags(string $text): string
|
||||||
{
|
{
|
||||||
// Collapse contextual role tags (e.g. [FATHER], [JUDGE: Andersen], [CHILD_1]) → [PERSON]
|
// Collapse contextual role tags (e.g. [FATHER], [JUDGE: Andersen], [CHILD_1]) → [PERSON]
|
||||||
$text = preg_replace('/\[(?:FATHER|MOTHER|CHILD(?:_\d+)?|GRANDPARENT|SIBLING|ATTORNEY|JUDGE(?::\s*[^\]]+)?|CASEWORKER(?::\s*[^\]]+)?|EXPERT_WITNESS(?::\s*[^\]]+)?|PERSON(?:_\d+)?)\]/u', '[PERSON]', $text) ?? $text;
|
$text = preg_replace('/\[(?:FATHER|MOTHER|CHILD(?:_\d+)?|GRANDPARENT|SIBLING|ATTORNEY(?::\s*[^\]]+)?|JUDGE(?::\s*[^\]]+)?|CASEWORKER(?::\s*[^\]]+)?|EXPERT_WITNESS(?::\s*[^\]]+)?|PERSON(?:_\d+)?)\]/u', '[PERSON]', $text) ?? $text;
|
||||||
return $text;
|
return $text;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1382,7 +1382,7 @@ PROMPT;
|
|||||||
|
|
||||||
// Replace named role tags (keeping consistent mapping per unique tag)
|
// Replace named role tags (keeping consistent mapping per unique tag)
|
||||||
$text = preg_replace_callback(
|
$text = preg_replace_callback(
|
||||||
'/\[(FATHER|MOTHER|CHILD(?:_\d+)?|GRANDPARENT|SIBLING|ATTORNEY|JUDGE(?::\s*[^\]]+)?|CASEWORKER(?::\s*[^\]]+)?|EXPERT_WITNESS(?::\s*[^\]]+)?|PERSON(?:_\d+)?)\]/u',
|
'/\[(FATHER|MOTHER|CHILD(?:_\d+)?|GRANDPARENT|SIBLING|ATTORNEY(?::\s*[^\]]+)?|JUDGE(?::\s*[^\]]+)?|CASEWORKER(?::\s*[^\]]+)?|EXPERT_WITNESS(?::\s*[^\]]+)?|PERSON(?:_\d+)?)\]/u',
|
||||||
function (array $m) use (&$nameCursor, &$personMap, $norwegianNames): string {
|
function (array $m) use (&$nameCursor, &$personMap, $norwegianNames): string {
|
||||||
$key = $m[1];
|
$key = $m[1];
|
||||||
if (!isset($personMap[$key])) {
|
if (!isset($personMap[$key])) {
|
||||||
|
|||||||
@@ -133,7 +133,7 @@ final class PricingCatalog
|
|||||||
'translate' => 1,
|
'translate' => 1,
|
||||||
'korrespond_refine' => 1,
|
'korrespond_refine' => 1,
|
||||||
'timeline' => 2,
|
'timeline' => 2,
|
||||||
'redact' => 2,
|
'redact' => 1, // minimum (gpt-4o-mini); azure_full overrides to 2 in api/redact.php
|
||||||
'barnevernet' => 3,
|
'barnevernet' => 3,
|
||||||
'advocate' => 3,
|
'advocate' => 3,
|
||||||
'korrespond' => 3,
|
'korrespond' => 3,
|
||||||
|
|||||||
+3
-5
@@ -19,10 +19,8 @@ require_once __DIR__ . '/includes/layout.php';
|
|||||||
<span class="control-label" data-i18n="redactEngine">Engine</span>
|
<span class="control-label" data-i18n="redactEngine">Engine</span>
|
||||||
<label><input type="radio" name="redactEngine" value="azure_mini" checked id="redactEngineAzureMini"> <span data-i18n="redactEngineAzureMini">Azure gpt-4o-mini</span> ★ <small class="control-hint">(fast)</small></label>
|
<label><input type="radio" name="redactEngine" value="azure_mini" checked id="redactEngineAzureMini"> <span data-i18n="redactEngineAzureMini">Azure gpt-4o-mini</span> ★ <small class="control-hint">(fast)</small></label>
|
||||||
<label><input type="radio" name="redactEngine" value="azure_full" id="redactEngineAzureFull"> <span data-i18n="redactEngineAzureFull">Azure gpt-4o</span> <small class="control-hint">(best)</small></label>
|
<label><input type="radio" name="redactEngine" value="azure_full" id="redactEngineAzureFull"> <span data-i18n="redactEngineAzureFull">Azure gpt-4o</span> <small class="control-hint">(best)</small></label>
|
||||||
<label><input type="radio" name="redactEngine" value="gpu" id="redactEngineGpu"> <span data-i18n="redactEngineGpu">GPU (cuttlefish)</span> <small class="control-hint">(local)</small></label>
|
|
||||||
<label><input type="radio" name="redactEngine" value="regex" id="redactEngineRegex"> <span data-i18n="redactEngineRegex">Regex only</span> <small class="control-hint">(free)</small></label>
|
|
||||||
</div>
|
</div>
|
||||||
<p class="upload-hint" data-i18n="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.</p>
|
<p class="upload-hint" data-i18n="redactEngineHint">gpt-4o-mini: 1 credit — fast, handles most documents well. gpt-4o: 2 credits — higher accuracy for complex or multi-person cases.</p>
|
||||||
|
|
||||||
<details class="advanced-panel" id="redactAdvanced">
|
<details class="advanced-panel" id="redactAdvanced">
|
||||||
<summary class="advanced-toggle" data-i18n="redactAdvancedToggle">Advanced settings</summary>
|
<summary class="advanced-toggle" data-i18n="redactAdvancedToggle">Advanced settings</summary>
|
||||||
@@ -95,10 +93,10 @@ require_once __DIR__ . '/includes/layout.php';
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="upload-zone" id="uploadZone" role="region" aria-label="File upload" data-i18n-aria="redactUploadAria">
|
<div class="upload-zone" id="uploadZone" role="region" aria-label="File upload" data-i18n-aria="redactUploadAria">
|
||||||
<input type="file" id="uploadInput" multiple accept=".pdf,.docx,.txt" aria-label="Choose files">
|
<input type="file" id="uploadInput" accept=".pdf,.docx,.txt" aria-label="Choose file">
|
||||||
<div id="uploadPrompt" class="upload-prompt">
|
<div id="uploadPrompt" class="upload-prompt">
|
||||||
<span class="upload-icon" aria-hidden="true">⇧</span>
|
<span class="upload-icon" aria-hidden="true">⇧</span>
|
||||||
<p><span data-i18n="redactUploadDrop">Drop up to 5 files here, or</span> <label for="uploadInput" class="upload-browse" data-i18n="redactUploadBrowse">browse</label></p>
|
<p><span data-i18n="redactUploadDrop">Drop one file here, or</span> <label for="uploadInput" class="upload-browse" data-i18n="redactUploadBrowse">browse</label></p>
|
||||||
<p class="upload-hint"><strong>PDF</strong>, <strong>DOCX</strong>, <strong>TXT</strong> — <span data-i18n="redactUploadHint">text extracted in memory, never stored</span></p>
|
<p class="upload-hint"><strong>PDF</strong>, <strong>DOCX</strong>, <strong>TXT</strong> — <span data-i18n="redactUploadHint">text extracted in memory, never stored</span></p>
|
||||||
</div>
|
</div>
|
||||||
<div id="uploadFileInfo" class="upload-file is-hidden">
|
<div id="uploadFileInfo" class="upload-file is-hidden">
|
||||||
|
|||||||
Reference in New Issue
Block a user