Skip to content

Commit d5eb394

Browse files
authored
Merge pull request #69 from ClayPulse/add-canvas
Optimize action registration & execution order
2 parents a2018ed + da18120 commit d5eb394

File tree

10 files changed

+408
-184
lines changed

10 files changed

+408
-184
lines changed

npm-packages/react-api/src/hooks/editor/use-register-action.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@ import useIMC from "../../lib/use-imc";
1717
* @param parameters Parameters of the command.
1818
* @param returns Return values of the command.
1919
* @param callbackHandler Callback handler function to handle the command.
20-
* @param isExtReady Whether the extension is ready to receive commands.
20+
* @param isExtReady Whether the extension is ready to receive commands.
21+
* Useful for actions that need to wait for some certain app state to be ready.
2122
*
2223
*/
2324
export default function useRegisterAction(

web/components/interface/editor-toolbar.tsx

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@ import Icon from "@/components/misc/icon";
44
import AppSettingsModal from "@/components/modals/app-settings-modal";
55
import usePlatformAIAssistant from "@/lib/hooks/use-platform-ai-assistant";
66
import useRecorder from "@/lib/hooks/use-recorder";
7-
import { Button, Divider, Tooltip } from "@heroui/react";
7+
import useScopedActions from "@/lib/hooks/use-scoped-actions";
8+
import { AppNodeData } from "@/lib/types";
9+
import { addToast, Button, Divider, Tooltip } from "@heroui/react";
810
import { AnimatePresence, motion } from "framer-motion";
911
import { useContext, useState } from "react";
1012
import AgentConfigModal from "../modals/agent-config-modal";
@@ -16,6 +18,7 @@ export default function EditorToolbar() {
1618

1719
const { chatWithAssistant } = usePlatformAIAssistant();
1820
const { isRecording, record } = useRecorder();
21+
const { runAction } = useScopedActions();
1922

2023
const [isAgentListModalOpen, setIsAgentListModalOpen] = useState(false);
2124
const [isAppSettingsModalOpen, setAppIsSettingsModalOpen] = useState(false);
@@ -78,7 +81,51 @@ export default function EditorToolbar() {
7881
</Button>
7982
</Tooltip> */}
8083

81-
{/* <Divider className="mx-1" orientation="vertical" /> */}
84+
<Tooltip content={"Run Workflow"}>
85+
<Button
86+
variant="light"
87+
isIconOnly
88+
className="text-default-foreground h-8 w-8 min-w-8 px-1 py-1"
89+
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+
runAction(
115+
{
116+
action: selectedAction,
117+
viewId: node.id,
118+
type: "app",
119+
},
120+
{},
121+
);
122+
}}
123+
>
124+
<Icon name="play_arrow" variant="round" />
125+
</Button>
126+
</Tooltip>
127+
128+
<Divider className="mx-1" orientation="vertical" />
82129
<Tooltip content={"Open Agentic Console"}>
83130
<Button
84131
variant={

web/components/providers/imc-provider.tsx

Lines changed: 78 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import { getVideoGenModel } from "@/lib/modalities/video-gen/video-gen";
1717
import { getAPIKey } from "@/lib/settings/api-manager-utils";
1818
import { IMCContextType } from "@/lib/types";
1919
import {
20+
Action,
2021
ImageModelConfig,
2122
IMCMessage,
2223
IMCMessageTypeEnum,
@@ -26,7 +27,7 @@ import {
2627
STTConfig,
2728
TTSConfig,
2829
} from "@pulse-editor/shared-utils";
29-
import { createContext, useContext, useEffect, useState } from "react";
30+
import { createContext, useContext, useEffect, useRef, useState } from "react";
3031
import { EditorContext } from "./editor-context-provider";
3132

3233
export const IMCContext = createContext<IMCContextType | undefined>(undefined);
@@ -39,38 +40,77 @@ export default function InterModuleCommunicationProvider({
3940
const editorContext = useContext(EditorContext);
4041

4142
const [polyIMC, setPolyIMC] = useState<PolyIMC | undefined>(undefined);
42-
const [imcInitializedMap, setImcInitializedMap] = useState<
43-
Map<string, boolean>
44-
>(new Map());
45-
const [resolvePromises, setResolvePromises] = useState<{
43+
const imcInitializedMapRef = useRef<Map<string, boolean>>(new Map());
44+
const imcInitializedResolvePromisesRef = useRef<{
4645
[key: string]: () => void;
4746
}>({});
4847

48+
const actionRegisteredMapRef = useRef<Map<string, boolean>>(new Map());
49+
const actionRegisteredResolvePromisesRef = useRef<{
50+
[key: string]: () => void;
51+
}>({});
52+
53+
useEffect(() => {
54+
// @ts-expect-error set window viewId
55+
window.viewId = "Pulse Editor Main";
56+
57+
return () => {
58+
// Cleanup the polyIMC instance when the component unmounts
59+
if (polyIMC) {
60+
polyIMC.close();
61+
setPolyIMC(undefined);
62+
}
63+
};
64+
}, []);
65+
66+
useEffect(() => {
67+
if (!polyIMC) {
68+
const newPolyIMC = new PolyIMC(getHandlerMap());
69+
setPolyIMC(newPolyIMC);
70+
}
71+
}, [polyIMC, setPolyIMC]);
72+
73+
// Update the base handler map as editor context changes
74+
useEffect(() => {
75+
if (polyIMC) {
76+
polyIMC.updateBaseReceiverHandlerMap(getHandlerMap());
77+
}
78+
}, [polyIMC, editorContext]);
79+
4980
function markIMCInitialized(viewId: string) {
50-
setImcInitializedMap((prev) => {
51-
const newMap = new Map(prev);
52-
newMap.set(viewId, true);
53-
return newMap;
54-
});
55-
if (resolvePromises[viewId]) {
56-
resolvePromises[viewId]();
57-
setResolvePromises((prev) => {
58-
const newPromises = { ...prev };
59-
delete newPromises[viewId];
60-
return newPromises;
61-
});
81+
imcInitializedMapRef.current.set(viewId, true);
82+
if (imcInitializedResolvePromisesRef.current[viewId]) {
83+
imcInitializedResolvePromisesRef.current[viewId]();
84+
delete imcInitializedResolvePromisesRef.current[viewId];
6285
}
6386
}
6487

6588
async function resolveWhenViewInitialized(viewId: string) {
6689
return new Promise<void>((resolve) => {
67-
if (imcInitializedMap.get(viewId)) {
90+
if (imcInitializedMapRef.current.get(viewId)) {
6891
resolve();
6992
} else {
70-
setResolvePromises((prev) => ({
71-
...prev,
72-
[viewId]: resolve,
73-
}));
93+
imcInitializedResolvePromisesRef.current[viewId] = resolve;
94+
}
95+
});
96+
}
97+
98+
function markActionRegistered(action: Action) {
99+
console.log(`Action registered: ${action.name}`);
100+
actionRegisteredMapRef.current.set(action.name, true);
101+
if (actionRegisteredResolvePromisesRef.current[action.name]) {
102+
actionRegisteredResolvePromisesRef.current[action.name]();
103+
delete actionRegisteredResolvePromisesRef.current[action.name];
104+
}
105+
}
106+
107+
async function resolveWhenActionRegistered(action: Action) {
108+
return new Promise<void>((resolve, reject) => {
109+
if (actionRegisteredMapRef.current.get(action.name)) {
110+
console.log(`Action "${action.name}" is already registered.`);
111+
resolve();
112+
} else {
113+
actionRegisteredResolvePromisesRef.current[action.name] = resolve;
74114
}
75115
});
76116
}
@@ -428,44 +468,32 @@ export default function InterModuleCommunicationProvider({
428468
return editorContext?.persistSettings?.envs ?? {};
429469
},
430470
],
471+
[
472+
IMCMessageTypeEnum.EditorRegisterAction,
473+
async (
474+
senderWindow: Window,
475+
message: IMCMessage,
476+
abortSignal?: AbortSignal,
477+
) => {
478+
const action: Action = message.payload;
479+
if (!action.name) {
480+
throw new Error("Action must have a name.");
481+
}
482+
// Mark this action as registered
483+
markActionRegistered(action);
484+
},
485+
],
431486
]);
432487

433488
return newMap;
434489
}
435-
436-
useEffect(() => {
437-
// @ts-expect-error set window viewId
438-
window.viewId = "Pulse Editor Main";
439-
440-
return () => {
441-
// Cleanup the polyIMC instance when the component unmounts
442-
if (polyIMC) {
443-
polyIMC.close();
444-
setPolyIMC(undefined);
445-
}
446-
};
447-
}, []);
448-
449-
useEffect(() => {
450-
if (!polyIMC) {
451-
const newPolyIMC = new PolyIMC(getHandlerMap());
452-
setPolyIMC(newPolyIMC);
453-
}
454-
}, [polyIMC, setPolyIMC]);
455-
456-
// Update the base handler map as editor context changes
457-
useEffect(() => {
458-
if (polyIMC) {
459-
polyIMC.updateBaseReceiverHandlerMap(getHandlerMap());
460-
}
461-
}, [polyIMC, editorContext]);
462-
463490
return (
464491
<IMCContext.Provider
465492
value={{
466493
polyIMC,
467494
resolveWhenViewInitialized,
468495
markIMCInitialized,
496+
resolveWhenActionRegistered,
469497
}}
470498
>
471499
{children}

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

Lines changed: 35 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
11
import { useAppInfo } from "@/lib/hooks/use-app-info";
22
import { useMenuActions } from "@/lib/hooks/use-menu-actions";
3-
import { AppInfoModalContent, CanvasViewConfig, MenuAction } from "@/lib/types";
3+
import {
4+
AppInfoModalContent,
5+
AppNodeData,
6+
CanvasViewConfig,
7+
MenuAction,
8+
} from "@/lib/types";
49
import { Button } from "@heroui/react";
10+
import { Action } from "@pulse-editor/shared-utils";
511
import {
612
addEdge,
713
applyEdgeChanges,
@@ -51,13 +57,16 @@ export default function CanvasView({ config }: { config?: CanvasViewConfig }) {
5157

5258
const containerRef = useRef<HTMLDivElement>(null);
5359

54-
const [nodes, setNodes] = useState<ReactFlowNode[]>([]);
60+
const [nodes, setNodes] = useState<ReactFlowNode<AppNodeData>[]>([]);
5561
const [edges, setEdges] = useState<ReactFlowEdge[]>([]);
5662

57-
const onNodesChange = useCallback((changes: NodeChange<ReactFlowNode>[]) => {
58-
console.log("Node changes:", changes);
59-
setNodes((nodesSnapshot) => applyNodeChanges(changes, nodesSnapshot));
60-
}, []);
63+
const onNodesChange = useCallback(
64+
(changes: NodeChange<ReactFlowNode<AppNodeData>>[]) => {
65+
console.log("Node changes:", changes);
66+
setNodes((nodesSnapshot) => applyNodeChanges(changes, nodesSnapshot));
67+
},
68+
[],
69+
);
6170
const onEdgesChange = useCallback(
6271
(changes: EdgeChange<{ id: string; source: string; target: string }>[]) => {
6372
console.log("Edge changes:", changes);
@@ -141,6 +150,7 @@ export default function CanvasView({ config }: { config?: CanvasViewConfig }) {
141150
};
142151
}, []);
143152

153+
// Add or remove nodes when config changes
144154
useEffect(() => {
145155
if (config) {
146156
// Added nodes
@@ -149,7 +159,7 @@ export default function CanvasView({ config }: { config?: CanvasViewConfig }) {
149159
);
150160

151161
if (newNodes && newNodes.length > 0) {
152-
const newAppNodes: ReactFlowNode[] =
162+
const newAppNodes: ReactFlowNode<AppNodeData>[] =
153163
newNodes?.map((appConfig) => {
154164
const containerBounds =
155165
containerRef.current?.getBoundingClientRect();
@@ -175,6 +185,24 @@ export default function CanvasView({ config }: { config?: CanvasViewConfig }) {
175185
data: {
176186
label: appConfig.app,
177187
config: appConfig,
188+
selectedAction: undefined,
189+
setSelectedAction: async (action: Action | undefined) => {
190+
// Update the node's data
191+
setNodes((nds) =>
192+
nds.map((node) => {
193+
if (node.id === appConfig.viewId) {
194+
return {
195+
...node,
196+
data: {
197+
...node.data,
198+
selectedAction: action,
199+
},
200+
};
201+
}
202+
return node;
203+
}),
204+
);
205+
},
178206
},
179207
type: "appNode",
180208
height: appConfig.recommendedHeight ?? 360,

web/components/views/canvas/nodes/app-node/app-node.tsx

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1+
import useScopedActions from "@/lib/hooks/use-scoped-actions";
12
import { useTabViewManager } from "@/lib/hooks/use-tab-view-manager";
2-
import { AppViewConfig } from "@/lib/types";
3+
import { AppNodeData } from "@/lib/types";
34
import { ViewModeEnum } from "@pulse-editor/shared-utils";
45
import { Node } from "@xyflow/react";
56
import { memo } from "react";
@@ -8,12 +9,14 @@ import BaseAppView from "../../../base/base-app-view";
89
import CanvasNodeViewLayout from "./layout";
910

1011
const AppNode = memo((props: any) => {
11-
const nodeProps = props as Node<{ config: AppViewConfig }>;
12+
const nodeProps = props as Node<AppNodeData>;
1213

13-
const { config }: { config: AppViewConfig } = nodeProps.data;
14+
const { config, selectedAction, setSelectedAction }: AppNodeData =
15+
nodeProps.data;
1416
const viewId = config.viewId;
1517

1618
const { createTabView, deleteAppViewInCanvasView } = useTabViewManager();
19+
const { actions } = useScopedActions(config.app);
1720

1821
async function openViewInFullScreen() {
1922
await createTabView(ViewModeEnum.App, {
@@ -25,6 +28,9 @@ const AppNode = memo((props: any) => {
2528
return (
2629
<CanvasNodeViewLayout
2730
viewId={viewId}
31+
actions={actions.map((a) => a.action)}
32+
selectedAction={selectedAction}
33+
setSelectedAction={setSelectedAction}
2834
controlActions={{
2935
fullscreen: () => {
3036
openViewInFullScreen();

0 commit comments

Comments
 (0)