From a06b348c34941fd12281a38253332df9530cf516 Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 24 Jun 2026 15:23:49 +0100 Subject: [PATCH] Preview artifact bytes inline in the sidebar Render produced-artifact bytes inline when a version has no embedded spec: HTML and SVG in the existing sandboxed iframe (with the restrictive CSP), images inline via a blob URL, and Markdown/CSV/JSON/plain text as escaped text; office documents and PDFs keep the download card. Bytes are fetched through the existing authenticated download path, version-aware, with object URLs revoked on change. Untrusted HTML/SVG renders only inside the scriptless sandboxed iframe and Markdown is rendered without raw HTML, so artifact content cannot script the app. --- .../src/components/DocumentArtifactView.tsx | 152 ++++++++++++++---- frontend/src/components/MarkdownPreview.tsx | 74 +++++++++ .../src/components/artifactViewUtils.test.ts | 64 ++++++++ frontend/src/components/artifactViewUtils.ts | 59 +++++++ frontend/src/components/useArtifactBytes.ts | 86 ++++++++++ 5 files changed, 404 insertions(+), 31 deletions(-) create mode 100644 frontend/src/components/MarkdownPreview.tsx create mode 100644 frontend/src/components/useArtifactBytes.ts diff --git a/frontend/src/components/DocumentArtifactView.tsx b/frontend/src/components/DocumentArtifactView.tsx index 987d2f52..d2edc6fe 100644 --- a/frontend/src/components/DocumentArtifactView.tsx +++ b/frontend/src/components/DocumentArtifactView.tsx @@ -1,14 +1,16 @@ import mermaid from 'mermaid'; import { Download, FileText, History, RotateCcw } from 'lucide-react'; -import { useEffect, useMemo, useRef, useState } from 'react'; +import { useEffect, useMemo, useRef, useState, type ReactNode } from 'react'; import { useSelector } from 'react-redux'; import userService from '../api/services/userService'; import { useDarkTheme } from '../hooks'; import { selectToken } from '../preferences/preferenceSlice'; +import MarkdownPreview from './MarkdownPreview'; import Spinner from './Spinner'; import { buildPreviewDocument, + bytesPreviewModeForMime, displayFilename, findCurrentVersion, formatBytes, @@ -16,9 +18,11 @@ import { sortVersionsDesc, triggerResponseDownload, type ArtifactVersion, + type BytesPreviewMode, type DocumentArtifact, } from './artifactViewUtils'; import { Button } from './ui/button'; +import { useArtifactBytes } from './useArtifactBytes'; import { Select, SelectContent, @@ -154,6 +158,74 @@ function DownloadCard({ ); } +/** + * Render a version's downloadable BYTES inline by fetching them through the + * authed download path. html/svg are embedded ONLY inside the scriptless + * `sandbox=""` + CSP iframe (never the app DOM); images are shown via an inert + * blob-URL ``; text/markdown render as escaped React content. On fetch + * error (or oversized text) it falls back to the supplied download card. + */ +function BytesPreview({ + artifactId, + version, + mode, + token, + fallback, +}: { + artifactId: string; + version: number; + mode: Exclude; + token: string | null; + fallback: ReactNode; +}) { + const state = useArtifactBytes(artifactId, version, mode, token); + + if (state.status === 'loading') { + return ( +
+ +
+ ); + } + if (state.status === 'error') { + return <>{fallback}; + } + if (state.status === 'image') { + return ( +
+ Artifact preview +
+ ); + } + // state.status === 'text' + if (mode === 'iframe-html' || mode === 'iframe-svg') { + return ( +