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.
Files changed (194) hide show
  1. package/README.md +81 -20
  2. package/board-live-cards-cli.js +37 -0
  3. package/browser/board-livegraph-runtime.js +1453 -0
  4. package/browser/board-livegraph-runtime.js.map +1 -0
  5. package/browser/card-compute.js +153 -433
  6. package/browser/live-cards.js +868 -115
  7. package/browser/live-cards.schema.json +90 -83
  8. package/dist/board-livegraph-runtime/index.cjs +1448 -0
  9. package/dist/board-livegraph-runtime/index.cjs.map +1 -0
  10. package/dist/board-livegraph-runtime/index.d.cts +101 -0
  11. package/dist/board-livegraph-runtime/index.d.ts +101 -0
  12. package/dist/board-livegraph-runtime/index.js +1441 -0
  13. package/dist/board-livegraph-runtime/index.js.map +1 -0
  14. package/dist/card-compute/index.cjs +266 -431
  15. package/dist/card-compute/index.cjs.map +1 -1
  16. package/dist/card-compute/index.d.cts +77 -49
  17. package/dist/card-compute/index.d.ts +77 -49
  18. package/dist/card-compute/index.js +263 -432
  19. package/dist/card-compute/index.js.map +1 -1
  20. package/dist/cli/board-live-cards-cli.cjs +2750 -0
  21. package/dist/cli/board-live-cards-cli.cjs.map +1 -0
  22. package/dist/cli/board-live-cards-cli.d.cts +205 -0
  23. package/dist/cli/board-live-cards-cli.d.ts +205 -0
  24. package/dist/cli/board-live-cards-cli.js +2702 -0
  25. package/dist/cli/board-live-cards-cli.js.map +1 -0
  26. package/dist/{constants-B2zqu10b.d.ts → constants-DuzE5n03.d.ts} +2 -2
  27. package/dist/{constants-DJZU1pwJ.d.cts → constants-ozjf1Ejw.d.cts} +2 -2
  28. package/dist/continuous-event-graph/index.cjs +258 -464
  29. package/dist/continuous-event-graph/index.cjs.map +1 -1
  30. package/dist/continuous-event-graph/index.d.cts +18 -358
  31. package/dist/continuous-event-graph/index.d.ts +18 -358
  32. package/dist/continuous-event-graph/index.js +255 -464
  33. package/dist/continuous-event-graph/index.js.map +1 -1
  34. package/dist/event-graph/index.cjs +4 -4
  35. package/dist/event-graph/index.cjs.map +1 -1
  36. package/dist/event-graph/index.d.cts +5 -5
  37. package/dist/event-graph/index.d.ts +5 -5
  38. package/dist/event-graph/index.js +4 -4
  39. package/dist/event-graph/index.js.map +1 -1
  40. package/dist/index.cjs +1684 -555
  41. package/dist/index.cjs.map +1 -1
  42. package/dist/index.d.cts +26 -7
  43. package/dist/index.d.ts +26 -7
  44. package/dist/index.js +1678 -555
  45. package/dist/index.js.map +1 -1
  46. package/dist/inference/index.cjs +138 -19
  47. package/dist/inference/index.cjs.map +1 -1
  48. package/dist/inference/index.d.cts +2 -2
  49. package/dist/inference/index.d.ts +2 -2
  50. package/dist/inference/index.js +138 -19
  51. package/dist/inference/index.js.map +1 -1
  52. package/dist/journal-DRfJiheM.d.cts +28 -0
  53. package/dist/journal-NLYuqege.d.ts +28 -0
  54. package/dist/live-cards-bridge-Or7fdEJV.d.ts +316 -0
  55. package/dist/live-cards-bridge-vGJ6tMzN.d.cts +316 -0
  56. package/dist/schedule-CMcZe5Ny.d.ts +21 -0
  57. package/dist/schedule-CiucyCan.d.cts +21 -0
  58. package/dist/step-machine/index.cjs +18 -1
  59. package/dist/step-machine/index.cjs.map +1 -1
  60. package/dist/step-machine/index.d.cts +2 -2
  61. package/dist/step-machine/index.d.ts +2 -2
  62. package/dist/step-machine/index.js +18 -1
  63. package/dist/step-machine/index.js.map +1 -1
  64. package/dist/stores/file.d.cts +1 -1
  65. package/dist/stores/file.d.ts +1 -1
  66. package/dist/stores/index.d.cts +1 -1
  67. package/dist/stores/index.d.ts +1 -1
  68. package/dist/stores/localStorage.d.cts +1 -1
  69. package/dist/stores/localStorage.d.ts +1 -1
  70. package/dist/stores/memory.d.cts +1 -1
  71. package/dist/stores/memory.d.ts +1 -1
  72. package/dist/{types-BwvgvlOO.d.cts → types-BzLD8bjb.d.cts} +1 -1
  73. package/dist/{types-ClRA8hzC.d.ts → types-C2eJ7DAV.d.ts} +1 -1
  74. package/dist/{types-DEj7OakX.d.cts → types-CMFSIjpc.d.cts} +39 -4
  75. package/dist/{types-DEj7OakX.d.ts → types-CMFSIjpc.d.ts} +39 -4
  76. package/dist/{types-FZ_eyErS.d.cts → types-ycun84cq.d.cts} +1 -0
  77. package/dist/{types-FZ_eyErS.d.ts → types-ycun84cq.d.ts} +1 -0
  78. package/dist/{validate-DEZ2Ymdb.d.ts → validate-DJQTQ6bP.d.ts} +1 -1
  79. package/dist/{validate-DqKTZg_o.d.cts → validate-ke92Cleg.d.cts} +1 -1
  80. package/examples/browser/boards/portfolio-tracker/cards/holdings-table.json +22 -0
  81. package/examples/browser/boards/portfolio-tracker/cards/portfolio-form.json +16 -0
  82. package/examples/browser/boards/portfolio-tracker/cards/portfolio-value.json +15 -0
  83. package/examples/browser/boards/portfolio-tracker/cards/price-fetch.json +15 -0
  84. package/examples/browser/boards/portfolio-tracker/fetch-prices.js +43 -0
  85. package/examples/browser/boards/portfolio-tracker/portfolio-tracker-task-executor.cjs +96 -0
  86. package/examples/browser/boards/portfolio-tracker/portfolio-tracker.bat +7 -0
  87. package/examples/browser/boards/portfolio-tracker/portfolio-tracker.js +217 -0
  88. package/examples/browser/livecards-browser/index.html +41 -0
  89. package/examples/browser/{index.html → step-machine-browser/index.html} +53 -53
  90. package/examples/cli/step-machine-cli/portfolio-tracker/cards/holdings-table.json +22 -0
  91. package/examples/cli/step-machine-cli/portfolio-tracker/cards/portfolio-form.json +43 -0
  92. package/examples/cli/step-machine-cli/portfolio-tracker/cards/portfolio-value.json +15 -0
  93. package/examples/cli/step-machine-cli/portfolio-tracker/cards/price-fetch.json +15 -0
  94. package/examples/cli/step-machine-cli/portfolio-tracker/fetch-prices.js +48 -0
  95. package/examples/cli/step-machine-cli/portfolio-tracker/handlers/_board-cli.js +58 -0
  96. package/examples/cli/step-machine-cli/portfolio-tracker/handlers/add-cards-cli.js +27 -0
  97. package/examples/cli/step-machine-cli/portfolio-tracker/handlers/init-board-cli.js +25 -0
  98. package/examples/cli/step-machine-cli/portfolio-tracker/handlers/reset-board-dir-cli.js +29 -0
  99. package/examples/cli/step-machine-cli/portfolio-tracker/handlers/retrigger-cli.js +27 -0
  100. package/examples/cli/step-machine-cli/portfolio-tracker/handlers/status-cli.js +25 -0
  101. package/examples/cli/step-machine-cli/portfolio-tracker/handlers/update-holdings-cli.js +37 -0
  102. package/examples/cli/step-machine-cli/portfolio-tracker/handlers/wait-completed-cli.js +53 -0
  103. package/examples/cli/step-machine-cli/portfolio-tracker/handlers/write-prices-cli.js +35 -0
  104. package/examples/cli/step-machine-cli/portfolio-tracker/portfolio-tracker.flow.yaml +227 -0
  105. package/examples/cli/step-machine-cli/portfolio-tracker/portfolio-tracker.input.json +38 -0
  106. package/examples/cli/step-machine-cli/portfolio-tracker/run-portfolio-tracker.bat +29 -0
  107. package/examples/cli/step-machine-demo/jsonata-init-board-cli.js +36 -0
  108. package/examples/cli/step-machine-demo/jsonata-init-board.flow.yaml +30 -0
  109. package/examples/cli/step-machine-demo/one-step-cli-only.flow.yaml +19 -0
  110. package/examples/cli/step-machine-demo/step-cli-echo-y.js +15 -0
  111. package/examples/cli/step-machine-demo/step2-double-cli.js +39 -0
  112. package/examples/cli/step-machine-demo/two-step-math-handlers.js +32 -0
  113. package/examples/cli/step-machine-demo/two-step-math.flow.yaml +31 -0
  114. package/examples/cli/step-machine-demo/two-step-mixed-handlers.js +24 -0
  115. package/examples/cli/step-machine-demo/two-step-mixed.flow.yaml +35 -0
  116. package/examples/example-board/board.yaml +23 -0
  117. package/examples/example-board/bootstrap_payload.json +1 -0
  118. package/examples/example-board/cards/card-chain-region-alert.json +39 -0
  119. package/examples/example-board/cards/card-chain-region-totals.json +26 -0
  120. package/examples/example-board/cards/card-chain-top-region.json +24 -0
  121. package/examples/example-board/cards/card-ex-actions.json +32 -0
  122. package/examples/example-board/cards/card-ex-chart.json +30 -0
  123. package/examples/example-board/cards/card-ex-filter.json +36 -0
  124. package/examples/example-board/cards/card-ex-filtered-by-preference.json +59 -0
  125. package/examples/example-board/cards/card-ex-form.json +91 -0
  126. package/examples/example-board/cards/card-ex-list.json +22 -0
  127. package/examples/example-board/cards/card-ex-markdown.json +17 -0
  128. package/examples/example-board/cards/card-ex-metric.json +19 -0
  129. package/examples/example-board/cards/card-ex-narrative.json +36 -0
  130. package/examples/example-board/cards/card-ex-source-http.json +28 -0
  131. package/examples/example-board/cards/card-ex-source.json +21 -0
  132. package/examples/example-board/cards/card-ex-status.json +35 -0
  133. package/examples/example-board/cards/card-ex-table.json +30 -0
  134. package/examples/example-board/cards/card-ex-todo.json +29 -0
  135. package/examples/example-board/demo-chat-handler.js +69 -0
  136. package/examples/example-board/demo-server.js +87 -0
  137. package/examples/example-board/demo-shell-browser.html +806 -0
  138. package/examples/example-board/demo-shell-with-server.html +280 -0
  139. package/examples/example-board/demo-shell.html +62 -0
  140. package/examples/example-board/demo-task-executor.js +255 -0
  141. package/examples/example-board/mock.db +15 -0
  142. package/examples/example-board/reusable-board-runtime-client.js +265 -0
  143. package/examples/example-board/reusable-runtime-artifacts-adapter.js +233 -0
  144. package/examples/example-board/reusable-server-runtime.js +1284 -0
  145. package/examples/index.html +799 -0
  146. package/examples/{batch → npm-libs/batch}/batch-step-machine.ts +1 -1
  147. package/examples/{continuous-event-graph → npm-libs/continuous-event-graph}/live-cards-board.ts +18 -18
  148. package/examples/{continuous-event-graph → npm-libs/continuous-event-graph}/live-portfolio-dashboard.ts +24 -24
  149. package/examples/{continuous-event-graph → npm-libs/continuous-event-graph}/portfolio-tracker.ts +1 -1
  150. package/examples/{continuous-event-graph → npm-libs/continuous-event-graph}/reactive-monitoring.ts +1 -1
  151. package/examples/{continuous-event-graph → npm-libs/continuous-event-graph}/reactive-pipeline.ts +1 -1
  152. package/examples/{continuous-event-graph → npm-libs/continuous-event-graph}/soc-incident-board.ts +1 -1
  153. package/examples/{continuous-event-graph → npm-libs/continuous-event-graph}/stock-dashboard.ts +1 -1
  154. package/examples/{event-graph → npm-libs/event-graph}/ci-cd-pipeline.ts +1 -1
  155. package/examples/{event-graph → npm-libs/event-graph}/executor-diamond.ts +1 -1
  156. package/examples/{event-graph → npm-libs/event-graph}/executor-pipeline.ts +1 -1
  157. package/examples/{event-graph → npm-libs/event-graph}/research-pipeline.ts +1 -1
  158. package/examples/{graph-of-graphs → npm-libs/graph-of-graphs}/multi-stage-etl.ts +1 -1
  159. package/examples/{graph-of-graphs → npm-libs/graph-of-graphs}/url-processing-pipeline.ts +1 -1
  160. package/examples/{inference → npm-libs/inference}/azure-deployment.ts +1 -1
  161. package/examples/{inference → npm-libs/inference}/copilot-cli.ts +1 -1
  162. package/examples/{inference → npm-libs/inference}/data-pipeline.ts +1 -1
  163. package/examples/{inference → npm-libs/inference}/pluggable-adapters.ts +1 -1
  164. package/examples/{node → npm-libs/node}/ai-conversation.ts +1 -1
  165. package/examples/{node → npm-libs/node}/simple-greeting.ts +2 -2
  166. package/examples/step-machine-cli/portfolio-tracker/cards/holdings-table.json +22 -0
  167. package/examples/step-machine-cli/portfolio-tracker/cards/portfolio-form.json +43 -0
  168. package/examples/step-machine-cli/portfolio-tracker/cards/portfolio-value.json +15 -0
  169. package/examples/step-machine-cli/portfolio-tracker/cards/price-fetch.json +15 -0
  170. package/examples/step-machine-cli/portfolio-tracker/fetch-prices.js +48 -0
  171. package/examples/step-machine-cli/portfolio-tracker/handlers/_board-cli.js +58 -0
  172. package/examples/step-machine-cli/portfolio-tracker/handlers/add-cards-cli.js +27 -0
  173. package/examples/step-machine-cli/portfolio-tracker/handlers/init-board-cli.js +25 -0
  174. package/examples/step-machine-cli/portfolio-tracker/handlers/reset-board-dir-cli.js +29 -0
  175. package/examples/step-machine-cli/portfolio-tracker/handlers/retrigger-cli.js +27 -0
  176. package/examples/step-machine-cli/portfolio-tracker/handlers/status-cli.js +25 -0
  177. package/examples/step-machine-cli/portfolio-tracker/handlers/update-holdings-cli.js +37 -0
  178. package/examples/step-machine-cli/portfolio-tracker/handlers/wait-completed-cli.js +53 -0
  179. package/examples/step-machine-cli/portfolio-tracker/handlers/write-prices-cli.js +35 -0
  180. package/examples/step-machine-cli/portfolio-tracker/portfolio-tracker-task-executor.cjs +96 -0
  181. package/examples/step-machine-cli/portfolio-tracker/portfolio-tracker.flow.yaml +227 -0
  182. package/examples/step-machine-cli/portfolio-tracker/portfolio-tracker.input.json +38 -0
  183. package/examples/step-machine-cli/portfolio-tracker/run-portfolio-tracker.bat +29 -0
  184. package/package.json +27 -2
  185. package/schema/board-status.schema.json +118 -0
  186. package/schema/card-runtime.schema.json +25 -0
  187. package/schema/flow.schema.json +5 -0
  188. package/schema/live-cards.schema.json +90 -83
  189. package/step-machine-cli.js +674 -0
  190. package/browser/ingest-board.js +0 -296
  191. package/examples/ingest.js +0 -733
  192. /package/examples/{flows → npm-libs/flows}/ai-conversation.yaml +0 -0
  193. /package/examples/{flows → npm-libs/flows}/order-processing.yaml +0 -0
  194. /package/examples/{flows → npm-libs/flows}/simple-greeting.yaml +0 -0
