deer-flow/frontend/src/components/workspace/settings/memory-settings-page.tsx
LofiSu 644229f968 feat(citations): add shared citation components and optimize code
## New Features
- Add `CitationLink` shared component for rendering citation hover cards
- Add `CitationsLoadingIndicator` component for showing loading state
- Add `removeAllCitations` utility to strip all citations from content
- Add backend support for removing citations when downloading markdown files
- Add i18n support for citation loading messages (en-US, zh-CN)

## Code Optimizations
- Remove duplicate `ExternalLinkBadge` component, reuse `CitationLink` instead
- Consolidate `remarkPlugins` config in `streamdownPlugins` to avoid duplication
- Remove unused imports: `Citation`, `buildCitationMap`, `extractDomainFromUrl`, etc.
- Remove unused `messages` parameter from `ToolCall` component
- Remove unused `isWriteFile` parameter from `ArtifactFilePreview` component
- Remove unused `useI18n` hook from `MessageContent` component

## Bug Fixes
- Fix `remarkGfm` plugin configuration that prevented table rendering
- Fix React Hooks rule violation: move `useMemo` to component top level
- Replace `||` with `??` for nullish coalescing in clipboard data

## Code Cleanup
- Remove debug console.log/info statements from:
  - `threads/hooks.ts`
  - `notification/hooks.ts`
  - `memory-settings-page.tsx`
- Fix import order in `message-group.tsx`

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-04 11:56:10 +08:00

176 lines
5.7 KiB
TypeScript

"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[] = [];
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);
}