yaml-flow 4.0.0 → 5.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/browser/board-livegraph-runtime.js +1453 -0
- package/browser/board-livegraph-runtime.js.map +1 -0
- package/browser/card-compute.js +36 -17
- package/browser/live-cards.js +848 -109
- package/browser/live-cards.schema.json +46 -21
- package/dist/board-livegraph-runtime/index.cjs +1448 -0
- package/dist/board-livegraph-runtime/index.cjs.map +1 -0
- package/dist/board-livegraph-runtime/index.d.cts +101 -0
- package/dist/board-livegraph-runtime/index.d.ts +101 -0
- package/dist/board-livegraph-runtime/index.js +1441 -0
- package/dist/board-livegraph-runtime/index.js.map +1 -0
- package/dist/card-compute/index.cjs +159 -44
- package/dist/card-compute/index.cjs.map +1 -1
- package/dist/card-compute/index.d.cts +36 -11
- package/dist/card-compute/index.d.ts +36 -11
- package/dist/card-compute/index.js +156 -44
- package/dist/card-compute/index.js.map +1 -1
- package/dist/cli/board-live-cards-cli.cjs +476 -105
- package/dist/cli/board-live-cards-cli.cjs.map +1 -1
- package/dist/cli/board-live-cards-cli.d.cts +8 -16
- package/dist/cli/board-live-cards-cli.d.ts +8 -16
- package/dist/cli/board-live-cards-cli.js +476 -106
- package/dist/cli/board-live-cards-cli.js.map +1 -1
- package/dist/continuous-event-graph/index.cjs +74 -33
- package/dist/continuous-event-graph/index.cjs.map +1 -1
- package/dist/continuous-event-graph/index.d.cts +7 -23
- package/dist/continuous-event-graph/index.d.ts +7 -23
- package/dist/continuous-event-graph/index.js +73 -32
- package/dist/continuous-event-graph/index.js.map +1 -1
- package/dist/index.cjs +1440 -56
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +21 -3
- package/dist/index.d.ts +21 -3
- package/dist/index.js +1434 -56
- package/dist/index.js.map +1 -1
- package/dist/journal-DRfJiheM.d.cts +28 -0
- package/dist/journal-NLYuqege.d.ts +28 -0
- package/dist/{journal-B_2JnBMF.d.ts → live-cards-bridge-Or7fdEJV.d.ts} +5 -32
- package/dist/{journal-BJDjWb5Q.d.cts → live-cards-bridge-vGJ6tMzN.d.cts} +5 -32
- package/dist/schedule-CMcZe5Ny.d.ts +21 -0
- package/dist/schedule-CiucyCan.d.cts +21 -0
- package/examples/browser/boards/portfolio-tracker/cards/holdings-table.json +1 -1
- package/examples/browser/boards/portfolio-tracker/cards/portfolio-form.json +3 -3
- package/examples/browser/boards/portfolio-tracker/cards/portfolio-value.json +1 -1
- package/examples/browser/boards/portfolio-tracker/cards/price-fetch.json +3 -3
- package/examples/browser/boards/portfolio-tracker/portfolio-tracker-task-executor.cjs +96 -0
- package/examples/browser/boards/portfolio-tracker/portfolio-tracker.js +33 -5
- package/examples/browser/livecards-browser/index.html +37 -684
- package/examples/cli/step-machine-cli/portfolio-tracker/cards/holdings-table.json +1 -1
- package/examples/cli/step-machine-cli/portfolio-tracker/cards/portfolio-form.json +3 -3
- package/examples/cli/step-machine-cli/portfolio-tracker/cards/portfolio-value.json +1 -1
- package/examples/cli/step-machine-cli/portfolio-tracker/cards/price-fetch.json +3 -3
- package/examples/cli/step-machine-cli/portfolio-tracker/handlers/update-holdings-cli.js +2 -2
- package/examples/example-board/board.yaml +23 -0
- package/examples/example-board/bootstrap_payload.json +1 -0
- package/examples/example-board/cards/card-chain-region-alert.json +39 -0
- package/examples/example-board/cards/card-chain-region-totals.json +26 -0
- package/examples/example-board/cards/card-chain-top-region.json +24 -0
- package/examples/example-board/cards/card-ex-actions.json +32 -0
- package/examples/example-board/cards/card-ex-chart.json +30 -0
- package/examples/example-board/cards/card-ex-filter.json +36 -0
- package/examples/example-board/cards/card-ex-filtered-by-preference.json +59 -0
- package/examples/example-board/cards/card-ex-form.json +91 -0
- package/examples/example-board/cards/card-ex-list.json +22 -0
- package/examples/example-board/cards/card-ex-markdown.json +17 -0
- package/examples/example-board/cards/card-ex-metric.json +19 -0
- package/examples/example-board/cards/card-ex-narrative.json +36 -0
- package/examples/example-board/cards/card-ex-source-http.json +28 -0
- package/examples/example-board/cards/card-ex-source.json +21 -0
- package/examples/example-board/cards/card-ex-status.json +35 -0
- package/examples/example-board/cards/card-ex-table.json +30 -0
- package/examples/example-board/cards/card-ex-todo.json +29 -0
- package/examples/example-board/demo-chat-handler.js +69 -0
- package/examples/example-board/demo-server.js +87 -0
- package/examples/example-board/demo-shell-browser.html +806 -0
- package/examples/example-board/demo-shell-with-server.html +280 -0
- package/examples/example-board/demo-shell.html +62 -0
- package/examples/example-board/demo-task-executor.js +255 -0
- package/examples/example-board/mock.db +15 -0
- package/examples/example-board/reusable-board-runtime-client.js +265 -0
- package/examples/example-board/reusable-runtime-artifacts-adapter.js +233 -0
- package/examples/example-board/reusable-server-runtime.js +1284 -0
- package/examples/index.html +16 -9
- package/examples/npm-libs/continuous-event-graph/live-cards-board.ts +17 -17
- package/examples/npm-libs/continuous-event-graph/live-portfolio-dashboard.ts +23 -23
- package/examples/step-machine-cli/portfolio-tracker/cards/holdings-table.json +1 -1
- package/examples/step-machine-cli/portfolio-tracker/cards/portfolio-form.json +3 -3
- package/examples/step-machine-cli/portfolio-tracker/cards/portfolio-value.json +1 -1
- package/examples/step-machine-cli/portfolio-tracker/cards/price-fetch.json +1 -1
- package/examples/step-machine-cli/portfolio-tracker/portfolio-tracker-task-executor.cjs +96 -0
- package/package.json +16 -2
- package/schema/card-runtime.schema.json +25 -0
- package/schema/live-cards.schema.json +46 -21
- package/browser/ingest-board.js +0 -296
- package/examples/ingest.js +0 -733
package/browser/live-cards.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// live-cards.js — LiveCards v3: Node-based Board/Canvas engine
|
|
2
2
|
//
|
|
3
|
-
// Schema: Each node has { id
|
|
4
|
-
// id, meta,
|
|
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,
|
|
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:.
|
|
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(
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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 + '">×</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 + '">×</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
|
-
|
|
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
|
-
|
|
238
|
-
|
|
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 =
|
|
760
|
-
const fi =
|
|
761
|
-
const stagedEl =
|
|
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))
|
|
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
|
-
|
|
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}">×</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
|
-
|
|
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 =
|
|
841
|
-
const input =
|
|
842
|
-
const sendBtn =
|
|
843
|
-
const attachBtn = canAttach ?
|
|
844
|
-
const fileInput = canAttach ?
|
|
845
|
-
const stagedEl = canAttach ?
|
|
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-
|
|
1067
|
-
h += _statusDot(node.
|
|
1068
|
-
h += `<span class="text-muted small">${_timeAgo(node.
|
|
1069
|
-
if (node.
|
|
1070
|
-
h += `<span class="badge bg-danger small" title="${_esc(node.
|
|
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
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
1132
|
-
node.
|
|
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
|
|
1740
|
+
// Merge into node card_data
|
|
1186
1741
|
const node = cfg.resolve(nodeId);
|
|
1187
1742
|
if (!node) return;
|
|
1188
|
-
if (!node.
|
|
1189
|
-
if (patch.status) node.
|
|
1190
|
-
if (patch.lastRun) node.
|
|
1191
|
-
if (patch.error !== undefined) node.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
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
|
-
|
|
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
|
|
1375
|
-
const
|
|
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 ((
|
|
1378
|
-
var src =
|
|
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 = '</>';
|
|
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.
|
|
1395
|
-
const
|
|
1396
|
-
const
|
|
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
|
};
|