088be4cAn interactive tutorial on the Dirichlet problem, accuracy against costJeremy Magland 1// One runnable section: controls, figures, and the MATLAB behind them.
2//
3// Each lab owns a numbl session for as long as it is on screen. Moving a
4// control rewrites params.json and re-runs; editing the script reboots the
5// session, which is why an edited lab waits for the Run button.
6import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
7import ScriptEditor from '../editor/ScriptEditor.tsx'
8import Controls from './Controls.tsx'
9import { Engine } from '../engine/runner.ts'
10import type { Params, ParamValue, RunResult } from '../engine/types.ts'
11import type { LabSpec } from './types.ts'
13export default function Lab({ spec }: { spec: LabSpec }) {
14 const [script, setScript] = useState(spec.script)
15 const [pending, setPending] = useState(spec.script)
16 const [params, setParams] = useState<Params>(spec.defaults)
17 const [result, setResult] = useState<RunResult<unknown> | null>(null)
18 const [running, setRunning] = useState(false)
19 const [ranOnce, setRanOnce] = useState(false)
20 const [showEditor, setShowEditor] = useState(Boolean(spec.editorOpen))
21 const [showLog, setShowLog] = useState(false)
22 const engine = useMemo(() => new Engine(), [])
23 const seq = useRef(0)
25 useEffect(() => () => engine.dispose(), [engine])
27 const run = useCallback(
28 async (src: string, p: Params) => {
29 const my = ++seq.current
30 setRunning(true)
31 const res = await engine.run(spec.driver, src, spec.derive ? spec.derive(p) : p)
32 if (seq.current !== my) return
33 setResult(res)
34 setRunning(false)
35 setRanOnce(true)
36 },
37 [engine, spec.driver, spec.derive],
38 )
40 // first run, unless the lab is heavy enough to wait to be asked
41 useEffect(() => {
42 if (!spec.manual) void run(script, params)
43 // eslint-disable-next-line react-hooks/exhaustive-deps
44 }, [])
46 // a control moved: re-run, debounced, but only once the lab is live
47 const paramsRef = useRef(params)
48 paramsRef.current = params
49 const onControl = useCallback(
50 (key: string, value: ParamValue) => {
51 const next = { ...paramsRef.current, [key]: value }
52 paramsRef.current = next
53 setParams(next)
54 },
55 [],
56 )
57 const firstParams = useRef(true)
58 useEffect(() => {
59 if (firstParams.current) {
60 firstParams.current = false
61 return
62 }
63 if (!ranOnce) return
64 const t = setTimeout(() => void run(script, params), 120)
65 return () => clearTimeout(t)
66 // eslint-disable-next-line react-hooks/exhaustive-deps
67 }, [params])
69 const dirty = pending !== script
70 const onRun = useCallback(() => {
71 setScript(pending)
72 void run(pending, paramsRef.current)
73 }, [pending, run])
75 const data = result && result.ok ? result.data : null
76 const summary = data && spec.summary ? spec.summary(data, params) : []
78 return (
79 <section className="lab" id={`lab-${spec.id}`}>
80 <header className="lab-head">
81 <h3>{spec.title}</h3>
82 {spec.caption && <p className="lab-caption">{spec.caption}</p>}
83 </header>
85 <Controls controls={spec.controls} params={params} onChange={onControl} />
87 <div className="lab-bar">
88 <button className={dirty || !ranOnce ? 'primary' : ''} onClick={onRun} disabled={running}>
89 {running ? 'running...' : dirty ? 'run edited script' : 'run'}
90 </button>
91 {result && (
92 <span className="stat">
93 <span className="stat-label">wall clock</span>
94 <span className="stat-value">{result.ms < 1000 ? `${result.ms.toFixed(0)} ms` : `${(result.ms / 1000).toFixed(2)} s`}</span>
95 </span>
96 )}
97 {summary.map((s) => (
98 <span className="stat" key={s.label}>
99 <span className="stat-label">{s.label}</span>
100 <span className="stat-value">{s.value}</span>
101 </span>
102 ))}
103 <span className="lab-bar-gap" />
104 <button className="ghost" onClick={() => setShowEditor((v) => !v)}>
105 {showEditor ? 'hide script' : 'show script'}
106 </button>
107 {result && result.output.trim() !== '' && (
108 <button className="ghost" onClick={() => setShowLog((v) => !v)}>
109 {showLog ? 'hide output' : 'output'}
110 </button>
111 )}
112 </div>
114 {result && !result.ok && <pre className="lab-error">{result.error}</pre>}
116 <div className="lab-figures">
117 {data ? spec.render(data, params) : <div className="lab-placeholder">{running ? 'running' : 'press run'}</div>}
118 </div>
120 {showLog && result && <pre className="lab-log">{result.output}</pre>}
122 {showEditor && (
123 <div className="lab-editor">
124 <div className="lab-editor-bar">
125 <span>the method, in MATLAB syntax</span>
126 <span className="lab-bar-gap" />
127 {dirty && <span className="warn">edited; press run</span>}
128 <button
129 className="ghost"
130 onClick={() => {
131 setPending(spec.script)
132 setScript(spec.script)
133 void run(spec.script, paramsRef.current)
134 }}
135 >
136 reset
137 </button>
138 </div>
139 <ScriptEditor value={pending} onChange={setPending} onRun={onRun} />
140 </div>
141 )}
142 </section>
143 )
144}