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 ( +