Timeline: group same-date/actor events, clean badges, Bedrock routing
- 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 <noreply@anthropic.com>
This commit is contained in:
@@ -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;
|
||||
|
||||
+84
-33
@@ -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 '<p class="timeline-empty">No matching events.</p>';
|
||||
}
|
||||
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 = `<li class="timeline-group-header" role="presentation"><span>${escapeHtml(label)}</span></li>`;
|
||||
: `${MONTHS[parseInt(mon, 10) - 1] || mon} ${year}`;
|
||||
monthHeader = `<li class="timeline-group-header" role="presentation"><span>${escapeHtml(label)}</span></li>`;
|
||||
lastGroupKey = key;
|
||||
}
|
||||
}
|
||||
const excerptHtml = ev.source_excerpt
|
||||
? `<small class="timeline-excerpt${showSources ? '' : ' is-hidden'}">${escapeHtml(ev.source_excerpt)}</small>`
|
||||
// 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'
|
||||
? `<span class="confidence-low-flag" title="Low confidence — date or event may be approximate">⚠</span>`
|
||||
: '';
|
||||
const copyText = [ev.date, ev.actor, ev.event].filter(Boolean).join(' · ');
|
||||
return `${groupHeader}<li class="timeline-item confidence-${escapeHtml(conf)}">
|
||||
// Collect unique times across the group
|
||||
const times = [...new Set(grp.events.map((ev) => ev.time).filter(Boolean))];
|
||||
const timeHtml = times.length ? `<span class="timeline-time">${escapeHtml(times.join(', '))}</span>` : '';
|
||||
const displayDate = fmtDate(grp.date);
|
||||
|
||||
if (grp.events.length === 1) {
|
||||
const ev = grp.events[0];
|
||||
const excerptHtml = ev.source_excerpt
|
||||
? `<small class="timeline-excerpt${showSources ? '' : ' is-hidden'}">${escapeHtml(ev.source_excerpt)}</small>`
|
||||
: '';
|
||||
const copyText = [grp.date, grp.actor, ev.event].filter(Boolean).join(' · ');
|
||||
return `${monthHeader}<li class="timeline-item confidence-${escapeHtml(worstConf)}">
|
||||
<div class="timeline-header">
|
||||
<strong class="timeline-date">${escapeHtml(displayDate)}${timeHtml}</strong>
|
||||
${lowFlag}
|
||||
<button type="button" class="timeline-copy-btn" data-copy="${escapeHtml(copyText)}" title="Copy event" aria-label="Copy event to clipboard">📋</button>
|
||||
</div>
|
||||
<span class="timeline-actor">${escapeHtml(grp.actor || 'unknown actor')}</span>
|
||||
<p class="timeline-event">${escapeHtml(ev.event || '')}</p>
|
||||
${excerptHtml}
|
||||
</li>`;
|
||||
}
|
||||
// Multi-event group: render as bullet list
|
||||
const bullets = grp.events.map((ev) => {
|
||||
const excerptHtml = ev.source_excerpt
|
||||
? `<small class="timeline-excerpt${showSources ? '' : ' is-hidden'}">${escapeHtml(ev.source_excerpt)}</small>`
|
||||
: '';
|
||||
const evLow = ev.confidence === 'low'
|
||||
? `<span class="confidence-low-flag" title="Low confidence">⚠</span>`
|
||||
: '';
|
||||
return `<li class="timeline-bullet">${escapeHtml(ev.event || '')}${evLow}${excerptHtml}</li>`;
|
||||
}).join('');
|
||||
const copyText = [grp.date, grp.actor, ...grp.events.map((ev) => ev.event)].filter(Boolean).join('\n');
|
||||
return `${monthHeader}<li class="timeline-item timeline-item--grouped confidence-${escapeHtml(worstConf)}">
|
||||
<div class="timeline-header">
|
||||
<strong class="timeline-date">${escapeHtml(ev.date || 'unknown')}${ev.time ? `<span class="timeline-time"> ${escapeHtml(ev.time)}</span>` : ''}</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>
|
||||
<button type="button" class="timeline-copy-btn" data-copy="${escapeHtml(copyText)}" title="Copy event" aria-label="Copy event to clipboard">📋</button>
|
||||
<strong class="timeline-date">${escapeHtml(displayDate)}${timeHtml}</strong>
|
||||
${lowFlag}
|
||||
<button type="button" class="timeline-copy-btn" data-copy="${escapeHtml(copyText)}" title="Copy events" aria-label="Copy events to clipboard">📋</button>
|
||||
</div>
|
||||
<span class="timeline-actor">${escapeHtml(ev.actor || 'unknown actor')}</span>
|
||||
<p class="timeline-event">${escapeHtml(ev.event || '')}</p>
|
||||
${excerptHtml}
|
||||
<span class="timeline-actor">${escapeHtml(grp.actor || 'unknown actor')}</span>
|
||||
<ul class="timeline-bullets">${bullets}</ul>
|
||||
</li>`;
|
||||
}).join('');
|
||||
return `<ol class="timeline-list">${items}</ol>`;
|
||||
|
||||
Reference in New Issue
Block a user