@bramas/simulant-ui 0.1.1 → 0.2.0
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.
- package/README.md +60 -0
- package/dist/SimulatorApp.jsx +782 -174
- package/dist/components/AlgorithmViewer.css +268 -74
- package/dist/components/AlgorithmViewer.jsx +171 -66
- package/dist/components/BranchPicker.css +74 -32
- package/dist/components/BranchPicker.jsx +9 -4
- package/dist/components/ConfigGraphGenerator.css +180 -38
- package/dist/components/ConfigGraphGenerator.jsx +155 -96
- package/dist/components/GenericEditor.jsx +18 -44
- package/dist/components/Icon.jsx +54 -0
- package/dist/components/ModelView.css +15 -15
- package/dist/components/SettingsLibrary.jsx +7 -5
- package/dist/components/Splitter.jsx +28 -0
- package/dist/components/StateInspector.css +59 -33
- package/dist/components/StateInspector.jsx +37 -4
- package/dist/components/theme.js +28 -0
- package/dist/pico.min.css +4 -4
- package/dist/renderers/ring.js +19 -10
- package/dist/simulator.css +1553 -363
- package/package.json +6 -6
- package/src/SimulatorApp.tsx +888 -219
- package/src/components/AlgorithmViewer.css +268 -74
- package/src/components/AlgorithmViewer.tsx +188 -75
- package/src/components/BranchPicker.css +74 -32
- package/src/components/BranchPicker.tsx +9 -4
- package/src/components/ConfigGraphGenerator.css +180 -38
- package/src/components/ConfigGraphGenerator.tsx +172 -114
- package/src/components/GenericEditor.tsx +25 -59
- package/src/components/Icon.tsx +76 -0
- package/src/components/ModelView.css +15 -15
- package/src/components/SettingsLibrary.tsx +7 -5
- package/src/components/Splitter.tsx +55 -0
- package/src/components/StateInspector.css +59 -33
- package/src/components/StateInspector.tsx +35 -4
- package/src/components/theme.ts +33 -0
- package/src/pico.min.css +4 -4
- package/src/renderers/ring.ts +22 -12
- package/src/simulator.css +1553 -363
|
@@ -1,101 +1,243 @@
|
|
|
1
|
-
/* The configuration graph (tokens in simulator.css). */
|
|
1
|
+
/* The configuration graph, filling its container (tokens in simulator.css). */
|
|
2
|
+
|
|
3
|
+
.config-graph {
|
|
4
|
+
display: flex;
|
|
5
|
+
flex-direction: column;
|
|
6
|
+
height: 100%;
|
|
7
|
+
min-height: 0;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
.cg-toolbar {
|
|
11
|
+
flex: none;
|
|
12
|
+
display: flex;
|
|
13
|
+
flex-direction: column;
|
|
14
|
+
gap: 8px;
|
|
15
|
+
padding: 10px 12px;
|
|
16
|
+
border-bottom: 1px solid var(--sim-border);
|
|
17
|
+
background: var(--sim-surface);
|
|
18
|
+
}
|
|
2
19
|
|
|
3
20
|
.cg-controls {
|
|
4
21
|
display: flex;
|
|
5
|
-
align-items:
|
|
6
|
-
gap:
|
|
22
|
+
align-items: center;
|
|
23
|
+
gap: 8px;
|
|
7
24
|
flex-wrap: wrap;
|
|
8
25
|
}
|
|
9
26
|
|
|
10
27
|
.cg-depth {
|
|
11
28
|
display: flex;
|
|
12
29
|
align-items: center;
|
|
13
|
-
gap:
|
|
30
|
+
gap: 8px;
|
|
14
31
|
white-space: nowrap;
|
|
32
|
+
font-size: 13px;
|
|
33
|
+
font-weight: 500;
|
|
34
|
+
color: var(--sim-text-2);
|
|
35
|
+
margin-right: 4px;
|
|
15
36
|
}
|
|
16
37
|
|
|
17
38
|
.cg-depth input {
|
|
18
|
-
width:
|
|
39
|
+
width: 5rem !important;
|
|
19
40
|
}
|
|
20
41
|
|
|
21
|
-
.cg-
|
|
22
|
-
|
|
23
|
-
|
|
42
|
+
.cg-divider {
|
|
43
|
+
width: 1px;
|
|
44
|
+
height: 22px;
|
|
45
|
+
margin: 0 4px;
|
|
46
|
+
background: var(--sim-border);
|
|
24
47
|
}
|
|
25
48
|
|
|
26
49
|
.cg-stats {
|
|
27
50
|
display: flex;
|
|
28
51
|
align-items: center;
|
|
29
52
|
flex-wrap: wrap;
|
|
30
|
-
gap:
|
|
31
|
-
margin-top: 0.75rem;
|
|
53
|
+
gap: 6px;
|
|
32
54
|
}
|
|
33
55
|
|
|
34
|
-
.cg-
|
|
56
|
+
.cg-notes {
|
|
35
57
|
display: flex;
|
|
36
|
-
align-items: center;
|
|
37
|
-
gap: 0.6rem;
|
|
38
58
|
flex-wrap: wrap;
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
59
|
+
gap: 4px 16px;
|
|
60
|
+
font-size: 12px;
|
|
61
|
+
color: var(--sim-muted);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
.cg-stale {
|
|
65
|
+
display: inline-flex;
|
|
66
|
+
align-items: center;
|
|
67
|
+
gap: 5px;
|
|
68
|
+
color: var(--sim-progress);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.cg-toolbar .sim-feedback {
|
|
72
|
+
margin: 0;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
.cg-body {
|
|
76
|
+
position: relative;
|
|
77
|
+
flex: 1;
|
|
78
|
+
min-height: 0;
|
|
79
|
+
display: flex;
|
|
80
|
+
flex-direction: column;
|
|
44
81
|
}
|
|
45
82
|
|
|
46
83
|
.cg-frame {
|
|
47
84
|
position: relative;
|
|
48
|
-
|
|
85
|
+
flex: 1;
|
|
86
|
+
min-height: 0;
|
|
87
|
+
overflow: hidden;
|
|
49
88
|
}
|
|
50
89
|
|
|
51
90
|
.cg-canvas {
|
|
91
|
+
position: absolute;
|
|
92
|
+
inset: 0;
|
|
52
93
|
display: block;
|
|
53
|
-
background: var(--sim-
|
|
54
|
-
|
|
55
|
-
|
|
94
|
+
background-color: var(--sim-stage);
|
|
95
|
+
background-image: radial-gradient(var(--sim-stage-dot) 1px, transparent 1.2px);
|
|
96
|
+
background-size: 18px 18px;
|
|
56
97
|
cursor: grab;
|
|
57
98
|
touch-action: none;
|
|
58
99
|
}
|
|
59
100
|
|
|
60
101
|
.cg-zoom {
|
|
61
102
|
position: absolute;
|
|
62
|
-
top:
|
|
63
|
-
right:
|
|
103
|
+
top: 12px;
|
|
104
|
+
right: 12px;
|
|
64
105
|
display: flex;
|
|
65
106
|
align-items: center;
|
|
66
|
-
gap:
|
|
107
|
+
gap: 2px;
|
|
67
108
|
background: var(--sim-surface);
|
|
68
109
|
border: 1px solid var(--sim-border);
|
|
69
110
|
border-radius: 8px;
|
|
70
|
-
padding:
|
|
111
|
+
padding: 3px;
|
|
112
|
+
box-shadow: var(--sim-shadow-md);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
.cg-zoom-value {
|
|
116
|
+
min-width: 3.2em;
|
|
117
|
+
text-align: center;
|
|
118
|
+
font-size: 12px;
|
|
119
|
+
font-variant-numeric: tabular-nums;
|
|
120
|
+
color: var(--sim-muted);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
.cg-zoom-sep {
|
|
124
|
+
width: 1px;
|
|
125
|
+
height: 16px;
|
|
126
|
+
margin: 0 2px;
|
|
127
|
+
background: var(--sim-border);
|
|
71
128
|
}
|
|
72
129
|
|
|
73
130
|
.cg-help {
|
|
74
|
-
|
|
75
|
-
|
|
131
|
+
position: absolute;
|
|
132
|
+
right: 12px;
|
|
133
|
+
bottom: 10px;
|
|
134
|
+
margin: 0;
|
|
135
|
+
padding: 3px 8px;
|
|
136
|
+
border-radius: 6px;
|
|
137
|
+
font-size: 11.5px;
|
|
138
|
+
color: var(--sim-muted);
|
|
139
|
+
background: color-mix(in srgb, var(--sim-surface) 85%, transparent);
|
|
140
|
+
pointer-events: none;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/* The selected node, in a card over the graph */
|
|
144
|
+
|
|
145
|
+
.cg-selection {
|
|
146
|
+
position: absolute;
|
|
147
|
+
left: 12px;
|
|
148
|
+
top: 12px;
|
|
149
|
+
z-index: 2;
|
|
150
|
+
display: flex;
|
|
151
|
+
flex-direction: column;
|
|
152
|
+
gap: 8px;
|
|
153
|
+
width: 196px;
|
|
154
|
+
padding: 10px;
|
|
155
|
+
border-radius: 10px;
|
|
156
|
+
background: var(--sim-surface);
|
|
157
|
+
border: 1px solid var(--sim-border);
|
|
158
|
+
box-shadow: var(--sim-shadow-lg);
|
|
159
|
+
font-size: 12.5px;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
.cg-selection-head {
|
|
163
|
+
display: flex;
|
|
164
|
+
align-items: center;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
.cg-preview {
|
|
168
|
+
align-self: center;
|
|
169
|
+
border-radius: 8px;
|
|
170
|
+
border: 1px solid var(--sim-border);
|
|
171
|
+
background-color: var(--sim-stage);
|
|
172
|
+
background-image: radial-gradient(var(--sim-stage-dot) 1px, transparent 1.2px);
|
|
173
|
+
background-size: 14px 14px;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
.cg-selection-facts {
|
|
177
|
+
display: grid;
|
|
178
|
+
grid-template-columns: auto 1fr;
|
|
179
|
+
gap: 3px 10px;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
.cg-selection-facts dt {
|
|
183
|
+
color: var(--sim-muted);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
.cg-selection-facts dd {
|
|
187
|
+
margin: 0;
|
|
188
|
+
min-width: 0;
|
|
189
|
+
overflow-wrap: anywhere;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
.cg-empty {
|
|
193
|
+
flex: 1;
|
|
194
|
+
display: flex;
|
|
195
|
+
flex-direction: column;
|
|
196
|
+
align-items: center;
|
|
197
|
+
justify-content: center;
|
|
198
|
+
gap: 10px;
|
|
199
|
+
padding: 28px 16px;
|
|
200
|
+
color: var(--sim-muted);
|
|
201
|
+
font-size: 13px;
|
|
202
|
+
text-align: center;
|
|
203
|
+
background-color: var(--sim-stage);
|
|
204
|
+
background-image: radial-gradient(var(--sim-stage-dot) 1px, transparent 1.2px);
|
|
205
|
+
background-size: 18px 18px;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
.cg-empty strong {
|
|
209
|
+
color: var(--sim-text);
|
|
210
|
+
font-size: 14px;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
.cg-empty span {
|
|
214
|
+
max-width: 28rem;
|
|
76
215
|
}
|
|
77
216
|
|
|
78
217
|
.cg-list,
|
|
79
218
|
.cg-tree {
|
|
80
|
-
|
|
81
|
-
|
|
219
|
+
flex: 1;
|
|
220
|
+
min-height: 0;
|
|
82
221
|
overflow: auto;
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
222
|
+
padding: 8px 12px;
|
|
223
|
+
font-size: 13px;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/* The selection card covers the top left: lists start beside it. */
|
|
227
|
+
.cg-body:has(.cg-selection) :is(.cg-list, .cg-tree) {
|
|
228
|
+
padding-left: 224px;
|
|
87
229
|
}
|
|
88
230
|
|
|
89
231
|
.cg-list summary {
|
|
90
232
|
cursor: pointer;
|
|
91
233
|
font-weight: 600;
|
|
92
|
-
margin:
|
|
234
|
+
margin: 4px 0;
|
|
93
235
|
}
|
|
94
236
|
|
|
95
237
|
.config-graph .cg-list-row,
|
|
96
238
|
.config-graph .cg-node-link {
|
|
97
239
|
display: flex;
|
|
98
|
-
gap:
|
|
240
|
+
gap: 12px;
|
|
99
241
|
align-items: baseline;
|
|
100
242
|
width: 100%;
|
|
101
243
|
text-align: left;
|
|
@@ -104,8 +246,8 @@
|
|
|
104
246
|
border-left: 3px solid transparent;
|
|
105
247
|
border-radius: 4px;
|
|
106
248
|
color: var(--sim-text);
|
|
107
|
-
padding:
|
|
108
|
-
font-size:
|
|
249
|
+
padding: 2px 8px;
|
|
250
|
+
font-size: 12.5px;
|
|
109
251
|
}
|
|
110
252
|
|
|
111
253
|
.config-graph .cg-node-link {
|
|
@@ -2,11 +2,14 @@
|
|
|
2
2
|
* The configuration graph of the current settings: every execution, round by round, up to a
|
|
3
3
|
* depth, generated by the project's graph worker. Shown as a drawing (by default: each node draws
|
|
4
4
|
* its configuration; drag to pan, scroll to zoom, double-click to open a node in the simulator),
|
|
5
|
-
* as a list by depth, or as a tree. The simulator's current path is highlighted.
|
|
5
|
+
* as a list by depth, or as a tree. The simulator's current path is highlighted. It fills the
|
|
6
|
+
* space it is given; the selected node is shown, drawn, in a card over the graph.
|
|
6
7
|
*/
|
|
7
8
|
import { createEffect, createMemo, createSignal, For, onCleanup, onMount, Show } from "solid-js";
|
|
9
|
+
import { Icon } from "./Icon.jsx";
|
|
8
10
|
import "./ConfigGraphGenerator.css";
|
|
9
|
-
|
|
11
|
+
/** The preview of the selected node, in CSS pixels. */
|
|
12
|
+
const PREVIEW = 132;
|
|
10
13
|
const EMPTY_STATS = { totalNodes: 0, successNodes: 0, failureNodes: 0, inProgressNodes: 0, maxDepthReached: 0 };
|
|
11
14
|
const STATUS_TEXT = { success: 'success', failure: 'failure', inProgress: 'in progress' };
|
|
12
15
|
const key = (path) => path.join('-');
|
|
@@ -35,9 +38,12 @@ export function ConfigGraphGenerator(props) {
|
|
|
35
38
|
const [pan, setPan] = createSignal({ x: 0, y: 0 });
|
|
36
39
|
const [zoom, setZoom] = createSignal(1);
|
|
37
40
|
const [width, setWidth] = createSignal(1000);
|
|
41
|
+
const [height, setHeight] = createSignal(560);
|
|
38
42
|
let canvas;
|
|
39
43
|
let frame;
|
|
40
44
|
let worker;
|
|
45
|
+
// Until the user pans or zooms, the graph keeps fitting its frame.
|
|
46
|
+
let moved = false;
|
|
41
47
|
const radius = props.configRadius || 60;
|
|
42
48
|
const horizontalSpacing = props.horizontalSpacing || 150;
|
|
43
49
|
const verticalSpacing = props.verticalSpacing || 180;
|
|
@@ -66,9 +72,9 @@ export function ConfigGraphGenerator(props) {
|
|
|
66
72
|
const xs = nodes.map(n => n.x), ys = nodes.map(n => n.y);
|
|
67
73
|
const margin = radius + 30;
|
|
68
74
|
const [x0, x1, y0, y1] = [Math.min(...xs) - margin, Math.max(...xs) + margin, Math.min(...ys) - margin, Math.max(...ys) + margin];
|
|
69
|
-
const z = Math.min(2, width() / (x1 - x0),
|
|
75
|
+
const z = Math.min(2, width() / (x1 - x0), height() / (y1 - y0));
|
|
70
76
|
setZoom(z);
|
|
71
|
-
setPan({ x: (width() - (x1 - x0) * z) / 2 - x0 * z, y: (
|
|
77
|
+
setPan({ x: (width() - (x1 - x0) * z) / 2 - x0 * z, y: (height() - (y1 - y0) * z) / 2 - y0 * z });
|
|
72
78
|
};
|
|
73
79
|
const generate = () => {
|
|
74
80
|
stop();
|
|
@@ -77,6 +83,7 @@ export function ConfigGraphGenerator(props) {
|
|
|
77
83
|
setGraph([]);
|
|
78
84
|
setStats(EMPTY_STATS);
|
|
79
85
|
setGraphSettings({ settings: props.settings });
|
|
86
|
+
moved = false;
|
|
80
87
|
if (!props.projectName || !props.createWorker) {
|
|
81
88
|
setError('This page has no graph worker.');
|
|
82
89
|
return;
|
|
@@ -132,11 +139,20 @@ export function ConfigGraphGenerator(props) {
|
|
|
132
139
|
if (s && props.onOpen)
|
|
133
140
|
props.onOpen(s.settings, node.path);
|
|
134
141
|
};
|
|
135
|
-
// The canvas
|
|
142
|
+
// The canvas fills its frame.
|
|
136
143
|
onMount(() => {
|
|
137
144
|
if (!frame)
|
|
138
145
|
return;
|
|
139
|
-
const observer = new ResizeObserver(entries =>
|
|
146
|
+
const observer = new ResizeObserver(entries => {
|
|
147
|
+
const { width: w, height: h } = entries[0].contentRect;
|
|
148
|
+
// A hidden frame (another view) has no size: keep the last one.
|
|
149
|
+
if (w <= 0 || h <= 0)
|
|
150
|
+
return;
|
|
151
|
+
setWidth(Math.max(300, Math.floor(w)));
|
|
152
|
+
setHeight(Math.max(240, Math.floor(h)));
|
|
153
|
+
if (!moved)
|
|
154
|
+
fit();
|
|
155
|
+
});
|
|
140
156
|
observer.observe(frame);
|
|
141
157
|
// Scrolling over the graph zooms it instead of scrolling the page: the listener cannot be passive.
|
|
142
158
|
canvas?.addEventListener('wheel', onWheel, { passive: false });
|
|
@@ -163,8 +179,10 @@ export function ConfigGraphGenerator(props) {
|
|
|
163
179
|
const { x: px, y: py } = pan();
|
|
164
180
|
const z = zoom();
|
|
165
181
|
const w = width();
|
|
182
|
+
const h = height();
|
|
166
183
|
const highlighted = onPath();
|
|
167
184
|
const sel = selected();
|
|
185
|
+
const theme = props.theme;
|
|
168
186
|
if (!canvas || view() !== 'visual')
|
|
169
187
|
return;
|
|
170
188
|
const ctx = canvas.getContext('2d');
|
|
@@ -172,7 +190,7 @@ export function ConfigGraphGenerator(props) {
|
|
|
172
190
|
return;
|
|
173
191
|
const ratio = window.devicePixelRatio || 1;
|
|
174
192
|
canvas.width = w * ratio;
|
|
175
|
-
canvas.height =
|
|
193
|
+
canvas.height = h * ratio;
|
|
176
194
|
const css = getComputedStyle(canvas);
|
|
177
195
|
const color = (name, fallback) => css.getPropertyValue(name).trim() || fallback;
|
|
178
196
|
const palette = {
|
|
@@ -182,13 +200,13 @@ export function ConfigGraphGenerator(props) {
|
|
|
182
200
|
};
|
|
183
201
|
const edge = color('--sim-border', '#ccc'), text = color('--sim-text', '#222'), accent = color('--sim-highlight', '#f5b301');
|
|
184
202
|
ctx.setTransform(ratio, 0, 0, ratio, 0, 0);
|
|
185
|
-
ctx.clearRect(0, 0, w,
|
|
203
|
+
ctx.clearRect(0, 0, w, h);
|
|
186
204
|
ctx.save();
|
|
187
205
|
ctx.translate(px, py);
|
|
188
206
|
ctx.scale(z, z);
|
|
189
207
|
const pad = radius * 2;
|
|
190
208
|
const visible = (n) => n.x !== undefined && n.y !== undefined
|
|
191
|
-
&& n.x >= -px / z - pad && n.x <= (w - px) / z + pad && n.y >= -py / z - pad && n.y <= (
|
|
209
|
+
&& n.x >= -px / z - pad && n.x <= (w - px) / z + pad && n.y >= -py / z - pad && n.y <= (h - py) / z + pad;
|
|
192
210
|
// Edges, the simulator's path on top.
|
|
193
211
|
for (const pass of [false, true]) {
|
|
194
212
|
ctx.strokeStyle = pass ? accent : edge;
|
|
@@ -231,7 +249,7 @@ export function ConfigGraphGenerator(props) {
|
|
|
231
249
|
ctx.translate(n.x, n.y);
|
|
232
250
|
const scale = (radius - 8) / 100;
|
|
233
251
|
ctx.scale(scale, scale);
|
|
234
|
-
props.drawConfig(ctx, config, session.model, { centerX: 0, centerY: 0, radius: 100, nodeRadius: 10, agentRadius: 5, showNodeLabels: false });
|
|
252
|
+
props.drawConfig(ctx, config, session.model, { centerX: 0, centerY: 0, radius: 100, nodeRadius: 10, agentRadius: 5, showNodeLabels: false, theme });
|
|
235
253
|
ctx.restore();
|
|
236
254
|
}
|
|
237
255
|
catch (e) {
|
|
@@ -261,7 +279,7 @@ export function ConfigGraphGenerator(props) {
|
|
|
261
279
|
if (drag) {
|
|
262
280
|
const next = { x: e.clientX - drag.x, y: e.clientY - drag.y };
|
|
263
281
|
if (Math.abs(next.x - pan().x) + Math.abs(next.y - pan().y) > 2)
|
|
264
|
-
drag.moved = true;
|
|
282
|
+
drag.moved = moved = true;
|
|
265
283
|
setPan(next);
|
|
266
284
|
}
|
|
267
285
|
else if (canvas) {
|
|
@@ -277,6 +295,7 @@ export function ConfigGraphGenerator(props) {
|
|
|
277
295
|
if (!canvas)
|
|
278
296
|
return;
|
|
279
297
|
e.preventDefault();
|
|
298
|
+
moved = true;
|
|
280
299
|
const rect = canvas.getBoundingClientRect();
|
|
281
300
|
const mx = e.clientX - rect.left, my = e.clientY - rect.top;
|
|
282
301
|
const z = Math.min(Math.max(0.05, zoom() * Math.exp(-e.deltaY * 0.0015)), 5);
|
|
@@ -285,8 +304,9 @@ export function ConfigGraphGenerator(props) {
|
|
|
285
304
|
setPan({ x: mx - wx * z, y: my - wy * z });
|
|
286
305
|
};
|
|
287
306
|
const zoomBy = (factor) => {
|
|
307
|
+
moved = true;
|
|
288
308
|
const z = Math.min(Math.max(0.05, zoom() * factor), 5);
|
|
289
|
-
const cx = width() / 2, cy =
|
|
309
|
+
const cx = width() / 2, cy = height() / 2;
|
|
290
310
|
const wx = (cx - pan().x) / zoom(), wy = (cy - pan().y) / zoom();
|
|
291
311
|
setZoom(z);
|
|
292
312
|
setPan({ x: cx - wx * z, y: cy - wy * z });
|
|
@@ -306,103 +326,142 @@ export function ConfigGraphGenerator(props) {
|
|
|
306
326
|
</div>)}
|
|
307
327
|
</Show>);
|
|
308
328
|
};
|
|
329
|
+
/** The selected node's configuration, drawn small. */
|
|
330
|
+
const Preview = (p) => {
|
|
331
|
+
let preview;
|
|
332
|
+
createEffect(() => {
|
|
333
|
+
const session = graphExecution();
|
|
334
|
+
const ctx = preview?.getContext('2d');
|
|
335
|
+
if (!preview || !ctx || !session || !props.drawConfig)
|
|
336
|
+
return;
|
|
337
|
+
const ratio = window.devicePixelRatio || 1;
|
|
338
|
+
preview.width = preview.height = PREVIEW * ratio;
|
|
339
|
+
ctx.setTransform(ratio, 0, 0, ratio, 0, 0);
|
|
340
|
+
ctx.clearRect(0, 0, PREVIEW, PREVIEW);
|
|
341
|
+
try {
|
|
342
|
+
const config = session.execution.get(p.node.path);
|
|
343
|
+
props.drawConfig(ctx, config, session.model, { centerX: PREVIEW / 2, centerY: PREVIEW / 2, radius: PREVIEW * 0.34, nodeRadius: 3, agentRadius: 4, showNodeLabels: false, theme: props.theme });
|
|
344
|
+
}
|
|
345
|
+
catch (e) {
|
|
346
|
+
console.error('Error drawing the selected configuration:', e);
|
|
347
|
+
}
|
|
348
|
+
});
|
|
349
|
+
return <canvas ref={preview} class="cg-preview" style={{ width: `${PREVIEW}px`, height: `${PREVIEW}px` }}/>;
|
|
350
|
+
};
|
|
309
351
|
return (<div class="config-graph">
|
|
310
|
-
<div class="cg-
|
|
311
|
-
<
|
|
312
|
-
|
|
313
|
-
|
|
352
|
+
<div class="cg-toolbar">
|
|
353
|
+
<div class="cg-controls">
|
|
354
|
+
<label class="cg-depth">
|
|
355
|
+
<span>Max depth</span>
|
|
356
|
+
<input type="number" value={maxDepth()} min="0" max="1000" aria-label="Max depth (rounds)" onInput={e => { const n = e.currentTarget.valueAsNumber; if (Number.isInteger(n) && n >= 0)
|
|
314
357
|
setMaxDepth(n); }}/>
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
<button onClick={
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
<
|
|
321
|
-
|
|
322
|
-
<span class="sim-spacer" style={{ flex: 1 }}/>
|
|
323
|
-
<Show when={graph().length > 0}>
|
|
324
|
-
<div class="cg-views" role="tablist" aria-label="Graph view">
|
|
325
|
-
<Show when={props.drawConfig}>
|
|
326
|
-
<button role="tab" aria-selected={view() === 'visual'} class={view() === 'visual' ? '' : 'sim-ghost'} onClick={() => setView('visual')}>Drawing</button>
|
|
327
|
-
</Show>
|
|
328
|
-
<button role="tab" aria-selected={view() === 'list'} class={view() === 'list' ? '' : 'sim-ghost'} onClick={() => setView('list')}>List</button>
|
|
329
|
-
<button role="tab" aria-selected={view() === 'tree'} class={view() === 'tree' ? '' : 'sim-ghost'} onClick={() => setView('tree')}>Tree</button>
|
|
330
|
-
</div>
|
|
331
|
-
</Show>
|
|
332
|
-
</div>
|
|
333
|
-
|
|
334
|
-
<Show when={error()}>
|
|
335
|
-
<div class="sim-notice sim-error">Could not generate the graph: {error()}</div>
|
|
336
|
-
</Show>
|
|
337
|
-
|
|
338
|
-
<Show when={stats().totalNodes > 0}>
|
|
339
|
-
<div class="cg-stats" aria-label="Statistics">
|
|
340
|
-
<span class="sim-chip">{stats().totalNodes} nodes{isGenerating() ? '…' : ''}</span>
|
|
341
|
-
<span class="sim-chip sim-success">{stats().successNodes} success</span>
|
|
342
|
-
<span class="sim-chip sim-failure">{stats().failureNodes} failure</span>
|
|
343
|
-
<span class="sim-chip sim-inProgress">{stats().inProgressNodes} in progress</span>
|
|
344
|
-
<span class="sim-chip">depth {stats().maxDepthReached}</span>
|
|
345
|
-
<Show when={stats().failureNodes > 0}>
|
|
346
|
-
<span class="sim-muted">The generation stops at the first failure.</span>
|
|
358
|
+
<span class="sim-muted">rounds</span>
|
|
359
|
+
</label>
|
|
360
|
+
<Show when={!isGenerating()} fallback={<button class="sim-btn" onClick={stop}><Icon name="stop" size={14}/> Stop</button>}>
|
|
361
|
+
<button class="sim-primary" onClick={generate}><Icon name="play" size={14}/> Generate graph</button>
|
|
362
|
+
</Show>
|
|
363
|
+
<Show when={graph().length > 0 && !isGenerating()}>
|
|
364
|
+
<button class="sim-ghost" onClick={exportGraph}><Icon name="download" size={14}/> Export JSON</button>
|
|
347
365
|
</Show>
|
|
348
|
-
<Show when={
|
|
349
|
-
<span class="
|
|
366
|
+
<Show when={stats().totalNodes > 0}>
|
|
367
|
+
<span class="cg-divider"/>
|
|
368
|
+
<div class="cg-stats" aria-label="Statistics">
|
|
369
|
+
<span class="sim-chip">{stats().totalNodes} nodes{isGenerating() ? '…' : ''}</span>
|
|
370
|
+
<span class="sim-chip sim-success">{stats().successNodes} success</span>
|
|
371
|
+
<span class="sim-chip sim-failure">{stats().failureNodes} failure</span>
|
|
372
|
+
<span class="sim-chip sim-inProgress">{stats().inProgressNodes} in progress</span>
|
|
373
|
+
<span class="sim-chip">depth {stats().maxDepthReached}</span>
|
|
374
|
+
</div>
|
|
375
|
+
</Show>
|
|
376
|
+
<span class="sim-spacer"/>
|
|
377
|
+
<Show when={graph().length > 0}>
|
|
378
|
+
<div class="sim-tabs" role="tablist" aria-label="Graph view">
|
|
379
|
+
<Show when={props.drawConfig}>
|
|
380
|
+
<button role="tab" aria-selected={view() === 'visual'} class={view() === 'visual' ? 'sim-tab sim-tab-current' : 'sim-tab'} onClick={() => setView('visual')}>Drawing</button>
|
|
381
|
+
</Show>
|
|
382
|
+
<button role="tab" aria-selected={view() === 'list'} class={view() === 'list' ? 'sim-tab sim-tab-current' : 'sim-tab'} onClick={() => setView('list')}>List</button>
|
|
383
|
+
<button role="tab" aria-selected={view() === 'tree'} class={view() === 'tree' ? 'sim-tab sim-tab-current' : 'sim-tab'} onClick={() => setView('tree')}>Tree</button>
|
|
384
|
+
</div>
|
|
350
385
|
</Show>
|
|
351
386
|
</div>
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
<button class="sim-ghost sim-icon" onClick={() => setSelected(null)} title="Clear the selection">✕</button>
|
|
363
|
-
</div>)}
|
|
364
|
-
</Show>
|
|
387
|
+
<Show when={stats().failureNodes > 0 || stale()}>
|
|
388
|
+
<div class="cg-notes">
|
|
389
|
+
<Show when={stats().failureNodes > 0}><span>The generation stops at the first failure.</span></Show>
|
|
390
|
+
<Show when={stale()}><span class="cg-stale"><Icon name="alert" size={13}/> Generated for other settings: generate again to follow the current ones.</span></Show>
|
|
391
|
+
</div>
|
|
392
|
+
</Show>
|
|
393
|
+
<Show when={error()}>
|
|
394
|
+
<div class="sim-feedback sim-settings-error"><Icon name="alert"/><div>Could not generate the graph: {error()}</div></div>
|
|
395
|
+
</Show>
|
|
396
|
+
</div>
|
|
365
397
|
|
|
366
|
-
<div
|
|
367
|
-
<
|
|
398
|
+
<div class="cg-body">
|
|
399
|
+
<div ref={frame} class="cg-frame" style={{ display: graph().length > 0 && view() === 'visual' ? undefined : 'none' }}>
|
|
400
|
+
<canvas ref={canvas} class="cg-canvas" style={{ width: `${width()}px`, height: `${height()}px` }} onMouseDown={onDown} onMouseMove={onMove} onMouseUp={onUp} onMouseLeave={() => { drag = null; }} onDblClick={e => { const n = nodeAt(e); if (n)
|
|
368
401
|
open(n); }}/>
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
402
|
+
<div class="cg-zoom">
|
|
403
|
+
<button class="sim-ghost sim-icon sim-sm" onClick={() => zoomBy(1 / 1.25)} aria-label="Zoom out" title="Zoom out"><Icon name="minus" size={14}/></button>
|
|
404
|
+
<span class="cg-zoom-value">{Math.round(zoom() * 100)}%</span>
|
|
405
|
+
<button class="sim-ghost sim-icon sim-sm" onClick={() => zoomBy(1.25)} aria-label="Zoom in" title="Zoom in"><Icon name="plus" size={14}/></button>
|
|
406
|
+
<span class="cg-zoom-sep"/>
|
|
407
|
+
<button class="sim-ghost sim-sm" onClick={() => { moved = false; fit(); }} title="Show the whole graph"><Icon name="fit" size={14}/> Fit</button>
|
|
408
|
+
</div>
|
|
409
|
+
<p class="cg-help">Drag to pan, scroll to zoom. Click a node to select it, double-click to open it in the simulator. The simulator's path is highlighted.</p>
|
|
374
410
|
</div>
|
|
375
|
-
<p class="sim-muted cg-help">Drag to pan, scroll to zoom. Click a node to select it, double-click to open it in the simulator. The simulator's path is highlighted.</p>
|
|
376
|
-
</div>
|
|
377
411
|
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
412
|
+
<Show when={graph().length > 0 && view() === 'list'}>
|
|
413
|
+
<div class="cg-list">
|
|
414
|
+
<For each={Array.from({ length: stats().maxDepthReached + 1 }, (_, i) => i)}>
|
|
415
|
+
{depth => {
|
|
382
416
|
const nodes = () => graph().filter(n => n.depth === depth);
|
|
383
417
|
return (<Show when={nodes().length > 0}>
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
418
|
+
<details open={depth < 3}>
|
|
419
|
+
<summary>Round {depth} <span class="sim-muted">({nodes().length} nodes)</span></summary>
|
|
420
|
+
<For each={nodes()}>
|
|
421
|
+
{n => (<button class={`cg-list-row cg-${n.status}${onPath().has(key(n.path)) ? ' cg-on-path' : ''}`} onClick={() => setSelected(n)} onDblClick={() => open(n)}>
|
|
422
|
+
<code>[{n.path.join(', ')}]</code>
|
|
423
|
+
<span>{STATUS_TEXT[n.status]}</span>
|
|
424
|
+
<span class="sim-muted">{n.childrenCount} children</span>
|
|
425
|
+
</button>)}
|
|
426
|
+
</For>
|
|
427
|
+
</details>
|
|
428
|
+
</Show>);
|
|
395
429
|
}}
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
430
|
+
</For>
|
|
431
|
+
</div>
|
|
432
|
+
</Show>
|
|
399
433
|
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
434
|
+
<Show when={graph().length > 0 && view() === 'tree'}>
|
|
435
|
+
<div class="cg-tree"><Tree path={[0]}/></div>
|
|
436
|
+
</Show>
|
|
403
437
|
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
438
|
+
<Show when={graph().length === 0 && !isGenerating() && !error()}>
|
|
439
|
+
<div class="cg-empty">
|
|
440
|
+
<Icon name="graph" size={32}/>
|
|
441
|
+
<strong>No graph yet</strong>
|
|
442
|
+
<span>Generate the graph to see every execution of these settings up to the depth, with the verdict of each configuration.</span>
|
|
443
|
+
<button class="sim-primary" onClick={generate}><Icon name="play" size={14}/> Generate up to depth {maxDepth()}</button>
|
|
444
|
+
</div>
|
|
445
|
+
</Show>
|
|
446
|
+
|
|
447
|
+
<Show when={selected()}>
|
|
448
|
+
{n => (<div class="cg-selection" role="region" aria-label="Selected configuration">
|
|
449
|
+
<div class="cg-selection-head">
|
|
450
|
+
<span class={`sim-chip sim-${n().status}`}>{STATUS_TEXT[n().status]}</span>
|
|
451
|
+
<span class="sim-spacer"/>
|
|
452
|
+
<button class="sim-ghost sim-icon sim-sm" onClick={() => setSelected(null)} aria-label="Clear the selection" title="Clear the selection"><Icon name="close" size={14}/></button>
|
|
453
|
+
</div>
|
|
454
|
+
<Show when={props.drawConfig}><Preview node={n()}/></Show>
|
|
455
|
+
<dl class="cg-selection-facts">
|
|
456
|
+
<dt>Path</dt><dd><code>[{n().path.join(', ')}]</code></dd>
|
|
457
|
+
<dt>Round</dt><dd>{n().depth}</dd>
|
|
458
|
+
<dt>Explored</dt><dd>{n().childrenCount} {n().childrenCount === 1 ? 'child' : 'children'}</dd>
|
|
459
|
+
</dl>
|
|
460
|
+
<Show when={props.onOpen}>
|
|
461
|
+
<button class="sim-primary sim-sm" onClick={() => open(n())}>Open in simulator</button>
|
|
462
|
+
</Show>
|
|
463
|
+
</div>)}
|
|
464
|
+
</Show>
|
|
465
|
+
</div>
|
|
407
466
|
</div>);
|
|
408
467
|
}
|