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:
+15
-1
@@ -10,5 +10,19 @@ $language = dbnToolsNormalizeLanguage($input['language'] ?? 'en');
|
||||
|
||||
dbnToolsWithTelemetry('timeline', $language, function () use ($input, $language): array {
|
||||
$text = dbnToolsString($input, 'text', 128000);
|
||||
return (new DbnLegalToolsService())->timeline($text, $language);
|
||||
|
||||
$validEngines = ['azure_mini', 'azure_full', 'gpu'];
|
||||
$engine = in_array((string)($input['engine'] ?? ''), $validEngines, true)
|
||||
? (string)$input['engine'] : 'azure_mini';
|
||||
|
||||
$validFocus = ['all', 'deadlines', 'hearings', 'cps'];
|
||||
$focus = in_array((string)($input['focus'] ?? ''), $validFocus, true)
|
||||
? (string)$input['focus'] : 'all';
|
||||
|
||||
$confidenceFilter = (string)($input['confidence_filter'] ?? '') === 'high_medium'
|
||||
? 'high_medium' : 'all';
|
||||
|
||||
$includeRelative = ($input['include_relative'] ?? true) !== false;
|
||||
|
||||
return (new DbnLegalToolsService())->timeline($text, $language, $engine, $focus, $confidenceFilter, $includeRelative);
|
||||
});
|
||||
|
||||
+45
-5
@@ -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;
|
||||
|
||||
+207
-9
@@ -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>
|
||||
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>${escapeHtml(ev.actor || 'unknown actor')}</span>
|
||||
<p>${escapeHtml(ev.event || '')}</p>
|
||||
${ev.source_excerpt ? `<small>${escapeHtml(ev.source_excerpt)}</small>` : ''}
|
||||
</li>
|
||||
`).join('')}</ol>`;
|
||||
<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) {
|
||||
|
||||
+78
-7
@@ -279,16 +279,35 @@ PROMPT;
|
||||
];
|
||||
}
|
||||
|
||||
public function timeline(string $text, string $language = 'en'): array
|
||||
{
|
||||
public function timeline(
|
||||
string $text,
|
||||
string $language = 'en',
|
||||
string $engine = 'azure_mini',
|
||||
string $focus = 'all',
|
||||
string $confidenceFilter = 'all',
|
||||
bool $includeRelative = true
|
||||
): array {
|
||||
$text = $this->requirePasteText($text);
|
||||
$engine = in_array($engine, ['azure_mini', 'azure_full', 'gpu'], true) ? $engine : 'azure_mini';
|
||||
$focus = in_array($focus, ['all', 'deadlines', 'hearings', 'cps'], true) ? $focus : 'all';
|
||||
|
||||
if ($engine !== 'gpu') {
|
||||
$this->azure->requireChat();
|
||||
}
|
||||
|
||||
$locale = $language === 'no' ? 'Norwegian' : 'English';
|
||||
|
||||
$focusInstruction = match ($focus) {
|
||||
'deadlines' => "\nFocus specifically on: legal deadlines, filing dates, response windows, appeal periods, and statutory time limits. Deprioritise narrative events with no legal deadline significance.",
|
||||
'hearings' => "\nFocus specifically on: court hearings, tribunal sessions, mediation sessions, formal meetings, and hearing-related procedural dates.",
|
||||
'cps' => "\nFocus specifically on: CPS (Barnevernet) interventions, home visits, case reviews, acute measures (akuttvedtak), and Fylkesnemnda proceedings.",
|
||||
default => '',
|
||||
};
|
||||
|
||||
$prompt = <<<PROMPT
|
||||
Build a chronological timeline from the pasted text in {$locale}.
|
||||
|
||||
Extract ALL dates, deadlines, milestones, and temporal references.
|
||||
Extract ALL dates, deadlines, milestones, and temporal references.{$focusInstruction}
|
||||
For each temporal reference provide:
|
||||
- "date": ISO 8601 date (YYYY-MM-DD) if determinable, otherwise a human-readable description
|
||||
- "date_type": one of absolute | relative | recurring | conditional | period
|
||||
@@ -314,12 +333,64 @@ Return JSON only:
|
||||
}
|
||||
PROMPT;
|
||||
|
||||
$json = $this->runJsonTool($prompt, $language, 4000);
|
||||
$system = $this->legalJsonSystemPrompt($language);
|
||||
$messages = [
|
||||
['role' => 'system', 'content' => $system],
|
||||
['role' => 'user', 'content' => $prompt],
|
||||
];
|
||||
$chatOptions = ['json' => true, 'temperature' => 0.1, 'max_tokens' => 4000];
|
||||
$deployLabel = $this->azure->chatDeployment();
|
||||
|
||||
try {
|
||||
if ($engine === 'gpu') {
|
||||
$response = $this->callGpuLlm($messages, $chatOptions);
|
||||
$deployLabel = 'GPU (cuttlefish)';
|
||||
} elseif ($engine === 'azure_full') {
|
||||
$response = $this->azure->withDeployment('gpt-4o')->chat($messages, $chatOptions);
|
||||
$deployLabel = 'gpt-4o';
|
||||
} else {
|
||||
$response = $this->azure->chat($messages, $chatOptions);
|
||||
$deployLabel = $this->azure->chatDeployment();
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
dbnToolsAbort('LLM request failed: ' . $e->getMessage(), 502, 'llm_error');
|
||||
}
|
||||
|
||||
$raw = (string)($response['choices'][0]['message']['content'] ?? '');
|
||||
$json = $this->azure->decodeJsonObject($raw);
|
||||
if (!$json) {
|
||||
dbnToolsAbort('The selected engine did not return valid structured JSON.', 502, 'llm_invalid_json');
|
||||
}
|
||||
|
||||
$events = is_array($json['events'] ?? null) ? $json['events'] : [];
|
||||
|
||||
// Post-filter: confidence
|
||||
if ($confidenceFilter === 'high_medium') {
|
||||
$events = array_values(array_filter($events, fn($ev) => ($ev['confidence'] ?? 'low') !== 'low'));
|
||||
}
|
||||
|
||||
// Post-filter: relative/recurring date types
|
||||
if (!$includeRelative) {
|
||||
$events = array_values(array_filter($events, fn($ev) => ($ev['date_type'] ?? 'absolute') === 'absolute'));
|
||||
}
|
||||
|
||||
$engineLabel = match ($engine) {
|
||||
'gpu' => 'GPU (cuttlefish)',
|
||||
'azure_full' => 'gpt-4o',
|
||||
default => $deployLabel ?? $this->azure->chatDeployment(),
|
||||
};
|
||||
|
||||
$focusLabel = match ($focus) {
|
||||
'deadlines' => 'legal deadlines',
|
||||
'hearings' => 'court hearings',
|
||||
'cps' => 'CPS milestones',
|
||||
default => 'all events',
|
||||
};
|
||||
|
||||
$trace = [
|
||||
$this->trace('Query interpretation', 'Extract dated events from pasted text without saving the text or output.', 'complete'),
|
||||
$this->trace('Query interpretation', "Extract {$focusLabel} from pasted text. Engine: {$engineLabel}. Without saving the text or output.", 'complete'),
|
||||
$this->trace('Search tools used', 'No external corpus search; source is the user-pasted text.', 'complete'),
|
||||
$this->trace('Evidence found', count($events) . ' event(s) identified.', count($events) ? 'complete' : 'warning'),
|
||||
$this->trace('Evidence found', count($events) . ' event(s) identified' . ($confidenceFilter === 'high_medium' ? ' (low-confidence filtered out)' : '') . '.', count($events) ? 'complete' : 'warning'),
|
||||
$this->trace('Citation confidence', 'Confidence is per event and based only on the pasted text.', 'complete'),
|
||||
$this->trace('Uncertainty / missing evidence', $this->uncertaintySummary($json['what_remains_uncertain'] ?? []), 'complete'),
|
||||
$this->trace('Next practical step', (string)($json['next_practical_step'] ?? 'Verify dates against original documents.'), 'complete'),
|
||||
@@ -337,7 +408,7 @@ PROMPT;
|
||||
'trace_metadata' => [
|
||||
'chunk_count' => count($events),
|
||||
'source_count' => 1,
|
||||
'deployment' => $this->azure->chatDeployment(),
|
||||
'deployment' => $engineLabel,
|
||||
],
|
||||
'disclaimer' => dbnToolsDisclaimer($language),
|
||||
];
|
||||
|
||||
+98
-1
@@ -6,5 +6,102 @@ $toolKind = 'Timeline Builder';
|
||||
$toolBadge = 'process-and-forget';
|
||||
require_once __DIR__ . '/includes/layout.php';
|
||||
?>
|
||||
<?php require_once __DIR__ . '/includes/tool_form.php'; ?>
|
||||
<form id="toolForm" class="tool-form">
|
||||
|
||||
<div class="lang-switcher" id="timelineLangSwitcher" role="group" aria-label="UI language">
|
||||
<button type="button" class="lang-btn is-active" data-lang="en">🇬🇧 EN</button>
|
||||
<button type="button" class="lang-btn" data-lang="no">🇳🇴 NO</button>
|
||||
<button type="button" class="lang-btn" data-lang="uk">🇺🇦 UK</button>
|
||||
<button type="button" class="lang-btn" data-lang="pl">🇵🇱 PL</button>
|
||||
</div>
|
||||
|
||||
<div class="control-row" id="timelineEngineControl">
|
||||
<span class="control-label" data-i18n="timelineEngine">Engine</span>
|
||||
<label><input type="radio" name="timelineEngine" value="azure_mini" checked id="timelineEngineAzureMini"> <span data-i18n="timelineEngineAzureMini">Azure gpt-4o-mini</span> ★ <small class="control-hint">(fast)</small></label>
|
||||
<label><input type="radio" name="timelineEngine" value="azure_full" id="timelineEngineAzureFull"> <span data-i18n="timelineEngineAzureFull">Azure gpt-4o</span> <small class="control-hint">(best)</small></label>
|
||||
<label><input type="radio" name="timelineEngine" value="gpu" id="timelineEngineGpu"> <span data-i18n="timelineEngineGpu">GPU (cuttlefish)</span> <small class="control-hint">(local)</small></label>
|
||||
</div>
|
||||
<p class="upload-hint" data-i18n="timelineEngineHint">Azure engines use your BNL Azure credits. GPU runs the local LiteLLM proxy on cuttlefish.</p>
|
||||
|
||||
<details class="advanced-panel" id="timelineAdvanced">
|
||||
<summary class="advanced-toggle" data-i18n="timelineAdvancedToggle">Advanced settings</summary>
|
||||
|
||||
<div class="control-row" id="timelineFocusControl">
|
||||
<span class="control-label" data-i18n="timelineFocus">Focus</span>
|
||||
<label><input type="radio" name="timelineFocus" value="all" checked> <span data-i18n="timelineFocusAll">All events</span> ★</label>
|
||||
<label><input type="radio" name="timelineFocus" value="deadlines"> <span data-i18n="timelineFocusDeadlines">Legal deadlines</span></label>
|
||||
<label><input type="radio" name="timelineFocus" value="hearings"> <span data-i18n="timelineFocusHearings">Court hearings</span></label>
|
||||
<label><input type="radio" name="timelineFocus" value="cps"> <span data-i18n="timelineFocusCps">CPS milestones</span></label>
|
||||
</div>
|
||||
<p class="upload-hint" data-i18n="timelineFocusHint">All events: extract every temporal reference. Legal deadlines: filing dates, appeal windows, statutory limits. Court hearings: tribunal and mediation sessions. CPS milestones: Barnevernet interventions and Fylkesnemnda proceedings.</p>
|
||||
|
||||
<div class="control-row" id="timelineConfidenceControl">
|
||||
<span class="control-label" data-i18n="timelineConfidence">Confidence</span>
|
||||
<label><input type="radio" name="confidenceFilter" value="all" checked> <span data-i18n="timelineConfidenceAll">Show all events</span> ★</label>
|
||||
<label><input type="radio" name="confidenceFilter" value="high_medium"> <span data-i18n="timelineConfidenceHighMed">Hide low-confidence</span></label>
|
||||
</div>
|
||||
<p class="upload-hint" data-i18n="timelineConfidenceHint">Show all: includes events the model is uncertain about (shown in grey). Hide low-confidence: only returns events the model is reasonably sure of.</p>
|
||||
|
||||
<div class="control-row" id="timelineDatesControl">
|
||||
<span class="control-label" data-i18n="timelineDates">Date types</span>
|
||||
<label><input type="checkbox" id="includeRelativeCheck" name="include_relative" checked> <span data-i18n="timelineIncludeRelative">Include relative / recurring dates</span></label>
|
||||
</div>
|
||||
<p class="upload-hint" data-i18n="timelineDatesHint">When checked, relative references ("three weeks later", "every Monday") and recurring dates are included. Uncheck to return only exact calendar dates.</p>
|
||||
|
||||
</details>
|
||||
|
||||
<div class="upload-zone" id="uploadZone" role="region" aria-label="File upload" data-i18n-aria="timelineUploadAria">
|
||||
<input type="file" id="uploadInput" multiple accept=".pdf,.docx,.txt" aria-label="Choose files">
|
||||
<div id="uploadPrompt" class="upload-prompt">
|
||||
<span class="upload-icon" aria-hidden="true">⇧</span>
|
||||
<p><span data-i18n="timelineUploadDrop">Drop up to 5 files here, or</span> <label for="uploadInput" class="upload-browse" data-i18n="timelineUploadBrowse">browse</label></p>
|
||||
<p class="upload-hint"><strong>PDF</strong>, <strong>DOCX</strong>, <strong>TXT</strong> — <span data-i18n="timelineUploadHint">text extracted in memory, never stored</span></p>
|
||||
</div>
|
||||
<div id="uploadFileInfo" class="upload-file is-hidden">
|
||||
<ul id="uploadFileList" class="upload-file-list"></ul>
|
||||
<button type="button" id="uploadClear" class="upload-clear" data-i18n="timelineUploadClear">× Clear</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<label class="input-label" for="toolInput" id="inputLabel" data-i18n="timelineInputLabel">Pasted text</label>
|
||||
<textarea id="toolInput" name="toolInput" rows="10" required data-i18n-placeholder="timelineInputPlaceholder" placeholder="Paste case notes, court decisions, or correspondence containing dates and events."></textarea>
|
||||
|
||||
<div class="form-footer">
|
||||
<p id="toolStatus" class="form-status" role="status" aria-live="polite"></p>
|
||||
<button id="runButton" type="submit" data-i18n="timelineRun">Run</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<section id="results" class="results" aria-live="polite">
|
||||
<div class="empty-state">
|
||||
<h3 data-i18n="timelineReadyTitle">Ready</h3>
|
||||
<p data-i18n="timelineReadyDesc">Paste text or upload a file, configure options, then run.</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Hidden stubs so tools.js element refs don't crash on this page -->
|
||||
<div class="is-hidden" id="languageControl" aria-hidden="true"><input type="radio" name="language" value="en" checked></div>
|
||||
<div class="is-hidden" id="redactionControl" aria-hidden="true"></div>
|
||||
<div class="is-hidden" id="audioZone" aria-hidden="true">
|
||||
<input type="file" id="audioInput" style="display:none">
|
||||
<div id="audioPrompt"></div>
|
||||
<div id="audioFileInfo"><ol id="audioQueueList"></ol><button type="button" id="audioClear"></button></div>
|
||||
</div>
|
||||
<div class="is-hidden" id="diarizeControl" aria-hidden="true">
|
||||
<input type="checkbox" id="diarizeCheck">
|
||||
<input type="number" id="numSpeakersInput">
|
||||
</div>
|
||||
<div class="is-hidden" id="transcribeLangControl" aria-hidden="true"><input type="radio" name="transcribeLang" value="no" checked></div>
|
||||
<div class="is-hidden" id="vocabControl" aria-hidden="true">
|
||||
<div id="vocabPresets"></div>
|
||||
<textarea id="initPromptInput"></textarea>
|
||||
</div>
|
||||
<div class="is-hidden" id="aliasSection" aria-hidden="true">
|
||||
<button type="button" id="addAliasRow"></button>
|
||||
<div id="aliasRows"></div>
|
||||
</div>
|
||||
<div class="is-hidden" id="exemptSection" aria-hidden="true">
|
||||
<button type="button" id="addExemptRow"></button>
|
||||
<div id="exemptRows"></div>
|
||||
</div>
|
||||
<?php require_once __DIR__ . '/includes/layout_footer.php'; ?>
|
||||
|
||||
Reference in New Issue
Block a user