yaml-flow 4.0.0 → 5.1.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 (96) hide show
  1. package/browser/board-livegraph-runtime.js +1453 -0
  2. package/browser/board-livegraph-runtime.js.map +1 -0
  3. package/browser/card-compute.js +36 -17
  4. package/browser/live-cards.js +848 -109
  5. package/browser/live-cards.schema.json +46 -21
  6. package/dist/board-livegraph-runtime/index.cjs +1448 -0
  7. package/dist/board-livegraph-runtime/index.cjs.map +1 -0
  8. package/dist/board-livegraph-runtime/index.d.cts +101 -0
  9. package/dist/board-livegraph-runtime/index.d.ts +101 -0
  10. package/dist/board-livegraph-runtime/index.js +1441 -0
  11. package/dist/board-livegraph-runtime/index.js.map +1 -0
  12. package/dist/card-compute/index.cjs +159 -44
  13. package/dist/card-compute/index.cjs.map +1 -1
  14. package/dist/card-compute/index.d.cts +36 -11
  15. package/dist/card-compute/index.d.ts +36 -11
  16. package/dist/card-compute/index.js +156 -44
  17. package/dist/card-compute/index.js.map +1 -1
  18. package/dist/cli/board-live-cards-cli.cjs +476 -105
  19. package/dist/cli/board-live-cards-cli.cjs.map +1 -1
  20. package/dist/cli/board-live-cards-cli.d.cts +8 -16
  21. package/dist/cli/board-live-cards-cli.d.ts +8 -16
  22. package/dist/cli/board-live-cards-cli.js +476 -106
  23. package/dist/cli/board-live-cards-cli.js.map +1 -1
  24. package/dist/continuous-event-graph/index.cjs +74 -33
  25. package/dist/continuous-event-graph/index.cjs.map +1 -1
  26. package/dist/continuous-event-graph/index.d.cts +7 -23
  27. package/dist/continuous-event-graph/index.d.ts +7 -23
  28. package/dist/continuous-event-graph/index.js +73 -32
  29. package/dist/continuous-event-graph/index.js.map +1 -1
  30. package/dist/index.cjs +1440 -56
  31. package/dist/index.cjs.map +1 -1
  32. package/dist/index.d.cts +21 -3
  33. package/dist/index.d.ts +21 -3
  34. package/dist/index.js +1434 -56
  35. package/dist/index.js.map +1 -1
  36. package/dist/journal-DRfJiheM.d.cts +28 -0
  37. package/dist/journal-NLYuqege.d.ts +28 -0
  38. package/dist/{journal-B_2JnBMF.d.ts → live-cards-bridge-Or7fdEJV.d.ts} +5 -32
  39. package/dist/{journal-BJDjWb5Q.d.cts → live-cards-bridge-vGJ6tMzN.d.cts} +5 -32
  40. package/dist/schedule-CMcZe5Ny.d.ts +21 -0
  41. package/dist/schedule-CiucyCan.d.cts +21 -0
  42. package/examples/browser/boards/portfolio-tracker/cards/holdings-table.json +1 -1
  43. package/examples/browser/boards/portfolio-tracker/cards/portfolio-form.json +3 -3
  44. package/examples/browser/boards/portfolio-tracker/cards/portfolio-value.json +1 -1
  45. package/examples/browser/boards/portfolio-tracker/cards/price-fetch.json +3 -3
  46. package/examples/browser/boards/portfolio-tracker/portfolio-tracker-task-executor.cjs +96 -0
  47. package/examples/browser/boards/portfolio-tracker/portfolio-tracker.js +33 -5
  48. package/examples/browser/livecards-browser/index.html +37 -684
  49. package/examples/cli/step-machine-cli/portfolio-tracker/cards/holdings-table.json +1 -1
  50. package/examples/cli/step-machine-cli/portfolio-tracker/cards/portfolio-form.json +3 -3
  51. package/examples/cli/step-machine-cli/portfolio-tracker/cards/portfolio-value.json +1 -1
  52. package/examples/cli/step-machine-cli/portfolio-tracker/cards/price-fetch.json +3 -3
  53. package/examples/cli/step-machine-cli/portfolio-tracker/handlers/update-holdings-cli.js +2 -2
  54. package/examples/example-board/board.yaml +23 -0
  55. package/examples/example-board/bootstrap_payload.json +1 -0
  56. package/examples/example-board/cards/card-chain-region-alert.json +39 -0
  57. package/examples/example-board/cards/card-chain-region-totals.json +26 -0
  58. package/examples/example-board/cards/card-chain-top-region.json +24 -0
  59. package/examples/example-board/cards/card-ex-actions.json +32 -0
  60. package/examples/example-board/cards/card-ex-chart.json +30 -0
  61. package/examples/example-board/cards/card-ex-filter.json +36 -0
  62. package/examples/example-board/cards/card-ex-filtered-by-preference.json +59 -0
  63. package/examples/example-board/cards/card-ex-form.json +91 -0
  64. package/examples/example-board/cards/card-ex-list.json +22 -0
  65. package/examples/example-board/cards/card-ex-markdown.json +17 -0
  66. package/examples/example-board/cards/card-ex-metric.json +19 -0
  67. package/examples/example-board/cards/card-ex-narrative.json +36 -0
  68. package/examples/example-board/cards/card-ex-source-http.json +28 -0
  69. package/examples/example-board/cards/card-ex-source.json +21 -0
  70. package/examples/example-board/cards/card-ex-status.json +35 -0
  71. package/examples/example-board/cards/card-ex-table.json +30 -0
  72. package/examples/example-board/cards/card-ex-todo.json +29 -0
  73. package/examples/example-board/demo-chat-handler.js +69 -0
  74. package/examples/example-board/demo-server-config.json +7 -0
  75. package/examples/example-board/demo-server.js +124 -0
  76. package/examples/example-board/demo-shell-browser.html +806 -0
  77. package/examples/example-board/demo-shell-with-server.html +280 -0
  78. package/examples/example-board/demo-shell.html +62 -0
  79. package/examples/example-board/demo-task-executor.js +255 -0
  80. package/examples/example-board/mock.db +15 -0
  81. package/examples/example-board/reusable-board-runtime-client.js +265 -0
  82. package/examples/example-board/reusable-runtime-artifacts-adapter.js +233 -0
  83. package/examples/example-board/reusable-server-runtime.js +1341 -0
  84. package/examples/index.html +16 -9
  85. package/examples/npm-libs/continuous-event-graph/live-cards-board.ts +17 -17
  86. package/examples/npm-libs/continuous-event-graph/live-portfolio-dashboard.ts +23 -23
  87. package/examples/step-machine-cli/portfolio-tracker/cards/holdings-table.json +1 -1
  88. package/examples/step-machine-cli/portfolio-tracker/cards/portfolio-form.json +3 -3
  89. package/examples/step-machine-cli/portfolio-tracker/cards/portfolio-value.json +1 -1
  90. package/examples/step-machine-cli/portfolio-tracker/cards/price-fetch.json +1 -1
  91. package/examples/step-machine-cli/portfolio-tracker/portfolio-tracker-task-executor.cjs +96 -0
  92. package/package.json +16 -2
  93. package/schema/card-runtime.schema.json +25 -0
  94. package/schema/live-cards.schema.json +46 -21
  95. package/browser/ingest-board.js +0 -296
  96. package/examples/ingest.js +0 -733
