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