From 509ea874f778d28092c518f3235ab5e858e0876d Mon Sep 17 00:00:00 2001 From: ruitanglin Date: Mon, 9 Feb 2026 04:03:15 +0800 Subject: [PATCH] fix(frontend): citations display + refactor link/citation utils MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Citations: no underline while streaming (message links); artifact markdown external links as citation cards - Refactor: add isExternalUrl, syntheticCitationFromLink in core/citations; shared externalLinkClass in lib/utils; simplify message-list-item and artifact-file-detail link rendering 修复引用展示并抽离链接/引用工具 - 引用:流式输出时链接不这下划线;Artifact 内 Markdown 外链以引用卡片展示 - 重构:core/citations 新增 isExternalUrl、syntheticCitationFromLink;lib/utils 共享 externalLinkClass;精简消息与 Artifact 中的链接渲染逻辑 Co-authored-by: Cursor --- .../artifacts/artifact-file-detail.tsx | 29 +++++++++++++------ .../workspace/messages/message-list-item.tsx | 24 +++++++++++---- frontend/src/core/citations/index.ts | 4 ++- frontend/src/core/citations/utils.ts | 19 ++++++++++++ frontend/src/lib/utils.ts | 12 ++++++-- 5 files changed, 69 insertions(+), 19 deletions(-) diff --git a/frontend/src/components/workspace/artifacts/artifact-file-detail.tsx b/frontend/src/components/workspace/artifacts/artifact-file-detail.tsx index e4315f3..1178899 100644 --- a/frontend/src/components/workspace/artifacts/artifact-file-detail.tsx +++ b/frontend/src/components/workspace/artifacts/artifact-file-detail.tsx @@ -35,15 +35,17 @@ import { useArtifactContent } from "@/core/artifacts/hooks"; import { urlOfArtifact } from "@/core/artifacts/utils"; import { buildCitationMap, + isExternalUrl, parseCitations, removeAllCitations, + syntheticCitationFromLink, } from "@/core/citations"; import { useI18n } from "@/core/i18n/hooks"; import { installSkill } from "@/core/skills/api"; import { streamdownPlugins } from "@/core/streamdown"; import { checkCodeFile, getFileName } from "@/core/utils/files"; import { env } from "@/env"; -import { cn } from "@/lib/utils"; +import { cn, externalLinkClass } from "@/lib/utils"; import { Tooltip } from "../tooltip"; @@ -309,11 +311,7 @@ export function ArtifactFilePreview({ href, children, }: React.AnchorHTMLAttributes) => { - if (!href) { - return {children}; - } - - // Only render as CitationLink badge if it's a citation (in citationMap) + if (!href) return {children}; const citation = citationMap.get(href); if (citation) { return ( @@ -322,14 +320,27 @@ export function ArtifactFilePreview({ ); } - - // All other links (including project URLs) render as plain links + if (isExternalUrl(href)) { + const linkText = + typeof children === "string" + ? children + : String(React.Children.toArray(children).join("")).trim() || + href; + return ( + + {children} + + ); + } return ( {children} diff --git a/frontend/src/components/workspace/messages/message-list-item.tsx b/frontend/src/components/workspace/messages/message-list-item.tsx index 7d5cc0d..7858951 100644 --- a/frontend/src/components/workspace/messages/message-list-item.tsx +++ b/frontend/src/components/workspace/messages/message-list-item.tsx @@ -31,7 +31,11 @@ import { } from "@/core/messages/utils"; import { useRehypeSplitWordsIntoSpans } from "@/core/rehype"; import { humanMessagePlugins, streamdownPlugins } from "@/core/streamdown"; -import { cn } from "@/lib/utils"; +import { + cn, + externalLinkClass, + externalLinkClassNoUnderline, +} from "@/lib/utils"; import { CopyButton } from "../copy-button"; @@ -79,20 +83,23 @@ 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 & { citationMap: Map; isHuman: boolean; + isLoadingCitations?: boolean; }) { if (!href) return {children}; 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 ( @@ -102,13 +109,13 @@ function MessageLink({ ); } - // All other links render as plain links + const noUnderline = !isHuman && isLoadingCitations; return ( {children} @@ -201,12 +208,17 @@ function MessageContent_({ // Shared markdown components const markdownComponents = useMemo(() => ({ a: (props: React.AnchorHTMLAttributes) => ( - + ), img: (props: React.ImgHTMLAttributes) => ( ), - }), [citationMap, thread_id, isHuman]); + }), [citationMap, thread_id, isHuman, isLoadingCitations]); // Render message response // Human messages use humanMessagePlugins (no autolink) to prevent URL bleeding into adjacent text diff --git a/frontend/src/core/citations/index.ts b/frontend/src/core/citations/index.ts index fd2a2aa..3efa83c 100644 --- a/frontend/src/core/citations/index.ts +++ b/frontend/src/core/citations/index.ts @@ -1,9 +1,11 @@ export { - parseCitations, buildCitationMap, extractDomainFromUrl, isCitationsBlockIncomplete, + isExternalUrl, + parseCitations, removeAllCitations, + syntheticCitationFromLink, } from "./utils"; export type { Citation, ParseCitationsResult } from "./utils"; diff --git a/frontend/src/core/citations/utils.ts b/frontend/src/core/citations/utils.ts index 699900b..1937af1 100644 --- a/frontend/src/core/citations/utils.ts +++ b/frontend/src/core/citations/utils.ts @@ -118,6 +118,25 @@ export function buildCitationMap( return map; } +/** + * Whether the URL is external (http/https). + */ +export function isExternalUrl(url: string): boolean { + return url.startsWith("http://") || url.startsWith("https://"); +} + +/** + * Build a synthetic Citation from a link (e.g. in artifact markdown without block). + */ +export function syntheticCitationFromLink(href: string, title: string): Citation { + return { + id: `artifact-cite-${href}`, + title: title || href, + url: href, + snippet: "", + }; +} + /** * Extract the domain name from a URL for display * diff --git a/frontend/src/lib/utils.ts b/frontend/src/lib/utils.ts index bd0c391..a414622 100644 --- a/frontend/src/lib/utils.ts +++ b/frontend/src/lib/utils.ts @@ -1,6 +1,12 @@ -import { clsx, type ClassValue } from "clsx" -import { twMerge } from "tailwind-merge" +import { clsx, type ClassValue } from "clsx"; +import { twMerge } from "tailwind-merge"; export function cn(...inputs: ClassValue[]) { - return twMerge(clsx(inputs)) + return twMerge(clsx(inputs)); } + +/** Shared class for external links (underline by default). */ +export const externalLinkClass = + "text-primary underline underline-offset-2 hover:no-underline"; +/** For streaming / loading state when link may be a citation (no underline). */ +export const externalLinkClassNoUnderline = "text-primary hover:underline";