@@ -1,7 +1,7 @@
1
1
  // live-cards.js — LiveCards v3: Node-based Board/Canvas engine
2
2
  //
3
- // Schema: Each node has { id, state } required; all else optional.
4
- // id, meta, state, requires, provides, sources, compute, view
3
+ // Schema: Each node has { id } required; all else optional.
4
+ // id, meta, card_data, requires, provides, sources, compute, view
5
5
  // Nodes with view render as cards; nodes with sources but no view render as source pills in canvas.
6
6
  // compute[] — ordered array of { bindTo, expr } JSONata steps → writes to node.computed_values (ephemeral)
7
7
  // sources[] — open objects: only bindTo + outputFile matter to the engine; all other fields are
@@ -14,7 +14,7 @@
14
14
  // Uses CardCompute (card-compute.js) for declarative compute expressions.
15
15
  //
16
16
  // API:
17
- // 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 });
18
18
  // engine.render(node, el, opts?) — render a card node into a DOM element
19
19
  // engine.update(nodeId, patch) — in-place update (status, re-render)
20
20
  // engine.destroy(nodeId) — tear down one node
@@ -64,12 +64,27 @@ var LiveCard = (function () {
64
64
  .lc-staged-file { display:flex; align-items:center; gap:.5rem; padding:.125rem 0; }
65
65
  .lc-chat-el { display:flex; flex-direction:column; }
66
66
  .lc-chat-body { flex:1; overflow-y:auto; max-height:300px; padding:.25rem; }
67
- .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; }
68
68
  .lc-chat-bubble-user { background:var(--bs-primary-bg-subtle,#cfe2ff); margin-left:auto; }
69
69
  .lc-chat-bubble-assistant { background:var(--bs-light,#f8f9fa); }
70
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; }
71
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; }
72
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; }
73
88
  @media (max-width:576px) {
74
89
  .lc-metric-value { font-size:1.5rem; }
75
90
  .lc-chart-wrap { min-height:150px; }
@@ -179,12 +194,42 @@ var LiveCard = (function () {
179
194
  sanitize: config.sanitize || null,
180
195
  chartLib: config.chartLib || null,
181
196
  onAction: config.onAction || function () {},
197
+ getChatMessages: config.getChatMessages || null,
182
198
  };
183
199
 
184
200
  const _cleanup = {}; // nodeId → { ac, timers, charts, unsubs }
185
201
  const _subs = {}; // nodeId → Set<callback>
186
202
  const _renderers = {}; // kind → fn
187
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
+ };
188
233
 
189
234
  // ---- Helpers ----
190
235
 
@@ -199,17 +244,466 @@ var LiveCard = (function () {
199
244
  return _cleanup[id];
200
245
  }
201
246
 
202
- function _runCompute(node) {
203
- if (!node.compute || !node.compute.length) return Promise.resolve();
204
- if (typeof CardCompute === 'undefined') return Promise.resolve();
205
- return CardCompute.run(node).catch(function (e) {
206
- 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);
207
448
  });
208
449
  }
209
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;
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);
686
+ }
687
+
210
688
  function _resolveBind(node, bind) {
211
- if (!bind) return undefined;
212
- 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];
213
707
  }
