yaml-flow 7.1.0 → 8.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (389) hide show
  1. package/browser/asset-integrity.json +8 -4
  2. package/browser/board-livecards-client.js +1 -1
  3. package/browser/board-livecards-localstorage.js +4 -4
  4. package/browser/live-cards.js +19 -3309
  5. package/cli/board-live-cards-lib-tjYsPt5U.d.ts +321 -0
  6. package/{dist/cli → cli}/browser-api/board-live-cards-browser-adapter.d.ts +3 -5
  7. package/{dist/cli → cli}/browser-api/card-store-browser-api.d.ts +1 -2
  8. package/{dist/cli → cli}/browser-api/card-store-browser-api.js +1 -1
  9. package/cli/execution-interface-ftO1W7Po.d.ts +286 -0
  10. package/{dist/cli → cli}/node/artifacts-store-cli.js +2 -2
  11. package/cli/node/batch-runner-cli.js +3 -0
  12. package/cli/node/board-live-cards-cli.js +15 -0
  13. package/{dist/cli → cli}/node/card-store-cli.js +1 -1
  14. package/{dist/cli → cli}/node/execution-adapter.d.ts +49 -4
  15. package/cli/node/execution-adapter.js +3 -0
  16. package/{dist/cli → cli}/node/fs-board-adapter.d.ts +24 -11
  17. package/cli/node/fs-board-adapter.js +14 -0
  18. package/cli/node/step-machine-cli.d.ts +7 -0
  19. package/cli/node/step-machine-cli.js +5 -0
  20. package/{dist/board-live-cards-public-5n1-syA3.d.cts → cli/types-C2YQXFwo.d.ts} +68 -5
  21. package/examples/board/.demo-setup/run-1778665078572-3466-a8ay4k/board-default/gandalf-runtime/.config/card-store-ref.json +1 -0
  22. package/examples/board/.demo-setup/run-1778665078572-3466-a8ay4k/board-default/gandalf-runtime/.config/chat-handler.json +1 -0
  23. package/examples/board/.demo-setup/run-1778665078572-3466-a8ay4k/board-default/gandalf-runtime/.config/outputs-store-ref.json +1 -0
  24. package/examples/board/.demo-setup/run-1778665078572-3466-a8ay4k/board-default/gandalf-runtime/.config/task-executor.json +1 -0
  25. package/examples/board/.demo-setup/run-1778665078572-3466-a8ay4k/board-default/gandalf-runtime/.state-snapshot/board/graph.json +29 -0
  26. package/examples/board/.demo-setup/run-1778665078572-3466-a8ay4k/board-default/gandalf-runtime/.state-snapshot/board/lastJournalProcessedId.json +1 -0
  27. package/examples/board/.demo-setup/run-1778665078572-3466-a8ay4k/board-default/gandalf-runtime-out/.outputs/status.json +25 -0
  28. package/examples/board/.demo-setup/run-1778665078572-3466-a8ay4k/board-default/runtime-out/.outputs/cards/card-market-prices/computed_values.json +67 -0
  29. package/examples/board/.demo-setup/run-1778665078572-3466-a8ay4k/board-default/runtime-out/.outputs/cards/card-portfolio/computed_values.json +1 -0
  30. package/examples/board/.demo-setup/run-1778665078572-3466-a8ay4k/board-default/runtime-out/.outputs/cards/card-portfolio-value/computed_values.json +52 -0
  31. package/examples/board/.demo-setup/run-1778665078572-3466-a8ay4k/board-default/runtime-out/.outputs/data-objects/holdings.json +22 -0
  32. package/examples/board/.demo-setup/run-1778665078572-3466-a8ay4k/board-default/runtime-out/.outputs/data-objects/positions.json +46 -0
  33. package/examples/board/.demo-setup/run-1778665078572-3466-a8ay4k/board-default/runtime-out/.outputs/data-objects/quotes.json +35 -0
  34. package/examples/board/.demo-setup/run-1778665078572-3466-a8ay4k/board-default/runtime-out/.outputs/status.json +113 -0
  35. package/examples/{example-board → board}/demo-server-config.json +0 -1
  36. package/examples/{example-board → board}/demo-server.js +23 -48
  37. package/examples/{example-board → board}/demo-shell-with-server.html +3 -3
  38. package/examples/{example-board → board}/demo-task-executor.js +71 -24
  39. package/examples/board/gandalf-cards/card-source-kinds.json +36 -0
  40. package/examples/board/gandalf-cards/cards/_index.json +7 -0
  41. package/examples/board/gandalf-cards/cards/card-source-kinds.json +64 -0
  42. package/examples/board/source-def-flows/copilot.flow.json +33 -0
  43. package/examples/board/source-def-flows/mock.flow.json +35 -0
  44. package/examples/board/source-def-flows/url-list.flow.json +33 -0
  45. package/examples/board/source-def-flows/url.flow.json +33 -0
  46. package/examples/board/source-def-flows/workiq.flow.json +34 -0
  47. package/examples/board/source-def-handlers/copilot-source-handler.js +141 -0
  48. package/examples/board/source-def-handlers/http-source-handler.js +145 -0
  49. package/examples/board/source_def_flows.json +249 -0
  50. package/examples/board/test/demo-http-test.js +317 -0
  51. package/examples/{example-board → board-local}/demo-shell-localstorage.html +4 -4
  52. package/examples/{browser/boards/portfolio-tracker → portfolio-tracker/handlers}/portfolio-tracker-fetch-prices.js +1 -1
  53. package/examples/{browser/boards/portfolio-tracker/portfolio-tracker-public.js → portfolio-tracker/portfolio-tracker.js} +11 -14
  54. package/examples/{browser/boards/portfolio-tracker → portfolio-tracker/test}/portfolio-t4.js +32 -50
  55. package/lib/artifacts-store-lib-public-BABrgFkV.d.ts +119 -0
  56. package/lib/artifacts-store-lib-public-DGa8BpJT.d.cts +119 -0
  57. package/lib/artifacts-store-public.cjs +2 -0
  58. package/lib/artifacts-store-public.d.cts +5 -0
  59. package/lib/artifacts-store-public.d.ts +5 -0
  60. package/lib/artifacts-store-public.js +2 -0
  61. package/lib/board-live-cards-node.cjs +14 -0
  62. package/lib/board-live-cards-node.d.cts +178 -0
  63. package/lib/board-live-cards-node.d.ts +178 -0
  64. package/lib/board-live-cards-node.js +14 -0
  65. package/lib/board-live-cards-public-BnmRAbQV.d.cts +383 -0
  66. package/{dist/board-live-cards-public-CK_J8uv0.d.ts → lib/board-live-cards-public-CsmYrvpd.d.ts} +142 -76
  67. package/lib/board-live-cards-public.cjs +3 -0
  68. package/lib/board-live-cards-public.d.cts +4 -0
  69. package/lib/board-live-cards-public.d.ts +4 -0
  70. package/lib/board-live-cards-public.js +3 -0
  71. package/lib/board-live-cards-server-runtime.cjs +9 -0
  72. package/lib/board-live-cards-server-runtime.d.cts +6 -0
  73. package/lib/board-live-cards-server-runtime.d.ts +6 -0
  74. package/lib/board-live-cards-server-runtime.js +9 -0
  75. package/lib/board-livegraph-runtime/index.cjs +3 -0
  76. package/{dist → lib}/board-livegraph-runtime/index.d.cts +1 -2
  77. package/{dist → lib}/board-livegraph-runtime/index.d.ts +1 -2
  78. package/lib/board-livegraph-runtime/index.js +3 -0
  79. package/{dist/storage-refs.cjs → lib/board-worker-adapter.cjs} +2 -2
  80. package/{dist/storage-refs.d.cts → lib/board-worker-adapter.d.cts} +4 -3
  81. package/{dist/storage-refs.d.ts → lib/board-worker-adapter.d.ts} +4 -3
  82. package/{dist/storage-refs.js → lib/board-worker-adapter.js} +2 -2
  83. package/{dist → lib}/card-compute/index.cjs +1 -1
  84. package/{dist → lib}/card-compute/index.js +1 -1
  85. package/lib/card-store-public.cjs +2 -0
  86. package/lib/card-store-public.d.cts +61 -0
  87. package/lib/card-store-public.d.ts +61 -0
  88. package/lib/card-store-public.js +2 -0
  89. package/lib/card-validation.cjs +10 -0
  90. package/lib/card-validation.d.cts +35 -0
  91. package/lib/card-validation.d.ts +35 -0
  92. package/lib/card-validation.js +10 -0
  93. package/{dist/constants-oCEbNpul.d.ts → lib/constants-BPVLb3Es.d.ts} +1 -1
  94. package/{dist/constants-BzZUyYlp.d.cts → lib/constants-DXxsRN9y.d.cts} +1 -1
  95. package/{dist → lib}/continuous-event-graph/index.cjs +2 -2
  96. package/{dist → lib}/continuous-event-graph/index.d.cts +3 -5
  97. package/{dist → lib}/continuous-event-graph/index.d.ts +3 -5
  98. package/{dist → lib}/continuous-event-graph/index.js +2 -2
  99. package/{dist → lib}/event-graph/index.d.cts +2 -2
  100. package/{dist → lib}/event-graph/index.d.ts +2 -2
  101. package/lib/execution-refs.cjs +3 -0
  102. package/{dist → lib}/execution-refs.d.cts +33 -12
  103. package/{dist → lib}/execution-refs.d.ts +33 -12
  104. package/lib/execution-refs.js +3 -0
  105. package/lib/index.cjs +25 -0
  106. package/{dist → lib}/index.d.cts +7 -8
  107. package/{dist → lib}/index.d.ts +7 -8
  108. package/lib/index.js +25 -0
  109. package/{dist/live-cards-bridge-BXbVTsna.d.cts → lib/live-cards-bridge-DC_ZU0eS.d.ts} +134 -3
  110. package/{dist/live-cards-bridge-Ds28XR15.d.ts → lib/live-cards-bridge-b25aAVvE.d.cts} +134 -3
  111. package/lib/loader-CuuLjxVA.d.cts +42 -0
  112. package/lib/loader-Zborm2pq.d.ts +42 -0
  113. package/lib/server-runtime/index.cjs +9 -0
  114. package/{dist → lib}/server-runtime/index.d.cts +4 -4
  115. package/{dist → lib}/server-runtime/index.d.ts +4 -4
  116. package/lib/server-runtime/index.js +9 -0
  117. package/lib/step-machine/index.d.cts +64 -0
  118. package/lib/step-machine/index.d.ts +64 -0
  119. package/lib/step-machine-public/index.cjs +5 -0
  120. package/{dist → lib}/step-machine-public/index.d.cts +14 -1
  121. package/{dist → lib}/step-machine-public/index.d.ts +14 -1
  122. package/lib/step-machine-public/index.js +5 -0
  123. package/lib/storage-interface-BhAON-gW.d.cts +84 -0
  124. package/lib/storage-interface-BhAON-gW.d.ts +84 -0
  125. package/lib/stores/index.cjs +3 -0
  126. package/lib/stores/index.d.cts +4 -0
  127. package/lib/stores/index.d.ts +4 -0
  128. package/lib/stores/index.js +3 -0
  129. package/lib/stores/kv.cjs +3 -0
  130. package/lib/stores/kv.d.cts +32 -0
  131. package/lib/stores/kv.d.ts +32 -0
  132. package/lib/stores/kv.js +3 -0
  133. package/{dist → lib}/stores/memory.d.cts +1 -1
  134. package/{dist → lib}/stores/memory.d.ts +1 -1
  135. package/{dist/types-HGDTWIun.d.ts → lib/types-CBxkYuLY.d.ts} +2 -1
  136. package/{dist/types-ycun84cq.d.cts → lib/types-DQ1bKuB1.d.cts} +11 -0
  137. package/{dist/types-ycun84cq.d.ts → lib/types-DQ1bKuB1.d.ts} +11 -0
  138. package/{dist/types-CU3DjTKL.d.cts → lib/types-DkFvgxwq.d.cts} +2 -1
  139. package/package.json +79 -119
  140. package/board-live-cards-cli.js +0 -37
  141. package/browser/board-livecards-client.js.map +0 -1
  142. package/browser/board-livecards-localstorage.js.map +0 -1
  143. package/browser/board-livegraph-engine.js +0 -3
  144. package/browser/board-livegraph-engine.js.map +0 -1
  145. package/browser/card-compute.js +0 -266
  146. package/browser/compute-jsonata.js.map +0 -1
  147. package/card-store.js +0 -37
  148. package/dist/batch/index.cjs.map +0 -1
  149. package/dist/batch/index.js.map +0 -1
  150. package/dist/board-live-cards-lib-Bg6EvCo5.d.cts +0 -136
  151. package/dist/board-live-cards-lib-jM2uYG1v.d.ts +0 -136
  152. package/dist/board-livegraph-runtime/index.cjs +0 -3
  153. package/dist/board-livegraph-runtime/index.cjs.map +0 -1
  154. package/dist/board-livegraph-runtime/index.js +0 -3
  155. package/dist/board-livegraph-runtime/index.js.map +0 -1
  156. package/dist/card-compute/index.cjs.map +0 -1
  157. package/dist/card-compute/index.js.map +0 -1
  158. package/dist/cli/browser-api/board-live-cards-browser-adapter.cjs +0 -3
  159. package/dist/cli/browser-api/board-live-cards-browser-adapter.cjs.map +0 -1
  160. package/dist/cli/browser-api/board-live-cards-browser-adapter.d.cts +0 -37
  161. package/dist/cli/browser-api/board-live-cards-browser-adapter.js.map +0 -1
  162. package/dist/cli/browser-api/card-store-browser-api.cjs +0 -2
  163. package/dist/cli/browser-api/card-store-browser-api.cjs.map +0 -1
  164. package/dist/cli/browser-api/card-store-browser-api.d.cts +0 -26
  165. package/dist/cli/browser-api/card-store-browser-api.js.map +0 -1
  166. package/dist/cli/node/artifacts-store-cli.cjs +0 -11
  167. package/dist/cli/node/artifacts-store-cli.cjs.map +0 -1
  168. package/dist/cli/node/artifacts-store-cli.d.cts +0 -8
  169. package/dist/cli/node/artifacts-store-cli.js.map +0 -1
  170. package/dist/cli/node/board-live-cards-cli.cjs +0 -15
  171. package/dist/cli/node/board-live-cards-cli.cjs.map +0 -1
  172. package/dist/cli/node/board-live-cards-cli.d.cts +0 -20
  173. package/dist/cli/node/board-live-cards-cli.js +0 -15
  174. package/dist/cli/node/board-live-cards-cli.js.map +0 -1
  175. package/dist/cli/node/card-store-cli.cjs +0 -8
  176. package/dist/cli/node/card-store-cli.cjs.map +0 -1
  177. package/dist/cli/node/card-store-cli.d.cts +0 -15
  178. package/dist/cli/node/card-store-cli.js.map +0 -1
  179. package/dist/cli/node/execution-adapter.cjs +0 -3
  180. package/dist/cli/node/execution-adapter.cjs.map +0 -1
  181. package/dist/cli/node/execution-adapter.d.cts +0 -174
  182. package/dist/cli/node/execution-adapter.js +0 -3
  183. package/dist/cli/node/execution-adapter.js.map +0 -1
  184. package/dist/cli/node/fs-board-adapter.cjs +0 -14
  185. package/dist/cli/node/fs-board-adapter.cjs.map +0 -1
  186. package/dist/cli/node/fs-board-adapter.d.cts +0 -204
  187. package/dist/cli/node/fs-board-adapter.js +0 -14
  188. package/dist/cli/node/fs-board-adapter.js.map +0 -1
  189. package/dist/cli/node/source-cli-task-executor.cjs +0 -11
  190. package/dist/cli/node/source-cli-task-executor.cjs.map +0 -1
  191. package/dist/cli/node/source-cli-task-executor.js.map +0 -1
  192. package/dist/config/index.cjs.map +0 -1
  193. package/dist/config/index.js.map +0 -1
  194. package/dist/continuous-event-graph/index.cjs.map +0 -1
  195. package/dist/continuous-event-graph/index.js.map +0 -1
  196. package/dist/event-graph/index.cjs.map +0 -1
  197. package/dist/event-graph/index.js.map +0 -1
  198. package/dist/execution-refs.cjs +0 -3
  199. package/dist/execution-refs.cjs.map +0 -1
  200. package/dist/execution-refs.js +0 -3
  201. package/dist/execution-refs.js.map +0 -1
  202. package/dist/index.cjs +0 -30
  203. package/dist/index.cjs.map +0 -1
  204. package/dist/index.js +0 -30
  205. package/dist/index.js.map +0 -1
  206. package/dist/inference/index.cjs +0 -7
  207. package/dist/inference/index.cjs.map +0 -1
  208. package/dist/inference/index.d.cts +0 -229
  209. package/dist/inference/index.d.ts +0 -229
  210. package/dist/inference/index.js +0 -7
  211. package/dist/inference/index.js.map +0 -1
  212. package/dist/server-runtime/index.cjs +0 -9
  213. package/dist/server-runtime/index.cjs.map +0 -1
  214. package/dist/server-runtime/index.js +0 -9
  215. package/dist/server-runtime/index.js.map +0 -1
  216. package/dist/step-machine/index.cjs.map +0 -1
  217. package/dist/step-machine/index.d.cts +0 -102
  218. package/dist/step-machine/index.d.ts +0 -102
  219. package/dist/step-machine/index.js.map +0 -1
  220. package/dist/step-machine-public/index.cjs +0 -3
  221. package/dist/step-machine-public/index.cjs.map +0 -1
  222. package/dist/step-machine-public/index.js +0 -3
  223. package/dist/step-machine-public/index.js.map +0 -1
  224. package/dist/storage-refs.cjs.map +0 -1
  225. package/dist/storage-refs.js.map +0 -1
  226. package/dist/stores/file.cjs +0 -2
  227. package/dist/stores/file.cjs.map +0 -1
  228. package/dist/stores/file.d.cts +0 -36
  229. package/dist/stores/file.d.ts +0 -36
  230. package/dist/stores/file.js +0 -2
  231. package/dist/stores/file.js.map +0 -1
  232. package/dist/stores/index.cjs +0 -2
  233. package/dist/stores/index.cjs.map +0 -1
  234. package/dist/stores/index.d.cts +0 -4
  235. package/dist/stores/index.d.ts +0 -4
  236. package/dist/stores/index.js +0 -2
  237. package/dist/stores/index.js.map +0 -1
  238. package/dist/stores/localStorage.cjs +0 -2
  239. package/dist/stores/localStorage.cjs.map +0 -1
  240. package/dist/stores/localStorage.d.cts +0 -34
  241. package/dist/stores/localStorage.d.ts +0 -34
  242. package/dist/stores/localStorage.js +0 -2
  243. package/dist/stores/localStorage.js.map +0 -1
  244. package/dist/stores/memory.cjs.map +0 -1
  245. package/dist/stores/memory.js.map +0 -1
  246. package/dist/types-CHSdoAAA.d.cts +0 -135
  247. package/dist/types-CoW0gQl3.d.ts +0 -135
  248. package/examples/browser/boards/portfolio-tracker/portfolio-tracker-fetch-prices.py +0 -201
  249. package/examples/browser/boards/portfolio-tracker/portfolio-tracker-http-test.js +0 -370
  250. package/examples/browser/boards/portfolio-tracker/portfolio-tracker-http-test.py +0 -398
  251. package/examples/browser/boards/portfolio-tracker/portfolio-tracker-inference-adapter.js +0 -196
  252. package/examples/browser/boards/portfolio-tracker/portfolio-tracker-server.js +0 -300
  253. package/examples/browser/boards/portfolio-tracker/portfolio-tracker-server.py +0 -617
  254. package/examples/browser/boards/portfolio-tracker/portfolio-tracker.py +0 -366
  255. package/examples/browser/livecards-browser/index.html +0 -41
  256. package/examples/browser/step-machine-browser/index.html +0 -367
  257. package/examples/cli/step-machine-cli/portfolio-tracker/--base-ref/.runtime-out +0 -1
  258. package/examples/cli/step-machine-cli/portfolio-tracker/--base-ref/board-graph.json +0 -32
  259. package/examples/cli/step-machine-cli/portfolio-tracker/cards/holdings-table.json +0 -22
  260. package/examples/cli/step-machine-cli/portfolio-tracker/cards/portfolio-form.json +0 -43
  261. package/examples/cli/step-machine-cli/portfolio-tracker/cards/portfolio-value.json +0 -15
  262. package/examples/cli/step-machine-cli/portfolio-tracker/cards/price-fetch.json +0 -15
  263. package/examples/cli/step-machine-cli/portfolio-tracker/fetch-prices.js +0 -48
  264. package/examples/cli/step-machine-cli/portfolio-tracker/handlers/_board-cli.js +0 -125
  265. package/examples/cli/step-machine-cli/portfolio-tracker/handlers/add-cards-cli.js +0 -32
  266. package/examples/cli/step-machine-cli/portfolio-tracker/handlers/init-board-cli.js +0 -26
  267. package/examples/cli/step-machine-cli/portfolio-tracker/handlers/poll-status-cli.js +0 -49
  268. package/examples/cli/step-machine-cli/portfolio-tracker/handlers/reset-board-dir-cli.js +0 -25
  269. package/examples/cli/step-machine-cli/portfolio-tracker/handlers/retrigger-cli.js +0 -23
  270. package/examples/cli/step-machine-cli/portfolio-tracker/handlers/status-cli.js +0 -21
  271. package/examples/cli/step-machine-cli/portfolio-tracker/handlers/update-holdings-cli.js +0 -38
  272. package/examples/cli/step-machine-cli/portfolio-tracker/handlers/wait-completed-cli.js +0 -48
  273. package/examples/cli/step-machine-cli/portfolio-tracker/handlers/write-prices-cli.js +0 -31
  274. package/examples/cli/step-machine-cli/portfolio-tracker/handlers-py/_board_pycli.py +0 -107
  275. package/examples/cli/step-machine-cli/portfolio-tracker/handlers-py/add-cards.py +0 -51
  276. package/examples/cli/step-machine-cli/portfolio-tracker/handlers-py/init-board.py +0 -45
  277. package/examples/cli/step-machine-cli/portfolio-tracker/handlers-py/poll-status.py +0 -71
  278. package/examples/cli/step-machine-cli/portfolio-tracker/handlers-py/reset-board-dir.py +0 -36
  279. package/examples/cli/step-machine-cli/portfolio-tracker/inline-python-demo.flow.yaml +0 -26
  280. package/examples/cli/step-machine-cli/portfolio-tracker/inline-python-handlers.py +0 -39
  281. package/examples/cli/step-machine-cli/portfolio-tracker/portfolio-tracker-pycli.flow.yaml +0 -80
  282. package/examples/cli/step-machine-cli/portfolio-tracker/portfolio-tracker.flow.yaml +0 -76
  283. package/examples/cli/step-machine-cli/portfolio-tracker/portfolio-tracker.input.json +0 -44
  284. package/examples/cli/step-machine-cli/portfolio-tracker/run-inline-python-demo-pycli.py +0 -43
  285. package/examples/cli/step-machine-cli/portfolio-tracker/run-portfolio-tracker-pycli.py +0 -77
  286. package/examples/cli/step-machine-cli/portfolio-tracker/run-portfolio-tracker.bat +0 -28
  287. package/examples/cli/step-machine-demo/jsonata-init-board-cli.js +0 -31
  288. package/examples/cli/step-machine-demo/jsonata-init-board.flow.yaml +0 -54
  289. package/examples/cli/step-machine-demo/one-step-cli-only.flow.yaml +0 -21
  290. package/examples/cli/step-machine-demo/step-cli-echo-y.js +0 -15
  291. package/examples/cli/step-machine-demo/step2-double-cli.js +0 -33
  292. package/examples/cli/step-machine-demo/two-step-math.flow.yaml +0 -93
  293. package/examples/cli/step-machine-demo/two-step-mixed.flow.yaml +0 -43
  294. package/examples/example-board/agent-instructions-cardlayout.md +0 -56
  295. package/examples/example-board/agent-instructions.md +0 -834
  296. package/examples/example-board/demo-shell.html +0 -63
  297. package/examples/index.html +0 -785
  298. package/examples/npm-libs/batch/batch-step-machine.ts +0 -121
  299. package/examples/npm-libs/continuous-event-graph/live-cards-board.ts +0 -215
  300. package/examples/npm-libs/continuous-event-graph/live-portfolio-dashboard.ts +0 -555
  301. package/examples/npm-libs/continuous-event-graph/portfolio-tracker.ts +0 -287
  302. package/examples/npm-libs/continuous-event-graph/reactive-monitoring.ts +0 -265
  303. package/examples/npm-libs/continuous-event-graph/reactive-pipeline.ts +0 -168
  304. package/examples/npm-libs/continuous-event-graph/soc-incident-board.ts +0 -287
  305. package/examples/npm-libs/continuous-event-graph/stock-dashboard.ts +0 -229
  306. package/examples/npm-libs/event-graph/ci-cd-pipeline.ts +0 -243
  307. package/examples/npm-libs/event-graph/executor-diamond.ts +0 -165
  308. package/examples/npm-libs/event-graph/executor-pipeline.ts +0 -161
  309. package/examples/npm-libs/event-graph/research-pipeline.ts +0 -137
  310. package/examples/npm-libs/flows/ai-conversation.yaml +0 -116
  311. package/examples/npm-libs/flows/order-processing.yaml +0 -143
  312. package/examples/npm-libs/flows/simple-greeting.yaml +0 -54
  313. package/examples/npm-libs/graph-of-graphs/multi-stage-etl.ts +0 -307
  314. package/examples/npm-libs/graph-of-graphs/url-processing-pipeline.ts +0 -254
  315. package/examples/npm-libs/inference/azure-deployment.ts +0 -149
  316. package/examples/npm-libs/inference/copilot-cli.ts +0 -138
  317. package/examples/npm-libs/inference/data-pipeline.ts +0 -145
  318. package/examples/npm-libs/inference/pluggable-adapters.ts +0 -254
  319. package/examples/npm-libs/node/ai-conversation.ts +0 -195
  320. package/examples/npm-libs/node/simple-greeting.ts +0 -101
  321. package/examples/step-machine-cli/portfolio-tracker/cards/holdings-table.json +0 -22
  322. package/examples/step-machine-cli/portfolio-tracker/cards/portfolio-form.json +0 -43
  323. package/examples/step-machine-cli/portfolio-tracker/cards/portfolio-value.json +0 -15
  324. package/examples/step-machine-cli/portfolio-tracker/cards/price-fetch.json +0 -15
  325. package/examples/step-machine-cli/portfolio-tracker/fetch-prices.js +0 -48
  326. package/examples/step-machine-cli/portfolio-tracker/handlers/_board-cli.js +0 -57
  327. package/examples/step-machine-cli/portfolio-tracker/handlers/add-cards-cli.js +0 -27
  328. package/examples/step-machine-cli/portfolio-tracker/handlers/init-board-cli.js +0 -25
  329. package/examples/step-machine-cli/portfolio-tracker/handlers/reset-board-dir-cli.js +0 -29
  330. package/examples/step-machine-cli/portfolio-tracker/handlers/retrigger-cli.js +0 -27
  331. package/examples/step-machine-cli/portfolio-tracker/handlers/status-cli.js +0 -25
  332. package/examples/step-machine-cli/portfolio-tracker/handlers/update-holdings-cli.js +0 -37
  333. package/examples/step-machine-cli/portfolio-tracker/handlers/wait-completed-cli.js +0 -53
  334. package/examples/step-machine-cli/portfolio-tracker/handlers/write-prices-cli.js +0 -35
  335. package/examples/step-machine-cli/portfolio-tracker/portfolio-tracker-task-executor.cjs +0 -96
  336. package/examples/step-machine-cli/portfolio-tracker/portfolio-tracker.flow.yaml +0 -227
  337. package/examples/step-machine-cli/portfolio-tracker/portfolio-tracker.input.json +0 -38
  338. package/examples/step-machine-cli/portfolio-tracker/run-portfolio-tracker.bat +0 -28
  339. package/step-machine-cli.js +0 -407
  340. /package/{dist/cli → cli}/browser-api/board-live-cards-browser-adapter.js +0 -0
  341. /package/{dist/board-livegraph-runtime → cli/browser-api}/jsonata-sync.cjs +0 -0
  342. /package/{dist/cli → cli}/node/artifacts-store-cli.d.ts +0 -0
  343. /package/{dist/cli/node/source-cli-task-executor.d.cts → cli/node/batch-runner-cli.d.ts} +0 -0
  344. /package/{dist/cli → cli}/node/board-live-cards-cli.d.ts +0 -0
  345. /package/{dist/cli → cli}/node/card-store-cli.d.ts +0 -0
  346. /package/{dist/card-compute → cli/node}/jsonata-sync.cjs +0 -0
  347. /package/{dist/cli → cli}/node/source-cli-task-executor.d.ts +0 -0
  348. /package/{dist/cli → cli}/node/source-cli-task-executor.js +0 -0
  349. /package/examples/{example-board → board}/cards/card-concentration.json +0 -0
  350. /package/examples/{example-board → board}/cards/card-my-identity.json +0 -0
  351. /package/examples/{example-board → board}/cards/card-portfolio-action.json +0 -0
  352. /package/examples/{example-board → board}/cards/card-portfolio-intelligence.json +0 -0
  353. /package/examples/{example-board → board}/cards/card-portfolio-risks.json +0 -0
  354. /package/examples/{example-board → board}/cards/card-rebalance-impact.json +0 -0
  355. /package/examples/{example-board → board}/cards/card-rebalance-sim.json +0 -0
  356. /package/examples/{example-board → board}/cards/cardT-market-prices.json +0 -0
  357. /package/examples/{example-board → board}/cards/cardT-portfolio-value.json +0 -0
  358. /package/examples/{example-board → board}/cards/cardT-portfolio.json +0 -0
  359. /package/examples/{example-board → board}/demo-chat-handler.js +0 -0
  360. /package/examples/{example-board → board}/scripts/copilot_wrapper.bat +0 -0
  361. /package/examples/{example-board → board}/scripts/copilot_wrapper_helper.ps1 +0 -0
  362. /package/examples/{example-board → board}/scripts/workiq_wrapper.mjs +0 -0
  363. /package/examples/{browser/boards/portfolio-tracker → board/test}/portfolio-tracker-sse-worker.js +0 -0
  364. /package/{dist → lib}/batch/index.cjs +0 -0
  365. /package/{dist → lib}/batch/index.d.cts +0 -0
  366. /package/{dist → lib}/batch/index.d.ts +0 -0
  367. /package/{dist → lib}/batch/index.js +0 -0
  368. /package/{dist/cli/browser-api → lib/board-livegraph-runtime}/jsonata-sync.cjs +0 -0
  369. /package/{dist → lib}/card-compute/index.d.cts +0 -0
  370. /package/{dist → lib}/card-compute/index.d.ts +0 -0
  371. /package/{dist/cli/node → lib/card-compute}/jsonata-sync.cjs +0 -0
  372. /package/{dist → lib}/config/index.cjs +0 -0
  373. /package/{dist → lib}/config/index.d.cts +0 -0
  374. /package/{dist → lib}/config/index.d.ts +0 -0
  375. /package/{dist → lib}/config/index.js +0 -0
  376. /package/{dist → lib}/continuous-event-graph/jsonata-sync.cjs +0 -0
  377. /package/{dist → lib}/event-graph/index.cjs +0 -0
  378. /package/{dist → lib}/event-graph/index.js +0 -0
  379. /package/{dist → lib}/jsonata-sync.cjs +0 -0
  380. /package/{dist → lib}/server-runtime/jsonata-sync.cjs +0 -0
  381. /package/{dist → lib}/step-machine/index.cjs +0 -0
  382. /package/{dist → lib}/step-machine/index.js +0 -0
  383. /package/{dist → lib}/step-machine-public/jsonata-sync.cjs +0 -0
  384. /package/{dist → lib}/stores/memory.cjs +0 -0
  385. /package/{dist → lib}/stores/memory.js +0 -0
  386. /package/{dist → lib}/types-BBhqYGhE.d.cts +0 -0
  387. /package/{dist → lib}/types-BBhqYGhE.d.ts +0 -0
  388. /package/{dist → lib}/validate-BAVzUJWa.d.ts +0 -0
  389. /package/{dist → lib}/validate-Dbu7ygys.d.cts +0 -0
