Timeline: document upload, upgraded prompt, CSV export, date_type badge
This commit is contained in:
+2
-2
@@ -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);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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'),
|
||||||
|
|||||||
Reference in New Issue
Block a user