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
25 changed files with 780 additions and 135 deletions

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