refactor(frontend): consolidate citation logic, slim exports and impl

- 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) => <ArtifactFilePreview ... cleanContent={p.cleanContent}
  citationMap={p.citationMap} />). 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 <cursoragent@cursor.com>

---
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 改为文件内私有,不再导出。
This commit is contained in:
LofiSu
2026-02-09 15:58:59 +08:00
parent d9a86c10e8
commit cef8d389fd
7 changed files with 73 additions and 86 deletions
@@ -22,10 +22,8 @@ import {
ChainOfThoughtStep,
} from "@/components/ai-elements/chain-of-thought";
import { CodeBlock } from "@/components/ai-elements/code-block";
import { CitationsLoadingIndicator } from "@/components/ai-elements/inline-citation";
import { MessageResponse } from "@/components/ai-elements/message";
import { Button } from "@/components/ui/button";
import { shouldShowCitationLoading, useParsedCitations } from "@/core/citations";
import { useI18n } from "@/core/i18n/hooks";
import {
extractReasoningContentFromMessage,
@@ -130,7 +128,12 @@ export function MessageGroup({
}
></ChainOfThoughtStep>
) : (
<ToolCall key={step.id} {...step} isLoading={isLoading} />
<ToolCall
key={step.id}
{...step}
isLoading={isLoading}
rehypePlugins={rehypePlugins}
/>
),
)}
{lastToolCallStep && (
@@ -140,6 +143,7 @@ export function MessageGroup({
{...lastToolCallStep}
isLast={true}
isLoading={isLoading}
rehypePlugins={rehypePlugins}
/>
</FlipDisplay>
)}
@@ -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<string, unknown>;
isLast?: boolean;
isLoading?: boolean;
rehypePlugins: ReturnType<typeof useRehypeSplitWordsIntoSpans>;
}) {
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({
</ChainOfThoughtSearchResult>
)}
</ChainOfThoughtStep>
{showCitationsLoading && (
<div className="mt-2 ml-8">
<CitationsLoadingIndicator citations={citations} />
</div>
{isMarkdown && (
<SafeCitationContent
content={fileContent}
isLoading={threadIsLoading && isLast}
rehypePlugins={rehypePlugins}
loadingOnly
className="mt-2 ml-8"
/>
)}
</>
);