import { DownloadIcon, FileTextIcon, LoaderIcon, FileTypeIcon } from "lucide-react"; import { useCallback, useEffect, useMemo, useState, type HTMLAttributes, } from "react"; import { toast } from "sonner"; import { Streamdown } from "streamdown"; import { Artifact, ArtifactAction, ArtifactActions, ArtifactContent, ArtifactHeader, ArtifactTitle, } from "@/components/ai-elements/artifact"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { DropdownSelector } from "@/components/ui/dropdown-selector"; import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"; import { CodeEditor } from "@/components/workspace/code-editor"; import { useArtifactContent } from "@/core/artifacts/hooks"; import { urlOfArtifact } from "@/core/artifacts/utils"; import { useI18n } from "@/core/i18n/hooks"; import { POST_MESSAGE_TYPES, sendToParent } from "@/core/iframe-messages"; import { installSkill } from "@/core/skills/api"; import { streamdownPlugins } from "@/core/streamdown"; import { checkCodeFile, getFileName } from "@/core/utils/files"; import { useMarkdownDownload } from "@/core/utils/markdown-download"; import { cn, copyToClipboard, truncateMiddle } from "@/lib/utils"; import { CitationLink } from "../citations/citation-link"; import { useArtifacts } from "./context"; export function ArtifactFileDetail({ className, filepath: filepathFromProps, threadId, }: { className?: string; filepath: string; threadId: string; }) { const { t } = useI18n(); const { artifacts, setOpen, select, fullscreen, setFullscreen } = useArtifacts(); const isWriteFile = useMemo(() => { return filepathFromProps.startsWith("write-file:"); }, [filepathFromProps]); const filepath = useMemo(() => { if (isWriteFile) { const url = new URL(filepathFromProps); return decodeURIComponent(url.pathname); } return filepathFromProps; }, [filepathFromProps, isWriteFile]); const isSkillFile = useMemo(() => { return filepath.endsWith(".skill"); }, [filepath]); const { isCodeFile, language } = useMemo(() => { if (isWriteFile) { let language = checkCodeFile(filepath).language; language ??= "text"; return { isCodeFile: true, language }; } // Treat .skill files as markdown (they contain SKILL.md) if (isSkillFile) { return { isCodeFile: true, language: "markdown" }; } return checkCodeFile(filepath); }, [filepath, isWriteFile, isSkillFile]); const previewable = useMemo(() => { return (language === "html" && !isWriteFile) || language === "markdown"; }, [isWriteFile, language]); const { content } = useArtifactContent({ threadId, filepath: filepathFromProps, enabled: isCodeFile && !isWriteFile, }); const displayContent = content ?? ""; const artifactOptions = useMemo(() => { return (artifacts ?? []).map((artifactPath) => ({ value: artifactPath, label: getFileName(artifactPath), })); }, [artifacts]); const [viewMode, setViewMode] = useState<"code" | "preview">("code"); const [isInstalling, setIsInstalling] = useState(false); const [zoom, setZoom] = useState(80); // 获取文件名(不含路径) const fileName = useMemo(() => getFileName(filepath), [filepath]); // 是否可以转换为docx/pdf(仅markdown文件支持) const canConvertToDocxPdf = language === "markdown"; // 使用 Markdown 下载 hook const { isDownloading, downloadAsDocx, downloadAsPdf } = useMarkdownDownload({ onError: (error, format) => { console.error(`Failed to download as ${format}:`, error); toast.error(`Failed to download as ${format.toUpperCase()}`); }, }); // 下载为 DOCX const handleDownloadDocx = useCallback(() => { if (content) { void downloadAsDocx(content, fileName); } }, [content, fileName, downloadAsDocx]); // 下载为 PDF const handleDownloadPdf = useCallback(() => { if (content) { void downloadAsPdf(content, fileName); } }, [content, fileName, downloadAsPdf]); // 全屏切换处理 const handleFullscreenToggle = useCallback(() => { const newFullscreen = !fullscreen; setFullscreen(newFullscreen); sendToParent({ type: POST_MESSAGE_TYPES.FULLSCREEN, fullscreen: newFullscreen, }); }, [fullscreen, setFullscreen]); useEffect(() => { if (previewable) { setViewMode("preview"); } else { setViewMode("code"); } }, [previewable]); 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 { toast.error(result.message ?? "Failed to install skill"); } } catch (error) { console.error("Failed to install skill:", error); toast.error("Failed to install skill"); } finally { setIsInstalling(false); } }, [threadId, filepath, isInstalling]); return ( // 给滚动遮挡头部定位relative
{previewable && ( { if (value) { setViewMode(value as "code" | "preview"); } }} > )} {/* 放大缩小选择器 */}
{isWriteFile ? (
{truncateMiddle(getFileName(filepath), 50)}
) : ( )}
{isCodeFile && ( { try { await copyToClipboard(displayContent ?? ""); toast.success(t.clipboard.copiedToClipboard); } catch (error) { toast.error("Failed to copy to clipboard"); console.error(error); } }} tooltip={t.clipboard.copyToClipboard} > )} {!isWriteFile && ( {isDownloading ? ( ) : ( )} {t.common.downloadOriginal} {/* DOCX 和 PDF 导出选项仅对 Markdown 文件显示。 */} {canConvertToDocxPdf && ( <> {isDownloading === "docx" ? t.common.loading : t.common.downloadAsDocx} {isDownloading === "pdf" ? t.common.loading : t.common.downloadAsPdf} )} )} {/* 全屏按钮 */} {fullscreen ? ( ) : ( )} {!fullscreen && ( setOpen(false)} tooltip={t.common.close} > )}
{/* 遮挡多余的滚动顶部 */}
{previewable && viewMode === "preview" && (language === "markdown" || language === "html") && ( )} {isCodeFile && viewMode === "code" && ( )} {!isCodeFile && (