diff --git a/implementation-plans/_index.md b/implementation-plans/_index.md index 83af8fa..41cab49 100644 --- a/implementation-plans/_index.md +++ b/implementation-plans/_index.md @@ -17,3 +17,4 @@ | #13 | Session config sidebar component | COMPLETED | [issue-013.md](issue-013.md) | | #14 | Preset configurations | COMPLETED | [issue-014.md](issue-014.md) | | #15 | Agent lineage visualization | COMPLETED | [issue-015.md](issue-015.md) | +| #16 | Memory candidates viewer | COMPLETED | [issue-016.md](issue-016.md) | diff --git a/implementation-plans/issue-016.md b/implementation-plans/issue-016.md new file mode 100644 index 0000000..0d50fdd --- /dev/null +++ b/implementation-plans/issue-016.md @@ -0,0 +1,37 @@ +--- +--- + +# Issue #16: Memory candidates viewer + +**Status:** COMPLETED +**Issue:** https://git.shahondin1624.de/llm-multiverse/llm-multiverse-ui/issues/16 +**Branch:** `feature/issue-16-memory-viewer` + +## Acceptance Criteria + +- [x] Dedicated `/memory` route +- [x] List of `MemoryCandidate` entries +- [x] Each entry shows: content, source badge, confidence score +- [x] Entries grouped by session +- [x] Confidence score visualized (progress bar with color-coding) +- [x] Filterable by source or confidence threshold + +## Implementation + +### New Files +- `src/lib/stores/memory.svelte.ts` — reactive store for memory candidates grouped by session ID, with localStorage persistence, sample/demo data, and `addCandidates()` / `getAllBySession()` / `clearSession()` API +- `src/lib/components/MemoryCandidateCard.svelte` — card component displaying a single memory candidate with content, color-coded source badge (Tool Output=blue, Model Knowledge=purple, Web=green, Unspecified=gray), and horizontal confidence progress bar with percentage +- `src/routes/memory/+page.svelte` — dedicated memory route with session-grouped layout, source dropdown filter, confidence threshold slider, empty state, and back-to-chat link +- `src/routes/memory/+page.ts` — SSR disabled for this route + +### Modified Files +- `src/routes/+page.svelte` — added Memory Candidates navigation link on the home page +- `src/routes/chat/+page.svelte` — added Memory link in the header next to Lineage; wired up capture of `finalResult.newMemoryCandidates` into the memory store + +### Key Decisions +- Uses sample/demo data seeded into localStorage on first load, since the orchestrator doesn't currently include memory candidates in typical responses +- `StoredMemoryCandidate` type decouples the UI store from protobuf Message dependency (same pattern as lineage's `SimpleAgentIdentifier`) +- Confidence bar color-coded: green (>=80%), amber (>=50%), red (<50%) +- Source badge colors match the project's existing badge conventions (blue, purple, green, gray) +- Grid layout (1-3 columns responsive) for candidate cards within each session group +- Filter controls use a source dropdown and a range slider for confidence threshold diff --git a/src/lib/components/MemoryCandidateCard.svelte b/src/lib/components/MemoryCandidateCard.svelte new file mode 100644 index 0000000..3a44179 --- /dev/null +++ b/src/lib/components/MemoryCandidateCard.svelte @@ -0,0 +1,51 @@ + + +
+
+ + {sourceBadge.label} + + + {confidencePct}% + +
+ +

{candidate.content}

