浏览代码

feat: Add RunDrawer #3355 (#3434)

### What problem does this PR solve?

feat: Translation test run form #3355
feat: Wrap QueryTable with Collapse #3355
feat: If the required fields are not filled in, the submit button will
be grayed out. #3355
feat: Add RunDrawer #3355

### Type of change


- [x] New Feature (non-breaking change which adds functionality)
tags/v0.14.0
balibabu 11 个月前
父节点
当前提交
e0659a4f0e
没有帐户链接到提交者的电子邮件

+ 26
- 1
web/src/hooks/document-hooks.ts 查看文件

@@ -4,8 +4,9 @@ import { IChangeParserConfigRequestBody } from '@/interfaces/request/document';
import i18n from '@/locales/config';
import chatService from '@/services/chat-service';
import kbService from '@/services/knowledge-service';
import { api_host } from '@/utils/api';
import api, { api_host } from '@/utils/api';
import { buildChunkHighlights } from '@/utils/document-util';
import { post } from '@/utils/request';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { UploadFile, message } from 'antd';
import { get } from 'lodash';
@@ -442,3 +443,27 @@ export const useUploadAndParseDocument = (uploadMethod: string) => {

return { data, loading, uploadAndParseDocument: mutateAsync };
};

export const useParseDocument = () => {
const {
data,
isPending: loading,
mutateAsync,
} = useMutation({
mutationKey: ['parseDocument'],
mutationFn: async (url: string) => {
try {
const data = await post(api.parse, { url });
if (data?.code === 0) {
message.success(i18n.t('message.uploaded'));
}
return data;
} catch (error) {
console.log('🚀 ~ mutationFn: ~ error:', error);
message.error('error');
}
},
});

return { parseDocument: mutateAsync, data, loading };
};

+ 19
- 1
web/src/hooks/login-hooks.ts 查看文件

