### 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
| @@ -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 }; | |||
| }; | |||
| @@ -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 }; | |||
| }; | |||
| @@ -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', | |||
| @@ -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”', | |||
| @@ -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', | |||
| @@ -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> | |||
| ); | |||
| } | |||
| @@ -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> | |||
| ); | |||
| } | |||
| @@ -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, | |||
| }; | |||
| @@ -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; | |||
| @@ -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; | |||
| } | |||
| @@ -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 | |||
| @@ -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, | |||
| }, | |||
| ]; | |||
| @@ -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} | |||
| /> | |||
| ), | |||
| }, | |||
| ]} | |||
| /> | |||
| ); | |||
| }; | |||
| @@ -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; | |||
| @@ -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> | |||
| @@ -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; | |||
| }; | |||
| @@ -31,8 +31,8 @@ function RagFlow() { | |||
| ></FlowHeader> | |||
| <Content style={{ margin: 0 }}> | |||
| <FlowCanvas | |||
| chatDrawerVisible={chatDrawerVisible} | |||
| hideChatDrawer={hideChatDrawer} | |||
| drawerVisible={chatDrawerVisible} | |||
| hideDrawer={hideChatDrawer} | |||
| ></FlowCanvas> | |||
| </Content> | |||
| </Layout> | |||
| @@ -0,0 +1,5 @@ | |||
| .formWrapper { | |||
| :global(.ant-form-item-label) { | |||
| font-weight: 600 !important; | |||
| } | |||
| } | |||
| @@ -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; | |||
| @@ -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> | |||
| ); | |||
| }; | |||
| @@ -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(); | |||
| @@ -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`, | |||
| @@ -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') { | |||
| @@ -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))', | |||
| @@ -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 { | |||