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
+89 -18
View File
@@ -279,16 +279,35 @@ PROMPT;
];
}
public function timeline(string $text, string $language = 'en'): array
{
$text = $this->requirePasteText($text);
$this->azure->requireChat();
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,30 +333,82 @@ 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'),
];
return [
'tool' => 'timeline',
'language' => $language,
'what_we_found' => (string)($json['what_we_found'] ?? ''),
'events' => $events,
'evidence_trail' => $json['evidence_trail'] ?? [['title' => 'Pasted text', 'excerpt' => 'Processed in-memory only; not stored.']],
'tool' => 'timeline',
'language' => $language,
'what_we_found' => (string)($json['what_we_found'] ?? ''),
'events' => $events,
'evidence_trail' => $json['evidence_trail'] ?? [['title' => 'Pasted text', 'excerpt' => 'Processed in-memory only; not stored.']],
'what_remains_uncertain' => $json['what_remains_uncertain'] ?? [],
'next_practical_step' => (string)($json['next_practical_step'] ?? ''),
'trace' => $trace,
'trace_metadata' => [
'chunk_count' => count($events),
'next_practical_step' => (string)($json['next_practical_step'] ?? ''),
'trace' => $trace,
'trace_metadata' => [
'chunk_count' => count($events),
'source_count' => 1,
'deployment' => $this->azure->chatDeployment(),
'deployment' => $engineLabel,
],
'disclaimer' => dbnToolsDisclaimer($language),
];