// One runnable section: controls, figures, and the MATLAB behind them. // // Each lab owns a numbl session for as long as it is on screen. Moving a // control rewrites params.json and re-runs; editing the script reboots the // session, which is why an edited lab waits for the Run button. import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import ScriptEditor from '../editor/ScriptEditor.tsx' import Controls from './Controls.tsx' import { Engine } from '../engine/runner.ts' import type { Params, ParamValue, RunResult } from '../engine/types.ts' import type { LabSpec } from './types.ts' export default function Lab({ spec }: { spec: LabSpec }) { const [script, setScript] = useState(spec.script) const [pending, setPending] = useState(spec.script) const [params, setParams] = useState(spec.defaults) const [result, setResult] = useState | null>(null) const [running, setRunning] = useState(false) const [ranOnce, setRanOnce] = useState(false) const [showEditor, setShowEditor] = useState(Boolean(spec.editorOpen)) const [showLog, setShowLog] = useState(false) const engine = useMemo(() => new Engine(), []) const seq = useRef(0) useEffect(() => () => engine.dispose(), [engine]) const run = useCallback( async (src: string, p: Params) => { const my = ++seq.current setRunning(true) const res = await engine.run(spec.driver, src, spec.derive ? spec.derive(p) : p) if (seq.current !== my) return setResult(res) setRunning(false) setRanOnce(true) }, [engine, spec.driver, spec.derive], ) // first run, unless the lab is heavy enough to wait to be asked useEffect(() => { if (!spec.manual) void run(script, params) // eslint-disable-next-line react-hooks/exhaustive-deps }, []) // a control moved: re-run, debounced, but only once the lab is live const paramsRef = useRef(params) paramsRef.current = params const onControl = useCallback( (key: string, value: ParamValue) => { const next = { ...paramsRef.current, [key]: value } paramsRef.current = next setParams(next) }, [], ) const firstParams = useRef(true) useEffect(() => { if (firstParams.current) { firstParams.current = false return } if (!ranOnce) return const t = setTimeout(() => void run(script, params), 120) return () => clearTimeout(t) // eslint-disable-next-line react-hooks/exhaustive-deps }, [params]) const dirty = pending !== script const onRun = useCallback(() => { setScript(pending) void run(pending, paramsRef.current) }, [pending, run]) const data = result && result.ok ? result.data : null const summary = data && spec.summary ? spec.summary(data, params) : [] return (

{spec.title}

{spec.caption &&

{spec.caption}

}
{result && ( wall clock {result.ms < 1000 ? `${result.ms.toFixed(0)} ms` : `${(result.ms / 1000).toFixed(2)} s`} )} {summary.map((s) => ( {s.label} {s.value} ))} {result && result.output.trim() !== '' && ( )}
{result && !result.ok &&
{result.error}
}
{data ? spec.render(data, params) :
{running ? 'running' : 'press run'}
}
{showLog && result &&
{result.output}
} {showEditor && (
the method, in MATLAB syntax {dirty && edited; press run}
)}
) }