From bddafea049da19f04b8f23f9aa99b6d46b341f44 Mon Sep 17 00:00:00 2001 From: davegilligan Date: Wed, 13 May 2026 08:10:40 +0200 Subject: [PATCH] Timeline: document upload, upgraded prompt, CSV export, date_type badge --- api/timeline.php | 4 ++-- assets/css/tools.css | 30 ++++++++++++++++++++++++++++++ assets/js/tools.js | 40 +++++++++++++++++++++++++++++++++------- includes/LegalTools.php | 25 +++++++++++++++++++------ 4 files changed, 84 insertions(+), 15 deletions(-) diff --git a/api/timeline.php b/api/timeline.php index f5f888f..31f3323 100644 --- a/api/timeline.php +++ b/api/timeline.php @@ -5,10 +5,10 @@ require_once __DIR__ . '/../includes/LegalTools.php'; dbnToolsRequireMethod('POST'); dbnToolsRequireAuth(); -$input = dbnToolsJsonInput(70000); +$input = dbnToolsJsonInput(400000); $language = dbnToolsNormalizeLanguage($input['language'] ?? 'en'); dbnToolsWithTelemetry('timeline', $language, function () use ($input, $language): array { - $text = dbnToolsString($input, 'text', 32000); + $text = dbnToolsString($input, 'text', 128000); return (new DbnLegalToolsService())->timeline($text, $language); }); diff --git a/assets/css/tools.css b/assets/css/tools.css index fd71490..2f8c4fc 100644 --- a/assets/css/tools.css +++ b/assets/css/tools.css @@ -1073,3 +1073,33 @@ p { color: var(--muted); margin: 0.35rem 0 0; } + +.timeline-export { margin-top: 1rem; } + +.export-csv-btn { + font-size: 0.8rem; + padding: 0.35rem 0.9rem; + border: 1px solid var(--line); + border-radius: 6px; + background: transparent; + color: var(--muted); + cursor: pointer; +} + +.export-csv-btn:hover { + background: var(--bg); + color: var(--ink); +} + +.date-type-badge { + display: inline-block; + font-size: 0.68rem; + padding: 0.1rem 0.45rem; + border-radius: 4px; + background: var(--bg); + color: var(--muted); + margin-left: 0.4rem; + vertical-align: middle; + text-transform: uppercase; + letter-spacing: 0.03em; +} diff --git a/assets/js/tools.js b/assets/js/tools.js index 6e863dc..a3adbaf 100644 --- a/assets/js/tools.js +++ b/assets/js/tools.js @@ -3,6 +3,8 @@ const state = { authenticated: Boolean(window.DBN_TOOLS_AUTHENTICATED), }; +let lastTimelineEvents = []; + const tools = { ask: { kind: 'Source-grounded Legal Ask', @@ -99,6 +101,9 @@ document.addEventListener('DOMContentLoaded', () => { els.healthButton.addEventListener('click', checkHealth); setupUpload(); setupAliases(); + els.results.addEventListener('click', (e) => { + if (e.target.closest('#exportCsvBtn')) exportTimelineCSV(lastTimelineEvents); + }); setTool(state.activeTool); if (state.authenticated) { @@ -125,7 +130,7 @@ function setTool(toolName) { els.input.placeholder = tool.placeholder; els.languageControl.classList.toggle('is-hidden', !tool.usesLanguage); els.redactionControl.classList.toggle('is-hidden', toolName !== 'redact'); - els.uploadZone.classList.toggle('is-hidden', toolName !== 'redact'); + els.uploadZone.classList.toggle('is-hidden', toolName !== 'redact' && toolName !== 'timeline'); els.aliasSection.classList.toggle('is-hidden', toolName !== 'redact'); resetUpload(); resetAliases(); @@ -421,7 +426,11 @@ function renderMainFinding(data) { return `
${escapeHtml(data.redacted_text || '')}
${renderEntityCounts(data.entity_counts)}`; } if (data.tool === 'timeline') { - return `

${escapeHtml(data.what_we_found || '')}

${renderTimeline(data.events || [])}`; + lastTimelineEvents = data.events || []; + const csvBtn = lastTimelineEvents.length + ? `
` + : ''; + return `

${escapeHtml(data.what_we_found || '')}

${renderTimeline(lastTimelineEvents)}${csvBtn}`; } if (data.tool === 'summarize') { return [ @@ -477,16 +486,33 @@ function renderTimeline(events) { if (!events.length) { return '

No events were identified.

'; } - return `
    ${events.map((event) => ` + return `
      ${events.map((ev) => `
    1. - ${escapeHtml(event.date || 'unknown')} - ${escapeHtml(event.actor || 'unknown actor')} -

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

      - ${event.source_excerpt ? `${escapeHtml(event.source_excerpt)}` : ''} + ${escapeHtml(ev.date || 'unknown')} + ${ev.date_type ? `${escapeHtml(ev.date_type)}` : ''} + ${escapeHtml(ev.actor || 'unknown actor')} +

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

      + ${ev.source_excerpt ? `${escapeHtml(ev.source_excerpt)}` : ''}
    2. `).join('')}
    `; } +function exportTimelineCSV(events) { + const header = ['Date', 'Date Type', 'Actor', 'Event', 'Source Excerpt', 'Confidence']; + const rows = events.map((ev) => [ + ev.date || '', ev.date_type || '', ev.actor || '', + ev.event || '', ev.source_excerpt || '', ev.confidence || '', + ]); + const csv = [header, ...rows] + .map((row) => row.map((cell) => `"${String(cell).replace(/"/g, '""')}"`).join(',')) + .join('\n'); + const blob = new Blob([csv], { type: 'text/csv' }); + const url = URL.createObjectURL(blob); + const a = Object.assign(document.createElement('a'), { href: url, download: 'timeline.csv' }); + a.click(); + URL.revokeObjectURL(url); +} + function renderEntityCounts(counts = {}) { const entries = Object.entries(counts).filter(([, count]) => Number(count) > 0); if (!entries.length) { diff --git a/includes/LegalTools.php b/includes/LegalTools.php index 5b21410..fef1075 100644 --- a/includes/LegalTools.php +++ b/includes/LegalTools.php @@ -286,7 +286,20 @@ PROMPT; $locale = $language === 'no' ? 'Norwegian' : 'English'; $prompt = <<runJsonTool($prompt, $language, 1600); + $json = $this->runJsonTool($prompt, $language, 4000); $events = is_array($json['events'] ?? null) ? $json['events'] : []; $trace = [ $this->trace('Query interpretation', 'Extract dated events from pasted text without saving the text or output.', 'complete'),