import React, { useMemo, useState } from 'react'; import { Pressable } from 'react-native'; import { Maximize2, Sparkles } from 'lucide-react-native'; import type { SuperagentToolRendererProps } from '../../types'; import { themedColor, useSuperagentTheme } from '../../theme'; import { ToolWidgetRow } from '../primitives/ToolWidgetRow'; import { buildArtifactPreviewHtml, canPreviewArtifact, isSvgArtifact, previewFailureApplies } from '../artifactPreviewUtils'; import { extractArtifact, getArtifactRenderState } from '../artifactToolUtils'; import { getFailureReason } from '../skillsAndConnectionsUtils'; import { getStringArg, isActionableTurn, parseToolArgs, type ToolWidgetStatus } from '../toolWidgetUtils'; import { ArtifactFullscreenModal, ArtifactInlinePreview } from './ArtifactPreview'; /** * artifacts (+ aliases) — native port of the web ArtifactToolUI status row. * Settled successful artifacts render a live sandboxed WebView preview (web * MessageArtifacts/ArtifactInlineCard parity — see ArtifactPreview) with a * fullscreen affordance in the header; when the inline WebView errors, the row * shows a preview-failure note (web parity: no metadata/type/source * disclosure), while a fullscreen error only closes the modal. Failures are * keyed to the failed markup, so a later artifact update retries. */ export function ArtifactWidget({ isLastAssistantMessage, toolCall }: SuperagentToolRendererProps) { const args = useMemo(() => parseToolArgs(toolCall), [toolCall]); const { scheme } = useSuperagentTheme(); const rawStatus = (toolCall.status || '').toLowerCase(); const isError = rawStatus === 'error' || rawStatus === 'failed'; // Extract whenever the call hasn't errored — socket/history updates can // leave the status stale ("running"/"pending") after the payload arrived. const artifact = isError ? null : extractArtifact(toolCall.client_artifact); // Turn-settling (getSettledWidgetStatus parity): once the message is no // longer the latest assistant turn, a still-"running" status means the // update was lost — a payload-less call settles to 'unavailable' (never to // a rendered success; only a delivered payload proves rendering). const state = getArtifactRenderState(rawStatus, Boolean(artifact), isActionableTurn(isLastAssistantMessage)); const title = artifact?._meta?.title || getStringArg(args, ['title']) || 'Artifact'; const command = getStringArg(args, ['command']) || 'create'; const statusLabel = state === 'running' ? command === 'rewrite' ? 'Updating artifact' : 'Rendering artifact' : state === 'failed' ? 'Failed to render artifact' : state === 'stopped' ? 'Rendering stopped' : state === 'unavailable' ? 'Artifact unavailable' : command === 'rewrite' ? 'Updated artifact' : 'Rendered artifact'; const typeArg = getStringArg(args, ['type']); // Failed/stopped render as an error row so they never read as completed // (skills/automation widget parity); 'unavailable' keeps the neutral muted // icon (ToolWidgetStatus 'stopped') — neither a success check nor an error. const rowStatus: ToolWidgetStatus = state === 'failed' || state === 'stopped' ? 'error' : state === 'running' ? 'running' : state === 'unavailable' ? 'stopped' : 'success'; const [inlineFailedFor, setInlineFailedFor] = useState(null); const [fullscreen, setFullscreen] = useState(false); // Preview only DELIVERED markup — the args `content` fallback is never used // for execution. const previewSource = artifact?.text; const previewFailed = previewFailureApplies(inlineFailedFor, previewSource); const showPreview = !previewFailed && canPreviewArtifact(rowStatus, previewSource); const previewHtml = useMemo( () => (showPreview && previewSource ? buildArtifactPreviewHtml(previewSource, { isDark: scheme === 'dark', isSvg: isSvgArtifact(typeArg, previewSource) }) : null), [previewSource, scheme, showPreview, typeArg], ); const failInlinePreview = () => { setInlineFailedFor(previewSource ?? null); setFullscreen(false); }; return ( <> setFullscreen(true)} > ) : undefined} chipText={title} failureText={isError ? getFailureReason('error', toolCall) : previewFailed ? 'Artifact preview failed to load' : null} icon={Sparkles} status={rowStatus} title={statusLabel} /> {/* Sibling of ToolWidgetRow, not nested in its indented rowBody — the web's MessageArtifacts lifts the live iframe out the same way so it gets the widget's full width instead of the body's icon-aligned inset. */} {previewHtml ? : null} {previewHtml ? ( setFullscreen(false)} // A fullscreen-only error falls back to the (healthy) inline // preview; reopening remounts a fresh WebView. onFail={() => setFullscreen(false)} title={title} visible={fullscreen} /> ) : null} ); }