Timeline: document upload, upgraded prompt, CSV export, date_type badge

This commit is contained in:
2026-05-13 08:10:40 +02:00
parent 634a4fa154
commit bddafea049
4 changed files with 84 additions and 15 deletions
+2 -2
View File
@@ -5,10 +5,10 @@ require_once __DIR__ . '/../includes/LegalTools.php';
dbnToolsRequireMethod('POST'); dbnToolsRequireMethod('POST');
dbnToolsRequireAuth(); dbnToolsRequireAuth();
$input = dbnToolsJsonInput(70000); $input = dbnToolsJsonInput(400000);
$language = dbnToolsNormalizeLanguage($input['language'] ?? 'en'); $language = dbnToolsNormalizeLanguage($input['language'] ?? 'en');
dbnToolsWithTelemetry('timeline', $language, function () use ($input, $language): array { dbnToolsWithTelemetry('timeline', $language, function () use ($input, $language): array {
$text = dbnToolsString($input, 'text', 32000); $text = dbnToolsString($input, 'text', 128000);
return (new DbnLegalToolsService())->timeline($text, $language); return (new DbnLegalToolsService())->timeline($text, $language);
}); });
+30
View File
@@ -1073,3 +1073,33 @@ p {
color: var(--muted); color: var(--muted);
margin: 0.35rem 0 0; 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;
}
+33 -7
View File
@@ -3,6 +3,8 @@ const state = {
authenticated: Boolean(window.DBN_TOOLS_AUTHENTICATED), authenticated: Boolean(window.DBN_TOOLS_AUTHENTICATED),
}; };
let lastTimelineEvents = [];
const tools = { const tools = {
ask: { ask: {
kind: 'Source-grounded Legal Ask', kind: 'Source-grounded Legal Ask',
@@ -99,6 +101,9 @@ document.addEventListener('DOMContentLoaded', () => {
els.healthButton.addEventListener('click', checkHealth); els.healthButton.addEventListener('click', checkHealth);
setupUpload(); setupUpload();
setupAliases(); setupAliases();
els.results.addEventListener('click', (e) => {
if (e.target.closest('#exportCsvBtn')) exportTimelineCSV(lastTimelineEvents);
});
setTool(state.activeTool); setTool(state.activeTool);
if (state.authenticated) { if (state.authenticated) {
@@ -125,7 +130,7 @@ function setTool(toolName) {
els.input.placeholder = tool.placeholder; els.input.placeholder = tool.placeholder;
els.languageControl.classList.toggle('is-hidden', !tool.usesLanguage); els.languageControl.classList.toggle('is-hidden', !tool.usesLanguage);
els.redactionControl.classList.toggle('is-hidden', toolName !== 'redact'); 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'); els.aliasSection.classList.toggle('is-hidden', toolName !== 'redact');
resetUpload(); resetUpload();
resetAliases(); resetAliases();
@@ -421,7 +426,11 @@ function renderMainFinding(data) {
return `<pre class="redacted-output">${escapeHtml(data.redacted_text || '')}</pre>${renderEntityCounts(data.entity_counts)}`; return `<pre class="redacted-output">${escapeHtml(data.redacted_text || '')}</pre>${renderEntityCounts(data.entity_counts)}`;
} }
if (data.tool === 'timeline') { if (data.tool === 'timeline') {
return `<p>${escapeHtml(data.what_we_found || '')}</p>${renderTimeline(data.events || [])}`; lastTimelineEvents = data.events || [];
const csvBtn = lastTimelineEvents.length
? `<div class="timeline-export"><button type="button" id="exportCsvBtn" class="export-csv-btn">Download CSV</button></div>`
: '';
return `<p>${escapeHtml(data.what_we_found || '')}</p>${renderTimeline(lastTimelineEvents)}${csvBtn}`;
} }
if (data.tool === 'summarize') { if (data.tool === 'summarize') {
return [ return [
@@ -477,16 +486,33 @@ function renderTimeline(events) {
if (!events.length) { if (!events.length) {
return '<p>No events were identified.</p>'; return '<p>No events were identified.</p>';
} }
return `<ol class="timeline-list">${events.map((event) => ` return `<ol class="timeline-list">${events.map((ev) => `
<li> <li>
<strong>${escapeHtml(event.date || 'unknown')}</strong> <strong>${escapeHtml(ev.date || 'unknown')}</strong>
<span>${escapeHtml(event.actor || 'unknown actor')}</span> ${ev.date_type ? `<span class="date-type-badge">${escapeHtml(ev.date_type)}</span>` : ''}
<p>${escapeHtml(event.event || '')}</p> <span>${escapeHtml(ev.actor || 'unknown actor')}</span>
${event.source_excerpt ? `<small>${escapeHtml(event.source_excerpt)}</small>` : ''} <p>${escapeHtml(ev.event || '')}</p>
${ev.source_excerpt ? `<small>${escapeHtml(ev.source_excerpt)}</small>` : ''}
</li> </li>
`).join('')}</ol>`; `).join('')}</ol>`;
} }
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 = {}) { function renderEntityCounts(counts = {}) {
const entries = Object.entries(counts).filter(([, count]) => Number(count) > 0); const entries = Object.entries(counts).filter(([, count]) => Number(count) > 0);
if (!entries.length) { if (!entries.length) {
+19 -6
View File
@@ -286,7 +286,20 @@ PROMPT;
$locale = $language === 'no' ? 'Norwegian' : 'English'; $locale = $language === 'no' ? 'Norwegian' : 'English';
$prompt = <<<PROMPT $prompt = <<<PROMPT
Build a chronological timeline from this pasted text in {$locale}. Keep uncertain dates explicit. Build a chronological timeline from the pasted text in {$locale}.
Extract ALL dates, deadlines, milestones, and temporal references.
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
- "actor": person, institution, or party involved — or "unknown"
- "event": concise description of what happened or is due
- "source_excerpt": the verbatim phrase from the text that grounds this event (≤ 30 words)
- "confidence": high | medium | low
Sort events chronologically (absolute dates first, then relative, then recurring).
Keep uncertain dates explicit — do not invent dates not in the text.
If multiple documents are separated by "--- Document: … ---" markers, note the source document in the event description where helpful.
Pasted text: Pasted text:
{$text} {$text}
@@ -294,14 +307,14 @@ Pasted text:
Return JSON only: Return JSON only:
{ {
"what_we_found": "short overview", "what_we_found": "short overview",
"events": [{"date":"YYYY-MM-DD, month/year, or unknown","actor":"actor or unknown","event":"event","source_excerpt":"short excerpt","confidence":"high|medium|low"}], "events": [{"date":"...","date_type":"absolute","actor":"...","event":"...","source_excerpt":"...","confidence":"high|medium|low"}],
"evidence_trail": [{"title":"Pasted text","excerpt":"short relevant excerpt"}], "evidence_trail": [{"title":"...","excerpt":"..."}],
"what_remains_uncertain": ["uncertainty"], "what_remains_uncertain": ["..."],
"next_practical_step": "one concrete next action" "next_practical_step": "..."
} }
PROMPT; PROMPT;
$json = $this->runJsonTool($prompt, $language, 1600); $json = $this->runJsonTool($prompt, $language, 4000);
$events = is_array($json['events'] ?? null) ? $json['events'] : []; $events = is_array($json['events'] ?? null) ? $json['events'] : [];
$trace = [ $trace = [
$this->trace('Query interpretation', 'Extract dated events from pasted text without saving the text or output.', 'complete'), $this->trace('Query interpretation', 'Extract dated events from pasted text without saving the text or output.', 'complete'),