import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import ScriptEditor from './editor/ScriptEditor.tsx' import Timeline from './viewer/Timeline.tsx' import ReportPanel from './viewer/ReportPanel.tsx' import BlockInspector from './viewer/BlockInspector.tsx' import { runScript, type RunHandle } from './engine/runner.ts' import { parseSeq, SeqParseError } from './seq/parseSeq.ts' import { reconstruct, type Reconstructed } from './seq/reconstruct.ts' import type { ParsedSeq } from './seq/types.ts' import { EXAMPLES, findExample } from './examples/index.ts' import ExamplesModal from './examples/ExamplesModal.tsx' interface ConsoleLine { kind: 'out' | 'err' | 'ok' text: string } interface LoadedSeq { name: string text: string seq: ParsedSeq rec: Reconstructed source: 'run' | 'upload' } type RunStatus = 'idle' | 'booting' | 'running' function loadSeqFile(name: string, text: string, source: 'run' | 'upload'): LoadedSeq { const seq = parseSeq(text) return { name, text, seq, rec: reconstruct(seq), source } } function download(name: string, text: string) { const url = URL.createObjectURL(new Blob([text], { type: 'text/plain' })) const a = document.createElement('a') a.href = url a.download = name a.click() URL.revokeObjectURL(url) } export default function App() { const [script, setScript] = useState(() => { const hashExample = readExampleFromHash() return (hashExample ?? EXAMPLES[0]).source }) const [scriptName, setScriptName] = useState(() => { const hashExample = readExampleFromHash() return `${(hashExample ?? EXAMPLES[0]).id}.m` }) const [consoleLines, setConsoleLines] = useState([]) const [runStatus, setRunStatus] = useState('idle') const [loaded, setLoaded] = useState([]) const [activeSeq, setActiveSeq] = useState(0) const [selectedBlock, setSelectedBlock] = useState(null) const [examplesOpen, setExamplesOpen] = useState(false) const [dropActive, setDropActive] = useState(false) const [leftWidth, setLeftWidth] = useState(46) // percent const runRef = useRef(null) const consoleBodyRef = useRef(null) const fileInputRef = useRef(null) const appendConsole = useCallback((kind: ConsoleLine['kind'], text: string) => { setConsoleLines((lines) => [...lines, { kind, text }]) }, []) useEffect(() => { const el = consoleBodyRef.current if (el) el.scrollTop = el.scrollHeight }, [consoleLines]) // doRun reads the current script through a ref so its identity is stable // (the CodeMirror keymap captures it once). const scriptRef = useRef(script) scriptRef.current = script const doRun = useCallback((src?: unknown) => { if (runRef.current) return // May be called from the Run button (MouseEvent), the editor keymap (no // arg), or loadExample (explicit source string). Only a string overrides // the ref, avoiding a race with the setScript state update. const source = typeof src === 'string' ? src : scriptRef.current setConsoleLines([]) setRunStatus('booting') const handle = runScript(source, { onOutput: (text) => { setRunStatus('running') appendConsole('out', text) }, }) runRef.current = handle handle.promise .then((result) => { if (result.aborted) { appendConsole('err', '— run cancelled —\n') return } if (!result.ok) { appendConsole('err', (result.error ?? 'unknown error') + '\n') } if (result.seqFiles.length > 0) { const files: LoadedSeq[] = [] for (const f of result.seqFiles) { try { files.push(loadSeqFile(f.name, f.text, 'run')) } catch (err) { appendConsole( 'err', `failed to parse ${f.name}: ${err instanceof SeqParseError ? err.message : String(err)}\n`, ) } } setLoaded(files) setActiveSeq(0) setSelectedBlock(null) } else if (result.ok) { appendConsole('err', 'The script finished without writing any .seq file — call seq.write(...).\n') } if (result.ok) { appendConsole('ok', `— finished in ${(result.elapsedMs / 1000).toFixed(1)} s —\n`) } }) .catch((err) => { appendConsole('err', `engine error: ${err instanceof Error ? err.message : String(err)}\n`) }) .finally(() => { runRef.current = null setRunStatus('idle') }) }, [appendConsole]) const doCancel = useCallback(() => { runRef.current?.cancel() }, []) const loadExample = useCallback((id: string) => { const ex = findExample(id) if (!ex) return // Load into the editor only; the user presses Run to execute it. setScript(ex.source) setScriptName(`${ex.id}.m`) window.location.hash = `example/${ex.id}` setExamplesOpen(false) }, []) const openSeqFile = useCallback( (file: File) => { file.text().then( (text) => { try { const f = loadSeqFile(file.name, text, 'upload') setLoaded([f]) setActiveSeq(0) setSelectedBlock(null) } catch (err) { appendConsole( 'err', `failed to parse ${file.name}: ${err instanceof Error ? err.message : String(err)}\n`, ) } }, () => appendConsole('err', `could not read ${file.name}\n`), ) }, [appendConsole], ) // Drag & drop anywhere useEffect(() => { let depth = 0 const onDragEnter = (e: DragEvent) => { if (!e.dataTransfer?.types.includes('Files')) return depth++ setDropActive(true) } const onDragLeave = () => { depth = Math.max(0, depth - 1) if (depth === 0) setDropActive(false) } const onDragOver = (e: DragEvent) => { if (e.dataTransfer?.types.includes('Files')) e.preventDefault() } const onDrop = (e: DragEvent) => { depth = 0 setDropActive(false) const file = e.dataTransfer?.files?.[0] if (file) { e.preventDefault() openSeqFile(file) } } window.addEventListener('dragenter', onDragEnter) window.addEventListener('dragleave', onDragLeave) window.addEventListener('dragover', onDragOver) window.addEventListener('drop', onDrop) return () => { window.removeEventListener('dragenter', onDragEnter) window.removeEventListener('dragleave', onDragLeave) window.removeEventListener('dragover', onDragOver) window.removeEventListener('drop', onDrop) } }, [openSeqFile]) // Divider drag const onDividerDown = useCallback((e: React.MouseEvent) => { e.preventDefault() const onMove = (ev: MouseEvent) => { const pct = (ev.clientX / window.innerWidth) * 100 setLeftWidth(Math.min(70, Math.max(25, pct))) } const onUp = () => { window.removeEventListener('mousemove', onMove) window.removeEventListener('mouseup', onUp) } window.addEventListener('mousemove', onMove) window.addEventListener('mouseup', onUp) }, []) const active = loaded[activeSeq] const running = runStatus !== 'idle' const consoleEmpty = consoleLines.length === 0 const statusLabel = useMemo(() => { if (runStatus === 'booting') return 'starting engine…' if (runStatus === 'running') return 'running…' return null }, [runStatus]) return (
seqlab powered by{' '} Pulseq {' '} +{' '} numbl
{ const file = e.target.files?.[0] if (file) openSeqFile(file) e.target.value = '' }} />
{running ? ( ) : ( )} {scriptName}
Console {statusLabel && · {statusLabel}}
{consoleEmpty && !running ? ( Press Run (Ctrl+Enter) to execute the script with pulseq on the numbl MATLAB runtime — entirely in this tab. ) : ( consoleLines.map((l, i) => ( {l.text} )) )}
{!active ? (

No sequence loaded

Run the script on the left to generate a .seq file, pick an example from the gallery, or drop / open an existing .seq file to explore it.

) : ( <> {loaded.length > 1 && (
{loaded.map((f, i) => ( ))}
)}

{active.name} {active.source === 'run' ? 'generated by script' : 'opened file'}

Timeline scroll to zoom · drag to pan · click a block · double-click resets

Block inspector {selectedBlock !== null && ( )}

{selectedBlock !== null ? ( setSelectedBlock(Math.min(Math.max(i, 0), active.seq.blocks.length - 1)) } /> ) : (

Click a block in the timeline above to inspect its events.

)}
)}
{examplesOpen && ( setExamplesOpen(false)} /> )}
) } function readExampleFromHash() { const m = window.location.hash.match(/^#example\/([\w-]+)/) return m ? findExample(m[1]) : undefined }