Skip to content

Commit

Permalink
added optional props: onConvert, onSave
Browse files Browse the repository at this point in the history
  • Loading branch information
lazToum committed Dec 10, 2024
1 parent 599fe08 commit c84c429
Show file tree
Hide file tree
Showing 17 changed files with 258 additions and 76 deletions.
1 change: 1 addition & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
"groq",
"groupchat",
"handlepos",
"ipynb",
"isinstance",
"jsdelivr",
"JSOM",
Expand Down
64 changes: 44 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,15 +50,25 @@ const isProd = import.meta.env.PROD;

// the actions should be handled by other components
// that use `Waldiez` as a child component

/**
*OnChange
*/
const onChange = null;
// const onChange = (flowJson: any) => {
// console.info(JSON.stringify(JSON.parse(flowJson), null, 2));
// console.info(JSON.stringify(JSON.parse(flowJson), null, 2));
// };

/**
* OnSave
* if enabled, add a listener for the key combination (ctrl+s/mod+s)
* to save the flow
* the flow string is the JSON stringified flow
* the action should be handled by the parent component
*/
const onSaveDev = (flowString: string) => {
console.info('saving', flowString);
};
const onSave = isProd ? null : onSaveDev;
/**
* UserInput
*/
Expand Down Expand Up @@ -102,6 +112,20 @@ const onRunDev = (flowString: string) => {
};
const onRun = isProd ? null : onRunDev;

/**
* OnConvert
* adds two buttons to the main panel, to convert the flow to python or jupyter notebook
* The action should be handled by the parent component
* the flow string is the JSON stringified flow
* the `to` parameter is either 'py' or 'ipynb'
* the conversion happens in the python part / backend
*/

const onConvertDev = (_flowString: string, to: 'py' | 'ipynb') => {
console.info('converting to', to);
};
const onConvert = isProd ? null : onConvertDev;

/**
* OnUpload
* on RAG user: adds a dropzone to upload files
Expand Down Expand Up @@ -138,28 +162,26 @@ const onUpload = isProd ? null : onUploadDev;
// PROD:
// either served and `VITE_VS_PATH` is set to the path, or
// use the default cdn (jsdelivr) that monaco loader uses
const vsPath = isProd ? (import.meta.env.VS_PATH ?? null) : 'vs';
// make sure the csp allows the cdn
let vsPath = !isProd ? 'vs' : (import.meta.env.VITE_VS_PATH ?? null);
if (!vsPath) {
// if set to empty string, make it null
vsPath = null;
}
/**
* Other props:
* we can use:
* `import { importFlow } from '@waldiez/react';`
* const flowJson = JSON.parse(flowJsonString);
* const flow = importFlow(flowJson);
* to import a flow from a waldiez/json file
* `import { importFlow } from '@waldiez';`
* to import an existing flow from a waldiez/json file
* then we can pass the additional props:
* - edges: Edge[]; // initial edges to render (flow.edges)
* - nodes: Node[]; // initial nodes to render (flow.nodes)
* - name: string; // flow name (flow.name)
* - description: string; // flow description (flow.description)
* - tags: string[]; // flow tags (flow.tags)
* - requirements: string[]; // flow requirements (flow.requirements)
* - createdAt?: string; // flow createdAt (flow.createdAt)
* - updatedAt?: string; // flow updatedAt (flow.updatedAt)
* - viewport: Viewport; // initial viewport (flow.viewport)
* - flowId: string; // a specific id for the flow
* - storageId: string; // storage id to use for getting/setting
* the theme mode (light/dark) and
* the sidebar state (open/closed)
* - edges: Edge[]; initial edges to render
* - nodes: Node[]; initial nodes to render
* - name: string;
* - description: string;
* - tags: string[];
* - requirements: string[];
* - createdAt?: string;
* - updatedAt?: string;
*/