@@ -1,14 +1,20 @@
1
1
  // live-cards.js — LiveCards v3: Node-based Board/Canvas engine
2
2
  //
3
- // Schema: Each node has { id, type, meta, data, view?, source?, state, compute? }
4
- // type "card" — renderable node with view.elements[]
5
- // type "source" data-only node (no view, shown as pill in canvas)
3
+ // Schema: Each node has { id } required; all else optional.
4
+ // id, meta, card_data, requires, provides, sources, compute, view
5
+ // Nodes with view render as cards; nodes with sources but no view render as source pills in canvas.
6
+ // compute[] — ordered array of { bindTo, expr } JSONata steps → writes to node.computed_values (ephemeral)
7
+ // sources[] — open objects: only bindTo + outputFile matter to the engine; all other fields are
8
+ // passed verbatim to the board's task-executor (--in JSON). Users define their own
9
+ // shape (kind, url, mailbox, channel, model, ...) per executor.
10
+ // requires[] — upstream node IDs; engine subscribes automatically
11
+ // provides[] — [{ bindTo, src }] explicit downstream token bindings
6
12
  //
7
13
  // Uses Bootstrap 5 for layout/forms, optional Chart.js for charts.
8
14
  // Uses CardCompute (card-compute.js) for declarative compute expressions.
9
15
  //
10
16
  // API:
11
- // const engine = LiveCard.init({ resolve, onPatch, onPatchState, onRefresh, onChat, markdown, sanitize, chartLib });
17
+ // const engine = LiveCard.init({ resolve, onPatch, onPatchState, onRefresh, onAction, getChatMessages, markdown, sanitize, chartLib });
12
18
  // engine.render(node, el, opts?) — render a card node into a DOM element
13
19
  // engine.update(nodeId, patch) — in-place update (status, re-render)
14
20
  // engine.destroy(nodeId) — tear down one node
