diff --git a/i18n/translate_util.py b/i18n/translate_util.py index f02e543b8..1611e45f7 100644 --- a/i18n/translate_util.py +++ b/i18n/translate_util.py @@ -1,4 +1,5 @@ """Translate the po file content to Chinese using LLM.""" + from typing import List, Dict, Any import asyncio import os @@ -147,6 +148,8 @@ "RAG": "RAG", "DB-GPT": "DB-GPT", "AWEL flow": "AWEL 工作流", + "Agent": "智能体", + "Agents": "智能体", }, "default": { "Transformer": "Transformer", @@ -159,6 +162,8 @@ "RAG": "RAG", "DB-GPT": "DB-GPT", "AWEL flow": "AWEL flow", + "Agent": "Agent", + "Agents": "Agents", }, } diff --git a/web/client/api/flow/index.ts b/web/client/api/flow/index.ts index 7f32ac1d9..2d5d2d1dd 100644 --- a/web/client/api/flow/index.ts +++ b/web/client/api/flow/index.ts @@ -6,6 +6,10 @@ import { IFlowRefreshParams, IFlowResponse, IFlowUpdateParam, + IGetKeysRequestParams, + IGetKeysResponseData, + IGetVariablesByKeyRequestParams, + IGetVariablesByKeyResponseData, IUploadFileRequestParams, IUploadFileResponse, } from '@/types/flow'; @@ -35,8 +39,8 @@ export const deleteFlowById = (id: string) => { return DELETE(`/api/v2/serve/awel/flows/${id}`); }; -export const getFlowNodes = () => { - return GET>(`/api/v2/serve/awel/nodes`); +export const getFlowNodes = (tags?: string) => { + return GET<{ tags?: string }, Array>(`/api/v2/serve/awel/nodes`, { tags }); }; export const refreshFlowNodeById = (data: IFlowRefreshParams) => { @@ -63,11 +67,22 @@ export const downloadFile = (fileId: string) => { return GET(`/api/v2/serve/file/files/dbgpt/${fileId}`); }; -// TODO:wait for interface update -export const getFlowTemplateList = () => { - return GET>('/api/v2/serve/awel/flow/templates'); -}; - export const getFlowTemplateById = (id: string) => { return GET(`/api/v2/serve/awel/flow/templates/${id}`); }; + +export const getFlowTemplates = () => { + return GET(`/api/v2/serve/awel/flow/templates`); +}; + +export const getKeys = (data?: IGetKeysRequestParams) => { + return GET>('/api/v2/serve/awel/variables/keys', data); +}; + +export const getVariablesByKey = (data: IGetVariablesByKeyRequestParams) => { + return GET('/api/v2/serve/awel/variables', data); +}; + +export const metadataBatch = (data: IUploadFileRequestParams) => { + return POST>('/api/v2/serve/file/files/metadata/batch', data); +}; diff --git a/web/components/flow/add-nodes-sider.tsx b/web/components/flow/add-nodes-sider.tsx index b5a5f6d3c..2db8f45c5 100644 --- a/web/components/flow/add-nodes-sider.tsx +++ b/web/components/flow/add-nodes-sider.tsx @@ -4,7 +4,8 @@ import { IFlowNode } from '@/types/flow'; import { FLOW_NODES_KEY } from '@/utils'; import { CaretLeftOutlined, CaretRightOutlined } from '@ant-design/icons'; import type { CollapseProps } from 'antd'; -import { Badge, Collapse, Input, Layout, Space } from 'antd'; +import { Badge, Collapse, Input, Layout, Space, Switch } from 'antd'; +import classnames from 'classnames'; import React, { useContext, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import StaticNodes from './static-nodes'; @@ -12,6 +13,8 @@ import StaticNodes from './static-nodes'; const { Search } = Input; const { Sider } = Layout; +const TAGS = JSON.stringify({ order: 'higher-order' }); + type GroupType = { category: string; categoryLabel: string; @@ -41,13 +44,16 @@ const AddNodesSider: React.FC = () => { const [resources, setResources] = useState>([]); const [operatorsGroup, setOperatorsGroup] = useState([]); const [resourcesGroup, setResourcesGroup] = useState([]); + const [isAllNodesVisible, setIsAllNodesVisible] = useState(false); useEffect(() => { - getNodes(); + getNodes(TAGS); }, []); - async function getNodes() { - const [_, data] = await apiInterceptors(getFlowNodes()); + // tags is optional, if tags is not passed, it will get all nodes + async function getNodes(tags?: string) { + const [_, data] = await apiInterceptors(getFlowNodes(tags)); + if (data && data.length > 0) { localStorage.setItem(FLOW_NODES_KEY, JSON.stringify(data)); const operatorNodes = data.filter(node => node.flow_type === 'operator'); @@ -166,6 +172,16 @@ const AddNodesSider: React.FC = () => { setSearchValue(val); } + function onModeChange() { + if (isAllNodesVisible) { + getNodes(TAGS); + } else { + getNodes(); + } + + setIsAllNodesVisible(!isAllNodesVisible); + } + return ( { onCollapse={collapsed => setCollapsed(collapsed)} > -

- {t('add_node')} -

+
+

+ {t('add_node')} +

+ + +
diff --git a/web/components/flow/canvas-modal/add-flow-variable-modal.tsx b/web/components/flow/canvas-modal/add-flow-variable-modal.tsx new file mode 100644 index 000000000..0c76ff19b --- /dev/null +++ b/web/components/flow/canvas-modal/add-flow-variable-modal.tsx @@ -0,0 +1,287 @@ +import { apiInterceptors, getKeys, getVariablesByKey } from '@/client/api'; +import { IFlowUpdateParam, IGetKeysResponseData, IVariableItem } from '@/types/flow'; +import { buildVariableString } from '@/utils/flow'; +import { MinusCircleOutlined, PlusOutlined } from '@ant-design/icons'; +import { Button, Cascader, Form, Input, InputNumber, Modal, Select, Space } from 'antd'; +import { DefaultOptionType } from 'antd/es/cascader'; +import { uniqBy } from 'lodash'; +import React, { useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; + +const { Option } = Select; +const VALUE_TYPES = ['str', 'int', 'float', 'bool', 'ref'] as const; + +type ValueType = (typeof VALUE_TYPES)[number]; +type Props = { + flowInfo?: IFlowUpdateParam; + setFlowInfo: React.Dispatch>; +}; + +export const AddFlowVariableModal: React.FC = ({ flowInfo, setFlowInfo }) => { + const { t } = useTranslation(); + const [isModalOpen, setIsModalOpen] = useState(false); + const [form] = Form.useForm(); + const [controlTypes, setControlTypes] = useState(['str']); + const [refVariableOptions, setRefVariableOptions] = useState([]); + + useEffect(() => { + getKeysData(); + }, []); + + const getKeysData = async () => { + const [err, res] = await apiInterceptors(getKeys()); + + if (err) return; + + const keyOptions = res?.map(({ key, label, scope }: IGetKeysResponseData) => ({ + value: key, + label, + scope, + isLeaf: false, + })); + + setRefVariableOptions(keyOptions); + }; + + const onFinish = (values: any) => { + const newFlowInfo = { ...flowInfo, variables: values?.parameters || [] } as IFlowUpdateParam; + setFlowInfo(newFlowInfo); + setIsModalOpen(false); + }; + + const onNameChange = (e: React.ChangeEvent, index: number) => { + const name = e.target.value; + + const newValue = name + ?.split('_') + ?.map(word => word.charAt(0).toUpperCase() + word.slice(1)) + ?.join(' '); + + form.setFields([ + { + name: ['parameters', index, 'label'], + value: newValue, + }, + ]); + }; + + const onValueTypeChange = (type: ValueType, index: number) => { + const newControlTypes = [...controlTypes]; + newControlTypes[index] = type; + setControlTypes(newControlTypes); + }; + + const loadData = (selectedOptions: DefaultOptionType[]) => { + const targetOption = selectedOptions[selectedOptions.length - 1]; + const { value, scope } = targetOption as DefaultOptionType & { scope: string }; + + setTimeout(async () => { + const [err, res] = await apiInterceptors(getVariablesByKey({ key: value as string, scope })); + + if (err) return; + if (res?.total_count === 0) { + targetOption.isLeaf = true; + return; + } + + const uniqueItems = uniqBy(res?.items, 'name'); + targetOption.children = uniqueItems?.map(item => ({ + value: item?.name, + label: item.label, + item: item, + })); + setRefVariableOptions([...refVariableOptions]); + }, 1000); + }; + + const onRefTypeValueChange = ( + value: (string | number | null)[], + selectedOptions: DefaultOptionType[], + index: number, + ) => { + // when select ref variable, must be select two options(key and variable) + if (value?.length !== 2) return; + + const [selectRefKey, selectedRefVariable] = selectedOptions as DefaultOptionType[]; + const selectedVariable = selectRefKey?.children?.find( + ({ value }) => value === selectedRefVariable?.value, + ) as DefaultOptionType & { item: IVariableItem }; + + // build variable string by rule + const variableStr = buildVariableString(selectedVariable?.item); + const parameters = form.getFieldValue('parameters'); + const param = parameters?.[index]; + if (param) { + param.value = variableStr; + param.category = selectedVariable?.item?.category; + param.value_type = selectedVariable?.item?.value_type; + + form.setFieldsValue({ + parameters: [...parameters], + }); + } + }; + + // Helper function to render the appropriate control component + const renderVariableValue = (type: string, index: number) => { + switch (type) { + case 'ref': + return ( + onRefTypeValueChange(value, selectedOptions, index)} + changeOnSelect + /> + ); + case 'str': + return ; + case 'int': + return ( + value?.replace(/[^\-?\d]/g, '') || 0} + style={{ width: '100%' }} + /> + ); + case 'float': + return ; + case 'bool': + return ( + + ); + default: + return ; + } + }; + + return ( + <> + , + , + ]} + > +
+ + {(fields, { add, remove }) => ( + <> + {fields.map(({ key, name, ...restField }, index) => ( + + + onNameChange(e, index)} /> + + + + + + + + + + + + {renderVariableValue(controlTypes[index], index)} + + + + + + + remove(name)} /> + + + ))} + + + + + + )} + +
+ + + ); +}; diff --git a/web/components/flow/canvas-modal/export-flow-modal.tsx b/web/components/flow/canvas-modal/export-flow-modal.tsx index 31850dc56..21f60efbe 100644 --- a/web/components/flow/canvas-modal/export-flow-modal.tsx +++ b/web/components/flow/canvas-modal/export-flow-modal.tsx @@ -1,5 +1,5 @@ import { IFlowData, IFlowUpdateParam } from '@/types/flow'; -import { Button, Form, Input, Modal, Radio, Space, message } from 'antd'; +import { Button, Form, Input, Modal, Radio, message } from 'antd'; import { useTranslation } from 'react-i18next'; import { ReactFlowInstance } from 'reactflow'; @@ -43,12 +43,17 @@ export const ExportFlowModal: React.FC = ({ return ( <> setIsExportFlowModalOpen(false)} - cancelButtonProps={{ className: 'hidden' }} - okButtonProps={{ className: 'hidden' }} + footer={[ + , + , + ]} >
= ({ - - - - - - -
diff --git a/web/components/flow/canvas-modal/flow-template-modal.tsx b/web/components/flow/canvas-modal/flow-template-modal.tsx new file mode 100644 index 000000000..dc940a266 --- /dev/null +++ b/web/components/flow/canvas-modal/flow-template-modal.tsx @@ -0,0 +1,89 @@ +import { getFlowTemplates } from '@/client/api'; +import CanvasWrapper from '@/pages/construct/flow/canvas/index'; +import type { TableProps } from 'antd'; +import { Button, Modal, Space, Table } from 'antd'; +import { useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; + +type Props = { + isFlowTemplateModalOpen: boolean; + setIsFlowTemplateModalOpen: (value: boolean) => void; +}; + +interface DataType { + key: string; + name: string; + age: number; + address: string; + tags: string[]; +} + +export const FlowTemplateModal: React.FC = ({ isFlowTemplateModalOpen, setIsFlowTemplateModalOpen }) => { + const { t } = useTranslation(); + const [dataSource, setDataSource] = useState([]); + + const onTemplateImport = (record: DataType) => { + if (record?.name) { + localStorage.setItem('importFlowData', JSON.stringify(record)); + CanvasWrapper(); + setIsFlowTemplateModalOpen(false); + } + }; + + const columns: TableProps['columns'] = [ + { + title: t('Template_Name'), + dataIndex: 'name', + key: 'name', + }, + { + title: t('Template_Label'), + dataIndex: 'label', + key: 'label', + }, + { + title: t('Template_Description'), + dataIndex: 'description', + key: 'description', + }, + { + title: t('Template_Action'), + key: 'action', + render: (_, record) => ( + + + + ), + }, + ]; + + useEffect(() => { + getFlowTemplates().then(res => { + console.log(res); + setDataSource(res?.data?.data?.items); + }); + }, []); + + return ( + <> + setIsFlowTemplateModalOpen(false)} + cancelButtonProps={{ className: 'hidden' }} + okButtonProps={{ className: 'hidden' }} + > + ; + + + ); +}; diff --git a/web/components/flow/canvas-modal/import-flow-modal.tsx b/web/components/flow/canvas-modal/import-flow-modal.tsx index fbf7e87df..3711b78ed 100644 --- a/web/components/flow/canvas-modal/import-flow-modal.tsx +++ b/web/components/flow/canvas-modal/import-flow-modal.tsx @@ -1,6 +1,7 @@ import { apiInterceptors, importFlow } from '@/client/api'; +import CanvasWrapper from '@/pages/construct/flow/canvas/index'; import { UploadOutlined } from '@ant-design/icons'; -import { Button, Form, GetProp, Modal, Radio, Space, Upload, UploadFile, UploadProps, message } from 'antd'; +import { Button, Form, GetProp, Modal, Radio, Upload, UploadFile, UploadProps, message } from 'antd'; import { useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { Edge, Node } from 'reactflow'; @@ -37,7 +38,9 @@ export const ImportFlowModal: React.FC = ({ isImportModalOpen, setIsImpor const [, , res] = await apiInterceptors(importFlow(formData)); if (res?.success) { - messageApi.success(t('Export_Flow_Success')); + messageApi.success(t('Import_Flow_Success')); + localStorage.setItem('importFlowData', JSON.stringify(res?.data)); + CanvasWrapper(); } else if (res?.err_msg) { messageApi.error(res?.err_msg); } @@ -61,12 +64,17 @@ export const ImportFlowModal: React.FC = ({ isImportModalOpen, setIsImpor return ( <> setIsImportFlowModalOpen(false)} - cancelButtonProps={{ className: 'hidden' }} - okButtonProps={{ className: 'hidden' }} + footer={[ + , + , + ]} >
= ({ isImportModalOpen, setIsImpor - + - - - - - - -
diff --git a/web/components/flow/canvas-modal/index.ts b/web/components/flow/canvas-modal/index.ts index 76abf29ea..65d3455c6 100644 --- a/web/components/flow/canvas-modal/index.ts +++ b/web/components/flow/canvas-modal/index.ts @@ -1,3 +1,5 @@ +export * from './add-flow-variable-modal'; export * from './export-flow-modal'; +export * from './flow-template-modal'; export * from './import-flow-modal'; export * from './save-flow-modal'; diff --git a/web/components/flow/canvas-modal/save-flow-modal.tsx b/web/components/flow/canvas-modal/save-flow-modal.tsx index ba37afcf5..0434af90f 100644 --- a/web/components/flow/canvas-modal/save-flow-modal.tsx +++ b/web/components/flow/canvas-modal/save-flow-modal.tsx @@ -1,9 +1,9 @@ import { addFlow, apiInterceptors, updateFlowById } from '@/client/api'; import { IFlowData, IFlowUpdateParam } from '@/types/flow'; import { mapHumpToUnderline } from '@/utils/flow'; -import { Button, Checkbox, Form, Input, Modal, Space, message } from 'antd'; -import { useSearchParams } from 'next/navigation'; -import { useState } from 'react'; +import { Button, Checkbox, Form, Input, Modal, message } from 'antd'; +import { useRouter } from 'next/router'; +import { useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { ReactFlowInstance } from 'reactflow'; @@ -22,13 +22,18 @@ export const SaveFlowModal: React.FC = ({ flowInfo, setIsSaveFlowModalOpen, }) => { - const [deploy, setDeploy] = useState(true); const { t } = useTranslation(); - const searchParams = useSearchParams(); - const id = searchParams?.get('id') || ''; + const router = useRouter(); const [form] = Form.useForm(); const [messageApi, contextHolder] = message.useMessage(); + const [deploy, setDeploy] = useState(false); + const [id, setId] = useState(router.query.id || ''); + + useEffect(() => { + setId(router.query.id || ''); + }, [router.query.id]); + function onLabelChange(e: React.ChangeEvent) { const label = e.target.value; // replace spaces with underscores, convert uppercase letters to lowercase, remove characters other than digits, letters, _, and -. @@ -45,14 +50,15 @@ export const SaveFlowModal: React.FC = ({ if (id) { const [, , res] = await apiInterceptors( - updateFlowById(id, { + updateFlowById(id.toString(), { name, label, description, editable, - uid: id, + uid: id.toString(), flow_data: reactFlowObject, state, + variables: flowInfo?.variables, }), ); @@ -70,12 +76,13 @@ export const SaveFlowModal: React.FC = ({ editable, flow_data: reactFlowObject, state, + variables: flowInfo?.variables, }), ); + if (res?.uid) { messageApi.success(t('save_flow_success')); - const history = window.history; - history.pushState(null, '', `/flow/canvas?id=${res.uid}`); + router.push(`/construct/flow/canvas?id=${res.uid}`, undefined, { shallow: true }); } } setIsSaveFlowModalOpen(false); @@ -84,14 +91,17 @@ export const SaveFlowModal: React.FC = ({ return ( <> { - setIsSaveFlowModalOpen(false); - }} - cancelButtonProps={{ className: 'hidden' }} - okButtonProps={{ className: 'hidden' }} + onCancel={() => setIsSaveFlowModalOpen(false)} + footer={[ + , + , + ]} >
= ({