@jiggai/kitchen 0.1.8 → 0.1.10-canary.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.next/BUILD_ID +1 -1
- package/.next/app-path-routes-manifest.json +5 -0
- package/.next/build-manifest.json +6 -6
- package/.next/prerender-manifest.json +3 -27
- package/.next/required-server-files.js +4 -4
- package/.next/required-server-files.json +4 -4
- package/.next/routes-manifest.json +30 -0
- package/.next/server/app/_global-error/page/build-manifest.json +4 -4
- package/.next/server/app/_global-error/page.js +2 -2
- package/.next/server/app/_global-error/page.js.nft.json +1 -1
- package/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
- package/.next/server/app/_global-error.html +2 -2
- package/.next/server/app/_global-error.rsc +1 -1
- package/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/_not-found/page/build-manifest.json +4 -4
- package/.next/server/app/_not-found/page.js +10 -9
- package/.next/server/app/_not-found/page.js.nft.json +1 -1
- package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next/server/app/_not-found.html +1 -1
- package/.next/server/app/_not-found.rsc +4 -4
- package/.next/server/app/_not-found.segments/_full.segment.rsc +4 -4
- package/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/.next/server/app/_not-found.segments/_index.segment.rsc +3 -3
- package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/agents/[agentId]/page/build-manifest.json +4 -4
- package/.next/server/app/agents/[agentId]/page.js +7 -7
- package/.next/server/app/agents/[agentId]/page.js.nft.json +1 -1
- package/.next/server/app/agents/[agentId]/page_client-reference-manifest.js +1 -1
- package/.next/server/app/api/agents/add/route.js +1 -1
- package/.next/server/app/api/agents/add/route.js.nft.json +1 -1
- package/.next/server/app/api/agents/files/route.js.nft.json +1 -1
- package/.next/server/app/api/agents/identity/route.js +1 -1
- package/.next/server/app/api/agents/identity/route.js.nft.json +1 -1
- package/.next/server/app/api/agents/route.js +1 -1
- package/.next/server/app/api/agents/route.js.nft.json +1 -1
- package/.next/server/app/api/gateway/restart/route.js +1 -1
- package/.next/server/app/api/gateway/restart/route.js.nft.json +1 -1
- package/.next/server/app/api/goals/[id]/promote/route.js.nft.json +1 -1
- package/.next/server/app/api/goals/[id]/route.js.nft.json +1 -1
- package/.next/server/app/api/goals/route.js.nft.json +1 -1
- package/.next/server/app/api/recipes/[id]/route.js.nft.json +1 -1
- package/.next/server/app/api/recipes/clone/route.js.nft.json +1 -1
- package/.next/server/app/api/recipes/delete/route.js.nft.json +1 -1
- package/.next/server/app/api/recipes/route.js +1 -1
- package/.next/server/app/api/recipes/route.js.nft.json +1 -1
- package/.next/server/app/api/recipes/team-agents/route.js.nft.json +1 -1
- package/.next/server/app/api/scaffold/route.js.nft.json +1 -1
- package/.next/server/app/api/swarms/start/route/app-paths-manifest.json +3 -0
- package/.next/server/app/api/swarms/start/route/build-manifest.json +11 -0
- package/.next/server/app/api/swarms/start/route/server-reference-manifest.json +4 -0
- package/.next/server/app/api/swarms/start/route.js +6 -0
- package/.next/server/app/api/swarms/start/route.js.map +5 -0
- package/.next/server/app/api/swarms/start/route.js.nft.json +1 -0
- package/.next/server/app/api/swarms/start/route_client-reference-manifest.js +2 -0
- package/.next/server/app/api/swarms/status/route/app-paths-manifest.json +3 -0
- package/.next/server/app/api/swarms/status/route/build-manifest.json +11 -0
- package/.next/server/app/api/swarms/status/route/server-reference-manifest.json +4 -0
- package/.next/server/app/api/swarms/status/route.js +6 -0
- package/.next/server/app/api/swarms/status/route.js.map +5 -0
- package/.next/server/app/api/swarms/status/route.js.nft.json +1 -0
- package/.next/server/app/api/swarms/status/route_client-reference-manifest.js +2 -0
- package/.next/server/app/api/teams/orchestrator/route/app-paths-manifest.json +3 -0
- package/.next/server/app/api/teams/orchestrator/route/build-manifest.json +11 -0
- package/.next/server/app/api/teams/orchestrator/route/server-reference-manifest.json +4 -0
- package/.next/server/app/api/teams/orchestrator/route.js +6 -0
- package/.next/server/app/api/teams/orchestrator/route.js.map +5 -0
- package/.next/server/app/api/teams/orchestrator/route.js.nft.json +1 -0
- package/.next/server/app/api/teams/orchestrator/route_client-reference-manifest.js +2 -0
- package/.next/server/app/api/teams/remove-team/route.js +1 -1
- package/.next/server/app/api/teams/remove-team/route.js.nft.json +1 -1
- package/.next/server/app/api/teams/skills/install/route.js +1 -1
- package/.next/server/app/api/teams/skills/install/route.js.nft.json +1 -1
- package/.next/server/app/api/teams/workflow-runs/route/app-paths-manifest.json +3 -0
- package/.next/server/app/api/teams/workflow-runs/route/build-manifest.json +11 -0
- package/.next/server/app/api/teams/workflow-runs/route/server-reference-manifest.json +4 -0
- package/.next/server/app/api/teams/workflow-runs/route.js +7 -0
- package/.next/server/app/api/teams/workflow-runs/route.js.map +5 -0
- package/.next/server/app/api/teams/workflow-runs/route.js.nft.json +1 -0
- package/.next/server/app/api/teams/workflow-runs/route_client-reference-manifest.js +2 -0
- package/.next/server/app/api/teams/workflows/route/app-paths-manifest.json +3 -0
- package/.next/server/app/api/teams/workflows/route/build-manifest.json +11 -0
- package/.next/server/app/api/teams/workflows/route/server-reference-manifest.json +4 -0
- package/.next/server/app/api/teams/workflows/route.js +6 -0
- package/.next/server/app/api/teams/workflows/route.js.map +5 -0
- package/.next/server/app/api/teams/workflows/route.js.nft.json +1 -0
- package/.next/server/app/api/teams/workflows/route_client-reference-manifest.js +2 -0
- package/.next/server/app/api/tickets/move/route.js +1 -1
- package/.next/server/app/api/tickets/move/route.js.nft.json +1 -1
- package/.next/server/app/channels/page/build-manifest.json +4 -4
- package/.next/server/app/channels/page.js +7 -7
- package/.next/server/app/channels/page.js.nft.json +1 -1
- package/.next/server/app/channels/page_client-reference-manifest.js +1 -1
- package/.next/server/app/channels.html +2 -2
- package/.next/server/app/channels.rsc +5 -5
- package/.next/server/app/channels.segments/_full.segment.rsc +5 -5
- package/.next/server/app/channels.segments/_head.segment.rsc +1 -1
- package/.next/server/app/channels.segments/_index.segment.rsc +3 -3
- package/.next/server/app/channels.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/channels.segments/channels/__PAGE__.segment.rsc +2 -2
- package/.next/server/app/channels.segments/channels.segment.rsc +1 -1
- package/.next/server/app/cron-jobs/page/build-manifest.json +4 -4
- package/.next/server/app/cron-jobs/page.js +7 -7
- package/.next/server/app/cron-jobs/page.js.nft.json +1 -1
- package/.next/server/app/cron-jobs/page_client-reference-manifest.js +1 -1
- package/.next/server/app/cron-jobs.html +1 -1
- package/.next/server/app/cron-jobs.rsc +6 -6
- package/.next/server/app/cron-jobs.segments/_full.segment.rsc +6 -6
- package/.next/server/app/cron-jobs.segments/_head.segment.rsc +1 -1
- package/.next/server/app/cron-jobs.segments/_index.segment.rsc +3 -3
- package/.next/server/app/cron-jobs.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/cron-jobs.segments/cron-jobs/__PAGE__.segment.rsc +3 -3
- package/.next/server/app/cron-jobs.segments/cron-jobs.segment.rsc +1 -1
- package/.next/server/app/goals/[id]/page/build-manifest.json +4 -4
- package/.next/server/app/goals/[id]/page.js +7 -7
- package/.next/server/app/goals/[id]/page.js.nft.json +1 -1
- package/.next/server/app/goals/[id]/page_client-reference-manifest.js +1 -1
- package/.next/server/app/goals/new/page/build-manifest.json +4 -4
- package/.next/server/app/goals/new/page.js +7 -7
- package/.next/server/app/goals/new/page.js.nft.json +1 -1
- package/.next/server/app/goals/new/page_client-reference-manifest.js +1 -1
- package/.next/server/app/goals/new.html +2 -2
- package/.next/server/app/goals/new.rsc +5 -5
- package/.next/server/app/goals/new.segments/_full.segment.rsc +5 -5
- package/.next/server/app/goals/new.segments/_head.segment.rsc +1 -1
- package/.next/server/app/goals/new.segments/_index.segment.rsc +3 -3
- package/.next/server/app/goals/new.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/goals/new.segments/goals/new/__PAGE__.segment.rsc +2 -2
- package/.next/server/app/goals/new.segments/goals/new.segment.rsc +1 -1
- package/.next/server/app/goals/new.segments/goals.segment.rsc +1 -1
- package/.next/server/app/goals/page/build-manifest.json +4 -4
- package/.next/server/app/goals/page.js +7 -7
- package/.next/server/app/goals/page.js.nft.json +1 -1
- package/.next/server/app/goals/page_client-reference-manifest.js +1 -1
- package/.next/server/app/goals.html +1 -1
- package/.next/server/app/goals.rsc +5 -5
- package/.next/server/app/goals.segments/_full.segment.rsc +5 -5
- package/.next/server/app/goals.segments/_head.segment.rsc +1 -1
- package/.next/server/app/goals.segments/_index.segment.rsc +3 -3
- package/.next/server/app/goals.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/goals.segments/goals/__PAGE__.segment.rsc +2 -2
- package/.next/server/app/goals.segments/goals.segment.rsc +1 -1
- package/.next/server/app/page/build-manifest.json +4 -4
- package/.next/server/app/page.js +7 -7
- package/.next/server/app/page.js.nft.json +1 -1
- package/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/server/app/recipes/[id]/page/build-manifest.json +4 -4
- package/.next/server/app/recipes/[id]/page.js +8 -8
- package/.next/server/app/recipes/[id]/page.js.nft.json +1 -1
- package/.next/server/app/recipes/[id]/page_client-reference-manifest.js +1 -1
- package/.next/server/app/recipes/page/build-manifest.json +4 -4
- package/.next/server/app/recipes/page.js +8 -8
- package/.next/server/app/recipes/page.js.nft.json +1 -1
- package/.next/server/app/recipes/page_client-reference-manifest.js +1 -1
- package/.next/server/app/settings/page/build-manifest.json +4 -4
- package/.next/server/app/settings/page.js +7 -7
- package/.next/server/app/settings/page.js.nft.json +1 -1
- package/.next/server/app/settings/page_client-reference-manifest.js +1 -1
- package/.next/server/app/settings.html +1 -1
- package/.next/server/app/settings.rsc +5 -5
- package/.next/server/app/settings.segments/_full.segment.rsc +5 -5
- package/.next/server/app/settings.segments/_head.segment.rsc +1 -1
- package/.next/server/app/settings.segments/_index.segment.rsc +3 -3
- package/.next/server/app/settings.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/settings.segments/settings/__PAGE__.segment.rsc +2 -2
- package/.next/server/app/settings.segments/settings.segment.rsc +1 -1
- package/.next/server/app/teams/[teamId]/page/build-manifest.json +4 -4
- package/.next/server/app/teams/[teamId]/page.js +8 -8
- package/.next/server/app/teams/[teamId]/page.js.nft.json +1 -1
- package/.next/server/app/teams/[teamId]/page_client-reference-manifest.js +1 -1
- package/.next/server/app/tickets/[ticket]/page/build-manifest.json +4 -4
- package/.next/server/app/tickets/[ticket]/page.js +7 -7
- package/.next/server/app/tickets/[ticket]/page.js.nft.json +1 -1
- package/.next/server/app/tickets/[ticket]/page_client-reference-manifest.js +1 -1
- package/.next/server/app/tickets/page/build-manifest.json +4 -4
- package/.next/server/app/tickets/page.js +7 -7
- package/.next/server/app/tickets/page.js.nft.json +1 -1
- package/.next/server/app/tickets/page_client-reference-manifest.js +1 -1
- package/.next/server/app-paths-manifest.json +5 -0
- package/.next/server/chunks/[root-of-the-server]__065a27a4._.js +3 -0
- package/.next/server/chunks/[root-of-the-server]__065a27a4._.js.map +1 -0
- package/.next/server/chunks/{[root-of-the-server]__94d68aa3._.js → [root-of-the-server]__1989d64f._.js} +2 -2
- package/.next/server/chunks/{[root-of-the-server]__94d68aa3._.js.map → [root-of-the-server]__1989d64f._.js.map} +1 -1
- package/.next/server/chunks/{[root-of-the-server]__c4ffbb03._.js → [root-of-the-server]__21b72903._.js} +2 -2
- package/.next/server/chunks/{[root-of-the-server]__c4ffbb03._.js.map → [root-of-the-server]__21b72903._.js.map} +1 -1
- package/.next/server/chunks/{[root-of-the-server]__0c01c5c7._.js → [root-of-the-server]__262d8a6e._.js} +2 -2
- package/.next/server/chunks/{[root-of-the-server]__0c01c5c7._.js.map → [root-of-the-server]__262d8a6e._.js.map} +1 -1
- package/.next/server/chunks/{[root-of-the-server]__b11231a9._.js → [root-of-the-server]__2f2c799d._.js} +2 -2
- package/.next/server/chunks/{[root-of-the-server]__b11231a9._.js.map → [root-of-the-server]__2f2c799d._.js.map} +1 -1
- package/.next/server/chunks/[root-of-the-server]__6f85770b._.js +3 -0
- package/.next/server/chunks/[root-of-the-server]__6f85770b._.js.map +1 -0
- package/.next/server/chunks/[root-of-the-server]__7cf49621._.js +3 -0
- package/.next/server/chunks/[root-of-the-server]__7cf49621._.js.map +1 -0
- package/.next/server/chunks/[root-of-the-server]__a27f8405._.js +1 -1
- package/.next/server/chunks/[root-of-the-server]__a27f8405._.js.map +1 -1
- package/.next/server/chunks/[root-of-the-server]__a5226713._.js +3 -0
- package/.next/server/chunks/[root-of-the-server]__a5226713._.js.map +1 -0
- package/.next/server/chunks/[root-of-the-server]__c7f92fce._.js +3 -0
- package/.next/server/chunks/[root-of-the-server]__c7f92fce._.js.map +1 -0
- package/.next/server/chunks/{[root-of-the-server]__d737ca42._.js → [root-of-the-server]__e8bfeaba._.js} +2 -2
- package/.next/server/chunks/{[root-of-the-server]__d737ca42._.js.map → [root-of-the-server]__e8bfeaba._.js.map} +1 -1
- package/.next/server/chunks/{[root-of-the-server]__4eda99a9._.js → [root-of-the-server]__ebaa0038._.js} +2 -2
- package/.next/server/chunks/{[root-of-the-server]__4eda99a9._.js.map → [root-of-the-server]__ebaa0038._.js.map} +1 -1
- package/.next/server/chunks/{[root-of-the-server]__b59b3cdd._.js → [root-of-the-server]__f5546a39._.js} +2 -2
- package/.next/server/chunks/{[root-of-the-server]__b59b3cdd._.js.map → [root-of-the-server]__f5546a39._.js.map} +1 -1
- package/.next/server/chunks/{[root-of-the-server]__e6184ba3._.js → [root-of-the-server]__fdda9176._.js} +2 -2
- package/.next/server/chunks/{[root-of-the-server]__e6184ba3._.js.map → [root-of-the-server]__fdda9176._.js.map} +1 -1
- package/.next/server/chunks/_next-internal_server_app_api_swarms_start_route_actions_b79e9029.js +3 -0
- package/.next/server/chunks/_next-internal_server_app_api_swarms_start_route_actions_b79e9029.js.map +1 -0
- package/.next/server/chunks/_next-internal_server_app_api_swarms_status_route_actions_54826df0.js +3 -0
- package/.next/server/chunks/_next-internal_server_app_api_swarms_status_route_actions_54826df0.js.map +1 -0
- package/.next/server/chunks/_next-internal_server_app_api_teams_orchestrator_route_actions_1949c463.js +3 -0
- package/.next/server/chunks/_next-internal_server_app_api_teams_orchestrator_route_actions_1949c463.js.map +1 -0
- package/.next/server/chunks/_next-internal_server_app_api_teams_workflow-runs_route_actions_8327ac7d.js +3 -0
- package/.next/server/chunks/_next-internal_server_app_api_teams_workflow-runs_route_actions_8327ac7d.js.map +1 -0
- package/.next/server/chunks/_next-internal_server_app_api_teams_workflows_route_actions_43dd6e07.js +3 -0
- package/.next/server/chunks/_next-internal_server_app_api_teams_workflows_route_actions_43dd6e07.js.map +1 -0
- package/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_1fe98a49.js +14 -0
- package/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_1fe98a49.js.map +1 -0
- package/.next/server/chunks/ssr/[root-of-the-server]__0d4cb2ba._.js +3 -0
- package/.next/server/chunks/ssr/[root-of-the-server]__0fd02d84._.js +3 -0
- package/.next/server/chunks/ssr/{[root-of-the-server]__4393e897._.js.map → [root-of-the-server]__0fd02d84._.js.map} +1 -1
- package/.next/server/chunks/ssr/[root-of-the-server]__285ae6b7._.js +3 -0
- package/.next/server/chunks/ssr/{[root-of-the-server]__46e08d44._.js.map → [root-of-the-server]__285ae6b7._.js.map} +1 -1
- package/.next/server/chunks/ssr/[root-of-the-server]__2bef6884._.js +3 -0
- package/.next/server/chunks/ssr/{[root-of-the-server]__15600e29._.js.map → [root-of-the-server]__2bef6884._.js.map} +1 -1
- package/.next/server/chunks/ssr/[root-of-the-server]__3efd25c4._.js +3 -0
- package/.next/server/chunks/ssr/{[root-of-the-server]__2a6f1e3e._.js.map → [root-of-the-server]__3efd25c4._.js.map} +1 -1
- package/.next/server/chunks/ssr/[root-of-the-server]__491e06fa._.js +1 -1
- package/.next/server/chunks/ssr/[root-of-the-server]__491e06fa._.js.map +1 -1
- package/.next/server/chunks/ssr/[root-of-the-server]__550d78f8._.js +3 -0
- package/.next/server/chunks/ssr/{[root-of-the-server]__bc3b27b0._.js.map → [root-of-the-server]__550d78f8._.js.map} +1 -1
- package/.next/server/chunks/ssr/{[root-of-the-server]__e2e52c6e._.js → [root-of-the-server]__78cdd31e._.js} +2 -2
- package/.next/server/chunks/ssr/[root-of-the-server]__8ecc89e4._.js +3 -0
- package/.next/server/chunks/ssr/{[root-of-the-server]__507d2fc9._.js.map → [root-of-the-server]__8ecc89e4._.js.map} +1 -1
- package/.next/server/chunks/ssr/[root-of-the-server]__92524828._.js +3 -0
- package/.next/server/chunks/ssr/{[root-of-the-server]__247198dc._.js.map → [root-of-the-server]__92524828._.js.map} +1 -1
- package/.next/server/chunks/ssr/[root-of-the-server]__9e1ab064._.js +3 -0
- package/.next/server/chunks/ssr/[root-of-the-server]__9e1ab064._.js.map +1 -0
- package/.next/server/chunks/ssr/[root-of-the-server]__a36a058d._.js +3 -0
- package/.next/server/chunks/ssr/{[root-of-the-server]__8d24c9c3._.js.map → [root-of-the-server]__a36a058d._.js.map} +1 -1
- package/.next/server/chunks/ssr/[root-of-the-server]__a8eca9e1._.js +3 -0
- package/.next/server/chunks/ssr/{[root-of-the-server]__82ce3aee._.js.map → [root-of-the-server]__a8eca9e1._.js.map} +1 -1
- package/.next/server/chunks/ssr/[root-of-the-server]__aa8e2649._.js +3 -0
- package/.next/server/chunks/ssr/{[root-of-the-server]__f75a61bf._.js.map → [root-of-the-server]__aa8e2649._.js.map} +1 -1
- package/.next/server/chunks/ssr/[root-of-the-server]__b2617fbf._.js +3 -0
- package/.next/server/chunks/ssr/{[root-of-the-server]__fd669584._.js.map → [root-of-the-server]__b2617fbf._.js.map} +1 -1
- package/.next/server/chunks/ssr/[root-of-the-server]__b5f65083._.js +3 -0
- package/.next/server/chunks/ssr/[root-of-the-server]__b5f65083._.js.map +1 -0
- package/.next/server/chunks/ssr/[root-of-the-server]__e5d416d5._.js +3 -0
- package/.next/server/chunks/ssr/{[root-of-the-server]__3ad3e5b1._.js.map → [root-of-the-server]__e5d416d5._.js.map} +1 -1
- package/.next/server/chunks/ssr/{[root-of-the-server]__b9356576._.js → [root-of-the-server]__f62d412e._.js} +2 -2
- package/.next/server/chunks/ssr/{[root-of-the-server]__b9356576._.js.map → [root-of-the-server]__f62d412e._.js.map} +1 -1
- package/.next/server/chunks/ssr/[root-of-the-server]__fbe5ff69._.js +3 -0
- package/.next/server/chunks/ssr/{[root-of-the-server]__3575e6da._.js.map → [root-of-the-server]__fbe5ff69._.js.map} +1 -1
- package/.next/server/chunks/ssr/{_8c45edba._.js → _0bdae23a._.js} +3 -3
- package/.next/server/chunks/ssr/{node_modules_next_dist_esm_build_templates_app-page_68c68167.js.map → _0bdae23a._.js.map} +1 -1
- package/.next/server/chunks/ssr/{_0808c2b9._.js → _347e2d56._.js} +3 -3
- package/.next/server/chunks/ssr/_347e2d56._.js.map +1 -0
- package/.next/server/chunks/ssr/{_68793c1f._.js → _49fb12c2._.js} +3 -3
- package/.next/server/chunks/ssr/_49fb12c2._.js.map +1 -0
- package/.next/server/chunks/ssr/{_3c8b2df6._.js → _536983a4._.js} +3 -3
- package/.next/server/chunks/ssr/_536983a4._.js.map +1 -0
- package/.next/server/chunks/ssr/{_cd23c5af._.js → _5b282394._.js} +3 -3
- package/.next/server/chunks/ssr/_5b282394._.js.map +1 -0
- package/.next/server/chunks/ssr/{_6e70b5a4._.js → _5cc24343._.js} +3 -3
- package/.next/server/chunks/ssr/_5cc24343._.js.map +1 -0
- package/.next/server/chunks/ssr/{_92140ca3._.js → _7eac37fb._.js} +3 -3
- package/.next/server/chunks/ssr/_7eac37fb._.js.map +1 -0
- package/.next/server/chunks/ssr/{_8c8207c1._.js → _8062e992._.js} +3 -3
- package/.next/server/chunks/ssr/_8062e992._.js.map +1 -0
- package/.next/server/chunks/ssr/{_25e6aab8._.js → _99994c05._.js} +3 -3
- package/.next/server/chunks/ssr/_99994c05._.js.map +1 -0
- package/.next/server/chunks/ssr/{_b5e9afcc._.js → _aafc99aa._.js} +3 -3
- package/.next/server/chunks/ssr/_aafc99aa._.js.map +1 -0
- package/.next/server/chunks/ssr/{_39e6e769._.js → _b7f3caf7._.js} +3 -3
- package/.next/server/chunks/ssr/_b7f3caf7._.js.map +1 -0
- package/.next/server/chunks/ssr/_c2041f88._.js +4 -0
- package/.next/server/chunks/ssr/_c2041f88._.js.map +1 -0
- package/.next/server/chunks/ssr/{_3158c108._.js → _f4d8af3b._.js} +3 -3
- package/.next/server/chunks/ssr/_f4d8af3b._.js.map +1 -0
- package/.next/server/chunks/ssr/{node_modules_next_dist_12287b3d._.js → node_modules_next_dist_8a37abed._.js} +2 -2
- package/.next/server/chunks/ssr/{node_modules_next_dist_12287b3d._.js.map → node_modules_next_dist_8a37abed._.js.map} +1 -1
- package/.next/server/chunks/ssr/{node_modules_next_dist_client_components_9774470f._.js → node_modules_next_dist_client_components_2fffaa3a._.js} +2 -2
- package/.next/server/chunks/ssr/{node_modules_next_dist_client_components_9774470f._.js.map → node_modules_next_dist_client_components_2fffaa3a._.js.map} +1 -1
- package/.next/server/chunks/ssr/{node_modules_next_dist_esm_build_templates_app-page_68c68167.js → node_modules_next_dist_esm_build_templates_app-page_b5ef32b4.js} +3 -3
- package/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_b5ef32b4.js.map +1 -0
- package/.next/server/chunks/ssr/src_app_HomeClient_tsx_f9f7568d._.js +1 -1
- package/.next/server/chunks/ssr/src_app_HomeClient_tsx_f9f7568d._.js.map +1 -1
- package/.next/server/chunks/ssr/src_app_global-error_tsx_aa661ba4._.js +3 -0
- package/.next/server/chunks/ssr/src_app_global-error_tsx_aa661ba4._.js.map +1 -0
- package/.next/server/chunks/ssr/src_app_not-found_tsx_3f23d179._.js +3 -0
- package/.next/server/chunks/ssr/src_app_not-found_tsx_3f23d179._.js.map +1 -0
- package/.next/server/chunks/ssr/src_app_teams_[teamId]_team-editor_tsx_2900b91d._.js +2 -2
- package/.next/server/chunks/ssr/src_app_teams_[teamId]_team-editor_tsx_2900b91d._.js.map +1 -1
- package/.next/server/middleware-build-manifest.js +4 -4
- package/.next/server/pages/404.html +1 -1
- package/.next/server/pages/500.html +2 -2
- package/.next/server/server-reference-manifest.js +1 -1
- package/.next/server/server-reference-manifest.json +1 -1
- package/.next/static/chunks/{40f7fde35327d214.js → 423ca3a7f09c1470.js} +1 -1
- package/.next/static/chunks/{82abf2d65f5428ae.js → 68a088aa49e6124a.js} +3 -3
- package/.next/static/chunks/96b0d0ef92ef0eca.css +3 -0
- package/.next/static/chunks/a92e9d0c91b71b55.js +1 -0
- package/.next/static/chunks/bd3fcd39d918f9b4.js +10 -0
- package/.next/static/chunks/c84580993f22c17a.js +1 -0
- package/.next/static/chunks/f3e3461eda4a8d62.js +1 -0
- package/.next/static/chunks/{turbopack-ae1340e658f67df8.js → turbopack-6c658f991a22d28a.js} +1 -1
- package/package.json +2 -2
- package/public/android-chrome-192x192.png +0 -0
- package/public/android-chrome-512x512.png +0 -0
- package/public/apple-touch-icon.png +0 -0
- package/public/chef.jpg +0 -0
- package/public/favicon-16x16.png +0 -0
- package/public/favicon-32x32.png +0 -0
- package/public/favicon.ico +0 -0
- package/src/app/HomeClient.tsx +1 -1
- package/src/app/api/cron/jobs/route.ts +12 -4
- package/src/app/api/swarms/start/route.ts +118 -0
- package/src/app/api/swarms/status/route.ts +50 -0
- package/src/app/api/teams/orchestrator/route.ts +218 -0
- package/src/app/api/teams/workflow-runs/route.ts +431 -0
- package/src/app/api/teams/workflows/route.ts +88 -0
- package/src/app/cron-jobs/page.tsx +1 -1
- package/src/app/global-error.tsx +50 -0
- package/src/app/not-found.tsx +8 -0
- package/src/app/recipes/page.tsx +1 -1
- package/src/app/settings/page.tsx +1 -1
- package/src/app/teams/[teamId]/OrchestratorPanel.tsx +228 -0
- package/src/app/teams/[teamId]/page.tsx +25 -29
- package/src/app/teams/[teamId]/team-editor.tsx +1656 -8
- package/src/app/tickets/[ticket]/page.tsx +3 -0
- package/src/app/tickets/page.tsx +3 -0
- package/src/components/AppShell.tsx +245 -31
- package/src/lib/workflows/README.md +11 -0
- package/src/lib/workflows/runs-storage.ts +71 -0
- package/src/lib/workflows/runs-types.ts +32 -0
- package/src/lib/workflows/storage.ts +82 -0
- package/src/lib/workflows/types.ts +50 -0
- package/src/lib/workflows/validate.ts +70 -0
- package/.next/server/app/tickets.html +0 -1
- package/.next/server/app/tickets.meta +0 -15
- package/.next/server/app/tickets.rsc +0 -19
- package/.next/server/app/tickets.segments/_full.segment.rsc +0 -19
- package/.next/server/app/tickets.segments/_head.segment.rsc +0 -6
- package/.next/server/app/tickets.segments/_index.segment.rsc +0 -6
- package/.next/server/app/tickets.segments/_tree.segment.rsc +0 -4
- package/.next/server/app/tickets.segments/tickets/__PAGE__.segment.rsc +0 -8
- package/.next/server/app/tickets.segments/tickets.segment.rsc +0 -4
- package/.next/server/chunks/ssr/[root-of-the-server]__15600e29._.js +0 -3
- package/.next/server/chunks/ssr/[root-of-the-server]__247198dc._.js +0 -3
- package/.next/server/chunks/ssr/[root-of-the-server]__2a6f1e3e._.js +0 -3
- package/.next/server/chunks/ssr/[root-of-the-server]__346f79e5._.js +0 -3
- package/.next/server/chunks/ssr/[root-of-the-server]__346f79e5._.js.map +0 -1
- package/.next/server/chunks/ssr/[root-of-the-server]__3575e6da._.js +0 -3
- package/.next/server/chunks/ssr/[root-of-the-server]__3ad3e5b1._.js +0 -3
- package/.next/server/chunks/ssr/[root-of-the-server]__3bc7ad0a._.js +0 -3
- package/.next/server/chunks/ssr/[root-of-the-server]__4393e897._.js +0 -3
- package/.next/server/chunks/ssr/[root-of-the-server]__46e08d44._.js +0 -3
- package/.next/server/chunks/ssr/[root-of-the-server]__507d2fc9._.js +0 -3
- package/.next/server/chunks/ssr/[root-of-the-server]__82ce3aee._.js +0 -3
- package/.next/server/chunks/ssr/[root-of-the-server]__8d24c9c3._.js +0 -3
- package/.next/server/chunks/ssr/[root-of-the-server]__a457c799._.js +0 -3
- package/.next/server/chunks/ssr/[root-of-the-server]__a457c799._.js.map +0 -1
- package/.next/server/chunks/ssr/[root-of-the-server]__bc3b27b0._.js +0 -3
- package/.next/server/chunks/ssr/[root-of-the-server]__f75a61bf._.js +0 -3
- package/.next/server/chunks/ssr/[root-of-the-server]__fd669584._.js +0 -3
- package/.next/server/chunks/ssr/_0808c2b9._.js.map +0 -1
- package/.next/server/chunks/ssr/_25e6aab8._.js.map +0 -1
- package/.next/server/chunks/ssr/_3158c108._.js.map +0 -1
- package/.next/server/chunks/ssr/_39e6e769._.js.map +0 -1
- package/.next/server/chunks/ssr/_3c8b2df6._.js.map +0 -1
- package/.next/server/chunks/ssr/_68793c1f._.js.map +0 -1
- package/.next/server/chunks/ssr/_6e70b5a4._.js.map +0 -1
- package/.next/server/chunks/ssr/_8c45edba._.js.map +0 -1
- package/.next/server/chunks/ssr/_8c8207c1._.js.map +0 -1
- package/.next/server/chunks/ssr/_92140ca3._.js.map +0 -1
- package/.next/server/chunks/ssr/_b5e9afcc._.js.map +0 -1
- package/.next/server/chunks/ssr/_cd23c5af._.js.map +0 -1
- package/.next/server/chunks/ssr/_d27483a1._.js +0 -4
- package/.next/server/chunks/ssr/_d27483a1._.js.map +0 -1
- package/.next/server/chunks/ssr/node_modules_next_dist_client_components_builtin_forbidden_45780354.js +0 -3
- package/.next/server/chunks/ssr/node_modules_next_dist_client_components_builtin_forbidden_45780354.js.map +0 -1
- package/.next/server/chunks/ssr/node_modules_next_dist_client_components_builtin_global-error_ece394eb.js +0 -3
- package/.next/server/chunks/ssr/node_modules_next_dist_client_components_builtin_global-error_ece394eb.js.map +0 -1
- package/.next/static/chunks/8484c54dc9a377e8.js +0 -10
- package/.next/static/chunks/8662fcc3cdff66f3.js +0 -1
- package/.next/static/chunks/a4e69b85b74277a7.css +0 -3
- package/.next/static/chunks/a9ed074e89b16a5e.js +0 -1
- /package/.next/server/chunks/ssr/{[root-of-the-server]__3bc7ad0a._.js.map → [root-of-the-server]__0d4cb2ba._.js.map} +0 -0
- /package/.next/server/chunks/ssr/{[root-of-the-server]__e2e52c6e._.js.map → [root-of-the-server]__78cdd31e._.js.map} +0 -0
- /package/.next/static/{2hgbnJUmLMe9COlc5rJd3 → LkBl31uBDAuYl-0TzH69D}/_buildManifest.js +0 -0
- /package/.next/static/{2hgbnJUmLMe9COlc5rJd3 → LkBl31uBDAuYl-0TzH69D}/_clientMiddlewareManifest.json +0 -0
- /package/.next/static/{2hgbnJUmLMe9COlc5rJd3 → LkBl31uBDAuYl-0TzH69D}/_ssgManifest.js +0 -0
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import { useEffect, useMemo, useState } from "react";
|
|
3
|
+
import { useEffect, useMemo, useRef, useState } from "react";
|
|
4
4
|
import { parse as parseYaml } from "yaml";
|
|
5
5
|
import { useRouter } from "next/navigation";
|
|
6
6
|
import { DeleteTeamModal } from "./DeleteTeamModal";
|
|
7
7
|
import { PublishChangesModal } from "./PublishChangesModal";
|
|
8
8
|
import { useToast } from "@/components/ToastProvider";
|
|
9
|
+
import { OrchestratorPanel } from "./OrchestratorPanel";
|
|
10
|
+
import type { WorkflowFileV1 } from "@/lib/workflows/types";
|
|
11
|
+
import { validateWorkflowFileV1 } from "@/lib/workflows/validate";
|
|
9
12
|
|
|
10
13
|
type RecipeListItem = {
|
|
11
14
|
id: string;
|
|
@@ -96,7 +99,7 @@ function forceFrontmatterTeamTeamId(md: string, teamId: string) {
|
|
|
96
99
|
return `---\n${next.join("\n")}\n---\n${body}`;
|
|
97
100
|
}
|
|
98
101
|
|
|
99
|
-
export default function TeamEditor({ teamId }: { teamId: string }) {
|
|
102
|
+
export default function TeamEditor({ teamId, initialTab }: { teamId: string; initialTab?: string }) {
|
|
100
103
|
const router = useRouter();
|
|
101
104
|
const [recipes, setRecipes] = useState<RecipeListItem[]>([]);
|
|
102
105
|
const [fromId, setFromId] = useState<string>("");
|
|
@@ -107,7 +110,15 @@ export default function TeamEditor({ teamId }: { teamId: string }) {
|
|
|
107
110
|
const [toName, setToName] = useState<string>(teamId);
|
|
108
111
|
const [content, setContent] = useState<string>("");
|
|
109
112
|
const [loadedRecipeHash, setLoadedRecipeHash] = useState<string | null>(null);
|
|
110
|
-
const [activeTab, setActiveTab] = useState<"recipe" | "agents" | "skills" | "cron" | "files">("recipe");
|
|
113
|
+
const [activeTab, setActiveTab] = useState<"recipe" | "agents" | "skills" | "cron" | "workflows" | "files" | "orchestrator">("recipe");
|
|
114
|
+
|
|
115
|
+
useEffect(() => {
|
|
116
|
+
const allowed = ["recipe", "agents", "skills", "cron", "workflows", "files", "orchestrator"] as const;
|
|
117
|
+
if (initialTab && (allowed as readonly string[]).includes(initialTab)) {
|
|
118
|
+
setActiveTab(initialTab as (typeof allowed)[number]);
|
|
119
|
+
}
|
|
120
|
+
}, [initialTab]);
|
|
121
|
+
|
|
111
122
|
const [loading, setLoading] = useState(true);
|
|
112
123
|
const [saving, setSaving] = useState(false);
|
|
113
124
|
const [publishing, setPublishing] = useState(false);
|
|
@@ -137,9 +148,147 @@ export default function TeamEditor({ teamId }: { teamId: string }) {
|
|
|
137
148
|
const [cronJobs, setCronJobs] = useState<unknown[]>([]);
|
|
138
149
|
const [cronLoading, setCronLoading] = useState(false);
|
|
139
150
|
|
|
151
|
+
const [workflowFiles, setWorkflowFiles] = useState<string[]>([]);
|
|
152
|
+
const [workflowFilesLoading, setWorkflowFilesLoading] = useState(false);
|
|
153
|
+
const [workflowFilesError, setWorkflowFilesError] = useState<string>("");
|
|
154
|
+
const [selectedWorkflowFile, setSelectedWorkflowFile] = useState<string>("");
|
|
155
|
+
const [workflowJsonText, setWorkflowJsonText] = useState<string>("");
|
|
156
|
+
const [workflowSaving, setWorkflowSaving] = useState(false);
|
|
157
|
+
const [workflowView, setWorkflowView] = useState<"canvas" | "json">("canvas");
|
|
158
|
+
const [workflowSelectedNodeId, setWorkflowSelectedNodeId] = useState<string>("");
|
|
159
|
+
const [workflowDragging, setWorkflowDragging] = useState<null | { nodeId: string; dx: number; dy: number; containerLeft: number; containerTop: number }>(null);
|
|
160
|
+
|
|
161
|
+
// Canvas editor helpers (minimal MVP): add/remove nodes + edges via inspector forms.
|
|
162
|
+
const [workflowNewNodeId, setWorkflowNewNodeId] = useState<string>("");
|
|
163
|
+
const [workflowNewNodeName, setWorkflowNewNodeName] = useState<string>("");
|
|
164
|
+
const [workflowNewNodeType, setWorkflowNewNodeType] = useState<WorkflowFileV1["nodes"][number]["type"]>("llm");
|
|
165
|
+
const [workflowNewEdgeFrom, setWorkflowNewEdgeFrom] = useState<string>("");
|
|
166
|
+
const [workflowNewEdgeTo, setWorkflowNewEdgeTo] = useState<string>("");
|
|
167
|
+
const [workflowNewEdgeLabel, setWorkflowNewEdgeLabel] = useState<string>("");
|
|
168
|
+
|
|
169
|
+
const [workflowEditorOpen, setWorkflowEditorOpen] = useState(false);
|
|
170
|
+
|
|
171
|
+
const [workflowCreateOpen, setWorkflowCreateOpen] = useState(false);
|
|
172
|
+
const [workflowCreateId, setWorkflowCreateId] = useState("new-workflow");
|
|
173
|
+
const [workflowCreateName, setWorkflowCreateName] = useState("New workflow");
|
|
174
|
+
|
|
175
|
+
const [workflowRuns, setWorkflowRuns] = useState<string[]>([]);
|
|
176
|
+
const [workflowRunsLoading, setWorkflowRunsLoading] = useState(false);
|
|
177
|
+
const [workflowRunsError, setWorkflowRunsError] = useState<string>("");
|
|
178
|
+
const [selectedWorkflowRunId, setSelectedWorkflowRunId] = useState<string>("");
|
|
179
|
+
const [selectedWorkflowRun, setSelectedWorkflowRun] = useState<unknown | null>(null);
|
|
180
|
+
|
|
140
181
|
const [teamAgents, setTeamAgents] = useState<Array<{ id: string; identityName?: string }>>([]);
|
|
141
182
|
const [teamAgentsLoading, setTeamAgentsLoading] = useState(false);
|
|
142
183
|
|
|
184
|
+
const workflowCanvasRef = useRef<HTMLDivElement | null>(null);
|
|
185
|
+
const workflowImportInputRef = useRef<HTMLInputElement | null>(null);
|
|
186
|
+
|
|
187
|
+
const workflowParsed = useMemo(() => {
|
|
188
|
+
const raw = String(workflowJsonText || "").trim();
|
|
189
|
+
if (!raw) return null;
|
|
190
|
+
try {
|
|
191
|
+
return JSON.parse(raw) as WorkflowFileV1;
|
|
192
|
+
} catch {
|
|
193
|
+
return null;
|
|
194
|
+
}
|
|
195
|
+
}, [workflowJsonText]);
|
|
196
|
+
|
|
197
|
+
const workflowParseError = useMemo(() => {
|
|
198
|
+
const raw = String(workflowJsonText || "").trim();
|
|
199
|
+
if (!raw) return "";
|
|
200
|
+
try {
|
|
201
|
+
JSON.parse(raw);
|
|
202
|
+
return "";
|
|
203
|
+
} catch (e: unknown) {
|
|
204
|
+
return e instanceof Error ? e.message : "Invalid JSON";
|
|
205
|
+
}
|
|
206
|
+
}, [workflowJsonText]);
|
|
207
|
+
|
|
208
|
+
const workflowValidation = useMemo(() => {
|
|
209
|
+
if (!workflowParsed) return { errors: [], warnings: [] };
|
|
210
|
+
try {
|
|
211
|
+
return validateWorkflowFileV1(workflowParsed);
|
|
212
|
+
} catch (e: unknown) {
|
|
213
|
+
return { errors: [e instanceof Error ? e.message : String(e)], warnings: [] };
|
|
214
|
+
}
|
|
215
|
+
}, [workflowParsed]);
|
|
216
|
+
|
|
217
|
+
useEffect(() => {
|
|
218
|
+
const wfId = String(workflowParsed?.id ?? "").trim();
|
|
219
|
+
if (!wfId) {
|
|
220
|
+
setWorkflowRuns([]);
|
|
221
|
+
setSelectedWorkflowRunId("");
|
|
222
|
+
setSelectedWorkflowRun(null);
|
|
223
|
+
setWorkflowRunsError("");
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
let canceled = false;
|
|
228
|
+
(async () => {
|
|
229
|
+
setWorkflowRunsLoading(true);
|
|
230
|
+
setWorkflowRunsError("");
|
|
231
|
+
try {
|
|
232
|
+
const res = await fetch(
|
|
233
|
+
`/api/teams/workflow-runs?teamId=${encodeURIComponent(teamId)}&workflowId=${encodeURIComponent(wfId)}`,
|
|
234
|
+
{ cache: "no-store" }
|
|
235
|
+
);
|
|
236
|
+
const json = await res.json();
|
|
237
|
+
if (!res.ok || !json.ok) throw new Error(json.error || "Failed to list runs");
|
|
238
|
+
const files = Array.isArray(json.files) ? json.files : [];
|
|
239
|
+
const list = files.map((f: unknown) => String(f ?? "").trim()).filter((f: string) => Boolean(f));
|
|
240
|
+
if (canceled) return;
|
|
241
|
+
setWorkflowRuns(list);
|
|
242
|
+
if (selectedWorkflowRunId && !list.some((f: string) => f === `${selectedWorkflowRunId}.run.json`)) {
|
|
243
|
+
setSelectedWorkflowRunId("");
|
|
244
|
+
setSelectedWorkflowRun(null);
|
|
245
|
+
}
|
|
246
|
+
} catch (e: unknown) {
|
|
247
|
+
if (canceled) return;
|
|
248
|
+
setWorkflowRunsError(e instanceof Error ? e.message : String(e));
|
|
249
|
+
} finally {
|
|
250
|
+
if (!canceled) setWorkflowRunsLoading(false);
|
|
251
|
+
}
|
|
252
|
+
})();
|
|
253
|
+
|
|
254
|
+
return () => {
|
|
255
|
+
canceled = true;
|
|
256
|
+
};
|
|
257
|
+
}, [teamId, workflowParsed?.id, selectedWorkflowRunId]);
|
|
258
|
+
|
|
259
|
+
useEffect(() => {
|
|
260
|
+
if (!workflowDragging) return;
|
|
261
|
+
|
|
262
|
+
function onMove(e: PointerEvent) {
|
|
263
|
+
if (!workflowDragging) return;
|
|
264
|
+
const wf = workflowParsed;
|
|
265
|
+
if (!wf) return;
|
|
266
|
+
|
|
267
|
+
const node = wf.nodes.find((n) => n.id === workflowDragging.nodeId);
|
|
268
|
+
if (!node) return;
|
|
269
|
+
|
|
270
|
+
const x = Math.max(0, Math.round(e.clientX - workflowDragging.containerLeft - workflowDragging.dx));
|
|
271
|
+
const y = Math.max(0, Math.round(e.clientY - workflowDragging.containerTop - workflowDragging.dy));
|
|
272
|
+
|
|
273
|
+
const next: WorkflowFileV1 = {
|
|
274
|
+
...wf,
|
|
275
|
+
nodes: wf.nodes.map((n) => (n.id === node.id ? { ...n, x, y } : n)),
|
|
276
|
+
};
|
|
277
|
+
setWorkflowJsonText(JSON.stringify(next, null, 2) + "\n");
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function onUp() {
|
|
281
|
+
setWorkflowDragging(null);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
window.addEventListener("pointermove", onMove);
|
|
285
|
+
window.addEventListener("pointerup", onUp);
|
|
286
|
+
return () => {
|
|
287
|
+
window.removeEventListener("pointermove", onMove);
|
|
288
|
+
window.removeEventListener("pointerup", onUp);
|
|
289
|
+
};
|
|
290
|
+
}, [workflowDragging, workflowParsed]);
|
|
291
|
+
|
|
143
292
|
const recipeAgents = useMemo(() => {
|
|
144
293
|
const md = String(content ?? "");
|
|
145
294
|
if (!md.startsWith("---\n")) return [] as Array<{ role: string; name?: string }>;
|
|
@@ -262,16 +411,18 @@ export default function TeamEditor({ teamId }: { teamId: string }) {
|
|
|
262
411
|
void (async () => {
|
|
263
412
|
setTeamFilesLoading(true);
|
|
264
413
|
setCronLoading(true);
|
|
414
|
+
setWorkflowFilesLoading(true);
|
|
265
415
|
setTeamAgentsLoading(true);
|
|
266
416
|
setSkillsLoading(true);
|
|
267
417
|
|
|
268
418
|
try {
|
|
269
|
-
const [filesRes, cronRes, agentsRes, skillsRes, availableSkillsRes] = await Promise.all([
|
|
419
|
+
const [filesRes, cronRes, agentsRes, skillsRes, availableSkillsRes, workflowsRes] = await Promise.all([
|
|
270
420
|
fetch(`/api/teams/files?teamId=${encodeURIComponent(teamId)}`, { cache: "no-store" }),
|
|
271
421
|
fetch(`/api/cron/jobs?teamId=${encodeURIComponent(teamId)}`, { cache: "no-store" }),
|
|
272
422
|
fetch("/api/agents", { cache: "no-store" }),
|
|
273
423
|
fetch(`/api/teams/skills?teamId=${encodeURIComponent(teamId)}`, { cache: "no-store" }),
|
|
274
424
|
fetch("/api/skills/available", { cache: "no-store" }),
|
|
425
|
+
fetch(`/api/teams/workflows?teamId=${encodeURIComponent(teamId)}`, { cache: "no-store" }),
|
|
275
426
|
]);
|
|
276
427
|
|
|
277
428
|
try {
|
|
@@ -304,6 +455,22 @@ export default function TeamEditor({ teamId }: { teamId: string }) {
|
|
|
304
455
|
// ignore
|
|
305
456
|
}
|
|
306
457
|
|
|
458
|
+
try {
|
|
459
|
+
const workflowsJson = (await workflowsRes.json()) as { ok?: boolean; files?: unknown[]; dir?: unknown };
|
|
460
|
+
if (workflowsRes.ok && workflowsJson.ok) {
|
|
461
|
+
const files = Array.isArray(workflowsJson.files) ? workflowsJson.files : [];
|
|
462
|
+
const list = files.map((f) => String(f ?? '').trim()).filter((f) => Boolean(f));
|
|
463
|
+
setWorkflowFiles(list);
|
|
464
|
+
setSelectedWorkflowFile((prev) => {
|
|
465
|
+
const p = String(prev ?? '').trim();
|
|
466
|
+
if (p && list.includes(p)) return p;
|
|
467
|
+
return list[0] ?? '';
|
|
468
|
+
});
|
|
469
|
+
}
|
|
470
|
+
} catch {
|
|
471
|
+
// ignore
|
|
472
|
+
}
|
|
473
|
+
|
|
307
474
|
try {
|
|
308
475
|
const agentsJson = (await agentsRes.json()) as { agents?: unknown[] };
|
|
309
476
|
if (agentsRes.ok) {
|
|
@@ -347,6 +514,7 @@ export default function TeamEditor({ teamId }: { teamId: string }) {
|
|
|
347
514
|
} finally {
|
|
348
515
|
setTeamFilesLoading(false);
|
|
349
516
|
setCronLoading(false);
|
|
517
|
+
setWorkflowFilesLoading(false);
|
|
350
518
|
setTeamAgentsLoading(false);
|
|
351
519
|
setSkillsLoading(false);
|
|
352
520
|
}
|
|
@@ -498,15 +666,16 @@ export default function TeamEditor({ teamId }: { teamId: string }) {
|
|
|
498
666
|
}
|
|
499
667
|
|
|
500
668
|
// Initial load only gates the minimal state (recipes + team meta). Everything else streams in.
|
|
501
|
-
if (loading) return <div className="ck-glass
|
|
669
|
+
if (loading) return <div className="ck-glass w-full p-6">Loading…</div>;
|
|
502
670
|
|
|
503
671
|
return (
|
|
504
|
-
<div className="ck-glass
|
|
672
|
+
<div className="ck-glass w-full p-6 sm:p-8">
|
|
505
673
|
<h1 className="text-2xl font-semibold tracking-tight">Team editor</h1>
|
|
506
674
|
<p className="mt-2 text-sm text-[color:var(--ck-text-secondary)]">
|
|
507
675
|
Bootstrap a <strong>custom team recipe</strong> for this installed team, without modifying builtin recipes.
|
|
508
676
|
</p>
|
|
509
677
|
|
|
678
|
+
|
|
510
679
|
<div className="mt-6 flex flex-wrap gap-2">
|
|
511
680
|
{(
|
|
512
681
|
[
|
|
@@ -514,12 +683,21 @@ export default function TeamEditor({ teamId }: { teamId: string }) {
|
|
|
514
683
|
{ id: "agents", label: "Agents" },
|
|
515
684
|
{ id: "skills", label: "Skills" },
|
|
516
685
|
{ id: "cron", label: "Cron" },
|
|
686
|
+
{ id: "workflows", label: "Workflows" },
|
|
517
687
|
{ id: "files", label: "Files" },
|
|
688
|
+
{ id: "orchestrator", label: "Orchestrator" },
|
|
518
689
|
] as const
|
|
519
690
|
).map((t) => (
|
|
520
691
|
<button
|
|
521
692
|
key={t.id}
|
|
522
|
-
onClick={() =>
|
|
693
|
+
onClick={() => {
|
|
694
|
+
setActiveTab(t.id);
|
|
695
|
+
try {
|
|
696
|
+
router.replace(`/teams/${encodeURIComponent(teamId)}?tab=${encodeURIComponent(t.id)}`, { scroll: false });
|
|
697
|
+
} catch {
|
|
698
|
+
// ignore
|
|
699
|
+
}
|
|
700
|
+
}}
|
|
523
701
|
className={
|
|
524
702
|
activeTab === t.id
|
|
525
703
|
? "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)]"
|
|
@@ -1018,7 +1196,1475 @@ export default function TeamEditor({ teamId }: { teamId: string }) {
|
|
|
1018
1196
|
</div>
|
|
1019
1197
|
) : null}
|
|
1020
1198
|
|
|
1021
|
-
{activeTab === "
|
|
1199
|
+
{activeTab === "workflows" ? (
|
|
1200
|
+
<div className="mt-6 grid grid-cols-1 gap-4 lg:grid-cols-3">
|
|
1201
|
+
<div className={workflowEditorOpen ? "ck-glass-strong p-4" : "ck-glass-strong p-4 lg:col-span-3"}>
|
|
1202
|
+
<div className="text-sm font-medium text-[color:var(--ck-text-primary)]">Workflows (file-first)</div>
|
|
1203
|
+
<div className="mt-2 text-xs text-[color:var(--ck-text-tertiary)]">
|
|
1204
|
+
Stored in <code>shared-context/workflows/<id>.workflow.json</code> inside the team workspace.
|
|
1205
|
+
</div>
|
|
1206
|
+
|
|
1207
|
+
{workflowFilesError ? (
|
|
1208
|
+
<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">
|
|
1209
|
+
{workflowFilesError}
|
|
1210
|
+
</div>
|
|
1211
|
+
) : null}
|
|
1212
|
+
|
|
1213
|
+
<div className="mt-3 flex flex-wrap gap-2">
|
|
1214
|
+
<button
|
|
1215
|
+
disabled={workflowSaving}
|
|
1216
|
+
onClick={() => {
|
|
1217
|
+
setWorkflowCreateId("new-workflow");
|
|
1218
|
+
setWorkflowCreateName("New workflow");
|
|
1219
|
+
setWorkflowCreateOpen(true);
|
|
1220
|
+
}}
|
|
1221
|
+
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"
|
|
1222
|
+
>
|
|
1223
|
+
Add workflow
|
|
1224
|
+
</button>
|
|
1225
|
+
|
|
1226
|
+
{workflowFiles.length === 0 ? (
|
|
1227
|
+
<button
|
|
1228
|
+
disabled={workflowSaving}
|
|
1229
|
+
onClick={async () => {
|
|
1230
|
+
setWorkflowSaving(true);
|
|
1231
|
+
setWorkflowFilesError("");
|
|
1232
|
+
try {
|
|
1233
|
+
// Create the MVP built-in template (Marketing Cadence v1) for THIS TEAM.
|
|
1234
|
+
const workflow: WorkflowFileV1 = {
|
|
1235
|
+
schema: "clawkitchen.workflow.v1",
|
|
1236
|
+
id: "marketing-cadence",
|
|
1237
|
+
name: "Marketing Cadence (v1)",
|
|
1238
|
+
version: 1,
|
|
1239
|
+
timezone: "America/New_York",
|
|
1240
|
+
triggers: [
|
|
1241
|
+
{ kind: "cron", id: "content-cadence", name: "Content cadence", enabled: true, expr: "0 9 * * 1,3,5", tz: "America/New_York" },
|
|
1242
|
+
{ kind: "cron", id: "seasonal-scan", name: "Seasonal scan", enabled: true, expr: "0 8 * * *", tz: "America/New_York" },
|
|
1243
|
+
{ kind: "cron", id: "weekly-recap", name: "Weekly recap", enabled: false, expr: "30 9 * * 1", tz: "America/New_York" },
|
|
1244
|
+
],
|
|
1245
|
+
nodes: [
|
|
1246
|
+
{ id: "start", type: "start", name: "Start", x: 80, y: 80 },
|
|
1247
|
+
{ id: "research", type: "llm", name: "Research", x: 320, y: 80 },
|
|
1248
|
+
{ id: "draft_assets", type: "llm", name: "Draft assets", x: 560, y: 80 },
|
|
1249
|
+
{ id: "qc_brand", type: "llm", name: "QC brand", x: 800, y: 80 },
|
|
1250
|
+
{ id: "approval", type: "human_approval", name: "Approval", x: 1040, y: 80 },
|
|
1251
|
+
{ id: "post_x", type: "tool", name: "Post: X", x: 1040, y: 240 },
|
|
1252
|
+
{ id: "post_instagram", type: "tool", name: "Post: Instagram", x: 1040, y: 360 },
|
|
1253
|
+
{ id: "post_tiktok", type: "tool", name: "Post: TikTok", x: 1040, y: 480 },
|
|
1254
|
+
{ id: "post_youtube", type: "tool", name: "Post: YouTube", x: 1040, y: 600 },
|
|
1255
|
+
{ id: "writeback", type: "tool", name: "Writeback", x: 800, y: 600 },
|
|
1256
|
+
{ id: "end", type: "end", name: "End", x: 560, y: 600 },
|
|
1257
|
+
],
|
|
1258
|
+
edges: [
|
|
1259
|
+
{ id: "e1", from: "start", to: "research" },
|
|
1260
|
+
{ id: "e2", from: "research", to: "draft_assets" },
|
|
1261
|
+
{ id: "e3", from: "draft_assets", to: "qc_brand" },
|
|
1262
|
+
{ id: "e4", from: "qc_brand", to: "approval" },
|
|
1263
|
+
{ id: "e5", from: "approval", to: "post_x", label: "approve" },
|
|
1264
|
+
{ id: "e6", from: "post_x", to: "post_instagram" },
|
|
1265
|
+
{ id: "e7", from: "post_instagram", to: "post_tiktok" },
|
|
1266
|
+
{ id: "e8", from: "post_tiktok", to: "post_youtube" },
|
|
1267
|
+
{ id: "e9", from: "post_youtube", to: "writeback" },
|
|
1268
|
+
{ id: "e10", from: "writeback", to: "end" },
|
|
1269
|
+
],
|
|
1270
|
+
meta: {
|
|
1271
|
+
templateId: "marketing-cadence-v1",
|
|
1272
|
+
approvalGateRequired: true,
|
|
1273
|
+
},
|
|
1274
|
+
};
|
|
1275
|
+
|
|
1276
|
+
const res = await fetch("/api/teams/workflows", {
|
|
1277
|
+
method: "POST",
|
|
1278
|
+
headers: { "content-type": "application/json" },
|
|
1279
|
+
body: JSON.stringify({ teamId, workflow }),
|
|
1280
|
+
});
|
|
1281
|
+
const json = await res.json();
|
|
1282
|
+
if (!res.ok || !json.ok) throw new Error(json.error || "Failed to write workflow");
|
|
1283
|
+
|
|
1284
|
+
// Refresh list
|
|
1285
|
+
const listRes = await fetch(`/api/teams/workflows?teamId=${encodeURIComponent(teamId)}`, { cache: "no-store" });
|
|
1286
|
+
const listJson = await listRes.json();
|
|
1287
|
+
if (listRes.ok && listJson.ok) {
|
|
1288
|
+
const files = Array.isArray(listJson.files) ? listJson.files : [];
|
|
1289
|
+
const list = files.map((f: unknown) => String(f ?? "").trim()).filter((f: string) => Boolean(f));
|
|
1290
|
+
setWorkflowFiles(list);
|
|
1291
|
+
setSelectedWorkflowFile("marketing-cadence.workflow.json");
|
|
1292
|
+
setWorkflowJsonText(JSON.stringify(workflow, null, 2) + "\n");
|
|
1293
|
+
setWorkflowEditorOpen(true);
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1296
|
+
flashMessage("Created workflow template: marketing-cadence", "success");
|
|
1297
|
+
} catch (e: unknown) {
|
|
1298
|
+
setWorkflowFilesError(e instanceof Error ? e.message : String(e));
|
|
1299
|
+
} finally {
|
|
1300
|
+
setWorkflowSaving(false);
|
|
1301
|
+
}
|
|
1302
|
+
}}
|
|
1303
|
+
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"
|
|
1304
|
+
>
|
|
1305
|
+
{workflowSaving ? "Working…" : "Create Marketing Cadence template"}
|
|
1306
|
+
</button>
|
|
1307
|
+
) : null}
|
|
1308
|
+
|
|
1309
|
+
<button
|
|
1310
|
+
disabled={workflowSaving}
|
|
1311
|
+
onClick={async () => {
|
|
1312
|
+
setWorkflowSaving(true);
|
|
1313
|
+
setWorkflowFilesError("");
|
|
1314
|
+
try {
|
|
1315
|
+
const res = await fetch(`/api/teams/workflows?teamId=${encodeURIComponent(teamId)}`, { cache: "no-store" });
|
|
1316
|
+
const json = await res.json();
|
|
1317
|
+
if (!res.ok || !json.ok) throw new Error(json.error || "Failed to list workflows");
|
|
1318
|
+
const files = Array.isArray(json.files) ? json.files : [];
|
|
1319
|
+
const list = files.map((f: unknown) => String(f ?? "").trim()).filter((f: string) => Boolean(f));
|
|
1320
|
+
setWorkflowFiles(list);
|
|
1321
|
+
flashMessage("Refreshed workflow list", "success");
|
|
1322
|
+
} catch (e: unknown) {
|
|
1323
|
+
setWorkflowFilesError(e instanceof Error ? e.message : String(e));
|
|
1324
|
+
} finally {
|
|
1325
|
+
setWorkflowSaving(false);
|
|
1326
|
+
}
|
|
1327
|
+
}}
|
|
1328
|
+
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"
|
|
1329
|
+
>
|
|
1330
|
+
Refresh
|
|
1331
|
+
</button>
|
|
1332
|
+
</div>
|
|
1333
|
+
|
|
1334
|
+
{workflowCreateOpen ? (
|
|
1335
|
+
<div className="fixed inset-0 z-[60] flex items-center justify-center bg-black/70 p-4">
|
|
1336
|
+
<div className="ck-glass-strong w-full max-w-lg rounded-[var(--ck-radius-lg)] border border-white/10 p-4 shadow-[var(--ck-shadow-2)]">
|
|
1337
|
+
<div className="text-sm font-medium text-[color:var(--ck-text-primary)]">Create workflow</div>
|
|
1338
|
+
<div className="mt-1 text-xs text-[color:var(--ck-text-tertiary)]">Creates a new file under shared-context/workflows/ for this team.</div>
|
|
1339
|
+
|
|
1340
|
+
<div className="mt-4 grid grid-cols-1 gap-3">
|
|
1341
|
+
<label className="grid gap-1">
|
|
1342
|
+
<span className="text-xs text-[color:var(--ck-text-secondary)]">Workflow id</span>
|
|
1343
|
+
<input
|
|
1344
|
+
value={workflowCreateId}
|
|
1345
|
+
onChange={(e) => setWorkflowCreateId(e.target.value)}
|
|
1346
|
+
className="rounded-[var(--ck-radius-sm)] border border-white/10 bg-black/20 px-3 py-2 text-sm text-[color:var(--ck-text-primary)]"
|
|
1347
|
+
placeholder="e.g. marketing-cadence"
|
|
1348
|
+
/>
|
|
1349
|
+
<span className="text-xs text-[color:var(--ck-text-tertiary)]">lowercase letters, numbers, dashes</span>
|
|
1350
|
+
</label>
|
|
1351
|
+
|
|
1352
|
+
<label className="grid gap-1">
|
|
1353
|
+
<span className="text-xs text-[color:var(--ck-text-secondary)]">Name</span>
|
|
1354
|
+
<input
|
|
1355
|
+
value={workflowCreateName}
|
|
1356
|
+
onChange={(e) => setWorkflowCreateName(e.target.value)}
|
|
1357
|
+
className="rounded-[var(--ck-radius-sm)] border border-white/10 bg-black/20 px-3 py-2 text-sm text-[color:var(--ck-text-primary)]"
|
|
1358
|
+
placeholder="New workflow"
|
|
1359
|
+
/>
|
|
1360
|
+
</label>
|
|
1361
|
+
</div>
|
|
1362
|
+
|
|
1363
|
+
<div className="mt-4 flex flex-wrap justify-end gap-2">
|
|
1364
|
+
<button
|
|
1365
|
+
type="button"
|
|
1366
|
+
onClick={() => setWorkflowCreateOpen(false)}
|
|
1367
|
+
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"
|
|
1368
|
+
>
|
|
1369
|
+
Cancel
|
|
1370
|
+
</button>
|
|
1371
|
+
<button
|
|
1372
|
+
type="button"
|
|
1373
|
+
onClick={() => {
|
|
1374
|
+
const safeId = String(workflowCreateId || "").trim();
|
|
1375
|
+
if (!safeId) {
|
|
1376
|
+
flashMessage("Workflow id is required", "error");
|
|
1377
|
+
return;
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1380
|
+
const workflow: WorkflowFileV1 = {
|
|
1381
|
+
schema: "clawkitchen.workflow.v1",
|
|
1382
|
+
id: safeId,
|
|
1383
|
+
name: String(workflowCreateName || safeId),
|
|
1384
|
+
version: 1,
|
|
1385
|
+
timezone: "America/New_York",
|
|
1386
|
+
triggers: [],
|
|
1387
|
+
nodes: [
|
|
1388
|
+
{ id: "start", type: "start", name: "Start", x: 120, y: 120 },
|
|
1389
|
+
{ id: "end", type: "end", name: "End", x: 420, y: 120 },
|
|
1390
|
+
],
|
|
1391
|
+
edges: [{ id: "e1", from: "start", to: "end" }],
|
|
1392
|
+
meta: {},
|
|
1393
|
+
};
|
|
1394
|
+
|
|
1395
|
+
setSelectedWorkflowFile(`${safeId}.workflow.json`);
|
|
1396
|
+
setWorkflowJsonText(JSON.stringify(workflow, null, 2) + "\n");
|
|
1397
|
+
setWorkflowCreateOpen(false);
|
|
1398
|
+
setWorkflowEditorOpen(true);
|
|
1399
|
+
}}
|
|
1400
|
+
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)]"
|
|
1401
|
+
>
|
|
1402
|
+
Create
|
|
1403
|
+
</button>
|
|
1404
|
+
</div>
|
|
1405
|
+
</div>
|
|
1406
|
+
</div>
|
|
1407
|
+
) : null}
|
|
1408
|
+
|
|
1409
|
+
<div className="mt-4 text-xs font-medium text-[color:var(--ck-text-secondary)]">Files</div>
|
|
1410
|
+
<ul className="mt-2 space-y-1">
|
|
1411
|
+
{workflowFilesLoading ? <li className="text-sm text-[color:var(--ck-text-secondary)]">Loading…</li> : null}
|
|
1412
|
+
{workflowFiles.length ? (
|
|
1413
|
+
workflowFiles.map((f) => {
|
|
1414
|
+
const isActive = selectedWorkflowFile === f;
|
|
1415
|
+
return (
|
|
1416
|
+
<li key={f} className="flex items-center justify-between gap-3 rounded-[var(--ck-radius-sm)] px-2 py-1 hover:bg-white/5">
|
|
1417
|
+
<button
|
|
1418
|
+
type="button"
|
|
1419
|
+
onClick={() => setSelectedWorkflowFile(f)}
|
|
1420
|
+
className="flex min-w-0 items-center gap-2 text-left"
|
|
1421
|
+
>
|
|
1422
|
+
<span className={isActive ? "text-green-400" : "text-white/25"}>{isActive ? "✓" : "•"}</span>
|
|
1423
|
+
<span
|
|
1424
|
+
className={
|
|
1425
|
+
isActive
|
|
1426
|
+
? "truncate text-base font-medium text-[color:var(--ck-text-primary)]"
|
|
1427
|
+
: "truncate text-base text-[color:var(--ck-text-secondary)]"
|
|
1428
|
+
}
|
|
1429
|
+
>
|
|
1430
|
+
{f}
|
|
1431
|
+
</span>
|
|
1432
|
+
</button>
|
|
1433
|
+
|
|
1434
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
1435
|
+
<button
|
|
1436
|
+
type="button"
|
|
1437
|
+
disabled={workflowSaving}
|
|
1438
|
+
onClick={async () => {
|
|
1439
|
+
setSelectedWorkflowFile(f);
|
|
1440
|
+
setWorkflowSaving(true);
|
|
1441
|
+
setWorkflowFilesError("");
|
|
1442
|
+
try {
|
|
1443
|
+
const id = String(f).replace(/\.workflow\.json$/i, "");
|
|
1444
|
+
const res = await fetch(
|
|
1445
|
+
`/api/teams/workflows?teamId=${encodeURIComponent(teamId)}&id=${encodeURIComponent(id)}`,
|
|
1446
|
+
{ cache: "no-store" }
|
|
1447
|
+
);
|
|
1448
|
+
const json = await res.json();
|
|
1449
|
+
if (!res.ok || !json.ok) throw new Error(json.error || "Failed to load workflow");
|
|
1450
|
+
setWorkflowJsonText(JSON.stringify(json.workflow, null, 2) + "\n");
|
|
1451
|
+
setWorkflowEditorOpen(true);
|
|
1452
|
+
} catch (e: unknown) {
|
|
1453
|
+
setWorkflowFilesError(e instanceof Error ? e.message : String(e));
|
|
1454
|
+
} finally {
|
|
1455
|
+
setWorkflowSaving(false);
|
|
1456
|
+
}
|
|
1457
|
+
}}
|
|
1458
|
+
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"
|
|
1459
|
+
>
|
|
1460
|
+
Edit
|
|
1461
|
+
</button>
|
|
1462
|
+
|
|
1463
|
+
<button
|
|
1464
|
+
type="button"
|
|
1465
|
+
disabled={workflowSaving}
|
|
1466
|
+
onClick={async () => {
|
|
1467
|
+
if (!confirm(`Delete workflow ${f}?`)) return;
|
|
1468
|
+
setWorkflowSaving(true);
|
|
1469
|
+
setWorkflowFilesError("");
|
|
1470
|
+
try {
|
|
1471
|
+
const id = String(f).replace(/\.workflow\.json$/i, "");
|
|
1472
|
+
const res = await fetch(
|
|
1473
|
+
`/api/teams/workflows?teamId=${encodeURIComponent(teamId)}&id=${encodeURIComponent(id)}`,
|
|
1474
|
+
{ method: "DELETE" }
|
|
1475
|
+
);
|
|
1476
|
+
const json = await res.json();
|
|
1477
|
+
if (!res.ok || !json.ok) throw new Error(json.error || "Failed to delete workflow");
|
|
1478
|
+
|
|
1479
|
+
const listRes = await fetch(`/api/teams/workflows?teamId=${encodeURIComponent(teamId)}`, { cache: "no-store" });
|
|
1480
|
+
const listJson = await listRes.json();
|
|
1481
|
+
if (listRes.ok && listJson.ok) {
|
|
1482
|
+
const files = Array.isArray(listJson.files) ? listJson.files : [];
|
|
1483
|
+
const list = files.map((x: unknown) => String(x ?? "").trim()).filter((x: string) => Boolean(x));
|
|
1484
|
+
setWorkflowFiles(list);
|
|
1485
|
+
}
|
|
1486
|
+
|
|
1487
|
+
if (selectedWorkflowFile === f) {
|
|
1488
|
+
setSelectedWorkflowFile("");
|
|
1489
|
+
setWorkflowJsonText("");
|
|
1490
|
+
}
|
|
1491
|
+
|
|
1492
|
+
flashMessage(`Deleted workflow: ${f}`, "success");
|
|
1493
|
+
} catch (e: unknown) {
|
|
1494
|
+
setWorkflowFilesError(e instanceof Error ? e.message : String(e));
|
|
1495
|
+
} finally {
|
|
1496
|
+
setWorkflowSaving(false);
|
|
1497
|
+
}
|
|
1498
|
+
}}
|
|
1499
|
+
className="rounded-[var(--ck-radius-sm)] border border-red-400/30 bg-red-500/10 px-3 py-2 text-sm font-medium text-red-100 shadow-[var(--ck-shadow-1)] hover:bg-red-500/20 disabled:opacity-50"
|
|
1500
|
+
>
|
|
1501
|
+
Delete
|
|
1502
|
+
</button>
|
|
1503
|
+
</div>
|
|
1504
|
+
</li>
|
|
1505
|
+
);
|
|
1506
|
+
})
|
|
1507
|
+
) : !workflowFilesLoading ? (
|
|
1508
|
+
<li className="text-sm text-[color:var(--ck-text-secondary)]">No workflows yet.</li>
|
|
1509
|
+
) : null}
|
|
1510
|
+
</ul>
|
|
1511
|
+
</div>
|
|
1512
|
+
|
|
1513
|
+
{workflowEditorOpen ? (
|
|
1514
|
+
<div className="fixed inset-0 z-50 bg-black/70">
|
|
1515
|
+
<div className="ck-glass-strong h-full w-full overflow-hidden border border-white/10 shadow-[var(--ck-shadow-2)]">
|
|
1516
|
+
<div className="flex items-center justify-between border-b border-white/10 bg-black/20 p-3">
|
|
1517
|
+
<div className="text-sm font-medium text-[color:var(--ck-text-primary)]">
|
|
1518
|
+
Workflow editor{selectedWorkflowFile ? ` — ${selectedWorkflowFile}` : ""}
|
|
1519
|
+
</div>
|
|
1520
|
+
<button
|
|
1521
|
+
type="button"
|
|
1522
|
+
onClick={() => setWorkflowEditorOpen(false)}
|
|
1523
|
+
className="rounded-[var(--ck-radius-sm)] border border-white/10 bg-white/5 px-3 py-2 text-xs font-medium text-[color:var(--ck-text-primary)] hover:bg-white/10"
|
|
1524
|
+
>
|
|
1525
|
+
Close
|
|
1526
|
+
</button>
|
|
1527
|
+
</div>
|
|
1528
|
+
|
|
1529
|
+
<div className="h-[calc(100%-3rem)] overflow-auto p-4">
|
|
1530
|
+
<div className="ck-glass-strong p-4 lg:col-span-2">
|
|
1531
|
+
<div className="flex flex-wrap items-center justify-between gap-3">
|
|
1532
|
+
<div className="text-sm font-medium text-[color:var(--ck-text-primary)]">Workflow editor</div>
|
|
1533
|
+
|
|
1534
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
1535
|
+
<div className="flex overflow-hidden rounded-[var(--ck-radius-sm)] border border-white/10">
|
|
1536
|
+
<button
|
|
1537
|
+
type="button"
|
|
1538
|
+
onClick={() => setWorkflowView("canvas")}
|
|
1539
|
+
className={
|
|
1540
|
+
workflowView === "canvas"
|
|
1541
|
+
? "bg-white/10 px-3 py-2 text-xs font-medium text-[color:var(--ck-text-primary)]"
|
|
1542
|
+
: "bg-transparent px-3 py-2 text-xs font-medium text-[color:var(--ck-text-secondary)] hover:bg-white/5"
|
|
1543
|
+
}
|
|
1544
|
+
>
|
|
1545
|
+
Canvas
|
|
1546
|
+
</button>
|
|
1547
|
+
<button
|
|
1548
|
+
type="button"
|
|
1549
|
+
onClick={() => setWorkflowView("json")}
|
|
1550
|
+
className={
|
|
1551
|
+
workflowView === "json"
|
|
1552
|
+
? "bg-white/10 px-3 py-2 text-xs font-medium text-[color:var(--ck-text-primary)]"
|
|
1553
|
+
: "bg-transparent px-3 py-2 text-xs font-medium text-[color:var(--ck-text-secondary)] hover:bg-white/5"
|
|
1554
|
+
}
|
|
1555
|
+
>
|
|
1556
|
+
JSON
|
|
1557
|
+
</button>
|
|
1558
|
+
</div>
|
|
1559
|
+
|
|
1560
|
+
<input
|
|
1561
|
+
ref={workflowImportInputRef}
|
|
1562
|
+
type="file"
|
|
1563
|
+
accept="application/json,.json"
|
|
1564
|
+
className="hidden"
|
|
1565
|
+
onChange={async (e) => {
|
|
1566
|
+
const file = e.target.files?.[0];
|
|
1567
|
+
// Reset the input so re-importing the same file still triggers onChange.
|
|
1568
|
+
e.target.value = "";
|
|
1569
|
+
if (!file) return;
|
|
1570
|
+
|
|
1571
|
+
setWorkflowFilesError("");
|
|
1572
|
+
try {
|
|
1573
|
+
const text = await file.text();
|
|
1574
|
+
const parsed = JSON.parse(text) as WorkflowFileV1;
|
|
1575
|
+
setWorkflowJsonText(JSON.stringify(parsed, null, 2) + "\n");
|
|
1576
|
+
setSelectedWorkflowFile("");
|
|
1577
|
+
flashMessage(`Imported workflow JSON: ${parsed.id || file.name}`, "success");
|
|
1578
|
+
} catch (err: unknown) {
|
|
1579
|
+
setWorkflowFilesError(err instanceof Error ? err.message : String(err));
|
|
1580
|
+
}
|
|
1581
|
+
}}
|
|
1582
|
+
/>
|
|
1583
|
+
|
|
1584
|
+
<button
|
|
1585
|
+
type="button"
|
|
1586
|
+
disabled={workflowSaving}
|
|
1587
|
+
onClick={() => workflowImportInputRef.current?.click()}
|
|
1588
|
+
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"
|
|
1589
|
+
>
|
|
1590
|
+
Import
|
|
1591
|
+
</button>
|
|
1592
|
+
|
|
1593
|
+
<button
|
|
1594
|
+
type="button"
|
|
1595
|
+
disabled={workflowSaving || !workflowParsed || Boolean(workflowParseError) || workflowValidation.errors.length > 0}
|
|
1596
|
+
onClick={() => {
|
|
1597
|
+
setWorkflowFilesError("");
|
|
1598
|
+
try {
|
|
1599
|
+
const wf = workflowParsed;
|
|
1600
|
+
if (!wf) throw new Error("No workflow loaded");
|
|
1601
|
+
if (workflowParseError) throw new Error(`Invalid JSON: ${workflowParseError}`);
|
|
1602
|
+
if (workflowValidation.errors.length) throw new Error("Fix workflow validation errors before exporting");
|
|
1603
|
+
|
|
1604
|
+
const filename = `${wf.id || "workflow"}.workflow.json`;
|
|
1605
|
+
const blob = new Blob([JSON.stringify(wf, null, 2) + "\n"], { type: "application/json" });
|
|
1606
|
+
const url = URL.createObjectURL(blob);
|
|
1607
|
+
const a = document.createElement("a");
|
|
1608
|
+
a.href = url;
|
|
1609
|
+
a.download = filename;
|
|
1610
|
+
document.body.appendChild(a);
|
|
1611
|
+
a.click();
|
|
1612
|
+
a.remove();
|
|
1613
|
+
URL.revokeObjectURL(url);
|
|
1614
|
+
flashMessage(`Exported: ${filename}`, "success");
|
|
1615
|
+
} catch (err: unknown) {
|
|
1616
|
+
setWorkflowFilesError(err instanceof Error ? err.message : String(err));
|
|
1617
|
+
}
|
|
1618
|
+
}}
|
|
1619
|
+
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"
|
|
1620
|
+
>
|
|
1621
|
+
Export
|
|
1622
|
+
</button>
|
|
1623
|
+
|
|
1624
|
+
<button
|
|
1625
|
+
disabled={workflowSaving || !workflowParsed || Boolean(workflowParseError) || workflowValidation.errors.length > 0}
|
|
1626
|
+
onClick={async () => {
|
|
1627
|
+
setWorkflowSaving(true);
|
|
1628
|
+
setWorkflowFilesError("");
|
|
1629
|
+
try {
|
|
1630
|
+
const wf = workflowParsed;
|
|
1631
|
+
if (!wf) throw new Error("No workflow loaded");
|
|
1632
|
+
if (workflowParseError) throw new Error(`Invalid JSON: ${workflowParseError}`);
|
|
1633
|
+
if (workflowValidation.errors.length) throw new Error("Fix workflow validation errors before saving");
|
|
1634
|
+
|
|
1635
|
+
const res = await fetch("/api/teams/workflows", {
|
|
1636
|
+
method: "POST",
|
|
1637
|
+
headers: { "content-type": "application/json" },
|
|
1638
|
+
body: JSON.stringify({ teamId, workflow: wf }),
|
|
1639
|
+
});
|
|
1640
|
+
const json = await res.json();
|
|
1641
|
+
if (!res.ok || !json.ok) throw new Error(json.error || "Failed to save workflow");
|
|
1642
|
+
flashMessage(`Saved workflow: ${wf.id}`, "success");
|
|
1643
|
+
|
|
1644
|
+
const listRes = await fetch(`/api/teams/workflows?teamId=${encodeURIComponent(teamId)}`, { cache: "no-store" });
|
|
1645
|
+
const listJson = await listRes.json();
|
|
1646
|
+
if (listRes.ok && listJson.ok) {
|
|
1647
|
+
const files = Array.isArray(listJson.files) ? listJson.files : [];
|
|
1648
|
+
const list = files.map((f: unknown) => String(f ?? "").trim()).filter((f: string) => Boolean(f));
|
|
1649
|
+
setWorkflowFiles(list);
|
|
1650
|
+
setSelectedWorkflowFile(`${wf.id}.workflow.json`);
|
|
1651
|
+
}
|
|
1652
|
+
} catch (e: unknown) {
|
|
1653
|
+
setWorkflowFilesError(e instanceof Error ? e.message : String(e));
|
|
1654
|
+
} finally {
|
|
1655
|
+
setWorkflowSaving(false);
|
|
1656
|
+
}
|
|
1657
|
+
}}
|
|
1658
|
+
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"
|
|
1659
|
+
>
|
|
1660
|
+
{workflowSaving ? "Saving…" : "Save"}
|
|
1661
|
+
</button>
|
|
1662
|
+
</div>
|
|
1663
|
+
</div>
|
|
1664
|
+
|
|
1665
|
+
{workflowParseError ? (
|
|
1666
|
+
<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">
|
|
1667
|
+
JSON parse error: {workflowParseError}
|
|
1668
|
+
</div>
|
|
1669
|
+
) : null}
|
|
1670
|
+
|
|
1671
|
+
{!workflowParseError && workflowValidation.errors.length ? (
|
|
1672
|
+
<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">
|
|
1673
|
+
<div className="font-medium">Workflow validation errors</div>
|
|
1674
|
+
<ul className="mt-2 list-disc space-y-1 pl-5">
|
|
1675
|
+
{workflowValidation.errors.map((e) => (
|
|
1676
|
+
<li key={e}>{e}</li>
|
|
1677
|
+
))}
|
|
1678
|
+
</ul>
|
|
1679
|
+
</div>
|
|
1680
|
+
) : null}
|
|
1681
|
+
|
|
1682
|
+
{!workflowParseError && !workflowValidation.errors.length && workflowValidation.warnings.length ? (
|
|
1683
|
+
<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">
|
|
1684
|
+
<div className="font-medium">Workflow validation warnings</div>
|
|
1685
|
+
<ul className="mt-2 list-disc space-y-1 pl-5">
|
|
1686
|
+
{workflowValidation.warnings.map((w) => (
|
|
1687
|
+
<li key={w}>{w}</li>
|
|
1688
|
+
))}
|
|
1689
|
+
</ul>
|
|
1690
|
+
</div>
|
|
1691
|
+
) : null}
|
|
1692
|
+
|
|
1693
|
+
{workflowView === "canvas" ? (
|
|
1694
|
+
<div className="mt-3 grid grid-cols-1 gap-3 lg:grid-cols-4">
|
|
1695
|
+
<div className="lg:col-span-3">
|
|
1696
|
+
<div
|
|
1697
|
+
ref={workflowCanvasRef}
|
|
1698
|
+
className="relative h-[calc(100vh-20rem)] min-h-[55vh] w-full overflow-auto rounded-[var(--ck-radius-sm)] border border-white/10 bg-black/20"
|
|
1699
|
+
>
|
|
1700
|
+
<div className="relative h-[900px] w-[1400px]">
|
|
1701
|
+
<svg className="absolute inset-0" width={1400} height={900}>
|
|
1702
|
+
{(workflowParsed?.edges ?? []).map((e) => {
|
|
1703
|
+
const wf = workflowParsed;
|
|
1704
|
+
if (!wf) return null;
|
|
1705
|
+
const a = wf.nodes.find((n) => n.id === e.from);
|
|
1706
|
+
const b = wf.nodes.find((n) => n.id === e.to);
|
|
1707
|
+
if (!a || !b) return null;
|
|
1708
|
+
|
|
1709
|
+
const ax = (typeof a.x === "number" ? a.x : 80) + 90;
|
|
1710
|
+
const ay = (typeof a.y === "number" ? a.y : 80) + 24;
|
|
1711
|
+
const bx = (typeof b.x === "number" ? b.x : 80) + 90;
|
|
1712
|
+
const by = (typeof b.y === "number" ? b.y : 80) + 24;
|
|
1713
|
+
|
|
1714
|
+
return (
|
|
1715
|
+
<g key={e.id}>
|
|
1716
|
+
<line x1={ax} y1={ay} x2={bx} y2={by} stroke="rgba(255,255,255,0.18)" strokeWidth={2} />
|
|
1717
|
+
{e.label ? (
|
|
1718
|
+
<text x={(ax + bx) / 2} y={(ay + by) / 2 - 6} fill="rgba(255,255,255,0.55)" fontSize={10} textAnchor="middle">
|
|
1719
|
+
{e.label}
|
|
1720
|
+
</text>
|
|
1721
|
+
) : null}
|
|
1722
|
+
</g>
|
|
1723
|
+
);
|
|
1724
|
+
})}
|
|
1725
|
+
</svg>
|
|
1726
|
+
|
|
1727
|
+
{(workflowParsed?.nodes ?? []).map((n, idx) => {
|
|
1728
|
+
const x = typeof n.x === "number" ? n.x : 80 + idx * 220;
|
|
1729
|
+
const y = typeof n.y === "number" ? n.y : 80;
|
|
1730
|
+
const selected = workflowSelectedNodeId === n.id;
|
|
1731
|
+
|
|
1732
|
+
return (
|
|
1733
|
+
<div
|
|
1734
|
+
key={n.id}
|
|
1735
|
+
role="button"
|
|
1736
|
+
tabIndex={0}
|
|
1737
|
+
onClick={() => setWorkflowSelectedNodeId(n.id)}
|
|
1738
|
+
onPointerDown={(e) => {
|
|
1739
|
+
if (e.button !== 0) return;
|
|
1740
|
+
const el = workflowCanvasRef.current;
|
|
1741
|
+
if (!el) return;
|
|
1742
|
+
const rect = el.getBoundingClientRect();
|
|
1743
|
+
const dx = e.clientX - rect.left - x;
|
|
1744
|
+
const dy = e.clientY - rect.top - y;
|
|
1745
|
+
setWorkflowSelectedNodeId(n.id);
|
|
1746
|
+
setWorkflowDragging({ nodeId: n.id, dx, dy, containerLeft: rect.left, containerTop: rect.top });
|
|
1747
|
+
}}
|
|
1748
|
+
className={
|
|
1749
|
+
selected
|
|
1750
|
+
? "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)]"
|
|
1751
|
+
: "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"
|
|
1752
|
+
}
|
|
1753
|
+
style={{ left: x, top: y, width: 180 }}
|
|
1754
|
+
>
|
|
1755
|
+
<div className="font-medium text-[color:var(--ck-text-primary)]">{n.name || n.id}</div>
|
|
1756
|
+
<div className="mt-0.5 text-[10px] uppercase tracking-wide text-[color:var(--ck-text-tertiary)]">{n.type}</div>
|
|
1757
|
+
</div>
|
|
1758
|
+
);
|
|
1759
|
+
})}
|
|
1760
|
+
</div>
|
|
1761
|
+
</div>
|
|
1762
|
+
|
|
1763
|
+
<div className="mt-2 text-xs text-[color:var(--ck-text-tertiary)]">
|
|
1764
|
+
Tip: click a node to inspect it; drag to reposition. Positions are stored in the workflow JSON (file-first).
|
|
1765
|
+
</div>
|
|
1766
|
+
</div>
|
|
1767
|
+
|
|
1768
|
+
<div className="rounded-[var(--ck-radius-sm)] border border-white/10 bg-black/20 p-3 lg:col-span-1">
|
|
1769
|
+
<div className="text-xs font-medium text-[color:var(--ck-text-secondary)]">Workflow</div>
|
|
1770
|
+
|
|
1771
|
+
{workflowParsed ? (
|
|
1772
|
+
(() => {
|
|
1773
|
+
const wf = workflowParsed;
|
|
1774
|
+
const tz = String(wf.timezone ?? "").trim() || "UTC";
|
|
1775
|
+
const triggers = wf.triggers ?? [];
|
|
1776
|
+
|
|
1777
|
+
const meta = wf.meta && typeof wf.meta === "object" && !Array.isArray(wf.meta) ? (wf.meta as Record<string, unknown>) : {};
|
|
1778
|
+
const approvalProvider = String(meta.approvalProvider ?? "telegram").trim() || "telegram";
|
|
1779
|
+
const approvalTarget = String(meta.approvalTarget ?? "").trim();
|
|
1780
|
+
|
|
1781
|
+
const presets = [
|
|
1782
|
+
{ label: "(no preset)", expr: "" },
|
|
1783
|
+
{ label: "Mon/Wed/Fri 09:00 local", expr: "0 9 * * 1,3,5" },
|
|
1784
|
+
{ label: "Daily 08:00 local", expr: "0 8 * * *" },
|
|
1785
|
+
{ label: "Mon 09:30 local", expr: "30 9 * * 1" },
|
|
1786
|
+
];
|
|
1787
|
+
|
|
1788
|
+
return (
|
|
1789
|
+
<div className="mt-2 space-y-4">
|
|
1790
|
+
<label className="block">
|
|
1791
|
+
<div className="text-[10px] uppercase tracking-wide text-[color:var(--ck-text-tertiary)]">timezone</div>
|
|
1792
|
+
<input
|
|
1793
|
+
value={tz}
|
|
1794
|
+
onChange={(e) => {
|
|
1795
|
+
const nextTz = String(e.target.value || "").trim() || "UTC";
|
|
1796
|
+
const next: WorkflowFileV1 = { ...wf, timezone: nextTz };
|
|
1797
|
+
setWorkflowJsonText(JSON.stringify(next, null, 2) + "\n");
|
|
1798
|
+
}}
|
|
1799
|
+
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)]"
|
|
1800
|
+
placeholder="America/New_York"
|
|
1801
|
+
/>
|
|
1802
|
+
</label>
|
|
1803
|
+
|
|
1804
|
+
<div className="rounded-[var(--ck-radius-sm)] border border-white/10 bg-black/25 p-2">
|
|
1805
|
+
<div className="text-[10px] uppercase tracking-wide text-[color:var(--ck-text-tertiary)]">approval channel (mvp)</div>
|
|
1806
|
+
<div className="mt-2 space-y-2">
|
|
1807
|
+
<label className="block">
|
|
1808
|
+
<div className="text-[10px] uppercase tracking-wide text-[color:var(--ck-text-tertiary)]">provider</div>
|
|
1809
|
+
<input
|
|
1810
|
+
value={approvalProvider}
|
|
1811
|
+
onChange={(e) => {
|
|
1812
|
+
const nextProvider = String(e.target.value || "").trim() || "telegram";
|
|
1813
|
+
const nextMeta = { ...meta, approvalProvider: nextProvider };
|
|
1814
|
+
const next: WorkflowFileV1 = { ...wf, meta: nextMeta };
|
|
1815
|
+
setWorkflowJsonText(JSON.stringify(next, null, 2) + "\n");
|
|
1816
|
+
}}
|
|
1817
|
+
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)]"
|
|
1818
|
+
placeholder="telegram"
|
|
1819
|
+
/>
|
|
1820
|
+
</label>
|
|
1821
|
+
|
|
1822
|
+
<label className="block">
|
|
1823
|
+
<div className="text-[10px] uppercase tracking-wide text-[color:var(--ck-text-tertiary)]">target</div>
|
|
1824
|
+
<input
|
|
1825
|
+
value={approvalTarget}
|
|
1826
|
+
onChange={(e) => {
|
|
1827
|
+
const nextTarget = String(e.target.value || "").trim();
|
|
1828
|
+
const nextMeta = { ...meta, approvalTarget: nextTarget };
|
|
1829
|
+
const next: WorkflowFileV1 = { ...wf, meta: nextMeta };
|
|
1830
|
+
setWorkflowJsonText(JSON.stringify(next, null, 2) + "\n");
|
|
1831
|
+
}}
|
|
1832
|
+
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)]"
|
|
1833
|
+
placeholder="(e.g. Telegram chat id)"
|
|
1834
|
+
/>
|
|
1835
|
+
<div className="mt-1 text-[10px] text-[color:var(--ck-text-tertiary)]">
|
|
1836
|
+
If set, sample runs that reach a human-approval node will send an approval packet via the gateway message tool.
|
|
1837
|
+
</div>
|
|
1838
|
+
</label>
|
|
1839
|
+
</div>
|
|
1840
|
+
</div>
|
|
1841
|
+
|
|
1842
|
+
<div>
|
|
1843
|
+
<div className="flex items-center justify-between gap-2">
|
|
1844
|
+
<div className="text-[10px] uppercase tracking-wide text-[color:var(--ck-text-tertiary)]">triggers</div>
|
|
1845
|
+
<button
|
|
1846
|
+
type="button"
|
|
1847
|
+
onClick={() => {
|
|
1848
|
+
const id = `t${Date.now()}`;
|
|
1849
|
+
const next: WorkflowFileV1 = {
|
|
1850
|
+
...wf,
|
|
1851
|
+
triggers: [
|
|
1852
|
+
...triggers,
|
|
1853
|
+
{ kind: "cron", id, name: "New trigger", enabled: true, expr: "0 9 * * 1-5", tz },
|
|
1854
|
+
],
|
|
1855
|
+
};
|
|
1856
|
+
setWorkflowJsonText(JSON.stringify(next, null, 2) + "\n");
|
|
1857
|
+
}}
|
|
1858
|
+
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"
|
|
1859
|
+
>
|
|
1860
|
+
+ Add
|
|
1861
|
+
</button>
|
|
1862
|
+
</div>
|
|
1863
|
+
|
|
1864
|
+
<div className="mt-2 space-y-2">
|
|
1865
|
+
{triggers.length ? (
|
|
1866
|
+
triggers.map((t, i) => {
|
|
1867
|
+
const kind = (t as { kind?: unknown }).kind;
|
|
1868
|
+
const isCron = kind === "cron";
|
|
1869
|
+
const id = String((t as { id?: unknown }).id ?? "");
|
|
1870
|
+
const name = String((t as { name?: unknown }).name ?? "");
|
|
1871
|
+
const enabled = Boolean((t as { enabled?: unknown }).enabled);
|
|
1872
|
+
const expr = String((t as { expr?: unknown }).expr ?? "");
|
|
1873
|
+
const trigTz = String((t as { tz?: unknown }).tz ?? tz);
|
|
1874
|
+
const cronFields = expr.trim().split(/\s+/).filter(Boolean);
|
|
1875
|
+
const cronLooksValid = !expr.trim() || cronFields.length === 5;
|
|
1876
|
+
|
|
1877
|
+
return (
|
|
1878
|
+
<div key={`${id}-${i}`} className="rounded-[var(--ck-radius-sm)] border border-white/10 bg-black/25 p-2">
|
|
1879
|
+
<div className="flex items-center justify-between gap-2">
|
|
1880
|
+
<div className="text-xs text-[color:var(--ck-text-primary)]">{name || id || `trigger-${i + 1}`}</div>
|
|
1881
|
+
<button
|
|
1882
|
+
type="button"
|
|
1883
|
+
onClick={() => {
|
|
1884
|
+
const next: WorkflowFileV1 = { ...wf, triggers: triggers.filter((_, idx) => idx !== i) };
|
|
1885
|
+
setWorkflowJsonText(JSON.stringify(next, null, 2) + "\n");
|
|
1886
|
+
}}
|
|
1887
|
+
className="text-[10px] text-[color:var(--ck-text-tertiary)] hover:text-[color:var(--ck-text-primary)]"
|
|
1888
|
+
>
|
|
1889
|
+
Remove
|
|
1890
|
+
</button>
|
|
1891
|
+
</div>
|
|
1892
|
+
|
|
1893
|
+
{!isCron ? (
|
|
1894
|
+
<div className="mt-1 text-xs text-[color:var(--ck-text-secondary)]">Unsupported trigger kind: {String(kind)}</div>
|
|
1895
|
+
) : null}
|
|
1896
|
+
|
|
1897
|
+
<div className="mt-2 grid grid-cols-1 gap-2">
|
|
1898
|
+
<label className="flex items-center gap-2 text-xs text-[color:var(--ck-text-secondary)]">
|
|
1899
|
+
<input
|
|
1900
|
+
type="checkbox"
|
|
1901
|
+
checked={enabled}
|
|
1902
|
+
onChange={(e) => {
|
|
1903
|
+
const nextEnabled = e.target.checked;
|
|
1904
|
+
const next: WorkflowFileV1 = {
|
|
1905
|
+
...wf,
|
|
1906
|
+
triggers: triggers.map((x, idx) =>
|
|
1907
|
+
idx === i ? (x.kind === "cron" ? { ...x, enabled: nextEnabled } : x) : x
|
|
1908
|
+
),
|
|
1909
|
+
};
|
|
1910
|
+
setWorkflowJsonText(JSON.stringify(next, null, 2) + "\n");
|
|
1911
|
+
}}
|
|
1912
|
+
/>
|
|
1913
|
+
Enabled
|
|
1914
|
+
</label>
|
|
1915
|
+
|
|
1916
|
+
<label className="block">
|
|
1917
|
+
<div className="text-[10px] uppercase tracking-wide text-[color:var(--ck-text-tertiary)]">name</div>
|
|
1918
|
+
<input
|
|
1919
|
+
value={name}
|
|
1920
|
+
onChange={(e) => {
|
|
1921
|
+
const nextName = e.target.value;
|
|
1922
|
+
const next: WorkflowFileV1 = {
|
|
1923
|
+
...wf,
|
|
1924
|
+
triggers: triggers.map((x, idx) =>
|
|
1925
|
+
idx === i ? (x.kind === "cron" ? { ...x, name: nextName } : x) : x
|
|
1926
|
+
),
|
|
1927
|
+
};
|
|
1928
|
+
setWorkflowJsonText(JSON.stringify(next, null, 2) + "\n");
|
|
1929
|
+
}}
|
|
1930
|
+
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)]"
|
|
1931
|
+
placeholder="Content cadence"
|
|
1932
|
+
/>
|
|
1933
|
+
</label>
|
|
1934
|
+
|
|
1935
|
+
<label className="block">
|
|
1936
|
+
<div className="text-[10px] uppercase tracking-wide text-[color:var(--ck-text-tertiary)]">schedule (cron)</div>
|
|
1937
|
+
<input
|
|
1938
|
+
value={expr}
|
|
1939
|
+
onChange={(e) => {
|
|
1940
|
+
const nextExpr = e.target.value;
|
|
1941
|
+
const next: WorkflowFileV1 = {
|
|
1942
|
+
...wf,
|
|
1943
|
+
triggers: triggers.map((x, idx) =>
|
|
1944
|
+
idx === i ? (x.kind === "cron" ? { ...x, expr: nextExpr } : x) : x
|
|
1945
|
+
),
|
|
1946
|
+
};
|
|
1947
|
+
setWorkflowJsonText(JSON.stringify(next, null, 2) + "\n");
|
|
1948
|
+
}}
|
|
1949
|
+
className={
|
|
1950
|
+
cronLooksValid
|
|
1951
|
+
? "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)]"
|
|
1952
|
+
: "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)]"
|
|
1953
|
+
}
|
|
1954
|
+
placeholder="0 9 * * 1,3,5"
|
|
1955
|
+
/>
|
|
1956
|
+
{!cronLooksValid ? (
|
|
1957
|
+
<div className="mt-1 text-[10px] text-red-200">
|
|
1958
|
+
Cron should be 5 fields (min hour dom month dow). You entered {cronFields.length}.
|
|
1959
|
+
</div>
|
|
1960
|
+
) : null}
|
|
1961
|
+
<div className="mt-1 grid grid-cols-1 gap-1">
|
|
1962
|
+
<select
|
|
1963
|
+
value={presets.some((p) => p.expr === expr) ? expr : ""}
|
|
1964
|
+
onChange={(e) => {
|
|
1965
|
+
const nextExpr = e.target.value;
|
|
1966
|
+
if (!nextExpr) return;
|
|
1967
|
+
const next: WorkflowFileV1 = {
|
|
1968
|
+
...wf,
|
|
1969
|
+
triggers: triggers.map((x, idx) =>
|
|
1970
|
+
idx === i ? (x.kind === "cron" ? { ...x, expr: nextExpr } : x) : x
|
|
1971
|
+
),
|
|
1972
|
+
};
|
|
1973
|
+
setWorkflowJsonText(JSON.stringify(next, null, 2) + "\n");
|
|
1974
|
+
}}
|
|
1975
|
+
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)]"
|
|
1976
|
+
>
|
|
1977
|
+
{presets.map((p) => (
|
|
1978
|
+
<option key={p.label} value={p.expr}>
|
|
1979
|
+
{p.label}
|
|
1980
|
+
</option>
|
|
1981
|
+
))}
|
|
1982
|
+
</select>
|
|
1983
|
+
<div className="text-[10px] text-[color:var(--ck-text-tertiary)]">Presets set the cron; edit freely for advanced schedules.</div>
|
|
1984
|
+
</div>
|
|
1985
|
+
</label>
|
|
1986
|
+
|
|
1987
|
+
<label className="block">
|
|
1988
|
+
<div className="text-[10px] uppercase tracking-wide text-[color:var(--ck-text-tertiary)]">timezone override</div>
|
|
1989
|
+
<input
|
|
1990
|
+
value={trigTz}
|
|
1991
|
+
onChange={(e) => {
|
|
1992
|
+
const nextTz = String(e.target.value || "").trim() || tz;
|
|
1993
|
+
const next: WorkflowFileV1 = {
|
|
1994
|
+
...wf,
|
|
1995
|
+
triggers: triggers.map((x, idx) =>
|
|
1996
|
+
idx === i ? (x.kind === "cron" ? { ...x, tz: nextTz } : x) : x
|
|
1997
|
+
),
|
|
1998
|
+
};
|
|
1999
|
+
setWorkflowJsonText(JSON.stringify(next, null, 2) + "\n");
|
|
2000
|
+
}}
|
|
2001
|
+
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)]"
|
|
2002
|
+
placeholder={tz}
|
|
2003
|
+
/>
|
|
2004
|
+
</label>
|
|
2005
|
+
</div>
|
|
2006
|
+
</div>
|
|
2007
|
+
);
|
|
2008
|
+
})
|
|
2009
|
+
) : (
|
|
2010
|
+
<div className="text-xs text-[color:var(--ck-text-secondary)]">No triggers yet.</div>
|
|
2011
|
+
)}
|
|
2012
|
+
</div>
|
|
2013
|
+
</div>
|
|
2014
|
+
|
|
2015
|
+
<div className="border-t border-white/10 pt-3">
|
|
2016
|
+
<div className="flex items-center justify-between gap-2">
|
|
2017
|
+
<div className="text-xs font-medium text-[color:var(--ck-text-secondary)]">Runs (history)</div>
|
|
2018
|
+
<div className="flex items-center gap-2">
|
|
2019
|
+
<button
|
|
2020
|
+
type="button"
|
|
2021
|
+
disabled={workflowSaving}
|
|
2022
|
+
onClick={async () => {
|
|
2023
|
+
const wfId = String(wf.id ?? "").trim();
|
|
2024
|
+
if (!wfId) return;
|
|
2025
|
+
setWorkflowRunsError("");
|
|
2026
|
+
try {
|
|
2027
|
+
const res = await fetch("/api/teams/workflow-runs", {
|
|
2028
|
+
method: "POST",
|
|
2029
|
+
headers: { "content-type": "application/json" },
|
|
2030
|
+
body: JSON.stringify({ teamId, workflowId: wfId, mode: "sample" }),
|
|
2031
|
+
});
|
|
2032
|
+
const json = await res.json();
|
|
2033
|
+
if (!res.ok || !json.ok) throw new Error(json.error || "Failed to create sample run");
|
|
2034
|
+
|
|
2035
|
+
const listRes = await fetch(
|
|
2036
|
+
`/api/teams/workflow-runs?teamId=${encodeURIComponent(teamId)}&workflowId=${encodeURIComponent(wfId)}`,
|
|
2037
|
+
{ cache: "no-store" }
|
|
2038
|
+
);
|
|
2039
|
+
const listJson = await listRes.json();
|
|
2040
|
+
if (!listRes.ok || !listJson.ok) throw new Error(listJson.error || "Failed to refresh runs");
|
|
2041
|
+
const files = Array.isArray(listJson.files) ? listJson.files : [];
|
|
2042
|
+
const list = files.map((f: unknown) => String(f ?? "").trim()).filter((f: string) => Boolean(f));
|
|
2043
|
+
setWorkflowRuns(list);
|
|
2044
|
+
flashMessage("Created sample run", "success");
|
|
2045
|
+
} catch (e: unknown) {
|
|
2046
|
+
setWorkflowRunsError(e instanceof Error ? e.message : String(e));
|
|
2047
|
+
}
|
|
2048
|
+
}}
|
|
2049
|
+
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"
|
|
2050
|
+
>
|
|
2051
|
+
+ Sample run
|
|
2052
|
+
</button>
|
|
2053
|
+
</div>
|
|
2054
|
+
</div>
|
|
2055
|
+
|
|
2056
|
+
{workflowRunsError ? (
|
|
2057
|
+
<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">
|
|
2058
|
+
{workflowRunsError}
|
|
2059
|
+
</div>
|
|
2060
|
+
) : null}
|
|
2061
|
+
|
|
2062
|
+
<div className="mt-2 space-y-1">
|
|
2063
|
+
{workflowRunsLoading ? (
|
|
2064
|
+
<div className="text-xs text-[color:var(--ck-text-secondary)]">Loading runs…</div>
|
|
2065
|
+
) : workflowRuns.length ? (
|
|
2066
|
+
workflowRuns.slice(0, 8).map((f) => {
|
|
2067
|
+
const runId = String(f).replace(/\.run\.json$/i, "");
|
|
2068
|
+
const selected = selectedWorkflowRunId === runId;
|
|
2069
|
+
return (
|
|
2070
|
+
<button
|
|
2071
|
+
key={f}
|
|
2072
|
+
type="button"
|
|
2073
|
+
onClick={async () => {
|
|
2074
|
+
const wfId = String(wf.id ?? "").trim();
|
|
2075
|
+
if (!wfId) return;
|
|
2076
|
+
setSelectedWorkflowRunId(runId);
|
|
2077
|
+
setSelectedWorkflowRun(null);
|
|
2078
|
+
setWorkflowRunsError("");
|
|
2079
|
+
try {
|
|
2080
|
+
const res = await fetch(
|
|
2081
|
+
`/api/teams/workflow-runs?teamId=${encodeURIComponent(teamId)}&workflowId=${encodeURIComponent(wfId)}&runId=${encodeURIComponent(runId)}`,
|
|
2082
|
+
{ cache: "no-store" }
|
|
2083
|
+
);
|
|
2084
|
+
const json = await res.json();
|
|
2085
|
+
if (!res.ok || !json.ok) throw new Error(json.error || "Failed to load run");
|
|
2086
|
+
setSelectedWorkflowRun(json.run);
|
|
2087
|
+
} catch (e: unknown) {
|
|
2088
|
+
setWorkflowRunsError(e instanceof Error ? e.message : String(e));
|
|
2089
|
+
}
|
|
2090
|
+
}}
|
|
2091
|
+
className={
|
|
2092
|
+
selected
|
|
2093
|
+
? "w-full rounded-[var(--ck-radius-sm)] bg-white/10 px-2 py-1 text-left text-[11px] text-[color:var(--ck-text-primary)]"
|
|
2094
|
+
: "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"
|
|
2095
|
+
}
|
|
2096
|
+
>
|
|
2097
|
+
{runId}
|
|
2098
|
+
</button>
|
|
2099
|
+
);
|
|
2100
|
+
})
|
|
2101
|
+
) : (
|
|
2102
|
+
<div className="text-xs text-[color:var(--ck-text-secondary)]">No runs yet.</div>
|
|
2103
|
+
)}
|
|
2104
|
+
</div>
|
|
2105
|
+
|
|
2106
|
+
{selectedWorkflowRun ? (
|
|
2107
|
+
(() => {
|
|
2108
|
+
const run =
|
|
2109
|
+
selectedWorkflowRun && typeof selectedWorkflowRun === "object"
|
|
2110
|
+
? (selectedWorkflowRun as Record<string, unknown>)
|
|
2111
|
+
: ({} as Record<string, unknown>);
|
|
2112
|
+
const nodesVal = (run as Record<string, unknown>).nodes;
|
|
2113
|
+
const nodes = Array.isArray(nodesVal) ? (nodesVal as unknown[]) : [];
|
|
2114
|
+
return (
|
|
2115
|
+
<div className="mt-2 rounded-[var(--ck-radius-sm)] border border-white/10 bg-black/25 p-2">
|
|
2116
|
+
<div className="flex flex-wrap items-center justify-between gap-2">
|
|
2117
|
+
<div className="text-[11px] font-medium text-[color:var(--ck-text-primary)]">Run detail</div>
|
|
2118
|
+
<div className="text-[10px] text-[color:var(--ck-text-tertiary)]">
|
|
2119
|
+
<span className="font-mono">{String(run?.status ?? "")}</span>
|
|
2120
|
+
{run?.startedAt ? <span> · {String(run.startedAt)}</span> : null}
|
|
2121
|
+
</div>
|
|
2122
|
+
</div>
|
|
2123
|
+
|
|
2124
|
+
{run?.summary ? (
|
|
2125
|
+
<div className="mt-1 text-[11px] text-[color:var(--ck-text-secondary)]">{String(run.summary)}</div>
|
|
2126
|
+
) : null}
|
|
2127
|
+
|
|
2128
|
+
{(() => {
|
|
2129
|
+
const status = String(run?.status ?? "");
|
|
2130
|
+
const approvalVal = (run as Record<string, unknown>).approval;
|
|
2131
|
+
const approval =
|
|
2132
|
+
approvalVal && typeof approvalVal === "object" ? (approvalVal as Record<string, unknown>) : ({} as Record<string, unknown>);
|
|
2133
|
+
const approvalState = String(approval.state ?? "");
|
|
2134
|
+
const approvalNodeId = String(approval.nodeId ?? "");
|
|
2135
|
+
const canAct = status === "waiting_for_approval" && approvalState === "pending" && approvalNodeId;
|
|
2136
|
+
|
|
2137
|
+
if (!canAct) return null;
|
|
2138
|
+
|
|
2139
|
+
return (
|
|
2140
|
+
<div className="mt-2 rounded-[var(--ck-radius-sm)] border border-amber-300/30 bg-amber-500/10 p-2">
|
|
2141
|
+
<div className="text-[10px] uppercase tracking-wide text-amber-100">approval required</div>
|
|
2142
|
+
<div className="mt-1 text-[11px] text-amber-50">
|
|
2143
|
+
Waiting on <span className="font-mono">{approvalNodeId}</span>
|
|
2144
|
+
</div>
|
|
2145
|
+
<div className="mt-2 flex flex-wrap gap-2">
|
|
2146
|
+
<button
|
|
2147
|
+
type="button"
|
|
2148
|
+
onClick={async () => {
|
|
2149
|
+
const wfId = String(wf.id ?? "").trim();
|
|
2150
|
+
const rId = String((run as Record<string, unknown>).id ?? "").trim();
|
|
2151
|
+
if (!wfId || !rId) return;
|
|
2152
|
+
setWorkflowRunsError("");
|
|
2153
|
+
try {
|
|
2154
|
+
const res = await fetch("/api/teams/workflow-runs", {
|
|
2155
|
+
method: "POST",
|
|
2156
|
+
headers: { "content-type": "application/json" },
|
|
2157
|
+
body: JSON.stringify({ teamId, workflowId: wfId, runId: rId, action: "approve" }),
|
|
2158
|
+
});
|
|
2159
|
+
const json = await res.json();
|
|
2160
|
+
if (!res.ok || !json.ok) throw new Error(json.error || "Failed to approve");
|
|
2161
|
+
|
|
2162
|
+
const detailRes = await fetch(
|
|
2163
|
+
`/api/teams/workflow-runs?teamId=${encodeURIComponent(teamId)}&workflowId=${encodeURIComponent(wfId)}&runId=${encodeURIComponent(rId)}`,
|
|
2164
|
+
{ cache: "no-store" }
|
|
2165
|
+
);
|
|
2166
|
+
const detailJson = await detailRes.json();
|
|
2167
|
+
if (!detailRes.ok || !detailJson.ok) throw new Error(detailJson.error || "Failed to reload run");
|
|
2168
|
+
setSelectedWorkflowRun(detailJson.run);
|
|
2169
|
+
flashMessage("Approved", "success");
|
|
2170
|
+
} catch (e: unknown) {
|
|
2171
|
+
setWorkflowRunsError(e instanceof Error ? e.message : String(e));
|
|
2172
|
+
}
|
|
2173
|
+
}}
|
|
2174
|
+
className="rounded-[var(--ck-radius-sm)] border border-emerald-300/30 bg-emerald-500/10 px-2 py-1 text-[10px] font-medium text-emerald-50 hover:bg-emerald-500/20"
|
|
2175
|
+
>
|
|
2176
|
+
Approve & continue
|
|
2177
|
+
</button>
|
|
2178
|
+
<button
|
|
2179
|
+
type="button"
|
|
2180
|
+
onClick={async () => {
|
|
2181
|
+
const wfId = String(wf.id ?? "").trim();
|
|
2182
|
+
const rId = String((run as Record<string, unknown>).id ?? "").trim();
|
|
2183
|
+
if (!wfId || !rId) return;
|
|
2184
|
+
setWorkflowRunsError("");
|
|
2185
|
+
try {
|
|
2186
|
+
const res = await fetch("/api/teams/workflow-runs", {
|
|
2187
|
+
method: "POST",
|
|
2188
|
+
headers: { "content-type": "application/json" },
|
|
2189
|
+
body: JSON.stringify({ teamId, workflowId: wfId, runId: rId, action: "request_changes" }),
|
|
2190
|
+
});
|
|
2191
|
+
const json = await res.json();
|
|
2192
|
+
if (!res.ok || !json.ok) throw new Error(json.error || "Failed to request changes");
|
|
2193
|
+
|
|
2194
|
+
const detailRes = await fetch(
|
|
2195
|
+
`/api/teams/workflow-runs?teamId=${encodeURIComponent(teamId)}&workflowId=${encodeURIComponent(wfId)}&runId=${encodeURIComponent(rId)}`,
|
|
2196
|
+
{ cache: "no-store" }
|
|
2197
|
+
);
|
|
2198
|
+
const detailJson = await detailRes.json();
|
|
2199
|
+
if (!detailRes.ok || !detailJson.ok) throw new Error(detailJson.error || "Failed to reload run");
|
|
2200
|
+
setSelectedWorkflowRun(detailJson.run);
|
|
2201
|
+
flashMessage("Requested changes", "success");
|
|
2202
|
+
} catch (e: unknown) {
|
|
2203
|
+
setWorkflowRunsError(e instanceof Error ? e.message : String(e));
|
|
2204
|
+
}
|
|
2205
|
+
}}
|
|
2206
|
+
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"
|
|
2207
|
+
>
|
|
2208
|
+
Request changes
|
|
2209
|
+
</button>
|
|
2210
|
+
<button
|
|
2211
|
+
type="button"
|
|
2212
|
+
onClick={async () => {
|
|
2213
|
+
const wfId = String(wf.id ?? "").trim();
|
|
2214
|
+
const rId = String((run as Record<string, unknown>).id ?? "").trim();
|
|
2215
|
+
if (!wfId || !rId) return;
|
|
2216
|
+
setWorkflowRunsError("");
|
|
2217
|
+
try {
|
|
2218
|
+
const res = await fetch("/api/teams/workflow-runs", {
|
|
2219
|
+
method: "POST",
|
|
2220
|
+
headers: { "content-type": "application/json" },
|
|
2221
|
+
body: JSON.stringify({ teamId, workflowId: wfId, runId: rId, action: "cancel" }),
|
|
2222
|
+
});
|
|
2223
|
+
const json = await res.json();
|
|
2224
|
+
if (!res.ok || !json.ok) throw new Error(json.error || "Failed to cancel");
|
|
2225
|
+
|
|
2226
|
+
const detailRes = await fetch(
|
|
2227
|
+
`/api/teams/workflow-runs?teamId=${encodeURIComponent(teamId)}&workflowId=${encodeURIComponent(wfId)}&runId=${encodeURIComponent(rId)}`,
|
|
2228
|
+
{ cache: "no-store" }
|
|
2229
|
+
);
|
|
2230
|
+
const detailJson = await detailRes.json();
|
|
2231
|
+
if (!detailRes.ok || !detailJson.ok) throw new Error(detailJson.error || "Failed to reload run");
|
|
2232
|
+
setSelectedWorkflowRun(detailJson.run);
|
|
2233
|
+
flashMessage("Canceled", "success");
|
|
2234
|
+
} catch (e: unknown) {
|
|
2235
|
+
setWorkflowRunsError(e instanceof Error ? e.message : String(e));
|
|
2236
|
+
}
|
|
2237
|
+
}}
|
|
2238
|
+
className="rounded-[var(--ck-radius-sm)] border border-red-400/30 bg-red-500/10 px-2 py-1 text-[10px] font-medium text-red-50 hover:bg-red-500/20"
|
|
2239
|
+
>
|
|
2240
|
+
Cancel
|
|
2241
|
+
</button>
|
|
2242
|
+
</div>
|
|
2243
|
+
<div className="mt-2 text-[10px] text-amber-100/80">
|
|
2244
|
+
Note: this is currently an in-app approval stub; channel delivery/resume wiring still TBD.
|
|
2245
|
+
</div>
|
|
2246
|
+
</div>
|
|
2247
|
+
);
|
|
2248
|
+
})()}
|
|
2249
|
+
|
|
2250
|
+
<div className="mt-2">
|
|
2251
|
+
<div className="text-[10px] uppercase tracking-wide text-[color:var(--ck-text-tertiary)]">per-node results</div>
|
|
2252
|
+
{nodes.length ? (
|
|
2253
|
+
<div className="mt-1 space-y-1">
|
|
2254
|
+
{nodes.slice(0, 50).map((n, idx) => {
|
|
2255
|
+
const node = n && typeof n === "object" ? (n as Record<string, unknown>) : ({} as Record<string, unknown>);
|
|
2256
|
+
const status = String(node.status ?? "");
|
|
2257
|
+
const statusColor =
|
|
2258
|
+
status === "success"
|
|
2259
|
+
? "text-emerald-200"
|
|
2260
|
+
: status === "error"
|
|
2261
|
+
? "text-red-200"
|
|
2262
|
+
: status === "running" || status === "waiting"
|
|
2263
|
+
? "text-amber-200"
|
|
2264
|
+
: "text-[color:var(--ck-text-secondary)]";
|
|
2265
|
+
const nodeId = String(node.nodeId ?? "");
|
|
2266
|
+
const errVal = node.error;
|
|
2267
|
+
const outputVal = node.output;
|
|
2268
|
+
return (
|
|
2269
|
+
<div key={`${nodeId || "node"}-${idx}`} className="rounded-[var(--ck-radius-sm)] border border-white/10 bg-white/5 p-2">
|
|
2270
|
+
<div className="flex items-center justify-between gap-2">
|
|
2271
|
+
<div className="text-[11px] text-[color:var(--ck-text-primary)]">
|
|
2272
|
+
<span className="font-mono">{nodeId}</span>
|
|
2273
|
+
</div>
|
|
2274
|
+
<div className={`text-[10px] font-medium ${statusColor}`}>{status}</div>
|
|
2275
|
+
</div>
|
|
2276
|
+
{errVal ? (
|
|
2277
|
+
<div className="mt-1 text-[10px] text-red-100">
|
|
2278
|
+
{typeof errVal === "string"
|
|
2279
|
+
? errVal
|
|
2280
|
+
: typeof errVal === "object" && errVal && "message" in errVal
|
|
2281
|
+
? String((errVal as Record<string, unknown>).message ?? "")
|
|
2282
|
+
: String(errVal)}
|
|
2283
|
+
</div>
|
|
2284
|
+
) : null}
|
|
2285
|
+
{typeof outputVal !== "undefined" ? (
|
|
2286
|
+
<pre className="mt-1 max-h-[120px] overflow-auto rounded-[var(--ck-radius-sm)] border border-white/10 bg-black/25 p-2 text-[10px] text-[color:var(--ck-text-secondary)]">
|
|
2287
|
+
{JSON.stringify(outputVal, null, 2)}
|
|
2288
|
+
</pre>
|
|
2289
|
+
) : null}
|
|
2290
|
+
</div>
|
|
2291
|
+
);
|
|
2292
|
+
})}
|
|
2293
|
+
</div>
|
|
2294
|
+
) : (
|
|
2295
|
+
<div className="mt-1 text-xs text-[color:var(--ck-text-secondary)]">No node results recorded on this run yet.</div>
|
|
2296
|
+
)}
|
|
2297
|
+
</div>
|
|
2298
|
+
|
|
2299
|
+
<details className="mt-2">
|
|
2300
|
+
<summary className="cursor-pointer select-none text-[10px] text-[color:var(--ck-text-tertiary)]">raw JSON</summary>
|
|
2301
|
+
<pre className="mt-2 max-h-[220px] overflow-auto rounded-[var(--ck-radius-sm)] border border-white/10 bg-black/25 p-2 text-[10px] text-[color:var(--ck-text-secondary)]">
|
|
2302
|
+
{JSON.stringify(selectedWorkflowRun, null, 2)}
|
|
2303
|
+
</pre>
|
|
2304
|
+
</details>
|
|
2305
|
+
</div>
|
|
2306
|
+
);
|
|
2307
|
+
})()
|
|
2308
|
+
) : null}
|
|
2309
|
+
</div>
|
|
2310
|
+
|
|
2311
|
+
<div className="border-t border-white/10 pt-3">
|
|
2312
|
+
<div className="text-xs font-medium text-[color:var(--ck-text-secondary)]">Nodes</div>
|
|
2313
|
+
|
|
2314
|
+
<div className="mt-2 rounded-[var(--ck-radius-sm)] border border-white/10 bg-black/25 p-2">
|
|
2315
|
+
<div className="grid grid-cols-1 gap-2">
|
|
2316
|
+
<label className="block">
|
|
2317
|
+
<div className="text-[10px] uppercase tracking-wide text-[color:var(--ck-text-tertiary)]">id</div>
|
|
2318
|
+
<input
|
|
2319
|
+
value={workflowNewNodeId}
|
|
2320
|
+
onChange={(e) => setWorkflowNewNodeId(e.target.value)}
|
|
2321
|
+
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)]"
|
|
2322
|
+
placeholder="e.g. draft_assets"
|
|
2323
|
+
/>
|
|
2324
|
+
<div className="mt-1 text-[10px] text-[color:var(--ck-text-tertiary)]">
|
|
2325
|
+
Tip: ids are file-first + portable; use lowercase letters, numbers, and underscores.
|
|
2326
|
+
</div>
|
|
2327
|
+
</label>
|
|
2328
|
+
|
|
2329
|
+
<label className="block">
|
|
2330
|
+
<div className="text-[10px] uppercase tracking-wide text-[color:var(--ck-text-tertiary)]">name (optional)</div>
|
|
2331
|
+
<input
|
|
2332
|
+
value={workflowNewNodeName}
|
|
2333
|
+
onChange={(e) => setWorkflowNewNodeName(e.target.value)}
|
|
2334
|
+
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)]"
|
|
2335
|
+
placeholder="Human-friendly label"
|
|
2336
|
+
/>
|
|
2337
|
+
</label>
|
|
2338
|
+
|
|
2339
|
+
<label className="block">
|
|
2340
|
+
<div className="text-[10px] uppercase tracking-wide text-[color:var(--ck-text-tertiary)]">type</div>
|
|
2341
|
+
<select
|
|
2342
|
+
value={workflowNewNodeType}
|
|
2343
|
+
onChange={(e) => setWorkflowNewNodeType(e.target.value as WorkflowFileV1["nodes"][number]["type"])}
|
|
2344
|
+
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)]"
|
|
2345
|
+
>
|
|
2346
|
+
<option value="start">start</option>
|
|
2347
|
+
<option value="end">end</option>
|
|
2348
|
+
<option value="llm">llm</option>
|
|
2349
|
+
<option value="tool">tool</option>
|
|
2350
|
+
<option value="condition">condition</option>
|
|
2351
|
+
<option value="delay">delay</option>
|
|
2352
|
+
<option value="human_approval">human_approval</option>
|
|
2353
|
+
</select>
|
|
2354
|
+
</label>
|
|
2355
|
+
|
|
2356
|
+
<button
|
|
2357
|
+
type="button"
|
|
2358
|
+
onClick={() => {
|
|
2359
|
+
const rawId = String(workflowNewNodeId || "").trim();
|
|
2360
|
+
const id = rawId.replace(/[^a-z0-9_\-]/gi, "_");
|
|
2361
|
+
if (!id) {
|
|
2362
|
+
flashMessage("Node id is required", "error");
|
|
2363
|
+
return;
|
|
2364
|
+
}
|
|
2365
|
+
if (wf.nodes.some((n) => n.id === id)) {
|
|
2366
|
+
flashMessage(`Node id already exists: ${id}`, "error");
|
|
2367
|
+
return;
|
|
2368
|
+
}
|
|
2369
|
+
|
|
2370
|
+
const maxX = wf.nodes.reduce((acc, n) => (typeof n.x === "number" ? Math.max(acc, n.x) : acc), 80);
|
|
2371
|
+
const nextNode = {
|
|
2372
|
+
id,
|
|
2373
|
+
type: workflowNewNodeType,
|
|
2374
|
+
name: String(workflowNewNodeName || "").trim() || id,
|
|
2375
|
+
x: maxX + 220,
|
|
2376
|
+
y: 80,
|
|
2377
|
+
} as WorkflowFileV1["nodes"][number];
|
|
2378
|
+
|
|
2379
|
+
const next: WorkflowFileV1 = { ...wf, nodes: [...wf.nodes, nextNode] };
|
|
2380
|
+
setWorkflowJsonText(JSON.stringify(next, null, 2) + "\n");
|
|
2381
|
+
setWorkflowSelectedNodeId(id);
|
|
2382
|
+
setWorkflowNewNodeId("");
|
|
2383
|
+
setWorkflowNewNodeName("");
|
|
2384
|
+
}}
|
|
2385
|
+
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"
|
|
2386
|
+
>
|
|
2387
|
+
+ Add node
|
|
2388
|
+
</button>
|
|
2389
|
+
</div>
|
|
2390
|
+
</div>
|
|
2391
|
+
|
|
2392
|
+
<div className="mt-2 space-y-1">
|
|
2393
|
+
{wf.nodes.map((n) => {
|
|
2394
|
+
const selected = workflowSelectedNodeId === n.id;
|
|
2395
|
+
return (
|
|
2396
|
+
<button
|
|
2397
|
+
key={n.id}
|
|
2398
|
+
type="button"
|
|
2399
|
+
onClick={() => setWorkflowSelectedNodeId(n.id)}
|
|
2400
|
+
className={
|
|
2401
|
+
selected
|
|
2402
|
+
? "w-full rounded-[var(--ck-radius-sm)] bg-white/10 px-2 py-1 text-left text-[11px] text-[color:var(--ck-text-primary)]"
|
|
2403
|
+
: "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"
|
|
2404
|
+
}
|
|
2405
|
+
>
|
|
2406
|
+
<span className="font-mono">{n.id}</span>
|
|
2407
|
+
<span className="ml-2 text-[10px] uppercase tracking-wide text-[color:var(--ck-text-tertiary)]">{n.type}</span>
|
|
2408
|
+
</button>
|
|
2409
|
+
);
|
|
2410
|
+
})}
|
|
2411
|
+
</div>
|
|
2412
|
+
</div>
|
|
2413
|
+
|
|
2414
|
+
<div className="border-t border-white/10 pt-3">
|
|
2415
|
+
<div className="text-xs font-medium text-[color:var(--ck-text-secondary)]">Edges</div>
|
|
2416
|
+
|
|
2417
|
+
<div className="mt-2 rounded-[var(--ck-radius-sm)] border border-white/10 bg-black/25 p-2">
|
|
2418
|
+
<div className="grid grid-cols-1 gap-2">
|
|
2419
|
+
<label className="block">
|
|
2420
|
+
<div className="text-[10px] uppercase tracking-wide text-[color:var(--ck-text-tertiary)]">from</div>
|
|
2421
|
+
<select
|
|
2422
|
+
value={workflowNewEdgeFrom}
|
|
2423
|
+
onChange={(e) => setWorkflowNewEdgeFrom(e.target.value)}
|
|
2424
|
+
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)]"
|
|
2425
|
+
>
|
|
2426
|
+
<option value="">(select)</option>
|
|
2427
|
+
{wf.nodes.map((n) => (
|
|
2428
|
+
<option key={n.id} value={n.id}>
|
|
2429
|
+
{n.id}
|
|
2430
|
+
</option>
|
|
2431
|
+
))}
|
|
2432
|
+
</select>
|
|
2433
|
+
</label>
|
|
2434
|
+
<label className="block">
|
|
2435
|
+
<div className="text-[10px] uppercase tracking-wide text-[color:var(--ck-text-tertiary)]">to</div>
|
|
2436
|
+
<select
|
|
2437
|
+
value={workflowNewEdgeTo}
|
|
2438
|
+
onChange={(e) => setWorkflowNewEdgeTo(e.target.value)}
|
|
2439
|
+
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)]"
|
|
2440
|
+
>
|
|
2441
|
+
<option value="">(select)</option>
|
|
2442
|
+
{wf.nodes.map((n) => (
|
|
2443
|
+
<option key={n.id} value={n.id}>
|
|
2444
|
+
{n.id}
|
|
2445
|
+
</option>
|
|
2446
|
+
))}
|
|
2447
|
+
</select>
|
|
2448
|
+
</label>
|
|
2449
|
+
<label className="block">
|
|
2450
|
+
<div className="text-[10px] uppercase tracking-wide text-[color:var(--ck-text-tertiary)]">label (optional)</div>
|
|
2451
|
+
<input
|
|
2452
|
+
value={workflowNewEdgeLabel}
|
|
2453
|
+
onChange={(e) => setWorkflowNewEdgeLabel(e.target.value)}
|
|
2454
|
+
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)]"
|
|
2455
|
+
placeholder="e.g. approve"
|
|
2456
|
+
/>
|
|
2457
|
+
</label>
|
|
2458
|
+
<button
|
|
2459
|
+
type="button"
|
|
2460
|
+
onClick={() => {
|
|
2461
|
+
const from = String(workflowNewEdgeFrom || "").trim();
|
|
2462
|
+
const to = String(workflowNewEdgeTo || "").trim();
|
|
2463
|
+
if (!from || !to) {
|
|
2464
|
+
flashMessage("Edge requires from + to", "error");
|
|
2465
|
+
return;
|
|
2466
|
+
}
|
|
2467
|
+
if (from === to) {
|
|
2468
|
+
flashMessage("Edge from/to must be different", "error");
|
|
2469
|
+
return;
|
|
2470
|
+
}
|
|
2471
|
+
const id = `e${Date.now()}`;
|
|
2472
|
+
const nextEdge: WorkflowFileV1["edges"][number] = {
|
|
2473
|
+
id,
|
|
2474
|
+
from,
|
|
2475
|
+
to,
|
|
2476
|
+
...(String(workflowNewEdgeLabel || "").trim() ? { label: String(workflowNewEdgeLabel).trim() } : {}),
|
|
2477
|
+
};
|
|
2478
|
+
const next: WorkflowFileV1 = { ...wf, edges: [...(wf.edges ?? []), nextEdge] };
|
|
2479
|
+
setWorkflowJsonText(JSON.stringify(next, null, 2) + "\n");
|
|
2480
|
+
setWorkflowNewEdgeLabel("");
|
|
2481
|
+
}}
|
|
2482
|
+
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"
|
|
2483
|
+
>
|
|
2484
|
+
+ Add edge
|
|
2485
|
+
</button>
|
|
2486
|
+
</div>
|
|
2487
|
+
</div>
|
|
2488
|
+
|
|
2489
|
+
<div className="mt-2 space-y-2">
|
|
2490
|
+
{(wf.edges ?? []).length ? (
|
|
2491
|
+
(wf.edges ?? []).map((e) => (
|
|
2492
|
+
<div key={e.id} className="rounded-[var(--ck-radius-sm)] border border-white/10 bg-black/25 p-2">
|
|
2493
|
+
<div className="flex items-center justify-between gap-2">
|
|
2494
|
+
<div className="text-[11px] text-[color:var(--ck-text-secondary)]">
|
|
2495
|
+
<span className="font-mono">{e.from}</span> → <span className="font-mono">{e.to}</span>
|
|
2496
|
+
{e.label ? <span className="ml-2 text-[10px] text-[color:var(--ck-text-tertiary)]">({e.label})</span> : null}
|
|
2497
|
+
</div>
|
|
2498
|
+
<button
|
|
2499
|
+
type="button"
|
|
2500
|
+
onClick={() => {
|
|
2501
|
+
const next: WorkflowFileV1 = { ...wf, edges: (wf.edges ?? []).filter((x) => x.id !== e.id) };
|
|
2502
|
+
setWorkflowJsonText(JSON.stringify(next, null, 2) + "\n");
|
|
2503
|
+
}}
|
|
2504
|
+
className="text-[10px] text-[color:var(--ck-text-tertiary)] hover:text-[color:var(--ck-text-primary)]"
|
|
2505
|
+
>
|
|
2506
|
+
Remove
|
|
2507
|
+
</button>
|
|
2508
|
+
</div>
|
|
2509
|
+
</div>
|
|
2510
|
+
))
|
|
2511
|
+
) : (
|
|
2512
|
+
<div className="text-xs text-[color:var(--ck-text-secondary)]">No edges yet.</div>
|
|
2513
|
+
)}
|
|
2514
|
+
</div>
|
|
2515
|
+
</div>
|
|
2516
|
+
|
|
2517
|
+
<div className="border-t border-white/10 pt-3">
|
|
2518
|
+
<div className="flex items-center justify-between gap-2">
|
|
2519
|
+
<div className="text-xs font-medium text-[color:var(--ck-text-secondary)]">Node inspector</div>
|
|
2520
|
+
{workflowSelectedNodeId ? (
|
|
2521
|
+
<button
|
|
2522
|
+
type="button"
|
|
2523
|
+
onClick={() => {
|
|
2524
|
+
const nodeId = workflowSelectedNodeId;
|
|
2525
|
+
const nextNodes = wf.nodes.filter((n) => n.id !== nodeId);
|
|
2526
|
+
const nextEdges = (wf.edges ?? []).filter((e) => e.from !== nodeId && e.to !== nodeId);
|
|
2527
|
+
const next: WorkflowFileV1 = { ...wf, nodes: nextNodes, edges: nextEdges };
|
|
2528
|
+
setWorkflowJsonText(JSON.stringify(next, null, 2) + "\n");
|
|
2529
|
+
setWorkflowSelectedNodeId("");
|
|
2530
|
+
}}
|
|
2531
|
+
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"
|
|
2532
|
+
>
|
|
2533
|
+
Delete node
|
|
2534
|
+
</button>
|
|
2535
|
+
) : null}
|
|
2536
|
+
</div>
|
|
2537
|
+
|
|
2538
|
+
{workflowSelectedNodeId ? (
|
|
2539
|
+
(() => {
|
|
2540
|
+
const node = wf.nodes.find((n) => n.id === workflowSelectedNodeId);
|
|
2541
|
+
if (!node) return <div className="mt-2 text-sm text-[color:var(--ck-text-secondary)]">No node selected.</div>;
|
|
2542
|
+
|
|
2543
|
+
return (
|
|
2544
|
+
<div className="mt-3 space-y-3">
|
|
2545
|
+
<div>
|
|
2546
|
+
<div className="text-[10px] uppercase tracking-wide text-[color:var(--ck-text-tertiary)]">id</div>
|
|
2547
|
+
<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)]">
|
|
2548
|
+
{node.id}
|
|
2549
|
+
</div>
|
|
2550
|
+
</div>
|
|
2551
|
+
|
|
2552
|
+
<label className="block">
|
|
2553
|
+
<div className="text-[10px] uppercase tracking-wide text-[color:var(--ck-text-tertiary)]">name</div>
|
|
2554
|
+
<input
|
|
2555
|
+
value={String(node.name ?? "")}
|
|
2556
|
+
onChange={(e) => {
|
|
2557
|
+
const nextName = e.target.value;
|
|
2558
|
+
const next: WorkflowFileV1 = {
|
|
2559
|
+
...wf,
|
|
2560
|
+
nodes: wf.nodes.map((n) => (n.id === node.id ? { ...n, name: nextName } : n)),
|
|
2561
|
+
};
|
|
2562
|
+
setWorkflowJsonText(JSON.stringify(next, null, 2) + "\n");
|
|
2563
|
+
}}
|
|
2564
|
+
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)]"
|
|
2565
|
+
placeholder="Optional"
|
|
2566
|
+
/>
|
|
2567
|
+
</label>
|
|
2568
|
+
|
|
2569
|
+
<label className="block">
|
|
2570
|
+
<div className="text-[10px] uppercase tracking-wide text-[color:var(--ck-text-tertiary)]">type</div>
|
|
2571
|
+
<select
|
|
2572
|
+
value={node.type}
|
|
2573
|
+
onChange={(e) => {
|
|
2574
|
+
const nextType = e.target.value as WorkflowFileV1["nodes"][number]["type"];
|
|
2575
|
+
const next: WorkflowFileV1 = {
|
|
2576
|
+
...wf,
|
|
2577
|
+
nodes: wf.nodes.map((n) => (n.id === node.id ? { ...n, type: nextType } : n)),
|
|
2578
|
+
};
|
|
2579
|
+
setWorkflowJsonText(JSON.stringify(next, null, 2) + "\n");
|
|
2580
|
+
}}
|
|
2581
|
+
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)]"
|
|
2582
|
+
>
|
|
2583
|
+
<option value="start">start</option>
|
|
2584
|
+
<option value="end">end</option>
|
|
2585
|
+
<option value="llm">llm</option>
|
|
2586
|
+
<option value="tool">tool</option>
|
|
2587
|
+
<option value="condition">condition</option>
|
|
2588
|
+
<option value="delay">delay</option>
|
|
2589
|
+
<option value="human_approval">human_approval</option>
|
|
2590
|
+
</select>
|
|
2591
|
+
</label>
|
|
2592
|
+
|
|
2593
|
+
<div className="grid grid-cols-2 gap-2">
|
|
2594
|
+
<label className="block">
|
|
2595
|
+
<div className="text-[10px] uppercase tracking-wide text-[color:var(--ck-text-tertiary)]">x</div>
|
|
2596
|
+
<input
|
|
2597
|
+
type="number"
|
|
2598
|
+
value={typeof node.x === "number" ? node.x : 0}
|
|
2599
|
+
onChange={(e) => {
|
|
2600
|
+
const nextX = Number(e.target.value);
|
|
2601
|
+
const next: WorkflowFileV1 = {
|
|
2602
|
+
...wf,
|
|
2603
|
+
nodes: wf.nodes.map((n) => (n.id === node.id ? { ...n, x: nextX } : n)),
|
|
2604
|
+
};
|
|
2605
|
+
setWorkflowJsonText(JSON.stringify(next, null, 2) + "\n");
|
|
2606
|
+
}}
|
|
2607
|
+
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)]"
|
|
2608
|
+
/>
|
|
2609
|
+
</label>
|
|
2610
|
+
<label className="block">
|
|
2611
|
+
<div className="text-[10px] uppercase tracking-wide text-[color:var(--ck-text-tertiary)]">y</div>
|
|
2612
|
+
<input
|
|
2613
|
+
type="number"
|
|
2614
|
+
value={typeof node.y === "number" ? node.y : 0}
|
|
2615
|
+
onChange={(e) => {
|
|
2616
|
+
const nextY = Number(e.target.value);
|
|
2617
|
+
const next: WorkflowFileV1 = {
|
|
2618
|
+
...wf,
|
|
2619
|
+
nodes: wf.nodes.map((n) => (n.id === node.id ? { ...n, y: nextY } : n)),
|
|
2620
|
+
};
|
|
2621
|
+
setWorkflowJsonText(JSON.stringify(next, null, 2) + "\n");
|
|
2622
|
+
}}
|
|
2623
|
+
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)]"
|
|
2624
|
+
/>
|
|
2625
|
+
</label>
|
|
2626
|
+
</div>
|
|
2627
|
+
|
|
2628
|
+
<div>
|
|
2629
|
+
<div className="text-[10px] uppercase tracking-wide text-[color:var(--ck-text-tertiary)]">config</div>
|
|
2630
|
+
<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)]">
|
|
2631
|
+
{JSON.stringify(node.config ?? {}, null, 2)}
|
|
2632
|
+
</pre>
|
|
2633
|
+
</div>
|
|
2634
|
+
</div>
|
|
2635
|
+
);
|
|
2636
|
+
})()
|
|
2637
|
+
) : (
|
|
2638
|
+
<div className="mt-2 text-sm text-[color:var(--ck-text-secondary)]">Select a node.</div>
|
|
2639
|
+
)}
|
|
2640
|
+
</div>
|
|
2641
|
+
</div>
|
|
2642
|
+
);
|
|
2643
|
+
})()
|
|
2644
|
+
) : (
|
|
2645
|
+
<div className="mt-2 text-sm text-[color:var(--ck-text-secondary)]">Load or create a workflow to edit triggers.</div>
|
|
2646
|
+
)}
|
|
2647
|
+
</div>
|
|
2648
|
+
</div>
|
|
2649
|
+
) : (
|
|
2650
|
+
<textarea
|
|
2651
|
+
value={workflowJsonText}
|
|
2652
|
+
onChange={(e) => setWorkflowJsonText(e.target.value)}
|
|
2653
|
+
className="mt-3 h-[55vh] w-full resize-none rounded-[var(--ck-radius-sm)] border border-white/10 bg-black/25 p-3 font-mono text-xs text-[color:var(--ck-text-primary)]"
|
|
2654
|
+
spellCheck={false}
|
|
2655
|
+
placeholder="Select a workflow from the left (or create the template)."
|
|
2656
|
+
/>
|
|
2657
|
+
)}
|
|
2658
|
+
</div>
|
|
2659
|
+
</div>
|
|
2660
|
+
</div>
|
|
2661
|
+
</div>
|
|
2662
|
+
) : null}
|
|
2663
|
+
|
|
2664
|
+
</div>
|
|
2665
|
+
) : null}
|
|
2666
|
+
|
|
2667
|
+
{activeTab === "files" ? (
|
|
1022
2668
|
<div className="mt-6 grid grid-cols-1 gap-4 lg:grid-cols-3">
|
|
1023
2669
|
<div className="ck-glass-strong p-4">
|
|
1024
2670
|
<div className="flex items-center justify-between gap-3">
|
|
@@ -1092,6 +2738,8 @@ export default function TeamEditor({ teamId }: { teamId: string }) {
|
|
|
1092
2738
|
</div>
|
|
1093
2739
|
) : null}
|
|
1094
2740
|
|
|
2741
|
+
{activeTab === "orchestrator" ? <OrchestratorPanel teamId={teamId} /> : null}
|
|
2742
|
+
|
|
1095
2743
|
{/* markdown editor lives below for convenience */}
|
|
1096
2744
|
{activeTab === "recipe" ? (
|
|
1097
2745
|
<div className="mt-6 ck-glass-strong p-4">
|