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

View File

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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