yaml-flow 3.1.1 → 5.0.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 +81 -20
- package/board-live-cards-cli.js +37 -0
- package/browser/board-livegraph-runtime.js +1453 -0
- package/browser/board-livegraph-runtime.js.map +1 -0
- package/browser/card-compute.js +153 -433
- package/browser/live-cards.js +868 -115
- package/browser/live-cards.schema.json +90 -83
- package/dist/board-livegraph-runtime/index.cjs +1448 -0
- package/dist/board-livegraph-runtime/index.cjs.map +1 -0
- package/dist/board-livegraph-runtime/index.d.cts +101 -0
- package/dist/board-livegraph-runtime/index.d.ts +101 -0
- package/dist/board-livegraph-runtime/index.js +1441 -0
- package/dist/board-livegraph-runtime/index.js.map +1 -0
- package/dist/card-compute/index.cjs +266 -431
- package/dist/card-compute/index.cjs.map +1 -1
- package/dist/card-compute/index.d.cts +77 -49
- package/dist/card-compute/index.d.ts +77 -49
- package/dist/card-compute/index.js +263 -432
- package/dist/card-compute/index.js.map +1 -1
- package/dist/cli/board-live-cards-cli.cjs +2750 -0
- package/dist/cli/board-live-cards-cli.cjs.map +1 -0
- package/dist/cli/board-live-cards-cli.d.cts +205 -0
- package/dist/cli/board-live-cards-cli.d.ts +205 -0
- package/dist/cli/board-live-cards-cli.js +2702 -0
- package/dist/cli/board-live-cards-cli.js.map +1 -0
- package/dist/{constants-B2zqu10b.d.ts → constants-DuzE5n03.d.ts} +2 -2
- package/dist/{constants-DJZU1pwJ.d.cts → constants-ozjf1Ejw.d.cts} +2 -2
- package/dist/continuous-event-graph/index.cjs +258 -464
- package/dist/continuous-event-graph/index.cjs.map +1 -1
- package/dist/continuous-event-graph/index.d.cts +18 -358
- package/dist/continuous-event-graph/index.d.ts +18 -358
- package/dist/continuous-event-graph/index.js +255 -464
- package/dist/continuous-event-graph/index.js.map +1 -1
- package/dist/event-graph/index.cjs +4 -4
- package/dist/event-graph/index.cjs.map +1 -1
- package/dist/event-graph/index.d.cts +5 -5
- package/dist/event-graph/index.d.ts +5 -5
- package/dist/event-graph/index.js +4 -4
- package/dist/event-graph/index.js.map +1 -1
- package/dist/index.cjs +1684 -555
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +26 -7
- package/dist/index.d.ts +26 -7
- package/dist/index.js +1678 -555
- package/dist/index.js.map +1 -1
- package/dist/inference/index.cjs +138 -19
- package/dist/inference/index.cjs.map +1 -1
- package/dist/inference/index.d.cts +2 -2
- package/dist/inference/index.d.ts +2 -2
- package/dist/inference/index.js +138 -19
- package/dist/inference/index.js.map +1 -1
- package/dist/journal-DRfJiheM.d.cts +28 -0
- package/dist/journal-NLYuqege.d.ts +28 -0
- package/dist/live-cards-bridge-Or7fdEJV.d.ts +316 -0
- package/dist/live-cards-bridge-vGJ6tMzN.d.cts +316 -0
- package/dist/schedule-CMcZe5Ny.d.ts +21 -0
- package/dist/schedule-CiucyCan.d.cts +21 -0
- package/dist/step-machine/index.cjs +18 -1
- package/dist/step-machine/index.cjs.map +1 -1
- package/dist/step-machine/index.d.cts +2 -2
- package/dist/step-machine/index.d.ts +2 -2
- package/dist/step-machine/index.js +18 -1
- package/dist/step-machine/index.js.map +1 -1
- package/dist/stores/file.d.cts +1 -1
- package/dist/stores/file.d.ts +1 -1
- package/dist/stores/index.d.cts +1 -1
- package/dist/stores/index.d.ts +1 -1
- package/dist/stores/localStorage.d.cts +1 -1
- package/dist/stores/localStorage.d.ts +1 -1
- package/dist/stores/memory.d.cts +1 -1
- package/dist/stores/memory.d.ts +1 -1
- package/dist/{types-BwvgvlOO.d.cts → types-BzLD8bjb.d.cts} +1 -1
- package/dist/{types-ClRA8hzC.d.ts → types-C2eJ7DAV.d.ts} +1 -1
- package/dist/{types-DEj7OakX.d.cts → types-CMFSIjpc.d.cts} +39 -4
- package/dist/{types-DEj7OakX.d.ts → types-CMFSIjpc.d.ts} +39 -4
- package/dist/{types-FZ_eyErS.d.cts → types-ycun84cq.d.cts} +1 -0
- package/dist/{types-FZ_eyErS.d.ts → types-ycun84cq.d.ts} +1 -0
- package/dist/{validate-DEZ2Ymdb.d.ts → validate-DJQTQ6bP.d.ts} +1 -1
- package/dist/{validate-DqKTZg_o.d.cts → validate-ke92Cleg.d.cts} +1 -1
- package/examples/browser/boards/portfolio-tracker/cards/holdings-table.json +22 -0
- package/examples/browser/boards/portfolio-tracker/cards/portfolio-form.json +16 -0
- package/examples/browser/boards/portfolio-tracker/cards/portfolio-value.json +15 -0
- package/examples/browser/boards/portfolio-tracker/cards/price-fetch.json +15 -0
- package/examples/browser/boards/portfolio-tracker/fetch-prices.js +43 -0
- package/examples/browser/boards/portfolio-tracker/portfolio-tracker-task-executor.cjs +96 -0
- package/examples/browser/boards/portfolio-tracker/portfolio-tracker.bat +7 -0
- package/examples/browser/boards/portfolio-tracker/portfolio-tracker.js +217 -0
- package/examples/browser/livecards-browser/index.html +41 -0
- package/examples/browser/{index.html → step-machine-browser/index.html} +53 -53
- package/examples/cli/step-machine-cli/portfolio-tracker/cards/holdings-table.json +22 -0
- package/examples/cli/step-machine-cli/portfolio-tracker/cards/portfolio-form.json +43 -0
- package/examples/cli/step-machine-cli/portfolio-tracker/cards/portfolio-value.json +15 -0
- package/examples/cli/step-machine-cli/portfolio-tracker/cards/price-fetch.json +15 -0
- package/examples/cli/step-machine-cli/portfolio-tracker/fetch-prices.js +48 -0
- package/examples/cli/step-machine-cli/portfolio-tracker/handlers/_board-cli.js +58 -0
- package/examples/cli/step-machine-cli/portfolio-tracker/handlers/add-cards-cli.js +27 -0
- package/examples/cli/step-machine-cli/portfolio-tracker/handlers/init-board-cli.js +25 -0
- package/examples/cli/step-machine-cli/portfolio-tracker/handlers/reset-board-dir-cli.js +29 -0
- package/examples/cli/step-machine-cli/portfolio-tracker/handlers/retrigger-cli.js +27 -0
- package/examples/cli/step-machine-cli/portfolio-tracker/handlers/status-cli.js +25 -0
- package/examples/cli/step-machine-cli/portfolio-tracker/handlers/update-holdings-cli.js +37 -0
- package/examples/cli/step-machine-cli/portfolio-tracker/handlers/wait-completed-cli.js +53 -0
- package/examples/cli/step-machine-cli/portfolio-tracker/handlers/write-prices-cli.js +35 -0
- package/examples/cli/step-machine-cli/portfolio-tracker/portfolio-tracker.flow.yaml +227 -0
- package/examples/cli/step-machine-cli/portfolio-tracker/portfolio-tracker.input.json +38 -0
- package/examples/cli/step-machine-cli/portfolio-tracker/run-portfolio-tracker.bat +29 -0
- package/examples/cli/step-machine-demo/jsonata-init-board-cli.js +36 -0
- package/examples/cli/step-machine-demo/jsonata-init-board.flow.yaml +30 -0
- package/examples/cli/step-machine-demo/one-step-cli-only.flow.yaml +19 -0
- package/examples/cli/step-machine-demo/step-cli-echo-y.js +15 -0
- package/examples/cli/step-machine-demo/step2-double-cli.js +39 -0
- package/examples/cli/step-machine-demo/two-step-math-handlers.js +32 -0
- package/examples/cli/step-machine-demo/two-step-math.flow.yaml +31 -0
- package/examples/cli/step-machine-demo/two-step-mixed-handlers.js +24 -0
- package/examples/cli/step-machine-demo/two-step-mixed.flow.yaml +35 -0
- package/examples/example-board/board.yaml +23 -0
- package/examples/example-board/bootstrap_payload.json +1 -0
- package/examples/example-board/cards/card-chain-region-alert.json +39 -0
- package/examples/example-board/cards/card-chain-region-totals.json +26 -0
- package/examples/example-board/cards/card-chain-top-region.json +24 -0
- package/examples/example-board/cards/card-ex-actions.json +32 -0
- package/examples/example-board/cards/card-ex-chart.json +30 -0
- package/examples/example-board/cards/card-ex-filter.json +36 -0
- package/examples/example-board/cards/card-ex-filtered-by-preference.json +59 -0
- package/examples/example-board/cards/card-ex-form.json +91 -0
- package/examples/example-board/cards/card-ex-list.json +22 -0
- package/examples/example-board/cards/card-ex-markdown.json +17 -0
- package/examples/example-board/cards/card-ex-metric.json +19 -0
- package/examples/example-board/cards/card-ex-narrative.json +36 -0
- package/examples/example-board/cards/card-ex-source-http.json +28 -0
- package/examples/example-board/cards/card-ex-source.json +21 -0
- package/examples/example-board/cards/card-ex-status.json +35 -0
- package/examples/example-board/cards/card-ex-table.json +30 -0
- package/examples/example-board/cards/card-ex-todo.json +29 -0
- package/examples/example-board/demo-chat-handler.js +69 -0
- package/examples/example-board/demo-server.js +87 -0
- package/examples/example-board/demo-shell-browser.html +806 -0
- package/examples/example-board/demo-shell-with-server.html +280 -0
- package/examples/example-board/demo-shell.html +62 -0
- package/examples/example-board/demo-task-executor.js +255 -0
- package/examples/example-board/mock.db +15 -0
- package/examples/example-board/reusable-board-runtime-client.js +265 -0
- package/examples/example-board/reusable-runtime-artifacts-adapter.js +233 -0
- package/examples/example-board/reusable-server-runtime.js +1284 -0
- package/examples/index.html +799 -0
- package/examples/{batch → npm-libs/batch}/batch-step-machine.ts +1 -1
- package/examples/{continuous-event-graph → npm-libs/continuous-event-graph}/live-cards-board.ts +18 -18
- package/examples/{continuous-event-graph → npm-libs/continuous-event-graph}/live-portfolio-dashboard.ts +24 -24
- package/examples/{continuous-event-graph → npm-libs/continuous-event-graph}/portfolio-tracker.ts +1 -1
- package/examples/{continuous-event-graph → npm-libs/continuous-event-graph}/reactive-monitoring.ts +1 -1
- package/examples/{continuous-event-graph → npm-libs/continuous-event-graph}/reactive-pipeline.ts +1 -1
- package/examples/{continuous-event-graph → npm-libs/continuous-event-graph}/soc-incident-board.ts +1 -1
- package/examples/{continuous-event-graph → npm-libs/continuous-event-graph}/stock-dashboard.ts +1 -1
- package/examples/{event-graph → npm-libs/event-graph}/ci-cd-pipeline.ts +1 -1
- package/examples/{event-graph → npm-libs/event-graph}/executor-diamond.ts +1 -1
- package/examples/{event-graph → npm-libs/event-graph}/executor-pipeline.ts +1 -1
- package/examples/{event-graph → npm-libs/event-graph}/research-pipeline.ts +1 -1
- package/examples/{graph-of-graphs → npm-libs/graph-of-graphs}/multi-stage-etl.ts +1 -1
- package/examples/{graph-of-graphs → npm-libs/graph-of-graphs}/url-processing-pipeline.ts +1 -1
- package/examples/{inference → npm-libs/inference}/azure-deployment.ts +1 -1
- package/examples/{inference → npm-libs/inference}/copilot-cli.ts +1 -1
- package/examples/{inference → npm-libs/inference}/data-pipeline.ts +1 -1
- package/examples/{inference → npm-libs/inference}/pluggable-adapters.ts +1 -1
- package/examples/{node → npm-libs/node}/ai-conversation.ts +1 -1
- package/examples/{node → npm-libs/node}/simple-greeting.ts +2 -2
- package/examples/step-machine-cli/portfolio-tracker/cards/holdings-table.json +22 -0
- package/examples/step-machine-cli/portfolio-tracker/cards/portfolio-form.json +43 -0
- package/examples/step-machine-cli/portfolio-tracker/cards/portfolio-value.json +15 -0
- package/examples/step-machine-cli/portfolio-tracker/cards/price-fetch.json +15 -0
- package/examples/step-machine-cli/portfolio-tracker/fetch-prices.js +48 -0
- package/examples/step-machine-cli/portfolio-tracker/handlers/_board-cli.js +58 -0
- package/examples/step-machine-cli/portfolio-tracker/handlers/add-cards-cli.js +27 -0
- package/examples/step-machine-cli/portfolio-tracker/handlers/init-board-cli.js +25 -0
- package/examples/step-machine-cli/portfolio-tracker/handlers/reset-board-dir-cli.js +29 -0
- package/examples/step-machine-cli/portfolio-tracker/handlers/retrigger-cli.js +27 -0
- package/examples/step-machine-cli/portfolio-tracker/handlers/status-cli.js +25 -0
- package/examples/step-machine-cli/portfolio-tracker/handlers/update-holdings-cli.js +37 -0
- package/examples/step-machine-cli/portfolio-tracker/handlers/wait-completed-cli.js +53 -0
- package/examples/step-machine-cli/portfolio-tracker/handlers/write-prices-cli.js +35 -0
- package/examples/step-machine-cli/portfolio-tracker/portfolio-tracker-task-executor.cjs +96 -0
- package/examples/step-machine-cli/portfolio-tracker/portfolio-tracker.flow.yaml +227 -0
- package/examples/step-machine-cli/portfolio-tracker/portfolio-tracker.input.json +38 -0
- package/examples/step-machine-cli/portfolio-tracker/run-portfolio-tracker.bat +29 -0
- package/package.json +27 -2
- package/schema/board-status.schema.json +118 -0
- package/schema/card-runtime.schema.json +25 -0
- package/schema/flow.schema.json +5 -0
- package/schema/live-cards.schema.json +90 -83
- package/step-machine-cli.js +674 -0
- package/browser/ingest-board.js +0 -296
- package/examples/ingest.js +0 -733
- /package/examples/{flows → npm-libs/flows}/ai-conversation.yaml +0 -0
- /package/examples/{flows → npm-libs/flows}/order-processing.yaml +0 -0
- /package/examples/{flows → npm-libs/flows}/simple-greeting.yaml +0 -0
|
@@ -0,0 +1,806 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
|
+
<title>Example Board Demo (Browser Runtime)</title>
|
|
7
|
+
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" />
|
|
8
|
+
<script src="https://cdn.jsdelivr.net/npm/jsonata/jsonata.min.js"></script>
|
|
9
|
+
<script src="../../browser/card-compute.js" onerror="this.onerror=null;this.src='https://cdn.jsdelivr.net/npm/yaml-flow@latest/browser/card-compute.js';"></script>
|
|
10
|
+
<script src="../../browser/live-cards.js" onerror="this.onerror=null;this.src='https://cdn.jsdelivr.net/npm/yaml-flow@latest/browser/live-cards.js';"></script>
|
|
11
|
+
<script src="../../browser/board-livegraph-runtime.js" onerror="this.onerror=null;this.src='https://cdn.jsdelivr.net/npm/yaml-flow@latest/browser/board-livegraph-runtime.js';"></script>
|
|
12
|
+
<script src="./reusable-runtime-artifacts-adapter.js"></script>
|
|
13
|
+
</head>
|
|
14
|
+
<body class="bg-light">
|
|
15
|
+
<div class="container-fluid py-3">
|
|
16
|
+
<div class="d-flex flex-wrap align-items-center justify-content-between gap-2 mb-3">
|
|
17
|
+
<div>
|
|
18
|
+
<h1 class="h4 mb-0" id="boardTitle">Example Board (Browser Runtime)</h1>
|
|
19
|
+
<div class="small text-muted" id="boardDesc"></div>
|
|
20
|
+
</div>
|
|
21
|
+
<div class="d-flex align-items-center gap-2">
|
|
22
|
+
<a class="btn btn-sm btn-outline-secondary" href="demo-shell-with-server.html">Open Server Runtime Shell</a>
|
|
23
|
+
<button class="btn btn-sm btn-outline-primary" id="modeBoard">Board</button>
|
|
24
|
+
<button class="btn btn-sm btn-outline-primary" id="modeCanvas">Canvas</button>
|
|
25
|
+
<button class="btn btn-sm btn-outline-secondary" id="autoLayout">Auto Layout</button>
|
|
26
|
+
<button class="btn btn-sm btn-outline-dark" id="refreshAll">Refresh All</button>
|
|
27
|
+
<div class="form-check ms-2">
|
|
28
|
+
<input class="form-check-input" type="checkbox" id="devModeToggle" />
|
|
29
|
+
<label class="form-check-label" for="devModeToggle">Dev Mode</label>
|
|
30
|
+
</div>
|
|
31
|
+
</div>
|
|
32
|
+
</div>
|
|
33
|
+
|
|
34
|
+
<div class="alert alert-info small py-2 mb-3">
|
|
35
|
+
Browser runtime mode: sources are executed in-browser through an opaque task executor.
|
|
36
|
+
Card source definitions are treated as task-executor-owned metadata.
|
|
37
|
+
</div>
|
|
38
|
+
|
|
39
|
+
<div class="alert alert-secondary small py-2 mb-3">
|
|
40
|
+
Contract and decision notes live in <a href="./demo-shell.html#assumptions-and-tradeoffs">Assumptions and Tradeoffs</a>.
|
|
41
|
+
</div>
|
|
42
|
+
|
|
43
|
+
<details id="debugPanelDetails" class="card border-warning mb-3 d-none">
|
|
44
|
+
<summary class="card-header py-2 d-flex justify-content-between align-items-center" style="cursor: pointer;">
|
|
45
|
+
<strong class="small">Live Debug Panel</strong>
|
|
46
|
+
<span class="badge text-bg-warning">browser-runtime</span>
|
|
47
|
+
</summary>
|
|
48
|
+
<div class="card-body py-2">
|
|
49
|
+
<pre id="debugPanelBody" class="small mb-0" style="white-space: pre-wrap; max-height: 220px; overflow: auto;">Initializing debug panel...</pre>
|
|
50
|
+
</div>
|
|
51
|
+
</details>
|
|
52
|
+
|
|
53
|
+
<div id="boardRoot"></div>
|
|
54
|
+
</div>
|
|
55
|
+
|
|
56
|
+
<script>
|
|
57
|
+
(function () {
|
|
58
|
+
function failInit(message) {
|
|
59
|
+
const root = document.getElementById('boardRoot');
|
|
60
|
+
if (root) {
|
|
61
|
+
root.innerHTML = `<div class="alert alert-danger">${message}</div>`;
|
|
62
|
+
}
|
|
63
|
+
console.error(message);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Get LocalStorageService from board-livegraph-runtime export
|
|
67
|
+
const LocalStorageService = (window.BoardLiveGraph && window.BoardLiveGraph.LocalStorageService);
|
|
68
|
+
if (!LocalStorageService) {
|
|
69
|
+
failInit('LocalStorageService not loaded. Run "npm run build:browser" first.');
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ========================================================================
|
|
74
|
+
// Demo Setup
|
|
75
|
+
// ========================================================================
|
|
76
|
+
const CARD_FILES = [
|
|
77
|
+
'card-ex-source.json',
|
|
78
|
+
'card-ex-source-http.json',
|
|
79
|
+
'card-ex-filter.json',
|
|
80
|
+
'card-ex-metric.json',
|
|
81
|
+
'card-ex-list.json',
|
|
82
|
+
'card-ex-chart.json',
|
|
83
|
+
'card-ex-table.json',
|
|
84
|
+
'card-ex-status.json',
|
|
85
|
+
'card-ex-markdown.json',
|
|
86
|
+
'card-ex-narrative.json',
|
|
87
|
+
'card-ex-todo.json',
|
|
88
|
+
'card-ex-form.json',
|
|
89
|
+
'card-ex-filtered-by-preference.json',
|
|
90
|
+
'card-ex-actions.json',
|
|
91
|
+
'card-chain-region-totals.json',
|
|
92
|
+
'card-chain-top-region.json',
|
|
93
|
+
'card-chain-region-alert.json',
|
|
94
|
+
];
|
|
95
|
+
|
|
96
|
+
const ORDER_SEED = [
|
|
97
|
+
{ id: 'ORD-1001', product: 'Widget A', quantity: 3, amount: 12400, region: 'North' },
|
|
98
|
+
{ id: 'ORD-1002', product: 'Widget B', quantity: 2, amount: 8700, region: 'South' },
|
|
99
|
+
{ id: 'ORD-1003', product: 'Widget A', quantity: 4, amount: 15200, region: 'East' },
|
|
100
|
+
{ id: 'ORD-1004', product: 'Widget C', quantity: 1, amount: 6300, region: 'West' },
|
|
101
|
+
{ id: 'ORD-1005', product: 'Widget B', quantity: 2, amount: 9100, region: 'North' },
|
|
102
|
+
{ id: 'ORD-1006', product: 'Widget C', quantity: 3, amount: 9800, region: 'South' },
|
|
103
|
+
];
|
|
104
|
+
|
|
105
|
+
const PRICE_SEED = [
|
|
106
|
+
{ product: 'Widget A', price: 4133.33, currency: 'USD' },
|
|
107
|
+
{ product: 'Widget B', price: 4450.0, currency: 'USD' },
|
|
108
|
+
{ product: 'Widget C', price: 3266.67, currency: 'USD' },
|
|
109
|
+
];
|
|
110
|
+
|
|
111
|
+
const clone = (x) => JSON.parse(JSON.stringify(x));
|
|
112
|
+
const nowIso = () => new Date().toISOString();
|
|
113
|
+
const MOCK_FS_PREFIX = 'yf:demo-surface:cards:';
|
|
114
|
+
|
|
115
|
+
function safeJsonParse(raw, fallback) {
|
|
116
|
+
if (!raw) return fallback;
|
|
117
|
+
try {
|
|
118
|
+
return JSON.parse(raw);
|
|
119
|
+
} catch {
|
|
120
|
+
return fallback;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function mockFsKey(cardId, dirName) {
|
|
125
|
+
return `${MOCK_FS_PREFIX}${String(cardId)}:${String(dirName)}`;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function readMockDir(cardId, dirName) {
|
|
129
|
+
const key = mockFsKey(cardId, dirName);
|
|
130
|
+
const parsed = safeJsonParse(localStorage.getItem(key), []);
|
|
131
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function writeMockDir(cardId, dirName, items) {
|
|
135
|
+
localStorage.setItem(mockFsKey(cardId, dirName), JSON.stringify(Array.isArray(items) ? items : []));
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function clearMockDir(cardId, dirName) {
|
|
139
|
+
localStorage.removeItem(mockFsKey(cardId, dirName));
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function toFileMetadataOnly(entry) {
|
|
143
|
+
if (!entry || typeof entry !== 'object') return null;
|
|
144
|
+
return {
|
|
145
|
+
name: entry.name || null,
|
|
146
|
+
stored_name: entry.stored_name || null,
|
|
147
|
+
size: typeof entry.size === 'number' ? entry.size : null,
|
|
148
|
+
mime_type: entry.mime_type || null,
|
|
149
|
+
path: entry.path || null,
|
|
150
|
+
uploaded_at: entry.uploaded_at || null,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function normalizeDisplayFileName(name) {
|
|
155
|
+
const input = String(name || '').trim();
|
|
156
|
+
if (!input) return 'upload.bin';
|
|
157
|
+
const parts = input.split(/[/\\]/);
|
|
158
|
+
const base = parts[parts.length - 1] || 'upload.bin';
|
|
159
|
+
return base;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function normalizeStem(rawStem) {
|
|
163
|
+
const normalized = String(rawStem || '')
|
|
164
|
+
.toLowerCase()
|
|
165
|
+
.replace(/\s+/g, '_')
|
|
166
|
+
.replace(/[^a-z0-9_-]/g, '_')
|
|
167
|
+
.replace(/_+/g, '_')
|
|
168
|
+
.replace(/^_+|_+$/g, '');
|
|
169
|
+
return normalized || 'file';
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function normalizeExt(rawExt) {
|
|
173
|
+
if (!rawExt || rawExt === '.') return '';
|
|
174
|
+
const extBody = String(rawExt).replace(/^\./, '').toLowerCase().replace(/[^a-z0-9]/g, '');
|
|
175
|
+
return extBody ? `.${extBody}` : '';
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function parseLeadingSerial(fileName) {
|
|
179
|
+
const m = String(fileName || '').match(/^(\d+)[-_]/);
|
|
180
|
+
return m ? parseInt(m[1], 10) : 0;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function nextSerialFromNames(names) {
|
|
184
|
+
let maxSeen = 0;
|
|
185
|
+
for (const name of names) {
|
|
186
|
+
const serial = parseLeadingSerial(name);
|
|
187
|
+
if (Number.isFinite(serial) && serial > maxSeen) maxSeen = serial;
|
|
188
|
+
}
|
|
189
|
+
return maxSeen + 1;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function buildStoredFileName(cardId, fileName) {
|
|
193
|
+
const maxLen = 32;
|
|
194
|
+
const displayName = normalizeDisplayFileName(fileName);
|
|
195
|
+
const extRaw = displayName.includes('.') ? displayName.slice(displayName.lastIndexOf('.')) : '';
|
|
196
|
+
const ext = normalizeExt(extRaw);
|
|
197
|
+
const stemRaw = extRaw ? displayName.slice(0, -extRaw.length) : displayName;
|
|
198
|
+
const stemNorm = normalizeStem(stemRaw);
|
|
199
|
+
|
|
200
|
+
const existing = readPersistedFileMetadata(cardId);
|
|
201
|
+
const names = existing.map((f) => f && typeof f.stored_name === 'string' ? f.stored_name : '').filter(Boolean);
|
|
202
|
+
let serial = nextSerialFromNames(names);
|
|
203
|
+
|
|
204
|
+
while (true) {
|
|
205
|
+
const prefix = `${String(serial).padStart(3, '0')}-`;
|
|
206
|
+
let keepExt = ext;
|
|
207
|
+
let stemBudget = maxLen - prefix.length - keepExt.length;
|
|
208
|
+
if (stemBudget < 1) {
|
|
209
|
+
keepExt = '';
|
|
210
|
+
stemBudget = maxLen - prefix.length;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const stem = stemNorm.slice(0, Math.max(1, stemBudget));
|
|
214
|
+
let candidate = `${prefix}${stem}${keepExt}`;
|
|
215
|
+
if (candidate.length > maxLen) {
|
|
216
|
+
candidate = candidate.slice(0, maxLen).replace(/\.$/, '');
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (!names.includes(candidate)) return candidate;
|
|
220
|
+
serial += 1;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function nextChatPath(cardId, role) {
|
|
225
|
+
const chats = readMockDir(cardId, 'chats');
|
|
226
|
+
const names = chats
|
|
227
|
+
.map((entry) => {
|
|
228
|
+
if (!entry || typeof entry.path !== 'string') return '';
|
|
229
|
+
const parts = entry.path.split('/');
|
|
230
|
+
return parts[parts.length - 1] || '';
|
|
231
|
+
})
|
|
232
|
+
.filter(Boolean);
|
|
233
|
+
const serial = nextSerialFromNames(names);
|
|
234
|
+
const safeRole = String(role || 'system').toLowerCase().replace(/[^a-z0-9_-]/g, '_') || 'system';
|
|
235
|
+
return `${cardId}/chats/${String(serial).padStart(3, '0')}_${safeRole}.txt`;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function readPersistedFileMetadata(cardId) {
|
|
239
|
+
return readMockDir(cardId, 'files')
|
|
240
|
+
.map(toFileMetadataOnly)
|
|
241
|
+
.filter(Boolean);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function readPersistedChatMessages(cardId) {
|
|
245
|
+
return readMockDir(cardId, 'chats')
|
|
246
|
+
.map((entry) => {
|
|
247
|
+
if (!entry || typeof entry !== 'object') return null;
|
|
248
|
+
return {
|
|
249
|
+
role: typeof entry.role === 'string' ? entry.role : 'system',
|
|
250
|
+
text: typeof entry.text === 'string' ? entry.text : '',
|
|
251
|
+
files: Array.isArray(entry.files) ? entry.files.map(toFileMetadataOnly).filter(Boolean) : [],
|
|
252
|
+
at: typeof entry.at === 'string' ? entry.at : null,
|
|
253
|
+
};
|
|
254
|
+
})
|
|
255
|
+
.filter(Boolean);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
async function fileToBase64(file) {
|
|
259
|
+
if (!file || typeof file.arrayBuffer !== 'function') return null;
|
|
260
|
+
const buffer = await file.arrayBuffer();
|
|
261
|
+
const bytes = new Uint8Array(buffer);
|
|
262
|
+
let binary = '';
|
|
263
|
+
const chunkSize = 0x8000;
|
|
264
|
+
for (let i = 0; i < bytes.length; i += chunkSize) {
|
|
265
|
+
const chunk = bytes.subarray(i, i + chunkSize);
|
|
266
|
+
binary += String.fromCharCode.apply(null, chunk);
|
|
267
|
+
}
|
|
268
|
+
return btoa(binary);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
async function persistFileForCard(cardId, fileLike, now) {
|
|
272
|
+
if (!fileLike) return null;
|
|
273
|
+
|
|
274
|
+
if (typeof fileLike === 'object' && typeof fileLike.name === 'string' && typeof fileLike.arrayBuffer === 'function') {
|
|
275
|
+
const name = normalizeDisplayFileName(fileLike.name);
|
|
276
|
+
const storedName = buildStoredFileName(cardId, name);
|
|
277
|
+
const metadata = {
|
|
278
|
+
name,
|
|
279
|
+
stored_name: storedName,
|
|
280
|
+
size: typeof fileLike.size === 'number' ? fileLike.size : null,
|
|
281
|
+
mime_type: fileLike.type || 'application/octet-stream',
|
|
282
|
+
path: `${cardId}/files/${storedName}`,
|
|
283
|
+
uploaded_at: now,
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
const filesDir = readMockDir(cardId, 'files');
|
|
287
|
+
filesDir.push({
|
|
288
|
+
...metadata,
|
|
289
|
+
content_base64: await fileToBase64(fileLike),
|
|
290
|
+
});
|
|
291
|
+
writeMockDir(cardId, 'files', filesDir);
|
|
292
|
+
return metadata;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if (typeof fileLike === 'object' && typeof fileLike.name === 'string') {
|
|
296
|
+
const displayName = normalizeDisplayFileName(fileLike.name);
|
|
297
|
+
const storedName = fileLike.stored_name || buildStoredFileName(cardId, displayName);
|
|
298
|
+
const metadata = {
|
|
299
|
+
name: displayName,
|
|
300
|
+
stored_name: storedName,
|
|
301
|
+
size: typeof fileLike.size === 'number' ? fileLike.size : null,
|
|
302
|
+
mime_type: fileLike.mime_type || 'application/octet-stream',
|
|
303
|
+
path: fileLike.path || `${cardId}/files/${storedName}`,
|
|
304
|
+
uploaded_at: fileLike.uploaded_at || now,
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
const filesDir = readMockDir(cardId, 'files');
|
|
308
|
+
const knownPaths = new Set(filesDir.map((entry) => entry && entry.path).filter(Boolean));
|
|
309
|
+
if (!knownPaths.has(metadata.path)) {
|
|
310
|
+
filesDir.push(metadata);
|
|
311
|
+
writeMockDir(cardId, 'files', filesDir);
|
|
312
|
+
}
|
|
313
|
+
return metadata;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
if (typeof fileLike === 'string') {
|
|
317
|
+
const displayName = normalizeDisplayFileName(fileLike);
|
|
318
|
+
const storedName = buildStoredFileName(cardId, displayName);
|
|
319
|
+
const metadata = {
|
|
320
|
+
name: displayName,
|
|
321
|
+
stored_name: storedName,
|
|
322
|
+
size: null,
|
|
323
|
+
mime_type: 'application/octet-stream',
|
|
324
|
+
path: `${cardId}/files/${storedName}`,
|
|
325
|
+
uploaded_at: now,
|
|
326
|
+
};
|
|
327
|
+
const filesDir = readMockDir(cardId, 'files');
|
|
328
|
+
filesDir.push(metadata);
|
|
329
|
+
writeMockDir(cardId, 'files', filesDir);
|
|
330
|
+
return metadata;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
return null;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function appendChatRecord(cardId, record) {
|
|
337
|
+
const chatsDir = readMockDir(cardId, 'chats');
|
|
338
|
+
const next = { ...(record || {}) };
|
|
339
|
+
if (!next.path) {
|
|
340
|
+
next.path = nextChatPath(cardId, next.role || 'system');
|
|
341
|
+
}
|
|
342
|
+
chatsDir.push(next);
|
|
343
|
+
writeMockDir(cardId, 'chats', chatsDir);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function renderDebugPanel(runtimeState, artifacts) {
|
|
347
|
+
const el = document.getElementById('debugPanelBody');
|
|
348
|
+
if (!el) return;
|
|
349
|
+
|
|
350
|
+
const tasks = runtimeState && runtimeState.state && runtimeState.state.tasks
|
|
351
|
+
? runtimeState.state.tasks
|
|
352
|
+
: {};
|
|
353
|
+
const availableOutputs = runtimeState && runtimeState.state && Array.isArray(runtimeState.state.availableOutputs)
|
|
354
|
+
? runtimeState.state.availableOutputs
|
|
355
|
+
: [];
|
|
356
|
+
|
|
357
|
+
const byStatus = {};
|
|
358
|
+
for (const task of Object.values(tasks)) {
|
|
359
|
+
const status = task && task.status ? task.status : 'unknown';
|
|
360
|
+
byStatus[status] = (byStatus[status] || 0) + 1;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
const cardRuntimeById = artifacts && artifacts.cardRuntimeById ? artifacts.cardRuntimeById : {};
|
|
364
|
+
const metric = cardRuntimeById['card-ex-metric'] && cardRuntimeById['card-ex-metric'].computed_values
|
|
365
|
+
? cardRuntimeById['card-ex-metric'].computed_values.totalRevenue
|
|
366
|
+
: undefined;
|
|
367
|
+
const topProducts = cardRuntimeById['card-ex-list'] && cardRuntimeById['card-ex-list'].computed_values
|
|
368
|
+
? cardRuntimeById['card-ex-list'].computed_values.topProducts
|
|
369
|
+
: undefined;
|
|
370
|
+
const regionCounts = cardRuntimeById['card-ex-chart'] && cardRuntimeById['card-ex-chart'].computed_values
|
|
371
|
+
? cardRuntimeById['card-ex-chart'].computed_values.regionCounts
|
|
372
|
+
: undefined;
|
|
373
|
+
const filteredOrders = cardRuntimeById['card-ex-table'] && cardRuntimeById['card-ex-table'].computed_values
|
|
374
|
+
? cardRuntimeById['card-ex-table'].computed_values.filtered
|
|
375
|
+
: undefined;
|
|
376
|
+
|
|
377
|
+
const watched = {
|
|
378
|
+
orders: availableOutputs.includes('orders'),
|
|
379
|
+
prices: availableOutputs.includes('prices'),
|
|
380
|
+
selections: availableOutputs.includes('selections'),
|
|
381
|
+
formToken: availableOutputs.includes('card-ex-form'),
|
|
382
|
+
};
|
|
383
|
+
|
|
384
|
+
const lines = [
|
|
385
|
+
`updatedAt: ${new Date().toISOString()}`,
|
|
386
|
+
`statusCounts: ${JSON.stringify(byStatus)}`,
|
|
387
|
+
`availableOutputs(${availableOutputs.length}): ${availableOutputs.slice().sort().join(', ')}`,
|
|
388
|
+
`watchedTokens: ${JSON.stringify(watched)}`,
|
|
389
|
+
`card-ex-metric.totalRevenue: ${JSON.stringify(metric)}`,
|
|
390
|
+
`card-ex-list.topProducts.length: ${Array.isArray(topProducts) ? topProducts.length : 'n/a'}`,
|
|
391
|
+
`card-ex-chart.regionCounts.length: ${Array.isArray(regionCounts) ? regionCounts.length : 'n/a'}`,
|
|
392
|
+
`card-ex-table.filtered.length: ${Array.isArray(filteredOrders) ? filteredOrders.length : 'n/a'}`,
|
|
393
|
+
];
|
|
394
|
+
|
|
395
|
+
el.textContent = lines.join('\n');
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function hasCompatiblePersistedArtifacts(cardDefinitions, cardRuntimeById) {
|
|
399
|
+
if (!cardRuntimeById || typeof cardRuntimeById !== 'object') return false;
|
|
400
|
+
const cards = Array.isArray(cardDefinitions) ? cardDefinitions : [];
|
|
401
|
+
if (!cards.length) return false;
|
|
402
|
+
return cards.every((card) => {
|
|
403
|
+
const artifact = cardRuntimeById[card.id];
|
|
404
|
+
if (!artifact || typeof artifact !== 'object') return false;
|
|
405
|
+
if (card.sources && card.sources.length > 0) {
|
|
406
|
+
if (!(artifact.fetched_sources && typeof artifact.fetched_sources === 'object' && !Array.isArray(artifact.fetched_sources))) {
|
|
407
|
+
return false;
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
if (card.requires && card.requires.length > 0) {
|
|
411
|
+
if (!(artifact.requires && typeof artifact.requires === 'object' && !Array.isArray(artifact.requires))) {
|
|
412
|
+
return false;
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
return true;
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
function parseBoardMeta(yamlText) {
|
|
420
|
+
const nameMatch = yamlText.match(/^name:\s*(.+)$/m);
|
|
421
|
+
const descMatch = yamlText.match(/^desc:\s*(.+)$/m);
|
|
422
|
+
return {
|
|
423
|
+
name: nameMatch ? nameMatch[1].trim() : 'Example Board',
|
|
424
|
+
desc: descMatch ? descMatch[1].trim() : '',
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
async function loadBoardFromFiles() {
|
|
429
|
+
const [boardYaml, ...cards] = await Promise.all([
|
|
430
|
+
fetch('./board.yaml').then(r => r.text()),
|
|
431
|
+
...CARD_FILES.map(name => fetch(`./cards/${name}`).then(r => r.json())),
|
|
432
|
+
]);
|
|
433
|
+
|
|
434
|
+
const boardMeta = parseBoardMeta(boardYaml);
|
|
435
|
+
for (const c of cards) {
|
|
436
|
+
c.card_data = c.card_data || {};
|
|
437
|
+
const persistedFiles = readPersistedFileMetadata(c.id);
|
|
438
|
+
if (persistedFiles.length > 0) c.card_data.files = persistedFiles;
|
|
439
|
+
|
|
440
|
+
const persistedChats = readPersistedChatMessages(c.id);
|
|
441
|
+
if (persistedChats.length > 0) c.card_data.messages = persistedChats;
|
|
442
|
+
|
|
443
|
+
// Persist card definition to localStorage (mirrors server's tmp/cards/<id>.json)
|
|
444
|
+
LocalStorageService.writeCard(c.id, c);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
return { board: boardMeta, cards };
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
function makeMockSourceServer() {
|
|
451
|
+
let orderRows = clone(ORDER_SEED);
|
|
452
|
+
let priceRows = clone(PRICE_SEED);
|
|
453
|
+
|
|
454
|
+
return {
|
|
455
|
+
fetchSource: async function (card, sourceDef) {
|
|
456
|
+
const mockKey = sourceDef && typeof sourceDef.mock === 'string' ? sourceDef.mock : '';
|
|
457
|
+
const script = String(sourceDef && sourceDef.script || sourceDef && sourceDef.cli || '').toLowerCase();
|
|
458
|
+
if (mockKey === 'orders') return clone(orderRows);
|
|
459
|
+
if (mockKey === 'prices') return clone(priceRows);
|
|
460
|
+
if (card.id === 'card-ex-source' || script.includes('fetch-orders')) return clone(orderRows);
|
|
461
|
+
if (card.id === 'card-ex-source-http' || script.includes('fetch-prices')) return clone(priceRows);
|
|
462
|
+
return null;
|
|
463
|
+
},
|
|
464
|
+
setDatasets: function (nextOrders, nextPrices) {
|
|
465
|
+
if (Array.isArray(nextOrders)) orderRows = clone(nextOrders);
|
|
466
|
+
if (Array.isArray(nextPrices)) priceRows = clone(nextPrices);
|
|
467
|
+
},
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
function replaceNodeInPlace(target, source) {
|
|
472
|
+
Object.keys(target).forEach(k => delete target[k]);
|
|
473
|
+
Object.assign(target, clone(source));
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
let board = null;
|
|
477
|
+
let runtime = null;
|
|
478
|
+
let runtimeUnsub = null;
|
|
479
|
+
let currentMode = 'board';
|
|
480
|
+
const nodesById = {};
|
|
481
|
+
|
|
482
|
+
function setDebugPanelVisibility(isDevModeOn) {
|
|
483
|
+
const details = document.getElementById('debugPanelDetails');
|
|
484
|
+
if (!details) return;
|
|
485
|
+
if (isDevModeOn) {
|
|
486
|
+
details.classList.remove('d-none');
|
|
487
|
+
} else {
|
|
488
|
+
details.open = false;
|
|
489
|
+
details.classList.add('d-none');
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
function toIsoTimeTag(iso) {
|
|
494
|
+
const d = new Date(iso);
|
|
495
|
+
const hh = String(d.getHours()).padStart(2, '0');
|
|
496
|
+
const mm = String(d.getMinutes()).padStart(2, '0');
|
|
497
|
+
const ss = String(d.getSeconds()).padStart(2, '0');
|
|
498
|
+
return `${hh}:${mm}:${ss}`;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
function compactFiles(files) {
|
|
502
|
+
if (!Array.isArray(files)) return [];
|
|
503
|
+
return files
|
|
504
|
+
.map((f) => {
|
|
505
|
+
if (!f) return null;
|
|
506
|
+
if (typeof f === 'string') return { name: f };
|
|
507
|
+
if (typeof f === 'object' && typeof f.name === 'string') return { name: f.name, size: f.size || null };
|
|
508
|
+
return null;
|
|
509
|
+
})
|
|
510
|
+
.filter(Boolean);
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
async function handleActionInBrowserRuntime(id, actionType, payload) {
|
|
514
|
+
const node = nodesById[id];
|
|
515
|
+
if (!node) return;
|
|
516
|
+
|
|
517
|
+
const now = nowIso();
|
|
518
|
+
const cardData = node.card_data || {};
|
|
519
|
+
|
|
520
|
+
if (actionType === 'chat-send') {
|
|
521
|
+
const text = payload && typeof payload.text === 'string' ? payload.text.trim() : '';
|
|
522
|
+
const rawFiles = Array.isArray(payload && payload.files) ? payload.files : [];
|
|
523
|
+
if (!text && rawFiles.length === 0) return;
|
|
524
|
+
|
|
525
|
+
const fileMetas = [];
|
|
526
|
+
for (const fileLike of rawFiles) {
|
|
527
|
+
const persisted = await persistFileForCard(id, fileLike, now);
|
|
528
|
+
if (persisted) fileMetas.push(persisted);
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
appendChatRecord(id, {
|
|
532
|
+
at: now,
|
|
533
|
+
role: 'user',
|
|
534
|
+
text,
|
|
535
|
+
files: fileMetas,
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
fileMetas.forEach((fileMeta) => {
|
|
539
|
+
if (!fileMeta || !fileMeta.stored_name) return;
|
|
540
|
+
const display = fileMeta.name || 'file';
|
|
541
|
+
appendChatRecord(id, {
|
|
542
|
+
at: now,
|
|
543
|
+
role: 'system',
|
|
544
|
+
text: `File ${display} uploaded as ${fileMeta.stored_name}.`,
|
|
545
|
+
files: [],
|
|
546
|
+
});
|
|
547
|
+
});
|
|
548
|
+
|
|
549
|
+
// Keep chat render state in sync with local-storage-backed chat entries.
|
|
550
|
+
runtime.patchCardState(id, {
|
|
551
|
+
messages: readPersistedChatMessages(id),
|
|
552
|
+
lastInteractionAt: now,
|
|
553
|
+
});
|
|
554
|
+
return;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
if (actionType === 'action') {
|
|
558
|
+
const buttonId = payload && typeof payload.buttonId === 'string' ? payload.buttonId : '';
|
|
559
|
+
if (!buttonId) return;
|
|
560
|
+
|
|
561
|
+
runtime.patchCardState(id, {
|
|
562
|
+
lastAction: { buttonId, at: now },
|
|
563
|
+
lastActionText: `${buttonId} @ ${toIsoTimeTag(now)}`,
|
|
564
|
+
});
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
function syncBoardNodes(nextNodes) {
|
|
569
|
+
const existingIds = new Set(board ? board.nodes.map(n => n.id) : []);
|
|
570
|
+
const nextById = Object.fromEntries(nextNodes.map(n => [n.id, n]));
|
|
571
|
+
|
|
572
|
+
if (board) {
|
|
573
|
+
for (const id of existingIds) {
|
|
574
|
+
if (!nextById[id]) board.remove(id);
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
for (const nextNode of nextNodes) {
|
|
579
|
+
const existing = nodesById[nextNode.id];
|
|
580
|
+
if (existing) replaceNodeInPlace(existing, nextNode);
|
|
581
|
+
else {
|
|
582
|
+
nodesById[nextNode.id] = clone(nextNode);
|
|
583
|
+
if (board) board.add(nodesById[nextNode.id]);
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
if (board) board.refresh();
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
async function bootstrap() {
|
|
591
|
+
const loaded = await loadBoardFromFiles();
|
|
592
|
+
const mockServer = makeMockSourceServer();
|
|
593
|
+
|
|
594
|
+
const createBoardLiveGraphRuntime = (window.BoardLiveGraph && window.BoardLiveGraph.createBoardLiveGraphRuntime);
|
|
595
|
+
if (!createBoardLiveGraphRuntime) {
|
|
596
|
+
throw new Error('BoardLiveGraph runtime not loaded. Run "npm run build:browser" first.');
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
document.getElementById('boardTitle').textContent = loaded.board.name + ' (Browser Runtime)';
|
|
600
|
+
document.getElementById('boardDesc').textContent = loaded.board.desc;
|
|
601
|
+
|
|
602
|
+
const cardDefinitions = loaded.cards;
|
|
603
|
+
const cardDefsById = Object.fromEntries(cardDefinitions.map(c => [c.id, c]));
|
|
604
|
+
const deepSet = (obj, path, value) => {
|
|
605
|
+
if (!path) return;
|
|
606
|
+
const parts = String(path).split('.');
|
|
607
|
+
let cur = obj;
|
|
608
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
609
|
+
const k = parts[i];
|
|
610
|
+
if (!cur[k] || typeof cur[k] !== 'object') cur[k] = {};
|
|
611
|
+
cur = cur[k];
|
|
612
|
+
}
|
|
613
|
+
cur[parts[parts.length - 1]] = value;
|
|
614
|
+
};
|
|
615
|
+
|
|
616
|
+
runtime = createBoardLiveGraphRuntime(loaded.cards, {
|
|
617
|
+
taskExecutor: async function ({ card }) {
|
|
618
|
+
const out = {};
|
|
619
|
+
for (const sourceDef of (card.sources || [])) {
|
|
620
|
+
const payload = await mockServer.fetchSource(card, sourceDef);
|
|
621
|
+
if (payload !== null && payload !== undefined) out[sourceDef.bindTo] = payload;
|
|
622
|
+
}
|
|
623
|
+
return out;
|
|
624
|
+
},
|
|
625
|
+
});
|
|
626
|
+
|
|
627
|
+
// Restore previous runtime artifacts from localStorage (mirrors reading runtime-out/ from disk on server)
|
|
628
|
+
const cardRuntimeById = LocalStorageService.readAllComputedArtifacts(cardDefinitions.map(c => c.id));
|
|
629
|
+
let statusSnapshot = LocalStorageService.readStatusSnapshot();
|
|
630
|
+
|
|
631
|
+
// Build initial renderable nodes from artifacts (same pattern as server shell)
|
|
632
|
+
const initialArtifacts = statusSnapshot && Object.keys(cardRuntimeById).length > 0 && hasCompatiblePersistedArtifacts(cardDefinitions, cardRuntimeById)
|
|
633
|
+
? { cardDefinitions, statusSnapshot, cardRuntimeById }
|
|
634
|
+
: buildBrowserArtifactsFromRuntime({
|
|
635
|
+
boardPath: 'browser',
|
|
636
|
+
cardDefinitions,
|
|
637
|
+
runtimeModels: runtime.getNodes(),
|
|
638
|
+
graphState: runtime.getState(),
|
|
639
|
+
});
|
|
640
|
+
|
|
641
|
+
const initialNodes = buildLiveCardModelsFromArtifacts(initialArtifacts);
|
|
642
|
+
for (const n of initialNodes) nodesById[n.id] = clone(n);
|
|
643
|
+
|
|
644
|
+
const engine = LiveCard.init({
|
|
645
|
+
resolve: (id) => nodesById[id],
|
|
646
|
+
onPatchState: (id, patch) => {
|
|
647
|
+
// Form submissions pass { fieldValues: {...} } from LiveCard form elements.
|
|
648
|
+
// Persist those values in localStorage card at the card's writeTo path.
|
|
649
|
+
if (patch && Object.keys(patch).length === 1 && patch.fieldValues) {
|
|
650
|
+
const cardDef = cardDefsById[id];
|
|
651
|
+
let writeTo = null;
|
|
652
|
+
if (cardDef && cardDef.view && Array.isArray(cardDef.view.elements)) {
|
|
653
|
+
for (const elem of cardDef.view.elements) {
|
|
654
|
+
if (elem && elem.data && elem.data.writeTo) {
|
|
655
|
+
writeTo = elem.data.writeTo;
|
|
656
|
+
break;
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
const statePatch = {};
|
|
662
|
+
if (typeof writeTo === 'string' && writeTo.startsWith('card_data.')) {
|
|
663
|
+
deepSet(statePatch, writeTo.slice('card_data.'.length), patch.fieldValues);
|
|
664
|
+
} else {
|
|
665
|
+
Object.assign(statePatch, patch.fieldValues);
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
// Apply patch to card definition in memory and in localStorage
|
|
669
|
+
const cardInMem = cardDefsById[id];
|
|
670
|
+
if (cardInMem) {
|
|
671
|
+
deepSet(cardInMem, 'card_data', { ...cardInMem.card_data, ...statePatch });
|
|
672
|
+
LocalStorageService.writeCard(id, cardInMem);
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
// Also patch the runtime (which may be used by compute)
|
|
676
|
+
runtime.patchCardState(id, statePatch);
|
|
677
|
+
return;
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
// Auto-commit: file-upload element reports staged files via _stagedFiles.
|
|
681
|
+
// Persist metadata immediately and update the rendered list — no commit button needed.
|
|
682
|
+
if (patch && Array.isArray(patch._stagedFiles) && patch._stagedFiles.length > 0) {
|
|
683
|
+
const now = nowIso();
|
|
684
|
+
const existing = readPersistedFileMetadata(id);
|
|
685
|
+
const seen = new Set(existing.map((f) => f && f.name ? f.name : String(f)));
|
|
686
|
+
for (const item of patch._stagedFiles) {
|
|
687
|
+
if (!item || typeof item.name !== 'string') continue;
|
|
688
|
+
if (seen.has(item.name)) continue;
|
|
689
|
+
existing.push(persistStagedFileMetadata(id, item));
|
|
690
|
+
seen.add(item.name);
|
|
691
|
+
}
|
|
692
|
+
writeStoredFiles(id, existing);
|
|
693
|
+
runtime.patchCardState(id, { files: existing, _stagedFiles: [] });
|
|
694
|
+
return;
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
runtime.patchCardState(id, patch || {});
|
|
698
|
+
},
|
|
699
|
+
onRefresh: (id) => runtime.retrigger(id),
|
|
700
|
+
onAction: (id, actionType, payload) => {
|
|
701
|
+
void handleActionInBrowserRuntime(id, actionType, payload || {});
|
|
702
|
+
},
|
|
703
|
+
getChatMessages: (id) => {
|
|
704
|
+
const items = readPersistedChatMessages(id);
|
|
705
|
+
return items.map((m) => ({
|
|
706
|
+
role: m && typeof m.role === 'string' ? m.role : 'system',
|
|
707
|
+
text: m && typeof m.text === 'string' ? m.text : '',
|
|
708
|
+
files: Array.isArray(m && m.files) ? m.files : [],
|
|
709
|
+
}));
|
|
710
|
+
},
|
|
711
|
+
});
|
|
712
|
+
|
|
713
|
+
board = LiveCard.Board(engine, document.getElementById('boardRoot'), {
|
|
714
|
+
nodes: Object.values(nodesById),
|
|
715
|
+
mode: currentMode,
|
|
716
|
+
canvas: { height: '72vh', overflow: 'auto' },
|
|
717
|
+
});
|
|
718
|
+
|
|
719
|
+
runtimeUnsub = runtime.subscribe(function () {
|
|
720
|
+
const runtimeState = runtime.getState();
|
|
721
|
+
// Build runtime artifacts (mirrors what CLI does after compute)
|
|
722
|
+
const artifacts = buildBrowserArtifactsFromRuntime({
|
|
723
|
+
boardPath: 'browser',
|
|
724
|
+
cardDefinitions,
|
|
725
|
+
runtimeModels: runtime.getNodes(),
|
|
726
|
+
graphState: runtimeState,
|
|
727
|
+
});
|
|
728
|
+
|
|
729
|
+
// Persist to localStorage (mirrors CLI writing to runtime-out/)
|
|
730
|
+
for (const [id, artifact] of Object.entries(artifacts.cardRuntimeById)) {
|
|
731
|
+
LocalStorageService.writeComputedArtifact(artifact);
|
|
732
|
+
}
|
|
733
|
+
LocalStorageService.writeStatusSnapshot(artifacts.statusSnapshot);
|
|
734
|
+
|
|
735
|
+
// Rebuild renderable nodes from persisted artifacts (same pattern as server shell)
|
|
736
|
+
syncBoardNodes(buildLiveCardModelsFromArtifacts(artifacts));
|
|
737
|
+
|
|
738
|
+
// Keep a compact runtime snapshot visible for debugging token flow issues.
|
|
739
|
+
renderDebugPanel(runtimeState, artifacts);
|
|
740
|
+
});
|
|
741
|
+
|
|
742
|
+
runtime.push({ type: 'inject-tokens', tokens: [], timestamp: nowIso() });
|
|
743
|
+
// Force a full recompute pass to recover from stale/incomplete localStorage artifacts.
|
|
744
|
+
runtime.retriggerAll();
|
|
745
|
+
|
|
746
|
+
window.demoLiveGraph = {
|
|
747
|
+
mode: 'browser',
|
|
748
|
+
runtime,
|
|
749
|
+
setDatasets: function (nextOrders, nextPrices) {
|
|
750
|
+
mockServer.setDatasets(nextOrders, nextPrices);
|
|
751
|
+
runtime.retriggerAll();
|
|
752
|
+
},
|
|
753
|
+
clearLocalStorage: function () {
|
|
754
|
+
LocalStorageService.clear();
|
|
755
|
+
Object.keys(localStorage).forEach((key) => {
|
|
756
|
+
if (String(key).startsWith(MOCK_FS_PREFIX)) localStorage.removeItem(key);
|
|
757
|
+
});
|
|
758
|
+
window.location.reload();
|
|
759
|
+
},
|
|
760
|
+
};
|
|
761
|
+
|
|
762
|
+
document.getElementById('modeBoard').addEventListener('click', function () {
|
|
763
|
+
currentMode = 'board';
|
|
764
|
+
if (board) board.setMode('board');
|
|
765
|
+
});
|
|
766
|
+
|
|
767
|
+
document.getElementById('modeCanvas').addEventListener('click', function () {
|
|
768
|
+
currentMode = 'canvas';
|
|
769
|
+
if (board) board.setMode('canvas');
|
|
770
|
+
});
|
|
771
|
+
|
|
772
|
+
document.getElementById('autoLayout').addEventListener('click', function () {
|
|
773
|
+
if (board) {
|
|
774
|
+
board.setMode('canvas');
|
|
775
|
+
currentMode = 'canvas';
|
|
776
|
+
board.autoLayout();
|
|
777
|
+
}
|
|
778
|
+
});
|
|
779
|
+
|
|
780
|
+
document.getElementById('refreshAll').addEventListener('click', function () {
|
|
781
|
+
if (runtime) runtime.retriggerAll();
|
|
782
|
+
});
|
|
783
|
+
|
|
784
|
+
document.getElementById('devModeToggle').addEventListener('change', function () {
|
|
785
|
+
if (board) board.setDevMode(this.checked);
|
|
786
|
+
setDebugPanelVisibility(this.checked);
|
|
787
|
+
});
|
|
788
|
+
|
|
789
|
+
// Keep debug UI out of the way unless Dev Mode is explicitly enabled.
|
|
790
|
+
setDebugPanelVisibility(document.getElementById('devModeToggle').checked);
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
bootstrap().catch(function (err) {
|
|
794
|
+
console.error(err);
|
|
795
|
+
document.getElementById('boardRoot').innerHTML =
|
|
796
|
+
`<div class="alert alert-danger">Failed to start browser runtime demo: ${String(err && err.message || err)}</div>`;
|
|
797
|
+
});
|
|
798
|
+
|
|
799
|
+
window.addEventListener('beforeunload', function () {
|
|
800
|
+
if (runtimeUnsub) runtimeUnsub();
|
|
801
|
+
if (runtime) runtime.dispose();
|
|
802
|
+
});
|
|
803
|
+
})();
|
|
804
|
+
</script>
|
|
805
|
+
</body>
|
|
806
|
+
</html>
|