Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 | import React, { useMemo, useEffect } from 'react';
import { Sliders, Activity } from 'lucide-react';
import { useLabState } from '../../useLabState';
import JournalSidebar from '../JournalSidebar';
export default function ArtifactViewer() {
const {
selectedArtifact,
iframeRef,
injectParamShim,
runtimeParams,
paramValues,
handleParamChange,
telemetryLog,
journalOpen,
} = useLabState();
const blobUrl = useMemo(() => {
if (!selectedArtifact?.content) return null;
const html = injectParamShim(selectedArtifact.content);
const blob = new Blob([html], { type: 'text/html' });
return URL.createObjectURL(blob);
}, [selectedArtifact?.id, selectedArtifact?.content]);
useEffect(() => {
return () => { if (blobUrl) URL.revokeObjectURL(blobUrl); };
}, [blobUrl]);
if (!selectedArtifact || !blobUrl) return null;
return (
<div className="flex-1 flex flex-row h-full relative overflow-hidden">
<div className="flex-1 flex flex-col h-full relative">
<iframe ref={iframeRef} key={selectedArtifact.id} src={blobUrl} className="flex-1 w-full bg-lab-bg border-none" sandbox="allow-scripts allow-forms allow-modals" />
</div>
{(Object.keys(runtimeParams).length > 0 || telemetryLog.length > 0) && (
<div className="w-64 bg-lab-bg-secondary/95 border-l border-lab-border flex flex-col overflow-hidden">
<div className="flex-1 overflow-y-auto p-6 custom-scrollbar space-y-8">
{Object.keys(runtimeParams).length > 0 && (
<div className="space-y-6">
<div className="flex items-center gap-2 mb-4 border-b border-lab-border pb-2"><Sliders className="w-3.5 h-3.5 text-lab-accent" /><span className="text-[10px] font-bold uppercase tracking-widest text-lab-fg-secondary">Laboratory Variables</span></div>
{Object.entries(runtimeParams).map(([key, cfg]) => (
<div key={key} className="space-y-3">
<div className="flex justify-between items-center"><label className="text-[10px] font-bold text-lab-fg uppercase">{String(cfg.label || key)}</label><span className="text-[10px] font-mono text-lab-accent">{String(paramValues[key])}</span></div>
<input type="range" min={cfg.min} max={cfg.max} step={cfg.step || 1} value={paramValues[key]} onChange={(e) => handleParamChange(key, parseFloat(e.target.value))} className="w-full accent-lab-accent bg-lab-bg-elevated h-1 rounded-lg appearance-none cursor-pointer" />
</div>
))}
</div>
)}
{telemetryLog.length > 0 && (
<div className="space-y-3">
<div className="flex items-center justify-between mb-2 border-b border-lab-border pb-2">
<div className="flex items-center gap-2"><Activity className="w-3.5 h-3.5 text-lab-accent" /><span className="text-[10px] font-bold uppercase tracking-widest text-lab-fg-secondary">Flow Data</span></div>
<div className="w-1.5 h-1.5 rounded-full bg-blue-500 animate-pulse"></div>
</div>
<div className="font-mono text-[9px] text-lab-accent/80 space-y-1">
{telemetryLog.slice(-15).reverse().map((t, i) => <div key={i} className="border-l border-blue-500/20 pl-2 py-0.5 truncate">{String(t)}</div>)}
</div>
</div>
)}
</div>
</div>
)}
{journalOpen && <JournalSidebar />}
</div>
);
}
|