From 2013648ee0b7f238b3ff1e7088a2c7477e60a715 Mon Sep 17 00:00:00 2001 From: davegilligan Date: Sun, 24 May 2026 01:27:26 +0200 Subject: [PATCH] =?UTF-8?q?Add=20manual=20'Save=20result'=20to=20all=20too?= =?UTF-8?q?ls=20=E2=80=94=20replaces=20auto-save?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- api/barnevernet.php | 14 ---- api/case/save-result.php | 54 ++++++++++++++ api/deep-research.php | 15 ---- api/discrepancy.php | 14 ---- api/korrespond.php | 15 ---- api/timeline.php | 15 ---- assets/css/tools.css | 94 +++++++++++++++++++++++ assets/js/barnevernet.js | 8 ++ assets/js/summarize.js | 11 +++ assets/js/tools.js | 135 +++++++++++++++++++++++++++++++++ case-result.php | 156 +++++++++++++++++++++++++++++++++++++-- includes/CaseResults.php | 22 +++++- 12 files changed, 473 insertions(+), 80 deletions(-) create mode 100644 api/case/save-result.php diff --git a/api/barnevernet.php b/api/barnevernet.php index efe93f3..4099ffc 100644 --- a/api/barnevernet.php +++ b/api/barnevernet.php @@ -3,7 +3,6 @@ declare(strict_types=1); require_once __DIR__ . '/../includes/bootstrap.php'; require_once __DIR__ . '/../includes/BvjAnalyzerAgent.php'; -require_once __DIR__ . '/../includes/CaseResults.php'; require_once __DIR__ . '/../includes/ToolModels.php'; dbnToolsRequireMethod('POST'); @@ -151,19 +150,6 @@ try { 'bvj_doc_type' => $result['doc_meta']['doc_type'] ?? null, ]); - if ($ftUid > 0) { - $ownerId = CaseStore::caseResolveClientId($ftUid); - $resultId = CaseResults::save($ftUid, $ownerId, 'barnevernet', $input, $result, [ - 'used_case_context' => $useMyCase ? 1 : 0, - 'case_doc_ids' => dbnToolsLastCaseDocIds(), - 'model' => $result['trace_metadata']['deployment'] ?? $engine, - 'latency_ms' => $result['latency_ms'], - 'credits_charged' => FreeTier::cost('barnevernet'), - ]); - if ($resultId > 0) { - $result['result_id'] = $resultId; - } - } $emit('final', ['result' => $result]); diff --git a/api/case/save-result.php b/api/case/save-result.php new file mode 100644 index 0000000..d291968 --- /dev/null +++ b/api/case/save-result.php @@ -0,0 +1,54 @@ + true, 'result_id' => $resultId]); diff --git a/api/deep-research.php b/api/deep-research.php index 086ac70..135bd82 100644 --- a/api/deep-research.php +++ b/api/deep-research.php @@ -3,7 +3,6 @@ declare(strict_types=1); require_once __DIR__ . '/../includes/bootstrap.php'; require_once __DIR__ . '/../includes/DeepResearchAgent.php'; -require_once __DIR__ . '/../includes/CaseResults.php'; require_once __DIR__ . '/../includes/ToolModels.php'; dbnToolsRequireMethod('POST'); @@ -156,20 +155,6 @@ try { 'advocate_role' => $advocateRole !== '' ? $advocateRole : null, ]); - if ($ftUid > 0) { - $toolSlug = $advocateRole !== '' ? 'advocate' : 'deep-research'; - $ownerId = CaseStore::caseResolveClientId($ftUid); - $resultId = CaseResults::save($ftUid, $ownerId, $toolSlug, $input, $result, [ - 'used_case_context' => $useMyCase ? 1 : 0, - 'case_doc_ids' => dbnToolsLastCaseDocIds(), - 'model' => $result['trace_metadata']['deployment'] ?? $engine, - 'latency_ms' => $result['latency_ms'], - 'credits_charged' => FreeTier::cost($toolSlug), - ]); - if ($resultId > 0) { - $result['result_id'] = $resultId; - } - } $emit('final', ['result' => $result]); diff --git a/api/discrepancy.php b/api/discrepancy.php index 46f1045..135e517 100644 --- a/api/discrepancy.php +++ b/api/discrepancy.php @@ -3,7 +3,6 @@ declare(strict_types=1); require_once __DIR__ . '/../includes/bootstrap.php'; require_once __DIR__ . '/../includes/DiscrepancyAgent.php'; -require_once __DIR__ . '/../includes/CaseResults.php'; require_once __DIR__ . '/../includes/ToolModels.php'; dbnToolsRequireMethod('POST'); @@ -147,19 +146,6 @@ try { 'deployment' => $result['trace_metadata']['deployment'] ?? null, ]); - if ($ftUid > 0) { - $ownerId = CaseStore::caseResolveClientId($ftUid); - $resultId = CaseResults::save($ftUid, $ownerId, 'discrepancy', $input, $result, [ - 'used_case_context' => $useMyCase ? 1 : 0, - 'case_doc_ids' => dbnToolsLastCaseDocIds(), - 'model' => $result['trace_metadata']['deployment'] ?? $engine, - 'latency_ms' => $result['latency_ms'], - 'credits_charged' => FreeTier::cost('discrepancy'), - ]); - if ($resultId > 0) { - $result['result_id'] = $resultId; - } - } $emit('final', ['result' => $result]); diff --git a/api/korrespond.php b/api/korrespond.php index b0982e5..c85df0b 100644 --- a/api/korrespond.php +++ b/api/korrespond.php @@ -3,7 +3,6 @@ declare(strict_types=1); require_once __DIR__ . '/../includes/bootstrap.php'; require_once __DIR__ . '/../includes/KorrespondAgent.php'; -require_once __DIR__ . '/../includes/CaseResults.php'; dbnToolsRequireMethod('POST'); dbnToolsRequireAuth(); @@ -185,20 +184,6 @@ try { 'deployment' => 'gpt-4o', ]); - // Premium: persist the run for paid (Plus/Pro) users so it shows up in Min Sak → Saved analyses. - if ($ftUid > 0) { - $ownerId = CaseStore::caseResolveClientId($ftUid); - $resultId = CaseResults::save($ftUid, $ownerId, 'korrespond', $input, $result, [ - 'used_case_context' => !empty($intake['use_my_case']) ? 1 : 0, - 'case_doc_ids' => dbnToolsLastCaseDocIds(), - 'model' => 'gpt-4o', - 'latency_ms' => $result['latency_ms'], - 'credits_charged' => FreeTier::cost('korrespond'), - ]); - if ($resultId > 0) { - $result['result_id'] = $resultId; - } - } $emit('final', ['result' => $result]); diff --git a/api/timeline.php b/api/timeline.php index 7e92137..041da65 100644 --- a/api/timeline.php +++ b/api/timeline.php @@ -2,7 +2,6 @@ declare(strict_types=1); require_once __DIR__ . '/../includes/LegalTools.php'; -require_once __DIR__ . '/../includes/CaseResults.php'; require_once __DIR__ . '/../includes/ToolModels.php'; dbnToolsRequireMethod('POST'); @@ -47,19 +46,5 @@ dbnToolsWithTelemetry('timeline', $language, function () use ($input, $language, $result = (new DbnLegalToolsService())->timeline($text, $language, $engine, $focus, $confidenceFilter, $includeRelative, $includeBackground, $userNotes); - // Persist for paid users (silently no-op for free) - if ($ftUid > 0) { - $ownerId = CaseStore::caseResolveClientId($ftUid); - $resultId = CaseResults::save($ftUid, $ownerId, 'timeline', $input, $result, [ - 'used_case_context' => $useMyCase ? 1 : 0, - 'case_doc_ids' => dbnToolsLastCaseDocIds(), - 'model' => $engine, - 'latency_ms' => (int)($result['latency_ms'] ?? 0), - 'credits_charged' => FreeTier::cost('timeline'), - ]); - if ($resultId > 0) { - $result['result_id'] = $resultId; - } - } return $result; }); diff --git a/assets/css/tools.css b/assets/css/tools.css index 61e168c..4e6b7c4 100644 --- a/assets/css/tools.css +++ b/assets/css/tools.css @@ -9173,3 +9173,97 @@ body.lt-landing { .account-survey-cta span { font-size: 0.8rem; opacity: 0.85; } .account-survey-cta:hover { background: #fde68a; } + +/* ── Save result widget ────────────────────────────────────────────────────── */ +.save-result-widget { + display: flex; + align-items: center; + gap: 0.5rem; + padding: 0.55rem 0.75rem; + margin-bottom: 1rem; + background: #f0f4ff; + border: 1px solid #c7d3f5; + border-radius: 8px; + font-size: 0.85rem; +} + +.save-result-btn { + background: none; + border: 1px solid var(--dbn-blue, #00205b); + color: var(--dbn-blue, #00205b); + border-radius: 6px; + padding: 0.3rem 0.75rem; + font-size: 0.82rem; + font-weight: 500; + cursor: pointer; + transition: background 0.15s, color 0.15s; + white-space: nowrap; +} +.save-result-btn:hover { + background: var(--dbn-blue, #00205b); + color: #fff; +} + +.save-result-prompt { + display: flex; + align-items: center; + gap: 0.4rem; + width: 100%; + flex-wrap: wrap; +} + +.save-result-input { + flex: 1 1 240px; + min-width: 0; + border: 1px solid var(--line, #d8dde7); + border-radius: 6px; + padding: 0.3rem 0.6rem; + font-size: 0.82rem; + color: var(--dbn-ink, #16130f); + background: #fff; +} +.save-result-input:focus { + outline: 2px solid var(--dbn-blue, #00205b); + outline-offset: 1px; +} + +.save-result-confirm { + background: var(--dbn-blue, #00205b); + color: #fff; + border: none; + border-radius: 6px; + padding: 0.3rem 0.75rem; + font-size: 0.82rem; + font-weight: 600; + cursor: pointer; +} +.save-result-confirm:disabled { opacity: 0.55; cursor: default; } + +.save-result-cancel { + background: none; + border: 1px solid var(--line, #d8dde7); + border-radius: 6px; + padding: 0.3rem 0.6rem; + font-size: 0.82rem; + cursor: pointer; + color: var(--muted, #667085); +} +.save-result-cancel:hover { border-color: #aaa; } + +.save-result-done { + width: 100%; +} +.save-result-link { + color: var(--dbn-teal, #0f766e); + font-weight: 600; + text-decoration: none; + font-size: 0.85rem; +} +.save-result-link:hover { text-decoration: underline; } + +.save-result-error { + color: var(--coral, #c2410c); + margin: 0; + font-size: 0.82rem; + width: 100%; +} diff --git a/assets/js/barnevernet.js b/assets/js/barnevernet.js index db51b67..d92a3d3 100644 --- a/assets/js/barnevernet.js +++ b/assets/js/barnevernet.js @@ -570,6 +570,14 @@ els.runButton.disabled = false; renderTrace(finalResult.trace || []); renderFinalResults(finalResult); + if (typeof window.dbnShowSaveResultButton === 'function') { + window.dbnShowSaveResultButton( + finalResult.tool || 'barnevernet', + payload, + finalResult, + { model: (finalResult.trace_metadata || {}).deployment || null, latency_ms: finalResult.latency_ms || 0 } + ); + } } // ── Progressive rendering (renders as stream events arrive) ──────────────── diff --git a/assets/js/summarize.js b/assets/js/summarize.js index f5e71ad..9107543 100644 --- a/assets/js/summarize.js +++ b/assets/js/summarize.js @@ -25,6 +25,7 @@ // ── State ───────────────────────────────────────────────────────────────── var _extractedFiles = []; // [{ name, text, chars }] var _currentLang = 'en'; + var _lastPayload = null; // ── Lang switcher ───────────────────────────────────────────────────────── document.querySelectorAll('.sum-lang-btn').forEach(function (btn) { @@ -177,6 +178,7 @@ slices: slices, }; if (docIds.length) payload.doc_ids = docIds; + _lastPayload = payload; setBusy(true); setStatus('Running…'); @@ -218,6 +220,15 @@ setStatus(data.detail || ''); } else if (data.event === 'final') { renderFinal(data); + if (typeof window.dbnShowSaveResultButton === 'function') { + window.dbnShowSaveResultButton( + 'summarize', + _lastPayload || {}, + data, + { model: data.engine || null, latency_ms: data.latency_ms || 0 }, + resultsEl + ); + } if (data.balance != null) { var credEl = document.getElementById('creditsRemaining'); if (credEl) credEl.textContent = data.balance; diff --git a/assets/js/tools.js b/assets/js/tools.js index 1a38beb..3ed5b40 100644 --- a/assets/js/tools.js +++ b/assets/js/tools.js @@ -401,6 +401,7 @@ let lastRedactedText = null; let lastOriginalText = ''; let lastRedactPayload = null; let lastRunEngine = null; +let lastToolPayload = null; const VOCAB_PRESETS = { barnerett: 'Barnevernet, Fylkesnemnda, barnevernloven, barneloven, barnets beste, samvær, foreldreansvar, omsorgsovertakelse, sakkyndig, advokat, prosessfullmektig, dommer, vitne, tolk, bistandsadvokat, fosterforeldre, fosterhjem, akuttvedtak, statsforvalter, Bufetat, saksbehandler, rettslig medhold, begjæring, samtykke, tilsynsfører', @@ -1110,6 +1111,8 @@ async function runTool(event) { payload.use_my_case = (typeof window.dbnGetUseMyCase === 'function') ? window.dbnGetUseMyCase() : false; } + lastToolPayload = { ...payload }; + setBusy(true); renderTrace([ { label: 'Query interpretation', detail: 'Preparing request.', status: 'running' }, @@ -1123,6 +1126,12 @@ async function runTool(event) { renderResults(data); renderTrace(data.trace || []); els.status.textContent = `Done in ${data.latency_ms || 0} ms.`; + if (['ask', 'redact', 'timeline'].includes(state.activeTool)) { + showSaveResultButton(state.activeTool, lastToolPayload, data, { + model: data.trace_metadata?.deployment || null, + latency_ms: data.latency_ms || 0, + }); + } } catch (error) { els.status.textContent = error.message; renderTrace([ @@ -1846,6 +1855,10 @@ async function runTranscribe() { if (!resp.ok || !data.ok) throw new Error(data.error?.message || 'Transcription failed.'); if (typeof renderTranscribeResult === 'function') renderTranscribeResult(data); else renderResults(data); + showSaveResultButton('transcribe', { audio_doc_id: storedAudioDocId }, data, { + model: data.model || null, + latency_ms: data.latency_ms || 0, + }); els.status.textContent = 'Done.'; } catch (err) { els.status.textContent = err.message; @@ -1976,6 +1989,10 @@ async function runTranscribe() { lastTranscriptData = merged; renderTranscriptResults(merged); + showSaveResultButton('transcribe', lastToolPayload || {}, merged, { + model: merged.model || null, + latency_ms: merged.latency_ms || 0, + }); const totalSec = Math.round(cumulativeOffset); const totalMin = Math.floor(totalSec / 60); @@ -2417,6 +2434,124 @@ function escapeHtml(value) { // ── Free-tier credit badge ──────────────────────────────────────────────── +// ── Save result widget ───────────────────────────────────────────────────── + +const _SAVEABLE_TOOL_LABELS = { + ask: 'Spørsmål & svar', + redact: 'Anonymisering', + timeline: 'Tidslinje', + transcribe: 'Transkripsjon', + summarize: 'Sammendrag', + barnevernet: 'BVJ-analyse', + 'deep-research': 'Dyp analyse', + advocate: 'Advokatutkast', + discrepancy: 'Motstrid', + korrespond: 'Korrespondanse', +}; + +function _isSaveEligibleUser() { + const tier = window.DBN_USER_TIER; + return typeof tier === 'string' && !['free', 'caveau'].includes(tier); +} + +function showSaveResultButton(tool, inputPayload, outputPayload, meta, containerEl) { + if (!_isSaveEligibleUser()) return; + if (!Object.prototype.hasOwnProperty.call(_SAVEABLE_TOOL_LABELS, tool)) return; + + document.getElementById('saveResultWidget')?.remove(); + + const label = _SAVEABLE_TOOL_LABELS[tool] || tool; + const now = new Date(); + const dateStr = now.toLocaleDateString('no-NO', { day: 'numeric', month: 'short', year: 'numeric' }); + const timeStr = now.toLocaleTimeString('no-NO', { hour: '2-digit', minute: '2-digit' }); + const dfTitle = `${label} — ${dateStr} ${timeStr}`; + + const widget = document.createElement('div'); + widget.id = 'saveResultWidget'; + widget.className = 'save-result-widget'; + widget.innerHTML = ` +
+ +
+ + + `; + + const resultsEl = containerEl || document.getElementById('results'); + if (!resultsEl) return; + resultsEl.insertBefore(widget, resultsEl.firstChild); + + // Set value after inserting so escaping is handled by the DOM + widget.querySelector('#saveResultTitle').value = dfTitle; + + const triggerBtn = widget.querySelector('#saveResultTrigger'); + const promptDiv = widget.querySelector('.save-result-prompt'); + const idleDiv = widget.querySelector('.save-result-idle'); + const doneDiv = widget.querySelector('.save-result-done'); + const errorP = widget.querySelector('.save-result-error'); + const titleInput = widget.querySelector('#saveResultTitle'); + const confirmBtn = widget.querySelector('#saveResultConfirm'); + const cancelBtn = widget.querySelector('#saveResultCancel'); + + triggerBtn.addEventListener('click', () => { + idleDiv.classList.add('is-hidden'); + promptDiv.classList.remove('is-hidden'); + titleInput.focus(); + titleInput.select(); + }); + + cancelBtn.addEventListener('click', () => { + promptDiv.classList.add('is-hidden'); + idleDiv.classList.remove('is-hidden'); + }); + + titleInput.addEventListener('keydown', (e) => { + if (e.key === 'Enter') confirmBtn.click(); + if (e.key === 'Escape') cancelBtn.click(); + }); + + confirmBtn.addEventListener('click', async () => { + const title = titleInput.value.trim(); + if (!title) { titleInput.focus(); return; } + + confirmBtn.disabled = true; + confirmBtn.textContent = 'Saving…'; + errorP.classList.add('is-hidden'); + + try { + const resp = await fetch('api/case/save-result.php', { + method: 'POST', + credentials: 'same-origin', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + tool, + title, + input_payload: inputPayload || {}, + output_payload: outputPayload || {}, + meta: meta || {}, + }), + }); + const data = await resp.json().catch(() => ({})); + if (!resp.ok || !data.ok) throw new Error(data.error?.message || 'Save failed.'); + promptDiv.classList.add('is-hidden'); + doneDiv.classList.remove('is-hidden'); + } catch (err) { + confirmBtn.disabled = false; + confirmBtn.textContent = 'Save'; + errorP.textContent = err.message; + errorP.classList.remove('is-hidden'); + } + }); +} + +window.dbnShowSaveResultButton = showSaveResultButton; + let _freeTierBalance = (typeof window.DBN_FREE_TIER_BALANCE === 'number') ? window.DBN_FREE_TIER_BALANCE : -1; function dbnUpdateCredits(balance) { diff --git a/case-result.php b/case-result.php index 29d5ad3..8b086f1 100644 --- a/case-result.php +++ b/case-result.php @@ -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) => '
  • ' . htmlspecialchars((string)$v, ENT_QUOTES, 'UTF-8') . '
  • ', $items)); + return '

    ' + . htmlspecialchars($heading, ENT_QUOTES, 'UTF-8') + . '

    '; +} + +function crField(string $label, string $value): string { + if ($value === '') return ''; + return '

    ' . htmlspecialchars($label, ENT_QUOTES, 'UTF-8') . ': ' + . htmlspecialchars($value, ENT_QUOTES, 'UTF-8') . '

    '; +} ?> @@ -159,11 +176,136 @@ foreach (['draft', 'response', 'answer', 'text', 'summary', 'markdown'] as $k) {

    Resultat

    - -
    + + + +

    + + + + + + + +

    Next Practical Step

    +

    + + + + +
    + + +

    Citations

    + +
    +
    +
    + +
    + + + + + + +
    + + +

    Entities Redacted

    + + $count): ?> + + + + + +
    + + +
    + Redaction Map +
    +
    + + + + + + + + + 0 ? "{$m}m " : '') . "{$s}s") ?> + + +

    Speakers: htmlspecialchars("$id: $role", ENT_QUOTES, 'UTF-8'), + array_keys($speakerRoles), array_values($speakerRoles) + )) ?>

    + + +
    + + +
    + Segments () +
    + + + + + + + + + + +
    s
    +
    +
    + + + + +

    + + +
    + + + + + + + + + + + + + + + + + +
    DateActorEventConfidence
    + +
    +
    + + + -

    Dette verktøyet returnerer strukturert output — se rådata under.

    + +
    + +

    Dette verktøyet returnerer strukturert output — se rådata under.

    + +
    Vis rådata (JSON)
    @@ -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=' + ; }); diff --git a/includes/CaseResults.php b/includes/CaseResults.php index da22501..f4c2899 100644 --- a/includes/CaseResults.php +++ b/includes/CaseResults.php @@ -29,6 +29,10 @@ final class CaseResults 'deep-research', 'discrepancy', 'timeline', + 'summarize', + 'ask', + 'redact', + 'transcribe', ]; /** True when the user is on a tier that gets saved results (Plus, Pro, or active Plus trial). */ @@ -71,8 +75,10 @@ final class CaseResults return 0; } - // Title default: first 80 chars of the most descriptive input field. - $title = self::deriveTitle($tool, $input); + // Caller may supply an explicit title (e.g. from a manual "Save result" prompt). + $title = isset($meta['title']) && trim((string)$meta['title']) !== '' + ? mb_substr(trim((string)$meta['title']), 0, 200, 'UTF-8') + : self::deriveTitle($tool, $input); $caseDocIds = $meta['case_doc_ids'] ?? []; if (!is_array($caseDocIds)) { @@ -228,6 +234,10 @@ final class CaseResults 'deep-research' => 'Dyp analyse', 'discrepancy' => 'Motstrid', 'timeline' => 'Tidslinje', + 'summarize' => 'Sammendrag', + 'ask' => 'Spørsmål & svar', + 'redact' => 'Anonymisering', + 'transcribe' => 'Transkripsjon', ][$tool] ?? ucfirst($tool); } @@ -241,6 +251,10 @@ final class CaseResults 'deep-research' => '🔬', 'discrepancy' => '🔍', 'timeline' => '📅', + 'summarize' => '📝', + 'ask' => '💬', + 'redact' => '🖊️', + 'transcribe' => '🎙️', ][$tool] ?? '📄'; } @@ -254,6 +268,10 @@ final class CaseResults 'deep-research' => [$input['question'] ?? null, $input['query'] ?? null, $input['topic'] ?? null], 'discrepancy' => [$input['focus'] ?? null, $input['context'] ?? null], 'timeline' => [$input['context'] ?? null, $input['text'] ?? null], + 'summarize' => [$input['text'] ?? null], + 'ask' => [$input['question'] ?? null], + 'redact' => [$input['text'] ?? null], + 'transcribe' => [$input['filename'] ?? null], default => [$input['title'] ?? null, $input['query'] ?? null, $input['text'] ?? null], }; foreach ($candidates as $c) {