|
| 1 | +import { useState } from 'react'; |
| 2 | +import { useEmlResource } from './useEmlResource'; |
| 3 | +import { PanelCard, StateBlock, ConfidenceBar } from './Panel'; |
| 4 | + |
| 5 | +interface MemoryView { |
| 6 | + id: string; |
| 7 | + kind: string; |
| 8 | + title: string; |
| 9 | + summary: string; |
| 10 | + confidence: number; |
| 11 | + freshness: number; |
| 12 | + contradictionScore?: number; |
| 13 | +} |
| 14 | + |
| 15 | +const KINDS = ['all', 'decision', 'failure', 'intent', 'gap', 'ownership', 'note'] as const; |
| 16 | + |
| 17 | +const KIND_ICON: Record<string, string> = { |
| 18 | + decision: 'gavel', |
| 19 | + failure: 'warning', |
| 20 | + intent: 'flag', |
| 21 | + gap: 'help', |
| 22 | + ownership: 'group', |
| 23 | + note: 'sticky_note_2', |
| 24 | +}; |
| 25 | + |
| 26 | +export function MemoryList({ repositoryId }: { repositoryId: string | null }) { |
| 27 | + const [kind, setKind] = useState<(typeof KINDS)[number]>('all'); |
| 28 | + const url = repositoryId |
| 29 | + ? `/api/eml/memories?repositoryId=${repositoryId}${kind === 'all' ? '' : `&kind=${kind}`}` |
| 30 | + : null; |
| 31 | + const { data, loading, error, disabled, reload } = useEmlResource<{ results: MemoryView[] }>(url); |
| 32 | + const results = data?.results ?? []; |
| 33 | + |
| 34 | + return ( |
| 35 | + <PanelCard |
| 36 | + title="Memories" |
| 37 | + icon="psychology" |
| 38 | + actions={ |
| 39 | + <div className="flex items-center gap-2"> |
| 40 | + <label htmlFor="mem-kind" className="sr-only"> |
| 41 | + Filter by kind |
| 42 | + </label> |
| 43 | + <select |
| 44 | + id="mem-kind" |
| 45 | + value={kind} |
| 46 | + onChange={(e) => setKind(e.target.value as (typeof KINDS)[number])} |
| 47 | + className="px-2 py-1.5 rounded-lg bg-surface-container border border-outline-variant/30 text-xs text-on-surface focus:outline-none focus:ring-2 focus:ring-tertiary" |
| 48 | + > |
| 49 | + {KINDS.map((k) => ( |
| 50 | + <option key={k} value={k}> |
| 51 | + {k} |
| 52 | + </option> |
| 53 | + ))} |
| 54 | + </select> |
| 55 | + </div> |
| 56 | + } |
| 57 | + > |
| 58 | + <StateBlock |
| 59 | + loading={loading} |
| 60 | + error={error} |
| 61 | + disabled={disabled} |
| 62 | + empty={results.length === 0} |
| 63 | + onRetry={reload} |
| 64 | + emptyLabel="No memories captured yet." |
| 65 | + /> |
| 66 | + {!loading && !error && !disabled && results.length > 0 && ( |
| 67 | + <ul className="space-y-3 max-h-[28rem] overflow-y-auto pr-1"> |
| 68 | + {results.map((m) => ( |
| 69 | + <li |
| 70 | + key={m.id} |
| 71 | + className="p-3 rounded-xl border border-outline-variant/15 hover:bg-surface-container/50 transition-colors" |
| 72 | + > |
| 73 | + <div className="flex items-start justify-between gap-3"> |
| 74 | + <div className="flex items-center gap-2 min-w-0"> |
| 75 | + <span className="material-symbols-outlined text-base text-tertiary" aria-hidden="true"> |
| 76 | + {KIND_ICON[m.kind] ?? 'memory'} |
| 77 | + </span> |
| 78 | + <span className="text-xs font-semibold uppercase tracking-wider text-on-surface-variant"> |
| 79 | + {m.kind} |
| 80 | + </span> |
| 81 | + </div> |
| 82 | + <ConfidenceBar value={m.confidence} /> |
| 83 | + </div> |
| 84 | + <h3 className="text-sm font-semibold text-on-surface mt-1.5">{m.title}</h3> |
| 85 | + {m.summary && <p className="text-sm text-on-surface-variant mt-0.5 line-clamp-2">{m.summary}</p>} |
| 86 | + {m.contradictionScore && m.contradictionScore > 0 ? ( |
| 87 | + <span className="inline-block mt-1.5 text-xs font-semibold text-error"> |
| 88 | + contradiction risk {(m.contradictionScore * 100).toFixed(0)}% |
| 89 | + </span> |
| 90 | + ) : null} |
| 91 | + </li> |
| 92 | + ))} |
| 93 | + </ul> |
| 94 | + )} |
| 95 | + </PanelCard> |
| 96 | + ); |
| 97 | +} |
0 commit comments