@intranefr/superbackend 1.4.3

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 (188) hide show
  1. package/.commiat +4 -0
  2. package/.env.example +47 -0
  3. package/README.md +110 -0
  4. package/index.js +94 -0
  5. package/package.json +67 -0
  6. package/public/css/styles.css +139 -0
  7. package/public/js/animations.js +41 -0
  8. package/sdk/error-tracking/browser/package.json +16 -0
  9. package/sdk/error-tracking/browser/src/core.js +270 -0
  10. package/sdk/error-tracking/browser/src/embed.js +18 -0
  11. package/sdk/error-tracking/browser/src/index.js +1 -0
  12. package/server.js +5 -0
  13. package/src/admin/endpointRegistry.js +300 -0
  14. package/src/controllers/admin.controller.js +321 -0
  15. package/src/controllers/adminAssets.controller.js +530 -0
  16. package/src/controllers/adminAssetsStorage.controller.js +260 -0
  17. package/src/controllers/adminEjsVirtual.controller.js +354 -0
  18. package/src/controllers/adminFeatureFlags.controller.js +155 -0
  19. package/src/controllers/adminHeadless.controller.js +1071 -0
  20. package/src/controllers/adminI18n.controller.js +604 -0
  21. package/src/controllers/adminJsonConfigs.controller.js +97 -0
  22. package/src/controllers/adminLlm.controller.js +273 -0
  23. package/src/controllers/adminMigration.controller.js +257 -0
  24. package/src/controllers/adminSeoConfig.controller.js +515 -0
  25. package/src/controllers/adminStats.controller.js +121 -0
  26. package/src/controllers/adminUploadNamespaces.controller.js +208 -0
  27. package/src/controllers/assets.controller.js +248 -0
  28. package/src/controllers/auth.controller.js +93 -0
  29. package/src/controllers/billing.controller.js +223 -0
  30. package/src/controllers/featureFlags.controller.js +35 -0
  31. package/src/controllers/forms.controller.js +217 -0
  32. package/src/controllers/globalSettings.controller.js +252 -0
  33. package/src/controllers/headlessCrud.controller.js +126 -0
  34. package/src/controllers/i18n.controller.js +12 -0
  35. package/src/controllers/invite.controller.js +249 -0
  36. package/src/controllers/jsonConfigs.controller.js +19 -0
  37. package/src/controllers/metrics.controller.js +149 -0
  38. package/src/controllers/notificationAdmin.controller.js +264 -0
  39. package/src/controllers/notifications.controller.js +131 -0
  40. package/src/controllers/org.controller.js +357 -0
  41. package/src/controllers/orgAdmin.controller.js +491 -0
  42. package/src/controllers/stripeAdmin.controller.js +410 -0
  43. package/src/controllers/user.controller.js +361 -0
  44. package/src/controllers/userAdmin.controller.js +277 -0
  45. package/src/controllers/waitingList.controller.js +167 -0
  46. package/src/controllers/webhook.controller.js +200 -0
  47. package/src/middleware/auth.js +66 -0
  48. package/src/middleware/errorCapture.js +170 -0
  49. package/src/middleware/headlessApiTokenAuth.js +57 -0
  50. package/src/middleware/org.js +108 -0
  51. package/src/middleware.js +901 -0
  52. package/src/models/ActionEvent.js +31 -0
  53. package/src/models/ActivityLog.js +41 -0
  54. package/src/models/Asset.js +84 -0
  55. package/src/models/AuditEvent.js +93 -0
  56. package/src/models/EmailLog.js +28 -0
  57. package/src/models/ErrorAggregate.js +72 -0
  58. package/src/models/FormSubmission.js +41 -0
  59. package/src/models/GlobalSetting.js +38 -0
  60. package/src/models/HeadlessApiToken.js +24 -0
  61. package/src/models/HeadlessModelDefinition.js +41 -0
  62. package/src/models/I18nEntry.js +77 -0
  63. package/src/models/I18nLocale.js +33 -0
  64. package/src/models/Invite.js +70 -0
  65. package/src/models/JsonConfig.js +46 -0
  66. package/src/models/Notification.js +60 -0
  67. package/src/models/Organization.js +57 -0
  68. package/src/models/OrganizationMember.js +43 -0
  69. package/src/models/StripeCatalogItem.js +77 -0
  70. package/src/models/StripeWebhookEvent.js +57 -0
  71. package/src/models/User.js +89 -0
  72. package/src/models/VirtualEjsFile.js +60 -0
  73. package/src/models/VirtualEjsFileVersion.js +43 -0
  74. package/src/models/VirtualEjsGroupChange.js +32 -0
  75. package/src/models/WaitingList.js +41 -0
  76. package/src/models/Webhook.js +63 -0
  77. package/src/models/Workflow.js +29 -0
  78. package/src/models/WorkflowExecution.js +12 -0
  79. package/src/routes/admin.routes.js +26 -0
  80. package/src/routes/adminAssets.routes.js +28 -0
  81. package/src/routes/adminAssetsStorage.routes.js +13 -0
  82. package/src/routes/adminAudit.routes.js +196 -0
  83. package/src/routes/adminEjsVirtual.routes.js +17 -0
  84. package/src/routes/adminErrors.routes.js +164 -0
  85. package/src/routes/adminFeatureFlags.routes.js +12 -0
  86. package/src/routes/adminHeadless.routes.js +38 -0
  87. package/src/routes/adminI18n.routes.js +22 -0
  88. package/src/routes/adminJsonConfigs.routes.js +15 -0
  89. package/src/routes/adminLlm.routes.js +12 -0
  90. package/src/routes/adminMigration.routes.js +81 -0
  91. package/src/routes/adminSeoConfig.routes.js +20 -0
  92. package/src/routes/adminUploadNamespaces.routes.js +13 -0
  93. package/src/routes/assets.routes.js +21 -0
  94. package/src/routes/auth.routes.js +12 -0
  95. package/src/routes/billing.routes.js +11 -0
  96. package/src/routes/errorTracking.routes.js +31 -0
  97. package/src/routes/featureFlags.routes.js +9 -0
  98. package/src/routes/forms.routes.js +9 -0
  99. package/src/routes/formsAdmin.routes.js +13 -0
  100. package/src/routes/globalSettings.routes.js +18 -0
  101. package/src/routes/headless.routes.js +15 -0
  102. package/src/routes/i18n.routes.js +8 -0
  103. package/src/routes/invite.routes.js +9 -0
  104. package/src/routes/jsonConfigs.routes.js +8 -0
  105. package/src/routes/log.routes.js +111 -0
  106. package/src/routes/metrics.routes.js +9 -0
  107. package/src/routes/notificationAdmin.routes.js +15 -0
  108. package/src/routes/notifications.routes.js +12 -0
  109. package/src/routes/org.routes.js +31 -0
  110. package/src/routes/orgAdmin.routes.js +20 -0
  111. package/src/routes/publicAssets.routes.js +7 -0
  112. package/src/routes/stripeAdmin.routes.js +20 -0
  113. package/src/routes/user.routes.js +22 -0
  114. package/src/routes/userAdmin.routes.js +15 -0
  115. package/src/routes/waitingList.routes.js +13 -0
  116. package/src/routes/waitingListAdmin.routes.js +9 -0
  117. package/src/routes/webhook.routes.js +32 -0
  118. package/src/routes/workflowWebhook.routes.js +54 -0
  119. package/src/routes/workflows.routes.js +110 -0
  120. package/src/services/assets.service.js +110 -0
  121. package/src/services/audit.service.js +62 -0
  122. package/src/services/auditLogger.js +165 -0
  123. package/src/services/ejsVirtual.service.js +614 -0
  124. package/src/services/email.service.js +351 -0
  125. package/src/services/errorLogger.js +221 -0
  126. package/src/services/featureFlags.service.js +202 -0
  127. package/src/services/forms.service.js +214 -0
  128. package/src/services/globalSettings.service.js +49 -0
  129. package/src/services/headlessApiTokens.service.js +158 -0
  130. package/src/services/headlessCrypto.service.js +31 -0
  131. package/src/services/headlessModels.service.js +356 -0
  132. package/src/services/i18n.service.js +314 -0
  133. package/src/services/i18nInferredKeys.service.js +337 -0
  134. package/src/services/jsonConfigs.service.js +392 -0
  135. package/src/services/llm.service.js +749 -0
  136. package/src/services/migration.service.js +581 -0
  137. package/src/services/migrationAssets/fsLocal.js +58 -0
  138. package/src/services/migrationAssets/index.js +134 -0
  139. package/src/services/migrationAssets/s3.js +75 -0
  140. package/src/services/migrationAssets/sftp.js +92 -0
  141. package/src/services/notification.service.js +212 -0
  142. package/src/services/objectStorage.service.js +514 -0
  143. package/src/services/seoConfig.service.js +402 -0
  144. package/src/services/storage.js +150 -0
  145. package/src/services/stripe.service.js +185 -0
  146. package/src/services/stripeHelper.service.js +264 -0
  147. package/src/services/uploadNamespaces.service.js +326 -0
  148. package/src/services/webhook.service.js +157 -0
  149. package/src/services/workflow.service.js +271 -0
  150. package/src/utils/asyncHandler.js +5 -0
  151. package/src/utils/encryption.js +80 -0
  152. package/src/utils/jwt.js +40 -0
  153. package/src/utils/orgRoles.js +156 -0
  154. package/src/utils/validation.js +26 -0
  155. package/src/utils/webhookRetry.js +93 -0
  156. package/views/admin-assets.ejs +444 -0
  157. package/views/admin-audit.ejs +283 -0
  158. package/views/admin-coolify-deploy.ejs +207 -0
  159. package/views/admin-dashboard-home.ejs +291 -0
  160. package/views/admin-dashboard.ejs +397 -0
  161. package/views/admin-ejs-virtual.ejs +280 -0
  162. package/views/admin-errors.ejs +368 -0
  163. package/views/admin-feature-flags.ejs +390 -0
  164. package/views/admin-forms.ejs +526 -0
  165. package/views/admin-global-settings.ejs +436 -0
  166. package/views/admin-headless.ejs +2020 -0
  167. package/views/admin-i18n-locales.ejs +221 -0
  168. package/views/admin-i18n.ejs +728 -0
  169. package/views/admin-json-configs.ejs +410 -0
  170. package/views/admin-llm.ejs +884 -0
  171. package/views/admin-metrics.ejs +274 -0
  172. package/views/admin-migration.ejs +814 -0
  173. package/views/admin-notifications.ejs +430 -0
  174. package/views/admin-organizations.ejs +984 -0
  175. package/views/admin-seo-config.ejs +673 -0
  176. package/views/admin-stripe-pricing.ejs +558 -0
  177. package/views/admin-test.ejs +342 -0
  178. package/views/admin-users.ejs +452 -0
  179. package/views/admin-waiting-list.ejs +547 -0
  180. package/views/admin-webhooks.ejs +329 -0
  181. package/views/admin-workflows.ejs +310 -0
  182. package/views/partials/admin-assets-script.ejs +2022 -0
  183. package/views/partials/admin-test-sidebar.ejs +14 -0
  184. package/views/partials/dashboard/nav-items.ejs +66 -0
  185. package/views/partials/dashboard/palette.ejs +63 -0
  186. package/views/partials/dashboard/sidebar.ejs +21 -0
  187. package/views/partials/dashboard/tab-bar.ejs +26 -0
  188. package/views/partials/footer.ejs +3 -0
