// v1.14.x-html-artifact-panes: full-height HTML artifact viewer. Renders the // model's HTML inside a sandboxed iframe — no allow-same-origin, srcdoc only // (no separate URL), CSP injected by the backend writer. JS runs inside the // iframe (interactive controls work) but fetch / WS / tracking pixels are // blocked by connect-src 'none' on the CSP. NO Copy button per the spec. // // Pane state is a reference only (chat_id + message_id + title); the iframe // payload is fetched on mount from // GET /api/chats/:chat_id/messages/:msg_id/html_artifact so that // sessions.workspace_panes jsonb stays small and message_parts.payload is the // single source of truth. import { useEffect, useState } from 'react'; import { Download, X } from 'lucide-react'; import { toast } from 'sonner'; import { api } from '@/api/client'; import type { HtmlArtifactState } from '@/api/types'; interface Props { chatId: string; state: HtmlArtifactState; onClose: () => void; } export function HtmlArtifactPane({ chatId, state, onClose }: Props) { const [downloading, setDownloading] = useState(false); const [htmlContent, setHtmlContent] = useState(null); const [loadError, setLoadError] = useState(null); useEffect(() => { let cancelled = false; setHtmlContent(null); setLoadError(null); void (async () => { try { const payload = await api.messages.getHtmlArtifact(chatId, state.message_id); if (cancelled) return; setHtmlContent(payload.html_content); } catch (err) { if (cancelled) return; setLoadError(err instanceof Error ? err.message : 'failed to load HTML artifact'); } })(); return () => { cancelled = true; }; }, [chatId, state.message_id]); async function download() { if (downloading) return; setDownloading(true); try { const { url, path } = await api.messages.downloadArtifact( chatId, state.message_id, 'html', ); const a = document.createElement('a'); a.href = url; a.rel = 'noopener'; a.click(); toast.success(`Saved to ${path}`); } catch (err) { toast.error(err instanceof Error ? err.message : 'download failed'); } finally { setDownloading(false); } } return (
{state.title || 'HTML artifact'}
{loadError ? (
Failed to load: {loadError}
) : htmlContent === null ? (
Loading HTML artifact…
) : (