+ +
+
+
+
+
+
diff --git a/src/lib/stores/memory.svelte.ts b/src/lib/stores/memory.svelte.ts new file mode 100644 index 0000000..72a181d --- /dev/null +++ b/src/lib/stores/memory.svelte.ts @@ -0,0 +1,127 @@ +import { SvelteMap } from 'svelte/reactivity'; +import { ResultSource } from '$lib/proto/llm_multiverse/v1/common_pb'; + +export interface StoredMemoryCandidate { + content: string; + source: ResultSource; + confidence: number; +} + +export interface SessionMemory { + sessionId: string; + candidates: StoredMemoryCandidate[]; +} + +const STORAGE_KEY = 'llm-multiverse-memory-candidates'; + +function loadMemory(): SvelteMap { + if (typeof localStorage === 'undefined') return new SvelteMap(); + try { + const raw = localStorage.getItem(STORAGE_KEY); + if (!raw) return new SvelteMap(); + const arr: [string, StoredMemoryCandidate[]][] = JSON.parse(raw); + return new SvelteMap(arr); + } catch { + return new SvelteMap(); + } +} + +function saveMemory(memory: SvelteMap) { + if (typeof localStorage === 'undefined') return; + localStorage.setItem(STORAGE_KEY, JSON.stringify([...memory.entries()])); +} + +function getSampleData(): SvelteMap { + const samples = new SvelteMap(); + samples.set('session-demo-alpha', [ + { + content: 'The user prefers TypeScript strict mode with no implicit any.', + source: ResultSource.MODEL_KNOWLEDGE, + confidence: 0.92 + }, + { + content: 'Project uses SvelteKit with Tailwind CSS v4 and Svelte 5 runes.', + source: ResultSource.TOOL_OUTPUT, + confidence: 0.98 + }, + { + content: 'Preferred testing framework is Vitest with Playwright for e2e.', + source: ResultSource.MODEL_KNOWLEDGE, + confidence: 0.75 + }, + { + content: 'The gRPC backend runs on port 50051 behind a Caddy reverse proxy.', + source: ResultSource.TOOL_OUTPUT, + confidence: 0.85 + } + ]); + samples.set('session-demo-beta', [ + { + content: 'User asked about deploying to Kubernetes with Helm charts.', + source: ResultSource.WEB, + confidence: 0.67 + }, + { + content: 'The container registry is at registry.example.com.', + source: ResultSource.TOOL_OUTPUT, + confidence: 0.91 + }, + { + content: 'Deployment target is a 3-node k3s cluster running on ARM64.', + source: ResultSource.WEB, + confidence: 0.58 + } + ]); + samples.set('session-demo-gamma', [ + { + content: 'Database schema uses PostgreSQL with pgvector extension for embeddings.', + source: ResultSource.TOOL_OUTPUT, + confidence: 0.95 + }, + { + content: 'The embedding model is all-MiniLM-L6-v2 with 384 dimensions.', + source: ResultSource.MODEL_KNOWLEDGE, + confidence: 0.82 + } + ]); + return samples; +} + +function createMemoryStore() { + const memory = $state>(loadMemory()); + + // Seed sample data if store is empty + if (memory.size === 0) { + const samples = getSampleData(); + for (const [id, candidates] of samples) { + memory.set(id, candidates); + } + saveMemory(memory); + } + + function addCandidates(sessionId: string, candidates: StoredMemoryCandidate[]) { + if (candidates.length === 0) return; + const existing = memory.get(sessionId) ?? []; + memory.set(sessionId, [...existing, ...candidates]); + saveMemory(memory); + } + + function getAllBySession(): SessionMemory[] { + return [...memory.entries()] + .map(([sessionId, candidates]) => ({ sessionId, candidates })) + .sort((a, b) => a.sessionId.localeCompare(b.sessionId)); + } + + function clearSession(sessionId: string) { + memory.delete(sessionId); + saveMemory(memory); + } + + return { + addCandidates, + getAllBySession, + clearSession + }; +} + +export const memoryStore = createMemoryStore(); diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index fae0170..d06f761 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -3,6 +3,7 @@ const chatHref = resolveRoute('/chat'); const lineageHref = resolveRoute('/lineage'); + const memoryHref = resolveRoute('/memory');

LLM Multiverse UI

@@ -12,4 +13,6 @@ Chat Agent Lineage + + Memory Candidates diff --git a/src/routes/chat/+page.svelte b/src/routes/chat/+page.svelte index 821e218..06899e1 100644 --- a/src/routes/chat/+page.svelte +++ b/src/routes/chat/+page.svelte @@ -18,6 +18,7 @@ import { processRequest, OrchestratorError } from '$lib/services/orchestrator'; import { OrchestrationState } from '$lib/proto/llm_multiverse/v1/orchestrator_pb'; import { sessionStore } from '$lib/stores/sessions.svelte'; + import { memoryStore } from '$lib/stores/memory.svelte'; let messages: ChatMessage[] = $state([]); let isStreaming = $state(false); @@ -30,6 +31,7 @@ ); let showConfig = $state(false); const lineageHref = resolveRoute('/lineage'); + const memoryHref = resolveRoute('/memory'); const isNonDefaultConfig = $derived( sessionConfig.overrideLevel !== OverrideLevel.NONE || @@ -106,6 +108,16 @@ } if (response.finalResult) { finalResult = response.finalResult; + if (response.finalResult.newMemoryCandidates.length > 0) { + memoryStore.addCandidates( + sessionId, + response.finalResult.newMemoryCandidates.map((mc) => ({ + content: mc.content, + source: mc.source, + confidence: mc.confidence + })) + ); + } } const idx = messages.length - 1; messages[idx] = { @@ -145,6 +157,12 @@ > Lineage + + Memory +