mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-05-20 07:01:03 +00:00
feat: add memory management actions and local filters in memory settings (#1467)
* Add MVP memory management actions * Fix memory settings locale coverage * Polish memory management interactions * Add memory search and type filters * Refine memory settings review feedback * docs: simplify memory settings review setup * fix: restore memory updater compatibility helpers * fix: address memory settings review feedback * docs: soften memory sample review wording --------- Co-authored-by: Willem Jiang <willem.jiang@gmail.com> Co-authored-by: JeffJiang <for-eleven@hotmail.com>
This commit is contained in:
@@ -1,9 +1,28 @@
|
||||
"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 { useMemory } from "@/core/memory/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";
|
||||
@@ -11,6 +30,20 @@ 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;
|
||||
@@ -19,41 +52,82 @@ function confidenceToLevelKey(confidence: unknown): {
|
||||
return { key: "unknown" };
|
||||
}
|
||||
|
||||
// Clamp to [0, 1] since confidence is expected to be a probability-like score.
|
||||
const value = Math.min(1, Math.max(0, confidence));
|
||||
|
||||
// 3 levels:
|
||||
// - veryHigh: [0.85, 1]
|
||||
// - high: [0.65, 0.85)
|
||||
// - normal: [0, 0.65)
|
||||
if (value >= 0.85) return { key: "veryHigh", value };
|
||||
if (value >= 0.65) return { key: "high", value };
|
||||
return { key: "normal", value };
|
||||
}
|
||||
|
||||
function formatMemorySection(
|
||||
title: string,
|
||||
summary: string,
|
||||
updatedAt: string | undefined,
|
||||
section: MemorySection,
|
||||
t: ReturnType<typeof useI18n>["t"],
|
||||
): string {
|
||||
const content =
|
||||
summary.trim() ||
|
||||
section.summary.trim() ||
|
||||
`<span class="text-muted-foreground">${t.settings.memory.markdown.empty}</span>`;
|
||||
return [
|
||||
`### ${title}`,
|
||||
`### ${section.title}`,
|
||||
content,
|
||||
"",
|
||||
updatedAt &&
|
||||
`> ${t.settings.memory.markdown.updatedAt}: \`${formatTimeAgo(updatedAt)}\``,
|
||||
section.updatedAt &&
|
||||
`> ${t.settings.memory.markdown.updatedAt}: \`${formatTimeAgo(section.updatedAt)}\``,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join("\n");
|
||||
}
|
||||
|
||||
function memoryToMarkdown(
|
||||
function buildMemorySectionGroups(
|
||||
memory: UserMemory,
|
||||
t: ReturnType<typeof useI18n>["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<typeof useI18n>["t"],
|
||||
) {
|
||||
const parts: string[] = [];
|
||||
|
||||
@@ -62,83 +136,14 @@ function memoryToMarkdown(
|
||||
`- **${t.common.lastUpdated}**: \`${formatTimeAgo(memory.lastUpdated)}\``,
|
||||
);
|
||||
|
||||
parts.push(`\n## ${t.settings.memory.markdown.userContext}`);
|
||||
parts.push(
|
||||
formatMemorySection(
|
||||
t.settings.memory.markdown.work,
|
||||
memory.user.workContext.summary,
|
||||
memory.user.workContext.updatedAt,
|
||||
t,
|
||||
),
|
||||
);
|
||||
parts.push(
|
||||
formatMemorySection(
|
||||
t.settings.memory.markdown.personal,
|
||||
memory.user.personalContext.summary,
|
||||
memory.user.personalContext.updatedAt,
|
||||
t,
|
||||
),
|
||||
);
|
||||
parts.push(
|
||||
formatMemorySection(
|
||||
t.settings.memory.markdown.topOfMind,
|
||||
memory.user.topOfMind.summary,
|
||||
memory.user.topOfMind.updatedAt,
|
||||
t,
|
||||
),
|
||||
);
|
||||
|
||||
parts.push(`\n## ${t.settings.memory.markdown.historyBackground}`);
|
||||
parts.push(
|
||||
formatMemorySection(
|
||||
t.settings.memory.markdown.recentMonths,
|
||||
memory.history.recentMonths.summary,
|
||||
memory.history.recentMonths.updatedAt,
|
||||
t,
|
||||
),
|
||||
);
|
||||
parts.push(
|
||||
formatMemorySection(
|
||||
t.settings.memory.markdown.earlierContext,
|
||||
memory.history.earlierContext.summary,
|
||||
memory.history.earlierContext.updatedAt,
|
||||
t,
|
||||
),
|
||||
);
|
||||
parts.push(
|
||||
formatMemorySection(
|
||||
t.settings.memory.markdown.longTermBackground,
|
||||
memory.history.longTermBackground.summary,
|
||||
memory.history.longTermBackground.updatedAt,
|
||||
t,
|
||||
),
|
||||
);
|
||||
|
||||
parts.push(`\n## ${t.settings.memory.markdown.facts}`);
|
||||
if (memory.facts.length === 0) {
|
||||
parts.push(
|
||||
`<span class="text-muted-foreground">${t.settings.memory.markdown.empty}</span>`,
|
||||
);
|
||||
} else {
|
||||
parts.push(
|
||||
[
|
||||
`| ${t.settings.memory.markdown.table.category} | ${t.settings.memory.markdown.table.confidence} | ${t.settings.memory.markdown.table.content} | ${t.settings.memory.markdown.table.source} | ${t.settings.memory.markdown.table.createdAt} |`,
|
||||
"|---|---|---|---|---|",
|
||||
...memory.facts.map((f) => {
|
||||
const { key, value } = confidenceToLevelKey(f.confidence);
|
||||
const levelLabel =
|
||||
t.settings.memory.markdown.table.confidenceLevel[key];
|
||||
const confidenceText =
|
||||
typeof value === "number" ? `${levelLabel}` : levelLabel;
|
||||
return `| ${upperFirst(f.category)} | ${confidenceText} | ${f.content} | [${t.settings.memory.markdown.table.view}](${pathOfThread(f.source)}) | ${formatTimeAgo(f.createdAt)} |`;
|
||||
}),
|
||||
].join("\n"),
|
||||
);
|
||||
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");
|
||||
|
||||
// Ensure every level-2 heading (##) is preceded by a horizontal rule.
|
||||
const lines = markdown.split("\n");
|
||||
const out: string[] = [];
|
||||
let i = 0;
|
||||
@@ -155,36 +160,355 @@ function memoryToMarkdown(
|
||||
return out.join("\n");
|
||||
}
|
||||
|
||||
export function MemorySettingsPage() {
|
||||
const { t } = useI18n();
|
||||
const { memory, isLoading, error } = useMemory();
|
||||
function isMemorySummaryEmpty(memory: UserMemory) {
|
||||
return (
|
||||
<SettingsSection
|
||||
title={t.settings.memory.title}
|
||||
description={t.settings.memory.description}
|
||||
>
|
||||
{isLoading ? (
|
||||
<div className="text-muted-foreground text-sm">{t.common.loading}</div>
|
||||
) : error ? (
|
||||
<div>Error: {error.message}</div>
|
||||
) : !memory ? (
|
||||
<div className="text-muted-foreground text-sm">
|
||||
{t.settings.memory.empty}
|
||||
</div>
|
||||
) : (
|
||||
<div className="rounded-lg border p-4">
|
||||
<Streamdown
|
||||
className="size-full [&>*:first-child]:mt-0 [&>*:last-child]:mb-0"
|
||||
{...streamdownPlugins}
|
||||
>
|
||||
{memoryToMarkdown(memory, t)}
|
||||
</Streamdown>
|
||||
</div>
|
||||
)}
|
||||
</SettingsSection>
|
||||
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<MemoryFact | null>(null);
|
||||
const [query, setQuery] = useState("");
|
||||
const [filter, setFilter] = useState<MemoryViewFilter>("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 (
|
||||
<>
|
||||
<SettingsSection
|
||||
title={t.settings.memory.title}
|
||||
description={t.settings.memory.description}
|
||||
>
|
||||
{isLoading ? (
|
||||
<div className="text-muted-foreground text-sm">{t.common.loading}</div>
|
||||
) : error ? (
|
||||
<div>Error: {error.message}</div>
|
||||
) : !memory ? (
|
||||
<div className="text-muted-foreground text-sm">
|
||||
{t.settings.memory.empty}
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
{isMemorySummaryEmpty(memory) && memory.facts.length === 0 ? (
|
||||
<div className="text-muted-foreground rounded-lg border border-dashed p-4 text-sm">
|
||||
{memoryFullyEmpty}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div className="flex flex-1 flex-col gap-3 sm:flex-row sm:items-center">
|
||||
<Input
|
||||
value={query}
|
||||
onChange={(event) => setQuery(event.target.value)}
|
||||
placeholder={searchPlaceholder}
|
||||
className="sm:max-w-xs"
|
||||
/>
|
||||
<ToggleGroup
|
||||
type="single"
|
||||
value={filter}
|
||||
onValueChange={(value) => {
|
||||
if (value) setFilter(value as MemoryViewFilter);
|
||||
}}
|
||||
variant="outline"
|
||||
>
|
||||
<ToggleGroupItem value="all">{filterAll}</ToggleGroupItem>
|
||||
<ToggleGroupItem value="facts">{filterFacts}</ToggleGroupItem>
|
||||
<ToggleGroupItem value="summaries">
|
||||
{filterSummaries}
|
||||
</ToggleGroupItem>
|
||||
</ToggleGroup>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
variant="destructive"
|
||||
onClick={() => setClearDialogOpen(true)}
|
||||
disabled={clearMemory.isPending}
|
||||
>
|
||||
{clearMemory.isPending ? t.common.loading : clearAllLabel}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{!hasMatchingVisibleContent && normalizedQuery ? (
|
||||
<div className="text-muted-foreground rounded-lg border border-dashed p-4 text-sm">
|
||||
{noMatches}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{shouldRenderSummariesBlock ? (
|
||||
<div className="rounded-lg border p-4">
|
||||
<div className="text-muted-foreground mb-4 text-sm">
|
||||
{summaryReadOnly}
|
||||
</div>
|
||||
<Streamdown
|
||||
className="size-full [&>*:first-child]:mt-0 [&>*:last-child]:mb-0"
|
||||
{...streamdownPlugins}
|
||||
>
|
||||
{summariesToMarkdown(memory, filteredSectionGroups, t)}
|
||||
</Streamdown>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{shouldRenderFactsBlock ? (
|
||||
<div className="rounded-lg border p-4">
|
||||
<div className="mb-4">
|
||||
<h3 className="text-base font-medium">
|
||||
{t.settings.memory.markdown.facts}
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
{filteredFacts.length === 0 ? (
|
||||
<div className="text-muted-foreground text-sm">
|
||||
{normalizedQuery ? noMatches : noFacts}
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{filteredFacts.map((fact) => {
|
||||
const { key } = confidenceToLevelKey(fact.confidence);
|
||||
const confidenceText =
|
||||
t.settings.memory.markdown.table.confidenceLevel[key];
|
||||
|
||||
return (
|
||||
<div
|
||||
key={fact.id}
|
||||
className="flex flex-col gap-3 rounded-md border p-3 sm:flex-row sm:items-start sm:justify-between"
|
||||
>
|
||||
<div className="min-w-0 space-y-2">
|
||||
<div className="flex flex-wrap gap-x-4 gap-y-1 text-sm">
|
||||
<span>
|
||||
<span className="text-muted-foreground">
|
||||
{t.settings.memory.markdown.table.category}:
|
||||
</span>{" "}
|
||||
{upperFirst(fact.category)}
|
||||
</span>
|
||||
<span>
|
||||
<span className="text-muted-foreground">
|
||||
{t.settings.memory.markdown.table.confidence}:
|
||||
</span>{" "}
|
||||
{confidenceText}
|
||||
</span>
|
||||
<span>
|
||||
<span className="text-muted-foreground">
|
||||
{t.settings.memory.markdown.table.createdAt}:
|
||||
</span>{" "}
|
||||
{formatTimeAgo(fact.createdAt)}
|
||||
</span>
|
||||
</div>
|
||||
<p className="break-words text-sm">{fact.content}</p>
|
||||
<Link
|
||||
href={pathOfThread(fact.source)}
|
||||
className="text-primary text-sm underline-offset-4 hover:underline"
|
||||
>
|
||||
{t.settings.memory.markdown.table.view}
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="text-destructive hover:text-destructive shrink-0"
|
||||
onClick={() => setFactToDelete(fact)}
|
||||
disabled={deleteMemoryFact.isPending}
|
||||
title={t.common.delete}
|
||||
aria-label={t.common.delete}
|
||||
>
|
||||
<Trash2Icon className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
)}
|
||||
</SettingsSection>
|
||||
|
||||
<Dialog open={clearDialogOpen} onOpenChange={setClearDialogOpen}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{clearAllConfirmTitle}</DialogTitle>
|
||||
<DialogDescription>
|
||||
{clearAllConfirmDescription}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogFooter>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setClearDialogOpen(false)}
|
||||
disabled={clearMemory.isPending}
|
||||
>
|
||||
{t.common.cancel}
|
||||
</Button>
|
||||
<Button
|
||||
variant="destructive"
|
||||
onClick={() => void handleClearMemory()}
|
||||
disabled={clearMemory.isPending}
|
||||
>
|
||||
{clearMemory.isPending ? t.common.loading : clearAllLabel}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<Dialog
|
||||
open={factToDelete !== null}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) {
|
||||
setFactToDelete(null);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{factDeleteConfirmTitle}</DialogTitle>
|
||||
<DialogDescription>
|
||||
{factDeleteConfirmDescription}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
{factToDelete ? (
|
||||
<div className="bg-muted rounded-md border p-3 text-sm">
|
||||
<div className="text-muted-foreground mb-1 font-medium">
|
||||
{factPreviewLabel}
|
||||
</div>
|
||||
<p className="break-words">
|
||||
{truncateFactPreview(factToDelete.content)}
|
||||
</p>
|
||||
</div>
|
||||
) : null}
|
||||
<DialogFooter>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setFactToDelete(null)}
|
||||
disabled={deleteMemoryFact.isPending}
|
||||
>
|
||||
{t.common.cancel}
|
||||
</Button>
|
||||
<Button
|
||||
variant="destructive"
|
||||
onClick={() => void handleDeleteFact()}
|
||||
disabled={deleteMemoryFact.isPending}
|
||||
>
|
||||
{deleteMemoryFact.isPending ? t.common.loading : t.common.delete}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user