Skip to content

Commit 00f3e60

Browse files
committed
Allow running workflow from menu actions
1 parent 8d4616c commit 00f3e60

File tree

9 files changed

+292
-145
lines changed

9 files changed

+292
-145
lines changed

web/components/interface/editor-toolbar.tsx

Lines changed: 4 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,10 @@
22

33
import Icon from "@/components/misc/icon";
44
import AppSettingsModal from "@/components/modals/app-settings-modal";
5+
import { useMenuActions } from "@/lib/hooks/use-menu-actions";
56
import usePlatformAIAssistant from "@/lib/hooks/use-platform-ai-assistant";
67
import useRecorder from "@/lib/hooks/use-recorder";
7-
import useScopedActions from "@/lib/hooks/use-scoped-actions";
8-
import { AppNodeData } from "@/lib/types";
9-
import { addToast, Button, Divider, Tooltip } from "@heroui/react";
8+
import { Button, Divider, Tooltip } from "@heroui/react";
109
import { AnimatePresence, motion } from "framer-motion";
1110
import { useContext, useState } from "react";
1211
import AgentConfigModal from "../modals/agent-config-modal";
@@ -18,7 +17,7 @@ export default function EditorToolbar() {
1817

1918
const { chatWithAssistant } = usePlatformAIAssistant();
2019
const { isRecording, record } = useRecorder();
21-
const { runAction } = useScopedActions();
20+
const { runMenuActionByName } = useMenuActions();
2221

2322
const [isAgentListModalOpen, setIsAgentListModalOpen] = useState(false);
2423
const [isAppSettingsModalOpen, setAppIsSettingsModalOpen] = useState(false);
@@ -87,54 +86,7 @@ export default function EditorToolbar() {
8786
isIconOnly
8887
className="text-default-foreground h-8 w-8 min-w-8 px-1 py-1"
8988
onPress={() => {
90-
const node = editorContext?.editorStates.selectedNode;
91-
92-
if (!node) {
93-
addToast({
94-
title: "No Node Selected",
95-
description:
96-
"Please select a node as a starting point to run the workflow.",
97-
color: "danger",
98-
});
99-
return;
100-
}
101-
102-
const { selectedAction } = node.data as AppNodeData;
103-
104-
if (!selectedAction) {
105-
addToast({
106-
title: "No Action Selected",
107-
description:
108-
"Please select an action for the node to run.",
109-
color: "danger",
110-
});
111-
return;
112-
}
113-
114-
editorContext.setEditorStates((prev) => ({
115-
...prev,
116-
runningNode: node,
117-
}));
118-
runAction(
119-
{
120-
action: selectedAction,
121-
viewId: node.id,
122-
type: "app",
123-
},
124-
{},
125-
).then((result) => {
126-
addToast({
127-
title: "Workflow Completed",
128-
description: `The workflow has completed with result: ${JSON.stringify(
129-
result,
130-
)}`,
131-
color: "success",
132-
});
133-
editorContext?.setEditorStates((prev) => ({
134-
...prev,
135-
runningNode: undefined,
136-
}));
137-
});
89+
runMenuActionByName("Run Workflow", "view");
13890
}}
13991
>
14092
<Icon name="play_arrow" variant="round" />

web/components/interface/navigation/nav-top-bar.tsx

Lines changed: 2 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import { useAuth } from "@/lib/hooks/use-auth";
33
import { useMenuActions } from "@/lib/hooks/use-menu-actions";
44
import { useWorkspace } from "@/lib/hooks/use-workspace";
55
import { getPlatform } from "@/lib/platform-api/platform-checker";
6-
import { MenuAction } from "@/lib/types";
76
import {
87
Button,
98
Dropdown,
@@ -46,39 +45,12 @@ export default function NavTopBar({
4645
// Use the 'app' query parameter to load specific extension app upon loading page
4746
const app = params.get("app");
4847

49-
const { menuActions } = useMenuActions();
48+
const { menuActions, runMenuActionByKeyboardShortcut } = useMenuActions();
5049

5150
// Handle menu action shortcuts
5251
useEffect(() => {
53-
async function runAction(action: MenuAction, event: KeyboardEvent) {
54-
if (action.shortcut) {
55-
// Parse shortcut like "Ctrl+Shift+X"
56-
const keys = action.shortcut
57-
.toLowerCase()
58-
.split("+")
59-
.map((k) => k.trim());
60-
const ctrl = keys.includes("ctrl") || keys.includes("cmd");
61-
const shift = keys.includes("shift");
62-
const alt = keys.includes("alt");
63-
const key = keys.find(
64-
(k) => !["ctrl", "cmd", "shift", "alt"].includes(k),
65-
);
66-
if (
67-
(ctrl ? event.ctrlKey || event.metaKey : true) &&
68-
(shift ? event.shiftKey : true) &&
69-
(alt ? event.altKey : true) &&
70-
event.key.toLowerCase() === key
71-
) {
72-
event.preventDefault();
73-
await action.actionFunc();
74-
}
75-
}
76-
}
77-
7852
async function handleKeyDown(event: KeyboardEvent) {
79-
for (const action of menuActions ?? []) {
80-
await runAction(action, event);
81-
}
53+
await runMenuActionByKeyboardShortcut(event);
8254
}
8355

8456
window.addEventListener("keydown", handleKeyDown);

web/components/views/canvas/canvas-view.tsx

Lines changed: 115 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
import { useAppInfo } from "@/lib/hooks/use-app-info";
22
import { useMenuActions } from "@/lib/hooks/use-menu-actions";
3+
import useWorkflow from "@/lib/hooks/use-workflow";
34
import {
45
AppInfoModalContent,
56
AppNodeData,
67
CanvasViewConfig,
78
MenuAction,
9+
Workflow,
810
} from "@/lib/types";
911
import { Button } from "@heroui/react";
1012
import { Action } from "@pulse-editor/shared-utils";
@@ -21,6 +23,7 @@ import {
2123
Edge as ReactFlowEdge,
2224
Node as ReactFlowNode,
2325
reconnectEdge,
26+
useNodes,
2427
useReactFlow,
2528
useViewport,
2629
} from "@xyflow/react";
@@ -52,36 +55,65 @@ export default function CanvasView({ config }: { config?: CanvasViewConfig }) {
5255
const { openAppInfoModal } = useAppInfo();
5356
const { registerMenuAction, unregisterMenuAction } = useMenuActions();
5457

58+
const [workflow, setWorkflow] = useState<Workflow | undefined>(undefined);
59+
const [entryPoint, setEntryPoint] = useState<
60+
ReactFlowNode<AppNodeData> | undefined
61+
>(undefined);
62+
63+
const { startWorkflow, runningNodes } = useWorkflow(workflow, entryPoint);
64+
5565
const viewport = useViewport();
5666
const { screenToFlowPosition } = useReactFlow();
67+
const nodes = useNodes<ReactFlowNode<AppNodeData>>();
5768

5869
const containerRef = useRef<HTMLDivElement>(null);
5970

60-
const [nodes, setNodes] = useState<ReactFlowNode<AppNodeData>[]>([]);
61-
const [edges, setEdges] = useState<ReactFlowEdge[]>([]);
62-
6371
const onNodesChange = useCallback(
6472
(changes: NodeChange<ReactFlowNode<AppNodeData>>[]) => {
6573
console.log("Node changes:", changes);
66-
setNodes((nodesSnapshot) => applyNodeChanges(changes, nodesSnapshot));
74+
setWorkflow((prev) => {
75+
if (!prev) return undefined;
76+
return {
77+
...prev,
78+
nodes: applyNodeChanges(changes, prev.nodes),
79+
};
80+
});
6781
},
6882
[],
6983
);
7084
const onEdgesChange = useCallback(
7185
(changes: EdgeChange<{ id: string; source: string; target: string }>[]) => {
7286
console.log("Edge changes:", changes);
73-
setEdges((edgesSnapshot) => applyEdgeChanges(changes, edgesSnapshot));
87+
setWorkflow((prev) => {
88+
if (!prev) return undefined;
89+
return {
90+
...prev,
91+
edges: applyEdgeChanges(changes, prev.edges),
92+
};
93+
});
7494
},
7595
[],
7696
);
7797
const onConnect = useCallback(
7898
(params: any) =>
79-
setEdges((edgesSnapshot) => addEdge(params, edgesSnapshot)),
99+
setWorkflow((prev) => {
100+
if (!prev) return undefined;
101+
return {
102+
...prev,
103+
edges: addEdge(params, prev.edges),
104+
};
105+
}),
80106
[],
81107
);
82108
const onReconnect = useCallback(
83109
(oldEdge: ReactFlowEdge, newConnection: Connection) =>
84-
setEdges((els) => reconnectEdge(oldEdge, newConnection, els)),
110+
setWorkflow((prev) => {
111+
if (!prev) return undefined;
112+
return {
113+
...prev,
114+
edges: reconnectEdge(oldEdge, newConnection, prev.edges),
115+
};
116+
}),
85117
[],
86118
);
87119

@@ -120,9 +152,8 @@ export default function CanvasView({ config }: { config?: CanvasViewConfig }) {
120152
reader.onload = (event) => {
121153
try {
122154
const workflow = JSON.parse(event.target?.result as string);
123-
if (workflow.nodes && workflow.edges) {
124-
setNodes(workflow.nodes);
125-
setEdges(workflow.edges);
155+
if (workflow) {
156+
setWorkflow(workflow);
126157
} else {
127158
alert("Invalid workflow file");
128159
}
@@ -150,12 +181,29 @@ export default function CanvasView({ config }: { config?: CanvasViewConfig }) {
150181
};
151182
}, []);
152183

184+
useEffect(() => {
185+
const action: MenuAction = {
186+
name: "Run Workflow",
187+
menuCategory: "view",
188+
description:
189+
"Run the current workflow from the selected or default entry point",
190+
shortcut: "Ctrl+Alt+R",
191+
actionFunc: async () => {
192+
await startWorkflow();
193+
},
194+
icon: "play_arrow",
195+
};
196+
197+
registerMenuAction(action, true);
198+
}, [entryPoint]);
199+
153200
// Add or remove nodes when config changes
154201
useEffect(() => {
155202
if (config) {
156203
// Added nodes
157204
const newNodes = config.nodes?.filter(
158-
(newNode) => !nodes.find((node) => node.id === newNode.viewId),
205+
(newNode) =>
206+
!workflow?.nodes.find((node) => node.id === newNode.viewId),
159207
);
160208

161209
if (newNodes && newNodes.length > 0) {
@@ -179,17 +227,15 @@ export default function CanvasView({ config }: { config?: CanvasViewConfig }) {
179227

180228
const flowCenter = screenToFlowPosition(screenCenter);
181229

182-
return {
183-
id: appConfig.viewId,
184-
position: flowCenter,
185-
data: {
186-
label: appConfig.app,
187-
config: appConfig,
188-
selectedAction: undefined,
189-
setSelectedAction: async (action: Action | undefined) => {
190-
// Update the node's data
191-
setNodes((nds) =>
192-
nds.map((node) => {
230+
const appNodeData: AppNodeData = {
231+
config: appConfig,
232+
selectedAction: undefined,
233+
setSelectedAction: async (action: Action | undefined) => {
234+
setWorkflow((prev) => {
235+
if (!prev) return undefined;
236+
return {
237+
...prev,
238+
nodes: prev.nodes.map((node) => {
193239
if (node.id === appConfig.viewId) {
194240
return {
195241
...node,
@@ -201,37 +247,69 @@ export default function CanvasView({ config }: { config?: CanvasViewConfig }) {
201247
}
202248
return node;
203249
}),
204-
);
205-
},
206-
isRunning: false,
250+
};
251+
});
207252
},
253+
isRunning:
254+
runningNodes?.find((n) => n.id === appConfig.viewId) !==
255+
undefined,
256+
};
257+
258+
return {
259+
id: appConfig.viewId,
260+
position: flowCenter,
261+
data: appNodeData,
208262
type: "appNode",
209263
height: appConfig.recommendedHeight ?? 360,
210264
width: appConfig.recommendedWidth ?? 640,
211265
};
212266
}) ?? [];
213267

214-
setNodes((nds) => nds.concat(newAppNodes));
268+
// Add new nodes to the workflow
269+
setWorkflow((prev) => {
270+
if (!prev) {
271+
return {
272+
nodes: newAppNodes,
273+
edges: [],
274+
};
275+
}
276+
return {
277+
...prev,
278+
nodes: prev.nodes.concat(newAppNodes),
279+
};
280+
});
215281
}
216282

217283
// Removed nodes
218-
const removedNodes = nodes.filter(
284+
const removedNodes = workflow?.nodes.filter(
219285
(node) => !config.nodes?.find((newNode) => newNode.viewId === node.id),
220286
);
221287

222288
if (removedNodes && removedNodes.length > 0) {
223-
setNodes((nds) =>
224-
nds.filter(
225-
(node) =>
226-
!removedNodes.find((removedNode) => removedNode.id === node.id),
227-
),
228-
);
289+
setWorkflow((prev) => {
290+
if (!prev) return undefined;
291+
return {
292+
...prev,
293+
nodes: prev.nodes.filter(
294+
(node) =>
295+
!removedNodes.find((removedNode) => removedNode.id === node.id),
296+
),
297+
};
298+
});
229299
}
230300
}
231-
}, [config, viewport]);
301+
}, [config, viewport, runningNodes]);
302+
303+
useEffect(() => {
304+
const selectedNodes = nodes.filter((node) => node.selected);
305+
if (selectedNodes.length > 0) {
306+
setEntryPoint(selectedNodes[0]);
307+
} else {
308+
setEntryPoint(undefined);
309+
}
310+
}, [nodes]);
232311

233312
async function exportWorkflow() {
234-
const workflow = { nodes, edges };
235313
const blob = new Blob([JSON.stringify(workflow, null, 2)], {
236314
type: "application/json",
237315
});
@@ -250,8 +328,8 @@ export default function CanvasView({ config }: { config?: CanvasViewConfig }) {
250328
id={`canvas-${config?.viewId}`}
251329
>
252330
<ReactFlow
253-
nodes={nodes}
254-
edges={edges}
331+
nodes={workflow?.nodes}
332+
edges={workflow?.edges}
255333
onNodesChange={onNodesChange}
256334
onEdgesChange={onEdgesChange}
257335
onConnect={onConnect}

0 commit comments

Comments
 (0)