@jiggai/kitchen 0.3.1 → 0.3.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 (307) hide show
  1. package/.next/BUILD_ID +1 -1
  2. package/.next/build-manifest.json +2 -2
  3. package/.next/server/app/_global-error.html +2 -2
  4. package/.next/server/app/_global-error.rsc +1 -1
  5. package/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  6. package/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  7. package/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  8. package/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  9. package/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  10. package/.next/server/app/_not-found.html +1 -1
  11. package/.next/server/app/_not-found.rsc +1 -1
  12. package/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  13. package/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  14. package/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  15. package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  16. package/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  17. package/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  18. package/.next/server/app/channels.html +2 -2
  19. package/.next/server/app/channels.rsc +1 -1
  20. package/.next/server/app/channels.segments/_full.segment.rsc +1 -1
  21. package/.next/server/app/channels.segments/_head.segment.rsc +1 -1
  22. package/.next/server/app/channels.segments/_index.segment.rsc +1 -1
  23. package/.next/server/app/channels.segments/_tree.segment.rsc +1 -1
  24. package/.next/server/app/channels.segments/channels/__PAGE__.segment.rsc +1 -1
  25. package/.next/server/app/channels.segments/channels.segment.rsc +1 -1
  26. package/.next/server/app/cron-jobs.html +1 -1
  27. package/.next/server/app/cron-jobs.rsc +1 -1
  28. package/.next/server/app/cron-jobs.segments/_full.segment.rsc +1 -1
  29. package/.next/server/app/cron-jobs.segments/_head.segment.rsc +1 -1
  30. package/.next/server/app/cron-jobs.segments/_index.segment.rsc +1 -1
  31. package/.next/server/app/cron-jobs.segments/_tree.segment.rsc +1 -1
  32. package/.next/server/app/cron-jobs.segments/cron-jobs/__PAGE__.segment.rsc +1 -1
  33. package/.next/server/app/cron-jobs.segments/cron-jobs.segment.rsc +1 -1
  34. package/.next/server/app/goals/new.html +2 -2
  35. package/.next/server/app/goals/new.rsc +1 -1
  36. package/.next/server/app/goals/new.segments/_full.segment.rsc +1 -1
  37. package/.next/server/app/goals/new.segments/_head.segment.rsc +1 -1
  38. package/.next/server/app/goals/new.segments/_index.segment.rsc +1 -1
  39. package/.next/server/app/goals/new.segments/_tree.segment.rsc +1 -1
  40. package/.next/server/app/goals/new.segments/goals/new/__PAGE__.segment.rsc +1 -1
  41. package/.next/server/app/goals/new.segments/goals/new.segment.rsc +1 -1
  42. package/.next/server/app/goals/new.segments/goals.segment.rsc +1 -1
  43. package/.next/server/app/goals.html +1 -1
  44. package/.next/server/app/goals.rsc +1 -1
  45. package/.next/server/app/goals.segments/_full.segment.rsc +1 -1
  46. package/.next/server/app/goals.segments/_head.segment.rsc +1 -1
  47. package/.next/server/app/goals.segments/_index.segment.rsc +1 -1
  48. package/.next/server/app/goals.segments/_tree.segment.rsc +1 -1
  49. package/.next/server/app/goals.segments/goals/__PAGE__.segment.rsc +1 -1
  50. package/.next/server/app/goals.segments/goals.segment.rsc +1 -1
  51. package/.next/server/app/settings.html +1 -1
  52. package/.next/server/app/settings.rsc +1 -1
  53. package/.next/server/app/settings.segments/_full.segment.rsc +1 -1
  54. package/.next/server/app/settings.segments/_head.segment.rsc +1 -1
  55. package/.next/server/app/settings.segments/_index.segment.rsc +1 -1
  56. package/.next/server/app/settings.segments/_tree.segment.rsc +1 -1
  57. package/.next/server/app/settings.segments/settings/__PAGE__.segment.rsc +1 -1
  58. package/.next/server/app/settings.segments/settings.segment.rsc +1 -1
  59. package/.next/server/pages/404.html +1 -1
  60. package/.next/server/pages/500.html +2 -2
  61. package/package.json +1 -2
  62. package/src/app/HomeClient.tsx +0 -207
  63. package/src/app/agents/[agentId]/agent-editor-tabs.tsx +0 -298
  64. package/src/app/agents/[agentId]/agent-editor.tsx +0 -468
  65. package/src/app/agents/[agentId]/page.tsx +0 -32
  66. package/src/app/api/__tests__/agents-add-route.test.ts +0 -143
  67. package/src/app/api/__tests__/agents-file-route.test.ts +0 -117
  68. package/src/app/api/__tests__/agents-files-route.test.ts +0 -61
  69. package/src/app/api/__tests__/agents-id-route.test.ts +0 -104
  70. package/src/app/api/__tests__/agents-identity-route.test.ts +0 -92
  71. package/src/app/api/__tests__/agents-route.test.ts +0 -54
  72. package/src/app/api/__tests__/agents-skills-install-route.test.ts +0 -131
  73. package/src/app/api/__tests__/agents-skills-route.test.ts +0 -64
  74. package/src/app/api/__tests__/agents-update-route.test.ts +0 -95
  75. package/src/app/api/__tests__/channels-bindings-route.test.ts +0 -143
  76. package/src/app/api/__tests__/cron-delete-route.test.ts +0 -93
  77. package/src/app/api/__tests__/cron-job-route.test.ts +0 -78
  78. package/src/app/api/__tests__/cron-jobs-route.test.ts +0 -116
  79. package/src/app/api/__tests__/cron-recipe-installed-route.test.ts +0 -114
  80. package/src/app/api/__tests__/gateway-restart-route.test.ts +0 -36
  81. package/src/app/api/__tests__/goals-promote-route.test.ts +0 -200
  82. package/src/app/api/__tests__/goals-route.test.ts +0 -184
  83. package/src/app/api/__tests__/ids-check-route.test.ts +0 -188
  84. package/src/app/api/__tests__/marketplace-recipes-route.test.ts +0 -123
  85. package/src/app/api/__tests__/recipes-clone-route.test.ts +0 -221
  86. package/src/app/api/__tests__/recipes-delete-route.test.ts +0 -248
  87. package/src/app/api/__tests__/recipes-id-route.test.ts +0 -166
  88. package/src/app/api/__tests__/recipes-route.test.ts +0 -57
  89. package/src/app/api/__tests__/recipes-team-agents-route.test.ts +0 -135
  90. package/src/app/api/__tests__/scaffold-route.test.ts +0 -173
  91. package/src/app/api/__tests__/settings-cron-installation-route.test.ts +0 -82
  92. package/src/app/api/__tests__/skills-available-route.test.ts +0 -47
  93. package/src/app/api/__tests__/swarms-start-route.test.ts +0 -79
  94. package/src/app/api/__tests__/swarms-status-route.test.ts +0 -42
  95. package/src/app/api/__tests__/teams-file-route.test.ts +0 -129
  96. package/src/app/api/__tests__/teams-files-route.test.ts +0 -57
  97. package/src/app/api/__tests__/teams-meta-route.test.ts +0 -113
  98. package/src/app/api/__tests__/teams-orchestrator-install-route.test.ts +0 -66
  99. package/src/app/api/__tests__/teams-orchestrator-route.test.ts +0 -59
  100. package/src/app/api/__tests__/teams-remove-team-route.test.ts +0 -122
  101. package/src/app/api/__tests__/teams-skills-install-route.test.ts +0 -78
  102. package/src/app/api/__tests__/teams-skills-route.test.ts +0 -73
  103. package/src/app/api/__tests__/teams-workflow-runs-route.test.ts +0 -85
  104. package/src/app/api/__tests__/teams-workflows-route.test.ts +0 -110
  105. package/src/app/api/__tests__/tickets-move-route.test.ts +0 -60
  106. package/src/app/api/agents/[id]/route.ts +0 -83
  107. package/src/app/api/agents/add/route.ts +0 -114
  108. package/src/app/api/agents/file/route.ts +0 -45
  109. package/src/app/api/agents/files/route.ts +0 -23
  110. package/src/app/api/agents/identity/route.ts +0 -41
  111. package/src/app/api/agents/route.ts +0 -22
  112. package/src/app/api/agents/skills/install/route.ts +0 -34
  113. package/src/app/api/agents/skills/route.ts +0 -39
  114. package/src/app/api/agents/update/route.ts +0 -52
  115. package/src/app/api/channels/bindings/route.ts +0 -63
  116. package/src/app/api/cron/__tests__/helpers.test.ts +0 -164
  117. package/src/app/api/cron/delete/route.ts +0 -23
  118. package/src/app/api/cron/helpers.ts +0 -172
  119. package/src/app/api/cron/job/route.ts +0 -22
  120. package/src/app/api/cron/jobs/route.ts +0 -52
  121. package/src/app/api/cron/recipe-installed/route.ts +0 -65
  122. package/src/app/api/gateway/restart/route.ts +0 -20
  123. package/src/app/api/goals/[id]/promote/route.ts +0 -119
  124. package/src/app/api/goals/[id]/route.ts +0 -54
  125. package/src/app/api/goals/route.ts +0 -44
  126. package/src/app/api/ids/check/route.ts +0 -113
  127. package/src/app/api/marketplace/recipes/[slug]/route.ts +0 -16
  128. package/src/app/api/marketplace/recipes/route.ts +0 -22
  129. package/src/app/api/recipes/[id]/route.ts +0 -62
  130. package/src/app/api/recipes/clone/route.ts +0 -106
  131. package/src/app/api/recipes/custom-team/route.ts +0 -193
  132. package/src/app/api/recipes/delete/helpers.ts +0 -65
  133. package/src/app/api/recipes/delete/route.ts +0 -73
  134. package/src/app/api/recipes/route.ts +0 -21
  135. package/src/app/api/recipes/team-agents/__tests__/helpers.test.ts +0 -156
  136. package/src/app/api/recipes/team-agents/helpers.ts +0 -151
  137. package/src/app/api/recipes/team-agents/route.ts +0 -80
  138. package/src/app/api/scaffold/__tests__/helpers.test.ts +0 -186
  139. package/src/app/api/scaffold/helpers.ts +0 -214
  140. package/src/app/api/scaffold/route.ts +0 -95
  141. package/src/app/api/settings/cron-installation/route.ts +0 -58
  142. package/src/app/api/skills/available/route.ts +0 -23
  143. package/src/app/api/swarms/start/route.ts +0 -100
  144. package/src/app/api/swarms/status/route.ts +0 -31
  145. package/src/app/api/teams/[teamId]/tickets/assign/route.ts +0 -105
  146. package/src/app/api/teams/[teamId]/tickets/assignees/route.ts +0 -27
  147. package/src/app/api/teams/[teamId]/tickets/delete/route.ts +0 -55
  148. package/src/app/api/teams/[teamId]/tickets/move/route.ts +0 -70
  149. package/src/app/api/teams/[teamId]/tickets/move-to-goals/route.ts +0 -56
  150. package/src/app/api/teams/file/route.ts +0 -46
  151. package/src/app/api/teams/files/route.ts +0 -63
  152. package/src/app/api/teams/memory/route.ts +0 -250
  153. package/src/app/api/teams/meta/route.ts +0 -43
  154. package/src/app/api/teams/orchestrator/install/route.ts +0 -129
  155. package/src/app/api/teams/orchestrator/route.ts +0 -216
  156. package/src/app/api/teams/remove-team/route.ts +0 -37
  157. package/src/app/api/teams/skills/install/route.ts +0 -18
  158. package/src/app/api/teams/skills/route.ts +0 -25
  159. package/src/app/api/teams/workflow-runs/route.ts +0 -534
  160. package/src/app/api/teams/workflow-templates/route.ts +0 -71
  161. package/src/app/api/teams/workflows/route.ts +0 -55
  162. package/src/app/api/tickets/assign/route.ts +0 -94
  163. package/src/app/api/tickets/assignees/route.ts +0 -24
  164. package/src/app/api/tickets/move/route.ts +0 -69
  165. package/src/app/channels/channels-client.tsx +0 -271
  166. package/src/app/channels/page.tsx +0 -5
  167. package/src/app/cron-jobs/cron-jobs-client.tsx +0 -243
  168. package/src/app/cron-jobs/page.tsx +0 -34
  169. package/src/app/favicon.ico +0 -0
  170. package/src/app/global-error.tsx +0 -50
  171. package/src/app/globals.css +0 -153
  172. package/src/app/goals/[id]/goal-editor.tsx +0 -162
  173. package/src/app/goals/[id]/page.tsx +0 -6
  174. package/src/app/goals/goals-client.tsx +0 -201
  175. package/src/app/goals/new/page.tsx +0 -81
  176. package/src/app/goals/page.tsx +0 -10
  177. package/src/app/layout.tsx +0 -53
  178. package/src/app/manifest.ts +0 -15
  179. package/src/app/not-found.tsx +0 -8
  180. package/src/app/page.tsx +0 -33
  181. package/src/app/recipes/CreateAgentModal.tsx +0 -156
  182. package/src/app/recipes/CreateCustomTeamModal.tsx +0 -375
  183. package/src/app/recipes/CreateModalShell.tsx +0 -55
  184. package/src/app/recipes/CreateTeamModal.tsx +0 -91
  185. package/src/app/recipes/[id]/RecipeEditor/RecipeEditorCreateModal.tsx +0 -72
  186. package/src/app/recipes/[id]/RecipeEditor/RecipeEditorPanel.tsx +0 -216
  187. package/src/app/recipes/[id]/RecipeEditor/index.tsx +0 -271
  188. package/src/app/recipes/[id]/RecipeEditor/recipe-editor-utils.ts +0 -46
  189. package/src/app/recipes/[id]/RecipeEditor/types.ts +0 -52
  190. package/src/app/recipes/[id]/page.tsx +0 -37
  191. package/src/app/recipes/page.tsx +0 -101
  192. package/src/app/recipes/recipes-client.tsx +0 -620
  193. package/src/app/settings/page.tsx +0 -26
  194. package/src/app/settings/settings-client.tsx +0 -91
  195. package/src/app/teams/[teamId]/CloneTeamModal.tsx +0 -116
  196. package/src/app/teams/[teamId]/OrchestratorPanel.tsx +0 -255
  197. package/src/app/teams/[teamId]/OrchestratorSetupModal.tsx +0 -184
  198. package/src/app/teams/[teamId]/PublishChangesModal.tsx +0 -43
  199. package/src/app/teams/[teamId]/page.tsx +0 -49
  200. package/src/app/teams/[teamId]/team-editor/TeamAgentsTab.tsx +0 -145
  201. package/src/app/teams/[teamId]/team-editor/TeamCronTab.tsx +0 -72
  202. package/src/app/teams/[teamId]/team-editor/TeamFilesTab.tsx +0 -74
  203. package/src/app/teams/[teamId]/team-editor/TeamMemoryTab.tsx +0 -349
  204. package/src/app/teams/[teamId]/team-editor/TeamRecipeTab.tsx +0 -151
  205. package/src/app/teams/[teamId]/team-editor/TeamSkillsTab.tsx +0 -68
  206. package/src/app/teams/[teamId]/team-editor/index.tsx +0 -558
  207. package/src/app/teams/[teamId]/team-editor/team-editor-data.ts +0 -255
  208. package/src/app/teams/[teamId]/team-editor/team-editor-utils.ts +0 -78
  209. package/src/app/teams/[teamId]/team-editor/types.ts +0 -34
  210. package/src/app/teams/[teamId]/tickets/[ticket]/page.tsx +0 -35
  211. package/src/app/teams/[teamId]/tickets/page.tsx +0 -15
  212. package/src/app/teams/[teamId]/workflows/[workflowId]/WorkflowCanvas.tsx +0 -111
  213. package/src/app/teams/[teamId]/workflows/[workflowId]/page.tsx +0 -27
  214. package/src/app/teams/[teamId]/workflows/[workflowId]/workflows-editor-client.tsx +0 -1608
  215. package/src/app/teams/[teamId]/workflows/page.tsx +0 -40
  216. package/src/app/teams/[teamId]/workflows/workflows-client.tsx +0 -494
  217. package/src/app/tickets/TicketDetailClient.tsx +0 -147
  218. package/src/app/tickets/TicketsBoardClient.tsx +0 -200
  219. package/src/app/tickets/[ticket]/TicketAssignControl.tsx +0 -112
  220. package/src/app/tickets/[ticket]/page.tsx +0 -36
  221. package/src/app/tickets/page.tsx +0 -10
  222. package/src/components/AppShell.tsx +0 -286
  223. package/src/components/ConfirmationModal.tsx +0 -81
  224. package/src/components/DeleteEntityModal.tsx +0 -41
  225. package/src/components/ErrorBoundary.tsx +0 -70
  226. package/src/components/FileListWithOptionalToggle.tsx +0 -86
  227. package/src/components/GoalFormFields.tsx +0 -163
  228. package/src/components/ScaffoldOverlay.tsx +0 -78
  229. package/src/components/ThemeToggle.tsx +0 -53
  230. package/src/components/ToastProvider.tsx +0 -163
  231. package/src/components/__tests__/ConfirmationModal.test.tsx +0 -109
  232. package/src/components/__tests__/ErrorBoundary.test.tsx +0 -39
  233. package/src/components/__tests__/FileListWithOptionalToggle.test.tsx +0 -109
  234. package/src/components/__tests__/GoalFormFields.test.tsx +0 -117
  235. package/src/components/delete-modals.tsx +0 -59
  236. package/src/components/icons.tsx +0 -48
  237. package/src/lib/__tests__/agent-workspace.test.ts +0 -44
  238. package/src/lib/__tests__/agents.test.ts +0 -36
  239. package/src/lib/__tests__/api-route-helpers.test.ts +0 -188
  240. package/src/lib/__tests__/cron.test.ts +0 -45
  241. package/src/lib/__tests__/editor-utils.test.ts +0 -38
  242. package/src/lib/__tests__/errors.test.ts +0 -15
  243. package/src/lib/__tests__/exec.test.ts +0 -13
  244. package/src/lib/__tests__/fetch-json.test.ts +0 -118
  245. package/src/lib/__tests__/gateway.test.ts +0 -234
  246. package/src/lib/__tests__/goal-promote.test.ts +0 -39
  247. package/src/lib/__tests__/goals-client.test.ts +0 -26
  248. package/src/lib/__tests__/goals.test.ts +0 -275
  249. package/src/lib/__tests__/json.test.ts +0 -15
  250. package/src/lib/__tests__/kitchen-api.test.ts +0 -32
  251. package/src/lib/__tests__/marketplace.test.ts +0 -116
  252. package/src/lib/__tests__/openclaw.test.ts +0 -129
  253. package/src/lib/__tests__/paths.test.ts +0 -136
  254. package/src/lib/__tests__/poll.test.ts +0 -26
  255. package/src/lib/__tests__/recipe-clone.test.ts +0 -85
  256. package/src/lib/__tests__/recipe-team-agents.test.ts +0 -70
  257. package/src/lib/__tests__/recipes.test.ts +0 -199
  258. package/src/lib/__tests__/scaffold-client.test.ts +0 -106
  259. package/src/lib/__tests__/scaffold.test.ts +0 -64
  260. package/src/lib/__tests__/slugify.test.ts +0 -23
  261. package/src/lib/__tests__/tickets.test.ts +0 -158
  262. package/src/lib/__tests__/type-guards.test.ts +0 -18
  263. package/src/lib/__tests__/use-slugified-id.test.tsx +0 -120
  264. package/src/lib/agent-workspace.ts +0 -14
  265. package/src/lib/agents.ts +0 -17
  266. package/src/lib/api-route-helpers.ts +0 -157
  267. package/src/lib/cron.ts +0 -40
  268. package/src/lib/editor-utils.ts +0 -18
  269. package/src/lib/errors.ts +0 -7
  270. package/src/lib/exec.ts +0 -4
  271. package/src/lib/fetch-json.ts +0 -29
  272. package/src/lib/gateway.ts +0 -100
  273. package/src/lib/goal-promote.ts +0 -27
  274. package/src/lib/goals-client.ts +0 -69
  275. package/src/lib/goals.ts +0 -171
  276. package/src/lib/json.ts +0 -10
  277. package/src/lib/kitchen-api.ts +0 -19
  278. package/src/lib/marketplace.ts +0 -46
  279. package/src/lib/openclaw.ts +0 -59
  280. package/src/lib/paths.ts +0 -69
  281. package/src/lib/poll.ts +0 -18
  282. package/src/lib/recipe-clone.ts +0 -42
  283. package/src/lib/recipe-team-agents.ts +0 -30
  284. package/src/lib/recipes.ts +0 -95
  285. package/src/lib/scaffold-client.ts +0 -31
  286. package/src/lib/scaffold.ts +0 -37
  287. package/src/lib/slugify.ts +0 -25
  288. package/src/lib/swarms.ts +0 -25
  289. package/src/lib/tickets.ts +0 -192
  290. package/src/lib/type-guards.ts +0 -3
  291. package/src/lib/use-slugified-id.ts +0 -35
  292. package/src/lib/workflows/README.md +0 -11
  293. package/src/lib/workflows/__tests__/storage.test.ts +0 -129
  294. package/src/lib/workflows/__tests__/validate.test.ts +0 -92
  295. package/src/lib/workflows/api-handlers.ts +0 -35
  296. package/src/lib/workflows/readdir.ts +0 -23
  297. package/src/lib/workflows/runs-storage.ts +0 -59
  298. package/src/lib/workflows/runs-types.ts +0 -42
  299. package/src/lib/workflows/storage.ts +0 -70
  300. package/src/lib/workflows/templates/index.ts +0 -1
  301. package/src/lib/workflows/templates/marketing-cadence-v1.ts +0 -142
  302. package/src/lib/workflows/types.ts +0 -48
  303. package/src/lib/workflows/validate.ts +0 -92
  304. package/src/proxy.ts +0 -28
  305. /package/.next/static/{z86RoqzzXXrWnpi229zP6 → Jrrrm9HH5bKkSrQhe1j93}/_buildManifest.js +0 -0
  306. /package/.next/static/{z86RoqzzXXrWnpi229zP6 → Jrrrm9HH5bKkSrQhe1j93}/_clientMiddlewareManifest.json +0 -0
  307. /package/.next/static/{z86RoqzzXXrWnpi229zP6 → Jrrrm9HH5bKkSrQhe1j93}/_ssgManifest.js +0 -0