@@ -58,12 +64,27 @@ var LiveCard = (function () {
58
64
  .lc-staged-file { display:flex; align-items:center; gap:.5rem; padding:.125rem 0; }
59
65
  .lc-chat-el { display:flex; flex-direction:column; }
60
66
  .lc-chat-body { flex:1; overflow-y:auto; max-height:300px; padding:.25rem; }
61
- .lc-chat-bubble { padding:.375rem .625rem; margin:.25rem 0; border-radius:.75rem; max-width:85%; word-wrap:break-word; font-size:.875rem; }
67
+ .lc-chat-bubble { padding:.5rem .75rem; margin:.375rem 0; border-radius:.75rem; max-width:85%; word-wrap:break-word; font-size:.875rem; line-height:1.4; }
62
68
  .lc-chat-bubble-user { background:var(--bs-primary-bg-subtle,#cfe2ff); margin-left:auto; }
63
69
  .lc-chat-bubble-assistant { background:var(--bs-light,#f8f9fa); }
64
70
  .lc-chat-bubble-system { background:transparent; color:var(--bs-secondary,#6c757d); font-style:italic; text-align:center; max-width:100%; font-size:.8rem; }
71
+ .lc-chat-bubble-pending { opacity:.85; }
72
+ .lc-chat-bubble-pending .spinner-border { width:.75rem; height:.75rem; margin-left:.4rem; border-width:.12em; vertical-align:middle; }
65
73
  .lc-chat-input-bar { display:flex; gap:.25rem; align-items:center; }
74
+ .lc-chat-modal-input-row { display:flex; align-items:center; gap:.375rem; }
75
+ .lc-chat-modal-input-row .form-control { min-width:0; }
76
+ .lc-chat-modal-input-row textarea.form-control { resize:none; overflow-y:hidden; min-height:38px; max-height:120px; }
66
77
  .lc-chat-processing { display:flex; align-items:center; gap:.5rem; padding:.25rem .5rem; color:var(--bs-secondary,#6c757d); font-size:.8rem; }
78
+ .lc-chat-modal-backdrop { position:fixed; inset:0; background:rgba(0,0,0,.45); z-index:12000; display:none; align-items:center; justify-content:center; padding:1rem; }
79
+ .lc-chat-modal-backdrop.lc-open { display:flex; }
80
+ .lc-chat-modal-backdrop .modal-dialog { max-height:90vh; }
81
+ .lc-chat-modal-backdrop .modal-content { display:flex; flex-direction:column; max-height:90vh; }
82
+ .lc-chat-modal-backdrop .modal-body { overflow-y:auto; flex:1; min-height:200px; padding:1rem; }
83
+ .lc-files-modal-backdrop { position:fixed; inset:0; background:rgba(0,0,0,.45); z-index:11950; display:none; align-items:center; justify-content:center; padding:1rem; }
84
+ .lc-files-modal-backdrop.lc-open { display:flex; }
85
+ .lc-files-modal-backdrop .modal-dialog { max-height:90vh; }
86
+ .lc-files-modal-backdrop .modal-content { display:flex; flex-direction:column; max-height:90vh; }
87
+ .lc-files-modal-backdrop .modal-body { overflow-y:auto; flex:1; min-height:200px; padding:1rem; }
67
88
  @media (max-width:576px) {
68
89
  .lc-metric-value { font-size:1.5rem; }
69
90
  .lc-chart-wrap { min-height:150px; }
@@ -85,9 +106,15 @@ var LiveCard = (function () {
85
106
  return String(str).replace(/[&<>"']/g, ch => _escMap[ch]);
86
107
  }
87
108
 
109
+ function _pathParts(path) {
110
+ if (!path || typeof path !== 'string') return [];
111
+ // Support both dot notation (a.b.c) and bracket notation (a.b[0].c).
112
+ return path.replace(/\[(\d+)\]/g, '.$1').split('.').filter(Boolean);
113
+ }
114
+
88
115
  function _deepGet(obj, path) {
89
116
  if (!path || !obj) return undefined;
90
- const parts = path.split('.');
117
+ const parts = _pathParts(path);
91
118
  let cur = obj;
92
119
  for (let i = 0; i < parts.length; i++) {
93
120
  if (cur == null) return undefined;
@@ -97,7 +124,8 @@ var LiveCard = (function () {
97
124
  }
98
125
 
99
126
  function _deepSet(obj, path, value) {
100
- const parts = path.split('.');
127
+ const parts = _pathParts(path);
128
+ if (!parts.length) return;
101
129
  let cur = obj;
102
130
  for (let i = 0; i < parts.length - 1; i++) {
103
131
  if (cur[parts[i]] == null || typeof cur[parts[i]] !== 'object') cur[parts[i]] = {};
@@ -166,12 +194,42 @@ var LiveCard = (function () {
166
194
  sanitize: config.sanitize || null,
167
195
  chartLib: config.chartLib || null,
168
196
  onAction: config.onAction || function () {},
197
+ getChatMessages: config.getChatMessages || null,
169
198
  };
170
199
 
171
200
  const _cleanup = {}; // nodeId → { ac, timers, charts, unsubs }
172
201
  const _subs = {}; // nodeId → Set<callback>
173
202
  const _renderers = {}; // kind → fn
174
203
  const _nodeEls = {}; // nodeId → { container, resultEl, uid }
204
+ const _chatModal = {
205
+ backdrop: null,
206
+ title: null,
207
+ body: null,
208
+ input: null,
209
+ fileInput: null,
210
+ staged: null,
211
+ sendBtn: null,
212
+ attachBtn: null,
213
+ closeBtn: null,
214
+ currentNodeId: null,
215
+ stagedFiles: [],
216
+ loading: false,
217
+ };
218
+ const _filesModal = {
219
+ backdrop: null,
220
+ title: null,
221
+ body: null,
222
+ staged: null,
223
+ fileInput: null,
224
+ dropzone: null,
225
+ uploadBtn: null,
226
+ attachBtn: null,
227
+ closeBtn: null,
228
+ currentNodeId: null,
229
+ stagedFiles: [],
230
+ pollingTimer: null,
231
+ loading: false,
232
+ };
175
233
 
176
234
  // ---- Helpers ----
177
235
 
@@ -186,17 +244,466 @@ var LiveCard = (function () {
186
244
  return _cleanup[id];
187
245
  }
188
246
 
189
- function _runCompute(node) {
190
- if (!node.compute) return;
191
- if (typeof CardCompute !== 'undefined') {
192
- try { CardCompute.run(node); }
193
- catch (e) { console.error('LiveCard compute error', node.id, e); }
247
+ function _runCompute() {
248
+ // Runtime payload is authoritative; UI never recomputes derived values.
249
+ return Promise.resolve();
250
+ }
251
+
252
+ function _ensureChatModal() {
253
+ if (_chatModal.backdrop) return;
254
+
255
+ const backdrop = document.createElement('div');
256
+ backdrop.className = 'lc-chat-modal-backdrop';
257
+ backdrop.innerHTML = '' +
258
+ '<div class="modal-dialog modal-lg modal-dialog-centered" role="dialog" aria-modal="true" aria-label="Card chat">' +
259
+ ' <div class="modal-content bg-white">' +
260
+ ' <div class="modal-header border-bottom p-3 d-flex align-items-center justify-content-between">' +
261
+ ' <h5 class="modal-title lc-chat-modal-title">Chat</h5>' +
262
+ ' <button type="button" class="btn btn-sm btn-outline-secondary" data-lc-chat-close aria-label="Close"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg></button>' +
263
+ ' </div>' +
264
+ ' <div class="modal-body bg-light" data-lc-chat-body></div>' +
265
+ ' <div class="modal-footer flex-column align-items-stretch border-top p-3 gap-3">' +
266
+ ' <div data-lc-chat-staged class="small w-100"></div>' +
267
+ ' <input type="file" class="d-none" data-lc-chat-file multiple>' +
268
+ ' <div class="lc-chat-modal-input-row mt-2">' +
269
+ ' <button type="button" class="btn btn-sm btn-outline-secondary" data-lc-chat-attach title="Attach files" aria-label="Attach files">' +
270
+ ' <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21.44 11.05l-9.19 9.19a6 6 0 01-8.49-8.49l9.19-9.19a4 4 0 015.66 5.66l-9.2 9.19a2 2 0 01-2.83-2.83l8.49-8.48"/></svg>' +
271
+ ' </button>' +
272
+ ' <textarea class="form-control" data-lc-chat-input rows="1" placeholder="Type a message..."></textarea>' +
273
+ ' <button type="button" class="btn btn-sm btn-primary" data-lc-chat-send aria-label="Send">' +
274
+ ' <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="22" y1="2" x2="11" y2="13"/><polygon points="22 2 15 22 11 13 2 9 22 2"/></svg>' +
275
+ ' </button>' +
276
+ ' </div>' +
277
+ ' </div>' +
278
+ ' </div>' +
279
+ '</div>';
280
+
281
+ document.body.appendChild(backdrop);
282
+ _chatModal.backdrop = backdrop;
283
+ _chatModal.title = backdrop.querySelector('.lc-chat-modal-title');
284
+ _chatModal.body = backdrop.querySelector('[data-lc-chat-body]');
285
+ _chatModal.input = backdrop.querySelector('[data-lc-chat-input]');
286
+ _chatModal.fileInput = backdrop.querySelector('[data-lc-chat-file]');
287
+ _chatModal.staged = backdrop.querySelector('[data-lc-chat-staged]');
288
+ _chatModal.sendBtn = backdrop.querySelector('[data-lc-chat-send]');
289
+ _chatModal.attachBtn = backdrop.querySelector('[data-lc-chat-attach]');
290
+ _chatModal.closeBtn = backdrop.querySelector('[data-lc-chat-close]');
291
+
292
+ function resizeChatInput() {
293
+ if (!_chatModal.input) return;
294
+ _chatModal.input.style.height = 'auto';
295
+ _chatModal.input.style.height = Math.min(_chatModal.input.scrollHeight, 120) + 'px';
296
+ }
297
+
298
+ const close = function () {
299
+ _chatModal.currentNodeId = null;
300
+ _chatModal.stagedFiles = [];
301
+ _chatModal.staged.innerHTML = '';
302
+ _chatModal.input.value = '';
303
+ resizeChatInput();
304
+ _chatModal.backdrop.classList.remove('lc-open');
305
+ };
306
+
307
+ function renderStagedFiles() {
308
+ if (!_chatModal.stagedFiles.length) {
309
+ _chatModal.staged.innerHTML = '';
310
+ return;
311
+ }
312
+ _chatModal.staged.innerHTML = _chatModal.stagedFiles.map(function (f, i) {
313
+ return '<span class="badge text-bg-light border me-1 mb-1">' + _esc(f.name || 'file') +
314
+ ' <button type="button" class="btn btn-sm btn-link text-danger p-0 ms-1" data-lc-rm-file="' + i + '">&times;</button></span>';
315
+ }).join('');
316
+ _chatModal.staged.querySelectorAll('[data-lc-rm-file]').forEach(function (btn) {
317
+ btn.addEventListener('click', function () {
318
+ const idx = parseInt(btn.getAttribute('data-lc-rm-file') || '-1', 10);
319
+ if (idx >= 0) _chatModal.stagedFiles.splice(idx, 1);
320
+ renderStagedFiles();
321
+ });
322
+ });
323
+ }
324
+
325
+ async function sendMessage() {
326
+ if (_chatModal.loading || !_chatModal.currentNodeId) return;
327
+ const nodeId = _chatModal.currentNodeId;
328
+ const text = (_chatModal.input.value || '').trim();
329
+ const files = _chatModal.stagedFiles.slice();
330
+ if (!text && !files.length) return;
331
+
332
+ _chatModal.loading = true;
333
+ _chatModal.sendBtn.disabled = true;
334
+ _chatModal.attachBtn.disabled = true;
335
+
336
+ _appendPendingModalChatMessage(text);
337
+
338
+ _chatModal.input.value = '';
339
+ _chatModal.stagedFiles = [];
340
+ resizeChatInput();
341
+ renderStagedFiles();
342
+
343
+ try {
344
+ await Promise.resolve(cfg.onAction(nodeId, 'chat-send', { text, files }));
345
+ } catch (err) {
346
+ _clearPendingModalChatMessages();
347
+ _appendModalChatMessage('system', 'Failed to send message: ' + String((err && err.message) || err), []);
348
+ } finally {
349
+ _chatModal.loading = false;
350
+ _chatModal.sendBtn.disabled = false;
351
+ _chatModal.attachBtn.disabled = false;
352
+ }
353
+ }
354
+
355
+ _chatModal.closeBtn.addEventListener('click', close);
356
+ backdrop.addEventListener('click', function (evt) {
357
+ if (evt.target === backdrop) close();
358
+ });
359
+ _chatModal.attachBtn.addEventListener('click', function () {
360
+ _chatModal.fileInput.click();
361
+ });
362
+ _chatModal.fileInput.addEventListener('change', function (evt) {
363
+ const files = evt.target && evt.target.files ? Array.from(evt.target.files) : [];
364
+ for (const f of files) {
365
+ if (!_chatModal.stagedFiles.find(function (x) { return x.name === f.name && x.size === f.size && x.lastModified === f.lastModified; })) {
366
+ _chatModal.stagedFiles.push(f);
367
+ }
368
+ }
369
+ evt.target.value = '';
370
+ renderStagedFiles();
371
+ });
372
+ _chatModal.sendBtn.addEventListener('click', sendMessage);
373
+ _chatModal.input.addEventListener('input', resizeChatInput);
374
+ _chatModal.input.addEventListener('keydown', function (evt) {
375
+ if (evt.key === 'Enter' && !evt.shiftKey) {
376
+ evt.preventDefault();
377
+ sendMessage();
378
+ }
379
+ });
380
+ resizeChatInput();
381
+ document.addEventListener('keydown', function (evt) {
382
+ if (evt.key === 'Escape' && _chatModal.backdrop && _chatModal.backdrop.classList.contains('lc-open')) close();
383
+ });
384
+ }
385
+
386
+ function _normalizeChatMessages(rawMessages) {
387
+ const list = Array.isArray(rawMessages) ? rawMessages : [];
388
+ return list.map(function (msg) {
389
+ if (!msg || typeof msg !== 'object') return null;
390
+ const role = typeof msg.role === 'string' ? msg.role : 'system';
391
+ const text = typeof msg.text === 'string'
392
+ ? msg.text
393
+ : (typeof msg.message === 'string' ? msg.message : '');
394
+ const files = Array.isArray(msg.files) ? msg.files : [];
395
+ return { role: role.toLowerCase(), text, files };
396
+ }).filter(Boolean);
397
+ }
398
+
399
+ function _appendModalChatMessage(role, text, files) {
400
+ _ensureChatModal();
401
+ if (!_chatModal.body) return;
402
+
403
+ const bubble = document.createElement('div');
404
+ const normalizedRole = role === 'user' || role === 'assistant' ? role : 'system';
405
+ const roleClass = normalizedRole === 'user'
406
+ ? 'lc-chat-bubble-user'
407
+ : (normalizedRole === 'assistant' ? 'lc-chat-bubble-assistant' : 'lc-chat-bubble-system');
408
+ bubble.className = 'lc-chat-bubble ' + roleClass;
409
+ bubble.textContent = text || '';
410
+
411
+ if (Array.isArray(files) && files.length) {
412
+ const meta = document.createElement('div');
413
+ meta.className = 'lc-chat-inline-meta';
414
+ meta.textContent = files.map(function (f) {
415
+ if (!f) return 'file';
416
+ return typeof f === 'string' ? f : (f.name || 'file');
417
+ }).join(', ');
418
+ bubble.appendChild(meta);
419
+ }
420
+
421
+ _chatModal.body.appendChild(bubble);
422
+ _chatModal.body.scrollTop = _chatModal.body.scrollHeight;
423
+ }
424
+
425
+ function _appendPendingModalChatMessage(text) {
426
+ _ensureChatModal();
427
+ if (!_chatModal.body) return;
428
+
429
+ const bubble = document.createElement('div');
430
+ bubble.className = 'lc-chat-bubble lc-chat-bubble-user lc-chat-bubble-pending';
431
+ bubble.setAttribute('data-lc-chat-pending', '1');
432
+ bubble.textContent = text || '';
433
+
434
+ const spinner = document.createElement('span');
435
+ spinner.className = 'spinner-border spinner-border-sm';
436
+ spinner.setAttribute('role', 'status');
437
+ spinner.setAttribute('aria-label', 'Sending');
438
+ bubble.appendChild(spinner);
439
+
440
+ _chatModal.body.appendChild(bubble);
441
+ _chatModal.body.scrollTop = _chatModal.body.scrollHeight;
442
+ }
443
+
444
+ function _clearPendingModalChatMessages() {
445
+ if (!_chatModal.body) return;
446
+ _chatModal.body.querySelectorAll('[data-lc-chat-pending="1"]').forEach(function (el) {
447
+ if (el && el.parentNode) el.parentNode.removeChild(el);
448
+ });
449
+ }
450
+
451
+ async function _refreshModalChatHistory(nodeId) {
452
+ if (_chatModal.currentNodeId !== nodeId) return;
453
+
454
+ const node = cfg.resolve(nodeId);
455
+ let messages = [];
456
+ if (typeof cfg.getChatMessages === 'function') {
457
+ try {
458
+ messages = await Promise.resolve(cfg.getChatMessages(nodeId));
459
+ } catch {
460
+ messages = [];
461
+ }
462
+ } else if (node && node.card_data && Array.isArray(node.card_data.messages)) {
463
+ messages = node.card_data.messages;
464
+ }
465
+
466
+ const normalized = _normalizeChatMessages(messages);
467
+ _chatModal.body.innerHTML = '';
468
+ if (!normalized.length) {
469
+ _chatModal.body.innerHTML = '<div class="text-muted small">No messages yet.</div>';
470
+ return;
471
+ }
472
+ normalized.forEach(function (m) { _appendModalChatMessage(m.role, m.text, m.files); });
473
+ }
474
+
475
+ async function openChatModal(nodeId) {
476
+ _ensureChatModal();
477
+ const node = cfg.resolve(nodeId);
478
+ if (!node) return;
479
+ const title = (node.card && node.card.meta && node.card.meta.title) || node.id;
480
+ _chatModal.currentNodeId = nodeId;
481
+ _chatModal.title.textContent = 'Chat: ' + title;
482
+ _chatModal.body.innerHTML = '<div class="text-muted small">Loading...</div>';
483
+ _chatModal.backdrop.classList.add('lc-open');
484
+
485
+ // Disable input controls when card_data.features.chat.disabled is true
486
+ const chatDisabled = !!(node.card_data && node.card_data.features && node.card_data.features.chat && node.card_data.features.chat.disabled);
487
+ _chatModal.input.disabled = chatDisabled;
488
+ _chatModal.attachBtn.disabled = chatDisabled;
489
+ _chatModal.sendBtn.disabled = chatDisabled;
490
+ _chatModal.input.placeholder = chatDisabled ? 'Chat is disabled for this card.' : 'Type a message...';
491
+
492
+ if (!chatDisabled) _chatModal.input.focus();
493
+ await _refreshModalChatHistory(nodeId);
494
+ }
495
+
496
+ function _ensureFilesModal() {
497
+ if (_filesModal.backdrop) return;
498
+
499
+ const backdrop = document.createElement('div');
500
+ backdrop.className = 'lc-files-modal-backdrop';
501
+ backdrop.innerHTML = '' +
502
+ '<div class="modal-dialog modal-lg modal-dialog-centered" role="dialog" aria-modal="true" aria-label="Card files">' +
503
+ ' <div class="modal-content bg-white">' +
504
+ ' <div class="modal-header border-bottom p-3 d-flex align-items-center justify-content-between">' +
505
+ ' <h5 class="modal-title lc-files-modal-title">Files</h5>' +
506
+ ' <button type="button" class="btn btn-sm btn-outline-secondary" data-lc-files-close aria-label="Close"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg></button>' +
507
+ ' </div>' +
508
+ ' <div class="modal-body bg-light" data-lc-files-body></div>' +
509
+ ' <div class="modal-footer flex-column align-items-stretch border-top p-3 gap-3">' +
510
+ ' <div class="lc-dropzone border-2 border-dashed p-4 text-center cursor-pointer rounded" data-lc-files-dz>' +
511
+ ' <div class="small text-muted mb-2">Drop files here or click to browse</div>' +
512
+ ' <input type="file" class="d-none" data-lc-files-input multiple>' +
513
+ ' </div>' +
514
+ ' <div data-lc-files-staged class="small w-100 d-flex flex-wrap gap-2"></div>' +
515
+ ' <div class="d-flex justify-content-end gap-2 w-100">' +
516
+ ' <button type="button" class="btn btn-sm btn-outline-secondary" data-lc-files-attach>Select files</button>' +
517
+ ' <button type="button" class="btn btn-sm btn-primary" data-lc-files-upload>Upload</button>' +
518
+ ' </div>' +
519
+ ' </div>' +
520
+ ' </div>' +
521
+ '</div>';
522
+
523
+ document.body.appendChild(backdrop);
524
+ _filesModal.backdrop = backdrop;
525
+ _filesModal.title = backdrop.querySelector('.lc-files-modal-title');
526
+ _filesModal.body = backdrop.querySelector('[data-lc-files-body]');
527
+ _filesModal.staged = backdrop.querySelector('[data-lc-files-staged]');
528
+ _filesModal.fileInput = backdrop.querySelector('[data-lc-files-input]');
529
+ _filesModal.dropzone = backdrop.querySelector('[data-lc-files-dz]');
530
+ _filesModal.uploadBtn = backdrop.querySelector('[data-lc-files-upload]');
531
+ _filesModal.attachBtn = backdrop.querySelector('[data-lc-files-attach]');
532
+ _filesModal.closeBtn = backdrop.querySelector('[data-lc-files-close]');
533
+
534
+ const close = function () {
535
+ _filesModal.currentNodeId = null;
536
+ _filesModal.stagedFiles = [];
537
+ _filesModal.staged.innerHTML = '';
538
+ _filesModal.backdrop.classList.remove('lc-open');
539
+ if (_filesModal.pollingTimer) {
540
+ clearInterval(_filesModal.pollingTimer);
541
+ _filesModal.pollingTimer = null;
542
+ }
543
+ };
544
+
545
+ function renderStagedFiles() {
546
+ if (!_filesModal.stagedFiles.length) {
547
+ _filesModal.staged.innerHTML = '';
548
+ return;
549
+ }
550
+ _filesModal.staged.innerHTML = _filesModal.stagedFiles.map(function (f, i) {
551
+ return '<span class="badge text-bg-light border me-1 mb-1">' + _esc(f.name || 'file') +
552
+ ' <button type="button" class="btn btn-sm btn-link text-danger p-0 ms-1" data-lc-files-rm="' + i + '">&times;</button></span>';
553
+ }).join('');
554
+ _filesModal.staged.querySelectorAll('[data-lc-files-rm]').forEach(function (btn) {
555
+ btn.addEventListener('click', function () {
556
+ const idx = parseInt(btn.getAttribute('data-lc-files-rm') || '-1', 10);
557
+ if (idx >= 0) _filesModal.stagedFiles.splice(idx, 1);
558
+ renderStagedFiles();
559
+ });
560
+ });
561
+ }
562
+
563
+ function addFiles(fileList) {
564
+ const files = Array.from(fileList || []);
565
+ for (const f of files) {
566
+ if (!_filesModal.stagedFiles.find(function (x) { return x.name === f.name && x.size === f.size && x.lastModified === f.lastModified; })) {
567
+ _filesModal.stagedFiles.push(f);
568
+ }
569
+ }
570
+ renderStagedFiles();
571
+ }
572
+
573
+ async function uploadFiles() {
574
+ if (_filesModal.loading || !_filesModal.currentNodeId || !_filesModal.stagedFiles.length) return;
575
+ const nodeId = _filesModal.currentNodeId;
576
+ const files = _filesModal.stagedFiles.slice();
577
+ _filesModal.loading = true;
578
+ _filesModal.uploadBtn.disabled = true;
579
+ _filesModal.attachBtn.disabled = true;
580
+ _filesModal.dropzone.classList.add('lc-disabled');
581
+
582
+ try {
583
+ await Promise.resolve(cfg.onAction(nodeId, 'file-upload', { files }));
584
+ _filesModal.stagedFiles = [];
585
+ renderStagedFiles();
586
+ _refreshFilesModalList(nodeId);
587
+ } catch (err) {
588
+ _filesModal.staged.innerHTML = '<span class="text-danger">Upload failed: ' + _esc(String((err && err.message) || err)) + '</span>';
589
+ } finally {
590
+ _filesModal.loading = false;
591
+ _filesModal.uploadBtn.disabled = false;
592
+ _filesModal.attachBtn.disabled = false;
593
+ _filesModal.dropzone.classList.remove('lc-disabled');
594
+ }
595
+ }
596
+
597
+ _filesModal.closeBtn.addEventListener('click', close);
598
+ backdrop.addEventListener('click', function (evt) {
599
+ if (evt.target === backdrop) close();
600
+ });
601
+ _filesModal.attachBtn.addEventListener('click', function () {
602
+ _filesModal.fileInput.click();
603
+ });
604
+ _filesModal.fileInput.addEventListener('change', function (evt) {
605
+ addFiles(evt.target && evt.target.files ? evt.target.files : []);
606
+ evt.target.value = '';
607
+ });
608
+ _filesModal.uploadBtn.addEventListener('click', uploadFiles);
609
+ _filesModal.dropzone.addEventListener('click', function () {
610
+ if (!_filesModal.loading) _filesModal.fileInput.click();
611
+ });
612
+ _filesModal.dropzone.addEventListener('dragover', function (evt) {
613
+ evt.preventDefault();
614
+ _filesModal.dropzone.classList.add('lc-drag-over');
615
+ });
616
+ _filesModal.dropzone.addEventListener('dragleave', function () {
617
+ _filesModal.dropzone.classList.remove('lc-drag-over');
618
+ });
619
+ _filesModal.dropzone.addEventListener('drop', function (evt) {
620
+ evt.preventDefault();
621
+ _filesModal.dropzone.classList.remove('lc-drag-over');
622
+ addFiles(evt.dataTransfer && evt.dataTransfer.files ? evt.dataTransfer.files : []);
623
+ });
624
+ document.addEventListener('keydown', function (evt) {
625
+ if (evt.key === 'Escape' && _filesModal.backdrop && _filesModal.backdrop.classList.contains('lc-open')) close();
626
+ });
627
+ }
628
+
629
+ function _currentNodeFiles(nodeId) {
630
+ const node = cfg.resolve(nodeId);
631
+ const files = node && node.card_data && Array.isArray(node.card_data.files) ? node.card_data.files : [];
632
+ return files.filter(Boolean);
633
+ }
634
+
635
+ function _refreshFilesModalList(nodeId) {
636
+ if (_filesModal.currentNodeId !== nodeId) return;
637
+ const files = _currentNodeFiles(nodeId);
638
+ if (!files.length) {
639
+ _filesModal.body.innerHTML = '<div class="alert alert-light border small mb-0">No files uploaded yet.</div>';
640
+ return;
194
641
  }
642
+
643
+ let h = '<div class="list-group list-group-flush">';
644
+ files.forEach(function (f, idx) {
645
+ const fileName = f && (f.name || f.stored_name) ? (f.name || f.stored_name) : 'file';
646
+ const sizeText = f && typeof f.size === 'number' ? ('size: ' + f.size + ' bytes') : '';
647
+ const stored = f && f.stored_name ? String(f.stored_name) : '';
648
+ const dl = stored
649
+ ? '/api/example-board/server/cards/' + encodeURIComponent(nodeId) + '/files/' + idx + '?sn=' + encodeURIComponent(stored)
650
+ : null;
651
+ h += '<div class="list-group-item d-flex align-items-center justify-content-between gap-2">';
652
+ h += '<div class="text-truncate"><div class="small fw-medium">' + _esc(fileName) + '</div>';
653
+ h += '<div class="small text-muted">' + _esc(sizeText) + '</div></div>';
654
+ if (dl) {
655
+ h += '<a class="btn btn-sm btn-outline-secondary flex-shrink-0" href="' + dl + '">Download</a>';
656
+ }
657
+ h += '</div>';
658
+ });
659
+ h += '</div>';
660
+ _filesModal.body.innerHTML = h;
661
+ }
662
+
663
+ function openFilesModal(nodeId) {
664
+ _ensureFilesModal();
665
+ const node = cfg.resolve(nodeId);
666
+ if (!node) return;
667
+
668
+ const title = (node.card && node.card.meta && node.card.meta.title) || node.id;
669
+ _filesModal.currentNodeId = nodeId;
670
+ _filesModal.title.textContent = 'Files: ' + title;
671
+ _filesModal.backdrop.classList.add('lc-open');
672
+
673
+ // Disable upload controls when card_data.features.files.disabled is true
674
+ const filesDisabled = !!(node.card_data && node.card_data.features && node.card_data.features.files && node.card_data.features.files.disabled);
675
+ _filesModal.dropzone.classList.toggle('lc-disabled', filesDisabled);
676
+ _filesModal.attachBtn.disabled = filesDisabled;
677
+ _filesModal.uploadBtn.disabled = filesDisabled;
678
+ _filesModal.fileInput.disabled = filesDisabled;
679
+
680
+ _refreshFilesModalList(nodeId);
681
+
682
+ if (_filesModal.pollingTimer) clearInterval(_filesModal.pollingTimer);
683
+ _filesModal.pollingTimer = setInterval(function () {
684
+ _refreshFilesModalList(nodeId);
685
+ }, 1000);
195
686
  }
196
687
 
197
688
  function _resolveBind(node, bind) {
198
- if (!bind) return undefined;
199
- return _deepGet(node, bind);
689
+ if (!bind || typeof bind !== 'string') return undefined;
690
+ const parts = _pathParts(bind);
691
+ if (!parts.length) return undefined;
692
+
693
+ const root = parts[0];
694
+ const rest = parts.slice(1).join('.');
695
+ const ns = {
696
+ card: node && node.card ? node.card : {},
697
+ card_data: node && node.card_data ? node.card_data : {},
698
+ fetched_sources: node && node.fetched_sources ? node.fetched_sources : {},
699
+ requires: node && node.requires ? node.requires : {},
700
+ computed_values: node && node.computed_values ? node.computed_values : {},
701
+ runtime_state: node && node.runtime_state ? node.runtime_state : {},
702
+ data_objects: node && node.data_objects ? node.data_objects : {},
703
+ };
704
+
705
+ if (!Object.prototype.hasOwnProperty.call(ns, root)) return undefined;
706
+ return rest ? _deepGet(ns[root], rest) : ns[root];
200
707
  }
201
708
 
202
709
  // ---- Pub/sub ----
@@ -213,7 +720,7 @@ var LiveCard = (function () {
213
720
  }
214
721
 
215
722
  function _autoSubscribe(node) {
216
- const requires = (node.data && node.data.requires) || [];
723
+ const requires = (node && node.card && Array.isArray(node.card.requires)) ? node.card.requires : [];
217
724
  if (!requires.length) return;
218
725
  const cleanup = _getCleanup(node.id);
219
726
  cleanup.unsubs = requires.map(upId => subscribe(upId, () => {
@@ -221,7 +728,6 @@ var LiveCard = (function () {
221
728
  if (!info || !info.resultEl) return;
222
729
  const updated = cfg.resolve(node.id);
223
730
  if (!updated) return;
224
- _runCompute(updated);
225
731
  _renderElements(updated, info.resultEl);
226
732
  notify(node.id);
227
733
  }));
@@ -664,7 +1170,30 @@ var LiveCard = (function () {
664
1170
 
665
1171
  function _renderText(data, el, elemDef) {
666
1172
  const ed = elemDef.data || {};
1173
+ const format = ed.format || 'default';
667
1174
  const style = ed.style || 'default';
1175
+
1176
+ // Handle file-links format
1177
+ if (format === 'file-links') {
1178
+ if (!Array.isArray(data) || data.length === 0) {
1179
+ el.innerHTML = '<div class="text-muted small">No files uploaded</div>';
1180
+ return;
1181
+ }
1182
+ const htmlParts = [];
1183
+ data.forEach((file, idx) => {
1184
+ if (!file || !file.stored_name) return;
1185
+ const name = file.name || file.stored_name;
1186
+ const cardId = elemDef.data && elemDef.data.cardId ? elemDef.data.cardId : 'unknown';
1187
+ const downloadUrl = `/api/example-board/server/cards/${encodeURIComponent(cardId)}/files/${idx}?sn=${encodeURIComponent(file.stored_name)}`;
1188
+ const size = file.size ? ` (${Math.round(file.size / 1024)}KB)` : '';
1189
+ htmlParts.push(`<div class="mb-2"><a href="${downloadUrl}" class="btn btn-sm btn-outline-secondary">${_esc(name)}${_esc(size)}</a></div>`);
1190
+ });
1191
+ const html = htmlParts.join('');
1192
+ el.innerHTML = html;
1193
+ return;
1194
+ }
1195
+
1196
+ // Default text rendering
668
1197
  const tag = style === 'heading' ? 'h4' : 'div';
669
1198
  const cls = style === 'muted' ? 'text-muted small' : (style === 'heading' ? 'fw-bold' : 'small');
670
1199
  el.innerHTML = `<${tag} class="${cls}">${_esc(data != null ? String(data) : '')}</${tag}>`;
@@ -694,6 +1223,7 @@ var LiveCard = (function () {
694
1223
  const signal = cleanup.ac.signal;
695
1224
  const ed = elemDef.data || {};
696
1225
  const uploaded = Array.isArray(data) ? data : [];
1226
+ const showUploadedList = ed.showUploadedList === true;
697
1227
  const showUpload = ed.upload !== false;
698
1228
  const accept = ed.accept || ['.txt','.csv','.md','.json','.html','.xml','.pdf','.xlsx','.docx','.pptx','.png','.jpg','.jpeg'];
699
1229
  const acceptSet = new Set(accept.map(e => e.toLowerCase()));
@@ -703,6 +1233,12 @@ var LiveCard = (function () {
703
1233
 
704
1234
  let stagedFiles = el._stagedFiles || [];
705
1235
  el._stagedFiles = stagedFiles;
1236
+ let uploadStatus = el._uploadStatus || {};
1237
+ el._uploadStatus = uploadStatus;
1238
+
1239
+ function keyForFile(f) {
1240
+ return `${f.name}::${f.size}::${f.lastModified || 0}`;
1241
+ }
706
1242
 
707
1243
  let h = '';
708
1244
 
@@ -717,7 +1253,7 @@ var LiveCard = (function () {
717
1253
  }
718
1254
 
719
1255
  // Uploaded files list
720
- if (uploaded.length) {
1256
+ if (showUploadedList && uploaded.length) {
721
1257
  h += '<div class="lc-uploaded-files">';
722
1258
  uploaded.forEach(f => {
723
1259
  const name = typeof f === 'string' ? f : (f.name || '');
@@ -742,36 +1278,68 @@ var LiveCard = (function () {
742
1278
  return;
743
1279
  }
744
1280
 
745
- const dz = document.getElementById(uid + '-dz');
746
- const fi = document.getElementById(uid + '-fi');
747
- const stagedEl = document.getElementById(uid + '-staged');
1281
+ const dz = el.querySelector('#' + uid + '-dz');
1282
+ const fi = el.querySelector('#' + uid + '-fi');
1283
+ const stagedEl = el.querySelector('#' + uid + '-staged');
748
1284
  if (!dz) return;
749
1285
 
750
1286
  function addFiles(fileList) {
1287
+ const newlyAdded = [];
751
1288
  for (const f of fileList) {
752
1289
  const ext = '.' + f.name.split('.').pop().toLowerCase();
753
1290
  if (!acceptSet.has(ext)) continue;
754
- if (!stagedFiles.find(s => s.name === f.name)) stagedFiles.push(f);
1291
+ if (!stagedFiles.find(s => s.name === f.name)) {
1292
+ stagedFiles.push(f);
1293
+ newlyAdded.push(f);
1294
+ uploadStatus[keyForFile(f)] = 'uploading';
1295
+ }
755
1296
  }
756
1297
  renderStaged();
757
- cfg.onPatchState(node.id, { _stagedFiles: stagedFiles.map(f => ({ name: f.name, size: f.size })) });
1298
+
1299
+ // Server demos can upload real file blobs immediately via onAction.
1300
+ if (newlyAdded.length && typeof cfg.onAction === 'function') {
1301
+ Promise.resolve(cfg.onAction(node.id, 'file-upload', { files: newlyAdded, elemId: elemDef.id }))
1302
+ .then(() => {
1303
+ const uploadedKeys = new Set(newlyAdded.map(keyForFile));
1304
+ stagedFiles = stagedFiles.filter((f) => !uploadedKeys.has(keyForFile(f)));
1305
+ el._stagedFiles = stagedFiles;
1306
+ newlyAdded.forEach((f) => { delete uploadStatus[keyForFile(f)]; });
1307
+ el._uploadStatus = uploadStatus;
1308
+ renderStaged();
1309
+ })
1310
+ .catch(() => {
1311
+ newlyAdded.forEach((f) => { uploadStatus[keyForFile(f)] = 'error'; });
1312
+ el._uploadStatus = uploadStatus;
1313
+ renderStaged();
1314
+ });
1315
+ }
758
1316
  }
759
1317
 
760
1318
  function renderStaged() {
761
1319
  if (!stagedFiles.length) { stagedEl.innerHTML = ''; return; }
762
1320
  let sh = '';
763
1321
  stagedFiles.forEach((f, i) => {
1322
+ const status = uploadStatus[keyForFile(f)] || 'ready';
764
1323
  sh += '<div class="lc-staged-file">';
765
1324
  sh += '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z"/><polyline points="14 2 14 8 20 8"/></svg>';
766
1325
  sh += `<span class="small flex-grow-1 text-truncate">${_esc(f.name)}</span>`;
1326
+ if (status === 'uploading') {
1327
+ sh += '<span class="spinner-border spinner-border-sm text-secondary me-1" role="status" aria-label="Uploading"></span>';
1328
+ } else if (status === 'error') {
1329
+ sh += '<span class="badge bg-danger-subtle text-danger border border-danger-subtle me-1">Failed</span>';
1330
+ }
767
1331
  sh += `<button class="btn btn-sm btn-link text-danger p-0 lc-rm-staged" data-idx="${i}">&times;</button>`;
768
1332
  sh += '</div>';
769
1333
  });
770
1334
  stagedEl.innerHTML = sh;
771
1335
  stagedEl.querySelectorAll('.lc-rm-staged').forEach(btn => {
772
1336
  btn.addEventListener('click', () => {
773
- stagedFiles.splice(parseInt(btn.dataset.idx), 1);
1337
+ const idx = parseInt(btn.dataset.idx);
1338
+ const f = stagedFiles[idx];
1339
+ if (f) delete uploadStatus[keyForFile(f)];
1340
+ stagedFiles.splice(idx, 1);
774
1341
  el._stagedFiles = stagedFiles;
1342
+ el._uploadStatus = uploadStatus;
775
1343
  renderStaged();
776
1344
  }, { signal });
777
1345
  });
@@ -787,7 +1355,7 @@ var LiveCard = (function () {
787
1355
 
788
1356
  el._fileUpload = {
789
1357
  getFiles: () => stagedFiles,
790
- clear: () => { stagedFiles = []; el._stagedFiles = []; renderStaged(); },
1358
+ clear: () => { stagedFiles = []; uploadStatus = {}; el._stagedFiles = []; el._uploadStatus = {}; renderStaged(); },
791
1359
  disable: () => { dz.classList.add('lc-disabled'); fi.disabled = true; },
792
1360
  enable: () => { dz.classList.remove('lc-disabled'); fi.disabled = false; },
793
1361
  };
@@ -823,12 +1391,12 @@ var LiveCard = (function () {
823
1391
 
824
1392
  el.innerHTML = h;
825
1393
 
826
- const body = document.getElementById(uid + '-body');
827
- const input = document.getElementById(uid + '-input');
828
- const sendBtn = document.getElementById(uid + '-send');
829
- const attachBtn = canAttach ? document.getElementById(uid + '-attach') : null;
830
- const fileInput = canAttach ? document.getElementById(uid + '-fi') : null;
831
- const stagedEl = canAttach ? document.getElementById(uid + '-staged') : null;
1394
+ const body = el.querySelector('#' + uid + '-body');
1395
+ const input = el.querySelector('#' + uid + '-input');
1396
+ const sendBtn = el.querySelector('#' + uid + '-send');
1397
+ const attachBtn = canAttach ? el.querySelector('#' + uid + '-attach') : null;
1398
+ const fileInput = canAttach ? el.querySelector('#' + uid + '-fi') : null;
1399
+ const stagedEl = canAttach ? el.querySelector('#' + uid + '-staged') : null;
832
1400
 
833
1401
  let stagedFiles = [];
834
1402
 
@@ -982,7 +1550,7 @@ var LiveCard = (function () {
982
1550
  // ===========================================================================
983
1551
 
984
1552
  function _renderElements(node, containerEl) {
985
- const view = node.view;
1553
+ const view = node && node.card ? node.card.view : null;
986
1554
  if (!view || !Array.isArray(view.elements)) { containerEl.innerHTML = ''; return; }
987
1555
 
988
1556
  if (_nodeEls[node.id]) _nodeEls[node.id].elements = {};
@@ -1040,27 +1608,40 @@ var LiveCard = (function () {
1040
1608
  const cleanup = _getCleanup(node.id);
1041
1609
  const signal = cleanup.ac.signal;
1042
1610
  const uid = 'lc-' + (node.id || 'x');
1043
- const features = (node.view && node.view.features) || {};
1611
+ const features = (node.card && node.card.view && node.card.view.features) || {};
1044
1612
 
1045
- // Run compute before render
1046
- _runCompute(node);
1613
+ // Run compute async before populating elements
1614
+ // (compute is triggered in the else branch below after DOM is ready)
1047
1615
 
1048
1616
  let h = `<div class="lc-card" id="${uid}">`;
1049
1617
 
1050
1618
  // Header bar: status dot + time-ago + refresh button
1051
1619
  const showRefresh = features.refresh !== false && cfg.onRefresh;
1052
- h += `<div class="d-flex align-items-center gap-2 mb-2">`;
1053
- h += _statusDot(node.state && node.state.status);
1054
- h += `<span class="text-muted small">${_timeAgo(node.state && node.state.lastRun)}</span>`;
1055
- if (node.state && node.state.status === 'error' && node.state.error) {
1056
- h += `<span class="badge bg-danger small" title="${_esc(node.state.error)}">Error</span>`;
1620
+ h += `<div class="d-flex align-items-center gap-1 mb-2">`;
1621
+ h += _statusDot(node.card_data && node.card_data.status);
1622
+ h += `<span class="text-muted small">${_timeAgo(node.card_data && node.card_data.lastRun)}</span>`;
1623
+ if (node.card_data && node.card_data.status === 'error' && node.card_data.error) {
1624
+ h += `<span class="badge bg-danger small" title="${_esc(node.card_data.error)}">Error</span>`;
1057
1625
  }
1626
+ h += '<div class="d-flex align-items-center gap-1 ms-auto">';
1627
+ const filesCount = (node && node.card_data && Array.isArray(node.card_data.files)) ? node.card_data.files.length : 0;
1628
+ // Files icon button (paperclip)
1629
+ h += `<button class="btn btn-sm btn-outline-secondary d-inline-flex align-items-center" id="${uid}-files-open" title="${filesCount > 0 ? 'Files (' + filesCount + ')' : 'Files'}">`;
1630
+ h += '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21.44 11.05l-9.19 9.19a6 6 0 01-8.49-8.49l9.19-9.19a4 4 0 015.66 5.66l-9.2 9.19a2 2 0 01-2.83-2.83l8.49-8.48"/></svg>';
1631
+ if (filesCount > 0) h += `<span class="ms-1 small" aria-label="${filesCount} files">${filesCount}</span>`;
1632
+ h += '</button>';
1633
+ // Chat icon button (speech bubble)
1634
+ h += `<button class="btn btn-sm btn-outline-secondary" id="${uid}-chat-open" title="Chat">`;
1635
+ h += '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15a2 2 0 01-2 2H7l-4 4V5a2 2 0 012-2h14a2 2 0 012 2z"/></svg>';
1636
+ h += '</button>';
1637
+ // Refresh icon button
1058
1638
  if (showRefresh) {
1059
- h += `<button class="btn btn-sm btn-outline-secondary ms-auto" id="${uid}-refresh" title="Refresh">`;
1639
+ h += `<button class="btn btn-sm btn-outline-secondary" id="${uid}-refresh" title="Refresh">`;
1060
1640
  h += '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="23 4 23 10 17 10"/><path d="M20.49 15a9 9 0 11-2.12-9.36L23 10"/></svg>';
1061
1641
  h += '</button>';
1062
1642
  }
1063
1643
  h += '</div>';
1644
+ h += '</div>';
1064
1645
 
1065
1646
  // Elements area
1066
1647
  h += `<div class="lc-result" id="${uid}-result"></div>`;
@@ -1068,18 +1649,7 @@ var LiveCard = (function () {
1068
1649
  // Notes section (feature toggle)
1069
1650
  if (features.notes && opts.showNotes !== false) {
1070
1651
  h += `<details class="mt-2"><summary class="small fw-medium">Notes</summary>`;
1071
- h += `<textarea class="form-control form-control-sm mt-1" id="${uid}-notes" rows="3" placeholder="Add notes...">${_esc((node.state && node.state._notes) || '')}</textarea></details>`;
1072
- }
1073
-
1074
- // Chat section (feature toggle)
1075
- if (features.chat && cfg.onChat && opts.showChat !== false) {
1076
- h += `<details class="mt-2"><summary class="small fw-medium">Chat</summary>`;
1077
- h += `<div class="lc-chat-messages" id="${uid}-chat"></div>`;
1078
- h += `<div class="input-group input-group-sm mt-1">`;
1079
- h += `<input type="text" class="form-control" id="${uid}-chatInput" placeholder="Ask about this card...">`;
1080
- h += `<button class="btn btn-outline-primary" id="${uid}-chatSend">`;
1081
- h += '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="22" y1="2" x2="11" y2="13"/><polygon points="22 2 15 22 11 13 2 9 22 2"/></svg>';
1082
- h += '</button></div></details>';
1652
+ h += `<textarea class="form-control form-control-sm mt-1" id="${uid}-notes" rows="3" placeholder="Add notes...">${_esc((node.card_data && node.card_data._notes) || '')}</textarea></details>`;
1083
1653
  }
1084
1654
 
1085
1655
  h += '</div>';
@@ -1089,12 +1659,12 @@ var LiveCard = (function () {
1089
1659
  const resultEl = document.getElementById(uid + '-result');
1090
1660
  _nodeEls[node.id] = { container: containerEl, resultEl, uid };
1091
1661
 
1092
- if (node.state && node.state.status === 'loading') {
1662
+ if (node.card_data && node.card_data.status === 'loading') {
1093
1663
  resultEl.innerHTML = '<div class="d-flex align-items-center gap-2"><span class="spinner-border spinner-border-sm text-muted"></span><span class="text-muted small">Loading…</span></div>';
1094
- } else if (node.state && node.state.status === 'error' && node.state.error) {
1095
- resultEl.innerHTML = `<div class="text-danger small fw-semibold">Refresh failed</div><pre class="text-muted small mt-1" style="white-space:pre-wrap">${_esc(node.state.error)}</pre>`;
1664
+ } else if (node.card_data && node.card_data.status === 'error' && node.card_data.error) {
1665
+ resultEl.innerHTML = `<div class="text-danger small fw-semibold">Refresh failed</div><pre class="text-muted small mt-1" style="white-space:pre-wrap">${_esc(node.card_data.error)}</pre>`;
1096
1666
  } else {
1097
- _renderElements(node, resultEl);
1667
+ _runCompute(node).then(function () { _renderElements(node, resultEl); });
1098
1668
  }
1099
1669
 
1100
1670
  // ---- Wire refresh ----
@@ -1107,6 +1677,22 @@ var LiveCard = (function () {
1107
1677
  }, { signal });
1108
1678
  }
1109
1679
 
1680
+ const chatBtn = document.getElementById(uid + '-chat-open');
1681
+ if (chatBtn) {
1682
+ chatBtn.addEventListener('click', (e) => {
1683
+ e.stopPropagation();
1684
+ openChatModal(node.id);
1685
+ }, { signal });
1686
+ }
1687
+
1688
+ const filesBtn = document.getElementById(uid + '-files-open');
1689
+ if (filesBtn) {
1690
+ filesBtn.addEventListener('click', (e) => {
1691
+ e.stopPropagation();
1692
+ openFilesModal(node.id);
1693
+ }, { signal });
1694
+ }
1695
+
1110
1696
  // ---- Wire notes ----
1111
1697
  const notesEl = document.getElementById(uid + '-notes');
1112
1698
  if (notesEl) {
@@ -1114,31 +1700,14 @@ var LiveCard = (function () {
1114
1700
  notesEl.addEventListener('input', () => {
1115
1701
  clearTimeout(nTimer);
1116
1702
  nTimer = setTimeout(() => {
1117
- if (!node.state) node.state = {};
1118
- node.state._notes = notesEl.value;
1703
+ if (!node.card_data) node.card_data = {};
1704
+ node.card_data._notes = notesEl.value;
1119
1705
  cfg.onPatch(node.id, { _notes: notesEl.value });
1120
1706
  }, 800);
1121
1707
  cleanup.timers.push(nTimer);
1122
1708
  }, { signal });
1123
1709
  }
1124
1710
 
1125
- // ---- Wire chat ----
1126
- const chatInput = document.getElementById(uid + '-chatInput');
1127
- const chatSend = document.getElementById(uid + '-chatSend');
1128
- if (chatInput && chatSend && cfg.onChat) {
1129
- const send = () => {
1130
- const msg = chatInput.value.trim();
1131
- if (!msg) return;
1132
- chatInput.value = '';
1133
- appendChatMessage(node.id, 'user', msg);
1134
- cfg.onChat(node.id, msg);
1135
- };
1136
- chatSend.addEventListener('click', send, { signal });
1137
- chatInput.addEventListener('keydown', e => {
1138
- if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); send(); }
1139
- }, { signal });
1140
- }
1141
-
1142
1711
  _autoSubscribe(node);
1143
1712
  }
1144
1713
 
@@ -1168,21 +1737,33 @@ var LiveCard = (function () {
1168
1737
  if (ts) ts.textContent = _timeAgo(patch.lastRun);
1169
1738
  }
1170
1739
 
1171
- // Merge into node state
1740
+ // Merge into node card_data
1172
1741
  const node = cfg.resolve(nodeId);
1173
1742
  if (!node) return;
1174
- if (!node.state) node.state = {};
1175
- if (patch.status) node.state.status = patch.status;
1176
- if (patch.lastRun) node.state.lastRun = patch.lastRun;
1177
- if (patch.error !== undefined) node.state.error = patch.error;
1743
+ if (!node.card_data) node.card_data = {};
1744
+ if (patch.status) node.card_data.status = patch.status;
1745
+ if (patch.lastRun) node.card_data.lastRun = patch.lastRun;
1746
+ if (patch.error !== undefined) node.card_data.error = patch.error;
1747
+ if (patch.files !== undefined) node.card_data.files = Array.isArray(patch.files) ? patch.files : [];
1748
+
1749
+ // Keep files count inline inside the files button in the header.
1750
+ const filesBtn = document.getElementById(info.uid + '-files-open');
1751
+ const fileCount = Array.isArray(node.card_data.files) ? node.card_data.files.length : 0;
1752
+ if (filesBtn) {
1753
+ filesBtn.title = fileCount > 0 ? ('Files (' + fileCount + ')') : 'Files';
1754
+ filesBtn.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21.44 11.05l-9.19 9.19a6 6 0 01-8.49-8.49l9.19-9.19a4 4 0 015.66 5.66l-9.2 9.19a2 2 0 01-2.83-2.83l8.49-8.48"/></svg>' + (fileCount > 0 ? ('<span class="ms-1 small" aria-label="' + fileCount + ' files">' + fileCount + '</span>') : '');
1755
+ }
1756
+
1757
+ // Remove legacy external count label if present from older renders.
1758
+ const filesCountEl = document.getElementById(info.uid + '-files-count');
1759
+ if (filesCountEl && filesCountEl.parentNode) filesCountEl.parentNode.removeChild(filesCountEl);
1178
1760
 
1179
- if (node.state.status === 'loading') {
1761
+ if (node.card_data.status === 'loading') {
1180
1762
  info.resultEl.innerHTML = '<div class="d-flex align-items-center gap-2"><span class="spinner-border spinner-border-sm text-muted"></span><span class="text-muted small">Loading…</span></div>';
1181
- } else if (node.state.status === 'error' && node.state.error) {
1182
- info.resultEl.innerHTML = `<div class="text-danger small fw-semibold">Refresh failed</div><pre class="text-muted small mt-1" style="white-space:pre-wrap">${_esc(node.state.error)}</pre>`;
1763
+ } else if (node.card_data.status === 'error' && node.card_data.error) {
1764
+ info.resultEl.innerHTML = `<div class="text-danger small fw-semibold">Refresh failed</div><pre class="text-muted small mt-1" style="white-space:pre-wrap">${_esc(node.card_data.error)}</pre>`;
1183
1765
  } else {
1184
- _runCompute(node);
1185
- _renderElements(node, info.resultEl);
1766
+ _runCompute(node).then(function () { _renderElements(node, info.resultEl); });
1186
1767
  }
1187
1768
  }
1188
1769
 
@@ -1211,15 +1792,21 @@ var LiveCard = (function () {
1211
1792
  // ===========================================================================
1212
1793
 
1213
1794
  function appendChatMessage(nodeId, role, text) {
1214
- const info = _nodeEls[nodeId];
1215
- if (!info) return;
1216
- const chatEl = info.container.querySelector('.lc-chat-messages');
1217
- if (!chatEl) return;
1218
- const msg = document.createElement('div');
1219
- msg.className = `lc-chat-msg small ${role === 'user' ? 'lc-chat-user' : 'lc-chat-assistant'}`;
1220
- msg.innerHTML = role === 'assistant' ? _renderMd(text) : _esc(text);
1221
- chatEl.appendChild(msg);
1222
- chatEl.scrollTop = chatEl.scrollHeight;
1795
+ if (_chatModal.currentNodeId !== nodeId) return;
1796
+ _appendModalChatMessage(role, text, []);
1797
+ }
1798
+
1799
+ function refreshOpenChatModal() {
1800
+ const nodeId = _chatModal.currentNodeId;
1801
+ if (!nodeId || !_chatModal.backdrop || !_chatModal.backdrop.classList.contains('lc-open')) return;
1802
+ _refreshModalChatHistory(nodeId).catch(function () {});
1803
+ }
1804
+
1805
+ function onServerSseEvent() {
1806
+ const nodeId = _chatModal.currentNodeId;
1807
+ if (!nodeId || !_chatModal.backdrop || !_chatModal.backdrop.classList.contains('lc-open')) return;
1808
+ _clearPendingModalChatMessages();
1809
+ _refreshModalChatHistory(nodeId).catch(function () {});
1223
1810
  }
1224
1811
 
1225
1812
  // ===========================================================================
@@ -1243,6 +1830,10 @@ var LiveCard = (function () {
1243
1830
  notify,
1244
1831
  subscribe,
1245
1832
  appendChatMessage,
1833
+ refreshOpenChatModal,
1834
+ onServerSseEvent,
1835
+ openChatModal,
1836
+ openFilesModal,
1246
1837
  getElement,
1247
1838
  registerRenderer(name, fn) { _renderers[name] = fn; },
1248
1839
  renderers: _renderers,
@@ -1256,6 +1847,7 @@ var LiveCard = (function () {
1256
1847
  function Board(engine, containerEl, opts) {
1257
1848
  opts = opts || {};
1258
1849
  const mode = { current: opts.mode || 'board' };
1850
+ const devMode = { current: opts.devMode || false };
1259
1851
  const nodeList = [];
1260
1852
  const nodeMap = {}; // id → { node, colEl, bodyEl }
1261
1853
  const _positions = {}; // id → { x, y, w, h } for canvas mode
@@ -1329,7 +1921,8 @@ var LiveCard = (function () {
1329
1921
  // ---- Helpers ----
1330
1922
 
1331
1923
  function _colWidth(node) {
1332
- if (node.view && node.view.layout && node.view.layout.board && node.view.layout.board.col) return node.view.layout.board.col;
1924
+ const view = node && node.card ? node.card.view : null;
1925
+ if (view && view.layout && view.layout.board && view.layout.board.col) return view.layout.board.col;
1333
1926
  return defaultCol;
1334
1927
  }
1335
1928
 
@@ -1339,8 +1932,8 @@ var LiveCard = (function () {
1339
1932
  if (_positions[node.id]) return; // already set
1340
1933
  if (explicit[node.id]) {
1341
1934
  _positions[node.id] = Object.assign({}, explicit[node.id]);
1342
- } else if (node.view && node.view.layout && node.view.layout.canvas && node.view.layout.canvas.x != null) {
1343
- _positions[node.id] = Object.assign({}, node.view.layout.canvas);
1935
+ } else if (node.card && node.card.view && node.card.view.layout && node.card.view.layout.canvas && node.card.view.layout.canvas.x != null) {
1936
+ _positions[node.id] = Object.assign({}, node.card.view.layout.canvas);
1344
1937
  } else {
1345
1938
  const col = (i % 4);
1346
1939
  const row = Math.floor(i / 4);
@@ -1350,7 +1943,142 @@ var LiveCard = (function () {
1350
1943
  }
1351
1944
 
1352
1945
  function _getRequires(node) {
1353
- return (node.data && node.data.requires) || [];
1946
+ return (node && node.card && Array.isArray(node.card.requires)) ? node.card.requires : [];
1947
+ }
1948
+
1949
+ function _showCardInspector(node) {
1950
+ const modal = document.createElement('div');
1951
+ modal.className = 'modal d-block';
1952
+ modal.style.cssText = 'position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.5); z-index: 10000; display: flex; align-items: center; justify-content: center;';
1953
+
1954
+ const dialog = document.createElement('div');
1955
+ dialog.className = 'modal-dialog';
1956
+ dialog.style.cssText = 'width: 92%; max-width: 980px; max-height: 88vh; overflow: auto;';
1957
+
1958
+ const content = document.createElement('div');
1959
+ content.className = 'modal-content';
1960
+
1961
+ const header = document.createElement('div');
1962
+ header.className = 'modal-header';
1963
+ header.innerHTML = `<h5 class="modal-title">Card Inspector: ${_esc((node.card && node.card.meta && node.card.meta.title) || node.id)}</h5><button type="button" class="btn-close" aria-label="Close"></button>`;
1964
+
1965
+ const closeModal = function () { modal.remove(); };
1966
+ header.querySelector('.btn-close').addEventListener('click', closeModal);
1967
+
1968
+ const body = document.createElement('div');
1969
+ body.className = 'modal-body';
1970
+ body.style.cssText = 'max-height: 64vh; overflow-y: auto;';
1971
+
1972
+ const cardSection = document.createElement('div');
1973
+ cardSection.className = 'mb-4';
1974
+ cardSection.innerHTML = '<h6 class="fw-semibold mb-2">Card Object (Editable)</h6>';
1975
+
1976
+ const editableCardObject = JSON.parse(JSON.stringify((node && node.card) ? node.card : {}));
1977
+
1978
+ const editor = document.createElement('textarea');
1979
+ editor.className = 'form-control form-control-sm font-monospace';
1980
+ editor.rows = 16;
1981
+ editor.style.whiteSpace = 'pre';
1982
+ editor.value = JSON.stringify(editableCardObject, null, 2);
1983
+
1984
+ const editorHint = document.createElement('div');
1985
+ editorHint.className = 'small text-muted mt-2';
1986
+ editorHint.textContent = 'Edit JSON and click Submit to apply updates to this card.';
1987
+
1988
+ const editorError = document.createElement('div');
1989
+ editorError.className = 'small text-danger mt-1 d-none';
1990
+
1991
+ const submitBtn = document.createElement('button');
1992
+ submitBtn.type = 'button';
1993
+ submitBtn.className = 'btn btn-primary btn-sm mb-2';
1994
+ submitBtn.textContent = 'Submit';
1995
+
1996
+ cardSection.appendChild(submitBtn);
1997
+ cardSection.appendChild(editor);
1998
+ cardSection.appendChild(editorHint);
1999
+ cardSection.appendChild(editorError);
2000
+ body.appendChild(cardSection);
2001
+
2002
+ const computedSection = document.createElement('div');
2003
+ computedSection.className = 'mb-4';
2004
+ computedSection.innerHTML = '<h6 class="fw-semibold mb-2">Computed Values (Read-only)</h6>';
2005
+ const computedValues = node.computed_values || {};
2006
+ computedSection.innerHTML += `<pre style="background: #f5f5f5; padding: 10px; border-radius: 4px; overflow-x: auto; font-size: 12px; white-space: pre-wrap; word-wrap: break-word;">${_esc(JSON.stringify(computedValues, null, 2))}</pre>`;
2007
+ body.appendChild(computedSection);
2008
+
2009
+ const sourcesSection = document.createElement('div');
2010
+ sourcesSection.className = 'mb-4';
2011
+ sourcesSection.innerHTML = '<h6 class="fw-semibold mb-2">Fetched Sources (Read-only)</h6>';
2012
+ const sourcesData = node.fetched_sources || {};
2013
+ sourcesSection.innerHTML += `<pre style="background: #f5f5f5; padding: 10px; border-radius: 4px; overflow-x: auto; font-size: 12px; white-space: pre-wrap; word-wrap: break-word;">${_esc(JSON.stringify(sourcesData, null, 2))}</pre>`;
2014
+ body.appendChild(sourcesSection);
2015
+
2016
+ const requiresSection = document.createElement('div');
2017
+ requiresSection.className = 'mb-4';
2018
+ requiresSection.innerHTML = '<h6 class="fw-semibold mb-2">Requires (Read-only)</h6>';
2019
+ const requiresData = node.requires || {};
2020
+ requiresSection.innerHTML += `<pre style="background: #f5f5f5; padding: 10px; border-radius: 4px; overflow-x: auto; font-size: 12px; white-space: pre-wrap; word-wrap: break-word;">${_esc(JSON.stringify(requiresData, null, 2))}</pre>`;
2021
+ body.appendChild(requiresSection);
2022
+
2023
+ const stateSection = document.createElement('div');
2024
+ stateSection.className = 'mb-2';
2025
+ stateSection.innerHTML = '<h6 class="fw-semibold mb-2">Runtime Status (Read-only)</h6>';
2026
+ const runtimeState = { status: node.card_data && node.card_data.status, lastRun: node.card_data && node.card_data.lastRun, error: node.card_data && node.card_data.error };
2027
+ stateSection.innerHTML += `<pre style="background: #f5f5f5; padding: 10px; border-radius: 4px; overflow-x: auto; font-size: 12px; white-space: pre-wrap; word-wrap: break-word;">${_esc(JSON.stringify(runtimeState, null, 2))}</pre>`;
2028
+ body.appendChild(stateSection);
2029
+
2030
+ const footer = document.createElement('div');
2031
+ footer.className = 'modal-footer';
2032
+ const closeBtn = document.createElement('button');
2033
+ closeBtn.type = 'button';
2034
+ closeBtn.className = 'btn btn-secondary';
2035
+ closeBtn.textContent = 'Close';
2036
+ closeBtn.addEventListener('click', closeModal);
2037
+
2038
+ submitBtn.addEventListener('click', function () {
2039
+ editorError.classList.add('d-none');
2040
+ editorError.textContent = '';
2041
+ try {
2042
+ const parsed = JSON.parse(editor.value);
2043
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
2044
+ throw new Error('Card Object must be a JSON object.');
2045
+ }
2046
+ if (parsed.id && parsed.id !== node.id) {
2047
+ throw new Error('Changing card id is not supported in the inspector.');
2048
+ }
2049
+
2050
+ const fixedId = node.id;
2051
+ const preservedRuntime = {
2052
+ card_data: node.card_data,
2053
+ fetched_sources: node.fetched_sources,
2054
+ requires: node.requires,
2055
+ computed_values: node.computed_values,
2056
+ runtime_state: node.runtime_state,
2057
+ data_objects: node.data_objects,
2058
+ };
2059
+ node.card = parsed;
2060
+ node.id = fixedId;
2061
+ Object.assign(node, preservedRuntime);
2062
+
2063
+ engine.notify(node.id, { inspector: 'card-object-updated' });
2064
+ _render();
2065
+
2066
+ submitBtn.textContent = '✓ Saved';
2067
+ setTimeout(function () { submitBtn.textContent = 'Submit'; }, 1200);
2068
+ closeModal();
2069
+ } catch (err) {
2070
+ editorError.textContent = 'Invalid JSON: ' + String((err && err.message) || err);
2071
+ editorError.classList.remove('d-none');
2072
+ }
2073
+ });
2074
+
2075
+ footer.appendChild(closeBtn);
2076
+ content.appendChild(header);
2077
+ content.appendChild(body);
2078
+ content.appendChild(footer);
2079
+ dialog.appendChild(content);
2080
+ modal.appendChild(dialog);
2081
+ document.body.appendChild(modal);
1354
2082
  }
1355
2083
 
1356
2084
  function _buildCardWrapper(node) {
@@ -1358,15 +2086,32 @@ var LiveCard = (function () {
1358
2086
  wrap.className = 'card shadow-sm h-100';
1359
2087
  const header = document.createElement('div');
1360
2088
  header.className = 'card-header d-flex align-items-center gap-2 py-2';
1361
- const title = (node.meta && node.meta.title) || node.id;
1362
- const tags = (node.meta && node.meta.tags) || [];
2089
+ const card = node && node.card ? node.card : {};
2090
+ const title = (card.meta && card.meta.title) || node.id;
2091
+ const tags = (card.meta && card.meta.tags) || [];
1363
2092
  let badgeHtml = '';
1364
- if (node.type === 'source' && node.source) {
1365
- badgeHtml = '<span class="badge bg-info text-dark ms-auto">' + _esc(node.source.kind || 'source') + '</span>';
2093
+ if ((card.sources && card.sources.length) && !card.view) {
2094
+ var src = card.sources[0] || {};
2095
+ badgeHtml = '<span class="badge bg-info text-dark ms-auto">' + _esc(src.kind || 'source') + '</span>';
1366
2096
  } else if (tags.length) {
1367
2097
  badgeHtml = tags.map(t => '<span class="badge bg-secondary ms-1">' + _esc(t) + '</span>').join('');
1368
2098
  }
1369
2099
  header.innerHTML = '<strong class="small">' + _esc(title) + '</strong>' + badgeHtml;
2100
+
2101
+ // Add dev mode code icon button if devMode is enabled
2102
+ if (devMode.current) {
2103
+ const codeBtn = document.createElement('button');
2104
+ codeBtn.className = 'btn btn-sm btn-outline-secondary';
2105
+ codeBtn.style.cssText = 'padding: 2px 6px; margin-left: auto;';
2106
+ codeBtn.innerHTML = '&lt;/&gt;';
2107
+ codeBtn.title = 'Inspect card data';
2108
+ codeBtn.addEventListener('click', function(e) {
2109
+ e.stopPropagation();
2110
+ _showCardInspector(node);
2111
+ });
2112
+ header.appendChild(codeBtn);
2113
+ }
2114
+
1370
2115
  const body = document.createElement('div');
1371
2116
  body.className = 'card-body p-2';
1372
2117
  wrap.appendChild(header);
@@ -1377,9 +2122,10 @@ var LiveCard = (function () {
1377
2122
  function _buildSourcePill(node) {
1378
2123
  const el = document.createElement('div');
1379
2124
  el.className = 'lc-source-node';
1380
- const status = (node.state && node.state.status) || 'fresh';
1381
- const title = (node.meta && node.meta.title) || node.id;
1382
- const kind = (node.source && node.source.kind) || 'source';
2125
+ const status = (node.card_data && node.card_data.status) || 'fresh';
2126
+ const card = node && node.card ? node.card : {};
2127
+ const title = (card.meta && card.meta.title) || node.id;
2128
+ const kind = (card.sources && card.sources[0] && card.sources[0].kind) || 'source';
1383
2129
  el.innerHTML = `<div class="lc-source-pill shadow-sm">
1384
2130
  ${_statusDot(status)}
1385
2131
  <span class="fw-medium">${_esc(title)}</span>
@@ -1396,10 +2142,10 @@ var LiveCard = (function () {
1396
2142
  gridEl.innerHTML = '';
1397
2143
 
1398
2144
  // Only card nodes in board mode, sorted by order
1399
- const cards = nodeList.filter(n => n.type === 'card').slice();
2145
+ const cards = nodeList.filter(n => n.card && n.card.view).slice();
1400
2146
  cards.sort((a, b) => {
1401
- const ao = (a.view && a.view.layout && a.view.layout.board && a.view.layout.board.order) || 0;
1402
- const bo = (b.view && b.view.layout && b.view.layout.board && b.view.layout.board.order) || 0;
2147
+ const ao = (a.card && a.card.view && a.card.view.layout && a.card.view.layout.board && a.card.view.layout.board.order) || 0;
2148
+ const bo = (b.card && b.card.view && b.card.view.layout && b.card.view.layout.board && b.card.view.layout.board.order) || 0;
1403
2149
  return ao - bo;
1404
2150
  });
1405
2151
 
@@ -1477,11 +2223,11 @@ var LiveCard = (function () {
1477
2223
  el.style.left = x + 'px'; el.style.top = y + 'px';
1478
2224
  // Persist
1479
2225
  _positions[node.id] = Object.assign(_positions[node.id] || {}, { x, y });
1480
- if (node.type === 'card' && node.view) {
1481
- if (!node.view.layout) node.view.layout = {};
1482
- if (!node.view.layout.canvas) node.view.layout.canvas = {};
1483
- node.view.layout.canvas.x = x;
1484
- node.view.layout.canvas.y = y;
2226
+ if (node.card && node.card.view) {
2227
+ if (!node.card.view.layout) node.card.view.layout = {};
2228
+ if (!node.card.view.layout.canvas) node.card.view.layout.canvas = {};
2229
+ node.card.view.layout.canvas.x = x;
2230
+ node.card.view.layout.canvas.y = y;
1485
2231
  }
1486
2232
  engine.notify(node.id);
1487
2233
  _drawEdges();
@@ -1499,7 +2245,7 @@ var LiveCard = (function () {
1499
2245
  nodeList.forEach(node => {
1500
2246
  const pos = _positions[node.id] || { x: 0, y: 0 };
1501
2247
 
1502
- if (node.type === 'source') {
2248
+ if ((!node.card || !node.card.view) && (node.card && node.card.sources && node.card.sources.length)) {
1503
2249
  const el = _buildSourcePill(node);
1504
2250
  el.dataset.nodeId = node.id;
1505
2251
  el.style.left = pos.x + 'px';
@@ -1596,7 +2342,7 @@ var LiveCard = (function () {
1596
2342
  w: (_positions[n.id] && _positions[n.id].w) || cvs.defaultW,
1597
2343
  };
1598
2344
  // Sync to card nodes
1599
- if (n.type === 'card' && n.view) {
2345
+ if (n.view) {
1600
2346
  if (!n.view.layout) n.view.layout = {};
1601
2347
  n.view.layout.canvas = Object.assign({}, _positions[n.id]);
1602
2348
  }
@@ -1646,6 +2392,11 @@ var LiveCard = (function () {
1646
2392
  _render();
1647
2393
  }
1648
2394
 
2395
+ function setDevMode(flag) {
2396
+ devMode.current = !!flag;
2397
+ _render();
2398
+ }
2399
+
1649
2400
  function destroy() {
1650
2401
  ac.abort();
1651
2402
  engine.destroyAll();
@@ -1668,9 +2419,11 @@ var LiveCard = (function () {
1668
2419
  refresh,
1669
2420
  clear,
1670
2421
  setMode,
2422
+ setDevMode,
1671
2423
  autoLayout,
1672
2424
  destroy,
1673
2425
  get mode() { return mode.current; },
2426
+ get devMode() { return devMode.current; },
1674
2427
  get nodes() { return nodeList.slice(); },
1675
2428
  get engine() { return engine; },
1676
2429
  };