@virentia/inspector 0.1.2

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/dist/index.mjs ADDED
@@ -0,0 +1,800 @@
1
+ import { createElement, useCallback, useEffect, useMemo, useRef, useState } from "react";
2
+ import { createRoot } from "react-dom/client";
3
+ import { Badge, Box, Button, Group, MantineProvider, Modal, Paper, ScrollArea, Stack, Switch, Text, Textarea, Title } from "@mantine/core";
4
+ import { event, reaction, scope, store } from "@virentia/core";
5
+ import { connectVirentiaInspector } from "@virentia/core/devtools";
6
+ import { ScopeProvider, useUnit } from "@virentia/react";
7
+ import { Background, Controls, Handle, Position, ReactFlow, ReactFlowProvider } from "@xyflow/react";
8
+ import { jsx, jsxs } from "react/jsx-runtime";
9
+ //#region lib/client/shared/api/devtools.ts
10
+ const emptySnapshot = {
11
+ nodes: [],
12
+ edges: [],
13
+ scopes: [],
14
+ breakpoints: []
15
+ };
16
+ //#endregion
17
+ //#region lib/client/shared/graph/index.ts
18
+ function createReactiveSelection(snapshot, selectedNodeId) {
19
+ if (!selectedNodeId || !snapshot.nodes.some((node) => node.id === selectedNodeId)) return null;
20
+ const nextByNode = /* @__PURE__ */ new Map();
21
+ const previousByNode = /* @__PURE__ */ new Map();
22
+ const nodeIds = new Set([selectedNodeId]);
23
+ const edgeIds = /* @__PURE__ */ new Set();
24
+ for (const node of snapshot.nodes) {
25
+ nextByNode.set(node.id, []);
26
+ previousByNode.set(node.id, []);
27
+ }
28
+ for (const edge of snapshot.edges) {
29
+ nextByNode.get(edge.source)?.push(edge);
30
+ previousByNode.get(edge.target)?.push(edge);
31
+ }
32
+ walkForward(selectedNodeId, /* @__PURE__ */ new Set());
33
+ walkBackward(selectedNodeId, /* @__PURE__ */ new Set());
34
+ return {
35
+ nodeIds: [...nodeIds],
36
+ edgeIds: [...edgeIds]
37
+ };
38
+ function walkForward(nodeId, visited) {
39
+ if (visited.has(nodeId)) return;
40
+ visited.add(nodeId);
41
+ nodeIds.add(nodeId);
42
+ for (const edge of nextByNode.get(nodeId) ?? []) {
43
+ edgeIds.add(edge.id);
44
+ nodeIds.add(edge.target);
45
+ walkForward(edge.target, visited);
46
+ }
47
+ }
48
+ function walkBackward(nodeId, visited) {
49
+ if (visited.has(nodeId)) return;
50
+ visited.add(nodeId);
51
+ nodeIds.add(nodeId);
52
+ for (const edge of previousByNode.get(nodeId) ?? []) {
53
+ edgeIds.add(edge.id);
54
+ nodeIds.add(edge.source);
55
+ walkBackward(edge.source, visited);
56
+ }
57
+ }
58
+ }
59
+ function createFlowLayout(snapshot) {
60
+ const incoming = /* @__PURE__ */ new Map();
61
+ const nextByNode = /* @__PURE__ */ new Map();
62
+ for (const node of snapshot.nodes) {
63
+ incoming.set(node.id, 0);
64
+ nextByNode.set(node.id, []);
65
+ }
66
+ for (const edge of snapshot.edges) {
67
+ incoming.set(edge.target, (incoming.get(edge.target) ?? 0) + 1);
68
+ nextByNode.get(edge.source)?.push(edge.target);
69
+ }
70
+ const roots = snapshot.nodes.filter((node) => (incoming.get(node.id) ?? 0) === 0);
71
+ const queue = [...roots.length ? roots : snapshot.nodes].map((node) => node.id);
72
+ const levelByNode = /* @__PURE__ */ new Map();
73
+ for (const id of queue) levelByNode.set(id, 0);
74
+ while (queue.length) {
75
+ const nodeId = queue.shift();
76
+ const level = levelByNode.get(nodeId) ?? 0;
77
+ for (const next of nextByNode.get(nodeId) ?? []) {
78
+ const nextLevel = Math.max(levelByNode.get(next) ?? 0, level + 1);
79
+ if (nextLevel !== levelByNode.get(next)) {
80
+ levelByNode.set(next, nextLevel);
81
+ queue.push(next);
82
+ }
83
+ }
84
+ }
85
+ for (const node of snapshot.nodes) if (!levelByNode.has(node.id)) levelByNode.set(node.id, 0);
86
+ const levels = /* @__PURE__ */ new Map();
87
+ for (const node of snapshot.nodes) {
88
+ const level = levelByNode.get(node.id) ?? 0;
89
+ const group = levels.get(level) ?? [];
90
+ group.push(node);
91
+ levels.set(level, group);
92
+ }
93
+ return snapshot.nodes.map((node) => {
94
+ const level = levelByNode.get(node.id) ?? 0;
95
+ const index = (levels.get(level) ?? []).findIndex((item) => item.id === node.id);
96
+ return {
97
+ id: node.id,
98
+ x: level * 220,
99
+ y: index * 86
100
+ };
101
+ });
102
+ }
103
+ //#endregion
104
+ //#region lib/client/shared/ui/node-label.tsx
105
+ function NodeLabel(props) {
106
+ return /* @__PURE__ */ jsxs(Box, {
107
+ className: "virentia-inspector__node-label",
108
+ children: [/* @__PURE__ */ jsx("span", {
109
+ className: "virentia-inspector__node-name",
110
+ children: props.node.name
111
+ }), /* @__PURE__ */ jsx("span", {
112
+ className: "virentia-inspector__node-type",
113
+ children: props.node.type
114
+ })]
115
+ });
116
+ }
117
+ //#endregion
118
+ //#region lib/client/shared/ui/flow-node.tsx
119
+ function UnitFlowNode(props) {
120
+ const data = props.data;
121
+ return /* @__PURE__ */ jsxs("div", {
122
+ className: "virentia-inspector__flow-node",
123
+ children: [
124
+ /* @__PURE__ */ jsx(Handle, {
125
+ className: "virentia-inspector__handle",
126
+ id: "target",
127
+ position: Position.Left,
128
+ type: "target"
129
+ }),
130
+ /* @__PURE__ */ jsx(NodeLabel, { node: data.node }),
131
+ /* @__PURE__ */ jsx(Handle, {
132
+ className: "virentia-inspector__handle",
133
+ id: "source",
134
+ position: Position.Right,
135
+ type: "source"
136
+ })
137
+ ]
138
+ });
139
+ }
140
+ //#endregion
141
+ //#region lib/client/shared/ui/timeline-row.tsx
142
+ function TimelineRow(props) {
143
+ const color = props.event.breakpoint ? "red" : props.event.failed ? "red" : props.event.stopped ? "yellow" : "green";
144
+ return /* @__PURE__ */ jsxs(Box, {
145
+ className: "virentia-inspector__timeline-row",
146
+ children: [
147
+ /* @__PURE__ */ jsxs(Group, {
148
+ justify: "space-between",
149
+ gap: "xs",
150
+ wrap: "nowrap",
151
+ children: [/* @__PURE__ */ jsx(Text, {
152
+ size: "sm",
153
+ fw: 650,
154
+ lineClamp: 1,
155
+ children: props.event.nodeName
156
+ }), /* @__PURE__ */ jsx(Badge, {
157
+ color,
158
+ variant: "light",
159
+ children: props.event.breakpoint ? "break" : props.event.nodeType
160
+ })]
161
+ }),
162
+ /* @__PURE__ */ jsxs(Text, {
163
+ size: "xs",
164
+ c: "dimmed",
165
+ children: [
166
+ props.event.scopeName ?? "No scope",
167
+ " · ",
168
+ props.event.duration.toFixed(1),
169
+ " ms"
170
+ ]
171
+ }),
172
+ /* @__PURE__ */ jsxs(Text, {
173
+ className: "virentia-inspector__payload",
174
+ children: ["payload: ", props.event.payload.preview]
175
+ }),
176
+ /* @__PURE__ */ jsxs(Text, {
177
+ className: "virentia-inspector__payload",
178
+ children: ["result: ", props.event.result.preview]
179
+ })
180
+ ]
181
+ });
182
+ }
183
+ //#endregion
184
+ //#region lib/client/modules/inspector/model.ts
185
+ const recordingChanged = event("inspector.recordingChanged");
186
+ const $recording = store(true, void 0, { name: "inspector.recording" });
187
+ reaction({
188
+ on: recordingChanged,
189
+ name: "inspector.applyRecording",
190
+ run(value) {
191
+ $recording.value = value;
192
+ }
193
+ });
194
+ //#endregion
195
+ //#region lib/client/modules/inspector/InspectorApp.tsx
196
+ const nodeTypes = { unit: UnitFlowNode };
197
+ const rightPanelLimits = {
198
+ default: 300,
199
+ min: 210,
200
+ max: 430
201
+ };
202
+ function VirentiaInspector(props) {
203
+ return /* @__PURE__ */ jsx(MantineProvider, {
204
+ defaultColorScheme: "dark",
205
+ forceColorScheme: "dark",
206
+ theme: { primaryColor: "green" },
207
+ children: /* @__PURE__ */ jsx(ScopeProvider, {
208
+ scope: useMemo(() => scope(), []),
209
+ children: /* @__PURE__ */ jsx(ReactFlowProvider, { children: /* @__PURE__ */ jsx(InspectorSurface, { channel: props.channel }) })
210
+ })
211
+ });
212
+ }
213
+ function InspectorSurface(props) {
214
+ const [snapshot, setSnapshot] = useState(emptySnapshot);
215
+ const [timeline, setTimeline] = useState([]);
216
+ const [selectedNodeId, setSelectedNodeId] = useState(null);
217
+ const [breakpointIds, setBreakpointIds] = useState([]);
218
+ const [showIsolatedUnits, setShowIsolatedUnits] = useState(false);
219
+ const [triggerNodeId, setTriggerNodeId] = useState(null);
220
+ const [triggerStage, setTriggerStage] = useState(null);
221
+ const [triggerModalOpened, setTriggerModalOpened] = useState(false);
222
+ const [payloadText, setPayloadText] = useState("");
223
+ const [draftBreakpointIds, setDraftBreakpointIds] = useState([]);
224
+ const [breakpointSelectionActive, setBreakpointSelectionActive] = useState(false);
225
+ const [triggerResult, setTriggerResult] = useState(null);
226
+ const [triggerError, setTriggerError] = useState(null);
227
+ const [contextMenu, setContextMenu] = useState(null);
228
+ const [rightPanelWidth, setRightPanelWidth] = useState(rightPanelLimits.default);
229
+ const recording = useUnit($recording);
230
+ const setRecording = useUnit(recordingChanged);
231
+ const clientRef = useRef(null);
232
+ const recordingRef = useRef(recording);
233
+ const breakpointPickerInitialIdsRef = useRef([]);
234
+ const breakpointPickerReturnStageRef = useRef("payload");
235
+ recordingRef.current = recording;
236
+ useEffect(() => {
237
+ const client = connectVirentiaInspector({ channel: props.channel });
238
+ clientRef.current = client;
239
+ const unsubscribe = client.subscribe((message) => {
240
+ if (message.type === "app") return;
241
+ if (message.type === "graph") {
242
+ setSnapshot(message.snapshot);
243
+ setBreakpointIds(message.snapshot.breakpoints);
244
+ return;
245
+ }
246
+ if (message.type === "timeline" && recordingRef.current) setTimeline((items) => [message.event, ...items].slice(0, 300));
247
+ });
248
+ client.requestGraph();
249
+ return () => {
250
+ unsubscribe();
251
+ client.dispose();
252
+ clientRef.current = null;
253
+ };
254
+ }, [props.channel]);
255
+ const visibleSnapshot = useMemo(() => showIsolatedUnits ? snapshot : hideIsolatedNodes(snapshot), [showIsolatedUnits, snapshot]);
256
+ const selectedFlow = useMemo(() => createReactiveSelection(visibleSnapshot, breakpointSelectionActive ? triggerNodeId : selectedNodeId), [
257
+ breakpointSelectionActive,
258
+ selectedNodeId,
259
+ triggerNodeId,
260
+ visibleSnapshot
261
+ ]);
262
+ const breakpointEligibleNodeIds = useMemo(() => new Set(selectedFlow?.nodeIds ?? []), [selectedFlow]);
263
+ const flow = useFlowGraph(visibleSnapshot, selectedFlow, {
264
+ breakpointIds: useMemo(() => new Set(draftBreakpointIds), [draftBreakpointIds]),
265
+ breakpointSelectionActive,
266
+ eligibleNodeIds: breakpointEligibleNodeIds
267
+ });
268
+ const triggerNode = useMemo(() => snapshot.nodes.find((node) => node.id === triggerNodeId) ?? null, [snapshot.nodes, triggerNodeId]);
269
+ const selectedBreakpointNodes = useMemo(() => draftBreakpointIds.flatMap((id) => {
270
+ const node = snapshot.nodes.find((item) => item.id === id);
271
+ return node ? [node] : [];
272
+ }), [draftBreakpointIds, snapshot.nodes]);
273
+ const breakpointSummary = selectedBreakpointNodes.length ? /* @__PURE__ */ jsx(Group, {
274
+ gap: 4,
275
+ mt: 6,
276
+ children: selectedBreakpointNodes.map((node) => /* @__PURE__ */ jsx(Badge, {
277
+ color: "red",
278
+ size: "xs",
279
+ variant: "light",
280
+ children: node.name
281
+ }, node.id))
282
+ }) : /* @__PURE__ */ jsx(Text, {
283
+ size: "xs",
284
+ c: "dimmed",
285
+ mt: 6,
286
+ children: "None selected"
287
+ });
288
+ const shellStyle = useMemo(() => ({ "--virentia-right-panel-width": `${rightPanelWidth}px` }), [rightPanelWidth]);
289
+ useEffect(() => {
290
+ if (selectedNodeId && !visibleSnapshot.nodes.some((node) => node.id === selectedNodeId)) setSelectedNodeId(null);
291
+ }, [selectedNodeId, visibleSnapshot.nodes]);
292
+ const beginDrawerResize = useCallback((event) => {
293
+ event.preventDefault();
294
+ const startX = event.clientX;
295
+ const startWidth = rightPanelWidth;
296
+ const move = (moveEvent) => {
297
+ const nextWidth = startWidth - (moveEvent.clientX - startX);
298
+ setRightPanelWidth(Math.min(rightPanelLimits.max, Math.max(rightPanelLimits.min, nextWidth)));
299
+ };
300
+ const end = () => {
301
+ window.removeEventListener("pointermove", move);
302
+ window.removeEventListener("pointerup", end);
303
+ };
304
+ window.addEventListener("pointermove", move);
305
+ window.addEventListener("pointerup", end, { once: true });
306
+ }, [rightPanelWidth]);
307
+ const updateBreakpoints = (nodeIds) => {
308
+ setBreakpointIds(nodeIds);
309
+ clientRef.current?.setBreakpoints(nodeIds);
310
+ };
311
+ const openTriggerModal = (nodeId) => {
312
+ setContextMenu(null);
313
+ setTriggerNodeId(nodeId);
314
+ setPayloadText("");
315
+ setDraftBreakpointIds([]);
316
+ setTriggerResult(null);
317
+ setTriggerError(null);
318
+ setTriggerStage("payload");
319
+ setTriggerModalOpened(true);
320
+ };
321
+ const resetTriggerFlow = () => {
322
+ setTriggerNodeId(null);
323
+ setTriggerStage(null);
324
+ setTriggerModalOpened(false);
325
+ setPayloadText("");
326
+ setDraftBreakpointIds([]);
327
+ setBreakpointSelectionActive(false);
328
+ setTriggerResult(null);
329
+ setTriggerError(null);
330
+ };
331
+ const requestCloseTriggerFlow = () => {
332
+ setTriggerModalOpened(false);
333
+ setBreakpointSelectionActive(false);
334
+ };
335
+ const handleTriggerModalExited = () => {
336
+ if (!breakpointSelectionActive && !triggerModalOpened) resetTriggerFlow();
337
+ };
338
+ const parsePayload = () => {
339
+ try {
340
+ const payload = payloadText.trim() ? JSON.parse(payloadText) : void 0;
341
+ setTriggerError(null);
342
+ return {
343
+ ok: true,
344
+ payload
345
+ };
346
+ } catch (error) {
347
+ setTriggerError(error instanceof Error ? error.message : String(error));
348
+ return { ok: false };
349
+ }
350
+ };
351
+ const acceptPayload = () => {
352
+ if (!parsePayload().ok) return;
353
+ setTriggerStage("confirm");
354
+ };
355
+ const startBreakpointSelection = () => {
356
+ if (!triggerNodeId || !parsePayload().ok) return;
357
+ breakpointPickerInitialIdsRef.current = draftBreakpointIds;
358
+ breakpointPickerReturnStageRef.current = triggerStage ?? "payload";
359
+ setTriggerResult(null);
360
+ setTriggerModalOpened(false);
361
+ setBreakpointSelectionActive(true);
362
+ setSelectedNodeId(triggerNodeId);
363
+ };
364
+ const finishBreakpointSelection = (accepted) => {
365
+ if (!accepted) setDraftBreakpointIds(breakpointPickerInitialIdsRef.current);
366
+ setBreakpointSelectionActive(false);
367
+ setTriggerStage(breakpointPickerReturnStageRef.current);
368
+ setTriggerModalOpened(true);
369
+ };
370
+ const triggerSelectedNode = async () => {
371
+ if (!triggerNodeId) return;
372
+ const parsed = parsePayload();
373
+ if (!parsed.ok) return;
374
+ const previousBreakpoints = breakpointIds;
375
+ const scopeId = snapshot.scopes[0]?.id ?? null;
376
+ updateBreakpoints(draftBreakpointIds);
377
+ const result = await clientRef.current?.triggerUnit({
378
+ nodeId: triggerNodeId,
379
+ scopeId,
380
+ payload: parsed.payload
381
+ });
382
+ updateBreakpoints(previousBreakpoints);
383
+ setTriggerResult(result ?? { ok: false });
384
+ setTriggerStage("result");
385
+ };
386
+ const handleNodeClick = (_, node) => {
387
+ setContextMenu(null);
388
+ if (breakpointSelectionActive) {
389
+ if (!breakpointEligibleNodeIds.has(node.id)) return;
390
+ setDraftBreakpointIds((ids) => ids.includes(node.id) ? ids.filter((id) => id !== node.id) : [...ids, node.id]);
391
+ return;
392
+ }
393
+ setSelectedNodeId(node.id);
394
+ };
395
+ const handleNodeContextMenu = (event, node) => {
396
+ event.preventDefault();
397
+ if (breakpointSelectionActive) return;
398
+ setSelectedNodeId(node.id);
399
+ setContextMenu({
400
+ nodeId: node.id,
401
+ x: event.clientX,
402
+ y: event.clientY
403
+ });
404
+ };
405
+ return /* @__PURE__ */ jsxs(Box, {
406
+ className: "virentia-inspector",
407
+ children: [
408
+ /* @__PURE__ */ jsxs(Box, {
409
+ className: "virentia-inspector__shell",
410
+ style: shellStyle,
411
+ children: [/* @__PURE__ */ jsxs(Box, {
412
+ className: "virentia-inspector__main",
413
+ children: [/* @__PURE__ */ jsxs(Box, {
414
+ className: "virentia-inspector__topbar",
415
+ children: [
416
+ /* @__PURE__ */ jsx(Group, {
417
+ gap: "xs",
418
+ wrap: "nowrap",
419
+ children: /* @__PURE__ */ jsx(Title, {
420
+ order: 4,
421
+ children: "Virentia"
422
+ })
423
+ }),
424
+ /* @__PURE__ */ jsxs(Group, {
425
+ gap: "xs",
426
+ children: [
427
+ /* @__PURE__ */ jsx(Switch, {
428
+ checked: showIsolatedUnits,
429
+ color: "green",
430
+ label: "Show isolated",
431
+ onChange: (event) => setShowIsolatedUnits(event.currentTarget.checked),
432
+ size: "xs"
433
+ }),
434
+ /* @__PURE__ */ jsxs(Badge, {
435
+ color: visibleSnapshot.nodes.length ? "green" : "gray",
436
+ variant: "light",
437
+ children: [visibleSnapshot.nodes.length, " units"]
438
+ }),
439
+ /* @__PURE__ */ jsxs(Badge, {
440
+ variant: "light",
441
+ color: "gray",
442
+ children: [visibleSnapshot.edges.length, " links"]
443
+ }),
444
+ breakpointIds.length ? /* @__PURE__ */ jsxs(Badge, {
445
+ variant: "light",
446
+ color: "red",
447
+ children: [breakpointIds.length, " breakpoints"]
448
+ }) : null
449
+ ]
450
+ }),
451
+ /* @__PURE__ */ jsx(Button, {
452
+ color: "green",
453
+ size: "xs",
454
+ variant: "light",
455
+ onClick: () => clientRef.current?.requestGraph(),
456
+ children: "Refresh"
457
+ })
458
+ ]
459
+ }), /* @__PURE__ */ jsx(Box, {
460
+ className: "virentia-inspector__flow",
461
+ children: /* @__PURE__ */ jsxs(ReactFlow, {
462
+ nodes: flow.nodes,
463
+ edges: flow.edges,
464
+ fitView: true,
465
+ nodeTypes,
466
+ minZoom: .15,
467
+ onNodeClick: handleNodeClick,
468
+ onNodeContextMenu: handleNodeContextMenu,
469
+ onPaneClick: () => {
470
+ setContextMenu(null);
471
+ if (!breakpointSelectionActive) setSelectedNodeId(null);
472
+ },
473
+ children: [/* @__PURE__ */ jsx(Background, {
474
+ gap: 18,
475
+ color: "#424242"
476
+ }), /* @__PURE__ */ jsx(Controls, {})]
477
+ })
478
+ })]
479
+ }), /* @__PURE__ */ jsxs(Stack, {
480
+ className: "virentia-inspector__drawer",
481
+ gap: "sm",
482
+ p: "sm",
483
+ children: [
484
+ /* @__PURE__ */ jsx("button", {
485
+ "aria-label": "Resize tools panel",
486
+ className: "virentia-inspector__resize-handle virentia-inspector__resize-handle--right",
487
+ onPointerDown: beginDrawerResize,
488
+ type: "button"
489
+ }),
490
+ /* @__PURE__ */ jsxs(Group, {
491
+ justify: "space-between",
492
+ children: [/* @__PURE__ */ jsx(Text, {
493
+ fw: 650,
494
+ size: "sm",
495
+ children: "Call history"
496
+ }), /* @__PURE__ */ jsxs(Group, {
497
+ gap: "xs",
498
+ children: [/* @__PURE__ */ jsx(Switch, {
499
+ checked: recording,
500
+ color: "green",
501
+ label: "Record",
502
+ onChange: (event) => setRecording(event.currentTarget.checked),
503
+ size: "xs"
504
+ }), /* @__PURE__ */ jsx(Button, {
505
+ color: "gray",
506
+ size: "xs",
507
+ variant: "subtle",
508
+ onClick: () => setTimeline([]),
509
+ children: "Clear"
510
+ })]
511
+ })]
512
+ }),
513
+ /* @__PURE__ */ jsx(ScrollArea, {
514
+ h: "calc(100vh - 66px)",
515
+ type: "auto",
516
+ children: timeline.map((item) => /* @__PURE__ */ jsx(TimelineRow, { event: item }, item.id))
517
+ })
518
+ ]
519
+ })]
520
+ }),
521
+ contextMenu ? /* @__PURE__ */ jsx(Paper, {
522
+ className: "virentia-inspector__context-menu",
523
+ shadow: "md",
524
+ style: {
525
+ left: contextMenu.x,
526
+ top: contextMenu.y
527
+ },
528
+ withBorder: true,
529
+ children: /* @__PURE__ */ jsx("button", {
530
+ className: "virentia-inspector__context-menu-item",
531
+ onClick: () => openTriggerModal(contextMenu.nodeId),
532
+ type: "button",
533
+ children: "Trigger unit"
534
+ })
535
+ }) : null,
536
+ breakpointSelectionActive ? /* @__PURE__ */ jsxs(Box, {
537
+ className: "virentia-inspector__bottom-toolbar",
538
+ children: [/* @__PURE__ */ jsxs(Box, { children: [/* @__PURE__ */ jsx(Text, {
539
+ fw: 650,
540
+ size: "sm",
541
+ children: "Select breakpoints"
542
+ }), /* @__PURE__ */ jsxs(Text, {
543
+ size: "xs",
544
+ c: "dimmed",
545
+ children: [
546
+ "Only units from the ",
547
+ triggerNode?.name ?? "",
548
+ " chain are available"
549
+ ]
550
+ })] }), /* @__PURE__ */ jsxs(Group, {
551
+ gap: "xs",
552
+ children: [/* @__PURE__ */ jsx(Button, {
553
+ color: "gray",
554
+ size: "xs",
555
+ variant: "subtle",
556
+ onClick: () => finishBreakpointSelection(false),
557
+ children: "Cancel"
558
+ }), /* @__PURE__ */ jsx(Button, {
559
+ color: "green",
560
+ size: "xs",
561
+ onClick: () => finishBreakpointSelection(true),
562
+ children: "Apply"
563
+ })]
564
+ })]
565
+ }) : null,
566
+ /* @__PURE__ */ jsxs(Modal, {
567
+ centered: true,
568
+ classNames: {
569
+ body: "virentia-inspector__modal-body",
570
+ content: "virentia-inspector__modal-content",
571
+ header: "virentia-inspector__modal-header",
572
+ title: "virentia-inspector__modal-title"
573
+ },
574
+ opened: triggerModalOpened && triggerStage !== null,
575
+ onClose: requestCloseTriggerFlow,
576
+ onExitTransitionEnd: handleTriggerModalExited,
577
+ padding: "sm",
578
+ radius: "sm",
579
+ size: 420,
580
+ title: triggerNode ? `Trigger: ${triggerNode.name}` : "Trigger unit",
581
+ children: [
582
+ triggerStage === "payload" ? /* @__PURE__ */ jsxs(Stack, {
583
+ gap: "xs",
584
+ children: [
585
+ /* @__PURE__ */ jsx(Textarea, {
586
+ autosize: true,
587
+ minRows: 5,
588
+ label: "Payload JSON",
589
+ size: "xs",
590
+ value: payloadText,
591
+ onChange: (event) => setPayloadText(event.currentTarget.value)
592
+ }),
593
+ triggerError ? /* @__PURE__ */ jsx(Text, {
594
+ size: "xs",
595
+ c: "red",
596
+ children: triggerError
597
+ }) : null,
598
+ /* @__PURE__ */ jsxs(Paper, {
599
+ className: "virentia-inspector__modal-section",
600
+ withBorder: true,
601
+ children: [/* @__PURE__ */ jsxs(Group, {
602
+ align: "flex-start",
603
+ justify: "space-between",
604
+ gap: "xs",
605
+ wrap: "nowrap",
606
+ children: [/* @__PURE__ */ jsxs(Box, { children: [/* @__PURE__ */ jsx(Text, {
607
+ fw: 650,
608
+ size: "xs",
609
+ children: "Breakpoints"
610
+ }), /* @__PURE__ */ jsx(Text, {
611
+ size: "xs",
612
+ c: "dimmed",
613
+ children: "The chain will stop after the selected units"
614
+ })] }), /* @__PURE__ */ jsx(Button, {
615
+ color: "green",
616
+ size: "xs",
617
+ variant: "light",
618
+ onClick: startBreakpointSelection,
619
+ children: "Select"
620
+ })]
621
+ }), breakpointSummary]
622
+ }),
623
+ /* @__PURE__ */ jsxs(Group, {
624
+ justify: "space-between",
625
+ mt: 4,
626
+ children: [/* @__PURE__ */ jsx(Button, {
627
+ color: "gray",
628
+ size: "xs",
629
+ variant: "subtle",
630
+ onClick: requestCloseTriggerFlow,
631
+ children: "Cancel"
632
+ }), /* @__PURE__ */ jsx(Button, {
633
+ color: "green",
634
+ size: "xs",
635
+ onClick: acceptPayload,
636
+ children: "Continue"
637
+ })]
638
+ })
639
+ ]
640
+ }) : null,
641
+ triggerStage === "confirm" ? /* @__PURE__ */ jsxs(Stack, {
642
+ gap: "xs",
643
+ children: [
644
+ /* @__PURE__ */ jsxs(Box, { children: [/* @__PURE__ */ jsx(Text, {
645
+ size: "xs",
646
+ c: "dimmed",
647
+ children: "Payload"
648
+ }), /* @__PURE__ */ jsx(Text, {
649
+ className: "virentia-inspector__payload",
650
+ children: payloadText.trim() || "undefined"
651
+ })] }),
652
+ /* @__PURE__ */ jsxs(Paper, {
653
+ className: "virentia-inspector__modal-section",
654
+ withBorder: true,
655
+ children: [/* @__PURE__ */ jsxs(Group, {
656
+ align: "flex-start",
657
+ justify: "space-between",
658
+ gap: "xs",
659
+ wrap: "nowrap",
660
+ children: [/* @__PURE__ */ jsxs(Box, { children: [/* @__PURE__ */ jsx(Text, {
661
+ fw: 650,
662
+ size: "xs",
663
+ children: "Breakpoints"
664
+ }), /* @__PURE__ */ jsx(Text, {
665
+ size: "xs",
666
+ c: "dimmed",
667
+ children: "You can change them before triggering"
668
+ })] }), /* @__PURE__ */ jsx(Button, {
669
+ color: "green",
670
+ size: "xs",
671
+ variant: "light",
672
+ onClick: startBreakpointSelection,
673
+ children: "Select"
674
+ })]
675
+ }), breakpointSummary]
676
+ }),
677
+ triggerError ? /* @__PURE__ */ jsx(Text, {
678
+ size: "xs",
679
+ c: "red",
680
+ children: triggerError
681
+ }) : null,
682
+ /* @__PURE__ */ jsxs(Group, {
683
+ justify: "space-between",
684
+ mt: 4,
685
+ children: [/* @__PURE__ */ jsx(Button, {
686
+ color: "gray",
687
+ size: "xs",
688
+ variant: "subtle",
689
+ onClick: requestCloseTriggerFlow,
690
+ children: "Cancel"
691
+ }), /* @__PURE__ */ jsx(Button, {
692
+ color: "green",
693
+ size: "xs",
694
+ onClick: () => void triggerSelectedNode(),
695
+ children: "Trigger"
696
+ })]
697
+ })
698
+ ]
699
+ }) : null,
700
+ triggerStage === "result" ? /* @__PURE__ */ jsxs(Stack, {
701
+ gap: "xs",
702
+ children: [/* @__PURE__ */ jsx(Badge, {
703
+ color: triggerResult?.ok ? "green" : "red",
704
+ size: "xs",
705
+ variant: "light",
706
+ children: triggerResult?.ok ? "Triggered successfully" : triggerResult?.error?.preview ?? "Error"
707
+ }), /* @__PURE__ */ jsx(Group, {
708
+ justify: "end",
709
+ children: /* @__PURE__ */ jsx(Button, {
710
+ color: "green",
711
+ size: "xs",
712
+ onClick: requestCloseTriggerFlow,
713
+ children: "Close"
714
+ })
715
+ })]
716
+ }) : null
717
+ ]
718
+ })
719
+ ]
720
+ });
721
+ }
722
+ function useFlowGraph(snapshot, selectedFlow, options) {
723
+ const selectedNodeIds = useMemo(() => new Set(selectedFlow?.nodeIds ?? []), [selectedFlow]);
724
+ const selectedEdgeIds = useMemo(() => new Set(selectedFlow?.edgeIds ?? []), [selectedFlow]);
725
+ return useMemo(() => {
726
+ const layout = new Map(createFlowLayout(snapshot).map((item) => [item.id, item]));
727
+ return {
728
+ nodes: snapshot.nodes.map((node) => {
729
+ const position = layout.get(node.id) ?? {
730
+ x: 0,
731
+ y: 0
732
+ };
733
+ const active = selectedNodeIds.has(node.id);
734
+ const eligible = options.eligibleNodeIds.has(node.id);
735
+ const breakpoint = options.breakpointIds.has(node.id);
736
+ return {
737
+ id: node.id,
738
+ className: [options.breakpointSelectionActive && !eligible ? "is-dimmed" : "", breakpoint ? "is-breakpoint" : ""].filter(Boolean).join(" "),
739
+ position: {
740
+ x: position.x,
741
+ y: position.y
742
+ },
743
+ type: "unit",
744
+ sourcePosition: Position.Right,
745
+ targetPosition: Position.Left,
746
+ selected: active,
747
+ data: { node }
748
+ };
749
+ }),
750
+ edges: snapshot.edges.map((edge) => {
751
+ const active = selectedEdgeIds.has(edge.id);
752
+ const owner = edge.kind === "owner";
753
+ const dimmed = options.breakpointSelectionActive && (!options.eligibleNodeIds.has(edge.source) || !options.eligibleNodeIds.has(edge.target));
754
+ return {
755
+ id: edge.id,
756
+ source: edge.source,
757
+ sourceHandle: "source",
758
+ target: edge.target,
759
+ targetHandle: "target",
760
+ animated: active,
761
+ type: "smoothstep",
762
+ style: {
763
+ opacity: dimmed ? .22 : 1,
764
+ stroke: active ? "var(--virentia-accent)" : owner ? "#424242" : "#696969",
765
+ strokeDasharray: owner ? "6 4" : void 0,
766
+ strokeWidth: active ? 3 : owner ? 1.1 : 1.4
767
+ }
768
+ };
769
+ })
770
+ };
771
+ }, [
772
+ options,
773
+ selectedEdgeIds,
774
+ selectedNodeIds,
775
+ snapshot
776
+ ]);
777
+ }
778
+ function hideIsolatedNodes(snapshot) {
779
+ const connectedNodeIds = /* @__PURE__ */ new Set();
780
+ for (const edge of snapshot.edges) if (edge.kind === "reactive") {
781
+ connectedNodeIds.add(edge.source);
782
+ connectedNodeIds.add(edge.target);
783
+ }
784
+ for (const node of snapshot.nodes) if (node.parentId && connectedNodeIds.has(node.id)) connectedNodeIds.add(node.parentId);
785
+ return {
786
+ ...snapshot,
787
+ breakpoints: snapshot.breakpoints.filter((id) => connectedNodeIds.has(id)),
788
+ edges: snapshot.edges.filter((edge) => connectedNodeIds.has(edge.source) && connectedNodeIds.has(edge.target)),
789
+ nodes: snapshot.nodes.filter((node) => connectedNodeIds.has(node.id))
790
+ };
791
+ }
792
+ //#endregion
793
+ //#region lib/client/index.ts
794
+ function mountVirentiaInspector(container, props = {}) {
795
+ const root = createRoot(container);
796
+ root.render(createElement(VirentiaInspector, props));
797
+ return root;
798
+ }
799
+ //#endregion
800
+ export { VirentiaInspector, mountVirentiaInspector };