feat(web): AWEL flow 2.0 frontend codes (#1898)

Co-authored-by: Fangyin Cheng <staneyffer@gmail.com>
Co-authored-by: 谨欣 <echo.cmy@antgroup.com>
Co-authored-by: 严志勇 <yanzhiyong@tiansuixiansheng.com>
Co-authored-by: yanzhiyong <932374019@qq.com>
This commit is contained in:
Dreammy23
2024-08-28 12:39:13 +08:00
committed by GitHub
parent 9502251c08
commit 131bc7b89b
60 changed files with 2334 additions and 2243 deletions

View File

@@ -174,7 +174,7 @@ function Database() {
onDelete(item);
}}
>
{t('Delete_Btn')}
</span>
),
},

View File

@@ -1,20 +1,48 @@
import { addFlow, apiInterceptors, getFlowById, updateFlowById } from '@/client/api';
import { apiInterceptors, getFlowById } from '@/client/api';
import MuiLoading from '@/components/common/loading';
import AddNodes from '@/components/flow/add-nodes';
import AddNodesSider from '@/components/flow/add-nodes-sider';
import ButtonEdge from '@/components/flow/button-edge';
import CanvasNode from '@/components/flow/canvas-node';
import { IFlowData, IFlowUpdateParam } from '@/types/flow';
import { checkFlowDataRequied, getUniqueNodeId, mapHumpToUnderline, mapUnderlineToHump } from '@/utils/flow';
import { FrownOutlined, SaveOutlined } from '@ant-design/icons';
import { App, Button, Checkbox, Divider, Form, Input, Modal, Space, message, notification } from 'antd';
import {
checkFlowDataRequied,
getUniqueNodeId,
mapUnderlineToHump,
} from '@/utils/flow';
import {
ExportOutlined,
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';
import React, {
DragEvent,
useCallback,
useEffect,
useRef,
useState,
} from 'react';
import { useTranslation } from 'react-i18next';
import ReactFlow, { Background, Connection, Controls, ReactFlowProvider, addEdge, useEdgesState, useNodesState, useReactFlow, Node } from 'reactflow';
import ReactFlow, {
Background,
Connection,
Controls,
ReactFlowProvider,
addEdge,
useEdgesState,
useNodesState,
useReactFlow,
Node,
} from 'reactflow';
import 'reactflow/dist/style.css';
import { useRouter } from 'next/router';
const { TextArea } = Input;
import {
SaveFlowModal,
ExportFlowModal,
ImportFlowModal,
} from '@/components/flow/canvas-modal';
interface Props {
// Define your component props here
@@ -24,9 +52,7 @@ const edgeTypes = { buttonedge: ButtonEdge };
const Canvas: React.FC<Props> = () => {
const { t } = useTranslation();
const { message } = App.useApp();
const { replace } = useRouter();
const [form] = Form.useForm();
const searchParams = useSearchParams();
const id = searchParams?.get('id') || '';
const reactFlow = useReactFlow();
@@ -35,8 +61,10 @@ const Canvas: React.FC<Props> = () => {
const [nodes, setNodes, onNodesChange] = useNodesState([]);
const [edges, setEdges, onEdgesChange] = useEdgesState([]);
const reactFlowWrapper = useRef<HTMLDivElement>(null);
const [isModalVisible, setIsModalVisible] = useState(false);
const [flowInfo, setFlowInfo] = useState<IFlowUpdateParam>();
const [isSaveFlowModalOpen, setIsSaveFlowModalOpen] = useState(false);
const [isExportFlowModalOpen, setIsExportFlowModalOpen] = useState(false);
const [isImportModalOpen, setIsImportFlowModalOpen] = useState(false);
async function getFlowData() {
setLoading(true);
@@ -81,7 +109,7 @@ const Canvas: React.FC<Props> = () => {
};
}
return node;
}),
})
);
}
@@ -98,13 +126,18 @@ const Canvas: React.FC<Props> = () => {
(event: DragEvent) => {
event.preventDefault();
const reactFlowBounds = reactFlowWrapper.current!.getBoundingClientRect();
const sidebarWidth = (
document.getElementsByClassName('ant-layout-sider')?.[0] as HTMLElement
)?.offsetWidth; // get sidebar width
let nodeStr = event.dataTransfer.getData('application/reactflow');
if (!nodeStr || typeof nodeStr === 'undefined') {
return;
}
const nodeData = JSON.parse(nodeStr);
const position = reactFlow.screenToFlowPosition({
x: event.clientX - reactFlowBounds.left,
x: event.clientX - reactFlowBounds.left + sidebarWidth,
y: event.clientY - reactFlowBounds.top,
});
const nodeId = getUniqueNodeId(nodeData, reactFlow.getNodes());
@@ -129,10 +162,10 @@ const Canvas: React.FC<Props> = () => {
};
}
return node;
}),
})
);
},
[reactFlow],
[reactFlow]
);
const onDragOver = useCallback((event: DragEvent) => {
@@ -140,18 +173,7 @@ const Canvas: React.FC<Props> = () => {
event.dataTransfer.dropEffect = 'move';
}, []);
function labelChange(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 -.
let result = label
.replace(/\s+/g, '_')
.replace(/[^a-z0-9_-]/g, '')
.toLowerCase();
result = result;
form.setFieldsValue({ name: result });
}
function clickSave() {
function onSave() {
const flowData = reactFlow.toObject() as IFlowData;
const [check, node, message] = checkFlowDataRequied(flowData);
if (!check && message) {
@@ -169,139 +191,116 @@ const Canvas: React.FC<Props> = () => {
};
}
return item;
}),
})
);
return notification.error({ message: 'Error', description: message, icon: <FrownOutlined className="text-red-600" /> });
return notification.error({
message: 'Error',
description: message,
icon: <FrownOutlined className='text-red-600' />,
});
}
setIsModalVisible(true);
setIsSaveFlowModalOpen(true);
}
async function handleSaveFlow() {
const { name, label, description = '', editable = false, deploy = false } = form.getFieldsValue();
const reactFlowObject = mapHumpToUnderline(reactFlow.toObject() as IFlowData);
if (id) {
const [, , res] = await apiInterceptors(updateFlowById(id, {
name,
label,
description,
editable,
uid: id,
flow_data: reactFlowObject,
state: deploy ? 'deployed' : 'developing',
}));
setIsModalVisible(false);
if (res?.success) {
message.success('编辑成功');
replace('/construct/flow');
setIsModalVisible(false);
} else if (res?.err_msg) {
message.error(res?.err_msg);
}
} else {
const [_, res] = await apiInterceptors(addFlow({ name, label, description, editable, flow_data: reactFlowObject, state: deploy ? 'deployed' : 'developing' }));
setIsModalVisible(false);
replace('/construct/flow');
message.success('创建成功');
}
function onExport() {
setIsExportFlowModalOpen(true);
}
function onImport() {
setIsImportFlowModalOpen(true);
}
const getButtonList = () => {
const buttonList = [
{
title: t('Import'),
icon: <ImportOutlined className='block text-xl' onClick={onImport} />,
},
{
title: t('save'),
icon: <SaveOutlined className='block text-xl' onClick={onSave} />,
},
];
if (id !== '') {
buttonList.unshift({
title: t('Export'),
icon: <ExportOutlined className='block text-xl' onClick={onExport} />,
});
}
return buttonList;
};
return (
<>
<MuiLoading visible={loading} />
<div className="my-2 mx-4 flex flex-row justify-end items-center">
<div className="w-8 h-8 rounded-md bg-stone-300 dark:bg-zinc-700 dark:text-zinc-200 flext justify-center items-center hover:text-blue-500 dark:hover:text-zinc-100">
<SaveOutlined className="block text-xl" onClick={clickSave} />
<div className='flex flex-row'>
<AddNodesSider />
<div className='flex flex-col flex-1'>
<Space className='my-2 mx-4 flex flex-row justify-end'>
{getButtonList().map(({ title, icon }) => (
<Tooltip
key={title}
title={title}
className='w-8 h-8 rounded-md bg-stone-300 dark:bg-zinc-700 dark:text-zinc-200 hover:text-blue-500 dark:hover:text-zinc-100'
>
{icon}
</Tooltip>
))}
</Space>
<Divider className='mt-0 mb-0' />
<div className='h-[calc(100vh-48px)] w-full' ref={reactFlowWrapper}>
<ReactFlow
nodes={nodes}
edges={edges}
nodeTypes={nodeTypes}
edgeTypes={edgeTypes}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onNodeClick={onNodesClick}
onConnect={onConnect}
onDrop={onDrop}
onDragOver={onDragOver}
minZoom={0.1}
fitView
deleteKeyCode={['Backspace', 'Delete']}
>
<Controls
className='flex flex-row items-center'
position='bottom-center'
/>
<Background color='#aaa' gap={16} />
{/* <AddNodes /> */}
</ReactFlow>
</div>
</div>
</div>
<Divider className="mt-0 mb-0" />
<div className="h-[calc(100vh-60px)] w-full" ref={reactFlowWrapper}>
<ReactFlow
nodes={nodes}
edges={edges}
nodeTypes={nodeTypes}
edgeTypes={edgeTypes}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onNodeClick={onNodesClick}
onConnect={onConnect}
onDrop={onDrop}
onDragOver={onDragOver}
minZoom={0.1}
fitView
deleteKeyCode={['Backspace', 'Delete']}
>
<Controls className="flex flex-row items-center" position="bottom-center" />
<Background color="#aaa" gap={16} />
<AddNodes />
</ReactFlow>
</div>
<Modal
title={t('flow_modal_title')}
open={isModalVisible}
onCancel={() => {
setIsModalVisible(false);
}}
cancelButtonProps={{ className: 'hidden' }}
okButtonProps={{ className: 'hidden' }}
>
<Form
name="flow_form"
form={form}
labelCol={{ span: 8 }}
wrapperCol={{ span: 16 }}
style={{ maxWidth: 600 }}
initialValues={{ remember: true }}
onFinish={handleSaveFlow}
autoComplete="off"
>
<Form.Item label="Title" name="label" initialValue={flowInfo?.label} rules={[{ required: true, message: 'Please input flow title!' }]}>
<Input onChange={labelChange} />
</Form.Item>
<Form.Item
label="Name"
name="name"
initialValue={flowInfo?.name}
rules={[
{ required: true, message: 'Please input flow name!' },
() => ({
validator(_, value) {
const regex = /^[a-zA-Z0-9_\-]+$/;
if (!regex.test(value)) {
return Promise.reject('Can only contain numbers, letters, underscores, and dashes');
}
return Promise.resolve();
},
}),
]}
>
<Input />
</Form.Item>
<Form.Item label="Description" initialValue={flowInfo?.description} name="description">
<TextArea rows={3} />
</Form.Item>
<Form.Item label="Editable" name="editable" initialValue={id ? flowInfo?.editable : true} valuePropName="checked">
<Checkbox></Checkbox>
</Form.Item>
<Form.Item label="Deploy" name="deploy" initialValue={id ? (flowInfo?.state === 'deployed' || flowInfo?.state === 'running') : true} valuePropName="checked">
<Checkbox></Checkbox>
</Form.Item>
<Form.Item wrapperCol={{ offset: 8, span: 16 }}>
<Space>
<Button
htmlType="button"
onClick={() => {
setIsModalVisible(false);
}}
>
Cancel
</Button>
<Button type="primary" htmlType="submit">
Submit
</Button>
</Space>
</Form.Item>
</Form>
</Modal>
<MuiLoading visible={loading} />
<SaveFlowModal
reactFlow={reactFlow}
flowInfo={flowInfo}
isSaveFlowModalOpen={isSaveFlowModalOpen}
setIsSaveFlowModalOpen={setIsSaveFlowModalOpen}
/>
<ExportFlowModal
reactFlow={reactFlow}
flowInfo={flowInfo}
isExportFlowModalOpen={isExportFlowModalOpen}
setIsExportFlowModalOpen={setIsExportFlowModalOpen}
/>
<ImportFlowModal
setNodes={setNodes}
setEdges={setEdges}
isImportModalOpen={isImportModalOpen}
setIsImportFlowModalOpen={setIsImportFlowModalOpen}
/>
</>
);
};

