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:
@@ -0,0 +1,74 @@
|
||||
"use client";
|
||||
|
||||
import type { ImgHTMLAttributes } from "react";
|
||||
import { useMemo } from "react";
|
||||
|
||||
import {
|
||||
CitationsLoadingIndicator,
|
||||
createCitationMarkdownComponents,
|
||||
} from "@/components/ai-elements/inline-citation";
|
||||
import {
|
||||
MessageResponse,
|
||||
type MessageResponseProps,
|
||||
} from "@/components/ai-elements/message";
|
||||
import {
|
||||
shouldShowCitationLoading,
|
||||
useParsedCitations,
|
||||
} from "@/core/citations";
|
||||
import { streamdownPlugins } from "@/core/streamdown";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export type SafeCitationContentProps = {
|
||||
content: string;
|
||||
isLoading: boolean;
|
||||
rehypePlugins: MessageResponseProps["rehypePlugins"];
|
||||
className?: string;
|
||||
remarkPlugins?: MessageResponseProps["remarkPlugins"];
|
||||
isHuman?: boolean;
|
||||
img?: (props: ImgHTMLAttributes<HTMLImageElement> & { threadId?: string; maxWidth?: string }) => React.ReactNode;
|
||||
};
|
||||
|
||||
/** Single place for citation-aware body: shows loading until citations complete (no half-finished refs), else body. */
|
||||
export function SafeCitationContent({
|
||||
content,
|
||||
isLoading,
|
||||
rehypePlugins,
|
||||
className,
|
||||
remarkPlugins = streamdownPlugins.remarkPlugins,
|
||||
isHuman = false,
|
||||
img,
|
||||
}: SafeCitationContentProps) {
|
||||
const { citations, cleanContent, citationMap } = useParsedCitations(content);
|
||||
const showLoading = shouldShowCitationLoading(content, cleanContent, isLoading);
|
||||
|
||||
if (showLoading) {
|
||||
return (
|
||||
<CitationsLoadingIndicator
|
||||
citations={citations}
|
||||
className={cn("my-2", className)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (!cleanContent) return null;
|
||||
|
||||
const components = useMemo(
|
||||
() =>
|
||||
createCitationMarkdownComponents({
|
||||
citationMap,
|
||||
isHuman,
|
||||
isLoadingCitations: false,
|
||||
img,
|
||||
}),
|
||||
[citationMap, isHuman, img],
|
||||
);
|
||||
return (
|
||||
<MessageResponse
|
||||
className={className}
|
||||
remarkPlugins={remarkPlugins}
|
||||
rehypePlugins={rehypePlugins}
|
||||
components={components}
|
||||
>
|
||||
{cleanContent}
|
||||
</MessageResponse>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user