mirror of
https://gitee.com/wanwujie/deer-flow
synced 2026-04-22 13:44:46 +08:00
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>
This commit is contained in:
@@ -22,14 +22,17 @@ import {
|
||||
ChainOfThoughtStep,
|
||||
} from "@/components/ai-elements/chain-of-thought";
|
||||
import { CodeBlock } from "@/components/ai-elements/code-block";
|
||||
import { CitationsLoadingIndicator } from "@/components/ai-elements/inline-citation";
|
||||
import { MessageResponse } from "@/components/ai-elements/message";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { parseCitations } from "@/core/citations";
|
||||
import { useI18n } from "@/core/i18n/hooks";
|
||||
import {
|
||||
extractReasoningContentFromMessage,
|
||||
findToolCallResult,
|
||||
} from "@/core/messages/utils";
|
||||
import { useRehypeSplitWordsIntoSpans } from "@/core/rehype";
|
||||
import { streamdownPlugins } from "@/core/streamdown";
|
||||
import { extractTitleFromMarkdown } from "@/core/utils/markdown";
|
||||
import { env } from "@/env";
|
||||
import { cn } from "@/lib/utils";
|
||||
@@ -38,6 +41,8 @@ import { useArtifacts } from "../artifacts";
|
||||
import { FlipDisplay } from "../flip-display";
|
||||
import { Tooltip } from "../tooltip";
|
||||
|
||||
import { useThread } from "./context";
|
||||
|
||||
export function MessageGroup({
|
||||
className,
|
||||
messages,
|
||||
@@ -115,7 +120,7 @@ export function MessageGroup({
|
||||
<ChainOfThoughtStep
|
||||
key={step.id}
|
||||
label={
|
||||
<MessageResponse rehypePlugins={rehypePlugins}>
|
||||
<MessageResponse remarkPlugins={streamdownPlugins.remarkPlugins} rehypePlugins={rehypePlugins}>
|
||||
{step.reasoning ?? ""}
|
||||
</MessageResponse>
|
||||
}
|
||||
@@ -165,7 +170,7 @@ export function MessageGroup({
|
||||
<ChainOfThoughtStep
|
||||
key={lastReasoningStep.id}
|
||||
label={
|
||||
<MessageResponse rehypePlugins={rehypePlugins}>
|
||||
<MessageResponse remarkPlugins={streamdownPlugins.remarkPlugins} rehypePlugins={rehypePlugins}>
|
||||
{lastReasoningStep.reasoning ?? ""}
|
||||
</MessageResponse>
|
||||
}
|
||||
@@ -198,6 +203,13 @@ function ToolCall({
|
||||
const { t } = useI18n();
|
||||
const { setOpen, autoOpen, autoSelect, selectedArtifact, select } =
|
||||
useArtifacts();
|
||||
const { thread } = useThread();
|
||||
const threadIsLoading = thread.isLoading;
|
||||
|
||||
// Move useMemo to top level to comply with React Hooks rules
|
||||
const fileContent = typeof args.content === "string" ? args.content : "";
|
||||
const { citations } = useMemo(() => parseCitations(fileContent), [fileContent]);
|
||||
|
||||
if (name === "web_search") {
|
||||
let label: React.ReactNode = t.toolCalls.searchForRelatedInfo;
|
||||
if (typeof args.query === "string") {
|
||||
@@ -353,29 +365,42 @@ function ToolCall({
|
||||
setOpen(true);
|
||||
}, 100);
|
||||
}
|
||||
|
||||
// Check if this is a markdown file with citations
|
||||
const isMarkdown = path?.toLowerCase().endsWith(".md") || path?.toLowerCase().endsWith(".markdown");
|
||||
const hasCitationsBlock = fileContent.includes("<citations>");
|
||||
const showCitationsLoading = isMarkdown && threadIsLoading && hasCitationsBlock && isLast;
|
||||
|
||||
return (
|
||||
<ChainOfThoughtStep
|
||||
key={id}
|
||||
className="cursor-pointer"
|
||||
label={description}
|
||||
icon={NotebookPenIcon}
|
||||
onClick={() => {
|
||||
select(
|
||||
new URL(
|
||||
`write-file:${path}?message_id=${messageId}&tool_call_id=${id}`,
|
||||
).toString(),
|
||||
);
|
||||
setOpen(true);
|
||||
}}
|
||||
>
|
||||
{path && (
|
||||
<Tooltip content={t.toolCalls.clickToViewContent}>
|
||||
<ChainOfThoughtSearchResult className="cursor-pointer">
|
||||
{path}
|
||||
</ChainOfThoughtSearchResult>
|
||||
</Tooltip>
|
||||
<>
|
||||
<ChainOfThoughtStep
|
||||
key={id}
|
||||
className="cursor-pointer"
|
||||
label={description}
|
||||
icon={NotebookPenIcon}
|
||||
onClick={() => {
|
||||
select(
|
||||
new URL(
|
||||
`write-file:${path}?message_id=${messageId}&tool_call_id=${id}`,
|
||||
).toString(),
|
||||
);
|
||||
setOpen(true);
|
||||
}}
|
||||
>
|
||||
{path && (
|
||||
<Tooltip content={t.toolCalls.clickToViewContent}>
|
||||
<ChainOfThoughtSearchResult className="cursor-pointer">
|
||||
{path}
|
||||
</ChainOfThoughtSearchResult>
|
||||
</Tooltip>
|
||||
)}
|
||||
</ChainOfThoughtStep>
|
||||
{showCitationsLoading && (
|
||||
<div className="ml-8 mt-2">
|
||||
<CitationsLoadingIndicator citations={citations} />
|
||||
</div>
|
||||
)}
|
||||
</ChainOfThoughtStep>
|
||||
</>
|
||||
);
|
||||
} else if (name === "bash") {
|
||||
const description: string | undefined = (args as { description: string })
|
||||
|
||||
Reference in New Issue
Block a user