mirror of
https://github.com/csunny/DB-GPT.git
synced 2025-08-06 10:54:29 +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."""
|
"""Translate the po file content to Chinese using LLM."""
|
||||||
|
|
||||||
from typing import List, Dict, Any
|
from typing import List, Dict, Any
|
||||||
import asyncio
|
import asyncio
|
||||||
import os
|
import os
|
||||||
@ -147,6 +148,8 @@ vocabulary_map = {
|
|||||||
"RAG": "RAG",
|
"RAG": "RAG",
|
||||||
"DB-GPT": "DB-GPT",
|
"DB-GPT": "DB-GPT",
|
||||||
"AWEL flow": "AWEL 工作流",
|
"AWEL flow": "AWEL 工作流",
|
||||||
|
"Agent": "智能体",
|
||||||
|
"Agents": "智能体",
|
||||||
},
|
},
|
||||||
"default": {
|
"default": {
|
||||||
"Transformer": "Transformer",
|
"Transformer": "Transformer",
|
||||||
@ -159,6 +162,8 @@ vocabulary_map = {
|
|||||||
"RAG": "RAG",
|
"RAG": "RAG",
|
||||||
"DB-GPT": "DB-GPT",
|
"DB-GPT": "DB-GPT",
|
||||||
"AWEL flow": "AWEL flow",
|
"AWEL flow": "AWEL flow",
|
||||||
|
"Agent": "Agent",
|
||||||
|
"Agents": "Agents",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -6,6 +6,10 @@ import {
|
|||||||
IFlowRefreshParams,
|
IFlowRefreshParams,
|
||||||
IFlowResponse,
|
IFlowResponse,
|
||||||
IFlowUpdateParam,
|
IFlowUpdateParam,
|
||||||
|
IGetKeysRequestParams,
|
||||||
|
IGetKeysResponseData,
|
||||||
|
IGetVariablesByKeyRequestParams,
|
||||||
|
IGetVariablesByKeyResponseData,
|
||||||
IUploadFileRequestParams,
|
IUploadFileRequestParams,
|
||||||
IUploadFileResponse,
|
IUploadFileResponse,
|
||||||
} from '@/types/flow';
|
} from '@/types/flow';
|
||||||
@ -35,8 +39,8 @@ export const deleteFlowById = (id: string) => {
|
|||||||
return DELETE<null, null>(`/api/v2/serve/awel/flows/${id}`);
|
return DELETE<null, null>(`/api/v2/serve/awel/flows/${id}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getFlowNodes = () => {
|
export const getFlowNodes = (tags?: string) => {
|
||||||
return GET<null, Array<IFlowNode>>(`/api/v2/serve/awel/nodes`);
|
return GET<{ tags?: string }, Array<IFlowNode>>(`/api/v2/serve/awel/nodes`, { tags });
|
||||||
};
|
};
|
||||||
|
|
||||||
export const refreshFlowNodeById = (data: IFlowRefreshParams) => {
|
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}`);
|
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) => {
|
export const getFlowTemplateById = (id: string) => {
|
||||||
return GET<null, any>(`/api/v2/serve/awel/flow/templates/${id}`);
|
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 { FLOW_NODES_KEY } from '@/utils';
|
||||||
import { CaretLeftOutlined, CaretRightOutlined } from '@ant-design/icons';
|
import { CaretLeftOutlined, CaretRightOutlined } from '@ant-design/icons';
|
||||||
import type { CollapseProps } from 'antd';
|
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 React, { useContext, useEffect, useMemo, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import StaticNodes from './static-nodes';
|
import StaticNodes from './static-nodes';
|
||||||
@ -12,6 +13,8 @@ import StaticNodes from './static-nodes';
|
|||||||
const { Search } = Input;
|
const { Search } = Input;
|
||||||
const { Sider } = Layout;
|
const { Sider } = Layout;
|
||||||
|
|
||||||
|
const TAGS = JSON.stringify({ order: 'higher-order' });
|
||||||
|
|
||||||
type GroupType = {
|
type GroupType = {
|
||||||
category: string;
|
category: string;
|
||||||
categoryLabel: string;
|
categoryLabel: string;
|
||||||
@ -41,13 +44,16 @@ const AddNodesSider: React.FC = () => {
|
|||||||
const [resources, setResources] = useState<Array<IFlowNode>>([]);
|
const [resources, setResources] = useState<Array<IFlowNode>>([]);
|
||||||
const [operatorsGroup, setOperatorsGroup] = useState<GroupType[]>([]);
|
const [operatorsGroup, setOperatorsGroup] = useState<GroupType[]>([]);
|
||||||
const [resourcesGroup, setResourcesGroup] = useState<GroupType[]>([]);
|
const [resourcesGroup, setResourcesGroup] = useState<GroupType[]>([]);
|
||||||
|
const [isAllNodesVisible, setIsAllNodesVisible] = useState<boolean>(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
getNodes();
|
getNodes(TAGS);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
async function getNodes() {
|
// tags is optional, if tags is not passed, it will get all nodes
|
||||||
const [_, data] = await apiInterceptors(getFlowNodes());
|
async function getNodes(tags?: string) {
|
||||||
|
const [_, data] = await apiInterceptors(getFlowNodes(tags));
|
||||||
|
|
||||||
if (data && data.length > 0) {
|
if (data && data.length > 0) {
|
||||||
localStorage.setItem(FLOW_NODES_KEY, JSON.stringify(data));
|
localStorage.setItem(FLOW_NODES_KEY, JSON.stringify(data));
|
||||||
const operatorNodes = data.filter(node => node.flow_type === 'operator');
|
const operatorNodes = data.filter(node => node.flow_type === 'operator');
|
||||||
@ -166,6 +172,16 @@ const AddNodesSider: React.FC = () => {
|
|||||||
setSearchValue(val);
|
setSearchValue(val);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onModeChange() {
|
||||||
|
if (isAllNodesVisible) {
|
||||||
|
getNodes(TAGS);
|
||||||
|
} else {
|
||||||
|
getNodes();
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsAllNodesVisible(!isAllNodesVisible);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Sider
|
<Sider
|
||||||
className='flex justify-center items-start nodrag bg-[#ffffff80] border-r border-[#d5e5f6] dark:bg-[#ffffff29] dark:border-[#ffffff66]'
|
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)}
|
onCollapse={collapsed => setCollapsed(collapsed)}
|
||||||
>
|
>
|
||||||
<Space direction='vertical' className='w-[280px] pt-4 px-4 overflow-hidden overflow-y-auto scrollbar-default'>
|
<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'>
|
<div className='flex justify-between align-middle'>
|
||||||
{t('add_node')}
|
<p className='w-full text-base font-semibold text-[#1c2533] dark:text-[rgba(255,255,255,0.85)] line-clamp-1'>
|
||||||
</p>
|
{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 />
|
<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 { 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 { useTranslation } from 'react-i18next';
|
||||||
import { ReactFlowInstance } from 'reactflow';
|
import { ReactFlowInstance } from 'reactflow';
|
||||||
|
|
||||||
@ -43,12 +43,17 @@ export const ExportFlowModal: React.FC<Props> = ({
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Modal
|
<Modal
|
||||||
centered
|
|
||||||
title={t('Export_Flow')}
|
title={t('Export_Flow')}
|
||||||
open={isExportFlowModalOpen}
|
open={isExportFlowModalOpen}
|
||||||
onCancel={() => setIsExportFlowModalOpen(false)}
|
onCancel={() => setIsExportFlowModalOpen(false)}
|
||||||
cancelButtonProps={{ className: 'hidden' }}
|
footer={[
|
||||||
okButtonProps={{ className: 'hidden' }}
|
<Button key='cancel' onClick={() => setIsExportFlowModalOpen(false)}>
|
||||||
|
{t('cancel')}
|
||||||
|
</Button>,
|
||||||
|
<Button key='submit' type='primary' onClick={() => form.submit()}>
|
||||||
|
{t('verify')}
|
||||||
|
</Button>,
|
||||||
|
]}
|
||||||
>
|
>
|
||||||
<Form
|
<Form
|
||||||
form={form}
|
form={form}
|
||||||
@ -79,17 +84,6 @@ export const ExportFlowModal: React.FC<Props> = ({
|
|||||||
<Form.Item hidden name='uid'>
|
<Form.Item hidden name='uid'>
|
||||||
<Input />
|
<Input />
|
||||||
</Form.Item>
|
</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>
|
</Form>
|
||||||
</Modal>
|
</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 { apiInterceptors, importFlow } from '@/client/api';
|
||||||
|
import CanvasWrapper from '@/pages/construct/flow/canvas/index';
|
||||||
import { UploadOutlined } from '@ant-design/icons';
|
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 { useEffect, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Edge, Node } from 'reactflow';
|
import { Edge, Node } from 'reactflow';
|
||||||
@ -37,7 +38,9 @@ export const ImportFlowModal: React.FC<Props> = ({ isImportModalOpen, setIsImpor
|
|||||||
const [, , res] = await apiInterceptors(importFlow(formData));
|
const [, , res] = await apiInterceptors(importFlow(formData));
|
||||||
|
|
||||||
if (res?.success) {
|
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) {
|
} else if (res?.err_msg) {
|
||||||
messageApi.error(res?.err_msg);
|
messageApi.error(res?.err_msg);
|
||||||
}
|
}
|
||||||
@ -61,12 +64,17 @@ export const ImportFlowModal: React.FC<Props> = ({ isImportModalOpen, setIsImpor
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Modal
|
<Modal
|
||||||
centered
|
|
||||||
title={t('Import_Flow')}
|
title={t('Import_Flow')}
|
||||||
open={isImportModalOpen}
|
open={isImportModalOpen}
|
||||||
onCancel={() => setIsImportFlowModalOpen(false)}
|
onCancel={() => setIsImportFlowModalOpen(false)}
|
||||||
cancelButtonProps={{ className: 'hidden' }}
|
footer={[
|
||||||
okButtonProps={{ className: 'hidden' }}
|
<Button key='cancel' onClick={() => setIsImportFlowModalOpen(false)}>
|
||||||
|
{t('cancel')}
|
||||||
|
</Button>,
|
||||||
|
<Button key='submit' type='primary' onClick={() => form.submit()}>
|
||||||
|
{t('verify')}
|
||||||
|
</Button>,
|
||||||
|
]}
|
||||||
>
|
>
|
||||||
<Form
|
<Form
|
||||||
form={form}
|
form={form}
|
||||||
@ -90,21 +98,12 @@ export const ImportFlowModal: React.FC<Props> = ({ isImportModalOpen, setIsImpor
|
|||||||
</Upload>
|
</Upload>
|
||||||
</Form.Item>
|
</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.Group>
|
||||||
<Radio value={true}>{t('Yes')}</Radio>
|
<Radio value={true}>{t('Yes')}</Radio>
|
||||||
<Radio value={false}>{t('No')}</Radio>
|
<Radio value={false}>{t('No')}</Radio>
|
||||||
</Radio.Group>
|
</Radio.Group>
|
||||||
</Form.Item>
|
</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>
|
</Form>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
export * from './add-flow-variable-modal';
|
||||||
export * from './export-flow-modal';
|
export * from './export-flow-modal';
|
||||||
|
export * from './flow-template-modal';
|
||||||
export * from './import-flow-modal';
|
export * from './import-flow-modal';
|
||||||
export * from './save-flow-modal';
|
export * from './save-flow-modal';
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
import { addFlow, apiInterceptors, updateFlowById } from '@/client/api';
|
import { addFlow, apiInterceptors, updateFlowById } from '@/client/api';
|
||||||
import { IFlowData, IFlowUpdateParam } from '@/types/flow';
|
import { IFlowData, IFlowUpdateParam } from '@/types/flow';
|
||||||
import { mapHumpToUnderline } from '@/utils/flow';
|
import { mapHumpToUnderline } from '@/utils/flow';
|
||||||
import { Button, Checkbox, Form, Input, Modal, Space, message } from 'antd';
|
import { Button, Checkbox, Form, Input, Modal, message } from 'antd';
|
||||||
import { useSearchParams } from 'next/navigation';
|
import { useRouter } from 'next/router';
|
||||||
import { useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { ReactFlowInstance } from 'reactflow';
|
import { ReactFlowInstance } from 'reactflow';
|
||||||
|
|
||||||
@ -22,13 +22,18 @@ export const SaveFlowModal: React.FC<Props> = ({
|
|||||||
flowInfo,
|
flowInfo,
|
||||||
setIsSaveFlowModalOpen,
|
setIsSaveFlowModalOpen,
|
||||||
}) => {
|
}) => {
|
||||||
const [deploy, setDeploy] = useState(true);
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const searchParams = useSearchParams();
|
const router = useRouter();
|
||||||
const id = searchParams?.get('id') || '';
|
|
||||||
const [form] = Form.useForm<IFlowUpdateParam>();
|
const [form] = Form.useForm<IFlowUpdateParam>();
|
||||||
const [messageApi, contextHolder] = message.useMessage();
|
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>) {
|
function onLabelChange(e: React.ChangeEvent<HTMLInputElement>) {
|
||||||
const label = e.target.value;
|
const label = e.target.value;
|
||||||
// replace spaces with underscores, convert uppercase letters to lowercase, remove characters other than digits, letters, _, and -.
|
// 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) {
|
if (id) {
|
||||||
const [, , res] = await apiInterceptors(
|
const [, , res] = await apiInterceptors(
|
||||||
updateFlowById(id, {
|
updateFlowById(id.toString(), {
|
||||||
name,
|
name,
|
||||||
label,
|
label,
|
||||||
description,
|
description,
|
||||||
editable,
|
editable,
|
||||||
uid: id,
|
uid: id.toString(),
|
||||||
flow_data: reactFlowObject,
|
flow_data: reactFlowObject,
|
||||||
state,
|
state,
|
||||||
|
variables: flowInfo?.variables,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -70,12 +76,13 @@ export const SaveFlowModal: React.FC<Props> = ({
|
|||||||
editable,
|
editable,
|
||||||
flow_data: reactFlowObject,
|
flow_data: reactFlowObject,
|
||||||
state,
|
state,
|
||||||
|
variables: flowInfo?.variables,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (res?.uid) {
|
if (res?.uid) {
|
||||||
messageApi.success(t('save_flow_success'));
|
messageApi.success(t('save_flow_success'));
|
||||||
const history = window.history;
|
router.push(`/construct/flow/canvas?id=${res.uid}`, undefined, { shallow: true });
|
||||||
history.pushState(null, '', `/flow/canvas?id=${res.uid}`);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
setIsSaveFlowModalOpen(false);
|
setIsSaveFlowModalOpen(false);
|
||||||
@ -84,14 +91,17 @@ export const SaveFlowModal: React.FC<Props> = ({
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Modal
|
<Modal
|
||||||
centered
|
|
||||||
title={t('flow_modal_title')}
|
title={t('flow_modal_title')}
|
||||||
open={isSaveFlowModalOpen}
|
open={isSaveFlowModalOpen}
|
||||||
onCancel={() => {
|
onCancel={() => setIsSaveFlowModalOpen(false)}
|
||||||
setIsSaveFlowModalOpen(false);
|
footer={[
|
||||||
}}
|
<Button key='cancel' onClick={() => setIsSaveFlowModalOpen(false)}>
|
||||||
cancelButtonProps={{ className: 'hidden' }}
|
{t('cancel')}
|
||||||
okButtonProps={{ className: 'hidden' }}
|
</Button>,
|
||||||
|
<Button key='submit' type='primary' onClick={() => form.submit()}>
|
||||||
|
{t('verify')}
|
||||||
|
</Button>,
|
||||||
|
]}
|
||||||
>
|
>
|
||||||
<Form
|
<Form
|
||||||
name='flow_form'
|
name='flow_form'
|
||||||
@ -137,7 +147,7 @@ export const SaveFlowModal: React.FC<Props> = ({
|
|||||||
<TextArea rows={3} />
|
<TextArea rows={3} />
|
||||||
</Form.Item>
|
</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 />
|
<Checkbox />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
@ -156,22 +166,6 @@ export const SaveFlowModal: React.FC<Props> = ({
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</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>
|
</Form>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
|
@ -19,7 +19,6 @@ type CanvasNodeProps = {
|
|||||||
function TypeLabel({ label }: { label: string }) {
|
function TypeLabel({ label }: { label: string }) {
|
||||||
return <div className='w-full h-8 align-middle font-semibold'>{label}</div>;
|
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 CanvasNode: React.FC<CanvasNodeProps> = ({ data }) => {
|
||||||
const node = 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];
|
const [changedKey, changedVal] = Object.entries(changedValues)[0];
|
||||||
|
|
||||||
if (!allValues?.force && forceTypeList.includes(changedKey)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
updateCurrentNodeValue(changedKey, changedVal);
|
updateCurrentNodeValue(changedKey, changedVal);
|
||||||
if (changedVal) {
|
if (changedVal) {
|
||||||
updateDependsNodeValue(changedKey, changedVal);
|
updateDependsNodeValue(changedKey, changedVal);
|
||||||
@ -142,7 +138,7 @@ const CanvasNode: React.FC<CanvasNodeProps> = ({ data }) => {
|
|||||||
return (
|
return (
|
||||||
<div className='bg-zinc-100 dark:bg-zinc-700 rounded p-2'>
|
<div className='bg-zinc-100 dark:bg-zinc-700 rounded p-2'>
|
||||||
<TypeLabel label='Outputs' />
|
<TypeLabel label='Outputs' />
|
||||||
{(outputs || []).map((output, index) => (
|
{outputs?.map((output, index) => (
|
||||||
<NodeHandler
|
<NodeHandler
|
||||||
key={`${data.id}_input_${index}`}
|
key={`${data.id}_input_${index}`}
|
||||||
node={data}
|
node={data}
|
||||||
@ -197,8 +193,11 @@ const CanvasNode: React.FC<CanvasNodeProps> = ({ data }) => {
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={classNames(
|
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-blue-500': node.selected || isHovered,
|
||||||
'border-stone-400 dark:border-white': !node.selected && !isHovered,
|
'border-stone-400 dark:border-white': !node.selected && !isHovered,
|
||||||
'border-dashed': flowType !== 'operator',
|
'border-dashed': flowType !== 'operator',
|
||||||
|
@ -131,6 +131,7 @@ const NodeParamHandler: React.FC<NodeParamHandlerProps> = ({ formValuesChange, n
|
|||||||
if (ui_type === 'slider' && data.is_list) {
|
if (ui_type === 'slider' && data.is_list) {
|
||||||
defaultValue = [0, 1];
|
defaultValue = [0, 1];
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form.Item
|
<Form.Item
|
||||||
className='mb-2'
|
className='mb-2'
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
/* eslint-disable react-hooks/rules-of-hooks */
|
/* eslint-disable react-hooks/rules-of-hooks */
|
||||||
|
import { metadataBatch } from '@/client/api';
|
||||||
import { IFlowNodeParameter } from '@/types/flow';
|
import { IFlowNodeParameter } from '@/types/flow';
|
||||||
import { convertKeysToCamelCase } from '@/utils/flow';
|
import { convertKeysToCamelCase } from '@/utils/flow';
|
||||||
import { UploadOutlined } from '@ant-design/icons';
|
import { UploadOutlined } from '@ant-design/icons';
|
||||||
import type { UploadProps } from 'antd';
|
import type { UploadFile, UploadProps } from 'antd';
|
||||||
import { Button, Upload, message } from 'antd';
|
import { Button, Upload, message } from 'antd';
|
||||||
import { useRef, useState } from 'react';
|
import { useEffect, useRef, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@ -16,6 +17,35 @@ export const renderUpload = (params: Props) => {
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const urlList = useRef<string[]>([]);
|
const urlList = useRef<string[]>([]);
|
||||||
const { data, formValuesChange } = params;
|
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 attr = convertKeysToCamelCase(data.ui?.attr || {});
|
||||||
const [uploading, setUploading] = useState(false);
|
const [uploading, setUploading] = useState(false);
|
||||||
@ -51,6 +81,7 @@ export const renderUpload = (params: Props) => {
|
|||||||
headers: {
|
headers: {
|
||||||
authorization: 'authorization-text',
|
authorization: 'authorization-text',
|
||||||
},
|
},
|
||||||
|
defaultFileList: fileList,
|
||||||
onChange(info) {
|
onChange(info) {
|
||||||
setUploading(true);
|
setUploading(true);
|
||||||
if (info.file.status !== 'uploading') {
|
if (info.file.status !== 'uploading') {
|
||||||
@ -73,19 +104,17 @@ export const renderUpload = (params: Props) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='p-2 text-sm text-center'>
|
<div className='p-2 text-sm text-center'>
|
||||||
{data.is_list ? (
|
<Upload
|
||||||
<Upload onRemove={handleFileRemove} {...props} {...attr} multiple={true} accept={uploadType}>
|
onRemove={handleFileRemove}
|
||||||
<Button loading={uploading} icon={<UploadOutlined />}>
|
{...props}
|
||||||
{t('Upload_Data')}
|
{...attr}
|
||||||
</Button>
|
multiple={data.is_list ? true : false}
|
||||||
</Upload>
|
accept={uploadType}
|
||||||
) : (
|
>
|
||||||
<Upload onRemove={handleFileRemove} {...props} {...attr} multiple={false} accept={uploadType}>
|
<Button loading={uploading} icon={<UploadOutlined />}>
|
||||||
<Button loading={uploading} icon={<UploadOutlined />}>
|
{t('Upload_Data')}
|
||||||
{t('Upload_Data')}
|
</Button>
|
||||||
</Button>
|
</Upload>
|
||||||
</Upload>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -16,4 +16,14 @@ export const FlowEn = {
|
|||||||
Export_File_Format: 'File_Format',
|
Export_File_Format: 'File_Format',
|
||||||
Yes: 'Yes',
|
Yes: 'Yes',
|
||||||
No: 'No',
|
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: '文件格式',
|
Export_File_Format: '文件格式',
|
||||||
Yes: '是',
|
Yes: '是',
|
||||||
No: '否',
|
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 { useRouter } from 'next/router';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import './style.css';
|
import './style.css';
|
||||||
|
|
||||||
function ConstructLayout({ children }: { children: React.ReactNode }) {
|
function ConstructLayout({ children }: { children: React.ReactNode }) {
|
||||||
const items = [
|
const items = [
|
||||||
{
|
{
|
||||||
@ -19,6 +20,15 @@ function ConstructLayout({ children }: { children: React.ReactNode }) {
|
|||||||
name: t('App'),
|
name: t('App'),
|
||||||
path: '/app',
|
path: '/app',
|
||||||
icon: <AppstoreOutlined />,
|
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',
|
key: 'flow',
|
||||||
@ -102,6 +112,15 @@ function ConstructLayout({ children }: { children: React.ReactNode }) {
|
|||||||
onTabClick={key => {
|
onTabClick={key => {
|
||||||
router.push(`/construct/${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>
|
</ConfigProvider>
|
||||||
</div>
|
</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'
|
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>
|
||||||
<div className='flex items-center gap-4 h-10'>
|
|
||||||
<Button
|
<Button
|
||||||
className='border-none text-white bg-button-gradient h-full flex items-center'
|
className='border-none text-white bg-button-gradient flex items-center'
|
||||||
icon={<PlusOutlined className='text-base' />}
|
icon={<PlusOutlined className='text-base' />}
|
||||||
onClick={handleCreate}
|
onClick={handleCreate}
|
||||||
>
|
>
|
||||||
{t('create_app')}
|
{t('create_app')}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div className=' w-full flex flex-wrap pb-12 mx-[-8px]'>
|
<div className=' w-full flex flex-wrap pb-12 mx-[-8px]'>
|
||||||
{apps.map(item => {
|
{apps.map(item => {
|
||||||
|
@ -1,6 +1,18 @@
|
|||||||
import { apiInterceptors, getFlowById } from '@/client/api';
|
import { apiInterceptors, getFlowById } from '@/client/api';
|
||||||
import MuiLoading from '@/components/common/loading';
|
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 { Divider, Space, Tooltip, message, notification } from 'antd';
|
||||||
import { useSearchParams } from 'next/navigation';
|
import { useSearchParams } from 'next/navigation';
|
||||||
import React, { DragEvent, useCallback, useEffect, useRef, useState } from 'react';
|
import React, { DragEvent, useCallback, useEffect, useRef, useState } from 'react';
|
||||||
@ -16,13 +28,6 @@ import ReactFlow, {
|
|||||||
useNodesState,
|
useNodesState,
|
||||||
useReactFlow,
|
useReactFlow,
|
||||||
} from 'reactflow';
|
} 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';
|
import 'reactflow/dist/style.css';
|
||||||
|
|
||||||
const nodeTypes = { customNode: CanvasNode };
|
const nodeTypes = { customNode: CanvasNode };
|
||||||
@ -30,19 +35,32 @@ const edgeTypes = { buttonedge: ButtonEdge };
|
|||||||
|
|
||||||
const Canvas: React.FC = () => {
|
const Canvas: React.FC = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
const id = searchParams?.get('id') || '';
|
const id = searchParams?.get('id') || '';
|
||||||
const reactFlow = useReactFlow();
|
const reactFlow = useReactFlow();
|
||||||
|
const [messageApi, contextHolder] = message.useMessage();
|
||||||
|
const reactFlowWrapper = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
const [nodes, setNodes, onNodesChange] = useNodesState([]);
|
const [nodes, setNodes, onNodesChange] = useNodesState([]);
|
||||||
const [edges, setEdges, onEdgesChange] = useEdgesState([]);
|
const [edges, setEdges, onEdgesChange] = useEdgesState([]);
|
||||||
const reactFlowWrapper = useRef<HTMLDivElement>(null);
|
|
||||||
const [flowInfo, setFlowInfo] = useState<IFlowUpdateParam>();
|
const [flowInfo, setFlowInfo] = useState<IFlowUpdateParam>();
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
const [isSaveFlowModalOpen, setIsSaveFlowModalOpen] = useState(false);
|
const [isSaveFlowModalOpen, setIsSaveFlowModalOpen] = useState(false);
|
||||||
const [isExportFlowModalOpen, setIsExportFlowModalOpen] = useState(false);
|
const [isExportFlowModalOpen, setIsExportFlowModalOpen] = useState(false);
|
||||||
const [isImportModalOpen, setIsImportFlowModalOpen] = 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() {
|
async function getFlowData() {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
@ -152,22 +170,24 @@ const Canvas: React.FC = () => {
|
|||||||
function onSave() {
|
function onSave() {
|
||||||
const flowData = reactFlow.toObject() as IFlowData;
|
const flowData = reactFlow.toObject() as IFlowData;
|
||||||
const [check, node, message] = checkFlowDataRequied(flowData);
|
const [check, node, message] = checkFlowDataRequied(flowData);
|
||||||
|
|
||||||
|
if (!node) {
|
||||||
|
messageApi.open({
|
||||||
|
type: 'warning',
|
||||||
|
content: t('Please_Add_Nodes_First'),
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!check && message) {
|
if (!check && message) {
|
||||||
setNodes(nds =>
|
setNodes(nds =>
|
||||||
nds.map(item => {
|
nds.map(item => ({
|
||||||
if (item.id === node?.id) {
|
...item,
|
||||||
item.data = {
|
data: {
|
||||||
...item.data,
|
...item.data,
|
||||||
invalid: true,
|
invalid: item.id === node?.id,
|
||||||
};
|
},
|
||||||
} else {
|
})),
|
||||||
item.data = {
|
|
||||||
...item.data,
|
|
||||||
invalid: false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return item;
|
|
||||||
}),
|
|
||||||
);
|
);
|
||||||
return notification.error({
|
return notification.error({
|
||||||
message: 'Error',
|
message: 'Error',
|
||||||
@ -178,19 +198,15 @@ const Canvas: React.FC = () => {
|
|||||||
setIsSaveFlowModalOpen(true);
|
setIsSaveFlowModalOpen(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
function onExport() {
|
|
||||||
setIsExportFlowModalOpen(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
function onImport() {
|
|
||||||
setIsImportFlowModalOpen(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
const getButtonList = () => {
|
const getButtonList = () => {
|
||||||
const buttonList = [
|
const buttonList = [
|
||||||
|
{
|
||||||
|
title: t('template'),
|
||||||
|
icon: <FileAddOutlined className='block text-xl' onClick={() => setIsFlowTemplateModalOpen(true)} />,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: t('Import'),
|
title: t('Import'),
|
||||||
icon: <ImportOutlined className='block text-xl' onClick={onImport} />,
|
icon: <ImportOutlined className='block text-xl' onClick={() => setIsImportFlowModalOpen(true)} />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: t('save'),
|
title: t('save'),
|
||||||
@ -201,7 +217,7 @@ const Canvas: React.FC = () => {
|
|||||||
if (id !== '') {
|
if (id !== '') {
|
||||||
buttonList.unshift({
|
buttonList.unshift({
|
||||||
title: t('Export'),
|
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']}
|
deleteKeyCode={['Backspace', 'Delete']}
|
||||||
>
|
>
|
||||||
<Controls className='flex flex-row items-center' position='bottom-center' />
|
<Controls className='flex flex-row items-center' position='bottom-center' />
|
||||||
|
|
||||||
<Background color='#aaa' gap={16} />
|
<Background color='#aaa' gap={16} />
|
||||||
{/* <AddNodes /> */}
|
|
||||||
|
<AddFlowVariableModal flowInfo={flowInfo} setFlowInfo={setFlowInfo} />
|
||||||
</ReactFlow>
|
</ReactFlow>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -274,6 +292,13 @@ const Canvas: React.FC = () => {
|
|||||||
isImportModalOpen={isImportModalOpen}
|
isImportModalOpen={isImportModalOpen}
|
||||||
setIsImportFlowModalOpen={setIsImportFlowModalOpen}
|
setIsImportFlowModalOpen={setIsImportFlowModalOpen}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<FlowTemplateModal
|
||||||
|
isFlowTemplateModalOpen={isFlowTemplateModalOpen}
|
||||||
|
setIsFlowTemplateModalOpen={setIsFlowTemplateModalOpen}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{contextHolder}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -126,7 +126,6 @@ function Flow() {
|
|||||||
copyFlowTemp.current = flow;
|
copyFlowTemp.current = flow;
|
||||||
form.setFieldValue('label', `${flow.label} Copy`);
|
form.setFieldValue('label', `${flow.label} Copy`);
|
||||||
form.setFieldValue('name', `${flow.name}_copy`);
|
form.setFieldValue('name', `${flow.name}_copy`);
|
||||||
setDeploy(true);
|
|
||||||
setEditable(true);
|
setEditable(true);
|
||||||
setShowModal(true);
|
setShowModal(true);
|
||||||
};
|
};
|
||||||
@ -256,8 +255,10 @@ function Flow() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Spin>
|
</Spin>
|
||||||
|
|
||||||
<Modal
|
<Modal
|
||||||
open={showModal}
|
open={showModal}
|
||||||
|
destroyOnClose
|
||||||
title='Copy AWEL Flow'
|
title='Copy AWEL Flow'
|
||||||
onCancel={() => {
|
onCancel={() => {
|
||||||
setShowModal(false);
|
setShowModal(false);
|
||||||
|
@ -6,7 +6,7 @@ import { ClearOutlined, LoadingOutlined, PauseCircleOutlined, RedoOutlined, Send
|
|||||||
import { EventStreamContentType, fetchEventSource } from '@microsoft/fetch-event-source';
|
import { EventStreamContentType, fetchEventSource } from '@microsoft/fetch-event-source';
|
||||||
import { useRequest } from 'ahooks';
|
import { useRequest } from 'ahooks';
|
||||||
import { Button, Input, Popover, Spin, Tag } from 'antd';
|
import { Button, Input, Popover, Spin, Tag } from 'antd';
|
||||||
import cls from 'classnames';
|
import classnames from 'classnames';
|
||||||
import { useSearchParams } from 'next/navigation';
|
import { useSearchParams } from 'next/navigation';
|
||||||
import React, { useContext, useEffect, useMemo, useState } from 'react';
|
import React, { useContext, useEffect, useMemo, useState } from 'react';
|
||||||
import { MobileChatContext } from '../';
|
import { MobileChatContext } from '../';
|
||||||
@ -245,7 +245,7 @@ const InputContainer: React.FC = () => {
|
|||||||
<div className='flex items-center justify-between text-lg font-bold'>
|
<div className='flex items-center justify-between text-lg font-bold'>
|
||||||
<Popover content='暂停回复' trigger={['hover']}>
|
<Popover content='暂停回复' trigger={['hover']}>
|
||||||
<PauseCircleOutlined
|
<PauseCircleOutlined
|
||||||
className={cls('p-2 cursor-pointer', {
|
className={classnames('p-2 cursor-pointer', {
|
||||||
'text-[#0c75fc]': canAbort,
|
'text-[#0c75fc]': canAbort,
|
||||||
'text-gray-400': !canAbort,
|
'text-gray-400': !canAbort,
|
||||||
})}
|
})}
|
||||||
@ -254,7 +254,7 @@ const InputContainer: React.FC = () => {
|
|||||||
</Popover>
|
</Popover>
|
||||||
<Popover content='再来一次' trigger={['hover']}>
|
<Popover content='再来一次' trigger={['hover']}>
|
||||||
<RedoOutlined
|
<RedoOutlined
|
||||||
className={cls('p-2 cursor-pointer', {
|
className={classnames('p-2 cursor-pointer', {
|
||||||
'text-gray-400': !history.length || !canNewChat,
|
'text-gray-400': !history.length || !canNewChat,
|
||||||
})}
|
})}
|
||||||
onClick={redo}
|
onClick={redo}
|
||||||
@ -265,7 +265,7 @@ const InputContainer: React.FC = () => {
|
|||||||
) : (
|
) : (
|
||||||
<Popover content='清除历史' trigger={['hover']}>
|
<Popover content='清除历史' trigger={['hover']}>
|
||||||
<ClearOutlined
|
<ClearOutlined
|
||||||
className={cls('p-2 cursor-pointer', {
|
className={classnames('p-2 cursor-pointer', {
|
||||||
'text-gray-400': !history.length || !canNewChat,
|
'text-gray-400': !history.length || !canNewChat,
|
||||||
})}
|
})}
|
||||||
onClick={clearHistory}
|
onClick={clearHistory}
|
||||||
@ -276,7 +276,7 @@ const InputContainer: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
{/* 输入框 */}
|
{/* 输入框 */}
|
||||||
<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',
|
'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,
|
'border-[#0c75fc] dark:border-[rgba(12,117,252,0.8)]': isFocus,
|
||||||
@ -323,7 +323,7 @@ const InputContainer: React.FC = () => {
|
|||||||
|
|
||||||
<Button
|
<Button
|
||||||
type='primary'
|
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,
|
'opacity-40 cursor-not-allowed': !userInput.trim() || !canNewChat,
|
||||||
})}
|
})}
|
||||||
onClick={onSubmit}
|
onClick={onSubmit}
|
||||||
|
@ -122,4 +122,4 @@ table {
|
|||||||
|
|
||||||
.rc-md-editor .editor-container>.section {
|
.rc-md-editor .editor-container>.section {
|
||||||
border-right: none !important;
|
border-right: none !important;
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "es5",
|
"target": "es6",
|
||||||
"lib": ["dom", "dom.iterable", "esnext"],
|
"lib": ["dom", "dom.iterable", "esnext"],
|
||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
|
@ -12,6 +12,7 @@ export type IFlowUpdateParam = {
|
|||||||
uid?: string;
|
uid?: string;
|
||||||
flow_data?: IFlowData;
|
flow_data?: IFlowData;
|
||||||
state?: FlowState;
|
state?: FlowState;
|
||||||
|
variables?: IVariableItem[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type IFlowRefreshParams = {
|
export type IFlowRefreshParams = {
|
||||||
@ -169,8 +170,9 @@ export type IFlowDataViewport = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type IFlowData = {
|
export type IFlowData = {
|
||||||
nodes: Array<IFlowDataNode>;
|
nodes: IFlowDataNode[];
|
||||||
edges: Array<IFlowDataEdge>;
|
edges: IFlowDataEdge[];
|
||||||
|
variables?: IVariableItem[];
|
||||||
viewport: IFlowDataViewport;
|
viewport: IFlowDataViewport;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -200,3 +202,54 @@ export type IUploadFileResponse = {
|
|||||||
bucket: string;
|
bucket: string;
|
||||||
uri?: 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';
|
import { Node } from 'reactflow';
|
||||||
|
|
||||||
export const getUniqueNodeId = (nodeData: IFlowNode, nodes: Node[]) => {
|
export const getUniqueNodeId = (nodeData: IFlowNode, nodes: Node[]) => {
|
||||||
@ -140,3 +140,57 @@ export const convertKeysToCamelCase = (obj: Record<string, any>): Record<string,
|
|||||||
|
|
||||||
return convert(obj);
|
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