@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,601 @@
1
+ /**
2
+ * The simulator page, in two views. The simulation: an execution of the selected experiment,
3
+ * step by step. The execution toolbar is on top (rounds, the adversary's choices, the path), the
4
+ * configuration and its state on the left, the algorithm on the right, the settings and saved
5
+ * settings below, then the configuration graph. The model (`#model`): the experiment's semantics
6
+ * and the declarations it is built from, as written, to compare with the paper.
7
+ */
8
+ import { batch, createEffect, createMemo, createSignal, For, onCleanup, onMount, Show } from "solid-js";
9
+ import { ConfigGraphGenerator } from "./components/ConfigGraphGenerator.jsx";
10
+ import { formValues, GenericEditor } from "./components/GenericEditor.jsx";
11
+ import { AlgorithmViewer } from "./components/AlgorithmViewer.jsx";
12
+ import { StateInspector } from "./components/StateInspector.jsx";
13
+ import { ModelView } from "./components/ModelView.jsx";
14
+ import { SettingsLibrary } from "./components/SettingsLibrary.jsx";
15
+ import { BranchPicker } from "./components/BranchPicker.jsx";
16
+ import { distinguishing, distinguishingChoices } from "./sim/branches.js";
17
+ const DEFAULT_STORAGE_KEY = 'project_settings_v2';
18
+ const THEME_KEY = 'simulant-theme';
19
+ const CANVAS = { width: 500, height: 300 };
20
+ // Browser storage may be unavailable (private windows, blocked site data): the page works without it.
21
+ function readStorage(key) {
22
+ try {
23
+ return JSON.parse(localStorage.getItem(key) || 'null');
24
+ }
25
+ catch {
26
+ return null;
27
+ }
28
+ }
29
+ function writeStorage(key, value) {
30
+ try {
31
+ localStorage.setItem(key, JSON.stringify(value));
32
+ }
33
+ catch {
34
+ // Not saved: nothing else to do.
35
+ }
36
+ }
37
+ function getSavedSettings(storageKey, project) {
38
+ return (readStorage(storageKey) ?? {})[project] ?? [];
39
+ }
40
+ function saveSettings(storageKey, project, name, settings, path) {
41
+ const all = readStorage(storageKey) ?? {};
42
+ const existing = all[project] ?? [];
43
+ let unique = name;
44
+ for (let counter = 1; existing.some(s => s.name === unique); counter++)
45
+ unique = `${name} (${counter})`;
46
+ all[project] = [...existing, { name: unique, settings, path }];
47
+ writeStorage(storageKey, all);
48
+ }
49
+ function deleteSettings(storageKey, project, name) {
50
+ const all = readStorage(storageKey) ?? {};
51
+ if (!all[project])
52
+ return;
53
+ all[project] = all[project].filter((s) => s.name !== name);
54
+ writeStorage(storageKey, all);
55
+ }
56
+ function encodeSettingForUrl(project, settings, path) {
57
+ return btoa(encodeURIComponent(JSON.stringify({ project, settings, path })));
58
+ }
59
+ function decodeSettingFromUrl(encoded) {
60
+ try {
61
+ return JSON.parse(decodeURIComponent(atob(encoded)));
62
+ }
63
+ catch {
64
+ return null;
65
+ }
66
+ }
67
+ /** Whether a key press belongs to a form field rather than to the page's shortcuts. */
68
+ const typing = (e) => {
69
+ const t = e.target;
70
+ return !!t && (t.isContentEditable || ['INPUT', 'TEXTAREA', 'SELECT'].includes(t.tagName));
71
+ };
72
+ const VERDICT_TEXT = { success: 'success', failure: 'failure', inProgress: 'not yet' };
73
+ /** What a configuration's verdict says about the execution, under each goal of the task. */
74
+ const VERDICT_TITLE = {
75
+ eventually: {
76
+ success: 'The task is achieved: the execution succeeds here.',
77
+ failure: 'The task fails in this configuration.',
78
+ inProgress: 'The task is not achieved yet.',
79
+ },
80
+ eventuallyAlways: {
81
+ success: 'The task holds in this configuration; the execution succeeds if it keeps holding from some round on.',
82
+ failure: 'The task fails in this configuration.',
83
+ inProgress: 'The task does not hold in this configuration (yet).',
84
+ },
85
+ safety: {
86
+ success: 'No failure in this configuration.',
87
+ failure: 'The task fails in this configuration.',
88
+ inProgress: 'No failure in this configuration.',
89
+ },
90
+ };
91
+ export const SimulatorApp = (props) => {
92
+ const Projects = props.projects;
93
+ const projectKeys = Object.keys(Projects);
94
+ const storageKey = props.storageKey ?? DEFAULT_STORAGE_KEY;
95
+ const getPredefinedSettings = props.getPredefinedSettings ?? (() => []);
96
+ const [project, setProject] = createSignal(projectKeys[0]);
97
+ const [projectUserSettings, setProjectUserSettings] = createSignal(null);
98
+ // The path in the execution tree: [initial configuration, child index, child index, ...].
99
+ const [executionPath, setExecutionPath] = createSignal([0]);
100
+ const [indexInPath, setIndexInPath] = createSignal(0);
101
+ const [selectedAgent, setSelectedAgent] = createSignal(null);
102
+ // The view, in the URL's hash so that a link can open the model.
103
+ const viewOfHash = () => window.location.hash === '#model' ? 'model' : 'simulation';
104
+ const [view, setView] = createSignal(viewOfHash());
105
+ const showView = (v) => {
106
+ setView(v);
107
+ history.replaceState(null, '', `${window.location.pathname}${window.location.search}${v === 'model' ? '#model' : ''}`);
108
+ };
109
+ const onHash = () => setView(viewOfHash());
110
+ window.addEventListener('hashchange', onHash);
111
+ onCleanup(() => window.removeEventListener('hashchange', onHash));
112
+ const [showConfigGraph, setShowConfigGraph] = createSignal(false);
113
+ const [setupLog, setSetupLog] = createSignal('');
114
+ const [toast, setToast] = createSignal('');
115
+ let canvas;
116
+ const goTo = (path, index = 0) => batch(() => {
117
+ setExecutionPath(path.length ? path : [0]);
118
+ setIndexInPath(Math.min(index, Math.max(0, path.length - 1)));
119
+ setSelectedAgent(null);
120
+ });
121
+ // Theme: the system's, unless the user picked one (remembered).
122
+ const systemDark = () => !!window.matchMedia?.('(prefers-color-scheme: dark)').matches;
123
+ const [theme, setTheme] = createSignal(readStorage(THEME_KEY));
124
+ const dark = () => (theme() ?? (systemDark() ? 'dark' : 'light')) === 'dark';
125
+ createEffect(() => {
126
+ const t = theme();
127
+ if (t)
128
+ document.documentElement.setAttribute('data-theme', t);
129
+ else
130
+ document.documentElement.removeAttribute('data-theme');
131
+ });
132
+ const toggleTheme = () => {
133
+ const next = dark() ? 'light' : 'dark';
134
+ setTheme(next);
135
+ writeStorage(THEME_KEY, next);
136
+ };
137
+ let toastTimer;
138
+ const notify = (text) => {
139
+ setToast(text);
140
+ clearTimeout(toastTimer);
141
+ toastTimer = setTimeout(() => setToast(''), 2500);
142
+ };
143
+ onCleanup(() => clearTimeout(toastTimer));
144
+ // Development updates of the sources (plan, Section 6.7): `projectRevision` restarts the
145
+ // project, `redrawTick` redraws the current configuration, `updateNotice` says what happened.
146
+ const [projectRevision, setProjectRevision] = createSignal(0);
147
+ const [redrawTick, setRedrawTick] = createSignal(0);
148
+ const [updateNotice, setUpdateNotice] = createSignal(null);
149
+ createEffect(() => {
150
+ const current = Projects[project()];
151
+ setUpdateNotice(null);
152
+ if (!current.subscribe)
153
+ return;
154
+ const unsubscribe = current.subscribe(update => {
155
+ if (update.kind === 'redraw') {
156
+ setUpdateNotice(n => n?.outOfDate ? null : n);
157
+ setRedrawTick(t => t + 1);
158
+ }
159
+ else if (update.kind === 'failed') {
160
+ setUpdateNotice({ outOfDate: true, text: update.diagnostics.join('\n') });
161
+ }
162
+ else {
163
+ const { settings, notes } = update.adjust(projectUserSettings());
164
+ batch(() => {
165
+ setUpdateNotice(notes.length ? { outOfDate: false, text: notes.join('\n') } : null);
166
+ setProjectUserSettings(settings);
167
+ goTo([0]);
168
+ setProjectRevision(r => r + 1);
169
+ });
170
+ }
171
+ });
172
+ onCleanup(unsubscribe);
173
+ });
174
+ const startedProject = createMemo(() => {
175
+ projectRevision();
176
+ try {
177
+ const current = Projects[project()];
178
+ const baseModel = current.setup(projectUserSettings());
179
+ setSetupLog('');
180
+ return { baseModel, execution: current.execute(baseModel) };
181
+ }
182
+ catch (e) {
183
+ console.error('Project setup failed:', e);
184
+ setSetupLog(e instanceof Error ? e.message : String(e));
185
+ return { baseModel: null, execution: null };
186
+ }
187
+ });
188
+ const canExplore = createMemo(() => !updateNotice()?.outOfDate && !!startedProject().execution);
189
+ const currentPath = () => executionPath().slice(0, indexInPath() + 1);
190
+ const config = createMemo(() => {
191
+ const { execution } = startedProject();
192
+ if (!execution)
193
+ return null;
194
+ try {
195
+ return execution.get(currentPath());
196
+ }
197
+ catch (e) {
198
+ console.error('Error getting config at path', executionPath(), e);
199
+ return null;
200
+ }
201
+ });
202
+ // The settings the execution runs: the form's defaults, overridden by the user's.
203
+ const editorSchema = createMemo(() => (projectRevision(), Projects[project()].editorSchema));
204
+ const effectiveSettings = createMemo(() => {
205
+ const schema = editorSchema();
206
+ return schema ? formValues(schema, projectUserSettings()) : projectUserSettings() ?? {};
207
+ });
208
+ const settingsWarnings = createMemo(() => {
209
+ projectRevision();
210
+ try {
211
+ return Projects[project()].settingsWarnings?.(projectUserSettings()) ?? [];
212
+ }
213
+ catch (e) {
214
+ return [`Could not check initial settings: ${e instanceof Error ? e.message : String(e)}`];
215
+ }
216
+ });
217
+ /** The task's verdict on the current configuration, with what it means under the task's goal. */
218
+ const verdict = createMemo(() => {
219
+ redrawTick();
220
+ const c = config();
221
+ const { execution, baseModel } = startedProject();
222
+ const current = Projects[project()];
223
+ if (!c || !execution)
224
+ return undefined;
225
+ try {
226
+ if (current.verdict) {
227
+ const { verdict: v, goal } = current.verdict(c, baseModel);
228
+ return { status: v, text: VERDICT_TEXT[v], title: VERDICT_TITLE[goal][v] };
229
+ }
230
+ const path = currentPath();
231
+ const v = current.executionStatusChecker?.(c, baseModel, path.map((_, i) => execution.get(path.slice(0, i + 1))));
232
+ return v && { status: v, text: VERDICT_TEXT[v], title: "The task's verdict on this configuration" };
233
+ }
234
+ catch {
235
+ return undefined;
236
+ }
237
+ });
238
+ const childCount = createMemo(() => {
239
+ const { execution } = startedProject();
240
+ if (!execution || !config())
241
+ return 0;
242
+ try {
243
+ return execution.getChildrenCount(currentPath());
244
+ }
245
+ catch {
246
+ return 0;
247
+ }
248
+ });
249
+ /** The branches of the step that leads to `path`: its siblings, with their witnesses. */
250
+ const branchesAt = (path, withVerdicts = true) => {
251
+ const { execution, baseModel } = startedProject();
252
+ if (!execution)
253
+ return [];
254
+ try {
255
+ const count = execution.getSiblingCount(path);
256
+ const parent = path.slice(0, -1);
257
+ const labels = Array.from({ length: count }, (_, i) => parent.length ? execution.getEdgeLabel?.([...parent, i]) || '' : `initial configuration ${i}`);
258
+ const witness = execution.getEdgeWitness;
259
+ const shorts = parent.length && witness
260
+ ? distinguishingChoices(Array.from({ length: count }, (_, i) => witness([...parent, i])))
261
+ : distinguishing(labels);
262
+ const verdict = Projects[project()].verdict;
263
+ return labels.map((label, i) => ({
264
+ index: i, label, short: shorts[i],
265
+ verdict: verdict && withVerdicts && count <= 200 ? verdict(execution.get([...parent, i]), baseModel).verdict : undefined,
266
+ }));
267
+ }
268
+ catch {
269
+ return [];
270
+ }
271
+ };
272
+ const branches = createMemo(() => (redrawTick(), config() ? branchesAt(currentPath()) : []));
273
+ const [pickingBranch, setPickingBranch] = createSignal(false);
274
+ /** What distinguishes the step to round `i` from the other branches of that round. */
275
+ const stepLabel = (i) => {
276
+ if (i === 0)
277
+ return 'initial configuration';
278
+ const path = executionPath().slice(0, i + 1);
279
+ const b = branchesAt(path, false)[path[i]];
280
+ return b ? b.short : '';
281
+ };
282
+ /** Draws a branch of the current step, small. */
283
+ const drawBranch = (index, ctx, size) => {
284
+ const { execution, baseModel } = startedProject();
285
+ const draw = Projects[project()].drawConfig;
286
+ if (!execution || !draw)
287
+ return;
288
+ const c = execution.get([...currentPath().slice(0, -1), index]);
289
+ draw(ctx, c, baseModel, { centerX: size / 2, centerY: size / 2, radius: size * 0.34, nodeRadius: 4, agentRadius: 4, showNodeLabels: false });
290
+ };
291
+ const inspected = createMemo(() => {
292
+ redrawTick();
293
+ const c = config();
294
+ const model = startedProject().baseModel;
295
+ return c && model ? Projects[project()].inspect?.(c, model) : undefined;
296
+ });
297
+ const algorithmViewInfo = createMemo(() => {
298
+ redrawTick();
299
+ const c = config();
300
+ const info = c ? Projects[project()].getAlgorithmViewInfo?.(c) : undefined;
301
+ if (!info)
302
+ return undefined;
303
+ // Why each agent took its decision in the step that led here.
304
+ let explanations = [];
305
+ try {
306
+ explanations = startedProject().execution?.explain?.(currentPath()) ?? [];
307
+ }
308
+ catch (e) {
309
+ console.error('Error explaining the step:', e);
310
+ }
311
+ return { ...info, agents: info.agents.map(a => ({ ...a, explanation: a.index !== undefined ? explanations[a.index] : undefined })) };
312
+ });
313
+ // Drawing, at the screen's resolution (renderers draw in CSS pixels).
314
+ createEffect(() => {
315
+ redrawTick();
316
+ const c = config();
317
+ const baseModel = startedProject().baseModel;
318
+ if (!canvas || !c || !baseModel)
319
+ return;
320
+ const ctx = canvas.getContext('2d');
321
+ if (!ctx)
322
+ return;
323
+ const ratio = window.devicePixelRatio || 1;
324
+ if (canvas.width !== CANVAS.width * ratio) {
325
+ canvas.width = CANVAS.width * ratio;
326
+ canvas.height = CANVAS.height * ratio;
327
+ }
328
+ ctx.setTransform(ratio, 0, 0, ratio, 0, 0);
329
+ ctx.clearRect(0, 0, CANVAS.width, CANVAS.height);
330
+ const current = Projects[project()];
331
+ try {
332
+ current.drawConfig?.(ctx, c, baseModel, { centerX: 250, centerY: 150, radius: 100, showNodeLabels: true });
333
+ }
334
+ catch (e) {
335
+ console.error('Error drawing config:', e);
336
+ }
337
+ });
338
+ // Clicking an agent selects it in the state inspector.
339
+ const handleCanvasClick = (e) => {
340
+ const c = config();
341
+ const baseModel = startedProject().baseModel;
342
+ const current = Projects[project()];
343
+ if (!canvas || !c || !baseModel || !current.getAgentsRenderInfo)
344
+ return;
345
+ const rect = canvas.getBoundingClientRect();
346
+ const x = (e.clientX - rect.left) * CANVAS.width / rect.width;
347
+ const y = (e.clientY - rect.top) * CANVAS.height / rect.height;
348
+ try {
349
+ const hit = current.getAgentsRenderInfo(c, baseModel, 250, 150, 100)
350
+ .find(a => Math.hypot(x - a.position.x, y - a.position.y) <= 10);
351
+ setSelectedAgent(hit ?? null);
352
+ }
353
+ catch (err) {
354
+ console.error('Error handling canvas click:', err);
355
+ }
356
+ };
357
+ // Navigation
358
+ const onNext = () => {
359
+ if (!canExplore())
360
+ return;
361
+ if (indexInPath() < executionPath().length - 1) {
362
+ setIndexInPath(i => i + 1);
363
+ }
364
+ else if (childCount() > 0) {
365
+ batch(() => {
366
+ setExecutionPath(p => [...p, 0]);
367
+ setIndexInPath(i => i + 1);
368
+ });
369
+ }
370
+ };
371
+ const onPrevious = () => {
372
+ if (canExplore() && indexInPath() > 0)
373
+ setIndexInPath(i => i - 1);
374
+ };
375
+ const setChoice = (index) => {
376
+ if (!canExplore())
377
+ return;
378
+ // A different choice at this step: the steps after it no longer apply.
379
+ setExecutionPath([...executionPath().slice(0, indexInPath()), index]);
380
+ };
381
+ const onKey = (e) => {
382
+ if (modelShown() || typing(e) || e.altKey || e.ctrlKey || e.metaKey)
383
+ return;
384
+ if (e.key === 'ArrowRight') {
385
+ onNext();
386
+ e.preventDefault();
387
+ }
388
+ else if (e.key === 'ArrowLeft') {
389
+ onPrevious();
390
+ e.preventDefault();
391
+ }
392
+ };
393
+ window.addEventListener('keydown', onKey);
394
+ onCleanup(() => window.removeEventListener('keydown', onKey));
395
+ // Settings
396
+ const [savedSettings, setSavedSettings] = createSignal([]);
397
+ createEffect(() => setSavedSettings(getSavedSettings(storageKey, project())));
398
+ const predefinedSettings = createMemo(() => getPredefinedSettings(project()));
399
+ // Settings saved or shared by an older version of a project may use older parameter names.
400
+ const loadedSettings = (name, settings) => Projects[name]?.normalizeSettings?.(settings) ?? settings;
401
+ const loadSettings = (s) => batch(() => {
402
+ setProjectUserSettings(loadedSettings(project(), s.settings));
403
+ goTo(Array.isArray(s.path) ? s.path : [0]);
404
+ });
405
+ const shareLink = (settings, path) => `${window.location.origin}${window.location.pathname}?shared=${encodeSettingForUrl(project(), settings, path)}`;
406
+ const copy = async (url) => {
407
+ try {
408
+ await navigator.clipboard.writeText(url);
409
+ notify('Link copied to the clipboard');
410
+ }
411
+ catch {
412
+ window.prompt('Copy this link:', url);
413
+ }
414
+ };
415
+ onMount(() => {
416
+ const shared = new URLSearchParams(window.location.search).get('shared');
417
+ const data = shared && decodeSettingFromUrl(shared);
418
+ if (data && data.project in Projects && data.settings) {
419
+ batch(() => {
420
+ setProject(data.project);
421
+ setProjectUserSettings(loadedSettings(data.project, data.settings));
422
+ const path = Array.isArray(data.path) ? data.path : [0];
423
+ // A shared link opens at the end of its path.
424
+ goTo(path, path.length - 1);
425
+ });
426
+ }
427
+ });
428
+ /** The model view's description of the experiment (redone when the sources change). */
429
+ const description = createMemo(() => {
430
+ projectRevision();
431
+ redrawTick();
432
+ try {
433
+ return Projects[project()].describe?.();
434
+ }
435
+ catch (e) {
436
+ console.error('Error describing the model:', e);
437
+ return undefined;
438
+ }
439
+ });
440
+ const modelShown = () => view() === 'model' && !!description();
441
+ const chooseProject = (name) => batch(() => {
442
+ setProjectUserSettings(null);
443
+ goTo([0]);
444
+ setProject(name);
445
+ });
446
+ return (<div class="sim-app">
447
+ <header class="sim-header">
448
+ <h1>{props.title ?? 'Simulant'}</h1>
449
+ <select value={project()} onChange={(e) => chooseProject(e.currentTarget.value)} aria-label="Simulation">
450
+ <For each={projectKeys}>{(p) => <option value={p}>{p}</option>}</For>
451
+ </select>
452
+ <Show when={description()}>
453
+ <div class="sim-tabs" role="tablist">
454
+ <button role="tab" aria-selected={view() === 'simulation'} class={view() === 'simulation' ? 'sim-tab sim-tab-current' : 'sim-tab'} onClick={() => showView('simulation')}>Simulation</button>
455
+ <button role="tab" aria-selected={view() === 'model'} class={view() === 'model' ? 'sim-tab sim-tab-current' : 'sim-tab'} onClick={() => showView('model')} title="The model and the experiment, as defined">Model</button>
456
+ </div>
457
+ </Show>
458
+ <span class="sim-spacer"/>
459
+ <button class="sim-ghost theme-toggle" onClick={toggleTheme} title={dark() ? 'Light theme' : 'Dark theme'}>
460
+ {dark() ? '☀' : '🌙'}
461
+ </button>
462
+ </header>
463
+
464
+ <Show when={updateNotice()}>
465
+ {notice => (<pre class={`sim-notice${notice().outOfDate ? ' sim-error' : ''}`}>
466
+ {notice().outOfDate ? 'Out of date: the sources do not compile; showing the last version that did.\n' : 'Sources updated; the execution restarted.\n'}
467
+ {notice().text}
468
+ </pre>)}
469
+ </Show>
470
+ <Show when={modelShown() && description()}>
471
+ {d => <ModelView description={d()} simulation={project()} onCopyLink={() => copy(`${shareLink(effectiveSettings(), currentPath())}#model`)}/>}
472
+ </Show>
473
+
474
+ <div class="sim-simulation" style={{ display: modelShown() ? 'none' : undefined }}>
475
+ <Show when={description()}>
476
+ {d => (<button class="sim-model-strip" onClick={() => showView('model')} title="Show the model and the experiment as defined">
477
+ <span class="sim-model-strip-label">Model</span>
478
+ <For each={d().summary}>{c => <span class="sim-model-strip-item">{c}</span>}</For>
479
+ <span class="sim-model-strip-more">details →</span>
480
+ </button>)}
481
+ </Show>
482
+
483
+ <section class="sim-card">
484
+ <div class="sim-toolbar">
485
+ <div class="sim-group">
486
+ <button class="sim-ghost sim-icon" onClick={() => setIndexInPath(0)} disabled={indexInPath() === 0 || !canExplore()} title="First step">«</button>
487
+ <button onClick={onPrevious} disabled={indexInPath() === 0 || !canExplore()} title="Previous step (←)">Previous {'<'}</button>
488
+ <span class="sim-round">Round {indexInPath()}</span>
489
+ <button onClick={onNext} disabled={!canExplore() || (indexInPath() === executionPath().length - 1 && childCount() === 0)} title="Next step (→)">{'>'} Next</button>
490
+ <button class="sim-ghost sim-icon" onClick={() => setIndexInPath(executionPath().length - 1)} disabled={indexInPath() === executionPath().length - 1 || !canExplore()} title="Last step of the path">»</button>
491
+ </div>
492
+ <Show when={verdict()}>
493
+ {v => <span class={`sim-chip sim-${v().status}`} title={v().title}>task: {v().text}</span>}
494
+ </Show>
495
+ <Show when={config() && childCount() === 0}>
496
+ <span class="sim-chip" title="No successor: the execution ends here">end of execution</span>
497
+ </Show>
498
+ <Show when={branches().length > 1}>
499
+ <span class="sim-divider"/>
500
+ <div class="sim-group sim-choices">
501
+ <button class="sim-ghost sim-icon" onClick={() => setChoice((currentPath().at(-1) + branches().length - 1) % branches().length)} disabled={!canExplore()} title="Previous branch">‹</button>
502
+ <span class="sim-branch">Branch {currentPath().at(-1)} ∈ [0, {branches().length - 1}]</span>
503
+ <button class="sim-ghost sim-icon" onClick={() => setChoice((currentPath().at(-1) + 1) % branches().length)} disabled={!canExplore()} title="Next branch">›</button>
504
+ <span class="sim-step-label sim-branch-label" title={branches()[currentPath().at(-1)]?.label}>{branches()[currentPath().at(-1)]?.short}</span>
505
+ <button class="sim-ghost" onClick={() => setPickingBranch(true)} disabled={!canExplore()} title="See every branch of this step and pick one">All branches</button>
506
+ </div>
507
+ </Show>
508
+ <span class="sim-spacer" style={{ flex: 1 }}/>
509
+ <button class="sim-ghost" onClick={() => copy(shareLink(effectiveSettings(), currentPath()))} title="Copy a link to these settings and this path">Share</button>
510
+ </div>
511
+ <div class="sim-path">
512
+ <span class="sim-path-label">Path</span>
513
+ <For each={executionPath()}>
514
+ {(step, i) => (<button class={`sim-step${i() === indexInPath() ? ' sim-current' : ''}${i() > indexInPath() ? ' sim-future' : ''}`} onClick={() => setIndexInPath(i())} title={`Round ${i()}: ${stepLabel(i())}`}>
515
+ {step}
516
+ </button>)}
517
+ </For>
518
+ <Show when={indexInPath() > 0 && stepLabel(indexInPath())}>
519
+ <span class="sim-step-label">last step: {stepLabel(indexInPath())}</span>
520
+ </Show>
521
+ </div>
522
+ </section>
523
+
524
+ <div class="sim-main">
525
+ <div class="sim-left">
526
+ <canvas ref={canvas} class="sim-canvas" style={{ width: `${CANVAS.width}px`, height: `${CANVAS.height}px` }} onClick={handleCanvasClick}/>
527
+ <Show when={inspected()}>
528
+ {state => <StateInspector state={state()} selected={selectedAgent()?.agentId ?? null} onClearSelection={() => setSelectedAgent(null)}/>}
529
+ </Show>
530
+ </div>
531
+ <Show when={config() && algorithmViewInfo()}>
532
+ {info => <AlgorithmViewer source={info().source} agents={info().agents}/>}
533
+ </Show>
534
+ </div>
535
+
536
+ <div class="sim-panels">
537
+ <section class="sim-card sim-settings">
538
+ <h3>
539
+ Settings
540
+ <span class="sim-spacer" style={{ flex: 1 }}/>
541
+ <button class="sim-ghost" onClick={() => batch(() => { setProjectUserSettings(null); goTo([0]); })} disabled={!projectUserSettings()}>Reset to defaults</button>
542
+ </h3>
543
+ <Show when={editorSchema()} keyed>
544
+ {schema => (<GenericEditor schema={schema} value={projectUserSettings()} onInput={(v) => batch(() => { setProjectUserSettings(v); goTo([0]); })}/>)}
545
+ </Show>
546
+ <Show when={setupLog()}>
547
+ <div class="sim-notice sim-error sim-settings-feedback" role="alert"><mark>{setupLog()}</mark></div>
548
+ </Show>
549
+ <Show when={!setupLog() && settingsWarnings().length > 0}>
550
+ <div class="sim-notice sim-settings-warning sim-settings-feedback" role="alert">
551
+ <strong>Initial settings warning</strong>
552
+ <For each={settingsWarnings()}>{warning => <div>{warning}</div>}</For>
553
+ <div>You can still explore this execution. Checks over the declared domains do not cover settings outside them.</div>
554
+ </div>
555
+ </Show>
556
+ </section>
557
+ <section class="sim-card">
558
+ <h3>Saved settings</h3>
559
+ <SettingsLibrary presets={predefinedSettings()} saved={savedSettings()} current={effectiveSettings()} onLoad={loadSettings} onShare={s => copy(shareLink(loadedSettings(project(), s.settings), s.path))} onDelete={name => {
560
+ deleteSettings(storageKey, project(), name);
561
+ setSavedSettings(getSavedSettings(storageKey, project()));
562
+ }} onSave={name => {
563
+ saveSettings(storageKey, project(), name, effectiveSettings(), currentPath());
564
+ setSavedSettings(getSavedSettings(storageKey, project()));
565
+ notify(`Saved "${name}"`);
566
+ }}/>
567
+ </section>
568
+ </div>
569
+
570
+ <Show when={Projects[project()].executionStatusChecker}>
571
+ <section class="sim-card sim-section">
572
+ <h3>
573
+ Configuration graph
574
+ <span class="sim-muted">every execution from these settings, round by round</span>
575
+ <span class="sim-spacer" style={{ flex: 1 }}/>
576
+ <button class={showConfigGraph() ? 'sim-ghost' : ''} onClick={() => setShowConfigGraph(v => !v)}>
577
+ {showConfigGraph() ? 'Hide Configuration Graph' : 'Show Configuration Graph'}
578
+ </button>
579
+ </h3>
580
+ <Show when={showConfigGraph() && !updateNotice()?.outOfDate && projectRevision() + 1} keyed>
581
+ <ConfigGraphGenerator revision={Projects[project()].revision?.()} settings={effectiveSettings()} setup={Projects[project()].setup} execute={Projects[project()].execute} executionStatusChecker={Projects[project()].executionStatusChecker} drawConfig={Projects[project()].drawConfig} projectName={project()} createWorker={props.createGraphWorker} currentPath={currentPath()} onOpen={(settings, path) => batch(() => {
582
+ setProjectUserSettings(settings);
583
+ goTo(path, path.length - 1);
584
+ window.scrollTo({ top: 0, behavior: 'smooth' });
585
+ })} configRadius={50} horizontalSpacing={140} verticalSpacing={150}/>
586
+ </Show>
587
+ </section>
588
+ </Show>
589
+
590
+ </div>
591
+
592
+ <Show when={pickingBranch() && branches().length > 1}>
593
+ <BranchPicker branches={branches()} current={currentPath().at(-1)} draw={Projects[project()].drawConfig ? drawBranch : undefined} onPick={i => { setChoice(i); setPickingBranch(false); }} onClose={() => setPickingBranch(false)}/>
594
+ </Show>
595
+
596
+ <Show when={toast()}>
597
+ <div class="sim-toast" role="status">{toast()}</div>
598
+ </Show>
599
+ </div>);
600
+ };
601
+ export default SimulatorApp;