"use client"; import { Trash2Icon } from "lucide-react"; import Link from "next/link"; import { useDeferredValue, useState } from "react"; import { toast } from "sonner"; import { Streamdown } from "streamdown"; import { Button } from "@/components/ui/button"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { Input } from "@/components/ui/input"; import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"; import { useI18n } from "@/core/i18n/hooks"; import { useClearMemory, useDeleteMemoryFact, useMemory, } from "@/core/memory/hooks"; import type { UserMemory } from "@/core/memory/types"; import { streamdownPlugins } from "@/core/streamdown/plugins"; import { pathOfThread } from "@/core/threads/utils"; import { formatTimeAgo } from "@/core/utils/datetime"; import { SettingsSection } from "./settings-section"; type MemoryViewFilter = "all" | "facts" | "summaries"; type MemoryFact = UserMemory["facts"][number]; type MemorySection = { title: string; summary: string; updatedAt?: string; }; type MemorySectionGroup = { title: string; sections: MemorySection[]; }; function confidenceToLevelKey(confidence: unknown): { key: "veryHigh" | "high" | "normal" | "unknown"; value?: number; } { if (typeof confidence !== "number" || !Number.isFinite(confidence)) { return { key: "unknown" }; } const value = Math.min(1, Math.max(0, confidence)); if (value >= 0.85) return { key: "veryHigh", value }; if (value >= 0.65) return { key: "high", value }; return { key: "normal", value }; } function formatMemorySection( section: MemorySection, t: ReturnType["t"], ): string { const content = section.summary.trim() || `${t.settings.memory.markdown.empty}`; return [ `### ${section.title}`, content, "", section.updatedAt && `> ${t.settings.memory.markdown.updatedAt}: \`${formatTimeAgo(section.updatedAt)}\``, ] .filter(Boolean) .join("\n"); } function buildMemorySectionGroups( memory: UserMemory, t: ReturnType["t"], ): MemorySectionGroup[] { return [ { title: t.settings.memory.markdown.userContext, sections: [ { title: t.settings.memory.markdown.work, summary: memory.user.workContext.summary, updatedAt: memory.user.workContext.updatedAt, }, { title: t.settings.memory.markdown.personal, summary: memory.user.personalContext.summary, updatedAt: memory.user.personalContext.updatedAt, }, { title: t.settings.memory.markdown.topOfMind, summary: memory.user.topOfMind.summary, updatedAt: memory.user.topOfMind.updatedAt, }, ], }, { title: t.settings.memory.markdown.historyBackground, sections: [ { title: t.settings.memory.markdown.recentMonths, summary: memory.history.recentMonths.summary, updatedAt: memory.history.recentMonths.updatedAt, }, { title: t.settings.memory.markdown.earlierContext, summary: memory.history.earlierContext.summary, updatedAt: memory.history.earlierContext.updatedAt, }, { title: t.settings.memory.markdown.longTermBackground, summary: memory.history.longTermBackground.summary, updatedAt: memory.history.longTermBackground.updatedAt, }, ], }, ]; } function summariesToMarkdown( memory: UserMemory, sectionGroups: MemorySectionGroup[], t: ReturnType["t"], ) { const parts: string[] = []; parts.push(`## ${t.settings.memory.markdown.overview}`); parts.push( `- **${t.common.lastUpdated}**: \`${formatTimeAgo(memory.lastUpdated)}\``, ); for (const group of sectionGroups) { parts.push(`\n## ${group.title}`); for (const section of group.sections) { parts.push(formatMemorySection(section, t)); } } const markdown = parts.join("\n\n"); const lines = markdown.split("\n"); const out: string[] = []; let i = 0; for (const line of lines) { i++; if (i !== 1 && line.startsWith("## ")) { if (out.length === 0 || out[out.length - 1] !== "---") { out.push("---"); } } out.push(line); } return out.join("\n"); } function isMemorySummaryEmpty(memory: UserMemory) { return ( memory.user.workContext.summary.trim() === "" && memory.user.personalContext.summary.trim() === "" && memory.user.topOfMind.summary.trim() === "" && memory.history.recentMonths.summary.trim() === "" && memory.history.earlierContext.summary.trim() === "" && memory.history.longTermBackground.summary.trim() === "" ); } function truncateFactPreview(content: string, maxLength = 140) { const normalized = content.replace(/\s+/g, " ").trim(); if (normalized.length <= maxLength) { return normalized; } const ellipsis = "..."; if (maxLength <= ellipsis.length) { return normalized.slice(0, maxLength); } return `${normalized.slice(0, maxLength - ellipsis.length)}${ellipsis}`; } function upperFirst(str: string) { return str.charAt(0).toUpperCase() + str.slice(1); } export function MemorySettingsPage() { const { t } = useI18n(); const { memory, isLoading, error } = useMemory(); const clearMemory = useClearMemory(); const deleteMemoryFact = useDeleteMemoryFact(); const [clearDialogOpen, setClearDialogOpen] = useState(false); const [factToDelete, setFactToDelete] = useState(null); const [query, setQuery] = useState(""); const [filter, setFilter] = useState("all"); const deferredQuery = useDeferredValue(query); const normalizedQuery = deferredQuery.trim().toLowerCase(); const clearAllLabel = t.settings.memory.clearAll ?? "Clear all memory"; const clearAllConfirmTitle = t.settings.memory.clearAllConfirmTitle ?? "Clear all memory?"; const clearAllConfirmDescription = t.settings.memory.clearAllConfirmDescription ?? "This will remove all saved summaries and facts. This action cannot be undone."; const clearAllSuccess = t.settings.memory.clearAllSuccess ?? "All memory cleared"; const factDeleteConfirmTitle = t.settings.memory.factDeleteConfirmTitle ?? "Delete this fact?"; const factDeleteConfirmDescription = t.settings.memory.factDeleteConfirmDescription ?? "This fact will be removed from memory immediately. This action cannot be undone."; const factDeleteSuccess = t.settings.memory.factDeleteSuccess ?? "Fact deleted"; const noFacts = t.settings.memory.noFacts ?? "No saved facts yet."; const summaryReadOnly = t.settings.memory.summaryReadOnly ?? "Summary sections are read-only for now. You can currently clear all memory or delete individual facts."; const memoryFullyEmpty = t.settings.memory.memoryFullyEmpty ?? "No memory saved yet."; const factPreviewLabel = t.settings.memory.factPreviewLabel ?? "Fact to delete"; const searchPlaceholder = t.settings.memory.searchPlaceholder ?? "Search memory"; const filterAll = t.settings.memory.filterAll ?? "All"; const filterFacts = t.settings.memory.filterFacts ?? "Facts"; const filterSummaries = t.settings.memory.filterSummaries ?? "Summaries"; const noMatches = t.settings.memory.noMatches ?? "No matching memory found"; const sectionGroups = memory ? buildMemorySectionGroups(memory, t) : []; const filteredSectionGroups = sectionGroups .map((group) => ({ ...group, sections: group.sections.filter((section) => normalizedQuery ? `${section.title} ${section.summary}` .toLowerCase() .includes(normalizedQuery) : true, ), })) .filter((group) => group.sections.length > 0); const filteredFacts = memory ? memory.facts.filter((fact) => normalizedQuery ? `${fact.content} ${fact.category}` .toLowerCase() .includes(normalizedQuery) : true, ) : []; const showSummaries = filter !== "facts"; const showFacts = filter !== "summaries"; const shouldRenderSummariesBlock = showSummaries && (filteredSectionGroups.length > 0 || !normalizedQuery); const shouldRenderFactsBlock = showFacts && (filteredFacts.length > 0 || !normalizedQuery || filter === "facts"); const hasMatchingVisibleContent = !memory || (showSummaries && filteredSectionGroups.length > 0) || (showFacts && filteredFacts.length > 0); async function handleClearMemory() { try { await clearMemory.mutateAsync(); toast.success(clearAllSuccess); setClearDialogOpen(false); } catch (err) { toast.error(err instanceof Error ? err.message : String(err)); } } async function handleDeleteFact() { if (!factToDelete) return; try { await deleteMemoryFact.mutateAsync(factToDelete.id); toast.success(factDeleteSuccess); setFactToDelete(null); } catch (err) { toast.error(err instanceof Error ? err.message : String(err)); } } return ( <> {isLoading ? (
{t.common.loading}
) : error ? (
Error: {error.message}
) : !memory ? (
{t.settings.memory.empty}
) : (
{isMemorySummaryEmpty(memory) && memory.facts.length === 0 ? (
{memoryFullyEmpty}
) : null}
setQuery(event.target.value)} placeholder={searchPlaceholder} className="sm:max-w-xs" /> { if (value) setFilter(value as MemoryViewFilter); }} variant="outline" > {filterAll} {filterFacts} {filterSummaries}
{!hasMatchingVisibleContent && normalizedQuery ? (
{noMatches}
) : null} {shouldRenderSummariesBlock ? (
{summaryReadOnly}
{summariesToMarkdown(memory, filteredSectionGroups, t)}
) : null} {shouldRenderFactsBlock ? (

{t.settings.memory.markdown.facts}

{filteredFacts.length === 0 ? (
{normalizedQuery ? noMatches : noFacts}
) : (
{filteredFacts.map((fact) => { const { key } = confidenceToLevelKey(fact.confidence); const confidenceText = t.settings.memory.markdown.table.confidenceLevel[key]; return (
{t.settings.memory.markdown.table.category}: {" "} {upperFirst(fact.category)} {t.settings.memory.markdown.table.confidence}: {" "} {confidenceText} {t.settings.memory.markdown.table.createdAt}: {" "} {formatTimeAgo(fact.createdAt)}

{fact.content}

{t.settings.memory.markdown.table.view}
); })}
)}
) : null}
)}
{clearAllConfirmTitle} {clearAllConfirmDescription} { if (!open) { setFactToDelete(null); } }} > {factDeleteConfirmTitle} {factDeleteConfirmDescription} {factToDelete ? (
{factPreviewLabel}

{truncateFactPreview(factToDelete.content)}

) : null}
); }