From 59c8fec7e73e44076bf68033a00a8e4e12e47259 Mon Sep 17 00:00:00 2001 From: ruitanglin Date: Mon, 9 Feb 2026 15:58:59 +0800 Subject: [PATCH] refactor(frontend): consolidate citation logic, slim exports and impl MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - SafeCitationContent: add loadingOnly and renderBody props. - loadingOnly: show only loading indicator or null (e.g. write_file step). - renderBody(parsed): custom body renderer (e.g. artifact preview). - message-group write_file: use SafeCitationContent(content, isLoading, rehypePlugins, loadingOnly) instead of local useParsedCitations + shouldShowCitationLoading + CitationsLoadingIndicator. Pass rehypePlugins into ToolCall. - artifact-file-detail markdown preview: use SafeCitationContent with renderBody((p) => ). Remove local shouldShowCitationLoading and CitationsLoadingIndicator branch. - core/citations: inline buildCitationMap into use-parsed-citations, remove from utils; stop exporting hasCitationsBlock (internal to shouldShowCitationLoading). - inline-citation: make InlineCitationCard, InlineCitationCardBody, InlineCitationSource file-private (no longer exported). Co-authored-by: Cursor --- refactor(前端): 收拢引用逻辑、精简导出与实现 - SafeCitationContent 新增 loadingOnly、renderBody。 - loadingOnly:仅显示加载或 null(如 write_file 步骤)。 - renderBody(parsed):自定义正文渲染(如 artifact 预览)。 - message-group write_file:改用 SafeCitationContent(loadingOnly),去掉 本地 useParsedCitations + shouldShowCitationLoading + CitationsLoadingIndicator, 并向 ToolCall 传入 rehypePlugins。 - artifact-file-detail 的 markdown 预览:改用 SafeCitationContent + renderBody 渲染 ArtifactFilePreview,去掉本地加载判断与 CitationsLoadingIndicator 分支。 - core/citations:buildCitationMap 内联到 use-parsed-citations 并从 utils 删除;hasCitationsBlock 不再导出(仅 shouldShowCitationLoading 内部使用)。 - inline-citation:InlineCitationCard/Body/Source 改为文件内私有,不再导出。 --- .../ai-elements/inline-citation.tsx | 24 +++++------ .../artifacts/artifact-file-detail.tsx | 43 ++++++++----------- .../workspace/messages/message-group.tsx | 32 +++++++------- .../messages/safe-citation-content.tsx | 37 ++++++++++------ frontend/src/core/citations/index.ts | 1 - .../core/citations/use-parsed-citations.ts | 6 +-- frontend/src/core/citations/utils.ts | 16 ------- 7 files changed, 73 insertions(+), 86 deletions(-) diff --git a/frontend/src/components/ai-elements/inline-citation.tsx b/frontend/src/components/ai-elements/inline-citation.tsx index d60ac07..506e428 100644 --- a/frontend/src/components/ai-elements/inline-citation.tsx +++ b/frontend/src/components/ai-elements/inline-citation.tsx @@ -29,35 +29,31 @@ import { import { Shimmer } from "./shimmer"; import { useI18n } from "@/core/i18n/hooks"; -export type InlineCitationCardProps = ComponentProps; +type InlineCitationCardProps = ComponentProps; -export const InlineCitationCard = (props: InlineCitationCardProps) => ( +const InlineCitationCard = (props: InlineCitationCardProps) => ( ); -export type InlineCitationCardBodyProps = ComponentProps<"div">; - -export const InlineCitationCardBody = ({ +const InlineCitationCardBody = ({ className, ...props -}: InlineCitationCardBodyProps) => ( +}: ComponentProps<"div">) => ( ); -export type InlineCitationSourceProps = ComponentProps<"div"> & { - title?: string; - url?: string; - description?: string; -}; - -export const InlineCitationSource = ({ +const InlineCitationSource = ({ title, url, description, className, children, ...props -}: InlineCitationSourceProps) => ( +}: ComponentProps<"div"> & { + title?: string; + url?: string; + description?: string; +}) => (
{title && (

{title}

diff --git a/frontend/src/components/workspace/artifacts/artifact-file-detail.tsx b/frontend/src/components/workspace/artifacts/artifact-file-detail.tsx index d459e6a..2bfae75 100644 --- a/frontend/src/components/workspace/artifacts/artifact-file-detail.tsx +++ b/frontend/src/components/workspace/artifacts/artifact-file-detail.tsx @@ -32,13 +32,11 @@ import { import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"; import { CodeEditor } from "@/components/workspace/code-editor"; import { useArtifactContent } from "@/core/artifacts/hooks"; -import { CitationsLoadingIndicator } from "@/components/ai-elements/inline-citation"; import { urlOfArtifact } from "@/core/artifacts/utils"; import type { Citation } from "@/core/citations"; import { contentWithoutCitationsFromParsed, removeAllCitations, - shouldShowCitationLoading, useParsedCitations, } from "@/core/citations"; import { useI18n } from "@/core/i18n/hooks"; @@ -50,6 +48,7 @@ import { cn } from "@/lib/utils"; import { Tooltip } from "../tooltip"; +import { SafeCitationContent } from "../messages/safe-citation-content"; import { useThread } from "../messages/context"; import { useArtifacts } from "./context"; @@ -252,31 +251,27 @@ export function ArtifactFileDetail({
- {previewable && viewMode === "preview" && ( + {previewable && + viewMode === "preview" && language === "markdown" && - content && - shouldShowCitationLoading( - content, - parsed.cleanContent, - thread.isLoading, - ) ? ( -
- -
- ) : ( - ( + + )} /> - ) - )} + )} {isCodeFile && viewMode === "code" && ( ) : ( - + ), )} {lastToolCallStep && ( @@ -140,6 +143,7 @@ export function MessageGroup({ {...lastToolCallStep} isLast={true} isLoading={isLoading} + rehypePlugins={rehypePlugins} /> )} @@ -197,6 +201,7 @@ function ToolCall({ result, isLast = false, isLoading = false, + rehypePlugins, }: { id?: string; messageId?: string; @@ -205,6 +210,7 @@ function ToolCall({ result?: string | Record; isLast?: boolean; isLoading?: boolean; + rehypePlugins: ReturnType; }) { const { t } = useI18n(); const { setOpen, autoOpen, autoSelect, selectedArtifact, select } = @@ -213,7 +219,6 @@ function ToolCall({ const threadIsLoading = thread.isLoading; const fileContent = typeof args.content === "string" ? args.content : ""; - const { citations, cleanContent } = useParsedCitations(fileContent); if (name === "web_search") { let label: React.ReactNode = t.toolCalls.searchForRelatedInfo; @@ -362,13 +367,6 @@ function ToolCall({ const isMarkdown = path?.toLowerCase().endsWith(".md") || path?.toLowerCase().endsWith(".markdown"); - const showCitationsLoading = - isMarkdown && - shouldShowCitationLoading( - fileContent, - cleanContent, - threadIsLoading && isLast, - ); return ( <> @@ -392,10 +390,14 @@ function ToolCall({ )} - {showCitationsLoading && ( -
- -
+ {isMarkdown && ( + )} ); diff --git a/frontend/src/components/workspace/messages/safe-citation-content.tsx b/frontend/src/components/workspace/messages/safe-citation-content.tsx index 36d8fec..e453b89 100644 --- a/frontend/src/components/workspace/messages/safe-citation-content.tsx +++ b/frontend/src/components/workspace/messages/safe-citation-content.tsx @@ -1,6 +1,7 @@ "use client"; import type { ImgHTMLAttributes } from "react"; +import type { ReactNode } from "react"; import { useMemo } from "react"; import { @@ -14,6 +15,7 @@ import { import { shouldShowCitationLoading, useParsedCitations, + type UseParsedCitationsResult, } from "@/core/citations"; import { streamdownPlugins } from "@/core/streamdown"; import { cn } from "@/lib/utils"; @@ -25,7 +27,11 @@ export type SafeCitationContentProps = { className?: string; remarkPlugins?: MessageResponseProps["remarkPlugins"]; isHuman?: boolean; - img?: (props: ImgHTMLAttributes & { threadId?: string; maxWidth?: string }) => React.ReactNode; + img?: (props: ImgHTMLAttributes & { threadId?: string; maxWidth?: string }) => ReactNode; + /** When true, only show loading indicator or null (e.g. write_file step). */ + loadingOnly?: boolean; + /** When set, use instead of default MessageResponse (e.g. artifact preview). */ + renderBody?: (parsed: UseParsedCitationsResult) => ReactNode; }; /** Single place for citation-aware body: shows loading until citations complete (no half-finished refs), else body. */ @@ -37,20 +43,12 @@ export function SafeCitationContent({ remarkPlugins = streamdownPlugins.remarkPlugins, isHuman = false, img, + loadingOnly = false, + renderBody, }: SafeCitationContentProps) { - const { citations, cleanContent, citationMap } = useParsedCitations(content); + const parsed = useParsedCitations(content); + const { citations, cleanContent, citationMap } = parsed; const showLoading = shouldShowCitationLoading(content, cleanContent, isLoading); - - if (showLoading) { - return ( - - ); - } - if (!cleanContent) return null; - const components = useMemo( () => createCitationMarkdownComponents({ @@ -61,6 +59,19 @@ export function SafeCitationContent({ }), [citationMap, isHuman, img], ); + + if (showLoading) { + return ( + + ); + } + if (loadingOnly) return null; + if (renderBody) return renderBody(parsed); + if (!cleanContent) return null; + return ( { const parsed = parseCitations(content ?? ""); - const citationMap = buildCitationMap(parsed.citations); + const citationMap = new Map(); + for (const c of parsed.citations) citationMap.set(c.url, c); return { citations: parsed.citations, cleanContent: parsed.cleanContent, diff --git a/frontend/src/core/citations/utils.ts b/frontend/src/core/citations/utils.ts index bcca859..fb5cded 100644 --- a/frontend/src/core/citations/utils.ts +++ b/frontend/src/core/citations/utils.ts @@ -120,22 +120,6 @@ export function parseCitations(content: string): ParseCitationsResult { return { citations, cleanContent }; } -/** - * Build a map from URL to Citation for quick lookup - * - * @param citations - Array of citations - * @returns Map with URL as key and Citation as value - */ -export function buildCitationMap( - citations: Citation[], -): Map { - const map = new Map(); - for (const citation of citations) { - map.set(citation.url, citation); - } - return map; -} - /** * Whether the URL is external (http/https). */