yaml-flow 7.0.0 → 8.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/asset-integrity.json +8 -4
- package/browser/board-livecards-client.js +1 -1
- package/browser/board-livecards-localstorage.js +5 -5
- package/browser/live-cards.js +19 -3307
- package/cli/board-live-cards-lib-tjYsPt5U.d.ts +321 -0
- package/{dist/cli → cli}/browser-api/board-live-cards-browser-adapter.d.ts +3 -5
- package/cli/browser-api/board-live-cards-browser-adapter.js +3 -0
- package/{dist/cli → cli}/browser-api/card-store-browser-api.d.ts +1 -2
- package/{dist/cli → cli}/browser-api/card-store-browser-api.js +1 -1
- package/cli/execution-interface-C_A6WCiK.d.ts +284 -0
- package/cli/node/artifacts-store-cli.js +11 -0
- package/cli/node/batch-runner-cli.js +3 -0
- package/cli/node/board-live-cards-cli.js +15 -0
- package/cli/node/card-store-cli.js +8 -0
- package/{dist/cli → cli}/node/execution-adapter.d.ts +3 -3
- package/cli/node/execution-adapter.js +3 -0
- package/{dist/cli → cli}/node/fs-board-adapter.d.ts +24 -11
- package/cli/node/fs-board-adapter.js +14 -0
- package/{dist/cli → cli}/node/source-cli-task-executor.js +2 -2
- package/cli/node/step-machine-cli.d.ts +7 -0
- package/cli/node/step-machine-cli.js +5 -0
- package/{dist/board-live-cards-public-CW5074xr.d.cts → cli/types-CziUxkiv.d.ts} +69 -7
- package/examples/board/.demo-setup/run-1778643703151-3360-dopnpv/board-default/gandalf-runtime/.config/card-store-ref.json +1 -0
- package/examples/board/.demo-setup/run-1778643703151-3360-dopnpv/board-default/gandalf-runtime/.config/chat-handler.json +1 -0
- package/examples/board/.demo-setup/run-1778643703151-3360-dopnpv/board-default/gandalf-runtime/.config/outputs-store-ref.json +1 -0
- package/examples/board/.demo-setup/run-1778643703151-3360-dopnpv/board-default/gandalf-runtime/.config/task-executor.json +1 -0
- package/examples/board/.demo-setup/run-1778643703151-3360-dopnpv/board-default/gandalf-runtime/.state-snapshot/board/graph.json +29 -0
- package/examples/board/.demo-setup/run-1778643703151-3360-dopnpv/board-default/gandalf-runtime/.state-snapshot/board/lastJournalProcessedId.json +1 -0
- package/examples/board/.demo-setup/run-1778643703151-3360-dopnpv/board-default/gandalf-runtime-out/.outputs/status.json +25 -0
- package/examples/board/.demo-setup/run-1778643703151-3360-dopnpv/board-default/runtime-out/.outputs/cards/card-market-prices/computed_values.json +67 -0
- package/examples/board/.demo-setup/run-1778643703151-3360-dopnpv/board-default/runtime-out/.outputs/cards/card-portfolio/computed_values.json +1 -0
- package/examples/board/.demo-setup/run-1778643703151-3360-dopnpv/board-default/runtime-out/.outputs/cards/card-portfolio-value/computed_values.json +52 -0
- package/examples/board/.demo-setup/run-1778643703151-3360-dopnpv/board-default/runtime-out/.outputs/data-objects/holdings.json +22 -0
- package/examples/board/.demo-setup/run-1778643703151-3360-dopnpv/board-default/runtime-out/.outputs/data-objects/positions.json +46 -0
- package/examples/board/.demo-setup/run-1778643703151-3360-dopnpv/board-default/runtime-out/.outputs/data-objects/quotes.json +35 -0
- package/examples/board/.demo-setup/run-1778643703151-3360-dopnpv/board-default/runtime-out/.outputs/status.json +113 -0
- package/examples/{example-board/cards/card-market-prices.json → board/cards/cardT-market-prices.json} +2 -2
- package/examples/{example-board/cards/card-portfolio.json → board/cards/cardT-portfolio.json} +3 -13
- package/examples/{example-board → board}/demo-server-config.json +0 -1
- package/examples/{example-board → board}/demo-server.js +70 -72
- package/examples/{example-board → board}/demo-shell-with-server.html +3 -3
- package/examples/{example-board → board}/demo-task-executor.js +75 -32
- package/examples/board/gandalf-cards/card-source-kinds.json +36 -0
- package/examples/board/gandalf-cards/cards/_index.json +7 -0
- package/examples/board/gandalf-cards/cards/card-source-kinds.json +64 -0
- package/examples/board/source-def-flows/copilot.flow.json +33 -0
- package/examples/board/source-def-flows/mock.flow.json +35 -0
- package/examples/board/source-def-flows/url-list.flow.json +33 -0
- package/examples/board/source-def-flows/url.flow.json +33 -0
- package/examples/board/source-def-flows/workiq.flow.json +34 -0
- package/examples/board/source-def-handlers/copilot-source-handler.js +141 -0
- package/examples/board/source-def-handlers/http-source-handler.js +145 -0
- package/examples/board/source_def_flows.json +249 -0
- package/examples/board/test/demo-http-test.js +317 -0
- package/examples/{example-board → board-local}/demo-shell-localstorage.html +4 -4
- package/examples/{browser/boards/portfolio-tracker → portfolio-tracker/handlers}/portfolio-tracker-fetch-prices.js +1 -1
- package/examples/{browser/boards/portfolio-tracker/portfolio-tracker-public.js → portfolio-tracker/portfolio-tracker.js} +11 -14
- package/examples/{browser/boards/portfolio-tracker → portfolio-tracker/test}/portfolio-t4.js +32 -50
- package/lib/artifacts-store-lib-public-BABrgFkV.d.ts +119 -0
- package/lib/artifacts-store-lib-public-DGa8BpJT.d.cts +119 -0
- package/lib/artifacts-store-public.cjs +2 -0
- package/lib/artifacts-store-public.d.cts +5 -0
- package/lib/artifacts-store-public.d.ts +5 -0
- package/lib/artifacts-store-public.js +2 -0
- package/lib/board-live-cards-node.cjs +14 -0
- package/{dist/cli/node/execution-adapter.d.cts → lib/board-live-cards-node.d.cts} +45 -85
- package/lib/board-live-cards-node.d.ts +134 -0
- package/lib/board-live-cards-node.js +14 -0
- package/lib/board-live-cards-public-BnmRAbQV.d.cts +383 -0
- package/{dist/board-live-cards-public-hnZo0mAf.d.ts → lib/board-live-cards-public-CsmYrvpd.d.ts} +142 -77
- package/lib/board-live-cards-public.cjs +3 -0
- package/lib/board-live-cards-public.d.cts +4 -0
- package/lib/board-live-cards-public.d.ts +4 -0
- package/lib/board-live-cards-public.js +3 -0
- package/lib/board-live-cards-server-runtime.cjs +9 -0
- package/lib/board-live-cards-server-runtime.d.cts +6 -0
- package/lib/board-live-cards-server-runtime.d.ts +6 -0
- package/lib/board-live-cards-server-runtime.js +9 -0
- package/lib/board-livegraph-runtime/index.cjs +3 -0
- package/{dist → lib}/board-livegraph-runtime/index.d.cts +1 -2
- package/{dist → lib}/board-livegraph-runtime/index.d.ts +1 -2
- package/lib/board-livegraph-runtime/index.js +3 -0
- package/lib/board-worker-adapter.cjs +10 -0
- package/{dist/storage-refs.d.cts → lib/board-worker-adapter.d.cts} +5 -5
- package/{dist/storage-refs.d.ts → lib/board-worker-adapter.d.ts} +5 -5
- package/lib/board-worker-adapter.js +10 -0
- package/{dist → lib}/card-compute/index.cjs +1 -1
- package/{dist → lib}/card-compute/index.js +1 -1
- package/lib/card-store-public.cjs +2 -0
- package/lib/card-store-public.d.cts +61 -0
- package/lib/card-store-public.d.ts +61 -0
- package/lib/card-store-public.js +2 -0
- package/lib/card-validation.cjs +10 -0
- package/lib/card-validation.d.cts +35 -0
- package/lib/card-validation.d.ts +35 -0
- package/lib/card-validation.js +10 -0
- package/{dist/constants-oCEbNpul.d.ts → lib/constants-BPVLb3Es.d.ts} +1 -1
- package/{dist/constants-BzZUyYlp.d.cts → lib/constants-DXxsRN9y.d.cts} +1 -1
- package/{dist → lib}/continuous-event-graph/index.cjs +2 -2
- package/{dist → lib}/continuous-event-graph/index.d.cts +3 -5
- package/{dist → lib}/continuous-event-graph/index.d.ts +3 -5
- package/{dist → lib}/continuous-event-graph/index.js +2 -2
- package/{dist → lib}/event-graph/index.d.cts +2 -2
- package/{dist → lib}/event-graph/index.d.ts +2 -2
- package/lib/execution-refs.cjs +3 -0
- package/{dist → lib}/execution-refs.d.cts +38 -14
- package/{dist → lib}/execution-refs.d.ts +38 -14
- package/lib/execution-refs.js +3 -0
- package/lib/index.cjs +25 -0
- package/{dist → lib}/index.d.cts +7 -8
- package/{dist → lib}/index.d.ts +7 -8
- package/lib/index.js +25 -0
- package/{dist/live-cards-bridge-BXbVTsna.d.cts → lib/live-cards-bridge-DC_ZU0eS.d.ts} +134 -3
- package/{dist/live-cards-bridge-Ds28XR15.d.ts → lib/live-cards-bridge-b25aAVvE.d.cts} +134 -3
- package/lib/loader-CuuLjxVA.d.cts +42 -0
- package/lib/loader-Zborm2pq.d.ts +42 -0
- package/lib/server-runtime/index.cjs +9 -0
- package/{dist → lib}/server-runtime/index.d.cts +4 -4
- package/{dist → lib}/server-runtime/index.d.ts +4 -4
- package/lib/server-runtime/index.js +9 -0
- package/lib/step-machine/index.d.cts +64 -0
- package/lib/step-machine/index.d.ts +64 -0
- package/lib/step-machine-public/index.cjs +5 -0
- package/{dist → lib}/step-machine-public/index.d.cts +21 -1
- package/{dist → lib}/step-machine-public/index.d.ts +21 -1
- package/lib/step-machine-public/index.js +5 -0
- package/lib/storage-interface-BhAON-gW.d.cts +84 -0
- package/lib/storage-interface-BhAON-gW.d.ts +84 -0
- package/lib/stores/index.cjs +3 -0
- package/lib/stores/index.d.cts +4 -0
- package/lib/stores/index.d.ts +4 -0
- package/lib/stores/index.js +3 -0
- package/lib/stores/kv.cjs +3 -0
- package/lib/stores/kv.d.cts +32 -0
- package/lib/stores/kv.d.ts +32 -0
- package/lib/stores/kv.js +3 -0
- package/{dist → lib}/stores/memory.d.cts +1 -1
- package/{dist → lib}/stores/memory.d.ts +1 -1
- package/{dist/types-B1ZRa4aI.d.ts → lib/types-CBxkYuLY.d.ts} +2 -1
- package/{dist/types-ycun84cq.d.cts → lib/types-DQ1bKuB1.d.cts} +11 -0
- package/{dist/types-ycun84cq.d.ts → lib/types-DQ1bKuB1.d.ts} +11 -0
- package/{dist/types-BxEFcVK9.d.cts → lib/types-DkFvgxwq.d.cts} +2 -1
- package/package.json +79 -119
- package/board-live-cards-cli.js +0 -37
- package/browser/board-livecards-client.js.map +0 -1
- package/browser/board-livecards-localstorage.js.map +0 -1
- package/browser/board-livegraph-engine.js +0 -3
- package/browser/board-livegraph-engine.js.map +0 -1
- package/browser/card-compute.js +0 -266
- package/browser/compute-jsonata.js.map +0 -1
- package/card-store.js +0 -37
- package/dist/batch/index.cjs.map +0 -1
- package/dist/batch/index.js.map +0 -1
- package/dist/board-live-cards-lib-Bg6EvCo5.d.cts +0 -136
- package/dist/board-live-cards-lib-jM2uYG1v.d.ts +0 -136
- package/dist/board-livegraph-runtime/index.cjs +0 -3
- package/dist/board-livegraph-runtime/index.cjs.map +0 -1
- package/dist/board-livegraph-runtime/index.js +0 -3
- package/dist/board-livegraph-runtime/index.js.map +0 -1
- package/dist/card-compute/index.cjs.map +0 -1
- package/dist/card-compute/index.js.map +0 -1
- package/dist/cli/browser-api/board-live-cards-browser-adapter.cjs +0 -3
- package/dist/cli/browser-api/board-live-cards-browser-adapter.cjs.map +0 -1
- package/dist/cli/browser-api/board-live-cards-browser-adapter.d.cts +0 -37
- package/dist/cli/browser-api/board-live-cards-browser-adapter.js +0 -3
- package/dist/cli/browser-api/board-live-cards-browser-adapter.js.map +0 -1
- package/dist/cli/browser-api/card-store-browser-api.cjs +0 -2
- package/dist/cli/browser-api/card-store-browser-api.cjs.map +0 -1
- package/dist/cli/browser-api/card-store-browser-api.d.cts +0 -26
- package/dist/cli/browser-api/card-store-browser-api.js.map +0 -1
- package/dist/cli/node/artifacts-store-cli.cjs +0 -11
- package/dist/cli/node/artifacts-store-cli.cjs.map +0 -1
- package/dist/cli/node/artifacts-store-cli.d.cts +0 -8
- package/dist/cli/node/artifacts-store-cli.js +0 -11
- package/dist/cli/node/artifacts-store-cli.js.map +0 -1
- package/dist/cli/node/board-live-cards-cli.cjs +0 -15
- package/dist/cli/node/board-live-cards-cli.cjs.map +0 -1
- package/dist/cli/node/board-live-cards-cli.d.cts +0 -20
- package/dist/cli/node/board-live-cards-cli.js +0 -15
- package/dist/cli/node/board-live-cards-cli.js.map +0 -1
- package/dist/cli/node/card-store-cli.cjs +0 -8
- package/dist/cli/node/card-store-cli.cjs.map +0 -1
- package/dist/cli/node/card-store-cli.d.cts +0 -15
- package/dist/cli/node/card-store-cli.js +0 -8
- package/dist/cli/node/card-store-cli.js.map +0 -1
- package/dist/cli/node/execution-adapter.cjs +0 -3
- package/dist/cli/node/execution-adapter.cjs.map +0 -1
- package/dist/cli/node/execution-adapter.js +0 -3
- package/dist/cli/node/execution-adapter.js.map +0 -1
- package/dist/cli/node/fs-board-adapter.cjs +0 -14
- package/dist/cli/node/fs-board-adapter.cjs.map +0 -1
- package/dist/cli/node/fs-board-adapter.d.cts +0 -204
- package/dist/cli/node/fs-board-adapter.js +0 -14
- package/dist/cli/node/fs-board-adapter.js.map +0 -1
- package/dist/cli/node/source-cli-task-executor.cjs +0 -11
- package/dist/cli/node/source-cli-task-executor.cjs.map +0 -1
- package/dist/cli/node/source-cli-task-executor.js.map +0 -1
- package/dist/config/index.cjs.map +0 -1
- package/dist/config/index.js.map +0 -1
- package/dist/continuous-event-graph/index.cjs.map +0 -1
- package/dist/continuous-event-graph/index.js.map +0 -1
- package/dist/event-graph/index.cjs.map +0 -1
- package/dist/event-graph/index.js.map +0 -1
- package/dist/execution-refs.cjs +0 -3
- package/dist/execution-refs.cjs.map +0 -1
- package/dist/execution-refs.js +0 -3
- package/dist/execution-refs.js.map +0 -1
- package/dist/index.cjs +0 -30
- package/dist/index.cjs.map +0 -1
- package/dist/index.js +0 -30
- package/dist/index.js.map +0 -1
- package/dist/inference/index.cjs +0 -7
- package/dist/inference/index.cjs.map +0 -1
- package/dist/inference/index.d.cts +0 -229
- package/dist/inference/index.d.ts +0 -229
- package/dist/inference/index.js +0 -7
- package/dist/inference/index.js.map +0 -1
- package/dist/server-runtime/index.cjs +0 -9
- package/dist/server-runtime/index.cjs.map +0 -1
- package/dist/server-runtime/index.js +0 -9
- package/dist/server-runtime/index.js.map +0 -1
- package/dist/step-machine/index.cjs.map +0 -1
- package/dist/step-machine/index.d.cts +0 -102
- package/dist/step-machine/index.d.ts +0 -102
- package/dist/step-machine/index.js.map +0 -1
- package/dist/step-machine-public/index.cjs +0 -2
- package/dist/step-machine-public/index.cjs.map +0 -1
- package/dist/step-machine-public/index.js +0 -2
- package/dist/step-machine-public/index.js.map +0 -1
- package/dist/storage-refs.cjs +0 -10
- package/dist/storage-refs.cjs.map +0 -1
- package/dist/storage-refs.js +0 -10
- package/dist/storage-refs.js.map +0 -1
- package/dist/stores/file.cjs +0 -2
- package/dist/stores/file.cjs.map +0 -1
- package/dist/stores/file.d.cts +0 -36
- package/dist/stores/file.d.ts +0 -36
- package/dist/stores/file.js +0 -2
- package/dist/stores/file.js.map +0 -1
- package/dist/stores/index.cjs +0 -2
- package/dist/stores/index.cjs.map +0 -1
- package/dist/stores/index.d.cts +0 -4
- package/dist/stores/index.d.ts +0 -4
- package/dist/stores/index.js +0 -2
- package/dist/stores/index.js.map +0 -1
- package/dist/stores/localStorage.cjs +0 -2
- package/dist/stores/localStorage.cjs.map +0 -1
- package/dist/stores/localStorage.d.cts +0 -34
- package/dist/stores/localStorage.d.ts +0 -34
- package/dist/stores/localStorage.js +0 -2
- package/dist/stores/localStorage.js.map +0 -1
- package/dist/stores/memory.cjs.map +0 -1
- package/dist/stores/memory.js.map +0 -1
- package/dist/types-CHSdoAAA.d.cts +0 -135
- package/dist/types-CoW0gQl3.d.ts +0 -135
- package/examples/browser/boards/portfolio-tracker/portfolio-tracker-fetch-prices.py +0 -201
- package/examples/browser/boards/portfolio-tracker/portfolio-tracker-http-test.js +0 -357
- package/examples/browser/boards/portfolio-tracker/portfolio-tracker-inference-adapter.js +0 -196
- package/examples/browser/boards/portfolio-tracker/portfolio-tracker-server.js +0 -300
- package/examples/browser/boards/portfolio-tracker/portfolio-tracker-server.py +0 -617
- package/examples/browser/boards/portfolio-tracker/portfolio-tracker.py +0 -366
- package/examples/browser/livecards-browser/index.html +0 -41
- package/examples/browser/step-machine-browser/index.html +0 -367
- package/examples/cli/step-machine-cli/portfolio-tracker/--base-ref/.runtime-out +0 -1
- package/examples/cli/step-machine-cli/portfolio-tracker/--base-ref/board-graph.json +0 -32
- package/examples/cli/step-machine-cli/portfolio-tracker/cards/holdings-table.json +0 -22
- package/examples/cli/step-machine-cli/portfolio-tracker/cards/portfolio-form.json +0 -43
- package/examples/cli/step-machine-cli/portfolio-tracker/cards/portfolio-value.json +0 -15
- package/examples/cli/step-machine-cli/portfolio-tracker/cards/price-fetch.json +0 -15
- package/examples/cli/step-machine-cli/portfolio-tracker/fetch-prices.js +0 -48
- package/examples/cli/step-machine-cli/portfolio-tracker/handlers/_board-cli.js +0 -125
- package/examples/cli/step-machine-cli/portfolio-tracker/handlers/add-cards-cli.js +0 -32
- package/examples/cli/step-machine-cli/portfolio-tracker/handlers/init-board-cli.js +0 -26
- package/examples/cli/step-machine-cli/portfolio-tracker/handlers/poll-status-cli.js +0 -49
- package/examples/cli/step-machine-cli/portfolio-tracker/handlers/reset-board-dir-cli.js +0 -25
- package/examples/cli/step-machine-cli/portfolio-tracker/handlers/retrigger-cli.js +0 -23
- package/examples/cli/step-machine-cli/portfolio-tracker/handlers/status-cli.js +0 -21
- package/examples/cli/step-machine-cli/portfolio-tracker/handlers/update-holdings-cli.js +0 -38
- package/examples/cli/step-machine-cli/portfolio-tracker/handlers/wait-completed-cli.js +0 -48
- package/examples/cli/step-machine-cli/portfolio-tracker/handlers/write-prices-cli.js +0 -31
- package/examples/cli/step-machine-cli/portfolio-tracker/handlers-py/_board_pycli.py +0 -107
- package/examples/cli/step-machine-cli/portfolio-tracker/handlers-py/add-cards.py +0 -51
- package/examples/cli/step-machine-cli/portfolio-tracker/handlers-py/init-board.py +0 -45
- package/examples/cli/step-machine-cli/portfolio-tracker/handlers-py/poll-status.py +0 -71
- package/examples/cli/step-machine-cli/portfolio-tracker/handlers-py/reset-board-dir.py +0 -36
- package/examples/cli/step-machine-cli/portfolio-tracker/inline-python-demo.flow.yaml +0 -26
- package/examples/cli/step-machine-cli/portfolio-tracker/inline-python-handlers.py +0 -39
- package/examples/cli/step-machine-cli/portfolio-tracker/portfolio-tracker-pycli.flow.yaml +0 -80
- package/examples/cli/step-machine-cli/portfolio-tracker/portfolio-tracker.flow.yaml +0 -76
- package/examples/cli/step-machine-cli/portfolio-tracker/portfolio-tracker.input.json +0 -44
- package/examples/cli/step-machine-cli/portfolio-tracker/run-inline-python-demo-pycli.py +0 -43
- package/examples/cli/step-machine-cli/portfolio-tracker/run-portfolio-tracker-pycli.py +0 -77
- package/examples/cli/step-machine-cli/portfolio-tracker/run-portfolio-tracker.bat +0 -28
- package/examples/cli/step-machine-demo/jsonata-init-board-cli.js +0 -31
- package/examples/cli/step-machine-demo/jsonata-init-board.flow.yaml +0 -54
- package/examples/cli/step-machine-demo/one-step-cli-only.flow.yaml +0 -21
- package/examples/cli/step-machine-demo/step-cli-echo-y.js +0 -15
- package/examples/cli/step-machine-demo/step2-double-cli.js +0 -33
- package/examples/cli/step-machine-demo/two-step-math.flow.yaml +0 -93
- package/examples/cli/step-machine-demo/two-step-mixed.flow.yaml +0 -43
- package/examples/example-board/agent-instructions-cardlayout.md +0 -56
- package/examples/example-board/agent-instructions.md +0 -834
- package/examples/example-board/cards/_index.json +0 -47
- package/examples/example-board/demo-shell.html +0 -63
- package/examples/index.html +0 -785
- package/examples/npm-libs/batch/batch-step-machine.ts +0 -121
- package/examples/npm-libs/continuous-event-graph/live-cards-board.ts +0 -215
- package/examples/npm-libs/continuous-event-graph/live-portfolio-dashboard.ts +0 -555
- package/examples/npm-libs/continuous-event-graph/portfolio-tracker.ts +0 -287
- package/examples/npm-libs/continuous-event-graph/reactive-monitoring.ts +0 -265
- package/examples/npm-libs/continuous-event-graph/reactive-pipeline.ts +0 -168
- package/examples/npm-libs/continuous-event-graph/soc-incident-board.ts +0 -287
- package/examples/npm-libs/continuous-event-graph/stock-dashboard.ts +0 -229
- package/examples/npm-libs/event-graph/ci-cd-pipeline.ts +0 -243
- package/examples/npm-libs/event-graph/executor-diamond.ts +0 -165
- package/examples/npm-libs/event-graph/executor-pipeline.ts +0 -161
- package/examples/npm-libs/event-graph/research-pipeline.ts +0 -137
- package/examples/npm-libs/flows/ai-conversation.yaml +0 -116
- package/examples/npm-libs/flows/order-processing.yaml +0 -143
- package/examples/npm-libs/flows/simple-greeting.yaml +0 -54
- package/examples/npm-libs/graph-of-graphs/multi-stage-etl.ts +0 -307
- package/examples/npm-libs/graph-of-graphs/url-processing-pipeline.ts +0 -254
- package/examples/npm-libs/inference/azure-deployment.ts +0 -149
- package/examples/npm-libs/inference/copilot-cli.ts +0 -138
- package/examples/npm-libs/inference/data-pipeline.ts +0 -145
- package/examples/npm-libs/inference/pluggable-adapters.ts +0 -254
- package/examples/npm-libs/node/ai-conversation.ts +0 -195
- package/examples/npm-libs/node/simple-greeting.ts +0 -101
- package/examples/step-machine-cli/portfolio-tracker/cards/holdings-table.json +0 -22
- package/examples/step-machine-cli/portfolio-tracker/cards/portfolio-form.json +0 -43
- package/examples/step-machine-cli/portfolio-tracker/cards/portfolio-value.json +0 -15
- package/examples/step-machine-cli/portfolio-tracker/cards/price-fetch.json +0 -15
- package/examples/step-machine-cli/portfolio-tracker/fetch-prices.js +0 -48
- package/examples/step-machine-cli/portfolio-tracker/handlers/_board-cli.js +0 -57
- package/examples/step-machine-cli/portfolio-tracker/handlers/add-cards-cli.js +0 -27
- package/examples/step-machine-cli/portfolio-tracker/handlers/init-board-cli.js +0 -25
- package/examples/step-machine-cli/portfolio-tracker/handlers/reset-board-dir-cli.js +0 -29
- package/examples/step-machine-cli/portfolio-tracker/handlers/retrigger-cli.js +0 -27
- package/examples/step-machine-cli/portfolio-tracker/handlers/status-cli.js +0 -25
- package/examples/step-machine-cli/portfolio-tracker/handlers/update-holdings-cli.js +0 -37
- package/examples/step-machine-cli/portfolio-tracker/handlers/wait-completed-cli.js +0 -53
- package/examples/step-machine-cli/portfolio-tracker/handlers/write-prices-cli.js +0 -35
- package/examples/step-machine-cli/portfolio-tracker/portfolio-tracker-task-executor.cjs +0 -96
- package/examples/step-machine-cli/portfolio-tracker/portfolio-tracker.flow.yaml +0 -227
- package/examples/step-machine-cli/portfolio-tracker/portfolio-tracker.input.json +0 -38
- package/examples/step-machine-cli/portfolio-tracker/run-portfolio-tracker.bat +0 -28
- package/step-machine-cli.js +0 -407
- /package/{dist/board-livegraph-runtime → cli/browser-api}/jsonata-sync.cjs +0 -0
- /package/{dist/cli → cli}/node/artifacts-store-cli.d.ts +0 -0
- /package/{dist/cli/node/source-cli-task-executor.d.cts → cli/node/batch-runner-cli.d.ts} +0 -0
- /package/{dist/cli → cli}/node/board-live-cards-cli.d.ts +0 -0
- /package/{dist/cli → cli}/node/card-store-cli.d.ts +0 -0
- /package/{dist/card-compute → cli/node}/jsonata-sync.cjs +0 -0
- /package/{dist/cli → cli}/node/source-cli-task-executor.d.ts +0 -0
- /package/examples/{example-board → board}/cards/card-concentration.json +0 -0
- /package/examples/{example-board → board}/cards/card-my-identity.json +0 -0
- /package/examples/{example-board → board}/cards/card-portfolio-action.json +0 -0
- /package/examples/{example-board → board}/cards/card-portfolio-intelligence.json +0 -0
- /package/examples/{example-board → board}/cards/card-portfolio-risks.json +0 -0
- /package/examples/{example-board → board}/cards/card-rebalance-impact.json +0 -0
- /package/examples/{example-board → board}/cards/card-rebalance-sim.json +0 -0
- /package/examples/{example-board/cards/card-portfolio-value.json → board/cards/cardT-portfolio-value.json} +0 -0
- /package/examples/{example-board → board}/demo-chat-handler.js +0 -0
- /package/examples/{example-board → board}/scripts/copilot_wrapper.bat +0 -0
- /package/examples/{example-board → board}/scripts/copilot_wrapper_helper.ps1 +0 -0
- /package/examples/{example-board → board}/scripts/workiq_wrapper.mjs +0 -0
- /package/examples/{browser/boards/portfolio-tracker → board/test}/portfolio-tracker-sse-worker.js +0 -0
- /package/{dist → lib}/batch/index.cjs +0 -0
- /package/{dist → lib}/batch/index.d.cts +0 -0
- /package/{dist → lib}/batch/index.d.ts +0 -0
- /package/{dist → lib}/batch/index.js +0 -0
- /package/{dist/cli/browser-api → lib/board-livegraph-runtime}/jsonata-sync.cjs +0 -0
- /package/{dist → lib}/card-compute/index.d.cts +0 -0
- /package/{dist → lib}/card-compute/index.d.ts +0 -0
- /package/{dist/cli/node → lib/card-compute}/jsonata-sync.cjs +0 -0
- /package/{dist → lib}/config/index.cjs +0 -0
- /package/{dist → lib}/config/index.d.cts +0 -0
- /package/{dist → lib}/config/index.d.ts +0 -0
- /package/{dist → lib}/config/index.js +0 -0
- /package/{dist → lib}/continuous-event-graph/jsonata-sync.cjs +0 -0
- /package/{dist → lib}/event-graph/index.cjs +0 -0
- /package/{dist → lib}/event-graph/index.js +0 -0
- /package/{dist → lib}/jsonata-sync.cjs +0 -0
- /package/{dist → lib}/server-runtime/jsonata-sync.cjs +0 -0
- /package/{dist → lib}/step-machine/index.cjs +0 -0
- /package/{dist → lib}/step-machine/index.js +0 -0
- /package/{dist → lib}/step-machine-public/jsonata-sync.cjs +0 -0
- /package/{dist → lib}/stores/memory.cjs +0 -0
- /package/{dist → lib}/stores/memory.js +0 -0
- /package/{dist → lib}/types-BBhqYGhE.d.cts +0 -0
- /package/{dist → lib}/types-BBhqYGhE.d.ts +0 -0
- /package/{dist → lib}/validate-BAVzUJWa.d.ts +0 -0
- /package/{dist → lib}/validate-Dbu7ygys.d.cts +0 -0
package/browser/live-cards.js
CHANGED
|
@@ -1,55 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
//
|
|
3
|
-
// Schema: Each node has { id } required; all else optional.
|
|
4
|
-
// id, meta, card_data, requires, provides, view
|
|
5
|
-
// Nodes with view render as cards; nodes with no view but with source_defs declared on the
|
|
6
|
-
// underlying card definition render as source pills in canvas mode (source_defs are runtime-only;
|
|
7
|
-
// they are not interpreted here).
|
|
8
|
-
// requires[] — upstream provider tokens; engine subscribes automatically
|
|
9
|
-
// provides[] — [{ bindTo, src }] explicit downstream token bindings
|
|
10
|
-
// computed_values — derived values produced by the runtime; rendered as-is, never recomputed here
|
|
11
|
-
//
|
|
12
|
-
// Rendering contract: this module renders derived state only. View bind paths resolve to one of
|
|
13
|
-
// card_data | requires | computed_values | runtime_state. Raw fetched-source payloads stay in the
|
|
14
|
-
// runtime and never reach the Board.
|
|
15
|
-
//
|
|
16
|
-
// Uses Bootstrap 5 for layout/forms, optional Chart.js for charts.
|
|
17
|
-
//
|
|
18
|
-
// API:
|
|
19
|
-
// const engine = LiveCard.init({ resolve, onPatch, onPatchState, onRefresh, onAction, getChatMessages, markdown, sanitize, chartLib });
|
|
20
|
-
// engine.render(node, el, opts?) — render a card node into a DOM element
|
|
21
|
-
// engine.update(nodeId, patch) — in-place update (status, re-render)
|
|
22
|
-
// engine.destroy(nodeId) — tear down one node
|
|
23
|
-
// engine.destroyAll() — tear down all
|
|
24
|
-
// engine.notify(nodeId, data?) — signal change → downstream recompute
|
|
25
|
-
// engine.subscribe(nodeId, cb) — listen for changes; returns unsub fn
|
|
26
|
-
// engine.appendChatMessage(nodeId, role, text)
|
|
27
|
-
// engine.registerRenderer(name, fn)
|
|
28
|
-
//
|
|
29
|
-
// Reactive board (preferred): state in, view out. No destructive re-renders.
|
|
30
|
-
// const board = LiveCard.Board(engine, el, { initialState, getNodeIds, selectNode, mode?, canvas? });
|
|
31
|
-
// board.setState(nextState) — diff vs prev; per-node updates only
|
|
32
|
-
// board.destroy()
|
|
33
|
-
//
|
|
34
|
-
// Imperative core (advanced): direct node-list manipulation.
|
|
35
|
-
// const core = LiveCard.BoardCore(engine, el, { nodes, positions?, mode, canvas });
|
|
36
|
-
// core.add(node), core.remove(id), core.reorder(ids), core.updateNode(id, model)
|
|
37
|
-
// core.setMode('board'|'canvas'), core.setDevMode(flag), core.autoLayout(), core.clear(), core.destroy()
|
|
38
|
-
|
|
39
|
-
// eslint-disable-next-line no-unused-vars
|
|
40
|
-
var LiveCard = (function () {
|
|
41
|
-
'use strict';
|
|
42
|
-
|
|
43
|
-
// ===========================================================================
|
|
44
|
-
// CSS injection (once)
|
|
45
|
-
// ===========================================================================
|
|
46
|
-
|
|
47
|
-
let _cssInjected = false;
|
|
48
|
-
function _injectCSS() {
|
|
49
|
-
if (_cssInjected) return;
|
|
50
|
-
_cssInjected = true;
|
|
51
|
-
const s = document.createElement('style');
|
|
52
|
-
s.textContent = `
|
|
1
|
+
(function(){'use strict';var lt=(function(){let Ye=false;function et(){if(Ye)return;Ye=true;let T=document.createElement("style");T.textContent=`
|
|
53
2
|
.lc-card { position:relative; }
|
|
54
3
|
.lc-status-dot { display:inline-block; width:8px; height:8px; border-radius:50%; flex-shrink:0; }
|
|
55
4
|
.lc-metric-value { font-size:2rem; font-weight:700; line-height:1.2; }
|
|
@@ -128,2338 +77,21 @@ var LiveCard = (function () {
|
|
|
128
77
|
.lc-chat-body { max-height:200px; }
|
|
129
78
|
.lc-chat-bubble { max-width:95%; }
|
|
130
79
|
}
|
|
131
|
-
`;
|
|
132
|
-
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
// ===========================================================================
|
|
136
|
-
// Global utilities
|
|
137
|
-
// ===========================================================================
|
|
138
|
-
|
|
139
|
-
const _escMap = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' };
|
|
140
|
-
function _esc(str) {
|
|
141
|
-
if (!str) return '';
|
|
142
|
-
return String(str).replace(/[&<>"']/g, ch => _escMap[ch]);
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
function _pathParts(path) {
|
|
146
|
-
if (!path || typeof path !== 'string') return [];
|
|
147
|
-
// Support both dot notation (a.b.c) and bracket notation (a.b[0].c).
|
|
148
|
-
return path.replace(/\[(\d+)\]/g, '.$1').split('.').filter(Boolean);
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
function _deepGet(obj, path) {
|
|
152
|
-
if (!path || !obj) return undefined;
|
|
153
|
-
const parts = _pathParts(path);
|
|
154
|
-
let cur = obj;
|
|
155
|
-
for (let i = 0; i < parts.length; i++) {
|
|
156
|
-
if (cur == null) return undefined;
|
|
157
|
-
cur = cur[parts[i]];
|
|
158
|
-
}
|
|
159
|
-
return cur;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
function _deepSet(obj, path, value) {
|
|
163
|
-
const parts = _pathParts(path);
|
|
164
|
-
if (!parts.length) return;
|
|
165
|
-
let cur = obj;
|
|
166
|
-
for (let i = 0; i < parts.length - 1; i++) {
|
|
167
|
-
if (cur[parts[i]] == null || typeof cur[parts[i]] !== 'object') cur[parts[i]] = {};
|
|
168
|
-
cur = cur[parts[i]];
|
|
169
|
-
}
|
|
170
|
-
cur[parts[parts.length - 1]] = value;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
function _statusDot(status) {
|
|
174
|
-
const colors = { fresh: 'var(--bs-success)', stale: 'var(--bs-warning)', error: 'var(--bs-danger)', loading: 'var(--bs-info)' };
|
|
175
|
-
return `<span class="lc-status-dot" style="background:${colors[status] || 'var(--bs-secondary)'}" title="${_esc(status || 'unknown')}"></span>`;
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
function _timeAgo(iso) {
|
|
179
|
-
if (!iso) return '';
|
|
180
|
-
const d = Math.floor((Date.now() - new Date(iso).getTime()) / 1000);
|
|
181
|
-
if (isNaN(d) || d < 0) return '';
|
|
182
|
-
if (d < 60) return d + 's ago';
|
|
183
|
-
if (d < 3600) return Math.floor(d / 60) + 'm ago';
|
|
184
|
-
if (d < 86400) return Math.floor(d / 3600) + 'h ago';
|
|
185
|
-
return Math.floor(d / 86400) + 'd ago';
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
function _parseThreshold(expr) {
|
|
189
|
-
const m = String(expr).match(/^(<=?|>=?|===?)\s*(.+)$/);
|
|
190
|
-
return m ? { op: m[1], value: parseFloat(m[2]) } : null;
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
function _evalThreshold(value, expr) {
|
|
194
|
-
const t = _parseThreshold(expr);
|
|
195
|
-
if (!t || isNaN(t.value)) return false;
|
|
196
|
-
switch (t.op) {
|
|
197
|
-
case '<': return value < t.value;
|
|
198
|
-
case '<=': return value <= t.value;
|
|
199
|
-
case '>': return value > t.value;
|
|
200
|
-
case '>=': return value >= t.value;
|
|
201
|
-
case '=': case '==': case '===': return value === t.value;
|
|
202
|
-
}
|
|
203
|
-
return false;
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
function _detectChartType(data) {
|
|
207
|
-
if (!data.length) return 'bar';
|
|
208
|
-
const s = data[0];
|
|
209
|
-
if (s.label !== undefined && s.value !== undefined && !s.x && !s.date) return 'pie';
|
|
210
|
-
if (s.x !== undefined || s.date !== undefined) return 'line';
|
|
211
|
-
return 'bar';
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
const _chartColors = ['#0d6efd','#198754','#ffc107','#dc3545','#6f42c1','#0dcaf0','#fd7e14','#20c997','#d63384','#6c757d'];
|
|
215
|
-
|
|
216
|
-
// ===========================================================================
|
|
217
|
-
// init — creates isolated engine instance
|
|
218
|
-
// ===========================================================================
|
|
219
|
-
|
|
220
|
-
function init(config) {
|
|
221
|
-
_injectCSS();
|
|
222
|
-
|
|
223
|
-
const cfg = {
|
|
224
|
-
resolve: config.resolve,
|
|
225
|
-
onPatch: config.onPatch || function () {},
|
|
226
|
-
onPatchState: config.onPatchState || function () {},
|
|
227
|
-
onRefresh: config.onRefresh || null,
|
|
228
|
-
onChat: config.onChat || null,
|
|
229
|
-
markdown: config.markdown || null,
|
|
230
|
-
sanitize: config.sanitize || null,
|
|
231
|
-
chartLib: config.chartLib || null,
|
|
232
|
-
onAction: config.onAction || function () {},
|
|
233
|
-
getChatMessages: config.getChatMessages || null,
|
|
234
|
-
};
|
|
235
|
-
|
|
236
|
-
const _cleanup = {}; // nodeId → { ac, timers, charts, unsubs }
|
|
237
|
-
const _subs = {}; // nodeId → Set<callback>
|
|
238
|
-
const _etState = {}; // stateKey → { baseRows, journalRows|null }
|
|
239
|
-
const _formState = {}; // stateKey → { baseValues, journal }
|
|
240
|
-
const _notesState = {}; // stateKey → { baseContent, journal|null }
|
|
241
|
-
const _todoState = {}; // stateKey → { currentState, pending } for todo dirty tracking
|
|
242
|
-
|
|
243
|
-
/**
|
|
244
|
-
* Overlay a "Saving…" spinner over `el` while a patch is in-flight.
|
|
245
|
-
* The overlay is removed automatically on the next SSE re-render because
|
|
246
|
-
* every editable renderer does `el.innerHTML = …` on refresh.
|
|
247
|
-
*/
|
|
248
|
-
function _showSavingOverlay(el) {
|
|
249
|
-
// Ensure the container is a positioned ancestor so the overlay can fill it.
|
|
250
|
-
if (getComputedStyle(el).position === 'static') el.style.position = 'relative';
|
|
251
|
-
const overlay = document.createElement('div');
|
|
252
|
-
overlay.className = 'lc-saving-overlay';
|
|
253
|
-
overlay.setAttribute('aria-live', 'polite');
|
|
254
|
-
overlay.style.cssText = [
|
|
255
|
-
'position:absolute', 'inset:0',
|
|
256
|
-
'background:rgba(255,255,255,0.78)',
|
|
257
|
-
'display:flex', 'align-items:center', 'justify-content:center',
|
|
258
|
-
'gap:0.5rem', 'z-index:20', 'border-radius:inherit',
|
|
259
|
-
'pointer-events:all', // blocks all clicks on underlying inputs
|
|
260
|
-
].join(';');
|
|
261
|
-
overlay.innerHTML =
|
|
262
|
-
'<span class="spinner-border spinner-border-sm text-primary" role="status" aria-hidden="true"></span>' +
|
|
263
|
-
'<span class="text-primary fw-medium small">Saving…</span>';
|
|
264
|
-
el.appendChild(overlay);
|
|
265
|
-
}
|
|
266
|
-
const _renderers = {}; // kind → fn
|
|
267
|
-
const _nodeEls = {}; // nodeId → { container, resultEl, uid }
|
|
268
|
-
const _chatModal = {
|
|
269
|
-
backdrop: null,
|
|
270
|
-
title: null,
|
|
271
|
-
body: null,
|
|
272
|
-
input: null,
|
|
273
|
-
fileInput: null,
|
|
274
|
-
staged: null,
|
|
275
|
-
sendBtn: null,
|
|
276
|
-
attachBtn: null,
|
|
277
|
-
closeBtn: null,
|
|
278
|
-
currentNodeId: null,
|
|
279
|
-
stagedFiles: [],
|
|
280
|
-
loading: false,
|
|
281
|
-
};
|
|
282
|
-
const _filesModal = {
|
|
283
|
-
backdrop: null,
|
|
284
|
-
title: null,
|
|
285
|
-
body: null,
|
|
286
|
-
staged: null,
|
|
287
|
-
fileInput: null,
|
|
288
|
-
dropzone: null,
|
|
289
|
-
uploadBtn: null,
|
|
290
|
-
attachBtn: null,
|
|
291
|
-
closeBtn: null,
|
|
292
|
-
currentNodeId: null,
|
|
293
|
-
stagedFiles: [],
|
|
294
|
-
pollingTimer: null,
|
|
295
|
-
loading: false,
|
|
296
|
-
};
|
|
297
|
-
|
|
298
|
-
// ---- Helpers ----
|
|
299
|
-
|
|
300
|
-
function _renderMd(text) {
|
|
301
|
-
if (!text) return '';
|
|
302
|
-
const html = cfg.markdown ? cfg.markdown(text) : _esc(text);
|
|
303
|
-
return cfg.sanitize ? cfg.sanitize(html) : html;
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
function _getCleanup(id) {
|
|
307
|
-
if (!_cleanup[id]) _cleanup[id] = { ac: new AbortController(), timers: [], charts: [], unsubs: [] };
|
|
308
|
-
return _cleanup[id];
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
function _ensureChatModal() {
|
|
312
|
-
if (_chatModal.backdrop) return;
|
|
313
|
-
|
|
314
|
-
const backdrop = document.createElement('div');
|
|
315
|
-
backdrop.className = 'lc-chat-modal-backdrop';
|
|
316
|
-
backdrop.innerHTML = '' +
|
|
317
|
-
'<div class="modal-dialog modal-lg modal-dialog-centered" role="dialog" aria-modal="true" aria-label="Card chat">' +
|
|
318
|
-
' <div class="modal-content bg-white">' +
|
|
319
|
-
' <div class="modal-header border-bottom p-3 d-flex align-items-center justify-content-between">' +
|
|
320
|
-
' <h5 class="modal-title lc-chat-modal-title">Chat</h5>' +
|
|
321
|
-
' <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>' +
|
|
322
|
-
' </div>' +
|
|
323
|
-
' <div class="modal-body bg-light" data-lc-chat-body></div>' +
|
|
324
|
-
' <div class="modal-footer flex-column align-items-stretch border-top p-3 gap-3">' +
|
|
325
|
-
' <div data-lc-chat-staged class="small w-100"></div>' +
|
|
326
|
-
' <input type="file" class="d-none" data-lc-chat-file multiple>' +
|
|
327
|
-
' <div class="lc-chat-modal-input-row mt-2">' +
|
|
328
|
-
' <button type="button" class="btn btn-sm btn-outline-secondary" data-lc-chat-attach title="Attach files" aria-label="Attach files">' +
|
|
329
|
-
' <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>' +
|
|
330
|
-
' </button>' +
|
|
331
|
-
' <textarea class="form-control" data-lc-chat-input rows="1" placeholder="Type a message..."></textarea>' +
|
|
332
|
-
' <button type="button" class="btn btn-sm btn-primary" data-lc-chat-send aria-label="Send">' +
|
|
333
|
-
' <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>' +
|
|
334
|
-
' </button>' +
|
|
335
|
-
' </div>' +
|
|
336
|
-
' </div>' +
|
|
337
|
-
' </div>' +
|
|
338
|
-
'</div>';
|
|
339
|
-
|
|
340
|
-
document.body.appendChild(backdrop);
|
|
341
|
-
_chatModal.backdrop = backdrop;
|
|
342
|
-
_chatModal.title = backdrop.querySelector('.lc-chat-modal-title');
|
|
343
|
-
_chatModal.body = backdrop.querySelector('[data-lc-chat-body]');
|
|
344
|
-
_chatModal.input = backdrop.querySelector('[data-lc-chat-input]');
|
|
345
|
-
_chatModal.fileInput = backdrop.querySelector('[data-lc-chat-file]');
|
|
346
|
-
_chatModal.staged = backdrop.querySelector('[data-lc-chat-staged]');
|
|
347
|
-
_chatModal.sendBtn = backdrop.querySelector('[data-lc-chat-send]');
|
|
348
|
-
_chatModal.attachBtn = backdrop.querySelector('[data-lc-chat-attach]');
|
|
349
|
-
_chatModal.closeBtn = backdrop.querySelector('[data-lc-chat-close]');
|
|
350
|
-
|
|
351
|
-
function resizeChatInput() {
|
|
352
|
-
if (!_chatModal.input) return;
|
|
353
|
-
_chatModal.input.style.height = 'auto';
|
|
354
|
-
_chatModal.input.style.height = Math.min(_chatModal.input.scrollHeight, 120) + 'px';
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
const close = function () {
|
|
358
|
-
_chatModal.currentNodeId = null;
|
|
359
|
-
_chatModal.stagedFiles = [];
|
|
360
|
-
_chatModal.staged.innerHTML = '';
|
|
361
|
-
_chatModal.input.value = '';
|
|
362
|
-
resizeChatInput();
|
|
363
|
-
_chatModal.backdrop.classList.remove('lc-open');
|
|
364
|
-
};
|
|
365
|
-
|
|
366
|
-
function renderStagedFiles() {
|
|
367
|
-
if (!_chatModal.stagedFiles.length) {
|
|
368
|
-
_chatModal.staged.innerHTML = '';
|
|
369
|
-
return;
|
|
370
|
-
}
|
|
371
|
-
_chatModal.staged.innerHTML = _chatModal.stagedFiles.map(function (f, i) {
|
|
372
|
-
return '<span class="badge text-bg-light border me-1 mb-1">' + _esc(f.name || 'file') +
|
|
373
|
-
' <button type="button" class="btn btn-sm btn-link text-danger p-0 ms-1" data-lc-rm-file="' + i + '">×</button></span>';
|
|
374
|
-
}).join('');
|
|
375
|
-
_chatModal.staged.querySelectorAll('[data-lc-rm-file]').forEach(function (btn) {
|
|
376
|
-
btn.addEventListener('click', function () {
|
|
377
|
-
const idx = parseInt(btn.getAttribute('data-lc-rm-file') || '-1', 10);
|
|
378
|
-
if (idx >= 0) _chatModal.stagedFiles.splice(idx, 1);
|
|
379
|
-
renderStagedFiles();
|
|
380
|
-
});
|
|
381
|
-
});
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
async function sendMessage() {
|
|
385
|
-
if (_chatModal.loading || !_chatModal.currentNodeId) return;
|
|
386
|
-
const nodeId = _chatModal.currentNodeId;
|
|
387
|
-
const text = (_chatModal.input.value || '').trim();
|
|
388
|
-
const files = _chatModal.stagedFiles.slice();
|
|
389
|
-
if (!text && !files.length) return;
|
|
390
|
-
|
|
391
|
-
_chatModal.loading = true;
|
|
392
|
-
_chatModal.sendBtn.disabled = true;
|
|
393
|
-
_chatModal.attachBtn.disabled = true;
|
|
394
|
-
|
|
395
|
-
_appendPendingModalChatMessage(text);
|
|
396
|
-
|
|
397
|
-
_chatModal.input.value = '';
|
|
398
|
-
_chatModal.stagedFiles = [];
|
|
399
|
-
resizeChatInput();
|
|
400
|
-
renderStagedFiles();
|
|
401
|
-
|
|
402
|
-
try {
|
|
403
|
-
await Promise.resolve(cfg.onAction(nodeId, 'chat-send', { text, files }));
|
|
404
|
-
} catch (err) {
|
|
405
|
-
_clearPendingModalChatMessages();
|
|
406
|
-
_appendModalChatMessage('system', 'Failed to send message: ' + String((err && err.message) || err), []);
|
|
407
|
-
} finally {
|
|
408
|
-
_chatModal.loading = false;
|
|
409
|
-
_chatModal.sendBtn.disabled = false;
|
|
410
|
-
_chatModal.attachBtn.disabled = false;
|
|
411
|
-
}
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
_chatModal.closeBtn.addEventListener('click', close);
|
|
415
|
-
backdrop.addEventListener('click', function (evt) {
|
|
416
|
-
if (evt.target === backdrop) close();
|
|
417
|
-
});
|
|
418
|
-
_chatModal.attachBtn.addEventListener('click', function () {
|
|
419
|
-
_chatModal.fileInput.click();
|
|
420
|
-
});
|
|
421
|
-
_chatModal.fileInput.addEventListener('change', function (evt) {
|
|
422
|
-
const files = evt.target && evt.target.files ? Array.from(evt.target.files) : [];
|
|
423
|
-
for (const f of files) {
|
|
424
|
-
if (!_chatModal.stagedFiles.find(function (x) { return x.name === f.name && x.size === f.size && x.lastModified === f.lastModified; })) {
|
|
425
|
-
_chatModal.stagedFiles.push(f);
|
|
426
|
-
}
|
|
427
|
-
}
|
|
428
|
-
evt.target.value = '';
|
|
429
|
-
renderStagedFiles();
|
|
430
|
-
});
|
|
431
|
-
_chatModal.sendBtn.addEventListener('click', sendMessage);
|
|
432
|
-
_chatModal.input.addEventListener('input', resizeChatInput);
|
|
433
|
-
_chatModal.input.addEventListener('keydown', function (evt) {
|
|
434
|
-
if (evt.key === 'Enter' && !evt.shiftKey) {
|
|
435
|
-
evt.preventDefault();
|
|
436
|
-
sendMessage();
|
|
437
|
-
}
|
|
438
|
-
});
|
|
439
|
-
resizeChatInput();
|
|
440
|
-
document.addEventListener('keydown', function (evt) {
|
|
441
|
-
if (evt.key === 'Escape' && _chatModal.backdrop && _chatModal.backdrop.classList.contains('lc-open')) close();
|
|
442
|
-
});
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
function _normalizeChatMessages(rawMessages) {
|
|
446
|
-
const list = Array.isArray(rawMessages) ? rawMessages : [];
|
|
447
|
-
return list.map(function (msg) {
|
|
448
|
-
if (!msg || typeof msg !== 'object') return null;
|
|
449
|
-
const role = typeof msg.role === 'string' ? msg.role : 'system';
|
|
450
|
-
const text = typeof msg.text === 'string'
|
|
451
|
-
? msg.text
|
|
452
|
-
: (typeof msg.message === 'string' ? msg.message : '');
|
|
453
|
-
const files = Array.isArray(msg.files) ? msg.files : [];
|
|
454
|
-
return { role: role.toLowerCase(), text, files };
|
|
455
|
-
}).filter(Boolean);
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
function _appendModalChatMessage(role, text, files) {
|
|
459
|
-
_ensureChatModal();
|
|
460
|
-
if (!_chatModal.body) return;
|
|
461
|
-
if (!text && !files) return; // skip empty messages
|
|
462
|
-
|
|
463
|
-
const normalizedRole = role === 'user' || role === 'assistant' ? role : 'system';
|
|
464
|
-
const roleClass = normalizedRole === 'user'
|
|
465
|
-
? 'lc-chat-bubble-user'
|
|
466
|
-
: (normalizedRole === 'assistant' ? 'lc-chat-bubble-assistant' : 'lc-chat-bubble-system');
|
|
467
|
-
|
|
468
|
-
const bubble = document.createElement('div');
|
|
469
|
-
bubble.className = 'lc-chat-bubble ' + roleClass;
|
|
470
|
-
|
|
471
|
-
if (normalizedRole !== 'system') {
|
|
472
|
-
// SVG icons: person for user, sparkle-star for assistant
|
|
473
|
-
const userSvg = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="8" r="4"/><path d="M4 20c0-4 3.6-7 8-7s8 3 8 7"/></svg>';
|
|
474
|
-
const asstSvg = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/></svg>';
|
|
475
|
-
const iconEl = document.createElement('span');
|
|
476
|
-
iconEl.className = 'lc-chat-icon';
|
|
477
|
-
iconEl.setAttribute('aria-hidden', 'true');
|
|
478
|
-
iconEl.innerHTML = normalizedRole === 'user' ? userSvg : asstSvg;
|
|
479
|
-
bubble.appendChild(iconEl);
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
const content = document.createElement('div');
|
|
483
|
-
content.className = 'lc-chat-bubble-content';
|
|
484
|
-
if (normalizedRole === 'assistant') {
|
|
485
|
-
content.innerHTML = _renderMd(text || '');
|
|
486
|
-
} else {
|
|
487
|
-
content.textContent = text || '';
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
if (Array.isArray(files) && files.length) {
|
|
491
|
-
const meta = document.createElement('div');
|
|
492
|
-
meta.className = 'small mt-1 text-muted';
|
|
493
|
-
meta.textContent = '\uD83D\uDCCE ' + files.map(function (f) {
|
|
494
|
-
if (!f) return 'file';
|
|
495
|
-
return typeof f === 'string' ? f : (f.name || 'file');
|
|
496
|
-
}).join(', ');
|
|
497
|
-
content.appendChild(meta);
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
bubble.appendChild(content);
|
|
501
|
-
_chatModal.body.appendChild(bubble);
|
|
502
|
-
_chatModal.body.scrollTop = _chatModal.body.scrollHeight;
|
|
503
|
-
}
|
|
504
|
-
|
|
505
|
-
function _appendPendingModalChatMessage(text) {
|
|
506
|
-
_ensureChatModal();
|
|
507
|
-
if (!_chatModal.body) return;
|
|
508
|
-
|
|
509
|
-
const bubble = document.createElement('div');
|
|
510
|
-
bubble.className = 'lc-chat-bubble lc-chat-bubble-user lc-chat-bubble-pending';
|
|
511
|
-
bubble.setAttribute('data-lc-chat-pending', '1');
|
|
512
|
-
bubble.textContent = text || '';
|
|
513
|
-
|
|
514
|
-
const spinner = document.createElement('span');
|
|
515
|
-
spinner.className = 'spinner-border spinner-border-sm';
|
|
516
|
-
spinner.setAttribute('role', 'status');
|
|
517
|
-
spinner.setAttribute('aria-label', 'Sending');
|
|
518
|
-
bubble.appendChild(spinner);
|
|
519
|
-
|
|
520
|
-
_chatModal.body.appendChild(bubble);
|
|
521
|
-
_chatModal.body.scrollTop = _chatModal.body.scrollHeight;
|
|
522
|
-
}
|
|
523
|
-
|
|
524
|
-
function _clearPendingModalChatMessages() {
|
|
525
|
-
if (!_chatModal.body) return;
|
|
526
|
-
_chatModal.body.querySelectorAll('[data-lc-chat-pending="1"]').forEach(function (el) {
|
|
527
|
-
if (el && el.parentNode) el.parentNode.removeChild(el);
|
|
528
|
-
});
|
|
529
|
-
}
|
|
530
|
-
|
|
531
|
-
async function _refreshModalChatHistory(nodeId) {
|
|
532
|
-
if (_chatModal.currentNodeId !== nodeId) return;
|
|
533
|
-
|
|
534
|
-
const node = cfg.resolve(nodeId);
|
|
535
|
-
let messages = [];
|
|
536
|
-
if (typeof cfg.getChatMessages === 'function') {
|
|
537
|
-
try {
|
|
538
|
-
messages = await Promise.resolve(cfg.getChatMessages(nodeId));
|
|
539
|
-
} catch {
|
|
540
|
-
messages = [];
|
|
541
|
-
}
|
|
542
|
-
} else if (node && node.card_data && Array.isArray(node.card_data.messages)) {
|
|
543
|
-
messages = node.card_data.messages;
|
|
544
|
-
}
|
|
545
|
-
|
|
546
|
-
const normalized = _normalizeChatMessages(messages);
|
|
547
|
-
_chatModal.body.innerHTML = '';
|
|
548
|
-
if (!normalized.length) {
|
|
549
|
-
_chatModal.body.innerHTML = '<div class="text-muted small">No messages yet.</div>';
|
|
550
|
-
_syncProcessingBar(nodeId);
|
|
551
|
-
return;
|
|
552
|
-
}
|
|
553
|
-
normalized.forEach(function (m) { _appendModalChatMessage(m.role, m.text, m.files); });
|
|
554
|
-
_syncProcessingBar(nodeId);
|
|
555
|
-
}
|
|
556
|
-
|
|
557
|
-
function _syncProcessingBar(nodeId) {
|
|
558
|
-
if (!_chatModal.body) return;
|
|
559
|
-
const node = nodeId ? cfg.resolve(nodeId) : null;
|
|
560
|
-
const isProcessing = !!(node && node.card_data && node.card_data.__chat_signal && node.card_data.__chat_signal.processing);
|
|
561
|
-
let ind = _chatModal.body.querySelector('.lc-chat-processing');
|
|
562
|
-
if (isProcessing) {
|
|
563
|
-
if (!ind) {
|
|
564
|
-
ind = document.createElement('div');
|
|
565
|
-
ind.className = 'lc-chat-processing';
|
|
566
|
-
ind.innerHTML = '<span class="spinner-border spinner-border-sm" role="status" aria-label="AI working"></span><span>\u2728 AI working\u2026</span>';
|
|
567
|
-
_chatModal.body.appendChild(ind);
|
|
568
|
-
}
|
|
569
|
-
_chatModal.body.scrollTop = _chatModal.body.scrollHeight;
|
|
570
|
-
} else {
|
|
571
|
-
if (ind) ind.remove();
|
|
572
|
-
}
|
|
573
|
-
}
|
|
574
|
-
|
|
575
|
-
async function openChatModal(nodeId) {
|
|
576
|
-
_ensureChatModal();
|
|
577
|
-
const node = cfg.resolve(nodeId);
|
|
578
|
-
if (!node) return;
|
|
579
|
-
const title = (node.card && node.card.meta && node.card.meta.title) || node.id;
|
|
580
|
-
_chatModal.currentNodeId = nodeId;
|
|
581
|
-
_chatModal.title.textContent = 'Chat: ' + title;
|
|
582
|
-
_chatModal.body.innerHTML = '<div class="text-muted small">Loading...</div>';
|
|
583
|
-
_chatModal.backdrop.classList.add('lc-open');
|
|
584
|
-
|
|
585
|
-
// Disable input controls when card_data.features.chat.disabled is true
|
|
586
|
-
const chatDisabled = !!(node.card_data && node.card_data.features && node.card_data.features.chat && node.card_data.features.chat.disabled);
|
|
587
|
-
_chatModal.input.disabled = chatDisabled;
|
|
588
|
-
_chatModal.attachBtn.disabled = chatDisabled;
|
|
589
|
-
_chatModal.sendBtn.disabled = chatDisabled;
|
|
590
|
-
_chatModal.input.placeholder = chatDisabled ? 'Chat is disabled for this card.' : 'Type a message...';
|
|
591
|
-
|
|
592
|
-
if (!chatDisabled) _chatModal.input.focus();
|
|
593
|
-
await _refreshModalChatHistory(nodeId);
|
|
594
|
-
}
|
|
595
|
-
|
|
596
|
-
function _ensureFilesModal() {
|
|
597
|
-
if (_filesModal.backdrop) return;
|
|
598
|
-
|
|
599
|
-
const backdrop = document.createElement('div');
|
|
600
|
-
backdrop.className = 'lc-files-modal-backdrop';
|
|
601
|
-
backdrop.innerHTML = '' +
|
|
602
|
-
'<div class="modal-dialog modal-lg modal-dialog-centered" role="dialog" aria-modal="true" aria-label="Card files">' +
|
|
603
|
-
' <div class="modal-content bg-white">' +
|
|
604
|
-
' <div class="modal-header border-bottom p-3 d-flex align-items-center justify-content-between">' +
|
|
605
|
-
' <h5 class="modal-title lc-files-modal-title">Files</h5>' +
|
|
606
|
-
' <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>' +
|
|
607
|
-
' </div>' +
|
|
608
|
-
' <div class="modal-body bg-light" data-lc-files-body></div>' +
|
|
609
|
-
' <div class="modal-footer flex-column align-items-stretch border-top p-3 gap-3">' +
|
|
610
|
-
' <div class="lc-dropzone border-2 border-dashed p-4 text-center cursor-pointer rounded" data-lc-files-dz>' +
|
|
611
|
-
' <div class="small text-muted mb-2">Drop files here or click to browse</div>' +
|
|
612
|
-
' <input type="file" class="d-none" data-lc-files-input multiple>' +
|
|
613
|
-
' </div>' +
|
|
614
|
-
' <div data-lc-files-staged class="small w-100 d-flex flex-wrap gap-2"></div>' +
|
|
615
|
-
' <div class="d-flex justify-content-end gap-2 w-100">' +
|
|
616
|
-
' <button type="button" class="btn btn-sm btn-outline-secondary" data-lc-files-attach>Select files</button>' +
|
|
617
|
-
' <button type="button" class="btn btn-sm btn-primary" data-lc-files-upload>Upload</button>' +
|
|
618
|
-
' </div>' +
|
|
619
|
-
' </div>' +
|
|
620
|
-
' </div>' +
|
|
621
|
-
'</div>';
|
|
622
|
-
|
|
623
|
-
document.body.appendChild(backdrop);
|
|
624
|
-
_filesModal.backdrop = backdrop;
|
|
625
|
-
_filesModal.title = backdrop.querySelector('.lc-files-modal-title');
|
|
626
|
-
_filesModal.body = backdrop.querySelector('[data-lc-files-body]');
|
|
627
|
-
_filesModal.staged = backdrop.querySelector('[data-lc-files-staged]');
|
|
628
|
-
_filesModal.fileInput = backdrop.querySelector('[data-lc-files-input]');
|
|
629
|
-
_filesModal.dropzone = backdrop.querySelector('[data-lc-files-dz]');
|
|
630
|
-
_filesModal.uploadBtn = backdrop.querySelector('[data-lc-files-upload]');
|
|
631
|
-
_filesModal.attachBtn = backdrop.querySelector('[data-lc-files-attach]');
|
|
632
|
-
_filesModal.closeBtn = backdrop.querySelector('[data-lc-files-close]');
|
|
633
|
-
|
|
634
|
-
const close = function () {
|
|
635
|
-
_filesModal.currentNodeId = null;
|
|
636
|
-
_filesModal.stagedFiles = [];
|
|
637
|
-
_filesModal.staged.innerHTML = '';
|
|
638
|
-
_filesModal.backdrop.classList.remove('lc-open');
|
|
639
|
-
if (_filesModal.pollingTimer) {
|
|
640
|
-
clearInterval(_filesModal.pollingTimer);
|
|
641
|
-
_filesModal.pollingTimer = null;
|
|
642
|
-
}
|
|
643
|
-
};
|
|
644
|
-
|
|
645
|
-
function renderStagedFiles() {
|
|
646
|
-
if (!_filesModal.stagedFiles.length) {
|
|
647
|
-
_filesModal.staged.innerHTML = '';
|
|
648
|
-
return;
|
|
649
|
-
}
|
|
650
|
-
_filesModal.staged.innerHTML = _filesModal.stagedFiles.map(function (f, i) {
|
|
651
|
-
return '<span class="badge text-bg-light border me-1 mb-1">' + _esc(f.name || 'file') +
|
|
652
|
-
' <button type="button" class="btn btn-sm btn-link text-danger p-0 ms-1" data-lc-files-rm="' + i + '">×</button></span>';
|
|
653
|
-
}).join('');
|
|
654
|
-
_filesModal.staged.querySelectorAll('[data-lc-files-rm]').forEach(function (btn) {
|
|
655
|
-
btn.addEventListener('click', function () {
|
|
656
|
-
const idx = parseInt(btn.getAttribute('data-lc-files-rm') || '-1', 10);
|
|
657
|
-
if (idx >= 0) _filesModal.stagedFiles.splice(idx, 1);
|
|
658
|
-
renderStagedFiles();
|
|
659
|
-
});
|
|
660
|
-
});
|
|
661
|
-
}
|
|
662
|
-
|
|
663
|
-
function addFiles(fileList) {
|
|
664
|
-
const files = Array.from(fileList || []);
|
|
665
|
-
for (const f of files) {
|
|
666
|
-
if (!_filesModal.stagedFiles.find(function (x) { return x.name === f.name && x.size === f.size && x.lastModified === f.lastModified; })) {
|
|
667
|
-
_filesModal.stagedFiles.push(f);
|
|
668
|
-
}
|
|
669
|
-
}
|
|
670
|
-
renderStagedFiles();
|
|
671
|
-
}
|
|
672
|
-
|
|
673
|
-
async function uploadFiles() {
|
|
674
|
-
if (_filesModal.loading || !_filesModal.currentNodeId || !_filesModal.stagedFiles.length) return;
|
|
675
|
-
const nodeId = _filesModal.currentNodeId;
|
|
676
|
-
const files = _filesModal.stagedFiles.slice();
|
|
677
|
-
_filesModal.loading = true;
|
|
678
|
-
_filesModal.uploadBtn.disabled = true;
|
|
679
|
-
_filesModal.attachBtn.disabled = true;
|
|
680
|
-
_filesModal.dropzone.classList.add('lc-disabled');
|
|
681
|
-
|
|
682
|
-
try {
|
|
683
|
-
await Promise.resolve(cfg.onAction(nodeId, 'file-upload', { files }));
|
|
684
|
-
_filesModal.stagedFiles = [];
|
|
685
|
-
renderStagedFiles();
|
|
686
|
-
_refreshFilesModalList(nodeId);
|
|
687
|
-
} catch (err) {
|
|
688
|
-
_filesModal.staged.innerHTML = '<span class="text-danger">Upload failed: ' + _esc(String((err && err.message) || err)) + '</span>';
|
|
689
|
-
} finally {
|
|
690
|
-
_filesModal.loading = false;
|
|
691
|
-
_filesModal.uploadBtn.disabled = false;
|
|
692
|
-
_filesModal.attachBtn.disabled = false;
|
|
693
|
-
_filesModal.dropzone.classList.remove('lc-disabled');
|
|
694
|
-
}
|
|
695
|
-
}
|
|
696
|
-
|
|
697
|
-
_filesModal.closeBtn.addEventListener('click', close);
|
|
698
|
-
backdrop.addEventListener('click', function (evt) {
|
|
699
|
-
if (evt.target === backdrop) close();
|
|
700
|
-
});
|
|
701
|
-
_filesModal.attachBtn.addEventListener('click', function () {
|
|
702
|
-
_filesModal.fileInput.click();
|
|
703
|
-
});
|
|
704
|
-
_filesModal.fileInput.addEventListener('change', function (evt) {
|
|
705
|
-
addFiles(evt.target && evt.target.files ? evt.target.files : []);
|
|
706
|
-
evt.target.value = '';
|
|
707
|
-
});
|
|
708
|
-
_filesModal.uploadBtn.addEventListener('click', uploadFiles);
|
|
709
|
-
_filesModal.dropzone.addEventListener('click', function () {
|
|
710
|
-
if (!_filesModal.loading) _filesModal.fileInput.click();
|
|
711
|
-
});
|
|
712
|
-
_filesModal.dropzone.addEventListener('dragover', function (evt) {
|
|
713
|
-
evt.preventDefault();
|
|
714
|
-
_filesModal.dropzone.classList.add('lc-drag-over');
|
|
715
|
-
});
|
|
716
|
-
_filesModal.dropzone.addEventListener('dragleave', function () {
|
|
717
|
-
_filesModal.dropzone.classList.remove('lc-drag-over');
|
|
718
|
-
});
|
|
719
|
-
_filesModal.dropzone.addEventListener('drop', function (evt) {
|
|
720
|
-
evt.preventDefault();
|
|
721
|
-
_filesModal.dropzone.classList.remove('lc-drag-over');
|
|
722
|
-
addFiles(evt.dataTransfer && evt.dataTransfer.files ? evt.dataTransfer.files : []);
|
|
723
|
-
});
|
|
724
|
-
document.addEventListener('keydown', function (evt) {
|
|
725
|
-
if (evt.key === 'Escape' && _filesModal.backdrop && _filesModal.backdrop.classList.contains('lc-open')) close();
|
|
726
|
-
});
|
|
727
|
-
}
|
|
728
|
-
|
|
729
|
-
function _currentNodeFiles(nodeId) {
|
|
730
|
-
const node = cfg.resolve(nodeId);
|
|
731
|
-
const files = node && node.card_data && Array.isArray(node.card_data.files) ? node.card_data.files : [];
|
|
732
|
-
return files.filter(Boolean);
|
|
733
|
-
}
|
|
734
|
-
|
|
735
|
-
function _refreshFilesModalList(nodeId) {
|
|
736
|
-
if (_filesModal.currentNodeId !== nodeId) return;
|
|
737
|
-
const files = _currentNodeFiles(nodeId);
|
|
738
|
-
if (!files.length) {
|
|
739
|
-
_filesModal.body.innerHTML = '<div class="alert alert-light border small mb-0">No files uploaded yet.</div>';
|
|
740
|
-
return;
|
|
741
|
-
}
|
|
742
|
-
|
|
743
|
-
let h = '<div class="list-group list-group-flush">';
|
|
744
|
-
files.forEach(function (f, idx) {
|
|
745
|
-
const fileName = f && (f.name || f.stored_name) ? (f.name || f.stored_name) : 'file';
|
|
746
|
-
const sizeText = f && typeof f.size === 'number' ? ('size: ' + f.size + ' bytes') : '';
|
|
747
|
-
const stored = f && f.stored_name ? String(f.stored_name) : '';
|
|
748
|
-
const dl = stored
|
|
749
|
-
? '/api/example-board/server/cards/' + encodeURIComponent(nodeId) + '/files/' + idx + '?sn=' + encodeURIComponent(stored)
|
|
750
|
-
: null;
|
|
751
|
-
h += '<div class="list-group-item d-flex align-items-center justify-content-between gap-2">';
|
|
752
|
-
h += '<div class="text-truncate"><div class="small fw-medium">' + _esc(fileName) + '</div>';
|
|
753
|
-
h += '<div class="small text-muted">' + _esc(sizeText) + '</div></div>';
|
|
754
|
-
if (dl) {
|
|
755
|
-
h += '<a class="btn btn-sm btn-outline-secondary flex-shrink-0" href="' + dl + '">Download</a>';
|
|
756
|
-
}
|
|
757
|
-
h += '</div>';
|
|
758
|
-
});
|
|
759
|
-
h += '</div>';
|
|
760
|
-
_filesModal.body.innerHTML = h;
|
|
761
|
-
}
|
|
762
|
-
|
|
763
|
-
function openFilesModal(nodeId) {
|
|
764
|
-
_ensureFilesModal();
|
|
765
|
-
const node = cfg.resolve(nodeId);
|
|
766
|
-
if (!node) return;
|
|
767
|
-
|
|
768
|
-
const title = (node.card && node.card.meta && node.card.meta.title) || node.id;
|
|
769
|
-
_filesModal.currentNodeId = nodeId;
|
|
770
|
-
_filesModal.title.textContent = 'Files: ' + title;
|
|
771
|
-
_filesModal.backdrop.classList.add('lc-open');
|
|
772
|
-
|
|
773
|
-
// Disable upload controls when card_data.features.files.disabled is true
|
|
774
|
-
const filesDisabled = !!(node.card_data && node.card_data.features && node.card_data.features.files && node.card_data.features.files.disabled);
|
|
775
|
-
_filesModal.dropzone.classList.toggle('lc-disabled', filesDisabled);
|
|
776
|
-
_filesModal.attachBtn.disabled = filesDisabled;
|
|
777
|
-
_filesModal.uploadBtn.disabled = filesDisabled;
|
|
778
|
-
_filesModal.fileInput.disabled = filesDisabled;
|
|
779
|
-
|
|
780
|
-
_refreshFilesModalList(nodeId);
|
|
781
|
-
|
|
782
|
-
if (_filesModal.pollingTimer) clearInterval(_filesModal.pollingTimer);
|
|
783
|
-
_filesModal.pollingTimer = setInterval(function () {
|
|
784
|
-
_refreshFilesModalList(nodeId);
|
|
785
|
-
}, 1000);
|
|
786
|
-
}
|
|
787
|
-
|
|
788
|
-
function _resolveBind(node, bind) {
|
|
789
|
-
if (!bind || typeof bind !== 'string') return undefined;
|
|
790
|
-
const parts = _pathParts(bind);
|
|
791
|
-
if (!parts.length) return undefined;
|
|
792
|
-
|
|
793
|
-
const root = parts[0];
|
|
794
|
-
const rest = parts.slice(1).join('.');
|
|
795
|
-
const ns = {
|
|
796
|
-
card: node && node.card ? node.card : {},
|
|
797
|
-
card_data: node && node.card_data ? node.card_data : {},
|
|
798
|
-
requires: node && node.requires ? node.requires : {},
|
|
799
|
-
computed_values: node && node.computed_values ? node.computed_values : {},
|
|
800
|
-
runtime_state: node && node.runtime_state ? node.runtime_state : {},
|
|
801
|
-
};
|
|
802
|
-
|
|
803
|
-
if (!Object.prototype.hasOwnProperty.call(ns, root)) return undefined;
|
|
804
|
-
return rest ? _deepGet(ns[root], rest) : ns[root];
|
|
805
|
-
}
|
|
806
|
-
|
|
807
|
-
// ---- Pub/sub ----
|
|
808
|
-
|
|
809
|
-
function notify(nodeId, data) {
|
|
810
|
-
const cbs = _subs[nodeId];
|
|
811
|
-
if (cbs) cbs.forEach(cb => { try { cb(nodeId, data); } catch (e) { console.error('LiveCard notify error', e); } });
|
|
812
|
-
}
|
|
813
|
-
|
|
814
|
-
function subscribe(nodeId, cb) {
|
|
815
|
-
if (!_subs[nodeId]) _subs[nodeId] = new Set();
|
|
816
|
-
_subs[nodeId].add(cb);
|
|
817
|
-
return () => _subs[nodeId].delete(cb);
|
|
818
|
-
}
|
|
819
|
-
|
|
820
|
-
function _autoSubscribe(node) {
|
|
821
|
-
const requires = (node && node.card && Array.isArray(node.card.requires)) ? node.card.requires : [];
|
|
822
|
-
if (!requires.length) return;
|
|
823
|
-
const cleanup = _getCleanup(node.id);
|
|
824
|
-
|
|
825
|
-
// Resolve required tokens to upstream node IDs via provides declarations.
|
|
826
|
-
// Build a token→nodeId map from all nodes the engine knows about.
|
|
827
|
-
const tokenMap = {};
|
|
828
|
-
const allNodeIds = Object.keys(_subs).concat(Object.keys(_nodeEls));
|
|
829
|
-
allNodeIds.forEach(function(nid) {
|
|
830
|
-
const n = cfg.resolve(nid);
|
|
831
|
-
if (!n || !n.card) return;
|
|
832
|
-
var provides = (Array.isArray(n.card.provides) && n.card.provides.length)
|
|
833
|
-
? n.card.provides.map(function(p) { return typeof p === 'string' ? p : (p.bindTo || p); })
|
|
834
|
-
: [n.id];
|
|
835
|
-
provides.forEach(function(tok) { tokenMap[tok] = n.id; });
|
|
836
|
-
});
|
|
837
|
-
|
|
838
|
-
// Subscribe to each upstream provider node (deduplicated)
|
|
839
|
-
const seen = {};
|
|
840
|
-
const upIds = [];
|
|
841
|
-
requires.forEach(function(token) {
|
|
842
|
-
var srcId = tokenMap[token] || token; // fallback: treat token as nodeId
|
|
843
|
-
if (!seen[srcId]) { seen[srcId] = true; upIds.push(srcId); }
|
|
844
|
-
});
|
|
845
|
-
|
|
846
|
-
cleanup.unsubs = upIds.map(upId => subscribe(upId, () => {
|
|
847
|
-
const info = _nodeEls[node.id];
|
|
848
|
-
if (!info || !info.resultEl) return;
|
|
849
|
-
const updated = cfg.resolve(node.id);
|
|
850
|
-
if (!updated) return;
|
|
851
|
-
_renderElements(updated, info.resultEl);
|
|
852
|
-
notify(node.id);
|
|
853
|
-
}));
|
|
854
|
-
}
|
|
855
|
-
|
|
856
|
-
// ===========================================================================
|
|
857
|
-
// Element renderers — each: (data, el, elemDef, node)
|
|
858
|
-
// ===========================================================================
|
|
859
|
-
|
|
860
|
-
// ---- table ----
|
|
861
|
-
|
|
862
|
-
function _renderTable(data, el, elemDef, node) {
|
|
863
|
-
const ed = elemDef.data || {};
|
|
864
|
-
if (!Array.isArray(data) || !data.length) {
|
|
865
|
-
el.innerHTML = `<p class="text-muted small">${_esc(ed.placeholder || 'No data')}</p>`;
|
|
866
|
-
return;
|
|
867
|
-
}
|
|
868
|
-
|
|
869
|
-
const limit = Math.min(data.length, ed.maxRows || 200);
|
|
870
|
-
const colSet = new Set();
|
|
871
|
-
for (let i = 0; i < Math.min(data.length, limit); i++) Object.keys(data[i]).forEach(k => colSet.add(k));
|
|
872
|
-
const cols = (ed.columns && ed.columns.length) ? ed.columns : [...colSet];
|
|
873
|
-
const sortable = ed.sortable !== false;
|
|
874
|
-
|
|
875
|
-
let sortCol = null, sortDir = 'asc';
|
|
876
|
-
const cleanup = _getCleanup(node.id);
|
|
877
|
-
|
|
878
|
-
function build() {
|
|
879
|
-
let rows = data.slice(0, limit);
|
|
880
|
-
if (sortCol !== null && sortable) {
|
|
881
|
-
rows = rows.slice().sort((a, b) => {
|
|
882
|
-
const av = a[cols[sortCol]], bv = b[cols[sortCol]];
|
|
883
|
-
if (av == null) return 1; if (bv == null) return -1;
|
|
884
|
-
if (typeof av === 'number' && typeof bv === 'number') return sortDir === 'asc' ? av - bv : bv - av;
|
|
885
|
-
return sortDir === 'asc' ? String(av).localeCompare(String(bv)) : String(bv).localeCompare(String(av));
|
|
886
|
-
});
|
|
887
|
-
}
|
|
888
|
-
|
|
889
|
-
let h = '<div class="table-responsive"><table class="table table-sm table-striped table-hover mb-0"><thead><tr>';
|
|
890
|
-
cols.forEach((c, i) => {
|
|
891
|
-
const arrow = sortCol === i ? (sortDir === 'asc' ? ' ↑' : ' ↓') : '';
|
|
892
|
-
const cursor = sortable ? ' style="cursor:pointer"' : '';
|
|
893
|
-
h += `<th class="small text-nowrap"${cursor} data-col="${i}">${_esc(c)}${arrow}</th>`;
|
|
894
|
-
});
|
|
895
|
-
h += '</tr></thead><tbody>';
|
|
896
|
-
rows.forEach(row => {
|
|
897
|
-
h += '<tr>';
|
|
898
|
-
cols.forEach(c => { const v = row[c]; h += `<td class="small">${_esc(v != null ? String(v) : '')}</td>`; });
|
|
899
|
-
h += '</tr>';
|
|
900
|
-
});
|
|
901
|
-
h += '</tbody></table></div>';
|
|
902
|
-
if (data.length > limit) h += `<p class="text-muted small mt-1">Showing ${limit} of ${data.length} rows</p>`;
|
|
903
|
-
el.innerHTML = h;
|
|
904
|
-
|
|
905
|
-
if (sortable) {
|
|
906
|
-
el.querySelectorAll('th[data-col]').forEach(th => {
|
|
907
|
-
th.addEventListener('click', () => {
|
|
908
|
-
const c = parseInt(th.dataset.col);
|
|
909
|
-
if (sortCol === c) sortDir = sortDir === 'asc' ? 'desc' : 'asc';
|
|
910
|
-
else { sortCol = c; sortDir = 'asc'; }
|
|
911
|
-
build();
|
|
912
|
-
}, { signal: cleanup.ac.signal });
|
|
913
|
-
});
|
|
914
|
-
}
|
|
915
|
-
}
|
|
916
|
-
build();
|
|
917
|
-
}
|
|
918
|
-
|
|
919
|
-
// ---- filter ----
|
|
920
|
-
|
|
921
|
-
function _renderFilter(data, el, elemDef, node) {
|
|
922
|
-
const cleanup = _getCleanup(node.id);
|
|
923
|
-
const signal = cleanup.ac.signal;
|
|
924
|
-
const ed = elemDef.data || {};
|
|
925
|
-
const writeTo = ed.writeTo;
|
|
926
|
-
const values = writeTo ? (_resolveBind(node, writeTo) || {}) : {};
|
|
927
|
-
const fields = (ed.fields && ed.fields.properties) || {};
|
|
928
|
-
|
|
929
|
-
const keys = (data && typeof data === 'object' && !Array.isArray(data)) ? Object.keys(data) : [];
|
|
930
|
-
if (!keys.length) { el.innerHTML = '<p class="text-muted small">No filter options</p>'; return; }
|
|
931
|
-
|
|
932
|
-
let h = '<div class="row g-2">';
|
|
933
|
-
keys.forEach(key => {
|
|
934
|
-
const options = Array.isArray(data[key]) ? data[key] : [];
|
|
935
|
-
const label = (fields[key] && fields[key].title) || key;
|
|
936
|
-
h += `<div class="col-12 col-sm-6 col-md-4"><label class="form-label small mb-1">${_esc(label)}</label>`;
|
|
937
|
-
h += `<select class="form-select form-select-sm" data-fk="${_esc(key)}"><option value="">All</option>`;
|
|
938
|
-
options.forEach(opt => {
|
|
939
|
-
const sel = String(opt) === String(values[key] || '') ? ' selected' : '';
|
|
940
|
-
h += `<option value="${_esc(String(opt))}"${sel}>${_esc(String(opt))}</option>`;
|
|
941
|
-
});
|
|
942
|
-
h += '</select></div>';
|
|
943
|
-
});
|
|
944
|
-
h += '</div>';
|
|
945
|
-
el.innerHTML = h;
|
|
946
|
-
|
|
947
|
-
el.querySelectorAll('select[data-fk]').forEach(sel => {
|
|
948
|
-
sel.addEventListener('change', () => {
|
|
949
|
-
const nv = {};
|
|
950
|
-
el.querySelectorAll('select[data-fk]').forEach(s => { if (s.value) nv[s.dataset.fk] = s.value; });
|
|
951
|
-
if (writeTo) _deepSet(node, writeTo, nv);
|
|
952
|
-
cfg.onPatchState(node.id, { fieldValues: nv });
|
|
953
|
-
notify(node.id, nv);
|
|
954
|
-
}, { signal });
|
|
955
|
-
});
|
|
956
|
-
}
|
|
957
|
-
|
|
958
|
-
// ---- metric ----
|
|
959
|
-
|
|
960
|
-
function _renderMetric(data, el, elemDef) {
|
|
961
|
-
let title = elemDef.label || '', value = '—', detail = '';
|
|
962
|
-
if (data && typeof data === 'object' && !Array.isArray(data)) {
|
|
963
|
-
title = data.title || data.label || data.metric || title;
|
|
964
|
-
value = data.value != null ? String(data.value) : '—';
|
|
965
|
-
detail = data.detail || '';
|
|
966
|
-
} else if (data != null) {
|
|
967
|
-
value = String(data);
|
|
968
|
-
}
|
|
969
|
-
let h = '<div class="text-center py-2">';
|
|
970
|
-
if (title) h += `<div class="text-muted small">${_esc(title)}</div>`;
|
|
971
|
-
h += `<div class="lc-metric-value">${_esc(value)}</div>`;
|
|
972
|
-
if (detail) h += `<div class="small mt-1">${_renderMd(detail)}</div>`;
|
|
973
|
-
h += '</div>';
|
|
974
|
-
el.innerHTML = h;
|
|
975
|
-
}
|
|
976
|
-
|
|
977
|
-
// ---- list ----
|
|
978
|
-
|
|
979
|
-
function _renderList(data, el, elemDef, node) {
|
|
980
|
-
const ed = elemDef.data || {};
|
|
981
|
-
if (data == null) { el.innerHTML = ''; return; }
|
|
982
|
-
|
|
983
|
-
if (typeof data === 'object' && !Array.isArray(data)) {
|
|
984
|
-
let h = '<dl class="row mb-0">';
|
|
985
|
-
Object.entries(data).forEach(([k, v]) => {
|
|
986
|
-
h += `<dt class="col-sm-5 small text-muted text-truncate">${_esc(k)}</dt>`;
|
|
987
|
-
h += `<dd class="col-sm-7 small mb-1">${_esc(v != null ? String(v) : '—')}</dd>`;
|
|
988
|
-
});
|
|
989
|
-
el.innerHTML = h + '</dl>';
|
|
990
|
-
return;
|
|
991
|
-
}
|
|
992
|
-
|
|
993
|
-
if (Array.isArray(data)) {
|
|
994
|
-
if (!data.length) { el.innerHTML = `<p class="text-muted small">${_esc(ed.placeholder || 'Empty')}</p>`; return; }
|
|
995
|
-
if (typeof data[0] === 'string' || typeof data[0] === 'number') {
|
|
996
|
-
const max = ed.maxRows || data.length;
|
|
997
|
-
let h = '<ul class="list-unstyled mb-0">';
|
|
998
|
-
data.slice(0, max).forEach(item => { h += `<li class="small mb-1">• ${_esc(String(item))}</li>`; });
|
|
999
|
-
el.innerHTML = h + '</ul>';
|
|
1000
|
-
return;
|
|
1001
|
-
}
|
|
1002
|
-
_renderTable(data, el, elemDef, node);
|
|
1003
|
-
return;
|
|
1004
|
-
}
|
|
1005
|
-
|
|
1006
|
-
el.innerHTML = `<div class="small">${_renderMd(String(data))}</div>`;
|
|
1007
|
-
}
|
|
1008
|
-
|
|
1009
|
-
// ---- chart ----
|
|
1010
|
-
|
|
1011
|
-
function _renderChart(data, el, elemDef, node) {
|
|
1012
|
-
const ed = elemDef.data || {};
|
|
1013
|
-
if (!cfg.chartLib) { _renderTable(data, el, elemDef, node); return; }
|
|
1014
|
-
if (!Array.isArray(data) || !data.length) { el.innerHTML = '<p class="text-muted small">No chart data</p>'; return; }
|
|
1015
|
-
|
|
1016
|
-
const cleanup = _getCleanup(node.id);
|
|
1017
|
-
const chartKey = elemDef.id || ('chart-' + Math.random().toString(36).slice(2, 8));
|
|
1018
|
-
const existingIdx = cleanup.charts.findIndex(c => c.key === chartKey);
|
|
1019
|
-
if (existingIdx >= 0) { cleanup.charts[existingIdx].inst.destroy(); cleanup.charts.splice(existingIdx, 1); }
|
|
1020
|
-
|
|
1021
|
-
const type = ed.chartType || _detectChartType(data);
|
|
1022
|
-
el.innerHTML = '<div class="lc-chart-wrap"><canvas></canvas></div>';
|
|
1023
|
-
const ctx = el.querySelector('canvas').getContext('2d');
|
|
1024
|
-
|
|
1025
|
-
let chartCfg;
|
|
1026
|
-
if (type === 'pie' || type === 'doughnut') {
|
|
1027
|
-
chartCfg = {
|
|
1028
|
-
type,
|
|
1029
|
-
data: {
|
|
1030
|
-
labels: data.map(r => r.label || r.name || ''),
|
|
1031
|
-
datasets: [{ data: data.map(r => r.value || 0), backgroundColor: _chartColors.slice(0, data.length) }],
|
|
1032
|
-
},
|
|
1033
|
-
};
|
|
1034
|
-
} else if (type === 'line') {
|
|
1035
|
-
chartCfg = {
|
|
1036
|
-
type: 'line',
|
|
1037
|
-
data: {
|
|
1038
|
-
labels: data.map(r => r.x || r.date || r.label || ''),
|
|
1039
|
-
datasets: [{ label: elemDef.label || 'Value', data: data.map(r => r.y || r.value || 0), borderColor: _chartColors[0], tension: 0.3, fill: false }],
|
|
1040
|
-
},
|
|
1041
|
-
};
|
|
1042
|
-
} else {
|
|
1043
|
-
const numKeys = Object.keys(data[0]).filter(k => typeof data[0][k] === 'number');
|
|
1044
|
-
const labelKey = Object.keys(data[0]).find(k => typeof data[0][k] === 'string');
|
|
1045
|
-
chartCfg = {
|
|
1046
|
-
type: 'bar',
|
|
1047
|
-
data: {
|
|
1048
|
-
labels: data.map(r => r.label || r.name || (labelKey ? r[labelKey] : '')),
|
|
1049
|
-
datasets: numKeys.map((k, i) => ({ label: k, data: data.map(r => r[k] || 0), backgroundColor: _chartColors[i % _chartColors.length] })),
|
|
1050
|
-
},
|
|
1051
|
-
};
|
|
1052
|
-
}
|
|
1053
|
-
chartCfg.options = Object.assign({
|
|
1054
|
-
responsive: true,
|
|
1055
|
-
maintainAspectRatio: false,
|
|
1056
|
-
plugins: { legend: { position: data.length > 8 ? 'bottom' : 'right' } },
|
|
1057
|
-
}, ed.chartOptions || {});
|
|
1058
|
-
|
|
1059
|
-
cleanup.charts.push({ key: chartKey, inst: new cfg.chartLib(ctx, chartCfg) });
|
|
1060
|
-
}
|
|
1061
|
-
|
|
1062
|
-
// ---- form ----
|
|
1063
|
-
|
|
1064
|
-
function _renderForm(data, el, elemDef, node) {
|
|
1065
|
-
const cleanup = _getCleanup(node.id);
|
|
1066
|
-
const signal = cleanup.ac.signal;
|
|
1067
|
-
const ed = elemDef.data || {};
|
|
1068
|
-
const writeTo = ed.writeTo;
|
|
1069
|
-
const schema = ed.fields || {};
|
|
1070
|
-
const props = schema.properties || {};
|
|
1071
|
-
const required = schema.required || [];
|
|
1072
|
-
|
|
1073
|
-
const stateKey = node.id + ':' + (ed.bind || writeTo || '');
|
|
1074
|
-
const baseValues = (data && typeof data === 'object' && !Array.isArray(data)) ? Object.assign({}, data) : {};
|
|
1075
|
-
|
|
1076
|
-
if (!_formState[stateKey]) {
|
|
1077
|
-
_formState[stateKey] = { baseValues, journal: {} };
|
|
1078
|
-
} else {
|
|
1079
|
-
_formState[stateKey].baseValues = baseValues;
|
|
1080
|
-
Object.keys(_formState[stateKey].journal).forEach(key => {
|
|
1081
|
-
if (_same(_formState[stateKey].journal[key], baseValues[key])) {
|
|
1082
|
-
delete _formState[stateKey].journal[key];
|
|
1083
|
-
}
|
|
1084
|
-
});
|
|
1085
|
-
}
|
|
1086
|
-
|
|
1087
|
-
const st = _formState[stateKey];
|
|
1088
|
-
|
|
1089
|
-
function _toInputValue(prop, inp) {
|
|
1090
|
-
if (prop.type === 'boolean') return !!inp.checked;
|
|
1091
|
-
if (prop.type === 'number' || prop.type === 'integer') return inp.value !== '' ? parseFloat(inp.value) : 0;
|
|
1092
|
-
return inp.value;
|
|
1093
|
-
}
|
|
1094
|
-
|
|
1095
|
-
function _same(a, b) {
|
|
1096
|
-
return JSON.stringify(a) === JSON.stringify(b);
|
|
1097
|
-
}
|
|
1098
|
-
|
|
1099
|
-
function getEffectiveValues() {
|
|
1100
|
-
return Object.assign({}, st.baseValues, st.journal);
|
|
1101
|
-
}
|
|
1102
|
-
|
|
1103
|
-
function isDirty() {
|
|
1104
|
-
return Object.keys(st.journal).length > 0;
|
|
1105
|
-
}
|
|
1106
|
-
|
|
1107
|
-
// Capture user edits into a journal overlay (only changed keys).
|
|
1108
|
-
function captureJournal(form) {
|
|
1109
|
-
form.querySelectorAll('[data-key]').forEach(inp => {
|
|
1110
|
-
const k = inp.dataset.key;
|
|
1111
|
-
const p = props[k];
|
|
1112
|
-
if (!p) return;
|
|
1113
|
-
const nextVal = _toInputValue(p, inp);
|
|
1114
|
-
const baseVal = st.baseValues[k];
|
|
1115
|
-
if (_same(nextVal, baseVal)) delete st.journal[k];
|
|
1116
|
-
else st.journal[k] = nextVal;
|
|
1117
|
-
});
|
|
1118
|
-
}
|
|
1119
|
-
|
|
1120
|
-
const form = document.createElement('form');
|
|
1121
|
-
form.className = 'row g-2';
|
|
1122
|
-
form.noValidate = true;
|
|
1123
|
-
|
|
1124
|
-
Object.keys(props).forEach(key => {
|
|
1125
|
-
const prop = props[key];
|
|
1126
|
-
const isReq = required.indexOf(key) >= 0;
|
|
1127
|
-
const compact = ['number', 'integer', 'boolean'].includes(prop.type) || prop.enum || prop.format === 'date';
|
|
1128
|
-
const col = document.createElement('div');
|
|
1129
|
-
col.className = compact ? 'col-12 col-md-6' : 'col-12';
|
|
1130
|
-
|
|
1131
|
-
let input;
|
|
1132
|
-
if (prop.type === 'boolean') {
|
|
1133
|
-
const wrap = document.createElement('div');
|
|
1134
|
-
wrap.className = 'form-check mt-3';
|
|
1135
|
-
input = document.createElement('input');
|
|
1136
|
-
input.type = 'checkbox'; input.className = 'form-check-input';
|
|
1137
|
-
const lbl = document.createElement('label');
|
|
1138
|
-
lbl.className = 'form-check-label small'; lbl.textContent = prop.title || key;
|
|
1139
|
-
wrap.appendChild(input); wrap.appendChild(lbl); col.appendChild(wrap);
|
|
1140
|
-
} else {
|
|
1141
|
-
const lbl = document.createElement('label');
|
|
1142
|
-
lbl.className = 'form-label small mb-1'; lbl.textContent = prop.title || key;
|
|
1143
|
-
col.appendChild(lbl);
|
|
1144
|
-
|
|
1145
|
-
if (prop.enum) {
|
|
1146
|
-
input = document.createElement('select');
|
|
1147
|
-
input.className = 'form-select form-select-sm';
|
|
1148
|
-
prop.enum.forEach(o => { const opt = document.createElement('option'); opt.value = o; opt.textContent = o; input.appendChild(opt); });
|
|
1149
|
-
} else if (prop.type === 'number' || prop.type === 'integer') {
|
|
1150
|
-
input = document.createElement('input');
|
|
1151
|
-
input.type = 'number'; input.className = 'form-control form-control-sm';
|
|
1152
|
-
if (prop.minimum != null) input.min = prop.minimum;
|
|
1153
|
-
if (prop.maximum != null) input.max = prop.maximum;
|
|
1154
|
-
if (prop.type === 'integer') input.step = '1';
|
|
1155
|
-
} else if (prop.format === 'date') {
|
|
1156
|
-
input = document.createElement('input');
|
|
1157
|
-
input.type = 'date'; input.className = 'form-control form-control-sm';
|
|
1158
|
-
} else {
|
|
1159
|
-
input = document.createElement('input');
|
|
1160
|
-
input.type = 'text'; input.className = 'form-control form-control-sm';
|
|
1161
|
-
if (prop.placeholder) input.placeholder = prop.placeholder;
|
|
1162
|
-
}
|
|
1163
|
-
col.appendChild(input);
|
|
1164
|
-
}
|
|
1165
|
-
|
|
1166
|
-
input.dataset.key = key;
|
|
1167
|
-
if (isReq) input.required = true;
|
|
1168
|
-
// Populate from effective values (base bind overlaid by local journal).
|
|
1169
|
-
const v = getEffectiveValues()[key];
|
|
1170
|
-
if (v != null) {
|
|
1171
|
-
if (prop.type === 'boolean') input.checked = !!v;
|
|
1172
|
-
else if (prop.format === 'date') input.value = String(v).slice(0, 10);
|
|
1173
|
-
else input.value = v;
|
|
1174
|
-
}
|
|
1175
|
-
form.appendChild(col);
|
|
1176
|
-
});
|
|
1177
|
-
|
|
1178
|
-
const btnCol = document.createElement('div');
|
|
1179
|
-
btnCol.className = 'col-12 mt-1';
|
|
1180
|
-
const discardBtn = document.createElement('button');
|
|
1181
|
-
discardBtn.type = 'button';
|
|
1182
|
-
discardBtn.className = 'btn btn-sm btn-outline-secondary me-2' + (isDirty() ? '' : ' d-none');
|
|
1183
|
-
discardBtn.textContent = 'Discard';
|
|
1184
|
-
const btn = document.createElement('button');
|
|
1185
|
-
btn.type = 'submit';
|
|
1186
|
-
btn.className = 'btn btn-sm btn-primary' + (isDirty() ? '' : ' d-none');
|
|
1187
|
-
btn.textContent = 'Save';
|
|
1188
|
-
btnCol.appendChild(discardBtn);
|
|
1189
|
-
btnCol.appendChild(btn);
|
|
1190
|
-
form.appendChild(btnCol);
|
|
1191
|
-
|
|
1192
|
-
el.innerHTML = '';
|
|
1193
|
-
el.appendChild(form);
|
|
1194
|
-
|
|
1195
|
-
// Real-time input → update journal + toggle Save/Discard buttons
|
|
1196
|
-
form.addEventListener('input', () => {
|
|
1197
|
-
captureJournal(form);
|
|
1198
|
-
const dirty = isDirty();
|
|
1199
|
-
btn.classList.toggle('d-none', !dirty);
|
|
1200
|
-
discardBtn.classList.toggle('d-none', !dirty);
|
|
1201
|
-
}, { signal });
|
|
1202
|
-
|
|
1203
|
-
form.addEventListener('submit', e => {
|
|
1204
|
-
e.preventDefault();
|
|
1205
|
-
if (!form.checkValidity()) { form.classList.add('was-validated'); return; }
|
|
1206
|
-
captureJournal(form);
|
|
1207
|
-
const nextValues = getEffectiveValues();
|
|
1208
|
-
cfg.onPatchState(node.id, { fieldValues: nextValues });
|
|
1209
|
-
btn.textContent = 'Saving...';
|
|
1210
|
-
_showSavingOverlay(el);
|
|
1211
|
-
}, { signal });
|
|
1212
|
-
|
|
1213
|
-
discardBtn.addEventListener('click', () => {
|
|
1214
|
-
st.journal = {};
|
|
1215
|
-
form.querySelectorAll('[data-key]').forEach(inp => {
|
|
1216
|
-
const k = inp.dataset.key;
|
|
1217
|
-
const p = props[k];
|
|
1218
|
-
if (!p) return;
|
|
1219
|
-
const v = st.baseValues[k];
|
|
1220
|
-
if (p.type === 'boolean') inp.checked = !!v;
|
|
1221
|
-
else if (p.format === 'date') inp.value = v != null ? String(v).slice(0, 10) : '';
|
|
1222
|
-
else inp.value = v != null ? v : '';
|
|
1223
|
-
});
|
|
1224
|
-
discardBtn.classList.add('d-none');
|
|
1225
|
-
btn.classList.add('d-none');
|
|
1226
|
-
}, { signal });
|
|
1227
|
-
}
|
|
1228
|
-
|
|
1229
|
-
// ---- notes ----
|
|
1230
|
-
|
|
1231
|
-
function _renderNotes(data, el, elemDef, node) {
|
|
1232
|
-
const cleanup = _getCleanup(node.id);
|
|
1233
|
-
const signal = cleanup.ac.signal;
|
|
1234
|
-
const ed = elemDef.data || {};
|
|
1235
|
-
const writeTo = ed.writeTo;
|
|
1236
|
-
const incomingContent = typeof data === 'string' ? data : '';
|
|
1237
|
-
|
|
1238
|
-
// Base + journal overlay model:
|
|
1239
|
-
// effective = journal when present, else baseContent from bind.
|
|
1240
|
-
const stateKey = node.id + ':' + ((ed.bind || writeTo) || '');
|
|
1241
|
-
if (!_notesState[stateKey]) {
|
|
1242
|
-
_notesState[stateKey] = { baseContent: incomingContent, journal: null };
|
|
1243
|
-
} else {
|
|
1244
|
-
_notesState[stateKey].baseContent = incomingContent;
|
|
1245
|
-
if (_notesState[stateKey].journal === incomingContent) {
|
|
1246
|
-
_notesState[stateKey].journal = null;
|
|
1247
|
-
}
|
|
1248
|
-
}
|
|
1249
|
-
const st = _notesState[stateKey];
|
|
1250
|
-
|
|
1251
|
-
function isDirty() {
|
|
1252
|
-
return st.journal != null;
|
|
1253
|
-
}
|
|
1254
|
-
|
|
1255
|
-
function getEffectiveContent() {
|
|
1256
|
-
return st.journal != null ? st.journal : st.baseContent;
|
|
1257
|
-
}
|
|
1258
|
-
|
|
1259
|
-
function setJournal(nextValue) {
|
|
1260
|
-
st.journal = nextValue === st.baseContent ? null : nextValue;
|
|
1261
|
-
}
|
|
1262
|
-
|
|
1263
|
-
el.innerHTML = `
|
|
1264
|
-
<textarea class="form-control form-control-sm lc-notes-textarea" rows="8" placeholder="Write markdown...">${_esc(getEffectiveContent())}</textarea>
|
|
80
|
+
`,document.head.appendChild(T);}let tt={"&":"&","<":"<",">":">",'"':""","'":"'"};function L(T){return T?String(T).replace(/[&<>"']/g,k=>tt[k]):""}function Be(T){return !T||typeof T!="string"?[]:T.replace(/\[(\d+)\]/g,".$1").split(".").filter(Boolean)}function nt(T,k){if(!k||!T)return;let I=Be(k),Y=T;for(let J=0;J<I.length;J++){if(Y==null)return;Y=Y[I[J]];}return Y}function Xe(T,k,I){let Y=Be(k);if(!Y.length)return;let J=T;for(let R=0;R<Y.length-1;R++)(J[Y[R]]==null||typeof J[Y[R]]!="object")&&(J[Y[R]]={}),J=J[Y[R]];J[Y[Y.length-1]]=I;}function Ke(T){return `<span class="lc-status-dot" style="background:${{fresh:"var(--bs-success)",stale:"var(--bs-warning)",error:"var(--bs-danger)",loading:"var(--bs-info)"}[T]||"var(--bs-secondary)"}" title="${L(T||"unknown")}"></span>`}function Je(T){if(!T)return "";let k=Math.floor((Date.now()-new Date(T).getTime())/1e3);return isNaN(k)||k<0?"":k<60?k+"s ago":k<3600?Math.floor(k/60)+"m ago":k<86400?Math.floor(k/3600)+"h ago":Math.floor(k/86400)+"d ago"}function at(T){let k=String(T).match(/^(<=?|>=?|===?)\s*(.+)$/);return k?{op:k[1],value:parseFloat(k[2])}:null}function Ue(T,k){let I=at(k);if(!I||isNaN(I.value))return false;switch(I.op){case "<":return T<I.value;case "<=":return T<=I.value;case ">":return T>I.value;case ">=":return T>=I.value;case "=":case "==":case "===":return T===I.value}return false}function st(T){if(!T.length)return "bar";let k=T[0];return k.label!==void 0&&k.value!==void 0&&!k.x&&!k.date?"pie":k.x!==void 0||k.date!==void 0?"line":"bar"}let Me=["#0d6efd","#198754","#ffc107","#dc3545","#6f42c1","#0dcaf0","#fd7e14","#20c997","#d63384","#6c757d"];function rt(T){et();let k={resolve:T.resolve,onPatch:T.onPatch||function(){},onPatchState:T.onPatchState||function(){},onRefresh:T.onRefresh||null,onChat:T.onChat||null,markdown:T.markdown||null,sanitize:T.sanitize||null,chartLib:T.chartLib||null,onAction:T.onAction||function(){},getChatMessages:T.getChatMessages||null,fileUrlBase:T.fileUrlBase||"/api/boards/default"},I={},Y={},J={},R={},D={},V={};function ie(e){getComputedStyle(e).position==="static"&&(e.style.position="relative");let t=document.createElement("div");t.className="lc-saving-overlay",t.setAttribute("aria-live","polite"),t.style.cssText=["position:absolute","inset:0","background:rgba(255,255,255,0.78)","display:flex","align-items:center","justify-content:center","gap:0.5rem","z-index:20","border-radius:inherit","pointer-events:all"].join(";"),t.innerHTML='<span class="spinner-border spinner-border-sm text-primary" role="status" aria-hidden="true"></span><span class="text-primary fw-medium small">Saving\u2026</span>',e.appendChild(t);}let P={},F={},c={backdrop:null,title:null,body:null,input:null,fileInput:null,staged:null,sendBtn:null,attachBtn:null,closeBtn:null,currentNodeId:null,stagedFiles:[],loading:false},x={backdrop:null,title:null,body:null,staged:null,fileInput:null,dropzone:null,uploadBtn:null,attachBtn:null,closeBtn:null,currentNodeId:null,stagedFiles:[],pollingTimer:null,loading:false};function Z(e){if(!e)return "";let t=k.markdown?k.markdown(e):L(e);return k.sanitize?k.sanitize(t):t}function X(e){return I[e]||(I[e]={ac:new AbortController,timers:[],charts:[],unsubs:[]}),I[e]}function de(){if(c.backdrop)return;let e=document.createElement("div");e.className="lc-chat-modal-backdrop",e.innerHTML='<div class="modal-dialog modal-lg modal-dialog-centered" role="dialog" aria-modal="true" aria-label="Card chat"> <div class="modal-content bg-white"> <div class="modal-header border-bottom p-3 d-flex align-items-center justify-content-between"> <h5 class="modal-title lc-chat-modal-title">Chat</h5> <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> </div> <div class="modal-body bg-light" data-lc-chat-body></div> <div class="modal-footer flex-column align-items-stretch border-top p-3 gap-3"> <div data-lc-chat-staged class="small w-100"></div> <input type="file" class="d-none" data-lc-chat-file multiple> <div class="lc-chat-modal-input-row mt-2"> <button type="button" class="btn btn-sm btn-outline-secondary" data-lc-chat-attach title="Attach files" aria-label="Attach files"> <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> </button> <textarea class="form-control" data-lc-chat-input rows="1" placeholder="Type a message..."></textarea> <button type="button" class="btn btn-sm btn-primary" data-lc-chat-send aria-label="Send"> <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> </button> </div> </div> </div></div>',document.body.appendChild(e),c.backdrop=e,c.title=e.querySelector(".lc-chat-modal-title"),c.body=e.querySelector("[data-lc-chat-body]"),c.input=e.querySelector("[data-lc-chat-input]"),c.fileInput=e.querySelector("[data-lc-chat-file]"),c.staged=e.querySelector("[data-lc-chat-staged]"),c.sendBtn=e.querySelector("[data-lc-chat-send]"),c.attachBtn=e.querySelector("[data-lc-chat-attach]"),c.closeBtn=e.querySelector("[data-lc-chat-close]");function t(){c.input&&(c.input.style.height="auto",c.input.style.height=Math.min(c.input.scrollHeight,120)+"px");}let a=function(){c.currentNodeId=null,c.stagedFiles=[],c.staged.innerHTML="",c.input.value="",t(),c.backdrop.classList.remove("lc-open");};function r(){if(!c.stagedFiles.length){c.staged.innerHTML="";return}c.staged.innerHTML=c.stagedFiles.map(function(n,l){return '<span class="badge text-bg-light border me-1 mb-1">'+L(n.name||"file")+' <button type="button" class="btn btn-sm btn-link text-danger p-0 ms-1" data-lc-rm-file="'+l+'">×</button></span>'}).join(""),c.staged.querySelectorAll("[data-lc-rm-file]").forEach(function(n){n.addEventListener("click",function(){let l=parseInt(n.getAttribute("data-lc-rm-file")||"-1",10);l>=0&&c.stagedFiles.splice(l,1),r();});});}async function p(){if(c.loading||!c.currentNodeId)return;let n=c.currentNodeId,l=(c.input.value||"").trim(),s=c.stagedFiles.slice();if(!(!l&&!s.length)){c.loading=true,c.sendBtn.disabled=true,c.attachBtn.disabled=true,B(l),c.input.value="",c.stagedFiles=[],t(),r();try{await Promise.resolve(k.onAction(n,"chat-send",{text:l,files:s}));}catch(d){U(),ae("system","Failed to send message: "+String(d&&d.message||d),[]);}finally{c.loading=false,c.sendBtn.disabled=false,c.attachBtn.disabled=false;}}}c.closeBtn.addEventListener("click",a),e.addEventListener("click",function(n){n.target===e&&a();}),c.attachBtn.addEventListener("click",function(){c.fileInput.click();}),c.fileInput.addEventListener("change",function(n){let l=n.target&&n.target.files?Array.from(n.target.files):[];for(let s of l)c.stagedFiles.find(function(d){return d.name===s.name&&d.size===s.size&&d.lastModified===s.lastModified})||c.stagedFiles.push(s);n.target.value="",r();}),c.sendBtn.addEventListener("click",p),c.input.addEventListener("input",t),c.input.addEventListener("keydown",function(n){n.key==="Enter"&&!n.shiftKey&&(n.preventDefault(),p());}),t(),document.addEventListener("keydown",function(n){n.key==="Escape"&&c.backdrop&&c.backdrop.classList.contains("lc-open")&&a();});}function Te(e){return (Array.isArray(e)?e:[]).map(function(a){if(!a||typeof a!="object")return null;let r=typeof a.role=="string"?a.role:"system",p=typeof a.text=="string"?a.text:typeof a.message=="string"?a.message:"",n=Array.isArray(a.files)?a.files:[];return {role:r.toLowerCase(),text:p,files:n}}).filter(Boolean)}function ae(e,t,a){if(de(),!c.body||!t&&!a)return;let r=e==="user"||e==="assistant"?e:"system",p=r==="user"?"lc-chat-bubble-user":r==="assistant"?"lc-chat-bubble-assistant":"lc-chat-bubble-system",n=document.createElement("div");if(n.className="lc-chat-bubble "+p,r!=="system"){let s='<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="8" r="4"/><path d="M4 20c0-4 3.6-7 8-7s8 3 8 7"/></svg>',d='<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/></svg>',u=document.createElement("span");u.className="lc-chat-icon",u.setAttribute("aria-hidden","true"),u.innerHTML=r==="user"?s:d,n.appendChild(u);}let l=document.createElement("div");if(l.className="lc-chat-bubble-content",r==="assistant"?l.innerHTML=Z(t||""):l.textContent=t||"",Array.isArray(a)&&a.length){let s=document.createElement("div");s.className="small mt-1 text-muted",s.textContent="\u{1F4CE} "+a.map(function(d){return d?typeof d=="string"?d:d.name||"file":"file"}).join(", "),l.appendChild(s);}n.appendChild(l),c.body.appendChild(n),c.body.scrollTop=c.body.scrollHeight;}function B(e){if(de(),!c.body)return;let t=document.createElement("div");t.className="lc-chat-bubble lc-chat-bubble-user lc-chat-bubble-pending",t.setAttribute("data-lc-chat-pending","1"),t.textContent=e||"";let a=document.createElement("span");a.className="spinner-border spinner-border-sm",a.setAttribute("role","status"),a.setAttribute("aria-label","Sending"),t.appendChild(a),c.body.appendChild(t),c.body.scrollTop=c.body.scrollHeight;}function U(){c.body&&c.body.querySelectorAll('[data-lc-chat-pending="1"]').forEach(function(e){e&&e.parentNode&&e.parentNode.removeChild(e);});}async function G(e){if(c.currentNodeId!==e)return;let t=k.resolve(e),a=[];if(typeof k.getChatMessages=="function")try{a=await Promise.resolve(k.getChatMessages(e));}catch{a=[];}else t&&t.card_data&&Array.isArray(t.card_data.messages)&&(a=t.card_data.messages);let r=Te(a);if(c.body.innerHTML="",!r.length){c.body.innerHTML='<div class="text-muted small">No messages yet.</div>',le(e);return}r.forEach(function(p){ae(p.role,p.text,p.files);}),le(e);}function le(e){if(!c.body)return;let t=e?k.resolve(e):null,a=!!(t&&t.card_data&&t.card_data.__chat_signal&&t.card_data.__chat_signal.processing),r=c.body.querySelector(".lc-chat-processing");a?(r||(r=document.createElement("div"),r.className="lc-chat-processing",r.innerHTML='<span class="spinner-border spinner-border-sm" role="status" aria-label="AI working"></span><span>\u2728 AI working\u2026</span>',c.body.appendChild(r)),c.body.scrollTop=c.body.scrollHeight):r&&r.remove();}async function pe(e){de();let t=k.resolve(e);if(!t)return;let a=t.card&&t.card.meta&&t.card.meta.title||t.id;c.currentNodeId=e,c.title.textContent="Chat: "+a,c.body.innerHTML='<div class="text-muted small">Loading...</div>',c.backdrop.classList.add("lc-open");let r=!!(t.card_data&&t.card_data.features&&t.card_data.features.chat&&t.card_data.features.chat.disabled);c.input.disabled=r,c.attachBtn.disabled=r,c.sendBtn.disabled=r,c.input.placeholder=r?"Chat is disabled for this card.":"Type a message...",r||c.input.focus(),await G(e);}function Ne(){if(x.backdrop)return;let e=document.createElement("div");e.className="lc-files-modal-backdrop",e.innerHTML='<div class="modal-dialog modal-lg modal-dialog-centered" role="dialog" aria-modal="true" aria-label="Card files"> <div class="modal-content bg-white"> <div class="modal-header border-bottom p-3 d-flex align-items-center justify-content-between"> <h5 class="modal-title lc-files-modal-title">Files</h5> <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> </div> <div class="modal-body bg-light" data-lc-files-body></div> <div class="modal-footer flex-column align-items-stretch border-top p-3 gap-3"> <div class="lc-dropzone border-2 border-dashed p-4 text-center cursor-pointer rounded" data-lc-files-dz> <div class="small text-muted mb-2">Drop files here or click to browse</div> <input type="file" class="d-none" data-lc-files-input multiple> </div> <div data-lc-files-staged class="small w-100 d-flex flex-wrap gap-2"></div> <div class="d-flex justify-content-end gap-2 w-100"> <button type="button" class="btn btn-sm btn-outline-secondary" data-lc-files-attach>Select files</button> <button type="button" class="btn btn-sm btn-primary" data-lc-files-upload>Upload</button> </div> </div> </div></div>',document.body.appendChild(e),x.backdrop=e,x.title=e.querySelector(".lc-files-modal-title"),x.body=e.querySelector("[data-lc-files-body]"),x.staged=e.querySelector("[data-lc-files-staged]"),x.fileInput=e.querySelector("[data-lc-files-input]"),x.dropzone=e.querySelector("[data-lc-files-dz]"),x.uploadBtn=e.querySelector("[data-lc-files-upload]"),x.attachBtn=e.querySelector("[data-lc-files-attach]"),x.closeBtn=e.querySelector("[data-lc-files-close]");let t=function(){x.currentNodeId=null,x.stagedFiles=[],x.staged.innerHTML="",x.backdrop.classList.remove("lc-open"),x.pollingTimer&&(clearInterval(x.pollingTimer),x.pollingTimer=null);};function a(){if(!x.stagedFiles.length){x.staged.innerHTML="";return}x.staged.innerHTML=x.stagedFiles.map(function(n,l){return '<span class="badge text-bg-light border me-1 mb-1">'+L(n.name||"file")+' <button type="button" class="btn btn-sm btn-link text-danger p-0 ms-1" data-lc-files-rm="'+l+'">×</button></span>'}).join(""),x.staged.querySelectorAll("[data-lc-files-rm]").forEach(function(n){n.addEventListener("click",function(){let l=parseInt(n.getAttribute("data-lc-files-rm")||"-1",10);l>=0&&x.stagedFiles.splice(l,1),a();});});}function r(n){let l=Array.from(n||[]);for(let s of l)x.stagedFiles.find(function(d){return d.name===s.name&&d.size===s.size&&d.lastModified===s.lastModified})||x.stagedFiles.push(s);a();}async function p(){if(x.loading||!x.currentNodeId||!x.stagedFiles.length)return;let n=x.currentNodeId,l=x.stagedFiles.slice();x.loading=true,x.uploadBtn.disabled=true,x.attachBtn.disabled=true,x.dropzone.classList.add("lc-disabled");try{await Promise.resolve(k.onAction(n,"file-upload",{files:l})),x.stagedFiles=[],a(),oe(n);}catch(s){x.staged.innerHTML='<span class="text-danger">Upload failed: '+L(String(s&&s.message||s))+"</span>";}finally{x.loading=false,x.uploadBtn.disabled=false,x.attachBtn.disabled=false,x.dropzone.classList.remove("lc-disabled");}}x.closeBtn.addEventListener("click",t),e.addEventListener("click",function(n){n.target===e&&t();}),x.attachBtn.addEventListener("click",function(){x.fileInput.click();}),x.fileInput.addEventListener("change",function(n){r(n.target&&n.target.files?n.target.files:[]),n.target.value="";}),x.uploadBtn.addEventListener("click",p),x.dropzone.addEventListener("click",function(){x.loading||x.fileInput.click();}),x.dropzone.addEventListener("dragover",function(n){n.preventDefault(),x.dropzone.classList.add("lc-drag-over");}),x.dropzone.addEventListener("dragleave",function(){x.dropzone.classList.remove("lc-drag-over");}),x.dropzone.addEventListener("drop",function(n){n.preventDefault(),x.dropzone.classList.remove("lc-drag-over"),r(n.dataTransfer&&n.dataTransfer.files?n.dataTransfer.files:[]);}),document.addEventListener("keydown",function(n){n.key==="Escape"&&x.backdrop&&x.backdrop.classList.contains("lc-open")&&t();});}function te(e){let t=k.resolve(e);return (t&&t.card_data&&Array.isArray(t.card_data.files)?t.card_data.files:[]).filter(Boolean)}function oe(e){if(x.currentNodeId!==e)return;let t=te(e);if(!t.length){x.body.innerHTML='<div class="alert alert-light border small mb-0">No files uploaded yet.</div>';return}let a='<div class="list-group list-group-flush">';t.forEach(function(r,p){let n=r&&(r.name||r.stored_name)?r.name||r.stored_name:"file",l=r&&typeof r.size=="number"?"size: "+r.size+" bytes":"",s=r&&r.stored_name?String(r.stored_name):"",d=s?k.fileUrlBase+"/cards/"+encodeURIComponent(e)+"/files/"+p+"?sn="+encodeURIComponent(s):null;a+='<div class="list-group-item d-flex align-items-center justify-content-between gap-2">',a+='<div class="text-truncate"><div class="small fw-medium">'+L(n)+"</div>",a+='<div class="small text-muted">'+L(l)+"</div></div>",d&&(a+='<a class="btn btn-sm btn-outline-secondary flex-shrink-0" href="'+d+'">Download</a>'),a+="</div>";}),a+="</div>",x.body.innerHTML=a;}function xe(e){Ne();let t=k.resolve(e);if(!t)return;let a=t.card&&t.card.meta&&t.card.meta.title||t.id;x.currentNodeId=e,x.title.textContent="Files: "+a,x.backdrop.classList.add("lc-open");let r=!!(t.card_data&&t.card_data.features&&t.card_data.features.files&&t.card_data.features.files.disabled);x.dropzone.classList.toggle("lc-disabled",r),x.attachBtn.disabled=r,x.uploadBtn.disabled=r,x.fileInput.disabled=r,oe(e),x.pollingTimer&&clearInterval(x.pollingTimer),x.pollingTimer=setInterval(function(){oe(e);},1e3);}function ce(e,t){if(!t||typeof t!="string")return;let a=Be(t);if(!a.length)return;let r=a[0],p=a.slice(1).join("."),n={card:e&&e.card?e.card:{},card_data:e&&e.card_data?e.card_data:{},requires:e&&e.requires?e.requires:{},computed_values:e&&e.computed_values?e.computed_values:{},runtime_state:e&&e.runtime_state?e.runtime_state:{}};if(Object.prototype.hasOwnProperty.call(n,r))return p?nt(n[r],p):n[r]}function me(e,t){let a=Y[e];a&&a.forEach(r=>{try{r(e,t);}catch(p){console.error("LiveCard notify error",p);}});}function Ae(e,t){return Y[e]||(Y[e]=new Set),Y[e].add(t),()=>Y[e].delete(t)}function we(e){let t=e&&e.card&&Array.isArray(e.card.requires)?e.card.requires:[];if(!t.length)return;let a=X(e.id),r={};Object.keys(Y).concat(Object.keys(F)).forEach(function(s){let d=k.resolve(s);if(!(!d||!d.card)){var u=Array.isArray(d.card.provides)&&d.card.provides.length?d.card.provides.map(function(v){return typeof v=="string"?v:v.bindTo||v}):[d.id];u.forEach(function(v){r[v]=d.id;});}});let n={},l=[];t.forEach(function(s){var d=r[s]||s;n[d]||(n[d]=true,l.push(d));}),a.unsubs=l.map(s=>Ae(s,()=>{let d=F[e.id];if(!d||!d.resultEl)return;let u=k.resolve(e.id);u&&(Se(u,d.resultEl),me(e.id));}));}function Ee(e,t,a,r){let p=a.data||{};if(!Array.isArray(e)||!e.length){t.innerHTML=`<p class="text-muted small">${L(p.placeholder||"No data")}</p>`;return}let n=Math.min(e.length,p.maxRows||200),l=new Set;for(let w=0;w<Math.min(e.length,n);w++)Object.keys(e[w]).forEach(S=>l.add(S));let s=p.columns&&p.columns.length?p.columns:[...l],d=p.sortable!==false,u=null,v="asc",m=X(r.id);function C(){let w=e.slice(0,n);u!==null&&d&&(w=w.slice().sort((M,$)=>{let j=M[s[u]],_=$[s[u]];return j==null?1:_==null?-1:typeof j=="number"&&typeof _=="number"?v==="asc"?j-_:_-j:v==="asc"?String(j).localeCompare(String(_)):String(_).localeCompare(String(j))}));let S='<div class="table-responsive"><table class="table table-sm table-striped table-hover mb-0"><thead><tr>';s.forEach((M,$)=>{let j=u===$?v==="asc"?" \u2191":" \u2193":"";S+=`<th class="small text-nowrap"${d?' style="cursor:pointer"':""} data-col="${$}">${L(M)}${j}</th>`;}),S+="</tr></thead><tbody>",w.forEach(M=>{S+="<tr>",s.forEach($=>{let j=M[$];S+=`<td class="small">${L(j!=null?String(j):"")}</td>`;}),S+="</tr>";}),S+="</tbody></table></div>",e.length>n&&(S+=`<p class="text-muted small mt-1">Showing ${n} of ${e.length} rows</p>`),t.innerHTML=S,d&&t.querySelectorAll("th[data-col]").forEach(M=>{M.addEventListener("click",()=>{let $=parseInt(M.dataset.col);u===$?v=v==="asc"?"desc":"asc":(u=$,v="asc"),C();},{signal:m.ac.signal});});}C();}function Ce(e,t,a,r){let n=X(r.id).ac.signal,l=a.data||{},s=l.writeTo,d=s?ce(r,s)||{}:{},u=l.fields&&l.fields.properties||{},v=e&&typeof e=="object"&&!Array.isArray(e)?Object.keys(e):[];if(!v.length){t.innerHTML='<p class="text-muted small">No filter options</p>';return}let m='<div class="row g-2">';v.forEach(C=>{let w=Array.isArray(e[C])?e[C]:[],S=u[C]&&u[C].title||C;m+=`<div class="col-12 col-sm-6 col-md-4"><label class="form-label small mb-1">${L(S)}</label>`,m+=`<select class="form-select form-select-sm" data-fk="${L(C)}"><option value="">All</option>`,w.forEach(M=>{let $=String(M)===String(d[C]||"")?" selected":"";m+=`<option value="${L(String(M))}"${$}>${L(String(M))}</option>`;}),m+="</select></div>";}),m+="</div>",t.innerHTML=m,t.querySelectorAll("select[data-fk]").forEach(C=>{C.addEventListener("change",()=>{let w={};t.querySelectorAll("select[data-fk]").forEach(S=>{S.value&&(w[S.dataset.fk]=S.value);}),s&&Xe(r,s,w),k.onPatchState(r.id,{fieldValues:w}),me(r.id,w);},{signal:n});});}function He(e,t,a){let r=a.label||"",p="\u2014",n="";e&&typeof e=="object"&&!Array.isArray(e)?(r=e.title||e.label||e.metric||r,p=e.value!=null?String(e.value):"\u2014",n=e.detail||""):e!=null&&(p=String(e));let l='<div class="text-center py-2">';r&&(l+=`<div class="text-muted small">${L(r)}</div>`),l+=`<div class="lc-metric-value">${L(p)}</div>`,n&&(l+=`<div class="small mt-1">${Z(n)}</div>`),l+="</div>",t.innerHTML=l;}function Le(e,t,a,r){let p=a.data||{};if(e==null){t.innerHTML="";return}if(typeof e=="object"&&!Array.isArray(e)){let n='<dl class="row mb-0">';Object.entries(e).forEach(([l,s])=>{n+=`<dt class="col-sm-5 small text-muted text-truncate">${L(l)}</dt>`,n+=`<dd class="col-sm-7 small mb-1">${L(s!=null?String(s):"\u2014")}</dd>`;}),t.innerHTML=n+"</dl>";return}if(Array.isArray(e)){if(!e.length){t.innerHTML=`<p class="text-muted small">${L(p.placeholder||"Empty")}</p>`;return}if(typeof e[0]=="string"||typeof e[0]=="number"){let n=p.maxRows||e.length,l='<ul class="list-unstyled mb-0">';e.slice(0,n).forEach(s=>{l+=`<li class="small mb-1">\u2022 ${L(String(s))}</li>`;}),t.innerHTML=l+"</ul>";return}Ee(e,t,a,r);return}t.innerHTML=`<div class="small">${Z(String(e))}</div>`;}function _e(e,t,a,r){let p=a.data||{};if(!k.chartLib){Ee(e,t,a,r);return}if(!Array.isArray(e)||!e.length){t.innerHTML='<p class="text-muted small">No chart data</p>';return}let n=X(r.id),l=a.id||"chart-"+Math.random().toString(36).slice(2,8),s=n.charts.findIndex(m=>m.key===l);s>=0&&(n.charts[s].inst.destroy(),n.charts.splice(s,1));let d=p.chartType||st(e);t.innerHTML='<div class="lc-chart-wrap"><canvas></canvas></div>';let u=t.querySelector("canvas").getContext("2d"),v;if(d==="pie"||d==="doughnut")v={type:d,data:{labels:e.map(m=>m.label||m.name||""),datasets:[{data:e.map(m=>m.value||0),backgroundColor:Me.slice(0,e.length)}]}};else if(d==="line")v={type:"line",data:{labels:e.map(m=>m.x||m.date||m.label||""),datasets:[{label:a.label||"Value",data:e.map(m=>m.y||m.value||0),borderColor:Me[0],tension:.3,fill:false}]}};else {let m=Object.keys(e[0]).filter(w=>typeof e[0][w]=="number"),C=Object.keys(e[0]).find(w=>typeof e[0][w]=="string");v={type:"bar",data:{labels:e.map(w=>w.label||w.name||(C?w[C]:"")),datasets:m.map((w,S)=>({label:w,data:e.map(M=>M[w]||0),backgroundColor:Me[S%Me.length]}))}};}v.options=Object.assign({responsive:true,maintainAspectRatio:false,plugins:{legend:{position:e.length>8?"bottom":"right"}}},p.chartOptions||{}),n.charts.push({key:l,inst:new k.chartLib(u,v)});}function ve(e,t,a,r){let n=X(r.id).ac.signal,l=a.data||{},s=l.writeTo,d=l.fields||{},u=d.properties||{},v=d.required||[],m=r.id+":"+(l.bind||s||""),C=e&&typeof e=="object"&&!Array.isArray(e)?Object.assign({},e):{};R[m]?(R[m].baseValues=C,Object.keys(R[m].journal).forEach(b=>{M(R[m].journal[b],C[b])&&delete R[m].journal[b];})):R[m]={baseValues:C,journal:{}};let w=R[m];function S(b,f){return b.type==="boolean"?!!f.checked:b.type==="number"||b.type==="integer"?f.value!==""?parseFloat(f.value):0:f.value}function M(b,f){return JSON.stringify(b)===JSON.stringify(f)}function $(){return Object.assign({},w.baseValues,w.journal)}function j(){return Object.keys(w.journal).length>0}function _(b){b.querySelectorAll("[data-key]").forEach(f=>{let z=f.dataset.key,q=u[z];if(!q)return;let W=S(q,f),O=w.baseValues[z];M(W,O)?delete w.journal[z]:w.journal[z]=W;});}let H=document.createElement("form");H.className="row g-2",H.noValidate=true,Object.keys(u).forEach(b=>{let f=u[b],z=v.indexOf(b)>=0,q=["number","integer","boolean"].includes(f.type)||f.enum||f.format==="date",W=document.createElement("div");W.className=q?"col-12 col-md-6":"col-12";let O;if(f.type==="boolean"){let K=document.createElement("div");K.className="form-check mt-3",O=document.createElement("input"),O.type="checkbox",O.className="form-check-input";let ee=document.createElement("label");ee.className="form-check-label small",ee.textContent=f.title||b,K.appendChild(O),K.appendChild(ee),W.appendChild(K);}else {let K=document.createElement("label");K.className="form-label small mb-1",K.textContent=f.title||b,W.appendChild(K),f.enum?(O=document.createElement("select"),O.className="form-select form-select-sm",f.enum.forEach(ee=>{let re=document.createElement("option");re.value=ee,re.textContent=ee,O.appendChild(re);})):f.type==="number"||f.type==="integer"?(O=document.createElement("input"),O.type="number",O.className="form-control form-control-sm",f.minimum!=null&&(O.min=f.minimum),f.maximum!=null&&(O.max=f.maximum),f.type==="integer"&&(O.step="1")):f.format==="date"?(O=document.createElement("input"),O.type="date",O.className="form-control form-control-sm"):(O=document.createElement("input"),O.type="text",O.className="form-control form-control-sm",f.placeholder&&(O.placeholder=f.placeholder)),W.appendChild(O);}O.dataset.key=b,z&&(O.required=true);let fe=$()[b];fe!=null&&(f.type==="boolean"?O.checked=!!fe:f.format==="date"?O.value=String(fe).slice(0,10):O.value=fe),H.appendChild(W);});let ne=document.createElement("div");ne.className="col-12 mt-1";let Q=document.createElement("button");Q.type="button",Q.className="btn btn-sm btn-outline-secondary me-2"+(j()?"":" d-none"),Q.textContent="Discard";let se=document.createElement("button");se.type="submit",se.className="btn btn-sm btn-primary"+(j()?"":" d-none"),se.textContent="Save",ne.appendChild(Q),ne.appendChild(se),H.appendChild(ne),t.innerHTML="",t.appendChild(H),H.addEventListener("input",()=>{_(H);let b=j();se.classList.toggle("d-none",!b),Q.classList.toggle("d-none",!b);},{signal:n}),H.addEventListener("submit",b=>{if(b.preventDefault(),!H.checkValidity()){H.classList.add("was-validated");return}_(H);let f=$();k.onPatchState(r.id,{fieldValues:f}),se.textContent="Saving...",ie(t);},{signal:n}),Q.addEventListener("click",()=>{w.journal={},H.querySelectorAll("[data-key]").forEach(b=>{let f=b.dataset.key,z=u[f];if(!z)return;let q=w.baseValues[f];z.type==="boolean"?b.checked=!!q:z.format==="date"?b.value=q!=null?String(q).slice(0,10):"":b.value=q??"";}),Q.classList.add("d-none"),se.classList.add("d-none");},{signal:n});}function ue(e,t,a,r){let n=X(r.id).ac.signal,l=a.data||{},s=l.writeTo,d=typeof e=="string"?e:"",u=r.id+":"+(l.bind||s||"");D[u]?(D[u].baseContent=d,D[u].journal===d&&(D[u].journal=null)):D[u]={baseContent:d,journal:null};let v=D[u];function m(){return v.journal!=null}function C(){return v.journal!=null?v.journal:v.baseContent}function w(_){v.journal=_===v.baseContent?null:_;}t.innerHTML=`
|
|
81
|
+
<textarea class="form-control form-control-sm lc-notes-textarea" rows="8" placeholder="Write markdown...">${L(C())}</textarea>
|
|
1265
82
|
<div class="mt-2">
|
|
1266
|
-
<button class="btn btn-sm btn-outline-secondary me-2 lc-n-discard${
|
|
1267
|
-
<button class="btn btn-sm btn-primary lc-n-save${
|
|
1268
|
-
</div>`;
|
|
1269
|
-
|
|
1270
|
-
const textarea = el.querySelector('.lc-notes-textarea');
|
|
1271
|
-
const discardBtn = el.querySelector('.lc-n-discard');
|
|
1272
|
-
const saveBtn = el.querySelector('.lc-n-save');
|
|
1273
|
-
|
|
1274
|
-
function syncDirtyButtons() {
|
|
1275
|
-
const dirty = isDirty();
|
|
1276
|
-
saveBtn.classList.toggle('d-none', !dirty);
|
|
1277
|
-
discardBtn.classList.toggle('d-none', !dirty);
|
|
1278
|
-
}
|
|
1279
|
-
|
|
1280
|
-
textarea.addEventListener('input', () => {
|
|
1281
|
-
setJournal(textarea.value);
|
|
1282
|
-
syncDirtyButtons();
|
|
1283
|
-
}, { signal });
|
|
1284
|
-
|
|
1285
|
-
saveBtn.addEventListener('click', () => {
|
|
1286
|
-
const nextValue = textarea.value;
|
|
1287
|
-
cfg.onPatchState(node.id, { notes: nextValue });
|
|
1288
|
-
saveBtn.textContent = 'Saving...';
|
|
1289
|
-
_showSavingOverlay(el);
|
|
1290
|
-
}, { signal });
|
|
1291
|
-
|
|
1292
|
-
discardBtn.addEventListener('click', () => {
|
|
1293
|
-
st.journal = null;
|
|
1294
|
-
textarea.value = st.baseContent || '';
|
|
1295
|
-
syncDirtyButtons();
|
|
1296
|
-
}, { signal });
|
|
1297
|
-
}
|
|
1298
|
-
|
|
1299
|
-
// ---- todo ----
|
|
1300
|
-
|
|
1301
|
-
// ---- editable-table ----
|
|
1302
|
-
// Renders an array bound via `data.bind` as an inline-editable table.
|
|
1303
|
-
// Each row is editable in-place; changes are saved on blur (change event).
|
|
1304
|
-
// `data.writeTo` persists changes back to card_data (same pattern as form).
|
|
1305
|
-
// `data.columns` restricts which columns appear (and in what order).
|
|
1306
|
-
// `data.schema.properties[col].type` ("number"/"integer") controls input type.
|
|
1307
|
-
// `data.addRow` (default true) shows "+ Add row" button.
|
|
1308
|
-
// `data.deleteRow` (default true) shows per-row delete button.
|
|
1309
|
-
function _renderEditableTable(data, el, elemDef, node) {
|
|
1310
|
-
const cleanup = _getCleanup(node.id);
|
|
1311
|
-
const signal = cleanup.ac.signal;
|
|
1312
|
-
const ed = elemDef.data || {};
|
|
1313
|
-
// Standard convention:
|
|
1314
|
-
// - bind = read source
|
|
1315
|
-
// - writeTo = explicit write target for editable views
|
|
1316
|
-
// If bind already points at card_data, default writeTo to bind.
|
|
1317
|
-
const writeTo = ed.writeTo || ((typeof ed.bind === 'string' && ed.bind.startsWith('card_data.')) ? ed.bind : undefined);
|
|
1318
|
-
const schemaProps = (ed.schema && ed.schema.properties) || {};
|
|
1319
|
-
const canAdd = ed.addRow !== false;
|
|
1320
|
-
const canDelete = ed.deleteRow !== false;
|
|
1321
|
-
|
|
1322
|
-
// Derive columns from rows if not specified
|
|
1323
|
-
function getCols(rows) {
|
|
1324
|
-
if (ed.columns && ed.columns.length) return ed.columns;
|
|
1325
|
-
const s = new Set();
|
|
1326
|
-
rows.forEach(r => { if (r && typeof r === 'object') Object.keys(r).forEach(k => s.add(k)); });
|
|
1327
|
-
return [...s];
|
|
1328
|
-
}
|
|
1329
|
-
|
|
1330
|
-
// Base + journal overlay model:
|
|
1331
|
-
// effectiveRows = journalRows if present, else baseRows(bind).
|
|
1332
|
-
// Dirty is determined by journal presence (supports Save/Discard UX).
|
|
1333
|
-
const stateKey = node.id + ':' + (ed.bind || writeTo || '');
|
|
1334
|
-
const incomingRows = Array.isArray(data) ? data : [];
|
|
1335
|
-
const incomingCopy = incomingRows.map(r => Object.assign({}, r));
|
|
1336
|
-
|
|
1337
|
-
if (!_etState[stateKey]) {
|
|
1338
|
-
_etState[stateKey] = { baseRows: incomingCopy, journalRows: null };
|
|
1339
|
-
} else {
|
|
1340
|
-
_etState[stateKey].baseRows = incomingCopy;
|
|
1341
|
-
if (_etState[stateKey].journalRows && JSON.stringify(_etState[stateKey].journalRows) === JSON.stringify(incomingCopy)) {
|
|
1342
|
-
_etState[stateKey].journalRows = null;
|
|
1343
|
-
}
|
|
1344
|
-
}
|
|
1345
|
-
|
|
1346
|
-
const st = _etState[stateKey];
|
|
1347
|
-
|
|
1348
|
-
function isDirty() {
|
|
1349
|
-
return Array.isArray(st.journalRows);
|
|
1350
|
-
}
|
|
1351
|
-
|
|
1352
|
-
function getEffectiveRows() {
|
|
1353
|
-
const rows = Array.isArray(st.journalRows) ? st.journalRows : st.baseRows;
|
|
1354
|
-
return rows.map(r => Object.assign({}, r));
|
|
1355
|
-
}
|
|
1356
|
-
|
|
1357
|
-
function updateJournal(nextRows) {
|
|
1358
|
-
if (JSON.stringify(nextRows) === JSON.stringify(st.baseRows)) st.journalRows = null;
|
|
1359
|
-
else st.journalRows = nextRows.map(r => Object.assign({}, r));
|
|
1360
|
-
}
|
|
1361
|
-
|
|
1362
|
-
function markDirty() {
|
|
1363
|
-
const saveBtn = el.querySelector('.lc-et-save');
|
|
1364
|
-
const discardBtn = el.querySelector('.lc-et-discard');
|
|
1365
|
-
if (saveBtn) saveBtn.classList.remove('d-none');
|
|
1366
|
-
if (discardBtn) discardBtn.classList.remove('d-none');
|
|
1367
|
-
}
|
|
1368
|
-
|
|
1369
|
-
function commitSave() {
|
|
1370
|
-
const rows = getEffectiveRows();
|
|
1371
|
-
cfg.onPatchState(node.id, { fieldValues: rows });
|
|
1372
|
-
const saveBtn = el.querySelector('.lc-et-save');
|
|
1373
|
-
if (saveBtn) saveBtn.textContent = 'Saving...';
|
|
1374
|
-
_showSavingOverlay(el);
|
|
1375
|
-
}
|
|
1376
|
-
|
|
1377
|
-
function commitDiscard() {
|
|
1378
|
-
st.journalRows = null;
|
|
1379
|
-
build();
|
|
1380
|
-
}
|
|
1381
|
-
|
|
1382
|
-
function build() {
|
|
1383
|
-
const rows = getEffectiveRows();
|
|
1384
|
-
const cols = getCols(rows);
|
|
1385
|
-
|
|
1386
|
-
if (!cols.length && !canAdd) {
|
|
1387
|
-
el.innerHTML = `<p class="text-muted small">${_esc(ed.placeholder || 'No data')}</p>`;
|
|
1388
|
-
return;
|
|
1389
|
-
}
|
|
1390
|
-
|
|
1391
|
-
let h = '<div class="table-responsive"><table class="table table-sm table-bordered mb-0 lc-editable-table"><thead><tr>';
|
|
1392
|
-
cols.forEach(c => { h += `<th class="small text-nowrap">${_esc(c)}</th>`; });
|
|
1393
|
-
if (canDelete) h += '<th style="width:2rem"></th>';
|
|
1394
|
-
h += '</tr></thead><tbody>';
|
|
1395
|
-
|
|
1396
|
-
rows.forEach((row, rowIdx) => {
|
|
1397
|
-
h += `<tr>`;
|
|
1398
|
-
cols.forEach(c => {
|
|
1399
|
-
const v = row[c];
|
|
1400
|
-
const prop = schemaProps[c] || {};
|
|
1401
|
-
const isNum = prop.type === 'number' || prop.type === 'integer' || (v != null && typeof v === 'number');
|
|
1402
|
-
const displayVal = v != null ? String(v) : '';
|
|
1403
|
-
h += `<td class="p-0">` +
|
|
1404
|
-
`<input type="${isNum ? 'number' : 'text'}" ` +
|
|
1405
|
-
`class="form-control form-control-sm border-0 rounded-0 lc-et-cell" ` +
|
|
1406
|
-
`data-row="${rowIdx}" data-col="${_esc(c)}" value="${_esc(displayVal)}"` +
|
|
1407
|
-
`${isNum ? ' step="any"' : ''}>` +
|
|
1408
|
-
`</td>`;
|
|
1409
|
-
});
|
|
1410
|
-
if (canDelete) {
|
|
1411
|
-
h += `<td class="text-center align-middle p-0">` +
|
|
1412
|
-
`<button class="btn btn-sm btn-link text-danger p-0 lc-et-del" data-row="${rowIdx}" title="Remove row">✕</button>` +
|
|
1413
|
-
`</td>`;
|
|
1414
|
-
}
|
|
1415
|
-
h += '</tr>';
|
|
1416
|
-
});
|
|
1417
|
-
|
|
1418
|
-
if (!rows.length) {
|
|
1419
|
-
const span = cols.length + (canDelete ? 1 : 0);
|
|
1420
|
-
h += `<tr><td colspan="${span}" class="text-muted small text-center">${_esc(ed.placeholder || 'No rows')}</td></tr>`;
|
|
1421
|
-
}
|
|
1422
|
-
|
|
1423
|
-
h += '</tbody></table></div>';
|
|
1424
|
-
let footer = '';
|
|
1425
|
-
if (canAdd) footer += '<button class="btn btn-sm btn-outline-secondary mt-1 me-1 lc-et-add">+ Add row</button>';
|
|
1426
|
-
footer += `<button class="btn btn-sm btn-outline-secondary mt-1 me-1 lc-et-discard${isDirty() ? '' : ' d-none'}">Discard</button>`;
|
|
1427
|
-
footer += `<button class="btn btn-sm btn-primary mt-1 lc-et-save${isDirty() ? '' : ' d-none'}">Save</button>`;
|
|
1428
|
-
el.innerHTML = h + footer;
|
|
1429
|
-
|
|
1430
|
-
// Cell edit → update journal overlay and toggle Save/Discard.
|
|
1431
|
-
el.querySelectorAll('.lc-et-cell').forEach(inp => {
|
|
1432
|
-
inp.addEventListener('change', () => {
|
|
1433
|
-
const rowIdx = parseInt(inp.dataset.row);
|
|
1434
|
-
const colName = inp.dataset.col;
|
|
1435
|
-
const prop = schemaProps[colName] || {};
|
|
1436
|
-
const isNum = prop.type === 'number' || prop.type === 'integer' || inp.type === 'number';
|
|
1437
|
-
const nextRows = getEffectiveRows();
|
|
1438
|
-
if (!nextRows[rowIdx]) return;
|
|
1439
|
-
nextRows[rowIdx] = Object.assign({}, nextRows[rowIdx]);
|
|
1440
|
-
nextRows[rowIdx][colName] = isNum ? (inp.value !== '' ? parseFloat(inp.value) : 0) : inp.value;
|
|
1441
|
-
updateJournal(nextRows);
|
|
1442
|
-
if (isDirty()) markDirty();
|
|
1443
|
-
else {
|
|
1444
|
-
const saveBtn = el.querySelector('.lc-et-save');
|
|
1445
|
-
const discardBtn = el.querySelector('.lc-et-discard');
|
|
1446
|
-
if (saveBtn) saveBtn.classList.add('d-none');
|
|
1447
|
-
if (discardBtn) discardBtn.classList.add('d-none');
|
|
1448
|
-
}
|
|
1449
|
-
}, { signal });
|
|
1450
|
-
});
|
|
1451
|
-
|
|
1452
|
-
// Delete row — updates journal and rebuilds.
|
|
1453
|
-
el.querySelectorAll('.lc-et-del').forEach(btn => {
|
|
1454
|
-
btn.addEventListener('click', () => {
|
|
1455
|
-
const rowIdx = parseInt(btn.dataset.row);
|
|
1456
|
-
const nextRows = getEffectiveRows().filter((_, i) => i !== rowIdx);
|
|
1457
|
-
updateJournal(nextRows);
|
|
1458
|
-
build();
|
|
1459
|
-
}, { signal });
|
|
1460
|
-
});
|
|
1461
|
-
|
|
1462
|
-
// Add row — appends blank row to journal and rebuilds.
|
|
1463
|
-
const addBtn = el.querySelector('.lc-et-add');
|
|
1464
|
-
if (addBtn) {
|
|
1465
|
-
addBtn.addEventListener('click', () => {
|
|
1466
|
-
const newRow = {};
|
|
1467
|
-
const nextRows = getEffectiveRows();
|
|
1468
|
-
getCols(nextRows).forEach(c => { newRow[c] = ''; });
|
|
1469
|
-
nextRows.push(newRow);
|
|
1470
|
-
updateJournal(nextRows);
|
|
1471
|
-
build();
|
|
1472
|
-
}, { signal });
|
|
1473
|
-
}
|
|
1474
|
-
|
|
1475
|
-
// Save/Discard controls.
|
|
1476
|
-
const discardBtn = el.querySelector('.lc-et-discard');
|
|
1477
|
-
if (discardBtn) {
|
|
1478
|
-
discardBtn.addEventListener('click', () => {
|
|
1479
|
-
commitDiscard();
|
|
1480
|
-
}, { signal });
|
|
1481
|
-
}
|
|
1482
|
-
|
|
1483
|
-
const saveBtn = el.querySelector('.lc-et-save');
|
|
1484
|
-
if (saveBtn) {
|
|
1485
|
-
saveBtn.addEventListener('click', () => {
|
|
1486
|
-
commitSave();
|
|
1487
|
-
saveBtn.textContent = '✓ Saved';
|
|
1488
|
-
setTimeout(() => { saveBtn.textContent = 'Save'; }, 1500);
|
|
1489
|
-
}, { signal });
|
|
1490
|
-
}
|
|
1491
|
-
}
|
|
1492
|
-
|
|
1493
|
-
build();
|
|
1494
|
-
}
|
|
1495
|
-
|
|
1496
|
-
// ---- todo ----
|
|
1497
|
-
|
|
1498
|
-
function _renderTodo(data, el, elemDef, node) {
|
|
1499
|
-
const cleanup = _getCleanup(node.id);
|
|
1500
|
-
const signal = cleanup.ac.signal;
|
|
1501
|
-
const ed = elemDef.data || {};
|
|
1502
|
-
const writeTo = ed.writeTo;
|
|
1503
|
-
|
|
1504
|
-
// --- Journal-style dirty tracking ---
|
|
1505
|
-
// currentState = last confirmed server state; pending = local working copy
|
|
1506
|
-
// On SSE re-render: if dirty (action in-flight), keep pending; if clean, sync from server
|
|
1507
|
-
const stateKey = node.id + ':' + (writeTo || '');
|
|
1508
|
-
const incomingItems = Array.isArray(data) ? data.map(r => Object.assign({}, r)) : [];
|
|
1509
|
-
|
|
1510
|
-
if (!_todoState[stateKey]) {
|
|
1511
|
-
_todoState[stateKey] = { currentState: incomingItems, pending: incomingItems.map(r => Object.assign({}, r)) };
|
|
1512
|
-
} else {
|
|
1513
|
-
const s = _todoState[stateKey];
|
|
1514
|
-
const wasDirty = JSON.stringify(s.currentState) !== JSON.stringify(s.pending);
|
|
1515
|
-
s.currentState = incomingItems;
|
|
1516
|
-
if (!wasDirty) s.pending = incomingItems.map(r => Object.assign({}, r));
|
|
1517
|
-
// if dirty, pending stays so in-flight changes survive the SSE tick
|
|
1518
|
-
}
|
|
1519
|
-
const st = _todoState[stateKey];
|
|
1520
|
-
|
|
1521
|
-
function save() {
|
|
1522
|
-
if (writeTo) _deepSet(node, writeTo, st.pending);
|
|
1523
|
-
cfg.onPatchState(node.id, { fieldValues: st.pending });
|
|
1524
|
-
notify(node.id, st.pending);
|
|
1525
|
-
// mark clean after save so next SSE sync resumes normally
|
|
1526
|
-
st.currentState = st.pending.map(r => Object.assign({}, r));
|
|
1527
|
-
}
|
|
1528
|
-
|
|
1529
|
-
function build() {
|
|
1530
|
-
const items = st.pending;
|
|
1531
|
-
let h = '<div class="lc-todo-list">';
|
|
1532
|
-
items.forEach((item, i) => {
|
|
1533
|
-
const chk = item.done ? ' checked' : '';
|
|
1534
|
-
const strike = item.done ? ' text-decoration-line-through text-muted' : '';
|
|
1535
|
-
h += `<div class="lc-todo-item">`;
|
|
1536
|
-
h += `<input class="form-check-input flex-shrink-0" type="checkbox"${chk} data-idx="${i}">`;
|
|
1537
|
-
h += `<span class="small flex-grow-1${strike}">${_esc(item.text)}</span>`;
|
|
1538
|
-
h += `<button class="btn btn-sm btn-link text-danger p-0" data-rm="${i}" title="Remove">×</button></div>`;
|
|
1539
|
-
});
|
|
1540
|
-
h += '</div>';
|
|
1541
|
-
h += '<div class="input-group input-group-sm mt-2"><input type="text" class="form-control" placeholder="Add item...">';
|
|
1542
|
-
h += '<button class="btn btn-outline-secondary lc-todo-add">+</button></div>';
|
|
1543
|
-
el.innerHTML = h;
|
|
1544
|
-
|
|
1545
|
-
el.querySelectorAll('input[data-idx]').forEach(cb => {
|
|
1546
|
-
cb.addEventListener('change', () => {
|
|
1547
|
-
st.pending[parseInt(cb.dataset.idx)].done = cb.checked;
|
|
1548
|
-
save(); build();
|
|
1549
|
-
}, { signal });
|
|
1550
|
-
});
|
|
1551
|
-
el.querySelectorAll('[data-rm]').forEach(btn => {
|
|
1552
|
-
btn.addEventListener('click', () => {
|
|
1553
|
-
st.pending.splice(parseInt(btn.dataset.rm), 1);
|
|
1554
|
-
save(); build();
|
|
1555
|
-
}, { signal });
|
|
1556
|
-
});
|
|
1557
|
-
const addInput = el.querySelector('.input-group input');
|
|
1558
|
-
const addBtn = el.querySelector('.lc-todo-add');
|
|
1559
|
-
const addItem = () => {
|
|
1560
|
-
const t = addInput.value.trim();
|
|
1561
|
-
if (!t) return;
|
|
1562
|
-
st.pending.push({ text: t, done: false });
|
|
1563
|
-
save(); build();
|
|
1564
|
-
};
|
|
1565
|
-
addBtn.addEventListener('click', addItem, { signal });
|
|
1566
|
-
addInput.addEventListener('keydown', e => { if (e.key === 'Enter') { e.preventDefault(); addItem(); } }, { signal });
|
|
1567
|
-
}
|
|
1568
|
-
build();
|
|
1569
|
-
}
|
|
1570
|
-
|
|
1571
|
-
// ---- alert ----
|
|
1572
|
-
|
|
1573
|
-
function _renderAlert(data, el, elemDef) {
|
|
1574
|
-
const ed = elemDef.data || {};
|
|
1575
|
-
const thresholds = ed.thresholds || {};
|
|
1576
|
-
const value = typeof data === 'number' ? data : (data && data.value != null ? data.value : null);
|
|
1577
|
-
|
|
1578
|
-
let level = 'unknown', color = 'secondary';
|
|
1579
|
-
if (value != null) {
|
|
1580
|
-
if (thresholds.green && _evalThreshold(value, thresholds.green)) { level = 'green'; color = 'success'; }
|
|
1581
|
-
else if (thresholds.amber && _evalThreshold(value, thresholds.amber)) { level = 'amber'; color = 'warning'; }
|
|
1582
|
-
else { level = 'red'; color = 'danger'; }
|
|
1583
|
-
}
|
|
1584
|
-
|
|
1585
|
-
el.innerHTML = `
|
|
83
|
+
<button class="btn btn-sm btn-outline-secondary me-2 lc-n-discard${m()?"":" d-none"}" type="button">Discard</button>
|
|
84
|
+
<button class="btn btn-sm btn-primary lc-n-save${m()?"":" d-none"}" type="button">Save</button>
|
|
85
|
+
</div>`;let S=t.querySelector(".lc-notes-textarea"),M=t.querySelector(".lc-n-discard"),$=t.querySelector(".lc-n-save");function j(){let _=m();$.classList.toggle("d-none",!_),M.classList.toggle("d-none",!_);}S.addEventListener("input",()=>{w(S.value),j();},{signal:n}),$.addEventListener("click",()=>{let _=S.value;k.onPatchState(r.id,{fieldValues:{notes:_}}),$.textContent="Saving...",ie(t);},{signal:n}),M.addEventListener("click",()=>{v.journal=null,S.value=v.baseContent||"",j();},{signal:n});}function ye(e,t,a,r){let n=X(r.id).ac.signal,l=a.data||{},s=l.writeTo||(typeof l.bind=="string"&&l.bind.startsWith("card_data.")?l.bind:void 0),d=l.schema&&l.schema.properties||{},u=l.addRow!==false,v=l.deleteRow!==false;function m(b){if(l.columns&&l.columns.length)return l.columns;let f=new Set;return b.forEach(z=>{z&&typeof z=="object"&&Object.keys(z).forEach(q=>f.add(q));}),[...f]}let C=r.id+":"+(l.bind||s||""),S=(Array.isArray(e)?e:[]).map(b=>Object.assign({},b));J[C]?(J[C].baseRows=S,J[C].journalRows&&JSON.stringify(J[C].journalRows)===JSON.stringify(S)&&(J[C].journalRows=null)):J[C]={baseRows:S,journalRows:null};let M=J[C];function $(){return Array.isArray(M.journalRows)}function j(){return (Array.isArray(M.journalRows)?M.journalRows:M.baseRows).map(f=>Object.assign({},f))}function _(b){JSON.stringify(b)===JSON.stringify(M.baseRows)?M.journalRows=null:M.journalRows=b.map(f=>Object.assign({},f));}function H(){let b=t.querySelector(".lc-et-save"),f=t.querySelector(".lc-et-discard");b&&b.classList.remove("d-none"),f&&f.classList.remove("d-none");}function ne(){let b=j();k.onPatchState(r.id,{fieldValues:b});let f=t.querySelector(".lc-et-save");f&&(f.textContent="Saving..."),ie(t);}function Q(){M.journalRows=null,se();}function se(){let b=j(),f=m(b);if(!f.length&&!u){t.innerHTML=`<p class="text-muted small">${L(l.placeholder||"No data")}</p>`;return}let z='<div class="table-responsive"><table class="table table-sm table-bordered mb-0 lc-editable-table"><thead><tr>';if(f.forEach(K=>{z+=`<th class="small text-nowrap">${L(K)}</th>`;}),v&&(z+='<th style="width:2rem"></th>'),z+="</tr></thead><tbody>",b.forEach((K,ee)=>{z+="<tr>",f.forEach(re=>{let ge=K[re],ke=d[re]||{},be=ke.type==="number"||ke.type==="integer"||ge!=null&&typeof ge=="number",qe=ge!=null?String(ge):"";z+=`<td class="p-0"><input type="${be?"number":"text"}" class="form-control form-control-sm border-0 rounded-0 lc-et-cell" data-row="${ee}" data-col="${L(re)}" value="${L(qe)}"${be?' step="any"':""}></td>`;}),v&&(z+=`<td class="text-center align-middle p-0"><button class="btn btn-sm btn-link text-danger p-0 lc-et-del" data-row="${ee}" title="Remove row">\u2715</button></td>`),z+="</tr>";}),!b.length){let K=f.length+(v?1:0);z+=`<tr><td colspan="${K}" class="text-muted small text-center">${L(l.placeholder||"No rows")}</td></tr>`;}z+="</tbody></table></div>";let q="";u&&(q+='<button class="btn btn-sm btn-outline-secondary mt-1 me-1 lc-et-add">+ Add row</button>'),q+=`<button class="btn btn-sm btn-outline-secondary mt-1 me-1 lc-et-discard${$()?"":" d-none"}">Discard</button>`,q+=`<button class="btn btn-sm btn-primary mt-1 lc-et-save${$()?"":" d-none"}">Save</button>`,t.innerHTML=z+q,t.querySelectorAll(".lc-et-cell").forEach(K=>{K.addEventListener("change",()=>{let ee=parseInt(K.dataset.row),re=K.dataset.col,ge=d[re]||{},ke=ge.type==="number"||ge.type==="integer"||K.type==="number",be=j();if(be[ee])if(be[ee]=Object.assign({},be[ee]),be[ee][re]=ke?K.value!==""?parseFloat(K.value):0:K.value,_(be),$())H();else {let qe=t.querySelector(".lc-et-save"),Qe=t.querySelector(".lc-et-discard");qe&&qe.classList.add("d-none"),Qe&&Qe.classList.add("d-none");}},{signal:n});}),t.querySelectorAll(".lc-et-del").forEach(K=>{K.addEventListener("click",()=>{let ee=parseInt(K.dataset.row),re=j().filter((ge,ke)=>ke!==ee);_(re),se();},{signal:n});});let W=t.querySelector(".lc-et-add");W&&W.addEventListener("click",()=>{let K={},ee=j();m(ee).forEach(re=>{K[re]="";}),ee.push(K),_(ee),se();},{signal:n});let O=t.querySelector(".lc-et-discard");O&&O.addEventListener("click",()=>{Q();},{signal:n});let fe=t.querySelector(".lc-et-save");fe&&fe.addEventListener("click",()=>{ne(),fe.textContent="\u2713 Saved",setTimeout(()=>{fe.textContent="Save";},1500);},{signal:n});}se();}function $e(e,t,a,r){let n=X(r.id).ac.signal,s=(a.data||{}).writeTo,d=r.id+":"+(s||""),u=Array.isArray(e)?e.map(w=>Object.assign({},w)):[];if(!V[d])V[d]={currentState:u,pending:u.map(w=>Object.assign({},w))};else {let w=V[d],S=JSON.stringify(w.currentState)!==JSON.stringify(w.pending);w.currentState=u,S||(w.pending=u.map(M=>Object.assign({},M)));}let v=V[d];function m(){s&&Xe(r,s,v.pending),k.onPatchState(r.id,{fieldValues:v.pending}),me(r.id,v.pending),v.currentState=v.pending.map(w=>Object.assign({},w));}function C(){let w=v.pending,S='<div class="lc-todo-list">';w.forEach((_,H)=>{let ne=_.done?" checked":"",Q=_.done?" text-decoration-line-through text-muted":"";S+='<div class="lc-todo-item">',S+=`<input class="form-check-input flex-shrink-0" type="checkbox"${ne} data-idx="${H}">`,S+=`<span class="small flex-grow-1${Q}">${L(_.text)}</span>`,S+=`<button class="btn btn-sm btn-link text-danger p-0" data-rm="${H}" title="Remove">\xD7</button></div>`;}),S+="</div>",S+='<div class="input-group input-group-sm mt-2"><input type="text" class="form-control" placeholder="Add item...">',S+='<button class="btn btn-outline-secondary lc-todo-add">+</button></div>',t.innerHTML=S,t.querySelectorAll("input[data-idx]").forEach(_=>{_.addEventListener("change",()=>{v.pending[parseInt(_.dataset.idx)].done=_.checked,m(),C();},{signal:n});}),t.querySelectorAll("[data-rm]").forEach(_=>{_.addEventListener("click",()=>{v.pending.splice(parseInt(_.dataset.rm),1),m(),C();},{signal:n});});let M=t.querySelector(".input-group input"),$=t.querySelector(".lc-todo-add"),j=()=>{let _=M.value.trim();_&&(v.pending.push({text:_,done:false}),m(),C());};$.addEventListener("click",j,{signal:n}),M.addEventListener("keydown",_=>{_.key==="Enter"&&(_.preventDefault(),j());},{signal:n});}C();}function Ie(e,t,a){let p=(a.data||{}).thresholds||{},n=typeof e=="number"?e:e&&e.value!=null?e.value:null,l="unknown",s="secondary";n!=null&&(p.green&&Ue(n,p.green)?(l="green",s="success"):p.amber&&Ue(n,p.amber)?(l="amber",s="warning"):(l="red",s="danger")),t.innerHTML=`
|
|
1586
86
|
<div class="d-flex align-items-center gap-3 py-2">
|
|
1587
|
-
<span class="lc-alert-dot lc-alert-${
|
|
87
|
+
<span class="lc-alert-dot lc-alert-${l}"></span>
|
|
1588
88
|
<div class="flex-grow-1">
|
|
1589
|
-
<div class="fw-bold">${
|
|
1590
|
-
${
|
|
89
|
+
<div class="fw-bold">${n!=null?L(String(n)):"\u2014"}</div>
|
|
90
|
+
${a.label?`<div class="text-muted small">${L(a.label)}</div>`:""}
|
|
1591
91
|
</div>
|
|
1592
|
-
<span class="badge bg-${
|
|
1593
|
-
</div>`;
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
// ---- narrative ----
|
|
1597
|
-
|
|
1598
|
-
function _renderNarrative(data, el) {
|
|
1599
|
-
const text = typeof data === 'string' ? data : (data && data.text ? data.text : '');
|
|
1600
|
-
if (!text) { el.innerHTML = '<p class="text-muted small fst-italic">No narrative yet. Click refresh to generate.</p>'; return; }
|
|
1601
|
-
el.innerHTML = `<div class="small">${_renderMd(text)}</div>`;
|
|
1602
|
-
}
|
|
1603
|
-
|
|
1604
|
-
// ---- badge ----
|
|
1605
|
-
|
|
1606
|
-
function _renderBadge(data, el, elemDef) {
|
|
1607
|
-
const ed = elemDef.data || {};
|
|
1608
|
-
const map = ed.colorMap || {};
|
|
1609
|
-
const val = data != null ? String(data) : '';
|
|
1610
|
-
const bsMap = { green: 'success', amber: 'warning', red: 'danger', blue: 'primary' };
|
|
1611
|
-
const bs = bsMap[map[val]] || map[val] || 'secondary';
|
|
1612
|
-
el.innerHTML = `<span class="badge bg-${_esc(bs)}">${_esc(val)}</span>`;
|
|
1613
|
-
}
|
|
1614
|
-
|
|
1615
|
-
// ---- text ----
|
|
1616
|
-
|
|
1617
|
-
function _renderText(data, el, elemDef) {
|
|
1618
|
-
const ed = elemDef.data || {};
|
|
1619
|
-
const format = ed.format || 'default';
|
|
1620
|
-
const style = elemDef.style || ed.style || 'default';
|
|
1621
|
-
const hideIfEmpty = ed.hideIfEmpty || elemDef.hideIfEmpty;
|
|
1622
|
-
|
|
1623
|
-
if (hideIfEmpty && (data == null || data === '')) { el.innerHTML = ''; return; }
|
|
1624
|
-
|
|
1625
|
-
// Handle file-links format
|
|
1626
|
-
if (format === 'file-links') {
|
|
1627
|
-
if (!Array.isArray(data) || data.length === 0) {
|
|
1628
|
-
el.innerHTML = '<div class="text-muted small">No files uploaded</div>';
|
|
1629
|
-
return;
|
|
1630
|
-
}
|
|
1631
|
-
const htmlParts = [];
|
|
1632
|
-
data.forEach((file, idx) => {
|
|
1633
|
-
if (!file || !file.stored_name) return;
|
|
1634
|
-
const name = file.name || file.stored_name;
|
|
1635
|
-
const cardId = elemDef.data && elemDef.data.cardId ? elemDef.data.cardId : 'unknown';
|
|
1636
|
-
const downloadUrl = `/api/example-board/server/cards/${encodeURIComponent(cardId)}/files/${idx}?sn=${encodeURIComponent(file.stored_name)}`;
|
|
1637
|
-
const size = file.size ? ` (${Math.round(file.size / 1024)}KB)` : '';
|
|
1638
|
-
htmlParts.push(`<div class="mb-2"><a href="${downloadUrl}" class="btn btn-sm btn-outline-secondary">${_esc(name)}${_esc(size)}</a></div>`);
|
|
1639
|
-
});
|
|
1640
|
-
const html = htmlParts.join('');
|
|
1641
|
-
el.innerHTML = html;
|
|
1642
|
-
return;
|
|
1643
|
-
}
|
|
1644
|
-
|
|
1645
|
-
// Default text rendering
|
|
1646
|
-
const tag = style === 'heading' ? 'h4' : 'div';
|
|
1647
|
-
const cls = style === 'muted' ? 'text-muted small'
|
|
1648
|
-
: style === 'muted-italic' ? 'text-muted small fst-italic'
|
|
1649
|
-
: style === 'heading' ? 'fw-bold'
|
|
1650
|
-
: 'small';
|
|
1651
|
-
el.innerHTML = `<${tag} class="${cls}">${_esc(data != null ? String(data) : '')}</${tag}>`;
|
|
1652
|
-
}
|
|
1653
|
-
|
|
1654
|
-
// ---- markdown ----
|
|
1655
|
-
|
|
1656
|
-
function _renderMarkdown(data, el) {
|
|
1657
|
-
let text = '';
|
|
1658
|
-
if (typeof data === 'string') text = data;
|
|
1659
|
-
else if (data && typeof data === 'object' && data.text) text = data.text;
|
|
1660
|
-
else if (data != null) text = JSON.stringify(data, null, 2);
|
|
1661
|
-
el.innerHTML = text ? _renderMd(text) : '';
|
|
1662
|
-
}
|
|
1663
|
-
|
|
1664
|
-
// ---- custom (fallback to JSON) ----
|
|
1665
|
-
|
|
1666
|
-
function _renderCustom(data, el) {
|
|
1667
|
-
if (data == null) { el.innerHTML = ''; return; }
|
|
1668
|
-
el.innerHTML = `<pre class="small mb-0">${_esc(JSON.stringify(data, null, 2))}</pre>`;
|
|
1669
|
-
}
|
|
1670
|
-
|
|
1671
|
-
// ---- file-upload ----
|
|
1672
|
-
|
|
1673
|
-
function _renderFileUpload(data, el, elemDef, node) {
|
|
1674
|
-
const cleanup = _getCleanup(node.id);
|
|
1675
|
-
const signal = cleanup.ac.signal;
|
|
1676
|
-
const ed = elemDef.data || {};
|
|
1677
|
-
const uploaded = Array.isArray(data) ? data : [];
|
|
1678
|
-
const showUploadedList = ed.showUploadedList === true;
|
|
1679
|
-
const showUpload = ed.upload !== false;
|
|
1680
|
-
const accept = ed.accept || ['.txt','.csv','.md','.json','.html','.xml','.pdf','.xlsx','.docx','.pptx','.png','.jpg','.jpeg'];
|
|
1681
|
-
const acceptSet = new Set(accept.map(e => e.toLowerCase()));
|
|
1682
|
-
const multiple = ed.multiple !== false;
|
|
1683
|
-
const placeholder = ed.placeholder || 'Drop files here or click to browse';
|
|
1684
|
-
const uid = 'lc-fu-' + (elemDef.id || Math.random().toString(36).slice(2, 8));
|
|
1685
|
-
|
|
1686
|
-
let stagedFiles = el._stagedFiles || [];
|
|
1687
|
-
el._stagedFiles = stagedFiles;
|
|
1688
|
-
let uploadStatus = el._uploadStatus || {};
|
|
1689
|
-
el._uploadStatus = uploadStatus;
|
|
1690
|
-
|
|
1691
|
-
function keyForFile(f) {
|
|
1692
|
-
return `${f.name}::${f.size}::${f.lastModified || 0}`;
|
|
1693
|
-
}
|
|
1694
|
-
|
|
1695
|
-
let h = '';
|
|
1696
|
-
|
|
1697
|
-
// Drop zone
|
|
1698
|
-
if (showUpload) {
|
|
1699
|
-
h += `<div class="lc-dropzone mb-2" id="${uid}-dz">`;
|
|
1700
|
-
h += '<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" class="text-muted mb-1"><path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4"/><polyline points="17 8 12 3 7 8"/><line x1="12" y1="3" x2="12" y2="15"/></svg>';
|
|
1701
|
-
h += `<div class="small text-muted">${_esc(placeholder)}</div>`;
|
|
1702
|
-
h += `<input type="file" id="${uid}-fi" class="d-none"${multiple ? ' multiple' : ''} accept="${accept.join(',')}">`;
|
|
1703
|
-
h += '</div>';
|
|
1704
|
-
h += `<div id="${uid}-staged"></div>`;
|
|
1705
|
-
}
|
|
1706
|
-
|
|
1707
|
-
// Uploaded files list
|
|
1708
|
-
if (showUploadedList && uploaded.length) {
|
|
1709
|
-
h += '<div class="lc-uploaded-files">';
|
|
1710
|
-
uploaded.forEach(f => {
|
|
1711
|
-
const name = typeof f === 'string' ? f : (f.name || '');
|
|
1712
|
-
const url = typeof f === 'string' ? null : f.url;
|
|
1713
|
-
h += '<div class="d-flex align-items-center gap-1 small mb-1">';
|
|
1714
|
-
h += '<svg width="12" height="12" 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>';
|
|
1715
|
-
if (url) h += `<a href="${_esc(url)}" class="text-truncate" target="_blank" download>${_esc(name)}</a>`;
|
|
1716
|
-
else h += `<span class="text-truncate">${_esc(name)}</span>`;
|
|
1717
|
-
h += '</div>';
|
|
1718
|
-
});
|
|
1719
|
-
h += '</div>';
|
|
1720
|
-
}
|
|
1721
|
-
|
|
1722
|
-
if (!showUpload && !uploaded.length) {
|
|
1723
|
-
h = `<p class="text-muted small">${_esc(ed.placeholder || 'No files')}</p>`;
|
|
1724
|
-
}
|
|
1725
|
-
|
|
1726
|
-
el.innerHTML = h;
|
|
1727
|
-
|
|
1728
|
-
if (!showUpload) {
|
|
1729
|
-
el._fileUpload = { getFiles: () => [], clear: () => {} };
|
|
1730
|
-
return;
|
|
1731
|
-
}
|
|
1732
|
-
|
|
1733
|
-
const dz = el.querySelector('#' + uid + '-dz');
|
|
1734
|
-
const fi = el.querySelector('#' + uid + '-fi');
|
|
1735
|
-
const stagedEl = el.querySelector('#' + uid + '-staged');
|
|
1736
|
-
if (!dz) return;
|
|
1737
|
-
|
|
1738
|
-
function addFiles(fileList) {
|
|
1739
|
-
const newlyAdded = [];
|
|
1740
|
-
for (const f of fileList) {
|
|
1741
|
-
const ext = '.' + f.name.split('.').pop().toLowerCase();
|
|
1742
|
-
if (!acceptSet.has(ext)) continue;
|
|
1743
|
-
if (!stagedFiles.find(s => s.name === f.name)) {
|
|
1744
|
-
stagedFiles.push(f);
|
|
1745
|
-
newlyAdded.push(f);
|
|
1746
|
-
uploadStatus[keyForFile(f)] = 'uploading';
|
|
1747
|
-
}
|
|
1748
|
-
}
|
|
1749
|
-
renderStaged();
|
|
1750
|
-
|
|
1751
|
-
// Server demos can upload real file blobs immediately via onAction.
|
|
1752
|
-
if (newlyAdded.length && typeof cfg.onAction === 'function') {
|
|
1753
|
-
Promise.resolve(cfg.onAction(node.id, 'file-upload', { files: newlyAdded, elemId: elemDef.id }))
|
|
1754
|
-
.then(() => {
|
|
1755
|
-
const uploadedKeys = new Set(newlyAdded.map(keyForFile));
|
|
1756
|
-
stagedFiles = stagedFiles.filter((f) => !uploadedKeys.has(keyForFile(f)));
|
|
1757
|
-
el._stagedFiles = stagedFiles;
|
|
1758
|
-
newlyAdded.forEach((f) => { delete uploadStatus[keyForFile(f)]; });
|
|
1759
|
-
el._uploadStatus = uploadStatus;
|
|
1760
|
-
renderStaged();
|
|
1761
|
-
})
|
|
1762
|
-
.catch(() => {
|
|
1763
|
-
newlyAdded.forEach((f) => { uploadStatus[keyForFile(f)] = 'error'; });
|
|
1764
|
-
el._uploadStatus = uploadStatus;
|
|
1765
|
-
renderStaged();
|
|
1766
|
-
});
|
|
1767
|
-
}
|
|
1768
|
-
}
|
|
1769
|
-
|
|
1770
|
-
function renderStaged() {
|
|
1771
|
-
if (!stagedFiles.length) { stagedEl.innerHTML = ''; return; }
|
|
1772
|
-
let sh = '';
|
|
1773
|
-
stagedFiles.forEach((f, i) => {
|
|
1774
|
-
const status = uploadStatus[keyForFile(f)] || 'ready';
|
|
1775
|
-
sh += '<div class="lc-staged-file">';
|
|
1776
|
-
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>';
|
|
1777
|
-
sh += `<span class="small flex-grow-1 text-truncate">${_esc(f.name)}</span>`;
|
|
1778
|
-
if (status === 'uploading') {
|
|
1779
|
-
sh += '<span class="spinner-border spinner-border-sm text-secondary me-1" role="status" aria-label="Uploading"></span>';
|
|
1780
|
-
} else if (status === 'error') {
|
|
1781
|
-
sh += '<span class="badge bg-danger-subtle text-danger border border-danger-subtle me-1">Failed</span>';
|
|
1782
|
-
}
|
|
1783
|
-
sh += `<button class="btn btn-sm btn-link text-danger p-0 lc-rm-staged" data-idx="${i}">×</button>`;
|
|
1784
|
-
sh += '</div>';
|
|
1785
|
-
});
|
|
1786
|
-
stagedEl.innerHTML = sh;
|
|
1787
|
-
stagedEl.querySelectorAll('.lc-rm-staged').forEach(btn => {
|
|
1788
|
-
btn.addEventListener('click', () => {
|
|
1789
|
-
const idx = parseInt(btn.dataset.idx);
|
|
1790
|
-
const f = stagedFiles[idx];
|
|
1791
|
-
if (f) delete uploadStatus[keyForFile(f)];
|
|
1792
|
-
stagedFiles.splice(idx, 1);
|
|
1793
|
-
el._stagedFiles = stagedFiles;
|
|
1794
|
-
el._uploadStatus = uploadStatus;
|
|
1795
|
-
renderStaged();
|
|
1796
|
-
}, { signal });
|
|
1797
|
-
});
|
|
1798
|
-
}
|
|
1799
|
-
|
|
1800
|
-
dz.addEventListener('click', () => fi.click(), { signal });
|
|
1801
|
-
dz.addEventListener('dragover', e => { e.preventDefault(); dz.classList.add('lc-drag-over'); }, { signal });
|
|
1802
|
-
dz.addEventListener('dragleave', () => dz.classList.remove('lc-drag-over'), { signal });
|
|
1803
|
-
dz.addEventListener('drop', e => { e.preventDefault(); dz.classList.remove('lc-drag-over'); addFiles(e.dataTransfer.files); }, { signal });
|
|
1804
|
-
fi.addEventListener('change', e => { addFiles(e.target.files); e.target.value = ''; }, { signal });
|
|
1805
|
-
|
|
1806
|
-
renderStaged();
|
|
1807
|
-
|
|
1808
|
-
el._fileUpload = {
|
|
1809
|
-
getFiles: () => stagedFiles,
|
|
1810
|
-
clear: () => { stagedFiles = []; uploadStatus = {}; el._stagedFiles = []; el._uploadStatus = {}; renderStaged(); },
|
|
1811
|
-
disable: () => { dz.classList.add('lc-disabled'); fi.disabled = true; },
|
|
1812
|
-
enable: () => { dz.classList.remove('lc-disabled'); fi.disabled = false; },
|
|
1813
|
-
};
|
|
1814
|
-
}
|
|
1815
|
-
|
|
1816
|
-
// ---- chat (element kind) ----
|
|
1817
|
-
|
|
1818
|
-
function _renderChatEl(data, el, elemDef, node) {
|
|
1819
|
-
const cleanup = _getCleanup(node.id);
|
|
1820
|
-
const signal = cleanup.ac.signal;
|
|
1821
|
-
const ed = elemDef.data || {};
|
|
1822
|
-
const messages = Array.isArray(data) ? data : [];
|
|
1823
|
-
const placeholder = ed.placeholder || 'Type a message...';
|
|
1824
|
-
const canAttach = ed.fileAttach === true;
|
|
1825
|
-
const accept = ed.fileAccept || ['.txt','.csv','.md','.json','.html','.xml','.pdf','.xlsx','.docx','.pptx','.png','.jpg','.jpeg'];
|
|
1826
|
-
const uid = 'lc-ch-' + (elemDef.id || Math.random().toString(36).slice(2, 8));
|
|
1827
|
-
|
|
1828
|
-
let h = '<div class="lc-chat-el">';
|
|
1829
|
-
h += `<div class="lc-chat-body" id="${uid}-body"></div>`;
|
|
1830
|
-
h += '<div class="lc-chat-input-bar">';
|
|
1831
|
-
if (canAttach) {
|
|
1832
|
-
h += `<input type="file" id="${uid}-fi" class="d-none" multiple accept="${accept.join(',')}">`;
|
|
1833
|
-
h += `<button class="btn btn-sm btn-outline-secondary" id="${uid}-attach" title="Attach files" type="button">`;
|
|
1834
|
-
h += '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><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>';
|
|
1835
|
-
h += '</button>';
|
|
1836
|
-
}
|
|
1837
|
-
h += `<input type="text" class="form-control form-control-sm flex-grow-1" id="${uid}-input" placeholder="${_esc(placeholder)}">`;
|
|
1838
|
-
h += `<button class="btn btn-sm btn-outline-primary" id="${uid}-send" type="button">`;
|
|
1839
|
-
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>';
|
|
1840
|
-
h += '</button></div>';
|
|
1841
|
-
if (canAttach) h += `<div id="${uid}-staged" class="mt-1"></div>`;
|
|
1842
|
-
h += '</div>';
|
|
1843
|
-
|
|
1844
|
-
el.innerHTML = h;
|
|
1845
|
-
|
|
1846
|
-
const body = el.querySelector('#' + uid + '-body');
|
|
1847
|
-
const input = el.querySelector('#' + uid + '-input');
|
|
1848
|
-
const sendBtn = el.querySelector('#' + uid + '-send');
|
|
1849
|
-
const attachBtn = canAttach ? el.querySelector('#' + uid + '-attach') : null;
|
|
1850
|
-
const fileInput = canAttach ? el.querySelector('#' + uid + '-fi') : null;
|
|
1851
|
-
const stagedEl = canAttach ? el.querySelector('#' + uid + '-staged') : null;
|
|
1852
|
-
|
|
1853
|
-
let stagedFiles = [];
|
|
1854
|
-
|
|
1855
|
-
function appendMsg(msg) {
|
|
1856
|
-
const bub = document.createElement('div');
|
|
1857
|
-
const roleClass = msg.role === 'user' ? 'lc-chat-bubble-user'
|
|
1858
|
-
: msg.role === 'assistant' ? 'lc-chat-bubble-assistant'
|
|
1859
|
-
: 'lc-chat-bubble-system';
|
|
1860
|
-
bub.className = 'lc-chat-bubble ' + roleClass;
|
|
1861
|
-
if (msg.role === 'assistant') {
|
|
1862
|
-
bub.innerHTML = _renderMd(msg.text || '');
|
|
1863
|
-
} else {
|
|
1864
|
-
bub.textContent = msg.text || '';
|
|
1865
|
-
}
|
|
1866
|
-
if (msg.files && msg.files.length) {
|
|
1867
|
-
const fDiv = document.createElement('div');
|
|
1868
|
-
fDiv.className = 'small mt-1';
|
|
1869
|
-
msg.files.forEach(f => {
|
|
1870
|
-
const name = typeof f === 'string' ? f : f.name;
|
|
1871
|
-
fDiv.innerHTML += '\uD83D\uDCCE ' + _esc(name) + '<br>';
|
|
1872
|
-
});
|
|
1873
|
-
bub.appendChild(fDiv);
|
|
1874
|
-
}
|
|
1875
|
-
body.appendChild(bub);
|
|
1876
|
-
}
|
|
1877
|
-
|
|
1878
|
-
messages.forEach(appendMsg);
|
|
1879
|
-
body.scrollTop = body.scrollHeight;
|
|
1880
|
-
|
|
1881
|
-
function renderStaged() {
|
|
1882
|
-
if (!stagedEl) return;
|
|
1883
|
-
if (!stagedFiles.length) { stagedEl.innerHTML = ''; return; }
|
|
1884
|
-
stagedEl.innerHTML = stagedFiles.map((f, i) =>
|
|
1885
|
-
`<div class="d-flex align-items-center gap-1 small"><span>\uD83D\uDCCE ${_esc(f.name)}</span><button class="btn btn-sm btn-link text-danger p-0 lc-rm-cs" data-idx="${i}">×</button></div>`
|
|
1886
|
-
).join('');
|
|
1887
|
-
stagedEl.querySelectorAll('.lc-rm-cs').forEach(btn => {
|
|
1888
|
-
btn.addEventListener('click', () => { stagedFiles.splice(parseInt(btn.dataset.idx), 1); renderStaged(); }, { signal });
|
|
1889
|
-
});
|
|
1890
|
-
}
|
|
1891
|
-
|
|
1892
|
-
if (attachBtn && fileInput) {
|
|
1893
|
-
const acceptS = new Set(accept.map(x => x.toLowerCase()));
|
|
1894
|
-
attachBtn.addEventListener('click', () => fileInput.click(), { signal });
|
|
1895
|
-
fileInput.addEventListener('change', e => {
|
|
1896
|
-
for (const f of e.target.files) {
|
|
1897
|
-
const ext = '.' + f.name.split('.').pop().toLowerCase();
|
|
1898
|
-
if (acceptS.has(ext) && !stagedFiles.find(s => s.name === f.name)) stagedFiles.push(f);
|
|
1899
|
-
}
|
|
1900
|
-
e.target.value = '';
|
|
1901
|
-
renderStaged();
|
|
1902
|
-
}, { signal });
|
|
1903
|
-
}
|
|
1904
|
-
|
|
1905
|
-
function doSend() {
|
|
1906
|
-
const text = input.value.trim();
|
|
1907
|
-
if (!text && !stagedFiles.length) return;
|
|
1908
|
-
const msg = { role: 'user', text: text || '' };
|
|
1909
|
-
if (stagedFiles.length) msg.files = stagedFiles.map(f => ({ name: f.name, size: f.size }));
|
|
1910
|
-
appendMsg(msg);
|
|
1911
|
-
body.scrollTop = body.scrollHeight;
|
|
1912
|
-
input.value = '';
|
|
1913
|
-
const filesToSend = stagedFiles.slice();
|
|
1914
|
-
stagedFiles = [];
|
|
1915
|
-
renderStaged();
|
|
1916
|
-
cfg.onAction(node.id, 'chat-send', { text: msg.text, files: filesToSend, elemId: elemDef.id });
|
|
1917
|
-
}
|
|
1918
|
-
|
|
1919
|
-
sendBtn.addEventListener('click', doSend, { signal });
|
|
1920
|
-
input.addEventListener('keydown', e => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); doSend(); } }, { signal });
|
|
1921
|
-
|
|
1922
|
-
el._chat = {
|
|
1923
|
-
appendMessage: (role, text, files) => { appendMsg({ role, text, files }); body.scrollTop = body.scrollHeight; },
|
|
1924
|
-
showProcessing: (text) => {
|
|
1925
|
-
let ind = body.querySelector('.lc-chat-processing');
|
|
1926
|
-
if (!ind) {
|
|
1927
|
-
ind = document.createElement('div');
|
|
1928
|
-
ind.className = 'lc-chat-processing';
|
|
1929
|
-
ind.innerHTML = '<span class="spinner-border spinner-border-sm"></span><span class="small">Processing...</span>';
|
|
1930
|
-
body.appendChild(ind);
|
|
1931
|
-
}
|
|
1932
|
-
if (text) ind.querySelector('.small').textContent = text;
|
|
1933
|
-
body.scrollTop = body.scrollHeight;
|
|
1934
|
-
},
|
|
1935
|
-
removeProcessing: () => { const ind = body.querySelector('.lc-chat-processing'); if (ind) ind.remove(); },
|
|
1936
|
-
disable: () => { input.disabled = true; sendBtn.disabled = true; if (attachBtn) attachBtn.disabled = true; },
|
|
1937
|
-
enable: () => { input.disabled = false; sendBtn.disabled = false; if (attachBtn) attachBtn.disabled = false; },
|
|
1938
|
-
};
|
|
1939
|
-
}
|
|
1940
|
-
|
|
1941
|
-
// ---- actions ----
|
|
1942
|
-
|
|
1943
|
-
function _renderActions(data, el, elemDef, node) {
|
|
1944
|
-
const cleanup = _getCleanup(node.id);
|
|
1945
|
-
const signal = cleanup.ac.signal;
|
|
1946
|
-
const ed = elemDef.data || {};
|
|
1947
|
-
const buttons = ed.buttons || (Array.isArray(data) ? data : []);
|
|
1948
|
-
if (!buttons.length) { el.innerHTML = ''; return; }
|
|
1949
|
-
|
|
1950
|
-
let h = '<div class="d-flex gap-2 flex-wrap">';
|
|
1951
|
-
buttons.forEach(btn => {
|
|
1952
|
-
const style = btn.style || 'outline-secondary';
|
|
1953
|
-
const size = btn.size || 'sm';
|
|
1954
|
-
const dis = typeof btn.disabled === 'string' ? _resolveBind(node, btn.disabled) : btn.disabled;
|
|
1955
|
-
h += `<button class="btn btn-${_esc(style)} btn-${size}" data-action-id="${_esc(btn.id)}"${dis ? ' disabled' : ''}>`;
|
|
1956
|
-
h += _esc(btn.label || btn.id);
|
|
1957
|
-
h += '</button>';
|
|
1958
|
-
});
|
|
1959
|
-
h += '</div>';
|
|
1960
|
-
el.innerHTML = h;
|
|
1961
|
-
|
|
1962
|
-
el.querySelectorAll('[data-action-id]').forEach(btnEl => {
|
|
1963
|
-
btnEl.addEventListener('click', () => {
|
|
1964
|
-
cfg.onAction(node.id, 'action', { buttonId: btnEl.dataset.actionId, elemId: elemDef.id });
|
|
1965
|
-
}, { signal });
|
|
1966
|
-
});
|
|
1967
|
-
|
|
1968
|
-
el._actions = {
|
|
1969
|
-
setDisabled: (buttonId, disabled) => {
|
|
1970
|
-
const b = el.querySelector(`[data-action-id="${buttonId}"]`);
|
|
1971
|
-
if (b) b.disabled = disabled;
|
|
1972
|
-
},
|
|
1973
|
-
setLabel: (buttonId, label) => {
|
|
1974
|
-
const b = el.querySelector(`[data-action-id="${buttonId}"]`);
|
|
1975
|
-
if (b) b.textContent = label;
|
|
1976
|
-
},
|
|
1977
|
-
};
|
|
1978
|
-
}
|
|
1979
|
-
|
|
1980
|
-
// ---- ref ----
|
|
1981
|
-
// Indirection element: resolves a bind path to get the view definition,
|
|
1982
|
-
// then dispatches to the real renderer. The resolved value may be:
|
|
1983
|
-
// - a string → treated directly as the element kind ("table", "chart", etc.)
|
|
1984
|
-
// - an object → { kind, label, data: { columns, chartType, chartOptions, writeTo } }
|
|
1985
|
-
// merged with static elemDef (static fields win for protection)
|
|
1986
|
-
// - null/undefined → falls back to elemDef.data.fallbackKind or shape-inferred kind
|
|
1987
|
-
//
|
|
1988
|
-
// Allowed kinds from resolved value (whitelist, unknown → "table"):
|
|
1989
|
-
// table, editable-table, chart, metric, list, badge, text, narrative, markdown
|
|
1990
|
-
//
|
|
1991
|
-
// Usage:
|
|
1992
|
-
// { "kind": "ref",
|
|
1993
|
-
// "data": { "bind": "computed_values.proposed_trades",
|
|
1994
|
-
// "viewBind": "card_data.display_mode",
|
|
1995
|
-
// "fallbackKind": "table" } }
|
|
1996
|
-
//
|
|
1997
|
-
// viewBind can point to any namespace: card_data, requires, computed_values, runtime_state.
|
|
1998
|
-
// If the resolved view object contains a "bind" sub-path, that overrides data.bind.
|
|
1999
|
-
const _REF_KIND_WHITELIST = new Set([
|
|
2000
|
-
'table','editable-table','chart','metric','list','badge',
|
|
2001
|
-
'text','narrative','markdown','form','filter','todo','alert',
|
|
2002
|
-
]);
|
|
2003
|
-
function _renderRef(data, el, elemDef, node) {
|
|
2004
|
-
const ed = elemDef.data || {};
|
|
2005
|
-
|
|
2006
|
-
// Resolve the view hint
|
|
2007
|
-
const viewRaw = ed.viewBind ? _resolveBind(node, ed.viewBind) : undefined;
|
|
2008
|
-
|
|
2009
|
-
let resolvedKind, resolvedExtra;
|
|
2010
|
-
if (typeof viewRaw === 'string' && viewRaw) {
|
|
2011
|
-
resolvedKind = viewRaw;
|
|
2012
|
-
resolvedExtra = {};
|
|
2013
|
-
} else if (viewRaw && typeof viewRaw === 'object' && !Array.isArray(viewRaw)) {
|
|
2014
|
-
resolvedKind = typeof viewRaw.kind === 'string' ? viewRaw.kind : undefined;
|
|
2015
|
-
resolvedExtra = viewRaw.data && typeof viewRaw.data === 'object' ? viewRaw.data : {};
|
|
2016
|
-
}
|
|
2017
|
-
|
|
2018
|
-
// Validate kind against whitelist; fall back to shape inference
|
|
2019
|
-
if (!resolvedKind || !_REF_KIND_WHITELIST.has(resolvedKind)) {
|
|
2020
|
-
resolvedKind = ed.fallbackKind && _REF_KIND_WHITELIST.has(ed.fallbackKind)
|
|
2021
|
-
? ed.fallbackKind
|
|
2022
|
-
: (Array.isArray(data) ? 'table' : typeof data === 'string' ? 'text' : 'narrative');
|
|
2023
|
-
}
|
|
2024
|
-
|
|
2025
|
-
// Build effective elemDef: resolved hints first, static elemDef fields override (card author wins)
|
|
2026
|
-
const mergedData = Object.assign({}, resolvedExtra, ed);
|
|
2027
|
-
delete mergedData.viewBind;
|
|
2028
|
-
delete mergedData.fallbackKind;
|
|
2029
|
-
|
|
2030
|
-
// If the resolved hint provided its own bind path, honour it (but static ed.bind still wins)
|
|
2031
|
-
if (!mergedData.bind && resolvedExtra.bind) mergedData.bind = resolvedExtra.bind;
|
|
2032
|
-
|
|
2033
|
-
const effectiveElemDef = Object.assign({}, elemDef, { kind: resolvedKind }, { data: mergedData });
|
|
2034
|
-
|
|
2035
|
-
// Re-resolve data using effective bind (may have changed)
|
|
2036
|
-
const effectiveData = mergedData.bind ? _resolveBind(node, mergedData.bind) : data;
|
|
2037
|
-
|
|
2038
|
-
const renderer = _renderers[resolvedKind] || _renderers.table;
|
|
2039
|
-
renderer(effectiveData, el, effectiveElemDef, node);
|
|
2040
|
-
}
|
|
2041
|
-
|
|
2042
|
-
// ---- Register built-in renderers ----
|
|
2043
|
-
|
|
2044
|
-
_renderers.table = _renderTable;
|
|
2045
|
-
_renderers['editable-table'] = _renderEditableTable;
|
|
2046
|
-
_renderers.filter = _renderFilter;
|
|
2047
|
-
_renderers.metric = _renderMetric;
|
|
2048
|
-
_renderers.list = _renderList;
|
|
2049
|
-
_renderers.chart = _renderChart;
|
|
2050
|
-
_renderers.form = _renderForm;
|
|
2051
|
-
_renderers.notes = _renderNotes;
|
|
2052
|
-
_renderers.todo = _renderTodo;
|
|
2053
|
-
_renderers.alert = _renderAlert;
|
|
2054
|
-
_renderers.narrative = _renderNarrative;
|
|
2055
|
-
_renderers.badge = _renderBadge;
|
|
2056
|
-
_renderers.text = _renderText;
|
|
2057
|
-
_renderers.markdown = _renderMarkdown;
|
|
2058
|
-
_renderers.custom = _renderCustom;
|
|
2059
|
-
_renderers['file-upload'] = _renderFileUpload;
|
|
2060
|
-
_renderers['chat'] = _renderChatEl;
|
|
2061
|
-
_renderers.actions = _renderActions;
|
|
2062
|
-
_renderers.ref = _renderRef;
|
|
2063
|
-
|
|
2064
|
-
// ===========================================================================
|
|
2065
|
-
// _renderElements — render all view.elements for a card node
|
|
2066
|
-
// ===========================================================================
|
|
2067
|
-
|
|
2068
|
-
function _renderElements(node, containerEl) {
|
|
2069
|
-
const view = node && node.card ? node.card.view : null;
|
|
2070
|
-
if (!view || !Array.isArray(view.elements)) { containerEl.innerHTML = ''; return; }
|
|
2071
|
-
|
|
2072
|
-
if (_nodeEls[node.id]) _nodeEls[node.id].elements = {};
|
|
2073
|
-
|
|
2074
|
-
const container = document.createElement('div');
|
|
2075
|
-
container.className = 'row g-2';
|
|
2076
|
-
|
|
2077
|
-
const _taskStatus = node.runtime_state && node.runtime_state.task_status;
|
|
2078
|
-
if (_taskStatus && _taskStatus !== 'completed') {
|
|
2079
|
-
const statusEl = document.createElement('div');
|
|
2080
|
-
statusEl.className = 'col-12 d-flex align-items-center gap-2 mb-1';
|
|
2081
|
-
var _statusIconHtml;
|
|
2082
|
-
if (_taskStatus === 'running') {
|
|
2083
|
-
_statusIconHtml = '<span class="spinner-border spinner-border-sm text-muted" style="width:.75rem;height:.75rem;flex-shrink:0"></span>';
|
|
2084
|
-
} else if (_taskStatus === 'failed') {
|
|
2085
|
-
_statusIconHtml = '<span style="font-size:.75rem;line-height:1;flex-shrink:0;color:#dc3545">⚠︎</span>'; // ⚠ (text variant)
|
|
2086
|
-
} else if (_taskStatus === 'not-started') {
|
|
2087
|
-
_statusIconHtml = '<span style="font-size:.75rem;line-height:1;flex-shrink:0" class="text-muted">○</span>'; // ○
|
|
2088
|
-
} else if (_taskStatus === 'inactivated') {
|
|
2089
|
-
_statusIconHtml = '<span style="font-size:.75rem;line-height:1;flex-shrink:0" class="text-muted">⊖</span>'; // ⊖
|
|
2090
|
-
} else {
|
|
2091
|
-
_statusIconHtml = '<span style="font-size:.75rem;line-height:1;flex-shrink:0" class="text-muted">–</span>'; // –
|
|
2092
|
-
}
|
|
2093
|
-
statusEl.innerHTML = _statusIconHtml + '<span class="text-muted" style="font-size:.75rem">' + _esc(_taskStatus) + '</span>';
|
|
2094
|
-
container.appendChild(statusEl);
|
|
2095
|
-
}
|
|
2096
|
-
|
|
2097
|
-
view.elements.forEach(elemDef => {
|
|
2098
|
-
// Visibility gate
|
|
2099
|
-
if (elemDef.visible) {
|
|
2100
|
-
const vis = _resolveBind(node, elemDef.visible);
|
|
2101
|
-
if (!vis) return;
|
|
2102
|
-
}
|
|
2103
|
-
|
|
2104
|
-
const data = elemDef.data && elemDef.data.bind ? _resolveBind(node, elemDef.data.bind) : undefined;
|
|
2105
|
-
const col = document.createElement('div');
|
|
2106
|
-
col.className = elemDef.className || 'col-12';
|
|
2107
|
-
|
|
2108
|
-
// Element label (except metric which handles its own)
|
|
2109
|
-
if (elemDef.label && elemDef.kind !== 'metric' && elemDef.kind !== 'alert') {
|
|
2110
|
-
const label = document.createElement('div');
|
|
2111
|
-
label.className = 'small text-muted fw-medium mb-1';
|
|
2112
|
-
label.textContent = elemDef.label;
|
|
2113
|
-
col.appendChild(label);
|
|
2114
|
-
}
|
|
2115
|
-
|
|
2116
|
-
const inner = document.createElement('div');
|
|
2117
|
-
col.appendChild(inner);
|
|
2118
|
-
|
|
2119
|
-
const renderer = _renderers[elemDef.kind] || _renderers.custom;
|
|
2120
|
-
try {
|
|
2121
|
-
renderer(data, inner, elemDef, node);
|
|
2122
|
-
} catch (e) {
|
|
2123
|
-
console.error('LiveCard render error', node.id, elemDef.kind, e);
|
|
2124
|
-
inner.innerHTML = `<div class="text-danger small">Render error: ${_esc(e.message)}</div>`;
|
|
2125
|
-
}
|
|
2126
|
-
|
|
2127
|
-
if (elemDef.id && _nodeEls[node.id]) _nodeEls[node.id].elements[elemDef.id] = inner;
|
|
2128
|
-
|
|
2129
|
-
container.appendChild(col);
|
|
2130
|
-
});
|
|
2131
|
-
|
|
2132
|
-
containerEl.innerHTML = '';
|
|
2133
|
-
containerEl.appendChild(container);
|
|
2134
|
-
}
|
|
2135
|
-
|
|
2136
|
-
// ===========================================================================
|
|
2137
|
-
// Core render
|
|
2138
|
-
// ===========================================================================
|
|
2139
|
-
|
|
2140
|
-
function render(node, containerEl, opts) {
|
|
2141
|
-
opts = opts || {};
|
|
2142
|
-
destroy(node.id);
|
|
2143
|
-
|
|
2144
|
-
const cleanup = _getCleanup(node.id);
|
|
2145
|
-
const signal = cleanup.ac.signal;
|
|
2146
|
-
const uid = 'lc-' + (node.id || 'x');
|
|
2147
|
-
const features = (node.card && node.card.view && node.card.view.features) || {};
|
|
2148
|
-
|
|
2149
|
-
// Run compute async before populating elements
|
|
2150
|
-
// (compute is triggered in the else branch below after DOM is ready)
|
|
2151
|
-
|
|
2152
|
-
let h = `<div class="lc-card" id="${uid}">`;
|
|
2153
|
-
|
|
2154
|
-
// Header bar: status dot + time-ago + refresh button
|
|
2155
|
-
const showRefresh = features.refresh !== false && cfg.onRefresh;
|
|
2156
|
-
h += `<div class="d-flex align-items-center gap-1 mb-2">`;
|
|
2157
|
-
h += _statusDot(node.card_data && node.card_data.status);
|
|
2158
|
-
h += `<span class="text-muted small">${_timeAgo(node.card_data && node.card_data.lastRun)}</span>`;
|
|
2159
|
-
if (node.card_data && node.card_data.status === 'error' && node.card_data.error) {
|
|
2160
|
-
h += `<span class="badge bg-danger small" title="${_esc(node.card_data.error)}">Error</span>`;
|
|
2161
|
-
}
|
|
2162
|
-
h += '<div class="d-flex align-items-center gap-1 ms-auto">';
|
|
2163
|
-
const filesCount = (node && node.card_data && Array.isArray(node.card_data.files)) ? node.card_data.files.length : 0;
|
|
2164
|
-
// Files icon button (paperclip)
|
|
2165
|
-
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'}">`;
|
|
2166
|
-
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>';
|
|
2167
|
-
if (filesCount > 0) h += `<span class="ms-1 small" aria-label="${filesCount} files">${filesCount}</span>`;
|
|
2168
|
-
h += '</button>';
|
|
2169
|
-
// Chat icon button (speech bubble)
|
|
2170
|
-
h += `<button class="btn btn-sm btn-outline-secondary d-inline-flex align-items-center" id="${uid}-chat-open" title="Chat">`;
|
|
2171
|
-
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>';
|
|
2172
|
-
h += '</button>';
|
|
2173
|
-
// Refresh icon button
|
|
2174
|
-
if (showRefresh) {
|
|
2175
|
-
h += `<button class="btn btn-sm btn-outline-secondary d-inline-flex align-items-center" id="${uid}-refresh" title="Refresh">`;
|
|
2176
|
-
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>';
|
|
2177
|
-
h += '</button>';
|
|
2178
|
-
}
|
|
2179
|
-
h += '</div>';
|
|
2180
|
-
h += '</div>';
|
|
2181
|
-
|
|
2182
|
-
// Inference status bar: completion criteria + task-completed tick
|
|
2183
|
-
const inferenceData = node.card_data && node.card_data.llm_task_completion_inference;
|
|
2184
|
-
const isTaskCompleted = !!(inferenceData && inferenceData.isTaskCompleted);
|
|
2185
|
-
const whenIs = node.card && typeof node.card.when_is_task_completed === 'string' && node.card.when_is_task_completed.trim();
|
|
2186
|
-
if (whenIs || isTaskCompleted) {
|
|
2187
|
-
h += `<div class="d-flex align-items-start gap-2 mb-2 px-1 py-1 rounded lc-inference-bar" style="background:rgba(0,0,0,.03)">`;
|
|
2188
|
-
if (isTaskCompleted) {
|
|
2189
|
-
h += `<span class="lc-inference-icon" title="Task completed" style="color:#198754;font-size:.75rem;line-height:1.2;flex-shrink:0">●</span>`;
|
|
2190
|
-
} else {
|
|
2191
|
-
h += `<span class="lc-inference-icon" style="color:#aaa;font-size:.75rem;line-height:1.4;flex-shrink:0" title="Awaiting inference">○</span>`;
|
|
2192
|
-
}
|
|
2193
|
-
if (whenIs) {
|
|
2194
|
-
h += `<span class="text-muted" style="font-size:.72rem;line-height:1.4;font-style:italic"><span style="opacity:.55;font-style:normal">done when:</span> ${_esc(whenIs)}</span>`;
|
|
2195
|
-
}
|
|
2196
|
-
h += `</div>`;
|
|
2197
|
-
}
|
|
2198
|
-
|
|
2199
|
-
// Elements area
|
|
2200
|
-
h += `<div class="lc-result" id="${uid}-result"></div>`;
|
|
2201
|
-
|
|
2202
|
-
h += '</div>';
|
|
2203
|
-
containerEl.innerHTML = h;
|
|
2204
|
-
|
|
2205
|
-
// ---- Render elements ----
|
|
2206
|
-
const resultEl = document.getElementById(uid + '-result');
|
|
2207
|
-
_nodeEls[node.id] = { container: containerEl, resultEl, uid };
|
|
2208
|
-
|
|
2209
|
-
if (node.card_data && node.card_data.status === 'error' && node.card_data.error) {
|
|
2210
|
-
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>`;
|
|
2211
|
-
} else {
|
|
2212
|
-
_renderElements(node, resultEl);
|
|
2213
|
-
}
|
|
2214
|
-
|
|
2215
|
-
// ---- Wire refresh ----
|
|
2216
|
-
const refreshBtn = document.getElementById(uid + '-refresh');
|
|
2217
|
-
if (refreshBtn && cfg.onRefresh) {
|
|
2218
|
-
refreshBtn.addEventListener('click', e => {
|
|
2219
|
-
e.stopPropagation();
|
|
2220
|
-
refreshBtn.disabled = true;
|
|
2221
|
-
cfg.onRefresh(node.id);
|
|
2222
|
-
}, { signal });
|
|
2223
|
-
}
|
|
2224
|
-
|
|
2225
|
-
const chatBtn = document.getElementById(uid + '-chat-open');
|
|
2226
|
-
if (chatBtn) {
|
|
2227
|
-
chatBtn.addEventListener('click', (e) => {
|
|
2228
|
-
e.stopPropagation();
|
|
2229
|
-
openChatModal(node.id);
|
|
2230
|
-
}, { signal });
|
|
2231
|
-
}
|
|
2232
|
-
|
|
2233
|
-
const filesBtn = document.getElementById(uid + '-files-open');
|
|
2234
|
-
if (filesBtn) {
|
|
2235
|
-
filesBtn.addEventListener('click', (e) => {
|
|
2236
|
-
e.stopPropagation();
|
|
2237
|
-
openFilesModal(node.id);
|
|
2238
|
-
}, { signal });
|
|
2239
|
-
}
|
|
2240
|
-
|
|
2241
|
-
_autoSubscribe(node);
|
|
2242
|
-
}
|
|
2243
|
-
|
|
2244
|
-
// ===========================================================================
|
|
2245
|
-
// In-place update
|
|
2246
|
-
// ===========================================================================
|
|
2247
|
-
|
|
2248
|
-
function update(nodeId, patch) {
|
|
2249
|
-
const info = _nodeEls[nodeId];
|
|
2250
|
-
if (!info) return;
|
|
2251
|
-
|
|
2252
|
-
const refreshBtn = document.getElementById(info.uid + '-refresh');
|
|
2253
|
-
if (refreshBtn) refreshBtn.disabled = false;
|
|
2254
|
-
|
|
2255
|
-
// Update status dot
|
|
2256
|
-
if (patch.status) {
|
|
2257
|
-
const dot = info.container.querySelector('.lc-status-dot');
|
|
2258
|
-
if (dot) {
|
|
2259
|
-
const c = { fresh: 'var(--bs-success)', stale: 'var(--bs-warning)', error: 'var(--bs-danger)', loading: 'var(--bs-info)' };
|
|
2260
|
-
dot.style.background = c[patch.status] || 'var(--bs-secondary)';
|
|
2261
|
-
dot.title = patch.status;
|
|
2262
|
-
}
|
|
2263
|
-
}
|
|
2264
|
-
|
|
2265
|
-
if (patch.lastRun) {
|
|
2266
|
-
const ts = info.container.querySelector('.lc-status-dot + .text-muted');
|
|
2267
|
-
if (ts) ts.textContent = _timeAgo(patch.lastRun);
|
|
2268
|
-
}
|
|
2269
|
-
|
|
2270
|
-
// Merge into node card_data
|
|
2271
|
-
const node = cfg.resolve(nodeId);
|
|
2272
|
-
if (!node) return;
|
|
2273
|
-
if (!node.card_data) node.card_data = {};
|
|
2274
|
-
if (patch.status) node.card_data.status = patch.status;
|
|
2275
|
-
if (patch.lastRun) node.card_data.lastRun = patch.lastRun;
|
|
2276
|
-
if (patch.error !== undefined) node.card_data.error = patch.error;
|
|
2277
|
-
if (patch.files !== undefined) node.card_data.files = Array.isArray(patch.files) ? patch.files : [];
|
|
2278
|
-
|
|
2279
|
-
// Keep files count inline inside the files button in the header.
|
|
2280
|
-
const filesBtn = document.getElementById(info.uid + '-files-open');
|
|
2281
|
-
const fileCount = Array.isArray(node.card_data.files) ? node.card_data.files.length : 0;
|
|
2282
|
-
if (filesBtn) {
|
|
2283
|
-
filesBtn.title = fileCount > 0 ? ('Files (' + fileCount + ')') : 'Files';
|
|
2284
|
-
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>') : '');
|
|
2285
|
-
}
|
|
2286
|
-
|
|
2287
|
-
// Remove legacy external count label if present from older renders.
|
|
2288
|
-
const filesCountEl = document.getElementById(info.uid + '-files-count');
|
|
2289
|
-
if (filesCountEl && filesCountEl.parentNode) filesCountEl.parentNode.removeChild(filesCountEl);
|
|
2290
|
-
|
|
2291
|
-
// Update inference status bar (tick / hourglass) if card_data changed
|
|
2292
|
-
const infBar = info.container.querySelector('.lc-inference-bar');
|
|
2293
|
-
if (infBar) {
|
|
2294
|
-
const infData = node.card_data && node.card_data.llm_task_completion_inference;
|
|
2295
|
-
const done = !!(infData && infData.isTaskCompleted);
|
|
2296
|
-
const iconEl = infBar.querySelector('.lc-inference-icon');
|
|
2297
|
-
if (iconEl) {
|
|
2298
|
-
iconEl.title = done ? 'Task completed' : 'Awaiting inference';
|
|
2299
|
-
iconEl.style.color = done ? '#198754' : '#aaa';
|
|
2300
|
-
iconEl.innerHTML = done ? '●' : '○';
|
|
2301
|
-
}
|
|
2302
|
-
}
|
|
2303
|
-
|
|
2304
|
-
if (node.card_data.status === 'error' && node.card_data.error) {
|
|
2305
|
-
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>`;
|
|
2306
|
-
} else {
|
|
2307
|
-
_renderElements(node, info.resultEl);
|
|
2308
|
-
}
|
|
2309
|
-
}
|
|
2310
|
-
|
|
2311
|
-
// ===========================================================================
|
|
2312
|
-
// Lifecycle
|
|
2313
|
-
// ===========================================================================
|
|
2314
|
-
|
|
2315
|
-
function destroy(nodeId) {
|
|
2316
|
-
const c = _cleanup[nodeId];
|
|
2317
|
-
if (c) {
|
|
2318
|
-
c.ac.abort();
|
|
2319
|
-
c.timers.forEach(t => clearTimeout(t));
|
|
2320
|
-
c.charts.forEach(ch => { try { ch.inst.destroy(); } catch (_) {} });
|
|
2321
|
-
if (c.unsubs) c.unsubs.forEach(u => u());
|
|
2322
|
-
delete _cleanup[nodeId];
|
|
2323
|
-
}
|
|
2324
|
-
delete _nodeEls[nodeId];
|
|
2325
|
-
}
|
|
2326
|
-
|
|
2327
|
-
function destroyAll() {
|
|
2328
|
-
Object.keys(_cleanup).forEach(destroy);
|
|
2329
|
-
}
|
|
2330
|
-
|
|
2331
|
-
// ===========================================================================
|
|
2332
|
-
// Chat
|
|
2333
|
-
// ===========================================================================
|
|
2334
|
-
|
|
2335
|
-
function appendChatMessage(nodeId, role, text) {
|
|
2336
|
-
if (_chatModal.currentNodeId !== nodeId) return;
|
|
2337
|
-
_appendModalChatMessage(role, text, []);
|
|
2338
|
-
}
|
|
2339
|
-
|
|
2340
|
-
function refreshOpenChatModal() {
|
|
2341
|
-
const nodeId = _chatModal.currentNodeId;
|
|
2342
|
-
if (!nodeId || !_chatModal.backdrop || !_chatModal.backdrop.classList.contains('lc-open')) return;
|
|
2343
|
-
_refreshModalChatHistory(nodeId).catch(function () {});
|
|
2344
|
-
}
|
|
2345
|
-
|
|
2346
|
-
function onServerSseEvent() {
|
|
2347
|
-
const nodeId = _chatModal.currentNodeId;
|
|
2348
|
-
if (!nodeId || !_chatModal.backdrop || !_chatModal.backdrop.classList.contains('lc-open')) return;
|
|
2349
|
-
_clearPendingModalChatMessages();
|
|
2350
|
-
_syncProcessingBar(nodeId);
|
|
2351
|
-
_refreshModalChatHistory(nodeId).catch(function () {});
|
|
2352
|
-
}
|
|
2353
|
-
|
|
2354
|
-
// ===========================================================================
|
|
2355
|
-
// Element access
|
|
2356
|
-
// ===========================================================================
|
|
2357
|
-
|
|
2358
|
-
function getElement(nodeId, elemId) {
|
|
2359
|
-
const info = _nodeEls[nodeId];
|
|
2360
|
-
return (info && info.elements && info.elements[elemId]) || null;
|
|
2361
|
-
}
|
|
2362
|
-
|
|
2363
|
-
// ===========================================================================
|
|
2364
|
-
// Return engine
|
|
2365
|
-
// ===========================================================================
|
|
2366
|
-
|
|
2367
|
-
return {
|
|
2368
|
-
render,
|
|
2369
|
-
update,
|
|
2370
|
-
destroy,
|
|
2371
|
-
destroyAll,
|
|
2372
|
-
notify,
|
|
2373
|
-
subscribe,
|
|
2374
|
-
appendChatMessage,
|
|
2375
|
-
refreshOpenChatModal,
|
|
2376
|
-
onServerSseEvent,
|
|
2377
|
-
openChatModal,
|
|
2378
|
-
openFilesModal,
|
|
2379
|
-
getElement,
|
|
2380
|
-
registerRenderer(name, fn) { _renderers[name] = fn; },
|
|
2381
|
-
renderers: _renderers,
|
|
2382
|
-
};
|
|
2383
|
-
}
|
|
2384
|
-
|
|
2385
|
-
// ===========================================================================
|
|
2386
|
-
// BoardCore — imperative grid (board) and DAG (canvas) modes.
|
|
2387
|
-
// Most callers should use Board (reactive wrapper) instead.
|
|
2388
|
-
// ===========================================================================
|
|
2389
|
-
|
|
2390
|
-
function BoardCore(engine, containerEl, opts) {
|
|
2391
|
-
opts = opts || {};
|
|
2392
|
-
const mode = { current: opts.mode || 'board' };
|
|
2393
|
-
const devMode = { current: opts.devMode || false };
|
|
2394
|
-
const nodeList = [];
|
|
2395
|
-
const nodeMap = {}; // id → { node, colEl, bodyEl }
|
|
2396
|
-
const _positions = {}; // id → { x, y, w, h } for canvas mode
|
|
2397
|
-
const showChat = opts.showChat || false;
|
|
2398
|
-
const defaultCol = opts.defaultCol || 6;
|
|
2399
|
-
|
|
2400
|
-
// Canvas config
|
|
2401
|
-
const co = opts.canvas || {};
|
|
2402
|
-
const cvs = {
|
|
2403
|
-
snap: co.snap || 20,
|
|
2404
|
-
zoomMin: (co.zoom && co.zoom.min) || 0.25,
|
|
2405
|
-
zoomMax: (co.zoom && co.zoom.max) || 2,
|
|
2406
|
-
zoom: (co.zoom && co.zoom.initial) || 1,
|
|
2407
|
-
edges: co.edges !== false,
|
|
2408
|
-
minWidth: co.minWidth || 220,
|
|
2409
|
-
maxWidth: co.maxWidth || 450,
|
|
2410
|
-
defaultW: co.defaultW || 350,
|
|
2411
|
-
gapX: co.gapX || 280,
|
|
2412
|
-
gapY: co.gapY || 320,
|
|
2413
|
-
padX: co.padX || 20,
|
|
2414
|
-
padY: co.padY || 20,
|
|
2415
|
-
cardMaxH: co.cardMaxH || 300,
|
|
2416
|
-
panX: 0, panY: 0,
|
|
2417
|
-
};
|
|
2418
|
-
const ac = new AbortController();
|
|
2419
|
-
const signal = ac.signal;
|
|
2420
|
-
const _edges = []; // LeaderLine instances for canvas edges
|
|
2421
|
-
|
|
2422
|
-
// Edge style config (from canvas opts)
|
|
2423
|
-
const edgeOpts = co.edgeStyle || {};
|
|
2424
|
-
const edgeCfg = {
|
|
2425
|
-
color: edgeOpts.color || 'rgba(108, 117, 125, 0.6)',
|
|
2426
|
-
size: edgeOpts.size || 2,
|
|
2427
|
-
dash: edgeOpts.dash !== false,
|
|
2428
|
-
animation: edgeOpts.animation !== false,
|
|
2429
|
-
endPlug: edgeOpts.endPlug || 'arrow1',
|
|
2430
|
-
};
|
|
2431
|
-
|
|
2432
|
-
// DOM containers
|
|
2433
|
-
const root = document.createElement('div');
|
|
2434
|
-
root.className = 'lc-board';
|
|
2435
|
-
containerEl.appendChild(root);
|
|
2436
|
-
|
|
2437
|
-
const gridEl = document.createElement('div');
|
|
2438
|
-
gridEl.className = 'row g-3 lc-board-grid';
|
|
2439
|
-
|
|
2440
|
-
const canvasEl = document.createElement('div');
|
|
2441
|
-
canvasEl.className = 'lc-canvas';
|
|
2442
|
-
canvasEl.style.cssText = 'position:relative;overflow:auto;width:100%;';
|
|
2443
|
-
const canvasInner = document.createElement('div');
|
|
2444
|
-
canvasInner.className = 'lc-canvas-inner';
|
|
2445
|
-
canvasInner.style.cssText = 'position:relative;transform-origin:0 0;min-width:100%;min-height:100%;';
|
|
2446
|
-
canvasEl.appendChild(canvasInner);
|
|
2447
|
-
|
|
2448
|
-
// SVG overlay for edges
|
|
2449
|
-
const svgEl = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
|
2450
|
-
svgEl.setAttribute('class', 'lc-canvas-edges');
|
|
2451
|
-
svgEl.style.cssText = 'position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none;overflow:hidden;z-index:0;';
|
|
2452
|
-
const defs = document.createElementNS('http://www.w3.org/2000/svg', 'defs');
|
|
2453
|
-
defs.innerHTML = '<marker id="lc-arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M 0 1 L 8 5 L 0 9 z" fill="rgba(108,117,125,0.55)"/></marker>';
|
|
2454
|
-
svgEl.appendChild(defs);
|
|
2455
|
-
canvasInner.appendChild(svgEl);
|
|
2456
|
-
|
|
2457
|
-
// Board/canvas CSS
|
|
2458
|
-
if (!document.getElementById('lc-board-css')) {
|
|
2459
|
-
const s = document.createElement('style');
|
|
2460
|
-
s.id = 'lc-board-css';
|
|
2461
|
-
s.textContent = `
|
|
2462
|
-
.lc-canvas-card { position:absolute; min-width:${cvs.minWidth}px; cursor:grab; user-select:none; z-index:1; }
|
|
92
|
+
<span class="badge bg-${s} fs-6">${L(l)}</span>
|
|
93
|
+
</div>`;}function ze(e,t){let a=typeof e=="string"?e:e&&e.text?e.text:"";if(!a){t.innerHTML='<p class="text-muted small fst-italic">No narrative yet. Click refresh to generate.</p>';return}t.innerHTML=`<div class="small">${Z(a)}</div>`;}function he(e,t,a){let p=(a.data||{}).colorMap||{},n=e!=null?String(e):"",s={green:"success",amber:"warning",red:"danger",blue:"primary"}[p[n]]||p[n]||"secondary";t.innerHTML=`<span class="badge bg-${L(s)}">${L(n)}</span>`;}function Re(e,t,a){let r=a.data||{},p=r.format||"default",n=a.style||r.style||"default";if((r.hideIfEmpty||a.hideIfEmpty)&&(e==null||e==="")){t.innerHTML="";return}if(p==="file-links"){if(!Array.isArray(e)||e.length===0){t.innerHTML='<div class="text-muted small">No files uploaded</div>';return}let u=[];e.forEach((m,C)=>{if(!m||!m.stored_name)return;let w=m.name||m.stored_name,S=a.data&&a.data.cardId?a.data.cardId:"unknown",M=`${k.fileUrlBase}/cards/${encodeURIComponent(S)}/files/${C}?sn=${encodeURIComponent(m.stored_name)}`,$=m.size?` (${Math.round(m.size/1024)}KB)`:"";u.push(`<div class="mb-2"><a href="${M}" class="btn btn-sm btn-outline-secondary">${L(w)}${L($)}</a></div>`);});let v=u.join("");t.innerHTML=v;return}let s=n==="heading"?"h4":"div",d=n==="muted"?"text-muted small":n==="muted-italic"?"text-muted small fst-italic":n==="heading"?"fw-bold":"small";t.innerHTML=`<${s} class="${d}">${L(e!=null?String(e):"")}</${s}>`;}function Oe(e,t){let a="";typeof e=="string"?a=e:e&&typeof e=="object"&&e.text?a=e.text:e!=null&&(a=JSON.stringify(e,null,2)),t.innerHTML=a?Z(a):"";}function Fe(e,t){if(e==null){t.innerHTML="";return}t.innerHTML=`<pre class="small mb-0">${L(JSON.stringify(e,null,2))}</pre>`;}function Pe(e,t,a,r){let n=X(r.id).ac.signal,l=a.data||{},s=Array.isArray(e)?e:[],d=l.showUploadedList===true,u=l.upload!==false,v=l.accept||[".txt",".csv",".md",".json",".html",".xml",".pdf",".xlsx",".docx",".pptx",".png",".jpg",".jpeg"],m=new Set(v.map(f=>f.toLowerCase())),C=l.multiple!==false,w=l.placeholder||"Drop files here or click to browse",S="lc-fu-"+(a.id||Math.random().toString(36).slice(2,8)),M=t._stagedFiles||[];t._stagedFiles=M;let $=t._uploadStatus||{};t._uploadStatus=$;function j(f){return `${f.name}::${f.size}::${f.lastModified||0}`}let _="";if(u&&(_+=`<div class="lc-dropzone mb-2" id="${S}-dz">`,_+='<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" class="text-muted mb-1"><path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4"/><polyline points="17 8 12 3 7 8"/><line x1="12" y1="3" x2="12" y2="15"/></svg>',_+=`<div class="small text-muted">${L(w)}</div>`,_+=`<input type="file" id="${S}-fi" class="d-none"${C?" multiple":""} accept="${v.join(",")}">`,_+="</div>",_+=`<div id="${S}-staged"></div>`),d&&s.length&&(_+='<div class="lc-uploaded-files">',s.forEach(f=>{let z=typeof f=="string"?f:f.name||"",q=typeof f=="string"?null:f.url;_+='<div class="d-flex align-items-center gap-1 small mb-1">',_+='<svg width="12" height="12" 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>',q?_+=`<a href="${L(q)}" class="text-truncate" target="_blank" download>${L(z)}</a>`:_+=`<span class="text-truncate">${L(z)}</span>`,_+="</div>";}),_+="</div>"),!u&&!s.length&&(_=`<p class="text-muted small">${L(l.placeholder||"No files")}</p>`),t.innerHTML=_,!u){t._fileUpload={getFiles:()=>[],clear:()=>{}};return}let H=t.querySelector("#"+S+"-dz"),ne=t.querySelector("#"+S+"-fi"),Q=t.querySelector("#"+S+"-staged");if(!H)return;function se(f){let z=[];for(let q of f){let W="."+q.name.split(".").pop().toLowerCase();m.has(W)&&(M.find(O=>O.name===q.name)||(M.push(q),z.push(q),$[j(q)]="uploading"));}b(),z.length&&typeof k.onAction=="function"&&Promise.resolve(k.onAction(r.id,"file-upload",{files:z,elemId:a.id})).then(()=>{let q=new Set(z.map(j));M=M.filter(W=>!q.has(j(W))),t._stagedFiles=M,z.forEach(W=>{delete $[j(W)];}),t._uploadStatus=$,b();}).catch(()=>{z.forEach(q=>{$[j(q)]="error";}),t._uploadStatus=$,b();});}function b(){if(!M.length){Q.innerHTML="";return}let f="";M.forEach((z,q)=>{let W=$[j(z)]||"ready";f+='<div class="lc-staged-file">',f+='<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>',f+=`<span class="small flex-grow-1 text-truncate">${L(z.name)}</span>`,W==="uploading"?f+='<span class="spinner-border spinner-border-sm text-secondary me-1" role="status" aria-label="Uploading"></span>':W==="error"&&(f+='<span class="badge bg-danger-subtle text-danger border border-danger-subtle me-1">Failed</span>'),f+=`<button class="btn btn-sm btn-link text-danger p-0 lc-rm-staged" data-idx="${q}">×</button>`,f+="</div>";}),Q.innerHTML=f,Q.querySelectorAll(".lc-rm-staged").forEach(z=>{z.addEventListener("click",()=>{let q=parseInt(z.dataset.idx),W=M[q];W&&delete $[j(W)],M.splice(q,1),t._stagedFiles=M,t._uploadStatus=$,b();},{signal:n});});}H.addEventListener("click",()=>ne.click(),{signal:n}),H.addEventListener("dragover",f=>{f.preventDefault(),H.classList.add("lc-drag-over");},{signal:n}),H.addEventListener("dragleave",()=>H.classList.remove("lc-drag-over"),{signal:n}),H.addEventListener("drop",f=>{f.preventDefault(),H.classList.remove("lc-drag-over"),se(f.dataTransfer.files);},{signal:n}),ne.addEventListener("change",f=>{se(f.target.files),f.target.value="";},{signal:n}),b(),t._fileUpload={getFiles:()=>M,clear:()=>{M=[],$={},t._stagedFiles=[],t._uploadStatus={},b();},disable:()=>{H.classList.add("lc-disabled"),ne.disabled=true;},enable:()=>{H.classList.remove("lc-disabled"),ne.disabled=false;}};}function De(e,t,a,r){let n=X(r.id).ac.signal,l=a.data||{},s=Array.isArray(e)?e:[],d=l.placeholder||"Type a message...",u=l.fileAttach===true,v=l.fileAccept||[".txt",".csv",".md",".json",".html",".xml",".pdf",".xlsx",".docx",".pptx",".png",".jpg",".jpeg"],m="lc-ch-"+(a.id||Math.random().toString(36).slice(2,8)),C='<div class="lc-chat-el">';C+=`<div class="lc-chat-body" id="${m}-body"></div>`,C+='<div class="lc-chat-input-bar">',u&&(C+=`<input type="file" id="${m}-fi" class="d-none" multiple accept="${v.join(",")}">`,C+=`<button class="btn btn-sm btn-outline-secondary" id="${m}-attach" title="Attach files" type="button">`,C+='<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><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>',C+="</button>"),C+=`<input type="text" class="form-control form-control-sm flex-grow-1" id="${m}-input" placeholder="${L(d)}">`,C+=`<button class="btn btn-sm btn-outline-primary" id="${m}-send" type="button">`,C+='<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>',C+="</button></div>",u&&(C+=`<div id="${m}-staged" class="mt-1"></div>`),C+="</div>",t.innerHTML=C;let w=t.querySelector("#"+m+"-body"),S=t.querySelector("#"+m+"-input"),M=t.querySelector("#"+m+"-send"),$=u?t.querySelector("#"+m+"-attach"):null,j=u?t.querySelector("#"+m+"-fi"):null,_=u?t.querySelector("#"+m+"-staged"):null,H=[];function ne(b){let f=document.createElement("div"),z=b.role==="user"?"lc-chat-bubble-user":b.role==="assistant"?"lc-chat-bubble-assistant":"lc-chat-bubble-system";if(f.className="lc-chat-bubble "+z,b.role==="assistant"?f.innerHTML=Z(b.text||""):f.textContent=b.text||"",b.files&&b.files.length){let q=document.createElement("div");q.className="small mt-1",b.files.forEach(W=>{let O=typeof W=="string"?W:W.name;q.innerHTML+="\u{1F4CE} "+L(O)+"<br>";}),f.appendChild(q);}w.appendChild(f);}s.forEach(ne),w.scrollTop=w.scrollHeight;function Q(){if(_){if(!H.length){_.innerHTML="";return}_.innerHTML=H.map((b,f)=>`<div class="d-flex align-items-center gap-1 small"><span>\u{1F4CE} ${L(b.name)}</span><button class="btn btn-sm btn-link text-danger p-0 lc-rm-cs" data-idx="${f}">×</button></div>`).join(""),_.querySelectorAll(".lc-rm-cs").forEach(b=>{b.addEventListener("click",()=>{H.splice(parseInt(b.dataset.idx),1),Q();},{signal:n});});}}if($&&j){let b=new Set(v.map(f=>f.toLowerCase()));$.addEventListener("click",()=>j.click(),{signal:n}),j.addEventListener("change",f=>{for(let z of f.target.files){let q="."+z.name.split(".").pop().toLowerCase();b.has(q)&&!H.find(W=>W.name===z.name)&&H.push(z);}f.target.value="",Q();},{signal:n});}function se(){let b=S.value.trim();if(!b&&!H.length)return;let f={role:"user",text:b||""};H.length&&(f.files=H.map(q=>({name:q.name,size:q.size}))),ne(f),w.scrollTop=w.scrollHeight,S.value="";let z=H.slice();H=[],Q(),k.onAction(r.id,"chat-send",{text:f.text,files:z,elemId:a.id});}M.addEventListener("click",se,{signal:n}),S.addEventListener("keydown",b=>{b.key==="Enter"&&!b.shiftKey&&(b.preventDefault(),se());},{signal:n}),t._chat={appendMessage:(b,f,z)=>{ne({role:b,text:f,files:z}),w.scrollTop=w.scrollHeight;},showProcessing:b=>{let f=w.querySelector(".lc-chat-processing");f||(f=document.createElement("div"),f.className="lc-chat-processing",f.innerHTML='<span class="spinner-border spinner-border-sm"></span><span class="small">Processing...</span>',w.appendChild(f)),b&&(f.querySelector(".small").textContent=b),w.scrollTop=w.scrollHeight;},removeProcessing:()=>{let b=w.querySelector(".lc-chat-processing");b&&b.remove();},disable:()=>{S.disabled=true,M.disabled=true,$&&($.disabled=true);},enable:()=>{S.disabled=false,M.disabled=false,$&&($.disabled=false);}};}function Ve(e,t,a,r){let n=X(r.id).ac.signal,s=(a.data||{}).buttons||(Array.isArray(e)?e:[]);if(!s.length){t.innerHTML="";return}let d='<div class="d-flex gap-2 flex-wrap">';s.forEach(u=>{let v=u.style||"outline-secondary",m=u.size||"sm",C=typeof u.disabled=="string"?ce(r,u.disabled):u.disabled;d+=`<button class="btn btn-${L(v)} btn-${m}" data-action-id="${L(u.id)}"${C?" disabled":""}>`,d+=L(u.label||u.id),d+="</button>";}),d+="</div>",t.innerHTML=d,t.querySelectorAll("[data-action-id]").forEach(u=>{u.addEventListener("click",()=>{k.onAction(r.id,"action",{buttonId:u.dataset.actionId,elemId:a.id});},{signal:n});}),t._actions={setDisabled:(u,v)=>{let m=t.querySelector(`[data-action-id="${u}"]`);m&&(m.disabled=v);},setLabel:(u,v)=>{let m=t.querySelector(`[data-action-id="${u}"]`);m&&(m.textContent=v);}};}let je=new Set(["table","editable-table","chart","metric","list","badge","text","narrative","markdown","form","filter","todo","alert"]);function We(e,t,a,r){let p=a.data||{},n=p.viewBind?ce(r,p.viewBind):void 0,l,s;typeof n=="string"&&n?(l=n,s={}):n&&typeof n=="object"&&!Array.isArray(n)&&(l=typeof n.kind=="string"?n.kind:void 0,s=n.data&&typeof n.data=="object"?n.data:{}),(!l||!je.has(l))&&(l=p.fallbackKind&&je.has(p.fallbackKind)?p.fallbackKind:Array.isArray(e)?"table":typeof e=="string"?"text":"narrative");let d=Object.assign({},s,p);delete d.viewBind,delete d.fallbackKind,!d.bind&&s.bind&&(d.bind=s.bind);let u=Object.assign({},a,{kind:l},{data:d}),v=d.bind?ce(r,d.bind):e;(P[l]||P.table)(v,t,u,r);}P.table=Ee,P["editable-table"]=ye,P.filter=Ce,P.metric=He,P.list=Le,P.chart=_e,P.form=ve,P.notes=ue,P.todo=$e,P.alert=Ie,P.narrative=ze,P.badge=he,P.text=Re,P.markdown=Oe,P.custom=Fe,P["file-upload"]=Pe,P.chat=De,P.actions=Ve,P.ref=We;function Se(e,t){let a=e&&e.card?e.card.view:null;if(!a||!Array.isArray(a.elements)){t.innerHTML="";return}F[e.id]&&(F[e.id].elements={});let r=document.createElement("div");r.className="row g-2";let p=e.runtime_state&&e.runtime_state.task_status;if(p&&p!=="completed"){let l=document.createElement("div");l.className="col-12 d-flex align-items-center gap-2 mb-1";var n;p==="running"?n='<span class="spinner-border spinner-border-sm text-muted" style="width:.75rem;height:.75rem;flex-shrink:0"></span>':p==="failed"?n='<span style="font-size:.75rem;line-height:1;flex-shrink:0;color:#dc3545">⚠︎</span>':p==="not-started"?n='<span style="font-size:.75rem;line-height:1;flex-shrink:0" class="text-muted">○</span>':p==="inactivated"?n='<span style="font-size:.75rem;line-height:1;flex-shrink:0" class="text-muted">⊖</span>':n='<span style="font-size:.75rem;line-height:1;flex-shrink:0" class="text-muted">–</span>',l.innerHTML=n+'<span class="text-muted" style="font-size:.75rem">'+L(p)+"</span>",r.appendChild(l);}a.elements.forEach(l=>{if(l.visible&&!ce(e,l.visible))return;let s=l.data&&l.data.bind?ce(e,l.data.bind):void 0,d=document.createElement("div");if(d.className=l.className||"col-12",l.label&&l.kind!=="metric"&&l.kind!=="alert"){let m=document.createElement("div");m.className="small text-muted fw-medium mb-1",m.textContent=l.label,d.appendChild(m);}let u=document.createElement("div");d.appendChild(u);let v=P[l.kind]||P.custom;try{v(s,u,l,e);}catch(m){console.error("LiveCard render error",e.id,l.kind,m),u.innerHTML=`<div class="text-danger small">Render error: ${L(m.message)}</div>`;}l.id&&F[e.id]&&(F[e.id].elements[l.id]=u),r.appendChild(d);}),t.innerHTML="",t.appendChild(r);}function i(e,t,a){h(e.id);let p=X(e.id).ac.signal,n="lc-"+(e.id||"x"),l=e.card&&e.card.view&&e.card.view.features||{},s=`<div class="lc-card" id="${n}">`,d=l.refresh!==false&&k.onRefresh;s+='<div class="d-flex align-items-center gap-1 mb-2">',s+=Ke(e.card_data&&e.card_data.status),s+=`<span class="text-muted small">${Je(e.card_data&&e.card_data.lastRun)}</span>`,e.card_data&&e.card_data.status==="error"&&e.card_data.error&&(s+=`<span class="badge bg-danger small" title="${L(e.card_data.error)}">Error</span>`),s+='<div class="d-flex align-items-center gap-1 ms-auto">';let u=e&&e.card_data&&Array.isArray(e.card_data.files)?e.card_data.files.length:0;s+=`<button class="btn btn-sm btn-outline-secondary d-inline-flex align-items-center" id="${n}-files-open" title="${u>0?"Files ("+u+")":"Files"}">`,s+='<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>',u>0&&(s+=`<span class="ms-1 small" aria-label="${u} files">${u}</span>`),s+="</button>",s+=`<button class="btn btn-sm btn-outline-secondary d-inline-flex align-items-center" id="${n}-chat-open" title="Chat">`,s+='<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>',s+="</button>",d&&(s+=`<button class="btn btn-sm btn-outline-secondary d-inline-flex align-items-center" id="${n}-refresh" title="Refresh">`,s+='<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>',s+="</button>"),s+="</div>",s+="</div>";let v=e.card_data&&e.card_data.llm_task_completion_inference,m=!!(v&&v.isTaskCompleted),C=e.card&&typeof e.card.when_is_task_completed=="string"&&e.card.when_is_task_completed.trim();(C||m)&&(s+='<div class="d-flex align-items-start gap-2 mb-2 px-1 py-1 rounded lc-inference-bar" style="background:rgba(0,0,0,.03)">',m?s+='<span class="lc-inference-icon" title="Task completed" style="color:#198754;font-size:.75rem;line-height:1.2;flex-shrink:0">●</span>':s+='<span class="lc-inference-icon" style="color:#aaa;font-size:.75rem;line-height:1.4;flex-shrink:0" title="Awaiting inference">○</span>',C&&(s+=`<span class="text-muted" style="font-size:.72rem;line-height:1.4;font-style:italic"><span style="opacity:.55;font-style:normal">done when:</span> ${L(C)}</span>`),s+="</div>"),s+=`<div class="lc-result" id="${n}-result"></div>`,s+="</div>",t.innerHTML=s;let w=document.getElementById(n+"-result");F[e.id]={container:t,resultEl:w,uid:n},e.card_data&&e.card_data.status==="error"&&e.card_data.error?w.innerHTML=`<div class="text-danger small fw-semibold">Refresh failed</div><pre class="text-muted small mt-1" style="white-space:pre-wrap">${L(e.card_data.error)}</pre>`:Se(e,w);let S=document.getElementById(n+"-refresh");S&&k.onRefresh&&S.addEventListener("click",j=>{j.stopPropagation(),S.disabled=true,k.onRefresh(e.id);},{signal:p});let M=document.getElementById(n+"-chat-open");M&&M.addEventListener("click",j=>{j.stopPropagation(),pe(e.id);},{signal:p});let $=document.getElementById(n+"-files-open");$&&$.addEventListener("click",j=>{j.stopPropagation(),xe(e.id);},{signal:p}),we(e);}function o(e,t){let a=F[e];if(!a)return;let r=document.getElementById(a.uid+"-refresh");if(r&&(r.disabled=false),t.status){let u=a.container.querySelector(".lc-status-dot");if(u){let v={fresh:"var(--bs-success)",stale:"var(--bs-warning)",error:"var(--bs-danger)",loading:"var(--bs-info)"};u.style.background=v[t.status]||"var(--bs-secondary)",u.title=t.status;}}if(t.lastRun){let u=a.container.querySelector(".lc-status-dot + .text-muted");u&&(u.textContent=Je(t.lastRun));}let p=k.resolve(e);if(!p)return;p.card_data||(p.card_data={}),t.status&&(p.card_data.status=t.status),t.lastRun&&(p.card_data.lastRun=t.lastRun),t.error!==void 0&&(p.card_data.error=t.error),t.files!==void 0&&(p.card_data.files=Array.isArray(t.files)?t.files:[]);let n=document.getElementById(a.uid+"-files-open"),l=Array.isArray(p.card_data.files)?p.card_data.files.length:0;n&&(n.title=l>0?"Files ("+l+")":"Files",n.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>'+(l>0?'<span class="ms-1 small" aria-label="'+l+' files">'+l+"</span>":""));let s=document.getElementById(a.uid+"-files-count");s&&s.parentNode&&s.parentNode.removeChild(s);let d=a.container.querySelector(".lc-inference-bar");if(d){let u=p.card_data&&p.card_data.llm_task_completion_inference,v=!!(u&&u.isTaskCompleted),m=d.querySelector(".lc-inference-icon");m&&(m.title=v?"Task completed":"Awaiting inference",m.style.color=v?"#198754":"#aaa",m.innerHTML=v?"●":"○");}p.card_data.status==="error"&&p.card_data.error?a.resultEl.innerHTML=`<div class="text-danger small fw-semibold">Refresh failed</div><pre class="text-muted small mt-1" style="white-space:pre-wrap">${L(p.card_data.error)}</pre>`:Se(p,a.resultEl);}function h(e){let t=I[e];t&&(t.ac.abort(),t.timers.forEach(a=>clearTimeout(a)),t.charts.forEach(a=>{try{a.inst.destroy();}catch{}}),t.unsubs&&t.unsubs.forEach(a=>a()),delete I[e]),delete F[e];}function y(){Object.keys(I).forEach(h);}function N(e,t,a){c.currentNodeId===e&&ae(t,a,[]);}function E(){let e=c.currentNodeId;!e||!c.backdrop||!c.backdrop.classList.contains("lc-open")||G(e).catch(function(){});}function g(){let e=c.currentNodeId;!e||!c.backdrop||!c.backdrop.classList.contains("lc-open")||(U(),le(e),G(e).catch(function(){}));}function A(e,t){let a=F[e];return a&&a.elements&&a.elements[t]||null}return {render:i,update:o,destroy:h,destroyAll:y,notify:me,subscribe:Ae,appendChatMessage:N,refreshOpenChatModal:E,onServerSseEvent:g,openChatModal:pe,openFilesModal:xe,getElement:A,registerRenderer(e,t){P[e]=t;},renderers:P}}function Ge(T,k,I){I=I||{};let Y={current:I.mode||"board"},J={current:I.devMode||false},R=[],D={},V={},ie=I.showChat||false,P=I.defaultCol||6,F=I.canvas||{},c={snap:F.snap||20,zoomMin:F.zoom&&F.zoom.min||.25,zoomMax:F.zoom&&F.zoom.max||2,zoom:F.zoom&&F.zoom.initial||1,edges:F.edges!==false,minWidth:F.minWidth||220,maxWidth:F.maxWidth||450,defaultW:F.defaultW||350,gapX:F.gapX||280,gapY:F.gapY||320,padX:F.padX||20,padY:F.padY||20,cardMaxH:F.cardMaxH||300,panX:0,panY:0},x=new AbortController,Z=x.signal,X=[],de=F.edgeStyle||{};({color:de.color||"rgba(108, 117, 125, 0.6)",size:de.size||2,dash:de.dash!==false,animation:de.animation!==false,endPlug:de.endPlug||"arrow1"});let ae=document.createElement("div");ae.className="lc-board",k.appendChild(ae);let B=document.createElement("div");B.className="row g-3 lc-board-grid";let U=document.createElement("div");U.className="lc-canvas",U.style.cssText="position:relative;overflow:auto;width:100%;";let G=document.createElement("div");G.className="lc-canvas-inner",G.style.cssText="position:relative;transform-origin:0 0;min-width:100%;min-height:100%;",U.appendChild(G);let le=document.createElementNS("http://www.w3.org/2000/svg","svg");le.setAttribute("class","lc-canvas-edges"),le.style.cssText="position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none;overflow:hidden;z-index:0;";let pe=document.createElementNS("http://www.w3.org/2000/svg","defs");if(pe.innerHTML='<marker id="lc-arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M 0 1 L 8 5 L 0 9 z" fill="rgba(108,117,125,0.55)"/></marker>',le.appendChild(pe),G.appendChild(le),!document.getElementById("lc-board-css")){let i=document.createElement("style");i.id="lc-board-css",i.textContent=`
|
|
94
|
+
.lc-canvas-card { position:absolute; min-width:${c.minWidth}px; cursor:grab; user-select:none; z-index:1; }
|
|
2463
95
|
.lc-canvas-card.lc-dragging { cursor:grabbing; z-index:10; box-shadow:0 8px 24px rgba(0,0,0,0.18)!important; }
|
|
2464
96
|
.lc-canvas-card .card-body { overflow:hidden; }
|
|
2465
97
|
.lc-canvas-card.lc-resizing { cursor:nwse-resize; z-index:10; }
|
|
@@ -2471,930 +103,10 @@ var LiveCard = (function () {
|
|
|
2471
103
|
@keyframes lc-edge-flow { to { stroke-dashoffset:-10; } }
|
|
2472
104
|
.lc-source-node { position:absolute; cursor:grab; user-select:none; z-index:1; }
|
|
2473
105
|
.lc-source-node.lc-dragging { cursor:grabbing; z-index:10; }
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
// ---- Helpers ----
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
const view = node && node.card ? node.card.view : null;
|
|
2482
|
-
if (view && view.layout && view.layout.board && view.layout.board.col) return view.layout.board.col;
|
|
2483
|
-
return defaultCol;
|
|
2484
|
-
}
|
|
2485
|
-
|
|
2486
|
-
function _initPositions() {
|
|
2487
|
-
const explicit = opts.positions || {};
|
|
2488
|
-
nodeList.forEach((node, i) => {
|
|
2489
|
-
if (_positions[node.id]) return; // already set
|
|
2490
|
-
if (explicit[node.id]) {
|
|
2491
|
-
_positions[node.id] = Object.assign({}, explicit[node.id]);
|
|
2492
|
-
} else if (node.card && node.card.view && node.card.view.layout && node.card.view.layout.canvas && node.card.view.layout.canvas.x != null) {
|
|
2493
|
-
_positions[node.id] = Object.assign({}, node.card.view.layout.canvas);
|
|
2494
|
-
} else {
|
|
2495
|
-
const col = (i % 4);
|
|
2496
|
-
const row = Math.floor(i / 4);
|
|
2497
|
-
_positions[node.id] = { x: col * cvs.gapX + cvs.padX, y: row * cvs.gapY + cvs.padY, w: cvs.defaultW };
|
|
2498
|
-
}
|
|
2499
|
-
});
|
|
2500
|
-
}
|
|
2501
|
-
|
|
2502
|
-
function _getRequires(node) {
|
|
2503
|
-
return (node && node.card && Array.isArray(node.card.requires)) ? node.card.requires : [];
|
|
2504
|
-
}
|
|
2505
|
-
|
|
2506
|
-
/**
|
|
2507
|
-
* Returns tokens this node provides.
|
|
2508
|
-
* Explicit: card.provides[].bindTo
|
|
2509
|
-
* Implicit default: the node's own id (if no provides declared)
|
|
2510
|
-
*/
|
|
2511
|
-
function _getProvides(node) {
|
|
2512
|
-
if (!node || !node.card) return [node ? node.id : ''];
|
|
2513
|
-
if (Array.isArray(node.card.provides) && node.card.provides.length > 0) {
|
|
2514
|
-
return node.card.provides.map(function(p) { return (typeof p === 'string') ? p : (p.bindTo || p); });
|
|
2515
|
-
}
|
|
2516
|
-
// Default: node provides a token equal to its own id
|
|
2517
|
-
return [node.id];
|
|
2518
|
-
}
|
|
2519
|
-
|
|
2520
|
-
/**
|
|
2521
|
-
* Build token → provider nodeId map from all nodes in the board.
|
|
2522
|
-
* Called before drawing edges so we can resolve requires tokens → source nodes.
|
|
2523
|
-
*/
|
|
2524
|
-
function _buildTokenMap() {
|
|
2525
|
-
var map = {};
|
|
2526
|
-
nodeList.forEach(function(node) {
|
|
2527
|
-
_getProvides(node).forEach(function(token) {
|
|
2528
|
-
map[token] = node.id;
|
|
2529
|
-
});
|
|
2530
|
-
});
|
|
2531
|
-
return map;
|
|
2532
|
-
}
|
|
2533
|
-
|
|
2534
|
-
/**
|
|
2535
|
-
* Resolve required tokens to provider node IDs.
|
|
2536
|
-
* Returns deduplicated array of source node IDs for a given consumer node.
|
|
2537
|
-
*/
|
|
2538
|
-
function _resolveEdgeSources(node, tokenMap) {
|
|
2539
|
-
var sources = [];
|
|
2540
|
-
var seen = {};
|
|
2541
|
-
_getRequires(node).forEach(function(token) {
|
|
2542
|
-
var srcId = tokenMap[token];
|
|
2543
|
-
if (srcId && !seen[srcId]) {
|
|
2544
|
-
seen[srcId] = true;
|
|
2545
|
-
sources.push(srcId);
|
|
2546
|
-
}
|
|
2547
|
-
});
|
|
2548
|
-
return sources;
|
|
2549
|
-
}
|
|
2550
|
-
|
|
2551
|
-
function _showCardInspector(node) {
|
|
2552
|
-
const modal = document.createElement('div');
|
|
2553
|
-
modal.className = 'modal d-block';
|
|
2554
|
-
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;';
|
|
2555
|
-
|
|
2556
|
-
const dialog = document.createElement('div');
|
|
2557
|
-
dialog.className = 'modal-dialog';
|
|
2558
|
-
dialog.style.cssText = 'width: 92%; max-width: 980px; max-height: 88vh; overflow: auto;';
|
|
2559
|
-
|
|
2560
|
-
const content = document.createElement('div');
|
|
2561
|
-
content.className = 'modal-content';
|
|
2562
|
-
|
|
2563
|
-
const header = document.createElement('div');
|
|
2564
|
-
header.className = 'modal-header';
|
|
2565
|
-
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>`;
|
|
2566
|
-
|
|
2567
|
-
const closeModal = function () { modal.remove(); };
|
|
2568
|
-
header.querySelector('.btn-close').addEventListener('click', closeModal);
|
|
2569
|
-
|
|
2570
|
-
const body = document.createElement('div');
|
|
2571
|
-
body.className = 'modal-body';
|
|
2572
|
-
body.style.cssText = 'max-height: 64vh; overflow-y: auto;';
|
|
2573
|
-
|
|
2574
|
-
const cardSection = document.createElement('div');
|
|
2575
|
-
cardSection.className = 'mb-4';
|
|
2576
|
-
cardSection.innerHTML = '<h6 class="fw-semibold mb-2">Card Definition (Read-only)</h6>';
|
|
2577
|
-
const cardDef = (node && node.card) ? node.card : {};
|
|
2578
|
-
cardSection.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(cardDef, null, 2))}</pre>`;
|
|
2579
|
-
body.appendChild(cardSection);
|
|
2580
|
-
|
|
2581
|
-
const computedSection = document.createElement('div');
|
|
2582
|
-
computedSection.className = 'mb-4';
|
|
2583
|
-
computedSection.innerHTML = '<h6 class="fw-semibold mb-2">Computed Values (Read-only)</h6>';
|
|
2584
|
-
const computedValues = node.computed_values || {};
|
|
2585
|
-
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>`;
|
|
2586
|
-
body.appendChild(computedSection);
|
|
2587
|
-
|
|
2588
|
-
const requiresSection = document.createElement('div');
|
|
2589
|
-
requiresSection.className = 'mb-4';
|
|
2590
|
-
requiresSection.innerHTML = '<h6 class="fw-semibold mb-2">Requires (Read-only)</h6>';
|
|
2591
|
-
const requiresData = node.requires || {};
|
|
2592
|
-
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>`;
|
|
2593
|
-
body.appendChild(requiresSection);
|
|
2594
|
-
|
|
2595
|
-
const stateSection = document.createElement('div');
|
|
2596
|
-
stateSection.className = 'mb-2';
|
|
2597
|
-
stateSection.innerHTML = '<h6 class="fw-semibold mb-2">Runtime Status (Read-only)</h6>';
|
|
2598
|
-
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 };
|
|
2599
|
-
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>`;
|
|
2600
|
-
body.appendChild(stateSection);
|
|
2601
|
-
|
|
2602
|
-
const footer = document.createElement('div');
|
|
2603
|
-
footer.className = 'modal-footer';
|
|
2604
|
-
const closeBtn = document.createElement('button');
|
|
2605
|
-
closeBtn.type = 'button';
|
|
2606
|
-
closeBtn.className = 'btn btn-secondary';
|
|
2607
|
-
closeBtn.textContent = 'Close';
|
|
2608
|
-
closeBtn.addEventListener('click', closeModal);
|
|
2609
|
-
|
|
2610
|
-
footer.appendChild(closeBtn);
|
|
2611
|
-
content.appendChild(header);
|
|
2612
|
-
content.appendChild(body);
|
|
2613
|
-
content.appendChild(footer);
|
|
2614
|
-
dialog.appendChild(content);
|
|
2615
|
-
modal.appendChild(dialog);
|
|
2616
|
-
document.body.appendChild(modal);
|
|
2617
|
-
}
|
|
2618
|
-
|
|
2619
|
-
function _buildCardWrapper(node) {
|
|
2620
|
-
const wrap = document.createElement('div');
|
|
2621
|
-
const card = node && node.card ? node.card : {};
|
|
2622
|
-
const isSimulation = card.meta && card.meta.simulation === true;
|
|
2623
|
-
const isGandalfCard = card.meta && card.meta._gandalfCard === true;
|
|
2624
|
-
const isRunning = node && node.runtime_state && node.runtime_state.task_status === 'running';
|
|
2625
|
-
const extraClass = isSimulation ? ' lc-simulation-card' : (isGandalfCard ? ' lc-gandalf-card' : '');
|
|
2626
|
-
wrap.className = 'card shadow-sm h-100' + extraClass + (isRunning ? ' lc-running' : '');
|
|
2627
|
-
const header = document.createElement('div');
|
|
2628
|
-
header.className = 'card-header d-flex align-items-center gap-2 py-2';
|
|
2629
|
-
const title = (card.meta && card.meta.title) || node.id;
|
|
2630
|
-
const tags = (card.meta && card.meta.tags) || [];
|
|
2631
|
-
let badgeHtml = '';
|
|
2632
|
-
if ((card.source_defs && card.source_defs.length) && !card.view) {
|
|
2633
|
-
var src = card.source_defs[0] || {};
|
|
2634
|
-
badgeHtml = '<span class="badge bg-info text-dark ms-auto">' + _esc(src.kind || 'source') + '</span>';
|
|
2635
|
-
} else if (tags.length) {
|
|
2636
|
-
badgeHtml = tags.map(t => '<span class="badge bg-secondary ms-1">' + _esc(t) + '</span>').join('');
|
|
2637
|
-
}
|
|
2638
|
-
header.innerHTML = '<strong class="small">' + _esc(title) + '</strong>' + badgeHtml;
|
|
2639
|
-
|
|
2640
|
-
// Gandalf cards: collapsible via caret — caret gets its own click listener,
|
|
2641
|
-
// header is left alone for dragging in canvas mode.
|
|
2642
|
-
if (isGandalfCard) {
|
|
2643
|
-
const caret = document.createElement('span');
|
|
2644
|
-
caret.className = 'lc-gandalf-caret';
|
|
2645
|
-
caret.title = 'Collapse / expand';
|
|
2646
|
-
caret.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"/></svg>';
|
|
2647
|
-
header.appendChild(caret);
|
|
2648
|
-
|
|
2649
|
-
const storageKey = 'lc-gandalf-collapsed:' + (node.id || title);
|
|
2650
|
-
if (sessionStorage.getItem(storageKey) === '1') {
|
|
2651
|
-
wrap.classList.add('lc-collapsed');
|
|
2652
|
-
header.dataset.gandalfCollapsed = '1';
|
|
2653
|
-
}
|
|
2654
|
-
|
|
2655
|
-
caret.addEventListener('click', function(e) {
|
|
2656
|
-
e.stopPropagation();
|
|
2657
|
-
const cardEl = caret.closest('.lc-gandalf-card') || wrap;
|
|
2658
|
-
cardEl.classList.toggle('lc-collapsed');
|
|
2659
|
-
sessionStorage.setItem(storageKey, cardEl.classList.contains('lc-collapsed') ? '1' : '0');
|
|
2660
|
-
});
|
|
2661
|
-
caret.addEventListener('pointerdown', e => e.stopPropagation()); // prevent drag start
|
|
2662
|
-
}
|
|
2663
|
-
if (isSimulation) {
|
|
2664
|
-
const simBtns = document.createElement('span');
|
|
2665
|
-
simBtns.className = 'd-inline-flex align-items-center gap-1 ms-auto';
|
|
2666
|
-
|
|
2667
|
-
const pinBtn = document.createElement('button');
|
|
2668
|
-
pinBtn.className = 'btn btn-sm btn-outline-success lc-sim-pin';
|
|
2669
|
-
pinBtn.style.cssText = 'padding: 2px 6px;';
|
|
2670
|
-
pinBtn.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="M12 17v5"/><path d="M9 2h6l-1 7h-4L9 2z"/><path d="M6 17h12l-2-4H8L6 17z"/></svg>';
|
|
2671
|
-
pinBtn.title = 'Pin this simulation card';
|
|
2672
|
-
pinBtn.dataset.nodeId = node.id;
|
|
2673
|
-
|
|
2674
|
-
const discardBtn = document.createElement('button');
|
|
2675
|
-
discardBtn.className = 'btn btn-sm btn-outline-danger lc-sim-discard';
|
|
2676
|
-
discardBtn.style.cssText = 'padding: 2px 6px;';
|
|
2677
|
-
discardBtn.innerHTML = '<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>';
|
|
2678
|
-
discardBtn.title = 'Discard this simulation card';
|
|
2679
|
-
discardBtn.dataset.nodeId = node.id;
|
|
2680
|
-
|
|
2681
|
-
simBtns.appendChild(pinBtn);
|
|
2682
|
-
simBtns.appendChild(discardBtn);
|
|
2683
|
-
header.appendChild(simBtns);
|
|
2684
|
-
}
|
|
2685
|
-
|
|
2686
|
-
// Add dev mode code icon button if devMode is enabled
|
|
2687
|
-
if (devMode.current) {
|
|
2688
|
-
const codeBtn = document.createElement('button');
|
|
2689
|
-
codeBtn.className = 'btn btn-sm btn-outline-secondary';
|
|
2690
|
-
codeBtn.style.cssText = 'padding: 2px 6px;' + (isSimulation ? '' : ' margin-left: auto;');
|
|
2691
|
-
codeBtn.innerHTML = '</>';
|
|
2692
|
-
codeBtn.title = 'Inspect card data';
|
|
2693
|
-
codeBtn.addEventListener('click', function(e) {
|
|
2694
|
-
e.stopPropagation();
|
|
2695
|
-
_showCardInspector(node);
|
|
2696
|
-
});
|
|
2697
|
-
header.appendChild(codeBtn);
|
|
2698
|
-
}
|
|
2699
|
-
|
|
2700
|
-
const body = document.createElement('div');
|
|
2701
|
-
body.className = 'card-body p-2';
|
|
2702
|
-
|
|
2703
|
-
// Token gem rows — requires gems above header, provides gems below body
|
|
2704
|
-
const requiresTokens = (card.requires && Array.isArray(card.requires)) ? card.requires : [];
|
|
2705
|
-
const providesTokens = (Array.isArray(card.provides) && card.provides.length)
|
|
2706
|
-
? card.provides.map(function(p) { return typeof p === 'string' ? p : (p.bindTo || p); })
|
|
2707
|
-
: [node.id];
|
|
2708
|
-
|
|
2709
|
-
// Requires gems — top of card (above header)
|
|
2710
|
-
if (requiresTokens.length) {
|
|
2711
|
-
const reqRow = document.createElement('div');
|
|
2712
|
-
reqRow.className = 'lc-token-row lc-token-row-requires';
|
|
2713
|
-
requiresTokens.forEach(function(token) {
|
|
2714
|
-
const gem = document.createElement('span');
|
|
2715
|
-
gem.className = 'lc-token-gem lc-token-gem-requires';
|
|
2716
|
-
gem.dataset.token = token;
|
|
2717
|
-
gem.title = token;
|
|
2718
|
-
reqRow.appendChild(gem);
|
|
2719
|
-
});
|
|
2720
|
-
wrap.appendChild(reqRow);
|
|
2721
|
-
}
|
|
2722
|
-
|
|
2723
|
-
wrap.appendChild(header);
|
|
2724
|
-
wrap.appendChild(body);
|
|
2725
|
-
|
|
2726
|
-
// Provides gems — bottom of card (below body)
|
|
2727
|
-
if (providesTokens.length) {
|
|
2728
|
-
const provRow = document.createElement('div');
|
|
2729
|
-
provRow.className = 'lc-token-row lc-token-row-provides';
|
|
2730
|
-
providesTokens.forEach(function(token) {
|
|
2731
|
-
const gem = document.createElement('span');
|
|
2732
|
-
gem.className = 'lc-token-gem lc-token-gem-provides';
|
|
2733
|
-
gem.dataset.token = token;
|
|
2734
|
-
gem.title = token;
|
|
2735
|
-
provRow.appendChild(gem);
|
|
2736
|
-
});
|
|
2737
|
-
wrap.appendChild(provRow);
|
|
2738
|
-
}
|
|
2739
|
-
|
|
2740
|
-
return { wrap, header, body };
|
|
2741
|
-
}
|
|
2742
|
-
|
|
2743
|
-
function _buildSourcePill(node) {
|
|
2744
|
-
const el = document.createElement('div');
|
|
2745
|
-
el.className = 'lc-source-node';
|
|
2746
|
-
const status = (node.card_data && node.card_data.status) || 'fresh';
|
|
2747
|
-
const card = node && node.card ? node.card : {};
|
|
2748
|
-
const title = (card.meta && card.meta.title) || node.id;
|
|
2749
|
-
const kind = (card.source_defs && card.source_defs[0] && card.source_defs[0].kind) || 'source';
|
|
2750
|
-
el.innerHTML = `<div class="lc-source-pill shadow-sm">
|
|
2751
|
-
${_statusDot(status)}
|
|
2752
|
-
<span class="fw-medium">${_esc(title)}</span>
|
|
2753
|
-
<span class="badge bg-info text-dark">${_esc(kind)}</span>
|
|
2754
|
-
</div>`;
|
|
2755
|
-
return el;
|
|
2756
|
-
}
|
|
2757
|
-
|
|
2758
|
-
// ---- Board mode ----
|
|
2759
|
-
|
|
2760
|
-
// Compute canvas inner size from card positions + padding
|
|
2761
|
-
function _fitCanvasToContent() {
|
|
2762
|
-
var pad = 100;
|
|
2763
|
-
var maxR = 0, maxB = 0;
|
|
2764
|
-
canvasInner.querySelectorAll('.lc-canvas-card,.lc-source-node').forEach(function(el) {
|
|
2765
|
-
var r = el.offsetLeft + el.offsetWidth;
|
|
2766
|
-
var b = el.offsetTop + el.offsetHeight;
|
|
2767
|
-
if (r > maxR) maxR = r;
|
|
2768
|
-
if (b > maxB) maxB = b;
|
|
2769
|
-
});
|
|
2770
|
-
canvasInner.style.width = (maxR + pad) + 'px';
|
|
2771
|
-
canvasInner.style.height = (maxB + pad) + 'px';
|
|
2772
|
-
}
|
|
2773
|
-
|
|
2774
|
-
function _renderBoard() {
|
|
2775
|
-
_destroyEdges();
|
|
2776
|
-
document.body.style.overflow = '';
|
|
2777
|
-
root.innerHTML = '';
|
|
2778
|
-
root.appendChild(gridEl);
|
|
2779
|
-
gridEl.innerHTML = '';
|
|
2780
|
-
|
|
2781
|
-
// Only card nodes in board mode, sorted by order
|
|
2782
|
-
const cards = nodeList.filter(n => n.card && n.card.view).slice();
|
|
2783
|
-
cards.sort((a, b) => {
|
|
2784
|
-
const ao = (a.card && a.card.view && a.card.view.layout && a.card.view.layout.board && a.card.view.layout.board.order) || 0;
|
|
2785
|
-
const bo = (b.card && b.card.view && b.card.view.layout && b.card.view.layout.board && b.card.view.layout.board.order) || 0;
|
|
2786
|
-
return ao - bo;
|
|
2787
|
-
});
|
|
2788
|
-
|
|
2789
|
-
cards.forEach(node => {
|
|
2790
|
-
const col = document.createElement('div');
|
|
2791
|
-
col.className = 'col-12 col-md-' + _colWidth(node);
|
|
2792
|
-
col.dataset.nodeId = node.id;
|
|
2793
|
-
const { wrap, body } = _buildCardWrapper(node);
|
|
2794
|
-
col.appendChild(wrap);
|
|
2795
|
-
gridEl.appendChild(col);
|
|
2796
|
-
nodeMap[node.id] = { node, colEl: col, bodyEl: body };
|
|
2797
|
-
engine.render(node, body, { showChat });
|
|
2798
|
-
});
|
|
2799
|
-
_updateTokenAvailability();
|
|
2800
|
-
}
|
|
2801
|
-
|
|
2802
|
-
// ---- Canvas mode ----
|
|
2803
|
-
|
|
2804
|
-
function _applyTransform() {
|
|
2805
|
-
canvasInner.style.transform = `translate(${cvs.panX}px,${cvs.panY}px) scale(${cvs.zoom})`;
|
|
2806
|
-
}
|
|
2807
|
-
|
|
2808
|
-
/**
|
|
2809
|
-
* Update token badge availability: a provides badge turns green when the
|
|
2810
|
-
* node has data; a requires badge turns green when the upstream provider
|
|
2811
|
-
* has data for that token.
|
|
2812
|
-
*/
|
|
2813
|
-
function _updateTokenAvailability() {
|
|
2814
|
-
var tokenMap = _buildTokenMap();
|
|
2815
|
-
// A node "has data" when card_data or computed_values is non-empty, or status is fresh/completed.
|
|
2816
|
-
var nodeHasData = {};
|
|
2817
|
-
nodeList.forEach(function(node) {
|
|
2818
|
-
var cd = node.card_data || (node.card && node.card.card_data);
|
|
2819
|
-
var cv = node.computed_values;
|
|
2820
|
-
var status = cd && cd.status;
|
|
2821
|
-
var hasOutput = (cd && Object.keys(cd).length > 0) || (cv && Object.keys(cv).length > 0);
|
|
2822
|
-
nodeHasData[node.id] = hasOutput || status === 'fresh' || status === 'completed';
|
|
2823
|
-
});
|
|
2824
|
-
|
|
2825
|
-
// Update all gem elements in root container
|
|
2826
|
-
var allGems = root.querySelectorAll('.lc-token-gem');
|
|
2827
|
-
allGems.forEach(function(gem) {
|
|
2828
|
-
var token = gem.dataset.token;
|
|
2829
|
-
if (!token) return;
|
|
2830
|
-
if (gem.classList.contains('lc-token-gem-provides')) {
|
|
2831
|
-
// The provides gem: green if this node has data
|
|
2832
|
-
var nodeEl = gem.closest('[data-node-id]');
|
|
2833
|
-
var nId = nodeEl && nodeEl.dataset.nodeId;
|
|
2834
|
-
gem.classList.toggle('lc-token-available', !!(nId && nodeHasData[nId]));
|
|
2835
|
-
} else if (gem.classList.contains('lc-token-gem-requires')) {
|
|
2836
|
-
// The requires gem: green if the upstream provider for this token has data
|
|
2837
|
-
var srcId = tokenMap[token];
|
|
2838
|
-
gem.classList.toggle('lc-token-available', !!(srcId && nodeHasData[srcId]));
|
|
2839
|
-
}
|
|
2840
|
-
});
|
|
2841
|
-
}
|
|
2842
|
-
|
|
2843
|
-
function _destroyEdges() {
|
|
2844
|
-
_edges.forEach(function(line) { try { line.remove(); } catch(e) { /* noop */ } });
|
|
2845
|
-
_edges.length = 0;
|
|
2846
|
-
}
|
|
2847
|
-
|
|
2848
|
-
function _repositionEdges() {
|
|
2849
|
-
_edges.forEach(function(line) { try { line.position(); } catch(e) { /* noop */ } });
|
|
2850
|
-
}
|
|
2851
|
-
|
|
2852
|
-
function _drawEdges() {
|
|
2853
|
-
_destroyEdges();
|
|
2854
|
-
svgEl.querySelectorAll('line,path').forEach(function(el) { el.remove(); });
|
|
2855
|
-
if (!cvs.edges) return;
|
|
2856
|
-
|
|
2857
|
-
// Build token → provider nodeId map
|
|
2858
|
-
var tokenMap = _buildTokenMap();
|
|
2859
|
-
|
|
2860
|
-
// SVG edges — rendered behind cards (z-index:0) for a clean look
|
|
2861
|
-
nodeList.forEach(function(node) {
|
|
2862
|
-
var tgtInfo = nodeMap[node.id];
|
|
2863
|
-
if (!tgtInfo || !tgtInfo.colEl) return;
|
|
2864
|
-
_getRequires(node).forEach(function(token) {
|
|
2865
|
-
var srcId = tokenMap[token];
|
|
2866
|
-
if (!srcId) return;
|
|
2867
|
-
var srcInfo = nodeMap[srcId];
|
|
2868
|
-
if (!srcInfo || !srcInfo.colEl) return;
|
|
2869
|
-
// Locate gems; fall back to card element if gem not found
|
|
2870
|
-
var srcGem = srcInfo.colEl.querySelector('.lc-token-gem-provides[data-token="' + token + '"]');
|
|
2871
|
-
var tgtGem = tgtInfo.colEl.querySelector('.lc-token-gem-requires[data-token="' + token + '"]');
|
|
2872
|
-
var sx, sy, tx, ty;
|
|
2873
|
-
var innerRect = canvasInner.getBoundingClientRect();
|
|
2874
|
-
if (srcGem) {
|
|
2875
|
-
var srcRect = srcGem.getBoundingClientRect();
|
|
2876
|
-
sx = (srcRect.left + srcRect.width / 2 - innerRect.left) / cvs.zoom;
|
|
2877
|
-
sy = (srcRect.bottom - innerRect.top) / cvs.zoom;
|
|
2878
|
-
} else {
|
|
2879
|
-
var sEl = srcInfo.colEl;
|
|
2880
|
-
sx = sEl.offsetLeft + sEl.offsetWidth / 2;
|
|
2881
|
-
sy = sEl.offsetTop + sEl.offsetHeight;
|
|
2882
|
-
}
|
|
2883
|
-
if (tgtGem) {
|
|
2884
|
-
var tgtRect = tgtGem.getBoundingClientRect();
|
|
2885
|
-
tx = (tgtRect.left + tgtRect.width / 2 - innerRect.left) / cvs.zoom;
|
|
2886
|
-
ty = (tgtRect.top - innerRect.top) / cvs.zoom;
|
|
2887
|
-
} else {
|
|
2888
|
-
var tEl = tgtInfo.colEl;
|
|
2889
|
-
tx = tEl.offsetLeft + tEl.offsetWidth / 2;
|
|
2890
|
-
ty = tEl.offsetTop;
|
|
2891
|
-
}
|
|
2892
|
-
// Route bezier curves around cards — offset control points outward
|
|
2893
|
-
var dx = tx - sx;
|
|
2894
|
-
var dy = ty - sy;
|
|
2895
|
-
var dist = Math.sqrt(dx * dx + dy * dy);
|
|
2896
|
-
var cpLen = Math.max(40, Math.min(dist * 0.4, 120));
|
|
2897
|
-
// Determine if src is roughly above, below, left, or right of target
|
|
2898
|
-
var absDx = Math.abs(dx);
|
|
2899
|
-
var absDy = Math.abs(dy);
|
|
2900
|
-
var cp1x, cp1y, cp2x, cp2y;
|
|
2901
|
-
if (absDy > absDx * 0.4) {
|
|
2902
|
-
// Mostly vertical — curve control points go straight down/up
|
|
2903
|
-
cp1x = sx; cp1y = sy + cpLen;
|
|
2904
|
-
cp2x = tx; cp2y = ty - cpLen;
|
|
2905
|
-
} else {
|
|
2906
|
-
// Mostly horizontal — swing control points outward to avoid overlapping cards
|
|
2907
|
-
var sideSign = dx > 0 ? 1 : -1;
|
|
2908
|
-
cp1x = sx + sideSign * cpLen; cp1y = sy + cpLen * 0.5;
|
|
2909
|
-
cp2x = tx - sideSign * cpLen; cp2y = ty - cpLen * 0.5;
|
|
2910
|
-
}
|
|
2911
|
-
var d = 'M ' + sx + ' ' + sy + ' C ' + cp1x + ' ' + cp1y + ', ' + cp2x + ' ' + cp2y + ', ' + tx + ' ' + ty;
|
|
2912
|
-
var path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
|
|
2913
|
-
path.setAttribute('d', d);
|
|
2914
|
-
path.setAttribute('fill', 'none');
|
|
2915
|
-
path.setAttribute('marker-end', 'url(#lc-arrow)');
|
|
2916
|
-
path.classList.add('lc-edge-path');
|
|
2917
|
-
svgEl.appendChild(path);
|
|
2918
|
-
});
|
|
2919
|
-
});
|
|
2920
|
-
}
|
|
2921
|
-
|
|
2922
|
-
function _makeDraggable(el, node) {
|
|
2923
|
-
let startX, startY, origX, origY, dragging = false;
|
|
2924
|
-
|
|
2925
|
-
el.addEventListener('pointerdown', e => {
|
|
2926
|
-
if (e.button !== 0) return;
|
|
2927
|
-
if (e.target.closest('input,textarea,select,button,a,.form-check-input')) return;
|
|
2928
|
-
dragging = true;
|
|
2929
|
-
el.classList.add('lc-dragging');
|
|
2930
|
-
el.setPointerCapture(e.pointerId);
|
|
2931
|
-
startX = e.clientX; startY = e.clientY;
|
|
2932
|
-
origX = el.offsetLeft; origY = el.offsetTop;
|
|
2933
|
-
e.preventDefault();
|
|
2934
|
-
}, { signal });
|
|
2935
|
-
|
|
2936
|
-
el.addEventListener('pointermove', e => {
|
|
2937
|
-
if (!dragging) return;
|
|
2938
|
-
const dx = (e.clientX - startX) / cvs.zoom;
|
|
2939
|
-
const dy = (e.clientY - startY) / cvs.zoom;
|
|
2940
|
-
el.style.left = (origX + dx) + 'px';
|
|
2941
|
-
el.style.top = (origY + dy) + 'px';
|
|
2942
|
-
if (_edges.length) _repositionEdges();
|
|
2943
|
-
else _drawEdges();
|
|
2944
|
-
}, { signal });
|
|
2945
|
-
|
|
2946
|
-
el.addEventListener('pointerup', () => {
|
|
2947
|
-
if (!dragging) return;
|
|
2948
|
-
dragging = false;
|
|
2949
|
-
el.classList.remove('lc-dragging');
|
|
2950
|
-
let x = el.offsetLeft, y = el.offsetTop;
|
|
2951
|
-
if (cvs.snap > 1) { x = Math.round(x / cvs.snap) * cvs.snap; y = Math.round(y / cvs.snap) * cvs.snap; }
|
|
2952
|
-
el.style.left = x + 'px'; el.style.top = y + 'px';
|
|
2953
|
-
// Persist
|
|
2954
|
-
_positions[node.id] = Object.assign(_positions[node.id] || {}, { x, y });
|
|
2955
|
-
if (node.card && node.card.view) {
|
|
2956
|
-
if (!node.card.view.layout) node.card.view.layout = {};
|
|
2957
|
-
if (!node.card.view.layout.canvas) node.card.view.layout.canvas = {};
|
|
2958
|
-
node.card.view.layout.canvas.x = x;
|
|
2959
|
-
node.card.view.layout.canvas.y = y;
|
|
2960
|
-
}
|
|
2961
|
-
engine.notify(node.id);
|
|
2962
|
-
_fitCanvasToContent();
|
|
2963
|
-
if (_edges.length) _repositionEdges();
|
|
2964
|
-
else _drawEdges();
|
|
2965
|
-
}, { signal });
|
|
2966
|
-
}
|
|
2967
|
-
|
|
2968
|
-
function _makeResizable(el, node) {
|
|
2969
|
-
const handle = document.createElement('div');
|
|
2970
|
-
handle.className = 'lc-resize-handle';
|
|
2971
|
-
el.appendChild(handle);
|
|
2972
|
-
el.style.overflow = 'visible';
|
|
2973
|
-
|
|
2974
|
-
let resizing = false, startX, startY, origW, origH;
|
|
2975
|
-
|
|
2976
|
-
handle.addEventListener('pointerdown', function(e) {
|
|
2977
|
-
if (e.button !== 0) return;
|
|
2978
|
-
e.stopPropagation();
|
|
2979
|
-
e.preventDefault();
|
|
2980
|
-
resizing = true;
|
|
2981
|
-
el.classList.add('lc-resizing');
|
|
2982
|
-
handle.setPointerCapture(e.pointerId);
|
|
2983
|
-
startX = e.clientX;
|
|
2984
|
-
startY = e.clientY;
|
|
2985
|
-
origW = el.offsetWidth;
|
|
2986
|
-
origH = el.offsetHeight;
|
|
2987
|
-
}, { signal });
|
|
2988
|
-
|
|
2989
|
-
handle.addEventListener('pointermove', function(e) {
|
|
2990
|
-
if (!resizing) return;
|
|
2991
|
-
const dw = (e.clientX - startX) / cvs.zoom;
|
|
2992
|
-
const dh = (e.clientY - startY) / cvs.zoom;
|
|
2993
|
-
const newW = Math.max(cvs.minWidth, origW + dw);
|
|
2994
|
-
const newH = Math.max(80, origH + dh);
|
|
2995
|
-
el.style.width = newW + 'px';
|
|
2996
|
-
el.style.height = newH + 'px';
|
|
2997
|
-
if (_edges.length) _repositionEdges();
|
|
2998
|
-
else _drawEdges();
|
|
2999
|
-
}, { signal });
|
|
3000
|
-
|
|
3001
|
-
handle.addEventListener('pointerup', function() {
|
|
3002
|
-
if (!resizing) return;
|
|
3003
|
-
resizing = false;
|
|
3004
|
-
el.classList.remove('lc-resizing');
|
|
3005
|
-
const w = el.offsetWidth;
|
|
3006
|
-
const h = el.offsetHeight;
|
|
3007
|
-
// Snap to grid
|
|
3008
|
-
const sw = cvs.snap > 1 ? Math.round(w / cvs.snap) * cvs.snap : w;
|
|
3009
|
-
const sh = cvs.snap > 1 ? Math.round(h / cvs.snap) * cvs.snap : h;
|
|
3010
|
-
el.style.width = sw + 'px';
|
|
3011
|
-
el.style.height = sh + 'px';
|
|
3012
|
-
// Persist dimensions
|
|
3013
|
-
_positions[node.id] = Object.assign(_positions[node.id] || {}, { w: sw, h: sh });
|
|
3014
|
-
if (node.card && node.card.view) {
|
|
3015
|
-
if (!node.card.view.layout) node.card.view.layout = {};
|
|
3016
|
-
if (!node.card.view.layout.canvas) node.card.view.layout.canvas = {};
|
|
3017
|
-
node.card.view.layout.canvas.w = sw;
|
|
3018
|
-
node.card.view.layout.canvas.h = sh;
|
|
3019
|
-
}
|
|
3020
|
-
engine.notify(node.id);
|
|
3021
|
-
_fitCanvasToContent();
|
|
3022
|
-
if (_edges.length) _repositionEdges();
|
|
3023
|
-
else _drawEdges();
|
|
3024
|
-
}, { signal });
|
|
3025
|
-
}
|
|
3026
|
-
|
|
3027
|
-
function _renderCanvas() {
|
|
3028
|
-
_destroyEdges();
|
|
3029
|
-
document.body.style.overflow = 'hidden';
|
|
3030
|
-
root.innerHTML = '';
|
|
3031
|
-
root.appendChild(canvasEl);
|
|
3032
|
-
// Fill remaining viewport height
|
|
3033
|
-
var top = canvasEl.getBoundingClientRect().top;
|
|
3034
|
-
canvasEl.style.height = co.height || ('calc(100vh - ' + top + 'px)');
|
|
3035
|
-
canvasInner.querySelectorAll('.lc-canvas-card,.lc-source-node').forEach(el => el.remove());
|
|
3036
|
-
svgEl.querySelectorAll('line,path').forEach(function(el) { el.remove(); });
|
|
3037
|
-
_initPositions();
|
|
3038
|
-
_applyTransform();
|
|
3039
|
-
|
|
3040
|
-
nodeList.forEach(node => {
|
|
3041
|
-
const pos = _positions[node.id] || { x: 0, y: 0 };
|
|
3042
|
-
|
|
3043
|
-
if ((!node.card || !node.card.view) && (node.card && node.card.source_defs && node.card.source_defs.length)) {
|
|
3044
|
-
const el = _buildSourcePill(node);
|
|
3045
|
-
el.dataset.nodeId = node.id;
|
|
3046
|
-
el.style.left = pos.x + 'px';
|
|
3047
|
-
el.style.top = pos.y + 'px';
|
|
3048
|
-
canvasInner.appendChild(el);
|
|
3049
|
-
nodeMap[node.id] = { node, colEl: el, bodyEl: null };
|
|
3050
|
-
_makeDraggable(el, node);
|
|
3051
|
-
} else {
|
|
3052
|
-
const el = document.createElement('div');
|
|
3053
|
-
const isSimCanvas = node.card && node.card.meta && node.card.meta.simulation === true;
|
|
3054
|
-
const isGandalfCanvas = node.card && node.card.meta && node.card.meta._gandalfCard === true;
|
|
3055
|
-
const canvasExtra = isSimCanvas ? ' lc-simulation-card' : (isGandalfCanvas ? ' lc-gandalf-card' : '');
|
|
3056
|
-
el.className = 'lc-canvas-card card shadow-sm' + canvasExtra;
|
|
3057
|
-
el.dataset.nodeId = node.id;
|
|
3058
|
-
el.style.left = pos.x + 'px';
|
|
3059
|
-
el.style.top = pos.y + 'px';
|
|
3060
|
-
if (pos.w) el.style.width = pos.w + 'px';
|
|
3061
|
-
if (pos.h) el.style.height = pos.h + 'px';
|
|
3062
|
-
|
|
3063
|
-
const { wrap, body } = _buildCardWrapper(node);
|
|
3064
|
-
while (wrap.firstChild) el.appendChild(wrap.firstChild);
|
|
3065
|
-
// Re-apply collapsed state: in canvas mode el is the card container, not wrap
|
|
3066
|
-
const movedHeader = el.querySelector('.card-header');
|
|
3067
|
-
if (movedHeader && movedHeader.dataset.gandalfCollapsed === '1') el.classList.add('lc-collapsed');
|
|
3068
|
-
canvasInner.appendChild(el);
|
|
3069
|
-
nodeMap[node.id] = { node, colEl: el, bodyEl: body };
|
|
3070
|
-
engine.render(node, body, { showChat: false });
|
|
3071
|
-
_makeDraggable(el, node);
|
|
3072
|
-
_makeResizable(el, node);
|
|
3073
|
-
}
|
|
3074
|
-
});
|
|
3075
|
-
|
|
3076
|
-
_updateTokenAvailability();
|
|
3077
|
-
|
|
3078
|
-
// Fit canvas to content then draw edges
|
|
3079
|
-
requestAnimationFrame(function() {
|
|
3080
|
-
_fitCanvasToContent();
|
|
3081
|
-
_drawEdges();
|
|
3082
|
-
});
|
|
3083
|
-
|
|
3084
|
-
// Reposition LeaderLine edges on scroll
|
|
3085
|
-
canvasEl.addEventListener('scroll', function() { _repositionEdges(); }, { signal, passive: true });
|
|
3086
|
-
|
|
3087
|
-
// Pan: middle-click or Ctrl+drag on background
|
|
3088
|
-
let panning = false, panStartX, panStartY, panOrigX, panOrigY;
|
|
3089
|
-
canvasEl.addEventListener('pointerdown', e => {
|
|
3090
|
-
if (e.target !== canvasEl && e.target !== canvasInner) return;
|
|
3091
|
-
if (e.button === 1 || (e.button === 0 && e.ctrlKey)) {
|
|
3092
|
-
panning = true; canvasEl.setPointerCapture(e.pointerId);
|
|
3093
|
-
panStartX = e.clientX; panStartY = e.clientY;
|
|
3094
|
-
panOrigX = cvs.panX; panOrigY = cvs.panY;
|
|
3095
|
-
e.preventDefault();
|
|
3096
|
-
}
|
|
3097
|
-
}, { signal });
|
|
3098
|
-
canvasEl.addEventListener('pointermove', e => {
|
|
3099
|
-
if (!panning) return;
|
|
3100
|
-
cvs.panX = panOrigX + (e.clientX - panStartX);
|
|
3101
|
-
cvs.panY = panOrigY + (e.clientY - panStartY);
|
|
3102
|
-
_applyTransform();
|
|
3103
|
-
_repositionEdges();
|
|
3104
|
-
}, { signal });
|
|
3105
|
-
canvasEl.addEventListener('pointerup', () => { panning = false; }, { signal });
|
|
3106
|
-
|
|
3107
|
-
// Zoom: Ctrl+wheel
|
|
3108
|
-
canvasEl.addEventListener('wheel', e => {
|
|
3109
|
-
if (!e.ctrlKey) return;
|
|
3110
|
-
e.preventDefault();
|
|
3111
|
-
const delta = e.deltaY > 0 ? 0.9 : 1.1;
|
|
3112
|
-
cvs.zoom = Math.min(cvs.zoomMax, Math.max(cvs.zoomMin, cvs.zoom * delta));
|
|
3113
|
-
_applyTransform();
|
|
3114
|
-
_repositionEdges();
|
|
3115
|
-
}, { signal, passive: false });
|
|
3116
|
-
}
|
|
3117
|
-
|
|
3118
|
-
function _render() {
|
|
3119
|
-
if (mode.current === 'canvas') _renderCanvas();
|
|
3120
|
-
else _renderBoard();
|
|
3121
|
-
}
|
|
3122
|
-
|
|
3123
|
-
// ---- Auto-layout (topological L → R) ----
|
|
3124
|
-
|
|
3125
|
-
function autoLayout() {
|
|
3126
|
-
const tokenMap = _buildTokenMap();
|
|
3127
|
-
const incoming = {};
|
|
3128
|
-
const levels = {};
|
|
3129
|
-
nodeList.forEach(n => { incoming[n.id] = []; levels[n.id] = 0; });
|
|
3130
|
-
nodeList.forEach(n => {
|
|
3131
|
-
_resolveEdgeSources(n, tokenMap).forEach(srcId => {
|
|
3132
|
-
if (incoming[n.id]) incoming[n.id].push(srcId);
|
|
3133
|
-
});
|
|
3134
|
-
});
|
|
3135
|
-
|
|
3136
|
-
let changed = true;
|
|
3137
|
-
while (changed) {
|
|
3138
|
-
changed = false;
|
|
3139
|
-
nodeList.forEach(n => {
|
|
3140
|
-
(incoming[n.id] || []).forEach(srcId => {
|
|
3141
|
-
if (levels[srcId] != null && levels[srcId] + 1 > levels[n.id]) {
|
|
3142
|
-
levels[n.id] = levels[srcId] + 1;
|
|
3143
|
-
changed = true;
|
|
3144
|
-
}
|
|
3145
|
-
});
|
|
3146
|
-
});
|
|
3147
|
-
}
|
|
3148
|
-
|
|
3149
|
-
const colCounts = {};
|
|
3150
|
-
nodeList.forEach(n => {
|
|
3151
|
-
const lv = levels[n.id] || 0;
|
|
3152
|
-
if (!colCounts[lv]) colCounts[lv] = 0;
|
|
3153
|
-
const row = colCounts[lv]++;
|
|
3154
|
-
_positions[n.id] = {
|
|
3155
|
-
x: lv * 400 + 40,
|
|
3156
|
-
y: row * 300 + 40,
|
|
3157
|
-
w: (_positions[n.id] && _positions[n.id].w) || cvs.defaultW,
|
|
3158
|
-
};
|
|
3159
|
-
// Sync to card nodes
|
|
3160
|
-
if (n.view) {
|
|
3161
|
-
if (!n.view.layout) n.view.layout = {};
|
|
3162
|
-
n.view.layout.canvas = Object.assign({}, _positions[n.id]);
|
|
3163
|
-
}
|
|
3164
|
-
});
|
|
3165
|
-
if (mode.current === 'canvas') _renderCanvas();
|
|
3166
|
-
}
|
|
3167
|
-
|
|
3168
|
-
// ---- Public API ----
|
|
3169
|
-
|
|
3170
|
-
function add(node) {
|
|
3171
|
-
if (nodeMap[node.id]) return;
|
|
3172
|
-
nodeList.push(node);
|
|
3173
|
-
_render();
|
|
3174
|
-
}
|
|
3175
|
-
|
|
3176
|
-
function remove(nodeId) {
|
|
3177
|
-
engine.destroy(nodeId);
|
|
3178
|
-
const idx = nodeList.findIndex(n => n.id === nodeId);
|
|
3179
|
-
if (idx >= 0) nodeList.splice(idx, 1);
|
|
3180
|
-
delete nodeMap[nodeId];
|
|
3181
|
-
delete _positions[nodeId];
|
|
3182
|
-
_render();
|
|
3183
|
-
}
|
|
3184
|
-
|
|
3185
|
-
function reorder(ids) {
|
|
3186
|
-
nodeList.length = 0;
|
|
3187
|
-
ids.forEach(id => {
|
|
3188
|
-
const info = nodeMap[id];
|
|
3189
|
-
if (info) nodeList.push(info.node);
|
|
3190
|
-
});
|
|
3191
|
-
_render();
|
|
3192
|
-
}
|
|
3193
|
-
|
|
3194
|
-
/**
|
|
3195
|
-
* Per-node update: replace runtime fields on the existing node object in place
|
|
3196
|
-
* and re-render only that node's body. Outer wrapper is rebuilt to pick up
|
|
3197
|
-
* status/badges, but the surrounding column element is reused so layout is stable.
|
|
3198
|
-
* Editable element state is preserved via journal overlays keyed by nodeId:bindPath.
|
|
3199
|
-
*/
|
|
3200
|
-
function updateNode(id, model) {
|
|
3201
|
-
const entry = nodeMap[id];
|
|
3202
|
-
if (!entry) throw new Error('updateNode: unknown node id ' + id);
|
|
3203
|
-
const node = entry.node;
|
|
3204
|
-
if (model && typeof model === 'object') {
|
|
3205
|
-
if (model.card !== undefined) node.card = model.card;
|
|
3206
|
-
if (model.card_data !== undefined) node.card_data = model.card_data;
|
|
3207
|
-
if (model.requires !== undefined) node.requires = model.requires;
|
|
3208
|
-
if (model.computed_values !== undefined) node.computed_values = model.computed_values;
|
|
3209
|
-
if (model.runtime_state !== undefined) node.runtime_state = model.runtime_state;
|
|
3210
|
-
}
|
|
3211
|
-
engine.destroy(id);
|
|
3212
|
-
if (mode.current === 'board') {
|
|
3213
|
-
const colEl = entry.colEl;
|
|
3214
|
-
colEl.innerHTML = '';
|
|
3215
|
-
const built = _buildCardWrapper(node);
|
|
3216
|
-
colEl.appendChild(built.wrap);
|
|
3217
|
-
nodeMap[id] = { node, colEl, bodyEl: built.body };
|
|
3218
|
-
engine.render(node, built.body, { showChat });
|
|
3219
|
-
} else {
|
|
3220
|
-
const el = entry.colEl;
|
|
3221
|
-
el.innerHTML = '';
|
|
3222
|
-
const built = _buildCardWrapper(node);
|
|
3223
|
-
while (built.wrap.firstChild) el.appendChild(built.wrap.firstChild);
|
|
3224
|
-
nodeMap[id] = { node, colEl: el, bodyEl: built.body };
|
|
3225
|
-
engine.render(node, built.body, { showChat: false });
|
|
3226
|
-
}
|
|
3227
|
-
_updateTokenAvailability();
|
|
3228
|
-
}
|
|
3229
|
-
|
|
3230
|
-
function clear() {
|
|
3231
|
-
_destroyEdges();
|
|
3232
|
-
engine.destroyAll();
|
|
3233
|
-
nodeList.length = 0;
|
|
3234
|
-
Object.keys(nodeMap).forEach(k => delete nodeMap[k]);
|
|
3235
|
-
Object.keys(_positions).forEach(k => delete _positions[k]);
|
|
3236
|
-
root.innerHTML = '';
|
|
3237
|
-
}
|
|
3238
|
-
|
|
3239
|
-
function setMode(m) {
|
|
3240
|
-
if (m !== 'board' && m !== 'canvas') return;
|
|
3241
|
-
mode.current = m;
|
|
3242
|
-
_render();
|
|
3243
|
-
}
|
|
3244
|
-
|
|
3245
|
-
function setDevMode(flag) {
|
|
3246
|
-
devMode.current = !!flag;
|
|
3247
|
-
_render();
|
|
3248
|
-
}
|
|
3249
|
-
|
|
3250
|
-
function destroy() {
|
|
3251
|
-
_destroyEdges();
|
|
3252
|
-
document.body.style.overflow = '';
|
|
3253
|
-
ac.abort();
|
|
3254
|
-
engine.destroyAll();
|
|
3255
|
-
nodeList.length = 0;
|
|
3256
|
-
Object.keys(nodeMap).forEach(k => delete nodeMap[k]);
|
|
3257
|
-
root.innerHTML = '';
|
|
3258
|
-
if (root.parentNode) root.parentNode.removeChild(root);
|
|
3259
|
-
}
|
|
3260
|
-
|
|
3261
|
-
// ---- Init ----
|
|
3262
|
-
if (opts.nodes && opts.nodes.length) {
|
|
3263
|
-
opts.nodes.forEach(n => nodeList.push(n));
|
|
3264
|
-
}
|
|
3265
|
-
_render();
|
|
3266
|
-
|
|
3267
|
-
return {
|
|
3268
|
-
add,
|
|
3269
|
-
remove,
|
|
3270
|
-
reorder,
|
|
3271
|
-
updateNode,
|
|
3272
|
-
clear,
|
|
3273
|
-
setMode,
|
|
3274
|
-
setDevMode,
|
|
3275
|
-
autoLayout,
|
|
3276
|
-
destroy,
|
|
3277
|
-
get mode() { return mode.current; },
|
|
3278
|
-
get devMode() { return devMode.current; },
|
|
3279
|
-
get nodes() { return nodeList.slice(); },
|
|
3280
|
-
get engine() { return engine; },
|
|
3281
|
-
};
|
|
3282
|
-
}
|
|
3283
|
-
|
|
3284
|
-
// ===========================================================================
|
|
3285
|
-
// Board — reactive host. State in, view out. No destructive re-renders.
|
|
3286
|
-
// ===========================================================================
|
|
3287
|
-
|
|
3288
|
-
function Board(engine, containerEl, opts) {
|
|
3289
|
-
opts = opts || {};
|
|
3290
|
-
const initialState = opts.initialState;
|
|
3291
|
-
const getNodeIds = opts.getNodeIds;
|
|
3292
|
-
const selectNode = opts.selectNode;
|
|
3293
|
-
if (typeof getNodeIds !== 'function' || typeof selectNode !== 'function') {
|
|
3294
|
-
throw new Error('LiveCard.Board requires getNodeIds and selectNode functions');
|
|
3295
|
-
}
|
|
3296
|
-
|
|
3297
|
-
let state = initialState;
|
|
3298
|
-
const prevModelsById = {};
|
|
3299
|
-
const prevFingerprintsById = {};
|
|
3300
|
-
|
|
3301
|
-
function _stableStringify(v) {
|
|
3302
|
-
if (v == null || typeof v !== 'object') return JSON.stringify(v);
|
|
3303
|
-
if (Array.isArray(v)) return '[' + v.map(_stableStringify).join(',') + ']';
|
|
3304
|
-
const keys = Object.keys(v).sort();
|
|
3305
|
-
return '{' + keys.map(k => JSON.stringify(k) + ':' + _stableStringify(v[k])).join(',') + '}';
|
|
3306
|
-
}
|
|
3307
|
-
|
|
3308
|
-
function _modelFingerprint(model) {
|
|
3309
|
-
if (!model || typeof model !== 'object') return 'null';
|
|
3310
|
-
return _stableStringify({
|
|
3311
|
-
card: model.card,
|
|
3312
|
-
card_data: model.card_data,
|
|
3313
|
-
requires: model.requires,
|
|
3314
|
-
computed_values: model.computed_values,
|
|
3315
|
-
runtime_state: model.runtime_state,
|
|
3316
|
-
});
|
|
3317
|
-
}
|
|
3318
|
-
|
|
3319
|
-
const initialIds = getNodeIds(state);
|
|
3320
|
-
const initialNodes = initialIds.map(id => {
|
|
3321
|
-
const m = selectNode(state, id);
|
|
3322
|
-
prevModelsById[id] = m;
|
|
3323
|
-
prevFingerprintsById[id] = _modelFingerprint(m);
|
|
3324
|
-
return m;
|
|
3325
|
-
});
|
|
3326
|
-
|
|
3327
|
-
const coreOpts = {};
|
|
3328
|
-
Object.keys(opts).forEach(k => {
|
|
3329
|
-
if (k === 'initialState' || k === 'getNodeIds' || k === 'selectNode' || k === 'nodes') return;
|
|
3330
|
-
coreOpts[k] = opts[k];
|
|
3331
|
-
});
|
|
3332
|
-
coreOpts.nodes = initialNodes;
|
|
3333
|
-
|
|
3334
|
-
const core = BoardCore(engine, containerEl, coreOpts);
|
|
3335
|
-
|
|
3336
|
-
function _changed(prevFingerprint, nextFingerprint) {
|
|
3337
|
-
return prevFingerprint !== nextFingerprint;
|
|
3338
|
-
}
|
|
3339
|
-
|
|
3340
|
-
function setState(nextStateOrUpdater) {
|
|
3341
|
-
const nextState = (typeof nextStateOrUpdater === 'function')
|
|
3342
|
-
? nextStateOrUpdater(state)
|
|
3343
|
-
: nextStateOrUpdater;
|
|
3344
|
-
if (nextState === undefined) return;
|
|
3345
|
-
|
|
3346
|
-
state = nextState;
|
|
3347
|
-
const nextIds = getNodeIds(state);
|
|
3348
|
-
const nextSet = new Set(nextIds);
|
|
3349
|
-
|
|
3350
|
-
// Removals
|
|
3351
|
-
Object.keys(prevModelsById).forEach(id => {
|
|
3352
|
-
if (!nextSet.has(id)) {
|
|
3353
|
-
core.remove(id);
|
|
3354
|
-
delete prevModelsById[id];
|
|
3355
|
-
delete prevFingerprintsById[id];
|
|
3356
|
-
}
|
|
3357
|
-
});
|
|
3358
|
-
|
|
3359
|
-
// Additions and per-node updates
|
|
3360
|
-
nextIds.forEach(id => {
|
|
3361
|
-
const next = selectNode(state, id);
|
|
3362
|
-
const prev = prevModelsById[id];
|
|
3363
|
-
const nextFingerprint = _modelFingerprint(next);
|
|
3364
|
-
const prevFingerprint = prevFingerprintsById[id];
|
|
3365
|
-
if (!prev) {
|
|
3366
|
-
core.add(next);
|
|
3367
|
-
} else if (_changed(prevFingerprint, nextFingerprint)) {
|
|
3368
|
-
core.updateNode(id, next);
|
|
3369
|
-
}
|
|
3370
|
-
prevModelsById[id] = next;
|
|
3371
|
-
prevFingerprintsById[id] = nextFingerprint;
|
|
3372
|
-
});
|
|
3373
|
-
|
|
3374
|
-
// Reorder if id sequence differs
|
|
3375
|
-
const currentOrder = core.nodes.map(n => n.id);
|
|
3376
|
-
const orderDiffers = nextIds.length !== currentOrder.length
|
|
3377
|
-
|| nextIds.some((id, i) => id !== currentOrder[i]);
|
|
3378
|
-
if (orderDiffers) core.reorder(nextIds);
|
|
3379
|
-
}
|
|
3380
|
-
|
|
3381
|
-
function destroy() {
|
|
3382
|
-
Object.keys(prevModelsById).forEach(k => delete prevModelsById[k]);
|
|
3383
|
-
Object.keys(prevFingerprintsById).forEach(k => delete prevFingerprintsById[k]);
|
|
3384
|
-
core.destroy();
|
|
3385
|
-
}
|
|
3386
|
-
|
|
3387
|
-
return {
|
|
3388
|
-
setState,
|
|
3389
|
-
destroy,
|
|
3390
|
-
core,
|
|
3391
|
-
get state() { return state; },
|
|
3392
|
-
};
|
|
3393
|
-
}
|
|
3394
|
-
|
|
3395
|
-
// ===========================================================================
|
|
3396
|
-
// Module export
|
|
3397
|
-
// ===========================================================================
|
|
3398
|
-
|
|
3399
|
-
return { init, Board, BoardCore };
|
|
3400
|
-
})();
|
|
106
|
+
`,document.head.appendChild(i);}function Ne(i){let o=i&&i.card?i.card.view:null;return o&&o.layout&&o.layout.board&&o.layout.board.col?o.layout.board.col:P}function te(){let i=I.positions||{};R.forEach((o,h)=>{if(!V[o.id])if(i[o.id])V[o.id]=Object.assign({},i[o.id]);else if(o.card&&o.card.view&&o.card.view.layout&&o.card.view.layout.canvas&&o.card.view.layout.canvas.x!=null)V[o.id]=Object.assign({},o.card.view.layout.canvas);else {let y=h%4,N=Math.floor(h/4);V[o.id]={x:y*c.gapX+c.padX,y:N*c.gapY+c.padY,w:c.defaultW};}});}function oe(i){return i&&i.card&&Array.isArray(i.card.requires)?i.card.requires:[]}function xe(i){return !i||!i.card?[i?i.id:""]:Array.isArray(i.card.provides)&&i.card.provides.length>0?i.card.provides.map(function(o){return typeof o=="string"?o:o.bindTo||o}):[i.id]}function ce(){var i={};return R.forEach(function(o){xe(o).forEach(function(h){i[h]=o.id;});}),i}function me(i,o){var h=[],y={};return oe(i).forEach(function(N){var E=o[N];E&&!y[E]&&(y[E]=true,h.push(E));}),h}function Ae(i){let o=document.createElement("div");o.className="modal d-block",o.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;";let h=document.createElement("div");h.className="modal-dialog",h.style.cssText="width: 92%; max-width: 980px; max-height: 88vh; overflow: auto;";let y=document.createElement("div");y.className="modal-content";let N=document.createElement("div");N.className="modal-header",N.innerHTML=`<h5 class="modal-title">Card Inspector: ${L(i.card&&i.card.meta&&i.card.meta.title||i.id)}</h5><button type="button" class="btn-close" aria-label="Close"></button>`;let E=function(){o.remove();};N.querySelector(".btn-close").addEventListener("click",E);let g=document.createElement("div");g.className="modal-body",g.style.cssText="max-height: 64vh; overflow-y: auto;";let A=document.createElement("div");A.className="mb-4",A.innerHTML='<h6 class="fw-semibold mb-2">Card Definition (Read-only)</h6>';let e=i&&i.card?i.card:{};A.innerHTML+=`<pre style="background: #f5f5f5; padding: 10px; border-radius: 4px; overflow-x: auto; font-size: 12px; white-space: pre-wrap; word-wrap: break-word;">${L(JSON.stringify(e,null,2))}</pre>`,g.appendChild(A);let t=document.createElement("div");t.className="mb-4",t.innerHTML='<h6 class="fw-semibold mb-2">Computed Values (Read-only)</h6>';let a=i.computed_values||{};t.innerHTML+=`<pre style="background: #f5f5f5; padding: 10px; border-radius: 4px; overflow-x: auto; font-size: 12px; white-space: pre-wrap; word-wrap: break-word;">${L(JSON.stringify(a,null,2))}</pre>`,g.appendChild(t);let r=document.createElement("div");r.className="mb-4",r.innerHTML='<h6 class="fw-semibold mb-2">Requires (Read-only)</h6>';let p=i.requires||{};r.innerHTML+=`<pre style="background: #f5f5f5; padding: 10px; border-radius: 4px; overflow-x: auto; font-size: 12px; white-space: pre-wrap; word-wrap: break-word;">${L(JSON.stringify(p,null,2))}</pre>`,g.appendChild(r);let n=document.createElement("div");n.className="mb-2",n.innerHTML='<h6 class="fw-semibold mb-2">Runtime Status (Read-only)</h6>';let l={status:i.card_data&&i.card_data.status,lastRun:i.card_data&&i.card_data.lastRun,error:i.card_data&&i.card_data.error};n.innerHTML+=`<pre style="background: #f5f5f5; padding: 10px; border-radius: 4px; overflow-x: auto; font-size: 12px; white-space: pre-wrap; word-wrap: break-word;">${L(JSON.stringify(l,null,2))}</pre>`,g.appendChild(n);let s=document.createElement("div");s.className="modal-footer";let d=document.createElement("button");d.type="button",d.className="btn btn-secondary",d.textContent="Close",d.addEventListener("click",E),s.appendChild(d),y.appendChild(N),y.appendChild(g),y.appendChild(s),h.appendChild(y),o.appendChild(h),document.body.appendChild(o);}function we(i){let o=document.createElement("div"),h=i&&i.card?i.card:{},y=h.meta&&h.meta.simulation===true,N=h.meta&&h.meta._gandalfCard===true,E=i&&i.runtime_state&&i.runtime_state.task_status==="running",g=y?" lc-simulation-card":N?" lc-gandalf-card":"";o.className="card shadow-sm h-100"+g+(E?" lc-running":"");let A=document.createElement("div");A.className="card-header d-flex align-items-center gap-2 py-2";let e=h.meta&&h.meta.title||i.id,t=h.meta&&h.meta.tags||[],a="";if(h.source_defs&&h.source_defs.length&&!h.view){var r=h.source_defs[0]||{};a='<span class="badge bg-info text-dark ms-auto">'+L(r.kind||"source")+"</span>";}else t.length&&(a=t.map(s=>'<span class="badge bg-secondary ms-1">'+L(s)+"</span>").join(""));if(A.innerHTML='<strong class="small">'+L(e)+"</strong>"+a,N){let s=document.createElement("span");s.className="lc-gandalf-caret",s.title="Collapse / expand",s.innerHTML='<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"/></svg>',A.appendChild(s);let d="lc-gandalf-collapsed:"+(i.id||e);sessionStorage.getItem(d)==="1"&&(o.classList.add("lc-collapsed"),A.dataset.gandalfCollapsed="1"),s.addEventListener("click",function(u){u.stopPropagation();let v=s.closest(".lc-gandalf-card")||o;v.classList.toggle("lc-collapsed"),sessionStorage.setItem(d,v.classList.contains("lc-collapsed")?"1":"0");}),s.addEventListener("pointerdown",u=>u.stopPropagation());}if(y){let s=document.createElement("span");s.className="d-inline-flex align-items-center gap-1 ms-auto";let d=document.createElement("button");d.className="btn btn-sm btn-outline-success lc-sim-pin",d.style.cssText="padding: 2px 6px;",d.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="M12 17v5"/><path d="M9 2h6l-1 7h-4L9 2z"/><path d="M6 17h12l-2-4H8L6 17z"/></svg>',d.title="Pin this simulation card",d.dataset.nodeId=i.id;let u=document.createElement("button");u.className="btn btn-sm btn-outline-danger lc-sim-discard",u.style.cssText="padding: 2px 6px;",u.innerHTML='<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>',u.title="Discard this simulation card",u.dataset.nodeId=i.id,s.appendChild(d),s.appendChild(u),A.appendChild(s);}if(J.current){let s=document.createElement("button");s.className="btn btn-sm btn-outline-secondary",s.style.cssText="padding: 2px 6px;"+(y?"":" margin-left: auto;"),s.innerHTML="</>",s.title="Inspect card data",s.addEventListener("click",function(d){d.stopPropagation(),Ae(i);}),A.appendChild(s);}let p=document.createElement("div");p.className="card-body p-2";let n=h.requires&&Array.isArray(h.requires)?h.requires:[],l=Array.isArray(h.provides)&&h.provides.length?h.provides.map(function(s){return typeof s=="string"?s:s.bindTo||s}):[i.id];if(n.length){let s=document.createElement("div");s.className="lc-token-row lc-token-row-requires",n.forEach(function(d){let u=document.createElement("span");u.className="lc-token-gem lc-token-gem-requires",u.dataset.token=d,u.title=d,s.appendChild(u);}),o.appendChild(s);}if(o.appendChild(A),o.appendChild(p),l.length){let s=document.createElement("div");s.className="lc-token-row lc-token-row-provides",l.forEach(function(d){let u=document.createElement("span");u.className="lc-token-gem lc-token-gem-provides",u.dataset.token=d,u.title=d,s.appendChild(u);}),o.appendChild(s);}return {wrap:o,header:A,body:p}}function Ee(i){let o=document.createElement("div");o.className="lc-source-node";let h=i.card_data&&i.card_data.status||"fresh",y=i&&i.card?i.card:{},N=y.meta&&y.meta.title||i.id,E=y.source_defs&&y.source_defs[0]&&y.source_defs[0].kind||"source";return o.innerHTML=`<div class="lc-source-pill shadow-sm">
|
|
107
|
+
${Ke(h)}
|
|
108
|
+
<span class="fw-medium">${L(N)}</span>
|
|
109
|
+
<span class="badge bg-info text-dark">${L(E)}</span>
|
|
110
|
+
</div>`,o}function Ce(){var i=100,o=0,h=0;G.querySelectorAll(".lc-canvas-card,.lc-source-node").forEach(function(y){var N=y.offsetLeft+y.offsetWidth,E=y.offsetTop+y.offsetHeight;N>o&&(o=N),E>h&&(h=E);}),G.style.width=o+i+"px",G.style.height=h+i+"px";}function He(){ve(),document.body.style.overflow="",ae.innerHTML="",ae.appendChild(B),B.innerHTML="";let i=R.filter(o=>o.card&&o.card.view).slice();i.sort((o,h)=>{let y=o.card&&o.card.view&&o.card.view.layout&&o.card.view.layout.board&&o.card.view.layout.board.order||0,N=h.card&&h.card.view&&h.card.view.layout&&h.card.view.layout.board&&h.card.view.layout.board.order||0;return y-N}),i.forEach(o=>{let h=document.createElement("div");h.className="col-12 col-md-"+Ne(o),h.dataset.nodeId=o.id;let{wrap:y,body:N}=we(o);h.appendChild(y),B.appendChild(h),D[o.id]={node:o,colEl:h,bodyEl:N},T.render(o,N,{showChat:ie});}),_e();}function Le(){G.style.transform=`translate(${c.panX}px,${c.panY}px) scale(${c.zoom})`;}function _e(){var i=ce(),o={};R.forEach(function(y){var N=y.card_data||y.card&&y.card.card_data,E=y.computed_values,g=N&&N.status,A=N&&Object.keys(N).length>0||E&&Object.keys(E).length>0;o[y.id]=A||g==="fresh"||g==="completed";});var h=ae.querySelectorAll(".lc-token-gem");h.forEach(function(y){var N=y.dataset.token;if(N){if(y.classList.contains("lc-token-gem-provides")){var E=y.closest("[data-node-id]"),g=E&&E.dataset.nodeId;y.classList.toggle("lc-token-available",!!(g&&o[g]));}else if(y.classList.contains("lc-token-gem-requires")){var A=i[N];y.classList.toggle("lc-token-available",!!(A&&o[A]));}}});}function ve(){X.forEach(function(i){try{i.remove();}catch{}}),X.length=0;}function ue(){X.forEach(function(i){try{i.position();}catch{}});}function ye(){if(ve(),le.querySelectorAll("line,path").forEach(function(o){o.remove();}),!!c.edges){var i=ce();R.forEach(function(o){var h=D[o.id];!h||!h.colEl||oe(o).forEach(function(y){var N=i[y];if(N){var E=D[N];if(!(!E||!E.colEl)){var g=E.colEl.querySelector('.lc-token-gem-provides[data-token="'+y+'"]'),A=h.colEl.querySelector('.lc-token-gem-requires[data-token="'+y+'"]'),e,t,a,r,p=G.getBoundingClientRect();if(g){var n=g.getBoundingClientRect();e=(n.left+n.width/2-p.left)/c.zoom,t=(n.bottom-p.top)/c.zoom;}else {var l=E.colEl;e=l.offsetLeft+l.offsetWidth/2,t=l.offsetTop+l.offsetHeight;}if(A){var s=A.getBoundingClientRect();a=(s.left+s.width/2-p.left)/c.zoom,r=(s.top-p.top)/c.zoom;}else {var d=h.colEl;a=d.offsetLeft+d.offsetWidth/2,r=d.offsetTop;}var u=a-e,v=r-t,m=Math.sqrt(u*u+v*v),C=Math.max(40,Math.min(m*.4,120)),w=Math.abs(u),S=Math.abs(v),M,$,j,_;if(S>w*.4)M=e,$=t+C,j=a,_=r-C;else {var H=u>0?1:-1;M=e+H*C,$=t+C*.5,j=a-H*C,_=r-C*.5;}var ne="M "+e+" "+t+" C "+M+" "+$+", "+j+" "+_+", "+a+" "+r,Q=document.createElementNS("http://www.w3.org/2000/svg","path");Q.setAttribute("d",ne),Q.setAttribute("fill","none"),Q.setAttribute("marker-end","url(#lc-arrow)"),Q.classList.add("lc-edge-path"),le.appendChild(Q);}}});});}}function $e(i,o){let h,y,N,E,g=false;i.addEventListener("pointerdown",A=>{A.button===0&&(A.target.closest("input,textarea,select,button,a,.form-check-input")||(g=true,i.classList.add("lc-dragging"),i.setPointerCapture(A.pointerId),h=A.clientX,y=A.clientY,N=i.offsetLeft,E=i.offsetTop,A.preventDefault()));},{signal:Z}),i.addEventListener("pointermove",A=>{if(!g)return;let e=(A.clientX-h)/c.zoom,t=(A.clientY-y)/c.zoom;i.style.left=N+e+"px",i.style.top=E+t+"px",X.length?ue():ye();},{signal:Z}),i.addEventListener("pointerup",()=>{if(!g)return;g=false,i.classList.remove("lc-dragging");let A=i.offsetLeft,e=i.offsetTop;c.snap>1&&(A=Math.round(A/c.snap)*c.snap,e=Math.round(e/c.snap)*c.snap),i.style.left=A+"px",i.style.top=e+"px",V[o.id]=Object.assign(V[o.id]||{},{x:A,y:e}),o.card&&o.card.view&&(o.card.view.layout||(o.card.view.layout={}),o.card.view.layout.canvas||(o.card.view.layout.canvas={}),o.card.view.layout.canvas.x=A,o.card.view.layout.canvas.y=e),T.notify(o.id),Ce(),X.length?ue():ye();},{signal:Z});}function Ie(i,o){let h=document.createElement("div");h.className="lc-resize-handle",i.appendChild(h),i.style.overflow="visible";let y=false,N,E,g,A;h.addEventListener("pointerdown",function(e){e.button===0&&(e.stopPropagation(),e.preventDefault(),y=true,i.classList.add("lc-resizing"),h.setPointerCapture(e.pointerId),N=e.clientX,E=e.clientY,g=i.offsetWidth,A=i.offsetHeight);},{signal:Z}),h.addEventListener("pointermove",function(e){if(!y)return;let t=(e.clientX-N)/c.zoom,a=(e.clientY-E)/c.zoom,r=Math.max(c.minWidth,g+t),p=Math.max(80,A+a);i.style.width=r+"px",i.style.height=p+"px",X.length?ue():ye();},{signal:Z}),h.addEventListener("pointerup",function(){if(!y)return;y=false,i.classList.remove("lc-resizing");let e=i.offsetWidth,t=i.offsetHeight,a=c.snap>1?Math.round(e/c.snap)*c.snap:e,r=c.snap>1?Math.round(t/c.snap)*c.snap:t;i.style.width=a+"px",i.style.height=r+"px",V[o.id]=Object.assign(V[o.id]||{},{w:a,h:r}),o.card&&o.card.view&&(o.card.view.layout||(o.card.view.layout={}),o.card.view.layout.canvas||(o.card.view.layout.canvas={}),o.card.view.layout.canvas.w=a,o.card.view.layout.canvas.h=r),T.notify(o.id),Ce(),X.length?ue():ye();},{signal:Z});}function ze(){ve(),document.body.style.overflow="hidden",ae.innerHTML="",ae.appendChild(U);var i=U.getBoundingClientRect().top;U.style.height=F.height||"calc(100vh - "+i+"px)",G.querySelectorAll(".lc-canvas-card,.lc-source-node").forEach(g=>g.remove()),le.querySelectorAll("line,path").forEach(function(g){g.remove();}),te(),Le(),R.forEach(g=>{let A=V[g.id]||{x:0,y:0};if((!g.card||!g.card.view)&&g.card&&g.card.source_defs&&g.card.source_defs.length){let e=Ee(g);e.dataset.nodeId=g.id,e.style.left=A.x+"px",e.style.top=A.y+"px",G.appendChild(e),D[g.id]={node:g,colEl:e,bodyEl:null},$e(e,g);}else {let e=document.createElement("div"),t=g.card&&g.card.meta&&g.card.meta.simulation===true,a=g.card&&g.card.meta&&g.card.meta._gandalfCard===true,r=t?" lc-simulation-card":a?" lc-gandalf-card":"";e.className="lc-canvas-card card shadow-sm"+r,e.dataset.nodeId=g.id,e.style.left=A.x+"px",e.style.top=A.y+"px",A.w&&(e.style.width=A.w+"px"),A.h&&(e.style.height=A.h+"px");let{wrap:p,body:n}=we(g);for(;p.firstChild;)e.appendChild(p.firstChild);let l=e.querySelector(".card-header");l&&l.dataset.gandalfCollapsed==="1"&&e.classList.add("lc-collapsed"),G.appendChild(e),D[g.id]={node:g,colEl:e,bodyEl:n},T.render(g,n,{showChat:false}),$e(e,g),Ie(e,g);}}),_e(),requestAnimationFrame(function(){Ce(),ye();}),U.addEventListener("scroll",function(){ue();},{signal:Z,passive:true});let o=false,h,y,N,E;U.addEventListener("pointerdown",g=>{g.target!==U&&g.target!==G||(g.button===1||g.button===0&&g.ctrlKey)&&(o=true,U.setPointerCapture(g.pointerId),h=g.clientX,y=g.clientY,N=c.panX,E=c.panY,g.preventDefault());},{signal:Z}),U.addEventListener("pointermove",g=>{o&&(c.panX=N+(g.clientX-h),c.panY=E+(g.clientY-y),Le(),ue());},{signal:Z}),U.addEventListener("pointerup",()=>{o=false;},{signal:Z}),U.addEventListener("wheel",g=>{if(!g.ctrlKey)return;g.preventDefault();let A=g.deltaY>0?.9:1.1;c.zoom=Math.min(c.zoomMax,Math.max(c.zoomMin,c.zoom*A)),Le(),ue();},{signal:Z,passive:false});}function he(){Y.current==="canvas"?ze():He();}function Re(){let i=ce(),o={},h={};R.forEach(E=>{o[E.id]=[],h[E.id]=0;}),R.forEach(E=>{me(E,i).forEach(g=>{o[E.id]&&o[E.id].push(g);});});let y=true;for(;y;)y=false,R.forEach(E=>{(o[E.id]||[]).forEach(g=>{h[g]!=null&&h[g]+1>h[E.id]&&(h[E.id]=h[g]+1,y=true);});});let N={};R.forEach(E=>{let g=h[E.id]||0;N[g]||(N[g]=0);let A=N[g]++;V[E.id]={x:g*400+40,y:A*300+40,w:V[E.id]&&V[E.id].w||c.defaultW},E.view&&(E.view.layout||(E.view.layout={}),E.view.layout.canvas=Object.assign({},V[E.id]));}),Y.current==="canvas"&&ze();}function Oe(i){D[i.id]||(R.push(i),he());}function Fe(i){T.destroy(i);let o=R.findIndex(h=>h.id===i);o>=0&&R.splice(o,1),delete D[i],delete V[i],he();}function Pe(i){R.length=0,i.forEach(o=>{let h=D[o];h&&R.push(h.node);}),he();}function De(i,o){let h=D[i];if(!h)throw new Error("updateNode: unknown node id "+i);let y=h.node;if(o&&typeof o=="object"&&(o.card!==void 0&&(y.card=o.card),o.card_data!==void 0&&(y.card_data=o.card_data),o.requires!==void 0&&(y.requires=o.requires),o.computed_values!==void 0&&(y.computed_values=o.computed_values),o.runtime_state!==void 0&&(y.runtime_state=o.runtime_state)),T.destroy(i),Y.current==="board"){let N=h.colEl;N.innerHTML="";let E=we(y);N.appendChild(E.wrap),D[i]={node:y,colEl:N,bodyEl:E.body},T.render(y,E.body,{showChat:ie});}else {let N=h.colEl;N.innerHTML="";let E=we(y);for(;E.wrap.firstChild;)N.appendChild(E.wrap.firstChild);D[i]={node:y,colEl:N,bodyEl:E.body},T.render(y,E.body,{showChat:false});}_e();}function Ve(){ve(),T.destroyAll(),R.length=0,Object.keys(D).forEach(i=>delete D[i]),Object.keys(V).forEach(i=>delete V[i]),ae.innerHTML="";}function je(i){i!=="board"&&i!=="canvas"||(Y.current=i,he());}function We(i){J.current=!!i,he();}function Se(){ve(),document.body.style.overflow="",x.abort(),T.destroyAll(),R.length=0,Object.keys(D).forEach(i=>delete D[i]),ae.innerHTML="",ae.parentNode&&ae.parentNode.removeChild(ae);}return I.nodes&&I.nodes.length&&I.nodes.forEach(i=>R.push(i)),he(),{add:Oe,remove:Fe,reorder:Pe,updateNode:De,clear:Ve,setMode:je,setDevMode:We,autoLayout:Re,destroy:Se,get mode(){return Y.current},get devMode(){return J.current},get nodes(){return R.slice()},get engine(){return T}}}function it(T,k,I){I=I||{};let Y=I.initialState,J=I.getNodeIds,R=I.selectNode;if(typeof J!="function"||typeof R!="function")throw new Error("LiveCard.Board requires getNodeIds and selectNode functions");let D=Y,V={},ie={};function P(B){return B==null||typeof B!="object"?JSON.stringify(B):Array.isArray(B)?"["+B.map(P).join(",")+"]":"{"+Object.keys(B).sort().map(G=>JSON.stringify(G)+":"+P(B[G])).join(",")+"}"}function F(B){return !B||typeof B!="object"?"null":P({card:B.card,card_data:B.card_data,requires:B.requires,computed_values:B.computed_values,runtime_state:B.runtime_state})}let x=J(D).map(B=>{let U=R(D,B);return V[B]=U,ie[B]=F(U),U}),Z={};Object.keys(I).forEach(B=>{B==="initialState"||B==="getNodeIds"||B==="selectNode"||B==="nodes"||(Z[B]=I[B]);}),Z.nodes=x;let X=Ge(T,k,Z);function de(B,U){return B!==U}function Te(B){let U=typeof B=="function"?B(D):B;if(U===void 0)return;D=U;let G=J(D),le=new Set(G);Object.keys(V).forEach(te=>{le.has(te)||(X.remove(te),delete V[te],delete ie[te]);}),G.forEach(te=>{let oe=R(D,te),xe=V[te],ce=F(oe),me=ie[te];xe?de(me,ce)&&X.updateNode(te,oe):X.add(oe),V[te]=oe,ie[te]=ce;});let pe=X.nodes.map(te=>te.id);(G.length!==pe.length||G.some((te,oe)=>te!==pe[oe]))&&X.reorder(G);}function ae(){Object.keys(V).forEach(B=>delete V[B]),Object.keys(ie).forEach(B=>delete ie[B]),X.destroy();}return {setState:Te,destroy:ae,core:X,get state(){return D}}}return {init:rt,Board:it,BoardCore:Ge}})(),Ze=lt;typeof globalThis<"u"&&(globalThis.LiveCard=Ze);
|
|
111
|
+
})();//# sourceMappingURL=live-cards.js.map
|
|
112
|
+
//# sourceMappingURL=live-cards.js.map
|