@@ -1,55 +1,4 @@
1
- // live-cards.js LiveCards v3: Node-based Board/Canvas engine
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,2340 +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
- document.head.appendChild(s);
133
- }
134
-
135
- // ===========================================================================
136
- // Global utilities
137
- // ===========================================================================
138
-
139
- const _escMap = { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' };
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 + '">&times;</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 + '">&times;</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={"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;"};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+'">&times;</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+'">&times;</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${isDirty() ? '' : ' d-none'}" type="button">Discard</button>
1267
- <button class="btn btn-sm btn-primary lc-n-save${isDirty() ? '' : ' d-none'}" type="button">Save</button>
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
- // Wrap in a named key so spread into card_data gives card_data.notes = "..."
1288
- // (same dict pattern as form/filter; writeTo is not required).
1289
- cfg.onPatchState(node.id, { fieldValues: { notes: nextValue } });
1290
- saveBtn.textContent = 'Saving...';
1291
- _showSavingOverlay(el);
1292
- }, { signal });
1293
-
1294
- discardBtn.addEventListener('click', () => {
1295
- st.journal = null;
1296
- textarea.value = st.baseContent || '';
1297
- syncDirtyButtons();
1298
- }, { signal });
1299
- }
1300
-
1301
- // ---- todo ----
1302
-
1303
- // ---- editable-table ----
1304
- // Renders an array bound via `data.bind` as an inline-editable table.
1305
- // Each row is editable in-place; changes are saved on blur (change event).
1306
- // `data.writeTo` persists changes back to card_data (same pattern as form).
1307
- // `data.columns` restricts which columns appear (and in what order).
1308
- // `data.schema.properties[col].type` ("number"/"integer") controls input type.
1309
- // `data.addRow` (default true) shows "+ Add row" button.
1310
- // `data.deleteRow` (default true) shows per-row delete button.
1311
- function _renderEditableTable(data, el, elemDef, node) {
1312
- const cleanup = _getCleanup(node.id);
1313
- const signal = cleanup.ac.signal;
1314
- const ed = elemDef.data || {};
1315
- // Standard convention:
1316
- // - bind = read source
1317
- // - writeTo = explicit write target for editable views
1318
- // If bind already points at card_data, default writeTo to bind.
1319
- const writeTo = ed.writeTo || ((typeof ed.bind === 'string' && ed.bind.startsWith('card_data.')) ? ed.bind : undefined);
1320
- const schemaProps = (ed.schema && ed.schema.properties) || {};
1321
- const canAdd = ed.addRow !== false;
1322
- const canDelete = ed.deleteRow !== false;
1323
-
1324
- // Derive columns from rows if not specified
1325
- function getCols(rows) {
1326
- if (ed.columns && ed.columns.length) return ed.columns;
1327
- const s = new Set();
1328
- rows.forEach(r => { if (r && typeof r === 'object') Object.keys(r).forEach(k => s.add(k)); });
1329
- return [...s];
1330
- }
1331
-
1332
- // Base + journal overlay model:
1333
- // effectiveRows = journalRows if present, else baseRows(bind).
1334
- // Dirty is determined by journal presence (supports Save/Discard UX).
1335
- const stateKey = node.id + ':' + (ed.bind || writeTo || '');
1336
- const incomingRows = Array.isArray(data) ? data : [];
1337
- const incomingCopy = incomingRows.map(r => Object.assign({}, r));
1338
-
1339
- if (!_etState[stateKey]) {
1340
- _etState[stateKey] = { baseRows: incomingCopy, journalRows: null };
1341
- } else {
1342
- _etState[stateKey].baseRows = incomingCopy;
1343
- if (_etState[stateKey].journalRows && JSON.stringify(_etState[stateKey].journalRows) === JSON.stringify(incomingCopy)) {
1344
- _etState[stateKey].journalRows = null;
1345
- }
1346
- }
1347
-
1348
- const st = _etState[stateKey];
1349
-
1350
- function isDirty() {
1351
- return Array.isArray(st.journalRows);
1352
- }
1353
-
1354
- function getEffectiveRows() {
1355
- const rows = Array.isArray(st.journalRows) ? st.journalRows : st.baseRows;
1356
- return rows.map(r => Object.assign({}, r));
1357
- }
1358
-
1359
- function updateJournal(nextRows) {
1360
- if (JSON.stringify(nextRows) === JSON.stringify(st.baseRows)) st.journalRows = null;
1361
- else st.journalRows = nextRows.map(r => Object.assign({}, r));
1362
- }
1363
-
1364
- function markDirty() {
1365
- const saveBtn = el.querySelector('.lc-et-save');
1366
- const discardBtn = el.querySelector('.lc-et-discard');
1367
- if (saveBtn) saveBtn.classList.remove('d-none');
1368
- if (discardBtn) discardBtn.classList.remove('d-none');
1369
- }
1370
-
1371
- function commitSave() {
1372
- const rows = getEffectiveRows();
1373
- cfg.onPatchState(node.id, { fieldValues: rows });
1374
- const saveBtn = el.querySelector('.lc-et-save');
1375
- if (saveBtn) saveBtn.textContent = 'Saving...';
1376
- _showSavingOverlay(el);
1377
- }
1378
-
1379
- function commitDiscard() {
1380
- st.journalRows = null;
1381
- build();
1382
- }
1383
-
1384
- function build() {
1385
- const rows = getEffectiveRows();
1386
- const cols = getCols(rows);
1387
-
1388
- if (!cols.length && !canAdd) {
1389
- el.innerHTML = `<p class="text-muted small">${_esc(ed.placeholder || 'No data')}</p>`;
1390
- return;
1391
- }
1392
-
1393
- let h = '<div class="table-responsive"><table class="table table-sm table-bordered mb-0 lc-editable-table"><thead><tr>';
1394
- cols.forEach(c => { h += `<th class="small text-nowrap">${_esc(c)}</th>`; });
1395
- if (canDelete) h += '<th style="width:2rem"></th>';
1396
- h += '</tr></thead><tbody>';
1397
-
1398
- rows.forEach((row, rowIdx) => {
1399
- h += `<tr>`;
1400
- cols.forEach(c => {
1401
- const v = row[c];
1402
- const prop = schemaProps[c] || {};
1403
- const isNum = prop.type === 'number' || prop.type === 'integer' || (v != null && typeof v === 'number');
1404
- const displayVal = v != null ? String(v) : '';
1405
- h += `<td class="p-0">` +
1406
- `<input type="${isNum ? 'number' : 'text'}" ` +
1407
- `class="form-control form-control-sm border-0 rounded-0 lc-et-cell" ` +
1408
- `data-row="${rowIdx}" data-col="${_esc(c)}" value="${_esc(displayVal)}"` +
1409
- `${isNum ? ' step="any"' : ''}>` +
1410
- `</td>`;
1411
- });
1412
- if (canDelete) {
1413
- h += `<td class="text-center align-middle p-0">` +
1414
- `<button class="btn btn-sm btn-link text-danger p-0 lc-et-del" data-row="${rowIdx}" title="Remove row">✕</button>` +
1415
- `</td>`;
1416
- }
1417
- h += '</tr>';
1418
- });
1419
-
1420
- if (!rows.length) {
1421
- const span = cols.length + (canDelete ? 1 : 0);
1422
- h += `<tr><td colspan="${span}" class="text-muted small text-center">${_esc(ed.placeholder || 'No rows')}</td></tr>`;
1423
- }
1424
-
1425
- h += '</tbody></table></div>';
1426
- let footer = '';
1427
- if (canAdd) footer += '<button class="btn btn-sm btn-outline-secondary mt-1 me-1 lc-et-add">+ Add row</button>';
1428
- footer += `<button class="btn btn-sm btn-outline-secondary mt-1 me-1 lc-et-discard${isDirty() ? '' : ' d-none'}">Discard</button>`;
1429
- footer += `<button class="btn btn-sm btn-primary mt-1 lc-et-save${isDirty() ? '' : ' d-none'}">Save</button>`;
1430
- el.innerHTML = h + footer;
1431
-
1432
- // Cell edit → update journal overlay and toggle Save/Discard.
1433
- el.querySelectorAll('.lc-et-cell').forEach(inp => {
1434
- inp.addEventListener('change', () => {
1435
- const rowIdx = parseInt(inp.dataset.row);
1436
- const colName = inp.dataset.col;
1437
- const prop = schemaProps[colName] || {};
1438
- const isNum = prop.type === 'number' || prop.type === 'integer' || inp.type === 'number';
1439
- const nextRows = getEffectiveRows();
1440
- if (!nextRows[rowIdx]) return;
1441
- nextRows[rowIdx] = Object.assign({}, nextRows[rowIdx]);
1442
- nextRows[rowIdx][colName] = isNum ? (inp.value !== '' ? parseFloat(inp.value) : 0) : inp.value;
1443
- updateJournal(nextRows);
1444
- if (isDirty()) markDirty();
1445
- else {
1446
- const saveBtn = el.querySelector('.lc-et-save');
1447
- const discardBtn = el.querySelector('.lc-et-discard');
1448
- if (saveBtn) saveBtn.classList.add('d-none');
1449
- if (discardBtn) discardBtn.classList.add('d-none');
1450
- }
1451
- }, { signal });
1452
- });
1453
-
1454
- // Delete row — updates journal and rebuilds.
1455
- el.querySelectorAll('.lc-et-del').forEach(btn => {
1456
- btn.addEventListener('click', () => {
1457
- const rowIdx = parseInt(btn.dataset.row);
1458
- const nextRows = getEffectiveRows().filter((_, i) => i !== rowIdx);
1459
- updateJournal(nextRows);
1460
- build();
1461
- }, { signal });
1462
- });
1463
-
1464
- // Add row — appends blank row to journal and rebuilds.
1465
- const addBtn = el.querySelector('.lc-et-add');
1466
- if (addBtn) {
1467
- addBtn.addEventListener('click', () => {
1468
- const newRow = {};
1469
- const nextRows = getEffectiveRows();
1470
- getCols(nextRows).forEach(c => { newRow[c] = ''; });
1471
- nextRows.push(newRow);
1472
- updateJournal(nextRows);
1473
- build();
1474
- }, { signal });
1475
- }
1476
-
1477
- // Save/Discard controls.
1478
- const discardBtn = el.querySelector('.lc-et-discard');
1479
- if (discardBtn) {
1480
- discardBtn.addEventListener('click', () => {
1481
- commitDiscard();
1482
- }, { signal });
1483
- }
1484
-
1485
- const saveBtn = el.querySelector('.lc-et-save');
1486
- if (saveBtn) {
1487
- saveBtn.addEventListener('click', () => {
1488
- commitSave();
1489
- saveBtn.textContent = '✓ Saved';
1490
- setTimeout(() => { saveBtn.textContent = 'Save'; }, 1500);
1491
- }, { signal });
1492
- }
1493
- }
1494
-
1495
- build();
1496
- }
1497
-
1498
- // ---- todo ----
1499
-
1500
- function _renderTodo(data, el, elemDef, node) {
1501
- const cleanup = _getCleanup(node.id);
1502
- const signal = cleanup.ac.signal;
1503
- const ed = elemDef.data || {};
1504
- const writeTo = ed.writeTo;
1505
-
1506
- // --- Journal-style dirty tracking ---
1507
- // currentState = last confirmed server state; pending = local working copy
1508
- // On SSE re-render: if dirty (action in-flight), keep pending; if clean, sync from server
1509
- const stateKey = node.id + ':' + (writeTo || '');
1510
- const incomingItems = Array.isArray(data) ? data.map(r => Object.assign({}, r)) : [];
1511
-
1512
- if (!_todoState[stateKey]) {
1513
- _todoState[stateKey] = { currentState: incomingItems, pending: incomingItems.map(r => Object.assign({}, r)) };
1514
- } else {
1515
- const s = _todoState[stateKey];
1516
- const wasDirty = JSON.stringify(s.currentState) !== JSON.stringify(s.pending);
1517
- s.currentState = incomingItems;
1518
- if (!wasDirty) s.pending = incomingItems.map(r => Object.assign({}, r));
1519
- // if dirty, pending stays so in-flight changes survive the SSE tick
1520
- }
1521
- const st = _todoState[stateKey];
1522
-
1523
- function save() {
1524
- if (writeTo) _deepSet(node, writeTo, st.pending);
1525
- cfg.onPatchState(node.id, { fieldValues: st.pending });
1526
- notify(node.id, st.pending);
1527
- // mark clean after save so next SSE sync resumes normally
1528
- st.currentState = st.pending.map(r => Object.assign({}, r));
1529
- }
1530
-
1531
- function build() {
1532
- const items = st.pending;
1533
- let h = '<div class="lc-todo-list">';
1534
- items.forEach((item, i) => {
1535
- const chk = item.done ? ' checked' : '';
1536
- const strike = item.done ? ' text-decoration-line-through text-muted' : '';
1537
- h += `<div class="lc-todo-item">`;
1538
- h += `<input class="form-check-input flex-shrink-0" type="checkbox"${chk} data-idx="${i}">`;
1539
- h += `<span class="small flex-grow-1${strike}">${_esc(item.text)}</span>`;
1540
- h += `<button class="btn btn-sm btn-link text-danger p-0" data-rm="${i}" title="Remove">×</button></div>`;
1541
- });
1542
- h += '</div>';
1543
- h += '<div class="input-group input-group-sm mt-2"><input type="text" class="form-control" placeholder="Add item...">';
1544
- h += '<button class="btn btn-outline-secondary lc-todo-add">+</button></div>';
1545
- el.innerHTML = h;
1546
-
1547
- el.querySelectorAll('input[data-idx]').forEach(cb => {
1548
- cb.addEventListener('change', () => {
1549
- st.pending[parseInt(cb.dataset.idx)].done = cb.checked;
1550
- save(); build();
1551
- }, { signal });
1552
- });
1553
- el.querySelectorAll('[data-rm]').forEach(btn => {
1554
- btn.addEventListener('click', () => {
1555
- st.pending.splice(parseInt(btn.dataset.rm), 1);
1556
- save(); build();
1557
- }, { signal });
1558
- });
1559
- const addInput = el.querySelector('.input-group input');
1560
- const addBtn = el.querySelector('.lc-todo-add');
1561
- const addItem = () => {
1562
- const t = addInput.value.trim();
1563
- if (!t) return;
1564
- st.pending.push({ text: t, done: false });
1565
- save(); build();
1566
- };
1567
- addBtn.addEventListener('click', addItem, { signal });
1568
- addInput.addEventListener('keydown', e => { if (e.key === 'Enter') { e.preventDefault(); addItem(); } }, { signal });
1569
- }
1570
- build();
1571
- }
1572
-
1573
- // ---- alert ----
1574
-
1575
- function _renderAlert(data, el, elemDef) {
1576
- const ed = elemDef.data || {};
1577
- const thresholds = ed.thresholds || {};
1578
- const value = typeof data === 'number' ? data : (data && data.value != null ? data.value : null);
1579
-
1580
- let level = 'unknown', color = 'secondary';
1581
- if (value != null) {
1582
- if (thresholds.green && _evalThreshold(value, thresholds.green)) { level = 'green'; color = 'success'; }
1583
- else if (thresholds.amber && _evalThreshold(value, thresholds.amber)) { level = 'amber'; color = 'warning'; }
1584
- else { level = 'red'; color = 'danger'; }
1585
- }
1586
-
1587
- 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=`
1588
86
  <div class="d-flex align-items-center gap-3 py-2">
1589
- <span class="lc-alert-dot lc-alert-${level}"></span>
87
+ <span class="lc-alert-dot lc-alert-${l}"></span>
1590
88
  <div class="flex-grow-1">
1591
- <div class="fw-bold">${value != null ? _esc(String(value)) : '—'}</div>
1592
- ${elemDef.label ? `<div class="text-muted small">${_esc(elemDef.label)}</div>` : ''}
89
+ <div class="fw-bold">${n!=null?L(String(n)):"\u2014"}</div>
90
+ ${a.label?`<div class="text-muted small">${L(a.label)}</div>`:""}
1593
91
  </div>
1594
- <span class="badge bg-${color} fs-6">${_esc(level)}</span>
1595
- </div>`;
1596
- }
1597
-
1598
- // ---- narrative ----
1599
-
1600
- function _renderNarrative(data, el) {
1601
- const text = typeof data === 'string' ? data : (data && data.text ? data.text : '');
1602
- if (!text) { el.innerHTML = '<p class="text-muted small fst-italic">No narrative yet. Click refresh to generate.</p>'; return; }
1603
- el.innerHTML = `<div class="small">${_renderMd(text)}</div>`;
1604
- }
1605
-
1606
- // ---- badge ----
1607
-
1608
- function _renderBadge(data, el, elemDef) {
1609
- const ed = elemDef.data || {};
1610
- const map = ed.colorMap || {};
1611
- const val = data != null ? String(data) : '';
1612
- const bsMap = { green: 'success', amber: 'warning', red: 'danger', blue: 'primary' };
1613
- const bs = bsMap[map[val]] || map[val] || 'secondary';
1614
- el.innerHTML = `<span class="badge bg-${_esc(bs)}">${_esc(val)}</span>`;
1615
- }
1616
-
1617
- // ---- text ----
1618
-
1619
- function _renderText(data, el, elemDef) {
1620
- const ed = elemDef.data || {};
1621
- const format = ed.format || 'default';
1622
- const style = elemDef.style || ed.style || 'default';
1623
- const hideIfEmpty = ed.hideIfEmpty || elemDef.hideIfEmpty;
1624
-
1625
- if (hideIfEmpty && (data == null || data === '')) { el.innerHTML = ''; return; }
1626
-
1627
- // Handle file-links format
1628
- if (format === 'file-links') {
1629
- if (!Array.isArray(data) || data.length === 0) {
1630
- el.innerHTML = '<div class="text-muted small">No files uploaded</div>';
1631
- return;
1632
- }
1633
- const htmlParts = [];
1634
- data.forEach((file, idx) => {
1635
- if (!file || !file.stored_name) return;
1636
- const name = file.name || file.stored_name;
1637
- const cardId = elemDef.data && elemDef.data.cardId ? elemDef.data.cardId : 'unknown';
1638
- const downloadUrl = `/api/example-board/server/cards/${encodeURIComponent(cardId)}/files/${idx}?sn=${encodeURIComponent(file.stored_name)}`;
1639
- const size = file.size ? ` (${Math.round(file.size / 1024)}KB)` : '';
1640
- htmlParts.push(`<div class="mb-2"><a href="${downloadUrl}" class="btn btn-sm btn-outline-secondary">${_esc(name)}${_esc(size)}</a></div>`);
1641
- });
1642
- const html = htmlParts.join('');
1643
- el.innerHTML = html;
1644
- return;
1645
- }
1646
-
1647
- // Default text rendering
1648
- const tag = style === 'heading' ? 'h4' : 'div';
1649
- const cls = style === 'muted' ? 'text-muted small'
1650
- : style === 'muted-italic' ? 'text-muted small fst-italic'
1651
- : style === 'heading' ? 'fw-bold'
1652
- : 'small';
1653
- el.innerHTML = `<${tag} class="${cls}">${_esc(data != null ? String(data) : '')}</${tag}>`;
1654
- }
1655
-
1656
- // ---- markdown ----
1657
-
1658
- function _renderMarkdown(data, el) {
1659
- let text = '';
1660
- if (typeof data === 'string') text = data;
1661
- else if (data && typeof data === 'object' && data.text) text = data.text;
1662
- else if (data != null) text = JSON.stringify(data, null, 2);
1663
- el.innerHTML = text ? _renderMd(text) : '';
1664
- }
1665
-
1666
- // ---- custom (fallback to JSON) ----
1667
-
1668
- function _renderCustom(data, el) {
1669
- if (data == null) { el.innerHTML = ''; return; }
1670
- el.innerHTML = `<pre class="small mb-0">${_esc(JSON.stringify(data, null, 2))}</pre>`;
1671
- }
1672
-
1673
- // ---- file-upload ----
1674
-
1675
- function _renderFileUpload(data, el, elemDef, node) {
1676
- const cleanup = _getCleanup(node.id);
1677
- const signal = cleanup.ac.signal;
1678
- const ed = elemDef.data || {};
1679
- const uploaded = Array.isArray(data) ? data : [];
1680
- const showUploadedList = ed.showUploadedList === true;
1681
- const showUpload = ed.upload !== false;
1682
- const accept = ed.accept || ['.txt','.csv','.md','.json','.html','.xml','.pdf','.xlsx','.docx','.pptx','.png','.jpg','.jpeg'];
1683
- const acceptSet = new Set(accept.map(e => e.toLowerCase()));
1684
- const multiple = ed.multiple !== false;
1685
- const placeholder = ed.placeholder || 'Drop files here or click to browse';
1686
- const uid = 'lc-fu-' + (elemDef.id || Math.random().toString(36).slice(2, 8));
1687
-
1688
- let stagedFiles = el._stagedFiles || [];
1689
- el._stagedFiles = stagedFiles;
1690
- let uploadStatus = el._uploadStatus || {};
1691
- el._uploadStatus = uploadStatus;
1692
-
1693
- function keyForFile(f) {
1694
- return `${f.name}::${f.size}::${f.lastModified || 0}`;
1695
- }
1696
-
1697
- let h = '';
1698
-
1699
- // Drop zone
1700
- if (showUpload) {
1701
- h += `<div class="lc-dropzone mb-2" id="${uid}-dz">`;
1702
- 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>';
1703
- h += `<div class="small text-muted">${_esc(placeholder)}</div>`;
1704
- h += `<input type="file" id="${uid}-fi" class="d-none"${multiple ? ' multiple' : ''} accept="${accept.join(',')}">`;
1705
- h += '</div>';
1706
- h += `<div id="${uid}-staged"></div>`;
1707
- }
1708
-
1709
- // Uploaded files list
1710
- if (showUploadedList && uploaded.length) {
1711
- h += '<div class="lc-uploaded-files">';
1712
- uploaded.forEach(f => {
1713
- const name = typeof f === 'string' ? f : (f.name || '');
1714
- const url = typeof f === 'string' ? null : f.url;
1715
- h += '<div class="d-flex align-items-center gap-1 small mb-1">';
1716
- 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>';
1717
- if (url) h += `<a href="${_esc(url)}" class="text-truncate" target="_blank" download>${_esc(name)}</a>`;
1718
- else h += `<span class="text-truncate">${_esc(name)}</span>`;
1719
- h += '</div>';
1720
- });
1721
- h += '</div>';
1722
- }
1723
-
1724
- if (!showUpload && !uploaded.length) {
1725
- h = `<p class="text-muted small">${_esc(ed.placeholder || 'No files')}</p>`;
1726
- }
1727
-
1728
- el.innerHTML = h;
1729
-
1730
- if (!showUpload) {
1731
- el._fileUpload = { getFiles: () => [], clear: () => {} };
1732
- return;
1733
- }
1734
-
1735
- const dz = el.querySelector('#' + uid + '-dz');
1736
- const fi = el.querySelector('#' + uid + '-fi');
1737
- const stagedEl = el.querySelector('#' + uid + '-staged');
1738
- if (!dz) return;
1739
-
1740
- function addFiles(fileList) {
1741
- const newlyAdded = [];
1742
- for (const f of fileList) {
1743
- const ext = '.' + f.name.split('.').pop().toLowerCase();
1744
- if (!acceptSet.has(ext)) continue;
1745
- if (!stagedFiles.find(s => s.name === f.name)) {
1746
- stagedFiles.push(f);
1747
- newlyAdded.push(f);
1748
- uploadStatus[keyForFile(f)] = 'uploading';
1749
- }
1750
- }
1751
- renderStaged();
1752
-
1753
- // Server demos can upload real file blobs immediately via onAction.
1754
- if (newlyAdded.length && typeof cfg.onAction === 'function') {
1755
- Promise.resolve(cfg.onAction(node.id, 'file-upload', { files: newlyAdded, elemId: elemDef.id }))
1756
- .then(() => {
1757
- const uploadedKeys = new Set(newlyAdded.map(keyForFile));
1758
- stagedFiles = stagedFiles.filter((f) => !uploadedKeys.has(keyForFile(f)));
1759
- el._stagedFiles = stagedFiles;
1760
- newlyAdded.forEach((f) => { delete uploadStatus[keyForFile(f)]; });
1761
- el._uploadStatus = uploadStatus;
1762
- renderStaged();
1763
- })
1764
- .catch(() => {
1765
- newlyAdded.forEach((f) => { uploadStatus[keyForFile(f)] = 'error'; });
1766
- el._uploadStatus = uploadStatus;
1767
- renderStaged();
1768
- });
1769
- }
1770
- }
1771
-
1772
- function renderStaged() {
1773
- if (!stagedFiles.length) { stagedEl.innerHTML = ''; return; }
1774
- let sh = '';
1775
- stagedFiles.forEach((f, i) => {
1776
- const status = uploadStatus[keyForFile(f)] || 'ready';
1777
- sh += '<div class="lc-staged-file">';
1778
- 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>';
1779
- sh += `<span class="small flex-grow-1 text-truncate">${_esc(f.name)}</span>`;
1780
- if (status === 'uploading') {
1781
- sh += '<span class="spinner-border spinner-border-sm text-secondary me-1" role="status" aria-label="Uploading"></span>';
1782
- } else if (status === 'error') {
1783
- sh += '<span class="badge bg-danger-subtle text-danger border border-danger-subtle me-1">Failed</span>';
1784
- }
1785
- sh += `<button class="btn btn-sm btn-link text-danger p-0 lc-rm-staged" data-idx="${i}">&times;</button>`;
1786
- sh += '</div>';
1787
- });
1788
- stagedEl.innerHTML = sh;
1789
- stagedEl.querySelectorAll('.lc-rm-staged').forEach(btn => {
1790
- btn.addEventListener('click', () => {
1791
- const idx = parseInt(btn.dataset.idx);
1792
- const f = stagedFiles[idx];
1793
- if (f) delete uploadStatus[keyForFile(f)];
1794
- stagedFiles.splice(idx, 1);
1795
- el._stagedFiles = stagedFiles;
1796
- el._uploadStatus = uploadStatus;
1797
- renderStaged();
1798
- }, { signal });
1799
- });
1800
- }
1801
-
1802
- dz.addEventListener('click', () => fi.click(), { signal });
1803
- dz.addEventListener('dragover', e => { e.preventDefault(); dz.classList.add('lc-drag-over'); }, { signal });
1804
- dz.addEventListener('dragleave', () => dz.classList.remove('lc-drag-over'), { signal });
1805
- dz.addEventListener('drop', e => { e.preventDefault(); dz.classList.remove('lc-drag-over'); addFiles(e.dataTransfer.files); }, { signal });
1806
- fi.addEventListener('change', e => { addFiles(e.target.files); e.target.value = ''; }, { signal });
1807
-
1808
- renderStaged();
1809
-
1810
- el._fileUpload = {
1811
- getFiles: () => stagedFiles,
1812
- clear: () => { stagedFiles = []; uploadStatus = {}; el._stagedFiles = []; el._uploadStatus = {}; renderStaged(); },
1813
- disable: () => { dz.classList.add('lc-disabled'); fi.disabled = true; },
1814
- enable: () => { dz.classList.remove('lc-disabled'); fi.disabled = false; },
1815
- };
1816
- }
1817
-
1818
- // ---- chat (element kind) ----
1819
-
1820
- function _renderChatEl(data, el, elemDef, node) {
1821
- const cleanup = _getCleanup(node.id);
1822
- const signal = cleanup.ac.signal;
1823
- const ed = elemDef.data || {};
1824
- const messages = Array.isArray(data) ? data : [];
1825
- const placeholder = ed.placeholder || 'Type a message...';
1826
- const canAttach = ed.fileAttach === true;
1827
- const accept = ed.fileAccept || ['.txt','.csv','.md','.json','.html','.xml','.pdf','.xlsx','.docx','.pptx','.png','.jpg','.jpeg'];
1828
- const uid = 'lc-ch-' + (elemDef.id || Math.random().toString(36).slice(2, 8));
1829
-
1830
- let h = '<div class="lc-chat-el">';
1831
- h += `<div class="lc-chat-body" id="${uid}-body"></div>`;
1832
- h += '<div class="lc-chat-input-bar">';
1833
- if (canAttach) {
1834
- h += `<input type="file" id="${uid}-fi" class="d-none" multiple accept="${accept.join(',')}">`;
1835
- h += `<button class="btn btn-sm btn-outline-secondary" id="${uid}-attach" title="Attach files" type="button">`;
1836
- 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>';
1837
- h += '</button>';
1838
- }
1839
- h += `<input type="text" class="form-control form-control-sm flex-grow-1" id="${uid}-input" placeholder="${_esc(placeholder)}">`;
1840
- h += `<button class="btn btn-sm btn-outline-primary" id="${uid}-send" type="button">`;
1841
- 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>';
1842
- h += '</button></div>';
1843
- if (canAttach) h += `<div id="${uid}-staged" class="mt-1"></div>`;
1844
- h += '</div>';
1845
-
1846
- el.innerHTML = h;
1847
-
1848
- const body = el.querySelector('#' + uid + '-body');
1849
- const input = el.querySelector('#' + uid + '-input');
1850
- const sendBtn = el.querySelector('#' + uid + '-send');
1851
- const attachBtn = canAttach ? el.querySelector('#' + uid + '-attach') : null;
1852
- const fileInput = canAttach ? el.querySelector('#' + uid + '-fi') : null;
1853
- const stagedEl = canAttach ? el.querySelector('#' + uid + '-staged') : null;
1854
-
1855
- let stagedFiles = [];
1856
-
1857
- function appendMsg(msg) {
1858
- const bub = document.createElement('div');
1859
- const roleClass = msg.role === 'user' ? 'lc-chat-bubble-user'
1860
- : msg.role === 'assistant' ? 'lc-chat-bubble-assistant'
1861
- : 'lc-chat-bubble-system';
1862
- bub.className = 'lc-chat-bubble ' + roleClass;
1863
- if (msg.role === 'assistant') {
1864
- bub.innerHTML = _renderMd(msg.text || '');
1865
- } else {
1866
- bub.textContent = msg.text || '';
1867
- }
1868
- if (msg.files && msg.files.length) {
1869
- const fDiv = document.createElement('div');
1870
- fDiv.className = 'small mt-1';
1871
- msg.files.forEach(f => {
1872
- const name = typeof f === 'string' ? f : f.name;
1873
- fDiv.innerHTML += '\uD83D\uDCCE ' + _esc(name) + '<br>';
1874
- });
1875
- bub.appendChild(fDiv);
1876
- }
1877
- body.appendChild(bub);
1878
- }
1879
-
1880
- messages.forEach(appendMsg);
1881
- body.scrollTop = body.scrollHeight;
1882
-
1883
- function renderStaged() {
1884
- if (!stagedEl) return;
1885
- if (!stagedFiles.length) { stagedEl.innerHTML = ''; return; }
1886
- stagedEl.innerHTML = stagedFiles.map((f, i) =>
1887
- `<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}">&times;</button></div>`
1888
- ).join('');
1889
- stagedEl.querySelectorAll('.lc-rm-cs').forEach(btn => {
1890
- btn.addEventListener('click', () => { stagedFiles.splice(parseInt(btn.dataset.idx), 1); renderStaged(); }, { signal });
1891
- });
1892
- }
1893
-
1894
- if (attachBtn && fileInput) {
1895
- const acceptS = new Set(accept.map(x => x.toLowerCase()));
1896
- attachBtn.addEventListener('click', () => fileInput.click(), { signal });
1897
- fileInput.addEventListener('change', e => {
1898
- for (const f of e.target.files) {
1899
- const ext = '.' + f.name.split('.').pop().toLowerCase();
1900
- if (acceptS.has(ext) && !stagedFiles.find(s => s.name === f.name)) stagedFiles.push(f);
1901
- }
1902
- e.target.value = '';
1903
- renderStaged();
1904
- }, { signal });
1905
- }
1906
-
1907
- function doSend() {
1908
- const text = input.value.trim();
1909
- if (!text && !stagedFiles.length) return;
1910
- const msg = { role: 'user', text: text || '' };
1911
- if (stagedFiles.length) msg.files = stagedFiles.map(f => ({ name: f.name, size: f.size }));
1912
- appendMsg(msg);
1913
- body.scrollTop = body.scrollHeight;
1914
- input.value = '';
1915
- const filesToSend = stagedFiles.slice();
1916
- stagedFiles = [];
1917
- renderStaged();
1918
- cfg.onAction(node.id, 'chat-send', { text: msg.text, files: filesToSend, elemId: elemDef.id });
1919
- }
1920
-
1921
- sendBtn.addEventListener('click', doSend, { signal });
1922
- input.addEventListener('keydown', e => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); doSend(); } }, { signal });
1923
-
1924
- el._chat = {
1925
- appendMessage: (role, text, files) => { appendMsg({ role, text, files }); body.scrollTop = body.scrollHeight; },
1926
- showProcessing: (text) => {
1927
- let ind = body.querySelector('.lc-chat-processing');
1928
- if (!ind) {
1929
- ind = document.createElement('div');
1930
- ind.className = 'lc-chat-processing';
1931
- ind.innerHTML = '<span class="spinner-border spinner-border-sm"></span><span class="small">Processing...</span>';
1932
- body.appendChild(ind);
1933
- }
1934
- if (text) ind.querySelector('.small').textContent = text;
1935
- body.scrollTop = body.scrollHeight;
1936
- },
1937
- removeProcessing: () => { const ind = body.querySelector('.lc-chat-processing'); if (ind) ind.remove(); },
1938
- disable: () => { input.disabled = true; sendBtn.disabled = true; if (attachBtn) attachBtn.disabled = true; },
1939
- enable: () => { input.disabled = false; sendBtn.disabled = false; if (attachBtn) attachBtn.disabled = false; },
1940
- };
1941
- }
1942
-
1943
- // ---- actions ----
1944
-
1945
- function _renderActions(data, el, elemDef, node) {
1946
- const cleanup = _getCleanup(node.id);
1947
- const signal = cleanup.ac.signal;
1948
- const ed = elemDef.data || {};
1949
- const buttons = ed.buttons || (Array.isArray(data) ? data : []);
1950
- if (!buttons.length) { el.innerHTML = ''; return; }
1951
-
1952
- let h = '<div class="d-flex gap-2 flex-wrap">';
1953
- buttons.forEach(btn => {
1954
- const style = btn.style || 'outline-secondary';
1955
- const size = btn.size || 'sm';
1956
- const dis = typeof btn.disabled === 'string' ? _resolveBind(node, btn.disabled) : btn.disabled;
1957
- h += `<button class="btn btn-${_esc(style)} btn-${size}" data-action-id="${_esc(btn.id)}"${dis ? ' disabled' : ''}>`;
1958
- h += _esc(btn.label || btn.id);
1959
- h += '</button>';
1960
- });
1961
- h += '</div>';
1962
- el.innerHTML = h;
1963
-
1964
- el.querySelectorAll('[data-action-id]').forEach(btnEl => {
1965
- btnEl.addEventListener('click', () => {
1966
- cfg.onAction(node.id, 'action', { buttonId: btnEl.dataset.actionId, elemId: elemDef.id });
1967
- }, { signal });
1968
- });
1969
-
1970
- el._actions = {
1971
- setDisabled: (buttonId, disabled) => {
1972
- const b = el.querySelector(`[data-action-id="${buttonId}"]`);
1973
- if (b) b.disabled = disabled;
1974
- },
1975
- setLabel: (buttonId, label) => {
1976
- const b = el.querySelector(`[data-action-id="${buttonId}"]`);
1977
- if (b) b.textContent = label;
1978
- },
1979
- };
1980
- }
1981
-
1982
- // ---- ref ----
1983
- // Indirection element: resolves a bind path to get the view definition,
1984
- // then dispatches to the real renderer. The resolved value may be:
1985
- // - a string → treated directly as the element kind ("table", "chart", etc.)
1986
- // - an object → { kind, label, data: { columns, chartType, chartOptions, writeTo } }
1987
- // merged with static elemDef (static fields win for protection)
1988
- // - null/undefined → falls back to elemDef.data.fallbackKind or shape-inferred kind
1989
- //
1990
- // Allowed kinds from resolved value (whitelist, unknown → "table"):
1991
- // table, editable-table, chart, metric, list, badge, text, narrative, markdown
1992
- //
1993
- // Usage:
1994
- // { "kind": "ref",
1995
- // "data": { "bind": "computed_values.proposed_trades",
1996
- // "viewBind": "card_data.display_mode",
1997
- // "fallbackKind": "table" } }
1998
- //
1999
- // viewBind can point to any namespace: card_data, requires, computed_values, runtime_state.
2000
- // If the resolved view object contains a "bind" sub-path, that overrides data.bind.
2001
- const _REF_KIND_WHITELIST = new Set([
2002
- 'table','editable-table','chart','metric','list','badge',
2003
- 'text','narrative','markdown','form','filter','todo','alert',
2004
- ]);
2005
- function _renderRef(data, el, elemDef, node) {
2006
- const ed = elemDef.data || {};
2007
-
2008
- // Resolve the view hint
2009
- const viewRaw = ed.viewBind ? _resolveBind(node, ed.viewBind) : undefined;
2010
-
2011
- let resolvedKind, resolvedExtra;
2012
- if (typeof viewRaw === 'string' && viewRaw) {
2013
- resolvedKind = viewRaw;
2014
- resolvedExtra = {};
2015
- } else if (viewRaw && typeof viewRaw === 'object' && !Array.isArray(viewRaw)) {
2016
- resolvedKind = typeof viewRaw.kind === 'string' ? viewRaw.kind : undefined;
2017
- resolvedExtra = viewRaw.data && typeof viewRaw.data === 'object' ? viewRaw.data : {};
2018
- }
2019
-
2020
- // Validate kind against whitelist; fall back to shape inference
2021
- if (!resolvedKind || !_REF_KIND_WHITELIST.has(resolvedKind)) {
2022
- resolvedKind = ed.fallbackKind && _REF_KIND_WHITELIST.has(ed.fallbackKind)
2023
- ? ed.fallbackKind
2024
- : (Array.isArray(data) ? 'table' : typeof data === 'string' ? 'text' : 'narrative');
2025
- }
2026
-
2027
- // Build effective elemDef: resolved hints first, static elemDef fields override (card author wins)
2028
- const mergedData = Object.assign({}, resolvedExtra, ed);
2029
- delete mergedData.viewBind;
2030
- delete mergedData.fallbackKind;
2031
-
2032
- // If the resolved hint provided its own bind path, honour it (but static ed.bind still wins)
2033
- if (!mergedData.bind && resolvedExtra.bind) mergedData.bind = resolvedExtra.bind;
2034
-
2035
- const effectiveElemDef = Object.assign({}, elemDef, { kind: resolvedKind }, { data: mergedData });
2036
-
2037
- // Re-resolve data using effective bind (may have changed)
2038
- const effectiveData = mergedData.bind ? _resolveBind(node, mergedData.bind) : data;
2039
-
2040
- const renderer = _renderers[resolvedKind] || _renderers.table;
2041
- renderer(effectiveData, el, effectiveElemDef, node);
2042
- }
2043
-
2044
- // ---- Register built-in renderers ----
2045
-
2046
- _renderers.table = _renderTable;
2047
- _renderers['editable-table'] = _renderEditableTable;
2048
- _renderers.filter = _renderFilter;
2049
- _renderers.metric = _renderMetric;
2050
- _renderers.list = _renderList;
2051
- _renderers.chart = _renderChart;
2052
- _renderers.form = _renderForm;
2053
- _renderers.notes = _renderNotes;
2054
- _renderers.todo = _renderTodo;
2055
- _renderers.alert = _renderAlert;
2056
- _renderers.narrative = _renderNarrative;
2057
- _renderers.badge = _renderBadge;
2058
- _renderers.text = _renderText;
2059
- _renderers.markdown = _renderMarkdown;
2060
- _renderers.custom = _renderCustom;
2061
- _renderers['file-upload'] = _renderFileUpload;
2062
- _renderers['chat'] = _renderChatEl;
2063
- _renderers.actions = _renderActions;
2064
- _renderers.ref = _renderRef;
2065
-
2066
- // ===========================================================================
2067
- // _renderElements — render all view.elements for a card node
2068
- // ===========================================================================
2069
-
2070
- function _renderElements(node, containerEl) {
2071
- const view = node && node.card ? node.card.view : null;
2072
- if (!view || !Array.isArray(view.elements)) { containerEl.innerHTML = ''; return; }
2073
-
2074
- if (_nodeEls[node.id]) _nodeEls[node.id].elements = {};
2075
-
2076
- const container = document.createElement('div');
2077
- container.className = 'row g-2';
2078
-
2079
- const _taskStatus = node.runtime_state && node.runtime_state.task_status;
2080
- if (_taskStatus && _taskStatus !== 'completed') {
2081
- const statusEl = document.createElement('div');
2082
- statusEl.className = 'col-12 d-flex align-items-center gap-2 mb-1';
2083
- var _statusIconHtml;
2084
- if (_taskStatus === 'running') {
2085
- _statusIconHtml = '<span class="spinner-border spinner-border-sm text-muted" style="width:.75rem;height:.75rem;flex-shrink:0"></span>';
2086
- } else if (_taskStatus === 'failed') {
2087
- _statusIconHtml = '<span style="font-size:.75rem;line-height:1;flex-shrink:0;color:#dc3545">&#x26A0;&#xFE0E;</span>'; // ⚠ (text variant)
2088
- } else if (_taskStatus === 'not-started') {
2089
- _statusIconHtml = '<span style="font-size:.75rem;line-height:1;flex-shrink:0" class="text-muted">&#x25CB;</span>'; // ○
2090
- } else if (_taskStatus === 'inactivated') {
2091
- _statusIconHtml = '<span style="font-size:.75rem;line-height:1;flex-shrink:0" class="text-muted">&#x2296;</span>'; // ⊖
2092
- } else {
2093
- _statusIconHtml = '<span style="font-size:.75rem;line-height:1;flex-shrink:0" class="text-muted">&#x2013;</span>'; // –
2094
- }
2095
- statusEl.innerHTML = _statusIconHtml + '<span class="text-muted" style="font-size:.75rem">' + _esc(_taskStatus) + '</span>';
2096
- container.appendChild(statusEl);
2097
- }
2098
-
2099
- view.elements.forEach(elemDef => {
2100
- // Visibility gate
2101
- if (elemDef.visible) {
2102
- const vis = _resolveBind(node, elemDef.visible);
2103
- if (!vis) return;
2104
- }
2105
-
2106
- const data = elemDef.data && elemDef.data.bind ? _resolveBind(node, elemDef.data.bind) : undefined;
2107
- const col = document.createElement('div');
2108
- col.className = elemDef.className || 'col-12';
2109
-
2110
- // Element label (except metric which handles its own)
2111
- if (elemDef.label && elemDef.kind !== 'metric' && elemDef.kind !== 'alert') {
2112
- const label = document.createElement('div');
2113
- label.className = 'small text-muted fw-medium mb-1';
2114
- label.textContent = elemDef.label;
2115
- col.appendChild(label);
2116
- }
2117
-
2118
- const inner = document.createElement('div');
2119
- col.appendChild(inner);
2120
-
2121
- const renderer = _renderers[elemDef.kind] || _renderers.custom;
2122
- try {
2123
- renderer(data, inner, elemDef, node);
2124
- } catch (e) {
2125
- console.error('LiveCard render error', node.id, elemDef.kind, e);
2126
- inner.innerHTML = `<div class="text-danger small">Render error: ${_esc(e.message)}</div>`;
2127
- }
2128
-
2129
- if (elemDef.id && _nodeEls[node.id]) _nodeEls[node.id].elements[elemDef.id] = inner;
2130
-
2131
- container.appendChild(col);
2132
- });
2133
-
2134
- containerEl.innerHTML = '';
2135
- containerEl.appendChild(container);
2136
- }
2137
-
2138
- // ===========================================================================
2139
- // Core render
2140
- // ===========================================================================
2141
-
2142
- function render(node, containerEl, opts) {
2143
- opts = opts || {};
2144
- destroy(node.id);
2145
-
2146
- const cleanup = _getCleanup(node.id);
2147
- const signal = cleanup.ac.signal;
2148
- const uid = 'lc-' + (node.id || 'x');
2149
- const features = (node.card && node.card.view && node.card.view.features) || {};
2150
-
2151
- // Run compute async before populating elements
2152
- // (compute is triggered in the else branch below after DOM is ready)
2153
-
2154
- let h = `<div class="lc-card" id="${uid}">`;
2155
-
2156
- // Header bar: status dot + time-ago + refresh button
2157
- const showRefresh = features.refresh !== false && cfg.onRefresh;
2158
- h += `<div class="d-flex align-items-center gap-1 mb-2">`;
2159
- h += _statusDot(node.card_data && node.card_data.status);
2160
- h += `<span class="text-muted small">${_timeAgo(node.card_data && node.card_data.lastRun)}</span>`;
2161
- if (node.card_data && node.card_data.status === 'error' && node.card_data.error) {
2162
- h += `<span class="badge bg-danger small" title="${_esc(node.card_data.error)}">Error</span>`;
2163
- }
2164
- h += '<div class="d-flex align-items-center gap-1 ms-auto">';
2165
- const filesCount = (node && node.card_data && Array.isArray(node.card_data.files)) ? node.card_data.files.length : 0;
2166
- // Files icon button (paperclip)
2167
- 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'}">`;
2168
- 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>';
2169
- if (filesCount > 0) h += `<span class="ms-1 small" aria-label="${filesCount} files">${filesCount}</span>`;
2170
- h += '</button>';
2171
- // Chat icon button (speech bubble)
2172
- h += `<button class="btn btn-sm btn-outline-secondary d-inline-flex align-items-center" id="${uid}-chat-open" title="Chat">`;
2173
- 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>';
2174
- h += '</button>';
2175
- // Refresh icon button
2176
- if (showRefresh) {
2177
- h += `<button class="btn btn-sm btn-outline-secondary d-inline-flex align-items-center" id="${uid}-refresh" title="Refresh">`;
2178
- 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>';
2179
- h += '</button>';
2180
- }
2181
- h += '</div>';
2182
- h += '</div>';
2183
-
2184
- // Inference status bar: completion criteria + task-completed tick
2185
- const inferenceData = node.card_data && node.card_data.llm_task_completion_inference;
2186
- const isTaskCompleted = !!(inferenceData && inferenceData.isTaskCompleted);
2187
- const whenIs = node.card && typeof node.card.when_is_task_completed === 'string' && node.card.when_is_task_completed.trim();
2188
- if (whenIs || isTaskCompleted) {
2189
- 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)">`;
2190
- if (isTaskCompleted) {
2191
- h += `<span class="lc-inference-icon" title="Task completed" style="color:#198754;font-size:.75rem;line-height:1.2;flex-shrink:0">&#x25CF;</span>`;
2192
- } else {
2193
- h += `<span class="lc-inference-icon" style="color:#aaa;font-size:.75rem;line-height:1.4;flex-shrink:0" title="Awaiting inference">&#x25CB;</span>`;
2194
- }
2195
- if (whenIs) {
2196
- 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>`;
2197
- }
2198
- h += `</div>`;
2199
- }
2200
-
2201
- // Elements area
2202
- h += `<div class="lc-result" id="${uid}-result"></div>`;
2203
-
2204
- h += '</div>';
2205
- containerEl.innerHTML = h;
2206
-
2207
- // ---- Render elements ----
2208
- const resultEl = document.getElementById(uid + '-result');
2209
- _nodeEls[node.id] = { container: containerEl, resultEl, uid };
2210
-
2211
- if (node.card_data && node.card_data.status === 'error' && node.card_data.error) {
2212
- 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>`;
2213
- } else {
2214
- _renderElements(node, resultEl);
2215
- }
2216
-
2217
- // ---- Wire refresh ----
2218
- const refreshBtn = document.getElementById(uid + '-refresh');
2219
- if (refreshBtn && cfg.onRefresh) {
2220
- refreshBtn.addEventListener('click', e => {
2221
- e.stopPropagation();
2222
- refreshBtn.disabled = true;
2223
- cfg.onRefresh(node.id);
2224
- }, { signal });
2225
- }
2226
-
2227
- const chatBtn = document.getElementById(uid + '-chat-open');
2228
- if (chatBtn) {
2229
- chatBtn.addEventListener('click', (e) => {
2230
- e.stopPropagation();
2231
- openChatModal(node.id);
2232
- }, { signal });
2233
- }
2234
-
2235
- const filesBtn = document.getElementById(uid + '-files-open');
2236
- if (filesBtn) {
2237
- filesBtn.addEventListener('click', (e) => {
2238
- e.stopPropagation();
2239
- openFilesModal(node.id);
2240
- }, { signal });
2241
- }
2242
-
2243
- _autoSubscribe(node);
2244
- }
2245
-
2246
- // ===========================================================================
2247
- // In-place update
2248
- // ===========================================================================
2249
-
2250
- function update(nodeId, patch) {
2251
- const info = _nodeEls[nodeId];
2252
- if (!info) return;
2253
-
2254
- const refreshBtn = document.getElementById(info.uid + '-refresh');
2255
- if (refreshBtn) refreshBtn.disabled = false;
2256
-
2257
- // Update status dot
2258
- if (patch.status) {
2259
- const dot = info.container.querySelector('.lc-status-dot');
2260
- if (dot) {
2261
- const c = { fresh: 'var(--bs-success)', stale: 'var(--bs-warning)', error: 'var(--bs-danger)', loading: 'var(--bs-info)' };
2262
- dot.style.background = c[patch.status] || 'var(--bs-secondary)';
2263
- dot.title = patch.status;
2264
- }
2265
- }
2266
-
2267
- if (patch.lastRun) {
2268
- const ts = info.container.querySelector('.lc-status-dot + .text-muted');
2269
- if (ts) ts.textContent = _timeAgo(patch.lastRun);
2270
- }
2271
-
2272
- // Merge into node card_data
2273
- const node = cfg.resolve(nodeId);
2274
- if (!node) return;
2275
- if (!node.card_data) node.card_data = {};
2276
- if (patch.status) node.card_data.status = patch.status;
2277
- if (patch.lastRun) node.card_data.lastRun = patch.lastRun;
2278
- if (patch.error !== undefined) node.card_data.error = patch.error;
2279
- if (patch.files !== undefined) node.card_data.files = Array.isArray(patch.files) ? patch.files : [];
2280
-
2281
- // Keep files count inline inside the files button in the header.
2282
- const filesBtn = document.getElementById(info.uid + '-files-open');
2283
- const fileCount = Array.isArray(node.card_data.files) ? node.card_data.files.length : 0;
2284
- if (filesBtn) {
2285
- filesBtn.title = fileCount > 0 ? ('Files (' + fileCount + ')') : 'Files';
2286
- 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>') : '');
2287
- }
2288
-
2289
- // Remove legacy external count label if present from older renders.
2290
- const filesCountEl = document.getElementById(info.uid + '-files-count');
2291
- if (filesCountEl && filesCountEl.parentNode) filesCountEl.parentNode.removeChild(filesCountEl);
2292
-
2293
- // Update inference status bar (tick / hourglass) if card_data changed
2294
- const infBar = info.container.querySelector('.lc-inference-bar');
2295
- if (infBar) {
2296
- const infData = node.card_data && node.card_data.llm_task_completion_inference;
2297
- const done = !!(infData && infData.isTaskCompleted);
2298
- const iconEl = infBar.querySelector('.lc-inference-icon');
2299
- if (iconEl) {
2300
- iconEl.title = done ? 'Task completed' : 'Awaiting inference';
2301
- iconEl.style.color = done ? '#198754' : '#aaa';
2302
- iconEl.innerHTML = done ? '&#x25CF;' : '&#x25CB;';
2303
- }
2304
- }
2305
-
2306
- if (node.card_data.status === 'error' && node.card_data.error) {
2307
- 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>`;
2308
- } else {
2309
- _renderElements(node, info.resultEl);
2310
- }
2311
- }
2312
-
2313
- // ===========================================================================
2314
- // Lifecycle
2315
- // ===========================================================================
2316
-
2317
- function destroy(nodeId) {
2318
- const c = _cleanup[nodeId];
2319
- if (c) {
2320
- c.ac.abort();
2321
- c.timers.forEach(t => clearTimeout(t));
2322
- c.charts.forEach(ch => { try { ch.inst.destroy(); } catch (_) {} });
2323
- if (c.unsubs) c.unsubs.forEach(u => u());
2324
- delete _cleanup[nodeId];
2325
- }
2326
- delete _nodeEls[nodeId];
2327
- }
2328
-
2329
- function destroyAll() {
2330
- Object.keys(_cleanup).forEach(destroy);
2331
- }
2332
-
2333
- // ===========================================================================
2334
- // Chat
2335
- // ===========================================================================
2336
-
2337
- function appendChatMessage(nodeId, role, text) {
2338
- if (_chatModal.currentNodeId !== nodeId) return;
2339
- _appendModalChatMessage(role, text, []);
2340
- }
2341
-
2342
- function refreshOpenChatModal() {
2343
- const nodeId = _chatModal.currentNodeId;
2344
- if (!nodeId || !_chatModal.backdrop || !_chatModal.backdrop.classList.contains('lc-open')) return;
2345
- _refreshModalChatHistory(nodeId).catch(function () {});
2346
- }
2347
-
2348
- function onServerSseEvent() {
2349
- const nodeId = _chatModal.currentNodeId;
2350
- if (!nodeId || !_chatModal.backdrop || !_chatModal.backdrop.classList.contains('lc-open')) return;
2351
- _clearPendingModalChatMessages();
2352
- _syncProcessingBar(nodeId);
2353
- _refreshModalChatHistory(nodeId).catch(function () {});
2354
- }
2355
-
2356
- // ===========================================================================
2357
- // Element access
2358
- // ===========================================================================
2359
-
2360
- function getElement(nodeId, elemId) {
2361
- const info = _nodeEls[nodeId];
2362
- return (info && info.elements && info.elements[elemId]) || null;
2363
- }
2364
-
2365
- // ===========================================================================
2366
- // Return engine
2367
- // ===========================================================================
2368
-
2369
- return {
2370
- render,
2371
- update,
2372
- destroy,
2373
- destroyAll,
2374
- notify,
2375
- subscribe,
2376
- appendChatMessage,
2377
- refreshOpenChatModal,
2378
- onServerSseEvent,
2379
- openChatModal,
2380
- openFilesModal,
2381
- getElement,
2382
- registerRenderer(name, fn) { _renderers[name] = fn; },
2383
- renderers: _renderers,
2384
- };
2385
- }
2386
-
2387
- // ===========================================================================
2388
- // BoardCore — imperative grid (board) and DAG (canvas) modes.
2389
- // Most callers should use Board (reactive wrapper) instead.
2390
- // ===========================================================================
2391
-
2392
- function BoardCore(engine, containerEl, opts) {
2393
- opts = opts || {};
2394
- const mode = { current: opts.mode || 'board' };
2395
- const devMode = { current: opts.devMode || false };
2396
- const nodeList = [];
2397
- const nodeMap = {}; // id → { node, colEl, bodyEl }
2398
- const _positions = {}; // id → { x, y, w, h } for canvas mode
2399
- const showChat = opts.showChat || false;
2400
- const defaultCol = opts.defaultCol || 6;
2401
-
2402
- // Canvas config
2403
- const co = opts.canvas || {};
2404
- const cvs = {
2405
- snap: co.snap || 20,
2406
- zoomMin: (co.zoom && co.zoom.min) || 0.25,
2407
- zoomMax: (co.zoom && co.zoom.max) || 2,
2408
- zoom: (co.zoom && co.zoom.initial) || 1,
2409
- edges: co.edges !== false,
2410
- minWidth: co.minWidth || 220,
2411
- maxWidth: co.maxWidth || 450,
2412
- defaultW: co.defaultW || 350,
2413
- gapX: co.gapX || 280,
2414
- gapY: co.gapY || 320,
2415
- padX: co.padX || 20,
2416
- padY: co.padY || 20,
2417
- cardMaxH: co.cardMaxH || 300,
2418
- panX: 0, panY: 0,
2419
- };
2420
- const ac = new AbortController();
2421
- const signal = ac.signal;
2422
- const _edges = []; // LeaderLine instances for canvas edges
2423
-
2424
- // Edge style config (from canvas opts)
2425
- const edgeOpts = co.edgeStyle || {};
2426
- const edgeCfg = {
2427
- color: edgeOpts.color || 'rgba(108, 117, 125, 0.6)',
2428
- size: edgeOpts.size || 2,
2429
- dash: edgeOpts.dash !== false,
2430
- animation: edgeOpts.animation !== false,
2431
- endPlug: edgeOpts.endPlug || 'arrow1',
2432
- };
2433
-
2434
- // DOM containers
2435
- const root = document.createElement('div');
2436
- root.className = 'lc-board';
2437
- containerEl.appendChild(root);
2438
-
2439
- const gridEl = document.createElement('div');
2440
- gridEl.className = 'row g-3 lc-board-grid';
2441
-
2442
- const canvasEl = document.createElement('div');
2443
- canvasEl.className = 'lc-canvas';
2444
- canvasEl.style.cssText = 'position:relative;overflow:auto;width:100%;';
2445
- const canvasInner = document.createElement('div');
2446
- canvasInner.className = 'lc-canvas-inner';
2447
- canvasInner.style.cssText = 'position:relative;transform-origin:0 0;min-width:100%;min-height:100%;';
2448
- canvasEl.appendChild(canvasInner);
2449
-
2450
- // SVG overlay for edges
2451
- const svgEl = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
2452
- svgEl.setAttribute('class', 'lc-canvas-edges');
2453
- svgEl.style.cssText = 'position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none;overflow:hidden;z-index:0;';
2454
- const defs = document.createElementNS('http://www.w3.org/2000/svg', 'defs');
2455
- 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>';
2456
- svgEl.appendChild(defs);
2457
- canvasInner.appendChild(svgEl);
2458
-
2459
- // Board/canvas CSS
2460
- if (!document.getElementById('lc-board-css')) {
2461
- const s = document.createElement('style');
2462
- s.id = 'lc-board-css';
2463
- s.textContent = `
2464
- .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}">&times;</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}">&times;</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">&#x26A0;&#xFE0E;</span>':p==="not-started"?n='<span style="font-size:.75rem;line-height:1;flex-shrink:0" class="text-muted">&#x25CB;</span>':p==="inactivated"?n='<span style="font-size:.75rem;line-height:1;flex-shrink:0" class="text-muted">&#x2296;</span>':n='<span style="font-size:.75rem;line-height:1;flex-shrink:0" class="text-muted">&#x2013;</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">&#x25CF;</span>':s+='<span class="lc-inference-icon" style="color:#aaa;font-size:.75rem;line-height:1.4;flex-shrink:0" title="Awaiting inference">&#x25CB;</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?"&#x25CF;":"&#x25CB;");}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; }
2465
95
  .lc-canvas-card.lc-dragging { cursor:grabbing; z-index:10; box-shadow:0 8px 24px rgba(0,0,0,0.18)!important; }
2466
96
  .lc-canvas-card .card-body { overflow:hidden; }
2467
97
  .lc-canvas-card.lc-resizing { cursor:nwse-resize; z-index:10; }
@@ -2473,930 +103,10 @@ var LiveCard = (function () {
2473
103
  @keyframes lc-edge-flow { to { stroke-dashoffset:-10; } }
2474
104
  .lc-source-node { position:absolute; cursor:grab; user-select:none; z-index:1; }
2475
105
  .lc-source-node.lc-dragging { cursor:grabbing; z-index:10; }
2476
- `;
2477
- document.head.appendChild(s);
2478
- }
2479
-
2480
- // ---- Helpers ----
2481
-
2482
- function _colWidth(node) {
2483
- const view = node && node.card ? node.card.view : null;
2484
- if (view && view.layout && view.layout.board && view.layout.board.col) return view.layout.board.col;
2485
- return defaultCol;
2486
- }
2487
-
2488
- function _initPositions() {
2489
- const explicit = opts.positions || {};
2490
- nodeList.forEach((node, i) => {
2491
- if (_positions[node.id]) return; // already set
2492
- if (explicit[node.id]) {
2493
- _positions[node.id] = Object.assign({}, explicit[node.id]);
2494
- } else if (node.card && node.card.view && node.card.view.layout && node.card.view.layout.canvas && node.card.view.layout.canvas.x != null) {
2495
- _positions[node.id] = Object.assign({}, node.card.view.layout.canvas);
2496
- } else {
2497
- const col = (i % 4);
2498
- const row = Math.floor(i / 4);
2499
- _positions[node.id] = { x: col * cvs.gapX + cvs.padX, y: row * cvs.gapY + cvs.padY, w: cvs.defaultW };
2500
- }
2501
- });
2502
- }
2503
-
2504
- function _getRequires(node) {
2505
- return (node && node.card && Array.isArray(node.card.requires)) ? node.card.requires : [];
2506
- }
2507
-
2508
- /**
2509
- * Returns tokens this node provides.
2510
- * Explicit: card.provides[].bindTo
2511
- * Implicit default: the node's own id (if no provides declared)
2512
- */
2513
- function _getProvides(node) {
2514
- if (!node || !node.card) return [node ? node.id : ''];
2515
- if (Array.isArray(node.card.provides) && node.card.provides.length > 0) {
2516
- return node.card.provides.map(function(p) { return (typeof p === 'string') ? p : (p.bindTo || p); });
2517
- }
2518
- // Default: node provides a token equal to its own id
2519
- return [node.id];
2520
- }
2521
-
2522
- /**
2523
- * Build token → provider nodeId map from all nodes in the board.
2524
- * Called before drawing edges so we can resolve requires tokens → source nodes.
2525
- */
2526
- function _buildTokenMap() {
2527
- var map = {};
2528
- nodeList.forEach(function(node) {
2529
- _getProvides(node).forEach(function(token) {
2530
- map[token] = node.id;
2531
- });
2532
- });
2533
- return map;
2534
- }
2535
-
2536
- /**
2537
- * Resolve required tokens to provider node IDs.
2538
- * Returns deduplicated array of source node IDs for a given consumer node.
2539
- */
2540
- function _resolveEdgeSources(node, tokenMap) {
2541
- var sources = [];
2542
- var seen = {};
2543
- _getRequires(node).forEach(function(token) {
2544
- var srcId = tokenMap[token];
2545
- if (srcId && !seen[srcId]) {
2546
- seen[srcId] = true;
2547
- sources.push(srcId);
2548
- }
2549
- });
2550
- return sources;
2551
- }
2552
-
2553
- function _showCardInspector(node) {
2554
- const modal = document.createElement('div');
2555
- modal.className = 'modal d-block';
2556
- 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;';
2557
-
2558
- const dialog = document.createElement('div');
2559
- dialog.className = 'modal-dialog';
2560
- dialog.style.cssText = 'width: 92%; max-width: 980px; max-height: 88vh; overflow: auto;';
2561
-
2562
- const content = document.createElement('div');
2563
- content.className = 'modal-content';
2564
-
2565
- const header = document.createElement('div');
2566
- header.className = 'modal-header';
2567
- 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>`;
2568
-
2569
- const closeModal = function () { modal.remove(); };
2570
- header.querySelector('.btn-close').addEventListener('click', closeModal);
2571
-
2572
- const body = document.createElement('div');
2573
- body.className = 'modal-body';
2574
- body.style.cssText = 'max-height: 64vh; overflow-y: auto;';
2575
-
2576
- const cardSection = document.createElement('div');
2577
- cardSection.className = 'mb-4';
2578
- cardSection.innerHTML = '<h6 class="fw-semibold mb-2">Card Definition (Read-only)</h6>';
2579
- const cardDef = (node && node.card) ? node.card : {};
2580
- 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>`;
2581
- body.appendChild(cardSection);
2582
-
2583
- const computedSection = document.createElement('div');
2584
- computedSection.className = 'mb-4';
2585
- computedSection.innerHTML = '<h6 class="fw-semibold mb-2">Computed Values (Read-only)</h6>';
2586
- const computedValues = node.computed_values || {};
2587
- 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>`;
2588
- body.appendChild(computedSection);
2589
-
2590
- const requiresSection = document.createElement('div');
2591
- requiresSection.className = 'mb-4';
2592
- requiresSection.innerHTML = '<h6 class="fw-semibold mb-2">Requires (Read-only)</h6>';
2593
- const requiresData = node.requires || {};
2594
- 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>`;
2595
- body.appendChild(requiresSection);
2596
-
2597
- const stateSection = document.createElement('div');
2598
- stateSection.className = 'mb-2';
2599
- stateSection.innerHTML = '<h6 class="fw-semibold mb-2">Runtime Status (Read-only)</h6>';
2600
- 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 };
2601
- 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>`;
2602
- body.appendChild(stateSection);
2603
-
2604
- const footer = document.createElement('div');
2605
- footer.className = 'modal-footer';
2606
- const closeBtn = document.createElement('button');
2607
- closeBtn.type = 'button';
2608
- closeBtn.className = 'btn btn-secondary';
2609
- closeBtn.textContent = 'Close';
2610
- closeBtn.addEventListener('click', closeModal);
2611
-
2612
- footer.appendChild(closeBtn);
2613
- content.appendChild(header);
2614
- content.appendChild(body);
2615
- content.appendChild(footer);
2616
- dialog.appendChild(content);
2617
- modal.appendChild(dialog);
2618
- document.body.appendChild(modal);
2619
- }
2620
-
2621
- function _buildCardWrapper(node) {
2622
- const wrap = document.createElement('div');
2623
- const card = node && node.card ? node.card : {};
2624
- const isSimulation = card.meta && card.meta.simulation === true;
2625
- const isGandalfCard = card.meta && card.meta._gandalfCard === true;
2626
- const isRunning = node && node.runtime_state && node.runtime_state.task_status === 'running';
2627
- const extraClass = isSimulation ? ' lc-simulation-card' : (isGandalfCard ? ' lc-gandalf-card' : '');
2628
- wrap.className = 'card shadow-sm h-100' + extraClass + (isRunning ? ' lc-running' : '');
2629
- const header = document.createElement('div');
2630
- header.className = 'card-header d-flex align-items-center gap-2 py-2';
2631
- const title = (card.meta && card.meta.title) || node.id;
2632
- const tags = (card.meta && card.meta.tags) || [];
2633
- let badgeHtml = '';
2634
- if ((card.source_defs && card.source_defs.length) && !card.view) {
2635
- var src = card.source_defs[0] || {};
2636
- badgeHtml = '<span class="badge bg-info text-dark ms-auto">' + _esc(src.kind || 'source') + '</span>';
2637
- } else if (tags.length) {
2638
- badgeHtml = tags.map(t => '<span class="badge bg-secondary ms-1">' + _esc(t) + '</span>').join('');
2639
- }
2640
- header.innerHTML = '<strong class="small">' + _esc(title) + '</strong>' + badgeHtml;
2641
-
2642
- // Gandalf cards: collapsible via caret — caret gets its own click listener,
2643
- // header is left alone for dragging in canvas mode.
2644
- if (isGandalfCard) {
2645
- const caret = document.createElement('span');
2646
- caret.className = 'lc-gandalf-caret';
2647
- caret.title = 'Collapse / expand';
2648
- 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>';
2649
- header.appendChild(caret);
2650
-
2651
- const storageKey = 'lc-gandalf-collapsed:' + (node.id || title);
2652
- if (sessionStorage.getItem(storageKey) === '1') {
2653
- wrap.classList.add('lc-collapsed');
2654
- header.dataset.gandalfCollapsed = '1';
2655
- }
2656
-
2657
- caret.addEventListener('click', function(e) {
2658
- e.stopPropagation();
2659
- const cardEl = caret.closest('.lc-gandalf-card') || wrap;
2660
- cardEl.classList.toggle('lc-collapsed');
2661
- sessionStorage.setItem(storageKey, cardEl.classList.contains('lc-collapsed') ? '1' : '0');
2662
- });
2663
- caret.addEventListener('pointerdown', e => e.stopPropagation()); // prevent drag start
2664
- }
2665
- if (isSimulation) {
2666
- const simBtns = document.createElement('span');
2667
- simBtns.className = 'd-inline-flex align-items-center gap-1 ms-auto';
2668
-
2669
- const pinBtn = document.createElement('button');
2670
- pinBtn.className = 'btn btn-sm btn-outline-success lc-sim-pin';
2671
- pinBtn.style.cssText = 'padding: 2px 6px;';
2672
- 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>';
2673
- pinBtn.title = 'Pin this simulation card';
2674
- pinBtn.dataset.nodeId = node.id;
2675
-
2676
- const discardBtn = document.createElement('button');
2677
- discardBtn.className = 'btn btn-sm btn-outline-danger lc-sim-discard';
2678
- discardBtn.style.cssText = 'padding: 2px 6px;';
2679
- 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>';
2680
- discardBtn.title = 'Discard this simulation card';
2681
- discardBtn.dataset.nodeId = node.id;
2682
-
2683
- simBtns.appendChild(pinBtn);
2684
- simBtns.appendChild(discardBtn);
2685
- header.appendChild(simBtns);
2686
- }
2687
-
2688
- // Add dev mode code icon button if devMode is enabled
2689
- if (devMode.current) {
2690
- const codeBtn = document.createElement('button');
2691
- codeBtn.className = 'btn btn-sm btn-outline-secondary';
2692
- codeBtn.style.cssText = 'padding: 2px 6px;' + (isSimulation ? '' : ' margin-left: auto;');
2693
- codeBtn.innerHTML = '&lt;/&gt;';
2694
- codeBtn.title = 'Inspect card data';
2695
- codeBtn.addEventListener('click', function(e) {
2696
- e.stopPropagation();
2697
- _showCardInspector(node);
2698
- });
2699
- header.appendChild(codeBtn);
2700
- }
2701
-
2702
- const body = document.createElement('div');
2703
- body.className = 'card-body p-2';
2704
-
2705
- // Token gem rows — requires gems above header, provides gems below body
2706
- const requiresTokens = (card.requires && Array.isArray(card.requires)) ? card.requires : [];
2707
- const providesTokens = (Array.isArray(card.provides) && card.provides.length)
2708
- ? card.provides.map(function(p) { return typeof p === 'string' ? p : (p.bindTo || p); })
2709
- : [node.id];
2710
-
2711
- // Requires gems — top of card (above header)
2712
- if (requiresTokens.length) {
2713
- const reqRow = document.createElement('div');
2714
- reqRow.className = 'lc-token-row lc-token-row-requires';
2715
- requiresTokens.forEach(function(token) {
2716
- const gem = document.createElement('span');
2717
- gem.className = 'lc-token-gem lc-token-gem-requires';
2718
- gem.dataset.token = token;
2719
- gem.title = token;
2720
- reqRow.appendChild(gem);
2721
- });
2722
- wrap.appendChild(reqRow);
2723
- }
2724
-
2725
- wrap.appendChild(header);
2726
- wrap.appendChild(body);
2727
-
2728
- // Provides gems — bottom of card (below body)
2729
- if (providesTokens.length) {
2730
- const provRow = document.createElement('div');
2731
- provRow.className = 'lc-token-row lc-token-row-provides';
2732
- providesTokens.forEach(function(token) {
2733
- const gem = document.createElement('span');
2734
- gem.className = 'lc-token-gem lc-token-gem-provides';
2735
- gem.dataset.token = token;
2736
- gem.title = token;
2737
- provRow.appendChild(gem);
2738
- });
2739
- wrap.appendChild(provRow);
2740
- }
2741
-
2742
- return { wrap, header, body };
2743
- }
2744
-
2745
- function _buildSourcePill(node) {
2746
- const el = document.createElement('div');
2747
- el.className = 'lc-source-node';
2748
- const status = (node.card_data && node.card_data.status) || 'fresh';
2749
- const card = node && node.card ? node.card : {};
2750
- const title = (card.meta && card.meta.title) || node.id;
2751
- const kind = (card.source_defs && card.source_defs[0] && card.source_defs[0].kind) || 'source';
2752
- el.innerHTML = `<div class="lc-source-pill shadow-sm">
2753
- ${_statusDot(status)}
2754
- <span class="fw-medium">${_esc(title)}</span>
2755
- <span class="badge bg-info text-dark">${_esc(kind)}</span>
2756
- </div>`;
2757
- return el;
2758
- }
2759
-
2760
- // ---- Board mode ----
2761
-
2762
- // Compute canvas inner size from card positions + padding
2763
- function _fitCanvasToContent() {
2764
- var pad = 100;
2765
- var maxR = 0, maxB = 0;
2766
- canvasInner.querySelectorAll('.lc-canvas-card,.lc-source-node').forEach(function(el) {
2767
- var r = el.offsetLeft + el.offsetWidth;
2768
- var b = el.offsetTop + el.offsetHeight;
2769
- if (r > maxR) maxR = r;
2770
- if (b > maxB) maxB = b;
2771
- });
2772
- canvasInner.style.width = (maxR + pad) + 'px';
2773
- canvasInner.style.height = (maxB + pad) + 'px';
2774
- }
2775
-
2776
- function _renderBoard() {
2777
- _destroyEdges();
2778
- document.body.style.overflow = '';
2779
- root.innerHTML = '';
2780
- root.appendChild(gridEl);
2781
- gridEl.innerHTML = '';
2782
-
2783
- // Only card nodes in board mode, sorted by order
2784
- const cards = nodeList.filter(n => n.card && n.card.view).slice();
2785
- cards.sort((a, b) => {
2786
- const ao = (a.card && a.card.view && a.card.view.layout && a.card.view.layout.board && a.card.view.layout.board.order) || 0;
2787
- const bo = (b.card && b.card.view && b.card.view.layout && b.card.view.layout.board && b.card.view.layout.board.order) || 0;
2788
- return ao - bo;
2789
- });
2790
-
2791
- cards.forEach(node => {
2792
- const col = document.createElement('div');
2793
- col.className = 'col-12 col-md-' + _colWidth(node);
2794
- col.dataset.nodeId = node.id;
2795
- const { wrap, body } = _buildCardWrapper(node);
2796
- col.appendChild(wrap);
2797
- gridEl.appendChild(col);
2798
- nodeMap[node.id] = { node, colEl: col, bodyEl: body };
2799
- engine.render(node, body, { showChat });
2800
- });
2801
- _updateTokenAvailability();
2802
- }
2803
-
2804
- // ---- Canvas mode ----
2805
-
2806
- function _applyTransform() {
2807
- canvasInner.style.transform = `translate(${cvs.panX}px,${cvs.panY}px) scale(${cvs.zoom})`;
2808
- }
2809
-
2810
- /**
2811
- * Update token badge availability: a provides badge turns green when the
2812
- * node has data; a requires badge turns green when the upstream provider
2813
- * has data for that token.
2814
- */
2815
- function _updateTokenAvailability() {
2816
- var tokenMap = _buildTokenMap();
2817
- // A node "has data" when card_data or computed_values is non-empty, or status is fresh/completed.
2818
- var nodeHasData = {};
2819
- nodeList.forEach(function(node) {
2820
- var cd = node.card_data || (node.card && node.card.card_data);
2821
- var cv = node.computed_values;
2822
- var status = cd && cd.status;
2823
- var hasOutput = (cd && Object.keys(cd).length > 0) || (cv && Object.keys(cv).length > 0);
2824
- nodeHasData[node.id] = hasOutput || status === 'fresh' || status === 'completed';
2825
- });
2826
-
2827
- // Update all gem elements in root container
2828
- var allGems = root.querySelectorAll('.lc-token-gem');
2829
- allGems.forEach(function(gem) {
2830
- var token = gem.dataset.token;
2831
- if (!token) return;
2832
- if (gem.classList.contains('lc-token-gem-provides')) {
2833
- // The provides gem: green if this node has data
2834
- var nodeEl = gem.closest('[data-node-id]');
2835
- var nId = nodeEl && nodeEl.dataset.nodeId;
2836
- gem.classList.toggle('lc-token-available', !!(nId && nodeHasData[nId]));
2837
- } else if (gem.classList.contains('lc-token-gem-requires')) {
2838
- // The requires gem: green if the upstream provider for this token has data
2839
- var srcId = tokenMap[token];
2840
- gem.classList.toggle('lc-token-available', !!(srcId && nodeHasData[srcId]));
2841
- }
2842
- });
2843
- }
2844
-
2845
- function _destroyEdges() {
2846
- _edges.forEach(function(line) { try { line.remove(); } catch(e) { /* noop */ } });
2847
- _edges.length = 0;
2848
- }
2849
-
2850
- function _repositionEdges() {
2851
- _edges.forEach(function(line) { try { line.position(); } catch(e) { /* noop */ } });
2852
- }
2853
-
2854
- function _drawEdges() {
2855
- _destroyEdges();
2856
- svgEl.querySelectorAll('line,path').forEach(function(el) { el.remove(); });
2857
- if (!cvs.edges) return;
2858
-
2859
- // Build token → provider nodeId map
2860
- var tokenMap = _buildTokenMap();
2861
-
2862
- // SVG edges — rendered behind cards (z-index:0) for a clean look
2863
- nodeList.forEach(function(node) {
2864
- var tgtInfo = nodeMap[node.id];
2865
- if (!tgtInfo || !tgtInfo.colEl) return;
2866
- _getRequires(node).forEach(function(token) {
2867
- var srcId = tokenMap[token];
2868
- if (!srcId) return;
2869
- var srcInfo = nodeMap[srcId];
2870
- if (!srcInfo || !srcInfo.colEl) return;
2871
- // Locate gems; fall back to card element if gem not found
2872
- var srcGem = srcInfo.colEl.querySelector('.lc-token-gem-provides[data-token="' + token + '"]');
2873
- var tgtGem = tgtInfo.colEl.querySelector('.lc-token-gem-requires[data-token="' + token + '"]');
2874
- var sx, sy, tx, ty;
2875
- var innerRect = canvasInner.getBoundingClientRect();
2876
- if (srcGem) {
2877
- var srcRect = srcGem.getBoundingClientRect();
2878
- sx = (srcRect.left + srcRect.width / 2 - innerRect.left) / cvs.zoom;
2879
- sy = (srcRect.bottom - innerRect.top) / cvs.zoom;
2880
- } else {
2881
- var sEl = srcInfo.colEl;
2882
- sx = sEl.offsetLeft + sEl.offsetWidth / 2;
2883
- sy = sEl.offsetTop + sEl.offsetHeight;
2884
- }
2885
- if (tgtGem) {
2886
- var tgtRect = tgtGem.getBoundingClientRect();
2887
- tx = (tgtRect.left + tgtRect.width / 2 - innerRect.left) / cvs.zoom;
2888
- ty = (tgtRect.top - innerRect.top) / cvs.zoom;
2889
- } else {
2890
- var tEl = tgtInfo.colEl;
2891
- tx = tEl.offsetLeft + tEl.offsetWidth / 2;
2892
- ty = tEl.offsetTop;
2893
- }
2894
- // Route bezier curves around cards — offset control points outward
2895
- var dx = tx - sx;
2896
- var dy = ty - sy;
2897
- var dist = Math.sqrt(dx * dx + dy * dy);
2898
- var cpLen = Math.max(40, Math.min(dist * 0.4, 120));
2899
- // Determine if src is roughly above, below, left, or right of target
2900
- var absDx = Math.abs(dx);
2901
- var absDy = Math.abs(dy);
2902
- var cp1x, cp1y, cp2x, cp2y;
2903
- if (absDy > absDx * 0.4) {
2904
- // Mostly vertical — curve control points go straight down/up
2905
- cp1x = sx; cp1y = sy + cpLen;
2906
- cp2x = tx; cp2y = ty - cpLen;
2907
- } else {
2908
- // Mostly horizontal — swing control points outward to avoid overlapping cards
2909
- var sideSign = dx > 0 ? 1 : -1;
2910
- cp1x = sx + sideSign * cpLen; cp1y = sy + cpLen * 0.5;
2911
- cp2x = tx - sideSign * cpLen; cp2y = ty - cpLen * 0.5;
2912
- }
2913
- var d = 'M ' + sx + ' ' + sy + ' C ' + cp1x + ' ' + cp1y + ', ' + cp2x + ' ' + cp2y + ', ' + tx + ' ' + ty;
2914
- var path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
2915
- path.setAttribute('d', d);
2916
- path.setAttribute('fill', 'none');
2917
- path.setAttribute('marker-end', 'url(#lc-arrow)');
2918
- path.classList.add('lc-edge-path');
2919
- svgEl.appendChild(path);
2920
- });
2921
- });
2922
- }
2923
-
2924
- function _makeDraggable(el, node) {
2925
- let startX, startY, origX, origY, dragging = false;
2926
-
2927
- el.addEventListener('pointerdown', e => {
2928
- if (e.button !== 0) return;
2929
- if (e.target.closest('input,textarea,select,button,a,.form-check-input')) return;
2930
- dragging = true;
2931
- el.classList.add('lc-dragging');
2932
- el.setPointerCapture(e.pointerId);
2933
- startX = e.clientX; startY = e.clientY;
2934
- origX = el.offsetLeft; origY = el.offsetTop;
2935
- e.preventDefault();
2936
- }, { signal });
2937
-
2938
- el.addEventListener('pointermove', e => {
2939
- if (!dragging) return;
2940
- const dx = (e.clientX - startX) / cvs.zoom;
2941
- const dy = (e.clientY - startY) / cvs.zoom;
2942
- el.style.left = (origX + dx) + 'px';
2943
- el.style.top = (origY + dy) + 'px';
2944
- if (_edges.length) _repositionEdges();
2945
- else _drawEdges();
2946
- }, { signal });
2947
-
2948
- el.addEventListener('pointerup', () => {
2949
- if (!dragging) return;
2950
- dragging = false;
2951
- el.classList.remove('lc-dragging');
2952
- let x = el.offsetLeft, y = el.offsetTop;
2953
- if (cvs.snap > 1) { x = Math.round(x / cvs.snap) * cvs.snap; y = Math.round(y / cvs.snap) * cvs.snap; }
2954
- el.style.left = x + 'px'; el.style.top = y + 'px';
2955
- // Persist
2956
- _positions[node.id] = Object.assign(_positions[node.id] || {}, { x, y });
2957
- if (node.card && node.card.view) {
2958
- if (!node.card.view.layout) node.card.view.layout = {};
2959
- if (!node.card.view.layout.canvas) node.card.view.layout.canvas = {};
2960
- node.card.view.layout.canvas.x = x;
2961
- node.card.view.layout.canvas.y = y;
2962
- }
2963
- engine.notify(node.id);
2964
- _fitCanvasToContent();
2965
- if (_edges.length) _repositionEdges();
2966
- else _drawEdges();
2967
- }, { signal });
2968
- }
2969
-
2970
- function _makeResizable(el, node) {
2971
- const handle = document.createElement('div');
2972
- handle.className = 'lc-resize-handle';
2973
- el.appendChild(handle);
2974
- el.style.overflow = 'visible';
2975
-
2976
- let resizing = false, startX, startY, origW, origH;
2977
-
2978
- handle.addEventListener('pointerdown', function(e) {
2979
- if (e.button !== 0) return;
2980
- e.stopPropagation();
2981
- e.preventDefault();
2982
- resizing = true;
2983
- el.classList.add('lc-resizing');
2984
- handle.setPointerCapture(e.pointerId);
2985
- startX = e.clientX;
2986
- startY = e.clientY;
2987
- origW = el.offsetWidth;
2988
- origH = el.offsetHeight;
2989
- }, { signal });
2990
-
2991
- handle.addEventListener('pointermove', function(e) {
2992
- if (!resizing) return;
2993
- const dw = (e.clientX - startX) / cvs.zoom;
2994
- const dh = (e.clientY - startY) / cvs.zoom;
2995
- const newW = Math.max(cvs.minWidth, origW + dw);
2996
- const newH = Math.max(80, origH + dh);
2997
- el.style.width = newW + 'px';
2998
- el.style.height = newH + 'px';
2999
- if (_edges.length) _repositionEdges();
3000
- else _drawEdges();
3001
- }, { signal });
3002
-
3003
- handle.addEventListener('pointerup', function() {
3004
- if (!resizing) return;
3005
- resizing = false;
3006
- el.classList.remove('lc-resizing');
3007
- const w = el.offsetWidth;
3008
- const h = el.offsetHeight;
3009
- // Snap to grid
3010
- const sw = cvs.snap > 1 ? Math.round(w / cvs.snap) * cvs.snap : w;
3011
- const sh = cvs.snap > 1 ? Math.round(h / cvs.snap) * cvs.snap : h;
3012
- el.style.width = sw + 'px';
3013
- el.style.height = sh + 'px';
3014
- // Persist dimensions
3015
- _positions[node.id] = Object.assign(_positions[node.id] || {}, { w: sw, h: sh });
3016
- if (node.card && node.card.view) {
3017
- if (!node.card.view.layout) node.card.view.layout = {};
3018
- if (!node.card.view.layout.canvas) node.card.view.layout.canvas = {};
3019
- node.card.view.layout.canvas.w = sw;
3020
- node.card.view.layout.canvas.h = sh;
3021
- }
3022
- engine.notify(node.id);
3023
- _fitCanvasToContent();
3024
- if (_edges.length) _repositionEdges();
3025
- else _drawEdges();
3026
- }, { signal });
3027
- }
3028
-
3029
- function _renderCanvas() {
3030
- _destroyEdges();
3031
- document.body.style.overflow = 'hidden';
3032
- root.innerHTML = '';
3033
- root.appendChild(canvasEl);
3034
- // Fill remaining viewport height
3035
- var top = canvasEl.getBoundingClientRect().top;
3036
- canvasEl.style.height = co.height || ('calc(100vh - ' + top + 'px)');
3037
- canvasInner.querySelectorAll('.lc-canvas-card,.lc-source-node').forEach(el => el.remove());
3038
- svgEl.querySelectorAll('line,path').forEach(function(el) { el.remove(); });
3039
- _initPositions();
3040
- _applyTransform();
3041
-
3042
- nodeList.forEach(node => {
3043
- const pos = _positions[node.id] || { x: 0, y: 0 };
3044
-
3045
- if ((!node.card || !node.card.view) && (node.card && node.card.source_defs && node.card.source_defs.length)) {
3046
- const el = _buildSourcePill(node);
3047
- el.dataset.nodeId = node.id;
3048
- el.style.left = pos.x + 'px';
3049
- el.style.top = pos.y + 'px';
3050
- canvasInner.appendChild(el);
3051
- nodeMap[node.id] = { node, colEl: el, bodyEl: null };
3052
- _makeDraggable(el, node);
3053
- } else {
3054
- const el = document.createElement('div');
3055
- const isSimCanvas = node.card && node.card.meta && node.card.meta.simulation === true;
3056
- const isGandalfCanvas = node.card && node.card.meta && node.card.meta._gandalfCard === true;
3057
- const canvasExtra = isSimCanvas ? ' lc-simulation-card' : (isGandalfCanvas ? ' lc-gandalf-card' : '');
3058
- el.className = 'lc-canvas-card card shadow-sm' + canvasExtra;
3059
- el.dataset.nodeId = node.id;
3060
- el.style.left = pos.x + 'px';
3061
- el.style.top = pos.y + 'px';
3062
- if (pos.w) el.style.width = pos.w + 'px';
3063
- if (pos.h) el.style.height = pos.h + 'px';
3064
-
3065
- const { wrap, body } = _buildCardWrapper(node);
3066
- while (wrap.firstChild) el.appendChild(wrap.firstChild);
3067
- // Re-apply collapsed state: in canvas mode el is the card container, not wrap
3068
- const movedHeader = el.querySelector('.card-header');
3069
- if (movedHeader && movedHeader.dataset.gandalfCollapsed === '1') el.classList.add('lc-collapsed');
3070
- canvasInner.appendChild(el);
3071
- nodeMap[node.id] = { node, colEl: el, bodyEl: body };
3072
- engine.render(node, body, { showChat: false });
3073
- _makeDraggable(el, node);
3074
- _makeResizable(el, node);
3075
- }
3076
- });
3077
-
3078
- _updateTokenAvailability();
3079
-
3080
- // Fit canvas to content then draw edges
3081
- requestAnimationFrame(function() {
3082
- _fitCanvasToContent();
3083
- _drawEdges();
3084
- });
3085
-
3086
- // Reposition LeaderLine edges on scroll
3087
- canvasEl.addEventListener('scroll', function() { _repositionEdges(); }, { signal, passive: true });
3088
-
3089
- // Pan: middle-click or Ctrl+drag on background
3090
- let panning = false, panStartX, panStartY, panOrigX, panOrigY;
3091
- canvasEl.addEventListener('pointerdown', e => {
3092
- if (e.target !== canvasEl && e.target !== canvasInner) return;
3093
- if (e.button === 1 || (e.button === 0 && e.ctrlKey)) {
3094
- panning = true; canvasEl.setPointerCapture(e.pointerId);
3095
- panStartX = e.clientX; panStartY = e.clientY;
3096
- panOrigX = cvs.panX; panOrigY = cvs.panY;
3097
- e.preventDefault();
3098
- }
3099
- }, { signal });
3100
- canvasEl.addEventListener('pointermove', e => {
3101
- if (!panning) return;
3102
- cvs.panX = panOrigX + (e.clientX - panStartX);
3103
- cvs.panY = panOrigY + (e.clientY - panStartY);
3104
- _applyTransform();
3105
- _repositionEdges();
3106
- }, { signal });
3107
- canvasEl.addEventListener('pointerup', () => { panning = false; }, { signal });
3108
-
3109
- // Zoom: Ctrl+wheel
3110
- canvasEl.addEventListener('wheel', e => {
3111
- if (!e.ctrlKey) return;
3112
- e.preventDefault();
3113
- const delta = e.deltaY > 0 ? 0.9 : 1.1;
3114
- cvs.zoom = Math.min(cvs.zoomMax, Math.max(cvs.zoomMin, cvs.zoom * delta));
3115
- _applyTransform();
3116
- _repositionEdges();
3117
- }, { signal, passive: false });
3118
- }
3119
-
3120
- function _render() {
3121
- if (mode.current === 'canvas') _renderCanvas();
3122
- else _renderBoard();
3123
- }
3124
-
3125
- // ---- Auto-layout (topological L → R) ----
3126
-
3127
- function autoLayout() {
3128
- const tokenMap = _buildTokenMap();
3129
- const incoming = {};
3130
- const levels = {};
3131
- nodeList.forEach(n => { incoming[n.id] = []; levels[n.id] = 0; });
3132
- nodeList.forEach(n => {
3133
- _resolveEdgeSources(n, tokenMap).forEach(srcId => {
3134
- if (incoming[n.id]) incoming[n.id].push(srcId);
3135
- });
3136
- });
3137
-
3138
- let changed = true;
3139
- while (changed) {
3140
- changed = false;
3141
- nodeList.forEach(n => {
3142
- (incoming[n.id] || []).forEach(srcId => {
3143
- if (levels[srcId] != null && levels[srcId] + 1 > levels[n.id]) {
3144
- levels[n.id] = levels[srcId] + 1;
3145
- changed = true;
3146
- }
3147
- });
3148
- });
3149
- }
3150
-
3151
- const colCounts = {};
3152
- nodeList.forEach(n => {
3153
- const lv = levels[n.id] || 0;
3154
- if (!colCounts[lv]) colCounts[lv] = 0;
3155
- const row = colCounts[lv]++;
3156
- _positions[n.id] = {
3157
- x: lv * 400 + 40,
3158
- y: row * 300 + 40,
3159
- w: (_positions[n.id] && _positions[n.id].w) || cvs.defaultW,
3160
- };
3161
- // Sync to card nodes
3162
- if (n.view) {
3163
- if (!n.view.layout) n.view.layout = {};
3164
- n.view.layout.canvas = Object.assign({}, _positions[n.id]);
3165
- }
3166
- });
3167
- if (mode.current === 'canvas') _renderCanvas();
3168
- }
3169
-
3170
- // ---- Public API ----
3171
-
3172
- function add(node) {
3173
- if (nodeMap[node.id]) return;
3174
- nodeList.push(node);
3175
- _render();
3176
- }
3177
-
3178
- function remove(nodeId) {
3179
- engine.destroy(nodeId);
3180
- const idx = nodeList.findIndex(n => n.id === nodeId);
3181
- if (idx >= 0) nodeList.splice(idx, 1);
3182
- delete nodeMap[nodeId];
3183
- delete _positions[nodeId];
3184
- _render();
3185
- }
3186
-
3187
- function reorder(ids) {
3188
- nodeList.length = 0;
3189
- ids.forEach(id => {
3190
- const info = nodeMap[id];
3191
- if (info) nodeList.push(info.node);
3192
- });
3193
- _render();
3194
- }
3195
-
3196
- /**
3197
- * Per-node update: replace runtime fields on the existing node object in place
3198
- * and re-render only that node's body. Outer wrapper is rebuilt to pick up
3199
- * status/badges, but the surrounding column element is reused so layout is stable.
3200
- * Editable element state is preserved via journal overlays keyed by nodeId:bindPath.
3201
- */
3202
- function updateNode(id, model) {
3203
- const entry = nodeMap[id];
3204
- if (!entry) throw new Error('updateNode: unknown node id ' + id);
3205
- const node = entry.node;
3206
- if (model && typeof model === 'object') {
3207
- if (model.card !== undefined) node.card = model.card;
3208
- if (model.card_data !== undefined) node.card_data = model.card_data;
3209
- if (model.requires !== undefined) node.requires = model.requires;
3210
- if (model.computed_values !== undefined) node.computed_values = model.computed_values;
3211
- if (model.runtime_state !== undefined) node.runtime_state = model.runtime_state;
3212
- }
3213
- engine.destroy(id);
3214
- if (mode.current === 'board') {
3215
- const colEl = entry.colEl;
3216
- colEl.innerHTML = '';
3217
- const built = _buildCardWrapper(node);
3218
- colEl.appendChild(built.wrap);
3219
- nodeMap[id] = { node, colEl, bodyEl: built.body };
3220
- engine.render(node, built.body, { showChat });
3221
- } else {
3222
- const el = entry.colEl;
3223
- el.innerHTML = '';
3224
- const built = _buildCardWrapper(node);
3225
- while (built.wrap.firstChild) el.appendChild(built.wrap.firstChild);
3226
- nodeMap[id] = { node, colEl: el, bodyEl: built.body };
3227
- engine.render(node, built.body, { showChat: false });
3228
- }
3229
- _updateTokenAvailability();
3230
- }
3231
-
3232
- function clear() {
3233
- _destroyEdges();
3234
- engine.destroyAll();
3235
- nodeList.length = 0;
3236
- Object.keys(nodeMap).forEach(k => delete nodeMap[k]);
3237
- Object.keys(_positions).forEach(k => delete _positions[k]);
3238
- root.innerHTML = '';
3239
- }
3240
-
3241
- function setMode(m) {
3242
- if (m !== 'board' && m !== 'canvas') return;
3243
- mode.current = m;
3244
- _render();
3245
- }
3246
-
3247
- function setDevMode(flag) {
3248
- devMode.current = !!flag;
3249
- _render();
3250
- }
3251
-
3252
- function destroy() {
3253
- _destroyEdges();
3254
- document.body.style.overflow = '';
3255
- ac.abort();
3256
- engine.destroyAll();
3257
- nodeList.length = 0;
3258
- Object.keys(nodeMap).forEach(k => delete nodeMap[k]);
3259
- root.innerHTML = '';
3260
- if (root.parentNode) root.parentNode.removeChild(root);
3261
- }
3262
-
3263
- // ---- Init ----
3264
- if (opts.nodes && opts.nodes.length) {
3265
- opts.nodes.forEach(n => nodeList.push(n));
3266
- }
3267
- _render();
3268
-
3269
- return {
3270
- add,
3271
- remove,
3272
- reorder,
3273
- updateNode,
3274
- clear,
3275
- setMode,
3276
- setDevMode,
3277
- autoLayout,
3278
- destroy,
3279
- get mode() { return mode.current; },
3280
- get devMode() { return devMode.current; },
3281
- get nodes() { return nodeList.slice(); },
3282
- get engine() { return engine; },
3283
- };
3284
- }
3285
-
3286
- // ===========================================================================
3287
- // Board — reactive host. State in, view out. No destructive re-renders.
3288
- // ===========================================================================
3289
-
3290
- function Board(engine, containerEl, opts) {
3291
- opts = opts || {};
3292
- const initialState = opts.initialState;
3293
- const getNodeIds = opts.getNodeIds;
3294
- const selectNode = opts.selectNode;
3295
- if (typeof getNodeIds !== 'function' || typeof selectNode !== 'function') {
3296
- throw new Error('LiveCard.Board requires getNodeIds and selectNode functions');
3297
- }
3298
-
3299
- let state = initialState;
3300
- const prevModelsById = {};
3301
- const prevFingerprintsById = {};
3302
-
3303
- function _stableStringify(v) {
3304
- if (v == null || typeof v !== 'object') return JSON.stringify(v);
3305
- if (Array.isArray(v)) return '[' + v.map(_stableStringify).join(',') + ']';
3306
- const keys = Object.keys(v).sort();
3307
- return '{' + keys.map(k => JSON.stringify(k) + ':' + _stableStringify(v[k])).join(',') + '}';
3308
- }
3309
-
3310
- function _modelFingerprint(model) {
3311
- if (!model || typeof model !== 'object') return 'null';
3312
- return _stableStringify({
3313
- card: model.card,
3314
- card_data: model.card_data,
3315
- requires: model.requires,
3316
- computed_values: model.computed_values,
3317
- runtime_state: model.runtime_state,
3318
- });
3319
- }
3320
-
3321
- const initialIds = getNodeIds(state);
3322
- const initialNodes = initialIds.map(id => {
3323
- const m = selectNode(state, id);
3324
- prevModelsById[id] = m;
3325
- prevFingerprintsById[id] = _modelFingerprint(m);
3326
- return m;
3327
- });
3328
-
3329
- const coreOpts = {};
3330
- Object.keys(opts).forEach(k => {
3331
- if (k === 'initialState' || k === 'getNodeIds' || k === 'selectNode' || k === 'nodes') return;
3332
- coreOpts[k] = opts[k];
3333
- });
3334
- coreOpts.nodes = initialNodes;
3335
-
3336
- const core = BoardCore(engine, containerEl, coreOpts);
3337
-
3338
- function _changed(prevFingerprint, nextFingerprint) {
3339
- return prevFingerprint !== nextFingerprint;
3340
- }
3341
-
3342
- function setState(nextStateOrUpdater) {
3343
- const nextState = (typeof nextStateOrUpdater === 'function')
3344
- ? nextStateOrUpdater(state)
3345
- : nextStateOrUpdater;
3346
- if (nextState === undefined) return;
3347
-
3348
- state = nextState;
3349
- const nextIds = getNodeIds(state);
3350
- const nextSet = new Set(nextIds);
3351
-
3352
- // Removals
3353
- Object.keys(prevModelsById).forEach(id => {
3354
- if (!nextSet.has(id)) {
3355
- core.remove(id);
3356
- delete prevModelsById[id];
3357
- delete prevFingerprintsById[id];
3358
- }
3359
- });
3360
-
3361
- // Additions and per-node updates
3362
- nextIds.forEach(id => {
3363
- const next = selectNode(state, id);
3364
- const prev = prevModelsById[id];
3365
- const nextFingerprint = _modelFingerprint(next);
3366
- const prevFingerprint = prevFingerprintsById[id];
3367
- if (!prev) {
3368
- core.add(next);
3369
- } else if (_changed(prevFingerprint, nextFingerprint)) {
3370
- core.updateNode(id, next);
3371
- }
3372
- prevModelsById[id] = next;
3373
- prevFingerprintsById[id] = nextFingerprint;
3374
- });
3375
-
3376
- // Reorder if id sequence differs
3377
- const currentOrder = core.nodes.map(n => n.id);
3378
- const orderDiffers = nextIds.length !== currentOrder.length
3379
- || nextIds.some((id, i) => id !== currentOrder[i]);
3380
- if (orderDiffers) core.reorder(nextIds);
3381
- }
3382
-
3383
- function destroy() {
3384
- Object.keys(prevModelsById).forEach(k => delete prevModelsById[k]);
3385
- Object.keys(prevFingerprintsById).forEach(k => delete prevFingerprintsById[k]);
3386
- core.destroy();
3387
- }
3388
-
3389
- return {
3390
- setState,
3391
- destroy,
3392
- core,
3393
- get state() { return state; },
3394
- };
3395
- }
3396
-
3397
- // ===========================================================================
3398
- // Module export
3399
- // ===========================================================================
3400
-
3401
- return { init, Board, BoardCore };
3402
- })();
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="&lt;/&gt;",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