mirror of
https://gitee.com/wanwujie/deer-flow
synced 2026-04-24 06:34:46 +08:00
refactor(frontend): simplify and deduplicate Citation-related code
- Extract removeCitationsBlocks in utils, reuse in parseCitations and removeAllCitations - Add hasCitationsBlock; isCitationsBlockIncomplete now uses it - Add useParsedCitations hook (parseCitations + buildCitationMap) for message/artifact - Add CitationAwareLink to unify link rendering (message-list-item + artifact-file-detail) - Add getCleanContent helper; message-group uses it and useParsedCitations - ArtifactFileDetail: single useParsedCitations, pass cleanContent/citationMap to Preview - Stop exporting buildCitationMap and removeCitationsBlocks from citations index - Remove duplicate MessageLink and inline link logic in artifact preview Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -5,7 +5,7 @@ import { memo, useMemo } from "react";
|
||||
import rehypeKatex from "rehype-katex";
|
||||
|
||||
import {
|
||||
CitationLink,
|
||||
CitationAwareLink,
|
||||
CitationsLoadingIndicator,
|
||||
} from "@/components/ai-elements/inline-citation";
|
||||
import {
|
||||
@@ -17,11 +17,9 @@ import {
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { resolveArtifactURL } from "@/core/artifacts/utils";
|
||||
import {
|
||||
type Citation,
|
||||
buildCitationMap,
|
||||
isCitationsBlockIncomplete,
|
||||
parseCitations,
|
||||
removeAllCitations,
|
||||
useParsedCitations,
|
||||
} from "@/core/citations";
|
||||
import {
|
||||
extractContentFromMessage,
|
||||
@@ -31,11 +29,7 @@ import {
|
||||
} from "@/core/messages/utils";
|
||||
import { useRehypeSplitWordsIntoSpans } from "@/core/rehype";
|
||||
import { humanMessagePlugins, streamdownPlugins } from "@/core/streamdown";
|
||||
import {
|
||||
cn,
|
||||
externalLinkClass,
|
||||
externalLinkClassNoUnderline,
|
||||
} from "@/lib/utils";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
import { CopyButton } from "../copy-button";
|
||||
|
||||
@@ -79,49 +73,6 @@ export function MessageListItem({
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom link component that handles citations and external links
|
||||
* Only links in citationMap are rendered as CitationLink badges
|
||||
* Other links (project URLs, regular links) are rendered as plain links
|
||||
* During citation loading (streaming), non-citation links are rendered without underline so they match final citation style (p3)
|
||||
*/
|
||||
function MessageLink({
|
||||
href,
|
||||
children,
|
||||
citationMap,
|
||||
isHuman,
|
||||
isLoadingCitations,
|
||||
}: React.AnchorHTMLAttributes<HTMLAnchorElement> & {
|
||||
citationMap: Map<string, Citation>;
|
||||
isHuman: boolean;
|
||||
isLoadingCitations?: boolean;
|
||||
}) {
|
||||
if (!href) return <span>{children}</span>;
|
||||
|
||||
const citation = citationMap.get(href);
|
||||
|
||||
// Only render as CitationLink badge if it's a citation (in citationMap) and not human message
|
||||
if (citation && !isHuman) {
|
||||
return (
|
||||
<CitationLink citation={citation} href={href}>
|
||||
{children}
|
||||
</CitationLink>
|
||||
);
|
||||
}
|
||||
|
||||
const noUnderline = !isHuman && isLoadingCitations;
|
||||
return (
|
||||
<a
|
||||
href={href}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className={noUnderline ? externalLinkClassNoUnderline : externalLinkClass}
|
||||
>
|
||||
{children}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom image component that handles artifact URLs
|
||||
*/
|
||||
@@ -165,50 +116,44 @@ function MessageContent_({
|
||||
const isHuman = message.type === "human";
|
||||
const { thread_id } = useParams<{ thread_id: string }>();
|
||||
|
||||
// Extract and parse citations and uploaded files from message content
|
||||
const { citations, cleanContent, uploadedFiles, isLoadingCitations } =
|
||||
useMemo(() => {
|
||||
const reasoningContent = extractReasoningContentFromMessage(message);
|
||||
const rawContent = extractContentFromMessage(message);
|
||||
// Content to parse for citations (and optionally uploaded files)
|
||||
const { contentToParse, uploadedFiles, isLoadingCitations } = useMemo(() => {
|
||||
const reasoningContent = extractReasoningContentFromMessage(message);
|
||||
const rawContent = extractContentFromMessage(message);
|
||||
|
||||
// When only reasoning content exists (no main content), also parse citations
|
||||
if (!isLoading && reasoningContent && !rawContent) {
|
||||
const { citations, cleanContent } = parseCitations(reasoningContent);
|
||||
return {
|
||||
citations,
|
||||
cleanContent,
|
||||
uploadedFiles: [],
|
||||
isLoadingCitations: false,
|
||||
};
|
||||
}
|
||||
if (!isLoading && reasoningContent && !rawContent) {
|
||||
return {
|
||||
contentToParse: reasoningContent,
|
||||
uploadedFiles: [] as UploadedFile[],
|
||||
isLoadingCitations: false,
|
||||
};
|
||||
}
|
||||
|
||||
// For human messages, parse uploaded files first
|
||||
if (isHuman && rawContent) {
|
||||
const { files, cleanContent: contentWithoutFiles } =
|
||||
parseUploadedFiles(rawContent);
|
||||
const { citations, cleanContent: finalContent } =
|
||||
parseCitations(contentWithoutFiles);
|
||||
return {
|
||||
citations,
|
||||
cleanContent: finalContent,
|
||||
uploadedFiles: files,
|
||||
isLoadingCitations: false,
|
||||
};
|
||||
}
|
||||
if (isHuman && rawContent) {
|
||||
const { files, cleanContent: contentWithoutFiles } =
|
||||
parseUploadedFiles(rawContent);
|
||||
return {
|
||||
contentToParse: contentWithoutFiles,
|
||||
uploadedFiles: files,
|
||||
isLoadingCitations: false,
|
||||
};
|
||||
}
|
||||
|
||||
const { citations, cleanContent } = parseCitations(rawContent ?? "");
|
||||
const isLoadingCitations =
|
||||
isLoading && isCitationsBlockIncomplete(rawContent ?? "");
|
||||
return {
|
||||
contentToParse: rawContent ?? "",
|
||||
uploadedFiles: [] as UploadedFile[],
|
||||
isLoadingCitations:
|
||||
isLoading && isCitationsBlockIncomplete(rawContent ?? ""),
|
||||
};
|
||||
}, [isLoading, message, isHuman]);
|
||||
|
||||
return { citations, cleanContent, uploadedFiles: [], isLoadingCitations };
|
||||
}, [isLoading, message, isHuman]);
|
||||
|
||||
const citationMap = useMemo(() => buildCitationMap(citations), [citations]);
|
||||
const { citations, cleanContent, citationMap } =
|
||||
useParsedCitations(contentToParse);
|
||||
|
||||
// Shared markdown components
|
||||
const markdownComponents = useMemo(() => ({
|
||||
a: (props: React.AnchorHTMLAttributes<HTMLAnchorElement>) => (
|
||||
<MessageLink
|
||||
<CitationAwareLink
|
||||
{...props}
|
||||
citationMap={citationMap}
|
||||
isHuman={isHuman}
|
||||
|
||||
Reference in New Issue
Block a user