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