@jungjaehoon/mama-os 0.18.2 → 0.19.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 (171) hide show
  1. package/dist/agent/agent-loop.d.ts +25 -0
  2. package/dist/agent/agent-loop.d.ts.map +1 -1
  3. package/dist/agent/agent-loop.js +67 -14
  4. package/dist/agent/agent-loop.js.map +1 -1
  5. package/dist/agent/code-act/host-bridge.d.ts.map +1 -1
  6. package/dist/agent/code-act/host-bridge.js +98 -0
  7. package/dist/agent/code-act/host-bridge.js.map +1 -1
  8. package/dist/agent/code-act/type-definition-generator.d.ts.map +1 -1
  9. package/dist/agent/code-act/type-definition-generator.js +0 -1
  10. package/dist/agent/code-act/type-definition-generator.js.map +1 -1
  11. package/dist/agent/gateway-tool-executor.d.ts +36 -1
  12. package/dist/agent/gateway-tool-executor.d.ts.map +1 -1
  13. package/dist/agent/gateway-tool-executor.js +938 -54
  14. package/dist/agent/gateway-tool-executor.js.map +1 -1
  15. package/dist/agent/gateway-tools.md +9 -0
  16. package/dist/agent/managed-agent-runtime-sync.d.ts +36 -0
  17. package/dist/agent/managed-agent-runtime-sync.d.ts.map +1 -0
  18. package/dist/agent/managed-agent-runtime-sync.js +207 -0
  19. package/dist/agent/managed-agent-runtime-sync.js.map +1 -0
  20. package/dist/agent/managed-agent-validation.d.ts +4 -0
  21. package/dist/agent/managed-agent-validation.d.ts.map +1 -0
  22. package/dist/agent/managed-agent-validation.js +84 -0
  23. package/dist/agent/managed-agent-validation.js.map +1 -0
  24. package/dist/agent/os-agent-capabilities.md +400 -0
  25. package/dist/agent/skill-loader.d.ts +2 -0
  26. package/dist/agent/skill-loader.d.ts.map +1 -1
  27. package/dist/agent/skill-loader.js +28 -0
  28. package/dist/agent/skill-loader.js.map +1 -1
  29. package/dist/agent/tool-registry.d.ts.map +1 -1
  30. package/dist/agent/tool-registry.js +66 -0
  31. package/dist/agent/tool-registry.js.map +1 -1
  32. package/dist/agent/types.d.ts +2 -1
  33. package/dist/agent/types.d.ts.map +1 -1
  34. package/dist/agent/types.js.map +1 -1
  35. package/dist/api/agent-handler.d.ts +34 -0
  36. package/dist/api/agent-handler.d.ts.map +1 -0
  37. package/dist/api/agent-handler.js +216 -0
  38. package/dist/api/agent-handler.js.map +1 -0
  39. package/dist/api/graph-api-types.d.ts +4 -0
  40. package/dist/api/graph-api-types.d.ts.map +1 -1
  41. package/dist/api/graph-api.d.ts +2 -2
  42. package/dist/api/graph-api.d.ts.map +1 -1
  43. package/dist/api/graph-api.js +480 -51
  44. package/dist/api/graph-api.js.map +1 -1
  45. package/dist/api/index.d.ts.map +1 -1
  46. package/dist/api/index.js +4 -0
  47. package/dist/api/index.js.map +1 -1
  48. package/dist/api/token-handler.d.ts +1 -0
  49. package/dist/api/token-handler.d.ts.map +1 -1
  50. package/dist/api/token-handler.js +4 -3
  51. package/dist/api/token-handler.js.map +1 -1
  52. package/dist/api/ui-command-handler.d.ts +48 -0
  53. package/dist/api/ui-command-handler.d.ts.map +1 -0
  54. package/dist/api/ui-command-handler.js +160 -0
  55. package/dist/api/ui-command-handler.js.map +1 -0
  56. package/dist/cli/commands/start.d.ts.map +1 -1
  57. package/dist/cli/commands/start.js +127 -1
  58. package/dist/cli/commands/start.js.map +1 -1
  59. package/dist/cli/config/config-manager.d.ts.map +1 -1
  60. package/dist/cli/config/config-manager.js +16 -31
  61. package/dist/cli/config/config-manager.js.map +1 -1
  62. package/dist/cli/runtime/agent-loop-init.d.ts.map +1 -1
  63. package/dist/cli/runtime/agent-loop-init.js +31 -7
  64. package/dist/cli/runtime/agent-loop-init.js.map +1 -1
  65. package/dist/cli/runtime/api-routes-init.d.ts +3 -0
  66. package/dist/cli/runtime/api-routes-init.d.ts.map +1 -1
  67. package/dist/cli/runtime/api-routes-init.js +283 -34
  68. package/dist/cli/runtime/api-routes-init.js.map +1 -1
  69. package/dist/cli/runtime/gateway-init.d.ts +2 -1
  70. package/dist/cli/runtime/gateway-init.d.ts.map +1 -1
  71. package/dist/cli/runtime/gateway-init.js +5 -1
  72. package/dist/cli/runtime/gateway-init.js.map +1 -1
  73. package/dist/connectors/framework/raw-store.d.ts +4 -0
  74. package/dist/connectors/framework/raw-store.d.ts.map +1 -1
  75. package/dist/connectors/framework/raw-store.js +33 -10
  76. package/dist/connectors/framework/raw-store.js.map +1 -1
  77. package/dist/db/agent-store.d.ts +115 -0
  78. package/dist/db/agent-store.d.ts.map +1 -0
  79. package/dist/db/agent-store.js +248 -0
  80. package/dist/db/agent-store.js.map +1 -0
  81. package/dist/db/migrations/agent-activity-validation-columns.d.ts +3 -0
  82. package/dist/db/migrations/agent-activity-validation-columns.d.ts.map +1 -0
  83. package/dist/db/migrations/agent-activity-validation-columns.js +22 -0
  84. package/dist/db/migrations/agent-activity-validation-columns.js.map +1 -0
  85. package/dist/db/migrations/agent-metrics-response-avg.d.ts +3 -0
  86. package/dist/db/migrations/agent-metrics-response-avg.d.ts.map +1 -0
  87. package/dist/db/migrations/agent-metrics-response-avg.js +19 -0
  88. package/dist/db/migrations/agent-metrics-response-avg.js.map +1 -0
  89. package/dist/db/migrations/agent-store-tables.d.ts +3 -0
  90. package/dist/db/migrations/agent-store-tables.d.ts.map +1 -0
  91. package/dist/db/migrations/agent-store-tables.js +59 -0
  92. package/dist/db/migrations/agent-store-tables.js.map +1 -0
  93. package/dist/db/migrations/token-usage-agent-version.d.ts +3 -0
  94. package/dist/db/migrations/token-usage-agent-version.d.ts.map +1 -0
  95. package/dist/db/migrations/token-usage-agent-version.js +16 -0
  96. package/dist/db/migrations/token-usage-agent-version.js.map +1 -0
  97. package/dist/db/migrations/validation-session-tables.d.ts +3 -0
  98. package/dist/db/migrations/validation-session-tables.d.ts.map +1 -0
  99. package/dist/db/migrations/validation-session-tables.js +59 -0
  100. package/dist/db/migrations/validation-session-tables.js.map +1 -0
  101. package/dist/gateways/message-router.d.ts +10 -0
  102. package/dist/gateways/message-router.d.ts.map +1 -1
  103. package/dist/gateways/message-router.js +188 -14
  104. package/dist/gateways/message-router.js.map +1 -1
  105. package/dist/gateways/types.d.ts +1 -1
  106. package/dist/gateways/types.d.ts.map +1 -1
  107. package/dist/multi-agent/agent-process-manager.js +1 -1
  108. package/dist/multi-agent/agent-process-manager.js.map +1 -1
  109. package/dist/multi-agent/conductor-persona.d.ts +13 -0
  110. package/dist/multi-agent/conductor-persona.d.ts.map +1 -0
  111. package/dist/multi-agent/conductor-persona.js +157 -0
  112. package/dist/multi-agent/conductor-persona.js.map +1 -0
  113. package/dist/multi-agent/dashboard-agent-persona.d.ts +1 -1
  114. package/dist/multi-agent/dashboard-agent-persona.d.ts.map +1 -1
  115. package/dist/multi-agent/dashboard-agent-persona.js +7 -3
  116. package/dist/multi-agent/dashboard-agent-persona.js.map +1 -1
  117. package/dist/multi-agent/delegation-manager.d.ts +5 -0
  118. package/dist/multi-agent/delegation-manager.d.ts.map +1 -1
  119. package/dist/multi-agent/delegation-manager.js +37 -0
  120. package/dist/multi-agent/delegation-manager.js.map +1 -1
  121. package/dist/multi-agent/ultrawork.d.ts +3 -0
  122. package/dist/multi-agent/ultrawork.d.ts.map +1 -1
  123. package/dist/multi-agent/ultrawork.js +9 -0
  124. package/dist/multi-agent/ultrawork.js.map +1 -1
  125. package/dist/validation/session-service.d.ts +72 -0
  126. package/dist/validation/session-service.d.ts.map +1 -0
  127. package/dist/validation/session-service.js +298 -0
  128. package/dist/validation/session-service.js.map +1 -0
  129. package/dist/validation/store.d.ts +25 -0
  130. package/dist/validation/store.d.ts.map +1 -0
  131. package/dist/validation/store.js +200 -0
  132. package/dist/validation/store.js.map +1 -0
  133. package/dist/validation/types.d.ts +119 -0
  134. package/dist/validation/types.d.ts.map +1 -0
  135. package/dist/validation/types.js +57 -0
  136. package/dist/validation/types.js.map +1 -0
  137. package/package.json +3 -3
  138. package/public/viewer/js/modules/agents.js +1148 -0
  139. package/public/viewer/js/modules/chat.js +20 -11
  140. package/public/viewer/js/modules/connector-feed.js +35 -0
  141. package/public/viewer/js/modules/dashboard.js +49 -0
  142. package/public/viewer/js/modules/memory.js +32 -0
  143. package/public/viewer/js/modules/settings.js +34 -79
  144. package/public/viewer/js/modules/wiki.js +59 -4
  145. package/public/viewer/js/utils/api.js +70 -0
  146. package/public/viewer/js/utils/dom.js +3 -0
  147. package/public/viewer/js/utils/ui-commands.js +93 -0
  148. package/public/viewer/log-viewer.html +2 -2
  149. package/public/viewer/src/modules/agents.ts +1299 -0
  150. package/public/viewer/src/modules/chat.ts +23 -14
  151. package/public/viewer/src/modules/connector-feed.ts +35 -0
  152. package/public/viewer/src/modules/dashboard.ts +50 -0
  153. package/public/viewer/src/modules/memory.ts +31 -0
  154. package/public/viewer/src/modules/settings.ts +36 -96
  155. package/public/viewer/src/modules/wiki.ts +73 -6
  156. package/public/viewer/src/types/global.d.ts +0 -9
  157. package/public/viewer/src/utils/api.ts +156 -2
  158. package/public/viewer/src/utils/dom.ts +6 -1
  159. package/public/viewer/src/utils/ui-commands.ts +118 -0
  160. package/public/viewer/viewer.css +105 -10
  161. package/public/viewer/viewer.html +1868 -777
  162. package/scripts/generate-gateway-tools.ts +5 -1
  163. package/public/viewer/js/modules/playground.js +0 -148
  164. package/public/viewer/js/modules/skills.js +0 -451
  165. package/public/viewer/src/modules/playground.ts +0 -173
  166. package/public/viewer/src/modules/skills.ts +0 -491
  167. package/templates/playgrounds/cron-workflow-lab.html +0 -1601
  168. package/templates/playgrounds/mama-log-viewer.html +0 -1341
  169. package/templates/playgrounds/skill-lab-playground.html +0 -1625
  170. package/templates/playgrounds/wave-visualizer.html +0 -694
  171. package/templates/skills/playground.md +0 -197
