@bramas/simulant-ui 0.0.0-stage → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/LICENCE.txt +515 -0
  2. package/dist/SimulatorApp.jsx +601 -0
  3. package/dist/components/AlgorithmViewer.css +270 -0
  4. package/dist/components/AlgorithmViewer.jsx +218 -0
  5. package/dist/components/BranchPicker.css +96 -0
  6. package/dist/components/BranchPicker.jsx +65 -0
  7. package/dist/components/ConfigGraphGenerator.css +129 -0
  8. package/dist/components/ConfigGraphGenerator.jsx +408 -0
  9. package/dist/components/GenericEditor.jsx +228 -0
  10. package/dist/components/LeanView.jsx +133 -0
  11. package/dist/components/ModelView.css +520 -0
  12. package/dist/components/ModelView.jsx +272 -0
  13. package/dist/components/SettingsLibrary.jsx +61 -0
  14. package/dist/components/StateInspector.css +136 -0
  15. package/dist/components/StateInspector.jsx +103 -0
  16. package/dist/components/highlight.js +99 -0
  17. package/dist/pico.min.css +4 -0
  18. package/dist/renderers/define.js +28 -0
  19. package/dist/renderers/ring.js +103 -0
  20. package/dist/sim/branches.js +28 -0
  21. package/dist/sim/describe.js +408 -0
  22. package/dist/sim/execution.js +42 -0
  23. package/dist/sim/project.js +349 -0
  24. package/dist/sim/snapshot.js +29 -0
  25. package/dist/sim/types.js +1 -0
  26. package/dist/simulator.css +593 -0
  27. package/package.json +50 -3
  28. package/src/SimulatorApp.tsx +661 -0
  29. package/src/components/AlgorithmViewer.css +270 -0
  30. package/src/components/AlgorithmViewer.tsx +256 -0
  31. package/src/components/BranchPicker.css +96 -0
  32. package/src/components/BranchPicker.tsx +90 -0
  33. package/src/components/ConfigGraphGenerator.css +129 -0
  34. package/src/components/ConfigGraphGenerator.tsx +448 -0
  35. package/src/components/GenericEditor.tsx +413 -0
  36. package/src/components/LeanView.tsx +162 -0
  37. package/src/components/ModelView.css +520 -0
  38. package/src/components/ModelView.tsx +317 -0
  39. package/src/components/SettingsLibrary.tsx +84 -0
  40. package/src/components/StateInspector.css +136 -0
  41. package/src/components/StateInspector.tsx +125 -0
  42. package/src/components/highlight.ts +100 -0
  43. package/src/pico.min.css +4 -0
  44. package/src/renderers/define.ts +54 -0
  45. package/src/renderers/ring.ts +112 -0
  46. package/src/sim/branches.ts +30 -0
  47. package/src/sim/describe.ts +416 -0
  48. package/src/sim/execution.ts +65 -0
  49. package/src/sim/project.ts +377 -0
  50. package/src/sim/snapshot.ts +60 -0
  51. package/src/sim/types.ts +28 -0
  52. package/src/simulator.css +593 -0
  53. package/README.md +0 -3
