feat(frontend): unify citation logic and prevent half-finished citations

- Add SafeCitationContent as single component for citation-aware body:
  useParsedCitations + shouldShowCitationLoading; show loading until
  citations complete, then render body with createCitationMarkdownComponents.
  Supports optional remarkPlugins, rehypePlugins, isHuman, img.

- Refactor MessageListItem: assistant message body now uses
  SafeCitationContent only; remove duplicate useParsedCitations,
  shouldShowCitationLoading, createCitationMarkdownComponents and
  CitationsLoadingIndicator logic. Human messages keep plain
  AIElementMessageResponse (no citation parsing).

- Use SafeCitationContent for clarification, present-files (message-list),
  thinking steps and write_file loading (message-group), subtask result
  (subtask-card). Artifact markdown preview keeps same guard
  (shouldShowCitationLoading) with ArtifactFilePreview.

- Unify loading condition: shouldShowCitationLoading(rawContent,
  cleanContent, isLoading) is the single source of truth. Show loading when
  (isLoading && hasCitationsBlock(rawContent)) or when
  (hasCitationsBlock(rawContent) && hasUnreplacedCitationRefs(cleanContent))
  so Pro/Ultra modes also show "loading citations" and half-finished
  [cite-N] never appear.

- message-group write_file: replace hasCitationsBlock + threadIsLoading
  with shouldShowCitationLoading(fileContent, cleanContent,
  threadIsLoading && isLast) for consistency.

- citations/utils: parse incomplete <citations> during streaming;
  remove isCitationsBlockIncomplete; keep hasUnreplacedCitationRefs
  internal; document display rule in file header.

Co-authored-by: Cursor <cursoragent@cursor.com>

---
feat(前端): 统一引用逻辑并杜绝半成品引用

- 新增 SafeCitationContent 作为引用正文的唯一出口:内部使用
  useParsedCitations + shouldShowCitationLoading,在引用未就绪时只显示
  「正在整理引用」,就绪后用 createCitationMarkdownComponents 渲染正文;
  支持可选 remarkPlugins、rehypePlugins、isHuman、img。

- 重构 MessageListItem:助手消息正文仅通过 SafeCitationContent 渲染,
  删除重复的 useParsedCitations、shouldShowCitationLoading、
  createCitationMarkdownComponents、CitationsLoadingIndicator 等逻辑;
  用户消息仍用 AIElementMessageResponse,不做引用解析。

- 澄清、present-files(message-list)、思考步骤与 write_file 加载
  (message-group)、子任务结果(subtask-card)均使用
  SafeCitationContent;Artifact 的 markdown 预览仍用同一 guard
  shouldShowCitationLoading,正文由 ArtifactFilePreview 渲染。

- 统一加载条件:shouldShowCitationLoading(rawContent, cleanContent,
  isLoading) 为唯一判断。在「流式中且已有引用块」或「有引用块且
  cleanContent 中仍有未替换的 [cite-N]」时仅显示加载,从而在 Pro/Ultra
  下也能看到「正在整理引用」,且永不出现半成品 [cite-N]。

- message-group 的 write_file:用 shouldShowCitationLoading(
  fileContent, cleanContent, threadIsLoading && isLast) 替代
  hasCitationsBlock + threadIsLoading,与其他场景一致。

- citations/utils:流式时解析未闭合的 <citations>;移除
  isCitationsBlockIncomplete;hasUnreplacedCitationRefs 保持内部使用;
  在文件头注释中说明展示规则。
This commit is contained in:
ruitanglin
2026-02-09 15:01:51 +08:00
parent 79c85d6410
commit a4268cb6d3
9 changed files with 309 additions and 161 deletions

View File

@@ -12,7 +12,14 @@ import {
externalLinkClassNoUnderline,
} from "@/lib/utils";
import { ExternalLinkIcon } from "lucide-react";
import { type ComponentProps, Children } from "react";
import {
type AnchorHTMLAttributes,
type ComponentProps,
type ImgHTMLAttributes,
type ReactElement,
type ReactNode,
Children,
} from "react";
import type { Citation } from "@/core/citations";
import {
extractDomainFromUrl,
@@ -202,6 +209,48 @@ export const CitationAwareLink = ({
);
};
/**
* Options for creating markdown components that render links as citations.
* Used by message list (all modes: Flash/Thinking/Pro/Ultra), artifact preview, and CoT.
*/
export type CreateCitationMarkdownComponentsOptions = {
citationMap: Map<string, Citation>;
isHuman?: boolean;
isLoadingCitations?: boolean;
syntheticExternal?: boolean;
/** Optional custom img component (e.g. MessageImage with threadId). Omit for artifact. */
img?: (props: ImgHTMLAttributes<HTMLImageElement> & { threadId?: string; maxWidth?: string }) => ReactNode;
};
/**
* Create markdown `components` (a, optional img) that use CitationAwareLink.
* Reused across message-list-item (all modes), artifact-file-detail, and any CoT markdown.
*/
export function createCitationMarkdownComponents(
options: CreateCitationMarkdownComponentsOptions,
): {
a: (props: AnchorHTMLAttributes<HTMLAnchorElement>) => ReactElement;
img?: (props: ImgHTMLAttributes<HTMLImageElement> & { threadId?: string; maxWidth?: string }) => ReactNode;
} {
const {
citationMap,
isHuman = false,
isLoadingCitations = false,
syntheticExternal = false,
img,
} = options;
const a = (props: AnchorHTMLAttributes<HTMLAnchorElement>) => (
<CitationAwareLink
{...props}
citationMap={citationMap}
isHuman={isHuman}
isLoadingCitations={isLoadingCitations}
syntheticExternal={syntheticExternal}
/>
);
return img ? { a, img } : { a };
}
/**
* Shared CitationsLoadingIndicator component
* Used across message-list-item and message-group to show loading citations