import { useI18n } from "@/i18n/I18nProvider";
import type { TKey } from "@/i18n/dict";
import {
type Memory,
type MemoryPriority,
type MemoryScope,
listMemories,
softDeleteMemory,
updateMemoryPriority,
} from "@/lib/queries";
import { formatDistanceToNow } from "date-fns";
import { ChevronDown, ChevronUp, Lock, Trash2 } from "lucide-react";
import { useEffect, useState } from "react";
const SCOPE_TABS: ("all" | MemoryScope)[] = ["all", "project", "tech", "user"];
const PRIORITY_ORDER: MemoryPriority[] = ["constraint", "strong", "preference", "info"];
const PRIORITY_COLOR: Record<MemoryPriority, string> = {
constraint: "text-red-600 border-red-300 bg-red-50",
strong: "text-yellow-700 border-yellow-300 bg-yellow-50",
preference: "text-cyan-700 border-cyan-300 bg-cyan-50",
info: "text-muted-foreground border-muted bg-muted/30",
};
export default function Memory() {
const { t } = useI18n();
const [scope, setScope] = useState<"all" | MemoryScope>("all");
const [search, setSearch] = useState("");
const [memories, setMemories] = useState<Memory[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
async function refresh() {
setLoading(true);
setError(null);
try {
const all = await listMemories({
scope: scope === "all" ? undefined : scope,
search: search.trim() || undefined,
});
setMemories(all);
} catch (e) {
setError(String((e as Error)?.message ?? e));
} finally {
setLoading(false);
}
}
useEffect(() => {
refresh();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [scope, search]);
async function handlePromote(m: Memory) {
const idx = PRIORITY_ORDER.indexOf(m.priority);
if (idx <= 0) return;
const next = PRIORITY_ORDER[idx - 1];
try {
await updateMemoryPriority(m.id, next);
setMemories((prev) => prev.map((x) => (x.id === m.id ? { ...x, priority: next } : x)));
} catch (e) {
setError(String((e as Error)?.message ?? e));
}
}
async function handleDemote(m: Memory) {
const idx = PRIORITY_ORDER.indexOf(m.priority);
if (idx >= PRIORITY_ORDER.length - 1) return;
const next = PRIORITY_ORDER[idx + 1];
try {
await updateMemoryPriority(m.id, next);
setMemories((prev) => prev.map((x) => (x.id === m.id ? { ...x, priority: next } : x)));
} catch (e) {
setError(String((e as Error)?.message ?? e));
}
}
async function handleDelete(m: Memory) {
try {
await softDeleteMemory(m.id);
setMemories((prev) => prev.filter((x) => x.id !== m.id));
} catch (e) {
setError(String((e as Error)?.message ?? e));
}
}
return (
<div className="p-6 space-y-4 max-w-5xl">
<header>
<h1 className="text-xl font-semibold">{t("memory.title")}</h1>
</header>
{error && (
<div className="rounded-md border border-red-300 bg-red-50 p-3 text-sm text-red-700">
{error}
</div>
)}
{/* Tabs */}
<div className="flex border-b">
{SCOPE_TABS.map((s) => (
<button
key={s}
type="button"
onClick={() => setScope(s)}
className={`px-4 py-2 text-sm border-b-2 -mb-px transition-colors ${
scope === s
? "border-foreground text-foreground font-medium"
: "border-transparent text-muted-foreground hover:text-foreground"
}`}
>
{t(`memory.tab.${s}` as TKey)}
</button>
))}
</div>
{/* Search */}
<div>
<input
type="search"
placeholder={t("memory.search")}
value={search}
onChange={(e) => setSearch(e.target.value)}
className="rounded-md border bg-background px-3 py-1.5 text-sm w-64 focus:outline-none focus:ring-2 focus:ring-ring"
/>
</div>
{/* List */}
<div className="rounded-md border bg-card divide-y">
{loading && (
<div className="p-4 text-sm text-muted-foreground">{t("common.loading")}</div>
)}
{!loading && memories.length === 0 && (
<div className="p-6 text-sm text-muted-foreground text-center">
{t("memory.empty")}
</div>
)}
{memories.map((m) => (
<MemoryItem
key={m.id}
memory={m}
onPromote={() => handlePromote(m)}
onDemote={() => handleDemote(m)}
onDelete={() => handleDelete(m)}
promotable={PRIORITY_ORDER.indexOf(m.priority) > 0}
demotable={PRIORITY_ORDER.indexOf(m.priority) < PRIORITY_ORDER.length - 1}
/>
))}
</div>
</div>
);
}
function MemoryItem({
memory,
onPromote,
onDemote,
onDelete,
promotable,
demotable,
}: {
memory: Memory;
onPromote: () => void;
onDemote: () => void;
onDelete: () => void;
promotable: boolean;
demotable: boolean;
}) {
const { t } = useI18n();
return (
<div className="p-4 space-y-2 hover:bg-muted/30">
<div className="flex items-center gap-2 flex-wrap">
<span
className={`text-[10px] uppercase font-semibold border rounded px-1.5 py-0.5 ${PRIORITY_COLOR[memory.priority]}`}
>
{t(`memory.priority.${memory.priority}` as TKey)}
{memory.priority === "constraint" && (
<Lock size={10} className="inline ml-1" />
)}
</span>
<span className="text-xs uppercase tracking-wide text-muted-foreground">
{memory.scope}
</span>
{memory.project && (
<span className="text-xs font-mono text-muted-foreground">
{memory.project}
</span>
)}
{memory.tech.length > 0 && (
<span className="text-xs text-cyan-600">{memory.tech.join(", ")}</span>
)}
<span className="text-xs text-muted-foreground ml-auto">
{formatDistanceToNow(new Date(memory.updated_at), { addSuffix: true })}
</span>
</div>
<div className="text-sm whitespace-pre-wrap">{memory.text}</div>
<div className="flex items-center gap-1">
<button
type="button"
onClick={onPromote}
disabled={!promotable}
title={t("memory.action.promote")}
className="p-1 rounded hover:bg-muted disabled:opacity-30 text-muted-foreground hover:text-foreground"
>
<ChevronUp size={14} />
</button>
<button
type="button"
onClick={onDemote}
disabled={!demotable}
title={t("memory.action.demote")}
className="p-1 rounded hover:bg-muted disabled:opacity-30 text-muted-foreground hover:text-foreground"
>
<ChevronDown size={14} />
</button>
<button
type="button"
onClick={onDelete}
title={t("memory.action.delete")}
className="p-1 rounded hover:bg-muted text-muted-foreground hover:text-red-600"
>
<Trash2 size={14} />
</button>
</div>
</div>
);
}