@@ -1,1601 +0,0 @@
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">
6
- <title>Cron Workflow Lab</title>
7
- <style>
8
- :root{
9
- --bg:#0f1117;--panel:#1a1d27;--surface:#222633;--line:#2d3348;
10
- --text:#e2e8f0;--muted:#7a8ba8;--dim:#4a5568;
11
- --accent:#6366f1;--accent-soft:rgba(99,102,241,.12);--accent-glow:rgba(99,102,241,.3);
12
- --ok:#22c55e;--ok-soft:rgba(34,197,94,.12);
13
- --warn:#f59e0b;--warn-soft:rgba(245,158,11,.12);
14
- --danger:#ef4444;--danger-soft:rgba(239,68,68,.12);
15
- --trigger:#f59e0b;--prompt:#6366f1;--condition:#ec4899;--action:#22c55e;
16
- --radius:10px;--shadow:0 4px 24px rgba(0,0,0,.3);
17
- }
18
- *{box-sizing:border-box;margin:0}
19
- body{font-family:'Pretendard','Inter',system-ui,-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;background:var(--bg);color:var(--text);height:100vh;overflow:hidden}
20
-
21
- .app{display:grid;grid-template-columns:280px 1fr;grid-template-rows:1fr auto;height:100vh}
22
-
23
- /* ── Sidebar ── */
24
- .sidebar{background:var(--panel);border-right:1px solid var(--line);display:flex;flex-direction:column;grid-row:1/3}
25
- .sidebar-header{padding:16px 18px;border-bottom:1px solid var(--line)}
26
- .sidebar-header h1{font-size:17px;font-weight:800;display:flex;align-items:center;gap:8px}
27
- .sidebar-header p{font-size:11px;color:var(--muted);margin-top:4px}
28
-
29
- .sidebar-section{padding:12px 14px;border-bottom:1px solid var(--line)}
30
- .sidebar-section h3{font-size:11px;text-transform:uppercase;letter-spacing:.08em;color:var(--muted);margin-bottom:10px;font-weight:700}
31
-
32
- /* Existing jobs */
33
- .job-list{flex:1;overflow-y:auto;padding:4px 8px}
34
- .job-card{padding:10px 12px;border-radius:8px;cursor:pointer;transition:all .15s;margin-bottom:4px;border:1px solid transparent}
35
- .job-card:hover{background:var(--surface);border-color:var(--line)}
36
- .job-card.active{background:var(--accent-soft);border-color:var(--accent)}
37
- .job-card .jname{font-size:13px;font-weight:700;display:flex;align-items:center;gap:6px}
38
- .job-card .jmeta{font-size:11px;color:var(--muted);margin-top:3px;font-family:monospace}
39
- .badge{display:inline-block;padding:1px 7px;border-radius:99px;font-size:10px;font-weight:700}
40
- .badge-on{background:var(--ok-soft);color:var(--ok)}
41
- .badge-off{background:var(--danger-soft);color:var(--danger)}
42
-
43
- .sidebar-status{padding:10px 16px;border-top:1px solid var(--line);font-size:11px;color:var(--muted);display:flex;align-items:center;gap:6px}
44
- .dot{width:7px;height:7px;border-radius:50%;flex-shrink:0}
45
- .dot-ok{background:var(--ok)}.dot-err{background:var(--danger)}
46
-
47
- /* ── Canvas ── */
48
- .canvas-area{position:relative;overflow:hidden;background:var(--bg)}
49
- .canvas-grid{position:absolute;inset:0;background-image:radial-gradient(circle,var(--line) .8px,transparent .8px);background-size:24px 24px;opacity:.4}
50
- #canvas{position:absolute;inset:0;width:100%;height:100%}
51
-
52
- /* Toolbar */
53
- .toolbar{position:absolute;top:14px;left:14px;display:flex;gap:6px;z-index:10}
54
- .tbtn{padding:7px 14px;border-radius:8px;font-size:12px;font-weight:600;cursor:pointer;border:1px solid var(--line);background:var(--panel);color:var(--text);transition:all .15s;display:flex;align-items:center;gap:5px}
55
- .tbtn:hover{border-color:var(--accent);color:var(--accent)}
56
- .tbtn.active{background:var(--accent);border-color:var(--accent);color:#fff}
57
-
58
- /* Template panel */
59
- .template-panel{position:absolute;top:52px;left:14px;background:var(--panel);border:1px solid var(--line);border-radius:var(--radius);padding:12px;z-index:10;width:240px;box-shadow:var(--shadow);display:none}
60
- .template-panel.show{display:block}
61
- .template-item{padding:8px 10px;border-radius:6px;cursor:pointer;font-size:12px;transition:background .12s;display:flex;align-items:center;gap:8px}
62
- .template-item:hover{background:var(--surface)}
63
- .template-item .ticon{font-size:16px}
64
- .template-item .tlabel{font-weight:600}
65
- .template-item .tdesc{font-size:10px;color:var(--muted);margin-top:2px}
66
-
67
- /* Node styles (rendered on SVG) */
68
- .node-group{cursor:grab}
69
- .node-group:active{cursor:grabbing}
70
- .node-body{rx:10;ry:10;stroke-width:2;filter:drop-shadow(0 2px 8px rgba(0,0,0,.3))}
71
- .node-port{cursor:crosshair;transition:r .1s}
72
- .node-port:hover{r:7}
73
-
74
- /* ── Detail Panel ── */
75
- .detail-panel{position:absolute;top:14px;right:14px;width:300px;background:var(--panel);border:1px solid var(--line);border-radius:var(--radius);box-shadow:var(--shadow);z-index:10;display:none;max-height:calc(100vh - 120px);overflow-y:auto}
76
- .detail-panel.show{display:block}
77
- .dp-header{padding:14px 16px;border-bottom:1px solid var(--line);display:flex;justify-content:space-between;align-items:center}
78
- .dp-header h3{font-size:14px;font-weight:700;display:flex;align-items:center;gap:6px}
79
- .dp-close{background:none;border:none;color:var(--muted);font-size:18px;cursor:pointer;padding:4px}
80
- .dp-close:hover{color:var(--text)}
81
- .dp-body{padding:14px 16px}
82
- .dp-field{margin-bottom:12px}
83
- .dp-label{font-size:11px;font-weight:600;color:var(--muted);margin-bottom:4px;display:block;text-transform:uppercase;letter-spacing:.05em}
84
- .dp-input{width:100%;background:var(--surface);border:1px solid var(--line);border-radius:6px;padding:8px 10px;font-size:13px;color:var(--text);outline:none;font-family:inherit}
85
- .dp-input:focus{border-color:var(--accent)}
86
- textarea.dp-input{resize:vertical;min-height:70px;line-height:1.5}
87
- select.dp-input{appearance:none;cursor:pointer}
88
- .dp-actions{padding:10px 16px;border-top:1px solid var(--line);display:flex;gap:8px}
89
-
90
- /* ── Bottom Bar ── */
91
- .bottom-bar{grid-column:2;background:var(--panel);border-top:1px solid var(--line);padding:12px 18px;display:flex;align-items:center;gap:12px}
92
- .prompt-preview{flex:1;background:var(--surface);border:1px solid var(--line);border-radius:8px;padding:10px 14px;font-size:12px;color:var(--muted);line-height:1.5;max-height:80px;overflow-y:auto;font-family:monospace;white-space:pre-wrap}
93
- .btn{padding:10px 20px;border-radius:8px;font-size:13px;font-weight:700;cursor:pointer;border:none;transition:all .15s;display:flex;align-items:center;gap:6px;white-space:nowrap}
94
- .btn-primary{background:var(--accent);color:#fff}.btn-primary:hover{background:#5558e6}
95
- .btn-ghost{background:transparent;color:var(--muted);border:1px solid var(--line)}.btn-ghost:hover{border-color:var(--accent);color:var(--accent)}
96
- .btn-send{background:var(--ok);color:#fff;font-size:14px;padding:10px 24px}.btn-send:hover{opacity:.9}
97
-
98
- /* ── Toast ── */
99
- .toast{position:fixed;bottom:20px;right:20px;background:var(--accent);color:#fff;padding:10px 18px;border-radius:8px;font-size:13px;font-weight:600;transform:translateY(60px);opacity:0;transition:all .3s;z-index:999}
100
- .toast.show{transform:translateY(0);opacity:1}
101
- .toast.error{background:var(--danger)}.toast.success{background:var(--ok)}
102
-
103
- /* ── Workflow name bar ── */
104
- .wf-name-bar{position:absolute;top:56px;left:50%;transform:translateX(-50%);z-index:10;display:flex;align-items:center;gap:8px;background:var(--panel);border:1px solid var(--line);border-radius:8px;padding:4px 6px}
105
- .wf-name-input{background:transparent;border:none;color:var(--text);font-size:14px;font-weight:700;text-align:center;outline:none;width:200px;font-family:inherit}
106
- .wf-name-input::placeholder{color:var(--dim)}
107
- .wf-status{font-size:10px;font-weight:700;padding:2px 8px;border-radius:99px}
108
- .wf-status-draft{background:var(--warn-soft);color:var(--warn)}
109
- .wf-status-deployed{background:var(--ok-soft);color:var(--ok)}
110
- .wf-status-paused{background:var(--surface);color:var(--muted)}
111
-
112
- /* ── Connection hint ── */
113
- .connecting-hint{position:absolute;bottom:80px;left:50%;transform:translateX(-50%);background:var(--accent);color:#fff;padding:8px 16px;border-radius:8px;font-size:12px;font-weight:600;z-index:10;display:none}
114
-
115
- /* ── Verification method (condition nodes) ── */
116
- .verify-method-grid{display:grid;grid-template-columns:1fr 1fr;gap:6px;margin-bottom:8px}
117
- .verify-method-btn{padding:8px;border-radius:6px;font-size:11px;font-weight:600;cursor:pointer;border:1px solid var(--line);background:var(--surface);color:var(--muted);text-align:center;transition:all .15s}
118
- .verify-method-btn:hover{border-color:var(--accent);color:var(--text)}
119
- .verify-method-btn.active{background:var(--accent-soft);border-color:var(--accent);color:var(--accent)}
120
- .verify-script{font-family:'JetBrains Mono',monospace;font-size:11px;background:#0d0f14;border:1px solid var(--line);border-radius:6px;padding:8px 10px;color:#a5d6ff;min-height:60px;resize:vertical;width:100%;outline:none;line-height:1.6}
121
- .verify-script:focus{border-color:var(--accent)}
122
- .verify-hint{font-size:10px;color:var(--dim);margin-top:4px;line-height:1.4}
123
- .verify-expect{display:flex;gap:6px;align-items:center;margin-top:6px}
124
- .verify-expect label{font-size:10px;color:var(--muted);white-space:nowrap}
125
- .verify-expect input{flex:1;font-size:11px;background:var(--surface);border:1px solid var(--line);border-radius:4px;padding:4px 8px;color:var(--text);outline:none;font-family:monospace}
126
-
127
- /* ── Agent feedback overlay ── */
128
- .agent-overlay{position:absolute;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,.5);z-index:50;display:none;align-items:center;justify-content:center}
129
- .agent-overlay.show{display:flex}
130
- .agent-card{background:var(--panel);border:1px solid var(--accent);border-radius:12px;padding:20px 24px;max-width:460px;width:90%;box-shadow:0 8px 32px rgba(99,102,241,.2);animation:slideUp .3s ease}
131
- @keyframes slideUp{from{transform:translateY(20px);opacity:0}to{transform:translateY(0);opacity:1}}
132
- .agent-card h3{font-size:15px;font-weight:800;margin-bottom:12px;display:flex;align-items:center;gap:8px}
133
- .agent-card p{font-size:12px;color:var(--muted);line-height:1.6;margin-bottom:12px}
134
- .agent-card .node-preview{background:var(--surface);border:1px solid var(--line);border-radius:8px;padding:10px;margin-bottom:12px;max-height:200px;overflow-y:auto;font-size:11px;line-height:1.6}
135
- .agent-card .actions{display:flex;gap:8px;justify-content:flex-end}
136
-
137
- /* ── Agent receive indicator ── */
138
- .agent-indicator{position:absolute;top:56px;right:14px;z-index:10;display:none;align-items:center;gap:6px;padding:6px 12px;background:var(--accent-soft);border:1px solid var(--accent);border-radius:8px;font-size:11px;color:var(--accent);font-weight:600;animation:pulse 2s infinite}
139
- .agent-indicator.show{display:flex}
140
- @keyframes pulse{0%,100%{opacity:1}50%{opacity:.6}}
141
-
142
- /* ── Zoom indicator ── */
143
- .zoom-indicator{position:absolute;bottom:92px;right:14px;z-index:10;padding:4px 10px;background:var(--panel);border:1px solid var(--line);border-radius:6px;font-size:10px;color:var(--muted);font-family:monospace;pointer-events:none}
144
- </style>
145
- </head>
146
- <body>
147
-
148
- <div class="app">
149
- <!-- Sidebar -->
150
- <div class="sidebar">
151
- <div class="sidebar-header">
152
- <h1>⚡ Cron Workflow</h1>
153
- <p>View Jobs · Design Workflows · Send to Chat</p>
154
- </div>
155
-
156
- <div style="padding:10px 14px">
157
- <div class="tbtn" onclick="newWorkflow()" style="width:100%;justify-content:center;background:var(--accent);border-color:var(--accent);color:#fff;font-weight:700">+ New Workflow</div>
158
- </div>
159
-
160
- <div class="sidebar-section">
161
- <h3>📐 My Workflows</h3>
162
- </div>
163
- <div class="job-list" id="workflowList"></div>
164
-
165
- <div class="sidebar-section">
166
- <h3>📋 Deployed Cron Jobs</h3>
167
- </div>
168
- <div class="job-list" id="jobList">
169
- <div style="padding:20px;text-align:center;color:var(--muted);font-size:12px">Loading...</div>
170
- </div>
171
-
172
- <div class="sidebar-status" id="sidebarStatus">
173
- <span class="dot dot-err"></span> Checking connection...
174
- </div>
175
- </div>
176
-
177
- <!-- Canvas -->
178
- <div class="canvas-area">
179
- <div class="canvas-grid"></div>
180
-
181
- <!-- Toolbar -->
182
- <div class="toolbar">
183
- <div class="tbtn" onclick="toggleTemplates()" id="btnTemplate">📋 Scenarios</div>
184
- <div class="tbtn" onclick="addNode('trigger')">⏰ Trigger</div>
185
- <div class="tbtn" onclick="addNode('prompt')">🤖 Prompt</div>
186
- <div class="tbtn" onclick="addNode('condition')">🔀 Condition</div>
187
- <div class="tbtn" onclick="addNode('action')">📤 Action</div>
188
- <div class="tbtn" onclick="requestDecompose()" style="background:var(--accent-soft);border-color:var(--accent);color:var(--accent)">🤖 Decompose</div>
189
- <div class="tbtn" onclick="zoomIn()" title="Zoom in">🔍+</div>
190
- <div class="tbtn" onclick="zoomOut()" title="Zoom out">🔍−</div>
191
- <div class="tbtn" onclick="fitAll()" title="Fit all">⊞</div>
192
- <div class="tbtn" onclick="clearCanvas()">🗑️</div>
193
- </div>
194
-
195
- <!-- Templates -->
196
- <div class="template-panel" id="templatePanel">
197
- <div class="template-item" onclick="loadTemplate('news')">
198
- <span class="ticon">📰</span>
199
- <div><div class="tlabel">News Monitoring</div><div class="tdesc">Daily morning keyword news search → summarize → notify</div></div>
200
- </div>
201
- <div class="template-item" onclick="loadTemplate('report')">
202
- <span class="ticon">📊</span>
203
- <div><div class="tlabel">Daily Report</div><div class="tdesc">Daily evening log analysis → generate report → send</div></div>
204
- </div>
205
- <div class="template-item" onclick="loadTemplate('health')">
206
- <span class="ticon">💓</span>
207
- <div><div class="tlabel">Service Health Check</div><div class="tdesc">Every 30 min URL check → alert on failure</div></div>
208
- </div>
209
- <div class="template-item" onclick="loadTemplate('cleanup')">
210
- <span class="ticon">🧹</span>
211
- <div><div class="tlabel">Cleanup Task</div><div class="tdesc">Weekly old file cleanup → report results</div></div>
212
- </div>
213
- <div class="template-item" onclick="loadTemplate('channel-monitor')">
214
- <span class="ticon">📡</span>
215
- <div><div class="tlabel">Channel Monitoring</div><div class="tdesc">Slack/Chatwork monitoring → Discord relay + daily Trello comparison</div></div>
216
- </div>
217
- <div class="template-item" onclick="loadTemplate('custom')">
218
- <span class="ticon">✨</span>
219
- <div><div class="tlabel">Blank Canvas</div><div class="tdesc">Build from scratch</div></div>
220
- </div>
221
- </div>
222
-
223
- <!-- Workflow Name Bar -->
224
- <div class="wf-name-bar">
225
- <input class="wf-name-input" id="wfName" placeholder="Enter workflow name" oninput="onWfNameChange()">
226
- <span class="wf-status wf-status-draft" id="wfStatus">draft</span>
227
- </div>
228
-
229
- <!-- SVG Canvas -->
230
- <svg id="canvas" xmlns="http://www.w3.org/2000/svg"></svg>
231
-
232
- <!-- Detail Panel -->
233
- <div class="detail-panel" id="detailPanel">
234
- <div class="dp-header">
235
- <h3 id="dpTitle">Node Settings</h3>
236
- <button class="dp-close" onclick="closeDetail()">&times;</button>
237
- </div>
238
- <div class="dp-body" id="dpBody"></div>
239
- <div class="dp-actions">
240
- <button class="btn btn-primary" onclick="saveNodeDetail()" style="flex:1">Apply</button>
241
- <button class="btn btn-ghost" onclick="deleteSelectedNode()" style="color:var(--danger)">Delete</button>
242
- </div>
243
- </div>
244
-
245
- <!-- Connection hint -->
246
- <div class="connecting-hint" id="connectHint">🔗 Click the input port (left ●) of the target node</div>
247
-
248
- <!-- Zoom indicator -->
249
- <div class="zoom-indicator" id="zoomIndicator">100%</div>
250
-
251
- <!-- Agent receive indicator -->
252
- <div class="agent-indicator" id="agentIndicator">🤖 Receiving agent response...</div>
253
-
254
- <!-- Agent feedback overlay -->
255
- <div class="agent-overlay" id="agentOverlay">
256
- <div class="agent-card">
257
- <h3>🤖 Agent Suggestion</h3>
258
- <p id="agentMessage">The workflow has been analyzed.</p>
259
- <div class="node-preview" id="agentPreview"></div>
260
- <div class="actions">
261
- <button class="btn btn-ghost" onclick="rejectAgent()">Dismiss</button>
262
- <button class="btn btn-ghost" onclick="editAgent()">Edit & Apply</button>
263
- <button class="btn btn-primary" onclick="acceptAgent()">Apply</button>
264
- </div>
265
- </div>
266
- </div>
267
- </div>
268
-
269
- <!-- Bottom Bar -->
270
- <div class="bottom-bar">
271
- <div class="prompt-preview" id="promptPreview">Build a workflow and the request will be generated here...</div>
272
- <button class="btn btn-ghost" onclick="copyPrompt()">📋 Copy</button>
273
- <button class="btn btn-ghost" onclick="testRun()">🧪 Test Run</button>
274
- <button class="btn btn-send" onclick="deployToChat()">🚀 Deploy</button>
275
- </div>
276
- </div>
277
-
278
- <div class="toast" id="toast"></div>
279
-
280
- <script>
281
- // ─── State ───
282
- const state = {
283
- // Current workflow
284
- wfId: null,
285
- wfName: '',
286
- wfStatus: 'draft', // draft | deployed | paused
287
- nodes: [],
288
- connections: [],
289
- // Multi-workflow store
290
- workflows: [], // [{id, name, status, nodes, connections, nextId, updatedAt}]
291
- existingJobs: [],
292
- // UI state
293
- selectedNode: null,
294
- dragging: null,
295
- dragOffset: {x:0,y:0},
296
- connecting: null,
297
- nextId: 1,
298
- panOffset: {x:0, y:0},
299
- // Zoom & pan
300
- zoom: 1,
301
- viewX: 0, viewY: 0,
302
- panning: false, panStart: null
303
- };
304
-
305
- const NODE_W = 180, NODE_H = 64;
306
- const COLORS = {trigger:'#f59e0b',prompt:'#6366f1',condition:'#ec4899',action:'#22c55e'};
307
- const ICONS = {trigger:'⏰',prompt:'🤖',condition:'🔀',action:'📤'};
308
- const LABELS = {trigger:'Trigger',prompt:'Prompt',condition:'Condition',action:'Action'};
309
-
310
- const CRON_PRESETS = {
311
- '* * * * *': 'Every minute',
312
- '*/5 * * * *': 'Every 5 minutes',
313
- '*/30 * * * *': 'Every 30 minutes',
314
- '0 * * * *': 'Every hour',
315
- '0 */2 * * *': 'Every 2 hours',
316
- '0 9 * * *': 'Daily at 09:00',
317
- '0 18 * * *': 'Daily at 18:00',
318
- '0 9 * * 1-5': 'Weekdays at 09:00',
319
- '0 0 * * 0': 'Every Sunday',
320
- '0 0 1 * *': '1st of every month'
321
- };
322
-
323
- // ─── Canvas SVG ───
324
- const svg = document.getElementById('canvas');
325
- const ns = 'http://www.w3.org/2000/svg';
326
-
327
- function el(tag, attrs={}) {
328
- const e = document.createElementNS(ns, tag);
329
- for (const [k,v] of Object.entries(attrs)) e.setAttribute(k, v);
330
- return e;
331
- }
332
-
333
- function updateViewBox() {
334
- const rect = svg.getBoundingClientRect();
335
- const w = rect.width / state.zoom;
336
- const h = rect.height / state.zoom;
337
- svg.setAttribute('viewBox', `${state.viewX} ${state.viewY} ${w} ${h}`);
338
- const zi = document.getElementById('zoomIndicator');
339
- if (zi) zi.textContent = Math.round(state.zoom * 100) + '%';
340
- }
341
-
342
- function svgPoint(clientX, clientY) {
343
- const rect = svg.getBoundingClientRect();
344
- return {
345
- x: state.viewX + (clientX - rect.left) / state.zoom,
346
- y: state.viewY + (clientY - rect.top) / state.zoom
347
- };
348
- }
349
-
350
- function render() {
351
- svg.innerHTML = '';
352
- updateViewBox();
353
-
354
- // Defs
355
- const defs = el('defs');
356
- for (const [type, color] of Object.entries(COLORS)) {
357
- const g = el('linearGradient', {id:`grad-${type}`, x1:'0', y1:'0', x2:'0', y2:'1'});
358
- g.appendChild(el('stop', {offset:'0%', 'stop-color': color, 'stop-opacity':'0.15'}));
359
- g.appendChild(el('stop', {offset:'100%', 'stop-color': color, 'stop-opacity':'0.05'}));
360
- defs.appendChild(g);
361
- }
362
- // Arrow marker
363
- const marker = el('marker', {id:'arrow', viewBox:'0 0 10 10', refX:'8', refY:'5', markerWidth:'8', markerHeight:'8', orient:'auto-start-reverse'});
364
- marker.appendChild(el('path', {d:'M 0 0 L 10 5 L 0 10 z', fill: 'var(--accent)'}));
365
- defs.appendChild(marker);
366
- svg.appendChild(defs);
367
-
368
- // Connections
369
- for (const conn of state.connections) {
370
- const from = state.nodes.find(n => n.id === conn.from);
371
- const to = state.nodes.find(n => n.id === conn.to);
372
- if (!from || !to) continue;
373
- const x1 = from.x + NODE_W, y1 = from.y + NODE_H/2;
374
- const x2 = to.x, y2 = to.y + NODE_H/2;
375
- const mx = (x1+x2)/2;
376
- const path = el('path', {
377
- d: `M${x1},${y1} C${mx},${y1} ${mx},${y2} ${x2},${y2}`,
378
- stroke: COLORS[from.type] || 'var(--accent)',
379
- 'stroke-width': '2',
380
- fill: 'none',
381
- 'stroke-opacity': '0.6',
382
- 'marker-end': 'url(#arrow)'
383
- });
384
- // Click to delete connection
385
- const hitPath = el('path', {
386
- d: `M${x1},${y1} C${mx},${y1} ${mx},${y2} ${x2},${y2}`,
387
- stroke: 'transparent', 'stroke-width': '12', fill: 'none', cursor: 'pointer'
388
- });
389
- hitPath.addEventListener('click', (e) => {
390
- e.stopPropagation();
391
- state.connections = state.connections.filter(c => c !== conn);
392
- render();
393
- updatePrompt();
394
- });
395
- svg.appendChild(path);
396
- svg.appendChild(hitPath);
397
-
398
- // Label on connection
399
- if (conn.label) {
400
- const lx = mx, ly = (y1+y2)/2 - 10;
401
- const t = el('text', {x:lx, y:ly, fill:'var(--muted)', 'font-size':'10', 'text-anchor':'middle', 'font-family':'inherit'});
402
- t.textContent = conn.label;
403
- svg.appendChild(t);
404
- }
405
- }
406
-
407
- // Temp connection line
408
- if (state.connecting && state._tempMouse) {
409
- const from = state.nodes.find(n => n.id === state.connecting.fromId);
410
- if (from) {
411
- const x1 = from.x + NODE_W, y1 = from.y + NODE_H/2;
412
- const path = el('path', {
413
- d: `M${x1},${y1} L${state._tempMouse.x},${state._tempMouse.y}`,
414
- stroke: COLORS[from.type], 'stroke-width': '2', fill: 'none', 'stroke-dasharray': '6,4', 'stroke-opacity': '0.6'
415
- });
416
- svg.appendChild(path);
417
- }
418
- }
419
-
420
- // Nodes
421
- for (const node of state.nodes) {
422
- const g = el('g', {transform: `translate(${node.x},${node.y})`, class: 'node-group'});
423
- const color = COLORS[node.type];
424
- const isSelected = state.selectedNode === node.id;
425
-
426
- // Body
427
- const body = el('rect', {
428
- width: NODE_W, height: NODE_H,
429
- fill: `url(#grad-${node.type})`,
430
- stroke: isSelected ? color : 'var(--line)',
431
- 'stroke-width': isSelected ? 2.5 : 1.5,
432
- rx: 10, ry: 10,
433
- class: 'node-body'
434
- });
435
- g.appendChild(body);
436
-
437
- // Color accent bar
438
- g.appendChild(el('rect', {x:0, y:0, width:4, height:NODE_H, fill:color, rx:2}));
439
-
440
- // Icon + label
441
- const icon = el('text', {x:16, y:26, fill:color, 'font-size':'14'});
442
- icon.textContent = ICONS[node.type];
443
- g.appendChild(icon);
444
-
445
- const label = el('text', {x:34, y:26, fill:'var(--text)', 'font-size':'12', 'font-weight':'700', 'font-family':'inherit'});
446
- label.textContent = node.label || LABELS[node.type];
447
- g.appendChild(label);
448
-
449
- // Subtitle
450
- const sub = el('text', {x:16, y:46, fill:'var(--muted)', 'font-size':'10', 'font-family':'inherit'});
451
- sub.textContent = getNodeSubtitle(node);
452
- g.appendChild(sub);
453
-
454
- // Input port (left) — except for trigger
455
- if (node.type !== 'trigger') {
456
- const ip = el('circle', {cx:0, cy:NODE_H/2, r:5, fill:'var(--bg)', stroke:color, 'stroke-width':2, class:'node-port'});
457
- ip.addEventListener('mousedown', (e) => { e.stopPropagation(); });
458
- ip.addEventListener('click', (e) => {
459
- e.stopPropagation();
460
- if (state.connecting) {
461
- finishConnection(node.id);
462
- }
463
- });
464
- g.appendChild(ip);
465
- }
466
-
467
- // Output port (right)
468
- const op = el('circle', {cx:NODE_W, cy:NODE_H/2, r:5, fill:color, stroke:color, 'stroke-width':2, class:'node-port'});
469
- op.addEventListener('mousedown', (e) => {
470
- e.stopPropagation();
471
- startConnection(node.id);
472
- });
473
- g.appendChild(op);
474
-
475
- // Drag
476
- g.addEventListener('mousedown', (e) => {
477
- if (e.target.classList.contains('node-port')) return;
478
- state.dragging = node.id;
479
- const pt = svgPoint(e.clientX, e.clientY);
480
- state.dragOffset = {x: pt.x - node.x, y: pt.y - node.y};
481
- e.preventDefault();
482
- });
483
-
484
- // Click to select
485
- g.addEventListener('click', (e) => {
486
- if (e.target.classList.contains('node-port')) return;
487
- selectNode(node.id);
488
- });
489
-
490
- svg.appendChild(g);
491
- }
492
-
493
- updatePrompt();
494
- }
495
-
496
- function getNodeSubtitle(node) {
497
- switch (node.type) {
498
- case 'trigger': return node.config.schedule || node.config.description || (node.config.cron ? cronToHuman(node.config.cron) : 'Specify when to run');
499
- case 'prompt': return node.config.prompt ? truncate(node.config.prompt, 24) : 'Prompt required';
500
- case 'condition': {
501
- const methods = {bash:'🖥',http:'🌐',string:'🔍',custom:'📝'};
502
- const m = methods[node.config.verifyMethod] || '🔀';
503
- return node.config.condition ? `${m} ${truncate(node.config.condition,20)}` : 'Condition required';
504
- }
505
- case 'action': return node.config.actionType || 'Action required';
506
- default: return '';
507
- }
508
- }
509
-
510
- function truncate(s, n) { return s.length > n ? s.slice(0, n) + '...' : s; }
511
-
512
- // ─── Node Operations ───
513
- function addNode(type, x, y, config) {
514
- const area = svg.getBoundingClientRect();
515
- const node = {
516
- id: 'n' + (state.nextId++),
517
- type,
518
- label: LABELS[type],
519
- x: x || 100 + Math.random() * (area.width - 300),
520
- y: y || 80 + Math.random() * (area.height - 200),
521
- config: config || getDefaultConfig(type)
522
- };
523
- state.nodes.push(node);
524
- render();
525
- selectNode(node.id);
526
- return node;
527
- }
528
-
529
- function getDefaultConfig(type) {
530
- switch(type) {
531
- case 'trigger': return {cron:'', description:'', schedule:''};
532
- case 'prompt': return {prompt:'', model:'default'};
533
- case 'condition': return {condition:'', trueLabel:'Pass', falseLabel:'Fail', verifyMethod:'bash', verifyScript:'', verifyExpect:'exit 0'};
534
- case 'action': return {actionType:'discord', target:'', message:''};
535
- default: return {};
536
- }
537
- }
538
-
539
- function deleteSelectedNode() {
540
- if (!state.selectedNode) return;
541
- state.nodes = state.nodes.filter(n => n.id !== state.selectedNode);
542
- state.connections = state.connections.filter(c => c.from !== state.selectedNode && c.to !== state.selectedNode);
543
- state.selectedNode = null;
544
- closeDetail();
545
- render();
546
- }
547
-
548
- function clearCanvas() {
549
- if (state.nodes.length === 0) return;
550
- state.nodes = [];
551
- state.connections = [];
552
- state.selectedNode = null;
553
- closeDetail();
554
- render();
555
- toast('Canvas cleared');
556
- }
557
-
558
- // ─── Connections ───
559
- function startConnection(fromId) {
560
- state.connecting = {fromId};
561
- document.getElementById('connectHint').style.display = 'block';
562
- svg.style.cursor = 'crosshair';
563
- }
564
-
565
- function finishConnection(toId) {
566
- if (!state.connecting) return;
567
- const {fromId} = state.connecting;
568
- if (fromId === toId) { cancelConnection(); return; }
569
- const exists = state.connections.some(c => c.from === fromId && c.to === toId);
570
- if (!exists) {
571
- state.connections.push({from: fromId, to: toId, label:''});
572
- }
573
- cancelConnection();
574
- render();
575
- }
576
-
577
- function cancelConnection() {
578
- state.connecting = null;
579
- state._tempMouse = null;
580
- document.getElementById('connectHint').style.display = 'none';
581
- svg.style.cursor = '';
582
- render();
583
- }
584
-
585
- // ─── Selection / Detail ───
586
- function selectNode(id) {
587
- state.selectedNode = id;
588
- const node = state.nodes.find(n => n.id === id);
589
- if (!node) return;
590
- showDetail(node);
591
- render();
592
- }
593
-
594
- function showDetail(node) {
595
- const panel = document.getElementById('detailPanel');
596
- const title = document.getElementById('dpTitle');
597
- const body = document.getElementById('dpBody');
598
-
599
- title.textContent = `${ICONS[node.type]} ${node.label}`;
600
- let html = `<div class="dp-field"><label class="dp-label">Name</label><input class="dp-input" id="nd-label" value="${esc(node.label)}"></div>`;
601
-
602
- switch(node.type) {
603
- case 'trigger':
604
- html += `
605
- <div class="dp-field"><label class="dp-label">📝 When should it run?</label><textarea class="dp-input" id="nd-schedule" rows="2" placeholder="e.g. Every hour from 9 AM to 7 PM daily\ne.g. Weekdays at 10 AM\ne.g. Every Monday morning">${esc(node.config.schedule||'')}</textarea><div style="font-size:10px;color:var(--muted);margin-top:4px">Describe in natural language and the OS agent will convert it to a cron expression</div></div>
606
- <div class="dp-field"><label class="dp-label">Quick Select</label><div style="display:flex;flex-wrap:wrap;gap:4px">${
607
- [['Daily at 9 AM','Daily at 9 AM'],['Daily at 6 PM','Daily at 6 PM'],['Weekdays at 9 AM','Weekdays at 9 AM'],['Every 30 minutes','Every 30 minutes'],['Every hour','Every hour'],['Every Monday','Every Monday morning'],['1st of month','1st of every month at midnight']].map(([val,label]) =>
608
- `<span style="padding:4px 10px;border-radius:6px;font-size:11px;cursor:pointer;background:var(--surface);color:var(--muted);border:1px solid var(--line);transition:all .15s" onmouseover="this.style.borderColor='var(--accent)';this.style.color='var(--text)'" onmouseout="this.style.borderColor='var(--line)';this.style.color='var(--muted)'" onclick="document.getElementById('nd-schedule').value='${val}'">${label}</span>`
609
- ).join('')
610
- }</div></div>
611
- <details style="margin-top:8px"><summary style="font-size:11px;color:var(--dim);cursor:pointer">Advanced: Enter cron expression directly</summary><div class="dp-field" style="margin-top:8px"><input class="dp-input" id="nd-cron" value="${esc(node.config.cron)}" placeholder="0 9-19 * * *" style="font-family:monospace;font-size:12px"></div></details>`;
612
- break;
613
- case 'prompt':
614
- html += `
615
- <div class="dp-field"><label class="dp-label">Prompt</label><textarea class="dp-input" id="nd-prompt" rows="4" placeholder="Task for the agent...">${esc(node.config.prompt)}</textarea></div>`;
616
- break;
617
- case 'condition':
618
- const vm = node.config.verifyMethod || 'bash';
619
- const verifyHints = {
620
- bash: 'Exit code 0 = success. e.g. curl -sf https://example.com',
621
- http: '判断 by HTTP status code. e.g. GET https://example.com → 200',
622
- string: 'Check if output contains a specific string. e.g. Output contains "OK"',
623
- custom: 'Free-form script. exit 0 = true, exit 1 = false'
624
- };
625
- const verifyPlaceholders = {
626
- bash: 'curl -sf https://example.com/health',
627
- http: 'https://example.com/api/status',
628
- string: 'String to find in previous step output',
629
- custom: '#!/bin/bash\nresult=$(curl -s https://api.example.com)\nif echo "$result" | jq -e ".status == \\"ok\\"" > /dev/null; then\n exit 0\nelse\n exit 1\nfi'
630
- };
631
- html += `
632
- <div class="dp-field"><label class="dp-label">Condition Description</label><input class="dp-input" id="nd-condition" value="${esc(node.config.condition)}" placeholder="e.g. Check if the service response is healthy"></div>
633
- <div class="dp-field"><label class="dp-label">🔬 Verification Method</label>
634
- <div class="verify-method-grid">
635
- <div class="verify-method-btn ${vm==='bash'?'active':''}" onclick="setVerifyMethod('bash')">🖥 Bash<br><span style="font-size:9px;font-weight:400">exit code</span></div>
636
- <div class="verify-method-btn ${vm==='http'?'active':''}" onclick="setVerifyMethod('http')">🌐 HTTP<br><span style="font-size:9px;font-weight:400">status code</span></div>
637
- <div class="verify-method-btn ${vm==='string'?'active':''}" onclick="setVerifyMethod('string')">🔍 String<br><span style="font-size:9px;font-weight:400">contains</span></div>
638
- <div class="verify-method-btn ${vm==='custom'?'active':''}" onclick="setVerifyMethod('custom')">📝 Custom<br><span style="font-size:9px;font-weight:400">script</span></div>
639
- </div>
640
- </div>
641
- <div class="dp-field"><label class="dp-label">Verification Script</label><textarea class="verify-script" id="nd-verify-script" rows="3" placeholder="${esc(verifyPlaceholders[vm])}">${esc(node.config.verifyScript||'')}</textarea><div class="verify-hint" id="nd-verify-hint">${verifyHints[vm]}</div></div>
642
- <div class="dp-field"><label class="dp-label">Expected Result</label><input class="dp-input" id="nd-verify-expect" value="${esc(node.config.verifyExpect||'exit 0')}" placeholder="exit 0" style="font-family:monospace;font-size:11px"></div>
643
- <div style="display:flex;gap:8px;margin-top:4px">
644
- <div class="dp-field" style="flex:1"><label class="dp-label">True Label</label><input class="dp-input" id="nd-true" value="${esc(node.config.trueLabel)}"></div>
645
- <div class="dp-field" style="flex:1"><label class="dp-label">False Label</label><input class="dp-input" id="nd-false" value="${esc(node.config.falseLabel)}"></div>
646
- </div>`;
647
- break;
648
- case 'action':
649
- html += `
650
- <div class="dp-field"><label class="dp-label">Action Type</label>
651
- <select class="dp-input" id="nd-actiontype">
652
- <option value="discord" ${node.config.actionType==='discord'?'selected':''}>Send to Discord</option>
653
- <option value="slack" ${node.config.actionType==='slack'?'selected':''}>Send to Slack</option>
654
- <option value="file" ${node.config.actionType==='file'?'selected':''}>Save to File</option>
655
- <option value="api" ${node.config.actionType==='api'?'selected':''}>API Call</option>
656
- <option value="email" ${node.config.actionType==='email'?'selected':''}>Email</option>
657
- </select>
658
- </div>
659
- <div class="dp-field"><label class="dp-label">Target</label><input class="dp-input" id="nd-target" value="${esc(node.config.target)}" placeholder="Channel ID, URL, etc."></div>
660
- <div class="dp-field"><label class="dp-label">Message Template</label><textarea class="dp-input" id="nd-message" rows="3" placeholder="Content to send...">${esc(node.config.message||'')}</textarea></div>`;
661
- break;
662
- }
663
- body.innerHTML = html;
664
- panel.classList.add('show');
665
- }
666
-
667
- function saveNodeDetail() {
668
- const node = state.nodes.find(n => n.id === state.selectedNode);
669
- if (!node) return;
670
- node.label = document.getElementById('nd-label')?.value || node.label;
671
-
672
- switch(node.type) {
673
- case 'trigger':
674
- node.config.schedule = document.getElementById('nd-schedule')?.value || '';
675
- node.config.cron = document.getElementById('nd-cron')?.value || '';
676
- break;
677
- case 'prompt':
678
- node.config.prompt = document.getElementById('nd-prompt')?.value || '';
679
- break;
680
- case 'condition':
681
- node.config.condition = document.getElementById('nd-condition')?.value || '';
682
- node.config.trueLabel = document.getElementById('nd-true')?.value || 'True';
683
- node.config.falseLabel = document.getElementById('nd-false')?.value || 'False';
684
- node.config.verifyScript = document.getElementById('nd-verify-script')?.value || '';
685
- node.config.verifyExpect = document.getElementById('nd-verify-expect')?.value || 'exit 0';
686
- break;
687
- case 'action':
688
- node.config.actionType = document.getElementById('nd-actiontype')?.value || 'discord';
689
- node.config.target = document.getElementById('nd-target')?.value || '';
690
- node.config.message = document.getElementById('nd-message')?.value || '';
691
- break;
692
- }
693
- render();
694
- toast('Node settings saved', 'success');
695
- }
696
-
697
- function closeDetail() {
698
- document.getElementById('detailPanel').classList.remove('show');
699
- }
700
-
701
- function esc(s) { return (s||'').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;').replace(/'/g,'&#39;'); }
702
-
703
- // ─── Templates ───
704
- function toggleTemplates() {
705
- document.getElementById('templatePanel').classList.toggle('show');
706
- }
707
-
708
- function loadTemplate(name) {
709
- state.nodes = [];
710
- state.connections = [];
711
- state.selectedNode = null;
712
- closeDetail();
713
-
714
- const cx = 80, cy = 120, gap = 220;
715
-
716
- switch(name) {
717
- case 'news': {
718
- const t = addNodeAt('trigger', cx, cy, {schedule:'Daily at 9 AM', cron:''});
719
- const p = addNodeAt('prompt', cx+gap, cy, {prompt:'Search today\'s top news and summarize in 3 lines'});
720
- const a = addNodeAt('action', cx+gap*2, cy, {actionType:'discord', target:'', message:'📰 Today\'s News Summary'});
721
- state.connections.push({from:t.id, to:p.id}, {from:p.id, to:a.id});
722
- break;
723
- }
724
- case 'report': {
725
- const t = addNodeAt('trigger', cx, cy, {schedule:'Weekdays at 6 PM', cron:''});
726
- const p = addNodeAt('prompt', cx+gap, cy, {prompt:'Analyze today\'s logs and write a daily report'});
727
- const a = addNodeAt('action', cx+gap*2, cy, {actionType:'file', target:'~/.mama/workspace/reports/', message:'Daily Report'});
728
- state.connections.push({from:t.id, to:p.id}, {from:p.id, to:a.id});
729
- break;
730
- }
731
- case 'health': {
732
- const t = addNodeAt('trigger', cx, cy, {schedule:'Every 30 minutes', cron:''});
733
- const p = addNodeAt('prompt', cx+gap, cy, {prompt:'Access https://example.com and verify the response'});
734
- const c = addNodeAt('condition', cx+gap*2, cy, {condition:'On response failure', trueLabel:'Healthy', falseLabel:'Down'});
735
- const a = addNodeAt('action', cx+gap*3, cy+60, {actionType:'discord', target:'', message:'⚠️ Service outage detected!'});
736
- state.connections.push({from:t.id, to:p.id}, {from:p.id, to:c.id}, {from:c.id, to:a.id, label:'Down'});
737
- break;
738
- }
739
- case 'cleanup': {
740
- const t = addNodeAt('trigger', cx, cy, {schedule:'Every Sunday at 3 AM', cron:''});
741
- const p = addNodeAt('prompt', cx+gap, cy, {prompt:'Clean up temp files and logs older than 7 days'});
742
- const a = addNodeAt('action', cx+gap*2, cy, {actionType:'discord', target:'', message:'🧹 Cleanup complete report'});
743
- state.connections.push({from:t.id, to:p.id}, {from:p.id, to:a.id});
744
- break;
745
- }
746
- case 'channel-monitor': {
747
- // ── Flow A: Poll every 5 min for new messages ──
748
- const t1 = addNodeAt('trigger', 60, 80, {schedule:'Every 5 minutes', cron:'*/5 * * * *'});
749
- t1.label = '5-min Polling';
750
-
751
- const p1 = addNodeAt('prompt', 280, 40, {prompt:'Check 5 Slack channels (#project-a, #project-b, #project-c, #design, #general) for new messages since last check. Summarize new message count and content per channel.'});
752
- p1.label = 'Check Slack 5ch';
753
-
754
- const p2 = addNodeAt('prompt', 280, 150, {prompt:'Check 3 Chatwork rooms (Dev Team, Design Team, Admin) for new messages since last check. Summarize new message count and content per room.'});
755
- p2.label = 'Check Chatwork 3ch';
756
-
757
- const c1 = addNodeAt('condition', 540, 40, {
758
- condition:'New messages in Slack?',
759
- verifyMethod:'string', verifyScript:'new message', verifyExpect:'contains',
760
- trueLabel:'Has messages', falseLabel:'None'
761
- });
762
- c1.label = 'Slack new?';
763
-
764
- const c2 = addNodeAt('condition', 540, 150, {
765
- condition:'New messages in Chatwork?',
766
- verifyMethod:'string', verifyScript:'new message', verifyExpect:'contains',
767
- trueLabel:'Has messages', falseLabel:'None'
768
- });
769
- c2.label = 'CW new?';
770
-
771
- const a1 = addNodeAt('action', 780, 40, {actionType:'discord', target:'#slack-feed', message:'📨 Slack new messages:\n{per-channel summary}'});
772
- a1.label = 'Discord: Slack';
773
-
774
- const a2 = addNodeAt('action', 780, 150, {actionType:'discord', target:'#chatwork-feed', message:'📨 Chatwork new messages:\n{per-room summary}'});
775
- a2.label = 'Discord: CW';
776
-
777
- // ── Flow B: Daily summary at 8:30 ──
778
- const t2 = addNodeAt('trigger', 60, 320, {schedule:'Daily at 8:30 AM', cron:'30 8 * * *'});
779
- t2.label = 'Daily 8:30';
780
-
781
- const p3 = addNodeAt('prompt', 280, 300, {prompt:'Compile all messages from yesterday to today across 5 Slack channels + 3 Chatwork rooms.\n\nFor each channel:\n- Key discussion summary (3 lines)\n- Decisions made\n- Action items (assignee, deadline)\n- New tasks mentioned'});
782
- p3.label = 'Compile All Messages';
783
-
784
- const p4 = addNodeAt('prompt', 540, 300, {prompt:'Compare the compiled action items and new tasks with the Trello board.\n\nOutput format:\n📋 In Progress: [card name] - [assignee] - [deadline]\n✅ Completed: [card name] - [completion date]\n🆕 New Tasks (not on Trello): [task name] - [source channel]\n🔥 High Priority: [with reason]'});
785
- p4.label = 'Trello Compare';
786
-
787
- const c3 = addNodeAt('condition', 540, 420, {
788
- condition:'Any new tasks not registered on Trello?',
789
- verifyMethod:'string', verifyScript:'🆕 New Tasks', verifyExpect:'contains',
790
- trueLabel:'Unregistered found', falseLabel:'All registered'
791
- });
792
- c3.label = 'Unregistered?';
793
-
794
- const a3 = addNodeAt('action', 780, 300, {actionType:'discord', target:'#daily-report', message:'📊 Daily Channel Report + Trello Status'});
795
- a3.label = 'Discord: Report';
796
-
797
- const a4 = addNodeAt('action', 780, 420, {actionType:'api', target:'Trello API', message:'Auto-create cards in Trello Inbox list for unregistered tasks'});
798
- a4.label = 'Create Trello Cards';
799
-
800
- // Connections - Flow A
801
- state.connections.push(
802
- {from:t1.id, to:p1.id, label:''},
803
- {from:t1.id, to:p2.id, label:''},
804
- {from:p1.id, to:c1.id, label:''},
805
- {from:p2.id, to:c2.id, label:''},
806
- {from:c1.id, to:a1.id, label:'Yes'},
807
- {from:c2.id, to:a2.id, label:'Yes'}
808
- );
809
- // Connections - Flow B
810
- state.connections.push(
811
- {from:t2.id, to:p3.id, label:''},
812
- {from:p3.id, to:p4.id, label:''},
813
- {from:p4.id, to:a3.id, label:''},
814
- {from:p4.id, to:c3.id, label:''},
815
- {from:c3.id, to:a4.id, label:'Yes'}
816
- );
817
-
818
- state.wfName = 'Slack/CW Monitoring + Daily Trello Report';
819
- document.getElementById('wfName').value = state.wfName;
820
- break;
821
- }
822
- case 'custom':
823
- break;
824
- }
825
- document.getElementById('templatePanel').classList.remove('show');
826
- render();
827
- toast(name === 'custom' ? 'Blank canvas ready' : 'Scenario loaded', 'success');
828
- }
829
-
830
- function addNodeAt(type, x, y, config) {
831
- const node = {
832
- id: 'n' + (state.nextId++),
833
- type, label: LABELS[type],
834
- x, y, config: {...getDefaultConfig(type), ...config}
835
- };
836
- state.nodes.push(node);
837
- return node;
838
- }
839
-
840
- // ─── Prompt Generation ───
841
- function updatePrompt() {
842
- const preview = document.getElementById('promptPreview');
843
- if (state.nodes.length === 0) {
844
- preview.textContent = 'Build a workflow and the request will be generated here...';
845
- return;
846
- }
847
- preview.textContent = generatePrompt();
848
- }
849
-
850
- function generatePrompt() {
851
- const triggers = state.nodes.filter(n => n.type === 'trigger');
852
- const prompts = state.nodes.filter(n => n.type === 'prompt');
853
- const conditions = state.nodes.filter(n => n.type === 'condition');
854
- const actions = state.nodes.filter(n => n.type === 'action');
855
-
856
- let lines = [];
857
- const name = state.wfName || 'Unnamed Workflow';
858
- lines.push(`Please register the following cron workflow:\n\nWorkflow Name: "${name}"`);
859
-
860
- // Build flow from connections
861
- const roots = triggers.length > 0 ? triggers : state.nodes.filter(n => !state.connections.some(c => c.to === n.id));
862
-
863
- for (const root of roots) {
864
- const chain = buildChain(root.id);
865
- lines.push(describeChain(chain));
866
- }
867
-
868
- // Orphan nodes
869
- const mentioned = new Set();
870
- state.connections.forEach(c => { mentioned.add(c.from); mentioned.add(c.to); });
871
- const orphans = state.nodes.filter(n => !mentioned.has(n.id) && !roots.includes(n));
872
- for (const o of orphans) {
873
- lines.push(`\n[Unconnected] ${ICONS[o.type]} ${o.label}: ${getNodeSubtitle(o)}`);
874
- }
875
-
876
- // Existing jobs context
877
- if (state.existingJobs.length > 0) {
878
- lines.push('\n---\nExisting registered jobs for reference:');
879
- for (const j of state.existingJobs) {
880
- lines.push(`- "${j.name}" (${j.cron_expr}) ${j.enabled ? 'Active' : 'Inactive'}`);
881
- }
882
- }
883
-
884
- return lines.join('\n');
885
- }
886
-
887
- function buildChain(startId) {
888
- const chain = [];
889
- const visited = new Set();
890
- const queue = [startId];
891
- while (queue.length > 0) {
892
- const current = queue.shift();
893
- if (!current || visited.has(current)) { continue; }
894
- visited.add(current);
895
- const node = state.nodes.find(n => n.id === current);
896
- if (node) { chain.push(node); }
897
- // BFS: follow ALL outgoing connections to capture branching workflows
898
- const outgoing = state.connections.filter(c => c.from === current);
899
- for (const conn of outgoing) {
900
- if (!visited.has(conn.to)) { queue.push(conn.to); }
901
- }
902
- }
903
- return chain;
904
- }
905
-
906
- function describeChain(chain) {
907
- if (chain.length === 0) return '';
908
- let parts = [];
909
- for (const node of chain) {
910
- switch(node.type) {
911
- case 'trigger':
912
- const sched = node.config.schedule || node.config.description || (node.config.cron ? cronToHuman(node.config.cron) : '(not set)');
913
- parts.push(`[Schedule] ${sched}${node.config.cron ? ' (cron: '+node.config.cron+')' : ''}`);
914
- break;
915
- case 'prompt':
916
- parts.push(`[Task] ${node.config.prompt || '(not set)'}`);
917
- break;
918
- case 'condition': {
919
- let condStr = `[Condition] ${node.config.condition || '(not set)'}`;
920
- const vm = node.config.verifyMethod || 'bash';
921
- const vmLabels = {bash:'Bash exit code',http:'HTTP status',string:'String contains check',custom:'Custom script'};
922
- condStr += `\n Verification: ${vmLabels[vm]}`;
923
- if (node.config.verifyScript) condStr += `\n Script: ${node.config.verifyScript}`;
924
- if (node.config.verifyExpect) condStr += `\n Expected: ${node.config.verifyExpect}`;
925
- parts.push(condStr);
926
- break;
927
- }
928
- case 'action':
929
- const types = {discord:'Send to Discord',slack:'Send to Slack',file:'Save to File',api:'API Call',email:'Email'};
930
- parts.push(`[Action] ${types[node.config.actionType]||node.config.actionType}${node.config.target ? ' → '+node.config.target : ''}${node.config.message ? ': '+node.config.message : ''}`);
931
- break;
932
- }
933
- }
934
- return parts.join('\n→ ');
935
- }
936
-
937
- // ─── Cron Utils ───
938
- function cronToHuman(expr) {
939
- const map = {
940
- '* * * * *':'Every minute','*/5 * * * *':'Every 5 minutes','*/10 * * * *':'Every 10 minutes',
941
- '*/15 * * * *':'Every 15 minutes','*/30 * * * *':'Every 30 minutes','0 * * * *':'Every hour',
942
- '0 */2 * * *':'Every 2 hours','0 */6 * * *':'Every 6 hours',
943
- '0 9 * * *':'Daily at 09:00','0 18 * * *':'Daily at 18:00','0 0 * * *':'Daily at midnight',
944
- '0 9 * * 1-5':'Weekdays at 09:00','0 18 * * 1-5':'Weekdays at 18:00',
945
- '0 0 * * 0':'Every Sunday','0 0 * * 1':'Every Monday',
946
- '0 3 * * 0':'Every Sunday at 03:00','0 0 1 * *':'1st of every month',
947
- };
948
- return map[expr] || expr;
949
- }
950
-
951
- // ─── Mouse Events ───
952
- svg.addEventListener('mousemove', (e) => {
953
- const pt = svgPoint(e.clientX, e.clientY);
954
-
955
- if (state.panning) {
956
- const dx = (e.clientX - state.panStart.cx) / state.zoom;
957
- const dy = (e.clientY - state.panStart.cy) / state.zoom;
958
- state.viewX = state.panStart.vx - dx;
959
- state.viewY = state.panStart.vy - dy;
960
- updateViewBox();
961
- return;
962
- }
963
- if (state.dragging) {
964
- const node = state.nodes.find(n => n.id === state.dragging);
965
- if (node) {
966
- node.x = pt.x - state.dragOffset.x;
967
- node.y = pt.y - state.dragOffset.y;
968
- render();
969
- }
970
- }
971
- if (state.connecting) {
972
- state._tempMouse = {x: pt.x, y: pt.y};
973
- render();
974
- }
975
- });
976
-
977
- svg.addEventListener('mouseup', () => {
978
- state.dragging = null;
979
- state.panning = false;
980
- state.panStart = null;
981
- });
982
-
983
- svg.addEventListener('mousedown', (e) => {
984
- if (e.target === svg || e.target.closest('.canvas-grid')) {
985
- // Middle button or shift+left = pan
986
- if (e.button === 1 || (e.button === 0 && e.shiftKey)) {
987
- state.panning = true;
988
- state.panStart = {cx: e.clientX, cy: e.clientY, vx: state.viewX, vy: state.viewY};
989
- e.preventDefault();
990
- return;
991
- }
992
- // Left click on empty = pan (no shift needed)
993
- if (e.button === 0 && e.target === svg) {
994
- state.panning = true;
995
- state.panStart = {cx: e.clientX, cy: e.clientY, vx: state.viewX, vy: state.viewY};
996
- }
997
- }
998
- });
999
-
1000
- svg.addEventListener('click', (e) => {
1001
- if (e.target === svg) {
1002
- if (state.connecting) cancelConnection();
1003
- state.selectedNode = null;
1004
- closeDetail();
1005
- render();
1006
- }
1007
- });
1008
-
1009
- document.addEventListener('keydown', (e) => {
1010
- if (e.key === 'Escape') {
1011
- if (state.connecting) cancelConnection();
1012
- state.selectedNode = null;
1013
- closeDetail();
1014
- render();
1015
- }
1016
- if (e.key === 'Delete' && state.selectedNode && !document.querySelector('.dp-input:focus')) {
1017
- deleteSelectedNode();
1018
- }
1019
- });
1020
-
1021
- // ─── Zoom ───
1022
- function zoomIn() { setZoom(state.zoom * 1.3); }
1023
- function zoomOut() { setZoom(state.zoom / 1.3); }
1024
-
1025
- function setZoom(z, cx, cy) {
1026
- const oldZ = state.zoom;
1027
- state.zoom = Math.max(0.15, Math.min(3, z));
1028
- // Zoom toward cursor position
1029
- if (cx != null && cy != null) {
1030
- const rect = svg.getBoundingClientRect();
1031
- const svxBefore = state.viewX + (cx - rect.left) / oldZ;
1032
- const svyBefore = state.viewY + (cy - rect.top) / oldZ;
1033
- const svxAfter = state.viewX + (cx - rect.left) / state.zoom;
1034
- const svyAfter = state.viewY + (cy - rect.top) / state.zoom;
1035
- state.viewX += svxBefore - svxAfter;
1036
- state.viewY += svyBefore - svyAfter;
1037
- }
1038
- updateViewBox();
1039
- render();
1040
- }
1041
-
1042
- function fitAll() {
1043
- if (state.nodes.length === 0) { state.zoom = 1; state.viewX = 0; state.viewY = 0; updateViewBox(); return; }
1044
- const pad = 60;
1045
- let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
1046
- for (const n of state.nodes) {
1047
- minX = Math.min(minX, n.x);
1048
- minY = Math.min(minY, n.y);
1049
- maxX = Math.max(maxX, n.x + NODE_W);
1050
- maxY = Math.max(maxY, n.y + NODE_H);
1051
- }
1052
- minX -= pad; minY -= pad; maxX += pad; maxY += pad;
1053
- const rect = svg.getBoundingClientRect();
1054
- const scaleX = rect.width / (maxX - minX);
1055
- const scaleY = rect.height / (maxY - minY);
1056
- state.zoom = Math.min(scaleX, scaleY, 2);
1057
- state.viewX = minX;
1058
- state.viewY = minY;
1059
- updateViewBox();
1060
- render();
1061
- }
1062
-
1063
- svg.addEventListener('wheel', (e) => {
1064
- e.preventDefault();
1065
- const factor = e.deltaY > 0 ? 0.9 : 1.1;
1066
- setZoom(state.zoom * factor, e.clientX, e.clientY);
1067
- }, {passive: false});
1068
-
1069
- // ─── Actions ───
1070
- function copyPrompt() {
1071
- const text = generatePrompt();
1072
- navigator.clipboard.writeText(text).then(() => toast('Copied!', 'success'));
1073
- }
1074
-
1075
- // sendToChat replaced by testRun() and deployToChat() below
1076
-
1077
- // ─── Load Existing Jobs ───
1078
- async function loadJobs() {
1079
- try {
1080
- const r = await fetch('/api/cron');
1081
- if (!r.ok) throw new Error(r.status);
1082
- const data = await r.json();
1083
- state.existingJobs = data.jobs || [];
1084
- renderJobList();
1085
- document.getElementById('sidebarStatus').innerHTML = '<span class="dot dot-ok"></span> API connected · ' + state.existingJobs.length + ' jobs';
1086
- } catch(e) {
1087
- document.getElementById('sidebarStatus').innerHTML = '<span class="dot dot-err"></span> API connection failed';
1088
- document.getElementById('jobList').innerHTML = '<div style="padding:16px;text-align:center;color:var(--muted);font-size:12px">API connection failed<br>Check if the cron API is running</div>';
1089
- }
1090
- }
1091
-
1092
- function renderJobList() {
1093
- const list = document.getElementById('jobList');
1094
- if (state.existingJobs.length === 0) {
1095
- list.innerHTML = '<div style="padding:16px;text-align:center;color:var(--muted);font-size:12px">No registered jobs</div>';
1096
- return;
1097
- }
1098
- list.innerHTML = state.existingJobs.map(j => `
1099
- <div class="job-card" data-job-id="${esc(j.id)}">
1100
- <div class="jname">${esc(j.name)} <span class="badge ${j.enabled?'badge-on':'badge-off'}">${j.enabled?'ON':'OFF'}</span></div>
1101
- <div class="jmeta">${esc(j.cron_expr)} · ${esc(cronToHuman(j.cron_expr))}</div>
1102
- </div>
1103
- `).join('');
1104
- // Event delegation for job cards
1105
- document.querySelectorAll('.job-card').forEach(card => {
1106
- card.addEventListener('click', function() {
1107
- const jobId = this.getAttribute('data-job-id');
1108
- importJob(jobId);
1109
- });
1110
- });
1111
- }
1112
-
1113
- function importJob(jobId) {
1114
- const job = state.existingJobs.find(j => j.id === jobId);
1115
- if (!job) return;
1116
- // Import as nodes on canvas
1117
- const area = svg.getBoundingClientRect();
1118
- const cx = 100, cy = 100 + state.nodes.length * 90;
1119
- const t = addNodeAt('trigger', cx, cy, {schedule: cronToHuman(job.cron_expr) || job.name, cron: job.cron_expr});
1120
- const p = addNodeAt('prompt', cx + 220, cy, {prompt: job.prompt});
1121
- state.connections.push({from: t.id, to: p.id});
1122
- render();
1123
- toast(`"${job.name}" imported`, 'success');
1124
- }
1125
-
1126
- // ─── Toast ───
1127
- function toast(msg, type='') {
1128
- const t = document.getElementById('toast');
1129
- t.textContent = msg;
1130
- t.className = 'toast show ' + type;
1131
- setTimeout(() => t.className = 'toast', 2500);
1132
- }
1133
-
1134
- // ─── Persistence (localStorage, multi-workflow) ───
1135
- const STORAGE_KEY = 'cron-workflow-lab-v2';
1136
-
1137
- function saveAllWorkflows() {
1138
- // Save current workflow into workflows list
1139
- if (state.wfId) {
1140
- const idx = state.workflows.findIndex(w => w.id === state.wfId);
1141
- const wfData = {
1142
- id: state.wfId,
1143
- name: state.wfName || 'Unnamed Workflow',
1144
- status: state.wfStatus,
1145
- nodes: state.nodes,
1146
- connections: state.connections,
1147
- nextId: state.nextId,
1148
- updatedAt: Date.now()
1149
- };
1150
- if (idx >= 0) state.workflows[idx] = wfData;
1151
- else state.workflows.push(wfData);
1152
- }
1153
- try {
1154
- localStorage.setItem(STORAGE_KEY, JSON.stringify({
1155
- workflows: state.workflows,
1156
- activeWfId: state.wfId
1157
- }));
1158
- } catch(e) {}
1159
- renderWorkflowList();
1160
- }
1161
-
1162
- function loadAllWorkflows() {
1163
- try {
1164
- const raw = localStorage.getItem(STORAGE_KEY);
1165
- if (!raw) {
1166
- // Migrate from v1
1167
- const v1 = localStorage.getItem('cron-workflow-lab-state');
1168
- if (v1) {
1169
- const d = JSON.parse(v1);
1170
- if (d.nodes && d.nodes.length > 0) {
1171
- const wf = {id:'wf_migrated', name:'(Migrated)', status:'draft', nodes:d.nodes, connections:d.connections||[], nextId:d.nextId||1, updatedAt:Date.now()};
1172
- state.workflows = [wf];
1173
- switchToWorkflow(wf.id);
1174
- localStorage.removeItem('cron-workflow-lab-state');
1175
- return true;
1176
- }
1177
- }
1178
- return false;
1179
- }
1180
- const data = JSON.parse(raw);
1181
- state.workflows = data.workflows || [];
1182
- if (data.activeWfId && state.workflows.some(w => w.id === data.activeWfId)) {
1183
- switchToWorkflow(data.activeWfId);
1184
- return true;
1185
- } else if (state.workflows.length > 0) {
1186
- switchToWorkflow(state.workflows[0].id);
1187
- return true;
1188
- }
1189
- } catch(e) {}
1190
- return false;
1191
- }
1192
-
1193
- function switchToWorkflow(wfId) {
1194
- // Save current first
1195
- if (state.wfId && state.wfId !== wfId) saveCurrentToList();
1196
- const wf = state.workflows.find(w => w.id === wfId);
1197
- if (!wf) return;
1198
- state.wfId = wf.id;
1199
- state.wfName = wf.name;
1200
- state.wfStatus = wf.status;
1201
- state.nodes = wf.nodes || [];
1202
- state.connections = wf.connections || [];
1203
- state.nextId = wf.nextId || 1;
1204
- state.selectedNode = null;
1205
- closeDetail();
1206
- document.getElementById('wfName').value = state.wfName;
1207
- updateStatusBadge();
1208
- render();
1209
- renderWorkflowList();
1210
- }
1211
-
1212
- function saveCurrentToList() {
1213
- if (!state.wfId) return;
1214
- const idx = state.workflows.findIndex(w => w.id === state.wfId);
1215
- const wfData = {id:state.wfId, name:state.wfName||'Unnamed', status:state.wfStatus, nodes:state.nodes, connections:state.connections, nextId:state.nextId, updatedAt:Date.now()};
1216
- if (idx >= 0) state.workflows[idx] = wfData;
1217
- else state.workflows.push(wfData);
1218
- }
1219
-
1220
- function onWfNameChange() {
1221
- state.wfName = document.getElementById('wfName').value;
1222
- saveAllWorkflows();
1223
- }
1224
-
1225
- function updateStatusBadge() {
1226
- const el = document.getElementById('wfStatus');
1227
- el.textContent = state.wfStatus;
1228
- el.className = 'wf-status wf-status-' + state.wfStatus;
1229
- }
1230
-
1231
- function renderWorkflowList() {
1232
- const list = document.getElementById('workflowList');
1233
- if (state.workflows.length === 0) {
1234
- list.innerHTML = '<div style="padding:12px;text-align:center;color:var(--dim);font-size:11px">No workflows</div>';
1235
- return;
1236
- }
1237
- const sorted = [...state.workflows].sort((a,b) => (b.updatedAt||0) - (a.updatedAt||0));
1238
- list.innerHTML = sorted.map(w => {
1239
- const active = w.id === state.wfId;
1240
- const statusMap = {draft:['📝','badge-off','draft'], deployed:['✅','badge-on','deployed'], paused:['⏸','','paused']};
1241
- const [icon, cls, label] = statusMap[w.status] || statusMap.draft;
1242
- const nodeCount = (w.nodes||[]).length;
1243
- return `<div class="job-card${active?' active':''}" data-workflow-id="${esc(w.id)}">
1244
- <div class="jname">${icon} ${esc(w.name||'Unnamed')} <span class="badge ${cls}" style="font-size:9px">${esc(label)}</span></div>
1245
- <div class="jmeta">${nodeCount} nodes · ${esc(timeAgo(w.updatedAt))}</div>
1246
- ${!active ? '<span class="workflow-delete" style="position:absolute;right:8px;top:8px;font-size:11px;color:var(--dim);cursor:pointer" data-workflow-id="'+esc(w.id)+'">×</span>' : ''}
1247
- </div>`;
1248
- }).join('');
1249
- // Event delegation for workflow cards
1250
- document.querySelectorAll('.job-card[data-workflow-id]').forEach(card => {
1251
- card.addEventListener('click', function(e) {
1252
- if (!e.target.classList.contains('workflow-delete')) {
1253
- const wfId = this.getAttribute('data-workflow-id');
1254
- switchToWorkflow(wfId);
1255
- }
1256
- });
1257
- });
1258
- // Event delegation for delete buttons
1259
- document.querySelectorAll('.workflow-delete').forEach(btn => {
1260
- btn.addEventListener('click', function(e) {
1261
- e.stopPropagation();
1262
- const wfId = this.getAttribute('data-workflow-id');
1263
- deleteWorkflow(wfId);
1264
- });
1265
- });
1266
- }
1267
-
1268
- function timeAgo(ts) {
1269
- if (!ts) return '';
1270
- const diff = Date.now() - ts;
1271
- if (diff < 60000) return 'just now';
1272
- if (diff < 3600000) return Math.floor(diff/60000) + 'm ago';
1273
- if (diff < 86400000) return Math.floor(diff/3600000) + 'h ago';
1274
- return Math.floor(diff/86400000) + 'd ago';
1275
- }
1276
-
1277
- function deleteWorkflow(wfId) {
1278
- if (!confirm('Delete this workflow?')) return;
1279
- state.workflows = state.workflows.filter(w => w.id !== wfId);
1280
- if (state.wfId === wfId) {
1281
- if (state.workflows.length > 0) switchToWorkflow(state.workflows[0].id);
1282
- else resetToEmpty();
1283
- }
1284
- saveAllWorkflows();
1285
- }
1286
-
1287
- function resetToEmpty() {
1288
- state.wfId = null;
1289
- state.wfName = '';
1290
- state.wfStatus = 'draft';
1291
- state.nodes = [];
1292
- state.connections = [];
1293
- state.nextId = 1;
1294
- state.selectedNode = null;
1295
- document.getElementById('wfName').value = '';
1296
- updateStatusBadge();
1297
- closeDetail();
1298
- render();
1299
- }
1300
-
1301
- // Auto-save on every render
1302
- var _saveDebounceTimer = null;
1303
- function debouncedSave() {
1304
- clearTimeout(_saveDebounceTimer);
1305
- _saveDebounceTimer = setTimeout(saveAllWorkflows, 500);
1306
- }
1307
- (function patchRender() {
1308
- const orig = window.render;
1309
- window.render = function() { orig(); debouncedSave(); };
1310
- })();
1311
-
1312
- // ─── New Workflow ───
1313
- function newWorkflow() {
1314
- // Save current first
1315
- if (state.wfId) saveCurrentToList();
1316
- const id = 'wf_' + Date.now() + '_' + Math.random().toString(36).slice(2,6);
1317
- state.wfId = id;
1318
- state.wfName = '';
1319
- state.wfStatus = 'draft';
1320
- state.nodes = [];
1321
- state.connections = [];
1322
- state.selectedNode = null;
1323
- state.nextId = 1;
1324
- document.getElementById('wfName').value = '';
1325
- updateStatusBadge();
1326
- closeDetail();
1327
- render();
1328
- // Focus name input
1329
- document.getElementById('wfName').focus();
1330
- toggleTemplates();
1331
- toast('New workflow — please name it');
1332
- }
1333
-
1334
- // ─── Test Run / Deploy ───
1335
- function testRun() {
1336
- const name = state.wfName || 'Unnamed Workflow';
1337
- if (state.nodes.length === 0) { toast('Add nodes first', 'error'); return; }
1338
- const prompt = 'Run the following workflow once as a test (do not register a cron, execute immediately):\n\n' + `Workflow: "${name}"\n` + generatePrompt();
1339
- window.parent.postMessage({type:'playground:sendToChat', message: prompt}, window.location.origin);
1340
- toast('Test run request sent!', 'success');
1341
- }
1342
-
1343
- function deployToChat() {
1344
- const name = state.wfName;
1345
- if (!name || !name.trim()) { toast('Enter a workflow name first', 'error'); document.getElementById('wfName').focus(); return; }
1346
- if (state.nodes.length === 0) { toast('Add nodes first', 'error'); return; }
1347
- const prompt = generatePrompt();
1348
- window.parent.postMessage({type:'playground:sendToChat', message: prompt}, window.location.origin);
1349
- state.wfStatus = 'deployed';
1350
- updateStatusBadge();
1351
- saveAllWorkflows();
1352
- toast(`"${name}" deploy request sent!`, 'success');
1353
- }
1354
-
1355
- // ─── Verify Method Selector ───
1356
- function setVerifyMethod(method) {
1357
- const node = state.nodes.find(n => n.id === state.selectedNode);
1358
- if (!node || node.type !== 'condition') return;
1359
- node.config.verifyMethod = method;
1360
- showDetail(node);
1361
- }
1362
-
1363
- // ─── Agent Decomposition Protocol ───
1364
- // JSON format the agent sends back:
1365
- // {
1366
- // type: 'workflow:inject',
1367
- // action: 'addNodes' | 'replaceAll' | 'suggest',
1368
- // message: 'human-readable explanation',
1369
- // workflow: { name?, nodes: [{type,label,config,x?,y?}], connections: [{fromIdx,toIdx,label?}] }
1370
- // }
1371
-
1372
- let _pendingAgentData = null;
1373
-
1374
- window.addEventListener('message', (e) => {
1375
- // SECURITY: Validate origin and source to prevent cross-origin attacks
1376
- if (e.origin !== window.location.origin) {
1377
- return;
1378
- }
1379
- if (e.source !== window.parent) {
1380
- return;
1381
- }
1382
-
1383
- const d = e.data;
1384
- if (!d || typeof d !== 'object') return;
1385
-
1386
- // Handle workflow injection from OS agent
1387
- if (d.type === 'workflow:inject') {
1388
- handleAgentInject(d);
1389
- return;
1390
- }
1391
- // Legacy: simple node add
1392
- if (d.type === 'workflow:addNode') {
1393
- const n = d.node;
1394
- if (n && n.type && COLORS[n.type]) {
1395
- addNode(n.type, n.x, n.y, n.config);
1396
- toast('Agent added a node', 'success');
1397
- }
1398
- return;
1399
- }
1400
- });
1401
-
1402
- function handleAgentInject(data) {
1403
- const indicator = document.getElementById('agentIndicator');
1404
- indicator.classList.remove('show');
1405
-
1406
- if (!data.workflow || !data.workflow.nodes || data.workflow.nodes.length === 0) {
1407
- toast('No nodes in agent response', 'error');
1408
- return;
1409
- }
1410
-
1411
- _pendingAgentData = data;
1412
-
1413
- // Show preview overlay
1414
- const overlay = document.getElementById('agentOverlay');
1415
- const msg = document.getElementById('agentMessage');
1416
- const preview = document.getElementById('agentPreview');
1417
-
1418
- msg.textContent = data.message || 'The workflow has been analyzed.';
1419
-
1420
- // Render preview of proposed nodes
1421
- const wf = data.workflow;
1422
- let previewHtml = '';
1423
- if (wf.name) previewHtml += `<div style="font-weight:700;margin-bottom:6px">📋 ${esc(wf.name)}</div>`;
1424
- previewHtml += '<div style="display:flex;flex-direction:column;gap:4px">';
1425
- for (let i = 0; i < wf.nodes.length; i++) {
1426
- const n = wf.nodes[i];
1427
- const color = COLORS[n.type] || 'var(--muted)';
1428
- const icon = ICONS[n.type] || '❓';
1429
- previewHtml += `<div style="display:flex;align-items:center;gap:8px;padding:4px 8px;border-radius:4px;border-left:3px solid ${color}">`;
1430
- previewHtml += `<span>${icon}</span>`;
1431
- previewHtml += `<div><span style="font-weight:600">${esc(n.label || LABELS[n.type] || n.type)}</span>`;
1432
- if (n.config) {
1433
- const sub = n.config.schedule || n.config.prompt || n.config.condition || n.config.actionType || '';
1434
- if (sub) previewHtml += `<div style="font-size:10px;color:var(--muted)">${esc(truncate(sub, 50))}</div>`;
1435
- }
1436
- previewHtml += '</div></div>';
1437
- if (i < wf.nodes.length - 1) previewHtml += '<div style="text-align:center;color:var(--dim);font-size:10px">↓</div>';
1438
- }
1439
- previewHtml += '</div>';
1440
-
1441
- // Show edge cases if present
1442
- if (data.edgeCases && data.edgeCases.length > 0) {
1443
- previewHtml += '<div style="margin-top:8px;padding-top:8px;border-top:1px solid var(--line)">';
1444
- previewHtml += '<div style="font-size:10px;font-weight:700;color:var(--warn);margin-bottom:4px">⚠️ Edge cases considered:</div>';
1445
- for (const ec of data.edgeCases) {
1446
- previewHtml += `<div style="font-size:10px;color:var(--muted);padding-left:8px">• ${esc(ec)}</div>`;
1447
- }
1448
- previewHtml += '</div>';
1449
- }
1450
-
1451
- preview.innerHTML = previewHtml;
1452
- overlay.classList.add('show');
1453
- }
1454
-
1455
- function acceptAgent() {
1456
- if (!_pendingAgentData) return;
1457
- const data = _pendingAgentData;
1458
- const wf = data.workflow;
1459
-
1460
- if (data.action === 'replaceAll') {
1461
- state.nodes = [];
1462
- state.connections = [];
1463
- state.nextId = 1;
1464
- }
1465
-
1466
- if (wf.name && !state.wfName) {
1467
- state.wfName = wf.name;
1468
- document.getElementById('wfName').value = wf.name;
1469
- }
1470
-
1471
- // Add nodes with layout
1472
- const newNodes = [];
1473
- const startX = 100, startY = 120, gapX = 220, gapY = 100;
1474
- for (let i = 0; i < wf.nodes.length; i++) {
1475
- const spec = wf.nodes[i];
1476
- const x = spec.x != null ? spec.x : startX + (i % 4) * gapX;
1477
- const y = spec.y != null ? spec.y : startY + Math.floor(i / 4) * gapY;
1478
- const config = {...getDefaultConfig(spec.type), ...(spec.config || {})};
1479
- const node = addNodeAt(spec.type, x, y, config);
1480
- if (spec.label) node.label = spec.label;
1481
- newNodes.push(node);
1482
- }
1483
-
1484
- // Add connections
1485
- if (wf.connections) {
1486
- for (const conn of wf.connections) {
1487
- const from = newNodes[conn.fromIdx];
1488
- const to = newNodes[conn.toIdx];
1489
- if (from && to) {
1490
- state.connections.push({from: from.id, to: to.id, label: conn.label || ''});
1491
- }
1492
- }
1493
- }
1494
-
1495
- closeAgentOverlay();
1496
- render();
1497
- toast(`${newNodes.length} nodes added`, 'success');
1498
- }
1499
-
1500
- function rejectAgent() {
1501
- _pendingAgentData = null;
1502
- closeAgentOverlay();
1503
- toast('Agent suggestion dismissed');
1504
- }
1505
-
1506
- function editAgent() {
1507
- // Apply nodes but open detail for first node
1508
- acceptAgent();
1509
- if (state.nodes.length > 0) {
1510
- selectNode(state.nodes[state.nodes.length - 1].id);
1511
- }
1512
- }
1513
-
1514
- function closeAgentOverlay() {
1515
- document.getElementById('agentOverlay').classList.remove('show');
1516
- _pendingAgentData = null;
1517
- }
1518
-
1519
- // ─── Decompose Request ───
1520
- function requestDecompose() {
1521
- const indicator = document.getElementById('agentIndicator');
1522
-
1523
- // If canvas has nodes, send them as context for refinement
1524
- let prompt;
1525
- if (state.nodes.length > 0) {
1526
- const currentJson = exportWorkflowJson();
1527
- prompt = `Analyze and improve the following cron workflow. Add missing edge cases, error handling, and conditional branches, then return a structured JSON.\n\nCurrent workflow:\n${JSON.stringify(currentJson, null, 2)}\n\n` + DECOMPOSE_INSTRUCTION;
1528
- } else if (state.wfName) {
1529
- prompt = `Design a "${state.wfName}" workflow. Infer the intent from the name and decompose it into structured JSON nodes.\n\n` + DECOMPOSE_INSTRUCTION;
1530
- } else {
1531
- prompt = `The user wants to create a cron workflow. Ask what they need, and once answered, decompose it into structured nodes as JSON.\n\n` + DECOMPOSE_INSTRUCTION;
1532
- }
1533
-
1534
- window.parent.postMessage({type:'playground:sendToChat', message: prompt}, window.location.origin);
1535
- indicator.classList.add('show');
1536
- toast('Decompose request sent to agent', 'success');
1537
- // Auto-hide indicator after 30s
1538
- setTimeout(() => indicator.classList.remove('show'), 30000);
1539
- }
1540
-
1541
- const DECOMPOSE_INSTRUCTION = `When responding, you MUST include a code block in the following JSON format (the playground will parse it):
1542
-
1543
- \`\`\`workflow-json
1544
- {
1545
- "type": "workflow:inject",
1546
- "action": "replaceAll",
1547
- "message": "Analysis result explanation",
1548
- "edgeCases": ["Edge case 1", "Edge case 2"],
1549
- "workflow": {
1550
- "name": "Workflow name",
1551
- "nodes": [
1552
- {"type": "trigger", "label": "Name", "config": {"schedule": "Natural language schedule"}},
1553
- {"type": "prompt", "label": "Name", "config": {"prompt": "Task description"}},
1554
- {"type": "condition", "label": "Name", "config": {"condition": "Condition description", "verifyMethod": "bash|http|string|custom", "verifyScript": "Verification script", "verifyExpect": "Expected value"}},
1555
- {"type": "action", "label": "Name", "config": {"actionType": "discord|slack|file|api|email", "target": "Target", "message": "Content"}}
1556
- ],
1557
- "connections": [
1558
- {"fromIdx": 0, "toIdx": 1},
1559
- {"fromIdx": 1, "toIdx": 2, "label": ""}
1560
- ]
1561
- }
1562
- }
1563
- \`\`\`
1564
-
1565
- Condition nodes MUST include a deterministic verification method (verifyMethod):
1566
- - bash: Run a command and judge by exit code (0 = true)
1567
- - http: Call a URL and judge by HTTP status (200 = true)
1568
- - string: Check if previous output contains a specific string
1569
- - custom: Complete bash script
1570
-
1571
- You MUST list edge cases (edgeCases). Include things the user may have overlooked.`;
1572
-
1573
- function exportWorkflowJson() {
1574
- const nodeMap = {};
1575
- state.nodes.forEach((n, i) => { nodeMap[n.id] = i; });
1576
- return {
1577
- name: state.wfName || '',
1578
- nodes: state.nodes.map(n => ({type:n.type, label:n.label, config:n.config})),
1579
- connections: state.connections.map(c => ({
1580
- fromIdx: nodeMap[c.from] ?? 0,
1581
- toIdx: nodeMap[c.to] ?? 0,
1582
- label: c.label || ''
1583
- }))
1584
- };
1585
- }
1586
-
1587
- // ─── Init ───
1588
- const restored = loadAllWorkflows();
1589
- loadJobs();
1590
- if (!restored) {
1591
- newWorkflow();
1592
- loadTemplate('channel-monitor');
1593
- setTimeout(fitAll, 100);
1594
- } else {
1595
- render();
1596
- renderWorkflowList();
1597
- toast('Previous work restored', 'success');
1598
- }
1599
- </script>
1600
- </body>
1601
- </html>