2026-01-17 15:19:53 +08:00
|
|
|
import {
|
2026-01-24 23:51:11 +08:00
|
|
|
Code2Icon,
|
2026-01-17 15:19:53 +08:00
|
|
|
CopyIcon,
|
|
|
|
|
DownloadIcon,
|
2026-01-24 23:51:11 +08:00
|
|
|
EyeIcon,
|
2026-01-31 22:10:05 +08:00
|
|
|
LoaderIcon,
|
2026-01-31 11:08:27 +08:00
|
|
|
PackageIcon,
|
2026-01-17 15:19:53 +08:00
|
|
|
SquareArrowOutUpRightIcon,
|
|
|
|
|
XIcon,
|
|
|
|
|
} from "lucide-react";
|
2026-01-29 12:29:13 +08:00
|
|
|
import * as React from "react";
|
2026-01-31 22:10:05 +08:00
|
|
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
2026-01-17 15:09:44 +08:00
|
|
|
import { toast } from "sonner";
|
2026-01-19 19:41:46 +08:00
|
|
|
import { Streamdown } from "streamdown";
|
2026-01-17 00:05:19 +08:00
|
|
|
|
2026-01-17 15:09:44 +08:00
|
|
|
import {
|
|
|
|
|
Artifact,
|
|
|
|
|
ArtifactAction,
|
|
|
|
|
ArtifactActions,
|
|
|
|
|
ArtifactContent,
|
|
|
|
|
ArtifactHeader,
|
|
|
|
|
ArtifactTitle,
|
|
|
|
|
} from "@/components/ai-elements/artifact";
|
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 保持内部使用;
在文件头注释中说明展示规则。
2026-02-09 15:01:51 +08:00
|
|
|
import { createCitationMarkdownComponents } from "@/components/ai-elements/inline-citation";
|
2026-01-17 17:21:37 +08:00
|
|
|
import { Select, SelectItem } from "@/components/ui/select";
|
|
|
|
|
import {
|
|
|
|
|
SelectContent,
|
|
|
|
|
SelectGroup,
|
|
|
|
|
SelectTrigger,
|
|
|
|
|
SelectValue,
|
|
|
|
|
} from "@/components/ui/select";
|
2026-01-19 19:41:46 +08:00
|
|
|
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
|
2026-01-21 10:46:43 +08:00
|
|
|
import { CodeEditor } from "@/components/workspace/code-editor";
|
2026-01-17 15:09:44 +08:00
|
|
|
import { useArtifactContent } from "@/core/artifacts/hooks";
|
|
|
|
|
import { urlOfArtifact } from "@/core/artifacts/utils";
|
2026-02-09 12:13:06 +08:00
|
|
|
import type { Citation } from "@/core/citations";
|
2026-01-30 16:41:18 +08:00
|
|
|
import {
|
2026-02-09 13:01:01 +08:00
|
|
|
contentWithoutCitationsFromParsed,
|
2026-02-04 11:56:10 +08:00
|
|
|
removeAllCitations,
|
2026-02-09 13:01:01 +08:00
|
|
|
useParsedCitations,
|
2026-01-30 16:41:18 +08:00
|
|
|
} from "@/core/citations";
|
2026-01-20 14:06:47 +08:00
|
|
|
import { useI18n } from "@/core/i18n/hooks";
|
2026-01-31 22:10:05 +08:00
|
|
|
import { installSkill } from "@/core/skills/api";
|
2026-01-30 16:41:18 +08:00
|
|
|
import { streamdownPlugins } from "@/core/streamdown";
|
2026-01-17 17:21:37 +08:00
|
|
|
import { checkCodeFile, getFileName } from "@/core/utils/files";
|
2026-02-01 10:55:08 +08:00
|
|
|
import { env } from "@/env";
|
2026-02-09 12:13:06 +08:00
|
|
|
import { cn } from "@/lib/utils";
|
2026-01-17 00:05:19 +08:00
|
|
|
|
2026-02-01 10:55:08 +08:00
|
|
|
import { Tooltip } from "../tooltip";
|
|
|
|
|
|
2026-02-09 15:58:59 +08:00
|
|
|
import { SafeCitationContent } from "../messages/safe-citation-content";
|
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 保持内部使用;
在文件头注释中说明展示规则。
2026-02-09 15:01:51 +08:00
|
|
|
import { useThread } from "../messages/context";
|
|
|
|
|
|
2026-01-17 15:09:44 +08:00
|
|
|
import { useArtifacts } from "./context";
|
|
|
|
|
|
2026-01-17 11:02:33 +08:00
|
|
|
export function ArtifactFileDetail({
|
|
|
|
|
className,
|
2026-01-18 17:13:15 +08:00
|
|
|
filepath: filepathFromProps,
|
2026-01-17 15:09:44 +08:00
|
|
|
threadId,
|
2026-01-17 11:02:33 +08:00
|
|
|
}: {
|
|
|
|
|
className?: string;
|
|
|
|
|
filepath: string;
|
2026-01-17 15:09:44 +08:00
|
|
|
threadId: string;
|
2026-01-17 11:02:33 +08:00
|
|
|
}) {
|
2026-01-20 14:06:47 +08:00
|
|
|
const { t } = useI18n();
|
2026-01-17 17:21:37 +08:00
|
|
|
const { artifacts, setOpen, select } = useArtifacts();
|
2026-01-18 17:13:15 +08:00
|
|
|
const isWriteFile = useMemo(() => {
|
|
|
|
|
return filepathFromProps.startsWith("write-file:");
|
|
|
|
|
}, [filepathFromProps]);
|
|
|
|
|
const filepath = useMemo(() => {
|
|
|
|
|
if (isWriteFile) {
|
|
|
|
|
const url = new URL(filepathFromProps);
|
2026-01-18 20:26:01 +08:00
|
|
|
return decodeURIComponent(url.pathname);
|
2026-01-18 17:13:15 +08:00
|
|
|
}
|
|
|
|
|
return filepathFromProps;
|
|
|
|
|
}, [filepathFromProps, isWriteFile]);
|
2026-01-31 22:27:06 +08:00
|
|
|
const isSkillFile = useMemo(() => {
|
|
|
|
|
return filepath.endsWith(".skill");
|
|
|
|
|
}, [filepath]);
|
2026-01-19 19:41:46 +08:00
|
|
|
const { isCodeFile, language } = useMemo(() => {
|
2026-01-18 17:13:15 +08:00
|
|
|
if (isWriteFile) {
|
|
|
|
|
let language = checkCodeFile(filepath).language;
|
2026-01-19 19:41:46 +08:00
|
|
|
language ??= "text";
|
2026-01-18 17:13:15 +08:00
|
|
|
return { isCodeFile: true, language };
|
|
|
|
|
}
|
2026-01-31 22:27:06 +08:00
|
|
|
// Treat .skill files as markdown (they contain SKILL.md)
|
|
|
|
|
if (isSkillFile) {
|
|
|
|
|
return { isCodeFile: true, language: "markdown" };
|
|
|
|
|
}
|
2026-01-18 17:13:15 +08:00
|
|
|
return checkCodeFile(filepath);
|
2026-01-31 22:27:06 +08:00
|
|
|
}, [filepath, isWriteFile, isSkillFile]);
|
2026-01-19 19:41:46 +08:00
|
|
|
const previewable = useMemo(() => {
|
|
|
|
|
return (language === "html" && !isWriteFile) || language === "markdown";
|
|
|
|
|
}, [isWriteFile, language]);
|
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 保持内部使用;
在文件头注释中说明展示规则。
2026-02-09 15:01:51 +08:00
|
|
|
const { thread } = useThread();
|
2026-01-17 15:09:44 +08:00
|
|
|
const { content } = useArtifactContent({
|
|
|
|
|
threadId,
|
2026-01-18 17:13:15 +08:00
|
|
|
filepath: filepathFromProps,
|
|
|
|
|
enabled: isCodeFile && !isWriteFile,
|
2026-01-17 15:09:44 +08:00
|
|
|
});
|
2026-01-30 16:41:18 +08:00
|
|
|
|
2026-02-09 12:13:06 +08:00
|
|
|
const parsed = useParsedCitations(
|
|
|
|
|
language === "markdown" ? (content ?? "") : "",
|
|
|
|
|
);
|
|
|
|
|
const cleanContent =
|
|
|
|
|
language === "markdown" && content ? parsed.cleanContent : (content ?? "");
|
2026-02-09 13:01:01 +08:00
|
|
|
const contentWithoutCitations =
|
|
|
|
|
language === "markdown" && content
|
|
|
|
|
? contentWithoutCitationsFromParsed(parsed)
|
|
|
|
|
: (content ?? "");
|
2026-02-04 11:56:10 +08:00
|
|
|
|
2026-01-19 19:41:46 +08:00
|
|
|
const [viewMode, setViewMode] = useState<"code" | "preview">("code");
|
2026-01-31 22:10:05 +08:00
|
|
|
const [isInstalling, setIsInstalling] = useState(false);
|
|
|
|
|
|
2026-01-19 19:41:46 +08:00
|
|
|
useEffect(() => {
|
|
|
|
|
if (previewable) {
|
|
|
|
|
setViewMode("preview");
|
|
|
|
|
} else {
|
|
|
|
|
setViewMode("code");
|
|
|
|
|
}
|
|
|
|
|
}, [previewable]);
|
2026-01-31 22:10:05 +08:00
|
|
|
|
|
|
|
|
const handleInstallSkill = useCallback(async () => {
|
|
|
|
|
if (isInstalling) return;
|
|
|
|
|
|
|
|
|
|
setIsInstalling(true);
|
|
|
|
|
try {
|
|
|
|
|
const result = await installSkill({
|
|
|
|
|
thread_id: threadId,
|
|
|
|
|
path: filepath,
|
|
|
|
|
});
|
|
|
|
|
if (result.success) {
|
|
|
|
|
toast.success(result.message);
|
|
|
|
|
} else {
|
2026-02-01 10:55:08 +08:00
|
|
|
toast.error(result.message ?? "Failed to install skill");
|
2026-01-31 22:10:05 +08:00
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error("Failed to install skill:", error);
|
|
|
|
|
toast.error("Failed to install skill");
|
|
|
|
|
} finally {
|
|
|
|
|
setIsInstalling(false);
|
|
|
|
|
}
|
|
|
|
|
}, [threadId, filepath, isInstalling]);
|
2026-01-17 00:02:03 +08:00
|
|
|
return (
|
2026-01-21 08:50:15 +08:00
|
|
|
<Artifact className={cn(className)}>
|
2026-01-17 17:21:37 +08:00
|
|
|
<ArtifactHeader className="px-2">
|
2026-01-19 19:41:46 +08:00
|
|
|
<div className="flex items-center gap-2">
|
2026-01-17 17:21:37 +08:00
|
|
|
<ArtifactTitle>
|
2026-01-18 17:13:15 +08:00
|
|
|
{isWriteFile ? (
|
|
|
|
|
<div className="px-2">{getFileName(filepath)}</div>
|
|
|
|
|
) : (
|
|
|
|
|
<Select value={filepath} onValueChange={select}>
|
|
|
|
|
<SelectTrigger className="border-none bg-transparent! shadow-none select-none focus:outline-0 active:outline-0">
|
|
|
|
|
<SelectValue placeholder="Select a file" />
|
|
|
|
|
</SelectTrigger>
|
|
|
|
|
<SelectContent className="select-none">
|
|
|
|
|
<SelectGroup>
|
|
|
|
|
{(artifacts ?? []).map((filepath) => (
|
|
|
|
|
<SelectItem key={filepath} value={filepath}>
|
|
|
|
|
{getFileName(filepath)}
|
|
|
|
|
</SelectItem>
|
|
|
|
|
))}
|
|
|
|
|
</SelectGroup>
|
|
|
|
|
</SelectContent>
|
|
|
|
|
</Select>
|
|
|
|
|
)}
|
2026-01-17 17:21:37 +08:00
|
|
|
</ArtifactTitle>
|
2026-01-17 15:09:44 +08:00
|
|
|
</div>
|
2026-01-19 19:41:46 +08:00
|
|
|
<div className="flex min-w-0 grow items-center justify-center">
|
|
|
|
|
{previewable && (
|
|
|
|
|
<ToggleGroup
|
|
|
|
|
className="mx-auto"
|
|
|
|
|
type="single"
|
|
|
|
|
variant="outline"
|
|
|
|
|
size="sm"
|
|
|
|
|
value={viewMode}
|
|
|
|
|
onValueChange={(value) =>
|
|
|
|
|
setViewMode(value as "code" | "preview")
|
|
|
|
|
}
|
|
|
|
|
>
|
2026-01-25 00:06:49 +08:00
|
|
|
<ToggleGroupItem value="code">
|
|
|
|
|
<Code2Icon />
|
|
|
|
|
</ToggleGroupItem>
|
|
|
|
|
<ToggleGroupItem value="preview">
|
|
|
|
|
<EyeIcon />
|
|
|
|
|
</ToggleGroupItem>
|
2026-01-19 19:41:46 +08:00
|
|
|
</ToggleGroup>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
2026-01-17 15:09:44 +08:00
|
|
|
<div className="flex items-center gap-2">
|
|
|
|
|
<ArtifactActions>
|
2026-01-31 11:08:27 +08:00
|
|
|
{!isWriteFile && filepath.endsWith(".skill") && (
|
2026-02-01 10:55:08 +08:00
|
|
|
<Tooltip content={t.toolCalls.skillInstallTooltip}>
|
|
|
|
|
<ArtifactAction
|
|
|
|
|
icon={isInstalling ? LoaderIcon : PackageIcon}
|
|
|
|
|
label={t.common.install}
|
|
|
|
|
tooltip={t.common.install}
|
|
|
|
|
disabled={
|
|
|
|
|
isInstalling ||
|
|
|
|
|
env.NEXT_PUBLIC_STATIC_WEBSITE_ONLY === "true"
|
|
|
|
|
}
|
|
|
|
|
onClick={handleInstallSkill}
|
|
|
|
|
/>
|
|
|
|
|
</Tooltip>
|
2026-01-31 11:08:27 +08:00
|
|
|
)}
|
2026-01-18 17:13:15 +08:00
|
|
|
{!isWriteFile && (
|
|
|
|
|
<a href={urlOfArtifact({ filepath, threadId })} target="_blank">
|
|
|
|
|
<ArtifactAction
|
|
|
|
|
icon={SquareArrowOutUpRightIcon}
|
2026-01-20 14:06:47 +08:00
|
|
|
label={t.common.openInNewWindow}
|
|
|
|
|
tooltip={t.common.openInNewWindow}
|
2026-01-18 17:13:15 +08:00
|
|
|
/>
|
|
|
|
|
</a>
|
|
|
|
|
)}
|
2026-01-17 15:09:44 +08:00
|
|
|
{isCodeFile && (
|
|
|
|
|
<ArtifactAction
|
|
|
|
|
icon={CopyIcon}
|
2026-01-20 14:06:47 +08:00
|
|
|
label={t.clipboard.copyToClipboard}
|
2026-01-17 15:09:44 +08:00
|
|
|
disabled={!content}
|
|
|
|
|
onClick={async () => {
|
|
|
|
|
try {
|
2026-02-04 11:56:10 +08:00
|
|
|
await navigator.clipboard.writeText(contentWithoutCitations ?? "");
|
2026-01-20 14:06:47 +08:00
|
|
|
toast.success(t.clipboard.copiedToClipboard);
|
2026-01-17 15:09:44 +08:00
|
|
|
} catch (error) {
|
|
|
|
|
toast.error("Failed to copy to clipboard");
|
|
|
|
|
console.error(error);
|
|
|
|
|
}
|
|
|
|
|
}}
|
2026-01-20 14:06:47 +08:00
|
|
|
tooltip={t.clipboard.copyToClipboard}
|
2026-01-17 15:09:44 +08:00
|
|
|
/>
|
|
|
|
|
)}
|
2026-01-18 17:13:15 +08:00
|
|
|
{!isWriteFile && (
|
|
|
|
|
<a
|
|
|
|
|
href={urlOfArtifact({ filepath, threadId, download: true })}
|
|
|
|
|
target="_blank"
|
|
|
|
|
>
|
|
|
|
|
<ArtifactAction
|
|
|
|
|
icon={DownloadIcon}
|
2026-01-20 14:06:47 +08:00
|
|
|
label={t.common.download}
|
|
|
|
|
tooltip={t.common.download}
|
2026-01-18 17:13:15 +08:00
|
|
|
/>
|
|
|
|
|
</a>
|
|
|
|
|
)}
|
2026-01-17 15:09:44 +08:00
|
|
|
<ArtifactAction
|
|
|
|
|
icon={XIcon}
|
2026-01-20 14:06:47 +08:00
|
|
|
label={t.common.close}
|
2026-01-17 15:09:44 +08:00
|
|
|
onClick={() => setOpen(false)}
|
2026-01-20 14:06:47 +08:00
|
|
|
tooltip={t.common.close}
|
2026-01-17 15:09:44 +08:00
|
|
|
/>
|
|
|
|
|
</ArtifactActions>
|
2026-01-17 00:02:03 +08:00
|
|
|
</div>
|
2026-01-17 15:09:44 +08:00
|
|
|
</ArtifactHeader>
|
|
|
|
|
<ArtifactContent className="p-0">
|
2026-02-09 15:58:59 +08:00
|
|
|
{previewable &&
|
|
|
|
|
viewMode === "preview" &&
|
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 保持内部使用;
在文件头注释中说明展示规则。
2026-02-09 15:01:51 +08:00
|
|
|
language === "markdown" &&
|
2026-02-09 15:58:59 +08:00
|
|
|
content && (
|
|
|
|
|
<SafeCitationContent
|
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 保持内部使用;
在文件头注释中说明展示规则。
2026-02-09 15:01:51 +08:00
|
|
|
content={content}
|
2026-02-09 15:58:59 +08:00
|
|
|
isLoading={thread.isLoading}
|
|
|
|
|
rehypePlugins={streamdownPlugins.rehypePlugins}
|
|
|
|
|
className="flex size-full items-center justify-center p-4 my-0"
|
|
|
|
|
renderBody={(p) => (
|
|
|
|
|
<ArtifactFilePreview
|
|
|
|
|
filepath={filepath}
|
|
|
|
|
threadId={threadId}
|
|
|
|
|
content={content}
|
|
|
|
|
language={language ?? "text"}
|
|
|
|
|
cleanContent={p.cleanContent}
|
|
|
|
|
citationMap={p.citationMap}
|
|
|
|
|
/>
|
|
|
|
|
)}
|
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 保持内部使用;
在文件头注释中说明展示规则。
2026-02-09 15:01:51 +08:00
|
|
|
/>
|
2026-02-09 15:58:59 +08:00
|
|
|
)}
|
2026-01-19 19:41:46 +08:00
|
|
|
{isCodeFile && viewMode === "code" && (
|
2026-01-21 09:33:33 +08:00
|
|
|
<CodeEditor
|
2026-01-19 19:41:46 +08:00
|
|
|
className="size-full resize-none rounded-none border-none"
|
2026-01-29 12:29:13 +08:00
|
|
|
value={cleanContent ?? ""}
|
2026-01-21 09:33:33 +08:00
|
|
|
readonly
|
2026-01-19 19:41:46 +08:00
|
|
|
/>
|
|
|
|
|
)}
|
|
|
|
|
{!isCodeFile && (
|
|
|
|
|
<iframe
|
|
|
|
|
className="size-full"
|
|
|
|
|
src={urlOfArtifact({ filepath, threadId })}
|
|
|
|
|
/>
|
|
|
|
|
)}
|
2026-01-17 15:09:44 +08:00
|
|
|
</ArtifactContent>
|
|
|
|
|
</Artifact>
|
2026-01-17 00:02:03 +08:00
|
|
|
);
|
|
|
|
|
}
|
2026-01-19 19:41:46 +08:00
|
|
|
|
|
|
|
|
export function ArtifactFilePreview({
|
|
|
|
|
filepath,
|
|
|
|
|
threadId,
|
|
|
|
|
content,
|
|
|
|
|
language,
|
2026-02-09 12:13:06 +08:00
|
|
|
cleanContent,
|
|
|
|
|
citationMap,
|
2026-01-19 19:41:46 +08:00
|
|
|
}: {
|
|
|
|
|
filepath: string;
|
|
|
|
|
threadId: string;
|
|
|
|
|
content: string;
|
|
|
|
|
language: string;
|
2026-02-09 12:13:06 +08:00
|
|
|
cleanContent: string;
|
|
|
|
|
citationMap: Map<string, Citation>;
|
2026-01-19 19:41:46 +08:00
|
|
|
}) {
|
|
|
|
|
if (language === "markdown") {
|
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 保持内部使用;
在文件头注释中说明展示规则。
2026-02-09 15:01:51 +08:00
|
|
|
const components = createCitationMarkdownComponents({
|
|
|
|
|
citationMap,
|
|
|
|
|
syntheticExternal: true,
|
|
|
|
|
});
|
2026-01-19 19:41:46 +08:00
|
|
|
return (
|
|
|
|
|
<div className="size-full px-4">
|
2026-01-28 19:15:11 +08:00
|
|
|
<Streamdown
|
|
|
|
|
className="size-full"
|
2026-01-30 16:41:18 +08:00
|
|
|
{...streamdownPlugins}
|
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 保持内部使用;
在文件头注释中说明展示规则。
2026-02-09 15:01:51 +08:00
|
|
|
components={components}
|
2026-01-28 19:15:11 +08:00
|
|
|
>
|
2026-01-29 12:29:13 +08:00
|
|
|
{cleanContent ?? ""}
|
2026-01-28 19:15:11 +08:00
|
|
|
</Streamdown>
|
2026-01-19 19:41:46 +08:00
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
if (language === "html") {
|
|
|
|
|
return (
|
|
|
|
|
<iframe
|
|
|
|
|
className="size-full"
|
|
|
|
|
src={urlOfArtifact({ filepath, threadId })}
|
|
|
|
|
/>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
return null;
|
|
|
|
|
}
|
2026-01-28 19:15:11 +08:00
|
|
|
|