feat(timeline): full form UI with engine selection and advanced settings

Add 4-language switcher (EN/NO/UK/PL), engine choice (Azure mini/full,
GPU/cuttlefish), and expandable Advanced panel (Focus, Confidence filter,
Date types) to timeline.php. Wire new params through api/timeline.php and
LegalTools::timeline() with engine routing, focus-aware prompt injection,
and confidence/date-type post-filters. Add TIMELINE_I18N to tools.js with
improved renderTimeline() confidence colour-coding and new CSS classes.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-15 00:59:12 +02:00
parent 30915bcb09
commit 7690ed17ee
5 changed files with 455 additions and 35 deletions
+45 -5
View File
@@ -362,25 +362,65 @@ p {
.timeline-list {
display: grid;
gap: 10px;
padding-left: 20px;
padding-left: 0;
list-style: none;
}
.timeline-list li {
padding-left: 4px;
.timeline-item {
padding: 10px 12px 10px 14px;
border-left: 4px solid var(--line);
border-radius: 0 6px 6px 0;
background: var(--bg-card, #fff);
}
.timeline-list span {
.timeline-item.confidence-high { border-left-color: #16a34a; }
.timeline-item.confidence-medium { border-left-color: #d97706; }
.timeline-item.confidence-low { border-left-color: #9ca3af; opacity: 0.75; font-style: italic; }
.timeline-header {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
margin-bottom: 4px;
}
.timeline-date {
font-size: 0.95rem;
}
.timeline-actor {
display: block;
color: var(--teal-dark);
font-weight: 700;
font-size: 0.85rem;
margin-bottom: 3px;
}
.timeline-list small {
.timeline-event {
margin: 4px 0;
}
.timeline-excerpt {
display: block;
margin-top: 5px;
color: var(--muted);
font-size: 0.82rem;
}
.confidence-badge {
font-size: 0.7rem;
font-weight: 600;
padding: 1px 6px;
border-radius: 999px;
text-transform: uppercase;
letter-spacing: 0.03em;
font-style: normal;
}
.confidence-badge--high { background: #dcfce7; color: #15803d; }
.confidence-badge--medium { background: #fef3c7; color: #b45309; }
.confidence-badge--low { background: #f3f4f6; color: #6b7280; }
.redacted-output {
max-height: 420px;
overflow: auto;
+208 -10
View File
@@ -222,6 +222,141 @@ const REDACT_I18N = {
},
};
const TIMELINE_I18N = {
en: {
timelineEngine: 'Engine',
timelineEngineAzureMini: 'Azure gpt-4o-mini',
timelineEngineAzureFull: 'Azure gpt-4o',
timelineEngineGpu: 'GPU (cuttlefish)',
timelineEngineHint: 'Azure engines use your BNL Azure credits. GPU runs the local LiteLLM proxy on cuttlefish.',
timelineAdvancedToggle: 'Advanced settings',
timelineFocus: 'Focus',
timelineFocusAll: 'All events',
timelineFocusDeadlines: 'Legal deadlines',
timelineFocusHearings: 'Court hearings',
timelineFocusCps: 'CPS milestones',
timelineFocusHint: 'All events: every temporal reference. Legal deadlines: filing dates, appeal windows, statutory limits. Court hearings: tribunal and mediation sessions. CPS milestones: Barnevernet interventions and Fylkesnemnda proceedings.',
timelineConfidence: 'Confidence',
timelineConfidenceAll: 'Show all events',
timelineConfidenceHighMed: 'Hide low-confidence',
timelineConfidenceHint: 'Show all: includes uncertain events (shown in grey). Hide low-confidence: only returns events the model is reasonably sure of.',
timelineDates: 'Date types',
timelineIncludeRelative: 'Include relative / recurring dates',
timelineDatesHint: 'When checked, relative references ("three weeks later", "every Monday") are included. Uncheck to return only exact calendar dates.',
timelineUploadAria: 'File upload',
timelineUploadDrop: 'Drop up to 5 files here, or',
timelineUploadBrowse: 'browse',
timelineUploadHint: 'text extracted in memory, never stored',
timelineUploadClear: '× Clear',
timelineInputLabel: 'Pasted text',
timelineInputPlaceholder: 'Paste case notes, court decisions, or correspondence containing dates and events.',
timelineRun: 'Run',
timelineRunning: 'Building timeline…',
timelineReadyTitle: 'Ready',
timelineReadyDesc: 'Paste text or upload a file, configure options, then run.',
timelineExportCsv: 'Download CSV',
},
no: {
timelineEngine: 'Motor',
timelineEngineAzureMini: 'Azure gpt-4o-mini',
timelineEngineAzureFull: 'Azure gpt-4o',
timelineEngineGpu: 'GPU (cuttlefish)',
timelineEngineHint: 'Azure-motorer bruker BNL Azure-kreditter. GPU kjører lokal LiteLLM-proxy på cuttlefish.',
timelineAdvancedToggle: 'Avanserte innstillinger',
timelineFocus: 'Fokus',
timelineFocusAll: 'Alle hendelser',
timelineFocusDeadlines: 'Juridiske frister',
timelineFocusHearings: 'Rettsmøter',
timelineFocusCps: 'Barnevernet-milepæler',
timelineFocusHint: 'Alle hendelser: alle temporale referanser. Juridiske frister: innleveringsfrister, ankefrister, lovpålagte frister. Rettsmøter: domstol- og meklingsmøter. Barnevernet: akuttvedtak og Fylkesnemnda-saker.',
timelineConfidence: 'Sikkerhet',
timelineConfidenceAll: 'Vis alle hendelser',
timelineConfidenceHighMed: 'Skjul usikre hendelser',
timelineConfidenceHint: 'Vis alle: inkluderer usikre hendelser (vist i grått). Skjul usikre: returnerer bare hendelser modellen er rimelig sikker på.',
timelineDates: 'Datotyper',
timelineIncludeRelative: 'Inkluder relative / gjentakende datoer',
timelineDatesHint: 'Når avkrysset inkluderes relative referanser ("tre uker senere", "hver mandag"). Fjern haken for å returnere kun eksakte kalenderdatoer.',
timelineUploadAria: 'Filopplasting',
timelineUploadDrop: 'Slipp opptil 5 filer her, eller',
timelineUploadBrowse: 'bla',
timelineUploadHint: 'tekst hentes i minnet, lagres aldri',
timelineUploadClear: '× Tøm',
timelineInputLabel: 'Limt inn tekst',
timelineInputPlaceholder: 'Lim inn saksdokumenter, rettsavgjørelser eller korrespondanse med datoer og hendelser.',
timelineRun: 'Kjør',
timelineRunning: 'Bygger tidslinje…',
timelineReadyTitle: 'Klar',
timelineReadyDesc: 'Lim inn tekst eller last opp en fil, konfigurer alternativene, og kjør.',
timelineExportCsv: 'Last ned CSV',
},
uk: {
timelineEngine: 'Рушій',
timelineEngineAzureMini: 'Azure gpt-4o-mini',
timelineEngineAzureFull: 'Azure gpt-4o',
timelineEngineGpu: 'GPU (cuttlefish)',
timelineEngineHint: 'Рушії Azure використовують кредити BNL Azure. GPU запускає локальний проксі LiteLLM на cuttlefish.',
timelineAdvancedToggle: 'Розширені налаштування',
timelineFocus: 'Фокус',
timelineFocusAll: 'Всі події',
timelineFocusDeadlines: 'Юридичні терміни',
timelineFocusHearings: 'Судові засідання',
timelineFocusCps: 'Вехи органів опіки',
timelineFocusHint: 'Всі події: кожне часове посилання. Юридичні терміни: дати подання, вікна апеляції. Судові засідання: трибунали та медіації. Опіка: втручання Barnevernet та провадження Fylkesnemnda.',
timelineConfidence: 'Достовірність',
timelineConfidenceAll: 'Показати всі події',
timelineConfidenceHighMed: 'Приховати малодостовірні',
timelineConfidenceHint: 'Показати всі: включає непевні події (сірим). Приховати малодостовірні: повертає лише впевнено визначені події.',
timelineDates: 'Типи дат',
timelineIncludeRelative: 'Включити відносні / повторювані дати',
timelineDatesHint: 'Якщо позначено, відносні посилання ("три тижні потому", "щопонеділка") включаються. Зніміть, щоб повертати лише точні календарні дати.',
timelineUploadAria: 'Завантаження файлів',
timelineUploadDrop: 'Перетягніть до 5 файлів сюди, або',
timelineUploadBrowse: 'огляд',
timelineUploadHint: 'текст обробляється в памʼяті, ніколи не зберігається',
timelineUploadClear: '× Очистити',
timelineInputLabel: 'Вставлений текст',
timelineInputPlaceholder: 'Вставте нотатки справи, судові рішення або кореспонденцію з датами та подіями.',
timelineRun: 'Запустити',
timelineRunning: 'Будую хронологію…',
timelineReadyTitle: 'Готово',
timelineReadyDesc: 'Вставте текст або завантажте файл, налаштуйте параметри, запустіть.',
timelineExportCsv: 'Завантажити CSV',
},
pl: {
timelineEngine: 'Silnik',
timelineEngineAzureMini: 'Azure gpt-4o-mini',
timelineEngineAzureFull: 'Azure gpt-4o',
timelineEngineGpu: 'GPU (cuttlefish)',
timelineEngineHint: 'Silniki Azure używają kredytów Azure BNL. GPU korzysta z lokalnego proxy LiteLLM na cuttlefish.',
timelineAdvancedToggle: 'Ustawienia zaawansowane',
timelineFocus: 'Fokus',
timelineFocusAll: 'Wszystkie zdarzenia',
timelineFocusDeadlines: 'Terminy prawne',
timelineFocusHearings: 'Rozprawy sądowe',
timelineFocusCps: 'Kamienie milowe OPS',
timelineFocusHint: 'Wszystkie: każde odniesienie czasowe. Terminy: daty złożenia, okna apelacyjne. Rozprawy: trybunały i mediacje. OPS: interwencje Barnevernet i postępowania Fylkesnemnda.',
timelineConfidence: 'Pewność',
timelineConfidenceAll: 'Pokaż wszystkie zdarzenia',
timelineConfidenceHighMed: 'Ukryj mało pewne',
timelineConfidenceHint: 'Pokaż wszystkie: zawiera niepewne zdarzenia (szarym). Ukryj mało pewne: zwraca tylko zdarzenia o rozsądnej pewności.',
timelineDates: 'Typy dat',
timelineIncludeRelative: 'Uwzględnij daty względne / cykliczne',
timelineDatesHint: 'Gdy zaznaczone, odniesienia względne ("trzy tygodnie później", "co poniedziałek") są uwzględniane. Odznacz, aby zwracać tylko dokładne daty kalendarzowe.',
timelineUploadAria: 'Przesyłanie pliku',
timelineUploadDrop: 'Upuść do 5 plików tutaj lub',
timelineUploadBrowse: 'przeglądaj',
timelineUploadHint: 'tekst wyodrębniany w pamięci, nigdy nie przechowywany',
timelineUploadClear: '× Wyczyść',
timelineInputLabel: 'Wklejony tekst',
timelineInputPlaceholder: 'Wklej notatki sprawy, decyzje sądowe lub korespondencję z datami i zdarzeniami.',
timelineRun: 'Uruchom',
timelineRunning: 'Tworzę oś czasu…',
timelineReadyTitle: 'Gotowe',
timelineReadyDesc: 'Wklej tekst lub wgraj plik, skonfiguruj opcje, uruchom.',
timelineExportCsv: 'Pobierz CSV',
},
};
let lastTimelineEvents = [];
let audioQueue = []; // [{file, status: 'pending'|'processing'|'done'|'error', result}]
let lastTranscriptData = null;
@@ -585,6 +720,56 @@ function setupRedactControls() {
applyRedactI18n(uiLang);
}
function currentTimelineT(key) {
const t = TIMELINE_I18N[uiLang] || TIMELINE_I18N.en;
return (key in t) ? t[key] : (TIMELINE_I18N.en[key] ?? key);
}
function applyTimelineI18n(lang) {
uiLang = lang;
localStorage.setItem('dbn-ui-lang', lang);
document.querySelectorAll('[data-i18n]').forEach((el) => {
const text = currentTimelineT(el.dataset.i18n);
if (text != null) el.textContent = text;
});
document.querySelectorAll('[data-i18n-placeholder]').forEach((el) => {
const text = currentTimelineT(el.dataset.i18nPlaceholder);
if (text != null) el.placeholder = text;
});
document.querySelectorAll('[data-i18n-aria]').forEach((el) => {
const text = currentTimelineT(el.dataset.i18nAria);
if (text != null) el.setAttribute('aria-label', text);
});
document.querySelectorAll('#timelineLangSwitcher .lang-btn').forEach((btn) => {
btn.classList.toggle('is-active', btn.dataset.lang === lang);
});
}
function currentTimelineEngine() {
return document.querySelector('input[name="timelineEngine"]:checked')?.value || 'azure_mini';
}
function currentTimelineFocus() {
return document.querySelector('input[name="timelineFocus"]:checked')?.value || 'all';
}
function currentConfidenceFilter() {
return document.querySelector('input[name="confidenceFilter"]:checked')?.value || 'all';
}
function currentIncludeRelative() {
return document.getElementById('includeRelativeCheck')?.checked ?? true;
}
function setupTimelineControls() {
const switcher = document.getElementById('timelineLangSwitcher');
if (!switcher) return;
switcher.querySelectorAll('.lang-btn').forEach((btn) => {
btn.addEventListener('click', () => applyTimelineI18n(btn.dataset.lang));
});
applyTimelineI18n(uiLang);
}
function setupExemptNames() {
const addBtn = document.getElementById('addExemptRow');
const rows = document.getElementById('exemptRows');
@@ -738,6 +923,7 @@ document.addEventListener('DOMContentLoaded', () => {
setupVocabPresets();
setupRedactControls();
setupExemptNames();
setupTimelineControls();
// Wire transcribe lang switcher (only present on transcribe page)
document.querySelectorAll('#uiLangSwitcher .lang-btn').forEach((btn) => {
btn.addEventListener('click', () => applyTranscribeI18n(btn.dataset.lang));
@@ -852,6 +1038,12 @@ async function runTool(event) {
payload.exempt_names = getExemptNames();
payload.redact_types = currentRedactTypes();
}
if (state.activeTool === 'timeline') {
payload.engine = currentTimelineEngine();
payload.focus = currentTimelineFocus();
payload.confidence_filter = currentConfidenceFilter();
payload.include_relative = currentIncludeRelative();
}
setBusy(true);
renderTrace([
@@ -1109,8 +1301,9 @@ function renderMainFinding(data) {
}
if (data.tool === 'timeline') {
lastTimelineEvents = data.events || [];
const csvLabel = currentTimelineT('timelineExportCsv') || 'Download CSV';
const csvBtn = lastTimelineEvents.length
? `<div class="timeline-export"><button type="button" id="exportCsvBtn" class="export-csv-btn">Download CSV</button></div>`
? `<div class="timeline-export"><button type="button" id="exportCsvBtn" class="export-csv-btn">${escapeHtml(csvLabel)}</button></div>`
: '';
return `<p>${escapeHtml(data.what_we_found || '')}</p>${renderTimeline(lastTimelineEvents)}${csvBtn}`;
}
@@ -1172,15 +1365,20 @@ function renderTimeline(events) {
if (!events.length) {
return '<p>No events were identified.</p>';
}
return `<ol class="timeline-list">${events.map((ev) => `
<li>
<strong>${escapeHtml(ev.date || 'unknown')}</strong>
${ev.date_type ? `<span class="date-type-badge">${escapeHtml(ev.date_type)}</span>` : ''}
<span>${escapeHtml(ev.actor || 'unknown actor')}</span>
<p>${escapeHtml(ev.event || '')}</p>
${ev.source_excerpt ? `<small>${escapeHtml(ev.source_excerpt)}</small>` : ''}
</li>
`).join('')}</ol>`;
return `<ol class="timeline-list">${events.map((ev) => {
const conf = ev.confidence || 'medium';
return `
<li class="timeline-item confidence-${escapeHtml(conf)}">
<div class="timeline-header">
<strong class="timeline-date">${escapeHtml(ev.date || 'unknown')}</strong>
${ev.date_type ? `<span class="date-type-badge">${escapeHtml(ev.date_type)}</span>` : ''}
<span class="confidence-badge confidence-badge--${escapeHtml(conf)}">${escapeHtml(conf)}</span>
</div>
<span class="timeline-actor">${escapeHtml(ev.actor || 'unknown actor')}</span>
<p class="timeline-event">${escapeHtml(ev.event || '')}</p>
${ev.source_excerpt ? `<small class="timeline-excerpt">${escapeHtml(ev.source_excerpt)}</small>` : ''}
</li>`;
}).join('')}</ol>`;
}
function exportTimelineCSV(events) {