Add manual 'Save result' to all tools — replaces auto-save
All tool results can now be saved to My Case manually. Users click 'Save result', type a description, and confirm. This replaces the previous silent auto-save on barnevernet/timeline/etc., giving users control over what stays and what it's called (supports multiple runs of the same tool with different titles). - CaseResults: extend ELIGIBLE_TOOLS to include summarize, ask, redact, transcribe; add toolLabel/toolIcon entries; support explicit title via meta['title'] in save() - api/case/save-result.php: new client-initiated save endpoint; accepts tool + title + input_payload + output_payload + meta - Remove CaseResults::save() auto-save from barnevernet, deep-research, discrepancy, korrespond, timeline API endpoints - tools.js: add showSaveResultButton() (exposed as window.dbnShowSaveResultButton); wire for ask, redact, timeline, transcribe (both file-upload and stored-audio paths) - barnevernet.js: wire save button after final result render - summarize.js: wire save button after renderFinal(); passes sumResults container so widget appears in the correct #sumResults div - case-result.php: rich tool-specific rendering for summarize, ask, redact, transcribe, timeline; update re-run link map to include all new tools - tools.css: styles for .save-result-widget and its states (idle, prompt, done, error) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+151
-5
@@ -65,14 +65,31 @@ if (!empty($caseDocIds)) {
|
||||
}
|
||||
}
|
||||
|
||||
// Best-effort extraction of the primary human-readable output
|
||||
// Best-effort extraction of the primary human-readable output (fallback for unknown tools)
|
||||
$primaryOutput = '';
|
||||
foreach (['draft', 'response', 'answer', 'text', 'summary', 'markdown'] as $k) {
|
||||
foreach (['draft', 'response', 'answer', 'what_we_found', 'text', 'transcript', 'redacted_text', 'summary', 'markdown'] as $k) {
|
||||
if (!empty($output[$k]) && is_string($output[$k])) {
|
||||
$primaryOutput = (string)$output[$k];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a list of strings as an unordered list within a collapsible block.
|
||||
*/
|
||||
function crListBlock(string $heading, array $items): string {
|
||||
if (empty($items)) return '';
|
||||
$lis = implode('', array_map(fn($v) => '<li>' . htmlspecialchars((string)$v, ENT_QUOTES, 'UTF-8') . '</li>', $items));
|
||||
return '<h3 style="font-family:\'Crimson Pro\',serif;font-size:1.05rem;color:#00205B;margin:1rem 0 0.35rem;">'
|
||||
. htmlspecialchars($heading, ENT_QUOTES, 'UTF-8')
|
||||
. '</h3><ul style="margin:0 0 0.5rem;padding-left:1.3rem;line-height:1.6;">' . $lis . '</ul>';
|
||||
}
|
||||
|
||||
function crField(string $label, string $value): string {
|
||||
if ($value === '') return '';
|
||||
return '<p><strong>' . htmlspecialchars($label, ENT_QUOTES, 'UTF-8') . ':</strong> '
|
||||
. htmlspecialchars($value, ENT_QUOTES, 'UTF-8') . '</p>';
|
||||
}
|
||||
?><!doctype html>
|
||||
<html lang="<?= htmlspecialchars($uiLang) ?>">
|
||||
<head>
|
||||
@@ -159,11 +176,136 @@ foreach (['draft', 'response', 'answer', 'text', 'summary', 'markdown'] as $k) {
|
||||
|
||||
<section class="cr-section">
|
||||
<h2>Resultat</h2>
|
||||
<?php if ($primaryOutput !== ''): ?>
|
||||
<div class="cr-output"><?= htmlspecialchars($primaryOutput) ?></div>
|
||||
|
||||
<?php if ($toolSlug === 'summarize'): ?>
|
||||
<?php if (!empty($output['what_we_found'])): ?>
|
||||
<p style="line-height:1.7;"><?= htmlspecialchars((string)$output['what_we_found']) ?></p>
|
||||
<?php endif; ?>
|
||||
<?= crListBlock('Key Facts', (array)($output['key_facts'] ?? [])) ?>
|
||||
<?= crListBlock('Dates', (array)($output['dates'] ?? [])) ?>
|
||||
<?= crListBlock('Parties', (array)($output['parties'] ?? [])) ?>
|
||||
<?= crListBlock('Legal References', (array)($output['legal_references_detected'] ?? [])) ?>
|
||||
<?= crListBlock('What Remains Uncertain', (array)($output['what_remains_uncertain'] ?? [])) ?>
|
||||
<?php if (!empty($output['next_practical_step'])): ?>
|
||||
<h3 style="font-family:'Crimson Pro',serif;font-size:1.05rem;color:#00205B;margin:1rem 0 0.35rem;">Next Practical Step</h3>
|
||||
<p><?= htmlspecialchars((string)$output['next_practical_step']) ?></p>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php elseif ($toolSlug === 'ask'): ?>
|
||||
<?php if (!empty($output['answer'])): ?>
|
||||
<div class="cr-output" style="margin-bottom:1rem;"><?= htmlspecialchars((string)$output['answer']) ?></div>
|
||||
<?php endif; ?>
|
||||
<?php if (!empty($output['citation_notes']) && is_array($output['citation_notes'])): ?>
|
||||
<h3 style="font-family:'Crimson Pro',serif;font-size:1.05rem;color:#00205B;margin:1rem 0 0.5rem;">Citations</h3>
|
||||
<?php foreach ($output['citation_notes'] as $cite): if (!is_array($cite)) continue; ?>
|
||||
<div style="border-left:3px solid #d8dde7;padding:0.4rem 0.75rem;margin-bottom:0.6rem;">
|
||||
<?php if (!empty($cite['title'])): ?><strong><?= htmlspecialchars((string)$cite['title']) ?></strong><br><?php endif; ?>
|
||||
<?php if (!empty($cite['citation'])): ?><em style="font-size:0.85rem;color:#667085;"><?= htmlspecialchars((string)$cite['citation']) ?></em><br><?php endif; ?>
|
||||
<?php if (!empty($cite['why_it_matters'])): ?><span style="font-size:0.9rem;"><?= htmlspecialchars((string)$cite['why_it_matters']) ?></span><?php endif; ?>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
<?= crListBlock('What Remains Uncertain', (array)($output['what_remains_uncertain'] ?? [])) ?>
|
||||
|
||||
<?php elseif ($toolSlug === 'redact'): ?>
|
||||
<?php if (!empty($output['redacted_text'])): ?>
|
||||
<pre class="cr-output" style="white-space:pre-wrap;background:#f9fafb;padding:1rem;border-radius:6px;max-height:500px;overflow-y:auto;"><?= htmlspecialchars((string)$output['redacted_text']) ?></pre>
|
||||
<?php endif; ?>
|
||||
<?php if (!empty($output['entity_counts']) && is_array($output['entity_counts'])): ?>
|
||||
<h3 style="font-family:'Crimson Pro',serif;font-size:1.05rem;color:#00205B;margin:1rem 0 0.5rem;">Entities Redacted</h3>
|
||||
<table style="border-collapse:collapse;font-size:0.9rem;width:auto;">
|
||||
<?php foreach ($output['entity_counts'] as $cat => $count): ?>
|
||||
<tr>
|
||||
<td style="padding:0.2rem 1rem 0.2rem 0;font-weight:600;"><?= htmlspecialchars((string)$cat) ?></td>
|
||||
<td style="padding:0.2rem 0;color:#374151;"><?= (int)$count ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</table>
|
||||
<?php endif; ?>
|
||||
<?php if (!empty($output['redaction_map'])): ?>
|
||||
<details class="cr-collapse" style="margin-top:1rem;">
|
||||
<summary>Redaction Map</summary>
|
||||
<pre><?= htmlspecialchars(json_encode($output['redaction_map'], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE) ?: '{}') ?></pre>
|
||||
</details>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php elseif ($toolSlug === 'transcribe'): ?>
|
||||
<?php
|
||||
$speakerRoles = is_array($output['speaker_roles'] ?? null) ? $output['speaker_roles'] : [];
|
||||
$segments = is_array($output['segments'] ?? null) ? $output['segments'] : [];
|
||||
?>
|
||||
<?= crField('Engine', (string)($output['model'] ?? '')) ?>
|
||||
<?= crField('Language', strtoupper((string)($output['language'] ?? ''))) ?>
|
||||
<?php if (!empty($output['duration_sec'])): ?>
|
||||
<?php $dur = (int)$output['duration_sec']; $m = intdiv($dur, 60); $s = $dur % 60; ?>
|
||||
<?= crField('Duration', ($m > 0 ? "{$m}m " : '') . "{$s}s") ?>
|
||||
<?php endif; ?>
|
||||
<?php if (!empty($speakerRoles)): ?>
|
||||
<p><strong>Speakers:</strong> <?= implode(', ', array_map(
|
||||
fn($id, $role) => htmlspecialchars("$id: $role", ENT_QUOTES, 'UTF-8'),
|
||||
array_keys($speakerRoles), array_values($speakerRoles)
|
||||
)) ?></p>
|
||||
<?php endif; ?>
|
||||
<?php if (!empty($output['transcript'])): ?>
|
||||
<div class="cr-output" style="max-height:420px;overflow-y:auto;background:#f9fafb;padding:1rem;border-radius:6px;margin-top:0.75rem;white-space:pre-wrap;"><?= htmlspecialchars((string)$output['transcript']) ?></div>
|
||||
<?php endif; ?>
|
||||
<?php if (!empty($segments)): ?>
|
||||
<details class="cr-collapse" style="margin-top:1rem;">
|
||||
<summary>Segments (<?= count($segments) ?>)</summary>
|
||||
<div style="overflow-x:auto;max-height:400px;overflow-y:auto;">
|
||||
<table style="border-collapse:collapse;font-size:0.83rem;width:100%;">
|
||||
<?php foreach ($segments as $seg): if (!is_array($seg)) continue; ?>
|
||||
<tr style="border-bottom:1px solid #f3f4f6;">
|
||||
<td style="padding:0.3rem 0.7rem 0.3rem 0;white-space:nowrap;color:#6b7280;font-family:'JetBrains Mono',monospace;"><?= number_format((float)($seg['start'] ?? 0), 1) ?>s</td>
|
||||
<?php if (!empty($seg['speaker'])): ?>
|
||||
<td style="padding:0.3rem 0.7rem;white-space:nowrap;font-weight:600;color:#374151;"><?= htmlspecialchars((string)$seg['speaker']) ?></td>
|
||||
<?php endif; ?>
|
||||
<td style="padding:0.3rem 0;"><?= htmlspecialchars((string)($seg['text'] ?? '')) ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</table>
|
||||
</div>
|
||||
</details>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php elseif ($toolSlug === 'timeline'): ?>
|
||||
<?php if (!empty($output['what_we_found'])): ?>
|
||||
<p style="line-height:1.7;"><?= htmlspecialchars((string)$output['what_we_found']) ?></p>
|
||||
<?php endif; ?>
|
||||
<?php if (!empty($output['events']) && is_array($output['events'])): ?>
|
||||
<div style="overflow-x:auto;margin-top:0.75rem;">
|
||||
<table style="border-collapse:collapse;width:100%;font-size:0.88rem;">
|
||||
<thead><tr style="border-bottom:2px solid #e5e7eb;text-align:left;">
|
||||
<th style="padding:0.4rem 0.75rem 0.4rem 0;">Date</th>
|
||||
<th style="padding:0.4rem 0.75rem;">Actor</th>
|
||||
<th style="padding:0.4rem 0.75rem;">Event</th>
|
||||
<th style="padding:0.4rem 0 0.4rem 0.75rem;">Confidence</th>
|
||||
</tr></thead>
|
||||
<tbody>
|
||||
<?php foreach ($output['events'] as $ev): if (!is_array($ev)) continue; ?>
|
||||
<tr style="border-bottom:1px solid #f3f4f6;">
|
||||
<td style="padding:0.4rem 0.75rem 0.4rem 0;white-space:nowrap;font-family:'JetBrains Mono',monospace;font-size:0.82rem;"><?= htmlspecialchars((string)($ev['date'] ?? '—')) ?></td>
|
||||
<td style="padding:0.4rem 0.75rem;color:#374151;"><?= htmlspecialchars((string)($ev['actor'] ?? '')) ?></td>
|
||||
<td style="padding:0.4rem 0.75rem;"><?= htmlspecialchars((string)($ev['event'] ?? '')) ?></td>
|
||||
<td style="padding:0.4rem 0 0.4rem 0.75rem;">
|
||||
<span style="font-size:0.78rem;padding:1px 6px;border-radius:999px;background:<?= ($ev['confidence'] ?? '') === 'high' ? '#dcfce7' : (($ev['confidence'] ?? '') === 'medium' ? '#fef9c3' : '#fee2e2') ?>;color:#374151;"><?= htmlspecialchars((string)($ev['confidence'] ?? '')) ?></span>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?= crListBlock('What Remains Uncertain', (array)($output['what_remains_uncertain'] ?? [])) ?>
|
||||
|
||||
<?php else: ?>
|
||||
<p class="cr-output-empty">Dette verktøyet returnerer strukturert output — se rådata under.</p>
|
||||
<?php if ($primaryOutput !== ''): ?>
|
||||
<div class="cr-output"><?= htmlspecialchars($primaryOutput) ?></div>
|
||||
<?php else: ?>
|
||||
<p class="cr-output-empty">Dette verktøyet returnerer strukturert output — se rådata under.</p>
|
||||
<?php endif; ?>
|
||||
<?php endif; ?>
|
||||
|
||||
<details class="cr-collapse" style="margin-top:1.25rem;">
|
||||
<summary>Vis rådata (JSON)</summary>
|
||||
<pre><?= htmlspecialchars(json_encode($output, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?: '{}') ?></pre>
|
||||
@@ -235,6 +377,10 @@ foreach (['draft', 'response', 'answer', 'text', 'summary', 'markdown'] as $k) {
|
||||
'deep-research': '/deep-research.php',
|
||||
'discrepancy': '/discrepancy.php',
|
||||
'timeline': '/timeline.php',
|
||||
'summarize': '/summarize.php',
|
||||
'ask': '/ask.php',
|
||||
'redact': '/redact.php',
|
||||
'transcribe': '/transcribe.php',
|
||||
}[tool] || '/dashboard.php';
|
||||
window.location.href = path + '?rerun=' + <?= (int)$result['id'] ?>;
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user