feat(timeline): add live filter, actor chips, group headers, copy button, source toggle, count badge

- Live search/filter bar: filters events by keyword across event, actor, source_excerpt, date
- Actor filter chips: click to filter by actor, multi-select, teal active state
- Year/month group headers when sorted chronologically (── 2023 ──, Mar 2024 ──)
- Per-event copy button (hover-revealed 📋): copies "date · actor · event" to clipboard
- "Hide/show sources" toggle: collapses all source excerpts without re-rendering
- Count badge: "23 events · 3 actors · 2022–2025" above the list
- applyTimelineFilters() unifies sort + actor + text filters in one re-render pass
- CSV export now includes end_date column
- Reset all filter state on each new run

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-18 15:46:59 +02:00
parent 59b39ff85b
commit ffcf887428
7 changed files with 807 additions and 49 deletions
+65 -11
View File
@@ -33,7 +33,8 @@ final class DbnDeepResearchAgent
?callable $emit = null,
string $advocateRole = '',
?array $priorContext = null,
string $branchNotes = ''
string $branchNotes = '',
array $subQuestionsOverride = []
): array {
$seedQuery = trim($seedQuery);
$pastedText = trim($pastedText);
@@ -88,17 +89,26 @@ final class DbnDeepResearchAgent
$this->stepTimings['interpretation'] = $this->elapsedMs($stepStart);
$emitStep('interpretation', 'Query interpretation', $interpretation['detail'], 'complete');
// STEP 2: Query expansion
$emitRunning('expansion', 'Query expansion', 'Generating sub-questions…');
// STEP 2: Query expansion (or use caller-supplied override)
$stepStart = microtime(true);
$expansion = $this->expandQueries($seedDescription, $interpretation['brief'], $interpretation['key_signals'], $controls['sub_q_count'], $language, $advocateRole);
$this->stepTimings['expansion'] = $this->elapsedMs($stepStart);
$subQuestions = $expansion['questions'];
$expansionStatus = $expansion['fallback'] ? 'warning' : 'complete';
$expansionDetail = $expansion['fallback']
? 'Could not parse sub-questions; falling back to retrieving on the seed query alone.'
: sprintf('Generated %d sub-questions to research the corpus from multiple angles.', count($subQuestions));
$emitStep('expansion', 'Query expansion', $expansionDetail, $expansionStatus);
if (!empty($subQuestionsOverride)) {
$subQuestions = array_values(array_filter($subQuestionsOverride, fn($sq) =>
is_array($sq) && !empty(trim((string)($sq['question'] ?? '')))
));
$this->stepTimings['expansion'] = $this->elapsedMs($stepStart);
$emitStep('expansion', 'Query expansion',
sprintf('Using %d custom sub-question(s) supplied by the user.', count($subQuestions)), 'complete');
} else {
$emitRunning('expansion', 'Query expansion', 'Generating sub-questions…');
$expansion = $this->expandQueries($seedDescription, $interpretation['brief'], $interpretation['key_signals'], $controls['sub_q_count'], $language, $advocateRole);
$this->stepTimings['expansion'] = $this->elapsedMs($stepStart);
$subQuestions = $expansion['questions'];
$expansionStatus = $expansion['fallback'] ? 'warning' : 'complete';
$expansionDetail = $expansion['fallback']
? 'Could not parse sub-questions; falling back to retrieving on the seed query alone.'
: sprintf('Generated %d sub-questions to research the corpus from multiple angles.', count($subQuestions));
$emitStep('expansion', 'Query expansion', $expansionDetail, $expansionStatus);
}
// STEP 3: Slice resolution
$emitRunning('slice_resolution', 'Slice resolution', 'Resolving slice toggles to document IDs…');
@@ -1164,6 +1174,50 @@ PROMPT;
return 'low';
}
public function generateSubQPreview(
string $seedQuery,
string $pastedText,
string $engine,
string $language,
array $controls,
string $advocateRole = '',
?array $priorContext = null,
string $branchNotes = ''
): array {
$seedQuery = trim($seedQuery);
$pastedText = trim($pastedText);
$engine = in_array($engine, ['azure_mini', 'azure_full', 'gpu'], true) ? $engine : 'azure_mini';
$language = dbnToolsNormalizeUiLanguage($language);
$controls = $this->normalizeControls($controls);
if ($seedQuery === '' && $pastedText === '') {
dbnToolsAbort('Provide a question or pasted text.', 422, 'missing_seed');
}
dbnToolsRequireClient();
dbnToolsBootCaveau();
$aiPortalRoot = dbnToolsAiPortalRoot();
require_once $aiPortalRoot . '/platform/includes/dbn_v6.php';
$seedDescription = $this->buildSeedDescription($seedQuery, $pastedText, []);
$interpretation = $this->interpretSeed($seedDescription, $language, $advocateRole, $priorContext, $branchNotes);
$expansion = $this->expandQueries(
$seedDescription,
$interpretation['brief'],
$interpretation['key_signals'],
$controls['sub_q_count'],
$language,
$advocateRole
);
return [
'ok' => true,
'interpretation' => $interpretation,
'sub_questions' => $expansion['questions'],
'fallback' => $expansion['fallback'] ?? false,
];
}
private function trace(string $label, string $detail, string $status = 'complete'): array
{
return [