diff --git a/frontend-web/src/components/workspace/artifacts/artifact-file-detail.tsx b/frontend-web/src/components/workspace/artifacts/artifact-file-detail.tsx index ef28a656..98013290 100644 --- a/frontend-web/src/components/workspace/artifacts/artifact-file-detail.tsx +++ b/frontend-web/src/components/workspace/artifacts/artifact-file-detail.tsx @@ -48,7 +48,13 @@ import { useI18n } from "@/core/i18n/hooks"; import { downloadMarkdownAsDocx } from "@/core/scheduled-tasks/markdown-docx"; import { installSkill } from "@/core/skills/api"; import { streamdownPlugins } from "@/core/streamdown"; -import { checkCodeFile, getFileName } from "@/core/utils/files"; +import { + checkCodeFile, + getFileName, + isImageFile, +} from "@/core/utils/files"; +import { getBackendBaseURL } from "@/core/config"; +import { resolveArtifactURL } from "@/core/artifacts/utils"; import { env } from "@/env"; import { desensitize } from "@/lib/desensitize"; import { cn } from "@/lib/utils"; @@ -96,21 +102,25 @@ export function ArtifactFileDetail({ const { t } = useI18n(); const { artifacts, setOpen, select } = useArtifacts(); const { isMock, thread, publicShareCode } = useThread(); - const isWriteFile = useMemo(() => { + const isWriteFileVirtual = useMemo(() => { return filepathFromProps.startsWith("write-file:"); }, [filepathFromProps]); const filepath = useMemo(() => { - if (isWriteFile) { + if (isWriteFileVirtual) { const url = new URL(filepathFromProps); return decodeURIComponent(url.pathname); } return filepathFromProps; - }, [filepathFromProps, isWriteFile]); + }, [filepathFromProps, isWriteFileVirtual]); + /** write_file 流式预览走 tool args 文本;图片等二进制产物始终走 artifacts API。 */ + const useWriteFileStreaming = useMemo(() => { + return isWriteFileVirtual && !isImageFile(filepath); + }, [filepath, isWriteFileVirtual]); const isSkillFile = useMemo(() => { return filepath.endsWith(".skill"); }, [filepath]); const { isCodeFile, language } = useMemo(() => { - if (isWriteFile) { + if (useWriteFileStreaming) { let nextLanguage = checkCodeFile(filepath).language; nextLanguage ??= "text"; return { isCodeFile: true, language: nextLanguage }; @@ -119,18 +129,21 @@ export function ArtifactFileDetail({ return { isCodeFile: true, language: "markdown" }; } return checkCodeFile(filepath); - }, [filepath, isWriteFile, isSkillFile]); + }, [filepath, useWriteFileStreaming, isSkillFile]); const isSupportPreview = useMemo(() => { return language === "html" || language === "markdown"; }, [language]); const { content, isApplyingEdit } = useArtifactContent({ threadId, filepath: filepathFromProps, - enabled: isCodeFile && !isWriteFile, + enabled: isCodeFile && !useWriteFileStreaming, }); const canEditArtifact = - language === "markdown" && !isWriteFile && !isSkillFile && !isMock; + language === "markdown" && + !useWriteFileStreaming && + !isSkillFile && + !isMock; const [draftContent, setDraftContent] = useState(""); const [hasLocalEdits, setHasLocalEdits] = useState(false); const updateArtifact = useUpdateArtifactContent(); @@ -138,12 +151,13 @@ export function ArtifactFileDetail({ const isHtmlStreaming = language === "html" && thread.isLoading; const hasMultipleArtifacts = (artifacts?.length ?? 0) > 1; const fileTypeLabel = useMemo(() => { + if (isImageFile(filepath)) return "IMG"; if (language === "html") return "HTML"; if (language === "markdown") return "MD"; if (isSkillFile) return "SKILL"; if (language) return language.toUpperCase(); return "FILE"; - }, [isSkillFile, language]); + }, [filepath, isSkillFile, language]); const [viewMode, setViewMode] = useState<"code" | "preview">("code"); const [allowAutoViewSwitch, setAllowAutoViewSwitch] = useState(true); @@ -453,7 +467,7 @@ export function ArtifactFileDetail({ {fileTypeLabel} - {isWriteFile ? ( + {useWriteFileStreaming ? (
{getFileName(filepath)}
) : hasMultipleArtifacts ? (