import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { SequencePanel } from './ui/SequencePanel.tsx' import { PhantomPanel } from './ui/PhantomPanel.tsx' import { KspaceView } from './kspace/KspaceView.tsx' import { BUILTIN_PHANTOMS, fetchBuiltinPhantom } from './phantom/builtins.ts' import type { Phantom } from './phantom/phantomTypes.ts' import { addSequence, deleteSequence, listSequences, renameSequence, } from './storage/seqStore.ts' import type { StoredSequence } from './storage/seqStore.ts' import { summarizeSeq } from './seq/summary.ts' import { useSimulation } from './sim/useSimulation.ts' function stripExt(name: string): string { return name.replace(/\.seq$/i, '') } export default function App() { const [sequences, setSequences] = useState([]) const [selectedSeqId, setSelectedSeqId] = useState(null) const [phantomId, setPhantomId] = useState(BUILTIN_PHANTOMS[0].id) const [phantom, setPhantom] = useState(null) const [phantomLoading, setPhantomLoading] = useState(false) const [uploadError, setUploadError] = useState(null) const phantomCache = useRef>(new Map()) const { state: sim, run, cancel, reset } = useSimulation() const refresh = useCallback(async () => { const list = await listSequences() setSequences(list) setSelectedSeqId((cur) => cur ?? (list.length ? list[0].id : null)) }, []) useEffect(() => { void refresh() }, [refresh]) // Load the selected phantom (cached). useEffect(() => { let cancelled = false const cached = phantomCache.current.get(phantomId) if (cached) { setPhantom(cached) return } const builtin = BUILTIN_PHANTOMS.find((b) => b.id === phantomId) if (!builtin) return setPhantomLoading(true) fetchBuiltinPhantom(builtin) .then((p) => { if (cancelled) return phantomCache.current.set(phantomId, p) setPhantom(p) }) .catch((err) => { if (!cancelled) console.error('Failed to load phantom', err) }) .finally(() => { if (!cancelled) setPhantomLoading(false) }) return () => { cancelled = true } }, [phantomId]) const selectedSeq = useMemo( () => sequences.find((s) => s.id === selectedSeqId) ?? null, [sequences, selectedSeqId], ) const summary = useMemo(() => (selectedSeq ? summarizeSeq(selectedSeq.text) : null), [selectedSeq]) const handleUpload = useCallback( async (files: FileList) => { setUploadError(null) let lastId: string | null = null for (const file of Array.from(files)) { const text = await file.text() const s = summarizeSeq(text) if (!s.ok) { setUploadError(`"${file.name}" is not a valid .seq file: ${s.error}`) continue } const rec = await addSequence(stripExt(file.name), text) lastId = rec.id } await refresh() if (lastId) setSelectedSeqId(lastId) }, [refresh], ) const handleRename = useCallback( async (id: string, name: string) => { await renameSequence(id, name) await refresh() }, [refresh], ) const handleDelete = useCallback( async (id: string) => { await deleteSequence(id) const list = await listSequences() setSequences(list) setSelectedSeqId((cur) => (cur === id ? (list[0]?.id ?? null) : cur)) }, [], ) const canRun = !!selectedSeq && !!phantom && sim.status !== 'running' const handleRun = useCallback(() => { if (!selectedSeq || !phantom) return reset() run(selectedSeq.text, phantom) }, [selectedSeq, phantom, run, reset]) const pct = sim.progress ? Math.round(sim.progress.fraction * 100) : 0 return (

mri-scanner

Upload a pulseq sequence, run a Bloch simulation on a digital phantom, and view the raw k-space.

seqlab ↗
{sim.status === 'done' && sim.result ? ( ) : (
{sim.status === 'running' ? ( <>

Simulating {selectedSeq?.name}…

) : ( <>

The raw k-space will appear here after you run a simulation.

)}
)}
) }