@@ -0,0 +1,317 @@
1
+ /**
2
+ * The model view: the experiment's semantics in words (left) beside the declarations it is built
3
+ * from, as written in their files (right), so that a reader can check them against the paper.
4
+ * Each fact links to the declarations it comes from; the round lists, phase by phase, which
5
+ * components act and what the adversary chooses. Definitions, which can be many, are grouped
6
+ * and folded below the other declarations; a reference or a click on a name opens one. In place
7
+ * of the facts, the generated Lean model (LeanView), linked to the same sources both ways.
8
+ */
9
+ import { For, Show, batch, createMemo, createSignal, onCleanup, type JSX } from 'solid-js';
10
+ import type { ModelDescription, SourceExcerpt, SourceRef } from '@bramas/simulant-core/types/project';
11
+ import { tokenizeAll } from './highlight';
12
+ import { displayPath } from '../sim/describe';
13
+ import { LeanView } from './LeanView';
14
+ import './AlgorithmViewer.css';
15
+ import './ModelView.css';
16
+
17
+ export interface ModelViewProps {
18
+ description: ModelDescription;
19
+ /** The simulation's name (the Lean module is named after it). */
20
+ simulation: string;
21
+ /** Copies a link that opens this view. */
22
+ onCopyLink?: () => void;
23
+ }
24
+
25
+ const KIND_LABEL: Record<SourceExcerpt['kind'], string> = {
26
+ experiment: 'experiment', model: 'model', component: 'component', sensor: 'sensor',
27
+ algorithm: 'algorithm', task: 'task', goal: 'goal', definition: 'definition',
28
+ };
29
+
30
+ const keyOf = (s: SourceExcerpt) => `${s.kind} ${s.name}`;
31
+ /** A definition folded: its first line after the doc comment and attributes. */
32
+ const signature = (s: SourceExcerpt) => (s.lines.find(l => /^def\s/.test(l.text)) ?? s.lines[0])?.text.replace(/\s*:=\s*$/, '') ?? s.name;
33
+ const range = (s: SourceExcerpt) => s.lines.length ? `${s.lines[0].number}–${s.lines[s.lines.length - 1].number}` : '';
34
+
35
+ export function ModelView(props: ModelViewProps) {
36
+ const d = () => props.description;
37
+ const defNames = createMemo(() => new Set(d().sources.filter(s => s.kind === 'definition').map(s => s.name)));
38
+ const excerpts = createMemo(() => d().sources.map(s => ({ ...s, key: keyOf(s), tokens: tokenizeAll(s.lines, defNames()) })));
39
+ const byKey = createMemo(() => new Map(excerpts().map(s => [s.key, s])));
40
+ const declarations = createMemo(() => excerpts().filter(s => s.kind !== 'definition'));
41
+ /** Definitions in file order, as a reader of the files meets them. */
42
+ const definitions = createMemo(() => excerpts().filter(s => s.kind === 'definition').sort((a, b) =>
43
+ a.file === b.file ? (a.lines[0]?.number ?? 0) - (b.lines[0]?.number ?? 0) : (a.file ?? '') < (b.file ?? '') ? -1 : 1));
44
+
45
+ // The group of definitions is folded at first, and so is each definition in it.
46
+ const [defsOpen, setDefsOpen] = createSignal(false);
47
+ const [openDefs, setOpenDefs] = createSignal<Set<string>>(new Set());
48
+ const [filter, setFilter] = createSignal('');
49
+ const shownDefs = createMemo(() => definitions().filter(s => s.name.toLowerCase().includes(filter().trim().toLowerCase())));
50
+ const toggleDef = (key: string) => setOpenDefs(o => { const n = new Set(o); if (n.has(key)) n.delete(key); else n.add(key); return n; });
51
+ let defsGroup: HTMLElement | undefined;
52
+ // The declaration (and line) a reference opened, highlighted for a moment.
53
+ const [flash, setFlash] = createSignal<SourceRef | undefined>();
54
+ const elements = new Map<string, HTMLElement>();
55
+ let timer: ReturnType<typeof setTimeout> | undefined;
56
+ onCleanup(() => clearTimeout(timer));
57
+
58
+ const show = (ref: SourceRef) => {
59
+ const target = byKey().get(ref.source);
60
+ if (target?.kind === 'definition') batch(() => {
61
+ setDefsOpen(true);
62
+ setOpenDefs(o => new Set(o).add(ref.source));
63
+ if (!target.name.toLowerCase().includes(filter().trim().toLowerCase())) setFilter('');
64
+ });
65
+ const el = elements.get(ref.source);
66
+ const line = ref.line !== undefined ? el?.querySelector(`[data-line-number="${ref.line}"]`) : undefined;
67
+ (line ?? el)?.scrollIntoView({ block: line ? 'center' : 'start', behavior: 'smooth' });
68
+ setFlash(ref);
69
+ clearTimeout(timer);
70
+ timer = setTimeout(() => setFlash(undefined), 1600);
71
+ };
72
+ const flashed = (source: string, line?: number) => flash()?.source === source && flash()?.line === line;
73
+
74
+ // The Lean model instead of the facts, and what in the sources it translates.
75
+ const [mode, setMode] = createSignal<'summary' | 'lean'>('summary');
76
+ const leanBlocks = createMemo(() => {
77
+ const l = d().lean;
78
+ return l && 'blocks' in l ? [...l.blocks, ...l.library.map(x => ({ refs: x.refs }))] : [];
79
+ });
80
+ const leanSources = createMemo(() => new Set(leanBlocks().flatMap(b => b.refs.map(r => r.source))));
81
+ const leanLines = createMemo(() => new Set(leanBlocks().flatMap(b => b.refs.flatMap(r => r.line !== undefined ? [`${r.source}:${r.line}`] : []))));
82
+ const [leanFocus, setLeanFocus] = createSignal<SourceRef & { seq: number }>();
83
+ let seq = 0;
84
+ const toLean = (ref: SourceRef) => setLeanFocus({ ...ref, seq: ++seq });
85
+ const refLabel = (r: SourceRef) => {
86
+ const s = byKey().get(r.source);
87
+ return s ? `${s.file ? displayPath(s.file) : s.name}:${r.line ?? s.lines[0]?.number}` : r.source;
88
+ };
89
+
90
+ const openDefinitions = () => {
91
+ setDefsOpen(true);
92
+ defsGroup?.scrollIntoView({ block: 'start', behavior: 'smooth' });
93
+ };
94
+
95
+ /**
96
+ * A declaration's lines; a definition's name opens it. Beside the Lean model, a click on a line
97
+ * finds its translation: that statement's, or the declaration's.
98
+ */
99
+ const Code = (p: { excerpt: ReturnType<typeof excerpts>[number] }): JSX.Element => (
100
+ <div class="algorithm-body">
101
+ <For each={p.excerpt.tokens}>
102
+ {line => (
103
+ <div
104
+ class={`algorithm-line${flashed(p.excerpt.key, line.number) ? ' mv-flash-line' : ''}${mode() === 'lean' && leanSources().has(p.excerpt.key) ? ' mv-to-lean' : ''}${mode() === 'lean' && leanLines().has(`${p.excerpt.key}:${line.number}`) ? ' mv-to-lean-line' : ''}`}
105
+ data-line-number={line.number}
106
+ title={mode() === 'lean' && leanSources().has(p.excerpt.key) ? 'Show its translation in Lean' : undefined}
107
+ onClick={e => {
108
+ if (mode() !== 'lean' || !leanSources().has(p.excerpt.key) || (e.target as HTMLElement).classList.contains('tok-link')) return;
109
+ const key = `${p.excerpt.key}:${line.number}`;
110
+ toLean(leanLines().has(key) ? { source: p.excerpt.key, line: line.number } : { source: p.excerpt.key });
111
+ }}
112
+ >
113
+ <span class="line-number">{line.number}</span>
114
+ <span class="line-content">
115
+ <For each={line.tokens}>
116
+ {t => t.kind === 'function' && defNames().has(t.text)
117
+ ? <span class="tok-function tok-link" onClick={() => show({ source: `definition ${t.text}` })} title={`Show the definition of ${t.text}`}>{t.text}</span>
118
+ : t.kind ? <span class={`tok-${t.kind}`}>{t.text}</span> : t.text}
119
+ </For>
120
+ </span>
121
+ </div>
122
+ )}
123
+ </For>
124
+ </div>
125
+ );
126
+
127
+ const download = () => {
128
+ const blob = new Blob([JSON.stringify(d().ir, null, 2) + '\n'], { type: 'application/json' });
129
+ const url = URL.createObjectURL(blob);
130
+ const a = document.createElement('a');
131
+ a.href = url;
132
+ a.download = `${d().experiment}.ir.json`;
133
+ a.click();
134
+ setTimeout(() => URL.revokeObjectURL(url), 1000);
135
+ };
136
+
137
+ const Ref = (p: { at: SourceRef }) => (
138
+ <Show when={byKey().get(p.at.source)}>
139
+ {s => (
140
+ <button class="mv-ref" onClick={() => show(p.at)} title={`Show ${s().name} as written (${s().file ?? ''})`}>
141
+ {s().file ? displayPath(s().file!) : s().name}:{p.at.line ?? s().lines[0]?.number}
142
+ </button>
143
+ )}
144
+ </Show>
145
+ );
146
+
147
+ const sections = () => d().sections;
148
+ const Section = (p: { section: ModelDescription['sections'][number] }) => (
149
+ <section class="sim-card mv-section">
150
+ <h3>{p.section.title}</h3>
151
+ <dl class="mv-facts">
152
+ <For each={p.section.facts}>
153
+ {f => (
154
+ <>
155
+ <dt>{f.label}</dt>
156
+ <dd>
157
+ <div class="mv-value">
158
+ <span class={f.code ? 'mv-code' : 'mv-strong'}>{f.value}</span>
159
+ <For each={f.refs ?? []}>{r => <Ref at={r} />}</For>
160
+ </div>
161
+ <Show when={f.note}><div class="mv-note">{f.note}</div></Show>
162
+ <Show when={f.details?.length}>
163
+ <ul class="mv-details"><For each={f.details}>{x => <li>{x}</li>}</For></ul>
164
+ </Show>
165
+ </dd>
166
+ </>
167
+ )}
168
+ </For>
169
+ </dl>
170
+ </section>
171
+ );
172
+
173
+ return (
174
+ <div class="model-view">
175
+ <section class="sim-card mv-intro">
176
+ <div class="mv-intro-head">
177
+ <h2>{d().experiment}</h2>
178
+ <div class="mv-chips">
179
+ <For each={d().summary}>{c => <span class="sim-chip">{c}</span>}</For>
180
+ </div>
181
+ <span class="sim-spacer" style={{ flex: 1 }} />
182
+ <Show when={d().lean}>
183
+ <div class="sim-tabs" role="tablist" aria-label="Model view">
184
+ <button role="tab" aria-selected={mode() === 'summary'} class={mode() === 'summary' ? 'sim-tab sim-tab-current' : 'sim-tab'} onClick={() => setMode('summary')}>Summary</button>
185
+ <button role="tab" aria-selected={mode() === 'lean'} class={mode() === 'lean' ? 'sim-tab sim-tab-current' : 'sim-tab'} onClick={() => setMode('lean')} title="The generated Lean model, linked to the sources">Lean</button>
186
+ </div>
187
+ </Show>
188
+ <Show when={props.onCopyLink}>
189
+ <button class="sim-ghost" onClick={() => props.onCopyLink!()} title="Copy a link that opens this view">Copy link</button>
190
+ </Show>
191
+ <button class="sim-ghost" onClick={download} title="Download the compiled experiment (the IR the simulator, the explorer and the Lean generator use)">Download IR</button>
192
+ </div>
193
+ <Show when={d().about}>
194
+ <div class="mv-about">
195
+ <For each={d().about!.split('\n\n')}>
196
+ {para => <p><For each={para.split('`')}>{(part, i) => i() % 2 ? <code>{part}</code> : part}</For></p>}
197
+ </For>
198
+ </div>
199
+ </Show>
200
+ <p class="sim-muted mv-help">
201
+ The semantics below are derived from the compiled experiment; each reference opens the declaration it comes from, as written.
202
+ <Show when={d().revision}> Sources revision <code>{d().revision}</code>.</Show>
203
+ </p>
204
+ </section>
205
+
206
+ <div class={`mv-grid${mode() === 'lean' && d().lean ? ' mv-grid-lean' : ''}`}>
207
+ <Show when={mode() === 'lean' && d().lean}>
208
+ {lean => (
209
+ <div class="mv-column">
210
+ <LeanView lean={lean()} simulation={props.simulation} refLabel={refLabel} onShowSource={show} focus={leanFocus()} />
211
+ </div>
212
+ )}
213
+ </Show>
214
+ <div class="mv-column" style={{ display: mode() === 'lean' && d().lean ? 'none' : undefined }}>
215
+ <For each={sections().slice(0, 2)}>{s => <Section section={s} />}</For>
216
+
217
+ <section class="sim-card mv-section">
218
+ <h3>Round <span class="sim-muted">what happens in each round, in order</span></h3>
219
+ <ol class="mv-round">
220
+ <For each={d().round}>
221
+ {p => (
222
+ <li classList={{ 'mv-idle': p.by?.length === 0 && !p.choices?.length }}>
223
+ <div class="mv-phase">{p.phase}</div>
224
+ <div class="mv-phase-body">
225
+ <div>{p.what}</div>
226
+ <Show when={p.by?.length || p.choices?.length}>
227
+ <div class="mv-phase-tags">
228
+ <For each={p.by ?? []}>{c => <span class="mv-tag" title="Component acting in this phase">{c}</span>}</For>
229
+ <For each={p.choices ?? []}>{c => <span class="mv-tag mv-adversary" title="Chosen by the adversary: every option is explored">adversary: {c}</span>}</For>
230
+ </div>
231
+ </Show>
232
+ </div>
233
+ </li>
234
+ )}
235
+ </For>
236
+ </ol>
237
+ </section>
238
+
239
+ <For each={sections().slice(2)}>{s => <Section section={s} />}</For>
240
+
241
+ <Show when={d().caveats.length}>
242
+ <section class="sim-card mv-section mv-caveats">
243
+ <h3>What the checks do not cover</h3>
244
+ <ul><For each={d().caveats}>{c => <li>{c}</li>}</For></ul>
245
+ </section>
246
+ </Show>
247
+ </div>
248
+
249
+ <div class="mv-sources">
250
+ <nav class="mv-toc" aria-label="Declarations">
251
+ <For each={declarations()}>
252
+ {s => (
253
+ <button class={`mv-toc-item mv-kind-${s.kind}`} onClick={() => show({ source: s.key })} title={`${KIND_LABEL[s.kind]} ${s.name}`}>
254
+ <span class="mv-kind">{KIND_LABEL[s.kind]}</span>{s.name}
255
+ </button>
256
+ )}
257
+ </For>
258
+ <Show when={definitions().length}>
259
+ <button class="mv-toc-item" onClick={openDefinitions} title="Show the definitions these declarations use">
260
+ <span class="mv-kind">definitions</span>{definitions().length}
261
+ </button>
262
+ </Show>
263
+ </nav>
264
+ <div class="mv-source-list">
265
+ <For each={declarations()} fallback={<p class="sim-muted">The sources of this experiment are not available.</p>}>
266
+ {s => (
267
+ <div class={`algorithm-viewer mv-source${flashed(s.key) ? ' mv-flash' : ''}`} ref={el => elements.set(s.key, el)}>
268
+ <div class="algorithm-header">
269
+ <span class={`mv-kind mv-kind-${s.kind}`}>{KIND_LABEL[s.kind]}</span>
270
+ <h3 class="algorithm-caption">{s.name}</h3>
271
+ <span class="algorithm-file" title={s.file}>{s.file ? displayPath(s.file) : ''}:{range(s)}</span>
272
+ <Show when={mode() === 'lean' && leanSources().has(s.key)}>
273
+ <span class="sim-spacer" style={{ flex: 1 }} />
274
+ <button class="sim-ghost mv-lean-button" onClick={() => toLean({ source: s.key })} title="Show its translation in Lean">Lean</button>
275
+ </Show>
276
+ </div>
277
+ <Code excerpt={s} />
278
+ </div>
279
+ )}
280
+ </For>
281
+
282
+ <Show when={definitions().length}>
283
+ <section class="algorithm-viewer mv-source mv-defs" ref={el => (defsGroup = el)}>
284
+ <button class="mv-defs-head" onClick={() => setDefsOpen(v => !v)} aria-expanded={defsOpen()}>
285
+ <span class="mv-fold">{defsOpen() ? '▾' : '▸'}</span>
286
+ <span class="algorithm-caption">Definitions</span>
287
+ <span class="sim-muted">{definitions().length}, in file order: functions the algorithm, the task and the parameter domains call</span>
288
+ </button>
289
+ <Show when={defsOpen()}>
290
+ <div class="mv-defs-tools">
291
+ <input type="search" placeholder="Filter by name" value={filter()} onInput={e => setFilter(e.currentTarget.value)} aria-label="Filter the definitions by name" />
292
+ <button class="sim-ghost" onClick={() => setOpenDefs(new Set(shownDefs().map(s => s.key)))}>Open all</button>
293
+ <button class="sim-ghost" onClick={() => setOpenDefs(new Set<string>())} disabled={!openDefs().size}>Fold all</button>
294
+ </div>
295
+ <For each={shownDefs()} fallback={<p class="sim-muted mv-defs-none">No definition matches.</p>}>
296
+ {s => (
297
+ <div class={`mv-def${flashed(s.key) ? ' mv-flash' : ''}`} ref={el => elements.set(s.key, el)}>
298
+ <button class="mv-def-head" onClick={() => toggleDef(s.key)} aria-expanded={openDefs().has(s.key)} title={openDefs().has(s.key) ? 'Fold' : 'Show the definition'}>
299
+ <span class="mv-fold">{openDefs().has(s.key) ? '▾' : '▸'}</span>
300
+ <span class="mv-def-signature">{signature(s)}</span>
301
+ <span class="algorithm-file" title={s.file}>{s.file ? displayPath(s.file) : ''}:{range(s)}</span>
302
+ </button>
303
+ <Show when={openDefs().has(s.key)}>
304
+ <Code excerpt={s} />
305
+ </Show>
306
+ </div>
307
+ )}
308
+ </For>
309
+ </Show>
310
+ </section>
311
+ </Show>
312
+ </div>
313
+ </div>
314
+ </div>
315
+ </div>
316
+ );
317
+ }
@@ -0,0 +1,84 @@
1
+ /**
2
+ * The settings a page can start from: the project's presets (read-only) and the settings saved
3
+ * in this browser. Each shows its parameters and the length of its path; it can be loaded or
4
+ * shared as a link, and saved settings can be deleted. The current settings are saved by name.
5
+ */
6
+ import { For, Show, createSignal } from 'solid-js';
7
+ import type { PredefinedSetting } from '@bramas/simulant-core/types/project';
8
+
9
+ export type SavedSetting = { name: string; settings: any; path?: number[] };
10
+
11
+ export interface SettingsLibraryProps {
12
+ presets: PredefinedSetting[];
13
+ saved: SavedSetting[];
14
+ /** The current settings (with defaults filled in), to mark the entries that match them. */
15
+ current: Record<string, unknown>;
16
+ onLoad: (s: SavedSetting) => void;
17
+ onShare: (s: SavedSetting) => void;
18
+ onDelete: (name: string) => void;
19
+ /** Saves the current settings and path under a name. */
20
+ onSave: (name: string) => void;
21
+ }
22
+
23
+ /** `N=10 · site=3`; long values are shortened. */
24
+ function summary(settings: any): string {
25
+ if (!settings || typeof settings !== 'object') return '';
26
+ return Object.entries(settings).map(([k, v]) => {
27
+ const s = typeof v === 'string' ? v : JSON.stringify(v);
28
+ return `${k}=${s.length > 24 ? s.slice(0, 22) + '…' : s}`;
29
+ }).join(' · ');
30
+ }
31
+
32
+ const same = (a: any, b: Record<string, unknown>) =>
33
+ !!a && Object.keys(b).every(k => JSON.stringify(a[k]) === JSON.stringify(b[k]));
34
+
35
+ export function SettingsLibrary(props: SettingsLibraryProps) {
36
+ const [name, setName] = createSignal('');
37
+ const save = (e: Event) => {
38
+ e.preventDefault();
39
+ const n = name().trim();
40
+ if (!n) return;
41
+ props.onSave(n);
42
+ setName('');
43
+ };
44
+
45
+ const Entry = (p: { s: SavedSetting; deletable: boolean }) => (
46
+ <li class={`sim-entry${same(p.s.settings, props.current) ? ' sim-entry-current' : ''}`}>
47
+ <div class="sim-entry-text">
48
+ <span class="sim-entry-name">{p.s.name}</span>
49
+ <span class="sim-muted">
50
+ {summary(p.s.settings)}
51
+ <Show when={p.s.path && p.s.path.length > 1}> · {p.s.path!.length - 1} steps</Show>
52
+ </span>
53
+ </div>
54
+ <div class="sim-entry-actions">
55
+ <button class="sim-ghost" onClick={() => props.onLoad(p.s)}>Load</button>
56
+ <button class="sim-ghost sim-icon" title="Copy a link to these settings" onClick={() => props.onShare(p.s)}>Share</button>
57
+ <Show when={p.deletable}>
58
+ <button class="sim-ghost sim-icon sim-danger" title="Delete" onClick={() => props.onDelete(p.s.name)}>Delete</button>
59
+ </Show>
60
+ </div>
61
+ </li>
62
+ );
63
+
64
+ return (
65
+ <div class="sim-library">
66
+ <Show when={props.presets.length > 0}>
67
+ <h4>Presets</h4>
68
+ <ul class="sim-entries">
69
+ <For each={props.presets}>{(s) => <Entry s={s} deletable={false} />}</For>
70
+ </ul>
71
+ </Show>
72
+ <h4>Saved in this browser</h4>
73
+ <Show when={props.saved.length > 0} fallback={<p class="sim-muted">Nothing saved yet: save the current settings and path below.</p>}>
74
+ <ul class="sim-entries">
75
+ <For each={props.saved}>{(s) => <Entry s={s} deletable={true} />}</For>
76
+ </ul>
77
+ </Show>
78
+ <form class="sim-save" onSubmit={save}>
79
+ <input type="text" placeholder="Name the current settings and path" value={name()} onInput={e => setName(e.currentTarget.value)} aria-label="Name of the saved settings" />
80
+ <button type="submit" disabled={!name().trim()}>Save</button>
81
+ </form>
82
+ </div>
83
+ );
84
+ }
@@ -0,0 +1,136 @@
1
+ /* The state inspector: the configuration as expandable trees (tokens in simulator.css). */
2
+
3
+ .state-inspector {
4
+ margin-top: 0.75rem;
5
+ text-align: left;
6
+ font-family: var(--sim-mono);
7
+ font-size: 12.5px;
8
+ background-color: var(--sim-surface);
9
+ color: var(--sim-text);
10
+ border: 1px solid var(--sim-border);
11
+ border-radius: var(--sim-radius);
12
+ padding: 0.6rem 0.85rem;
13
+ }
14
+
15
+ .si-section + .si-section {
16
+ margin-top: 0.5rem;
17
+ padding-top: 0.5rem;
18
+ border-top: 1px solid var(--sim-border);
19
+ }
20
+
21
+ .state-inspector h4 {
22
+ margin: 0 0 0.25rem;
23
+ font-size: 0.8rem;
24
+ font-weight: 600;
25
+ text-transform: uppercase;
26
+ letter-spacing: 0.04em;
27
+ color: var(--sim-muted);
28
+ font-family: var(--pico-font-family, system-ui, sans-serif);
29
+ display: flex;
30
+ align-items: center;
31
+ gap: 0.75rem;
32
+ }
33
+
34
+ .state-inspector h4.si-inline {
35
+ display: inline;
36
+ margin: 0;
37
+ }
38
+
39
+ .si-row {
40
+ display: flex;
41
+ align-items: baseline;
42
+ gap: 0.4rem;
43
+ line-height: 1.55;
44
+ white-space: nowrap;
45
+ overflow: hidden;
46
+ text-overflow: ellipsis;
47
+ }
48
+
49
+ .si-clickable {
50
+ cursor: pointer;
51
+ border-radius: 4px;
52
+ }
53
+
54
+ .si-clickable:hover {
55
+ background-color: var(--sim-surface-2);
56
+ }
57
+
58
+ .si-toggle {
59
+ display: inline-block;
60
+ width: 1em;
61
+ flex-shrink: 0;
62
+ color: var(--sim-muted);
63
+ }
64
+
65
+ .si-children {
66
+ margin-left: 1.1em;
67
+ border-left: 1px dotted var(--sim-border);
68
+ padding-left: 0.4em;
69
+ }
70
+
71
+ .si-key {
72
+ color: var(--tok-key);
73
+ }
74
+
75
+ .si-key::after {
76
+ content: ':';
77
+ color: var(--sim-muted);
78
+ }
79
+
80
+ .si-summary,
81
+ .si-empty {
82
+ color: var(--sim-muted);
83
+ overflow: hidden;
84
+ text-overflow: ellipsis;
85
+ }
86
+
87
+ .si-number { color: var(--tok-number); }
88
+ .si-boolean { color: var(--tok-action); }
89
+ .si-string { color: var(--tok-string); }
90
+ .si-none { color: var(--sim-muted); font-style: italic; }
91
+ .si-compound { color: var(--sim-text); }
92
+
93
+ .si-agent {
94
+ border-radius: 4px;
95
+ border-left: 3px solid transparent;
96
+ padding-left: 0.2rem;
97
+ }
98
+
99
+ .si-agent.si-selected {
100
+ border-left-color: var(--sim-highlight);
101
+ background-color: var(--sim-highlight-soft);
102
+ }
103
+
104
+ .si-agent.si-hidden .si-agent-name,
105
+ .si-agent.si-hidden .si-agent-summary {
106
+ opacity: 0.5;
107
+ text-decoration: line-through;
108
+ }
109
+
110
+ .si-agent.si-faulty .si-agent-summary {
111
+ color: var(--sim-failure);
112
+ }
113
+
114
+ .si-swatch {
115
+ width: 10px;
116
+ height: 10px;
117
+ border-radius: 2px;
118
+ flex-shrink: 0;
119
+ align-self: center;
120
+ }
121
+
122
+ .si-agent-name {
123
+ font-weight: 600;
124
+ }
125
+
126
+ .si-agent-summary {
127
+ color: var(--sim-muted);
128
+ }
129
+
130
+ .state-inspector .si-clear {
131
+ font-size: 0.72rem;
132
+ --pico-form-element-spacing-vertical: 0;
133
+ --pico-form-element-spacing-horizontal: 0.45rem;
134
+ text-transform: none;
135
+ letter-spacing: 0;
136
+ }
@@ -0,0 +1,125 @@
1
+ /**
2
+ * The current configuration, for any project: the world, each agent (position, line, flags,
3
+ * component fields, public state, memory) and the parameters, as trees whose nodes expand. What
4
+ * is expanded is kept while stepping through the execution.
5
+ */
6
+ import { For, Show, createSignal, type JSX } from 'solid-js';
7
+ import type { InspectedState } from '@bramas/simulant-core/types/project';
8
+ import './StateInspector.css';
9
+
10
+ export interface StateInspectorProps {
11
+ state: InspectedState;
12
+ /** The agent selected on the canvas: shown expanded and highlighted. */
13
+ selected?: number | null;
14
+ onClearSelection?: () => void;
15
+ }
16
+
17
+ const isTree = (v: unknown): v is object => typeof v === 'object' && v !== null && (Array.isArray(v) ? v.length > 0 : Object.keys(v).length > 0);
18
+
19
+ /** A short one-line form of a value. */
20
+ function inline(v: unknown): string {
21
+ if (v === null || v === undefined) return 'none';
22
+ if (Array.isArray(v)) return `[${v.map(inline).join(', ')}]`;
23
+ if (typeof v === 'object') return `{ ${Object.entries(v).map(([k, x]) => `${k}: ${inline(x)}`).join(', ')} }`;
24
+ return String(v);
25
+ }
26
+ const SHORT = 40;
27
+
28
+ function Leaf(props: { value: unknown }): JSX.Element {
29
+ const v = props.value;
30
+ const kind = v === null || v === undefined ? 'none' : Array.isArray(v) || typeof v === 'object' ? 'compound' : typeof v;
31
+ return <span class={`si-value si-${kind}`}>{inline(v)}</span>;
32
+ }
33
+
34
+ export function StateInspector(props: StateInspectorProps) {
35
+ // Expanded nodes, by path; the default applies until a node is toggled.
36
+ const [open, setOpen] = createSignal<Record<string, boolean>>({});
37
+ const isOpen = (path: string, byDefault: boolean) => open()[path] ?? byDefault;
38
+ const toggle = (path: string, byDefault: boolean) => setOpen(o => ({ ...o, [path]: !(o[path] ?? byDefault) }));
39
+
40
+ function Node(p: { name: string; value: unknown; path: string; depth: number }): JSX.Element {
41
+ const expandable = () => isTree(p.value) && inline(p.value).length > SHORT;
42
+ const byDefault = p.depth === 0;
43
+ return (
44
+ <div class="si-node">
45
+ <Show when={expandable()} fallback={
46
+ <div class="si-row"><span class="si-toggle" /><span class="si-key">{p.name}</span><Leaf value={p.value} /></div>
47
+ }>
48
+ <div class="si-row si-clickable" onClick={() => toggle(p.path, byDefault)}>
49
+ <span class="si-toggle">{isOpen(p.path, byDefault) ? '▾' : '▸'}</span>
50
+ <span class="si-key">{p.name}</span>
51
+ <Show when={!isOpen(p.path, byDefault)}>
52
+ <span class="si-summary">{Array.isArray(p.value) ? `[${(p.value as unknown[]).length}]` : `{${Object.keys(p.value as object).length}}`} {inline(p.value).slice(0, SHORT)}…</span>
53
+ </Show>
54
+ </div>
55
+ <Show when={isOpen(p.path, byDefault)}>
56
+ <div class="si-children">
57
+ <For each={Object.entries(p.value as object)}>
58
+ {([k, v]) => <Node name={k} value={v} path={`${p.path}/${k}`} depth={p.depth + 1} />}
59
+ </For>
60
+ </div>
61
+ </Show>
62
+ </Show>
63
+ </div>
64
+ );
65
+ }
66
+
67
+ const Fields = (p: { value: Record<string, unknown>; path: string }) => (
68
+ <For each={Object.entries(p.value)} fallback={<div class="si-empty">nothing</div>}>
69
+ {([k, v]) => <Node name={k} value={v} path={`${p.path}/${k}`} depth={1} />}
70
+ </For>
71
+ );
72
+
73
+ return (
74
+ <div class="state-inspector" data-state={JSON.stringify(props.state)}>
75
+ <div class="si-section">
76
+ <h4>World</h4>
77
+ <Fields value={props.state.world} path="world" />
78
+ </div>
79
+
80
+ <div class="si-section">
81
+ <h4>
82
+ Agents
83
+ <Show when={props.selected !== null && props.selected !== undefined}>
84
+ <button class="si-clear" onClick={() => props.onClearSelection?.()}>Clear selection</button>
85
+ </Show>
86
+ </h4>
87
+ <For each={props.state.agents}>
88
+ {(agent) => {
89
+ const path = `agent/${agent.id}`;
90
+ const selected = () => props.selected === agent.id;
91
+ const expanded = () => selected() || isOpen(path, false);
92
+ return (
93
+ <div class={`si-agent si-${agent.status}${selected() ? ' si-selected' : ''}`} data-agent={agent.id}>
94
+ <div class="si-row si-clickable" onClick={() => toggle(path, false)}>
95
+ <span class="si-toggle">{expanded() ? '▾' : '▸'}</span>
96
+ <span class="si-swatch" style={{ 'background-color': agent.color }} />
97
+ <span class="si-agent-name">{agent.name}</span>
98
+ <span class="si-agent-summary">
99
+ node {String(agent.value.position)}
100
+ <Show when={agent.value.line !== undefined}> · line {String(agent.value.line)}</Show>
101
+ <Show when={agent.status !== 'correct'}> · {agent.status === 'hidden' ? 'gone' : 'faulty'}</Show>
102
+ </span>
103
+ </div>
104
+ <Show when={expanded()}>
105
+ <div class="si-children"><Fields value={agent.value} path={path} /></div>
106
+ </Show>
107
+ </div>
108
+ );
109
+ }}
110
+ </For>
111
+ </div>
112
+
113
+ <div class="si-section">
114
+ <div class="si-row si-clickable" onClick={() => toggle('params', false)}>
115
+ <span class="si-toggle">{isOpen('params', false) ? '▾' : '▸'}</span>
116
+ <h4 class="si-inline">Parameters</h4>
117
+ <Show when={!isOpen('params', false)}><span class="si-summary">{inline(props.state.params)}</span></Show>
118
+ </div>
119
+ <Show when={isOpen('params', false)}>
120
+ <div class="si-children"><Fields value={props.state.params} path="params" /></div>
121
+ </Show>
122
+ </div>
123
+ </div>
124
+ );
125
+ }