All files / src/components/Artifacts ArtifactViewer.jsx

0% Statements 0/17
0% Branches 0/21
0% Functions 0/7
0% Lines 0/13

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>
  );
}