@@ -2,7 +2,9 @@ import { Authorization } from '@/constants/authorization';
import userService from '@/services/user-service';
import authorizationUtil from '@/utils/authorization-util';
import { useMutation } from '@tanstack/react-query';
import { message } from 'antd';
import { Form, message } from 'antd';
import { FormInstance } from 'antd/lib';
import { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { history } from 'umi';

@@ -95,3 +97,19 @@ export const useLogout = () => {

return { data, loading, logout: mutateAsync };
};

export const useHandleSubmittable = (form: FormInstance) => {
const [submittable, setSubmittable] = useState<boolean>(false);

// Watch all values
const values = Form.useWatch([], form);

useEffect(() => {
form
.validateFields({ validateOnly: true })
.then(() => setSubmittable(true))
.catch(() => setSubmittable(false));
}, [form, values]);

return { submittable };
};

+ 5
- 1
web/src/locales/en.ts 查看文件

@@ -32,6 +32,7 @@ export default {
s: 'S',
pleaseSelect: 'Please select',
pleaseInput: 'Please input',
submit: 'Submit',
},
login: {
login: 'Sign in',
@@ -176,7 +177,7 @@ export default {
chunkTokenNumber: 'Chunk token number',
chunkTokenNumberMessage: 'Chunk token number is required',
embeddingModelTip:
"The model that converts chunks into embeddings. It cannot be changed once the knowledge base has chunks. To switch to a different embedding model, You must delete all chunks in the knowledge base.",
'The model that converts chunks into embeddings. It cannot be changed once the knowledge base has chunks. To switch to a different embedding model, You must delete all chunks in the knowledge base.',
permissionsTip:
"If set to 'Team', all team members will be able to manage the knowledge base.",
chunkTokenNumberTip:
@@ -1025,6 +1026,9 @@ The above is the content you need to summarize.`,
content: 'Content',
operationResults: 'Operation Results',
autosaved: 'Autosaved',
optional: 'Optional',
pasteFileLink: 'Paste file link',
testRun: 'Test Run',
},
footer: {
profile: 'All rights reserved @ React',

+ 4
- 0
web/src/locales/zh-traditional.ts 查看文件

@@ -32,6 +32,7 @@ export default {
s: '秒',
pleaseSelect: '請選擇',
pleaseInput: '請輸入',
submit: '提交',
},
login: {
login: '登入',
@@ -985,6 +986,9 @@ export default {
content: '內容',
operationResults: '運行結果',
autosaved: '已自動儲存',
optional: '可選項',
pasteFileLink: '貼上文件連結',
testRun: '試運行',
},
footer: {
profile: '“保留所有權利 @ react”',

+ 4
- 0
web/src/locales/zh.ts 查看文件

@@ -32,6 +32,7 @@ export default {
s: '秒',
pleaseSelect: '请选择',
pleaseInput: '请输入',
submit: '提交',
},
login: {
login: '登录',
@@ -1005,6 +1006,9 @@ export default {
content: '内容',
operationResults: '运行结果',
autosaved: '已自动保存',
optional: '可选项',
pasteFileLink: '粘贴文件链接',
testRun: '试运行',
},
footer: {
profile: 'All rights reserved @ React',

+ 76
- 24
web/src/pages/flow/canvas/index.tsx 查看文件

@@ -1,4 +1,5 @@
import { useCallback } from 'react';
import { useSetModalState } from '@/hooks/common-hooks';
import { useCallback, useEffect } from 'react';
import ReactFlow, {
Background,
ConnectionMode,
@@ -8,14 +9,17 @@ import ReactFlow, {
import 'reactflow/dist/style.css';
import ChatDrawer from '../chat/drawer';
import { Operator } from '../constant';
import FlowDrawer from '../flow-drawer';
import FormDrawer from '../flow-drawer';
import {
useGetBeginNodeDataQuery,
useHandleDrop,
useSelectCanvasData,
useShowDrawer,
useShowFormDrawer,
useValidateConnection,
useWatchNodeFormDataChange,
} from '../hooks';
import { BeginQuery } from '../interface';
import RunDrawer from '../run-drawer';
import { ButtonEdge } from './edge';
import styles from './index.less';
import { RagNode } from './node';
@@ -53,11 +57,11 @@ const edgeTypes = {
};

interface IProps {
chatDrawerVisible: boolean;
hideChatDrawer(): void;
drawerVisible: boolean;
hideDrawer(): void;
}

function FlowCanvas({ chatDrawerVisible, hideChatDrawer }: IProps) {
function FlowCanvas({ drawerVisible, hideDrawer }: IProps) {
const {
nodes,
edges,
@@ -67,26 +71,65 @@ function FlowCanvas({ chatDrawerVisible, hideChatDrawer }: IProps) {
onSelectionChange,
} = useSelectCanvasData();
const isValidConnection = useValidateConnection();
const {
visible: runVisible,
showModal: showRunModal,
hideModal: hideRunModal,
} = useSetModalState();
const {
visible: chatVisible,
showModal: showChatModal,
hideModal: hideChatModal,
} = useSetModalState();

const { formDrawerVisible, hideFormDrawer, showFormDrawer, clickedNode } =
useShowFormDrawer();

const onPaneClick = useCallback(() => {
hideFormDrawer();
}, [hideFormDrawer]);

const { drawerVisible, hideDrawer, showDrawer, clickedNode } =
useShowDrawer();
const { onDrop, onDragOver, setReactFlowInstance } = useHandleDrop();

useWatchNodeFormDataChange();

const hideRunOrChatDrawer = useCallback(() => {
hideChatModal();
hideRunModal();
hideDrawer();
}, [hideChatModal, hideDrawer, hideRunModal]);

const onNodeClick: NodeMouseHandler = useCallback(
(e, node) => {
if (node.data.label !== Operator.Note) {
showDrawer(node);
hideRunOrChatDrawer();
showFormDrawer(node);
}
},
[showDrawer],
[hideRunOrChatDrawer, showFormDrawer],
);

const onPaneClick = useCallback(() => {
hideDrawer();
}, [hideDrawer]);
const getBeginNodeDataQuery = useGetBeginNodeDataQuery();

const { onDrop, onDragOver, setReactFlowInstance } = useHandleDrop();

useWatchNodeFormDataChange();
useEffect(() => {
if (drawerVisible) {
const query: BeginQuery[] = getBeginNodeDataQuery();
if (query.length > 0) {
showRunModal();
hideChatModal();
} else {
showChatModal();
hideRunModal();
}
}
}, [
hideChatModal,
hideRunModal,
showChatModal,
showRunModal,
drawerVisible,
getBeginNodeDataQuery,
]);

return (
<div className={styles.canvasWrapper}>
@@ -147,17 +190,26 @@ function FlowCanvas({ chatDrawerVisible, hideChatDrawer }: IProps) {
<Background />
<Controls />
</ReactFlow>
<FlowDrawer
node={clickedNode}
visible={drawerVisible}
hideModal={hideDrawer}
></FlowDrawer>
{chatDrawerVisible && (
{formDrawerVisible && (
<FormDrawer
node={clickedNode}
visible={formDrawerVisible}
hideModal={hideFormDrawer}
></FormDrawer>
)}
{chatVisible && (
<ChatDrawer
visible={chatDrawerVisible}
hideModal={hideChatDrawer}
visible={chatVisible}
hideModal={hideRunOrChatDrawer}
></ChatDrawer>
)}

{runVisible && (
<RunDrawer
hideModal={hideRunOrChatDrawer}
showModal={showChatModal}
></RunDrawer>
)}
</div>
);
}

+ 28
- 6
web/src/pages/flow/canvas/node/begin-node.tsx 查看文件

@@ -1,9 +1,15 @@
import { Flex } from 'antd';
import classNames from 'classnames';
import get from 'lodash/get';
import { useTranslation } from 'react-i18next';
import { Handle, NodeProps, Position } from 'reactflow';
import { Operator, operatorMap } from '../../constant';
import { NodeData } from '../../interface';
import {
BeginQueryType,
BeginQueryTypeIconMap,
Operator,
operatorMap,
} from '../../constant';
import { BeginQuery, NodeData } from '../../interface';
import OperatorIcon from '../../operator-icon';
import { RightHandleStyle } from './handle-icon';
import styles from './index.less';
@@ -11,15 +17,13 @@ import styles from './index.less';
// TODO: do not allow other nodes to connect to this node
export function BeginNode({ selected, data }: NodeProps<NodeData>) {
const { t } = useTranslation();
const query: BeginQuery[] = get(data, 'form.query', []);

return (
<section
className={classNames(styles.ragNode, {
[styles.selectedNode]: selected,
})}
style={{
width: 100,
}}
>
<Handle
type="source"
@@ -29,7 +33,7 @@ export function BeginNode({ selected, data }: NodeProps<NodeData>) {
style={RightHandleStyle}
></Handle>

<Flex align="center" justify={'space-around'}>
<Flex align="center" justify={'center'} gap={10}>
<OperatorIcon
name={data.label as Operator}
fontSize={24}
@@ -37,6 +41,24 @@ export function BeginNode({ selected, data }: NodeProps<NodeData>) {
></OperatorIcon>
<div className={styles.nodeTitle}>{t(`flow.begin`)}</div>
</Flex>
<Flex gap={8} vertical className={styles.generateParameters}>
{query.map((x, idx) => {
const Icon = BeginQueryTypeIconMap[x.type as BeginQueryType];
return (
<Flex
key={idx}
align="center"
gap={6}
className={styles.conditionBlock}
>
<Icon className="size-4" />
<label htmlFor="">{x.key}</label>
<span className={styles.parameterValue}>{x.name}</span>
<span className="flex-1">{x.optional ? 'Yes' : 'No'}</span>
</Flex>
);
})}
</Flex>
</section>
);
}

+ 17
- 8
web/src/pages/flow/constant.tsx 查看文件

@@ -43,6 +43,15 @@ import {
SendOutlined,
} from '@ant-design/icons';
import upperFirst from 'lodash/upperFirst';
import {
CloudUpload,
Link2,
ListOrdered,
OptionIcon,
TextCursorInput,
ToggleLeft,
WrapText,
} from 'lucide-react';

export enum Operator {
Begin = 'Begin',
@@ -2870,12 +2879,12 @@ export enum BeginQueryType {
Url = 'url',
}

export const BeginQueryTypeMap = {
[BeginQueryType.Line]: 'input',
[BeginQueryType.Paragraph]: 'textarea',
[BeginQueryType.Options]: 'select',
[BeginQueryType.File]: 'file',
[BeginQueryType.Integer]: 'inputnumber',
[BeginQueryType.Boolean]: 'switch',
[BeginQueryType.Url]: 'input',
export const BeginQueryTypeIconMap = {
[BeginQueryType.Line]: TextCursorInput,
[BeginQueryType.Paragraph]: WrapText,
[BeginQueryType.Options]: OptionIcon,
[BeginQueryType.File]: CloudUpload,
[BeginQueryType.Integer]: ListOrdered,
[BeginQueryType.Boolean]: ToggleLeft,
[BeginQueryType.Url]: Link2,
};

+ 2
- 2
web/src/pages/flow/flow-drawer/index.tsx 查看文件

@@ -83,7 +83,7 @@ const FormMap = {

const EmptyContent = () => <div></div>;

const FlowDrawer = ({
const FormDrawer = ({
visible,
hideModal,
node,
@@ -152,4 +152,4 @@ const FlowDrawer = ({
);
};

export default FlowDrawer;
export default FormDrawer;

+ 24
- 0
web/src/pages/flow/form/begin-form/index.less 查看文件

@@ -0,0 +1,24 @@
.dynamicInputVariable {
background-color: #ebe9e9;
:global(.ant-collapse-content) {
background-color: #f6f6f6;
}
:global(.ant-collapse-content-box) {
padding: 0 !important;
}
margin-bottom: 20px;
.title {
font-weight: 600;
font-size: 16px;
}

.addButton {
color: rgb(22, 119, 255);
font-weight: 600;
}
}

.addButton {
color: rgb(22, 119, 255);
font-weight: 600;
}

+ 11
- 7
web/src/pages/flow/form/begin-form/index.tsx 查看文件

@@ -1,17 +1,20 @@
import { useTranslate } from '@/hooks/common-hooks';
import { PlusOutlined } from '@ant-design/icons';
import { Button, Form, Input } from 'antd';
import { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { BeginQuery, IOperatorForm } from '../../interface';
import { useEditQueryRecord } from './hooks';
import { ModalForm } from './paramater-modal';
import QueryTable from './query-table';

import styles from './index.less';

type FieldType = {
prologue?: string;
};

const BeginForm = ({ onValuesChange, form }: IOperatorForm) => {
const { t } = useTranslate('chat');
const { t } = useTranslation();
const {
ok,
currentRecord,
@@ -55,9 +58,9 @@ const BeginForm = ({ onValuesChange, form }: IOperatorForm) => {
>
<Form.Item<FieldType>
name={'prologue'}
label={t('setAnOpener')}
tooltip={t('setAnOpenerTip')}
initialValue={t('setAnOpenerInitial')}
label={t('chat.setAnOpener')}
tooltip={t('chat.setAnOpenerTip')}
initialValue={t('chat.setAnOpenerInitial')}
>
<Input.TextArea autoSize={{ minRows: 5 }} />
</Form.Item>
@@ -65,7 +68,6 @@ const BeginForm = ({ onValuesChange, form }: IOperatorForm) => {
<Form.Item name="query" noStyle />

<Form.Item
label="Query List"
shouldUpdate={(prevValues, curValues) =>
prevValues.query !== curValues.query
}
@@ -86,9 +88,11 @@ const BeginForm = ({ onValuesChange, form }: IOperatorForm) => {
htmlType="button"
style={{ margin: '0 8px' }}
onClick={() => showModal()}
icon={<PlusOutlined />}
block
className={styles.addButton}
>
Add +
{t('flow.addItem')}
</Button>
{visible && (
<ModalForm

+ 11
- 2
web/src/pages/flow/form/begin-form/paramater-modal.tsx 查看文件

@@ -3,7 +3,7 @@ import { IModalProps } from '@/interfaces/common';
import { Form, Input, Modal, Select, Switch } from 'antd';
import { DefaultOptionType } from 'antd/es/select';
import { useEffect, useMemo } from 'react';
import { BeginQueryType } from '../../constant';
import { BeginQueryType, BeginQueryTypeIconMap } from '../../constant';
import { BeginQuery } from '../../interface';
import BeginDynamicOptions from './begin-dynamic-options';

@@ -20,10 +20,19 @@ export const ModalForm = ({
const options = useMemo(() => {
return Object.values(BeginQueryType).reduce<DefaultOptionType[]>(
(pre, cur) => {
const Icon = BeginQueryTypeIconMap[cur];

return [
...pre,
{
label: cur,
label: (
<div className="flex items-center gap-2">
<Icon
className={`size-${cur === BeginQueryType.Options ? 4 : 5}`}
></Icon>
{cur}
</div>
),
value: cur,
},
];

+ 27
- 6
web/src/pages/flow/form/begin-form/query-table.tsx 查看文件

@@ -1,8 +1,11 @@
import { DeleteOutlined, EditOutlined } from '@ant-design/icons';
import type { TableProps } from 'antd';
import { Space, Table, Tooltip } from 'antd';
import { Collapse, Space, Table, Tooltip } from 'antd';
import { BeginQuery } from '../../interface';

import { useTranslation } from 'react-i18next';
import styles from './index.less';

interface IProps {
data: BeginQuery[];
deleteRecord(index: number): void;
@@ -10,6 +13,8 @@ interface IProps {
}

const QueryTable = ({ data, deleteRecord, showModal }: IProps) => {
const { t } = useTranslation();

const columns: TableProps<BeginQuery>['columns'] = [
{
title: 'Key',
@@ -25,7 +30,7 @@ const QueryTable = ({ data, deleteRecord, showModal }: IProps) => {
),
},
{
title: 'Name',
title: t('flow.name'),
dataIndex: 'name',
key: 'name',
ellipsis: {
@@ -38,18 +43,18 @@ const QueryTable = ({ data, deleteRecord, showModal }: IProps) => {
),
},
{
title: 'Type',
title: t('flow.type'),
dataIndex: 'type',
key: 'type',
},
{
title: 'Optional',
title: t('flow.optional'),
dataIndex: 'optional',
key: 'optional',
render: (optional) => (optional ? 'Yes' : 'No'),
},
{
title: 'Action',
title: t('common.action'),
key: 'action',
render: (_, record, idx) => (
<Space>
@@ -64,7 +69,23 @@ const QueryTable = ({ data, deleteRecord, showModal }: IProps) => {
];

return (
<Table<BeginQuery> columns={columns} dataSource={data} pagination={false} />
<Collapse
defaultActiveKey={['1']}
className={styles.dynamicInputVariable}
items={[
{
key: '1',
label: <span className={styles.title}>{t('flow.input')}</span>,
children: (
<Table<BeginQuery>
columns={columns}
dataSource={data}
pagination={false}
/>
),
},
]}
/>
);
};


+ 16
- 6
web/src/pages/flow/form/components/dynamic-input-variable.tsx 查看文件

@@ -1,7 +1,7 @@
import { MinusCircleOutlined, PlusOutlined } from '@ant-design/icons';
import { Button, Collapse, Flex, Form, Input, Select } from 'antd';

import { useCallback } from 'react';
import { PropsWithChildren, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { useBuildComponentIdSelectOptions } from '../../hooks';
import styles from './index.less';
@@ -95,9 +95,10 @@ const DynamicVariableForm = ({ nodeId }: IProps) => {
);
};

const DynamicInputVariable = ({ nodeId }: IProps) => {
const { t } = useTranslation();

export function FormCollapse({
children,
title,
}: PropsWithChildren<{ title: string }>) {
return (
<Collapse
className={styles.dynamicInputVariable}
@@ -105,12 +106,21 @@ const DynamicInputVariable = ({ nodeId }: IProps) => {
items={[
{
key: '1',
label: <span className={styles.title}>{t('flow.input')}</span>,
children: <DynamicVariableForm nodeId={nodeId}></DynamicVariableForm>,
label: <span className={styles.title}>{title}</span>,
children,
},
]}
/>
);
}

const DynamicInputVariable = ({ nodeId }: IProps) => {
const { t } = useTranslation();
return (
<FormCollapse title={t('flow.input')}>
<DynamicVariableForm nodeId={nodeId}></DynamicVariableForm>
</FormCollapse>
);
};

export default DynamicInputVariable;

+ 16
- 3
web/src/pages/flow/header/index.tsx 查看文件

@@ -3,13 +3,16 @@ import { useSetModalState, useTranslate } from '@/hooks/common-hooks';
import { useFetchFlow } from '@/hooks/flow-hooks';
import { ArrowLeftOutlined } from '@ant-design/icons';
import { Button, Flex, Space } from 'antd';
import { useCallback } from 'react';
import { Link, useParams } from 'umi';
import FlowIdModal from '../flow-id-modal';
import {
useGetBeginNodeDataQuery,
useSaveGraph,
useSaveGraphBeforeOpeningDebugDrawer,
useWatchAgentChange,
} from '../hooks';
import { BeginQuery } from '../interface';
import styles from './index.less';

interface IProps {
@@ -19,7 +22,7 @@ interface IProps {

const FlowHeader = ({ showChatDrawer, chatDrawerVisible }: IProps) => {
const { saveGraph } = useSaveGraph();
const handleRun = useSaveGraphBeforeOpeningDebugDrawer(showChatDrawer);
const { handleRun } = useSaveGraphBeforeOpeningDebugDrawer(showChatDrawer);
const { data } = useFetchFlow();
const { t } = useTranslate('flow');
const {
@@ -30,6 +33,16 @@ const FlowHeader = ({ showChatDrawer, chatDrawerVisible }: IProps) => {
const { visible, hideModal, showModal } = useSetModalState();
const { id } = useParams();
const time = useWatchAgentChange(chatDrawerVisible);
const getBeginNodeDataQuery = useGetBeginNodeDataQuery();

const handleRunAgent = useCallback(() => {
const query: BeginQuery[] = getBeginNodeDataQuery();
if (query.length > 0) {
showChatDrawer();
} else {
handleRun();
}
}, [getBeginNodeDataQuery, handleRun, showChatDrawer]);

return (
<>
@@ -51,10 +64,10 @@ const FlowHeader = ({ showChatDrawer, chatDrawerVisible }: IProps) => {
</div>
</Space>
<Space size={'large'}>
<Button onClick={handleRun}>
<Button onClick={handleRunAgent}>
<b>{t('run')}</b>
</Button>
<Button type="primary" onClick={saveGraph}>
<Button type="primary" onClick={() => saveGraph()}>
<b>{t('save')}</b>
</Button>
{/* <Button type="primary" onClick={showOverviewModal} disabled>

web/src/pages/flow/hooks.ts → web/src/pages/flow/hooks.tsx 查看文件

@@ -21,6 +21,7 @@ import { Variable } from '@/interfaces/database/chat';
import api from '@/utils/api';
import { useDebounceEffect } from 'ahooks';
import { FormInstance, message } from 'antd';
import { DefaultOptionType } from 'antd/es/select';
import dayjs from 'dayjs';
import { humanId } from 'human-id';
import { get, lowerFirst } from 'lodash';
@@ -65,7 +66,12 @@ import {
initialWikipediaValues,
initialYahooFinanceValues,
} from './constant';
import { ICategorizeForm, IRelevantForm, ISwitchForm } from './interface';
import {
BeginQuery,
ICategorizeForm,
IRelevantForm,
ISwitchForm,
} from './interface';
import useGraphStore, { RFState } from './store';
import {
buildDslComponentsByGraph,
@@ -225,49 +231,60 @@ export const useHandleDrop = () => {
return { onDrop, onDragOver, setReactFlowInstance };
};

export const useShowDrawer = () => {
export const useShowFormDrawer = () => {
const {
clickedNodeId: clickNodeId,
setClickedNodeId,
getNode,
} = useGraphStore((state) => state);
const {
visible: drawerVisible,
hideModal: hideDrawer,
showModal: showDrawer,
visible: formDrawerVisible,
hideModal: hideFormDrawer,
showModal: showFormDrawer,
} = useSetModalState();

const handleShow = useCallback(
(node: Node) => {
setClickedNodeId(node.id);
showDrawer();
showFormDrawer();
},
[showDrawer, setClickedNodeId],
[showFormDrawer, setClickedNodeId],
);

return {
drawerVisible,
hideDrawer,
showDrawer: handleShow,
formDrawerVisible,
hideFormDrawer,
showFormDrawer: handleShow,
clickedNode: getNode(clickNodeId),
};
};

export const useSaveGraph = () => {
const { data } = useFetchFlow();
const { setFlow } = useSetFlow();
const { setFlow, loading } = useSetFlow();
const { id } = useParams();
const { nodes, edges } = useGraphStore((state) => state);
const saveGraph = useCallback(async () => {
const dslComponents = buildDslComponentsByGraph(nodes, edges);
return setFlow({
id,
title: data.title,
dsl: { ...data.dsl, graph: { nodes, edges }, components: dslComponents },
});
}, [nodes, edges, setFlow, id, data]);
useEffect(() => {}, [nodes]);
const saveGraph = useCallback(
async (currentNodes?: Node[]) => {
const dslComponents = buildDslComponentsByGraph(
currentNodes ?? nodes,
edges,
);
return setFlow({
id,
title: data.title,
dsl: {
...data.dsl,
graph: { nodes: currentNodes ?? nodes, edges },
components: dslComponents,
},
});
},
[nodes, edges, setFlow, id, data],
);

return { saveGraph };
return { saveGraph, loading };
};

export const useHandleFormValuesChange = (id?: string) => {
@@ -420,32 +437,46 @@ export const useHandleNodeNameChange = ({
return { name, handleNameBlur, handleNameChange };
};

export const useGetBeginNodeDataQuery = () => {
const getNode = useGraphStore((state) => state.getNode);

const getBeginNodeDataQuery = useCallback(() => {
return get(getNode('begin'), 'data.form.query', []);
}, [getNode]);

return getBeginNodeDataQuery;
};

export const useSaveGraphBeforeOpeningDebugDrawer = (show: () => void) => {
const { id } = useParams();
const { saveGraph } = useSaveGraph();
const { saveGraph, loading } = useSaveGraph();
const { resetFlow } = useResetFlow();
const { refetch } = useFetchFlow();
const { send } = useSendMessageWithSse(api.runCanvas);
const handleRun = useCallback(async () => {
const saveRet = await saveGraph();
if (saveRet?.code === 0) {
// Call the reset api before opening the run drawer each time
const resetRet = await resetFlow();
// After resetting, all previous messages will be cleared.
if (resetRet?.code === 0) {
// fetch prologue
const sendRet = await send({ id });
if (receiveMessageError(sendRet)) {
message.error(sendRet?.data?.message);
} else {
refetch();
show();

const handleRun = useCallback(
async (nextNodes?: Node[]) => {
const saveRet = await saveGraph(nextNodes);
if (saveRet?.code === 0) {
// Call the reset api before opening the run drawer each time
const resetRet = await resetFlow();
// After resetting, all previous messages will be cleared.
if (resetRet?.code === 0) {
// fetch prologue
const sendRet = await send({ id });
if (receiveMessageError(sendRet)) {
message.error(sendRet?.data?.message);
} else {
refetch();
show();
}
}
}
}
}, [saveGraph, resetFlow, id, send, show, refetch]);
},
[saveGraph, resetFlow, send, id, refetch, show],
);

return handleRun;
return { handleRun, loading };
};

export const useReplaceIdWithName = () => {
@@ -596,8 +627,10 @@ const ExcludedNodes = [

export const useBuildComponentIdSelectOptions = (nodeId?: string) => {
const nodes = useGraphStore((state) => state.nodes);
const getBeginNodeDataQuery = useGetBeginNodeDataQuery();
const query: BeginQuery[] = getBeginNodeDataQuery();

const options = useMemo(() => {
const componentIdOptions = useMemo(() => {
return nodes
.filter(
(x) =>
@@ -606,17 +639,40 @@ export const useBuildComponentIdSelectOptions = (nodeId?: string) => {
.map((x) => ({ label: x.data.name, value: x.id }));
}, [nodes, nodeId]);

return options;
const groupedOptions = [
{
label: <span>Component id</span>,
title: 'Component Id',
options: componentIdOptions,
},
{
label: <span>Begin input</span>,
title: 'Begin input',
options: query.map((x) => ({
label: x.name,
value: `begin@${x.key}`,
})),
},
];

return groupedOptions;
};

export const useGetComponentLabelByValue = (nodeId: string) => {
const options = useBuildComponentIdSelectOptions(nodeId);
const flattenOptions = useMemo(
() =>
options.reduce<DefaultOptionType[]>((pre, cur) => {
return [...pre, ...cur.options];
}, []),
[options],
);

const getLabel = useCallback(
(val?: string) => {
return options.find((x) => x.value === val)?.label;
return flattenOptions.find((x) => x.value === val)?.label;
},
[options],
[flattenOptions],
);
return getLabel;
};

+ 2
- 2
web/src/pages/flow/index.tsx 查看文件

@@ -31,8 +31,8 @@ function RagFlow() {
></FlowHeader>
<Content style={{ margin: 0 }}>
<FlowCanvas
chatDrawerVisible={chatDrawerVisible}
hideChatDrawer={hideChatDrawer}
drawerVisible={chatDrawerVisible}
hideDrawer={hideChatDrawer}
></FlowCanvas>
</Content>
</Layout>

+ 5
- 0
web/src/pages/flow/run-drawer/index.less 查看文件

@@ -0,0 +1,5 @@
.formWrapper {
:global(.ant-form-item-label) {
font-weight: 600 !important;
}
}

+ 284
- 0
web/src/pages/flow/run-drawer/index.tsx 查看文件

@@ -0,0 +1,284 @@
import { Authorization } from '@/constants/authorization';
import { useSetModalState } from '@/hooks/common-hooks';
import { useSetSelectedRecord } from '@/hooks/logic-hooks';
import { useHandleSubmittable } from '@/hooks/login-hooks';
import { IModalProps } from '@/interfaces/common';
import api from '@/utils/api';
import { getAuthorization } from '@/utils/authorization-util';
import { InboxOutlined } from '@ant-design/icons';
import {
Button,
Drawer,
Flex,
Form,
FormItemProps,
Input,
InputNumber,
Select,
Switch,
Upload,
} from 'antd';
import { pick } from 'lodash';
import { Link2, Trash2 } from 'lucide-react';
import { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { BeginQueryType } from '../constant';
import {
useGetBeginNodeDataQuery,
useSaveGraphBeforeOpeningDebugDrawer,
} from '../hooks';
import { BeginQuery } from '../interface';
import useGraphStore from '../store';
import { getDrawerWidth } from '../utils';
import { PopoverForm } from './popover-form';

import styles from './index.less';

const RunDrawer = ({
hideModal,
showModal: showChatModal,
}: IModalProps<any>) => {
const { t } = useTranslation();
const [form] = Form.useForm();
const updateNodeForm = useGraphStore((state) => state.updateNodeForm);
const {
visible,
hideModal: hidePopover,
switchVisible,
showModal: showPopover,
} = useSetModalState();
const { setRecord, currentRecord } = useSetSelectedRecord<number>();
const { submittable } = useHandleSubmittable(form);

const handleShowPopover = useCallback(
(idx: number) => () => {
setRecord(idx);
showPopover();
},
[setRecord, showPopover],
);

const handleRemoveUrl = useCallback(
(key: number, index: number) => () => {
const list: any[] = form.getFieldValue(key);

form.setFieldValue(
key,
list.filter((_, idx) => idx !== index),
);
},
[form],
);

const getBeginNodeDataQuery = useGetBeginNodeDataQuery();
const query: BeginQuery[] = getBeginNodeDataQuery();

const normFile = (e: any) => {
if (Array.isArray(e)) {
return e;
}
return e?.fileList;
};

const renderWidget = useCallback(
(q: BeginQuery, idx: number) => {
const props: FormItemProps & { key: number } = {
key: idx,
label: q.name,
name: idx,
};
if (q.optional === false) {
props.rules = [{ required: true }];
}

const urlList: { url: string; result: string }[] =
form.getFieldValue(idx) || [];

const BeginQueryTypeMap = {
[BeginQueryType.Line]: (
<Form.Item {...props}>
<Input></Input>
</Form.Item>
),
[BeginQueryType.Paragraph]: (
<Form.Item {...props}>
<Input.TextArea rows={4}></Input.TextArea>
</Form.Item>
),
[BeginQueryType.Options]: (
<Form.Item {...props}>
<Select
allowClear
options={q.options?.map((x) => ({ label: x, value: x })) ?? []}
></Select>
</Form.Item>
),
[BeginQueryType.File]: (
<Form.Item
{...props}
valuePropName="fileList"
getValueFromEvent={normFile}
>
<Upload.Dragger
name="file"
action={api.parse}
multiple
headers={{ [Authorization]: getAuthorization() }}
>
<p className="ant-upload-drag-icon">
<InboxOutlined />
</p>
<p className="ant-upload-text">{t('fileManager.uploadTitle')}</p>
<p className="ant-upload-hint">
{t('fileManager.uploadDescription')}
</p>
</Upload.Dragger>
</Form.Item>
),
[BeginQueryType.Integer]: (
<Form.Item {...props}>
<InputNumber></InputNumber>
</Form.Item>
),
[BeginQueryType.Boolean]: (
<Form.Item valuePropName={'checked'} {...props}>
<Switch></Switch>
</Form.Item>
),
[BeginQueryType.Url]: (
<>
<Form.Item
{...pick(props, ['key', 'label', 'rules'])}
required={!q.optional}
className={urlList.length > 0 ? 'mb-1' : ''}
>
<PopoverForm visible={visible} switchVisible={switchVisible}>
<Button
onClick={handleShowPopover(idx)}
className="text-buttonBlueText"
>
{t('flow.pasteFileLink')}
</Button>
</PopoverForm>
</Form.Item>
<Form.Item name={idx} noStyle {...pick(props, ['rules'])} />
<Form.Item
noStyle
shouldUpdate={(prevValues, curValues) =>
prevValues[idx] !== curValues[idx]
}
>
{({ getFieldValue }) => {
const urlInfo: { url: string; result: string }[] =
getFieldValue(idx) || [];
return urlInfo.length ? (
<Flex vertical gap={8} className="mb-3">
{urlInfo.map((u, index) => (
<div
key={index}
className="flex items-center justify-between gap-2 hover:bg-slate-100 group"
>
<Link2 className="size-5"></Link2>
<span className="flex-1 truncate"> {u.url}</span>
<Trash2
className="size-4 invisible group-hover:visible cursor-pointer"
onClick={handleRemoveUrl(idx, index)}
/>
</div>
))}
</Flex>
) : null;
}}
</Form.Item>
</>
),
};

return BeginQueryTypeMap[q.type as BeginQueryType];
},
[form, handleRemoveUrl, handleShowPopover, switchVisible, t, visible],
);

const { handleRun } = useSaveGraphBeforeOpeningDebugDrawer(showChatModal!);

const handleRunAgent = useCallback(
(nextValues: Record<string, any>) => {
const currentNodes = updateNodeForm('begin', nextValues, ['query']);
handleRun(currentNodes);
hideModal?.();
},
[handleRun, hideModal, updateNodeForm],
);

const onOk = useCallback(async () => {
const values = await form.validateFields();
const nextValues = Object.entries(values).map(([key, value]) => {
const item = query[Number(key)];
let nextValue = value;
if (Array.isArray(value)) {
nextValue = ``;

value.forEach((x, idx) => {
if (x?.originFileObj instanceof File) {
if (idx === 0) {
nextValue += `${x.name}\n\n${x.response.data}\n\n`;
} else {
nextValue += `${x.response.data}\n\n`;
}
} else {
if (idx === 0) {
nextValue += `${x.url}\n\n${x.result}\n\n`;
} else {
nextValue += `${x.result}\n\n`;
}
}
});
}
return { ...item, value: nextValue };
});
handleRunAgent(nextValues);
}, [form, handleRunAgent, query]);

return (
<Drawer
title={t('flow.testRun')}
placement="right"
onClose={hideModal}
open
getContainer={false}
width={getDrawerWidth()}
mask={false}
>
<section className={styles.formWrapper}>
<Form.Provider
onFormFinish={(name, { values, forms }) => {
if (name === 'urlForm') {
const { basicForm } = forms;
const urlInfo = basicForm.getFieldValue(currentRecord) || [];
basicForm.setFieldsValue({
[currentRecord]: [...urlInfo, values],
});
hidePopover();
}
}}
>
<Form
name="basicForm"
autoComplete="off"
layout={'vertical'}
form={form}
>
{query.map((x, idx) => {
return renderWidget(x, idx);
})}
</Form>
</Form.Provider>
</section>
<Button type={'primary'} block onClick={onOk} disabled={!submittable}>
{t('common.next')}
</Button>
</Drawer>
);
};

export default RunDrawer;

+ 74
- 0
web/src/pages/flow/run-drawer/popover-form.tsx 查看文件

@@ -0,0 +1,74 @@
import { useParseDocument } from '@/hooks/document-hooks';
import { useResetFormOnCloseModal } from '@/hooks/logic-hooks';
import { IModalProps } from '@/interfaces/common';
import { Button, Form, Input, Popover } from 'antd';
import { PropsWithChildren } from 'react';
import { useTranslation } from 'react-i18next';

const reg =
/^(((ht|f)tps?):\/\/)?([^!@#$%^&*?.\s-]([^!@#$%^&*?.\s]{0,63}[^!@#$%^&*?.\s])?\.)+[a-z]{2,6}\/?/;

export const PopoverForm = ({
children,
visible,
switchVisible,
}: PropsWithChildren<IModalProps<any>>) => {
const [form] = Form.useForm();
const { parseDocument, loading } = useParseDocument();
const { t } = useTranslation();

useResetFormOnCloseModal({
form,
visible,
});

const onOk = async () => {
const values = await form.validateFields();
const val = values.url;

if (reg.test(val)) {
const ret = await parseDocument(val);
if (ret?.data?.code === 0) {
form.setFieldValue('result', ret?.data?.data);
form.submit();
}
}
};

const content = (
<Form form={form} name="urlForm">
<Form.Item
name="url"
rules={[{ required: true, type: 'url' }]}
className="m-0"
>
<Input
onPressEnter={(e) => e.preventDefault()}
placeholder={t('flow.pasteFileLink')}
suffix={
<Button
type="primary"
onClick={onOk}
size={'small'}
loading={loading}
>
{t('common.submit')}
</Button>
}
/>
</Form.Item>
<Form.Item name={'result'} noStyle />
</Form>
);

return (
<Popover
content={content}
open={visible}
trigger={'click'}
onOpenChange={switchVisible}
>
{children}
</Popover>
);
};

+ 22
- 19
web/src/pages/flow/store.ts 查看文件

@@ -47,7 +47,7 @@ export type RFState = {
nodeId: string,
values: any,
path?: (string | number)[],
) => void;
) => Node[];
onSelectionChange: OnSelectionChangeFunc;
addNode: (nodes: Node) => void;
getNode: (id?: string | null) => Node<NodeData> | undefined;
@@ -331,27 +331,30 @@ const useGraphStore = create<RFState>()(
values: any,
path: (string | number)[] = [],
) => {
set({
nodes: get().nodes.map((node) => {
if (node.id === nodeId) {
let nextForm: Record<string, unknown> = { ...node.data.form };
if (path.length === 0) {
nextForm = Object.assign(nextForm, values);
} else {
lodashSet(nextForm, path, values);
}
return {
...node,
data: {
...node.data,
form: nextForm,
},
} as any;
const nextNodes = get().nodes.map((node) => {
if (node.id === nodeId) {
let nextForm: Record<string, unknown> = { ...node.data.form };
if (path.length === 0) {
nextForm = Object.assign(nextForm, values);
} else {
lodashSet(nextForm, path, values);
}
return {
...node,
data: {
...node.data,
form: nextForm,
},
} as any;
}

return node;
}),
return node;
});
set({
nodes: nextNodes,
});

return nextNodes;
},
updateSwitchFormData: (source, sourceHandle, target) => {
const { updateNodeForm } = get();

+ 1
- 0
web/src/utils/api.ts 查看文件

@@ -62,6 +62,7 @@ export default {
web_crawl: `${api_host}/document/web_crawl`,
document_infos: `${api_host}/document/infos`,
upload_and_parse: `${api_host}/document/upload_and_parse`,
parse: `${api_host}/document/parse`,

// chat
setDialog: `${api_host}/dialog/set`,

+ 2
- 2
web/src/utils/request.ts 查看文件

@@ -99,8 +99,8 @@ request.interceptors.request.use((url: string, options: any) => {
});

request.interceptors.response.use(async (response: any, options) => {
if (response?.status === 413) {
message.error(RetcodeMessage[413]);
if (response?.status === 413 || response?.status === 504) {
message.error(RetcodeMessage[response?.status as ResultCode]);
}

if (options.responseType === 'blob') {

+ 1
- 0
web/tailwind.config.js 查看文件

@@ -24,6 +24,7 @@ module.exports = {
ring: 'hsl(var(--ring))',
background: 'var(--background)',
foreground: 'hsl(var(--foreground))',
buttonBlueText: 'var(--button-blue-text)',
primary: {
DEFAULT: 'hsl(var(--primary))',
foreground: 'hsl(var(--primary-foreground))',

+ 2
- 0
web/tailwind.css 查看文件

@@ -37,6 +37,8 @@

--background-inverse-standard: rgba(58, 56, 65, 0.15);
--background-inverse-standard-foreground: rgb(92, 81, 81);

--button-blue-text: rgb(22, 119, 255);
}

.dark {

正在加载...
取消
保存