'use client' import { useCallback, useEffect, useMemo, useState } from 'react' import { Brain, Loader2, Pin, PinOff, Pencil, Plus, RotateCcw, Trash2, X } from 'lucide-react' import { Button } from '@/components/ui/button' import { Badge } from '@/components/ui/badge' import { Switch } from '@/components/ui/switch' import { EmptyState } from '@/components/ui/empty-state' import { Skeleton } from '@/components/ui/skeleton' import { useToast } from '@/components/ui/use-toast' import { SettingsGroup, SettingsReveal, SettingsRow, SettingsRowEnd, SettingsRowNote, SettingsSeg, SettingsSelect, SettingsTextarea, } from '@/components/settings/SettingsRows' import { useCanWrite } from '@/lib/hooks/use-can-write' import { cn, formatDateLong } from '@/lib/utils' type Kind = 'fact' | 'preference' | 'pattern' | 'correction' type Source = 'composer' | 'user_taught' | 'agent_learned' | 'derived' interface AgentMemoryRow { id: string kind: Kind content: string source: Source source_ref: string | null relevance_score: number is_pinned: boolean is_active: boolean last_accessed_at: string | null created_at: string updated_at: string } const KIND_LABEL: Record = { fact: 'Fakta', preference: 'Preferens', pattern: 'Mönster', correction: 'Korrigering', } const SOURCE_LABEL: Record = { composer: 'Inläst vid uppstart', user_taught: 'Du lärde mig', agent_learned: 'Jag noterade', derived: 'Härlett', } const KIND_FILTER: { value: 'all' | Kind; label: string }[] = [ { value: 'all', label: 'Alla' }, { value: 'fact', label: 'Fakta' }, { value: 'preference', label: 'Preferenser' }, { value: 'pattern', label: 'Mönster' }, { value: 'correction', label: 'Korrigeringar' }, ] // The API returns errors either as a plain string (legacy/validation) or as // the canonical { code, message } envelope; extract something renderable. function apiErrorText(error: unknown): string | undefined { if (typeof error === 'string') return error if (error && typeof error === 'object' && 'message' in error) { const m = (error as { message?: unknown }).message return typeof m === 'string' ? m : undefined } return undefined } export function AgentMemoryPanel() { const { toast } = useToast() const { canWrite } = useCanWrite() const [rows, setRows] = useState(null) const [includeDismissed, setIncludeDismissed] = useState(false) const [kindFilter, setKindFilter] = useState<'all' | Kind>('all') const [busyId, setBusyId] = useState(null) const [editingId, setEditingId] = useState(null) const [editDraft, setEditDraft] = useState('') const [showAdd, setShowAdd] = useState(false) const [newContent, setNewContent] = useState('') const [newKind, setNewKind] = useState('fact') const [adding, setAdding] = useState(false) const load = useCallback(async () => { const params = new URLSearchParams() if (includeDismissed) params.set('include_dismissed', 'true') if (kindFilter !== 'all') params.set('kind', kindFilter) const res = await fetch(`/api/agent/memory?${params.toString()}`) const json = await res.json() if (!res.ok) { toast({ title: 'Kunde inte hämta minne', description: apiErrorText(json.error), variant: 'destructive' }) setRows([]) return } setRows(json.data as AgentMemoryRow[]) }, [includeDismissed, kindFilter, toast]) useEffect(() => { void load() }, [load]) const counts = useMemo(() => { const active = rows?.filter((r) => r.is_active).length ?? 0 const pinned = rows?.filter((r) => r.is_active && r.is_pinned).length ?? 0 const dismissed = rows?.filter((r) => !r.is_active).length ?? 0 return { active, pinned, dismissed } }, [rows]) async function patch(id: string, body: Partial>) { setBusyId(id) try { const res = await fetch(`/api/agent/memory/${id}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body), }) const json = await res.json() if (!res.ok) { toast({ title: 'Kunde inte uppdatera', description: apiErrorText(json.error), variant: 'destructive' }) return } setRows((prev) => prev?.map((r) => (r.id === id ? (json.data as AgentMemoryRow) : r)) ?? null) } finally { setBusyId(null) } } async function addMemory() { if (newContent.trim().length < 2) return setAdding(true) try { const res = await fetch('/api/agent/memory', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ content: newContent.trim(), kind: newKind }), }) const json = await res.json() if (!res.ok) { toast({ title: 'Kunde inte spara minne', description: apiErrorText(json.error), variant: 'destructive' }) return } setRows((prev) => [json.data as AgentMemoryRow, ...(prev ?? [])]) setNewContent('') setNewKind('fact') setShowAdd(false) toast({ title: 'Minne sparat' }) } finally { setAdding(false) } } function startEdit(row: AgentMemoryRow) { setEditingId(row.id) setEditDraft(row.content) } async function saveEdit(row: AgentMemoryRow) { const next = editDraft.trim() if (next.length < 2 || next === row.content) { setEditingId(null) return } await patch(row.id, { content: next }) setEditingId(null) } // The view wrapper in AssistantSettingsContent already provides the gap // under the segmented control, so the group starts flush (pt-0). return ( Bokföringsassistenten använder dessa anteckningar för att ge dig rätt råd. Fäst det som alltid ska vara med, redigera fel, eller dölj det som inte längre stämmer. Upp till 30 minnen ingår i samtal per tur. } className="pt-0 first:pt-0" > {/* Toolbar row: kind filter + the add-memory entry point. */}
{canWrite && ( )}
{canWrite && (
setNewContent(e.target.value)} placeholder="T.ex. Vi använder Stripe för B2C-betalningar; utbetalningar landar på 1930 var måndag." rows={3} maxLength={2000} aria-label="Nytt minne" className="w-full border-border" />
Typ setNewKind(e.target.value as Kind)} aria-label="Typ" > {(Object.keys(KIND_LABEL) as Kind[]).map((k) => ( ))}
)} {/* Dynamic status stays visible; the static "how it's used" copy lives in the group help above. */} {rows && (

{counts.active} aktiva · {counts.pinned} fästa {includeDismissed && counts.dismissed > 0 ? ` · ${counts.dismissed} dolda` : ''}

)} {rows === null && (
{[0, 1, 2].map((i) => ( ))}
)} {rows && rows.length === 0 && ( )} {rows && rows.length > 0 && (
    {rows.map((row) => { const isEditing = editingId === row.id const isBusy = busyId === row.id const dimmed = !row.is_active return (
  • {canWrite && row.is_active ? ( ) : (
    {row.is_pinned && }
    )}
    {KIND_LABEL[row.kind]} · {SOURCE_LABEL[row.source]} {dimmed && Dold}
    {isEditing ? ( setEditDraft(e.target.value)} rows={3} maxLength={2000} autoFocus aria-label="Redigera minne" className="w-full border-border" /> ) : (

    {row.content}

    )}

    Skapad {formatDateLong(row.created_at)} {row.updated_at !== row.created_at && ` · uppdaterad ${formatDateLong(row.updated_at)}`}

    {canWrite && (
    {isEditing ? ( <> ) : row.is_active ? ( <> ) : ( )}
    )}
  • ) })}
)}
) }