@@ -0,0 +1,310 @@
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>Workflow Editor - SaaSBackend</title>
7
+ <script src="https://cdn.tailwindcss.com"></script>
8
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@tabler/icons-webfont@latest/dist/tabler-icons.min.css">
9
+ <style>
10
+ [x-cloak] { display: none !important; }
11
+ .node-connector { width: 2px; @apply bg-gray-200 h-8 mx-auto; }
12
+ .canvas-no-select { user-select: none; -webkit-user-select: none; }
13
+ </style>
14
+ </head>
15
+ <body class="bg-gray-50 min-h-screen pb-20">
16
+ <div x-data="workflowEditor()" class="w-full px-6 py-6" x-cloak>
17
+ <!-- Toasts Notifications -->
18
+ <div id="toast-container" class="fixed bottom-4 right-4 z-[30000] space-y-2 pointer-events-none" style="min-width: 200px;">
19
+ <template x-for="toast in toasts" :key="toast.id">
20
+ <div x-show="toast.show"
21
+ x-transition:enter="transition ease-out duration-300"
22
+ x-transition:enter-start="opacity-0 translate-y-2"
23
+ x-transition:enter-end="opacity-100 translate-y-0"
24
+ x-transition:leave="transition ease-in duration-200"
25
+ x-transition:leave-start="opacity-100 translate-y-0"
26
+ x-transition:leave-end="opacity-0 translate-y-2"
27
+ :class="{'bg-green-600': toast.type === 'success', 'bg-red-600': toast.type === 'error', 'bg-blue-600': toast.type === 'info'}"
28
+ class="px-4 py-3 rounded-xl text-white text-sm font-bold shadow-2xl flex items-center gap-3 pointer-events-auto border border-white/10">
29
+ <div class="bg-white/20 p-1 rounded-lg">
30
+ <i :class="{'ti ti-check': toast.type === 'success', 'ti ti-x': toast.type === 'error', 'ti ti-info-circle': toast.type === 'info'}" class="text-lg"></i>
31
+ </div>
32
+ <span x-text="toast.message"></span>
33
+ </div>
34
+ </template>
35
+ </div>
36
+
37
+ <!-- Confirmation Modal -->
38
+ <div x-show="confirmModal.show" class="fixed inset-0 z-[20000] flex items-center justify-center bg-slate-900/50 backdrop-blur-sm" x-cloak>
39
+ <div class="bg-white rounded-2xl shadow-2xl w-full max-w-md p-6 border border-slate-200" @click.away="confirmModal.show = false">
40
+ <div class="flex items-center gap-4 text-amber-600 mb-4">
41
+ <div class="p-3 bg-amber-50 rounded-full"><i class="ti ti-alert-triangle text-2xl"></i></div>
42
+ <h3 class="text-xl font-bold text-slate-800" x-text="confirmModal.title"></h3>
43
+ </div>
44
+ <p class="text-slate-600 text-sm mb-6" x-text="confirmModal.message"></p>
45
+ <div class="flex gap-3 justify-end">
46
+ <button @click="confirmModal.show = false" class="px-4 py-2 text-sm font-bold text-slate-400 hover:text-slate-600 uppercase">Cancel</button>
47
+ <button @click="confirmModal.onConfirm(); confirmModal.show = false;" class="px-6 py-2 bg-red-600 text-white rounded-xl text-sm font-bold hover:bg-red-700 shadow-lg transition-all uppercase">Confirm</button>
48
+ </div>
49
+ </div>
50
+ </div>
51
+
52
+ <!-- Advanced Test Panel Overlay -->
53
+ <div x-show="showTestPanel" class="fixed inset-0 z-[25000] flex items-center justify-center p-4 bg-slate-900/60 backdrop-blur-sm" x-cloak>
54
+ <div class="bg-slate-900 rounded-2xl p-6 w-full max-w-4xl text-white shadow-2xl border border-slate-700" @click.away="showTestPanel = false">
55
+ <div class="flex justify-between items-center mb-6">
56
+ <h3 class="text-sm font-bold flex items-center gap-2"><i class="ti ti-flask text-yellow-400"></i> Advanced Test Runner</h3>
57
+ <button @click="showTestPanel = false" class="text-gray-400 hover:text-white p-2 hover:bg-slate-800 rounded-lg transition-colors"><i class="ti ti-x text-xl"></i></button>
58
+ </div>
59
+ <div class="grid grid-cols-1 md:grid-cols-3 gap-6">
60
+ <div class="md:col-span-2 space-y-4">
61
+ <div class="flex gap-2">
62
+ <select x-model="testConfig.method" class="bg-slate-800 border border-slate-700 rounded-lg text-xs p-2.5 focus:ring-2 ring-indigo-500"><option>GET</option><option>POST</option></select>
63
+ <input type="text" :value="getWebhookUrl()" readonly class="bg-slate-800 border border-slate-700 rounded-lg text-xs p-2.5 flex-1 text-gray-400 font-mono">
64
+ </div>
65
+ <div class="grid grid-cols-2 gap-4">
66
+ <div class="space-y-2">
67
+ <label class="text-[10px] font-bold text-gray-500 uppercase">Query Params (JSON)</label>
68
+ <textarea x-model="testConfig.query" class="w-full bg-slate-800 border border-slate-700 rounded-lg text-xs p-3 font-mono h-32 focus:ring-2 ring-indigo-500"></textarea>
69
+ </div>
70
+ <div class="space-y-2">
71
+ <label class="text-[10px] font-bold text-gray-500 uppercase">Body Payload (JSON)</label>
72
+ <textarea x-model="testConfig.body" class="w-full bg-slate-800 border border-slate-700 rounded-lg text-xs p-3 font-mono h-32 focus:ring-2 ring-indigo-500"></textarea>
73
+ </div>
74
+ </div>
75
+ </div>
76
+ <div class="bg-slate-800/50 rounded-xl p-5 border border-slate-700 space-y-4">
77
+ <div class="flex items-center justify-between"><span class="text-[10px] font-bold text-gray-500 uppercase">Last Execution</span><span :class="testResult.status === 'completed' ? 'text-green-400' : 'text-red-400'" class="text-[10px] font-bold uppercase" x-text="testResult.status || 'None'"></span></div>
78
+ <div class="h-48 overflow-y-auto text-[10px] font-mono space-y-1 text-gray-400 bg-slate-900/50 p-3 rounded-lg border border-slate-800">
79
+ <template x-for="log in testResult.log"><div class="border-l-2 border-slate-700 pl-2 py-1"><span :class="log.status === 'success' ? 'text-green-500' : 'text-red-500'" x-text="log.type.toUpperCase()"></span>: <span x-text="log.message || (log.duration + 'ms')"></span></div></template>
80
+ </div>
81
+ <div class="space-y-2">
82
+ <button @click="runFullTest()" :disabled="testing" class="w-full py-3 bg-indigo-600 hover:bg-indigo-700 rounded-xl text-xs font-bold transition-all disabled:opacity-50"><span x-show="!testing">RUN FULL WORKFLOW</span><span x-show="testing">EXECUTING...</span></button>
83
+ <button @click="runFullTest(true)" :disabled="testing" class="w-full py-3 bg-slate-700 hover:bg-slate-600 rounded-xl text-xs font-bold transition-all disabled:opacity-50"><span>SET TEST DATASET ONLY</span></button>
84
+ </div>
85
+ </div>
86
+ </div>
87
+ </div>
88
+ </div>
89
+
90
+ <!-- List View -->
91
+ <template x-if="view === 'list'">
92
+ <div>
93
+ <div class="flex justify-between items-center mb-8">
94
+ <div><h1 class="text-2xl font-bold text-gray-900">Workflows</h1><p class="text-sm text-gray-500">Automate backend logic.</p></div>
95
+ <button @click="createNew()" class="px-4 py-2 bg-indigo-600 text-white rounded-lg text-sm font-medium hover:bg-indigo-700 flex items-center gap-2"><i class="ti ti-plus"></i> New Workflow</button>
96
+ </div>
97
+ <div class="grid grid-cols-1 gap-4">
98
+ <template x-for="wf in workflows" :key="wf._id">
99
+ <div class="bg-white p-5 rounded-xl border border-gray-200 shadow-sm hover:shadow-md transition-all flex items-center justify-between">
100
+ <div class="flex items-center gap-4"><div class="p-3 bg-indigo-50 text-indigo-600 rounded-lg"><i class="ti ti-robot text-xl"></i></div><div><h3 class="font-bold text-gray-900" x-text="wf.name"></h3><div class="flex items-center gap-2 text-xs text-gray-400"><span x-text="wf.nodes?.length || 0"></span> nodes <span>•</span><span :class="wf.status === 'active' ? 'text-green-600' : 'text-gray-400'" x-text="wf.status.toUpperCase()"></span></div></div></div>
101
+ <div class="flex items-center gap-2"><button @click="editWorkflow(wf)" class="px-3 py-1.5 bg-gray-50 text-gray-600 rounded-lg text-xs font-semibold hover:bg-gray-100">EDIT</button><button @click="deleteWorkflow(wf._id)" class="p-2 text-gray-400 hover:text-red-500"><i class="ti ti-trash"></i></button></div>
102
+ </div>
103
+ </template>
104
+ </div>
105
+ </div>
106
+ </template>
107
+
108
+ <!-- Workspace (Editor/Canvas) -->
109
+ <template x-if="view === 'editor' || view === 'canvas'">
110
+ <div class="flex flex-col h-full">
111
+ <!-- TOP NAV -->
112
+ <div class="flex justify-between items-center mb-6">
113
+ <div class="flex items-center gap-4">
114
+ <button @click="view = 'list'" class="p-2 hover:bg-gray-100 rounded-lg text-gray-400"><i class="ti ti-list text-xl"></i></button>
115
+ <div>
116
+ <input type="text" x-model="workflow.name" class="text-2xl font-bold bg-transparent border-none focus:ring-0 p-0 w-64" placeholder="Workflow Name">
117
+ <div class="flex items-center gap-2">
118
+ <p class="text-sm text-gray-500">Status: <select x-model="workflow.status" class="bg-transparent border-none text-xs p-0 focus:ring-0 cursor-pointer" :class="workflow.status === 'active' ? 'text-green-600 font-bold' : 'text-gray-400'"><option value="inactive">INACTIVE</option><option value="active">ACTIVE</option></select></p>
119
+ <button x-show="workflow._id" @click="showRuns = !showRuns" class="text-[10px] font-bold text-indigo-600 hover:underline uppercase" x-text="showRuns ? 'Hide Runs' : 'Show Runs'"></button>
120
+ </div>
121
+ </div>
122
+ </div>
123
+ <div class="flex items-center gap-3">
124
+ <button @click="testWorkflow()" class="px-4 py-2 bg-white border border-gray-200 rounded-lg text-sm font-medium hover:bg-gray-50 flex items-center gap-2"><i class="ti ti-player-play"></i> Test</button>
125
+ <button @click="saveWorkflow()" class="px-4 py-2 bg-indigo-600 text-white rounded-lg text-sm font-medium hover:bg-indigo-700 flex items-center gap-2"><i class="ti ti-device-floppy"></i> Save</button>
126
+ </div>
127
+ </div>
128
+
129
+ <!-- TABS -->
130
+ <div class="flex justify-start mb-6">
131
+ <div class="flex bg-gray-100 p-1 rounded-lg">
132
+ <button @click="view = 'editor'" :class="view === 'editor' ? 'bg-white shadow-sm text-indigo-600' : 'text-gray-500'" class="px-4 py-1.5 rounded-md text-xs font-bold transition-all flex items-center gap-2"><i class="ti ti-edit"></i> Stack</button>
133
+ <button @click="view = 'canvas'" :class="view === 'canvas' ? 'bg-white shadow-sm text-indigo-600' : 'text-gray-500'" class="px-4 py-1.5 rounded-md text-xs font-bold transition-all flex items-center gap-2"><i class="ti ti-viewport"></i> Canvas</button>
134
+ </div>
135
+ </div>
136
+
137
+ <!-- CANVAS VIEW -->
138
+ <div x-show="view === 'canvas'" class="fixed inset-x-0 bottom-0 z-[10000] bg-slate-900 overflow-hidden flex flex-row" style="top: 154px;" x-cloak>
139
+ <div class="flex-1 relative cursor-grab active:cursor-grabbing overflow-hidden" @mousedown="canvasStartPan($event)" @mousemove="canvasDoPan($event)" @mouseup="canvasEndPan()" @wheel="canvasDoZoom($event)">
140
+ <div class="absolute inset-0 origin-center flex items-center justify-center pointer-events-none" :style="'transform: translate(' + canvasPan.x + 'px, ' + canvasPan.y + 'px) scale(' + canvasZoom + ')'">
141
+ <div class="flex flex-col items-center pointer-events-auto">
142
+ <div @click="openCanvasEdit('entrypoint')" class="bg-indigo-600 p-4 rounded-xl shadow-xl text-white mb-4 w-64 text-center cursor-pointer hover:ring-4 ring-indigo-400/50 transition-all"><i class="ti ti-webhook text-2xl mb-1"></i><div class="font-bold uppercase text-xs">Entrypoint</div></div>
143
+ <div class="flex flex-col items-center">
144
+ <button @click="openCanvasAdd(workflow.nodes)" class="w-6 h-6 bg-slate-700 text-slate-400 rounded-full flex items-center justify-center hover:bg-indigo-600 hover:text-white transition-all shadow-lg z-10 border-2 border-slate-900"><i class="ti ti-plus text-xs font-bold"></i></button>
145
+ <div class="flex flex-col items-center"><template x-for="(node, idx) in workflow.nodes" :key="node.id"><div class="flex flex-col items-center"><div class="w-0.5 h-8 bg-indigo-500/30"></div><div x-html="renderCanvasNode(node, 'workflow.nodes', idx)"></div></div></template></div>
146
+ </div>
147
+ </div>
148
+ </div>
149
+ </div>
150
+ <!-- Canvas Drawer -->
151
+ <div x-show="canvasDrawer.open" x-transition class="w-96 bg-white border-l border-slate-200 shadow-2xl z-30 flex flex-col" x-cloak>
152
+ <div class="p-4 border-b border-slate-100 flex justify-between items-center bg-slate-50"><h3 class="font-bold text-slate-800 flex items-center gap-2"><i :class="canvasDrawer.mode === 'edit' ? 'ti ti-edit' : 'ti ti-plus'" class="text-indigo-600"></i><span x-text="canvasDrawer.mode === 'edit' ? 'Edit Node' : 'Add Node'"></span></h3><button @click="canvasDrawer.open = false" class="text-slate-400 hover:text-slate-600 p-1 hover:bg-slate-200 rounded"><i class="ti ti-x"></i></button></div>
153
+ <div class="flex-1 overflow-y-auto p-4">
154
+ <template x-if="canvasDrawer.mode === 'add'"><div class="space-y-4"><input type="text" x-model="canvasDrawer.search" placeholder="Search..." class="w-full px-4 py-2 bg-slate-100 border-none rounded-lg text-sm"><div class="space-y-2"><template x-for="type in filteredCanvasNodes" :key="type.id"><button @click="addNodeToCanvas(type.id)" class="w-full p-3 flex items-start gap-4 hover:bg-slate-50 rounded-xl border border-transparent transition-all text-left"><div :class="type.color" class="p-2 rounded-lg text-white shadow-sm"><i :class="'ti ' + type.icon" class="text-xl"></i></div><div><div class="font-bold text-slate-800 text-sm" x-text="type.name"></div><div class="text-[10px] text-slate-500" x-text="type.desc"></div></div></button></template></div></div></template>
155
+ <template x-if="(canvasDrawer.mode === 'edit' || canvasDrawer.mode === 'edit_entrypoint') && canvasDrawer.node">
156
+ <div class="space-y-6">
157
+ <div class="flex items-center gap-4 pb-4 border-b border-slate-50"><div :class="canvasDrawer.mode === 'edit_entrypoint' ? 'bg-indigo-600' : getNodeColor(canvasDrawer.node.type)" class="p-3 rounded-xl text-white"><i :class="canvasDrawer.mode === 'edit_entrypoint' ? 'ti ti-webhook' : getNodeIcon(canvasDrawer.node.type)" class="text-2xl"></i></div><div><div class="text-[10px] font-bold text-slate-400 uppercase" x-text="canvasDrawer.mode === 'edit_entrypoint' ? 'Entrypoint' : canvasDrawer.node.type"></div><template x-if="canvasDrawer.mode === 'edit_entrypoint'"><div class="font-bold text-slate-800 text-lg">Webhook Config</div></template><template x-if="canvasDrawer.mode !== 'edit_entrypoint'"><input type="text" x-model="canvasDrawer.node.name" class="font-bold text-slate-800 text-lg border-none p-0 focus:ring-0 w-full"></template></div></div>
158
+ <div class="space-y-4">
159
+ <template x-if="canvasDrawer.node.type === 'llm'"><div class="space-y-4"><div class="grid grid-cols-3 gap-2"><div class="space-y-1"><label class="text-[10px] font-bold text-slate-400 uppercase">Provider</label><input type="text" x-model="canvasDrawer.node.provider" class="w-full text-xs bg-slate-50 border border-slate-200 rounded-lg p-2"></div><div class="space-y-1"><label class="text-[10px] font-bold text-slate-400 uppercase">Model</label><input type="text" x-model="canvasDrawer.node.model" class="w-full text-xs bg-slate-50 border border-slate-200 rounded-lg p-2"></div><div class="space-y-1"><label class="text-[10px] font-bold text-slate-400 uppercase">Temp</label><input type="range" x-model="canvasDrawer.node.temperature" min="0" max="2" step="0.1" class="w-full"></div></div><textarea x-model="canvasDrawer.node.prompt" class="w-full text-xs bg-slate-50 border border-slate-200 rounded-lg p-2 h-64 font-mono"></textarea></div></template>
160
+ <template x-if="canvasDrawer.node.type === 'http'"><div class="space-y-4"><div class="flex gap-2"><select x-model="canvasDrawer.node.method" class="text-xs border rounded p-2"><option>GET</option><option>POST</option></select><input type="text" x-model="canvasDrawer.node.url" class="flex-1 text-xs border rounded p-2" placeholder="URL"></div><div class="space-y-1"><label class="text-[10px] font-bold text-slate-400 uppercase">Headers (JSON)</label><textarea x-model="canvasDrawer.node.headersStr" @input="try { canvasDrawer.node.headers = JSON.parse($el.value) } catch(e) {}" class="w-full text-xs bg-slate-50 border border-slate-200 rounded-lg p-2 h-32 font-mono"></textarea></div><div class="space-y-1"><label class="text-[10px] font-bold text-slate-400 uppercase">Body (JSON)</label><textarea x-model="canvasDrawer.node.bodyStr" @input="try { canvasDrawer.node.body = JSON.parse($el.value) } catch(e) {}" class="w-full text-xs bg-slate-50 border border-slate-200 rounded-lg p-2 h-32 font-mono"></textarea></div></div></template>
161
+ <template x-if="canvasDrawer.node.type === 'if'"><div class="space-y-2"><label class="text-[10px] font-bold text-slate-400 uppercase">JS Condition</label><input type="text" x-model="canvasDrawer.node.condition" class="w-full text-xs bg-slate-50 border border-slate-200 rounded-lg p-2 font-mono" placeholder="context.payload.body.amount > 100"></div></template>
162
+ <template x-if="canvasDrawer.node.type === 'parallel'"><div class="space-y-4"><div class="flex justify-between items-center"><label class="text-[10px] font-bold text-slate-400 uppercase">Branches</label><button @click="canvasDrawer.node.branches.push([]); if(!canvasDrawer.node.branchNames) canvasDrawer.node.branchNames = []; canvasDrawer.node.branchNames.push('New Branch')" class="text-[10px] font-bold text-indigo-600 hover:underline uppercase">Add Branch</button></div><div class="space-y-2"><template x-for="(branch, idx) in canvasDrawer.node.branches" :key="idx"><div class="flex gap-2 items-center bg-slate-50 p-2 rounded-lg border border-slate-100"><input type="text" x-model="(canvasDrawer.node.branchNames = canvasDrawer.node.branchNames || [])[idx]" :placeholder="'Branch ' + (idx + 1)" class="flex-1 text-xs bg-transparent border-none p-0 focus:ring-0 font-bold text-slate-700"><button @click="canvasDrawer.node.branches.splice(idx, 1); canvasDrawer.node.branchNames.splice(idx, 1)" class="text-slate-300 hover:text-red-500 transition-colors"><i class="ti ti-trash text-sm"></i></button></div></template></div></div></template>
163
+ <template x-if="canvasDrawer.node.type === 'exit'"><div class="space-y-2"><label class="text-[10px] font-bold text-slate-400 uppercase">Response JSON</label><textarea x-model="canvasDrawer.node.bodyStr" @input="updateExitBody(canvasDrawer.node)" class="w-full text-xs bg-slate-50 border border-slate-200 rounded-lg p-2 h-48 font-mono"></textarea></div></template>
164
+ <div class="pt-6 border-t border-slate-100 space-y-4">
165
+ <div class="flex items-center justify-between"><label class="text-[10px] font-bold text-slate-400 uppercase">Node Testing</label><button @click="testIsolatedNode(canvasDrawer.node)" :disabled="!workflow.testDataset" class="px-3 py-1 bg-indigo-50 text-indigo-600 rounded-lg text-[10px] font-bold hover:bg-indigo-100 disabled:opacity-50">Test Node</button></div>
166
+ <template x-if="canvasDrawer.node.testOutput"><div class="space-y-2"><div class="flex justify-between items-center"><span class="text-[10px] font-bold text-gray-400 uppercase">Last Result</span><span class="text-[10px] text-green-600 font-bold uppercase">SUCCESS</span></div><div class="bg-slate-900 rounded-xl p-3 border border-slate-800 shadow-inner max-h-48 overflow-y-auto"><pre class="text-[10px] font-mono text-indigo-300" x-text="JSON.stringify(canvasDrawer.node.testOutput, null, 2)"></pre></div></div></template>
167
+ <div class="space-y-2 bg-slate-50 p-3 rounded-xl border border-slate-100"><span class="text-[10px] font-bold text-slate-500 uppercase">Expected Context</span><pre class="text-[9px] font-mono text-slate-500 overflow-x-auto" x-text="JSON.stringify(workflow.testDataset, null, 2)"></pre></div>
168
+ <button @click="confirmDeleteNode(canvasDrawer.node.id)" class="w-full py-2 text-xs font-bold text-red-500 hover:bg-red-50 rounded-lg border border-red-100 uppercase">Delete Node</button>
169
+ </div>
170
+ </div>
171
+ </div>
172
+ </template>
173
+ </div>
174
+ </div>
175
+ <div class="absolute bottom-6 left-1/2 -translate-x-1/2 bg-slate-800/80 backdrop-blur border border-slate-700 px-4 py-2 rounded-full flex gap-6 text-[10px] font-bold text-slate-400 uppercase pointer-events-none z-20"><div>Scroll to Zoom</div><div>Drag to Pan</div><div>ESC to Exit</div></div>
176
+ </div>
177
+
178
+ <!-- STACK WORKSPACE CONTENT -->
179
+ <div x-show="view === 'editor'" class="space-y-6">
180
+ <div x-show="showRuns" x-transition class="bg-white rounded-xl border border-gray-200 shadow-sm mb-8 overflow-hidden">
181
+ <div class="p-4 border-b border-gray-100 bg-gray-50 flex justify-between items-center"><h3 class="text-sm font-bold text-gray-700 uppercase">Execution History</h3><button @click="loadRuns()" class="text-xs text-indigo-600 hover:text-indigo-700"><i class="ti ti-refresh"></i> Refresh</button></div>
182
+ <div class="max-h-64 overflow-y-auto">
183
+ <table class="w-full text-left text-xs"><thead class="bg-gray-50 text-gray-400 font-bold sticky top-0 uppercase"><tr><th class="px-4 py-2">Date</th><th class="px-4 py-2">Status</th><th class="px-4 py-2">Duration</th><th class="px-4 py-2">Nodes</th><th class="px-4 py-2">Action</th></tr></thead><tbody class="divide-y divide-gray-100"><template x-for="run in runs" :key="run._id"><tr class="hover:bg-gray-50"><td class="px-4 py-2 text-gray-500" x-text="new Date(run.executedAt).toLocaleString()"></td><td class="px-4 py-2"><span :class="{'text-green-600 bg-green-50': run.status === 'completed', 'text-red-600 bg-red-50': run.status === 'failed'}" class="px-2 py-0.5 rounded text-[10px] font-bold" x-text="run.status.toUpperCase()"></span></td><td class="px-4 py-2 text-gray-500" x-text="run.duration + 'ms'"></td><td class="px-4 py-2 text-gray-500" x-text="run.log?.length || 0"></td><td class="px-4 py-2"><button @click="inspectRun(run)" class="text-indigo-600 hover:underline">Inspect</button></td></tr></template></tbody></table>
184
+ </div>
185
+ </div>
186
+ <template x-if="inspectedRun">
187
+ <div class="bg-white rounded-xl border border-gray-200 shadow-lg overflow-hidden mb-8" x-data="{ selectedLogIndex: 0 }">
188
+ <div class="p-4 border-b border-gray-100 bg-gray-50 flex justify-between items-center"><div class="flex items-center gap-3"><h3 class="text-sm font-bold text-gray-700 uppercase">Run Details</h3><span :class="{'text-green-600 bg-green-50': inspectedRun.status === 'completed', 'text-red-600 bg-red-50': inspectedRun.status === 'failed'}" class="px-2 py-0.5 rounded text-[10px] font-bold" x-text="inspectedRun.status.toUpperCase()"></span></div><button @click="inspectedRun = null" class="text-gray-400 hover:text-gray-600"><i class="ti ti-x"></i></button></div>
189
+ <div class="flex flex-col md:flex-row h-[500px]">
190
+ <div class="w-full md:w-1/3 border-r border-gray-100 overflow-y-auto p-4 space-y-2">
191
+ <template x-for="(log, idx) in inspectedRun.log" :key="idx"><button @click="selectedLogIndex = idx" :class="selectedLogIndex === idx ? 'bg-indigo-50 border-indigo-200' : 'hover:bg-gray-50'" class="w-full text-left p-3 rounded-lg border transition-all"><div class="flex justify-between items-center mb-1"><span class="text-xs font-bold text-gray-900" x-text="log.nodeName || log.type.toUpperCase()"></span><span class="text-[9px] font-mono text-gray-400" x-text="log.duration + 'ms'"></span></div><span :class="log.status === 'success' ? 'bg-green-100 text-green-700' : 'bg-red-100 text-red-700'" class="text-[8px] px-1.5 py-0.5 rounded font-bold uppercase" x-text="log.status"></span></button></template>
192
+ </div>
193
+ <div class="flex-1 overflow-y-auto p-6 space-y-6 bg-gray-50/50">
194
+ <div class="grid grid-cols-2 gap-4"><div class="bg-white p-4 rounded-xl border shadow-sm"><span class="text-[10px] font-bold text-gray-400 uppercase block mb-2">Payload</span><pre class="text-[10px] font-mono text-gray-600" x-text="JSON.stringify(inspectedRun.context?.payload, null, 2)"></pre></div><div class="bg-white p-4 rounded-xl border shadow-sm"><span class="text-[10px] font-bold text-gray-400 uppercase block mb-2">Result</span><pre class="text-[10px] font-mono text-indigo-600 font-bold" x-text="JSON.stringify(inspectedRun.context?.lastNode, null, 2)"></pre></div></div>
195
+ <template x-if="inspectedRun.log[selectedLogIndex]"><div class="bg-white rounded-xl border p-4"><pre class="text-[10px] font-mono text-gray-700 overflow-x-auto" x-text="JSON.stringify(inspectedRun.log[selectedLogIndex].result, null, 2)"></pre></div></template>
196
+ </div>
197
+ </div>
198
+ </div>
199
+ </template>
200
+ <div class="bg-white p-6 rounded-xl border border-gray-200 shadow-sm mb-4"><h2 class="text-lg font-semibold flex items-center gap-2 mb-4"><i class="ti ti-webhook text-indigo-500"></i> Webhook Entrypoint</h2><div class="grid grid-cols-1 md:grid-cols-2 gap-4"><div class="space-y-2"><label class="text-[10px] font-bold text-gray-400 uppercase">Allowed Methods</label><div class="flex gap-2"><template x-for="m in ['GET', 'POST', 'PUT', 'DELETE']"><button @click="toggleMethod(m)" :class="workflow.entrypoint.allowedMethods?.includes(m) ? 'bg-indigo-100 text-indigo-700' : 'bg-gray-50'" class="px-2 py-1 text-[10px] font-bold border rounded uppercase" x-text="m"></button></template></div></div></div></div>
201
+ <div class="w-full pb-8">
202
+ <div class="min-w-max space-y-0 px-4">
203
+ <template x-for="(node, index) in workflow.nodes" :key="node.id"><div class="flex flex-col items-center"><div class="node-connector"></div><div class="w-full bg-white p-6 rounded-xl border border-gray-200 shadow-sm relative"><div class="flex items-center justify-between mb-4"><div class="flex items-center gap-3"><div :class="getNodeColor(node.type)" class="p-2 rounded-lg text-white relative"><i :class="getNodeIcon(node.type)" class="text-lg"></i></div><h3 class="font-bold text-gray-900" x-text="node.type.toUpperCase()"></h3></div><button @click="deleteNode(node.id)" class="text-gray-400 hover:text-red-500"><i class="ti ti-trash"></i></button></div><div x-show="node.testOutput" class="mt-4 p-3 bg-gray-900 rounded-lg overflow-hidden"><pre class="text-[10px] text-green-400 font-mono" x-text="JSON.stringify(node.testOutput, null, 2)"></pre></div></div></div></template>
204
+ <div class="flex flex-col items-center mt-4"><div class="node-connector"></div><div class="flex gap-2"><button @click="addNode('llm')" class="p-2 bg-indigo-50 text-indigo-600 rounded-full hover:bg-indigo-100"><i class="ti ti-brain"></i></button><button @click="addNode('if')" class="p-2 bg-yellow-50 text-yellow-600 rounded-full hover:bg-yellow-100"><i class="ti ti-git-branch"></i></button><button @click="addNode('http')" class="p-2 bg-blue-50 text-blue-600 rounded-full hover:bg-blue-100"><i class="ti ti-world"></i></button><button @click="addNode('parallel')" class="p-2 bg-purple-50 text-purple-600 rounded-full hover:bg-purple-100"><i class="ti ti-git-merge"></i></button><button @click="addNode('exit')" class="p-2 bg-red-50 text-red-600 rounded-full hover:bg-red-100"><i class="ti ti-door-exit"></i></button></div></div>
205
+ </div>
206
+ </div>
207
+ </div>
208
+ </div>
209
+ </template>
210
+
211
+ <script>
212
+ function workflowEditor() {
213
+ const workflowIdFromUrl = window.location.pathname.split('/').pop();
214
+ return {
215
+ view: 'list', workflows: [], workflow: { name: '', status: 'inactive', entrypoint: { awaitResponse: false, allowedMethods: ['POST', 'GET'], auth: { type: 'none' } }, testDataset: { method: 'POST', body: { message: 'Hello' }, query: {}, headers: {} }, nodes: [] },
216
+ showTestPanel: false, testing: false, showRuns: false, testConfig: { method: 'POST', body: '{"message":"Hello"}', query: '{}', headers: '{}' }, testResult: { status: null, log: [], context: null }, runs: [], inspectedRun: null, toasts: [], canvasZoom: 1, canvasPan: { x: 0, y: 0 }, isPanning: false, panStart: { x: 0, y: 0 },
217
+ canvasDrawer: { open: false, mode: 'edit', node: null, parentArray: null, search: '' }, confirmModal: { show: false, title: '', message: '', onConfirm: () => {} },
218
+ init() {
219
+ window.canvasZoomSensitivity = 0.025; window.alpineWorkflow = this;
220
+ this.loadWorkflows().then(() => {
221
+ if (workflowIdFromUrl !== 'new' && workflowIdFromUrl.length > 5) {
222
+ const wf = this.workflows.find(w => w._id === workflowIdFromUrl);
223
+ if (wf) this.editWorkflow(wf); else fetch(`/saas/api/workflows/${workflowIdFromUrl}`).then(res => res.ok && res.json().then(data => this.editWorkflow(data)));
224
+ } else if (workflowIdFromUrl === 'new') this.createNew();
225
+ });
226
+ },
227
+ async loadWorkflows() { const res = await fetch('/saas/api/workflows'); this.workflows = await res.json(); },
228
+ createNew() { this.workflow = { name: 'New Workflow', status: 'inactive', entrypoint: { awaitResponse: false, allowedMethods: ['POST', 'GET'], auth: { type: 'none' } }, testDataset: { method: 'POST', body: { message: 'Hello' }, query: {}, headers: {} }, nodes: [] }; this.view = 'editor'; },
229
+ editWorkflow(wf) {
230
+ const currentView = this.view; this.workflow = JSON.parse(JSON.stringify(wf)); this.showRuns = false; this.runs = []; this.inspectedRun = null;
231
+ if (!this.workflow.testDataset) this.workflow.testDataset = { method: 'POST', body: { message: 'Hello' }, query: {}, headers: {} };
232
+ this.workflow.nodes.forEach(n => { if (n.type === 'exit') n.bodyStr = JSON.stringify(n.body, null, 2); n.testOutput = n.testResult || null; });
233
+ this.view = (currentView === 'list') ? 'editor' : currentView;
234
+ },
235
+ toggleMethod(m) { this.workflow.entrypoint.allowedMethods = this.workflow.entrypoint.allowedMethods || []; const idx = this.workflow.entrypoint.allowedMethods.indexOf(m); if (idx > -1) this.workflow.entrypoint.allowedMethods.splice(idx, 1); else this.workflow.entrypoint.allowedMethods.push(m); },
236
+ async deleteWorkflow(id) { if (confirm('Are you sure?')) { await fetch(`/saas/api/workflows/${id}`, { method: 'DELETE' }); await this.loadWorkflows(); } },
237
+ getWebhookUrl() { return window.location.origin + '/saas/w/' + (this.workflow._id || 'NEW'); },
238
+ copyWebhookUrl() { navigator.clipboard.writeText(this.getWebhookUrl()); this.showToast('Copied!'); },
239
+ addNode(type) { this.workflow.nodes.push({ id: 'node_' + Date.now(), type, name: 'node_' + (this.workflow.nodes.length + 1) }); },
240
+ deleteNode(id, array = null) {
241
+ const targetArray = array || this.workflow.nodes; const idx = targetArray.findIndex(n => n.id === id);
242
+ if (idx > -1) { targetArray.splice(idx, 1); return true; }
243
+ for (const n of targetArray) { if (n.then && this.deleteNode(id, n.then)) return true; if (n.else && this.deleteNode(id, n.else)) return true; if (n.branches) for (const b of n.branches) if (this.deleteNode(id, b)) return true; }
244
+ return false;
245
+ },
246
+ updateExitBody(node) { try { node.body = JSON.parse(node.bodyStr); } catch (e) {} },
247
+ getNodeIcon(type) { return { llm: 'ti-brain', if: 'ti-git-branch', http: 'ti-world', exit: 'ti-door-exit', parallel: 'ti-git-merge' }[type] || 'ti-circle'; },
248
+ getNodeColor(type) { return { llm: 'bg-indigo-500', if: 'bg-yellow-500', http: 'bg-blue-500', exit: 'bg-red-500', parallel: 'bg-purple-500' }[type]; },
249
+ async saveWorkflow(silent = false) {
250
+ const res = await fetch(this.workflow._id ? `/saas/api/workflows/${this.workflow._id}` : '/saas/api/workflows', { method: this.workflow._id ? 'PUT' : 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(this.workflow) });
251
+ if (res.ok) { const saved = await res.json(); if (!silent) this.showToast('Saved!'); await this.loadWorkflows(); this.editWorkflow(saved); }
252
+ },
253
+ async runFullTest(entrypointOnly = false) {
254
+ if (!this.workflow._id) return this.showToast('Save first', 'error');
255
+ try {
256
+ const payload = { method: this.testConfig.method, body: JSON.parse(this.testConfig.body), query: JSON.parse(this.testConfig.query), headers: JSON.parse(this.testConfig.headers) };
257
+ if (entrypointOnly) { this.workflow.testDataset = payload; await this.saveWorkflow(true); return this.showToast('Dataset set.'); }
258
+ this.testing = true; const res = await fetch(`/saas/api/workflows/${this.workflow._id}/test`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) });
259
+ this.testResult = await res.json(); if (this.showRuns) this.loadRuns(); this.showToast('Ran.');
260
+ } catch (e) { this.showToast('Failed', 'error'); } finally { this.testing = false; }
261
+ },
262
+ confirmDeleteNode(id) { this.confirmModal = { show: true, title: 'Delete?', message: 'Sure?', onConfirm: () => this.deleteNode(id) }; },
263
+ async testIsolatedNode(node) {
264
+ const context = { entrypoint: this.workflow.testDataset, payload: this.workflow.testDataset, lastNode: this.workflow.testDataset };
265
+ try {
266
+ const res = await fetch(`/saas/api/workflows/${this.workflow._id}/nodes/${node.id}/test`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ context, node }) });
267
+ const data = await res.json(); node.testOutput = data.result; node.testResult = data.result; this.showToast('Tested');
268
+ } catch (e) { this.showToast('Failed', 'error'); }
269
+ },
270
+ async loadRuns() { if (!this.workflow._id) return; const res = await fetch(`/saas/api/workflows/${this.workflow._id}/runs`); this.runs = await res.json(); },
271
+ inspectRun(run) { this.inspectedRun = run; }, testWorkflow() { this.showTestPanel = true; },
272
+ showToast(message, type = 'success') {
273
+ const id = Date.now(); this.toasts.push({ id, message, type, show: true });
274
+ setTimeout(() => { const t = this.toasts.find(x => x.id === id); if (t) t.show = false; setTimeout(() => this.toasts = this.toasts.filter(x => x.id !== id), 500); }, 3000);
275
+ },
276
+ openCanvasEdit(node) { if (node === 'entrypoint') this.canvasDrawer = { open: true, mode: 'edit_entrypoint', node: this.workflow.entrypoint }; else this.canvasDrawer = { open: true, mode: 'edit', node }; },
277
+ openCanvasAdd(parentArray, index = null) { this.canvasDrawer = { open: true, mode: 'add', parentArray, insertIndex: index, search: '' }; },
278
+ addNodeToCanvas(type) {
279
+ const newNode = { id: 'node_' + Date.now(), type, name: 'node_' + Date.now().toString().slice(-4) };
280
+ if (type === 'llm') { newNode.prompt = ''; newNode.provider = 'openrouter'; newNode.model = 'minimax/minimax-m2.1'; newNode.temperature = 0.7; }
281
+ if (type === 'if') { newNode.condition = ''; newNode.then = []; newNode.else = []; }
282
+ if (type === 'parallel') { newNode.branches = [[], []]; }
283
+ if (type === 'http') { newNode.method = 'GET'; newNode.url = ''; newNode.headers = {}; newNode.body = {}; }
284
+ if (type === 'exit') { newNode.body = { success: true }; newNode.bodyStr = '{"success":true}'; }
285
+ if (this.canvasDrawer.insertIndex !== null) this.canvasDrawer.parentArray.splice(this.canvasDrawer.insertIndex, 0, newNode); else this.canvasDrawer.parentArray.push(newNode);
286
+ this.canvasDrawer.open = false; this.showToast('Added');
287
+ },
288
+ get filteredCanvasNodes() {
289
+ const types = [ { id: 'llm', name: 'LLM Node', icon: 'ti-brain', color: 'bg-indigo-500', desc: 'AI generation' }, { id: 'if', name: 'IF Condition', icon: 'ti-git-branch', color: 'bg-yellow-500', desc: 'Logic split' }, { id: 'http', name: 'HTTP Request', icon: 'ti-world', color: 'bg-blue-500', desc: 'API calls' }, { id: 'parallel', name: 'Parallel Path', icon: 'ti-git-merge', color: 'bg-purple-500', desc: 'Multi-branch' }, { id: 'exit', name: 'Exit', icon: 'ti-door-exit', color: 'bg-red-500', desc: 'Response' } ];
290
+ return this.canvasDrawer.search ? types.filter(t => t.name.toLowerCase().includes(this.canvasDrawer.search.toLowerCase())) : types;
291
+ },
292
+ canvasStartPan(e) { this.isPanning = true; this.panStart = { x: e.clientX - this.canvasPan.x, y: e.clientY - this.canvasPan.y }; },
293
+ canvasDoPan(e) { if (!this.isPanning) return; this.canvasPan = { x: e.clientX - this.panStart.x, y: e.clientY - this.panStart.y }; },
294
+ canvasEndPan() { this.isPanning = false; },
295
+ canvasDoZoom(e) { e.preventDefault(); const s = window.canvasZoomSensitivity || 0.025; this.canvasZoom = Math.min(Math.max(0.2, this.canvasZoom + (e.deltaY > 0 ? -s : s)), 3); },
296
+ renderCanvasNode(node, path, idx) {
297
+ const icon = this.getNodeIcon(node.type); const color = this.getNodeColor(node.type); const nodePath = 'window.alpineWorkflow.' + path + '[' + idx + ']'; const arrayPath = 'window.alpineWorkflow.' + path;
298
+ let branches = '';
299
+ if (node.type === 'if') branches = '<div class="flex flex-col items-center w-full"><div class="w-0.5 h-8 bg-slate-200"></div><div class="flex gap-16"><div class="flex flex-col items-center">TRUE<div class="p-4 bg-green-50 rounded-2xl border border-green-100"><button onclick="event.stopPropagation(); window.alpineWorkflow.openCanvasAdd(' + nodePath + '.then)" class="w-6 h-6 bg-white border rounded-full">+</button><div>' + (node.then || []).map((bn, i) => this.renderCanvasNode(bn, path + '[' + idx + '].then', i)).join('') + '</div></div></div><div class="flex flex-col items-center">FALSE<div class="p-4 bg-red-50 rounded-2xl border border-red-100"><button onclick="event.stopPropagation(); window.alpineWorkflow.openCanvasAdd(' + nodePath + '.else)" class="w-6 h-6 bg-white border rounded-full">+</button><div>' + (node.else || []).map((bn, i) => this.renderCanvasNode(bn, path + '[' + idx + '].else', i)).join('') + '</div></div></div></div></div>';
300
+ const badge = node.testResult ? '<div class="absolute -top-2 -right-2 bg-green-500 text-white text-[8px] px-1.5 rounded-full">MOCK</div>' : '';
301
+ return '<div class="flex flex-col items-center"><div class="relative bg-white p-4 rounded-2xl shadow-xl border-2 min-w-[220px] cursor-pointer group" onclick="window.alpineWorkflow.openCanvasEdit(' + nodePath + ')">' + badge + '<div class="flex items-center gap-3"><div class="' + color + ' p-2 rounded-xl text-white"><i class="ti ' + icon + '"></i></div><div><div class="font-bold text-[11px] uppercase">' + node.type + '</div><div class="text-[10px] text-slate-400">' + (node.name || 'unnamed') + '</div></div></div></div>' + branches + '<div class="w-0.5 h-6 bg-slate-200"></div><button onclick="event.stopPropagation(); window.alpineWorkflow.openCanvasAdd(\'' + path + '\', ' + (idx + 1) + ')" class="w-8 h-8 bg-white border-2 rounded-full">+</button></div>';
302
+ }
303
+ }
304
+ }
305
+ </script>
306
+ </div>
307
+
308
+ <script src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
309
+ </body>
310
+ </html>