linguclaw 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (168) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +161 -0
  3. package/dist/agent-system.d.ts +196 -0
  4. package/dist/agent-system.d.ts.map +1 -0
  5. package/dist/agent-system.js +738 -0
  6. package/dist/agent-system.js.map +1 -0
  7. package/dist/alphabeta.d.ts +54 -0
  8. package/dist/alphabeta.d.ts.map +1 -0
  9. package/dist/alphabeta.js +193 -0
  10. package/dist/alphabeta.js.map +1 -0
  11. package/dist/browser.d.ts +62 -0
  12. package/dist/browser.d.ts.map +1 -0
  13. package/dist/browser.js +224 -0
  14. package/dist/browser.js.map +1 -0
  15. package/dist/cli.d.ts +7 -0
  16. package/dist/cli.d.ts.map +1 -0
  17. package/dist/cli.js +565 -0
  18. package/dist/cli.js.map +1 -0
  19. package/dist/code-parser.d.ts +39 -0
  20. package/dist/code-parser.d.ts.map +1 -0
  21. package/dist/code-parser.js +385 -0
  22. package/dist/code-parser.js.map +1 -0
  23. package/dist/config.d.ts +66 -0
  24. package/dist/config.d.ts.map +1 -0
  25. package/dist/config.js +232 -0
  26. package/dist/config.js.map +1 -0
  27. package/dist/core/engine.d.ts +359 -0
  28. package/dist/core/engine.d.ts.map +1 -0
  29. package/dist/core/engine.js +127 -0
  30. package/dist/core/engine.js.map +1 -0
  31. package/dist/daemon.d.ts +29 -0
  32. package/dist/daemon.d.ts.map +1 -0
  33. package/dist/daemon.js +212 -0
  34. package/dist/daemon.js.map +1 -0
  35. package/dist/email-receiver.d.ts +63 -0
  36. package/dist/email-receiver.d.ts.map +1 -0
  37. package/dist/email-receiver.js +553 -0
  38. package/dist/email-receiver.js.map +1 -0
  39. package/dist/git-integration.d.ts +180 -0
  40. package/dist/git-integration.d.ts.map +1 -0
  41. package/dist/git-integration.js +850 -0
  42. package/dist/git-integration.js.map +1 -0
  43. package/dist/inbox.d.ts +84 -0
  44. package/dist/inbox.d.ts.map +1 -0
  45. package/dist/inbox.js +198 -0
  46. package/dist/inbox.js.map +1 -0
  47. package/dist/index.d.ts +6 -0
  48. package/dist/index.d.ts.map +1 -0
  49. package/dist/index.js +41 -0
  50. package/dist/index.js.map +1 -0
  51. package/dist/languages/cpp.d.ts +51 -0
  52. package/dist/languages/cpp.d.ts.map +1 -0
  53. package/dist/languages/cpp.js +930 -0
  54. package/dist/languages/cpp.js.map +1 -0
  55. package/dist/languages/csharp.d.ts +79 -0
  56. package/dist/languages/csharp.d.ts.map +1 -0
  57. package/dist/languages/csharp.js +1776 -0
  58. package/dist/languages/csharp.js.map +1 -0
  59. package/dist/languages/go.d.ts +50 -0
  60. package/dist/languages/go.d.ts.map +1 -0
  61. package/dist/languages/go.js +882 -0
  62. package/dist/languages/go.js.map +1 -0
  63. package/dist/languages/java.d.ts +47 -0
  64. package/dist/languages/java.d.ts.map +1 -0
  65. package/dist/languages/java.js +649 -0
  66. package/dist/languages/java.js.map +1 -0
  67. package/dist/languages/python.d.ts +47 -0
  68. package/dist/languages/python.d.ts.map +1 -0
  69. package/dist/languages/python.js +655 -0
  70. package/dist/languages/python.js.map +1 -0
  71. package/dist/languages/rust.d.ts +61 -0
  72. package/dist/languages/rust.d.ts.map +1 -0
  73. package/dist/languages/rust.js +1064 -0
  74. package/dist/languages/rust.js.map +1 -0
  75. package/dist/logger.d.ts +20 -0
  76. package/dist/logger.d.ts.map +1 -0
  77. package/dist/logger.js +133 -0
  78. package/dist/logger.js.map +1 -0
  79. package/dist/longterm-memory.d.ts +47 -0
  80. package/dist/longterm-memory.d.ts.map +1 -0
  81. package/dist/longterm-memory.js +300 -0
  82. package/dist/longterm-memory.js.map +1 -0
  83. package/dist/memory.d.ts +42 -0
  84. package/dist/memory.d.ts.map +1 -0
  85. package/dist/memory.js +274 -0
  86. package/dist/memory.js.map +1 -0
  87. package/dist/messaging.d.ts +103 -0
  88. package/dist/messaging.d.ts.map +1 -0
  89. package/dist/messaging.js +645 -0
  90. package/dist/messaging.js.map +1 -0
  91. package/dist/multi-provider.d.ts +69 -0
  92. package/dist/multi-provider.d.ts.map +1 -0
  93. package/dist/multi-provider.js +484 -0
  94. package/dist/multi-provider.js.map +1 -0
  95. package/dist/orchestrator.d.ts +65 -0
  96. package/dist/orchestrator.d.ts.map +1 -0
  97. package/dist/orchestrator.js +441 -0
  98. package/dist/orchestrator.js.map +1 -0
  99. package/dist/plugins.d.ts +52 -0
  100. package/dist/plugins.d.ts.map +1 -0
  101. package/dist/plugins.js +215 -0
  102. package/dist/plugins.js.map +1 -0
  103. package/dist/prism-orchestrator.d.ts +26 -0
  104. package/dist/prism-orchestrator.d.ts.map +1 -0
  105. package/dist/prism-orchestrator.js +191 -0
  106. package/dist/prism-orchestrator.js.map +1 -0
  107. package/dist/prism.d.ts +46 -0
  108. package/dist/prism.d.ts.map +1 -0
  109. package/dist/prism.js +188 -0
  110. package/dist/prism.js.map +1 -0
  111. package/dist/privacy.d.ts +23 -0
  112. package/dist/privacy.d.ts.map +1 -0
  113. package/dist/privacy.js +220 -0
  114. package/dist/privacy.js.map +1 -0
  115. package/dist/proactive.d.ts +30 -0
  116. package/dist/proactive.d.ts.map +1 -0
  117. package/dist/proactive.js +260 -0
  118. package/dist/proactive.js.map +1 -0
  119. package/dist/refactoring-engine.d.ts +100 -0
  120. package/dist/refactoring-engine.d.ts.map +1 -0
  121. package/dist/refactoring-engine.js +717 -0
  122. package/dist/refactoring-engine.js.map +1 -0
  123. package/dist/resilience.d.ts +43 -0
  124. package/dist/resilience.d.ts.map +1 -0
  125. package/dist/resilience.js +200 -0
  126. package/dist/resilience.js.map +1 -0
  127. package/dist/safety.d.ts +40 -0
  128. package/dist/safety.d.ts.map +1 -0
  129. package/dist/safety.js +133 -0
  130. package/dist/safety.js.map +1 -0
  131. package/dist/sandbox.d.ts +33 -0
  132. package/dist/sandbox.d.ts.map +1 -0
  133. package/dist/sandbox.js +173 -0
  134. package/dist/sandbox.js.map +1 -0
  135. package/dist/scheduler.d.ts +72 -0
  136. package/dist/scheduler.d.ts.map +1 -0
  137. package/dist/scheduler.js +374 -0
  138. package/dist/scheduler.js.map +1 -0
  139. package/dist/semantic-memory.d.ts +70 -0
  140. package/dist/semantic-memory.d.ts.map +1 -0
  141. package/dist/semantic-memory.js +430 -0
  142. package/dist/semantic-memory.js.map +1 -0
  143. package/dist/skills.d.ts +97 -0
  144. package/dist/skills.d.ts.map +1 -0
  145. package/dist/skills.js +575 -0
  146. package/dist/skills.js.map +1 -0
  147. package/dist/static/dashboard.html +853 -0
  148. package/dist/static/hub.html +772 -0
  149. package/dist/static/index.html +818 -0
  150. package/dist/static/logo.svg +24 -0
  151. package/dist/static/workflow-editor.html +913 -0
  152. package/dist/tools.d.ts +67 -0
  153. package/dist/tools.d.ts.map +1 -0
  154. package/dist/tools.js +303 -0
  155. package/dist/tools.js.map +1 -0
  156. package/dist/types.d.ts +295 -0
  157. package/dist/types.d.ts.map +1 -0
  158. package/dist/types.js +90 -0
  159. package/dist/types.js.map +1 -0
  160. package/dist/web.d.ts +76 -0
  161. package/dist/web.d.ts.map +1 -0
  162. package/dist/web.js +2139 -0
  163. package/dist/web.js.map +1 -0
  164. package/dist/workflow-engine.d.ts +114 -0
  165. package/dist/workflow-engine.d.ts.map +1 -0
  166. package/dist/workflow-engine.js +855 -0
  167. package/dist/workflow-engine.js.map +1 -0
  168. package/package.json +77 -0
