diff --git a/frontend/src/components/workspace/artifacts/artifact-file-detail.tsx b/frontend/src/components/workspace/artifacts/artifact-file-detail.tsx index cfd4e5db..2638acbe 100644 --- a/frontend/src/components/workspace/artifacts/artifact-file-detail.tsx +++ b/frontend/src/components/workspace/artifacts/artifact-file-detail.tsx @@ -33,7 +33,7 @@ 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 { resolveArtifactURL, urlOfArtifact } from "@/core/artifacts/utils"; import { useI18n } from "@/core/i18n/hooks"; import { streamdownPlugins } from "@/core/streamdown"; import { checkCodeFile, getFileName } from "@/core/utils/files"; @@ -474,6 +474,7 @@ export function ArtifactFileDetail({ content={displayContent} language={language ?? "text"} zoom={zoom} + threadId={threadId} /> )} @@ -505,12 +506,17 @@ export function ArtifactFilePreview({ content, language, zoom = 100, + threadId, }: { content: string; language: string; zoom?: number; + threadId?: string; }) { const zoomScale = zoom / 100; + const normalizedContent = useMemo(() => { + return rewriteArtifactImagePaths(content ?? "", threadId); + }, [content, threadId]); if (language === "markdown") { return ( @@ -523,7 +529,7 @@ export function ArtifactFilePreview({ {...streamdownPlugins} components={{ a: CitationLink }} > - {content ?? ""} + {normalizedContent} @@ -535,7 +541,7 @@ export function ArtifactFilePreview({ className="size-full" containerClassName="h-full mb-[207px]" title="Artifact preview" - srcDoc={content} + srcDoc={normalizedContent} sandbox="allow-scripts allow-forms" style={{ zoom: zoomScale }} /> @@ -582,6 +588,30 @@ function PreviewIframe({ ); } +function rewriteArtifactImagePaths(content: string, threadId?: string) { + if (!threadId || !/\/?mnt\/user-data\//.test(content)) { + return content; + } + + const markdownRewritten = content.replace( + /!\[([^\]]*)\]\(\s*(\/?mnt\/user-data\/outputs\/[^)\s]+)\s*\)/g, + (_full, alt, rawPath) => { + const normalizedPath = rawPath.startsWith("/") ? rawPath : `/${rawPath}`; + const artifactUrl = resolveArtifactURL(normalizedPath, threadId); + return `![${alt}](${artifactUrl})`; + }, + ); + + return markdownRewritten.replace( + /(]*\bsrc\s*=\s*)(["'])(\/?mnt\/user-data\/outputs\/[^"']+)\2/gi, + (_full, prefix, quote, rawPath) => { + const normalizedPath = rawPath.startsWith("/") ? rawPath : `/${rawPath}`; + const artifactUrl = resolveArtifactURL(normalizedPath, threadId); + return `${prefix}${quote}${artifactUrl}${quote}`; + }, + ); +} + type ArtifactPreviewKind = | "html" | "image"