214
708
 
215
709
  // ---- Pub/sub ----
@@ -226,7 +720,7 @@ var LiveCard = (function () {
226
720
  }
227
721
 
228
722
  function _autoSubscribe(node) {
229
- const requires = node.requires || [];
723
+ const requires = (node && node.card && Array.isArray(node.card.requires)) ? node.card.requires : [];
230
724
  if (!requires.length) return;
231
725
  const cleanup = _getCleanup(node.id);
232
726
  cleanup.unsubs = requires.map(upId => subscribe(upId, () => {
@@ -234,10 +728,8 @@ var LiveCard = (function () {
234
728
  if (!info || !info.resultEl) return;
235
729
  const updated = cfg.resolve(node.id);
236
730
  if (!updated) return;
237
- _runCompute(updated).then(function () {
238
- _renderElements(updated, info.resultEl);
239
- notify(node.id);
240
- });
731
+ _renderElements(updated, info.resultEl);
732
+ notify(node.id);
241
733
  }));
242
734
  }
243
735
 
@@ -678,7 +1170,30 @@ var LiveCard = (function () {
678
1170
 
679
1171
  function _renderText(data, el, elemDef) {
680
1172
  const ed = elemDef.data || {};
1173
+ const format = ed.format || 'default';
681
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
682
1197
  const tag = style === 'heading' ? 'h4' : 'div';
683
1198
  const cls = style === 'muted' ? 'text-muted small' : (style === 'heading' ? 'fw-bold' : 'small');
684
1199
  el.innerHTML = `<${tag} class="${cls}">${_esc(data != null ? String(data) : '')}</${tag}>`;
@@ -708,6 +1223,7 @@ var LiveCard = (function () {
708
1223
  const signal = cleanup.ac.signal;
709
1224
  const ed = elemDef.data || {};
710
1225
  const uploaded = Array.isArray(data) ? data : [];
1226
+ const showUploadedList = ed.showUploadedList === true;
711
1227
  const showUpload = ed.upload !== false;
712
1228
  const accept = ed.accept || ['.txt','.csv','.md','.json','.html','.xml','.pdf','.xlsx','.docx','.pptx','.png','.jpg','.jpeg'];
713
1229
  const acceptSet = new Set(accept.map(e => e.toLowerCase()));
@@ -717,6 +1233,12 @@ var LiveCard = (function () {
717
1233
 
718
1234
  let stagedFiles = el._stagedFiles || [];
719
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
+ }
720
1242
 
721
1243
  let h = '';
722
1244
 
@@ -731,7 +1253,7 @@ var LiveCard = (function () {
731
1253
  }
732
1254
 
733
1255
  // Uploaded files list
734
- if (uploaded.length) {
1256
+ if (showUploadedList && uploaded.length) {
735
1257
  h += '<div class="lc-uploaded-files">';
736
1258
  uploaded.forEach(f => {
737
1259
  const name = typeof f === 'string' ? f : (f.name || '');
@@ -756,36 +1278,68 @@ var LiveCard = (function () {
756
1278
  return;
757
1279
  }
758
1280
 
759
- const dz = document.getElementById(uid + '-dz');
760
- const fi = document.getElementById(uid + '-fi');
761
- 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');
762
1284
  if (!dz) return;
763
1285
 
764
1286
  function addFiles(fileList) {
1287
+ const newlyAdded = [];
765
1288
  for (const f of fileList) {
766
1289
  const ext = '.' + f.name.split('.').pop().toLowerCase();
767
1290
  if (!acceptSet.has(ext)) continue;
768
- 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
+ }
769
1296
  }
770
1297
  renderStaged();
771
- 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
+ }
772
1316
  }
773
1317
 
774
1318
  function renderStaged() {
775
1319
  if (!stagedFiles.length) { stagedEl.innerHTML = ''; return; }
776
1320
  let sh = '';
777
1321
  stagedFiles.forEach((f, i) => {
1322
+ const status = uploadStatus[keyForFile(f)] || 'ready';
778
1323
  sh += '<div class="lc-staged-file">';
779
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>';
780
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
+ }
781
1331
  sh += `<button class="btn btn-sm btn-link text-danger p-0 lc-rm-staged" data-idx="${i}">&times;</button>`;
782
1332
  sh += '</div>';
783
1333
  });
784
1334
  stagedEl.innerHTML = sh;
785
1335
  stagedEl.querySelectorAll('.lc-rm-staged').forEach(btn => {
786
1336
  btn.addEventListener('click', () => {
787
- 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);
788
1341
  el._stagedFiles = stagedFiles;
1342
+ el._uploadStatus = uploadStatus;
789
1343
  renderStaged();
790
1344
  }, { signal });
791
1345
  });
@@ -801,7 +1355,7 @@ var LiveCard = (function () {
801
1355
 
802
1356
  el._fileUpload = {
803
1357
  getFiles: () => stagedFiles,
804
- clear: () => { stagedFiles = []; el._stagedFiles = []; renderStaged(); },
1358
+ clear: () => { stagedFiles = []; uploadStatus = {}; el._stagedFiles = []; el._uploadStatus = {}; renderStaged(); },
805
1359
  disable: () => { dz.classList.add('lc-disabled'); fi.disabled = true; },
806
1360
  enable: () => { dz.classList.remove('lc-disabled'); fi.disabled = false; },
807
1361
  };
@@ -837,12 +1391,12 @@ var LiveCard = (function () {
837
1391
 
838
1392
  el.innerHTML = h;
839
1393
 
840
- const body = document.getElementById(uid + '-body');
841
- const input = document.getElementById(uid + '-input');
842
- const sendBtn = document.getElementById(uid + '-send');
843
- const attachBtn = canAttach ? document.getElementById(uid + '-attach') : null;
844
- const fileInput = canAttach ? document.getElementById(uid + '-fi') : null;
845
- 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;
846
1400
 
847
1401
  let stagedFiles = [];
848
1402
 
@@ -996,7 +1550,7 @@ var LiveCard = (function () {
996
1550
  // ===========================================================================
997
1551
 
998
1552
  function _renderElements(node, containerEl) {
999
- const view = node.view;
1553
+ const view = node && node.card ? node.card.view : null;
1000
1554
  if (!view || !Array.isArray(view.elements)) { containerEl.innerHTML = ''; return; }
1001
1555
 
1002
1556
  if (_nodeEls[node.id]) _nodeEls[node.id].elements = {};
@@ -1054,7 +1608,7 @@ var LiveCard = (function () {
1054
1608
  const cleanup = _getCleanup(node.id);
1055
1609
  const signal = cleanup.ac.signal;
1056
1610
  const uid = 'lc-' + (node.id || 'x');
1057
- const features = (node.view && node.view.features) || {};
1611
+ const features = (node.card && node.card.view && node.card.view.features) || {};
1058
1612
 
1059
1613
  // Run compute async before populating elements
1060
1614
  // (compute is triggered in the else branch below after DOM is ready)
@@ -1063,18 +1617,31 @@ var LiveCard = (function () {
1063
1617
 
1064
1618
  // Header bar: status dot + time-ago + refresh button
1065
1619
  const showRefresh = features.refresh !== false && cfg.onRefresh;
1066
- h += `<div class="d-flex align-items-center gap-2 mb-2">`;
1067
- h += _statusDot(node.state && node.state.status);
1068
- h += `<span class="text-muted small">${_timeAgo(node.state && node.state.lastRun)}</span>`;
1069
- if (node.state && node.state.status === 'error' && node.state.error) {
1070
- 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>`;
1071
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
1072
1638
  if (showRefresh) {
1073
- 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">`;
1074
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>';
1075
1641
  h += '</button>';
1076
1642
  }
1077
1643
  h += '</div>';
1644
+ h += '</div>';
1078
1645
 
1079
1646
  // Elements area
1080
1647
  h += `<div class="lc-result" id="${uid}-result"></div>`;
@@ -1082,18 +1649,7 @@ var LiveCard = (function () {
1082
1649
  // Notes section (feature toggle)
1083
1650
  if (features.notes && opts.showNotes !== false) {
1084
1651
  h += `<details class="mt-2"><summary class="small fw-medium">Notes</summary>`;
1085
- 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>`;
1086
- }
1087
-
1088
- // Chat section (feature toggle)
1089
- if (features.chat && cfg.onChat && opts.showChat !== false) {
1090
- h += `<details class="mt-2"><summary class="small fw-medium">Chat</summary>`;
1091
- h += `<div class="lc-chat-messages" id="${uid}-chat"></div>`;
1092
- h += `<div class="input-group input-group-sm mt-1">`;
1093
- h += `<input type="text" class="form-control" id="${uid}-chatInput" placeholder="Ask about this card...">`;
1094
- h += `<button class="btn btn-outline-primary" id="${uid}-chatSend">`;
1095
- 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>';
1096
- 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>`;
1097
1653
  }
1098
1654
 
1099
1655
  h += '</div>';
@@ -1103,10 +1659,10 @@ var LiveCard = (function () {
1103
1659
  const resultEl = document.getElementById(uid + '-result');
1104
1660
  _nodeEls[node.id] = { container: containerEl, resultEl, uid };
1105
1661
 
1106
- if (node.state && node.state.status === 'loading') {
1662
+ if (node.card_data && node.card_data.status === 'loading') {
1107
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>';
1108
- } else if (node.state && node.state.status === 'error' && node.state.error) {
1109
- 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>`;
1110
1666
  } else {
1111
1667
  _runCompute(node).then(function () { _renderElements(node, resultEl); });
1112
1668
  }
@@ -1121,6 +1677,22 @@ var LiveCard = (function () {
1121
1677
  }, { signal });
1122
1678
  }
1123
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
+
1124
1696
  // ---- Wire notes ----
1125
1697
  const notesEl = document.getElementById(uid + '-notes');
1126
1698
  if (notesEl) {
@@ -1128,31 +1700,14 @@ var LiveCard = (function () {
1128
1700
  notesEl.addEventListener('input', () => {
1129
1701
  clearTimeout(nTimer);
1130
1702
  nTimer = setTimeout(() => {
1131
- if (!node.state) node.state = {};
1132
- node.state._notes = notesEl.value;
1703
+ if (!node.card_data) node.card_data = {};
1704
+ node.card_data._notes = notesEl.value;
1133
1705
  cfg.onPatch(node.id, { _notes: notesEl.value });
1134
1706
  }, 800);
1135
1707
  cleanup.timers.push(nTimer);
1136
1708
  }, { signal });
1137
1709
  }
1138
1710
 
1139
- // ---- Wire chat ----
1140
- const chatInput = document.getElementById(uid + '-chatInput');
1141
- const chatSend = document.getElementById(uid + '-chatSend');
1142
- if (chatInput && chatSend && cfg.onChat) {
1143
- const send = () => {
1144
- const msg = chatInput.value.trim();
1145
- if (!msg) return;
1146
- chatInput.value = '';
1147
- appendChatMessage(node.id, 'user', msg);
1148
- cfg.onChat(node.id, msg);
1149
- };
1150
- chatSend.addEventListener('click', send, { signal });
1151
- chatInput.addEventListener('keydown', e => {
1152
- if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); send(); }
1153
- }, { signal });
1154
- }
1155
-
1156
1711
  _autoSubscribe(node);
1157
1712
  }
1158
1713
 
@@ -1182,18 +1737,31 @@ var LiveCard = (function () {
1182
1737
  if (ts) ts.textContent = _timeAgo(patch.lastRun);
1183
1738
  }
1184
1739
 
1185
- // Merge into node state
1740
+ // Merge into node card_data
1186
1741
  const node = cfg.resolve(nodeId);
1187
1742
  if (!node) return;
1188
- if (!node.state) node.state = {};
1189
- if (patch.status) node.state.status = patch.status;
1190
- if (patch.lastRun) node.state.lastRun = patch.lastRun;
1191
- 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);
1192
1760
 
1193
- if (node.state.status === 'loading') {
1761
+ if (node.card_data.status === 'loading') {
1194
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>';
1195
- } else if (node.state.status === 'error' && node.state.error) {
1196
- 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>`;
1197
1765
  } else {
1198
1766
  _runCompute(node).then(function () { _renderElements(node, info.resultEl); });
1199
1767
  }
@@ -1224,15 +1792,21 @@ var LiveCard = (function () {
1224
1792
  // ===========================================================================
1225
1793
 
1226
1794
  function appendChatMessage(nodeId, role, text) {
1227
- const info = _nodeEls[nodeId];
1228
- if (!info) return;
1229
- const chatEl = info.container.querySelector('.lc-chat-messages');
1230
- if (!chatEl) return;
1231
- const msg = document.createElement('div');
1232
- msg.className = `lc-chat-msg small ${role === 'user' ? 'lc-chat-user' : 'lc-chat-assistant'}`;
1233
- msg.innerHTML = role === 'assistant' ? _renderMd(text) : _esc(text);
1234
- chatEl.appendChild(msg);
1235
- 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 () {});
1236
1810
  }
1237
1811
 
1238
1812
  // ===========================================================================
@@ -1256,6 +1830,10 @@ var LiveCard = (function () {
1256
1830
  notify,
1257
1831
  subscribe,
1258
1832
  appendChatMessage,
1833
+ refreshOpenChatModal,
1834
+ onServerSseEvent,
1835
+ openChatModal,
1836
+ openFilesModal,
1259
1837
  getElement,
1260
1838
  registerRenderer(name, fn) { _renderers[name] = fn; },
1261
1839
  renderers: _renderers,
@@ -1269,6 +1847,7 @@ var LiveCard = (function () {
1269
1847
  function Board(engine, containerEl, opts) {
1270
1848
  opts = opts || {};
1271
1849
  const mode = { current: opts.mode || 'board' };
1850
+ const devMode = { current: opts.devMode || false };
1272
1851
  const nodeList = [];
1273
1852
  const nodeMap = {}; // id → { node, colEl, bodyEl }
1274
1853
  const _positions = {}; // id → { x, y, w, h } for canvas mode
@@ -1342,7 +1921,8 @@ var LiveCard = (function () {
1342
1921
  // ---- Helpers ----
1343
1922
 
1344
1923
  function _colWidth(node) {
1345
- 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;
1346
1926
  return defaultCol;
1347
1927
  }
1348
1928
 
@@ -1352,8 +1932,8 @@ var LiveCard = (function () {
1352
1932
  if (_positions[node.id]) return; // already set
1353
1933
  if (explicit[node.id]) {
1354
1934
  _positions[node.id] = Object.assign({}, explicit[node.id]);
1355
- } else if (node.view && node.view.layout && node.view.layout.canvas && node.view.layout.canvas.x != null) {
1356
- _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);
1357
1937
  } else {
1358
1938
  const col = (i % 4);
1359
1939
  const row = Math.floor(i / 4);
@@ -1363,7 +1943,142 @@ var LiveCard = (function () {
1363
1943
  }
1364
1944
 
1365
1945
  function _getRequires(node) {
1366
- return node.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);
1367
2082
  }
1368
2083
 
1369
2084
  function _buildCardWrapper(node) {
@@ -1371,16 +2086,32 @@ var LiveCard = (function () {
1371
2086
  wrap.className = 'card shadow-sm h-100';
1372
2087
  const header = document.createElement('div');
1373
2088
  header.className = 'card-header d-flex align-items-center gap-2 py-2';
1374
- const title = (node.meta && node.meta.title) || node.id;
1375
- 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) || [];
1376
2092
  let badgeHtml = '';
1377
- if ((node.sources && node.sources.length) && !node.view) {
1378
- var src = node.sources[0] || {};
2093
+ if ((card.sources && card.sources.length) && !card.view) {
2094
+ var src = card.sources[0] || {};
1379
2095
  badgeHtml = '<span class="badge bg-info text-dark ms-auto">' + _esc(src.kind || 'source') + '</span>';
1380
2096
  } else if (tags.length) {
1381
2097
  badgeHtml = tags.map(t => '<span class="badge bg-secondary ms-1">' + _esc(t) + '</span>').join('');
1382
2098
  }
1383
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
+
1384
2115
  const body = document.createElement('div');
1385
2116
  body.className = 'card-body p-2';
1386
2117
  wrap.appendChild(header);
@@ -1391,9 +2122,10 @@ var LiveCard = (function () {
1391
2122
  function _buildSourcePill(node) {
1392
2123
  const el = document.createElement('div');
1393
2124
  el.className = 'lc-source-node';
1394
- const status = (node.state && node.state.status) || 'fresh';
1395
- const title = (node.meta && node.meta.title) || node.id;
1396
- const kind = (node.sources && node.sources[0] && node.sources[0].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';
1397
2129
  el.innerHTML = `<div class="lc-source-pill shadow-sm">
1398
2130
  ${_statusDot(status)}
1399
2131
  <span class="fw-medium">${_esc(title)}</span>
@@ -1410,10 +2142,10 @@ var LiveCard = (function () {
1410
2142
  gridEl.innerHTML = '';
1411
2143
 
1412
2144
  // Only card nodes in board mode, sorted by order
1413
- const cards = nodeList.filter(n => n.view).slice();
2145
+ const cards = nodeList.filter(n => n.card && n.card.view).slice();
1414
2146
  cards.sort((a, b) => {
1415
- const ao = (a.view && a.view.layout && a.view.layout.board && a.view.layout.board.order) || 0;
1416
- 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;
1417
2149
  return ao - bo;
1418
2150
  });
1419
2151
 
@@ -1491,11 +2223,11 @@ var LiveCard = (function () {
1491
2223
  el.style.left = x + 'px'; el.style.top = y + 'px';
1492
2224
  // Persist
1493
2225
  _positions[node.id] = Object.assign(_positions[node.id] || {}, { x, y });
1494
- if (node.view) {
1495
- if (!node.view.layout) node.view.layout = {};
1496
- if (!node.view.layout.canvas) node.view.layout.canvas = {};
1497
- node.view.layout.canvas.x = x;
1498
- 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;
1499
2231
  }
1500
2232
  engine.notify(node.id);
1501
2233
  _drawEdges();
@@ -1513,7 +2245,7 @@ var LiveCard = (function () {
1513
2245
  nodeList.forEach(node => {
1514
2246
  const pos = _positions[node.id] || { x: 0, y: 0 };
1515
2247
 
1516
- if (!node.view && (node.sources && node.sources.length)) {
2248
+ if ((!node.card || !node.card.view) && (node.card && node.card.sources && node.card.sources.length)) {
1517
2249
  const el = _buildSourcePill(node);
1518
2250
  el.dataset.nodeId = node.id;
1519
2251
  el.style.left = pos.x + 'px';
@@ -1660,6 +2392,11 @@ var LiveCard = (function () {
1660
2392
  _render();
1661
2393
  }
1662
2394
 
2395
+ function setDevMode(flag) {
2396
+ devMode.current = !!flag;
2397
+ _render();
2398
+ }
2399
+
1663
2400
  function destroy() {
1664
2401
  ac.abort();
1665
2402
  engine.destroyAll();
@@ -1682,9 +2419,11 @@ var LiveCard = (function () {
1682
2419
  refresh,
1683
2420
  clear,
1684
2421
  setMode,
2422
+ setDevMode,
1685
2423
  autoLayout,
1686
2424
  destroy,
1687
2425
  get mode() { return mode.current; },
2426
+ get devMode() { return devMode.current; },
1688
2427
  get nodes() { return nodeList.slice(); },
1689
2428
  get engine() { return engine; },
1690
2429
  };