├── .envrc ├── .eslintrc.json ├── .gitignore ├── .prettierrc ├── README.md ├── commons ├── constants.ts ├── regex.ts ├── types.ts └── zod.ts ├── components ├── AppStatus │ └── index.tsx ├── LibTree │ ├── Branch.tsx │ ├── Node.tsx │ ├── Tree.tsx │ └── types.ts ├── LibraryPanel │ ├── NodeSkeleton.tsx │ ├── index.tsx │ └── nodes │ │ ├── Fields.tsx │ │ ├── ToolbarSkeleton.tsx │ │ ├── autogen │ │ ├── AssistantAgent.tsx │ │ ├── CustomFunction.tsx │ │ ├── GPTAssistantAgent.tsx │ │ ├── GroupChat.tsx │ │ └── UserProxy.tsx │ │ └── nodeTypes.ts ├── TopBar │ └── index.tsx ├── UI │ ├── App.tsx │ └── Mobile.tsx ├── Workstation │ └── index.tsx └── modals │ ├── AppStatus │ └── APIKeyModal.tsx │ ├── ContextMenu │ ├── ContextMenuItem.tsx │ ├── ContextMenuModal.tsx │ └── DefaultContextMenuItem.tsx │ ├── ToastMessageModal.tsx │ └── _BaseModal.tsx ├── contexts ├── ModalContext.tsx └── ValidatorContext.tsx ├── docs └── image.png ├── hooks ├── useDnDFlow.tsx ├── useKeyboardListener.ts ├── useModalContext.ts └── useMountedState.ts ├── ide.code-workspace ├── next.config.js ├── package.json ├── pages ├── _app.tsx ├── _document.tsx └── index.tsx ├── postcss.config.js ├── public ├── demo-short.gif ├── demo.gif ├── favicon.ico ├── x-force-ide.svg ├── x-force.svg └── x-forceIDE.png ├── stores ├── useAppStore.tsx └── useDnDStore.ts ├── styles ├── globals.css └── reactflow.css ├── tailwind.config.ts ├── transpiler ├── helpers.ts └── primitive.ts ├── tsconfig.json ├── types └── enum.ts ├── utils └── nodeUtils.ts └── yarn.lock /.envrc: -------------------------------------------------------------------------------- 1 | dotenv 2 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["next/core-web-vitals", "eslint:recommended", "plugin:@typescript-eslint/recommended"], 3 | "parser": "@typescript-eslint/parser", 4 | "plugins": ["@typescript-eslint"], 5 | "root": true, 6 | "rules": { 7 | "no-console": "error", 8 | "@typescript-eslint/no-unused-vars": [ 9 | "error", 10 | { 11 | "argsIgnorePattern": "^_", 12 | "varsIgnorePattern": "^_", 13 | "caughtErrorsIgnorePattern": "^_" 14 | } 15 | ] 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | 38 | 39 | /agi 40 | /secret_transpiler 41 | .env 42 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "trailingComma": "all", 4 | "singleQuote": true, 5 | "printWidth": 120, 6 | "tabWidth": 2 7 | } 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # X-Force IDE 2 | 3 | ![Alt text](docs/image.png) 4 | 5 | ### Create your own workforce @ [ide.x-force.ai](https://ide.x-force.ai) 6 | 7 | ## What is X-Force IDE? 8 | 9 | X-Force IDE is a low-code, agent-as-a-service UI framework that enables you to create agent-based workforces through a drag-and-drop interface. You can export the workforces you create as Python scripts and run them on your local machine. 10 | 11 | ## Guides 12 | 13 | [Getting Started Guide](https://x-force.notion.site/Introduction-to-X-Force-IDE-b92c434802de4df6a58c83fd5d292c33) 14 | 15 | [Examples](https://x-force.notion.site/Example-Workforces-fff4a8ee317c4e76b226fef321c098ba) 16 | 17 | ## Contributing 18 | 19 | This project welcomes contributions and suggestions. You can open pull request/issue! 20 | -------------------------------------------------------------------------------- /commons/constants.ts: -------------------------------------------------------------------------------- 1 | export const MODAL_ROOT_DIV_ID = 'MODAL_ROOT'; 2 | export const MODAL_Z_INDEX = 'z-10'; 3 | export const XF_APP_STORE = 'X-FORCE_APP_STORE'; 4 | export const LOCAL_HISTORY_KEY = 'X-FORCE_USER_FLOW'; 5 | export const DND_ID = 'XForceIDE'; 6 | export const DATA_TRANSFER_KEY = 'application/reactflow'; 7 | export const GETTING_STARTED_GUIDE_URL = 8 | 'https://x-force.notion.site/Introduction-to-X-Force-IDE-b92c434802de4df6a58c83fd5d292c33'; 9 | export const EXAMPLES_URL = 'https://x-force.notion.site/Example-Workforces-fff4a8ee317c4e76b226fef321c098ba'; 10 | export const GITHUB_URL = 'https://github.com/xforceai/ide'; 11 | export const ABOUT_X_FORCE_URL = 'https://x-force.ai/about'; 12 | -------------------------------------------------------------------------------- /commons/regex.ts: -------------------------------------------------------------------------------- 1 | export const VARIABLE_NAME_REGEX = /^[a-zA-Z_][a-zA-Z0-9_]*$/; 2 | -------------------------------------------------------------------------------- /commons/types.ts: -------------------------------------------------------------------------------- 1 | export type PositionType = { top?: number; right?: number; bottom?: number; left?: number }; 2 | 3 | export type ContextMenuItemType = React.HTMLProps & { 4 | item: React.JSX.Element | string; 5 | subs?: ContextMenuItemType[]; 6 | }; 7 | -------------------------------------------------------------------------------- /commons/zod.ts: -------------------------------------------------------------------------------- 1 | import { VARIABLE_NAME_REGEX } from '@/commons/regex'; 2 | import { AgentSelectionStrategyEnum, OAIModelsEnum } from '@/types/enum'; 3 | import { z } from 'zod'; 4 | 5 | const VariableName = z 6 | .string({ 7 | required_error: 8 | 'Variable name must start with a letter or underscore, and can only contain letters, numbers, and underscores.', 9 | }) 10 | .regex(VARIABLE_NAME_REGEX, { 11 | message: 12 | 'Variable name must start with a letter or underscore, and can only contain letters, numbers, and underscores.', 13 | }); 14 | 15 | const LLMEnum = z.nativeEnum(OAIModelsEnum); 16 | const AgentSelectionEnum = z.nativeEnum(AgentSelectionStrategyEnum); 17 | 18 | // 19 | 20 | const UserProxy = z.object({ 21 | variableName: VariableName, 22 | initialPrompt: z 23 | .string({ 24 | required_error: 'Initial Prompt is required. Your workforce will take this prompt to start the conversation.', 25 | }) 26 | .min(1, { message: 'Initial Prompt is required. Your workforce will take this prompt to start the conversation.' }), 27 | }); 28 | const GroupChat = z.object({ 29 | variableName: VariableName, 30 | maxRounds: z.number().optional(), 31 | agentSelection: AgentSelectionEnum.default(AgentSelectionStrategyEnum.AUTO), 32 | }); 33 | const GPTAssistantAgent = z.object({ 34 | variableName: VariableName, 35 | OAIId: z 36 | .string({ required_error: 'The OpenAI Id of the assistant agent is required.' }) 37 | .min(1, { message: 'The OpenAI Id of the assistant agent is required.' }), 38 | }); 39 | const AssistantAgent = z.object({ 40 | variableName: VariableName, 41 | systemMessage: z.string().optional(), 42 | }); 43 | const CustomFunction = z.object({ 44 | func: z.string().optional(), 45 | }); 46 | 47 | const LLMOpenAI = z.object({ 48 | model: LLMEnum.default(OAIModelsEnum.GPT_3_5_TURBO), 49 | apiKey: z.string().optional(), 50 | }); 51 | 52 | export const DnDFlowValidationSchema = z.array( 53 | z.object({ 54 | USER_PROXY: z.optional(UserProxy), 55 | GROUP_CHAT: z.optional(GroupChat), 56 | GPT_ASSISTANT_AGENT: z.optional(GPTAssistantAgent), 57 | ASSISTANT_AGENT: z.optional(AssistantAgent), 58 | CUSTOM_FUNCTION: z.optional(CustomFunction), 59 | LLM_OPENAI: z.optional(LLMOpenAI), 60 | }), 61 | ); 62 | 63 | export type DnDFlowValidationSchemaType = z.infer; 64 | -------------------------------------------------------------------------------- /components/AppStatus/index.tsx: -------------------------------------------------------------------------------- 1 | import SettingModal from '@/components/modals/AppStatus/APIKeyModal'; 2 | import { ModalContext } from '@/contexts/ModalContext'; 3 | import useMountedState from '@/hooks/useMountedState'; 4 | import useAppStore from '@/stores/useAppStore'; 5 | import { CheckCircleIcon, XCircleIcon } from '@heroicons/react/24/outline'; 6 | import React from 'react'; 7 | 8 | type AppStatusItemProps = { 9 | name: string; 10 | value: boolean; 11 | onClick: () => void; 12 | }; 13 | const AppStatusItem: React.FC = ({ name, value, onClick }) => { 14 | return ( 15 |
16 |

{name}