@@ -0,0 +1,913 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>LinguClaw — Workflow Editor</title>
7
+ <style>
8
+ :root{--bg0:#0a0a0b;--bg1:#121214;--bg2:#1a1a1c;--bg3:#222224;--border:#2e2e30;--accent:#5b5bd6;--accent2:#4a4ac2;--accent3:#6c6ce0;--glow:rgba(91,91,214,.15);--green:#4ade80;--red:#f87171;--yellow:#fbbf24;--cyan:#22d3ee;--orange:#fb923c;--text:#f0f0f1;--dim:#9ca3af;--muted:#6b7280;--r:8px;--r2:12px;--node-trigger:#16a34a;--node-action:#3b82f6;--node-condition:#f59e0b;--node-transform:#8b5cf6;--node-output:#ef4444}
9
+ *{box-sizing:border-box;margin:0;padding:0}
10
+ body{font-family:'Inter','SF Pro Display',-apple-system,sans-serif;background:var(--bg0);color:var(--text);height:100vh;overflow:hidden;display:flex;flex-direction:column}
11
+ ::-webkit-scrollbar{width:5px}::-webkit-scrollbar-track{background:transparent}::-webkit-scrollbar-thumb{background:var(--border);border-radius:3px}
12
+
13
+ /* Top bar */
14
+ .wf-top{display:flex;align-items:center;height:48px;padding:0 16px;background:var(--bg1);border-bottom:1px solid var(--border);gap:12px;flex-shrink:0;z-index:100}
15
+ .wf-top .back{display:flex;align-items:center;gap:6px;color:var(--dim);cursor:pointer;font-size:13px;font-weight:500;padding:6px 10px;border-radius:6px;text-decoration:none;transition:all .15s}
16
+ .wf-top .back:hover{background:var(--bg2);color:var(--text)}
17
+ .wf-top .back svg{width:16px;height:16px}
18
+ .wf-top .title{font-weight:600;font-size:14px;cursor:pointer;padding:4px 8px;border-radius:4px;border:1px solid transparent;transition:border .15s}
19
+ .wf-top .title:hover{border-color:var(--border)}
20
+ .wf-top .sep{width:1px;height:24px;background:var(--border)}
21
+ .wf-actions{display:flex;align-items:center;gap:6px;margin-left:auto}
22
+ .btn{padding:7px 14px;border:none;border-radius:7px;cursor:pointer;font-size:12px;font-weight:600;transition:all .15s;display:inline-flex;align-items:center;gap:5px}
23
+ .btn:disabled{opacity:.4;cursor:default}
24
+ .btn-p{background:var(--accent);color:#fff}.btn-p:hover:not(:disabled){background:var(--accent2)}
25
+ .btn-g{background:transparent;color:var(--dim);border:1px solid var(--border)}.btn-g:hover{color:var(--text);background:var(--bg2)}
26
+ .btn-s{background:var(--green);color:#000}.btn-s:hover{filter:brightness(1.1)}
27
+ .btn-d{background:rgba(239,68,68,.1);color:var(--red);border:1px solid rgba(239,68,68,.2)}.btn-d:hover{background:rgba(239,68,68,.2)}
28
+ .btn svg{width:14px;height:14px}
29
+
30
+ /* Main layout */
31
+ .wf-main{display:flex;flex:1;overflow:hidden}
32
+
33
+ /* Node palette */
34
+ .palette{width:260px;background:var(--bg1);border-right:1px solid var(--border);display:flex;flex-direction:column;flex-shrink:0;z-index:50}
35
+ .palette-search{padding:10px;border-bottom:1px solid var(--border)}
36
+ .palette-search input{width:100%;padding:8px 12px;background:var(--bg2);border:1px solid var(--border);border-radius:7px;color:var(--text);font-size:12px;outline:none;transition:border .15s}
37
+ .palette-search input:focus{border-color:var(--accent)}
38
+ .palette-search input::placeholder{color:var(--muted)}
39
+ .palette-list{flex:1;overflow-y:auto;padding:8px}
40
+ .palette-cat{margin-bottom:12px}
41
+ .palette-cat-title{font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.8px;color:var(--muted);padding:4px 8px 6px;user-select:none}
42
+ .palette-node{display:flex;align-items:center;gap:8px;padding:8px 10px;border-radius:7px;cursor:grab;font-size:12px;font-weight:500;color:var(--dim);border:1px solid transparent;transition:all .15s;user-select:none;margin-bottom:2px}
43
+ .palette-node:hover{background:var(--bg2);color:var(--text);border-color:var(--border)}
44
+ .palette-node:active{cursor:grabbing}
45
+ .palette-node .pn-icon{width:28px;height:28px;border-radius:6px;display:flex;align-items:center;justify-content:center;flex-shrink:0}
46
+ .palette-node .pn-icon svg{width:14px;height:14px}
47
+ .palette-node[data-type="trigger"] .pn-icon{background:rgba(22,163,74,.15);color:var(--node-trigger)}
48
+ .palette-node[data-type="action"] .pn-icon{background:rgba(59,130,246,.15);color:var(--node-action)}
49
+ .palette-node[data-type="condition"] .pn-icon{background:rgba(245,158,11,.15);color:var(--node-condition)}
50
+ .palette-node[data-type="transform"] .pn-icon{background:rgba(139,92,246,.15);color:var(--node-transform)}
51
+ .palette-node[data-type="output"] .pn-icon{background:rgba(239,68,68,.15);color:var(--node-output)}
52
+ .palette-node .pn-info{min-width:0}
53
+ .palette-node .pn-name{font-weight:600;font-size:12px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
54
+ .palette-node .pn-desc{font-size:10px;color:var(--muted);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
55
+
56
+ /* Canvas */
57
+ .canvas-wrap{flex:1;position:relative;overflow:hidden;background:var(--bg0)}
58
+ .canvas-bg{position:absolute;inset:0;background-image:radial-gradient(circle,var(--border) 1px,transparent 1px);background-size:24px 24px;pointer-events:none}
59
+ #canvas{position:absolute;inset:0;width:100%;height:100%}
60
+ #nodeLayer{position:absolute;inset:0;pointer-events:none}
61
+
62
+ /* Zoom controls */
63
+ .zoom-ctrl{position:absolute;bottom:16px;right:16px;display:flex;gap:4px;z-index:60}
64
+ .zoom-ctrl button{width:32px;height:32px;background:var(--bg2);border:1px solid var(--border);border-radius:6px;color:var(--dim);cursor:pointer;display:flex;align-items:center;justify-content:center;transition:all .15s}
65
+ .zoom-ctrl button:hover{background:var(--bg3);color:var(--text)}
66
+ .zoom-ctrl .zoom-val{padding:0 8px;background:var(--bg2);border:1px solid var(--border);border-radius:6px;font-size:11px;color:var(--dim);display:flex;align-items:center}
67
+
68
+ /* Canvas nodes */
69
+ .wf-node{position:absolute;min-width:180px;background:var(--bg2);border:1.5px solid var(--border);border-radius:10px;cursor:move;pointer-events:all;transition:box-shadow .2s;user-select:none;z-index:10}
70
+ .wf-node:hover{box-shadow:0 4px 20px rgba(0,0,0,.4)}
71
+ .wf-node.selected{border-color:var(--accent);box-shadow:0 0 0 2px var(--glow),0 4px 20px rgba(0,0,0,.4)}
72
+ .wf-node.running{border-color:var(--yellow);box-shadow:0 0 0 2px rgba(251,191,36,.2)}
73
+ .wf-node.success{border-color:var(--green);box-shadow:0 0 0 2px rgba(74,222,128,.2)}
74
+ .wf-node.error{border-color:var(--red);box-shadow:0 0 0 2px rgba(248,113,113,.2)}
75
+ .wf-node-header{display:flex;align-items:center;gap:8px;padding:10px 12px;border-bottom:1px solid var(--border);border-radius:10px 10px 0 0}
76
+ .wf-node-header .n-icon{width:26px;height:26px;border-radius:6px;display:flex;align-items:center;justify-content:center;flex-shrink:0}
77
+ .wf-node-header .n-icon svg{width:13px;height:13px}
78
+ .wf-node[data-type="trigger"] .n-icon{background:rgba(22,163,74,.2);color:var(--node-trigger)}
79
+ .wf-node[data-type="action"] .n-icon{background:rgba(59,130,246,.2);color:var(--node-action)}
80
+ .wf-node[data-type="condition"] .n-icon{background:rgba(245,158,11,.2);color:var(--node-condition)}
81
+ .wf-node[data-type="transform"] .n-icon{background:rgba(139,92,246,.2);color:var(--node-transform)}
82
+ .wf-node[data-type="output"] .n-icon{background:rgba(239,68,68,.2);color:var(--node-output)}
83
+ .wf-node[data-type="trigger"] .wf-node-header{background:rgba(22,163,74,.06)}
84
+ .wf-node[data-type="action"] .wf-node-header{background:rgba(59,130,246,.06)}
85
+ .wf-node[data-type="condition"] .wf-node-header{background:rgba(245,158,11,.06)}
86
+ .wf-node[data-type="transform"] .wf-node-header{background:rgba(139,92,246,.06)}
87
+ .wf-node[data-type="output"] .wf-node-header{background:rgba(239,68,68,.06)}
88
+ .wf-node-header .n-name{font-size:12px;font-weight:600;flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
89
+ .wf-node-header .n-del{width:20px;height:20px;border-radius:4px;display:none;align-items:center;justify-content:center;cursor:pointer;color:var(--muted);transition:all .15s}
90
+ .wf-node-header .n-del:hover{background:rgba(239,68,68,.15);color:var(--red)}
91
+ .wf-node:hover .n-del{display:flex}
92
+ .wf-node-body{padding:8px 12px;font-size:11px;color:var(--dim);min-height:20px}
93
+ .wf-node-body .n-param{display:flex;align-items:center;justify-content:space-between;padding:2px 0}
94
+ .wf-node-body .n-param .n-key{color:var(--muted);font-size:10px}
95
+ .wf-node-body .n-param .n-val{color:var(--dim);font-size:10px;max-width:100px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
96
+
97
+ /* Ports */
98
+ .port{position:absolute;width:14px;height:14px;border-radius:50%;border:2px solid var(--border);background:var(--bg1);cursor:crosshair;z-index:20;transition:transform .15s,border-color .15s;pointer-events:all}
99
+ .port::after{content:'';position:absolute;inset:-6px;border-radius:50%;pointer-events:none}
100
+ .port:hover{transform:scale(1.4);border-color:var(--accent)}
101
+ .port.input{left:-7px}
102
+ .port.output{right:-7px}
103
+ .port.connected{background:var(--accent);border-color:var(--accent)}
104
+
105
+ /* Connection SVG */
106
+ .conn-svg{position:absolute;inset:0;width:100%;height:100%;overflow:visible;pointer-events:none;z-index:5}
107
+ .conn-svg path{fill:none;stroke:var(--border);stroke-width:2;pointer-events:stroke;cursor:pointer;transition:stroke .2s}
108
+ .conn-svg path:hover{stroke:var(--accent);stroke-width:2.5}
109
+ .conn-svg path.active{stroke:var(--accent)}
110
+ .conn-svg path.running{stroke:var(--yellow);stroke-dasharray:8 4;animation:dash .5s linear infinite}
111
+ .conn-svg path.success{stroke:var(--green)}
112
+ .conn-svg path.error{stroke:var(--red)}
113
+ .conn-svg path.drop-target{stroke:var(--accent3);stroke-width:3.5;filter:drop-shadow(0 0 6px var(--accent))}
114
+ @keyframes dash{to{stroke-dashoffset:-12}}
115
+
116
+ /* Properties panel */
117
+ .props{width:300px;background:var(--bg1);border-left:1px solid var(--border);display:flex;flex-direction:column;flex-shrink:0;z-index:50;transform:translateX(100%);transition:transform .25s ease}
118
+ .props.open{transform:translateX(0)}
119
+ .props-head{padding:14px 16px;border-bottom:1px solid var(--border);display:flex;align-items:center;justify-content:space-between}
120
+ .props-head h3{font-size:14px;font-weight:600;display:flex;align-items:center;gap:6px}
121
+ .props-head .close{width:24px;height:24px;border-radius:4px;display:flex;align-items:center;justify-content:center;cursor:pointer;color:var(--muted);border:none;background:none;transition:all .15s}
122
+ .props-head .close:hover{background:var(--bg2);color:var(--text)}
123
+ .props-body{flex:1;overflow-y:auto;padding:14px 16px}
124
+ .props-field{margin-bottom:14px}
125
+ .props-field label{display:block;font-size:11px;font-weight:600;color:var(--dim);margin-bottom:4px;letter-spacing:.3px}
126
+ .props-field input,.props-field select,.props-field textarea{width:100%;padding:8px 12px;background:var(--bg2);border:1px solid var(--border);border-radius:7px;color:var(--text);font-size:12px;outline:none;transition:border .15s;font-family:inherit}
127
+ .props-field input:focus,.props-field select:focus,.props-field textarea:focus{border-color:var(--accent)}
128
+ .props-field textarea{min-height:80px;resize:vertical}
129
+ .props-field .hint{font-size:10px;color:var(--muted);margin-top:3px}
130
+ .props-field select{appearance:none;background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='%2364748b'%3E%3Cpath d='M6 8L1 3h10z'/%3E%3C/svg%3E");background-repeat:no-repeat;background-position:right 10px center;padding-right:28px}
131
+
132
+ /* Toast */
133
+ .toast{position:fixed;top:16px;right:16px;padding:10px 16px;border-radius:8px;font-size:13px;font-weight:500;z-index:9999;opacity:0;transform:translateY(-8px);transition:all .25s;pointer-events:none}
134
+ .toast.show{opacity:1;transform:translateY(0)}.toast.ok{background:#0d2818;color:var(--green);border:1px solid rgba(34,197,94,.3)}.toast.err{background:#2a0f0f;color:var(--red);border:1px solid rgba(239,68,68,.3)}.toast.info{background:#0f1b2d;color:var(--accent3);border:1px solid rgba(59,130,246,.3)}
135
+
136
+ /* Modal */
137
+ .modal-bg{display:none;position:fixed;inset:0;background:rgba(0,0,0,.6);backdrop-filter:blur(4px);z-index:200;align-items:center;justify-content:center;opacity:0;transition:opacity .2s}
138
+ .modal-bg.open{display:flex;opacity:1}
139
+ .modal{background:var(--bg1);border:1px solid var(--border);border-radius:12px;padding:24px;width:90%;max-width:450px;box-shadow:0 20px 60px rgba(0,0,0,.5)}
140
+ .modal h3{font-size:16px;margin-bottom:16px}
141
+ .modal .fld{margin-bottom:12px}
142
+ .modal .fld label{display:block;font-size:11px;font-weight:600;color:var(--dim);margin-bottom:4px}
143
+ .modal .fld input,.modal .fld textarea{width:100%;padding:8px 12px;background:var(--bg2);border:1px solid var(--border);border-radius:7px;color:var(--text);font-size:12px;outline:none}
144
+ .modal .acts{display:flex;gap:8px;justify-content:flex-end;margin-top:20px}
145
+
146
+ /* Empty state */
147
+ .empty-canvas{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;z-index:6;pointer-events:none}
148
+ .empty-canvas .ec-inner{text-align:center;pointer-events:all}
149
+ .empty-canvas .ec-inner svg{width:48px;height:48px;color:var(--muted);opacity:.3;margin-bottom:12px}
150
+ .empty-canvas .ec-inner p{font-size:14px;color:var(--muted);margin-bottom:4px}
151
+ .empty-canvas .ec-inner .sub{font-size:12px;color:var(--border)}
152
+
153
+ /* Minimap */
154
+ .minimap{position:absolute;bottom:16px;left:16px;width:160px;height:100px;background:var(--bg1);border:1px solid var(--border);border-radius:8px;z-index:60;overflow:hidden;opacity:.7;transition:opacity .15s}
155
+ .minimap:hover{opacity:1}
156
+ .minimap canvas{width:100%;height:100%}
157
+ </style>
158
+ </head>
159
+ <body>
160
+ <div id="toast" class="toast"></div>
161
+ <div class="wf-top">
162
+ <a class="back" href="/" id="backBtn"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M19 12H5m7-7-7 7 7 7"/></svg> Back</a>
163
+ <div class="sep"></div>
164
+ <div class="title" id="wfTitle" onclick="renameWorkflow()">Untitled Workflow</div>
165
+ <span style="font-size:11px;color:var(--muted)" id="wfStatus">Draft</span>
166
+ <div class="wf-actions">
167
+ <button class="btn btn-g" id="undoBtn" onclick="undo()" title="Undo"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 7v6h6"/><path d="M21 17a9 9 0 0 0-9-9 9 9 0 0 0-6.69 3L3 13"/></svg></button>
168
+ <button class="btn btn-g" id="redoBtn" onclick="redo()" title="Redo"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 7v6h-6"/><path d="M3 17a9 9 0 0 1 9-9 9 9 0 0 1 6.69 3L21 13"/></svg></button>
169
+ <div class="sep"></div>
170
+ <button class="btn btn-g" onclick="saveWorkflow()"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"/><polyline points="17 21 17 13 7 13 7 21"/><polyline points="7 3 7 8 15 8"/></svg> Save</button>
171
+ <button class="btn btn-s" onclick="executeWorkflow()"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="5 3 19 12 5 21 5 3"/></svg> Execute</button>
172
+ </div>
173
+ </div>
174
+ <div class="wf-main">
175
+ <div class="palette">
176
+ <div class="palette-search"><input id="nodeSearch" placeholder="Search nodes..." oninput="filterNodes()"></div>
177
+ <div class="palette-list" id="paletteList"></div>
178
+ </div>
179
+ <div class="canvas-wrap" id="canvasWrap">
180
+ <div class="canvas-bg" id="canvasBg"></div>
181
+ <svg class="conn-svg" id="connSvg" overflow="visible"></svg>
182
+ <div id="nodeLayer"></div>
183
+ <div class="empty-canvas" id="emptyCanvas">
184
+ <div class="ec-inner">
185
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M12 5v14m-7-7h14"/></svg>
186
+ <p>Drag nodes from the palette to start</p>
187
+ <div class="sub">or double-click the canvas to add a node</div>
188
+ </div>
189
+ </div>
190
+ <div class="zoom-ctrl">
191
+ <button onclick="zoomIn()"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg></button>
192
+ <div class="zoom-val" id="zoomVal">100%</div>
193
+ <button onclick="zoomOut()"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="5" y1="12" x2="19" y2="12"/></svg></button>
194
+ <button onclick="zoomFit()"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M8 3H5a2 2 0 0 0-2 2v3m18 0V5a2 2 0 0 0-2-2h-3m0 18h3a2 2 0 0 0 2-2v-3M3 16v3a2 2 0 0 0 2 2h3"/></svg></button>
195
+ </div>
196
+ </div>
197
+ <div class="props" id="propsPanel">
198
+ <div class="props-head">
199
+ <h3 id="propsTitle">Node Properties</h3>
200
+ <button class="close" onclick="closeProps()"><svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg></button>
201
+ </div>
202
+ <div class="props-body" id="propsBody"></div>
203
+ </div>
204
+ </div>
205
+
206
+ <div class="modal-bg" id="renameMod"><div class="modal"><h3>Rename Workflow</h3>
207
+ <div class="fld"><label>Name</label><input id="renameIn"></div>
208
+ <div class="fld"><label>Description</label><textarea id="renameDesc" rows="3"></textarea></div>
209
+ <div class="acts"><button class="btn btn-g" onclick="closeModal('renameMod')">Cancel</button><button class="btn btn-p" onclick="applyRename()">Save</button></div>
210
+ </div></div>
211
+
212
+ <script>
213
+ /* ==================== State ==================== */
214
+ var workflowId = new URLSearchParams(location.search).get('id');
215
+ var nodeDefs = [];
216
+ var workflow = { id: '', name: 'Untitled Workflow', description: '', nodes: [], connections: [], active: false };
217
+ var selectedNodeId = null;
218
+ var zoom = 1, panX = 0, panY = 0;
219
+ var draggingNode = null, dragOffX = 0, dragOffY = 0;
220
+ var connecting = null; // {sourceNodeId, sourceOutput, tempLine}
221
+ var undoStack = [], redoStack = [];
222
+
223
+ /* ==================== Icons ==================== */
224
+ var icons = {
225
+ play:'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="5 3 19 12 5 21"/></svg>',
226
+ clock:'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>',
227
+ webhook:'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 16.98h1a2 2 0 0 0 2-2v-1a2 2 0 0 0-4 0"/><path d="M6 16.98H5a2 2 0 0 1-2-2v-1a2 2 0 0 1 4 0"/><path d="M12 3a2 2 0 0 0-2 2v1a2 2 0 0 0 4 0V5a2 2 0 0 0-2-2z"/><path d="M6 12h12"/></svg>',
228
+ mail:'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"/><polyline points="22,6 12,13 2,6"/></svg>',
229
+ globe:'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></svg>',
230
+ terminal:'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="4 17 10 11 4 5"/><line x1="12" y1="19" x2="20" y2="19"/></svg>',
231
+ brain:'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9.5 2A2.5 2.5 0 0 1 12 4.5v15a2.5 2.5 0 0 1-4.96.44 2.5 2.5 0 0 1-2.96-3.08 3 3 0 0 1-.34-5.58 2.5 2.5 0 0 1 1.32-4.24 2.5 2.5 0 0 1 1.98-3A2.5 2.5 0 0 1 9.5 2z"/><path d="M14.5 2A2.5 2.5 0 0 0 12 4.5v15a2.5 2.5 0 0 0 4.96.44 2.5 2.5 0 0 0 2.96-3.08 3 3 0 0 0 .34-5.58 2.5 2.5 0 0 0-1.32-4.24 2.5 2.5 0 0 0-1.98-3A2.5 2.5 0 0 0 14.5 2z"/></svg>',
232
+ send:'<svg 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>',
233
+ file:'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/></svg>',
234
+ 'file-plus':'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><line x1="12" y1="18" x2="12" y2="12"/><line x1="9" y1="15" x2="15" y2="15"/></svg>',
235
+ database:'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><ellipse cx="12" cy="5" rx="9" ry="3"/><path d="M21 12c0 1.66-4 3-9 3s-9-1.34-9-3"/><path d="M3 5v14c0 1.66 4 3 9 3s9-1.34 9-3V5"/></svg>',
236
+ 'git-branch':'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="6" y1="3" x2="6" y2="15"/><circle cx="18" cy="6" r="3"/><circle cx="6" cy="18" r="3"/><path d="M18 9a9 9 0 0 1-9 9"/></svg>',
237
+ code:'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/></svg>',
238
+ braces:'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M8 3H7a2 2 0 0 0-2 2v5a2 2 0 0 1-2 2 2 2 0 0 1 2 2v5c0 1.1.9 2 2 2h1"/><path d="M16 21h1a2 2 0 0 0 2-2v-5c0-1.1.9-2 2-2a2 2 0 0 1-2-2V5a2 2 0 0 0-2-2h-1"/></svg>',
239
+ type:'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="4 7 4 4 20 4 20 7"/><line x1="9" y1="20" x2="15" y2="20"/><line x1="12" y1="4" x2="12" y2="20"/></svg>',
240
+ 'git-merge':'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="18" cy="18" r="3"/><circle cx="6" cy="6" r="3"/><path d="M6 21V9a9 9 0 0 0 9 9"/></svg>',
241
+ 'file-text':'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/></svg>'
242
+ };
243
+
244
+ /* ==================== Helpers ==================== */
245
+ function esc(s){if(!s)return '';var d=document.createElement('div');d.textContent=String(s);return d.innerHTML}
246
+ function toast(m,t){var e=document.getElementById('toast');e.textContent=m;e.className='toast show '+(t||'info');clearTimeout(e._t);e._t=setTimeout(function(){e.className='toast'},3000)}
247
+ function openModal(id){document.getElementById(id).classList.add('open')}
248
+ function closeModal(id){document.getElementById(id).classList.remove('open')}
249
+ function genId(){return 'n_'+Math.random().toString(36).substr(2,9)}
250
+
251
+ /* ==================== Init ==================== */
252
+ var defaultNodeDefs=[
253
+ {id:'trigger-manual',name:'Manual Trigger',type:'trigger',category:'Triggers',icon:'play',inputs:0,outputs:1,configFields:[]},
254
+ {id:'trigger-schedule',name:'Schedule',type:'trigger',category:'Triggers',icon:'clock',inputs:0,outputs:1,configFields:[{key:'cron',label:'Cron','type':'text','default':'0 * * * *'}]},
255
+ {id:'trigger-webhook',name:'Webhook',type:'trigger',category:'Triggers',icon:'webhook',inputs:0,outputs:1,configFields:[{key:'path',label:'Path','type':'text','default':'/hook'}]},
256
+ {id:'action-http',name:'HTTP Request',type:'action',category:'Actions',icon:'globe',inputs:1,outputs:1,configFields:[{key:'url',label:'URL','type':'text','default':'https://'}, {key:'method',label:'Method','type':'select','default':'GET'}]},
257
+ {id:'action-code',name:'Run Code',type:'action',category:'Actions',icon:'code',inputs:1,outputs:1,configFields:[{key:'code',label:'Code','type':'code','default':'return items;'}]},
258
+ {id:'action-email',name:'Send Email',type:'action',category:'Actions',icon:'mail',inputs:1,outputs:1,configFields:[{key:'to',label:'To','type':'text'},{key:'subject',label:'Subject','type':'text'}]},
259
+ {id:'action-llm',name:'AI / LLM',type:'action',category:'Actions',icon:'brain',inputs:1,outputs:1,configFields:[{key:'prompt',label:'Prompt','type':'code','default':'Summarize:'}]},
260
+ {id:'transform-set',name:'Set Fields',type:'transform',category:'Transform',icon:'braces',inputs:1,outputs:1,configFields:[{key:'fields',label:'Fields','type':'code','default':'{}'}]},
261
+ {id:'transform-filter',name:'Filter',type:'transform',category:'Transform',icon:'type',inputs:1,outputs:1,configFields:[{key:'condition',label:'Condition','type':'text','default':'item.value > 0'}]},
262
+ {id:'condition-if',name:'IF',type:'condition',category:'Logic',icon:'git-branch',inputs:1,outputs:2,configFields:[{key:'condition',label:'Condition','type':'text','default':'true'}]},
263
+ {id:'condition-switch',name:'Switch',type:'condition',category:'Logic',icon:'git-merge',inputs:1,outputs:3,configFields:[{key:'field',label:'Field','type':'text'}]},
264
+ {id:'output-respond',name:'Respond',type:'output',category:'Output',icon:'send',inputs:1,outputs:0,configFields:[{key:'body',label:'Body','type':'code','default':'{}'}]},
265
+ {id:'output-file',name:'Write File',type:'output',category:'Output',icon:'file-plus',inputs:1,outputs:0,configFields:[{key:'path',label:'Path','type':'text'}]}
266
+ ];
267
+
268
+ async function init(){
269
+ // Load node definitions
270
+ try{var r=await fetch('/api/workflows/node-definitions');nodeDefs=await r.json()}catch(e){console.warn('API unavailable, using default node defs');nodeDefs=defaultNodeDefs}
271
+ renderPalette();
272
+
273
+ // Load or create workflow
274
+ if(workflowId){
275
+ try{var r=await fetch('/api/workflows/'+workflowId);if(r.ok){workflow=await r.json();document.getElementById('wfTitle').textContent=workflow.name;document.getElementById('wfStatus').textContent=workflow.active?'Active':'Draft';renderAll()}}catch(e){toast('Failed to load workflow','err')}
276
+ }
277
+ setupCanvasEvents();
278
+ pushUndo();
279
+ }
280
+
281
+ /* ==================== Palette ==================== */
282
+ function renderPalette(){
283
+ var cats={};nodeDefs.forEach(function(d){if(!cats[d.category])cats[d.category]=[];cats[d.category].push(d)});
284
+ var h='';for(var cat in cats){
285
+ h+='<div class="palette-cat"><div class="palette-cat-title">'+esc(cat)+'</div>';
286
+ cats[cat].forEach(function(d){
287
+ h+='<div class="palette-node" data-type="'+d.type+'" data-def="'+d.id+'" draggable="true">';
288
+ h+='<div class="pn-icon">'+(icons[d.icon]||icons.play)+'</div>';
289
+ h+='<div class="pn-info"><div class="pn-name">'+esc(d.name)+'</div><div class="pn-desc">'+esc(d.description)+'</div></div></div>';
290
+ });
291
+ h+='</div>';
292
+ }
293
+ document.getElementById('paletteList').innerHTML=h;
294
+ // Drag events
295
+ document.querySelectorAll('.palette-node').forEach(function(el){
296
+ el.addEventListener('dragstart',function(e){e.dataTransfer.setData('defId',el.dataset.def);e.dataTransfer.effectAllowed='copy'});
297
+ });
298
+ }
299
+
300
+ function filterNodes(){
301
+ var q=document.getElementById('nodeSearch').value.toLowerCase();
302
+ document.querySelectorAll('.palette-node').forEach(function(el){
303
+ var name=el.querySelector('.pn-name').textContent.toLowerCase();
304
+ var desc=el.querySelector('.pn-desc').textContent.toLowerCase();
305
+ el.style.display=(name.includes(q)||desc.includes(q))?'flex':'none';
306
+ });
307
+ }
308
+
309
+ /* ==================== Canvas ==================== */
310
+ function findPort(e){
311
+ var els=document.elementsFromPoint(e.clientX,e.clientY);
312
+ for(var i=0;i<els.length;i++){if(els[i].classList.contains('port'))return els[i]}
313
+ return null;
314
+ }
315
+ function findNode(e){
316
+ var els=document.elementsFromPoint(e.clientX,e.clientY);
317
+ for(var i=0;i<els.length;i++){if(els[i].classList.contains('wf-node'))return els[i];if(els[i].closest&&els[i].closest('.wf-node'))return els[i].closest('.wf-node')}
318
+ return null;
319
+ }
320
+ function findConnectionAtPoint(cx,cy){
321
+ // Check SVG paths under point
322
+ var els=document.elementsFromPoint(cx,cy);
323
+ for(var i=0;i<els.length;i++){
324
+ var el=els[i];
325
+ if(el.tagName==='path'&&el.dataset.connId){
326
+ return workflow.connections.find(function(c){return c.id===el.dataset.connId})||null;
327
+ }
328
+ }
329
+ // Fallback: distance-based check for nearby connections
330
+ var wrap=document.getElementById('canvasWrap');
331
+ var rect=wrap.getBoundingClientRect();
332
+ var px=(cx-rect.left-panX)/zoom;
333
+ var py=(cy-rect.top-panY)/zoom;
334
+ var bestDist=20,bestConn=null;
335
+ workflow.connections.forEach(function(conn){
336
+ var src=workflow.nodes.find(function(n){return n.id===conn.sourceNodeId});
337
+ var tgt=workflow.nodes.find(function(n){return n.id===conn.targetNodeId});
338
+ if(!src||!tgt)return;
339
+ var srcEl=document.getElementById('node_'+conn.sourceNodeId);
340
+ var tgtEl=document.getElementById('node_'+conn.targetNodeId);
341
+ if(!srcEl||!tgtEl)return;
342
+ var x1=src.position.x+srcEl.offsetWidth,y1=src.position.y+42+(conn.sourceOutput||0)*20;
343
+ var x2=tgt.position.x,y2=tgt.position.y+42+(conn.targetInput||0)*20;
344
+ var d=distToBeizer(px,py,x1,y1,x2,y2);
345
+ if(d<bestDist){bestDist=d;bestConn=conn}
346
+ });
347
+ return bestConn;
348
+ }
349
+ function distToBeizer(px,py,x1,y1,x2,y2){
350
+ // Sample bezier at N points and find min distance
351
+ var cp=Math.max(60,Math.abs(x2-x1)*0.5);
352
+ var min=Infinity;
353
+ for(var t=0;t<=1;t+=0.05){
354
+ var it=1-t;
355
+ var bx=it*it*it*x1+3*it*it*t*(x1+cp)+3*it*t*t*(x2-cp)+t*t*t*x2;
356
+ var by=it*it*it*y1+3*it*it*t*y1+3*it*t*t*y2+t*t*t*y2;
357
+ var dx=px-bx,dy=py-by;
358
+ var d=Math.sqrt(dx*dx+dy*dy);
359
+ if(d<min)min=d;
360
+ }
361
+ return min;
362
+ }
363
+
364
+ function setupCanvasEvents(){
365
+ var wrap=document.getElementById('canvasWrap');
366
+
367
+ // Drop from palette
368
+ wrap.addEventListener('dragover',function(e){
369
+ e.preventDefault();e.dataTransfer.dropEffect='copy';
370
+ // Highlight connection under cursor
371
+ document.querySelectorAll('.conn-svg path.drop-target').forEach(function(p){p.classList.remove('drop-target')});
372
+ var conn=findConnectionAtPoint(e.clientX,e.clientY);
373
+ if(conn){document.querySelectorAll('.conn-svg path[data-conn-id="'+conn.id+'"]').forEach(function(p){p.classList.add('drop-target')})}
374
+ });
375
+ wrap.addEventListener('dragleave',function(){document.querySelectorAll('.conn-svg path.drop-target').forEach(function(p){p.classList.remove('drop-target')})});
376
+ wrap.addEventListener('drop',function(e){
377
+ e.preventDefault();
378
+ document.querySelectorAll('.conn-svg path.drop-target').forEach(function(p){p.classList.remove('drop-target')});
379
+ var defId=e.dataTransfer.getData('defId');if(!defId)return;
380
+ var def=nodeDefs.find(function(d){return d.id===defId});
381
+ if(!def)return;
382
+ var rect=wrap.getBoundingClientRect();
383
+ var x=(e.clientX-rect.left-panX)/zoom;
384
+ var y=(e.clientY-rect.top-panY)/zoom;
385
+
386
+ // Check if dropped on a connection
387
+ var conn=findConnectionAtPoint(e.clientX,e.clientY);
388
+ if(conn&&def.inputs>0&&def.outputs>0){
389
+ // Insert node between the two connected nodes
390
+ var newNode=addNode(defId,x,y,true);
391
+ if(newNode){
392
+ var oldConn=workflow.connections.find(function(c){return c.id===conn.id});
393
+ if(oldConn){
394
+ // Remove old connection
395
+ workflow.connections=workflow.connections.filter(function(c){return c.id!==conn.id});
396
+ // Add: source → new node
397
+ workflow.connections.push({id:'c_'+Math.random().toString(36).substr(2,9),sourceNodeId:oldConn.sourceNodeId,sourceOutput:oldConn.sourceOutput,targetNodeId:newNode.id,targetInput:0});
398
+ // Add: new node → target
399
+ workflow.connections.push({id:'c_'+Math.random().toString(36).substr(2,9),sourceNodeId:newNode.id,sourceOutput:0,targetNodeId:oldConn.targetNodeId,targetInput:oldConn.targetInput});
400
+ renderConnections();
401
+ pushUndo();
402
+ }
403
+ }
404
+ }else{
405
+ addNode(defId,x,y);
406
+ }
407
+ });
408
+
409
+ // Single delegated mousedown on wrap
410
+ var panning=false,lastX,lastY;
411
+ wrap.addEventListener('mousedown',function(e){
412
+ if(e.button!==0){
413
+ // Middle/right click = pan
414
+ panning=true;lastX=e.clientX;lastY=e.clientY;wrap.style.cursor='grabbing';e.preventDefault();
415
+ return;
416
+ }
417
+
418
+ // Check if clicking on a port
419
+ var port=findPort(e);
420
+ if(port&&port.classList.contains('output')){
421
+ // Start connection drag
422
+ e.preventDefault();
423
+ e.stopPropagation();
424
+ connecting={sourceNodeId:port.dataset.nodeId,sourceOutput:parseInt(port.dataset.idx)};
425
+ port.style.borderColor='var(--accent)';
426
+ port.style.transform='scale(1.4)';
427
+ return;
428
+ }
429
+
430
+ // Check if clicking on a delete button
431
+ var del=null;
432
+ var els=document.elementsFromPoint(e.clientX,e.clientY);
433
+ for(var i=0;i<els.length;i++){if(els[i].classList.contains('n-del')){del=els[i];break}}
434
+ if(del)return; // let the onclick handle it
435
+
436
+ // Check if clicking on a node (not a port)
437
+ var nodeEl=findNode(e);
438
+ if(nodeEl&&nodeEl.id&&nodeEl.id.startsWith('node_')){
439
+ var nid=nodeEl.id.replace('node_','');
440
+ selectNode(nid);
441
+ draggingNode=nid;
442
+ var rect=nodeEl.getBoundingClientRect();
443
+ dragOffX=(e.clientX-rect.left)/zoom;
444
+ dragOffY=(e.clientY-rect.top)/zoom;
445
+ e.preventDefault();
446
+ return;
447
+ }
448
+
449
+ // Click on empty canvas = deselect + pan
450
+ selectNode(null);
451
+ panning=true;lastX=e.clientX;lastY=e.clientY;wrap.style.cursor='grabbing';
452
+ });
453
+
454
+ window.addEventListener('mousemove',function(e){
455
+ if(panning){panX+=e.clientX-lastX;panY+=e.clientY-lastY;lastX=e.clientX;lastY=e.clientY;applyTransform()}
456
+ if(draggingNode){
457
+ var rect=wrap.getBoundingClientRect();
458
+ var x=(e.clientX-rect.left-panX)/zoom-dragOffX;
459
+ var y=(e.clientY-rect.top-panY)/zoom-dragOffY;
460
+ var node=workflow.nodes.find(function(n){return n.id===draggingNode});
461
+ if(node){node.position.x=Math.round(x/12)*12;node.position.y=Math.round(y/12)*12;renderNodePosition(node.id)}
462
+ renderConnections();
463
+ }
464
+ if(connecting){
465
+ renderTempConnection(e.clientX,e.clientY);
466
+ }
467
+ });
468
+ window.addEventListener('mouseup',function(e){
469
+ if(panning){panning=false;wrap.style.cursor=''}
470
+ if(draggingNode){pushUndo();draggingNode=null}
471
+ if(connecting){endConnection(e);connecting=null;removeTempLine()}
472
+ });
473
+
474
+ // Zoom with wheel
475
+ wrap.addEventListener('wheel',function(e){
476
+ e.preventDefault();
477
+ var rect=wrap.getBoundingClientRect();
478
+ var mx=e.clientX-rect.left, my=e.clientY-rect.top;
479
+ var oldZoom=zoom;
480
+ if(e.deltaY<0)zoom=Math.min(zoom*1.1,3);
481
+ else zoom=Math.max(zoom/1.1,0.2);
482
+ panX=mx-(mx-panX)*(zoom/oldZoom);
483
+ panY=my-(my-panY)*(zoom/oldZoom);
484
+ applyTransform();
485
+ document.getElementById('zoomVal').textContent=Math.round(zoom*100)+'%';
486
+ },{passive:false});
487
+
488
+ wrap.addEventListener('contextmenu',function(e){e.preventDefault()});
489
+
490
+ // Keyboard
491
+ document.addEventListener('keydown',function(e){
492
+ if(e.key==='Delete'||e.key==='Backspace'){
493
+ if(document.activeElement.tagName==='INPUT'||document.activeElement.tagName==='TEXTAREA')return;
494
+ if(selectedNodeId){deleteNode(selectedNodeId);e.preventDefault()}
495
+ }
496
+ if(e.ctrlKey&&e.key==='z'){undo();e.preventDefault()}
497
+ if(e.ctrlKey&&e.key==='y'){redo();e.preventDefault()}
498
+ if(e.ctrlKey&&e.key==='s'){saveWorkflow();e.preventDefault()}
499
+ });
500
+ }
501
+
502
+ function applyTransform(){
503
+ var layer=document.getElementById('nodeLayer');
504
+ layer.style.transform='translate('+panX+'px,'+panY+'px) scale('+zoom+')';
505
+ layer.style.transformOrigin='0 0';
506
+ var svg=document.getElementById('connSvg');
507
+ svg.style.transform='translate('+panX+'px,'+panY+'px) scale('+zoom+')';
508
+ svg.style.transformOrigin='0 0';
509
+ var bg=document.getElementById('canvasBg');
510
+ bg.style.backgroundPosition=panX+'px '+panY+'px';
511
+ bg.style.backgroundSize=(24*zoom)+'px '+(24*zoom)+'px';
512
+ }
513
+
514
+ function zoomIn(){zoom=Math.min(zoom*1.2,3);applyTransform();document.getElementById('zoomVal').textContent=Math.round(zoom*100)+'%'}
515
+ function zoomOut(){zoom=Math.max(zoom/1.2,0.2);applyTransform();document.getElementById('zoomVal').textContent=Math.round(zoom*100)+'%'}
516
+ function zoomFit(){
517
+ if(!workflow.nodes.length){zoom=1;panX=0;panY=0;applyTransform();return}
518
+ var minX=Infinity,minY=Infinity,maxX=-Infinity,maxY=-Infinity;
519
+ workflow.nodes.forEach(function(n){
520
+ minX=Math.min(minX,n.position.x);minY=Math.min(minY,n.position.y);
521
+ maxX=Math.max(maxX,n.position.x+200);maxY=Math.max(maxY,n.position.y+80);
522
+ });
523
+ var wrap=document.getElementById('canvasWrap').getBoundingClientRect();
524
+ var w=maxX-minX+100,h=maxY-minY+100;
525
+ zoom=Math.min(wrap.width/w,wrap.height/h,1.5);
526
+ panX=(wrap.width-w*zoom)/2-minX*zoom;
527
+ panY=(wrap.height-h*zoom)/2-minY*zoom;
528
+ applyTransform();
529
+ document.getElementById('zoomVal').textContent=Math.round(zoom*100)+'%';
530
+ }
531
+
532
+ /* ==================== Node Management ==================== */
533
+ function addNode(defId,x,y,returnNode){
534
+ var def=nodeDefs.find(function(d){return d.id===defId});if(!def)return null;
535
+ var node={id:genId(),definitionId:defId,type:def.type,name:def.name,config:{},position:{x:Math.round(x/12)*12||200,y:Math.round(y/12)*12||200}};
536
+ // Set defaults
537
+ def.configFields.forEach(function(f){if(f.default!==undefined)node.config[f.key]=f.default});
538
+ workflow.nodes.push(node);
539
+ renderNode(node);
540
+ selectNode(node.id);
541
+ updateEmptyState();
542
+ if(!returnNode)pushUndo();
543
+ return returnNode?node:null;
544
+ }
545
+
546
+ function deleteNode(id){
547
+ workflow.nodes=workflow.nodes.filter(function(n){return n.id!==id});
548
+ workflow.connections=workflow.connections.filter(function(c){return c.sourceNodeId!==id&&c.targetNodeId!==id});
549
+ var el=document.getElementById('node_'+id);if(el)el.remove();
550
+ if(selectedNodeId===id){selectedNodeId=null;closeProps()}
551
+ renderConnections();
552
+ updateEmptyState();
553
+ pushUndo();
554
+ }
555
+
556
+ function selectNode(id){
557
+ document.querySelectorAll('.wf-node.selected').forEach(function(el){el.classList.remove('selected')});
558
+ selectedNodeId=id;
559
+ if(id){
560
+ var el=document.getElementById('node_'+id);if(el)el.classList.add('selected');
561
+ openProps(id);
562
+ }else{closeProps()}
563
+ }
564
+
565
+ function updateEmptyState(){
566
+ document.getElementById('emptyCanvas').style.display=workflow.nodes.length>0?'none':'flex';
567
+ }
568
+
569
+ /* ==================== Render ==================== */
570
+ function renderAll(){
571
+ document.getElementById('nodeLayer').innerHTML='';
572
+ workflow.nodes.forEach(function(n){renderNode(n)});
573
+ renderConnections();
574
+ updateEmptyState();
575
+ }
576
+
577
+ function renderNode(node){
578
+ var def=nodeDefs.find(function(d){return d.id===node.definitionId});
579
+ if(!def)return;
580
+ var el=document.createElement('div');
581
+ el.id='node_'+node.id;
582
+ el.className='wf-node'+(selectedNodeId===node.id?' selected':'');
583
+ el.dataset.type=node.type;
584
+ el.style.left=node.position.x+'px';
585
+ el.style.top=node.position.y+'px';
586
+
587
+ var h='<div class="wf-node-header">';
588
+ h+='<div class="n-icon">'+(icons[def.icon]||icons.play)+'</div>';
589
+ h+='<div class="n-name">'+esc(node.name)+'</div>';
590
+ h+='<div class="n-del" onclick="event.stopPropagation();deleteNode(\''+node.id+'\')"><svg viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg></div>';
591
+ h+='</div>';
592
+
593
+ // Body - show key config params
594
+ h+='<div class="wf-node-body">';
595
+ var shown=0;
596
+ def.configFields.forEach(function(f){
597
+ if(shown>=2)return;
598
+ var v=node.config[f.key];
599
+ if(v!==undefined&&v!==''){
600
+ h+='<div class="n-param"><span class="n-key">'+esc(f.label)+'</span><span class="n-val">'+esc(String(v))+'</span></div>';
601
+ shown++;
602
+ }
603
+ });
604
+ if(!shown)h+='<div style="color:var(--muted);font-size:10px">Click to configure</div>';
605
+ h+='</div>';
606
+ el.innerHTML=h;
607
+
608
+ // Input ports
609
+ for(var i=0;i<def.inputs;i++){
610
+ var port=document.createElement('div');
611
+ port.className='port input';
612
+ port.dataset.nodeId=node.id;
613
+ port.dataset.idx=i;
614
+ port.style.top=(35+i*20)+'px';
615
+ // Check if connected
616
+ if(workflow.connections.some(function(c){return c.targetNodeId===node.id&&c.targetInput===i}))port.classList.add('connected');
617
+ el.appendChild(port);
618
+ }
619
+
620
+ // Output ports
621
+ for(var i=0;i<def.outputs;i++){
622
+ var port=document.createElement('div');
623
+ port.className='port output';
624
+ port.dataset.nodeId=node.id;
625
+ port.dataset.idx=i;
626
+ port.style.top=(35+i*20)+'px';
627
+ if(workflow.connections.some(function(c){return c.sourceNodeId===node.id&&c.sourceOutput===i}))port.classList.add('connected');
628
+ el.appendChild(port);
629
+ }
630
+
631
+ el.addEventListener('dblclick',function(e){
632
+ e.stopPropagation();
633
+ openProps(node.id);
634
+ });
635
+
636
+ document.getElementById('nodeLayer').appendChild(el);
637
+ }
638
+
639
+ function renderNodePosition(id){
640
+ var node=workflow.nodes.find(function(n){return n.id===id});if(!node)return;
641
+ var el=document.getElementById('node_'+id);if(!el)return;
642
+ el.style.left=node.position.x+'px';
643
+ el.style.top=node.position.y+'px';
644
+ }
645
+
646
+ /* ==================== Connections ==================== */
647
+ function renderConnections(){
648
+ var svg=document.getElementById('connSvg');
649
+ svg.innerHTML='';
650
+ workflow.connections.forEach(function(c){
651
+ var srcEl=document.getElementById('node_'+c.sourceNodeId);
652
+ var tgtEl=document.getElementById('node_'+c.targetNodeId);
653
+ if(!srcEl||!tgtEl)return;
654
+ var srcNode=workflow.nodes.find(function(n){return n.id===c.sourceNodeId});
655
+ var tgtNode=workflow.nodes.find(function(n){return n.id===c.targetNodeId});
656
+ if(!srcNode||!tgtNode)return;
657
+
658
+ var x1=srcNode.position.x+srcEl.offsetWidth;
659
+ var y1=srcNode.position.y+42+c.sourceOutput*20;
660
+ var x2=tgtNode.position.x;
661
+ var y2=tgtNode.position.y+42+c.targetInput*20;
662
+
663
+ var dx=Math.abs(x2-x1)*0.5;
664
+ var d='M'+x1+','+y1+' C'+(x1+dx)+','+y1+' '+(x2-dx)+','+y2+' '+x2+','+y2;
665
+ // Invisible wide hit area for drag detection
666
+ var hitPath=document.createElementNS('http://www.w3.org/2000/svg','path');
667
+ hitPath.setAttribute('d',d);
668
+ hitPath.dataset.connId=c.id;
669
+ hitPath.setAttribute('fill','none');
670
+ hitPath.setAttribute('stroke','transparent');
671
+ hitPath.setAttribute('stroke-width','18');
672
+ hitPath.style.pointerEvents='stroke';
673
+ hitPath.style.cursor='pointer';
674
+ // Visible path
675
+ var path=document.createElementNS('http://www.w3.org/2000/svg','path');
676
+ path.setAttribute('d',d);
677
+ path.dataset.connId=c.id;
678
+ path.setAttribute('fill','none');
679
+ path.setAttribute('stroke','#6b7280');
680
+ path.setAttribute('stroke-width','2');
681
+ path.style.pointerEvents='none';
682
+ var clickHandler=function(conn){return function(e){
683
+ e.stopPropagation();
684
+ if(confirm('Remove this connection?')){
685
+ workflow.connections=workflow.connections.filter(function(cn){return cn.id!==conn.id});
686
+ renderConnections();
687
+ pushUndo();
688
+ }
689
+ }}(c);
690
+ hitPath.addEventListener('click',clickHandler);
691
+ svg.appendChild(hitPath);
692
+ svg.appendChild(path);
693
+ });
694
+
695
+ // Update port connected states
696
+ document.querySelectorAll('.port').forEach(function(p){
697
+ p.classList.remove('connected');
698
+ var nId=p.dataset.nodeId,idx=parseInt(p.dataset.idx);
699
+ if(p.classList.contains('output')){
700
+ if(workflow.connections.some(function(c){return c.sourceNodeId===nId&&c.sourceOutput===idx}))p.classList.add('connected');
701
+ }else{
702
+ if(workflow.connections.some(function(c){return c.targetNodeId===nId&&c.targetInput===idx}))p.classList.add('connected');
703
+ }
704
+ });
705
+ }
706
+
707
+ function renderTempConnection(mx,my){
708
+ removeTempLine();
709
+ if(!connecting)return;
710
+ var srcEl=document.getElementById('node_'+connecting.sourceNodeId);
711
+ var srcNode=workflow.nodes.find(function(n){return n.id===connecting.sourceNodeId});
712
+ if(!srcEl||!srcNode)return;
713
+
714
+ var wrap=document.getElementById('canvasWrap').getBoundingClientRect();
715
+ var x1=srcNode.position.x+srcEl.offsetWidth;
716
+ var y1=srcNode.position.y+42+connecting.sourceOutput*20;
717
+ var x2=(mx-wrap.left-panX)/zoom;
718
+ var y2=(my-wrap.top-panY)/zoom;
719
+
720
+ var dx=Math.abs(x2-x1)*0.5;
721
+ var d='M'+x1+','+y1+' C'+(x1+dx)+','+y1+' '+(x2-dx)+','+y2+' '+x2+','+y2;
722
+ var svg=document.getElementById('connSvg');
723
+ var path=document.createElementNS('http://www.w3.org/2000/svg','path');
724
+ path.setAttribute('d',d);
725
+ path.setAttribute('fill','none');
726
+ path.setAttribute('stroke','#5b5bd6');
727
+ path.setAttribute('stroke-width','2');
728
+ path.setAttribute('stroke-dasharray','6 3');
729
+ path.id='tempConn';
730
+ svg.appendChild(path);
731
+ }
732
+
733
+ function removeTempLine(){var t=document.getElementById('tempConn');if(t)t.remove()}
734
+
735
+ function endConnection(e){
736
+ if(!connecting)return;
737
+ // Find target input port under mouse
738
+ var el=findPort(e);
739
+ if(!el||!el.classList.contains('input'))return;
740
+
741
+ var targetNodeId=el.dataset.nodeId;
742
+ var targetInput=parseInt(el.dataset.idx);
743
+
744
+ // No self-connection
745
+ if(targetNodeId===connecting.sourceNodeId)return;
746
+
747
+ // No duplicate connections
748
+ if(workflow.connections.some(function(c){
749
+ return c.sourceNodeId===connecting.sourceNodeId&&c.sourceOutput===connecting.sourceOutput&&
750
+ c.targetNodeId===targetNodeId&&c.targetInput===targetInput;
751
+ }))return;
752
+
753
+ // Remove existing connection to this input (only one allowed per input)
754
+ workflow.connections=workflow.connections.filter(function(c){
755
+ return !(c.targetNodeId===targetNodeId&&c.targetInput===targetInput);
756
+ });
757
+
758
+ workflow.connections.push({
759
+ id:'c_'+Math.random().toString(36).substr(2,9),
760
+ sourceNodeId:connecting.sourceNodeId,
761
+ sourceOutput:connecting.sourceOutput,
762
+ targetNodeId:targetNodeId,
763
+ targetInput:targetInput
764
+ });
765
+ renderConnections();
766
+ pushUndo();
767
+ }
768
+
769
+ /* ==================== Properties Panel ==================== */
770
+ function openProps(nodeId){
771
+ var node=workflow.nodes.find(function(n){return n.id===nodeId});if(!node)return;
772
+ var def=nodeDefs.find(function(d){return d.id===node.definitionId});if(!def)return;
773
+
774
+ document.getElementById('propsPanel').classList.add('open');
775
+ document.getElementById('propsTitle').textContent=node.name;
776
+
777
+ var h='<div class="props-field"><label>Node Name</label><input value="'+esc(node.name)+'" onchange="updateNodeName(\''+node.id+'\',this.value)"></div>';
778
+
779
+ def.configFields.forEach(function(f){
780
+ h+='<div class="props-field"><label>'+esc(f.label)+(f.required?' *':'')+'</label>';
781
+ var val=node.config[f.key];if(val===undefined)val=f.default||'';
782
+
783
+ if(f.type==='select'){
784
+ h+='<select onchange="updateNodeConfig(\''+node.id+'\',\''+f.key+'\',this.value)">';
785
+ (f.options||[]).forEach(function(o){h+='<option value="'+esc(o.value)+'"'+(String(val)===String(o.value)?' selected':'')+'>'+esc(o.label)+'</option>'});
786
+ h+='</select>';
787
+ }else if(f.type==='boolean'){
788
+ h+='<select onchange="updateNodeConfig(\''+node.id+'\',\''+f.key+'\',this.value===\'true\')">';
789
+ h+='<option value="true"'+(val?' selected':'')+'>Yes</option>';
790
+ h+='<option value="false"'+(!val?' selected':'')+'>No</option></select>';
791
+ }else if(f.type==='code'||f.type==='json'){
792
+ h+='<textarea rows="5" style="font-family:monospace;font-size:11px" placeholder="'+esc(f.placeholder||'')+'" onchange="updateNodeConfig(\''+node.id+'\',\''+f.key+'\',this.value)">'+esc(String(val))+'</textarea>';
793
+ }else if(f.type==='number'){
794
+ h+='<input type="number" value="'+esc(String(val))+'" placeholder="'+esc(f.placeholder||'')+'" onchange="updateNodeConfig(\''+node.id+'\',\''+f.key+'\',parseFloat(this.value))">';
795
+ }else{
796
+ h+='<input value="'+esc(String(val))+'" placeholder="'+esc(f.placeholder||'')+'" onchange="updateNodeConfig(\''+node.id+'\',\''+f.key+'\',this.value)">';
797
+ }
798
+ if(f.placeholder)h+='<div class="hint">'+esc(f.placeholder)+'</div>';
799
+ h+='</div>';
800
+ });
801
+
802
+ document.getElementById('propsBody').innerHTML=h;
803
+ }
804
+
805
+ function closeProps(){document.getElementById('propsPanel').classList.remove('open');selectedNodeId=null;document.querySelectorAll('.wf-node.selected').forEach(function(el){el.classList.remove('selected')})}
806
+
807
+ function updateNodeName(id,name){
808
+ var node=workflow.nodes.find(function(n){return n.id===id});if(!node)return;
809
+ node.name=name;
810
+ var el=document.getElementById('node_'+id);if(el){el.querySelector('.n-name').textContent=name}
811
+ document.getElementById('propsTitle').textContent=name;
812
+ }
813
+
814
+ function updateNodeConfig(id,key,value){
815
+ var node=workflow.nodes.find(function(n){return n.id===id});if(!node)return;
816
+ node.config[key]=value;
817
+ // Re-render node body to show updated config preview
818
+ var def=nodeDefs.find(function(d){return d.id===node.definitionId});if(!def)return;
819
+ var body=document.querySelector('#node_'+id+' .wf-node-body');
820
+ if(body){
821
+ var h='',shown=0;
822
+ def.configFields.forEach(function(f){
823
+ if(shown>=2)return;
824
+ var v=node.config[f.key];
825
+ if(v!==undefined&&v!==''){h+='<div class="n-param"><span class="n-key">'+esc(f.label)+'</span><span class="n-val">'+esc(String(v))+'</span></div>';shown++}
826
+ });
827
+ if(!shown)h='<div style="color:var(--muted);font-size:10px">Click to configure</div>';
828
+ body.innerHTML=h;
829
+ }
830
+ }
831
+
832
+ /* ==================== Undo/Redo ==================== */
833
+ function pushUndo(){
834
+ undoStack.push(JSON.stringify({nodes:workflow.nodes,connections:workflow.connections}));
835
+ if(undoStack.length>50)undoStack.shift();
836
+ redoStack=[];
837
+ }
838
+ function undo(){
839
+ if(undoStack.length<=1)return;
840
+ redoStack.push(undoStack.pop());
841
+ var state=JSON.parse(undoStack[undoStack.length-1]);
842
+ workflow.nodes=state.nodes;workflow.connections=state.connections;
843
+ renderAll();
844
+ }
845
+ function redo(){
846
+ if(!redoStack.length)return;
847
+ var state=JSON.parse(redoStack.pop());
848
+ workflow.nodes=state.nodes;workflow.connections=state.connections;
849
+ undoStack.push(JSON.stringify(state));
850
+ renderAll();
851
+ }
852
+
853
+ /* ==================== Save/Execute ==================== */
854
+ async function saveWorkflow(){
855
+ try{
856
+ if(workflowId){
857
+ await fetch('/api/workflows/'+workflowId,{method:'PUT',headers:{'Content-Type':'application/json'},body:JSON.stringify({
858
+ name:workflow.name,description:workflow.description,nodes:workflow.nodes,connections:workflow.connections
859
+ })});
860
+ }else{
861
+ var r=await fetch('/api/workflows',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({name:workflow.name,description:workflow.description})});
862
+ var wf=await r.json();workflowId=wf.id;workflow.id=wf.id;
863
+ await fetch('/api/workflows/'+workflowId,{method:'PUT',headers:{'Content-Type':'application/json'},body:JSON.stringify({nodes:workflow.nodes,connections:workflow.connections})});
864
+ history.replaceState(null,'','?id='+workflowId);
865
+ }
866
+ toast('Workflow saved','ok');
867
+ }catch(e){toast('Save failed: '+e.message,'err')}
868
+ }
869
+
870
+ async function executeWorkflow(){
871
+ if(!workflowId){await saveWorkflow();if(!workflowId)return}
872
+ await saveWorkflow();
873
+
874
+ // Visual reset
875
+ document.querySelectorAll('.wf-node').forEach(function(el){el.classList.remove('running','success','error')});
876
+ document.querySelectorAll('.conn-svg path').forEach(function(p){p.classList.remove('running','success','error')});
877
+
878
+ try{
879
+ toast('Executing workflow...','info');
880
+ var r=await fetch('/api/workflows/'+workflowId+'/execute',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({})});
881
+ var result=await r.json();
882
+
883
+ if(result.error){toast('Execution failed: '+result.error,'err');return}
884
+
885
+ // Animate results
886
+ (result.nodeResults||[]).forEach(function(nr){
887
+ var el=document.getElementById('node_'+nr.nodeId);
888
+ if(el){el.classList.add(nr.status==='success'?'success':'error')}
889
+ });
890
+
891
+ var msg='Workflow '+(result.status==='success'?'completed':'failed')+' in '+(result.duration/1000).toFixed(1)+'s';
892
+ toast(msg,result.status==='success'?'ok':'err');
893
+ }catch(e){toast('Execution error: '+e.message,'err')}
894
+ }
895
+
896
+ /* ==================== Rename ==================== */
897
+ function renameWorkflow(){
898
+ document.getElementById('renameIn').value=workflow.name;
899
+ document.getElementById('renameDesc').value=workflow.description||'';
900
+ openModal('renameMod');
901
+ }
902
+ function applyRename(){
903
+ workflow.name=document.getElementById('renameIn').value||'Untitled Workflow';
904
+ workflow.description=document.getElementById('renameDesc').value||'';
905
+ document.getElementById('wfTitle').textContent=workflow.name;
906
+ closeModal('renameMod');
907
+ }
908
+
909
+ /* ==================== Start ==================== */
910
+ init();
911
+ </script>
912
+ </body>
913
+ </html>