@@ -1,1608 +0,0 @@
1
- "use client";
2
-
3
- import { useEffect, useMemo, useRef, useState } from "react";
4
- import type { WorkflowFileV1 } from "@/lib/workflows/types";
5
- import { validateWorkflowFileV1 } from "@/lib/workflows/validate";
6
-
7
- type LoadState =
8
- | { kind: "loading" }
9
- | { kind: "error"; error: string }
10
- | { kind: "ready"; jsonText: string };
11
-
12
- function draftKey(teamId: string, workflowId: string) {
13
- return `ck-wf-draft:${teamId}:${workflowId}`;
14
- }
15
-
16
- export default function WorkflowsEditorClient({
17
- teamId,
18
- workflowId,
19
- draft,
20
- }: {
21
- teamId: string;
22
- workflowId: string;
23
- draft: boolean;
24
- }) {
25
- const [view, setView] = useState<"canvas" | "json">("canvas");
26
- const [saving, setSaving] = useState(false);
27
- const [status, setStatus] = useState<LoadState>({ kind: "loading" });
28
- const [actionError, setActionError] = useState<string>("");
29
- const importInputRef = useRef<HTMLInputElement | null>(null);
30
-
31
- const [toolsCollapsed, setToolsCollapsed] = useState(false);
32
-
33
- // Canvas: selection, drag, node/edge creation.
34
- const canvasRef = useRef<HTMLDivElement | null>(null);
35
- const [selectedNodeId, setSelectedNodeId] = useState<string>("");
36
- const [dragging, setDragging] = useState<null | { nodeId: string; dx: number; dy: number; left: number; top: number }>(null);
37
-
38
- const [activeTool, setActiveTool] = useState<
39
- | { kind: "select" }
40
- | { kind: "add-node"; nodeType: WorkflowFileV1["nodes"][number]["type"] }
41
- | { kind: "connect" }
42
- >({ kind: "select" });
43
- const [connectFromNodeId, setConnectFromNodeId] = useState<string>("");
44
-
45
- const [agents, setAgents] = useState<Array<{ id: string; identityName?: string }>>([]);
46
- const [agentsError, setAgentsError] = useState<string>("");
47
-
48
- // Inspector state (parity with modal)
49
- const [workflowRuns, setWorkflowRuns] = useState<string[]>([]);
50
- const [workflowRunsLoading, setWorkflowRunsLoading] = useState(false);
51
- const [workflowRunsError, setWorkflowRunsError] = useState("");
52
- const [selectedWorkflowRunId, setSelectedWorkflowRunId] = useState<string>("");
53
-
54
- const [newNodeId, setNewNodeId] = useState("");
55
- const [newNodeName, setNewNodeName] = useState("");
56
- const [newNodeType, setNewNodeType] = useState<WorkflowFileV1["nodes"][number]["type"]>("llm");
57
-
58
- const [newEdgeFrom, setNewEdgeFrom] = useState("");
59
- const [newEdgeTo, setNewEdgeTo] = useState("");
60
- const [newEdgeLabel, setNewEdgeLabel] = useState("");
61
-
62
- useEffect(() => {
63
- (async () => {
64
- try {
65
- if (draft) {
66
- const stored = sessionStorage.getItem(draftKey(teamId, workflowId));
67
- if (stored) {
68
- setStatus({ kind: "ready", jsonText: stored });
69
- return;
70
- }
71
-
72
- // New draft: initialize a clean workflow instead of trying to fetch an existing file.
73
- const initial: WorkflowFileV1 = {
74
- schema: "clawkitchen.workflow.v1",
75
- id: workflowId,
76
- name: "New workflow",
77
- timezone: "UTC",
78
- nodes: [
79
- { id: "start", type: "start", name: "start", x: 80, y: 80, config: {} },
80
- { id: "end", type: "end", name: "end", x: 520, y: 80, config: {} },
81
- ],
82
- edges: [{ id: "e1", from: "start", to: "end" }],
83
- };
84
- const text = JSON.stringify(initial, null, 2) + "\n";
85
- setStatus({ kind: "ready", jsonText: text });
86
- try {
87
- sessionStorage.setItem(draftKey(teamId, workflowId), text);
88
- } catch {
89
- // ignore
90
- }
91
- return;
92
- }
93
-
94
- const res = await fetch(
95
- `/api/teams/workflows?teamId=${encodeURIComponent(teamId)}&id=${encodeURIComponent(workflowId)}`,
96
- { cache: "no-store" }
97
- );
98
- const json = (await res.json()) as { ok?: boolean; error?: string; workflow?: unknown };
99
- if (!res.ok || !json.ok) throw new Error(json.error || "Failed to load workflow");
100
- setStatus({ kind: "ready", jsonText: JSON.stringify(json.workflow, null, 2) + "\n" });
101
- } catch (e: unknown) {
102
- setStatus({ kind: "error", error: e instanceof Error ? e.message : String(e) });
103
- }
104
- })();
105
- }, [teamId, workflowId, draft]);
106
-
107
- useEffect(() => {
108
- (async () => {
109
- setAgentsError("");
110
- try {
111
- const res = await fetch("/api/agents", { cache: "no-store" });
112
- const json = (await res.json()) as { agents?: Array<{ id?: unknown; identityName?: unknown }>; error?: string; message?: string };
113
- if (!res.ok) throw new Error(json.error || json.message || "Failed to load agents");
114
- const list = Array.isArray(json.agents) ? json.agents : [];
115
- const filtered = list
116
- .map((a) => ({ id: String(a.id ?? "").trim(), identityName: typeof a.identityName === "string" ? a.identityName : undefined }))
117
- .filter((a) => a.id && a.id.startsWith(`${teamId}-`));
118
- setAgents(filtered);
119
- } catch (e: unknown) {
120
- setAgentsError(e instanceof Error ? e.message : String(e));
121
- setAgents([]);
122
- }
123
- })();
124
- }, [teamId]);
125
-
126
- const parsed = useMemo(() => {
127
- if (status.kind !== "ready") return { wf: null as WorkflowFileV1 | null, err: "" };
128
- try {
129
- const wf = JSON.parse(status.jsonText) as WorkflowFileV1;
130
- return { wf, err: "" };
131
- } catch (e: unknown) {
132
- return { wf: null, err: e instanceof Error ? e.message : String(e) };
133
- }
134
- }, [status]);
135
-
136
- const validation = useMemo(() => {
137
- if (!parsed.wf) return { errors: [], warnings: [] as string[] };
138
- return validateWorkflowFileV1(parsed.wf);
139
- }, [parsed.wf]);
140
-
141
- function setWorkflow(next: WorkflowFileV1) {
142
- const text = JSON.stringify(next, null, 2) + "\n";
143
- setStatus({ kind: "ready", jsonText: text });
144
- if (draft) {
145
- try {
146
- sessionStorage.setItem(draftKey(teamId, workflowId), text);
147
- } catch {
148
- // ignore
149
- }
150
- }
151
- }
152
-
153
- async function onSave() {
154
- if (status.kind !== "ready") return;
155
- if (!parsed.wf) return;
156
- if (parsed.err) return;
157
- if (validation.errors.length) return;
158
-
159
- setSaving(true);
160
- setActionError("");
161
- try {
162
- const res = await fetch("/api/teams/workflows", {
163
- method: "POST",
164
- headers: { "content-type": "application/json" },
165
- body: JSON.stringify({ teamId, workflow: parsed.wf }),
166
- });
167
- const json = (await res.json()) as { ok?: boolean; error?: string };
168
- if (!res.ok || !json.ok) throw new Error(json.error || "Failed to save workflow");
169
-
170
- // Clear draft cache once persisted.
171
- try {
172
- sessionStorage.removeItem(draftKey(teamId, workflowId));
173
- } catch {
174
- // ignore
175
- }
176
- } catch (e: unknown) {
177
- setActionError(e instanceof Error ? e.message : String(e));
178
- } finally {
179
- setSaving(false);
180
- }
181
- }
182
-
183
- function onExport() {
184
- if (!parsed.wf) return;
185
- if (parsed.err) return;
186
- if (validation.errors.length) return;
187
-
188
- const filename = `${parsed.wf.id || workflowId}.workflow.json`;
189
- const blob = new Blob([JSON.stringify(parsed.wf, null, 2) + "\n"], { type: "application/json" });
190
- const url = URL.createObjectURL(blob);
191
- const a = document.createElement("a");
192
- a.href = url;
193
- a.download = filename;
194
- document.body.appendChild(a);
195
- a.click();
196
- a.remove();
197
- URL.revokeObjectURL(url);
198
- }
199
-
200
- if (status.kind === "loading") return <div className="ck-glass w-full p-6">Loading…</div>;
201
- if (status.kind === "error") return <div className="ck-glass w-full p-6">{status.error}</div>;
202
-
203
- // (section collapse uses native <details> to keep this file simple)
204
-
205
- return (
206
- <div className="flex h-full min-h-0 w-full flex-1 flex-col">
207
- <div className="flex flex-wrap items-center justify-between gap-3 px-3 py-3">
208
- <div className="flex min-w-0 items-center gap-2">
209
- <a
210
- href={`/teams/${encodeURIComponent(teamId)}?tab=workflows`}
211
- className="rounded-[var(--ck-radius-sm)] border border-white/10 bg-white/5 px-3 py-2 text-sm font-medium text-[color:var(--ck-text-primary)] hover:bg-white/10"
212
- >
213
- Back
214
- </a>
215
- <div className="min-w-0">
216
- <div className="truncate text-base font-medium text-[color:var(--ck-text-primary)]">
217
- {workflowId}.workflow.json
218
- </div>
219
- <div className="mt-0.5 text-sm text-[color:var(--ck-text-tertiary)]">Team: {teamId}</div>
220
- </div>
221
- </div>
222
-
223
- <div className="flex flex-wrap items-center gap-2">
224
- <div className="flex overflow-hidden rounded-[var(--ck-radius-sm)] border border-white/10">
225
- <button
226
- type="button"
227
- onClick={() => setView("canvas")}
228
- className={
229
- view === "canvas"
230
- ? "bg-white/10 px-3 py-2 text-xs font-medium text-[color:var(--ck-text-primary)]"
231
- : "bg-transparent px-3 py-2 text-xs font-medium text-[color:var(--ck-text-secondary)] hover:bg-white/5"
232
- }
233
- >
234
- Canvas
235
- </button>
236
- <button
237
- type="button"
238
- onClick={() => setView("json")}
239
- className={
240
- view === "json"
241
- ? "bg-white/10 px-3 py-2 text-xs font-medium text-[color:var(--ck-text-primary)]"
242
- : "bg-transparent px-3 py-2 text-xs font-medium text-[color:var(--ck-text-secondary)] hover:bg-white/5"
243
- }
244
- >
245
- JSON
246
- </button>
247
- </div>
248
-
249
- <input
250
- ref={importInputRef}
251
- type="file"
252
- accept="application/json,.json"
253
- className="hidden"
254
- onChange={async (e) => {
255
- const file = e.target.files?.[0];
256
- // Reset the input so re-importing the same file still triggers onChange.
257
- e.target.value = "";
258
- if (!file) return;
259
-
260
- setActionError("");
261
- try {
262
- const text = await file.text();
263
- const next = JSON.parse(text) as WorkflowFileV1;
264
- setStatus({ kind: "ready", jsonText: JSON.stringify(next, null, 2) + "\n" });
265
- if (draft) {
266
- try {
267
- sessionStorage.setItem(draftKey(teamId, workflowId), JSON.stringify(next, null, 2) + "\n");
268
- } catch {
269
- // ignore
270
- }
271
- }
272
- } catch (err: unknown) {
273
- setActionError(err instanceof Error ? err.message : String(err));
274
- }
275
- }}
276
- />
277
-
278
- <button
279
- type="button"
280
- disabled={saving}
281
- onClick={() => importInputRef.current?.click()}
282
- className="rounded-[var(--ck-radius-sm)] border border-white/10 bg-white/5 px-3 py-2 text-sm font-medium text-[color:var(--ck-text-primary)] shadow-[var(--ck-shadow-1)] hover:bg-white/10 disabled:opacity-50"
283
- >
284
- Import
285
- </button>
286
-
287
- <button
288
- type="button"
289
- disabled={!parsed.wf || Boolean(parsed.err) || validation.errors.length > 0}
290
- onClick={onExport}
291
- className="rounded-[var(--ck-radius-sm)] border border-white/10 bg-white/5 px-3 py-2 text-sm font-medium text-[color:var(--ck-text-primary)] shadow-[var(--ck-shadow-1)] hover:bg-white/10 disabled:opacity-50"
292
- >
293
- Export
294
- </button>
295
-
296
- <button
297
- type="button"
298
- disabled={saving || !parsed.wf || Boolean(parsed.err) || validation.errors.length > 0}
299
- onClick={onSave}
300
- className="rounded-[var(--ck-radius-sm)] bg-[var(--ck-accent-red)] px-3 py-2 text-sm font-medium text-white shadow-[var(--ck-shadow-1)] disabled:opacity-50"
301
- >
302
- {saving ? "Saving…" : "Save"}
303
- </button>
304
-
305
- {/* Back button lives in the left header. */}
306
- </div>
307
- </div>
308
-
309
- {parsed.err ? (
310
- <div className="mt-3 rounded-[var(--ck-radius-sm)] border border-yellow-400/30 bg-yellow-500/10 p-3 text-sm text-yellow-100">
311
- JSON parse error: {parsed.err}
312
- </div>
313
- ) : null}
314
- {!parsed.err && validation.errors.length ? (
315
- <div className="mt-3 rounded-[var(--ck-radius-sm)] border border-red-400/30 bg-red-500/10 p-3 text-sm text-red-100">
316
- <div className="font-medium">Workflow validation errors</div>
317
- <ul className="mt-2 list-disc space-y-1 pl-5">
318
- {validation.errors.map((e) => (
319
- <li key={e}>{e}</li>
320
- ))}
321
- </ul>
322
- </div>
323
- ) : null}
324
-
325
- {!parsed.err && !validation.errors.length && validation.warnings.length ? (
326
- <div className="mt-3 rounded-[var(--ck-radius-sm)] border border-yellow-400/30 bg-yellow-500/10 p-3 text-sm text-yellow-100">
327
- <div className="font-medium">Workflow validation warnings</div>
328
- <ul className="mt-2 list-disc space-y-1 pl-5">
329
- {validation.warnings.map((w) => (
330
- <li key={w}>{w}</li>
331
- ))}
332
- </ul>
333
- </div>
334
- ) : null}
335
-
336
- {actionError ? (
337
- <div className="mt-3 rounded-[var(--ck-radius-sm)] border border-red-400/30 bg-red-500/10 p-3 text-sm text-red-100">
338
- {actionError}
339
- </div>
340
- ) : null}
341
-
342
- <div className="flex min-h-0 flex-1 gap-0">
343
- {view === "json" ? (
344
- <textarea
345
- value={status.jsonText}
346
- onChange={(e) => {
347
- const t = e.target.value;
348
- setStatus({ kind: "ready", jsonText: t });
349
- if (draft) {
350
- try {
351
- sessionStorage.setItem(draftKey(teamId, workflowId), t);
352
- } catch {
353
- // ignore
354
- }
355
- }
356
- }}
357
- className="h-full min-h-0 w-full flex-1 resize-none rounded-[var(--ck-radius-sm)] border border-white/10 bg-black/20 p-3 font-mono text-xs text-[color:var(--ck-text-primary)]"
358
- />
359
- ) : (
360
- <div
361
- ref={canvasRef}
362
- className="relative h-full min-h-0 w-full flex-1 overflow-auto bg-black/20"
363
- onClick={(e) => {
364
- if (activeTool.kind !== "add-node") return;
365
- const wf = parsed.wf;
366
- if (!wf) return;
367
- const el = canvasRef.current;
368
- if (!el) return;
369
-
370
- // Only create when clicking on the canvas background (not a node).
371
- const target = e.target as HTMLElement | null;
372
- if (target && target.closest("[data-wf-node='1']")) return;
373
-
374
- const rect = el.getBoundingClientRect();
375
- const clickX = e.clientX - rect.left + el.scrollLeft;
376
- const clickY = e.clientY - rect.top + el.scrollTop;
377
-
378
- const base = activeTool.nodeType.replace(/[^a-z0-9_\-]/gi, "_");
379
- const used = new Set(wf.nodes.map((n) => n.id));
380
- let i = 1;
381
- let id = `${base}_${i}`;
382
- while (used.has(id)) {
383
- i++;
384
- id = `${base}_${i}`;
385
- }
386
-
387
- const x = Math.max(0, clickX - 90);
388
- const y = Math.max(0, clickY - 24);
389
-
390
- const nextNode: WorkflowFileV1["nodes"][number] = {
391
- id,
392
- type: activeTool.nodeType,
393
- name: id,
394
- x,
395
- y,
396
- config: {},
397
- };
398
-
399
- setWorkflow({ ...wf, nodes: [...wf.nodes, nextNode] });
400
- setSelectedNodeId(id);
401
- setActiveTool({ kind: "select" });
402
- }}
403
- >
404
- <div className="relative h-[1200px] w-[2200px]">
405
- {/* Tool palette / agent palette */}
406
- <div
407
- className={
408
- toolsCollapsed
409
- ? "sticky left-3 top-3 z-20 w-[44px] overflow-hidden rounded-[var(--ck-radius-sm)] border border-white/10 bg-black/40 p-2 backdrop-blur"
410
- : "sticky left-3 top-3 z-20 w-[260px] rounded-[var(--ck-radius-sm)] border border-white/10 bg-black/40 p-2 backdrop-blur"
411
- }
412
- >
413
- <div className="flex items-center justify-between gap-2">
414
- <div className={toolsCollapsed ? "hidden" : "text-[10px] font-medium uppercase tracking-wide text-[color:var(--ck-text-tertiary)]"}>Tools</div>
415
- <button
416
- type="button"
417
- onClick={() => setToolsCollapsed((v) => !v)}
418
- className="rounded-[var(--ck-radius-sm)] border border-white/10 bg-white/5 px-2 py-1 text-[10px] text-[color:var(--ck-text-secondary)] hover:bg-white/10"
419
- title={toolsCollapsed ? "Expand" : "Collapse"}
420
- >
421
- {toolsCollapsed ? ">" : "<"}
422
- </button>
423
- </div>
424
- {toolsCollapsed ? (
425
- <div className="mt-2 flex flex-col items-center gap-2">
426
- {(
427
- [
428
- {
429
- key: "select",
430
- label: "Select",
431
- active: activeTool.kind === "select",
432
- onClick: () => {
433
- setActiveTool({ kind: "select" });
434
- setConnectFromNodeId("");
435
- },
436
- icon: (
437
- <svg viewBox="0 0 24 24" className="h-4 w-4" fill="none" aria-hidden="true">
438
- <path d="M5 4l7 16 2-7 7-2L5 4Z" stroke="currentColor" strokeWidth="1.6" strokeLinejoin="round" />
439
- </svg>
440
- ),
441
- },
442
- {
443
- key: "connect",
444
- label: "Connect",
445
- active: activeTool.kind === "connect",
446
- onClick: () => {
447
- setActiveTool({ kind: "connect" });
448
- setConnectFromNodeId("");
449
- },
450
- icon: (
451
- <svg viewBox="0 0 24 24" className="h-4 w-4" fill="none" aria-hidden="true">
452
- <path d="M10 13a5 5 0 0 1 0-7l1.2-1.2a5 5 0 0 1 7 7L17 12" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" />
453
- <path d="M14 11a5 5 0 0 1 0 7L12.8 19.2a5 5 0 1 1-7-7L7 12" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" />
454
- </svg>
455
- ),
456
- },
457
- {
458
- key: "llm",
459
- label: "LLM",
460
- active: activeTool.kind === "add-node" && activeTool.nodeType === "llm",
461
- onClick: () => {
462
- setActiveTool({ kind: "add-node", nodeType: "llm" });
463
- setConnectFromNodeId("");
464
- },
465
- icon: (
466
- <svg viewBox="0 0 24 24" className="h-4 w-4" fill="none" aria-hidden="true">
467
- <path d="M12 2l1.5 6.5L20 10l-6.5 1.5L12 18l-1.5-6.5L4 10l6.5-1.5L12 2Z" stroke="currentColor" strokeWidth="1.6" strokeLinejoin="round" />
468
- </svg>
469
- ),
470
- },
471
- {
472
- key: "tool",
473
- label: "Tool",
474
- active: activeTool.kind === "add-node" && activeTool.nodeType === "tool",
475
- onClick: () => {
476
- setActiveTool({ kind: "add-node", nodeType: "tool" });
477
- setConnectFromNodeId("");
478
- },
479
- icon: (
480
- <svg viewBox="0 0 24 24" className="h-4 w-4" fill="none" aria-hidden="true">
481
- <path d="M14.5 7.5l2 2-8.5 8.5H6v-2l8.5-8.5Z" stroke="currentColor" strokeWidth="1.6" strokeLinejoin="round" />
482
- <path d="M12 6a4 4 0 0 0-5 5l3-3 2 2 3-3A4 4 0 0 0 12 6Z" stroke="currentColor" strokeWidth="1.6" strokeLinejoin="round" />
483
- </svg>
484
- ),
485
- },
486
- {
487
- key: "condition",
488
- label: "If",
489
- active: activeTool.kind === "add-node" && activeTool.nodeType === "condition",
490
- onClick: () => {
491
- setActiveTool({ kind: "add-node", nodeType: "condition" });
492
- setConnectFromNodeId("");
493
- },
494
- icon: (
495
- <svg viewBox="0 0 24 24" className="h-4 w-4" fill="none" aria-hidden="true">
496
- <path d="M7 4v7a3 3 0 0 0 3 3h7" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" />
497
- <path d="M17 10l3 3-3 3" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round" />
498
- </svg>
499
- ),
500
- },
501
- {
502
- key: "delay",
503
- label: "Delay",
504
- active: activeTool.kind === "add-node" && activeTool.nodeType === "delay",
505
- onClick: () => {
506
- setActiveTool({ kind: "add-node", nodeType: "delay" });
507
- setConnectFromNodeId("");
508
- },
509
- icon: (
510
- <svg viewBox="0 0 24 24" className="h-4 w-4" fill="none" aria-hidden="true">
511
- <path d="M12 7v5l3 2" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round" />
512
- <path d="M21 12a9 9 0 1 1-9-9" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" />
513
- </svg>
514
- ),
515
- },
516
- {
517
- key: "approval",
518
- label: "Approval",
519
- active: activeTool.kind === "add-node" && activeTool.nodeType === "human_approval",
520
- onClick: () => {
521
- setActiveTool({ kind: "add-node", nodeType: "human_approval" });
522
- setConnectFromNodeId("");
523
- },
524
- icon: (
525
- <svg viewBox="0 0 24 24" className="h-4 w-4" fill="none" aria-hidden="true">
526
- <path d="M20 6 9 17l-5-5" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" />
527
- </svg>
528
- ),
529
- },
530
- {
531
- key: "end",
532
- label: "End",
533
- active: activeTool.kind === "add-node" && activeTool.nodeType === "end",
534
- onClick: () => {
535
- setActiveTool({ kind: "add-node", nodeType: "end" });
536
- setConnectFromNodeId("");
537
- },
538
- icon: (
539
- <svg viewBox="0 0 24 24" className="h-4 w-4" fill="none" aria-hidden="true">
540
- <path d="M7 7h10v10H7V7Z" stroke="currentColor" strokeWidth="1.6" strokeLinejoin="round" />
541
- </svg>
542
- ),
543
- },
544
- ] as const
545
- ).map((b) => (
546
- <button
547
- key={b.key}
548
- type="button"
549
- onClick={b.onClick}
550
- className={
551
- b.active
552
- ? "flex h-8 w-8 items-center justify-center rounded-[var(--ck-radius-sm)] bg-white/10 text-[color:var(--ck-text-primary)]"
553
- : "flex h-8 w-8 items-center justify-center rounded-[var(--ck-radius-sm)] border border-white/10 bg-white/5 text-[color:var(--ck-text-secondary)] hover:bg-white/10"
554
- }
555
- title={b.label}
556
- aria-label={b.label}
557
- >
558
- {b.icon}
559
- </button>
560
- ))}
561
- </div>
562
- ) : (
563
- <div className="mt-2 grid grid-cols-2 gap-2">
564
- <button
565
- type="button"
566
- onClick={() => {
567
- setActiveTool({ kind: "select" });
568
- setConnectFromNodeId("");
569
- }}
570
- className={
571
- activeTool.kind === "select"
572
- ? "rounded-[var(--ck-radius-sm)] bg-white/10 px-2 py-1 text-xs text-[color:var(--ck-text-primary)]"
573
- : "rounded-[var(--ck-radius-sm)] border border-white/10 bg-white/5 px-2 py-1 text-xs text-[color:var(--ck-text-secondary)] hover:bg-white/10"
574
- }
575
- >
576
- Select
577
- </button>
578
- <button
579
- type="button"
580
- onClick={() => {
581
- setActiveTool({ kind: "connect" });
582
- setConnectFromNodeId("");
583
- }}
584
- className={
585
- activeTool.kind === "connect"
586
- ? "rounded-[var(--ck-radius-sm)] bg-white/10 px-2 py-1 text-xs text-[color:var(--ck-text-primary)]"
587
- : "rounded-[var(--ck-radius-sm)] border border-white/10 bg-white/5 px-2 py-1 text-xs text-[color:var(--ck-text-secondary)] hover:bg-white/10"
588
- }
589
- title="Click a node, then click another node to create an edge"
590
- >
591
- Connect
592
- </button>
593
-
594
- {([
595
- { t: "llm", label: "LLM" },
596
- { t: "tool", label: "Tool" },
597
- { t: "condition", label: "If" },
598
- { t: "delay", label: "Delay" },
599
- { t: "human_approval", label: "Approve" },
600
- { t: "end", label: "End" },
601
- ] as Array<{ t: WorkflowFileV1["nodes"][number]["type"]; label: string }>).map((x) => (
602
- <button
603
- key={x.t}
604
- type="button"
605
- onClick={() => {
606
- setActiveTool({ kind: "add-node", nodeType: x.t });
607
- setConnectFromNodeId("");
608
- }}
609
- className={
610
- activeTool.kind === "add-node" && activeTool.nodeType === x.t
611
- ? "rounded-[var(--ck-radius-sm)] bg-white/10 px-2 py-1 text-xs text-[color:var(--ck-text-primary)]"
612
- : "rounded-[var(--ck-radius-sm)] border border-white/10 bg-white/5 px-2 py-1 text-xs text-[color:var(--ck-text-secondary)] hover:bg-white/10"
613
- }
614
- title="Select tool, then click on the canvas to place"
615
- >
616
- + {x.label}
617
- </button>
618
- ))}
619
- </div>
620
- )}
621
-
622
- {!toolsCollapsed && activeTool.kind === "connect" && connectFromNodeId ? (
623
- <div className="mt-2 text-xs text-[color:var(--ck-text-secondary)]">Connecting from: <span className="font-mono">{connectFromNodeId}</span></div>
624
- ) : null}
625
- {!toolsCollapsed && activeTool.kind === "add-node" ? (
626
- <div className="mt-2 text-xs text-[color:var(--ck-text-secondary)]">Click on the canvas to place a <span className="font-mono">{activeTool.nodeType}</span> node.</div>
627
- ) : null}
628
-
629
- <div className="mt-3 border-t border-white/10 pt-3">
630
- <div className={toolsCollapsed ? "hidden" : "flex items-center justify-between gap-2"}>
631
- <div className="text-[10px] font-medium uppercase tracking-wide text-[color:var(--ck-text-tertiary)]">Agents</div>
632
- <div className="text-[10px] text-[color:var(--ck-text-tertiary)]">drag → node</div>
633
- </div>
634
-
635
- {toolsCollapsed ? (
636
- <button
637
- type="button"
638
- onClick={() => setToolsCollapsed(false)}
639
- className="mt-2 flex h-8 w-8 items-center justify-center rounded-[var(--ck-radius-sm)] border border-white/10 bg-white/5 text-[color:var(--ck-text-secondary)] hover:bg-white/10"
640
- title="Expand to see agents"
641
- aria-label="Expand to see agents"
642
- >
643
- <svg viewBox="0 0 24 24" className="h-4 w-4" fill="none" aria-hidden="true">
644
- <path d="M16 11a4 4 0 1 0-8 0" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" />
645
- <path d="M4 20a8 8 0 0 1 16 0" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" />
646
- </svg>
647
- </button>
648
- ) : (
649
- <>
650
- {agentsError ? <div className="mt-1 text-[11px] text-red-200">{agentsError}</div> : null}
651
- <div className="mt-2 max-h-[140px] space-y-1 overflow-auto">
652
- {agents.length ? (
653
- agents.map((a) => (
654
- <div
655
- key={a.id}
656
- draggable
657
- onDragStart={(e) => {
658
- e.dataTransfer.setData("text/plain", a.id);
659
- e.dataTransfer.effectAllowed = "copy";
660
- }}
661
- className="cursor-grab rounded-[var(--ck-radius-sm)] border border-white/10 bg-white/5 px-2 py-1 text-xs text-[color:var(--ck-text-secondary)] hover:bg-white/10"
662
- title={a.id}
663
- >
664
- {a.identityName ? a.identityName : a.id.replace(`${teamId}-`, "")}
665
- </div>
666
- ))
667
- ) : (
668
- <div className="text-xs text-[color:var(--ck-text-tertiary)]">No team agents found.</div>
669
- )}
670
- </div>
671
- </>
672
- )}
673
- </div>
674
- </div>
675
-
676
- <svg className="absolute inset-0" width={2200} height={1200}>
677
- {(parsed.wf?.edges ?? []).map((e) => {
678
- const wf = parsed.wf;
679
- if (!wf) return null;
680
- const a = wf.nodes.find((n) => n.id === e.from);
681
- const b = wf.nodes.find((n) => n.id === e.to);
682
- if (!a || !b) return null;
683
- const ax = (typeof a.x === "number" ? a.x : 80) + 90;
684
- const ay = (typeof a.y === "number" ? a.y : 80) + 24;
685
- const bx = (typeof b.x === "number" ? b.x : 80) + 90;
686
- const by = (typeof b.y === "number" ? b.y : 80) + 24;
687
- return <line key={e.id} x1={ax} y1={ay} x2={bx} y2={by} stroke="rgba(255,255,255,0.18)" strokeWidth={2} />;
688
- })}
689
- </svg>
690
-
691
- {(parsed.wf?.nodes ?? []).map((n, idx) => {
692
- const x = typeof n.x === "number" ? n.x : 80 + idx * 220;
693
- const y = typeof n.y === "number" ? n.y : 80;
694
- const selected = selectedNodeId === n.id;
695
- return (
696
- <div
697
- key={n.id}
698
- role="button"
699
- tabIndex={0}
700
- data-wf-node="1"
701
- draggable={activeTool.kind === "select"}
702
- onDragStart={(e) => {
703
- // allow agent pills to be dropped; do not start a browser drag ghost for nodes.
704
- if (activeTool.kind !== "select") return;
705
- e.dataTransfer.setData("text/plain", "");
706
- }}
707
- onDragOver={(e) => {
708
- // Allow dropping agents.
709
- if (e.dataTransfer.types.includes("text/plain")) e.preventDefault();
710
- }}
711
- onDrop={(e) => {
712
- const wf = parsed.wf;
713
- if (!wf) return;
714
- e.preventDefault();
715
- e.stopPropagation();
716
- const agentId = String(e.dataTransfer.getData("text/plain") || "").trim();
717
- if (!agentId) return;
718
-
719
- const nextNodes = wf.nodes.map((node) => {
720
- if (node.id !== n.id) return node;
721
- const cfg = node.config && typeof node.config === "object" && !Array.isArray(node.config) ? node.config : {};
722
- return { ...node, config: { ...cfg, agentId } };
723
- });
724
- setWorkflow({ ...wf, nodes: nextNodes });
725
- setSelectedNodeId(n.id);
726
- }}
727
- onClick={(e) => {
728
- e.stopPropagation();
729
- const wf = parsed.wf;
730
- if (activeTool.kind === "connect") {
731
- if (!wf) return;
732
- if (!connectFromNodeId) {
733
- setConnectFromNodeId(n.id);
734
- setSelectedNodeId(n.id);
735
- return;
736
- }
737
- const from = connectFromNodeId;
738
- const to = n.id;
739
- setConnectFromNodeId("");
740
- if (!from || !to || from === to) return;
741
- const exists = (wf.edges ?? []).some((e) => e.from === from && e.to === to);
742
- if (exists) return;
743
- const id = `e${Date.now()}`;
744
- const nextEdge: WorkflowFileV1["edges"][number] = { id, from, to };
745
- setWorkflow({ ...wf, edges: [...(wf.edges ?? []), nextEdge] });
746
- return;
747
- }
748
-
749
- setSelectedNodeId(n.id);
750
- }}
751
- onPointerDown={(e) => {
752
- if (activeTool.kind !== "select") return;
753
- if (e.button !== 0) return;
754
- const el = canvasRef.current;
755
- if (!el) return;
756
- const rect = el.getBoundingClientRect();
757
- setSelectedNodeId(n.id);
758
- try {
759
- e.currentTarget.setPointerCapture(e.pointerId);
760
- } catch {
761
- // ignore
762
- }
763
- e.preventDefault();
764
- setDragging({ nodeId: n.id, dx: e.clientX - rect.left - x, dy: e.clientY - rect.top - y, left: rect.left, top: rect.top });
765
- }}
766
- onPointerUp={(e) => {
767
- try {
768
- e.currentTarget.releasePointerCapture(e.pointerId);
769
- } catch {
770
- // ignore
771
- }
772
- setDragging(null);
773
- }}
774
- onPointerMove={(e) => {
775
- if (!dragging) return;
776
- if (dragging.nodeId !== n.id) return;
777
- const wf = parsed.wf;
778
- if (!wf) return;
779
- const el = canvasRef.current;
780
- if (!el) return;
781
- const nextX = e.clientX - dragging.left - dragging.dx;
782
- const nextY = e.clientY - dragging.top - dragging.dy;
783
- const nextNodes = wf.nodes.map((node) => (node.id === n.id ? { ...node, x: nextX, y: nextY } : node));
784
- const next: WorkflowFileV1 = { ...wf, nodes: nextNodes };
785
- setStatus({ kind: "ready", jsonText: JSON.stringify(next, null, 2) + "\n" });
786
- }}
787
- className={
788
- selected
789
- ? "absolute cursor-grab rounded-[var(--ck-radius-sm)] border border-white/25 bg-white/10 px-3 py-2 text-xs text-[color:var(--ck-text-primary)] shadow-[var(--ck-shadow-1)]"
790
- : "absolute cursor-grab rounded-[var(--ck-radius-sm)] border border-white/10 bg-white/5 px-3 py-2 text-xs text-[color:var(--ck-text-secondary)] hover:bg-white/10"
791
- }
792
- style={{ left: x, top: y, width: 180 }}
793
- >
794
- <div className="font-medium text-[color:var(--ck-text-primary)]">{n.name || n.id}</div>
795
- <div className="mt-0.5 text-[10px] uppercase tracking-wide text-[color:var(--ck-text-tertiary)]">{n.type}</div>
796
- {(() => {
797
- const cfg = n.config && typeof n.config === "object" && !Array.isArray(n.config) ? (n.config as Record<string, unknown>) : null;
798
- const agentId = cfg ? String(cfg.agentId ?? "").trim() : "";
799
- if (!agentId) return null;
800
- const short = agentId.replace(`${teamId}-`, "");
801
- return <div className="mt-1 text-[10px] text-[color:var(--ck-text-secondary)]">Agent: {short}</div>;
802
- })()}
803
- </div>
804
- );
805
- })}
806
-
807
- {/* Inline in-canvas node inspector (requirement #6) */}
808
- {(() => {
809
- const wf = parsed.wf;
810
- if (!wf) return null;
811
- if (!selectedNodeId) return null;
812
- const node = wf.nodes.find((n) => n.id === selectedNodeId);
813
- if (!node) return null;
814
-
815
- const x = typeof node.x === "number" ? node.x : 80;
816
- const y = typeof node.y === "number" ? node.y : 80;
817
- const cfg = node.config && typeof node.config === "object" && !Array.isArray(node.config) ? (node.config as Record<string, unknown>) : {};
818
- const agentId = String(cfg.agentId ?? "").trim();
819
-
820
- return (
821
- <div
822
- className="absolute z-10 w-[320px] rounded-[var(--ck-radius-sm)] border border-white/15 bg-black/60 p-3 shadow-[var(--ck-shadow-1)] backdrop-blur"
823
- style={{ left: x + 200, top: y }}
824
- >
825
- <div className="flex items-center justify-between gap-2">
826
- <div className="text-xs font-medium text-[color:var(--ck-text-primary)]">{node.name || node.id}</div>
827
- <button
828
- type="button"
829
- onClick={() => setSelectedNodeId("")}
830
- className="text-[10px] text-[color:var(--ck-text-tertiary)] hover:text-[color:var(--ck-text-primary)]"
831
- >
832
- Close
833
- </button>
834
- </div>
835
-
836
- <div className="mt-2 space-y-2">
837
- <label className="block">
838
- <div className="text-[10px] uppercase tracking-wide text-[color:var(--ck-text-tertiary)]">name</div>
839
- <input
840
- value={String(node.name ?? "")}
841
- onChange={(e) => {
842
- const nextName = e.target.value;
843
- setWorkflow({ ...wf, nodes: wf.nodes.map((n) => (n.id === node.id ? { ...n, name: nextName } : n)) });
844
- }}
845
- className="mt-1 w-full rounded-[var(--ck-radius-sm)] border border-white/10 bg-black/30 px-2 py-1 text-xs text-[color:var(--ck-text-primary)]"
846
- placeholder="Optional"
847
- />
848
- </label>
849
-
850
- <label className="block">
851
- <div className="text-[10px] uppercase tracking-wide text-[color:var(--ck-text-tertiary)]">type</div>
852
- <select
853
- value={node.type}
854
- onChange={(e) => {
855
- const nextType = e.target.value as WorkflowFileV1["nodes"][number]["type"];
856
- setWorkflow({ ...wf, nodes: wf.nodes.map((n) => (n.id === node.id ? { ...n, type: nextType } : n)) });
857
- }}
858
- className="mt-1 w-full rounded-[var(--ck-radius-sm)] border border-white/10 bg-black/30 px-2 py-1 text-xs text-[color:var(--ck-text-primary)]"
859
- >
860
- <option value="start">start</option>
861
- <option value="end">end</option>
862
- <option value="llm">llm</option>
863
- <option value="tool">tool</option>
864
- <option value="condition">condition</option>
865
- <option value="delay">delay</option>
866
- <option value="human_approval">human_approval</option>
867
- </select>
868
- </label>
869
-
870
- <label className="block">
871
- <div className="text-[10px] uppercase tracking-wide text-[color:var(--ck-text-tertiary)]">agentId</div>
872
- <input
873
- value={agentId}
874
- onChange={(e) => {
875
- const nextAgentId = String(e.target.value || "").trim();
876
- const nextCfg = { ...cfg, ...(nextAgentId ? { agentId: nextAgentId } : {}) };
877
- if (!nextAgentId) delete (nextCfg as Record<string, unknown>).agentId;
878
- setWorkflow({ ...wf, nodes: wf.nodes.map((n) => (n.id === node.id ? { ...n, config: nextCfg } : n)) });
879
- }}
880
- className="mt-1 w-full rounded-[var(--ck-radius-sm)] border border-white/10 bg-black/30 px-2 py-1 text-xs text-[color:var(--ck-text-primary)]"
881
- placeholder="(drag an agent onto the node or type)"
882
- />
883
- </label>
884
-
885
- {node.type === "human_approval" ? (
886
- <div className="rounded-[var(--ck-radius-sm)] border border-white/10 bg-black/20 p-2">
887
- <div className="text-[10px] uppercase tracking-wide text-[color:var(--ck-text-tertiary)]">approval config</div>
888
-
889
- <div className="mt-2 space-y-2">
890
- <label className="block">
891
- <div className="text-[10px] uppercase tracking-wide text-[color:var(--ck-text-tertiary)]">provider</div>
892
- <input
893
- value={String((cfg as Record<string, unknown>).provider ?? "")}
894
- onChange={(e) => {
895
- const v = String(e.target.value || "").trim();
896
- const nextCfg = { ...cfg, ...(v ? { provider: v } : {}) };
897
- if (!v) delete (nextCfg as Record<string, unknown>).provider;
898
- setWorkflow({ ...wf, nodes: wf.nodes.map((n) => (n.id === node.id ? { ...n, config: nextCfg } : n)) });
899
- }}
900
- className="mt-1 w-full rounded-[var(--ck-radius-sm)] border border-white/10 bg-black/25 px-2 py-1 text-xs text-[color:var(--ck-text-primary)]"
901
- placeholder="telegram"
902
- />
903
- </label>
904
-
905
- <label className="block">
906
- <div className="text-[10px] uppercase tracking-wide text-[color:var(--ck-text-tertiary)]">target</div>
907
- <input
908
- value={String((cfg as Record<string, unknown>).target ?? "")}
909
- onChange={(e) => {
910
- const v = String(e.target.value || "").trim();
911
- const nextCfg = { ...cfg, ...(v ? { target: v } : {}) };
912
- if (!v) delete (nextCfg as Record<string, unknown>).target;
913
- setWorkflow({ ...wf, nodes: wf.nodes.map((n) => (n.id === node.id ? { ...n, config: nextCfg } : n)) });
914
- }}
915
- className="mt-1 w-full rounded-[var(--ck-radius-sm)] border border-white/10 bg-black/25 px-2 py-1 text-xs text-[color:var(--ck-text-primary)]"
916
- placeholder="(e.g. Telegram chat id)"
917
- />
918
- <div className="mt-1 text-[10px] text-[color:var(--ck-text-tertiary)]">Overrides workflow-level default when set.</div>
919
- </label>
920
-
921
- <label className="block">
922
- <div className="text-[10px] uppercase tracking-wide text-[color:var(--ck-text-tertiary)]">messageTemplate (optional)</div>
923
- <textarea
924
- value={String((cfg as Record<string, unknown>).messageTemplate ?? "")}
925
- onChange={(e) => {
926
- const v = String(e.target.value || "");
927
- const nextCfg = { ...cfg, ...(v.trim() ? { messageTemplate: v } : {}) };
928
- if (!v.trim()) delete (nextCfg as Record<string, unknown>).messageTemplate;
929
- setWorkflow({ ...wf, nodes: wf.nodes.map((n) => (n.id === node.id ? { ...n, config: nextCfg } : n)) });
930
- }}
931
- className="mt-1 h-[70px] w-full resize-none rounded-[var(--ck-radius-sm)] border border-white/10 bg-black/25 p-2 font-mono text-[10px] text-[color:var(--ck-text-primary)]"
932
- placeholder="Approval needed for {{workflowName}} (run {{runId}})"
933
- spellCheck={false}
934
- />
935
- <div className="mt-1 text-[10px] text-[color:var(--ck-text-tertiary)]">
936
- Vars: {"{{workflowName}}"}, {"{{workflowId}}"}, {"{{runId}}"}, {"{{nodeId}}"}
937
- </div>
938
- </label>
939
- </div>
940
- </div>
941
- ) : null}
942
-
943
- <label className="block">
944
- <div className="text-[10px] uppercase tracking-wide text-[color:var(--ck-text-tertiary)]">config (json)</div>
945
- <textarea
946
- value={JSON.stringify(cfg, null, 2)}
947
- onChange={(e) => {
948
- try {
949
- const nextCfg = JSON.parse(e.target.value) as Record<string, unknown>;
950
- if (!nextCfg || typeof nextCfg !== "object" || Array.isArray(nextCfg)) return;
951
- setWorkflow({ ...wf, nodes: wf.nodes.map((n) => (n.id === node.id ? { ...n, config: nextCfg } : n)) });
952
- } catch {
953
- // ignore invalid JSON while typing
954
- }
955
- }}
956
- className="mt-1 h-[140px] w-full resize-none rounded-[var(--ck-radius-sm)] border border-white/10 bg-black/30 p-2 font-mono text-[10px] text-[color:var(--ck-text-primary)]"
957
- spellCheck={false}
958
- />
959
- <div className="mt-1 text-[10px] text-[color:var(--ck-text-tertiary)]">(Edits apply when JSON is valid.)</div>
960
- </label>
961
- </div>
962
- </div>
963
- );
964
- })()}
965
- </div>
966
- </div>
967
- )}
968
-
969
- <div className="w-[380px] shrink-0 overflow-auto p-3 text-sm">
970
- <div className="space-y-3">
971
- {parsed.wf ? (
972
- (() => {
973
- const wf = parsed.wf;
974
- const tz = String(wf.timezone ?? "").trim() || "UTC";
975
- const triggers = wf.triggers ?? [];
976
-
977
- const meta = wf.meta && typeof wf.meta === "object" && !Array.isArray(wf.meta) ? (wf.meta as Record<string, unknown>) : {};
978
- const approvalProvider = String(meta.approvalProvider ?? "telegram").trim() || "telegram";
979
- const approvalTarget = String(meta.approvalTarget ?? "").trim();
980
-
981
- // Cron schedule suggestions.
982
- // Note: dev-team automation defaults should avoid the 02:00–07:00 America/New_York blackout window.
983
- // We keep presets in "safe" hours by default.
984
- const presets = [
985
- { label: "(no preset)", expr: "" },
986
- { label: "Weekdays 09:00 local", expr: "0 9 * * 1-5" },
987
- { label: "Mon/Wed/Fri 09:00 local", expr: "0 9 * * 1,3,5" },
988
- { label: "Daily 08:00 local", expr: "0 8 * * *" },
989
- { label: "Daily 12:00 local", expr: "0 12 * * *" },
990
- { label: "Mon 09:30 local", expr: "30 9 * * 1" },
991
- ];
992
-
993
- return (
994
- <div className="space-y-3">
995
- <details open className="rounded-[var(--ck-radius-sm)] border border-white/10 bg-black/15">
996
- <summary className="cursor-pointer list-none px-3 py-2 text-sm font-medium text-[color:var(--ck-text-primary)]">Workflow</summary>
997
- <div className="px-3 pb-3">
998
- <label className="block">
999
- <div className="text-[11px] font-medium text-[color:var(--ck-text-tertiary)]">Timezone</div>
1000
- <input
1001
- value={tz}
1002
- onChange={(e) => {
1003
- const nextTz = String(e.target.value || "").trim() || "UTC";
1004
- setWorkflow({ ...wf, timezone: nextTz });
1005
- }}
1006
- className="mt-1 w-full rounded-[var(--ck-radius-sm)] border border-white/10 bg-black/25 px-2 py-1 text-sm text-[color:var(--ck-text-primary)]"
1007
- placeholder="America/New_York"
1008
- />
1009
- </label>
1010
- </div>
1011
- </details>
1012
-
1013
- <details open className="rounded-[var(--ck-radius-sm)] border border-white/10 bg-black/15">
1014
- <summary className="cursor-pointer list-none px-3 py-2 text-sm font-medium text-[color:var(--ck-text-primary)]">Approval Channel</summary>
1015
- <div className="px-3 pb-3 space-y-2">
1016
- <label className="block">
1017
- <div className="text-[10px] uppercase tracking-wide text-[color:var(--ck-text-tertiary)]">provider</div>
1018
- <input
1019
- value={approvalProvider}
1020
- onChange={(e) => {
1021
- const nextProvider = String(e.target.value || "").trim() || "telegram";
1022
- setWorkflow({ ...wf, meta: { ...meta, approvalProvider: nextProvider } });
1023
- }}
1024
- className="mt-1 w-full rounded-[var(--ck-radius-sm)] border border-white/10 bg-black/25 px-2 py-1 text-xs text-[color:var(--ck-text-primary)]"
1025
- placeholder="telegram"
1026
- />
1027
- </label>
1028
-
1029
- <label className="block">
1030
- <div className="text-[10px] uppercase tracking-wide text-[color:var(--ck-text-tertiary)]">target</div>
1031
- <input
1032
- value={approvalTarget}
1033
- onChange={(e) => {
1034
- const nextTarget = String(e.target.value || "").trim();
1035
- setWorkflow({ ...wf, meta: { ...meta, approvalTarget: nextTarget } });
1036
- }}
1037
- className="mt-1 w-full rounded-[var(--ck-radius-sm)] border border-white/10 bg-black/25 px-2 py-1 text-xs text-[color:var(--ck-text-primary)]"
1038
- placeholder="(e.g. Telegram chat id)"
1039
- />
1040
- <div className="mt-1 text-[10px] text-[color:var(--ck-text-tertiary)]">
1041
- If set, runs that reach a human-approval node can send an approval packet via the gateway message tool.
1042
- </div>
1043
- </label>
1044
- </div>
1045
- </details>
1046
-
1047
- <details open className="rounded-[var(--ck-radius-sm)] border border-white/10 bg-black/15">
1048
- <summary className="cursor-pointer list-none px-3 py-2 text-sm font-medium text-[color:var(--ck-text-primary)]">Triggers</summary>
1049
- <div className="px-3 pb-3">
1050
- <div className="flex items-center justify-between gap-2">
1051
- <div className="text-[10px] uppercase tracking-wide text-[color:var(--ck-text-tertiary)]">triggers</div>
1052
- <button
1053
- type="button"
1054
- onClick={() => {
1055
- const id = `t${Date.now()}`;
1056
- setWorkflow({
1057
- ...wf,
1058
- triggers: [
1059
- ...triggers,
1060
- { kind: "cron", id, name: "New trigger", enabled: true, expr: "0 9 * * 1-5", tz },
1061
- ],
1062
- });
1063
- }}
1064
- className="rounded-[var(--ck-radius-sm)] border border-white/10 bg-white/5 px-2 py-1 text-[10px] font-medium text-[color:var(--ck-text-primary)] hover:bg-white/10"
1065
- >
1066
- + Add
1067
- </button>
1068
- </div>
1069
-
1070
- <div className="mt-2 space-y-2">
1071
- {triggers.length ? (
1072
- triggers.map((t, i) => {
1073
- const kind = (t as { kind?: unknown }).kind;
1074
- const isCron = kind === "cron";
1075
- const id = String((t as { id?: unknown }).id ?? "");
1076
- const name = String((t as { name?: unknown }).name ?? "");
1077
- const enabled = Boolean((t as { enabled?: unknown }).enabled);
1078
- const expr = String((t as { expr?: unknown }).expr ?? "");
1079
- const trigTz = String((t as { tz?: unknown }).tz ?? tz);
1080
- const cronFields = expr.trim().split(/\s+/).filter(Boolean);
1081
- const cronLooksValid = !expr.trim() || cronFields.length === 5;
1082
-
1083
- return (
1084
- <div key={`${id}-${i}`} className="rounded-[var(--ck-radius-sm)] border border-white/10 bg-black/25 p-2">
1085
- <div className="flex items-center justify-between gap-2">
1086
- <div className="text-xs text-[color:var(--ck-text-primary)]">{name || id || `trigger-${i + 1}`}</div>
1087
- <button
1088
- type="button"
1089
- onClick={() => setWorkflow({ ...wf, triggers: triggers.filter((_, idx) => idx !== i) })}
1090
- className="text-[10px] text-[color:var(--ck-text-tertiary)] hover:text-[color:var(--ck-text-primary)]"
1091
- >
1092
- Remove
1093
- </button>
1094
- </div>
1095
-
1096
- {!isCron ? (
1097
- <div className="mt-1 text-xs text-[color:var(--ck-text-secondary)]">Unsupported trigger kind: {String(kind)}</div>
1098
- ) : null}
1099
-
1100
- <div className="mt-2 grid grid-cols-1 gap-2">
1101
- <label className="flex items-center gap-2 text-xs text-[color:var(--ck-text-secondary)]">
1102
- <input
1103
- type="checkbox"
1104
- checked={enabled}
1105
- onChange={(e) => {
1106
- const nextEnabled = e.target.checked;
1107
- setWorkflow({
1108
- ...wf,
1109
- triggers: triggers.map((x, idx) => (idx === i && x.kind === "cron" ? { ...x, enabled: nextEnabled } : x)),
1110
- });
1111
- }}
1112
- />
1113
- Enabled
1114
- </label>
1115
-
1116
- <label className="block">
1117
- <div className="text-[10px] uppercase tracking-wide text-[color:var(--ck-text-tertiary)]">name</div>
1118
- <input
1119
- value={name}
1120
- onChange={(e) => {
1121
- const nextName = e.target.value;
1122
- setWorkflow({
1123
- ...wf,
1124
- triggers: triggers.map((x, idx) => (idx === i && x.kind === "cron" ? { ...x, name: nextName } : x)),
1125
- });
1126
- }}
1127
- className="mt-1 w-full rounded-[var(--ck-radius-sm)] border border-white/10 bg-black/25 px-2 py-1 text-xs text-[color:var(--ck-text-primary)]"
1128
- placeholder="Content cadence"
1129
- />
1130
- </label>
1131
-
1132
- <label className="block">
1133
- <div className="text-[10px] uppercase tracking-wide text-[color:var(--ck-text-tertiary)]">schedule (cron)</div>
1134
- <input
1135
- value={expr}
1136
- onChange={(e) => {
1137
- const nextExpr = e.target.value;
1138
- setWorkflow({
1139
- ...wf,
1140
- triggers: triggers.map((x, idx) => (idx === i && x.kind === "cron" ? { ...x, expr: nextExpr } : x)),
1141
- });
1142
- }}
1143
- className={
1144
- cronLooksValid
1145
- ? "mt-1 w-full rounded-[var(--ck-radius-sm)] border border-white/10 bg-black/25 px-2 py-1 font-mono text-[11px] text-[color:var(--ck-text-primary)]"
1146
- : "mt-1 w-full rounded-[var(--ck-radius-sm)] border border-red-400/50 bg-black/25 px-2 py-1 font-mono text-[11px] text-[color:var(--ck-text-primary)]"
1147
- }
1148
- placeholder="0 9 * * 1,3,5"
1149
- />
1150
- {!cronLooksValid ? (
1151
- <div className="mt-1 text-[10px] text-red-200">
1152
- Cron should be 5 fields (min hour dom month dow). You entered {cronFields.length}.
1153
- </div>
1154
- ) : null}
1155
- <div className="mt-1 grid grid-cols-1 gap-1">
1156
- <select
1157
- value={presets.some((p) => p.expr === expr) ? expr : ""}
1158
- onChange={(e) => {
1159
- const nextExpr = e.target.value;
1160
- setWorkflow({
1161
- ...wf,
1162
- triggers: triggers.map((x, idx) => (idx === i && x.kind === "cron" ? { ...x, expr: nextExpr } : x)),
1163
- });
1164
- }}
1165
- className="w-full rounded-[var(--ck-radius-sm)] border border-white/10 bg-black/25 px-2 py-1 text-[11px] text-[color:var(--ck-text-secondary)]"
1166
- >
1167
- {presets.map((p) => (
1168
- <option key={p.label} value={p.expr}>
1169
- {p.label}
1170
- </option>
1171
- ))}
1172
- </select>
1173
- <div className="text-[10px] text-[color:var(--ck-text-tertiary)]">Presets set the cron; edit freely for advanced schedules.</div>
1174
- </div>
1175
- </label>
1176
-
1177
- <label className="block">
1178
- <div className="text-[10px] uppercase tracking-wide text-[color:var(--ck-text-tertiary)]">timezone override</div>
1179
- <input
1180
- value={trigTz}
1181
- onChange={(e) => {
1182
- const nextTz = String(e.target.value || "").trim() || tz;
1183
- setWorkflow({
1184
- ...wf,
1185
- triggers: triggers.map((x, idx) => (idx === i && x.kind === "cron" ? { ...x, tz: nextTz } : x)),
1186
- });
1187
- }}
1188
- className="mt-1 w-full rounded-[var(--ck-radius-sm)] border border-white/10 bg-black/25 px-2 py-1 text-xs text-[color:var(--ck-text-primary)]"
1189
- placeholder={tz}
1190
- />
1191
- </label>
1192
- </div>
1193
- </div>
1194
- );
1195
- })
1196
- ) : (
1197
- <div className="text-xs text-[color:var(--ck-text-secondary)]">No triggers yet.</div>
1198
- )}
1199
- </div>
1200
- </div>
1201
- </details>
1202
-
1203
- <details open className="rounded-[var(--ck-radius-sm)] border border-white/10 bg-black/15">
1204
- <summary className="cursor-pointer list-none px-3 py-2 text-sm font-medium text-[color:var(--ck-text-primary)]">Runs</summary>
1205
- <div className="px-3 pb-3">
1206
- <div className="flex items-center justify-between gap-2">
1207
- <div className="text-xs font-medium text-[color:var(--ck-text-secondary)]">Runs (history)</div>
1208
- <button
1209
- type="button"
1210
- disabled={saving}
1211
- onClick={async () => {
1212
- const wfId = String(wf.id ?? "").trim();
1213
- if (!wfId) return;
1214
- setWorkflowRunsError("");
1215
- setWorkflowRunsLoading(true);
1216
- try {
1217
- const res = await fetch("/api/teams/workflow-runs", {
1218
- method: "POST",
1219
- headers: { "content-type": "application/json" },
1220
- body: JSON.stringify({ teamId, workflowId: wfId, mode: "sample" }),
1221
- });
1222
- const json = await res.json();
1223
- if (!res.ok || !json.ok) throw new Error(json.error || "Failed to create sample run");
1224
-
1225
- const listRes = await fetch(
1226
- `/api/teams/workflow-runs?teamId=${encodeURIComponent(teamId)}&workflowId=${encodeURIComponent(wfId)}`,
1227
- { cache: "no-store" }
1228
- );
1229
- const listJson = await listRes.json();
1230
- if (!listRes.ok || !listJson.ok) throw new Error(listJson.error || "Failed to refresh runs");
1231
- const files = Array.isArray(listJson.files) ? listJson.files : [];
1232
- const list = files.map((f: unknown) => String(f ?? "").trim()).filter((f: string) => Boolean(f));
1233
- setWorkflowRuns(list);
1234
- } catch (e: unknown) {
1235
- setWorkflowRunsError(e instanceof Error ? e.message : String(e));
1236
- } finally {
1237
- setWorkflowRunsLoading(false);
1238
- }
1239
- }}
1240
- className="rounded-[var(--ck-radius-sm)] border border-white/10 bg-white/5 px-2 py-1 text-[10px] font-medium text-[color:var(--ck-text-primary)] hover:bg-white/10 disabled:opacity-50"
1241
- >
1242
- + Sample run
1243
- </button>
1244
- </div>
1245
-
1246
- {workflowRunsError ? (
1247
- <div className="mt-2 rounded-[var(--ck-radius-sm)] border border-red-400/30 bg-red-500/10 p-2 text-xs text-red-100">
1248
- {workflowRunsError}
1249
- </div>
1250
- ) : null}
1251
-
1252
- <div className="mt-2 space-y-1">
1253
- {workflowRunsLoading ? (
1254
- <div className="text-xs text-[color:var(--ck-text-secondary)]">Loading runs…</div>
1255
- ) : workflowRuns.length ? (
1256
- workflowRuns.slice(0, 8).map((f) => {
1257
- const runId = String(f).replace(/\.run\.json$/i, "");
1258
- const selected = selectedWorkflowRunId === runId;
1259
- return (
1260
- <button
1261
- key={f}
1262
- type="button"
1263
- onClick={async () => {
1264
- const wfId = String(wf.id ?? "").trim();
1265
- if (!wfId) return;
1266
- setSelectedWorkflowRunId(runId);
1267
- setWorkflowRunsError("");
1268
- try {
1269
- const res = await fetch(
1270
- `/api/teams/workflow-runs?teamId=${encodeURIComponent(teamId)}&workflowId=${encodeURIComponent(wfId)}&runId=${encodeURIComponent(runId)}`,
1271
- { cache: "no-store" }
1272
- );
1273
- const json = await res.json();
1274
- if (!res.ok || !json.ok) throw new Error(json.error || "Failed to load run");
1275
- // (run detail rendering not implemented yet; selecting stores runId only)
1276
- } catch (e: unknown) {
1277
- setWorkflowRunsError(e instanceof Error ? e.message : String(e));
1278
- }
1279
- }}
1280
- className={
1281
- selected
1282
- ? "w-full rounded-[var(--ck-radius-sm)] bg-white/10 px-2 py-1 text-left text-[11px] text-[color:var(--ck-text-primary)]"
1283
- : "w-full rounded-[var(--ck-radius-sm)] px-2 py-1 text-left text-[11px] text-[color:var(--ck-text-secondary)] hover:bg-white/5"
1284
- }
1285
- >
1286
- {runId}
1287
- </button>
1288
- );
1289
- })
1290
- ) : (
1291
- <div className="text-xs text-[color:var(--ck-text-secondary)]">No runs yet.</div>
1292
- )}
1293
- </div>
1294
-
1295
- <details open className="mt-3 rounded-[var(--ck-radius-sm)] border border-white/10 bg-black/10">
1296
- <summary className="cursor-pointer list-none px-3 py-2 text-sm font-medium text-[color:var(--ck-text-primary)]">Nodes</summary>
1297
- <div className="px-3 pb-3">
1298
-
1299
- <div className="mt-2 rounded-[var(--ck-radius-sm)] border border-white/10 bg-black/25 p-2">
1300
- <div className="grid grid-cols-1 gap-2">
1301
- <label className="block">
1302
- <div className="text-[10px] uppercase tracking-wide text-[color:var(--ck-text-tertiary)]">id</div>
1303
- <input
1304
- value={newNodeId}
1305
- onChange={(e) => setNewNodeId(e.target.value)}
1306
- className="mt-1 w-full rounded-[var(--ck-radius-sm)] border border-white/10 bg-black/25 px-2 py-1 text-xs text-[color:var(--ck-text-primary)]"
1307
- placeholder="e.g. draft_assets"
1308
- />
1309
- </label>
1310
-
1311
- <label className="block">
1312
- <div className="text-[10px] uppercase tracking-wide text-[color:var(--ck-text-tertiary)]">name (optional)</div>
1313
- <input
1314
- value={newNodeName}
1315
- onChange={(e) => setNewNodeName(e.target.value)}
1316
- className="mt-1 w-full rounded-[var(--ck-radius-sm)] border border-white/10 bg-black/25 px-2 py-1 text-xs text-[color:var(--ck-text-primary)]"
1317
- placeholder="Human-friendly label"
1318
- />
1319
- </label>
1320
-
1321
- <label className="block">
1322
- <div className="text-[10px] uppercase tracking-wide text-[color:var(--ck-text-tertiary)]">type</div>
1323
- <select
1324
- value={newNodeType}
1325
- onChange={(e) => setNewNodeType(e.target.value as WorkflowFileV1["nodes"][number]["type"])}
1326
- className="mt-1 w-full rounded-[var(--ck-radius-sm)] border border-white/10 bg-black/25 px-2 py-1 text-xs text-[color:var(--ck-text-primary)]"
1327
- >
1328
- <option value="start">start</option>
1329
- <option value="end">end</option>
1330
- <option value="llm">llm</option>
1331
- <option value="tool">tool</option>
1332
- <option value="condition">condition</option>
1333
- <option value="delay">delay</option>
1334
- <option value="human_approval">human_approval</option>
1335
- </select>
1336
- </label>
1337
-
1338
- <button
1339
- type="button"
1340
- onClick={() => {
1341
- const rawId = String(newNodeId || "").trim();
1342
- const id = rawId.replace(/[^a-z0-9_\-]/gi, "_");
1343
- if (!id) return;
1344
- if (wf.nodes.some((n) => n.id === id)) return;
1345
-
1346
- const maxX = wf.nodes.reduce((acc, n) => (typeof n.x === "number" ? Math.max(acc, n.x) : acc), 80);
1347
- const nextNode = {
1348
- id,
1349
- type: newNodeType,
1350
- name: String(newNodeName || "").trim() || id,
1351
- x: maxX + 220,
1352
- y: 80,
1353
- } as WorkflowFileV1["nodes"][number];
1354
-
1355
- setWorkflow({ ...wf, nodes: [...wf.nodes, nextNode] });
1356
- setSelectedNodeId(id);
1357
- setNewNodeId("");
1358
- setNewNodeName("");
1359
- }}
1360
- className="rounded-[var(--ck-radius-sm)] border border-white/10 bg-white/5 px-2 py-1 text-[11px] font-medium text-[color:var(--ck-text-primary)] hover:bg-white/10"
1361
- >
1362
- + Add node
1363
- </button>
1364
- </div>
1365
- </div>
1366
-
1367
- <div className="mt-2 space-y-1">
1368
- {wf.nodes.map((n) => {
1369
- const selected = selectedNodeId === n.id;
1370
- return (
1371
- <button
1372
- key={n.id}
1373
- type="button"
1374
- onClick={() => setSelectedNodeId(n.id)}
1375
- className={
1376
- selected
1377
- ? "w-full rounded-[var(--ck-radius-sm)] bg-white/10 px-2 py-1 text-left text-[11px] text-[color:var(--ck-text-primary)]"
1378
- : "w-full rounded-[var(--ck-radius-sm)] px-2 py-1 text-left text-[11px] text-[color:var(--ck-text-secondary)] hover:bg-white/5"
1379
- }
1380
- >
1381
- <span className="font-mono">{n.id}</span>
1382
- <span className="ml-2 text-[10px] uppercase tracking-wide text-[color:var(--ck-text-tertiary)]">{n.type}</span>
1383
- </button>
1384
- );
1385
- })}
1386
- </div>
1387
- </div>
1388
- </details>
1389
-
1390
- <details open className="mt-3 rounded-[var(--ck-radius-sm)] border border-white/10 bg-black/10">
1391
- <summary className="cursor-pointer list-none px-3 py-2 text-sm font-medium text-[color:var(--ck-text-primary)]">Edges</summary>
1392
- <div className="px-3 pb-3">
1393
-
1394
- <div className="mt-2 rounded-[var(--ck-radius-sm)] border border-white/10 bg-black/25 p-2">
1395
- <div className="grid grid-cols-1 gap-2">
1396
- <label className="block">
1397
- <div className="text-[10px] uppercase tracking-wide text-[color:var(--ck-text-tertiary)]">from</div>
1398
- <select
1399
- value={newEdgeFrom}
1400
- onChange={(e) => setNewEdgeFrom(e.target.value)}
1401
- className="mt-1 w-full rounded-[var(--ck-radius-sm)] border border-white/10 bg-black/25 px-2 py-1 text-xs text-[color:var(--ck-text-primary)]"
1402
- >
1403
- <option value="">(select)</option>
1404
- {wf.nodes.map((n) => (
1405
- <option key={n.id} value={n.id}>
1406
- {n.id}
1407
- </option>
1408
- ))}
1409
- </select>
1410
- </label>
1411
-
1412
- <label className="block">
1413
- <div className="text-[10px] uppercase tracking-wide text-[color:var(--ck-text-tertiary)]">to</div>
1414
- <select
1415
- value={newEdgeTo}
1416
- onChange={(e) => setNewEdgeTo(e.target.value)}
1417
- className="mt-1 w-full rounded-[var(--ck-radius-sm)] border border-white/10 bg-black/25 px-2 py-1 text-xs text-[color:var(--ck-text-primary)]"
1418
- >
1419
- <option value="">(select)</option>
1420
- {wf.nodes.map((n) => (
1421
- <option key={n.id} value={n.id}>
1422
- {n.id}
1423
- </option>
1424
- ))}
1425
- </select>
1426
- </label>
1427
-
1428
- <label className="block">
1429
- <div className="text-[10px] uppercase tracking-wide text-[color:var(--ck-text-tertiary)]">label (optional)</div>
1430
- <input
1431
- value={newEdgeLabel}
1432
- onChange={(e) => setNewEdgeLabel(e.target.value)}
1433
- className="mt-1 w-full rounded-[var(--ck-radius-sm)] border border-white/10 bg-black/25 px-2 py-1 text-xs text-[color:var(--ck-text-primary)]"
1434
- placeholder="e.g. approve"
1435
- />
1436
- </label>
1437
-
1438
- <button
1439
- type="button"
1440
- onClick={() => {
1441
- const from = String(newEdgeFrom || "").trim();
1442
- const to = String(newEdgeTo || "").trim();
1443
- if (!from || !to) return;
1444
- if (from === to) return;
1445
- const id = `e${Date.now()}`;
1446
- const nextEdge: WorkflowFileV1["edges"][number] = {
1447
- id,
1448
- from,
1449
- to,
1450
- ...(String(newEdgeLabel || "").trim() ? { label: String(newEdgeLabel).trim() } : {}),
1451
- };
1452
- setWorkflow({ ...wf, edges: [...(wf.edges ?? []), nextEdge] });
1453
- setNewEdgeLabel("");
1454
- }}
1455
- className="rounded-[var(--ck-radius-sm)] border border-white/10 bg-white/5 px-2 py-1 text-[11px] font-medium text-[color:var(--ck-text-primary)] hover:bg-white/10"
1456
- >
1457
- + Add edge
1458
- </button>
1459
- </div>
1460
- </div>
1461
-
1462
- <div className="mt-2 space-y-2">
1463
- {(wf.edges ?? []).length ? (
1464
- (wf.edges ?? []).map((e) => (
1465
- <div key={e.id} className="rounded-[var(--ck-radius-sm)] border border-white/10 bg-black/25 p-2">
1466
- <div className="flex items-center justify-between gap-2">
1467
- <div className="text-[11px] text-[color:var(--ck-text-secondary)]">
1468
- <span className="font-mono">{e.from}</span> → <span className="font-mono">{e.to}</span>
1469
- {e.label ? <span className="ml-2 text-[10px] text-[color:var(--ck-text-tertiary)]">({e.label})</span> : null}
1470
- </div>
1471
- <button
1472
- type="button"
1473
- onClick={() => setWorkflow({ ...wf, edges: (wf.edges ?? []).filter((x) => x.id !== e.id) })}
1474
- className="text-[10px] text-[color:var(--ck-text-tertiary)] hover:text-[color:var(--ck-text-primary)]"
1475
- >
1476
- Remove
1477
- </button>
1478
- </div>
1479
- </div>
1480
- ))
1481
- ) : (
1482
- <div className="text-xs text-[color:var(--ck-text-secondary)]">No edges yet.</div>
1483
- )}
1484
- </div>
1485
- </div>
1486
- </details>
1487
-
1488
- <details open className="mt-3 rounded-[var(--ck-radius-sm)] border border-white/10 bg-black/10">
1489
- <summary className="cursor-pointer list-none px-3 py-2 text-sm font-medium text-[color:var(--ck-text-primary)]">Node inspector</summary>
1490
- <div className="px-3 pb-3">
1491
- <div className="flex items-center justify-between gap-2">
1492
- {selectedNodeId ? (
1493
- <button
1494
- type="button"
1495
- onClick={() => {
1496
- const nodeId = selectedNodeId;
1497
- const nextNodes = wf.nodes.filter((n) => n.id !== nodeId);
1498
- const nextEdges = (wf.edges ?? []).filter((e) => e.from !== nodeId && e.to !== nodeId);
1499
- setWorkflow({ ...wf, nodes: nextNodes, edges: nextEdges });
1500
- setSelectedNodeId("");
1501
- }}
1502
- className="rounded-[var(--ck-radius-sm)] border border-white/10 bg-white/5 px-2 py-1 text-[10px] font-medium text-red-100 hover:bg-white/10"
1503
- >
1504
- Delete node
1505
- </button>
1506
- ) : null}
1507
- </div>
1508
-
1509
- {selectedNodeId ? (
1510
- (() => {
1511
- const node = wf.nodes.find((n) => n.id === selectedNodeId);
1512
- if (!node) return <div className="mt-2 text-sm text-[color:var(--ck-text-secondary)]">No node selected.</div>;
1513
-
1514
- return (
1515
- <div className="mt-3 space-y-3">
1516
- <div>
1517
- <div className="text-[10px] uppercase tracking-wide text-[color:var(--ck-text-tertiary)]">id</div>
1518
- <div className="mt-1 rounded-[var(--ck-radius-sm)] border border-white/10 bg-white/5 px-2 py-1 text-xs text-[color:var(--ck-text-primary)]">
1519
- {node.id}
1520
- </div>
1521
- </div>
1522
-
1523
- <label className="block">
1524
- <div className="text-[10px] uppercase tracking-wide text-[color:var(--ck-text-tertiary)]">name</div>
1525
- <input
1526
- value={String(node.name ?? "")}
1527
- onChange={(e) => {
1528
- const nextName = e.target.value;
1529
- setWorkflow({ ...wf, nodes: wf.nodes.map((n) => (n.id === node.id ? { ...n, name: nextName } : n)) });
1530
- }}
1531
- className="mt-1 w-full rounded-[var(--ck-radius-sm)] border border-white/10 bg-black/25 px-2 py-1 text-xs text-[color:var(--ck-text-primary)]"
1532
- placeholder="Optional"
1533
- />
1534
- </label>
1535
-
1536
- <label className="block">
1537
- <div className="text-[10px] uppercase tracking-wide text-[color:var(--ck-text-tertiary)]">type</div>
1538
- <select
1539
- value={node.type}
1540
- onChange={(e) => {
1541
- const nextType = e.target.value as WorkflowFileV1["nodes"][number]["type"];
1542
- setWorkflow({ ...wf, nodes: wf.nodes.map((n) => (n.id === node.id ? { ...n, type: nextType } : n)) });
1543
- }}
1544
- className="mt-1 w-full rounded-[var(--ck-radius-sm)] border border-white/10 bg-black/25 px-2 py-1 text-xs text-[color:var(--ck-text-primary)]"
1545
- >
1546
- <option value="start">start</option>
1547
- <option value="end">end</option>
1548
- <option value="llm">llm</option>
1549
- <option value="tool">tool</option>
1550
- <option value="condition">condition</option>
1551
- <option value="delay">delay</option>
1552
- <option value="human_approval">human_approval</option>
1553
- </select>
1554
- </label>
1555
-
1556
- <div className="grid grid-cols-2 gap-2">
1557
- <label className="block">
1558
- <div className="text-[10px] uppercase tracking-wide text-[color:var(--ck-text-tertiary)]">x</div>
1559
- <input
1560
- type="number"
1561
- value={typeof node.x === "number" ? node.x : 0}
1562
- onChange={(e) => {
1563
- const nextX = Number(e.target.value);
1564
- setWorkflow({ ...wf, nodes: wf.nodes.map((n) => (n.id === node.id ? { ...n, x: nextX } : n)) });
1565
- }}
1566
- className="mt-1 w-full rounded-[var(--ck-radius-sm)] border border-white/10 bg-black/25 px-2 py-1 text-xs text-[color:var(--ck-text-primary)]"
1567
- />
1568
- </label>
1569
- <label className="block">
1570
- <div className="text-[10px] uppercase tracking-wide text-[color:var(--ck-text-tertiary)]">y</div>
1571
- <input
1572
- type="number"
1573
- value={typeof node.y === "number" ? node.y : 0}
1574
- onChange={(e) => {
1575
- const nextY = Number(e.target.value);
1576
- setWorkflow({ ...wf, nodes: wf.nodes.map((n) => (n.id === node.id ? { ...n, y: nextY } : n)) });
1577
- }}
1578
- className="mt-1 w-full rounded-[var(--ck-radius-sm)] border border-white/10 bg-black/25 px-2 py-1 text-xs text-[color:var(--ck-text-primary)]"
1579
- />
1580
- </label>
1581
- </div>
1582
-
1583
- <div>
1584
- <div className="text-[10px] uppercase tracking-wide text-[color:var(--ck-text-tertiary)]">config</div>
1585
- <pre className="mt-1 max-h-[200px] overflow-auto rounded-[var(--ck-radius-sm)] border border-white/10 bg-black/25 p-2 text-[10px] text-[color:var(--ck-text-secondary)]">
1586
- {JSON.stringify(node.config ?? {}, null, 2)}
1587
- </pre>
1588
- </div>
1589
- </div>
1590
- );
1591
- })()
1592
- ) : (
1593
- <div className="mt-2 text-sm text-[color:var(--ck-text-secondary)]">Select a node.</div>
1594
- )}
1595
- </div>
1596
- </details>
1597
- </div>
1598
- </details>
1599
- </div>
1600
- );
1601
- })()
1602
- ) : null}
1603
- </div>
1604
- </div>
1605
- </div>
1606
- </div>
1607
- );
1608
- }