17 | {!value ? ( 18 |
19 | Not set it up yet! 20 | 21 |
22 | ) : ( 23 |
24 | Ready 25 | 26 |
27 | )} 28 |
29 | ); 30 | }; 31 | 32 | const AppStatus: React.FC = () => { 33 | const isMounted = useMountedState(); 34 | const { setModal } = React.useContext(ModalContext); 35 | const { oaiKey } = useAppStore(); 36 | 37 | const onClickSetAPIKey = () => { 38 | setModal(); 39 | }; 40 | 41 | if (!isMounted()) return <>; 42 | return ( 43 |
44 |

XF IDE Status

45 | 46 |
47 | ); 48 | }; 49 | 50 | export default AppStatus; 51 | -------------------------------------------------------------------------------- /components/LibTree/Branch.tsx: -------------------------------------------------------------------------------- 1 | import Node from '@/components/LibTree/Node'; 2 | import { TreeProps } from '@/components/LibTree/types'; 3 | import React from 'react'; 4 | 5 | type BranchProps = { 6 | item: TreeProps['data'][number]; 7 | level: number; 8 | }; 9 | 10 | function Branch({ item, level }: BranchProps): React.JSX.Element { 11 | const [expanded, setExpanded] = React.useState(item.initiallyExpanded ?? false); 12 | const hasChild = item?.children?.length; 13 | 14 | const renderSubBranches = () => { 15 | if (hasChild) { 16 | const newLevel = level + 1; 17 | return item?.children?.map((el) => ); 18 | } 19 | }; 20 | 21 | const onExpand = () => { 22 | setExpanded((prev) => !prev); 23 | }; 24 | 25 | return ( 26 |
27 | 28 | {expanded && renderSubBranches()} 29 |
30 | ); 31 | } 32 | 33 | export default Branch; 34 | -------------------------------------------------------------------------------- /components/LibTree/Node.tsx: -------------------------------------------------------------------------------- 1 | import { TreeProps } from '@/components/LibTree/types'; 2 | import React from 'react'; 3 | 4 | type NodeProps = { 5 | item: TreeProps['data'][number]; 6 | onExpand: () => void; 7 | isExpanded?: boolean; 8 | }; 9 | 10 | function Node(props: NodeProps): React.JSX.Element { 11 | const hasChild = props.item.children; 12 | const hasComponent = props.item.jsxElement ? true : false; 13 | 14 | return ( 15 |
20 | {hasComponent ? ( 21 | props.item.jsxElement 22 | ) : ( 23 |

24 | {hasChild ? (props.isExpanded ? '▾ ' : '▸ ') : ''} 25 | {props.item.name} 26 |

27 | )} 28 |
29 | ); 30 | } 31 | 32 | export default Node; 33 | -------------------------------------------------------------------------------- /components/LibTree/Tree.tsx: -------------------------------------------------------------------------------- 1 | import Branch from './Branch'; 2 | import { TreeProps } from './types'; 3 | 4 | export default function Tree(props: TreeProps) { 5 | return props.data?.map((n) => ); 6 | } 7 | -------------------------------------------------------------------------------- /components/LibTree/types.ts: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | type DefaultDataProps = { 4 | id: string; 5 | name: string; 6 | initiallyExpanded?: boolean; 7 | jsxElement?: React.JSX.Element; 8 | draggable?: boolean; 9 | onDrag?: (event: React.DragEvent) => void; 10 | children?: DefaultDataProps[]; 11 | } & T; 12 | export type TreeProps = { 13 | data: (DefaultDataProps & T)[]; 14 | }; 15 | -------------------------------------------------------------------------------- /components/LibraryPanel/NodeSkeleton.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | type TreeNodeProps = { 4 | name: string; 5 | content: React.JSX.Element; 6 | }; 7 | const NodeSkeleton: React.FC = ({ name, content }: TreeNodeProps) => { 8 | return ( 9 |
10 |
11 |

{name}

12 |
13 |
{content}
14 |
15 | ); 16 | }; 17 | export default NodeSkeleton; 18 | -------------------------------------------------------------------------------- /components/LibraryPanel/index.tsx: -------------------------------------------------------------------------------- 1 | import { DATA_TRANSFER_KEY, GETTING_STARTED_GUIDE_URL } from '@/commons/constants'; 2 | import Tree from '@/components/LibTree/Tree'; 3 | import { TreeProps } from '@/components/LibTree/types'; 4 | import NodeSkeleton from '@/components/LibraryPanel/NodeSkeleton'; 5 | import { XForceNodeType, X_FORCE_NODES } from '@/components/LibraryPanel/nodes/nodeTypes'; 6 | import Image from 'next/image'; 7 | import React from 'react'; 8 | import { v4 as uuidv4 } from 'uuid'; 9 | 10 | export const PANEL_WIDTH = 320; // w-80 in tailwind 11 | 12 | const LibraryPanel = () => { 13 | const [informationVisible, setInformationVisible] = React.useState(true); 14 | 15 | const onDragStart = (event: React.DragEvent, node: XForceNodeType) => { 16 | const newNode = { ...node, id: `${node.id}__${uuidv4()}` }; 17 | event.dataTransfer.setData(DATA_TRANSFER_KEY, JSON.stringify(newNode)); 18 | event.dataTransfer.effectAllowed = 'move'; 19 | }; 20 | 21 | const treeData: TreeProps['data'] = [ 22 | { 23 | id: 'n2', 24 | name: 'Agents', 25 | initiallyExpanded: true, 26 | draggable: false, 27 | children: [ 28 | { 29 | id: 'n1', 30 | name: 'GroupChat', 31 | onDrag: (event) => onDragStart(event, X_FORCE_NODES.GROUP_CHAT), 32 | jsxElement: ( 33 | 37 | GroupChat 38 | is a subclass of ConversableAgent configured with a default system message. 39 | 40 | } 41 | /> 42 | ), 43 | }, 44 | { 45 | id: 'n2', 46 | name: 'UserProxy', 47 | onDrag: (event) => onDragStart(event, X_FORCE_NODES.USER_PROXY), 48 | jsxElement: ( 49 | 53 | UserProxy 54 | 55 | acts as a mediator between the agent workforce and the user, facilitating feedback communication. 56 | 57 | 58 | } 59 | /> 60 | ), 61 | }, 62 | { 63 | id: 'n3', 64 | name: 'GPTAssistantAgent', 65 | onDrag: (event) => onDragStart(event, X_FORCE_NODES.GPT_ASSISTANT_AGENT), 66 | jsxElement: ( 67 | 71 | 72 | GPTAssistantAgent 73 | 74 | 75 | built on OpenAI’s assistant API, enables the integration of any OpenAI Assistant into your 76 | workforce. 77 | 78 | 79 | } 80 | /> 81 | ), 82 | }, 83 | { 84 | id: 'n4', 85 | name: 'AssistantAgent', 86 | onDrag: (event) => onDragStart(event, X_FORCE_NODES.ASSISTANT_AGENT), 87 | jsxElement: ( 88 | 92 | AssistantAgent 93 | is an agent you can configure with a system message. 94 | 95 | } 96 | /> 97 | ), 98 | }, 99 | ], 100 | }, 101 | { 102 | id: 'n3', 103 | name: 'Tools', 104 | initiallyExpanded: true, 105 | children: [ 106 | { 107 | id: 'n1', 108 | name: 'CustomFunction', 109 | onDrag: (event) => onDragStart(event, X_FORCE_NODES.CUSTOM_FUNCTION), 110 | jsxElement: ( 111 | 115 | CustomFunction 116 | 117 | allows you to add your own functions to GPTAssistantAgent to enhance them with specific 118 | functionalities. 119 | 120 | 121 | } 122 | /> 123 | ), 124 | }, 125 | ], 126 | }, 127 | ]; 128 | 129 | return ( 130 |
131 |
132 |
133 | software 2.0 134 |
135 |
136 |
137 |

Library

138 | {informationVisible && ( 139 |
140 |
141 |

Tip

'Library' includes a list of nodes that you 142 | can add to the workstation (the panel on the right). You can simply drag and drop them into the 143 | workstation. 144 |
145 | software 2.0 146 |
147 |

setInformationVisible(false)}> 148 | I understand! 149 |

150 |

window.open(GETTING_STARTED_GUIDE_URL, '_blank')} 153 | > 154 | Learn more... 155 |

