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:
Dreammy23 2024-09-10 09:39:40 +08:00 committed by GitHub
parent fe29f977f3
commit 746e4fda37
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
23 changed files with 767 additions and 156 deletions

View File

@ -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",
},
}

View File

@ -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}`);
};
// TODOwait 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);
};

View File

@ -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 />

View 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>
</>
);
};

View File

@ -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>

View 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>
</>
);
};

View File

@ -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>

View File

@ -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';

View File

@ -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>

View File

@ -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',

View File

@ -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'

View File

@ -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>
);
};

View File

@ -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',
};

View File

@ -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: '操作',
};

View File

@ -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>

View File

@ -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 => {

View File

@ -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}
</>
);
};

View File

@ -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);

View File

@ -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}

View File

@ -122,4 +122,4 @@ table {
.rc-md-editor .editor-container>.section {
border-right: none !important;
}
}

View File

@ -1,6 +1,6 @@
{
"compilerOptions": {
"target": "es5",
"target": "es6",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,

View File

@ -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;
};

View File

@ -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}}`;
}