Files
llm-multiverse-ui/src/routes/memory/+page.svelte
shahondin1624 2c6c961e08 feat: structured artifact rendering, UX improvements
- Render structured artifacts from agent results with type-aware
  formatting: code blocks with syntax highlighting and copy button,
  terminal-style command output, search result cards, and text findings
- Make FinalResult panel collapsible (default collapsed) with scrollable
  content (max-h-96) to prevent dominating the chat view
- Add clickable URL detection in summaries and artifact content
- Fix code block contrast for both light and dark mode
- Animate progress bar with pulse ring on active step and gradient
  shimmer on connecting lines
- Fix tab-switching bug: use module-level orchestrationStore singleton
  so orchestration state survives route navigation
- Remove sample/demo data seeding and clean up persisted localStorage
  entries from previous sample sessions
- Remove showSampleBadge prop from PageHeader
- Regenerate proto types for new Artifact message and ArtifactType enum
- Update README project structure (remove deleted data/ directory)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 23:13:33 +01:00

121 lines
4.4 KiB
Svelte

<script lang="ts">
import { resolveRoute } from '$app/paths';
import { ResultSource } from '$lib/proto/llm_multiverse/v1/common_pb';
import MemoryCandidateCard from '$lib/components/MemoryCandidateCard.svelte';
import PageHeader from '$lib/components/PageHeader.svelte';
import { memoryStore } from '$lib/stores/memory.svelte';
const chatHref = resolveRoute('/chat');
let sourceFilter = $state<ResultSource | 'all'>('all');
let confidenceThreshold = $state(0);
const allSessions = $derived(memoryStore.getAllBySession());
const filteredSessions = $derived(
allSessions
.map((session) => ({
...session,
candidates: session.candidates.filter((c) => {
if (sourceFilter !== 'all' && c.source !== sourceFilter) return false;
if (c.confidence < confidenceThreshold) return false;
return true;
})
}))
.filter((session) => session.candidates.length > 0)
);
const totalCandidates = $derived(
filteredSessions.reduce((sum, s) => sum + s.candidates.length, 0)
);
const sourceOptions: { value: ResultSource | 'all'; label: string }[] = [
{ value: 'all', label: 'All Sources' },
{ value: ResultSource.TOOL_OUTPUT, label: 'Tool Output' },
{ value: ResultSource.MODEL_KNOWLEDGE, label: 'Model Knowledge' },
{ value: ResultSource.WEB, label: 'Web' },
{ value: ResultSource.UNSPECIFIED, label: 'Unspecified' }
];
</script>
<div class="flex h-screen flex-col overflow-hidden bg-gray-50 dark:bg-gray-900">
<PageHeader title="Memory Candidates" backHref={chatHref} />
<!-- Filters -->
<div class="border-b border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-900 px-4 py-3 md:px-6">
<div class="flex flex-col gap-3 sm:flex-row sm:flex-wrap sm:items-center sm:gap-4">
<div class="flex items-center gap-2">
<label for="source-filter" class="text-xs font-medium text-gray-500 dark:text-gray-400">Source</label>
<select
id="source-filter"
bind:value={sourceFilter}
class="rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 px-2.5 py-1.5 text-sm text-gray-700 dark:text-gray-300 focus:border-blue-500 focus:ring-1 focus:ring-blue-500 focus:outline-none"
>
{#each sourceOptions as opt (opt.value)}
<option value={opt.value}>{opt.label}</option>
{/each}
</select>
</div>
<div class="flex items-center gap-2">
<label for="confidence-threshold" class="text-xs font-medium text-gray-500 dark:text-gray-400">
Min Confidence
</label>
<input
id="confidence-threshold"
type="range"
min="0"
max="1"
step="0.05"
bind:value={confidenceThreshold}
class="h-2 w-32 cursor-pointer appearance-none rounded-lg bg-gray-200 dark:bg-gray-700 accent-blue-600"
/>
<span class="w-10 text-right text-xs font-medium text-gray-600 dark:text-gray-400">
{Math.round(confidenceThreshold * 100)}%
</span>
</div>
<span class="text-xs text-gray-400 dark:text-gray-500">
{totalCandidates} candidate{totalCandidates !== 1 ? 's' : ''}
</span>
</div>
</div>
<!-- Content -->
<main class="flex-1 overflow-auto p-4 md:p-6">
{#if filteredSessions.length === 0}
<div class="flex flex-col items-center justify-center py-16 text-center">
<div class="mb-3 text-4xl text-gray-300 dark:text-gray-600">&#128203;</div>
<p class="text-sm font-medium text-gray-500 dark:text-gray-400">No memory candidates found</p>
<p class="mt-1 text-xs text-gray-400 dark:text-gray-500">
{#if sourceFilter !== 'all' || confidenceThreshold > 0}
Try adjusting your filters.
{:else}
Memory candidates will appear here as they are captured from orchestration results.
{/if}
</p>
</div>
{:else}
<div class="space-y-6">
{#each filteredSessions as session (session.sessionId)}
<section>
<div class="mb-3 flex items-center gap-3">
<h2 class="text-sm font-semibold text-gray-900 dark:text-gray-100">
Session: <span class="font-mono text-xs text-gray-600 dark:text-gray-400">{session.sessionId}</span>
</h2>
<span class="rounded-full bg-gray-100 dark:bg-gray-700 px-2 py-0.5 text-xs font-medium text-gray-600 dark:text-gray-400">
{session.candidates.length}
</span>
</div>
<div class="grid gap-3 sm:grid-cols-1 md:grid-cols-2 lg:grid-cols-3">
{#each session.candidates as candidate, i (session.sessionId + '-' + i)}
<MemoryCandidateCard {candidate} />
{/each}
</div>
</section>
{/each}
</div>
{/if}
</main>
</div>