156 |
157 |
158 | )} 159 | 160 |
161 | {process.env.NEXT_PUBLIC_VERSION_NUMBER && ( 162 | 165 | {process.env.NEXT_PUBLIC_VERSION_NUMBER} 166 | 167 | )} 168 |
169 | ); 170 | }; 171 | 172 | export default LibraryPanel; 173 | // 174 | -------------------------------------------------------------------------------- /components/LibraryPanel/nodes/Fields.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | type FieldSchemaType = { 4 | field: React.ReactElement; 5 | errors?: string; 6 | }; 7 | const FieldSchema: React.FC = ({ field, errors }) => { 8 | return ( 9 |
10 | {field} 11 | {errors} 12 |
13 | ); 14 | }; 15 | 16 | type InputFieldProps = { 17 | label: string; 18 | type: React.HTMLInputTypeAttribute; 19 | onChange: (e: React.ChangeEvent) => void; 20 | value?: string; 21 | required?: boolean; 22 | placeholder?: string; 23 | }; 24 | export const InputField: React.FC = ({ label, type, onChange, value, required, placeholder }) => { 25 | const requiredString = required ? '*' : ''; 26 | 27 | return ( 28 |
29 |

30 | {label} 31 | {requiredString} 32 |

33 | 40 |
41 | ); 42 | }; 43 | 44 | type Props = { 45 | label: string; 46 | selected: string; 47 | options: string[]; 48 | onChange: (e: React.ChangeEvent) => void; 49 | required?: boolean; 50 | }; 51 | export const SelectField: React.FC = ({ selected, onChange, options, label, required }) => { 52 | const requiredString = required ? '*' : ''; 53 | 54 | return ( 55 |
56 |

57 | {label} 58 | {requiredString} 59 |

60 | 71 |
72 | ); 73 | }; 74 | 75 | export default FieldSchema; 76 | -------------------------------------------------------------------------------- /components/LibraryPanel/nodes/ToolbarSkeleton.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | type Props = { 4 | header: React.JSX.Element; 5 | content: React.JSX.Element; 6 | }; 7 | 8 | type ClsHeaderSkeletonProps = { 9 | name: string; 10 | }; 11 | export const ClsHeaderSkeleton: React.FC = ({ name }) => { 12 | return ( 13 |

14 | class 15 | {name} 16 |

17 | ); 18 | }; 19 | 20 | type MethodHeaderSkeletonProps = { 21 | name: string; 22 | }; 23 | export const MethodHeaderSkeleton: React.FC = ({ name }) => { 24 | return ( 25 |

26 | def 27 | {name} 28 |

29 | ); 30 | }; 31 | type DefaultContentProps = { 32 | name?: string; 33 | description: string; 34 | docTeaser?: string; 35 | }; 36 | export const DefaultContent: React.FC = ({ name, description, docTeaser }) => { 37 | return ( 38 | <> 39 |

40 | {name && {name}} 41 | {description} 42 |

43 |

{docTeaser}

44 | 45 | ); 46 | }; 47 | 48 | export const ToolbarSkeleton: React.FC = ({ header, content }) => { 49 | return ( 50 |
51 |
{header}
52 |
{content}
53 |
54 | ); 55 | }; 56 | -------------------------------------------------------------------------------- /components/LibraryPanel/nodes/autogen/AssistantAgent.tsx: -------------------------------------------------------------------------------- 1 | import FieldSchema, { InputField, SelectField } from '@/components/LibraryPanel/nodes/Fields'; 2 | import { ClsHeaderSkeleton, DefaultContent, ToolbarSkeleton } from '@/components/LibraryPanel/nodes/ToolbarSkeleton'; 3 | import { ValidatorContext } from '@/contexts/ValidatorContext'; 4 | import useDnDStore from '@/stores/useDnDStore'; 5 | import { OAIModelsEnum } from '@/types/enum'; 6 | import { InformationCircleIcon } from '@heroicons/react/24/outline'; 7 | import React from 'react'; 8 | import { Handle, NodeToolbar, Position, NodeProps as ReactFlowNodeProps, useReactFlow } from 'reactflow'; 9 | import { XForceNodesEnum } from '../nodeTypes'; 10 | 11 | const AssistantAgent: React.FC = (props) => { 12 | const { errors } = React.useContext(ValidatorContext); 13 | const { addNodeData } = useDnDStore(); 14 | const { getNode } = useReactFlow(); 15 | 16 | const data = getNode(props.id)?.data; 17 | const [toolbarVisible, setToolbarVisible] = React.useState(false); 18 | 19 | const onAgentNameChange = (e: React.ChangeEvent) => { 20 | const val = e.target.value.trim(); 21 | addNodeData(props.id, { variableName: val }); 22 | }; 23 | 24 | const onSystemPromptChange = (e: React.ChangeEvent) => { 25 | const val = e.target.value; 26 | addNodeData(props.id, { systemPrompt: val }); 27 | }; 28 | 29 | return ( 30 |
31 |
34 |
AssistantAgent
35 | setToolbarVisible(true)} 39 | onMouseLeave={() => setToolbarVisible(false)} 40 | /> 41 | 42 | } 44 | content={ 45 | 50 | } 51 | /> 52 | 53 |
54 |
55 | 65 | } 66 | errors={errors?.[props.id]?.variableName} 67 | /> 68 | 77 | } 78 | errors={errors?.[props.id]?.systemPrompt} 79 | /> 80 | addNodeData(props.id, { selectedModel: e.target.value })} 86 | options={Object.values(OAIModelsEnum)} 87 | /> 88 | } 89 | errors={errors?.[props.id]?.selectedModel} 90 | /> 91 |
92 | 93 |
94 | ); 95 | }; 96 | 97 | export default AssistantAgent; 98 | -------------------------------------------------------------------------------- /components/LibraryPanel/nodes/autogen/CustomFunction.tsx: -------------------------------------------------------------------------------- 1 | import { XForceNodesEnum } from '@/components/LibraryPanel/nodes/nodeTypes'; 2 | import { DefaultContent, MethodHeaderSkeleton, ToolbarSkeleton } from '@/components/LibraryPanel/nodes/ToolbarSkeleton'; 3 | import { ValidatorContext } from '@/contexts/ValidatorContext'; 4 | import useDnDStore from '@/stores/useDnDStore'; 5 | import { InformationCircleIcon } from '@heroicons/react/24/outline'; 6 | import React from 'react'; 7 | import { Handle, NodeToolbar, Position, NodeProps as ReactFlowNodeProps } from 'reactflow'; 8 | 9 | import { highlight, languages } from 'prismjs'; 10 | import 'prismjs/components/prism-clike'; 11 | import 'prismjs/components/prism-python'; 12 | import 'prismjs/themes/prism.css'; 13 | import Editor from 'react-simple-code-editor'; 14 | 15 | const CustomFunction: React.FC = (props) => { 16 | const { errors } = React.useContext(ValidatorContext); 17 | const { addNodeData } = useDnDStore(); 18 | const [toolbarVisible, setToolbarVisible] = React.useState(false); 19 | const [code, setCode] = React.useState(''); 20 | 21 | const onCustomFuncChange = (code: string) => { 22 | addNodeData(props.id, { func: code }); 23 | setCode(code); 24 | }; 25 | 26 | return ( 27 |
28 |
31 |
Custom Function
32 | setToolbarVisible(true)} 36 | onMouseLeave={() => setToolbarVisible(false)} 37 | /> 38 | 39 | } 41 | content={ 42 | 46 | } 47 | /> 48 | 49 |
50 |
51 |
52 | highlight(code || '', languages.python!, 'py')} 57 | padding={10} 58 | className="max-w-96 max-h-96 min-h-16 overflow-y-auto bg-white w-full" 59 | style={{ 60 | fontFamily: '"Fira code", "Fira Mono", monospace', 61 | fontSize: 12, 62 | }} 63 | textareaClassName="outline-none w-80" 64 | /> 65 |
66 | {errors?.[props.id]?.func && {errors?.[props.id]?.func}} 67 |
68 | 69 |
70 | ); 71 | }; 72 | 73 | export default CustomFunction; 74 | -------------------------------------------------------------------------------- /components/LibraryPanel/nodes/autogen/GPTAssistantAgent.tsx: -------------------------------------------------------------------------------- 1 | import FieldSchema, { InputField, SelectField } from '@/components/LibraryPanel/nodes/Fields'; 2 | import { ClsHeaderSkeleton, DefaultContent, ToolbarSkeleton } from '@/components/LibraryPanel/nodes/ToolbarSkeleton'; 3 | import { ValidatorContext } from '@/contexts/ValidatorContext'; 4 | import useDnDStore from '@/stores/useDnDStore'; 5 | import { OAIModelsEnum } from '@/types/enum'; 6 | import { InformationCircleIcon } from '@heroicons/react/24/outline'; 7 | import React from 'react'; 8 | import { Handle, NodeToolbar, Position, NodeProps as ReactFlowNodeProps, useReactFlow } from 'reactflow'; 9 | import { XForceNodesEnum } from '../nodeTypes'; 10 | 11 | const GPTAssistantAgent: React.FC = (props) => { 12 | const { errors } = React.useContext(ValidatorContext); 13 | const { addNodeData } = useDnDStore(); 14 | const { getNode } = useReactFlow(); 15 | 16 | const data = getNode(props.id)?.data; 17 | const [toolbarVisible, setToolbarVisible] = React.useState(false); 18 | 19 | const onAgentNameChange = React.useCallback( 20 | (evt: React.ChangeEvent) => { 21 | const val = evt.target.value.trim(); 22 | addNodeData(props.id, { variableName: val }); 23 | }, 24 | [addNodeData, props.id], 25 | ); 26 | const onOAIIdChange = React.useCallback( 27 | (evt: React.ChangeEvent) => { 28 | const val = evt.target.value.trim(); 29 | addNodeData(props.id, { OAIId: val }); 30 | }, 31 | [addNodeData, props.id], 32 | ); 33 | 34 | return ( 35 |
36 |
39 |
GPTAssistantAgent
40 | setToolbarVisible(true)} 44 | onMouseLeave={() => setToolbarVisible(false)} 45 | /> 46 | 47 | } 49 | content={ 50 | 55 | } 56 | /> 57 | 58 |
59 |
60 | 70 | } 71 | errors={errors?.[props.id]?.variableName} 72 | /> 73 | 83 | } 84 | errors={errors?.[props.id]?.OAIId} 85 | /> 86 | addNodeData(props.id, { selectedModel: e.target.value })} 92 | options={Object.values(OAIModelsEnum)} 93 | /> 94 | } 95 | errors={errors?.[props.id]?.selectedModel} 96 | /> 97 |
98 | 99 | 100 | 101 |
102 | ); 103 | }; 104 | 105 | export default GPTAssistantAgent; 106 | -------------------------------------------------------------------------------- /components/LibraryPanel/nodes/autogen/GroupChat.tsx: -------------------------------------------------------------------------------- 1 | import FieldSchema, { InputField, SelectField } from '@/components/LibraryPanel/nodes/Fields'; 2 | import { XForceNodesEnum } from '@/components/LibraryPanel/nodes/nodeTypes'; 3 | import { ClsHeaderSkeleton, DefaultContent, ToolbarSkeleton } from '@/components/LibraryPanel/nodes/ToolbarSkeleton'; 4 | import { ValidatorContext } from '@/contexts/ValidatorContext'; 5 | import useDnDStore from '@/stores/useDnDStore'; 6 | import { AgentSelectionStrategyEnum, OAIModelsEnum } from '@/types/enum'; 7 | import { InformationCircleIcon } from '@heroicons/react/24/outline'; 8 | import React, { memo } from 'react'; 9 | import { Handle, NodeToolbar, Position, NodeProps as ReactFlowNodeProps, useReactFlow } from 'reactflow'; 10 | 11 | const GroupChat: React.FC = (props) => { 12 | const { errors } = React.useContext(ValidatorContext); 13 | const { addNodeData } = useDnDStore(); 14 | const { getNode } = useReactFlow(); 15 | const [toolbarVisible, setToolbarVisible] = React.useState(false); 16 | const data = getNode(props.id)?.data; 17 | 18 | const onAgentNameChange = (e: React.ChangeEvent) => { 19 | const val = e.target.value.trim(); 20 | addNodeData(props.id, { variableName: val }); 21 | }; 22 | const onMaxRoundsChange = (e: React.ChangeEvent) => { 23 | const val = e.target.value.trim(); 24 | addNodeData(props.id, { maxRounds: parseInt(val) }); 25 | }; 26 | const onSpeakerSelectionChange = (e: React.ChangeEvent) => { 27 | const val = e.target.value; 28 | addNodeData(props.id, { agentSelection: val }); 29 | }; 30 | const onLLMChange = (e: React.ChangeEvent) => { 31 | const val = e.target.value; 32 | addNodeData(props.id, { selectedModel: val }); 33 | }; 34 | 35 | return ( 36 |
37 |
38 |
GroupChat
39 | setToolbarVisible(true)} 43 | onMouseLeave={() => setToolbarVisible(false)} 44 | /> 45 | 46 | } 48 | content={ 49 | 54 | } 55 | /> 56 | 57 |
58 |
59 | 69 | } 70 | errors={errors?.[props.id]?.variableName} 71 | /> 72 | 81 | } 82 | errors={errors?.[props.id]?.maxRounds} 83 | /> 84 | 92 | } 93 | errors={errors?.[props.id]?.agentSelection} 94 | /> 95 | 103 | } 104 | errors={errors?.[props.id]?.selectedModel} 105 | /> 106 |
107 | 108 |
109 | ); 110 | }; 111 | 112 | export default memo(GroupChat); 113 | -------------------------------------------------------------------------------- /components/LibraryPanel/nodes/autogen/UserProxy.tsx: -------------------------------------------------------------------------------- 1 | import FieldSchema, { InputField } from '@/components/LibraryPanel/nodes/Fields'; 2 | import { ClsHeaderSkeleton, DefaultContent, ToolbarSkeleton } from '@/components/LibraryPanel/nodes/ToolbarSkeleton'; 3 | import { ValidatorContext } from '@/contexts/ValidatorContext'; 4 | import useDnDStore from '@/stores/useDnDStore'; 5 | import { InformationCircleIcon } from '@heroicons/react/24/outline'; 6 | import React from 'react'; 7 | import { Handle, NodeToolbar, Position, NodeProps as ReactFlowNodeProps, useReactFlow } from 'reactflow'; 8 | import { XForceNodesEnum } from '../nodeTypes'; 9 | 10 | const UserProxy: React.FC = (props) => { 11 | const { errors } = React.useContext(ValidatorContext); 12 | const { addNodeData } = useDnDStore(); 13 | const { getNode } = useReactFlow(); 14 | const [toolbarVisible, setToolbarVisible] = React.useState(false); 15 | 16 | const data = getNode(props.id)?.data; 17 | const onVarNameChange = React.useCallback( 18 | (evt: React.ChangeEvent) => { 19 | const val = evt.target.value.trim(); 20 | addNodeData(props.id, { variableName: val }); 21 | }, 22 | [addNodeData, props.id], 23 | ); 24 | const onPromptChange = React.useCallback( 25 | (evt: React.ChangeEvent) => { 26 | const val = evt.target.value; 27 | addNodeData(props.id, { initialPrompt: val }); 28 | }, 29 | [addNodeData, props.id], 30 | ); 31 | 32 | return ( 33 |
34 |
35 |
UserProxy
36 | setToolbarVisible(true)} 40 | onMouseLeave={() => setToolbarVisible(false)} 41 | /> 42 | 43 | } 45 | content={ 46 | 53 | } 54 | /> 55 | 56 |
57 |
58 | 68 | } 69 | errors={errors?.[props.id]?.variableName} 70 | /> 71 | 81 | } 82 | errors={errors?.[props.id]?.initialPrompt} 83 | /> 84 |
85 | 86 | 87 |
88 | ); 89 | }; 90 | 91 | export default UserProxy; 92 | -------------------------------------------------------------------------------- /components/LibraryPanel/nodes/nodeTypes.ts: -------------------------------------------------------------------------------- 1 | import AssistantAgent from '@/components/LibraryPanel/nodes/autogen/AssistantAgent'; 2 | import CustomFunction from '@/components/LibraryPanel/nodes/autogen/CustomFunction'; 3 | import GPTAssistantAgent from '@/components/LibraryPanel/nodes/autogen/GPTAssistantAgent'; 4 | import GroupChat from '@/components/LibraryPanel/nodes/autogen/GroupChat'; 5 | import UserProxy from '@/components/LibraryPanel/nodes/autogen/UserProxy'; 6 | import React from 'react'; 7 | import { NodeProps, Node as ReactFlowNode } from 'reactflow'; 8 | 9 | export enum XForceNodesEnum { 10 | USER_PROXY = 'USER_PROXY', 11 | GROUP_CHAT = 'GROUP_CHAT', 12 | GPT_ASSISTANT_AGENT = 'GPT_ASSISTANT_AGENT', 13 | CUSTOM_FUNCTION = 'CUSTOM_FUNCTION', 14 | ASSISTANT_AGENT = 'ASSISTANT_AGENT', 15 | } 16 | export type XForceNodeDataType = { 17 | connectivity: { 18 | input: XForceNodesEnum[] | null; 19 | output: XForceNodesEnum[] | null; 20 | }; 21 | }; 22 | export type XForceNodeType = Omit, 'position'>; 23 | 24 | export const X_FORCE_NODES: { [k in XForceNodesEnum]: XForceNodeType } = { 25 | GROUP_CHAT: { 26 | id: XForceNodesEnum.GROUP_CHAT, 27 | type: XForceNodesEnum.GROUP_CHAT, 28 | dragHandle: `.${XForceNodesEnum.GROUP_CHAT}`, 29 | data: { 30 | connectivity: { 31 | input: [XForceNodesEnum.USER_PROXY, XForceNodesEnum.GPT_ASSISTANT_AGENT, XForceNodesEnum.ASSISTANT_AGENT], 32 | output: null, 33 | }, 34 | }, 35 | }, 36 | USER_PROXY: { 37 | id: XForceNodesEnum.USER_PROXY, 38 | type: XForceNodesEnum.USER_PROXY, 39 | dragHandle: `.${XForceNodesEnum.USER_PROXY}`, 40 | data: { 41 | connectivity: { 42 | input: null, 43 | output: [XForceNodesEnum.GROUP_CHAT], 44 | }, 45 | }, 46 | }, 47 | GPT_ASSISTANT_AGENT: { 48 | id: XForceNodesEnum.GPT_ASSISTANT_AGENT, 49 | type: XForceNodesEnum.GPT_ASSISTANT_AGENT, 50 | dragHandle: `.${XForceNodesEnum.GPT_ASSISTANT_AGENT}`, 51 | data: { 52 | connectivity: { 53 | input: [XForceNodesEnum.CUSTOM_FUNCTION], 54 | output: [XForceNodesEnum.GROUP_CHAT], 55 | }, 56 | }, 57 | }, 58 | CUSTOM_FUNCTION: { 59 | id: XForceNodesEnum.CUSTOM_FUNCTION, 60 | type: XForceNodesEnum.CUSTOM_FUNCTION, 61 | dragHandle: `.${XForceNodesEnum.CUSTOM_FUNCTION}`, 62 | data: { 63 | connectivity: { 64 | input: null, 65 | output: [XForceNodesEnum.GPT_ASSISTANT_AGENT], 66 | }, 67 | }, 68 | }, 69 | ASSISTANT_AGENT: { 70 | id: XForceNodesEnum.ASSISTANT_AGENT, 71 | type: XForceNodesEnum.ASSISTANT_AGENT, 72 | dragHandle: `.${XForceNodesEnum.ASSISTANT_AGENT}`, 73 | data: { 74 | connectivity: { 75 | input: null, 76 | output: [XForceNodesEnum.GROUP_CHAT], 77 | }, 78 | }, 79 | }, 80 | }; 81 | export const CUSTOM_X_FORCE_NODES: { [_ in XForceNodesEnum]: React.ComponentType } = { 82 | USER_PROXY: UserProxy, 83 | GROUP_CHAT: GroupChat, 84 | GPT_ASSISTANT_AGENT: GPTAssistantAgent, 85 | CUSTOM_FUNCTION: CustomFunction, 86 | ASSISTANT_AGENT: AssistantAgent, 87 | }; 88 | -------------------------------------------------------------------------------- /components/TopBar/index.tsx: -------------------------------------------------------------------------------- 1 | import { ModalContext } from '@/contexts/ModalContext'; 2 | import { toPng } from 'html-to-image'; 3 | import React from 'react'; 4 | import { getRectOfNodes, getTransformForBounds, useReactFlow } from 'reactflow'; 5 | 6 | import { EXAMPLES_URL, GETTING_STARTED_GUIDE_URL, GITHUB_URL } from '@/commons/constants'; 7 | import { ContextMenuItemType } from '@/commons/types'; 8 | import ContextMenuModal from '@/components/modals/ContextMenu/ContextMenuModal'; 9 | import ToastMessageModal from '@/components/modals/ToastMessageModal'; 10 | import { ValidatorContext } from '@/contexts/ValidatorContext'; 11 | import useAppStore from '@/stores/useAppStore'; 12 | import useDnDStore from '@/stores/useDnDStore'; 13 | import { CODE_BUILDER } from '@/transpiler/primitive'; 14 | 15 | type MenuItemProps = React.HTMLProps & { 16 | name: string; 17 | }; 18 | const MenuItem: React.FC = (props: MenuItemProps) => { 19 | const { name } = props; 20 | return ( 21 |
22 |
23 |

{name}

24 |
25 |
26 | ); 27 | }; 28 | 29 | const IMAGE_WIDTH = 1024; 30 | const IMAGE_HEIGHT = 768; 31 | 32 | const TopBar: React.FC = () => { 33 | const { setModal } = React.useContext(ModalContext); 34 | const { getNodes, getEdges } = useReactFlow(); 35 | const { nodes, clearGraph } = useDnDStore(); 36 | const { oaiKey } = useAppStore(); 37 | const { validate } = React.useContext(ValidatorContext); 38 | 39 | const onClickExportAsPython = () => { 40 | const isValid = validate(nodes); 41 | if (!isValid) { 42 | return; 43 | } 44 | 45 | const element = document.createElement('a'); 46 | const file = new Blob([CODE_BUILDER(getNodes(), getEdges(), oaiKey)], { type: 'text/plain' }); 47 | 48 | element.href = URL.createObjectURL(file); 49 | element.download = 'my x-force workflow.py'; 50 | document.body.appendChild(element); 51 | element.click(); 52 | }; 53 | const onClickExportAsPNG = async () => { 54 | const nodesBounds = getRectOfNodes(getNodes()); 55 | const transform = getTransformForBounds(nodesBounds, IMAGE_WIDTH, IMAGE_HEIGHT, 0.5, 2); 56 | const viewport = document.querySelector('.react-flow__viewport'); 57 | if (!viewport) return null; 58 | const png = await toPng(viewport as HTMLElement, { 59 | backgroundColor: '#fff', 60 | width: IMAGE_WIDTH, 61 | height: IMAGE_HEIGHT, 62 | style: { 63 | width: '1024', 64 | height: '768', 65 | transform: `translate(${transform[0]}px, ${transform[1]}px) scale(${transform[2]})`, 66 | }, 67 | }); 68 | const a = document.createElement('a'); 69 | a.setAttribute('download', 'my x-force workflow.png'); 70 | a.setAttribute('href', png); 71 | a.click(); 72 | }; 73 | 74 | const onSave = () => { 75 | setModal(); 76 | }; 77 | const onClearGraph = () => { 78 | if (nodes) { 79 | if (confirm('Your changes will be destroyed, are you sure you want to create new workstation?')) { 80 | clearGraph(); 81 | } 82 | } 83 | }; 84 | const CTX_MENU__FILE: ContextMenuItemType[] = [ 85 | { item: 'New', onClick: onClearGraph }, 86 | { item: 'Save', onClick: onSave }, 87 | { 88 | item: 'Export As', 89 | subs: [ 90 | { item: 'Python Code...', onClick: onClickExportAsPython }, 91 | { item: 'PNG...', onClick: onClickExportAsPNG }, 92 | ], 93 | }, 94 | ]; 95 | 96 | const onClickFile = (e: React.MouseEvent) => { 97 | e.preventDefault(); 98 | e.stopPropagation(); 99 | setModal(); 100 | }; 101 | return ( 102 |
103 |
104 |
105 | 106 |
107 |
108 | window.open(GETTING_STARTED_GUIDE_URL, '_blank')} /> 109 | window.open(EXAMPLES_URL, '_blank')} /> 110 | window.open(GITHUB_URL, '_blank')} /> 111 |
112 |
113 |
114 | ); 115 | }; 116 | 117 | export default TopBar; 118 | -------------------------------------------------------------------------------- /components/UI/App.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactFlow, { Background } from 'reactflow'; 3 | 4 | import { DND_ID } from '@/commons/constants'; 5 | import AppStatus from '@/components/AppStatus'; 6 | import LibraryPanel from '@/components/LibraryPanel'; 7 | import { CUSTOM_X_FORCE_NODES } from '@/components/LibraryPanel/nodes/nodeTypes'; 8 | import TopBar from '@/components/TopBar'; 9 | import EmptyWorkstation from '@/components/Workstation'; 10 | import ToastMessageModal from '@/components/modals/ToastMessageModal'; 11 | import { ModalContext } from '@/contexts/ModalContext'; 12 | import useDnDFlow from '@/hooks/useDnDFlow'; 13 | import useKeyboardListener from '@/hooks/useKeyboardListener'; 14 | import useMountedState from '@/hooks/useMountedState'; 15 | import useDnDStore from '@/stores/useDnDStore'; 16 | 17 | const AppX = () => { 18 | const { nodes, edges, onNodesChange, onEdgesChange, onConnect, onInit } = useDnDStore(); 19 | const isMounted = useMountedState(); 20 | const { onNodeDragOver, onNodeDropToWorkstation, isValidConnection, onNodeContextMenu, onEdgeContextMenu } = 21 | useDnDFlow(); 22 | const { setModal } = React.useContext(ModalContext); 23 | 24 | useKeyboardListener({ 25 | onSave: { modal: }, 26 | }); 27 | 28 | return ( 29 |
30 | 31 |
32 | 33 | 34 | setModal(null)} 48 | className={DND_ID} 49 | attributionPosition="bottom-left" 50 | > 51 | {isMounted() && nodes.length ? : null} 52 | 53 | 54 |
55 |
56 | ); 57 | }; 58 | 59 | export default AppX; 60 | -------------------------------------------------------------------------------- /components/UI/Mobile.tsx: -------------------------------------------------------------------------------- 1 | import { GETTING_STARTED_GUIDE_URL } from '@/commons/constants'; 2 | import Image from 'next/image'; 3 | import React from 'react'; 4 | 5 | type WelcomePanelButtonProps = React.HTMLProps & { 6 | name: string; 7 | }; 8 | const WelcomePanelButton: React.FC = (props) => { 9 | return ( 10 |
11 |
12 |

{props.name}

13 |
14 |
15 | ); 16 | }; 17 | const MobilePanel = () => { 18 | const onClickGettingStartedGuide = () => { 19 | window.open(GETTING_STARTED_GUIDE_URL, '_blank'); 20 | }; 21 | 22 | return ( 23 |
24 |
25 |
26 | software 2.0 35 |

36 | Create task specific agent workforces for your custom business logic using diagrams. 37 |

38 |

39 | You can drag and drop agents from the "Library", connect them whatever you like, give them an 40 | initial task, export them as a Python Script and run it on your local machine. 41 | {'\n\n'} 42 | To learn more, follow our{' '} 43 | 44 | Getting Started 45 | {' '} 46 | guide. 47 | {'\n\n'} 48 | We support enterprises by providing them Cloud Runs with our operating system build to run LLMs. Contact us 49 | to learn more{' '} 50 | 51 | enterprise-support@x-force.ai 52 | 53 | . 54 | {/* {'\n\n'} 55 | Learn more about X-Force at x-force.ai/about 56 | ! */} 57 |

58 | {'software 67 |
68 |
69 |
70 | alert("Currently we're not supporting mobile runs, please enter with your computer!")} 74 | /> 75 |

(you need to start with a computer!)

76 |
77 | 82 |
83 |
84 |
85 | ); 86 | }; 87 | 88 | export default MobilePanel; 89 | -------------------------------------------------------------------------------- /components/Workstation/index.tsx: -------------------------------------------------------------------------------- 1 | import { GETTING_STARTED_GUIDE_URL } from '@/commons/constants'; 2 | import useMountedState from '@/hooks/useMountedState'; 3 | import useDnDStore from '@/stores/useDnDStore'; 4 | import Image from 'next/image'; 5 | import React from 'react'; 6 | 7 | type ButtonProps = React.HTMLProps & { 8 | name: string; 9 | }; 10 | const Button: React.FC = (props) => { 11 | return ( 12 |
13 |
14 |

{props.name}

15 |
16 |
17 | ); 18 | }; 19 | const EmptyWorkstation = () => { 20 | const { nodes } = useDnDStore(); 21 | const isMounted = useMountedState(); 22 | const [panelVisible, setPanelVisible] = React.useState(true); 23 | 24 | const onNodeDragOver: React.DragEventHandler = React.useCallback( 25 | (event: React.DragEvent) => { 26 | event.preventDefault(); 27 | setPanelVisible(false); 28 | }, 29 | [], 30 | ); 31 | 32 | const onClickNewProject = () => setPanelVisible(false); 33 | const onClickGettingStartedGuide = () => { 34 | window.open(GETTING_STARTED_GUIDE_URL, '_blank'); 35 | }; 36 | 37 | if (!panelVisible || !isMounted()) return <>; 38 | if (isMounted() && nodes.length) return <>; 39 | return ( 40 |
45 |
46 |
47 | software 2.0 48 |

49 | Create task specific agent workforces for your custom business logic using diagrams. 50 |

51 |

52 | You can drag and drop agents from the "Library" to here, connect them whatever you like, give them 53 | an initial task, export them as a Python Script and run it on your local machine. 54 | {'\n\n'} 55 | To learn more, follow our{' '} 56 | 57 | Getting Started 58 | {' '} 59 | guide. 60 | {'\n\n\n'} 61 | We support enterprises by providing them Cloud Runs with our operating system build to run LLMs. Contact us 62 | to learn more{' '} 63 | 68 | enterprise-support@x-force.ai 69 | 70 | . 71 |

72 |
73 |
74 |
77 |
78 |
79 | ); 80 | }; 81 | 82 | export default EmptyWorkstation; 83 | -------------------------------------------------------------------------------- /components/modals/AppStatus/APIKeyModal.tsx: -------------------------------------------------------------------------------- 1 | import { ModalContext } from '@/contexts/ModalContext'; 2 | import useAppStore from '@/stores/useAppStore'; 3 | import { XMarkIcon } from '@heroicons/react/24/outline'; 4 | import React from 'react'; 5 | 6 | const APIKeyModal: React.FC = () => { 7 | const { oaiKey, setOAIKey } = useAppStore(); 8 | const { setModal } = React.useContext(ModalContext); 9 | 10 | const onChangeAPIKey = React.useCallback( 11 | (e: React.ChangeEvent) => { 12 | const val = e.target.value; 13 | setOAIKey(val); 14 | }, 15 | [setOAIKey], 16 | ); 17 | 18 | return ( 19 |
20 |
21 |

Set Up LLM Key

22 | setModal(null)} className="cursor-pointer" /> 23 |
24 |
25 |
26 | Provider 27 | OpenAI 28 |
29 |
30 |

Key

31 | 38 |

39 | Your API Key will never going to be stored on cloud, it will be temporarily saved only for the current 40 | session, and will be deleted whenever you close this session. If you don’t add your API Key, you’ll need to 41 | manually add it to the code after you export as Python script. 42 |

43 |
44 |
45 |
46 | ); 47 | }; 48 | 49 | export default APIKeyModal; 50 | -------------------------------------------------------------------------------- /components/modals/ContextMenu/ContextMenuItem.tsx: -------------------------------------------------------------------------------- 1 | import { ContextMenuItemType } from '@/commons/types'; 2 | import DefaultContextMenuItem from '@/components/modals/ContextMenu/DefaultContextMenuItem'; 3 | import React from 'react'; 4 | 5 | const ContextMenuItem: React.FC = (props) => { 6 | const [isChildrenVisible, setChildrenVisible] = React.useState(false); 7 | const hasChildren = props.subs ?? false; 8 | 9 | const renderChildren = () => { 10 | return ( 11 |
    16 | {props.subs?.map((subItem, index) => ( 17 | 18 | ))} 19 |
20 | ); 21 | }; 22 | 23 | return ( 24 |
  • setChildrenVisible(true)} 29 | onMouseLeave={() => setChildrenVisible(false)} 30 | > 31 | {typeof props.item === 'string' ? : props.item} 32 | {hasChildren && renderChildren()} 33 |
  • 34 | ); 35 | }; 36 | 37 | export default ContextMenuItem; 38 | -------------------------------------------------------------------------------- /components/modals/ContextMenu/ContextMenuModal.tsx: -------------------------------------------------------------------------------- 1 | import { ContextMenuItemType } from '@/commons/types'; 2 | import ContextMenuItem from '@/components/modals/ContextMenu/ContextMenuItem'; 3 | import { ModalContext } from '@/contexts/ModalContext'; 4 | import React from 'react'; 5 | 6 | type ContextMenuModalPropsType = { 7 | menu: ContextMenuItemType[]; 8 | style: React.CSSProperties; 9 | }; 10 | const ContextMenuModal: React.FC = ({ menu, style }) => { 11 | const ref = React.useRef(null); 12 | const { setModal } = React.useContext(ModalContext); 13 | React.useEffect(() => { 14 | const onClick = (_: MouseEvent) => { 15 | // TODO [P2]: if ref.current contains e.target, and if e.target have a child, don't close the modal 16 | setModal(null); 17 | }; 18 | document.addEventListener('click', onClick); 19 | return () => { 20 | document.removeEventListener('click', onClick); 21 | }; 22 | }, [setModal]); 23 | 24 | const renderItems = () => { 25 | return menu?.map((menuItem, index) => ); 26 | }; 27 | return ( 28 |
    33 |
      {renderItems()}
    34 |
    35 | ); 36 | }; 37 | 38 | export default ContextMenuModal; 39 | -------------------------------------------------------------------------------- /components/modals/ContextMenu/DefaultContextMenuItem.tsx: -------------------------------------------------------------------------------- 1 | import { ContextMenuItemType } from '@/commons/types'; 2 | import { ChevronRightIcon } from '@heroicons/react/24/solid'; 3 | import React from 'react'; 4 | 5 | const DefaultContextMenuItem: React.FC & ContextMenuItemType> = (props) => { 6 | return ( 7 |

    12 | {props.item} 13 | {props.subs ? : ''} 14 |

    15 | ); 16 | }; 17 | 18 | export default DefaultContextMenuItem; 19 | -------------------------------------------------------------------------------- /components/modals/ToastMessageModal.tsx: -------------------------------------------------------------------------------- 1 | import { ModalContext } from '@/contexts/ModalContext'; 2 | import React from 'react'; 3 | 4 | type ContextMenuModalPropsType = { 5 | msg: string; 6 | style?: React.CSSProperties; 7 | }; 8 | const ToastMessageModal: React.FC = ({ msg, style }) => { 9 | const { setModal } = React.useContext(ModalContext); 10 | React.useEffect(() => { 11 | const timer = setTimeout(() => setModal(null), 3000); 12 | return () => { 13 | clearTimeout(timer); 14 | }; 15 | }, [setModal]); 16 | 17 | React.useEffect(() => { 18 | // onClick anywhere else, discard the modal. 19 | const onClick = (_: MouseEvent) => { 20 | setModal(null); 21 | }; 22 | document.addEventListener('click', onClick); 23 | return () => { 24 | document.removeEventListener('click', onClick); 25 | }; 26 | }, [setModal]); 27 | 28 | return ( 29 |
    e.stopPropagation()} 32 | style={style} 33 | > 34 |

    {msg}

    35 |
    36 | ); 37 | }; 38 | 39 | export default ToastMessageModal; 40 | -------------------------------------------------------------------------------- /components/modals/_BaseModal.tsx: -------------------------------------------------------------------------------- 1 | import { MODAL_ROOT_DIV_ID, MODAL_Z_INDEX } from '@/commons/constants'; 2 | import { ModalContext } from '@/contexts/ModalContext'; 3 | import React from 'react'; 4 | import ReactDOM from 'react-dom'; 5 | 6 | const Modal: React.FC = () => { 7 | const [CONTAINER, SET_CONTAINER] = React.useState(null); 8 | const { modal } = React.useContext(ModalContext); 9 | 10 | React.useEffect(() => { 11 | const TANK = document.getElementById(MODAL_ROOT_DIV_ID); 12 | SET_CONTAINER(TANK); 13 | }, []); 14 | if (!modal || !CONTAINER) return null; 15 | 16 | return ReactDOM.createPortal( 17 |
    18 | {modal} 19 |
    , 20 | CONTAINER, 21 | ); 22 | }; 23 | 24 | export default Modal; 25 | -------------------------------------------------------------------------------- /contexts/ModalContext.tsx: -------------------------------------------------------------------------------- 1 | import Modal from '@/components/modals/_BaseModal'; 2 | import useModalContext from '@/hooks/useModalContext'; 3 | import React from 'react'; 4 | 5 | type ModalContextReturnType = { 6 | modal: React.JSX.Element | null; 7 | setModal: React.Dispatch>; 8 | }; 9 | 10 | export const ModalContext = React.createContext({ 11 | modal: null, 12 | setModal: () => null, 13 | }); 14 | 15 | type ModalContextProviderProps = { 16 | children: React.JSX.Element; 17 | }; 18 | const ModalContextProvider = ({ children }: ModalContextProviderProps) => { 19 | const { modal, setModal } = useModalContext(); 20 | return ( 21 | 22 | 23 | {children} 24 | 25 | ); 26 | }; 27 | 28 | export default ModalContextProvider; 29 | -------------------------------------------------------------------------------- /contexts/ValidatorContext.tsx: -------------------------------------------------------------------------------- 1 | import { DnDFlowValidationSchema } from '@/commons/zod'; 2 | import useDnDStore from '@/stores/useDnDStore'; 3 | import _ from 'lodash'; 4 | import React from 'react'; 5 | import { Node } from 'reactflow'; 6 | 7 | type ErrorObjType = { 8 | [key: string]: { 9 | [key: string]: string; 10 | }; 11 | }; 12 | 13 | type ValidatorContextType = { 14 | errors: ErrorObjType; 15 | validate: (nds: Node[]) => boolean; 16 | }; 17 | 18 | export const ValidatorContext = React.createContext({ 19 | errors: {}, 20 | validate: () => false, 21 | }); 22 | 23 | type ModalContextProviderProps = { 24 | children: React.JSX.Element; 25 | }; 26 | 27 | const ValidatorContextProvider = ({ children }: ModalContextProviderProps) => { 28 | const [errors, setErrors] = React.useState({}); 29 | const [prev, setPrev] = React.useState<{ [x: string]: string }[]>(); 30 | const [nds, setNds] = React.useState([]); 31 | const { nodes } = useDnDStore(); 32 | 33 | const _getZodData = React.useCallback((nds: Node[]) => { 34 | return nds.map(({ type, data }) => { 35 | return { [type as string]: data }; 36 | }); 37 | }, []); 38 | 39 | const validate = React.useCallback( 40 | (nds: Node[]): boolean => { 41 | setNds(nds); 42 | try { 43 | setErrors({}); 44 | const zodData = _getZodData(nds); 45 | setPrev(zodData); 46 | const zodResults = DnDFlowValidationSchema.safeParse(zodData); 47 | // publish errors to consumers 48 | if (!zodResults.success) { 49 | const issues = zodResults.error.issues; 50 | const updatedErrors: ErrorObjType = {}; 51 | issues.forEach(({ path, message }) => { 52 | const nodeIndex = path[0] as number; 53 | const nodeId = nds[nodeIndex].id; 54 | const fieldName = path[path.length - 1]; 55 | 56 | if (!updatedErrors[nodeId as string]) { 57 | updatedErrors[nodeId as string] = {}; 58 | } 59 | updatedErrors[nodeId as string][fieldName as string] = message; 60 | }); 61 | setErrors(updatedErrors); 62 | } 63 | return zodResults.success; 64 | } catch (err) { 65 | // toast msg 66 | return true; 67 | } 68 | }, 69 | [_getZodData], 70 | ); 71 | 72 | React.useEffect(() => { 73 | const filteredNodes = nodes.filter((node) => nds?.find((n) => n.id === node.id)); 74 | if (prev && !_.isEqual(_getZodData(filteredNodes), prev)) { 75 | // after first submission, now validate the nodes has been validated in onChange event. 76 | validate(filteredNodes); 77 | } 78 | }, [_getZodData, nds, nodes, prev, validate]); 79 | 80 | return {children}; 81 | }; 82 | 83 | export default ValidatorContextProvider; 84 | -------------------------------------------------------------------------------- /docs/image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xforceai/ide/5800128f95e9a27e2b8cf0dfa2fe28b09de9566f/docs/image.png -------------------------------------------------------------------------------- /hooks/useDnDFlow.tsx: -------------------------------------------------------------------------------- 1 | import { DATA_TRANSFER_KEY, LOCAL_HISTORY_KEY } from '@/commons/constants'; 2 | import { ContextMenuItemType } from '@/commons/types'; 3 | import { XForceNodesEnum, X_FORCE_NODES } from '@/components/LibraryPanel/nodes/nodeTypes'; 4 | import ContextMenuModal from '@/components/modals/ContextMenu/ContextMenuModal'; 5 | import ToastMessageModal from '@/components/modals/ToastMessageModal'; 6 | import { ModalContext } from '@/contexts/ModalContext'; 7 | import useDnDStore from '@/stores/useDnDStore'; 8 | import { extractNodeName } from '@/utils/nodeUtils'; 9 | import { includes } from 'lodash'; 10 | import React from 'react'; 11 | import { Connection, Edge, Node, ReactFlowProps } from 'reactflow'; 12 | 13 | type ReturnType = { 14 | onNodeDragOver: ReactFlowProps['onDragOver']; 15 | onNodeDropToWorkstation: ReactFlowProps['onDrop']; 16 | isValidConnection: ReactFlowProps['isValidConnection']; 17 | onNodeContextMenu: ReactFlowProps['onNodeContextMenu']; 18 | onEdgeContextMenu: ReactFlowProps['onEdgeContextMenu']; 19 | /**** customs ****/ 20 | onSaveGraph: () => void; 21 | }; 22 | 23 | function useDnDFlow(): ReturnType { 24 | const { rfInstance, addNode, deleteNode, deleteEdge } = useDnDStore(); 25 | const { setModal } = React.useContext(ModalContext); 26 | 27 | const onNodeDragOver: React.DragEventHandler = React.useCallback( 28 | (event: React.DragEvent) => { 29 | event.preventDefault(); 30 | event.dataTransfer.dropEffect = 'move'; 31 | }, 32 | [], 33 | ); 34 | const onNodeDropToWorkstation: React.DragEventHandler = (e) => { 35 | e.preventDefault(); 36 | const node = JSON.parse(e.dataTransfer.getData(DATA_TRANSFER_KEY)); 37 | 38 | if (typeof node === 'undefined' || !node) return; 39 | 40 | const pos = rfInstance?.screenToFlowPosition({ 41 | x: e.clientX, 42 | y: e.clientY, 43 | }); 44 | node['position'] = pos; 45 | addNode(node); 46 | }; 47 | 48 | const isValidConnection = (connection: Connection): boolean => { 49 | if (!connection.source || !connection.target) return false; 50 | const sourceKey = extractNodeName(connection.source); 51 | const targetKey = extractNodeName(connection.target); 52 | 53 | const targetNode = 54 | targetKey && targetKey in XForceNodesEnum ? X_FORCE_NODES[targetKey as keyof typeof XForceNodesEnum] : null; 55 | 56 | if (!targetNode) return false; 57 | return includes(targetNode.data.connectivity.input, sourceKey); 58 | }; 59 | 60 | const onNodeContextMenu = React.useCallback( 61 | (event: React.MouseEvent, node: Node) => { 62 | const CTX_MENU__NODE: ContextMenuItemType[] = [{ item: 'Delete Node', onClick: () => deleteNode(node) }]; 63 | setModal(); 64 | }, 65 | [deleteNode, setModal], 66 | ); 67 | 68 | const onEdgeContextMenu = React.useCallback( 69 | (event: React.MouseEvent, edge: Edge) => { 70 | const CTX_MENU__EDGE: ContextMenuItemType[] = [{ item: 'Delete Edge', onClick: () => deleteEdge(edge) }]; 71 | setModal(); 72 | }, 73 | [deleteEdge, setModal], 74 | ); 75 | 76 | /**** custom methods ****/ 77 | const onSaveGraph = (): boolean => { 78 | try { 79 | if (rfInstance) { 80 | const flow = rfInstance.toObject(); 81 | localStorage.setItem(LOCAL_HISTORY_KEY, JSON.stringify(flow)); 82 | return true; 83 | } 84 | } catch (err) { 85 | setModal(); 86 | return false; 87 | } 88 | return false; 89 | }; 90 | 91 | return { 92 | onNodeDragOver, 93 | onNodeDropToWorkstation, 94 | isValidConnection, 95 | onNodeContextMenu, 96 | onEdgeContextMenu, 97 | onSaveGraph, 98 | }; 99 | } 100 | 101 | export default useDnDFlow; 102 | -------------------------------------------------------------------------------- /hooks/useKeyboardListener.ts: -------------------------------------------------------------------------------- 1 | import { ModalContext } from '@/contexts/ModalContext'; 2 | import React, { ReactElement } from 'react'; 3 | 4 | type ArgsType = { 5 | onSave?: { 6 | modal: ReactElement; 7 | }; 8 | }; 9 | function useKeyboardListener({ onSave }: ArgsType) { 10 | const { setModal } = React.useContext(ModalContext); 11 | 12 | const onKeyDown = React.useCallback( 13 | (e: KeyboardEvent) => { 14 | if ((e.metaKey || e.ctrlKey) && e.key === 's') { 15 | e.preventDefault(); 16 | if (onSave) { 17 | setModal(onSave.modal || null); 18 | } 19 | } 20 | }, 21 | [onSave, setModal], 22 | ); 23 | 24 | React.useEffect(() => { 25 | document.addEventListener('keydown', onKeyDown); 26 | return () => { 27 | document.removeEventListener('keydown', onKeyDown); 28 | }; 29 | }, [onKeyDown]); 30 | } 31 | 32 | export default useKeyboardListener; 33 | -------------------------------------------------------------------------------- /hooks/useModalContext.ts: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | type UseModalContextReturnType = { 4 | modal: React.JSX.Element | null; 5 | setModal: React.Dispatch>; 6 | }; 7 | 8 | function useModalContext(): UseModalContextReturnType { 9 | const [modal, setModal] = React.useState(null); 10 | return { modal, setModal }; 11 | } 12 | 13 | export default useModalContext; 14 | -------------------------------------------------------------------------------- /hooks/useMountedState.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useEffect, useRef } from 'react'; 2 | 3 | export default function useMountedState(): () => boolean { 4 | const mountedRef = useRef(false); 5 | const get = useCallback(() => mountedRef.current, []); 6 | 7 | useEffect(() => { 8 | mountedRef.current = true; 9 | 10 | return () => { 11 | mountedRef.current = false; 12 | }; 13 | }, []); 14 | 15 | return get; 16 | } 17 | -------------------------------------------------------------------------------- /ide.code-workspace: -------------------------------------------------------------------------------- 1 | { 2 | "folders": [ 3 | { 4 | "name": "ide", 5 | "path": "." 6 | } 7 | ], 8 | "settings": { 9 | "editor.formatOnSave": true, 10 | "editor.defaultFormatter": "esbenp.prettier-vscode", 11 | "editor.codeActionsOnSave": { 12 | "source.fixAll.eslint": "explicit", 13 | "source.organizeImports": "always" 14 | }, 15 | "eslint.validate": ["javascript", "javascriptreact", "typescript", "typescriptreact"], 16 | "javascript.preferences.importModuleSpecifier": "non-relative", 17 | "javascript.preferences.importModuleSpecifierEnding": "minimal", 18 | "javascript.updateImportsOnFileMove.enabled": "always", 19 | "typescript.preferences.importModuleSpecifier": "non-relative", 20 | "typescript.preferences.importModuleSpecifierEnding": "minimal", 21 | "typescript.updateImportsOnFileMove.enabled": "always", 22 | "cSpell.words": ["esbenp", "nextjs", "OPENAI", "partialize", "reactflow", "zustand"] 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | reactStrictMode: true, 4 | images: { 5 | remotePatterns: [ 6 | { 7 | protocol: 'https', 8 | hostname: '**', 9 | }, 10 | ], 11 | }, 12 | }; 13 | 14 | module.exports = nextConfig; 15 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ide.x-force.ai", 3 | "version": "0.1.0", 4 | "private": true, 5 | "description": "a tool that builds the tools to build the tool.", 6 | "scripts": { 7 | "dev": "next dev", 8 | "build": "next build", 9 | "start": "next start", 10 | "lint": "next lint" 11 | }, 12 | "dependencies": { 13 | "@heroicons/react": "^2.0.18", 14 | "@svgr/webpack": "^8.1.0", 15 | "@vercel/analytics": "^1.1.2", 16 | "ace-builds": "^1.32.3", 17 | "html-to-image": "^1.11.11", 18 | "lodash": "^4.17.21", 19 | "next": "14.0.2", 20 | "prismjs": "^1.29.0", 21 | "react": "^18", 22 | "react-ace": "^10.1.0", 23 | "react-dnd": "^16.0.1", 24 | "react-dnd-html5-backend": "^16.0.1", 25 | "react-dom": "^18", 26 | "react-simple-code-editor": "^0.13.1", 27 | "reactflow": "^11.10.1", 28 | "uuid": "^9.0.1", 29 | "zod": "^3.22.4" 30 | }, 31 | "devDependencies": { 32 | "@types/lodash": "^4.14.202", 33 | "@types/node": "^20", 34 | "@types/prismjs": "^1.26.3", 35 | "@types/react": "^18", 36 | "@types/react-dom": "^18", 37 | "@types/uuid": "^9.0.7", 38 | "@typescript-eslint/eslint-plugin": "^6.18.1", 39 | "@typescript-eslint/parser": "^6.18.1", 40 | "autoprefixer": "^10.0.1", 41 | "eslint": "^8", 42 | "eslint-config-next": "14.0.2", 43 | "postcss": "^8", 44 | "tailwindcss": "^3.3.0", 45 | "typescript": "^5" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import type { AppProps } from 'next/app'; 2 | import { Manrope } from 'next/font/google'; 3 | 4 | import '@/styles/globals.css'; 5 | import 'reactflow/dist/style.css'; 6 | import '@/styles/reactflow.css'; 7 | 8 | const manrope = Manrope({ 9 | weight: ['400', '500', '600', '700'], 10 | style: ['normal'], 11 | subsets: ['latin'], 12 | }); 13 | 14 | export default function App({ Component, pageProps }: AppProps) { 15 | return ( 16 |
    17 | 18 |
    19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /pages/_document.tsx: -------------------------------------------------------------------------------- 1 | import { MODAL_ROOT_DIV_ID } from '@/commons/constants'; 2 | import { Html, Head, Main, NextScript } from 'next/document'; 3 | 4 | export default function Document() { 5 | return ( 6 | 7 | 8 | 9 |
    10 | 11 |
    12 | 13 | 14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /pages/index.tsx: -------------------------------------------------------------------------------- 1 | import AppX from '@/components/UI/App'; 2 | import MobilePanel from '@/components/UI/Mobile'; 3 | import ModalContextProvider from '@/contexts/ModalContext'; 4 | import ValidatorContextProvider from '@/contexts/ValidatorContext'; 5 | import { Analytics } from '@vercel/analytics/react'; 6 | import React from 'react'; 7 | import { ReactFlowProvider } from 'reactflow'; 8 | 9 | const IDE = () => { 10 | const disableDefaultContextMenu = (e: React.MouseEvent) => { 11 | e.preventDefault(); 12 | }; 13 | return ( 14 | <> 15 |
    16 |
    17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 |
    26 |
    27 |
    28 | 29 |
    30 | 31 | ); 32 | }; 33 | 34 | export default IDE; 35 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /public/demo-short.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xforceai/ide/5800128f95e9a27e2b8cf0dfa2fe28b09de9566f/public/demo-short.gif -------------------------------------------------------------------------------- /public/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xforceai/ide/5800128f95e9a27e2b8cf0dfa2fe28b09de9566f/public/demo.gif -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xforceai/ide/5800128f95e9a27e2b8cf0dfa2fe28b09de9566f/public/favicon.ico -------------------------------------------------------------------------------- /public/x-force-ide.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /public/x-force.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /public/x-forceIDE.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xforceai/ide/5800128f95e9a27e2b8cf0dfa2fe28b09de9566f/public/x-forceIDE.png -------------------------------------------------------------------------------- /stores/useAppStore.tsx: -------------------------------------------------------------------------------- 1 | import { XF_APP_STORE } from '@/commons/constants'; 2 | import { create } from 'zustand'; 3 | import { createJSONStorage, persist } from 'zustand/middleware'; 4 | 5 | type StoreType = { 6 | oaiKey: string; 7 | setOAIKey: (key: string) => void; 8 | }; 9 | 10 | const useAppStore = create()( 11 | persist( 12 | (set) => ({ 13 | oaiKey: '', 14 | setOAIKey: (key: string) => set({ oaiKey: key }), 15 | }), 16 | { 17 | name: XF_APP_STORE, 18 | storage: createJSONStorage(() => localStorage), 19 | partialize: (state) => { 20 | state.oaiKey = ''; 21 | return state; 22 | }, 23 | }, 24 | ), 25 | ); 26 | 27 | export default useAppStore; 28 | -------------------------------------------------------------------------------- /stores/useDnDStore.ts: -------------------------------------------------------------------------------- 1 | import { LOCAL_HISTORY_KEY } from '@/commons/constants'; 2 | import { 3 | Connection, 4 | Edge, 5 | EdgeChange, 6 | MarkerType, 7 | Node, 8 | NodeChange, 9 | OnConnect, 10 | OnEdgesChange, 11 | OnInit, 12 | OnNodesChange, 13 | ReactFlowInstance, 14 | addEdge, 15 | applyEdgeChanges, 16 | applyNodeChanges, 17 | } from 'reactflow'; 18 | import { create } from 'zustand'; 19 | import { createJSONStorage, persist } from 'zustand/middleware'; 20 | 21 | type RFState = { 22 | rfInstance: ReactFlowInstance | null; 23 | nodes: Node[]; 24 | edges: Edge[]; 25 | onNodesChange: OnNodesChange; 26 | onEdgesChange: OnEdgesChange; 27 | onConnect: OnConnect; 28 | onInit: OnInit; 29 | // custom methods 30 | addNode: (node: Node) => void; 31 | deleteNode: (node: Node) => void; 32 | addNodeData: (nodeId: string, data: { [k: string]: unknown }) => void; 33 | deleteEdge: (edge: Edge) => void; 34 | clearGraph: () => void; 35 | }; 36 | 37 | const useDnDStore = create()( 38 | persist( 39 | (set, get) => ({ 40 | rfInstance: null, 41 | nodes: [], 42 | edges: [], 43 | onNodesChange: (changes: NodeChange[]) => { 44 | // default reactflow prop, triggers on node drag, select, and move. 45 | set({ 46 | nodes: applyNodeChanges(changes, get().nodes), 47 | }); 48 | }, 49 | onEdgesChange: (changes: EdgeChange[]) => { 50 | // default reactflow prop, triggers on edge select or remove 51 | set({ 52 | edges: applyEdgeChanges(changes, get().edges), 53 | }); 54 | }, 55 | onConnect: (connection: Connection) => { 56 | // triggers whenever user connects two nodes. 57 | set({ 58 | edges: addEdge({ ...connection, markerEnd: { type: MarkerType.ArrowClosed } }, get().edges), 59 | }); 60 | }, 61 | onInit: (rf: ReactFlowInstance) => { 62 | set({ rfInstance: rf }); 63 | }, 64 | 65 | /**** custom methods ****/ 66 | addNode: (node: Node) => { 67 | // triggers whenever users drags and drops item from panel to workstation. 68 | set({ 69 | nodes: get().nodes.concat(node), 70 | }); 71 | }, 72 | deleteNode: (node: Node) => { 73 | // whenever user deletes a node from the workstation. 74 | set({ 75 | nodes: get().nodes.filter((n) => n.id !== node.id), 76 | edges: get().edges.filter((e) => e.source !== node.id), 77 | }); 78 | }, 79 | addNodeData: (nodeId: string, data: { [k: string]: unknown }) => { 80 | set({ 81 | nodes: get().nodes.map((n) => { 82 | if (n.id === nodeId) { 83 | return { 84 | ...n, 85 | data: { 86 | ...n.data, 87 | ...data, 88 | }, 89 | }; 90 | } 91 | return n; 92 | }), 93 | }); 94 | }, 95 | deleteEdge: (edge: Edge) => { 96 | // whenever user deletes an edge from the workstation. 97 | set({ 98 | edges: get().edges.filter((e) => e.id !== edge.id), 99 | }); 100 | }, 101 | clearGraph: () => { 102 | set({ 103 | nodes: [], 104 | edges: [], 105 | }); 106 | }, 107 | }), 108 | { 109 | name: LOCAL_HISTORY_KEY, 110 | storage: createJSONStorage(() => localStorage), 111 | }, 112 | ), 113 | ); 114 | 115 | export default useDnDStore; 116 | -------------------------------------------------------------------------------- /styles/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer base { 6 | input[type='number']::-webkit-inner-spin-button, 7 | input[type='number']::-webkit-outer-spin-button { 8 | -webkit-appearance: none; 9 | margin: 0; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /styles/reactflow.css: -------------------------------------------------------------------------------- 1 | .XForceIDE .react-flow__handle { 2 | background: #9ca3af; 3 | height: 2px; 4 | } 5 | .XForceIDE .react-flow__node-custominput { 6 | background: #fff; 7 | } 8 | 9 | .XForceIDE .react-flow__handle-connecting { 10 | background: #ff6060; 11 | } 12 | 13 | .XForceIDE .react-flow__handle-valid { 14 | background: #55dd99; 15 | } 16 | -------------------------------------------------------------------------------- /tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from 'tailwindcss'; 2 | 3 | const config: Config = { 4 | important: true, 5 | content: [ 6 | './pages/**/*.{js,ts,jsx,tsx,mdx}', 7 | './components/**/*.{js,ts,jsx,tsx,mdx}', 8 | './app/**/*.{js,ts,jsx,tsx,mdx}', 9 | ], 10 | theme: { 11 | extend: { 12 | backgroundImage: { 13 | 'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))', 14 | 'gradient-conic': 'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))', 15 | }, 16 | }, 17 | }, 18 | plugins: [], 19 | }; 20 | export default config; 21 | -------------------------------------------------------------------------------- /transpiler/helpers.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xforceai/ide/5800128f95e9a27e2b8cf0dfa2fe28b09de9566f/transpiler/helpers.ts -------------------------------------------------------------------------------- /transpiler/primitive.ts: -------------------------------------------------------------------------------- 1 | /* 2 | primitive implementation of a transpiler (:p), there are smarter ways to do this, but this was enough for the first version. 3 | 4 | Please create an issue / pr if you can improve this! 5 | 6 | a simplified hint from one of our internal modules; 7 | ```ts 8 | (graph): string => { 9 | const graphAST = graphParser.parse(graph) 10 | const pyAST = this.gen(graphAST) 11 | const pyCode = pyCodeGen.gen(pyAST) 12 | return pyCode; 13 | }(); 14 | ``` 15 | */ 16 | import { XForceNodesEnum } from '@/components/LibraryPanel/nodes/nodeTypes'; 17 | import { OAIModelsEnum } from '@/types/enum'; 18 | import { extractNodeName } from '@/utils/nodeUtils'; 19 | import { Edge as ReactFlowEdge, Node as ReactFlowNode } from 'reactflow'; 20 | 21 | const CODE_SKELETON = (c: string) => { 22 | return `import os 23 | import autogen 24 | from autogen.agentchat.contrib.gpt_assistant_agent import GPTAssistantAgent 25 | from autogen import UserProxyAgent 26 | 27 | from dotenv import load_dotenv 28 | 29 | load_dotenv() 30 | 31 | # ----------------- # 32 | 33 | ${c} 34 | `; 35 | }; 36 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 37 | export const NODE_TO_CODE_SCHEMA: { [k in XForceNodesEnum]: (params: any) => string } = { 38 | GROUP_CHAT: ({ 39 | variableName, 40 | maxRounds, 41 | agentSelection, 42 | agents, 43 | config, 44 | }: { 45 | variableName: string; 46 | maxRounds: number; 47 | agentSelection: string; 48 | agents: string[]; 49 | config: string; 50 | }) => 51 | `${variableName} = autogen.GroupChat(agents=[${agents}], messages=[], max_round=${ 52 | maxRounds || 15 53 | }, speaker_selection_method="${agentSelection || 'auto'}") 54 | ${variableName}_manager = autogen.GroupChatManager(groupchat=${variableName}, llm_config = {"config_list": [${config}]})`, 55 | USER_PROXY: ({ variableName }: { variableName: string }) => 56 | `${variableName} = UserProxyAgent(name="${variableName}", human_input_mode="ALWAYS", max_consecutive_auto_reply=1, code_execution_config={ 57 | "work_dir": "x-force-execution-dir", 58 | "use_docker": False, 59 | },)`, 60 | GPT_ASSISTANT_AGENT: ({ 61 | variableName, 62 | OAIId, 63 | funcMap, 64 | config, 65 | }: { 66 | variableName: string; 67 | OAIId: string; 68 | config: string; 69 | funcMap?: string; 70 | }) => `${variableName} = GPTAssistantAgent(name="${variableName}", llm_config = {"config_list": [${config}], "assistant_id":"${OAIId}"}) 71 | ${funcMap ? `${variableName}.register_function(function_map=${funcMap || 'None'})` : ''}`, 72 | CUSTOM_FUNCTION: ({ func }: { func: string }) => `${func || ''}`, 73 | ASSISTANT_AGENT: ({ 74 | variableName, 75 | systemPrompt, 76 | config, 77 | }: { 78 | variableName: string; 79 | systemPrompt: string; 80 | config: string; 81 | }) => `${variableName} = autogen.AssistantAgent( 82 | name="${variableName}", 83 | llm_config={"config_list": [${config}]}, 84 | ${systemPrompt ? `system_message="${systemPrompt}"` : ``} 85 | )`, 86 | }; 87 | export const CODE_BUILDER = (nodes: ReactFlowNode[], edges: ReactFlowEdge[], oaiKey: string) => { 88 | const codes: string[] = []; 89 | const m = new Map(); 90 | 91 | const getConfig = (el: ReactFlowNode) => { 92 | return { 93 | model: el?.data?.selectedModel || OAIModelsEnum.GPT_3_5_TURBO, 94 | api_key: oaiKey || '', 95 | }; 96 | }; 97 | 98 | // nodes 99 | const customFuncs = nodes.filter((node) => node.type === XForceNodesEnum.CUSTOM_FUNCTION); 100 | const gptAssistants = nodes.filter((node) => node.type === XForceNodesEnum.GPT_ASSISTANT_AGENT); 101 | const assistantAgents = nodes.filter((node) => node.type === XForceNodesEnum.ASSISTANT_AGENT); 102 | const userProxies = nodes.filter((node) => node.type === XForceNodesEnum.USER_PROXY); 103 | const groupChats = nodes.filter((node) => node.type === XForceNodesEnum.GROUP_CHAT); 104 | 105 | customFuncs.forEach((el) => { 106 | const regex = /def\s+([a-zA-Z_]\w*)\s*\(/g; 107 | let match; 108 | const functionNames = new Set(); 109 | while ((match = regex.exec(el.data?.func)) !== null) { 110 | functionNames.add(match[1]); 111 | } 112 | 113 | m.set(el.id, Array.from(functionNames)); 114 | const key = el.type as keyof typeof XForceNodesEnum; 115 | const codeblock = NODE_TO_CODE_SCHEMA[key]?.(el.data || undefined); 116 | codes.push(codeblock); 117 | }); 118 | 119 | gptAssistants.forEach((el) => { 120 | m.set(el.id, el?.data?.variableName); 121 | const key = el.type as keyof typeof XForceNodesEnum; 122 | const connectedFuncNames = edges 123 | .filter( 124 | (e) => 125 | extractNodeName(e.source || '') === XForceNodesEnum.CUSTOM_FUNCTION && 126 | extractNodeName(e.target || '') === XForceNodesEnum.GPT_ASSISTANT_AGENT, 127 | ) 128 | .map((e) => { 129 | const funcNames = m.get(e.source); 130 | return funcNames.map((f: string) => `"${f}": ${f}`); 131 | }); 132 | const functionMap = `{${connectedFuncNames}}`; 133 | const codeblock = NODE_TO_CODE_SCHEMA[key]?.( 134 | { ...el.data, funcMap: functionMap, config: JSON.stringify(getConfig(el)) } || undefined, 135 | ); 136 | codes.push(codeblock); 137 | }); 138 | 139 | assistantAgents.forEach((el) => { 140 | m.set(el.id, el?.data?.variableName); 141 | const key = el.type as keyof typeof XForceNodesEnum; 142 | const codeblock = NODE_TO_CODE_SCHEMA[key]?.({ ...el.data, config: JSON.stringify(getConfig(el)) } || undefined); 143 | codes.push(codeblock); 144 | }); 145 | 146 | userProxies.forEach((el) => { 147 | m.set(el.id, el?.data?.variableName); 148 | const key = el.type as keyof typeof XForceNodesEnum; 149 | const codeblock = NODE_TO_CODE_SCHEMA[key]?.(el.data || undefined); 150 | codes.push(codeblock); 151 | }); 152 | 153 | groupChats.forEach((el) => { 154 | m.set(el.id, el?.data?.variableName); 155 | 156 | const agents = edges 157 | .filter((e) => extractNodeName(e.target || '') === XForceNodesEnum.GROUP_CHAT) 158 | .map((e) => m.get(e.source)); 159 | const key = el.type as keyof typeof XForceNodesEnum; 160 | const codeblock = NODE_TO_CODE_SCHEMA[key]?.( 161 | { ...el.data, agents, config: JSON.stringify(getConfig(el)) } || undefined, 162 | ); 163 | codes.push(codeblock); 164 | }); 165 | 166 | userProxies.forEach((el) => { 167 | const connectedGCs = edges.filter( 168 | (e) => 169 | extractNodeName(e.source || '') === XForceNodesEnum.USER_PROXY && 170 | extractNodeName(e.target || '') === XForceNodesEnum.GROUP_CHAT, 171 | ); 172 | connectedGCs.forEach((gc) => { 173 | const gcVarName = m.get(gc?.target); 174 | const codeblock = `${el.data?.variableName}.initiate_chat(${gcVarName}_manager, message="${el?.data?.initialPrompt}")`; 175 | codes.push(codeblock); 176 | }); 177 | }); 178 | 179 | // build code 180 | return CODE_SKELETON(codes.join('\n')); 181 | }; 182 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "strict": true, 12 | "noEmit": true, 13 | "esModuleInterop": true, 14 | "module": "esnext", 15 | "moduleResolution": "bundler", 16 | "resolveJsonModule": true, 17 | "isolatedModules": true, 18 | "jsx": "preserve", 19 | "incremental": true, 20 | "paths": { 21 | "@/*": [ 22 | "./*" 23 | ] 24 | }, 25 | "typeRoots": [ 26 | "./types", 27 | "./node_modules/@types" 28 | ], 29 | "plugins": [ 30 | { 31 | "name": "next" 32 | } 33 | ] 34 | }, 35 | "include": [ 36 | "next-env.d.ts", 37 | "**/*.ts", 38 | "**/*.tsx", 39 | ".next/types/**/*.ts" 40 | ], 41 | "exclude": [ 42 | "node_modules" 43 | ] 44 | } 45 | -------------------------------------------------------------------------------- /types/enum.ts: -------------------------------------------------------------------------------- 1 | export enum OAIModelsEnum { 2 | GPT_3_5_TURBO = 'gpt-3.5-turbo', 3 | GPT_3_5_TURBO_16K = 'gpt-3.5-turbo-16k', 4 | GPT_4 = 'gpt-4', 5 | GPT_4_32K = 'gpt-4-32k', 6 | } 7 | 8 | export enum AgentSelectionStrategyEnum { 9 | AUTO = 'auto', 10 | MANUAL = 'manual', 11 | RANDOM = 'random', 12 | RR = 'round_robin', 13 | } 14 | -------------------------------------------------------------------------------- /utils/nodeUtils.ts: -------------------------------------------------------------------------------- 1 | export const NODE_NAME_REGEX = /^(.*?)__[^_]+$/; 2 | export const extractNodeName = (name: string) => { 3 | const match = name.match(NODE_NAME_REGEX); 4 | return match ? match[1] : null; 5 | }; 6 | --------------------------------------------------------------------------------