View File

@@ -1,12 +1,31 @@
import BlurredCard, { ChatButton, InnerDropdown } from '@/new-components/common/blurredCard';
import BlurredCard, {
ChatButton,
InnerDropdown,
} from '@/new-components/common/blurredCard';
import ConstructLayout from '@/new-components/layout/Construct';
import { ChatContext } from '@/app/chat-context';
import { apiInterceptors, deleteFlowById, getFlows, newDialogue, updateFlowAdmins, addFlow } from '@/client/api';
import {
apiInterceptors,
deleteFlowById,
getFlows,
newDialogue,
addFlow,
} from '@/client/api';
import MyEmpty from '@/components/common/MyEmpty';
import { IFlow, IFlowUpdateParam } from '@/types/flow';
import { PlusOutlined } from '@ant-design/icons';
import { useRequest } from 'ahooks';
import { Button, Modal, Popconfirm, Select, Spin, Tag, message, Form, Input, Checkbox } from 'antd';
import {
Button,
Modal,
Popconfirm,
Spin,
Tag,
message,
Form,
Input,
Checkbox,
} from 'antd';
import { t } from 'i18next';
import { concat, debounce } from 'lodash';
import moment from 'moment';
@@ -19,18 +38,14 @@ function Flow() {
const router = useRouter();
const { model } = useContext(ChatContext);
const [messageApi, contextHolder] = message.useMessage();
const [form] = Form.useForm<Pick<IFlow, 'label' | 'name'>>();
const [flowList, setFlowList] = useState<Array<IFlow>>([]);
const [adminOpen, setAdminOpen] = useState<boolean>(false);
const [curFlow, setCurFLow] = useState<IFlow>();
const [admins, setAdmins] = useState<string[]>([]);
const copyFlowTemp = useRef<IFlow>();
const [showModal, setShowModal] = useState(false);
const [deploy, setDeploy] = useState(false);
const [editable, setEditable] = useState(false);
const [form] = Form.useForm<Pick<IFlow, 'label' | 'name'>>();
// 分页信息
const totalRef = useRef<{
current_page: number;
@@ -41,18 +56,14 @@ function Flow() {
const scrollRef = useRef<HTMLDivElement>(null);
// 获取列表
const {
run: getFlowListRun,
loading,
refresh: refreshFlowList,
} = useRequest(
const { run: getFlowListRun, loading } = useRequest(
async (params: any) =>
await apiInterceptors(
getFlows({
page: 1,
page_size: 12,
...params,
}),
})
),
{
cacheKey: 'query-flow-list',
@@ -66,7 +77,7 @@ function Flow() {
};
},
throttleWait: 300,
},
}
);
const { i18n } = useTranslation();
@@ -112,7 +123,9 @@ function Flow() {
}, [loading, handleScroll, loadMoreData]);
const handleChat = async (flow: IFlow) => {
const [, res] = await apiInterceptors(newDialogue({ chat_mode: 'chat_agent' }));
const [, res] = await apiInterceptors(
newDialogue({ chat_mode: 'chat_agent' })
);
if (res) {
const queryStr = qs.stringify({
scene: 'chat_flow',
@@ -131,30 +144,6 @@ function Flow() {
}
}
useEffect(() => {
if (curFlow?.admins?.length) {
setAdmins(curFlow?.admins);
} else {
setAdmins([]);
}
}, [curFlow]);
// 更新管理员
const { run: updateAdmins, loading: adminLoading } = useRequest(
async (value: string[]) => await apiInterceptors(updateFlowAdmins({ uid: curFlow?.uid || '', admins: value })),
{
manual: true,
onSuccess: (data) => {
const [error] = data;
if (!error) {
message.success('更新成功');
} else {
message.error('更新失败');
}
},
},
);
const handleCopy = (flow: IFlow) => {
copyFlowTemp.current = flow;
form.setFieldValue('label', `${flow.label} Copy`);
@@ -166,7 +155,8 @@ function Flow() {
const onFinish = async (val: { name: string; label: string }) => {
if (!copyFlowTemp.current) return;
const { source, uid, dag_id, gmt_created, gmt_modified, state, ...params } = copyFlowTemp.current;
const { source, uid, dag_id, gmt_created, gmt_modified, state, ...params } =
copyFlowTemp.current;
const data: IFlowUpdateParam = {
...params,
editable,
@@ -181,18 +171,15 @@ function Flow() {
}
};
const handleChange = async (value: string[]) => {
setAdmins(value);
await updateAdmins(value);
await refreshFlowList();
};
return (
<ConstructLayout>
<Spin spinning={loading}>
<div className="relative h-screen w-full p-4 md:p-6 overflow-y-auto" ref={scrollRef}>
<div className="flex justify-between items-center mb-6">
<div className="flex items-center gap-4">
<div
className='relative h-screen w-full p-4 md:p-6 overflow-y-auto'
ref={scrollRef}
>
<div className='flex justify-between items-center mb-6'>
<div className='flex items-center gap-4'>
{/* <Input
variant="filled"
prefix={<SearchOutlined />}
@@ -204,9 +191,9 @@ function Flow() {
/> */}
</div>
<div className="flex items-center gap-4">
<div className='flex items-center gap-4'>
<Button
className="border-none text-white bg-button-gradient"
className='border-none text-white bg-button-gradient'
icon={<PlusOutlined />}
onClick={() => {
router.push('/construct/flow/canvas');
@@ -216,13 +203,13 @@ function Flow() {
</Button>
</div>
</div>
<div className="flex flex-wrap mx-[-8px] pb-12 justify-start items-stretch">
<div className='flex flex-wrap mx-[-8px] pb-12 justify-start items-stretch'>
{flowList.map((flow) => (
<BlurredCard
description={flow.description}
name={flow.name}
key={flow.uid}
logo="/pictures/flow.png"
logo='/pictures/flow.png'
onClick={() => {
router.push('/construct/flow/canvas?id=' + flow.uid);
}}
@@ -230,19 +217,6 @@ function Flow() {
<InnerDropdown
menu={{
items: [
// {
// key: 'edit',
// label: (
// <span
// onClick={() => {
// setAdminOpen(true);
// setCurFLow(flow);
// }}
// >
// 权限管理
// </span>
// ),
// },
{
key: 'copy',
label: (
@@ -251,15 +225,20 @@ function Flow() {
handleCopy(flow);
}}
>
{t('copy')}
{t('Copy_Btn')}
</span>
),
},
{
key: 'del',
label: (
<Popconfirm title="Are you sure to delete this flow?" onConfirm={() => deleteFlow(flow)}>
<span className="text-red-400"></span>
<Popconfirm
title='Are you sure to delete this flow?'
onConfirm={() => deleteFlow(flow)}
>
<span className='text-red-400'>
{t('Delete_Btn')}
</span>
</Popconfirm>
),
},
@@ -270,16 +249,36 @@ function Flow() {
rightTopHover={false}
Tags={
<div>
<Tag color={flow.source === 'DBGPT-WEB' ? 'green' : 'blue'}>{flow.source}</Tag>
<Tag color={flow.editable ? 'green' : 'gray'}>{flow.editable ? 'Editable' : 'Can not Edit'}</Tag>
<Tag color={flow.state === 'load_failed' ? 'red' : flow.state === 'running' ? 'green' : 'blue'}>{flow.state}</Tag>
<Tag color={flow.source === 'DBGPT-WEB' ? 'green' : 'blue'}>
{flow.source}
</Tag>
<Tag color={flow.editable ? 'green' : 'gray'}>
{flow.editable ? 'Editable' : 'Can not Edit'}
</Tag>
<Tag
color={
flow.state === 'load_failed'
? 'red'
: flow.state === 'running'
? 'green'
: 'blue'
}
>
{flow.state}
</Tag>
</div>
}
LeftBottom={
<div key={i18n.language + 'flow'} className="flex gap-2">
<div key={i18n.language + 'flow'} className='flex gap-2'>
<span>{flow?.nick_name}</span>
<span></span>
{flow?.gmt_modified && <span>{moment(flow?.gmt_modified).fromNow() + ' ' + t('update')}</span>}
{flow?.gmt_modified && (
<span>
{moment(flow?.gmt_modified).fromNow() +
' ' +
t('update')}
</span>
)}
</div>
}
RightBottom={
@@ -292,40 +291,26 @@ function Flow() {
}
/>
))}
{flowList.length === 0 && <MyEmpty description="No flow found" />}
{flowList.length === 0 && <MyEmpty description='No flow found' />}
</div>
</div>
</Spin>
<Modal title="权限管理" open={adminOpen} onCancel={() => setAdminOpen(false)} footer={null}>
<div className="py-4">
<div className="mb-1">0</div>
<Select
mode="tags"
value={admins}
style={{ width: '100%' }}
onChange={handleChange}
tokenSeparators={[',']}
options={admins?.map((item: string) => ({ label: item, value: item }))}
loading={adminLoading}
/>
</div>
</Modal>
<Modal
open={showModal}
title="Copy AWEL Flow"
title='Copy AWEL Flow'
onCancel={() => {
setShowModal(false);
}}
footer={false}
>
<Form form={form} onFinish={onFinish} className="mt-6">
<Form.Item name="name" label="Name" rules={[{ required: true }]}>
<Form form={form} onFinish={onFinish} className='mt-6'>
<Form.Item name='name' label='Name' rules={[{ required: true }]}>
<Input />
</Form.Item>
<Form.Item name="label" label="Label" rules={[{ required: true }]}>
<Form.Item name='label' label='Label' rules={[{ required: true }]}>
<Input />
</Form.Item>
<Form.Item label="editable">
<Form.Item label='editable'>
<Checkbox
value={editable}
checked={editable}
@@ -335,7 +320,7 @@ function Flow() {
}}
/>
</Form.Item>
<Form.Item label="deploy">
<Form.Item label='deploy'>
<Checkbox
value={deploy}
checked={deploy}
@@ -345,8 +330,8 @@ function Flow() {
}}
/>
</Form.Item>
<div className="flex justify-end">
<Button type="primary" htmlType="submit">
<div className='flex justify-end'>
<Button type='primary' htmlType='submit'>
{t('Submit')}
</Button>
</div>