### 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
| import i18n from '@/locales/config'; | import i18n from '@/locales/config'; | ||||
| import chatService from '@/services/chat-service'; | import chatService from '@/services/chat-service'; | ||||
| import kbService from '@/services/knowledge-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 { buildChunkHighlights } from '@/utils/document-util'; | ||||
| import { post } from '@/utils/request'; | |||||
| import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; | import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; | ||||
| import { UploadFile, message } from 'antd'; | import { UploadFile, message } from 'antd'; | ||||
| import { get } from 'lodash'; | import { get } from 'lodash'; | ||||
| return { data, loading, uploadAndParseDocument: mutateAsync }; | 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 }; | |||||
| }; |
| import userService from '@/services/user-service'; | import userService from '@/services/user-service'; | ||||
| import authorizationUtil from '@/utils/authorization-util'; | import authorizationUtil from '@/utils/authorization-util'; | ||||
| import { useMutation } from '@tanstack/react-query'; | 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 { useTranslation } from 'react-i18next'; | ||||
| import { history } from 'umi'; | import { history } from 'umi'; | ||||
| return { data, loading, logout: mutateAsync }; | 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 }; | |||||
| }; |
| s: 'S', | s: 'S', | ||||
| pleaseSelect: 'Please select', | pleaseSelect: 'Please select', | ||||
| pleaseInput: 'Please input', | pleaseInput: 'Please input', | ||||
| submit: 'Submit', | |||||
| }, | }, | ||||
| login: { | login: { | ||||
| login: 'Sign in', | login: 'Sign in', | ||||
| chunkTokenNumber: 'Chunk token number', | chunkTokenNumber: 'Chunk token number', | ||||
| chunkTokenNumberMessage: 'Chunk token number is required', | chunkTokenNumberMessage: 'Chunk token number is required', | ||||
| embeddingModelTip: | 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: | permissionsTip: | ||||
| "If set to 'Team', all team members will be able to manage the knowledge base.", | "If set to 'Team', all team members will be able to manage the knowledge base.", | ||||
| chunkTokenNumberTip: | chunkTokenNumberTip: | ||||
| content: 'Content', | content: 'Content', | ||||
| operationResults: 'Operation Results', | operationResults: 'Operation Results', | ||||
| autosaved: 'Autosaved', | autosaved: 'Autosaved', | ||||
| optional: 'Optional', | |||||
| pasteFileLink: 'Paste file link', | |||||
| testRun: 'Test Run', | |||||
| }, | }, | ||||
| footer: { | footer: { | ||||
| profile: 'All rights reserved @ React', | profile: 'All rights reserved @ React', |
| s: '秒', | s: '秒', | ||||
| pleaseSelect: '請選擇', | pleaseSelect: '請選擇', | ||||
| pleaseInput: '請輸入', | pleaseInput: '請輸入', | ||||
| submit: '提交', | |||||
| }, | }, | ||||
| login: { | login: { | ||||
| login: '登入', | login: '登入', | ||||
| content: '內容', | content: '內容', | ||||
| operationResults: '運行結果', | operationResults: '運行結果', | ||||
| autosaved: '已自動儲存', | autosaved: '已自動儲存', | ||||
| optional: '可選項', | |||||
| pasteFileLink: '貼上文件連結', | |||||
| testRun: '試運行', | |||||
| }, | }, | ||||
| footer: { | footer: { | ||||
| profile: '“保留所有權利 @ react”', | profile: '“保留所有權利 @ react”', |
| s: '秒', | s: '秒', | ||||
| pleaseSelect: '请选择', | pleaseSelect: '请选择', | ||||
| pleaseInput: '请输入', | pleaseInput: '请输入', | ||||
| submit: '提交', | |||||
| }, | }, | ||||
| login: { | login: { | ||||
| login: '登录', | login: '登录', | ||||
| content: '内容', | content: '内容', | ||||
| operationResults: '运行结果', | operationResults: '运行结果', | ||||
| autosaved: '已自动保存', | autosaved: '已自动保存', | ||||
| optional: '可选项', | |||||
| pasteFileLink: '粘贴文件链接', | |||||
| testRun: '试运行', | |||||
| }, | }, | ||||
| footer: { | footer: { | ||||
| profile: 'All rights reserved @ React', | profile: 'All rights reserved @ React', |
| import { useCallback } from 'react'; | |||||
| import { useSetModalState } from '@/hooks/common-hooks'; | |||||
| import { useCallback, useEffect } from 'react'; | |||||
| import ReactFlow, { | import ReactFlow, { | ||||
| Background, | Background, | ||||
| ConnectionMode, | ConnectionMode, | ||||
| import 'reactflow/dist/style.css'; | import 'reactflow/dist/style.css'; | ||||
| import ChatDrawer from '../chat/drawer'; | import ChatDrawer from '../chat/drawer'; | ||||
| import { Operator } from '../constant'; | import { Operator } from '../constant'; | ||||
| import FlowDrawer from '../flow-drawer'; | |||||
| import FormDrawer from '../flow-drawer'; | |||||
| import { | import { | ||||
| useGetBeginNodeDataQuery, | |||||
| useHandleDrop, | useHandleDrop, | ||||
| useSelectCanvasData, | useSelectCanvasData, | ||||
| useShowDrawer, | |||||
| useShowFormDrawer, | |||||
| useValidateConnection, | useValidateConnection, | ||||
| useWatchNodeFormDataChange, | useWatchNodeFormDataChange, | ||||
| } from '../hooks'; | } from '../hooks'; | ||||
| import { BeginQuery } from '../interface'; | |||||
| import RunDrawer from '../run-drawer'; | |||||
| import { ButtonEdge } from './edge'; | import { ButtonEdge } from './edge'; | ||||
| import styles from './index.less'; | import styles from './index.less'; | ||||
| import { RagNode } from './node'; | import { RagNode } from './node'; | ||||
| }; | }; | ||||
| interface IProps { | interface IProps { | ||||
| chatDrawerVisible: boolean; | |||||
| hideChatDrawer(): void; | |||||
| drawerVisible: boolean; | |||||
| hideDrawer(): void; | |||||
| } | } | ||||
| function FlowCanvas({ chatDrawerVisible, hideChatDrawer }: IProps) { | |||||
| function FlowCanvas({ drawerVisible, hideDrawer }: IProps) { | |||||
| const { | const { | ||||
| nodes, | nodes, | ||||
| edges, | edges, | ||||
| onSelectionChange, | onSelectionChange, | ||||
| } = useSelectCanvasData(); | } = useSelectCanvasData(); | ||||
| const isValidConnection = useValidateConnection(); | 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( | const onNodeClick: NodeMouseHandler = useCallback( | ||||
| (e, node) => { | (e, node) => { | ||||
| if (node.data.label !== Operator.Note) { | 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 ( | return ( | ||||
| <div className={styles.canvasWrapper}> | <div className={styles.canvasWrapper}> | ||||
| <Background /> | <Background /> | ||||
| <Controls /> | <Controls /> | ||||
| </ReactFlow> | </ReactFlow> | ||||
| <FlowDrawer | |||||
| node={clickedNode} | |||||
| visible={drawerVisible} | |||||
| hideModal={hideDrawer} | |||||
| ></FlowDrawer> | |||||
| {chatDrawerVisible && ( | |||||
| {formDrawerVisible && ( | |||||
| <FormDrawer | |||||
| node={clickedNode} | |||||
| visible={formDrawerVisible} | |||||
| hideModal={hideFormDrawer} | |||||
| ></FormDrawer> | |||||
| )} | |||||
| {chatVisible && ( | |||||
| <ChatDrawer | <ChatDrawer | ||||
| visible={chatDrawerVisible} | |||||
| hideModal={hideChatDrawer} | |||||
| visible={chatVisible} | |||||
| hideModal={hideRunOrChatDrawer} | |||||
| ></ChatDrawer> | ></ChatDrawer> | ||||
| )} | )} | ||||
| {runVisible && ( | |||||
| <RunDrawer | |||||
| hideModal={hideRunOrChatDrawer} | |||||
| showModal={showChatModal} | |||||
| ></RunDrawer> | |||||
| )} | |||||
| </div> | </div> | ||||
| ); | ); | ||||
| } | } |
| import { Flex } from 'antd'; | import { Flex } from 'antd'; | ||||
| import classNames from 'classnames'; | import classNames from 'classnames'; | ||||
| import get from 'lodash/get'; | |||||
| import { useTranslation } from 'react-i18next'; | import { useTranslation } from 'react-i18next'; | ||||
| import { Handle, NodeProps, Position } from 'reactflow'; | 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 OperatorIcon from '../../operator-icon'; | ||||
| import { RightHandleStyle } from './handle-icon'; | import { RightHandleStyle } from './handle-icon'; | ||||
| import styles from './index.less'; | import styles from './index.less'; | ||||
| // TODO: do not allow other nodes to connect to this node | // TODO: do not allow other nodes to connect to this node | ||||
| export function BeginNode({ selected, data }: NodeProps<NodeData>) { | export function BeginNode({ selected, data }: NodeProps<NodeData>) { | ||||
| const { t } = useTranslation(); | const { t } = useTranslation(); | ||||
| const query: BeginQuery[] = get(data, 'form.query', []); | |||||
| return ( | return ( | ||||
| <section | <section | ||||
| className={classNames(styles.ragNode, { | className={classNames(styles.ragNode, { | ||||
| [styles.selectedNode]: selected, | [styles.selectedNode]: selected, | ||||
| })} | })} | ||||
| style={{ | |||||
| width: 100, | |||||
| }} | |||||
| > | > | ||||
| <Handle | <Handle | ||||
| type="source" | type="source" | ||||
| style={RightHandleStyle} | style={RightHandleStyle} | ||||
| ></Handle> | ></Handle> | ||||
| <Flex align="center" justify={'space-around'}> | |||||
| <Flex align="center" justify={'center'} gap={10}> | |||||
| <OperatorIcon | <OperatorIcon | ||||
| name={data.label as Operator} | name={data.label as Operator} | ||||
| fontSize={24} | fontSize={24} | ||||
| ></OperatorIcon> | ></OperatorIcon> | ||||
| <div className={styles.nodeTitle}>{t(`flow.begin`)}</div> | <div className={styles.nodeTitle}>{t(`flow.begin`)}</div> | ||||
| </Flex> | </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> | </section> | ||||
| ); | ); | ||||
| } | } |
| SendOutlined, | SendOutlined, | ||||
| } from '@ant-design/icons'; | } from '@ant-design/icons'; | ||||
| import upperFirst from 'lodash/upperFirst'; | import upperFirst from 'lodash/upperFirst'; | ||||
| import { | |||||
| CloudUpload, | |||||
| Link2, | |||||
| ListOrdered, | |||||
| OptionIcon, | |||||
| TextCursorInput, | |||||
| ToggleLeft, | |||||
| WrapText, | |||||
| } from 'lucide-react'; | |||||
| export enum Operator { | export enum Operator { | ||||
| Begin = 'Begin', | Begin = 'Begin', | ||||
| Url = 'url', | 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, | |||||
| }; | }; |
| const EmptyContent = () => <div></div>; | const EmptyContent = () => <div></div>; | ||||
| const FlowDrawer = ({ | |||||
| const FormDrawer = ({ | |||||
| visible, | visible, | ||||
| hideModal, | hideModal, | ||||
| node, | node, | ||||
| ); | ); | ||||
| }; | }; | ||||
| export default FlowDrawer; | |||||
| export default FormDrawer; |
| .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; | |||||
| } |
| import { useTranslate } from '@/hooks/common-hooks'; | |||||
| import { PlusOutlined } from '@ant-design/icons'; | |||||
| import { Button, Form, Input } from 'antd'; | import { Button, Form, Input } from 'antd'; | ||||
| import { useCallback } from 'react'; | import { useCallback } from 'react'; | ||||
| import { useTranslation } from 'react-i18next'; | |||||
| import { BeginQuery, IOperatorForm } from '../../interface'; | import { BeginQuery, IOperatorForm } from '../../interface'; | ||||
| import { useEditQueryRecord } from './hooks'; | import { useEditQueryRecord } from './hooks'; | ||||
| import { ModalForm } from './paramater-modal'; | import { ModalForm } from './paramater-modal'; | ||||
| import QueryTable from './query-table'; | import QueryTable from './query-table'; | ||||
| import styles from './index.less'; | |||||
| type FieldType = { | type FieldType = { | ||||
| prologue?: string; | prologue?: string; | ||||
| }; | }; | ||||
| const BeginForm = ({ onValuesChange, form }: IOperatorForm) => { | const BeginForm = ({ onValuesChange, form }: IOperatorForm) => { | ||||
| const { t } = useTranslate('chat'); | |||||
| const { t } = useTranslation(); | |||||
| const { | const { | ||||
| ok, | ok, | ||||
| currentRecord, | currentRecord, | ||||
| > | > | ||||
| <Form.Item<FieldType> | <Form.Item<FieldType> | ||||
| name={'prologue'} | 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 }} /> | <Input.TextArea autoSize={{ minRows: 5 }} /> | ||||
| </Form.Item> | </Form.Item> | ||||
| <Form.Item name="query" noStyle /> | <Form.Item name="query" noStyle /> | ||||
| <Form.Item | <Form.Item | ||||
| label="Query List" | |||||
| shouldUpdate={(prevValues, curValues) => | shouldUpdate={(prevValues, curValues) => | ||||
| prevValues.query !== curValues.query | prevValues.query !== curValues.query | ||||
| } | } | ||||
| htmlType="button" | htmlType="button" | ||||
| style={{ margin: '0 8px' }} | style={{ margin: '0 8px' }} | ||||
| onClick={() => showModal()} | onClick={() => showModal()} | ||||
| icon={<PlusOutlined />} | |||||
| block | block | ||||
| className={styles.addButton} | |||||
| > | > | ||||
| Add + | |||||
| {t('flow.addItem')} | |||||
| </Button> | </Button> | ||||
| {visible && ( | {visible && ( | ||||
| <ModalForm | <ModalForm |
| import { Form, Input, Modal, Select, Switch } from 'antd'; | import { Form, Input, Modal, Select, Switch } from 'antd'; | ||||
| import { DefaultOptionType } from 'antd/es/select'; | import { DefaultOptionType } from 'antd/es/select'; | ||||
| import { useEffect, useMemo } from 'react'; | import { useEffect, useMemo } from 'react'; | ||||
| import { BeginQueryType } from '../../constant'; | |||||
| import { BeginQueryType, BeginQueryTypeIconMap } from '../../constant'; | |||||
| import { BeginQuery } from '../../interface'; | import { BeginQuery } from '../../interface'; | ||||
| import BeginDynamicOptions from './begin-dynamic-options'; | import BeginDynamicOptions from './begin-dynamic-options'; | ||||
| const options = useMemo(() => { | const options = useMemo(() => { | ||||
| return Object.values(BeginQueryType).reduce<DefaultOptionType[]>( | return Object.values(BeginQueryType).reduce<DefaultOptionType[]>( | ||||
| (pre, cur) => { | (pre, cur) => { | ||||
| const Icon = BeginQueryTypeIconMap[cur]; | |||||
| return [ | return [ | ||||
| ...pre, | ...pre, | ||||
| { | { | ||||
| label: cur, | |||||
| label: ( | |||||
| <div className="flex items-center gap-2"> | |||||
| <Icon | |||||
| className={`size-${cur === BeginQueryType.Options ? 4 : 5}`} | |||||
| ></Icon> | |||||
| {cur} | |||||
| </div> | |||||
| ), | |||||
| value: cur, | value: cur, | ||||
| }, | }, | ||||
| ]; | ]; |
| import { DeleteOutlined, EditOutlined } from '@ant-design/icons'; | import { DeleteOutlined, EditOutlined } from '@ant-design/icons'; | ||||
| import type { TableProps } from 'antd'; | import type { TableProps } from 'antd'; | ||||
| import { Space, Table, Tooltip } from 'antd'; | |||||
| import { Collapse, Space, Table, Tooltip } from 'antd'; | |||||
| import { BeginQuery } from '../../interface'; | import { BeginQuery } from '../../interface'; | ||||
| import { useTranslation } from 'react-i18next'; | |||||
| import styles from './index.less'; | |||||
| interface IProps { | interface IProps { | ||||
| data: BeginQuery[]; | data: BeginQuery[]; | ||||
| deleteRecord(index: number): void; | deleteRecord(index: number): void; | ||||
| } | } | ||||
| const QueryTable = ({ data, deleteRecord, showModal }: IProps) => { | const QueryTable = ({ data, deleteRecord, showModal }: IProps) => { | ||||
| const { t } = useTranslation(); | |||||
| const columns: TableProps<BeginQuery>['columns'] = [ | const columns: TableProps<BeginQuery>['columns'] = [ | ||||
| { | { | ||||
| title: 'Key', | title: 'Key', | ||||
| ), | ), | ||||
| }, | }, | ||||
| { | { | ||||
| title: 'Name', | |||||
| title: t('flow.name'), | |||||
| dataIndex: 'name', | dataIndex: 'name', | ||||
| key: 'name', | key: 'name', | ||||
| ellipsis: { | ellipsis: { | ||||
| ), | ), | ||||
| }, | }, | ||||
| { | { | ||||
| title: 'Type', | |||||
| title: t('flow.type'), | |||||
| dataIndex: 'type', | dataIndex: 'type', | ||||
| key: 'type', | key: 'type', | ||||
| }, | }, | ||||
| { | { | ||||
| title: 'Optional', | |||||
| title: t('flow.optional'), | |||||
| dataIndex: 'optional', | dataIndex: 'optional', | ||||
| key: 'optional', | key: 'optional', | ||||
| render: (optional) => (optional ? 'Yes' : 'No'), | render: (optional) => (optional ? 'Yes' : 'No'), | ||||
| }, | }, | ||||
| { | { | ||||
| title: 'Action', | |||||
| title: t('common.action'), | |||||
| key: 'action', | key: 'action', | ||||
| render: (_, record, idx) => ( | render: (_, record, idx) => ( | ||||
| <Space> | <Space> | ||||
| ]; | ]; | ||||
| return ( | 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} | |||||
| /> | |||||
| ), | |||||
| }, | |||||
| ]} | |||||
| /> | |||||
| ); | ); | ||||
| }; | }; | ||||
| import { MinusCircleOutlined, PlusOutlined } from '@ant-design/icons'; | import { MinusCircleOutlined, PlusOutlined } from '@ant-design/icons'; | ||||
| import { Button, Collapse, Flex, Form, Input, Select } from 'antd'; | import { Button, Collapse, Flex, Form, Input, Select } from 'antd'; | ||||
| import { useCallback } from 'react'; | |||||
| import { PropsWithChildren, useCallback } from 'react'; | |||||
| import { useTranslation } from 'react-i18next'; | import { useTranslation } from 'react-i18next'; | ||||
| import { useBuildComponentIdSelectOptions } from '../../hooks'; | import { useBuildComponentIdSelectOptions } from '../../hooks'; | ||||
| import styles from './index.less'; | import styles from './index.less'; | ||||
| ); | ); | ||||
| }; | }; | ||||
| const DynamicInputVariable = ({ nodeId }: IProps) => { | |||||
| const { t } = useTranslation(); | |||||
| export function FormCollapse({ | |||||
| children, | |||||
| title, | |||||
| }: PropsWithChildren<{ title: string }>) { | |||||
| return ( | return ( | ||||
| <Collapse | <Collapse | ||||
| className={styles.dynamicInputVariable} | className={styles.dynamicInputVariable} | ||||
| items={[ | items={[ | ||||
| { | { | ||||
| key: '1', | 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; | export default DynamicInputVariable; |
| import { useFetchFlow } from '@/hooks/flow-hooks'; | import { useFetchFlow } from '@/hooks/flow-hooks'; | ||||
| import { ArrowLeftOutlined } from '@ant-design/icons'; | import { ArrowLeftOutlined } from '@ant-design/icons'; | ||||
| import { Button, Flex, Space } from 'antd'; | import { Button, Flex, Space } from 'antd'; | ||||
| import { useCallback } from 'react'; | |||||
| import { Link, useParams } from 'umi'; | import { Link, useParams } from 'umi'; | ||||
| import FlowIdModal from '../flow-id-modal'; | import FlowIdModal from '../flow-id-modal'; | ||||
| import { | import { | ||||
| useGetBeginNodeDataQuery, | |||||
| useSaveGraph, | useSaveGraph, | ||||
| useSaveGraphBeforeOpeningDebugDrawer, | useSaveGraphBeforeOpeningDebugDrawer, | ||||
| useWatchAgentChange, | useWatchAgentChange, | ||||
| } from '../hooks'; | } from '../hooks'; | ||||
| import { BeginQuery } from '../interface'; | |||||
| import styles from './index.less'; | import styles from './index.less'; | ||||
| interface IProps { | interface IProps { | ||||
| const FlowHeader = ({ showChatDrawer, chatDrawerVisible }: IProps) => { | const FlowHeader = ({ showChatDrawer, chatDrawerVisible }: IProps) => { | ||||
| const { saveGraph } = useSaveGraph(); | const { saveGraph } = useSaveGraph(); | ||||
| const handleRun = useSaveGraphBeforeOpeningDebugDrawer(showChatDrawer); | |||||
| const { handleRun } = useSaveGraphBeforeOpeningDebugDrawer(showChatDrawer); | |||||
| const { data } = useFetchFlow(); | const { data } = useFetchFlow(); | ||||
| const { t } = useTranslate('flow'); | const { t } = useTranslate('flow'); | ||||
| const { | const { | ||||
| const { visible, hideModal, showModal } = useSetModalState(); | const { visible, hideModal, showModal } = useSetModalState(); | ||||
| const { id } = useParams(); | const { id } = useParams(); | ||||
| const time = useWatchAgentChange(chatDrawerVisible); | 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 ( | return ( | ||||
| <> | <> | ||||
| </div> | </div> | ||||
| </Space> | </Space> | ||||
| <Space size={'large'}> | <Space size={'large'}> | ||||
| <Button onClick={handleRun}> | |||||
| <Button onClick={handleRunAgent}> | |||||
| <b>{t('run')}</b> | <b>{t('run')}</b> | ||||
| </Button> | </Button> | ||||
| <Button type="primary" onClick={saveGraph}> | |||||
| <Button type="primary" onClick={() => saveGraph()}> | |||||
| <b>{t('save')}</b> | <b>{t('save')}</b> | ||||
| </Button> | </Button> | ||||
| {/* <Button type="primary" onClick={showOverviewModal} disabled> | {/* <Button type="primary" onClick={showOverviewModal} disabled> |
| import api from '@/utils/api'; | import api from '@/utils/api'; | ||||
| import { useDebounceEffect } from 'ahooks'; | import { useDebounceEffect } from 'ahooks'; | ||||
| import { FormInstance, message } from 'antd'; | import { FormInstance, message } from 'antd'; | ||||
| import { DefaultOptionType } from 'antd/es/select'; | |||||
| import dayjs from 'dayjs'; | import dayjs from 'dayjs'; | ||||
| import { humanId } from 'human-id'; | import { humanId } from 'human-id'; | ||||
| import { get, lowerFirst } from 'lodash'; | import { get, lowerFirst } from 'lodash'; | ||||
| initialWikipediaValues, | initialWikipediaValues, | ||||
| initialYahooFinanceValues, | initialYahooFinanceValues, | ||||
| } from './constant'; | } from './constant'; | ||||
| import { ICategorizeForm, IRelevantForm, ISwitchForm } from './interface'; | |||||
| import { | |||||
| BeginQuery, | |||||
| ICategorizeForm, | |||||
| IRelevantForm, | |||||
| ISwitchForm, | |||||
| } from './interface'; | |||||
| import useGraphStore, { RFState } from './store'; | import useGraphStore, { RFState } from './store'; | ||||
| import { | import { | ||||
| buildDslComponentsByGraph, | buildDslComponentsByGraph, | ||||
| return { onDrop, onDragOver, setReactFlowInstance }; | return { onDrop, onDragOver, setReactFlowInstance }; | ||||
| }; | }; | ||||
| export const useShowDrawer = () => { | |||||
| export const useShowFormDrawer = () => { | |||||
| const { | const { | ||||
| clickedNodeId: clickNodeId, | clickedNodeId: clickNodeId, | ||||
| setClickedNodeId, | setClickedNodeId, | ||||
| getNode, | getNode, | ||||
| } = useGraphStore((state) => state); | } = useGraphStore((state) => state); | ||||
| const { | const { | ||||
| visible: drawerVisible, | |||||
| hideModal: hideDrawer, | |||||
| showModal: showDrawer, | |||||
| visible: formDrawerVisible, | |||||
| hideModal: hideFormDrawer, | |||||
| showModal: showFormDrawer, | |||||
| } = useSetModalState(); | } = useSetModalState(); | ||||
| const handleShow = useCallback( | const handleShow = useCallback( | ||||
| (node: Node) => { | (node: Node) => { | ||||
| setClickedNodeId(node.id); | setClickedNodeId(node.id); | ||||
| showDrawer(); | |||||
| showFormDrawer(); | |||||
| }, | }, | ||||
| [showDrawer, setClickedNodeId], | |||||
| [showFormDrawer, setClickedNodeId], | |||||
| ); | ); | ||||
| return { | return { | ||||
| drawerVisible, | |||||
| hideDrawer, | |||||
| showDrawer: handleShow, | |||||
| formDrawerVisible, | |||||
| hideFormDrawer, | |||||
| showFormDrawer: handleShow, | |||||
| clickedNode: getNode(clickNodeId), | clickedNode: getNode(clickNodeId), | ||||
| }; | }; | ||||
| }; | }; | ||||
| export const useSaveGraph = () => { | export const useSaveGraph = () => { | ||||
| const { data } = useFetchFlow(); | const { data } = useFetchFlow(); | ||||
| const { setFlow } = useSetFlow(); | |||||
| const { setFlow, loading } = useSetFlow(); | |||||
| const { id } = useParams(); | const { id } = useParams(); | ||||
| const { nodes, edges } = useGraphStore((state) => state); | 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) => { | export const useHandleFormValuesChange = (id?: string) => { | ||||
| return { name, handleNameBlur, handleNameChange }; | 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) => { | export const useSaveGraphBeforeOpeningDebugDrawer = (show: () => void) => { | ||||
| const { id } = useParams(); | const { id } = useParams(); | ||||
| const { saveGraph } = useSaveGraph(); | |||||
| const { saveGraph, loading } = useSaveGraph(); | |||||
| const { resetFlow } = useResetFlow(); | const { resetFlow } = useResetFlow(); | ||||
| const { refetch } = useFetchFlow(); | const { refetch } = useFetchFlow(); | ||||
| const { send } = useSendMessageWithSse(api.runCanvas); | 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 = () => { | export const useReplaceIdWithName = () => { | ||||
| export const useBuildComponentIdSelectOptions = (nodeId?: string) => { | export const useBuildComponentIdSelectOptions = (nodeId?: string) => { | ||||
| const nodes = useGraphStore((state) => state.nodes); | const nodes = useGraphStore((state) => state.nodes); | ||||
| const getBeginNodeDataQuery = useGetBeginNodeDataQuery(); | |||||
| const query: BeginQuery[] = getBeginNodeDataQuery(); | |||||
| const options = useMemo(() => { | |||||
| const componentIdOptions = useMemo(() => { | |||||
| return nodes | return nodes | ||||
| .filter( | .filter( | ||||
| (x) => | (x) => | ||||
| .map((x) => ({ label: x.data.name, value: x.id })); | .map((x) => ({ label: x.data.name, value: x.id })); | ||||
| }, [nodes, nodeId]); | }, [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) => { | export const useGetComponentLabelByValue = (nodeId: string) => { | ||||
| const options = useBuildComponentIdSelectOptions(nodeId); | const options = useBuildComponentIdSelectOptions(nodeId); | ||||
| const flattenOptions = useMemo( | |||||
| () => | |||||
| options.reduce<DefaultOptionType[]>((pre, cur) => { | |||||
| return [...pre, ...cur.options]; | |||||
| }, []), | |||||
| [options], | |||||
| ); | |||||
| const getLabel = useCallback( | const getLabel = useCallback( | ||||
| (val?: string) => { | (val?: string) => { | ||||
| return options.find((x) => x.value === val)?.label; | |||||
| return flattenOptions.find((x) => x.value === val)?.label; | |||||
| }, | }, | ||||
| [options], | |||||
| [flattenOptions], | |||||
| ); | ); | ||||
| return getLabel; | return getLabel; | ||||
| }; | }; |
| ></FlowHeader> | ></FlowHeader> | ||||
| <Content style={{ margin: 0 }}> | <Content style={{ margin: 0 }}> | ||||
| <FlowCanvas | <FlowCanvas | ||||
| chatDrawerVisible={chatDrawerVisible} | |||||
| hideChatDrawer={hideChatDrawer} | |||||
| drawerVisible={chatDrawerVisible} | |||||
| hideDrawer={hideChatDrawer} | |||||
| ></FlowCanvas> | ></FlowCanvas> | ||||
| </Content> | </Content> | ||||
| </Layout> | </Layout> |
| .formWrapper { | |||||
| :global(.ant-form-item-label) { | |||||
| font-weight: 600 !important; | |||||
| } | |||||
| } |
| 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; |
| 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> | |||||
| ); | |||||
| }; |
| nodeId: string, | nodeId: string, | ||||
| values: any, | values: any, | ||||
| path?: (string | number)[], | path?: (string | number)[], | ||||
| ) => void; | |||||
| ) => Node[]; | |||||
| onSelectionChange: OnSelectionChangeFunc; | onSelectionChange: OnSelectionChangeFunc; | ||||
| addNode: (nodes: Node) => void; | addNode: (nodes: Node) => void; | ||||
| getNode: (id?: string | null) => Node<NodeData> | undefined; | getNode: (id?: string | null) => Node<NodeData> | undefined; | ||||
| values: any, | values: any, | ||||
| path: (string | number)[] = [], | 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) => { | updateSwitchFormData: (source, sourceHandle, target) => { | ||||
| const { updateNodeForm } = get(); | const { updateNodeForm } = get(); |
| web_crawl: `${api_host}/document/web_crawl`, | web_crawl: `${api_host}/document/web_crawl`, | ||||
| document_infos: `${api_host}/document/infos`, | document_infos: `${api_host}/document/infos`, | ||||
| upload_and_parse: `${api_host}/document/upload_and_parse`, | upload_and_parse: `${api_host}/document/upload_and_parse`, | ||||
| parse: `${api_host}/document/parse`, | |||||
| // chat | // chat | ||||
| setDialog: `${api_host}/dialog/set`, | setDialog: `${api_host}/dialog/set`, |
| }); | }); | ||||
| request.interceptors.response.use(async (response: any, options) => { | 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') { | if (options.responseType === 'blob') { |
| ring: 'hsl(var(--ring))', | ring: 'hsl(var(--ring))', | ||||
| background: 'var(--background)', | background: 'var(--background)', | ||||
| foreground: 'hsl(var(--foreground))', | foreground: 'hsl(var(--foreground))', | ||||
| buttonBlueText: 'var(--button-blue-text)', | |||||
| primary: { | primary: { | ||||
| DEFAULT: 'hsl(var(--primary))', | DEFAULT: 'hsl(var(--primary))', | ||||
| foreground: 'hsl(var(--primary-foreground))', | foreground: 'hsl(var(--primary-foreground))', |
| --background-inverse-standard: rgba(58, 56, 65, 0.15); | --background-inverse-standard: rgba(58, 56, 65, 0.15); | ||||
| --background-inverse-standard-foreground: rgb(92, 81, 81); | --background-inverse-standard-foreground: rgb(92, 81, 81); | ||||
| --button-blue-text: rgb(22, 119, 255); | |||||
| } | } | ||||
| .dark { | .dark { |