import { useState, useEffect, useCallback, useRef } from 'react'; import type { Notebook, CellOutput, SimulationResponse } from '../../lib/types'; import * as api from '../../lib/api'; import { EmbedCell } from './EmbedCell'; import { Loader2 } from 'lucide-react'; interface EmbedViewerProps { notebookId: string; initialTheme: string; } export default function EmbedViewer({ notebookId, initialTheme }: EmbedViewerProps) { const [notebook, setNotebook] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [runningCells, setRunningCells] = useState>(new Set()); const [theme, setTheme] = useState<'dark' | 'light'>( initialTheme === 'dark' ? 'dark' : 'light', ); // Sync theme class on without stomping other classes useEffect(() => { document.documentElement.classList.remove('dark', 'light'); document.documentElement.classList.add(theme); }, [theme]); // Listen for postMessage theme changes from parent iframe host useEffect(() => { function handleMessage(event: MessageEvent) { if (event.data?.type === 'spicebook-theme') { const incoming = event.data.theme; if (incoming === 'dark' || incoming === 'light') { setTheme(incoming); } } } window.addEventListener('message', handleMessage); return () => window.removeEventListener('message', handleMessage); }, []); // Keep a ref to notebook for use inside handleRun to avoid stale closures const notebookRef = useRef(notebook); notebookRef.current = notebook; // Load notebook useEffect(() => { let cancelled = false; setLoading(true); setError(null); api.getNotebook(notebookId).then( (nb) => { if (!cancelled) { setNotebook(nb); setLoading(false); } }, (err) => { if (!cancelled) { setError(err instanceof Error ? err.message : 'Failed to load notebook'); setLoading(false); } }, ); return () => { cancelled = true; }; }, [notebookId]); // Run a SPICE cell — reads notebook via ref to avoid stale closures const handleRun = useCallback( async (cellId: string) => { const nb = notebookRef.current; if (!nb) return; const cell = nb.cells.find((c) => c.id === cellId); if (!cell || cell.type !== 'spice') return; setRunningCells((prev) => new Set(prev).add(cellId)); try { let result: SimulationResponse; try { result = await api.runCell(notebookId, cellId); } catch { result = await api.runSimulation(cell.source, nb.metadata.engine); } const output: CellOutput = { output_type: result.success ? 'simulation_result' : 'error', data: { success: result.success, waveform: result.waveform || null, log: result.log, error: result.error || null, elapsed_seconds: result.elapsed_seconds, }, timestamp: new Date().toISOString(), }; setNotebook((prev) => { if (!prev) return prev; return { ...prev, cells: prev.cells.map((c) => { if (c.id !== cellId) return c; const preserved = c.outputs.filter((o) => o.output_type === 'schematic'); return { ...c, outputs: [output, ...preserved] }; }), }; }); } catch (err) { const output: CellOutput = { output_type: 'error', data: { success: false, log: '', error: err instanceof Error ? err.message : 'Simulation failed', elapsed_seconds: 0, }, timestamp: new Date().toISOString(), }; setNotebook((prev) => { if (!prev) return prev; return { ...prev, cells: prev.cells.map((c) => { if (c.id !== cellId) return c; const preserved = c.outputs.filter((o) => o.output_type === 'schematic'); return { ...c, outputs: [output, ...preserved] }; }), }; }); } finally { setRunningCells((prev) => { const next = new Set(prev); next.delete(cellId); return next; }); } }, [notebookId], ); if (loading) { return (

Loading simulation...

); } if (error) { return (

Simulation not found

{error}

); } if (!notebook) return null; return (

{notebook.metadata.title}

{notebook.cells.map((cell) => ( ))}
); }