From 234ab7278bf837b305f5aa8ad11502facd4f7470 Mon Sep 17 00:00:00 2001 From: davegilligan Date: Mon, 25 May 2026 23:21:35 +0200 Subject: [PATCH] Timeline: group same-date/actor events, clean badges, Bedrock routing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - renderTimeline(): group consecutive same-date+actor events into one card with a bullet list; single events keep their current layout - Date format: YYYY-MM-DD → "1 Jun 2023" (3-letter month, international) - Time shown in header when available - Remove date_type badge; confidence badge replaced by amber ⚠ flag on low-confidence events only (high/medium border colour still shows) - LegalTools.php: resolve azure_full/azure_mini to Bedrock Sonnet/Haiku when DbnBedrockGateway is active; claude_sonnet/claude_haiku also handled - timeline.php + api/timeline.php: engine labels updated (Claude Haiku/Sonnet); claude_haiku + claude_sonnet added to valid engine list - i18n engine labels updated in all 4 languages Co-Authored-By: Claude Sonnet 4.6 --- api/timeline.php | 2 +- assets/css/tools.css | 23 ++++++++ assets/js/tools.js | 117 ++++++++++++++++++++++++++++------------ includes/LegalTools.php | 26 ++++++--- timeline.php | 6 +-- 5 files changed, 130 insertions(+), 44 deletions(-) diff --git a/api/timeline.php b/api/timeline.php index 03f36e0..2561aeb 100644 --- a/api/timeline.php +++ b/api/timeline.php @@ -8,7 +8,7 @@ dbnToolsRequireMethod('POST'); dbnToolsRequireAuth(); $input = dbnToolsJsonInput(1500000); $language = dbnToolsNormalizeLanguage($input['language'] ?? 'en'); -$_validEngines = ['nova_lite', 'azure_mini', 'azure_full']; +$_validEngines = ['nova_lite', 'azure_mini', 'azure_full', 'claude_haiku', 'claude_sonnet']; $_requestedEngine = in_array((string)($input['engine'] ?? ''), $_validEngines, true) ? (string)$input['engine'] : 'azure_mini'; diff --git a/assets/css/tools.css b/assets/css/tools.css index 774a013..b6271fb 100644 --- a/assets/css/tools.css +++ b/assets/css/tools.css @@ -849,6 +849,29 @@ p { font-size: 0.82rem; } +.confidence-low-flag { + color: #d97706; + font-size: 0.88rem; + margin-left: 4px; + vertical-align: middle; + flex-shrink: 0; +} + +.timeline-bullets { + margin: 4px 0 0 0; + padding-left: 1.25em; + list-style: disc; +} + +.timeline-bullet { + margin: 4px 0; + line-height: 1.45; +} + +.timeline-bullet .timeline-excerpt { + margin-top: 2px; +} + .confidence-badge { font-size: 0.7rem; font-weight: 600; diff --git a/assets/js/tools.js b/assets/js/tools.js index d5dc037..23bb7c7 100644 --- a/assets/js/tools.js +++ b/assets/js/tools.js @@ -221,9 +221,9 @@ const REDACT_I18N = { const TIMELINE_I18N = { en: { timelineEngine: 'Engine', - timelineEngineAzureMini: 'Azure gpt-4o-mini', - timelineEngineAzureFull: 'Azure gpt-4o', - timelineEngineHint: 'gpt-4o-mini: 1 credit — fast, handles most timelines well. gpt-4o: 2 credits — higher accuracy for complex multi-actor cases.', + timelineEngineAzureMini: 'Standard', + timelineEngineAzureFull: 'Deep', + timelineEngineHint: 'Standard uses Claude Haiku 4.5 — fast and accurate for most documents (1 credit). Deep uses Claude Sonnet 4.6 — better for complex multi-actor cases (2 credits).', timelineAdvancedToggle: 'Advanced settings', timelineFocus: 'Focus', timelineFocusAll: 'All events', @@ -262,9 +262,9 @@ const TIMELINE_I18N = { }, no: { timelineEngine: 'Motor', - timelineEngineAzureMini: 'Azure gpt-4o-mini', - timelineEngineAzureFull: 'Azure gpt-4o', - timelineEngineHint: 'gpt-4o-mini: 1 kreditt — rask, håndterer de fleste tidslinjer godt. gpt-4o: 2 kreditter — høyere nøyaktighet for komplekse saker med mange aktører.', + timelineEngineAzureMini: 'Standard', + timelineEngineAzureFull: 'Dyp', + timelineEngineHint: 'Standard bruker Claude Haiku 4.5 — rask og nøyaktig for de fleste dokumenter (1 kreditt). Dyp bruker Claude Sonnet 4.6 — bedre for komplekse saker med mange aktører (2 kreditter).', timelineAdvancedToggle: 'Avanserte innstillinger', timelineFocus: 'Fokus', timelineFocusAll: 'Alle hendelser', @@ -303,9 +303,9 @@ const TIMELINE_I18N = { }, uk: { timelineEngine: 'Рушій', - timelineEngineAzureMini: 'Azure gpt-4o-mini', - timelineEngineAzureFull: 'Azure gpt-4o', - timelineEngineHint: 'gpt-4o-mini: 1 кредит — швидко, добре справляється з більшістю хронологій. gpt-4o: 2 кредити — вища точність для складних справ з багатьма учасниками.', + timelineEngineAzureMini: 'Стандарт', + timelineEngineAzureFull: 'Глибокий', + timelineEngineHint: 'Стандарт використовує Claude Haiku 4.5 — швидко і точно для більшості документів (1 кредит). Глибокий використовує Claude Sonnet 4.6 — краще для складних справ (2 кредити).', timelineAdvancedToggle: 'Розширені налаштування', timelineFocus: 'Фокус', timelineFocusAll: 'Всі події', @@ -344,9 +344,9 @@ const TIMELINE_I18N = { }, pl: { timelineEngine: 'Silnik', - timelineEngineAzureMini: 'Azure gpt-4o-mini', - timelineEngineAzureFull: 'Azure gpt-4o', - timelineEngineHint: 'gpt-4o-mini: 1 kredyt — szybki, sprawdza się w większości osi czasu. gpt-4o: 2 kredyty — wyższa dokładność dla złożonych spraw z wieloma uczestnikami.', + timelineEngineAzureMini: 'Standard', + timelineEngineAzureFull: 'Zaawansowany', + timelineEngineHint: 'Standard używa Claude Haiku 4.5 — szybki i dokładny dla większości dokumentów (1 kredyt). Zaawansowany używa Claude Sonnet 4.6 — lepszy dla złożonych spraw z wieloma uczestnikami (2 kredyty).', timelineAdvancedToggle: 'Ustawienia zaawansowane', timelineFocus: 'Fokus', timelineFocusAll: 'Wszystkie zdarzenia', @@ -1854,38 +1854,89 @@ function renderTimeline(events, grouped = false) { } return '

No matching events.

'; } - const MONTH_NAMES = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec']; + const MONTHS = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec']; + function fmtDate(d) { + if (!d || !/^\d{4}-\d{2}-\d{2}/.test(d)) return d || 'unknown'; + const [y, m, day] = d.split('-'); + return `${parseInt(day, 10)} ${MONTHS[parseInt(m, 10) - 1] || m} ${y}`; + } + // Group consecutive events with the same date + actor into one card + const groups = []; + for (const ev of events) { + const key = `${ev.date || ''}||${ev.actor || ''}`; + const last = groups[groups.length - 1]; + if (last && last.key === key) { + last.events.push(ev); + } else { + groups.push({ key, date: ev.date || '', actor: ev.actor || '', events: [ev] }); + } + } + const CONF_RANK = { high: 0, medium: 1, low: 2 }; let lastGroupKey = null; - const items = events.map((ev) => { - const conf = ev.confidence || 'medium'; - let groupHeader = ''; - if (grouped && /^\d{4}-\d{2}/.test(ev.date || '')) { - const year = ev.date.slice(0, 4); - const mon = ev.date.slice(5, 7); + const items = groups.map((grp) => { + let monthHeader = ''; + if (grouped && /^\d{4}-\d{2}/.test(grp.date)) { + const year = grp.date.slice(0, 4); + const mon = grp.date.slice(5, 7); const key = `${year}-${mon}`; if (key !== lastGroupKey) { const prevYear = lastGroupKey ? lastGroupKey.slice(0, 4) : null; - const label = (year !== prevYear) + const label = year !== prevYear ? year - : `${MONTH_NAMES[parseInt(mon, 10) - 1] || mon} ${year}`; - groupHeader = ``; + : `${MONTHS[parseInt(mon, 10) - 1] || mon} ${year}`; + monthHeader = ``; lastGroupKey = key; } } - const excerptHtml = ev.source_excerpt - ? `${escapeHtml(ev.source_excerpt)}` + // Worst confidence in the group (drives border colour) + const worstConf = grp.events.reduce( + (w, ev) => (CONF_RANK[ev.confidence || 'medium'] ?? 1) > (CONF_RANK[w] ?? 1) ? (ev.confidence || 'medium') : w, + 'high' + ); + const lowFlag = worstConf === 'low' + ? `` : ''; - const copyText = [ev.date, ev.actor, ev.event].filter(Boolean).join(' · '); - return `${groupHeader}
  • + // Collect unique times across the group + const times = [...new Set(grp.events.map((ev) => ev.time).filter(Boolean))]; + const timeHtml = times.length ? `${escapeHtml(times.join(', '))}` : ''; + const displayDate = fmtDate(grp.date); + + if (grp.events.length === 1) { + const ev = grp.events[0]; + const excerptHtml = ev.source_excerpt + ? `${escapeHtml(ev.source_excerpt)}` + : ''; + const copyText = [grp.date, grp.actor, ev.event].filter(Boolean).join(' · '); + return `${monthHeader}
  • +
    + ${escapeHtml(displayDate)}${timeHtml} + ${lowFlag} + +
    + ${escapeHtml(grp.actor || 'unknown actor')} +

    ${escapeHtml(ev.event || '')}

    + ${excerptHtml} +
  • `; + } + // Multi-event group: render as bullet list + const bullets = grp.events.map((ev) => { + const excerptHtml = ev.source_excerpt + ? `${escapeHtml(ev.source_excerpt)}` + : ''; + const evLow = ev.confidence === 'low' + ? `` + : ''; + return `
  • ${escapeHtml(ev.event || '')}${evLow}${excerptHtml}
  • `; + }).join(''); + const copyText = [grp.date, grp.actor, ...grp.events.map((ev) => ev.event)].filter(Boolean).join('\n'); + return `${monthHeader}
  • - ${escapeHtml(ev.date || 'unknown')}${ev.time ? ` ${escapeHtml(ev.time)}` : ''} - ${ev.date_type ? `${escapeHtml(ev.date_type)}` : ''} - ${escapeHtml(conf)} - + ${escapeHtml(displayDate)}${timeHtml} + ${lowFlag} +
    - ${escapeHtml(ev.actor || 'unknown actor')} -

    ${escapeHtml(ev.event || '')}

    - ${excerptHtml} + ${escapeHtml(grp.actor || 'unknown actor')} +
      ${bullets}
  • `; }).join(''); return `
      ${items}
    `; diff --git a/includes/LegalTools.php b/includes/LegalTools.php index f0571d3..b31d502 100644 --- a/includes/LegalTools.php +++ b/includes/LegalTools.php @@ -506,18 +506,25 @@ PROMPT; ['role' => 'system', 'content' => $system], ['role' => 'user', 'content' => $prompt], ]; - $maxTokens = match ($engine) { 'azure_full' => 8000, 'nova_lite' => 2000, default => 4000 }; + $isBedrock = $this->azure instanceof DbnBedrockGateway; + $maxTokens = match ($engine) { 'azure_full', 'claude_sonnet' => 8000, 'nova_lite' => 2000, default => 4000 }; $chatOptions = ['json' => true, 'temperature' => 0.1, 'max_tokens' => $maxTokens, 'timeout' => 120]; - $deployLabel = match ($engine) { 'azure_full' => 'gpt-4o', 'nova_lite' => 'nova-lite', default => 'gpt-4o-mini' }; + $deployLabel = match (true) { + $engine === 'nova_lite' => 'nova-lite', + $engine === 'azure_full' || $engine === 'claude_sonnet' => $isBedrock ? 'claude-sonnet-bedrock' : 'gpt-4o', + default => $isBedrock ? 'claude-haiku-bedrock' : 'gpt-4o-mini', + }; $onProgress && $onProgress("Calling {$deployLabel}\u{2026}"); try { if ($engine === 'nova_lite') { $response = dbnToolsCallGpuLlm($messages, ['model' => 'nova-lite', 'max_tokens' => $maxTokens, 'temperature' => 0.1, 'timeout' => 120]); - } elseif ($engine === 'azure_full') { - $response = $this->azure->withDeployment('gpt-4o')->chat($messages, $chatOptions); + } elseif ($engine === 'azure_full' || $engine === 'claude_sonnet') { + $deploy = $isBedrock ? DbnBedrockModelRouter::LITELLM_SONNET : 'gpt-4o'; + $response = $this->azure->withDeployment($deploy)->chat($messages, $chatOptions); } else { - $response = $this->azure->withDeployment('gpt-4o-mini')->chat($messages, $chatOptions); + $deploy = $isBedrock ? DbnBedrockModelRouter::LITELLM_HAIKU : 'gpt-4o-mini'; + $response = $this->azure->withDeployment($deploy)->chat($messages, $chatOptions); } } catch (Throwable $e) { $msg = $e->getMessage(); @@ -570,7 +577,7 @@ PROMPT; $events = array_values(array_filter($events, fn($ev) => ($ev['date_type'] ?? 'absolute') === 'absolute')); } - $engineLabel = match ($engine) { 'azure_full' => 'gpt-4o', 'nova_lite' => 'nova-lite', default => 'gpt-4o-mini' }; + $engineLabel = $deployLabel; $focusLabel = match ($focus) { 'deadlines' => 'legal deadlines', @@ -620,7 +627,12 @@ PROMPT; ?callable $onProgress, int $inputDateHintCount ): array { - $engineLabel = match ($engine) { 'azure_full' => 'gpt-4o', 'nova_lite' => 'nova-lite', default => 'gpt-4o-mini' }; + $isBedrock = $this->azure instanceof DbnBedrockGateway; + $engineLabel = match (true) { + $engine === 'nova_lite' => 'nova-lite', + $engine === 'azure_full' || $engine === 'claude_sonnet' => $isBedrock ? 'claude-sonnet-bedrock' : 'gpt-4o', + default => $isBedrock ? 'claude-haiku-bedrock' : 'gpt-4o-mini', + }; $chunkSize = $this->timelineChunkSize($engine); $chunks = $this->timelineTextChunks($text, $chunkSize, 900); $chunkCount = count($chunks); diff --git a/timeline.php b/timeline.php index ac814bc..37eb468 100644 --- a/timeline.php +++ b/timeline.php @@ -26,10 +26,10 @@ require_once __DIR__ . '/includes/layout.php';
    Mode - - + +
    -

    Quick uses Amazon Bedrock nova-lite (fast, cheap). Standard and Deep use Azure OpenAI.

    +

    Standard uses Claude Haiku 4.5 — fast and accurate for most documents. Deep uses Claude Sonnet 4.6 — better for complex multi-actor cases.

    Advanced settings