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)
This commit is contained in:
balibabu 2024-11-15 15:17:23 +08:00 committed by GitHub
parent a854bc22d1
commit e0659a4f0e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
25 changed files with 780 additions and 135 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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,27 +71,66 @@ 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 { drawerVisible, hideDrawer, showDrawer, clickedNode } =
useShowDrawer();
const onNodeClick: NodeMouseHandler = useCallback(
(e, node) => {
if (node.data.label !== Operator.Note) {
showDrawer(node);
}
},
[showDrawer],
);
const { formDrawerVisible, hideFormDrawer, showFormDrawer, clickedNode } =
useShowFormDrawer();
const onPaneClick = useCallback(() => {
hideDrawer();
}, [hideDrawer]);
hideFormDrawer();
}, [hideFormDrawer]);
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) {
hideRunOrChatDrawer();
showFormDrawer(node);
}
},
[hideRunOrChatDrawer, showFormDrawer],
);
const getBeginNodeDataQuery = useGetBeginNodeDataQuery();
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}>
<svg
@ -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>
);
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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