'use client' import { useCallback, useEffect, useMemo, useState } from 'react' import { Brain, Loader2, Pin, PinOff, Pencil, Plus, RotateCcw, Trash2, X } from 'lucide-react' import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' import { Button } from '@/components/ui/button' import { Badge } from '@/components/ui/badge' import { Textarea } from '@/components/ui/textarea' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select' import { EmptyState } from '@/components/ui/empty-state' import { Skeleton } from '@/components/ui/skeleton' import { useToast } from '@/components/ui/use-toast' import { useCanWrite } from '@/lib/hooks/use-can-write' import { 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) } return (
Vad min assistent kommer ihåg 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.
{canWrite && ( )}
{showAdd && canWrite && (