@you-agent-factory/factory-visualizers 0.0.0 → 0.0.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.js ADDED
@@ -0,0 +1,1469 @@
1
+ import { jsx, jsxs, Fragment } from "react/jsx-runtime";
2
+ import { Text, Button, Heading, FactoryEmulatorControls as FactoryEmulatorControls$1 } from "@you-agent-factory/components";
3
+ import { Component, useId, useRef, useState, useEffect, useMemo } from "react";
4
+ import { ReactFlow, Background, Controls } from "@xyflow/react";
5
+ import { safeParseFactoryVisualizationLayout, safeParseFactoryRecording } from "@you-agent-factory/client";
6
+ import { GraphNodeShell, GraphNodeButton } from "@you-agent-factory/components/graphs";
7
+ import { canonicalizeFactoryEvents, projectFactoryTopologyAtTick, projectFactoryWorkProgressAtTick, projectFactoryLoadAtTick, projectFactoryActivityAtTick } from "@you-agent-factory/factory-replay";
8
+ const ERROR_MESSAGES = {
9
+ endpoint: "The prepared topology contains invalid edge endpoints.",
10
+ layout: "The topology layout could not be prepared.",
11
+ projection: "The prepared topology projection could not be read.",
12
+ "react-flow": "The topology renderer failed.",
13
+ render: "The visualizer could not render."
14
+ };
15
+ class FactoryVisualizerInternalError extends Error {
16
+ kind;
17
+ originalCause;
18
+ constructor(kind, cause) {
19
+ super(ERROR_MESSAGES[kind]);
20
+ this.name = "FactoryVisualizerInternalError";
21
+ this.kind = kind;
22
+ this.originalCause = cause;
23
+ }
24
+ }
25
+ function toFactoryVisualizerError(kind, cause) {
26
+ const safeCause = sanitizeCause(cause);
27
+ return {
28
+ ...safeCause ? { cause: safeCause } : {},
29
+ kind,
30
+ message: ERROR_MESSAGES[kind],
31
+ recoverable: true
32
+ };
33
+ }
34
+ function normalizeFactoryVisualizerError(error, fallbackKind) {
35
+ if (error instanceof FactoryVisualizerInternalError) {
36
+ return toFactoryVisualizerError(error.kind, error.originalCause);
37
+ }
38
+ return toFactoryVisualizerError(fallbackKind, error);
39
+ }
40
+ function factoryVisualizerErrorKey(error) {
41
+ return [
42
+ error.kind,
43
+ error.message,
44
+ error.recoverable ? "recoverable" : "terminal",
45
+ error.cause?.name ?? "",
46
+ error.cause?.code ?? ""
47
+ ].join(":");
48
+ }
49
+ function sanitizeCause(cause) {
50
+ if (!(cause instanceof Error)) return void 0;
51
+ const code = readSafeCode(cause);
52
+ return {
53
+ ...code ? { code } : {},
54
+ name: safeErrorName(cause.name)
55
+ };
56
+ }
57
+ function readSafeCode(error) {
58
+ const code = error.code;
59
+ return typeof code === "string" && /^[A-Z0-9_-]{1,64}$/.test(code) ? code : void 0;
60
+ }
61
+ function safeErrorName(name) {
62
+ return /^[A-Za-z][A-Za-z0-9]*Error$/.test(name) ? name : "Error";
63
+ }
64
+ class FactoryEmulatorErrorBoundary extends Component {
65
+ state = {};
66
+ static getDerivedStateFromError() {
67
+ return { error: normalizeFactoryVisualizerError(void 0, "render") };
68
+ }
69
+ componentDidCatch(error, _errorInfo) {
70
+ const diagnostic = normalizeFactoryVisualizerError(error, "render");
71
+ this.setState({ error: diagnostic });
72
+ this.props.onError?.(diagnostic);
73
+ }
74
+ render() {
75
+ if (!this.state.error && !this.props.failure) return this.props.children;
76
+ const failure = this.props.failure;
77
+ return /* @__PURE__ */ jsx(
78
+ "section",
79
+ {
80
+ "aria-label": this.props.regionLabel,
81
+ className: "factory-emulator-failure",
82
+ children: /* @__PURE__ */ jsxs("div", { className: "factory-emulator-failure__content", role: "alert", children: [
83
+ /* @__PURE__ */ jsx(Text, { as: "p", children: failure?.message ?? "The Factory emulator could not be shown." }),
84
+ failure?.recoveryAction ? /* @__PURE__ */ jsx(Button, { onClick: failure.recoveryAction.onRecover, type: "button", children: failure.recoveryAction.label }) : null
85
+ ] })
86
+ }
87
+ );
88
+ }
89
+ }
90
+ function isValidAvailableState(state) {
91
+ return state.status === "available" && Number.isSafeInteger(state.earliestTick) && Number.isSafeInteger(state.latestTick) && Number.isSafeInteger(state.selectedTick) && state.earliestTick >= 0 && state.earliestTick <= state.latestTick && state.selectedTick >= state.earliestTick && state.selectedTick <= state.latestTick;
92
+ }
93
+ function FactoryTimelineScrubber({
94
+ className,
95
+ disabled = false,
96
+ formatTick,
97
+ messages,
98
+ onFollowLatest,
99
+ onSelectTick,
100
+ state,
101
+ ...sectionProps
102
+ }) {
103
+ const descriptionId = useId();
104
+ const classNames = ["factory-timeline-scrubber", className].filter(Boolean).join(" ");
105
+ const availableState = isValidAvailableState(state) ? state : null;
106
+ const isCurrent = availableState?.mode === "current";
107
+ const interactionDisabled = disabled || availableState === null;
108
+ const status = availableState ? disabled ? messages.disabled : availableState.mode === "history" ? messages.historyMode : messages.currentMode : messages.unavailable;
109
+ const actionDescription = isCurrent ? messages.alreadyFollowingLatest : status;
110
+ function handleSelection(event) {
111
+ if (!availableState || disabled) {
112
+ return;
113
+ }
114
+ const nextTick = event.currentTarget.valueAsNumber;
115
+ if (Number.isSafeInteger(nextTick) && nextTick >= availableState.earliestTick && nextTick <= availableState.latestTick) {
116
+ onSelectTick(nextTick);
117
+ }
118
+ }
119
+ return /* @__PURE__ */ jsxs(
120
+ "section",
121
+ {
122
+ "aria-label": messages.regionLabel,
123
+ className: classNames,
124
+ "data-timeline-mode": availableState?.mode ?? "unavailable",
125
+ ...sectionProps,
126
+ children: [
127
+ /* @__PURE__ */ jsxs("header", { className: "factory-timeline-scrubber__header", children: [
128
+ /* @__PURE__ */ jsx(Heading, { as: "h2", children: messages.title }),
129
+ availableState ? /* @__PURE__ */ jsx(Text, { as: "p", className: "factory-timeline-scrubber__position", children: messages.position(
130
+ formatTick(availableState.selectedTick),
131
+ formatTick(availableState.latestTick)
132
+ ) }) : null
133
+ ] }),
134
+ /* @__PURE__ */ jsxs("div", { className: "factory-timeline-scrubber__controls", children: [
135
+ /* @__PURE__ */ jsx(
136
+ "input",
137
+ {
138
+ "aria-describedby": descriptionId,
139
+ "aria-label": messages.sliderLabel,
140
+ "aria-valuetext": availableState ? formatTick(availableState.selectedTick) : void 0,
141
+ className: "factory-timeline-scrubber__range",
142
+ disabled: interactionDisabled,
143
+ max: availableState?.latestTick ?? 0,
144
+ min: availableState?.earliestTick ?? 0,
145
+ onChange: handleSelection,
146
+ step: 1,
147
+ type: "range",
148
+ value: availableState?.selectedTick ?? 0
149
+ }
150
+ ),
151
+ /* @__PURE__ */ jsx(
152
+ Button,
153
+ {
154
+ "aria-describedby": descriptionId,
155
+ disabled: interactionDisabled || isCurrent,
156
+ onClick: onFollowLatest,
157
+ tone: "outline",
158
+ children: messages.followLatest
159
+ }
160
+ )
161
+ ] }),
162
+ /* @__PURE__ */ jsx(
163
+ Text,
164
+ {
165
+ as: "p",
166
+ className: "factory-timeline-scrubber__status",
167
+ id: !isCurrent || disabled ? descriptionId : void 0,
168
+ role: "status",
169
+ children: status
170
+ }
171
+ ),
172
+ isCurrent && !disabled ? /* @__PURE__ */ jsx(
173
+ Text,
174
+ {
175
+ as: "p",
176
+ className: "factory-timeline-scrubber__action-status",
177
+ id: descriptionId,
178
+ children: actionDescription
179
+ }
180
+ ) : null
181
+ ]
182
+ }
183
+ );
184
+ }
185
+ function FactoryEmulatorControls({
186
+ className,
187
+ failure,
188
+ formatTick,
189
+ onFollowLatest,
190
+ onError,
191
+ onPause,
192
+ onPlay,
193
+ onRestart,
194
+ onSelectTick,
195
+ onStep,
196
+ showPlaybackControls = true,
197
+ showTimelineScrubber = true,
198
+ timeline,
199
+ ...playbackProps
200
+ }) {
201
+ const isViewingHistory = timeline.state.status === "available" && timeline.state.mode === "history";
202
+ function returnToLatestBefore(action) {
203
+ if (isViewingHistory) onFollowLatest();
204
+ action();
205
+ }
206
+ function selectTick(tick) {
207
+ if (timeline.state.status === "available" && tick < timeline.state.latestTick)
208
+ onPause();
209
+ onSelectTick(tick);
210
+ }
211
+ return /* @__PURE__ */ jsx(
212
+ FactoryEmulatorErrorBoundary,
213
+ {
214
+ failure,
215
+ onError,
216
+ regionLabel: "Factory emulator controls",
217
+ children: /* @__PURE__ */ jsxs(
218
+ "section",
219
+ {
220
+ "aria-label": "Factory emulator controls",
221
+ className: ["factory-emulator-controls", className].filter(Boolean).join(" "),
222
+ children: [
223
+ showPlaybackControls || playbackProps.showRuntimeStatus !== false || playbackProps.showSpeedControl !== false ? /* @__PURE__ */ jsx(
224
+ FactoryEmulatorControls$1,
225
+ {
226
+ ...playbackProps,
227
+ onPause,
228
+ onPlay: () => returnToLatestBefore(onPlay),
229
+ onRestart,
230
+ showPlaybackActions: showPlaybackControls,
231
+ onStep: () => returnToLatestBefore(onStep)
232
+ }
233
+ ) : null,
234
+ showTimelineScrubber ? /* @__PURE__ */ jsx(
235
+ FactoryTimelineScrubber,
236
+ {
237
+ disabled: timeline.disabled,
238
+ formatTick,
239
+ messages: timeline.messages,
240
+ onFollowLatest,
241
+ onSelectTick: selectTick,
242
+ state: timeline.state
243
+ }
244
+ ) : null
245
+ ]
246
+ }
247
+ )
248
+ }
249
+ );
250
+ }
251
+ const VISIBLE_ACTIVE_WORK_ROWS = 3;
252
+ function projectFactoryTopologyActiveWork(activity) {
253
+ const durationByWorkId = /* @__PURE__ */ new Map();
254
+ for (const overlay of activity.activeDispatchOverlays) {
255
+ const durationTicks = Math.max(
256
+ 0,
257
+ activity.selectedTick - overlay.startedTick
258
+ );
259
+ for (const workId of overlay.workIds ?? []) {
260
+ const previousDuration = durationByWorkId.get(workId);
261
+ if (previousDuration === void 0 || durationTicks > previousDuration)
262
+ durationByWorkId.set(workId, durationTicks);
263
+ }
264
+ }
265
+ const rows = [...durationByWorkId].sort(([left], [right]) => left.localeCompare(right)).map(([id, durationTicks]) => ({ durationTicks, id }));
266
+ return {
267
+ overflowCount: Math.max(0, rows.length - VISIBLE_ACTIVE_WORK_ROWS),
268
+ rows: rows.slice(0, VISIBLE_ACTIVE_WORK_ROWS)
269
+ };
270
+ }
271
+ const DEFAULT_FACTORY_TOPOLOGY_CHROME_PRESET = "full";
272
+ const FACTORY_TOPOLOGY_CHROME_PRESETS = {
273
+ full: {
274
+ background: true,
275
+ legend: true,
276
+ viewportControls: true,
277
+ visibilityControls: true
278
+ },
279
+ minimal: {
280
+ background: true,
281
+ legend: false,
282
+ viewportControls: true,
283
+ visibilityControls: false
284
+ },
285
+ none: {
286
+ background: false,
287
+ legend: false,
288
+ viewportControls: false,
289
+ visibilityControls: false
290
+ }
291
+ };
292
+ function resolveFactoryTopologyChrome(configuration = {}) {
293
+ const preset = configuration.preset ?? DEFAULT_FACTORY_TOPOLOGY_CHROME_PRESET;
294
+ const resolvedPreset = FACTORY_TOPOLOGY_CHROME_PRESETS[preset];
295
+ return {
296
+ background: configuration.background ?? resolvedPreset.background,
297
+ legend: configuration.legend ?? resolvedPreset.legend,
298
+ viewportControls: configuration.viewportControls ?? resolvedPreset.viewportControls,
299
+ visibilityControls: configuration.visibilityControls ?? resolvedPreset.visibilityControls
300
+ };
301
+ }
302
+ const columnByKind = {
303
+ resource: 0,
304
+ worker: 1,
305
+ "work-type": 2,
306
+ "work-state": 3,
307
+ workstation: 4
308
+ };
309
+ const DEFAULT_ANNOTATION_WIDTH = 240;
310
+ function projectFactoryTopologyFlow(projection, messages, selectedNodeId, onSelectNode, prefersReducedMotion = false, layout) {
311
+ try {
312
+ const { connections, nodes: topologyNodes } = projection.topology;
313
+ const nodeById = new Map(topologyNodes.map((node) => [node.id, node]));
314
+ const validEndpoints = connections.every(
315
+ (connection) => connectionHasRenderedEndpoints(connection, nodeById)
316
+ );
317
+ const nodeData = nodePresentationData(projection, connections, layout);
318
+ return {
319
+ edges: validEndpoints ? projectEdges(connections, projection.activity, prefersReducedMotion) : [],
320
+ nodes: [
321
+ ...projectTopologyNodes(
322
+ topologyNodes,
323
+ messages,
324
+ selectedNodeId,
325
+ onSelectNode,
326
+ nodeData
327
+ ),
328
+ ...projectAnnotations(layout, messages)
329
+ ],
330
+ validEndpoints
331
+ };
332
+ } catch (error) {
333
+ if (error instanceof FactoryVisualizerInternalError) throw error;
334
+ throw new FactoryVisualizerInternalError("projection", error);
335
+ }
336
+ }
337
+ function nodePresentationData(projection, connections, layout) {
338
+ return {
339
+ activeDetailNodeIds: activityDetailNodeIds(
340
+ projection.activity,
341
+ connections,
342
+ projection.load.resourceOccupancy,
343
+ projection.load.workStateCounts
344
+ ),
345
+ activityCountByNode: activityCounts(projection.activity),
346
+ emptyStateByNode: new Map(
347
+ (layout?.nodeEmptyStates ?? []).map((state) => [
348
+ state.nodeId,
349
+ state.content
350
+ ])
351
+ ),
352
+ occupancyByNode: new Map(
353
+ projection.load.resourceOccupancy.map((occupancy) => [
354
+ occupancy.resourceNodeId,
355
+ occupancy
356
+ ])
357
+ ),
358
+ workStateCountByNode: new Map(
359
+ projection.load.workStateCounts.map((count) => [
360
+ count.workStateNodeId,
361
+ count
362
+ ])
363
+ )
364
+ };
365
+ }
366
+ function projectTopologyNodes(topologyNodes, messages, selectedNodeId, onSelectNode, data) {
367
+ const rowByKind = /* @__PURE__ */ new Map();
368
+ return topologyNodes.map((node) => {
369
+ const row = rowByKind.get(node.kind) ?? 0;
370
+ rowByKind.set(node.kind, row + 1);
371
+ const occupancy = data.occupancyByNode.get(node.id);
372
+ const workStateCount = data.workStateCountByNode.get(node.id);
373
+ return {
374
+ data: {
375
+ activityCount: data.activityCountByNode.get(node.id) ?? 0,
376
+ ...data.activeDetailNodeIds.has(node.id) ? {} : { emptyState: data.emptyStateByNode.get(node.id) },
377
+ messages,
378
+ node,
379
+ ...occupancy ? {
380
+ occupancy: {
381
+ capacity: occupancy.capacity,
382
+ evidence: occupancy.evidence,
383
+ occupied: occupancy.occupiedQuantity
384
+ }
385
+ } : {},
386
+ onSelectNode,
387
+ selected: selectedNodeId === node.id,
388
+ ...workStateCount ? {
389
+ workStateCount: {
390
+ count: workStateCount.count,
391
+ evidence: workStateCount.evidence
392
+ }
393
+ } : {}
394
+ },
395
+ draggable: false,
396
+ id: node.id,
397
+ position: layoutNode(node.kind, row),
398
+ selectable: false,
399
+ type: "factoryTopologyNode"
400
+ };
401
+ });
402
+ }
403
+ function projectAnnotations(layout, messages) {
404
+ return (layout?.annotations ?? []).map((annotation) => ({
405
+ data: { annotation, messages },
406
+ draggable: false,
407
+ id: `annotation:${annotation.id}`,
408
+ position: annotation.position,
409
+ selectable: false,
410
+ type: "factoryTopologyAnnotation",
411
+ style: {
412
+ ...annotation.size ? { height: annotation.size.height } : {},
413
+ width: annotation.size?.width ?? DEFAULT_ANNOTATION_WIDTH
414
+ }
415
+ }));
416
+ }
417
+ function projectEdges(connections, activity, prefersReducedMotion) {
418
+ return connections.map((connection) => ({
419
+ animated: !prefersReducedMotion && activity.activeDispatchOverlays.some(
420
+ (overlay) => overlay.connectionIds.includes(connection.id)
421
+ ),
422
+ data: { relationship: connection.kind },
423
+ id: connection.id,
424
+ source: connection.source.nodeId,
425
+ sourceHandle: connection.source.handleId,
426
+ target: connection.target.nodeId,
427
+ targetHandle: connection.target.handleId
428
+ }));
429
+ }
430
+ function connectionHasRenderedEndpoints(connection, nodeById) {
431
+ const source = nodeById.get(connection.source.nodeId);
432
+ const target = nodeById.get(connection.target.nodeId);
433
+ return Boolean(
434
+ source?.handles.some(
435
+ (handle) => handle.id === connection.source.handleId && handle.role === "source"
436
+ ) && target?.handles.some(
437
+ (handle) => handle.id === connection.target.handleId && handle.role === "target"
438
+ )
439
+ );
440
+ }
441
+ function activityCounts(activity) {
442
+ const counts = /* @__PURE__ */ new Map();
443
+ for (const overlay of activity.activeDispatchOverlays) {
444
+ for (const nodeId of /* @__PURE__ */ new Set([
445
+ overlay.workerNodeId,
446
+ overlay.workstationNodeId,
447
+ ...overlay.resourceNodeIds ?? []
448
+ ])) {
449
+ if (nodeId) counts.set(nodeId, (counts.get(nodeId) ?? 0) + 1);
450
+ }
451
+ }
452
+ return counts;
453
+ }
454
+ function activityDetailNodeIds(activity, connections, resourceOccupancy, workStateCounts) {
455
+ const nodeIds = new Set(activity.activeWorkstationNodeIds);
456
+ const connectionById = new Map(
457
+ connections.map((connection) => [connection.id, connection])
458
+ );
459
+ for (const occupancy of resourceOccupancy)
460
+ if (occupancy.evidence === "known") nodeIds.add(occupancy.resourceNodeId);
461
+ for (const state of workStateCounts)
462
+ if (state.evidence === "known" && typeof state.count === "number" && state.count > 0)
463
+ nodeIds.add(state.workStateNodeId);
464
+ for (const overlay of activity.activeDispatchOverlays) {
465
+ for (const nodeId of [
466
+ overlay.workerNodeId,
467
+ overlay.workstationNodeId,
468
+ ...overlay.resourceNodeIds ?? []
469
+ ])
470
+ if (nodeId) nodeIds.add(nodeId);
471
+ for (const connectionId of overlay.connectionIds) {
472
+ const connection = connectionById.get(connectionId);
473
+ if (connection) {
474
+ nodeIds.add(connection.source.nodeId);
475
+ nodeIds.add(connection.target.nodeId);
476
+ }
477
+ }
478
+ }
479
+ return nodeIds;
480
+ }
481
+ function layoutNode(kind, row) {
482
+ const column = columnByKind[kind];
483
+ if (column === void 0 || !Number.isSafeInteger(row) || row < 0)
484
+ throw new FactoryVisualizerInternalError("layout");
485
+ return { x: column * 260, y: row * 170 };
486
+ }
487
+ const nodeTypes = {
488
+ factoryTopologyAnnotation: FactoryTopologyAnnotationView,
489
+ factoryTopologyNode: FactoryTopologyNodeView
490
+ };
491
+ function FactoryTopologyAnnotationView({
492
+ data
493
+ }) {
494
+ const { annotation, messages } = data;
495
+ if (annotation.kind === "image") {
496
+ return /* @__PURE__ */ jsx(
497
+ FactoryTopologyAnnotationImage,
498
+ {
499
+ annotation,
500
+ messages
501
+ }
502
+ );
503
+ }
504
+ return /* @__PURE__ */ jsxs(
505
+ "aside",
506
+ {
507
+ className: "factory-topology-replay__annotation",
508
+ "data-tone": annotation.tone ?? "neutral",
509
+ children: [
510
+ annotation.title ? /* @__PURE__ */ jsx("strong", { className: "factory-topology-replay__annotation-title", children: annotation.title }) : null,
511
+ /* @__PURE__ */ jsx("span", { className: "factory-topology-replay__annotation-body", children: annotation.body })
512
+ ]
513
+ }
514
+ );
515
+ }
516
+ function FactoryTopologyAnnotationImage({
517
+ annotation,
518
+ messages
519
+ }) {
520
+ const image = useEmbeddedImageUrl(annotation.source);
521
+ return /* @__PURE__ */ jsx("figure", { className: "factory-topology-replay__annotation factory-topology-replay__annotation--image", children: image.status === "ready" ? /* @__PURE__ */ jsx(
522
+ "img",
523
+ {
524
+ alt: annotation.altText,
525
+ className: "factory-topology-replay__annotation-image",
526
+ onError: image.fail,
527
+ src: image.url
528
+ }
529
+ ) : /* @__PURE__ */ jsxs(
530
+ "div",
531
+ {
532
+ className: "factory-topology-replay__annotation-image-state",
533
+ role: image.status === "failed" ? "alert" : "status",
534
+ children: [
535
+ /* @__PURE__ */ jsx("span", { className: "sr-only", children: annotation.altText }),
536
+ image.status === "failed" ? messages.imageFailed : messages.imageLoading
537
+ ]
538
+ }
539
+ ) });
540
+ }
541
+ function FactoryTopologyNodeView({ data }) {
542
+ const {
543
+ activityCount,
544
+ emptyState,
545
+ messages,
546
+ node,
547
+ occupancy,
548
+ onSelectNode,
549
+ selected,
550
+ workStateCount
551
+ } = data;
552
+ const state = selected ? "selected" : "default";
553
+ const handles = node.handles.map((handle) => ({
554
+ connectable: false,
555
+ id: handle.id,
556
+ label: handle.id,
557
+ side: handle.role === "target" ? "left" : "right",
558
+ type: handle.role
559
+ }));
560
+ const content = /* @__PURE__ */ jsxs(
561
+ GraphNodeShell,
562
+ {
563
+ className: activityCount > 0 ? "factory-topology-replay__node--active" : "",
564
+ "data-dispatch-activity": activityCount > 0 ? "active" : "inactive",
565
+ handles,
566
+ nodeKind: node.kind,
567
+ showStateIndicator: false,
568
+ state,
569
+ children: [
570
+ /* @__PURE__ */ jsx("strong", { className: "factory-topology-replay__node-title", children: node.label }),
571
+ /* @__PURE__ */ jsx("span", { className: "factory-topology-replay__node-kind", children: node.kind }),
572
+ /* @__PURE__ */ jsx("div", { className: "factory-topology-replay__node-activity-detail", children: emptyState ? /* @__PURE__ */ jsx(
573
+ FactoryTopologyNodeEmptyStateView,
574
+ {
575
+ content: emptyState,
576
+ messages
577
+ }
578
+ ) : /* @__PURE__ */ jsxs("span", { className: "factory-topology-replay__node-cue", children: [
579
+ activityCount > 0 ? "●" : "○",
580
+ " ",
581
+ activityCount > 0 ? messages.activeDispatches(activityCount) : messages.inactiveDispatches
582
+ ] }) }),
583
+ node.kind === "resource" ? /* @__PURE__ */ jsxs("span", { className: "factory-topology-replay__node-cue", children: [
584
+ "◫",
585
+ " ",
586
+ occupancy?.evidence === "known" && occupancy.occupied !== void 0 && occupancy.capacity !== void 0 ? messages.resourceOccupancy(occupancy.occupied, occupancy.capacity) : messages.resourceOccupancyUnavailable
587
+ ] }) : null,
588
+ node.kind === "work-state" ? /* @__PURE__ */ jsxs("span", { className: "factory-topology-replay__node-cue", children: [
589
+ "∑",
590
+ " ",
591
+ workStateCount?.evidence === "known" && workStateCount.count !== void 0 ? messages.workStateCount(workStateCount.count) : messages.workStateCountUnavailable
592
+ ] }) : null,
593
+ selected ? /* @__PURE__ */ jsxs("span", { className: "factory-topology-replay__node-cue", children: [
594
+ "✓ ",
595
+ messages.selectedNode
596
+ ] }) : null
597
+ ]
598
+ }
599
+ );
600
+ return onSelectNode ? /* @__PURE__ */ jsx(
601
+ GraphNodeButton,
602
+ {
603
+ "aria-label": messages.nodeLabel(node.kind, node.label),
604
+ className: "factory-topology-replay__node-button",
605
+ graphState: state,
606
+ onClick: () => onSelectNode(node),
607
+ children: content
608
+ }
609
+ ) : /* @__PURE__ */ jsx(
610
+ "figure",
611
+ {
612
+ "aria-label": messages.nodeLabel(node.kind, node.label),
613
+ className: "factory-topology-replay__node-static",
614
+ children: content
615
+ }
616
+ );
617
+ }
618
+ function FactoryTopologyNodeEmptyStateView({
619
+ content,
620
+ messages
621
+ }) {
622
+ return content.kind === "image" ? /* @__PURE__ */ jsx(FactoryTopologyEmptyStateImage, { content, messages }) : /* @__PURE__ */ jsx("span", { className: "factory-topology-replay__node-empty-state", children: content.text });
623
+ }
624
+ function FactoryTopologyEmptyStateImage({
625
+ content,
626
+ messages
627
+ }) {
628
+ const image = useEmbeddedImageUrl(content.source);
629
+ return image.status === "ready" ? /* @__PURE__ */ jsx(
630
+ "img",
631
+ {
632
+ alt: content.altText,
633
+ className: "factory-topology-replay__node-empty-state-image",
634
+ onError: image.fail,
635
+ src: image.url
636
+ }
637
+ ) : /* @__PURE__ */ jsxs(
638
+ "span",
639
+ {
640
+ className: "factory-topology-replay__node-empty-state",
641
+ role: image.status === "failed" ? "alert" : "status",
642
+ children: [
643
+ /* @__PURE__ */ jsx("span", { className: "sr-only", children: content.altText }),
644
+ image.status === "failed" ? messages.imageFailed : messages.imageLoading
645
+ ]
646
+ }
647
+ );
648
+ }
649
+ function useEmbeddedImageUrl(source) {
650
+ const urlRef = useRef(void 0);
651
+ const [state, setState] = useState({ status: "loading" });
652
+ useEffect(() => {
653
+ try {
654
+ const url = URL.createObjectURL(
655
+ new Blob([decodeEmbeddedImage(source.base64)], {
656
+ type: source.mediaType
657
+ })
658
+ );
659
+ urlRef.current = url;
660
+ setState({ status: "ready", url });
661
+ return () => {
662
+ if (urlRef.current === url) {
663
+ URL.revokeObjectURL(url);
664
+ urlRef.current = void 0;
665
+ }
666
+ };
667
+ } catch {
668
+ setState({ status: "failed" });
669
+ return void 0;
670
+ }
671
+ }, [source]);
672
+ return {
673
+ fail: () => {
674
+ if (urlRef.current) {
675
+ URL.revokeObjectURL(urlRef.current);
676
+ urlRef.current = void 0;
677
+ }
678
+ setState({ status: "failed" });
679
+ },
680
+ ...state
681
+ };
682
+ }
683
+ function decodeEmbeddedImage(base64) {
684
+ const decoded = atob(base64);
685
+ const bytes = new Uint8Array(decoded.length);
686
+ for (let index = 0; index < decoded.length; index += 1)
687
+ bytes[index] = decoded.charCodeAt(index);
688
+ return bytes.buffer;
689
+ }
690
+ function FactoryTopologyStateRegion({
691
+ messages,
692
+ onRetry,
693
+ state
694
+ }) {
695
+ return /* @__PURE__ */ jsx(
696
+ "section",
697
+ {
698
+ "aria-busy": state === "loading" ? "true" : void 0,
699
+ "aria-label": messages.regionLabel,
700
+ className: "factory-topology-replay factory-topology-replay--state",
701
+ children: /* @__PURE__ */ jsx(
702
+ FactoryTopologyStatePresentation,
703
+ {
704
+ messages,
705
+ onRetry,
706
+ state
707
+ }
708
+ )
709
+ }
710
+ );
711
+ }
712
+ function FactoryTopologyStatePresentation({
713
+ messages,
714
+ onRetry,
715
+ state
716
+ }) {
717
+ return /* @__PURE__ */ jsxs(
718
+ "div",
719
+ {
720
+ className: "factory-topology-replay__state",
721
+ role: state === "failed" ? "alert" : "status",
722
+ children: [
723
+ /* @__PURE__ */ jsx(Text, { as: "p", children: messages[state] }),
724
+ state === "failed" && onRetry ? /* @__PURE__ */ jsx(Button, { onClick: onRetry, type: "button", children: messages.retry }) : null
725
+ ]
726
+ }
727
+ );
728
+ }
729
+ class FactoryTopologyErrorBoundary extends Component {
730
+ state = {};
731
+ reportedErrors = /* @__PURE__ */ new Set();
732
+ static getDerivedStateFromError() {
733
+ return { error: toFactoryVisualizerError("render") };
734
+ }
735
+ componentDidCatch(error, _errorInfo) {
736
+ const diagnostic = normalizeFactoryVisualizerError(
737
+ error,
738
+ this.props.errorKind
739
+ );
740
+ this.setState({ error: diagnostic });
741
+ this.report(diagnostic);
742
+ }
743
+ componentDidUpdate(previousProps) {
744
+ if (this.state.error && resetKeysChanged(previousProps.resetKeys, this.props.resetKeys)) {
745
+ this.setState({ error: void 0 });
746
+ }
747
+ }
748
+ render() {
749
+ if (!this.state.error) return this.props.children;
750
+ const presentation = /* @__PURE__ */ jsx(
751
+ FactoryTopologyStatePresentation,
752
+ {
753
+ messages: this.props.messages,
754
+ onRetry: this.props.onRetry,
755
+ state: "failed"
756
+ }
757
+ );
758
+ return this.props.withinRegion ? presentation : /* @__PURE__ */ jsx(
759
+ "section",
760
+ {
761
+ "aria-label": this.props.messages.regionLabel,
762
+ className: "factory-topology-replay factory-topology-replay--state",
763
+ children: presentation
764
+ }
765
+ );
766
+ }
767
+ report(error) {
768
+ const key = factoryVisualizerErrorKey(error);
769
+ if (this.reportedErrors.has(key)) return;
770
+ this.reportedErrors.add(key);
771
+ this.props.onError?.(error);
772
+ }
773
+ }
774
+ function useDistinctTopologyErrorReport(error, onError) {
775
+ const reportedErrors = useRef(/* @__PURE__ */ new Set());
776
+ useEffect(() => {
777
+ if (!error) return;
778
+ const key = error.kind === "layout-validation" ? error.issues.map(
779
+ (issue) => `${issue.category}:${issue.code}:${issue.path.join(".")}`
780
+ ).join("|") : factoryVisualizerErrorKey(error);
781
+ if (reportedErrors.current.has(key)) return;
782
+ reportedErrors.current.add(key);
783
+ onError?.(error);
784
+ }, [error, onError]);
785
+ }
786
+ function resetKeysChanged(previous, current) {
787
+ return previous.length !== current.length || previous.some((value, index) => value !== current[index]);
788
+ }
789
+ function FactoryTopologyReplay(props) {
790
+ return /* @__PURE__ */ jsx(
791
+ FactoryTopologyErrorBoundary,
792
+ {
793
+ errorKind: "render",
794
+ messages: props.messages,
795
+ onError: props.onError,
796
+ onRetry: props.onRetry,
797
+ resetKeys: [props.state, props.messages],
798
+ children: /* @__PURE__ */ jsx(FactoryTopologyReplayContent, { ...props })
799
+ }
800
+ );
801
+ }
802
+ function FactoryTopologyReplayContent({
803
+ chrome,
804
+ messages,
805
+ onError,
806
+ layout,
807
+ onRetry,
808
+ onSelectNode,
809
+ selectedNodeId,
810
+ state
811
+ }) {
812
+ if (state.status !== "ready") {
813
+ return /* @__PURE__ */ jsx(
814
+ FactoryTopologyStateRegion,
815
+ {
816
+ messages,
817
+ state: state.status,
818
+ onRetry
819
+ }
820
+ );
821
+ }
822
+ return /* @__PURE__ */ jsx(
823
+ PreparedTopology,
824
+ {
825
+ chrome,
826
+ messages,
827
+ onError,
828
+ onRetry,
829
+ onSelectNode,
830
+ layout,
831
+ projection: state.projection,
832
+ selectedNodeId
833
+ }
834
+ );
835
+ }
836
+ function PreparedTopology({
837
+ chrome,
838
+ messages,
839
+ onError,
840
+ layout,
841
+ onRetry,
842
+ onSelectNode,
843
+ projection,
844
+ selectedNodeId
845
+ }) {
846
+ const prefersReducedMotion = usePrefersReducedMotion();
847
+ const resolvedChrome = resolveFactoryTopologyChrome(chrome);
848
+ const prepared = useMemo(() => {
849
+ try {
850
+ const parsedLayout = layout === void 0 ? void 0 : safeParseFactoryVisualizationLayout(layout, {
851
+ canonicalNodeIds: new Set(
852
+ projection.topology.nodes.map((node) => node.id)
853
+ )
854
+ });
855
+ if (parsedLayout && !parsedLayout.success) {
856
+ return {
857
+ error: {
858
+ issues: parsedLayout.issues.map(({ category, code, path }) => ({
859
+ category,
860
+ code,
861
+ path
862
+ })),
863
+ kind: "layout-validation",
864
+ message: "The topology layout could not be prepared.",
865
+ recoverable: true
866
+ },
867
+ status: "failed"
868
+ };
869
+ }
870
+ const flow = projectFactoryTopologyFlow(
871
+ projection,
872
+ messages,
873
+ selectedNodeId,
874
+ onSelectNode,
875
+ prefersReducedMotion,
876
+ parsedLayout?.data
877
+ );
878
+ return flow.validEndpoints ? { flow, status: "ready" } : { error: toFactoryVisualizerError("endpoint"), status: "failed" };
879
+ } catch (error) {
880
+ return {
881
+ error: normalizeFactoryVisualizerError(error, "projection"),
882
+ status: "failed"
883
+ };
884
+ }
885
+ }, [
886
+ messages,
887
+ onSelectNode,
888
+ prefersReducedMotion,
889
+ projection,
890
+ layout,
891
+ selectedNodeId
892
+ ]);
893
+ useDistinctTopologyErrorReport(
894
+ prepared.status === "failed" ? prepared.error : void 0,
895
+ onError
896
+ );
897
+ if (prepared.status === "failed") {
898
+ return /* @__PURE__ */ jsx(
899
+ FactoryTopologyStateRegion,
900
+ {
901
+ messages,
902
+ state: "failed",
903
+ onRetry
904
+ }
905
+ );
906
+ }
907
+ return /* @__PURE__ */ jsx(
908
+ "section",
909
+ {
910
+ "aria-label": messages.regionLabel,
911
+ className: "factory-topology-replay",
912
+ "data-endpoints-valid": "true",
913
+ "data-reduced-motion": prefersReducedMotion ? "true" : "false",
914
+ children: /* @__PURE__ */ jsx(
915
+ FactoryTopologyErrorBoundary,
916
+ {
917
+ errorKind: "react-flow",
918
+ messages,
919
+ onError,
920
+ onRetry,
921
+ resetKeys: [projection, messages, selectedNodeId, onSelectNode, layout],
922
+ withinRegion: true,
923
+ children: /* @__PURE__ */ jsx(
924
+ ReactFlowCanvas,
925
+ {
926
+ activity: projection.activity,
927
+ chrome: resolvedChrome,
928
+ flow: prepared.flow,
929
+ messages
930
+ }
931
+ )
932
+ }
933
+ )
934
+ }
935
+ );
936
+ }
937
+ function ReactFlowCanvas({
938
+ activity,
939
+ chrome,
940
+ flow,
941
+ messages
942
+ }) {
943
+ const chromeMessages = resolveTopologyChromeMessages(messages);
944
+ const [annotationsVisible, setAnnotationsVisible] = useState(true);
945
+ const visibleNodes = annotationsVisible ? flow.nodes : flow.nodes.filter((node) => node.type !== "factoryTopologyAnnotation");
946
+ const hasAnnotations = flow.nodes.some(
947
+ (node) => node.type === "factoryTopologyAnnotation"
948
+ );
949
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
950
+ /* @__PURE__ */ jsx(ActiveWorkSummary, { activity, messages }),
951
+ chrome.legend ? /* @__PURE__ */ jsx(TopologyLegend, { messages: chromeMessages }) : null,
952
+ chrome.visibilityControls && hasAnnotations ? /* @__PURE__ */ jsx(
953
+ "button",
954
+ {
955
+ "aria-pressed": annotationsVisible,
956
+ className: "factory-topology-replay__annotation-toggle",
957
+ onClick: () => setAnnotationsVisible((visible) => !visible),
958
+ type: "button",
959
+ children: annotationsVisible ? messages.annotationsVisible : messages.annotationsHidden
960
+ }
961
+ ) : null,
962
+ /* @__PURE__ */ jsxs(
963
+ ReactFlow,
964
+ {
965
+ edges: flow.edges,
966
+ edgesFocusable: false,
967
+ elementsSelectable: false,
968
+ fitView: true,
969
+ fitViewOptions: { includeHiddenNodes: false },
970
+ nodes: visibleNodes,
971
+ nodesConnectable: false,
972
+ nodesDraggable: false,
973
+ nodeTypes,
974
+ onNodeClick: preserveNestedNodePointerEvents,
975
+ panOnDrag: true,
976
+ proOptions: { hideAttribution: true },
977
+ children: [
978
+ chrome.background ? /* @__PURE__ */ jsx(Background, {}) : null,
979
+ chrome.viewportControls ? /* @__PURE__ */ jsx(
980
+ Controls,
981
+ {
982
+ "aria-label": chromeMessages.viewportControlsLabel,
983
+ showInteractive: false
984
+ }
985
+ ) : null
986
+ ]
987
+ },
988
+ annotationsVisible ? "annotations-visible" : "annotations-hidden"
989
+ )
990
+ ] });
991
+ }
992
+ function ActiveWorkSummary({
993
+ activity,
994
+ messages
995
+ }) {
996
+ const activeWork = projectFactoryTopologyActiveWork(activity);
997
+ if (activeWork.rows.length === 0) return null;
998
+ return /* @__PURE__ */ jsxs("fieldset", { className: "factory-topology-replay__active-work", children: [
999
+ /* @__PURE__ */ jsx("legend", { children: messages.activeWorkRegionLabel ?? "Active Work" }),
1000
+ /* @__PURE__ */ jsx("ul", { children: activeWork.rows.map((work) => /* @__PURE__ */ jsxs("li", { children: [
1001
+ /* @__PURE__ */ jsx("span", { children: work.id }),
1002
+ /* @__PURE__ */ jsx("span", { children: messages.activeWorkDuration?.(work.durationTicks) ?? `Active for ${work.durationTicks} ticks` })
1003
+ ] }, work.id)) }),
1004
+ activeWork.overflowCount > 0 ? /* @__PURE__ */ jsx("p", { children: messages.activeWorkOverflow?.(activeWork.overflowCount) ?? `${activeWork.overflowCount} more active Work` }) : null
1005
+ ] });
1006
+ }
1007
+ function TopologyLegend({ messages }) {
1008
+ return /* @__PURE__ */ jsxs("fieldset", { className: "factory-topology-replay__legend", children: [
1009
+ /* @__PURE__ */ jsx("legend", { children: messages.legendLabel }),
1010
+ /* @__PURE__ */ jsxs("ul", { children: [
1011
+ /* @__PURE__ */ jsxs("li", { children: [
1012
+ /* @__PURE__ */ jsx(
1013
+ "span",
1014
+ {
1015
+ "aria-hidden": "true",
1016
+ className: "factory-topology-replay__legend-swatch factory-topology-replay__legend-swatch--active"
1017
+ }
1018
+ ),
1019
+ messages.legendActiveRoute
1020
+ ] }),
1021
+ /* @__PURE__ */ jsxs("li", { children: [
1022
+ /* @__PURE__ */ jsx(
1023
+ "span",
1024
+ {
1025
+ "aria-hidden": "true",
1026
+ className: "factory-topology-replay__legend-swatch"
1027
+ }
1028
+ ),
1029
+ messages.legendInactiveRoute
1030
+ ] })
1031
+ ] })
1032
+ ] });
1033
+ }
1034
+ function resolveTopologyChromeMessages(messages) {
1035
+ return {
1036
+ legendActiveRoute: messages.legendActiveRoute ?? "Active route",
1037
+ legendInactiveRoute: messages.legendInactiveRoute ?? "Inactive route",
1038
+ legendLabel: messages.legendLabel ?? "Topology legend",
1039
+ viewportControlsLabel: messages.viewportControlsLabel ?? "Topology viewport controls"
1040
+ };
1041
+ }
1042
+ function preserveNestedNodePointerEvents() {
1043
+ }
1044
+ const REDUCED_MOTION_QUERY = "(prefers-reduced-motion: reduce)";
1045
+ function usePrefersReducedMotion() {
1046
+ const [prefersReducedMotion, setPrefersReducedMotion] = useState(
1047
+ () => reducedMotionMediaQuery()?.matches ?? false
1048
+ );
1049
+ useEffect(() => {
1050
+ const mediaQuery = reducedMotionMediaQuery();
1051
+ if (!mediaQuery) return;
1052
+ const updatePreference = () => setPrefersReducedMotion(mediaQuery.matches);
1053
+ updatePreference();
1054
+ mediaQuery.addEventListener("change", updatePreference);
1055
+ return () => mediaQuery.removeEventListener("change", updatePreference);
1056
+ }, []);
1057
+ return prefersReducedMotion;
1058
+ }
1059
+ function reducedMotionMediaQuery() {
1060
+ return typeof window === "undefined" || typeof window.matchMedia !== "function" ? void 0 : window.matchMedia(REDUCED_MOTION_QUERY);
1061
+ }
1062
+ const CATEGORY_PRESENTATION = {
1063
+ queued: { cue: "○" },
1064
+ active: { cue: "▶" },
1065
+ completed: { cue: "✓" },
1066
+ failed: { cue: "!" },
1067
+ unclassified: { cue: "?" }
1068
+ };
1069
+ const CATEGORY_ORDER = [
1070
+ "queued",
1071
+ "active",
1072
+ "completed",
1073
+ "failed",
1074
+ "unclassified"
1075
+ ];
1076
+ function categoryMessage(count, formattedCount, message) {
1077
+ return count === 1 ? message.singular(formattedCount) : message.plural(formattedCount);
1078
+ }
1079
+ function WorkProgressVisualizer({
1080
+ className,
1081
+ formatNumber,
1082
+ messages,
1083
+ projection,
1084
+ ...sectionProps
1085
+ }) {
1086
+ const classNames = ["factory-work-progress", className].filter(Boolean).join(" ");
1087
+ return /* @__PURE__ */ jsxs(
1088
+ "section",
1089
+ {
1090
+ "aria-label": messages.regionLabel,
1091
+ className: classNames,
1092
+ "data-work-progress-total": projection.total,
1093
+ ...sectionProps,
1094
+ children: [
1095
+ /* @__PURE__ */ jsxs("header", { className: "factory-work-progress__header", children: [
1096
+ /* @__PURE__ */ jsx(Heading, { as: "h2", children: messages.title }),
1097
+ /* @__PURE__ */ jsx(Text, { as: "p", className: "factory-work-progress__total", children: messages.total(formatNumber(projection.total)) })
1098
+ ] }),
1099
+ projection.total === 0 ? /* @__PURE__ */ jsx(Text, { as: "p", className: "factory-work-progress__empty", role: "status", children: messages.empty }) : /* @__PURE__ */ jsx("ul", { className: "factory-work-progress__categories", children: CATEGORY_ORDER.map((category) => {
1100
+ const count = projection.counts[category];
1101
+ const formattedCount = formatNumber(count);
1102
+ return /* @__PURE__ */ jsxs(
1103
+ "li",
1104
+ {
1105
+ className: "factory-work-progress__category",
1106
+ "data-work-progress-category": category,
1107
+ children: [
1108
+ /* @__PURE__ */ jsx("span", { "aria-hidden": "true", className: "factory-work-progress__cue", children: CATEGORY_PRESENTATION[category].cue }),
1109
+ /* @__PURE__ */ jsx("span", { className: "factory-work-progress__message", children: categoryMessage(
1110
+ count,
1111
+ formattedCount,
1112
+ messages.categories[category]
1113
+ ) })
1114
+ ]
1115
+ },
1116
+ category
1117
+ );
1118
+ }) })
1119
+ ]
1120
+ }
1121
+ );
1122
+ }
1123
+ const PRESET_VISIBILITY = {
1124
+ full: {
1125
+ playbackControls: true,
1126
+ runtimeStatus: true,
1127
+ speedControl: true,
1128
+ submission: true,
1129
+ timelineScrubber: true,
1130
+ workProgress: true
1131
+ },
1132
+ compact: {
1133
+ playbackControls: true,
1134
+ runtimeStatus: true,
1135
+ speedControl: false,
1136
+ submission: false,
1137
+ timelineScrubber: true,
1138
+ workProgress: true
1139
+ },
1140
+ "display-only": {
1141
+ playbackControls: false,
1142
+ runtimeStatus: false,
1143
+ speedControl: false,
1144
+ submission: false,
1145
+ timelineScrubber: false,
1146
+ workProgress: false
1147
+ }
1148
+ };
1149
+ function FactoryEmulatorView({
1150
+ className,
1151
+ controls,
1152
+ failure,
1153
+ onError,
1154
+ preset = "full",
1155
+ submission,
1156
+ topology,
1157
+ visibility,
1158
+ workProgress,
1159
+ ...sectionProps
1160
+ }) {
1161
+ const regions = { ...PRESET_VISIBILITY[preset], ...visibility };
1162
+ const showControls = regions.playbackControls || regions.speedControl || regions.runtimeStatus;
1163
+ return /* @__PURE__ */ jsx(
1164
+ FactoryEmulatorErrorBoundary,
1165
+ {
1166
+ failure,
1167
+ onError,
1168
+ regionLabel: "Factory emulator view",
1169
+ children: /* @__PURE__ */ jsxs(
1170
+ "section",
1171
+ {
1172
+ "aria-label": "Factory emulator view",
1173
+ className: ["factory-emulator-view", className].filter(Boolean).join(" "),
1174
+ "data-preset": preset,
1175
+ ...sectionProps,
1176
+ children: [
1177
+ showControls ? /* @__PURE__ */ jsx(
1178
+ FactoryEmulatorControls,
1179
+ {
1180
+ ...controls,
1181
+ onError: combineErrorReports(
1182
+ controls.onError,
1183
+ onError
1184
+ ),
1185
+ showPlaybackControls: regions.playbackControls,
1186
+ showRuntimeStatus: regions.runtimeStatus,
1187
+ showSpeedControl: regions.speedControl,
1188
+ showTimelineScrubber: regions.timelineScrubber
1189
+ }
1190
+ ) : regions.timelineScrubber ? /* @__PURE__ */ jsx(
1191
+ FactoryEmulatorControls,
1192
+ {
1193
+ ...controls,
1194
+ onError: combineErrorReports(
1195
+ controls.onError,
1196
+ onError
1197
+ ),
1198
+ showPlaybackControls: false,
1199
+ showRuntimeStatus: false,
1200
+ showSpeedControl: false,
1201
+ showTimelineScrubber: true
1202
+ }
1203
+ ) : null,
1204
+ /* @__PURE__ */ jsx(
1205
+ FactoryTopologyReplay,
1206
+ {
1207
+ ...topology,
1208
+ onError: combineErrorReports(
1209
+ topology.onError,
1210
+ onError
1211
+ )
1212
+ }
1213
+ ),
1214
+ regions.workProgress ? /* @__PURE__ */ jsx(WorkProgressVisualizer, { ...workProgress }) : null,
1215
+ regions.submission && submission ? /* @__PURE__ */ jsx(
1216
+ "section",
1217
+ {
1218
+ "aria-label": "Factory emulator submission",
1219
+ className: "factory-emulator-view__submission",
1220
+ children: submission
1221
+ }
1222
+ ) : null
1223
+ ]
1224
+ }
1225
+ )
1226
+ }
1227
+ );
1228
+ }
1229
+ function combineErrorReports(primary, secondary) {
1230
+ if (!secondary || primary === secondary) return primary;
1231
+ if (!primary) return secondary;
1232
+ return (error) => {
1233
+ primary(error);
1234
+ secondary(error);
1235
+ };
1236
+ }
1237
+ const MAX_CACHED_RECORDING_PROJECTIONS = 32;
1238
+ function createRecordingProjectionCache() {
1239
+ return { projections: /* @__PURE__ */ new Map() };
1240
+ }
1241
+ function FactoryRecordingTopologyReplay({
1242
+ defaultSelectedTick,
1243
+ formatNumber,
1244
+ layout,
1245
+ messages,
1246
+ onError,
1247
+ onSelectNode,
1248
+ recording,
1249
+ selectedNodeId,
1250
+ state
1251
+ }) {
1252
+ const status = state?.status ?? "ready";
1253
+ const recordingInput = state?.status === "ready" ? state.recording : recording;
1254
+ const parsed = useMemo(
1255
+ () => status === "ready" ? safeParseFactoryRecording(recordingInput) : void 0,
1256
+ [recordingInput, status]
1257
+ );
1258
+ const validationError = useMemo(
1259
+ () => !parsed || parsed.success ? void 0 : toRecordingValidationDiagnostic(
1260
+ parsed.issues,
1261
+ messages.validationFailed
1262
+ ),
1263
+ [messages.validationFailed, parsed]
1264
+ );
1265
+ useDistinctRecordingErrorReport(validationError, onError);
1266
+ useDistinctVisualizerErrorReport(
1267
+ state?.status === "failed" ? state.error : void 0,
1268
+ onError
1269
+ );
1270
+ if (status === "loading" || status === "failed") {
1271
+ return /* @__PURE__ */ jsx(FactoryTopologyReplay, { messages: messages.topology, state: { status } });
1272
+ }
1273
+ if (!parsed?.success) {
1274
+ return /* @__PURE__ */ jsx(
1275
+ FactoryTopologyReplay,
1276
+ {
1277
+ messages: messages.topology,
1278
+ onError,
1279
+ state: { status: "failed" }
1280
+ }
1281
+ );
1282
+ }
1283
+ return /* @__PURE__ */ jsx(
1284
+ ValidatedRecordingReplay,
1285
+ {
1286
+ defaultSelectedTick,
1287
+ formatNumber,
1288
+ layout,
1289
+ messages,
1290
+ onError,
1291
+ onSelectNode,
1292
+ recording: parsed.data,
1293
+ selectedNodeId
1294
+ },
1295
+ parsed.data.id
1296
+ );
1297
+ }
1298
+ function ValidatedRecordingReplay({
1299
+ defaultSelectedTick,
1300
+ formatNumber,
1301
+ layout,
1302
+ messages,
1303
+ onError,
1304
+ onSelectNode,
1305
+ recording,
1306
+ selectedNodeId
1307
+ }) {
1308
+ const events = useMemo(
1309
+ () => canonicalizeFactoryEvents(recording.events),
1310
+ [recording.events]
1311
+ );
1312
+ const ticks = useMemo(() => recordedTicks(events), [events]);
1313
+ const latestTick = ticks.at(-1) ?? 0;
1314
+ const defaultTick = defaultSelectedTick !== void 0 && ticks.includes(defaultSelectedTick) ? defaultSelectedTick : latestTick;
1315
+ const [mode, setMode] = useState(
1316
+ () => defaultSelectedTick !== void 0 && ticks.includes(defaultSelectedTick) ? "history" : "current"
1317
+ );
1318
+ const [fixedTick, setFixedTick] = useState(defaultTick);
1319
+ const effectiveMode = mode === "history" && ticks.includes(fixedTick) ? "history" : "current";
1320
+ const selectedTick = effectiveMode === "current" ? latestTick : fixedTick;
1321
+ const projectionCache = useRef(
1322
+ createRecordingProjectionCache()
1323
+ );
1324
+ const prepared = useMemo(
1325
+ () => projectRecordingAtTick(events, selectedTick, projectionCache.current),
1326
+ [events, selectedTick]
1327
+ );
1328
+ function selectTick(requestedTick) {
1329
+ const resolvedTick = resolveRecordedTick(
1330
+ ticks,
1331
+ selectedTick,
1332
+ requestedTick
1333
+ );
1334
+ setFixedTick(resolvedTick);
1335
+ setMode("history");
1336
+ }
1337
+ function followLatest() {
1338
+ setMode("current");
1339
+ }
1340
+ return /* @__PURE__ */ jsxs(
1341
+ "section",
1342
+ {
1343
+ "aria-label": messages.regionLabel,
1344
+ className: "factory-recording-topology-replay",
1345
+ "data-selected-tick": selectedTick,
1346
+ children: [
1347
+ /* @__PURE__ */ jsx(Text, { as: "p", className: "factory-recording-topology-replay__tick", children: messages.selectedTick(formatNumber(selectedTick)) }),
1348
+ /* @__PURE__ */ jsx(
1349
+ FactoryTimelineScrubber,
1350
+ {
1351
+ formatTick: formatNumber,
1352
+ messages: messages.timeline,
1353
+ onFollowLatest: followLatest,
1354
+ onSelectTick: selectTick,
1355
+ state: {
1356
+ earliestTick: ticks[0] ?? 0,
1357
+ latestTick,
1358
+ mode: effectiveMode,
1359
+ selectedTick,
1360
+ status: "available"
1361
+ }
1362
+ }
1363
+ ),
1364
+ /* @__PURE__ */ jsx(
1365
+ FactoryTopologyReplay,
1366
+ {
1367
+ layout,
1368
+ messages: messages.topology,
1369
+ onError,
1370
+ onSelectNode,
1371
+ selectedNodeId,
1372
+ state: {
1373
+ ...prepared.topology.nodes.length === 0 ? { status: "empty" } : {
1374
+ projection: {
1375
+ activity: prepared.activity,
1376
+ load: prepared.load,
1377
+ topology: prepared.topology
1378
+ },
1379
+ status: "ready"
1380
+ }
1381
+ }
1382
+ }
1383
+ ),
1384
+ /* @__PURE__ */ jsx(
1385
+ WorkProgressVisualizer,
1386
+ {
1387
+ formatNumber,
1388
+ messages: messages.progress,
1389
+ projection: prepared.progress
1390
+ }
1391
+ )
1392
+ ]
1393
+ }
1394
+ );
1395
+ }
1396
+ function recordedTicks(events) {
1397
+ const ticks = [...new Set(events.map((event) => event.context.tick))];
1398
+ return ticks.length > 0 ? ticks : [0];
1399
+ }
1400
+ function resolveRecordedTick(ticks, selectedTick, requestedTick) {
1401
+ if (ticks.includes(requestedTick)) return requestedTick;
1402
+ if (requestedTick > selectedTick) {
1403
+ return ticks.find((tick) => tick >= requestedTick) ?? ticks.at(-1) ?? 0;
1404
+ }
1405
+ return [...ticks].reverse().find((tick) => tick <= requestedTick) ?? ticks[0] ?? 0;
1406
+ }
1407
+ function projectRecordingAtTick(events, tick, cache) {
1408
+ if (cache.events !== events) {
1409
+ cache.events = events;
1410
+ cache.projections.clear();
1411
+ }
1412
+ const cached = cache.projections.get(tick);
1413
+ if (cached) return cached;
1414
+ const projection = {
1415
+ activity: projectFactoryActivityAtTick({ events, tick }),
1416
+ load: projectFactoryLoadAtTick({ events, tick }),
1417
+ progress: projectFactoryWorkProgressAtTick({ events, tick }),
1418
+ topology: projectFactoryTopologyAtTick({ events, tick })
1419
+ };
1420
+ cache.projections.set(tick, projection);
1421
+ if (cache.projections.size > MAX_CACHED_RECORDING_PROJECTIONS) {
1422
+ const oldestTick = cache.projections.keys().next().value;
1423
+ if (oldestTick !== void 0) cache.projections.delete(oldestTick);
1424
+ }
1425
+ return projection;
1426
+ }
1427
+ function toRecordingValidationDiagnostic(issues, message) {
1428
+ return {
1429
+ issues: issues.map(({ category, code, path }) => ({
1430
+ category,
1431
+ code,
1432
+ path
1433
+ })),
1434
+ kind: "recording-validation",
1435
+ message,
1436
+ recoverable: false
1437
+ };
1438
+ }
1439
+ function useDistinctRecordingErrorReport(error, onError) {
1440
+ const reported = useRef(/* @__PURE__ */ new Set());
1441
+ useEffect(() => {
1442
+ if (!error) return;
1443
+ const key = error.issues.map((issue) => `${issue.category}:${issue.code}:${issue.path.join(".")}`).join("|");
1444
+ if (reported.current.has(key)) return;
1445
+ reported.current.add(key);
1446
+ onError?.(error);
1447
+ }, [error, onError]);
1448
+ }
1449
+ function useDistinctVisualizerErrorReport(error, onError) {
1450
+ const reported = useRef(/* @__PURE__ */ new Set());
1451
+ useEffect(() => {
1452
+ if (!error) return;
1453
+ const key = factoryVisualizerErrorKey(error);
1454
+ if (reported.current.has(key)) return;
1455
+ reported.current.add(key);
1456
+ onError?.(error);
1457
+ }, [error, onError]);
1458
+ }
1459
+ export {
1460
+ DEFAULT_FACTORY_TOPOLOGY_CHROME_PRESET,
1461
+ FactoryEmulatorControls,
1462
+ FactoryEmulatorView,
1463
+ FactoryRecordingTopologyReplay,
1464
+ FactoryTimelineScrubber,
1465
+ FactoryTopologyReplay,
1466
+ WorkProgressVisualizer,
1467
+ projectFactoryTopologyFlow,
1468
+ resolveFactoryTopologyChrome
1469
+ };