mirror of
https://github.com/csunny/DB-GPT.git
synced 2025-07-30 23:28:35 +00:00
feat(web): Add DAG variables to web flow (#1981)
Co-authored-by: Fangyin Cheng <staneyffer@gmail.com> Co-authored-by: 谨欣 <echo.cmy@antgroup.com> Co-authored-by: yanzhiyong <932374019@qq.com> Co-authored-by: 严志勇 <yanzhiyong@tiansuixiansheng.com>
This commit is contained in:
parent
fe29f977f3
commit
746e4fda37
@ -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 @@ vocabulary_map = {
|
||||
"RAG": "RAG",
|
||||
"DB-GPT": "DB-GPT",
|
||||
"AWEL flow": "AWEL 工作流",
|
||||
"Agent": "智能体",
|
||||
"Agents": "智能体",
|
||||
},
|
||||
"default": {
|
||||
"Transformer": "Transformer",
|
||||
@ -159,6 +162,8 @@ vocabulary_map = {
|
||||
"RAG": "RAG",
|
||||
"DB-GPT": "DB-GPT",
|
||||
"AWEL flow": "AWEL flow",
|
||||
"Agent": "Agent",
|
||||
"Agents": "Agents",
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -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<null, null>(`/api/v2/serve/awel/flows/${id}`);
|
||||
};
|
||||
|
||||
export const getFlowNodes = () => {
|
||||
return GET<null, Array<IFlowNode>>(`/api/v2/serve/awel/nodes`);
|
||||
export const getFlowNodes = (tags?: string) => {
|
||||
return GET<{ tags?: string }, Array<IFlowNode>>(`/api/v2/serve/awel/nodes`, { tags });
|
||||
};
|
||||
|
||||
export const refreshFlowNodeById = (data: IFlowRefreshParams) => {
|
||||
@ -63,11 +67,22 @@ export const downloadFile = (fileId: string) => {
|
||||
return GET<null, any>(`/api/v2/serve/file/files/dbgpt/${fileId}`);
|
||||
};
|
||||
|
||||
// TODO:wait for interface update
|
||||
export const getFlowTemplateList = () => {
|
||||
return GET<null, Array<any>>('/api/v2/serve/awel/flow/templates');
|
||||
};
|
||||
|
||||
export const getFlowTemplateById = (id: string) => {
|
||||
return GET<null, any>(`/api/v2/serve/awel/flow/templates/${id}`);
|
||||
};
|
||||
|
||||
export const getFlowTemplates = () => {
|
||||
return GET<null, any>(`/api/v2/serve/awel/flow/templates`);
|
||||
};
|
||||
|
||||
export const getKeys = (data?: IGetKeysRequestParams) => {
|
||||
return GET<IGetKeysRequestParams, Array<IGetKeysResponseData>>('/api/v2/serve/awel/variables/keys', data);
|
||||
};
|
||||
|
||||
export const getVariablesByKey = (data: IGetVariablesByKeyRequestParams) => {
|
||||
return GET<IGetVariablesByKeyRequestParams, IGetVariablesByKeyResponseData>('/api/v2/serve/awel/variables', data);
|
||||
};
|
||||
|
||||
export const metadataBatch = (data: IUploadFileRequestParams) => {
|
||||
return POST<IUploadFileRequestParams, Array<IUploadFileResponse>>('/api/v2/serve/file/files/metadata/batch', data);
|
||||
};
|
||||
|
@ -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<Array<IFlowNode>>([]);
|
||||
const [operatorsGroup, setOperatorsGroup] = useState<GroupType[]>([]);
|
||||
const [resourcesGroup, setResourcesGroup] = useState<GroupType[]>([]);
|
||||
const [isAllNodesVisible, setIsAllNodesVisible] = useState<boolean>(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 (
|
||||
<Sider
|
||||
className='flex justify-center items-start nodrag bg-[#ffffff80] border-r border-[#d5e5f6] dark:bg-[#ffffff29] dark:border-[#ffffff66]'
|
||||
@ -179,9 +195,19 @@ const AddNodesSider: React.FC = () => {
|
||||
onCollapse={collapsed => setCollapsed(collapsed)}
|
||||
>
|
||||
<Space direction='vertical' className='w-[280px] pt-4 px-4 overflow-hidden overflow-y-auto scrollbar-default'>
|
||||
<p className='w-full text-base font-semibold text-[#1c2533] dark:text-[rgba(255,255,255,0.85)] line-clamp-1'>
|
||||
{t('add_node')}
|
||||
</p>
|
||||
<div className='flex justify-between align-middle'>
|
||||
<p className='w-full text-base font-semibold text-[#1c2533] dark:text-[rgba(255,255,255,0.85)] line-clamp-1'>
|
||||
{t('add_node')}
|
||||
</p>
|
||||
|
||||
<Switch
|
||||
checkedChildren='高阶'
|
||||
unCheckedChildren='全部'
|
||||
onClick={onModeChange}
|
||||
className={classnames('w-20', { 'bg-zinc-400': isAllNodesVisible })}
|
||||
defaultChecked
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Search placeholder='Search node' onSearch={searchNode} allowClear />
|
||||
|
||||
|
287
web/components/flow/canvas-modal/add-flow-variable-modal.tsx
Normal file
287
web/components/flow/canvas-modal/add-flow-variable-modal.tsx
Normal file
@ -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<React.SetStateAction<IFlowUpdateParam | undefined>>;
|
||||
};
|
||||
|
||||
export const AddFlowVariableModal: React.FC<Props> = ({ flowInfo, setFlowInfo }) => {
|
||||
const { t } = useTranslation();
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const [form] = Form.useForm();
|
||||
const [controlTypes, setControlTypes] = useState<ValueType[]>(['str']);
|
||||
const [refVariableOptions, setRefVariableOptions] = useState<DefaultOptionType[]>([]);
|
||||
|
||||
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<HTMLInputElement>, 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 (
|
||||
<Cascader
|
||||
placeholder='Select Value'
|
||||
options={refVariableOptions}
|
||||
loadData={loadData}
|
||||
onChange={(value, selectedOptions) => onRefTypeValueChange(value, selectedOptions, index)}
|
||||
changeOnSelect
|
||||
/>
|
||||
);
|
||||
case 'str':
|
||||
return <Input placeholder='Parameter Value' />;
|
||||
case 'int':
|
||||
return (
|
||||
<InputNumber
|
||||
step={1}
|
||||
placeholder='Parameter Value'
|
||||
parser={value => value?.replace(/[^\-?\d]/g, '') || 0}
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
);
|
||||
case 'float':
|
||||
return <InputNumber placeholder='Parameter Value' style={{ width: '100%' }} />;
|
||||
case 'bool':
|
||||
return (
|
||||
<Select placeholder='Select Value'>
|
||||
<Option value={true}>True</Option>
|
||||
<Option value={false}>False</Option>
|
||||
</Select>
|
||||
);
|
||||
default:
|
||||
return <Input placeholder='Parameter Value' />;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
type='primary'
|
||||
className='flex items-center justify-center rounded-full left-4 top-4'
|
||||
style={{ zIndex: 1050 }}
|
||||
icon={<PlusOutlined />}
|
||||
onClick={() => setIsModalOpen(true)}
|
||||
/>
|
||||
|
||||
<Modal
|
||||
title={t('Add_Global_Variable_of_Flow')}
|
||||
width={1000}
|
||||
open={isModalOpen}
|
||||
styles={{
|
||||
body: {
|
||||
minHeight: '40vh',
|
||||
maxHeight: '65vh',
|
||||
overflow: 'scroll',
|
||||
backgroundColor: 'rgba(0,0,0,0.02)',
|
||||
padding: '0 8px',
|
||||
borderRadius: 4,
|
||||
},
|
||||
}}
|
||||
onCancel={() => setIsModalOpen(false)}
|
||||
footer={[
|
||||
<Button key='cancel' onClick={() => setIsModalOpen(false)}>
|
||||
{t('cancel')}
|
||||
</Button>,
|
||||
<Button key='submit' type='primary' onClick={() => form.submit()}>
|
||||
{t('verify')}
|
||||
</Button>,
|
||||
]}
|
||||
>
|
||||
<Form
|
||||
name='dynamic_form_nest_item'
|
||||
onFinish={onFinish}
|
||||
form={form}
|
||||
autoComplete='off'
|
||||
layout='vertical'
|
||||
className='mt-8'
|
||||
initialValues={{ parameters: flowInfo?.variables || [{}] }}
|
||||
>
|
||||
<Form.List name='parameters'>
|
||||
{(fields, { add, remove }) => (
|
||||
<>
|
||||
{fields.map(({ key, name, ...restField }, index) => (
|
||||
<Space key={key} className='hover:bg-gray-100 pt-2 pl-2'>
|
||||
<Form.Item
|
||||
{...restField}
|
||||
name={[name, 'name']}
|
||||
label={`参数 ${index + 1} 名称`}
|
||||
style={{ width: 140 }}
|
||||
rules={[
|
||||
{ required: true, message: 'Missing parameter name' },
|
||||
{
|
||||
pattern: /^[a-zA-Z0-9]+(_[a-zA-Z0-9]+)*$/,
|
||||
message: '名称必须是字母、数字或下划线,并使用下划线分隔多个单词',
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input placeholder='Parameter Name' onChange={e => onNameChange(e, index)} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
{...restField}
|
||||
name={[name, 'label']}
|
||||
label='标题'
|
||||
style={{ width: 130 }}
|
||||
rules={[{ required: true, message: 'Missing parameter label' }]}
|
||||
>
|
||||
<Input placeholder='Parameter Label' />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
{...restField}
|
||||
name={[name, 'value_type']}
|
||||
label='类型'
|
||||
style={{ width: 100 }}
|
||||
rules={[{ required: true, message: 'Missing parameter type' }]}
|
||||
>
|
||||
<Select placeholder='Select' onChange={value => onValueTypeChange(value, index)}>
|
||||
{VALUE_TYPES.map(type => (
|
||||
<Option key={type} value={type}>
|
||||
{type}
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
{...restField}
|
||||
name={[name, 'value']}
|
||||
label='值'
|
||||
style={{ width: 320 }}
|
||||
rules={[{ required: true, message: 'Missing parameter value' }]}
|
||||
>
|
||||
{renderVariableValue(controlTypes[index], index)}
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item {...restField} name={[name, 'description']} label='描述' style={{ width: 170 }}>
|
||||
<Input placeholder='Parameter Description' />
|
||||
</Form.Item>
|
||||
|
||||
<MinusCircleOutlined onClick={() => remove(name)} />
|
||||
|
||||
<Form.Item name={[name, 'key']} hidden initialValue='dbgpt.core.flow.params' />
|
||||
<Form.Item name={[name, 'scope']} hidden initialValue='flow_priv' />
|
||||
<Form.Item name={[name, 'category']} hidden initialValue='common' />
|
||||
</Space>
|
||||
))}
|
||||
|
||||
<Form.Item>
|
||||
<Button type='dashed' onClick={() => add()} block icon={<PlusOutlined />}>
|
||||
{t('Add_Parameter')}
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</>
|
||||
)}
|
||||
</Form.List>
|
||||
</Form>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
};
|
@ -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<Props> = ({
|
||||
return (
|
||||
<>
|
||||
<Modal
|
||||
centered
|
||||
title={t('Export_Flow')}
|
||||
open={isExportFlowModalOpen}
|
||||
onCancel={() => setIsExportFlowModalOpen(false)}
|
||||
cancelButtonProps={{ className: 'hidden' }}
|
||||
okButtonProps={{ className: 'hidden' }}
|
||||
footer={[
|
||||
<Button key='cancel' onClick={() => setIsExportFlowModalOpen(false)}>
|
||||
{t('cancel')}
|
||||
</Button>,
|
||||
<Button key='submit' type='primary' onClick={() => form.submit()}>
|
||||
{t('verify')}
|
||||
</Button>,
|
||||
]}
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
@ -79,17 +84,6 @@ export const ExportFlowModal: React.FC<Props> = ({
|
||||
<Form.Item hidden name='uid'>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item wrapperCol={{ offset: 14, span: 8 }}>
|
||||
<Space>
|
||||
<Button htmlType='button' onClick={() => setIsExportFlowModalOpen(false)}>
|
||||
{t('cancel')}
|
||||
</Button>
|
||||
<Button type='primary' htmlType='submit'>
|
||||
{t('verify')}
|
||||
</Button>
|
||||
</Space>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
|
||||
|
89
web/components/flow/canvas-modal/flow-template-modal.tsx
Normal file
89
web/components/flow/canvas-modal/flow-template-modal.tsx
Normal file
@ -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<Props> = ({ 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<DataType>['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) => (
|
||||
<Space size='middle'>
|
||||
<Button
|
||||
type='link'
|
||||
onClick={() => {
|
||||
onTemplateImport(record);
|
||||
}}
|
||||
block
|
||||
>
|
||||
{t('Import_From_Template')}
|
||||
</Button>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
useEffect(() => {
|
||||
getFlowTemplates().then(res => {
|
||||
console.log(res);
|
||||
setDataSource(res?.data?.data?.items);
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal
|
||||
className='w-[700px]'
|
||||
title={t('Import_From_Template')}
|
||||
open={isFlowTemplateModalOpen}
|
||||
onCancel={() => setIsFlowTemplateModalOpen(false)}
|
||||
cancelButtonProps={{ className: 'hidden' }}
|
||||
okButtonProps={{ className: 'hidden' }}
|
||||
>
|
||||
<Table className='w-full' dataSource={dataSource} columns={columns} />;
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
};
|
@ -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<Props> = ({ 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<Props> = ({ isImportModalOpen, setIsImpor
|
||||
return (
|
||||
<>
|
||||
<Modal
|
||||
centered
|
||||
title={t('Import_Flow')}
|
||||
open={isImportModalOpen}
|
||||
onCancel={() => setIsImportFlowModalOpen(false)}
|
||||
cancelButtonProps={{ className: 'hidden' }}
|
||||
okButtonProps={{ className: 'hidden' }}
|
||||
footer={[
|
||||
<Button key='cancel' onClick={() => setIsImportFlowModalOpen(false)}>
|
||||
{t('cancel')}
|
||||
</Button>,
|
||||
<Button key='submit' type='primary' onClick={() => form.submit()}>
|
||||
{t('verify')}
|
||||
</Button>,
|
||||
]}
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
@ -90,21 +98,12 @@ export const ImportFlowModal: React.FC<Props> = ({ isImportModalOpen, setIsImpor
|
||||
</Upload>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item name='save_flow' label={t('Save_After_Import')}>
|
||||
<Form.Item name='save_flow' label={t('Save_After_Import')} hidden>
|
||||
<Radio.Group>
|
||||
<Radio value={true}>{t('Yes')}</Radio>
|
||||
<Radio value={false}>{t('No')}</Radio>
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item wrapperCol={{ offset: 14, span: 8 }}>
|
||||
<Space>
|
||||
<Button onClick={() => setIsImportFlowModalOpen(false)}>{t('cancel')}</Button>
|
||||
<Button type='primary' htmlType='submit'>
|
||||
{t('verify')}
|
||||
</Button>
|
||||
</Space>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
|
||||
|
@ -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';
|
||||
|
@ -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<Props> = ({
|
||||
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<IFlowUpdateParam>();
|
||||
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<HTMLInputElement>) {
|
||||
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<Props> = ({
|
||||
|
||||
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<Props> = ({
|
||||
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<Props> = ({
|
||||
return (
|
||||
<>
|
||||
<Modal
|
||||
centered
|
||||
title={t('flow_modal_title')}
|
||||
open={isSaveFlowModalOpen}
|
||||
onCancel={() => {
|
||||
setIsSaveFlowModalOpen(false);
|
||||
}}
|
||||
cancelButtonProps={{ className: 'hidden' }}
|
||||
okButtonProps={{ className: 'hidden' }}
|
||||
onCancel={() => setIsSaveFlowModalOpen(false)}
|
||||
footer={[
|
||||
<Button key='cancel' onClick={() => setIsSaveFlowModalOpen(false)}>
|
||||
{t('cancel')}
|
||||
</Button>,
|
||||
<Button key='submit' type='primary' onClick={() => form.submit()}>
|
||||
{t('verify')}
|
||||
</Button>,
|
||||
]}
|
||||
>
|
||||
<Form
|
||||
name='flow_form'
|
||||
@ -137,7 +147,7 @@ export const SaveFlowModal: React.FC<Props> = ({
|
||||
<TextArea rows={3} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label='Editable' name='editable' initialValue={flowInfo?.editable} valuePropName='checked'>
|
||||
<Form.Item label='Editable' name='editable' initialValue={flowInfo?.editable || true} valuePropName='checked'>
|
||||
<Checkbox />
|
||||
</Form.Item>
|
||||
|
||||
@ -156,22 +166,6 @@ export const SaveFlowModal: React.FC<Props> = ({
|
||||
}}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item wrapperCol={{ offset: 14, span: 8 }}>
|
||||
<Space>
|
||||
<Button
|
||||
htmlType='button'
|
||||
onClick={() => {
|
||||
setIsSaveFlowModalOpen(false);
|
||||
}}
|
||||
>
|
||||
{t('cancel')}
|
||||
</Button>
|
||||
<Button type='primary' htmlType='submit'>
|
||||
{t('verify')}
|
||||
</Button>
|
||||
</Space>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
|
||||
|
@ -19,7 +19,6 @@ type CanvasNodeProps = {
|
||||
function TypeLabel({ label }: { label: string }) {
|
||||
return <div className='w-full h-8 align-middle font-semibold'>{label}</div>;
|
||||
}
|
||||
const forceTypeList = ['file', 'multiple_files', 'time'];
|
||||
|
||||
const CanvasNode: React.FC<CanvasNodeProps> = ({ data }) => {
|
||||
const node = data;
|
||||
@ -125,12 +124,9 @@ const CanvasNode: React.FC<CanvasNodeProps> = ({ data }) => {
|
||||
});
|
||||
}
|
||||
|
||||
function onParameterValuesChange(changedValues: any, allValues: any) {
|
||||
function onParameterValuesChange(changedValues: any) {
|
||||
const [changedKey, changedVal] = Object.entries(changedValues)[0];
|
||||
|
||||
if (!allValues?.force && forceTypeList.includes(changedKey)) {
|
||||
return;
|
||||
}
|
||||
updateCurrentNodeValue(changedKey, changedVal);
|
||||
if (changedVal) {
|
||||
updateDependsNodeValue(changedKey, changedVal);
|
||||
@ -142,7 +138,7 @@ const CanvasNode: React.FC<CanvasNodeProps> = ({ data }) => {
|
||||
return (
|
||||
<div className='bg-zinc-100 dark:bg-zinc-700 rounded p-2'>
|
||||
<TypeLabel label='Outputs' />
|
||||
{(outputs || []).map((output, index) => (
|
||||
{outputs?.map((output, index) => (
|
||||
<NodeHandler
|
||||
key={`${data.id}_input_${index}`}
|
||||
node={data}
|
||||
@ -197,8 +193,11 @@ const CanvasNode: React.FC<CanvasNodeProps> = ({ data }) => {
|
||||
>
|
||||
<div
|
||||
className={classNames(
|
||||
'w-80 h-auto rounded-xl shadow-md px-2 py-4 border bg-white dark:bg-zinc-800 cursor-grab flex flex-col space-y-2 text-sm',
|
||||
'h-auto rounded-xl shadow-md px-2 py-4 border bg-white dark:bg-zinc-800 cursor-grab flex flex-col space-y-2 text-sm',
|
||||
{
|
||||
'w-80': node?.tags?.ui_size === 'middle' || !node?.tags?.ui_size,
|
||||
'w-[256px]': node?.tags?.ui_size === 'small',
|
||||
'w-[530px]': node?.tags?.ui_size === 'large',
|
||||
'border-blue-500': node.selected || isHovered,
|
||||
'border-stone-400 dark:border-white': !node.selected && !isHovered,
|
||||
'border-dashed': flowType !== 'operator',
|
||||
|
@ -131,6 +131,7 @@ const NodeParamHandler: React.FC<NodeParamHandlerProps> = ({ formValuesChange, n
|
||||
if (ui_type === 'slider' && data.is_list) {
|
||||
defaultValue = [0, 1];
|
||||
}
|
||||
|
||||
return (
|
||||
<Form.Item
|
||||
className='mb-2'
|
||||
|
@ -1,10 +1,11 @@
|
||||
/* eslint-disable react-hooks/rules-of-hooks */
|
||||
import { metadataBatch } from '@/client/api';
|
||||
import { IFlowNodeParameter } from '@/types/flow';
|
||||
import { convertKeysToCamelCase } from '@/utils/flow';
|
||||
import { UploadOutlined } from '@ant-design/icons';
|
||||
import type { UploadProps } from 'antd';
|
||||
import type { UploadFile, UploadProps } from 'antd';
|
||||
import { Button, Upload, message } from 'antd';
|
||||
import { useRef, useState } from 'react';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
type Props = {
|
||||
@ -16,6 +17,35 @@ export const renderUpload = (params: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const urlList = useRef<string[]>([]);
|
||||
const { data, formValuesChange } = params;
|
||||
const [fileList, setFileList] = useState<UploadFile[]>([]);
|
||||
|
||||
// 获取上传文件元数据
|
||||
useEffect(() => {
|
||||
if (data.value) {
|
||||
let uris: string[] = [];
|
||||
typeof data.value === 'string' ? uris.push(data.value) : (uris = data.value);
|
||||
const parameter: any = {
|
||||
uris,
|
||||
};
|
||||
metadataBatch(parameter)
|
||||
.then(res => {
|
||||
const urlList: UploadFile[] = [];
|
||||
for (let index = 0; index < res.data.data.length; index++) {
|
||||
const element = res.data.data[index];
|
||||
urlList.push({
|
||||
uid: element.file_id,
|
||||
name: element.file_name,
|
||||
status: 'done',
|
||||
url: element.uri,
|
||||
});
|
||||
}
|
||||
setFileList(urlList);
|
||||
})
|
||||
.catch(error => {
|
||||
console.log(error);
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
|
||||
const attr = convertKeysToCamelCase(data.ui?.attr || {});
|
||||
const [uploading, setUploading] = useState(false);
|
||||
@ -51,6 +81,7 @@ export const renderUpload = (params: Props) => {
|
||||
headers: {
|
||||
authorization: 'authorization-text',
|
||||
},
|
||||
defaultFileList: fileList,
|
||||
onChange(info) {
|
||||
setUploading(true);
|
||||
if (info.file.status !== 'uploading') {
|
||||
@ -73,19 +104,17 @@ export const renderUpload = (params: Props) => {
|
||||
|
||||
return (
|
||||
<div className='p-2 text-sm text-center'>
|
||||
{data.is_list ? (
|
||||
<Upload onRemove={handleFileRemove} {...props} {...attr} multiple={true} accept={uploadType}>
|
||||
<Button loading={uploading} icon={<UploadOutlined />}>
|
||||
{t('Upload_Data')}
|
||||
</Button>
|
||||
</Upload>
|
||||
) : (
|
||||
<Upload onRemove={handleFileRemove} {...props} {...attr} multiple={false} accept={uploadType}>
|
||||
<Button loading={uploading} icon={<UploadOutlined />}>
|
||||
{t('Upload_Data')}
|
||||
</Button>
|
||||
</Upload>
|
||||
)}
|
||||
<Upload
|
||||
onRemove={handleFileRemove}
|
||||
{...props}
|
||||
{...attr}
|
||||
multiple={data.is_list ? true : false}
|
||||
accept={uploadType}
|
||||
>
|
||||
<Button loading={uploading} icon={<UploadOutlined />}>
|
||||
{t('Upload_Data')}
|
||||
</Button>
|
||||
</Upload>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -16,4 +16,14 @@ export const FlowEn = {
|
||||
Export_File_Format: 'File_Format',
|
||||
Yes: 'Yes',
|
||||
No: 'No',
|
||||
Please_Add_Nodes_First: 'Please add nodes first',
|
||||
Add_Global_Variable_of_Flow: 'Add global variable of flow',
|
||||
Add_Parameter: 'Add Parameter',
|
||||
Higher_Order_Nodes: 'Higher Order',
|
||||
All_Nodes: 'All',
|
||||
Import_From_Template: 'Import from template',
|
||||
Template_Description: 'Description',
|
||||
Template_Name: 'Template Name',
|
||||
Template_Label: 'Label',
|
||||
Template_Action: 'Action',
|
||||
};
|
||||
|
@ -16,4 +16,14 @@ export const FlowZn = {
|
||||
Export_File_Format: '文件格式',
|
||||
Yes: '是',
|
||||
No: '否',
|
||||
Please_Add_Nodes_First: '请先添加节点',
|
||||
Add_Global_Variable_of_Flow: '添加 Flow 全局变量',
|
||||
Add_Parameter: '添加参数',
|
||||
Higher_Order_Nodes: '高阶',
|
||||
All_Nodes: '所有',
|
||||
Import_From_Template: '导入模版',
|
||||
Template_Description: '描述',
|
||||
Template_Name: '模版名称',
|
||||
Template_Label: '标签',
|
||||
Template_Action: '操作',
|
||||
};
|
||||
|
@ -12,6 +12,7 @@ import { t } from 'i18next';
|
||||
import { useRouter } from 'next/router';
|
||||
import React from 'react';
|
||||
import './style.css';
|
||||
|
||||
function ConstructLayout({ children }: { children: React.ReactNode }) {
|
||||
const items = [
|
||||
{
|
||||
@ -19,6 +20,15 @@ function ConstructLayout({ children }: { children: React.ReactNode }) {
|
||||
name: t('App'),
|
||||
path: '/app',
|
||||
icon: <AppstoreOutlined />,
|
||||
// operations: (
|
||||
// <Button
|
||||
// className='border-none text-white bg-button-gradient h-full flex items-center'
|
||||
// icon={<PlusOutlined className='text-base' />}
|
||||
// // onClick={handleCreate}
|
||||
// >
|
||||
// {t('create_app')}
|
||||
// </Button>
|
||||
// ),
|
||||
},
|
||||
{
|
||||
key: 'flow',
|
||||
@ -102,6 +112,15 @@ function ConstructLayout({ children }: { children: React.ReactNode }) {
|
||||
onTabClick={key => {
|
||||
router.push(`/construct/${key}`);
|
||||
}}
|
||||
// tabBarExtraContent={
|
||||
// <Button
|
||||
// className='border-none text-white bg-button-gradient h-full flex items-center'
|
||||
// icon={<PlusOutlined className='text-base' />}
|
||||
// // onClick={handleCreate}
|
||||
// >
|
||||
// {t('create_app')}
|
||||
// </Button>
|
||||
// }
|
||||
/>
|
||||
</ConfigProvider>
|
||||
</div>
|
||||
|
@ -308,15 +308,14 @@ export default function AppContent() {
|
||||
className='w-[230px] h-[40px] border-1 border-white backdrop-filter backdrop-blur-lg bg-white bg-opacity-30 dark:border-[#6f7f95] dark:bg-[#6f7f95] dark:bg-opacity-60'
|
||||
/>
|
||||
</div>
|
||||
<div className='flex items-center gap-4 h-10'>
|
||||
<Button
|
||||
className='border-none text-white bg-button-gradient h-full flex items-center'
|
||||
icon={<PlusOutlined className='text-base' />}
|
||||
onClick={handleCreate}
|
||||
>
|
||||
{t('create_app')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
className='border-none text-white bg-button-gradient flex items-center'
|
||||
icon={<PlusOutlined className='text-base' />}
|
||||
onClick={handleCreate}
|
||||
>
|
||||
{t('create_app')}
|
||||
</Button>
|
||||
</div>
|
||||
<div className=' w-full flex flex-wrap pb-12 mx-[-8px]'>
|
||||
{apps.map(item => {
|
||||
|
@ -1,6 +1,18 @@
|
||||
import { apiInterceptors, getFlowById } from '@/client/api';
|
||||
import MuiLoading from '@/components/common/loading';
|
||||
import { ExportOutlined, FrownOutlined, ImportOutlined, SaveOutlined } from '@ant-design/icons';
|
||||
import AddNodesSider from '@/components/flow/add-nodes-sider';
|
||||
import ButtonEdge from '@/components/flow/button-edge';
|
||||
import {
|
||||
AddFlowVariableModal,
|
||||
ExportFlowModal,
|
||||
FlowTemplateModal,
|
||||
ImportFlowModal,
|
||||
SaveFlowModal,
|
||||
} from '@/components/flow/canvas-modal';
|
||||
import CanvasNode from '@/components/flow/canvas-node';
|
||||
import { IFlowData, IFlowUpdateParam } from '@/types/flow';
|
||||
import { checkFlowDataRequied, getUniqueNodeId, mapUnderlineToHump } from '@/utils/flow';
|
||||
import { ExportOutlined, FileAddOutlined, FrownOutlined, ImportOutlined, SaveOutlined } from '@ant-design/icons';
|
||||
import { Divider, Space, Tooltip, message, notification } from 'antd';
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
import React, { DragEvent, useCallback, useEffect, useRef, useState } from 'react';
|
||||
@ -16,13 +28,6 @@ import ReactFlow, {
|
||||
useNodesState,
|
||||
useReactFlow,
|
||||
} from 'reactflow';
|
||||
// import AddNodes from '@/components/flow/add-nodes';
|
||||
import AddNodesSider from '@/components/flow/add-nodes-sider';
|
||||
import ButtonEdge from '@/components/flow/button-edge';
|
||||
import { ExportFlowModal, ImportFlowModal, SaveFlowModal } from '@/components/flow/canvas-modal';
|
||||
import CanvasNode from '@/components/flow/canvas-node';
|
||||
import { IFlowData, IFlowUpdateParam } from '@/types/flow';
|
||||
import { checkFlowDataRequied, getUniqueNodeId, mapUnderlineToHump } from '@/utils/flow';
|
||||
import 'reactflow/dist/style.css';
|
||||
|
||||
const nodeTypes = { customNode: CanvasNode };
|
||||
@ -30,19 +35,32 @@ const edgeTypes = { buttonedge: ButtonEdge };
|
||||
|
||||
const Canvas: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const searchParams = useSearchParams();
|
||||
const id = searchParams?.get('id') || '';
|
||||
const reactFlow = useReactFlow();
|
||||
const [messageApi, contextHolder] = message.useMessage();
|
||||
const reactFlowWrapper = useRef<HTMLDivElement>(null);
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [nodes, setNodes, onNodesChange] = useNodesState([]);
|
||||
const [edges, setEdges, onEdgesChange] = useEdgesState([]);
|
||||
const reactFlowWrapper = useRef<HTMLDivElement>(null);
|
||||
|
||||
const [flowInfo, setFlowInfo] = useState<IFlowUpdateParam>();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [isSaveFlowModalOpen, setIsSaveFlowModalOpen] = useState(false);
|
||||
const [isExportFlowModalOpen, setIsExportFlowModalOpen] = useState(false);
|
||||
const [isImportModalOpen, setIsImportFlowModalOpen] = useState(false);
|
||||
const [isFlowTemplateModalOpen, setIsFlowTemplateModalOpen] = useState(false);
|
||||
|
||||
if (localStorage.getItem('importFlowData')) {
|
||||
const importFlowData = JSON.parse(localStorage.getItem('importFlowData') || '');
|
||||
localStorage.removeItem('importFlowData');
|
||||
setLoading(true);
|
||||
const flowData = mapUnderlineToHump(importFlowData.flow_data);
|
||||
setFlowInfo(importFlowData);
|
||||
setNodes(flowData.nodes);
|
||||
setEdges(flowData.edges);
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
async function getFlowData() {
|
||||
setLoading(true);
|
||||
@ -152,22 +170,24 @@ const Canvas: React.FC = () => {
|
||||
function onSave() {
|
||||
const flowData = reactFlow.toObject() as IFlowData;
|
||||
const [check, node, message] = checkFlowDataRequied(flowData);
|
||||
|
||||
if (!node) {
|
||||
messageApi.open({
|
||||
type: 'warning',
|
||||
content: t('Please_Add_Nodes_First'),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!check && message) {
|
||||
setNodes(nds =>
|
||||
nds.map(item => {
|
||||
if (item.id === node?.id) {
|
||||
item.data = {
|
||||
...item.data,
|
||||
invalid: true,
|
||||
};
|
||||
} else {
|
||||
item.data = {
|
||||
...item.data,
|
||||
invalid: false,
|
||||
};
|
||||
}
|
||||
return item;
|
||||
}),
|
||||
nds.map(item => ({
|
||||
...item,
|
||||
data: {
|
||||
...item.data,
|
||||
invalid: item.id === node?.id,
|
||||
},
|
||||
})),
|
||||
);
|
||||
return notification.error({
|
||||
message: 'Error',
|
||||
@ -178,19 +198,15 @@ const Canvas: React.FC = () => {
|
||||
setIsSaveFlowModalOpen(true);
|
||||
}
|
||||
|
||||
function onExport() {
|
||||
setIsExportFlowModalOpen(true);
|
||||
}
|
||||
|
||||
function onImport() {
|
||||
setIsImportFlowModalOpen(true);
|
||||
}
|
||||
|
||||
const getButtonList = () => {
|
||||
const buttonList = [
|
||||
{
|
||||
title: t('template'),
|
||||
icon: <FileAddOutlined className='block text-xl' onClick={() => setIsFlowTemplateModalOpen(true)} />,
|
||||
},
|
||||
{
|
||||
title: t('Import'),
|
||||
icon: <ImportOutlined className='block text-xl' onClick={onImport} />,
|
||||
icon: <ImportOutlined className='block text-xl' onClick={() => setIsImportFlowModalOpen(true)} />,
|
||||
},
|
||||
{
|
||||
title: t('save'),
|
||||
@ -201,7 +217,7 @@ const Canvas: React.FC = () => {
|
||||
if (id !== '') {
|
||||
buttonList.unshift({
|
||||
title: t('Export'),
|
||||
icon: <ExportOutlined className='block text-xl' onClick={onExport} />,
|
||||
icon: <ExportOutlined className='block text-xl' onClick={() => setIsExportFlowModalOpen(true)} />,
|
||||
});
|
||||
}
|
||||
|
||||
@ -245,8 +261,10 @@ const Canvas: React.FC = () => {
|
||||
deleteKeyCode={['Backspace', 'Delete']}
|
||||
>
|
||||
<Controls className='flex flex-row items-center' position='bottom-center' />
|
||||
|
||||
<Background color='#aaa' gap={16} />
|
||||
{/* <AddNodes /> */}
|
||||
|
||||
<AddFlowVariableModal flowInfo={flowInfo} setFlowInfo={setFlowInfo} />
|
||||
</ReactFlow>
|
||||
</div>
|
||||
</div>
|
||||
@ -274,6 +292,13 @@ const Canvas: React.FC = () => {
|
||||
isImportModalOpen={isImportModalOpen}
|
||||
setIsImportFlowModalOpen={setIsImportFlowModalOpen}
|
||||
/>
|
||||
|
||||
<FlowTemplateModal
|
||||
isFlowTemplateModalOpen={isFlowTemplateModalOpen}
|
||||
setIsFlowTemplateModalOpen={setIsFlowTemplateModalOpen}
|
||||
/>
|
||||
|
||||
{contextHolder}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -126,7 +126,6 @@ function Flow() {
|
||||
copyFlowTemp.current = flow;
|
||||
form.setFieldValue('label', `${flow.label} Copy`);
|
||||
form.setFieldValue('name', `${flow.name}_copy`);
|
||||
setDeploy(true);
|
||||
setEditable(true);
|
||||
setShowModal(true);
|
||||
};
|
||||
@ -256,8 +255,10 @@ function Flow() {
|
||||
</div>
|
||||
</div>
|
||||
</Spin>
|
||||
|
||||
<Modal
|
||||
open={showModal}
|
||||
destroyOnClose
|
||||
title='Copy AWEL Flow'
|
||||
onCancel={() => {
|
||||
setShowModal(false);
|
||||
|
@ -6,7 +6,7 @@ import { ClearOutlined, LoadingOutlined, PauseCircleOutlined, RedoOutlined, Send
|
||||
import { EventStreamContentType, fetchEventSource } from '@microsoft/fetch-event-source';
|
||||
import { useRequest } from 'ahooks';
|
||||
import { Button, Input, Popover, Spin, Tag } from 'antd';
|
||||
import cls from 'classnames';
|
||||
import classnames from 'classnames';
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
import React, { useContext, useEffect, useMemo, useState } from 'react';
|
||||
import { MobileChatContext } from '../';
|
||||
@ -245,7 +245,7 @@ const InputContainer: React.FC = () => {
|
||||
<div className='flex items-center justify-between text-lg font-bold'>
|
||||
<Popover content='暂停回复' trigger={['hover']}>
|
||||
<PauseCircleOutlined
|
||||
className={cls('p-2 cursor-pointer', {
|
||||
className={classnames('p-2 cursor-pointer', {
|
||||
'text-[#0c75fc]': canAbort,
|
||||
'text-gray-400': !canAbort,
|
||||
})}
|
||||
@ -254,7 +254,7 @@ const InputContainer: React.FC = () => {
|
||||
</Popover>
|
||||
<Popover content='再来一次' trigger={['hover']}>
|
||||
<RedoOutlined
|
||||
className={cls('p-2 cursor-pointer', {
|
||||
className={classnames('p-2 cursor-pointer', {
|
||||
'text-gray-400': !history.length || !canNewChat,
|
||||
})}
|
||||
onClick={redo}
|
||||
@ -265,7 +265,7 @@ const InputContainer: React.FC = () => {
|
||||
) : (
|
||||
<Popover content='清除历史' trigger={['hover']}>
|
||||
<ClearOutlined
|
||||
className={cls('p-2 cursor-pointer', {
|
||||
className={classnames('p-2 cursor-pointer', {
|
||||
'text-gray-400': !history.length || !canNewChat,
|
||||
})}
|
||||
onClick={clearHistory}
|
||||
@ -276,7 +276,7 @@ const InputContainer: React.FC = () => {
|
||||
</div>
|
||||
{/* 输入框 */}
|
||||
<div
|
||||
className={cls(
|
||||
className={classnames(
|
||||
'flex py-2 px-3 items-center justify-between bg-white dark:bg-[#242733] dark:border-[#6f7f95] rounded-xl border',
|
||||
{
|
||||
'border-[#0c75fc] dark:border-[rgba(12,117,252,0.8)]': isFocus,
|
||||
@ -323,7 +323,7 @@ const InputContainer: React.FC = () => {
|
||||
|
||||
<Button
|
||||
type='primary'
|
||||
className={cls('flex items-center justify-center rounded-lg bg-button-gradient border-0 ml-2', {
|
||||
className={classnames('flex items-center justify-center rounded-lg bg-button-gradient border-0 ml-2', {
|
||||
'opacity-40 cursor-not-allowed': !userInput.trim() || !canNewChat,
|
||||
})}
|
||||
onClick={onSubmit}
|
||||
|
@ -122,4 +122,4 @@ table {
|
||||
|
||||
.rc-md-editor .editor-container>.section {
|
||||
border-right: none !important;
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"target": "es6",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
|
@ -12,6 +12,7 @@ export type IFlowUpdateParam = {
|
||||
uid?: string;
|
||||
flow_data?: IFlowData;
|
||||
state?: FlowState;
|
||||
variables?: IVariableItem[];
|
||||
};
|
||||
|
||||
export type IFlowRefreshParams = {
|
||||
@ -169,8 +170,9 @@ export type IFlowDataViewport = {
|
||||
};
|
||||
|
||||
export type IFlowData = {
|
||||
nodes: Array<IFlowDataNode>;
|
||||
edges: Array<IFlowDataEdge>;
|
||||
nodes: IFlowDataNode[];
|
||||
edges: IFlowDataEdge[];
|
||||
variables?: IVariableItem[];
|
||||
viewport: IFlowDataViewport;
|
||||
};
|
||||
|
||||
@ -200,3 +202,54 @@ export type IUploadFileResponse = {
|
||||
bucket: string;
|
||||
uri?: string;
|
||||
};
|
||||
|
||||
export type IGetKeysRequestParams = {
|
||||
user_name?: string;
|
||||
sys_code?: string;
|
||||
category?: string;
|
||||
};
|
||||
|
||||
export type IGetKeysResponseData = {
|
||||
key: string;
|
||||
label: string;
|
||||
description: string;
|
||||
value_type: string;
|
||||
category: string;
|
||||
scope: string;
|
||||
scope_key: string | null;
|
||||
};
|
||||
|
||||
export type IGetVariablesByKeyRequestParams = {
|
||||
key: string;
|
||||
scope: string;
|
||||
scope_key?: string;
|
||||
user_name?: string;
|
||||
sys_code?: string;
|
||||
page?: number;
|
||||
page_size?: number;
|
||||
};
|
||||
|
||||
export type IGetVariablesByKeyResponseData = {
|
||||
items: IVariableItem[];
|
||||
total_count: number;
|
||||
total_pages: number;
|
||||
page: number;
|
||||
page_size: number;
|
||||
};
|
||||
|
||||
export type IVariableItem = {
|
||||
key: string;
|
||||
label: string;
|
||||
description: string | null;
|
||||
value_type: string;
|
||||
category: string;
|
||||
scope: string;
|
||||
scope_key: string | null;
|
||||
name: string;
|
||||
value: string;
|
||||
enabled: boolean;
|
||||
user_name: string | null;
|
||||
sys_code: string | null;
|
||||
id: number;
|
||||
[key: string]: any;
|
||||
};
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { IFlowData, IFlowDataNode, IFlowNode } from '@/types/flow';
|
||||
import { IFlowData, IFlowDataNode, IFlowNode, IVariableItem } from '@/types/flow';
|
||||
import { Node } from 'reactflow';
|
||||
|
||||
export const getUniqueNodeId = (nodeData: IFlowNode, nodes: Node[]) => {
|
||||
@ -140,3 +140,57 @@ export const convertKeysToCamelCase = (obj: Record<string, any>): Record<string,
|
||||
|
||||
return convert(obj);
|
||||
};
|
||||
|
||||
function escapeVariable(value: string, enableEscape: boolean): string {
|
||||
if (!enableEscape) {
|
||||
return value;
|
||||
}
|
||||
return value.replace(/@/g, '\\@').replace(/#/g, '\\#').replace(/%/g, '\\%').replace(/:/g, '\\:');
|
||||
}
|
||||
|
||||
export function buildVariableString(variableDict: IVariableItem): string {
|
||||
const scopeSig = '@';
|
||||
const sysCodeSig = '#';
|
||||
const userSig = '%';
|
||||
const kvSig = ':';
|
||||
const enableEscape = true;
|
||||
|
||||
const specialChars = new Set([scopeSig, sysCodeSig, userSig, kvSig]);
|
||||
|
||||
const newVariableDict: Partial<IVariableItem> = {
|
||||
key: variableDict.key || '',
|
||||
name: variableDict.name || '',
|
||||
scope: variableDict.scope || '',
|
||||
scope_key: variableDict.scope_key || '',
|
||||
sys_code: variableDict.sys_code || '',
|
||||
user_name: variableDict.user_name || '',
|
||||
};
|
||||
|
||||
// Check for special characters in values
|
||||
for (const [key, value] of Object.entries(newVariableDict)) {
|
||||
if (value && [...specialChars].some(char => (value as string).includes(char))) {
|
||||
if (enableEscape) {
|
||||
newVariableDict[key] = escapeVariable(value as string, enableEscape);
|
||||
} else {
|
||||
throw new Error(
|
||||
`${key} contains special characters, error value: ${value}, special characters: ${[...specialChars].join(', ')}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const { key, name, scope, scope_key, sys_code, user_name } = newVariableDict;
|
||||
|
||||
let variableStr = `${key}`;
|
||||
|
||||
if (name) variableStr += `${kvSig}${name}`;
|
||||
if (scope || scope_key) {
|
||||
variableStr += `${scopeSig}${scope}`;
|
||||
if (scope_key) {
|
||||
variableStr += `${kvSig}${scope_key}`;
|
||||
}
|
||||
}
|
||||
if (sys_code) variableStr += `${sysCodeSig}${sys_code}`;
|
||||
if (user_name) variableStr += `${userSig}${user_name}`;
|
||||
return `\${${variableStr}}`;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user