mirror of
https://gitee.com/wanwujie/deer-flow
synced 2026-04-17 11:44:44 +08:00
feat: add memory settings page
This commit is contained in:
@@ -58,11 +58,12 @@ export function Hero({ className }: { className?: string }) {
|
||||
</h1>
|
||||
<p
|
||||
className="mt-8 scale-105 text-center text-2xl text-shadow-sm"
|
||||
style={{ color: "rgb(180,180,185)" }}
|
||||
style={{ color: "rgb(182,182,188)" }}
|
||||
>
|
||||
DeerFlow is an open-source SuperAgent that researches, codes, and
|
||||
creates.
|
||||
<br />
|
||||
creates. With the help of sandboxes, tools and skills, it handles
|
||||
With the help of sandboxes, memories, tools and skills, it handles
|
||||
<br />
|
||||
different levels of tasks that could take minutes to hours.
|
||||
</p>
|
||||
|
||||
@@ -11,12 +11,7 @@ const features: BentoCardProps[] = [
|
||||
color: COLOR,
|
||||
label: "Context Engineering",
|
||||
title: "Long/Short-term Memory",
|
||||
description: (
|
||||
<div>
|
||||
<div>Now the agent can better understand you</div>
|
||||
<div className="text-muted-foreground">Coming soon</div>
|
||||
</div>
|
||||
),
|
||||
description: "Now the agent can better understand you",
|
||||
},
|
||||
{
|
||||
color: COLOR,
|
||||
|
||||
@@ -0,0 +1,177 @@
|
||||
"use client";
|
||||
|
||||
import { Streamdown } from "streamdown";
|
||||
|
||||
import { useI18n } from "@/core/i18n/hooks";
|
||||
import { 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";
|
||||
|
||||
function confidenceToLevelKey(confidence: unknown): {
|
||||
key: "veryHigh" | "high" | "normal" | "unknown";
|
||||
value?: number;
|
||||
} {
|
||||
if (typeof confidence !== "number" || !Number.isFinite(confidence)) {
|
||||
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 memoryToMarkdown(
|
||||
memory: UserMemory,
|
||||
t: ReturnType<typeof useI18n>["t"],
|
||||
) {
|
||||
const parts: string[] = [];
|
||||
|
||||
console.info(memory);
|
||||
|
||||
parts.push(`## ${t.settings.memory.markdown.overview}`);
|
||||
parts.push(`- **${t.common.version}**: \`${memory.version}\``);
|
||||
parts.push(
|
||||
`- **${t.common.lastUpdated}**: \`${formatTimeAgo(memory.lastUpdated)}\``,
|
||||
);
|
||||
|
||||
parts.push(`\n## ${t.settings.memory.markdown.userContext}`);
|
||||
parts.push(
|
||||
[
|
||||
`### ${t.settings.memory.markdown.work}`,
|
||||
memory.user.workContext.summary || "-",
|
||||
"",
|
||||
memory.user.workContext.updatedAt &&
|
||||
`> ${t.settings.memory.markdown.updatedAt}: \`${formatTimeAgo(memory.user.workContext.updatedAt)}\``,
|
||||
].join("\n"),
|
||||
);
|
||||
parts.push(
|
||||
[
|
||||
`### ${t.settings.memory.markdown.personal}`,
|
||||
memory.user.personalContext.summary || "-",
|
||||
"",
|
||||
memory.user.personalContext.updatedAt &&
|
||||
`> ${t.settings.memory.markdown.updatedAt}: \`${formatTimeAgo(memory.user.personalContext.updatedAt)}\``,
|
||||
].join("\n"),
|
||||
);
|
||||
parts.push(
|
||||
[
|
||||
`### ${t.settings.memory.markdown.topOfMind}`,
|
||||
memory.user.topOfMind.summary || "-",
|
||||
"",
|
||||
memory.user.topOfMind.updatedAt &&
|
||||
`> ${t.settings.memory.markdown.updatedAt}: \`${formatTimeAgo(memory.user.topOfMind.updatedAt)}\``,
|
||||
].join("\n"),
|
||||
);
|
||||
|
||||
parts.push(`\n## ${t.settings.memory.markdown.historyBackground}`);
|
||||
parts.push(
|
||||
[
|
||||
`### ${t.settings.memory.markdown.recentMonths}`,
|
||||
memory.history.recentMonths.summary || "-",
|
||||
"",
|
||||
memory.history.recentMonths.updatedAt &&
|
||||
`> ${t.settings.memory.markdown.updatedAt}: \`${formatTimeAgo(memory.history.recentMonths.updatedAt)}\``,
|
||||
].join("\n"),
|
||||
);
|
||||
parts.push(
|
||||
[
|
||||
`### ${t.settings.memory.markdown.earlierContext}`,
|
||||
memory.history.earlierContext.summary || "-",
|
||||
"",
|
||||
memory.history.earlierContext.updatedAt &&
|
||||
`> ${t.settings.memory.markdown.updatedAt}: \`${formatTimeAgo(memory.history.earlierContext.updatedAt)}\``,
|
||||
].join("\n"),
|
||||
);
|
||||
parts.push(
|
||||
[
|
||||
`### ${t.settings.memory.markdown.longTermBackground}`,
|
||||
memory.history.longTermBackground.summary || "-",
|
||||
"",
|
||||
memory.history.longTermBackground.updatedAt &&
|
||||
`> ${t.settings.memory.markdown.updatedAt}: \`${formatTimeAgo(memory.history.longTermBackground.updatedAt)}\``,
|
||||
].join("\n"),
|
||||
);
|
||||
|
||||
parts.push(`\n## ${t.settings.memory.markdown.facts}`);
|
||||
if (memory.facts.length === 0) {
|
||||
parts.push(`_${t.settings.memory.markdown.empty}_`);
|
||||
} 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"),
|
||||
);
|
||||
}
|
||||
|
||||
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;
|
||||
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");
|
||||
}
|
||||
|
||||
export function MemorySettingsPage() {
|
||||
const { t } = useI18n();
|
||||
const { memory, isLoading, error } = useMemory();
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
function upperFirst(str: string) {
|
||||
return str.charAt(0).toUpperCase() + str.slice(1);
|
||||
}
|
||||
@@ -1,6 +1,12 @@
|
||||
"use client";
|
||||
|
||||
import { BellIcon, PaletteIcon, SparklesIcon, WrenchIcon } from "lucide-react";
|
||||
import {
|
||||
BellIcon,
|
||||
BrainIcon,
|
||||
PaletteIcon,
|
||||
SparklesIcon,
|
||||
WrenchIcon,
|
||||
} from "lucide-react";
|
||||
import { useMemo, useState } from "react";
|
||||
|
||||
import {
|
||||
@@ -12,6 +18,7 @@ import {
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import { AcknowledgePage } from "@/components/workspace/settings/acknowledge-page";
|
||||
import { AppearanceSettingsPage } from "@/components/workspace/settings/appearance-settings-page";
|
||||
import { MemorySettingsPage } from "@/components/workspace/settings/memory-settings-page";
|
||||
import { NotificationSettingsPage } from "@/components/workspace/settings/notification-settings-page";
|
||||
import { SkillSettingsPage } from "@/components/workspace/settings/skill-settings-page";
|
||||
import { ToolSettingsPage } from "@/components/workspace/settings/tool-settings-page";
|
||||
@@ -20,6 +27,7 @@ import { cn } from "@/lib/utils";
|
||||
|
||||
type SettingsSection =
|
||||
| "appearance"
|
||||
| "memory"
|
||||
| "tools"
|
||||
| "skills"
|
||||
| "notification"
|
||||
@@ -29,11 +37,8 @@ type SettingsDialogProps = React.ComponentProps<typeof Dialog> & {
|
||||
defaultSection?: SettingsSection;
|
||||
};
|
||||
|
||||
export function SettingsDialog({
|
||||
defaultSection = "appearance",
|
||||
onOpenChange,
|
||||
...dialogProps
|
||||
}: SettingsDialogProps) {
|
||||
export function SettingsDialog(props: SettingsDialogProps) {
|
||||
const { defaultSection = "appearance", ...dialogProps } = props;
|
||||
const { t } = useI18n();
|
||||
const [activeSection, setActiveSection] =
|
||||
useState<SettingsSection>(defaultSection);
|
||||
@@ -50,18 +55,27 @@ export function SettingsDialog({
|
||||
label: t.settings.sections.notification,
|
||||
icon: BellIcon,
|
||||
},
|
||||
{
|
||||
id: "memory",
|
||||
label: t.settings.sections.memory,
|
||||
icon: BrainIcon,
|
||||
},
|
||||
{ id: "tools", label: t.settings.sections.tools, icon: WrenchIcon },
|
||||
{ id: "skills", label: t.settings.sections.skills, icon: SparklesIcon },
|
||||
],
|
||||
[
|
||||
t.settings.sections.appearance,
|
||||
t.settings.sections.memory,
|
||||
t.settings.sections.tools,
|
||||
t.settings.sections.skills,
|
||||
t.settings.sections.notification,
|
||||
],
|
||||
);
|
||||
return (
|
||||
<Dialog {...dialogProps} onOpenChange={onOpenChange}>
|
||||
<Dialog
|
||||
{...dialogProps}
|
||||
onOpenChange={(open) => props.onOpenChange?.(open)}
|
||||
>
|
||||
<DialogContent
|
||||
className="flex h-[75vh] max-h-[calc(100vh-2rem)] flex-col sm:max-w-5xl md:max-w-6xl"
|
||||
aria-describedby={undefined}
|
||||
@@ -100,10 +114,11 @@ export function SettingsDialog({
|
||||
<ScrollArea className="h-full min-h-0 rounded-lg border">
|
||||
<div className="space-y-8 p-6">
|
||||
{activeSection === "appearance" && <AppearanceSettingsPage />}
|
||||
{activeSection === "memory" && <MemorySettingsPage />}
|
||||
{activeSection === "tools" && <ToolSettingsPage />}
|
||||
{activeSection === "skills" && (
|
||||
<SkillSettingsPage
|
||||
onClose={() => onOpenChange?.(false)}
|
||||
onClose={() => props.onOpenChange?.(false)}
|
||||
/>
|
||||
)}
|
||||
{activeSection === "notification" && <NotificationSettingsPage />}
|
||||
|
||||
Reference in New Issue
Block a user