mirror of
https://gitee.com/wanwujie/deer-flow
synced 2026-04-24 14:44: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:
@@ -1,6 +1,7 @@
|
||||
export {
|
||||
buildCitationMap,
|
||||
extractDomainFromUrl,
|
||||
getCleanContent,
|
||||
hasCitationsBlock,
|
||||
isCitationsBlockIncomplete,
|
||||
isExternalUrl,
|
||||
parseCitations,
|
||||
@@ -8,4 +9,6 @@ export {
|
||||
syntheticCitationFromLink,
|
||||
} from "./utils";
|
||||
|
||||
export { useParsedCitations } from "./use-parsed-citations";
|
||||
export type { UseParsedCitationsResult } from "./use-parsed-citations";
|
||||
export type { Citation, ParseCitationsResult } from "./utils";
|
||||
|
||||
28
frontend/src/core/citations/use-parsed-citations.ts
Normal file
28
frontend/src/core/citations/use-parsed-citations.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
"use client";
|
||||
|
||||
import { useMemo } from "react";
|
||||
|
||||
import { buildCitationMap, parseCitations } from "./utils";
|
||||
import type { Citation } from "./utils";
|
||||
|
||||
export interface UseParsedCitationsResult {
|
||||
citations: Citation[];
|
||||
cleanContent: string;
|
||||
citationMap: Map<string, Citation>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse content for citations and build citation map. Memoized by content.
|
||||
* Use in message and artifact components to avoid repeating parseCitations + buildCitationMap.
|
||||
*/
|
||||
export function useParsedCitations(content: string): UseParsedCitationsResult {
|
||||
return useMemo(() => {
|
||||
const parsed = parseCitations(content ?? "");
|
||||
const citationMap = buildCitationMap(parsed.citations);
|
||||
return {
|
||||
citations: parsed.citations,
|
||||
cleanContent: parsed.cleanContent,
|
||||
citationMap,
|
||||
};
|
||||
}, [content]);
|
||||
}
|
||||
@@ -67,14 +67,7 @@ export function parseCitations(content: string): ParseCitationsResult {
|
||||
}
|
||||
}
|
||||
|
||||
// Remove ALL citations blocks from content (both complete and incomplete)
|
||||
cleanContent = content.replace(/<citations>[\s\S]*?<\/citations>/g, "").trim();
|
||||
|
||||
// Also remove incomplete citations blocks (during streaming)
|
||||
// Match <citations> without closing tag or <citations> followed by anything until end of string
|
||||
if (cleanContent.includes("<citations>")) {
|
||||
cleanContent = cleanContent.replace(/<citations>[\s\S]*$/g, "").trim();
|
||||
}
|
||||
cleanContent = removeCitationsBlocks(content);
|
||||
|
||||
// Convert [cite-N] references to markdown links
|
||||
// Example: [cite-1] -> [Title](url)
|
||||
@@ -102,6 +95,13 @@ export function parseCitations(content: string): ParseCitationsResult {
|
||||
return { citations, cleanContent };
|
||||
}
|
||||
|
||||
/**
|
||||
* Return content with citations block removed and [cite-N] replaced by markdown links.
|
||||
*/
|
||||
export function getCleanContent(content: string): string {
|
||||
return parseCitations(content ?? "").cleanContent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a map from URL to Citation for quick lookup
|
||||
*
|
||||
@@ -153,6 +153,26 @@ export function extractDomainFromUrl(url: string): string {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove all <citations> blocks from content (complete and incomplete).
|
||||
* Does not remove [cite-N] or markdown links; use removeAllCitations for that.
|
||||
*/
|
||||
export function removeCitationsBlocks(content: string): string {
|
||||
if (!content) return content;
|
||||
let result = content.replace(/<citations>[\s\S]*?<\/citations>/g, "").trim();
|
||||
if (result.includes("<citations>")) {
|
||||
result = result.replace(/<citations>[\s\S]*$/g, "").trim();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether content contains a <citations> block (open tag).
|
||||
*/
|
||||
export function hasCitationsBlock(content: string): boolean {
|
||||
return Boolean(content?.includes("<citations>"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if content is still receiving the citations block (streaming)
|
||||
* This helps determine if we should wait before parsing
|
||||
@@ -161,15 +181,7 @@ export function extractDomainFromUrl(url: string): string {
|
||||
* @returns true if citations block appears to be incomplete
|
||||
*/
|
||||
export function isCitationsBlockIncomplete(content: string): boolean {
|
||||
if (!content) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if we have an opening tag but no closing tag
|
||||
const hasOpenTag = content.includes("<citations>");
|
||||
const hasCloseTag = content.includes("</citations>");
|
||||
|
||||
return hasOpenTag && !hasCloseTag;
|
||||
return hasCitationsBlock(content) && !content.includes("</citations>");
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -188,11 +200,8 @@ export function removeAllCitations(content: string): string {
|
||||
return content;
|
||||
}
|
||||
|
||||
let result = content;
|
||||
|
||||
// Step 1: Remove all <citations> blocks (complete and incomplete)
|
||||
result = result.replace(/<citations>[\s\S]*?<\/citations>/g, "");
|
||||
result = result.replace(/<citations>[\s\S]*$/g, "");
|
||||
let result = removeCitationsBlocks(content);
|
||||
|
||||
// Step 2: Remove all [cite-N] references
|
||||
result = result.replace(/\[cite-\d+\]/g, "");
|
||||
|
||||
Reference in New Issue
Block a user