const startApp = () => {
Expand All @@ -172,8 +194,10 @@ const startApp = () => {
storageId="storage-0"
inputPrompt={inputPrompt}
onRun={onRun}
onConvert={onConvert}
onChange={onChange}
onUpload={onUpload}
onSave={onSave}
/>
</React.StrictMode>
);
Expand Down
2 changes: 1 addition & 1 deletion eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ const defaultConfig = eslintTs.config({
eqeqeq: 'error',
'prefer-arrow-callback': 'error',
'tsdoc/syntax': 'warn',
complexity: ['error', 16],
complexity: ['error', 20],
'max-depth': ['error', 4],
'max-nested-callbacks': ['error', 4],
'max-statements': ['error', 11, { ignoreTopLevelFunctions: true }],
Expand Down
27 changes: 27 additions & 0 deletions src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,17 @@ const onChange = null;
// console.info(JSON.stringify(JSON.parse(flowJson), null, 2));
// };

/**
* OnSave
* if enabled, add a listener for the key combination (ctrl+s/mod+s)
* to save the flow
* the flow string is the JSON stringified flow
* the action should be handled by the parent component
*/
const onSaveDev = (flowString: string) => {
console.info('saving', flowString);
};
const onSave = isProd ? null : onSaveDev;
/**
* UserInput
*/
Expand Down Expand Up @@ -60,6 +71,20 @@ const onRunDev = (flowString: string) => {
};
const onRun = isProd ? null : onRunDev;

/**
* OnConvert
* adds two buttons to the main panel, to convert the flow to python or jupyter notebook
* The action should be handled by the parent component
* the flow string is the JSON stringified flow
* the `to` parameter is either 'py' or 'ipynb'
* the conversion happens in the python part / backend
*/

const onConvertDev = (_flowString: string, to: 'py' | 'ipynb') => {
console.info('converting to', to);
};
const onConvert = isProd ? null : onConvertDev;

/**
* OnUpload
* on RAG user: adds a dropzone to upload files
Expand Down Expand Up @@ -128,8 +153,10 @@ export const startApp = () => {
storageId="storage-0"
inputPrompt={inputPrompt}
onRun={onRun}
onConvert={onConvert}
onChange={onChange}
onUpload={onUpload}
onSave={onSave}
/>
</React.StrictMode>
);
Expand Down
16 changes: 15 additions & 1 deletion src/waldiez/components/flow/hooks/useKeys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,14 @@ import { useHotkeys } from 'react-hotkeys-hook';
import { useTemporalStore, useWaldiezContext } from '@waldiez/store';
import { getFlowRoot } from '@waldiez/utils';

export const useKeys = (flowId: string) => {
export const useKeys = (flowId: string, onSave?: ((flow: string) => void) | null) => {
const { undo, redo, futureStates, pastStates } = useTemporalStore(state => state);
const deleteAgent = useWaldiezContext(selector => selector.deleteAgent);
const deleteEdge = useWaldiezContext(selector => selector.deleteEdge);
const deleteModel = useWaldiezContext(selector => selector.deleteModel);
const deleteSkill = useWaldiezContext(selector => selector.deleteSkill);
const saveFlow = useWaldiezContext(selector => selector.saveFlow);
const listenForSave = typeof onSave === 'function';
const isFlowVisible = () => {
// if on jupyter, we might have more than one tabs with a flow
// let's check if the current flow is visible (i.e. we are in the right tab)
Expand Down Expand Up @@ -42,6 +44,18 @@ export const useKeys = (flowId: string) => {
},
{ scopes: flowId }
);
if (listenForSave) {
useHotkeys(
'mod+s',
event => {
if (isFlowVisible()) {
event.preventDefault();
saveFlow();
}
},
{ scopes: flowId }
);
}
const onDeleteKey = (event: KeyboardEvent) => {
const target = event.target;
const isNode = target instanceof Element && target.classList.contains('react-flow__node');
Expand Down
55 changes: 44 additions & 11 deletions src/waldiez/components/flow/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ import { Background, BackgroundVariant, Controls, Panel, ReactFlow } from '@xyfl

import { useEffect, useState } from 'react';
import { FaPlusCircle } from 'react-icons/fa';
import { FaPython } from 'react-icons/fa6';
import { FaCirclePlay } from 'react-icons/fa6';
import { SiJupyter } from 'react-icons/si';

import { useDnD, useFlowEvents, useKeys, useRun, useTheme } from '@waldiez/components/flow/hooks';
import { UserInputModal } from '@waldiez/components/flow/modals';
Expand All @@ -15,13 +17,14 @@ import { selector } from '@waldiez/store/selector';
import { isDarkMode, setDarkMode } from '@waldiez/theme';

export const WaldiezFlow = (props: WaldiezFlowProps) => {
const { flowId, storageId, onRun: runner, inputPrompt, onUserInput } = props;
const { flowId, storageId, onRun: runner, inputPrompt, onUserInput, onConvert, onSave } = props;
const store = useWaldiezContext(selector);
const showNodes = useWaldiezContext(selector => selector.showNodes);
const onFlowChanged = useWaldiezContext(selector => selector.onFlowChanged);
const includeRunButton = typeof runner === 'function';
const includeConvertIcons = typeof onConvert === 'function';
const { isDark, onThemeToggle } = useTheme(flowId, storageId);
const { onKeyDown } = useKeys(flowId);
const { onKeyDown } = useKeys(flowId, onSave);
const [selectedNodeType, setSelectedNodeType] = useState<WaldiezNodeType>('agent');
const {
onAddNode,
Expand Down Expand Up @@ -49,6 +52,14 @@ export const WaldiezFlow = (props: WaldiezFlowProps) => {
showNodes('agent');
setDarkMode(flowId, storageId, isDarkMode(flowId, storageId), true);
}, []);
const convertToPy = () => {
const flow = onFlowChanged();
onConvert?.(JSON.stringify(flow), 'py');
};
const convertToIpynb = () => {
const flow = onFlowChanged();
onConvert?.(JSON.stringify(flow), 'ipynb');
};
const colorMode = isDark ? 'dark' : 'light';
const rfInstance = store.get().rfInstance;
const nodes = store.get().nodes;
Expand Down Expand Up @@ -108,17 +119,39 @@ export const WaldiezFlow = (props: WaldiezFlowProps) => {
</button>
</Panel>
)}
{includeRunButton && (
{(includeRunButton || includeConvertIcons) && (
<Panel position="top-right">
<div className="editor-nav-actions">
<button
className="editor-nav-action"
onClick={onRun}
title="Run flow"
data-testid="run-flow"
>
<FaCirclePlay />
</button>
{includeConvertIcons && (
<button
className="editor-nav-action to-python"
onClick={convertToPy}
title="Convert to Python"
data-testid={`convert-${flowId}-to-py`}
>
<FaPython />
</button>
)}
{includeConvertIcons && (
<button
className="editor-nav-action to-jupyter"
onClick={convertToIpynb}
title="Convert to Jupyter Notebook"
data-testid={`convert-${flowId}-to-ipynb`}
>
<SiJupyter />
</button>
)}
{includeRunButton && (
<button
className="editor-nav-action"
onClick={onRun}
title="Run flow"
data-testid={`run-${flowId}`}
>
<FaCirclePlay />
</button>
)}
</div>
</Panel>
)}
Expand Down
2 changes: 2 additions & 0 deletions src/waldiez/components/flow/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ export type WaldiezFlowProps = {
prompt: string;
} | null;
onRun?: ((flow: string) => void) | null;
onSave?: ((flow: string) => void) | null;
onConvert?: ((flow: string, to: 'py' | 'ipynb') => void) | null;
onChange?: ((content: string) => void) | null;
onUserInput?: ((userInput: string) => void) | null;
onUpload?: ((files: File[]) => Promise<string[]>) | null;
Expand Down
10 changes: 9 additions & 1 deletion src/waldiez/store/creator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@ export const createWaldiezStore = (props?: WaldiezStoreProps) => {
updatedAt = new Date().toISOString(),
viewport = { zoom: 1, x: 50, y: 50 },
onUpload = null,
onChange = null
onChange = null,
onSave = null
} = props || {};
let { storageId } = props || {};
if (!storageId) {
Expand Down Expand Up @@ -72,6 +73,7 @@ export const createWaldiezStore = (props?: WaldiezStoreProps) => {
getViewport: () => get().viewport,
onUpload,
onChange,
onSave,
// edges
getEdges: () => get().edges,
onEdgesChange: (changes: EdgeChange[]) => EdgesStore.onEdgesChange(changes, get, set),
Expand Down Expand Up @@ -183,6 +185,12 @@ export const createWaldiezStore = (props?: WaldiezStoreProps) => {
onChange(JSON.stringify(flow));
}
return flow;
},
saveFlow: () => {
if (typeof onSave === 'function') {
const flow = FlowStore.exportFlow(false, get);
onSave(JSON.stringify(flow));
}
}
}),
{
Expand Down
4 changes: 3 additions & 1 deletion src/waldiez/store/provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export function WaldiezProvider({
const storageId = props.storageId;
const onUpload = props.onUpload ?? null;
const onChange = props.onChange ?? null;
const onSave = props.onSave ?? null;
const rfInstance = props.rfInstance;
if (!storeRef.current) {
storeRef.current = createWaldiezStore({
Expand All @@ -47,7 +48,8 @@ export function WaldiezProvider({
edges,
rfInstance,
onUpload,
onChange
onChange,
onSave
});
}
return <WaldiezContext.Provider value={storeRef.current}>{children}</WaldiezContext.Provider>;
Expand Down
2 changes: 2 additions & 0 deletions src/waldiez/store/selector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ export const selector = (store: WaldiezState) => ({
importAgent: store.importAgent,
exportAgent: store.exportAgent,
// flow
saveFlow: store.saveFlow,
getFlowInfo: store.getFlowInfo,
updateFlow: store.updateFlow,
updateFlowOrder: store.updateFlowOrder,
Expand All @@ -80,5 +81,6 @@ export const selector = (store: WaldiezState) => ({
onViewportChange: store.onViewportChange,
onUpload: store.onUpload,
onChange: store.onChange,
onSave: store.onSave,
onFlowChanged: store.onFlowChanged
});
2 changes: 2 additions & 0 deletions src/waldiez/store/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ export type WaldiezStoreProps = {
nodes: Node[]; // only react flow related (no data)
onUpload?: ((files: File[]) => Promise<string[]>) | null; // handler for file uploads (send to backend)
onChange?: ((content: string) => void) | null; // handler for changes in the flow (send to backend)
onSave?: ((flow: string) => void) | null; // handler for saving the flow (send to backend)
};

export type WaldiezFlowInfo = {
Expand Down Expand Up @@ -152,6 +153,7 @@ export type WaldiezState = WaldiezStoreProps & {
) => WaldiezAgentNode;
exportAgent: (agentId: string, skipLinks: boolean) => { [key: string]: unknown } | null;
// flow
saveFlow: () => void;
updateFlow: (data: { name: string; description: string; tags: string[]; requirements: string[] }) => void;
updateFlowOrder: (data: { id: string; order: number }[]) => void;
getFlowEdges: () => [WaldiezEdge[], WaldiezEdge[]];
Expand Down
Loading

0 comments on commit c84c429

Please sign in to comment.