From 9c9f2dbe3f6c5e52c2ee97dff066b7bf3bed9d35 Mon Sep 17 00:00:00 2001 From: balibabu Date: Tue, 25 Feb 2025 11:32:01 +0800 Subject: [PATCH] Feat: Add FormDrawer to agent page. #3221 (#5323) ### What problem does this PR solve? Feat: Add FormDrawer to agent page. #3221 ### Type of change - [x] New Feature (non-breaking change which adds functionality) --- web/src/pages/agent/canvas/index.tsx | 60 +---- web/src/pages/agent/context.ts | 6 + web/src/pages/agent/debug-content/index.less | 5 + web/src/pages/agent/debug-content/index.tsx | 238 ++++++++++++++++++ .../agent/debug-content/popover-form.tsx | 74 ++++++ web/src/pages/agent/form-drawer/index.less | 21 ++ web/src/pages/agent/form-drawer/index.tsx | 216 ++++++++++++++++ .../form-drawer/single-debug-drawer/index.tsx | 81 ++++++ web/src/pages/agent/form-hooks.ts | 77 ++++++ .../pages/agent/form/akshare-form/index.tsx | 21 ++ .../pages/agent/form/answer-form/index.tsx | 5 + web/src/pages/agent/form/arxiv-form/index.tsx | 36 +++ .../agent/form/baidu-fanyi-form/index.tsx | 71 ++++++ web/src/pages/agent/form/baidu-form/index.tsx | 21 ++ .../form/begin-form/begin-dynamic-options.tsx | 68 +++++ web/src/pages/agent/form/begin-form/hooks.ts | 50 ++++ .../pages/agent/form/begin-form/index.less | 24 ++ web/src/pages/agent/form/begin-form/index.tsx | 111 ++++++++ .../agent/form/begin-form/paramater-modal.tsx | 124 +++++++++ .../agent/form/begin-form/query-table.tsx | 92 +++++++ web/src/pages/agent/form/bing-form/index.tsx | 42 ++++ .../categorize-form/dynamic-categorize.tsx | 225 +++++++++++++++++ .../pages/agent/form/categorize-form/hooks.ts | 45 ++++ .../agent/form/categorize-form/index.less | 13 + .../agent/form/categorize-form/index.tsx | 43 ++++ .../components/dynamic-input-variable.tsx | 130 ++++++++++ .../pages/agent/form/components/index.less | 22 ++ .../agent/form/concentrator-form/index.tsx | 17 ++ .../pages/agent/form/crawler-form/index.tsx | 38 +++ web/src/pages/agent/form/deepl-form/index.tsx | 36 +++ .../agent/form/duckduckgo-form/index.tsx | 38 +++ web/src/pages/agent/form/email-form/index.tsx | 53 ++++ .../pages/agent/form/exesql-form/index.tsx | 88 +++++++ .../form/generate-form/dynamic-parameters.tsx | 101 ++++++++ .../pages/agent/form/generate-form/hooks.ts | 70 ++++++ .../pages/agent/form/generate-form/index.less | 21 ++ .../pages/agent/form/generate-form/index.tsx | 57 +++++ .../pages/agent/form/github-form/index.tsx | 21 ++ .../pages/agent/form/google-form/index.tsx | 34 +++ .../agent/form/google-scholar-form/index.tsx | 75 ++++++ .../form/invoke-form/dynamic-variables.tsx | 130 ++++++++++ web/src/pages/agent/form/invoke-form/hooks.ts | 97 +++++++ .../pages/agent/form/invoke-form/index.less | 44 ++++ .../pages/agent/form/invoke-form/index.tsx | 78 ++++++ .../pages/agent/form/iteration-from/index.tsx | 94 +++++++ web/src/pages/agent/form/jin10-form/index.tsx | 145 +++++++++++ .../agent/form/keyword-extract-form/index.tsx | 32 +++ .../pages/agent/form/message-form/index.less | 16 ++ .../pages/agent/form/message-form/index.tsx | 87 +++++++ .../pages/agent/form/pubmed-form/index.tsx | 32 +++ .../pages/agent/form/qweather-form/index.tsx | 88 +++++++ .../pages/agent/form/relevant-form/hooks.ts | 53 ++++ .../pages/agent/form/relevant-form/index.tsx | 49 ++++ .../pages/agent/form/retrieval-form/index.tsx | 54 ++++ .../form/rewrite-question-form/index.tsx | 41 +++ .../pages/agent/form/switch-form/index.less | 21 ++ .../pages/agent/form/switch-form/index.tsx | 204 +++++++++++++++ .../pages/agent/form/template-form/index.tsx | 24 ++ .../pages/agent/form/tushare-form/index.tsx | 83 ++++++ .../pages/agent/form/wencai-form/index.tsx | 36 +++ .../pages/agent/form/wikipedia-form/index.tsx | 28 +++ .../agent/form/yahoo-finance-form/index.tsx | 40 +++ web/src/pages/agent/index.tsx | 29 ++- 63 files changed, 4005 insertions(+), 70 deletions(-) create mode 100644 web/src/pages/agent/context.ts create mode 100644 web/src/pages/agent/debug-content/index.less create mode 100644 web/src/pages/agent/debug-content/index.tsx create mode 100644 web/src/pages/agent/debug-content/popover-form.tsx create mode 100644 web/src/pages/agent/form-drawer/index.less create mode 100644 web/src/pages/agent/form-drawer/index.tsx create mode 100644 web/src/pages/agent/form-drawer/single-debug-drawer/index.tsx create mode 100644 web/src/pages/agent/form-hooks.ts create mode 100644 web/src/pages/agent/form/akshare-form/index.tsx create mode 100644 web/src/pages/agent/form/answer-form/index.tsx create mode 100644 web/src/pages/agent/form/arxiv-form/index.tsx create mode 100644 web/src/pages/agent/form/baidu-fanyi-form/index.tsx create mode 100644 web/src/pages/agent/form/baidu-form/index.tsx create mode 100644 web/src/pages/agent/form/begin-form/begin-dynamic-options.tsx create mode 100644 web/src/pages/agent/form/begin-form/hooks.ts create mode 100644 web/src/pages/agent/form/begin-form/index.less create mode 100644 web/src/pages/agent/form/begin-form/index.tsx create mode 100644 web/src/pages/agent/form/begin-form/paramater-modal.tsx create mode 100644 web/src/pages/agent/form/begin-form/query-table.tsx create mode 100644 web/src/pages/agent/form/bing-form/index.tsx create mode 100644 web/src/pages/agent/form/categorize-form/dynamic-categorize.tsx create mode 100644 web/src/pages/agent/form/categorize-form/hooks.ts create mode 100644 web/src/pages/agent/form/categorize-form/index.less create mode 100644 web/src/pages/agent/form/categorize-form/index.tsx create mode 100644 web/src/pages/agent/form/components/dynamic-input-variable.tsx create mode 100644 web/src/pages/agent/form/components/index.less create mode 100644 web/src/pages/agent/form/concentrator-form/index.tsx create mode 100644 web/src/pages/agent/form/crawler-form/index.tsx create mode 100644 web/src/pages/agent/form/deepl-form/index.tsx create mode 100644 web/src/pages/agent/form/duckduckgo-form/index.tsx create mode 100644 web/src/pages/agent/form/email-form/index.tsx create mode 100644 web/src/pages/agent/form/exesql-form/index.tsx create mode 100644 web/src/pages/agent/form/generate-form/dynamic-parameters.tsx create mode 100644 web/src/pages/agent/form/generate-form/hooks.ts create mode 100644 web/src/pages/agent/form/generate-form/index.less create mode 100644 web/src/pages/agent/form/generate-form/index.tsx create mode 100644 web/src/pages/agent/form/github-form/index.tsx create mode 100644 web/src/pages/agent/form/google-form/index.tsx create mode 100644 web/src/pages/agent/form/google-scholar-form/index.tsx create mode 100644 web/src/pages/agent/form/invoke-form/dynamic-variables.tsx create mode 100644 web/src/pages/agent/form/invoke-form/hooks.ts create mode 100644 web/src/pages/agent/form/invoke-form/index.less create mode 100644 web/src/pages/agent/form/invoke-form/index.tsx create mode 100644 web/src/pages/agent/form/iteration-from/index.tsx create mode 100644 web/src/pages/agent/form/jin10-form/index.tsx create mode 100644 web/src/pages/agent/form/keyword-extract-form/index.tsx create mode 100644 web/src/pages/agent/form/message-form/index.less create mode 100644 web/src/pages/agent/form/message-form/index.tsx create mode 100644 web/src/pages/agent/form/pubmed-form/index.tsx create mode 100644 web/src/pages/agent/form/qweather-form/index.tsx create mode 100644 web/src/pages/agent/form/relevant-form/hooks.ts create mode 100644 web/src/pages/agent/form/relevant-form/index.tsx create mode 100644 web/src/pages/agent/form/retrieval-form/index.tsx create mode 100644 web/src/pages/agent/form/rewrite-question-form/index.tsx create mode 100644 web/src/pages/agent/form/switch-form/index.less create mode 100644 web/src/pages/agent/form/switch-form/index.tsx create mode 100644 web/src/pages/agent/form/template-form/index.tsx create mode 100644 web/src/pages/agent/form/tushare-form/index.tsx create mode 100644 web/src/pages/agent/form/wencai-form/index.tsx create mode 100644 web/src/pages/agent/form/wikipedia-form/index.tsx create mode 100644 web/src/pages/agent/form/yahoo-finance-form/index.tsx diff --git a/web/src/pages/agent/canvas/index.tsx b/web/src/pages/agent/canvas/index.tsx index 80129c28c..7503b5eaf 100644 --- a/web/src/pages/agent/canvas/index.tsx +++ b/web/src/pages/agent/canvas/index.tsx @@ -1,20 +1,12 @@ -import { - Tooltip, - TooltipContent, - TooltipTrigger, -} from '@/components/ui/tooltip'; import { Background, ConnectionMode, - ControlButton, - Controls, NodeTypes, ReactFlow, } from '@xyflow/react'; import '@xyflow/react/dist/style.css'; -import { Book, FolderInput, FolderOutput } from 'lucide-react'; // import ChatDrawer from '../chat/drawer'; -// import FormDrawer from '../flow-drawer'; +import FormDrawer from '../form-drawer'; import { useHandleDrop, useSelectCanvasData, @@ -22,10 +14,7 @@ import { useWatchNodeFormDataChange, } from '../hooks'; import { useBeforeDelete } from '../hooks/use-before-delete'; -import { useHandleExportOrImportJsonFile } from '../hooks/use-export-json'; -import { useOpenDocument } from '../hooks/use-open-document'; import { useShowDrawer } from '../hooks/use-show-drawer'; -// import JsonUploadModal from '../json-upload-modal'; // import RunDrawer from '../run-drawer'; import { ButtonEdge } from './edge'; import styles from './index.less'; @@ -88,16 +77,6 @@ function FlowCanvas({ drawerVisible, hideDrawer }: IProps) { const { onDrop, onDragOver, setReactFlowInstance } = useHandleDrop(); - const { - handleExportJson, - handleImportJson, - fileUploadVisible, - onFileUploadOk, - hideFileUploadModal, - } = useHandleExportOrImportJsonFile(); - - const openDocument = useOpenDocument(); - const { onNodeClick, onPaneClick, @@ -173,34 +152,8 @@ function FlowCanvas({ drawerVisible, hideDrawer }: IProps) { onBeforeDelete={handleBeforeDelete} > - - - - - - - Import - - - - - - - - Export - - - - - - - - Document - - - - {/* {formDrawerVisible && ( + {formDrawerVisible && ( - )} */} + )} {/* {chatVisible && ( - )} - {fileUploadVisible && ( - )} */} ); diff --git a/web/src/pages/agent/context.ts b/web/src/pages/agent/context.ts new file mode 100644 index 000000000..fe51d8d6d --- /dev/null +++ b/web/src/pages/agent/context.ts @@ -0,0 +1,6 @@ +import { RAGFlowNodeType } from '@/interfaces/database/flow'; +import { createContext } from 'react'; + +export const FlowFormContext = createContext( + undefined, +); diff --git a/web/src/pages/agent/debug-content/index.less b/web/src/pages/agent/debug-content/index.less new file mode 100644 index 000000000..fda707810 --- /dev/null +++ b/web/src/pages/agent/debug-content/index.less @@ -0,0 +1,5 @@ +.formWrapper { + :global(.ant-form-item-label) { + font-weight: 600 !important; + } +} diff --git a/web/src/pages/agent/debug-content/index.tsx b/web/src/pages/agent/debug-content/index.tsx new file mode 100644 index 000000000..f5493c764 --- /dev/null +++ b/web/src/pages/agent/debug-content/index.tsx @@ -0,0 +1,238 @@ +import { Authorization } from '@/constants/authorization'; +import { useSetModalState } from '@/hooks/common-hooks'; +import { useSetSelectedRecord } from '@/hooks/logic-hooks'; +import { useHandleSubmittable } from '@/hooks/login-hooks'; +import api from '@/utils/api'; +import { getAuthorization } from '@/utils/authorization-util'; +import { UploadOutlined } from '@ant-design/icons'; +import { + Button, + Form, + FormItemProps, + Input, + InputNumber, + Select, + Switch, + Upload, +} from 'antd'; +import { UploadChangeParam, UploadFile } from 'antd/es/upload'; +import { pick } from 'lodash'; +import { Link } from 'lucide-react'; +import React, { useCallback, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { BeginQueryType } from '../constant'; +import { BeginQuery } from '../interface'; +import { PopoverForm } from './popover-form'; + +import styles from './index.less'; + +interface IProps { + parameters: BeginQuery[]; + ok(parameters: any[]): void; + isNext?: boolean; + loading?: boolean; + submitButtonDisabled?: boolean; +} + +const DebugContent = ({ + parameters, + ok, + isNext = true, + loading = false, + submitButtonDisabled = false, +}: IProps) => { + const { t } = useTranslation(); + const [form] = Form.useForm(); + const { + visible, + hideModal: hidePopover, + switchVisible, + showModal: showPopover, + } = useSetModalState(); + const { setRecord, currentRecord } = useSetSelectedRecord(); + const { submittable } = useHandleSubmittable(form); + const [isUploading, setIsUploading] = useState(false); + + const handleShowPopover = useCallback( + (idx: number) => () => { + setRecord(idx); + showPopover(); + }, + [setRecord, showPopover], + ); + + const normFile = (e: any) => { + if (Array.isArray(e)) { + return e; + } + return e?.fileList; + }; + + const onChange = useCallback( + (optional: boolean) => + ({ fileList }: UploadChangeParam) => { + if (!optional) { + setIsUploading(fileList.some((x) => x.status === 'uploading')); + } + }, + [], + ); + + const renderWidget = useCallback( + (q: BeginQuery, idx: number) => { + const props: FormItemProps & { key: number } = { + key: idx, + label: q.name ?? q.key, + name: idx, + }; + if (q.optional === false) { + props.rules = [{ required: true }]; + } + + const urlList: { url: string; result: string }[] = + form.getFieldValue(idx) || []; + + const BeginQueryTypeMap = { + [BeginQueryType.Line]: ( + + + + ), + [BeginQueryType.Paragraph]: ( + + + + ), + [BeginQueryType.Options]: ( + + + + ), + [BeginQueryType.File]: ( + + +
+ + + + + + 0 ? 'mb-1' : ''} + noStyle + > + + + + +
+
+ +
+ ), + [BeginQueryType.Integer]: ( + + + + ), + [BeginQueryType.Boolean]: ( + + + + ), + }; + + return ( + BeginQueryTypeMap[q.type as BeginQueryType] ?? + BeginQueryTypeMap[BeginQueryType.Paragraph] + ); + }, + [form, handleShowPopover, onChange, switchVisible, t, visible], + ); + + const onOk = useCallback(async () => { + const values = await form.validateFields(); + const nextValues = Object.entries(values).map(([key, value]) => { + const item = parameters[Number(key)]; + let nextValue = value; + if (Array.isArray(value)) { + nextValue = ``; + + value.forEach((x) => { + nextValue += + x?.originFileObj instanceof File + ? `${x.name}\n${x.response?.data}\n----\n` + : `${x.url}\n${x.result}\n----\n`; + }); + } + return { ...item, value: nextValue }; + }); + + ok(nextValues); + }, [form, ok, parameters]); + + return ( + <> +
+ { + if (name === 'urlForm') { + const { basicForm } = forms; + const urlInfo = basicForm.getFieldValue(currentRecord) || []; + basicForm.setFieldsValue({ + [currentRecord]: [...urlInfo, { ...values, name: values.url }], + }); + hidePopover(); + } + }} + > +
+ {parameters.map((x, idx) => { + return renderWidget(x, idx); + })} +
+
+
+ + + ); +}; + +export default DebugContent; diff --git a/web/src/pages/agent/debug-content/popover-form.tsx b/web/src/pages/agent/debug-content/popover-form.tsx new file mode 100644 index 000000000..557e3185b --- /dev/null +++ b/web/src/pages/agent/debug-content/popover-form.tsx @@ -0,0 +1,74 @@ +import { useParseDocument } from '@/hooks/document-hooks'; +import { useResetFormOnCloseModal } from '@/hooks/logic-hooks'; +import { IModalProps } from '@/interfaces/common'; +import { Button, Form, Input, Popover } from 'antd'; +import { PropsWithChildren } from 'react'; +import { useTranslation } from 'react-i18next'; + +const reg = + /^(((ht|f)tps?):\/\/)?([^!@#$%^&*?.\s-]([^!@#$%^&*?.\s]{0,63}[^!@#$%^&*?.\s])?\.)+[a-z]{2,6}\/?/; + +export const PopoverForm = ({ + children, + visible, + switchVisible, +}: PropsWithChildren>) => { + 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 = ( +
+ + e.preventDefault()} + placeholder={t('flow.pasteFileLink')} + suffix={ + + } + /> + + + + ); + + return ( + + {children} + + ); +}; diff --git a/web/src/pages/agent/form-drawer/index.less b/web/src/pages/agent/form-drawer/index.less new file mode 100644 index 000000000..1348cc66d --- /dev/null +++ b/web/src/pages/agent/form-drawer/index.less @@ -0,0 +1,21 @@ +.title { + flex-basis: 60px; +} + +.formWrapper { + :global(.ant-form-item-label) { + font-weight: 600; + } +} + +.operatorDescription { + font-size: 14px; + padding-top: 16px; + font-weight: normal; +} + +.formDrawer { + :global(.ant-drawer-content-wrapper) { + transform: translateX(0) !important; + } +} diff --git a/web/src/pages/agent/form-drawer/index.tsx b/web/src/pages/agent/form-drawer/index.tsx new file mode 100644 index 000000000..40afa61f7 --- /dev/null +++ b/web/src/pages/agent/form-drawer/index.tsx @@ -0,0 +1,216 @@ +import { useTranslate } from '@/hooks/common-hooks'; +import { IModalProps } from '@/interfaces/common'; +import { CloseOutlined } from '@ant-design/icons'; +import { Drawer, Flex, Form, Input } from 'antd'; +import { get, isPlainObject, lowerFirst } from 'lodash'; +import { Play } from 'lucide-react'; +import { useEffect, useRef } from 'react'; +import { BeginId, Operator, operatorMap } from '../constant'; +import AkShareForm from '../form/akshare-form'; +import AnswerForm from '../form/answer-form'; +import ArXivForm from '../form/arxiv-form'; +import BaiduFanyiForm from '../form/baidu-fanyi-form'; +import BaiduForm from '../form/baidu-form'; +import BeginForm from '../form/begin-form'; +import BingForm from '../form/bing-form'; +import CategorizeForm from '../form/categorize-form'; +import CrawlerForm from '../form/crawler-form'; +import DeepLForm from '../form/deepl-form'; +import DuckDuckGoForm from '../form/duckduckgo-form'; +import EmailForm from '../form/email-form'; +import ExeSQLForm from '../form/exesql-form'; +import GenerateForm from '../form/generate-form'; +import GithubForm from '../form/github-form'; +import GoogleForm from '../form/google-form'; +import GoogleScholarForm from '../form/google-scholar-form'; +import InvokeForm from '../form/invoke-form'; +import Jin10Form from '../form/jin10-form'; +import KeywordExtractForm from '../form/keyword-extract-form'; +import MessageForm from '../form/message-form'; +import PubMedForm from '../form/pubmed-form'; +import QWeatherForm from '../form/qweather-form'; +import RelevantForm from '../form/relevant-form'; +import RetrievalForm from '../form/retrieval-form'; +import RewriteQuestionForm from '../form/rewrite-question-form'; +import SwitchForm from '../form/switch-form'; +import TemplateForm from '../form/template-form'; +import TuShareForm from '../form/tushare-form'; +import WenCaiForm from '../form/wencai-form'; +import WikipediaForm from '../form/wikipedia-form'; +import YahooFinanceForm from '../form/yahoo-finance-form'; +import { useHandleFormValuesChange, useHandleNodeNameChange } from '../hooks'; +import OperatorIcon from '../operator-icon'; +import { + buildCategorizeListFromObject, + getDrawerWidth, + needsSingleStepDebugging, +} from '../utils'; +import SingleDebugDrawer from './single-debug-drawer'; + +import { RAGFlowNodeType } from '@/interfaces/database/flow'; +import { FlowFormContext } from '../context'; +import { RunTooltip } from '../flow-tooltip'; +import IterationForm from '../form/iteration-from'; +import styles from './index.less'; + +interface IProps { + node?: RAGFlowNodeType; + singleDebugDrawerVisible: IModalProps['visible']; + hideSingleDebugDrawer: IModalProps['hideModal']; + showSingleDebugDrawer: IModalProps['showModal']; +} + +const FormMap = { + [Operator.Begin]: BeginForm, + [Operator.Retrieval]: RetrievalForm, + [Operator.Generate]: GenerateForm, + [Operator.Answer]: AnswerForm, + [Operator.Categorize]: CategorizeForm, + [Operator.Message]: MessageForm, + [Operator.Relevant]: RelevantForm, + [Operator.RewriteQuestion]: RewriteQuestionForm, + [Operator.Baidu]: BaiduForm, + [Operator.DuckDuckGo]: DuckDuckGoForm, + [Operator.KeywordExtract]: KeywordExtractForm, + [Operator.Wikipedia]: WikipediaForm, + [Operator.PubMed]: PubMedForm, + [Operator.ArXiv]: ArXivForm, + [Operator.Google]: GoogleForm, + [Operator.Bing]: BingForm, + [Operator.GoogleScholar]: GoogleScholarForm, + [Operator.DeepL]: DeepLForm, + [Operator.GitHub]: GithubForm, + [Operator.BaiduFanyi]: BaiduFanyiForm, + [Operator.QWeather]: QWeatherForm, + [Operator.ExeSQL]: ExeSQLForm, + [Operator.Switch]: SwitchForm, + [Operator.WenCai]: WenCaiForm, + [Operator.AkShare]: AkShareForm, + [Operator.YahooFinance]: YahooFinanceForm, + [Operator.Jin10]: Jin10Form, + [Operator.TuShare]: TuShareForm, + [Operator.Crawler]: CrawlerForm, + [Operator.Invoke]: InvokeForm, + [Operator.Concentrator]: () => <>, + [Operator.Note]: () => <>, + [Operator.Template]: TemplateForm, + [Operator.Email]: EmailForm, + [Operator.Iteration]: IterationForm, + [Operator.IterationStart]: () => <>, +}; + +const EmptyContent = () =>
; + +const FormDrawer = ({ + visible, + hideModal, + node, + singleDebugDrawerVisible, + hideSingleDebugDrawer, + showSingleDebugDrawer, +}: IModalProps & IProps) => { + const operatorName: Operator = node?.data.label as Operator; + const OperatorForm = FormMap[operatorName] ?? EmptyContent; + const [form] = Form.useForm(); + const { name, handleNameBlur, handleNameChange } = useHandleNodeNameChange({ + id: node?.id, + data: node?.data, + }); + const previousId = useRef(node?.id); + + const { t } = useTranslate('flow'); + + const { handleValuesChange } = useHandleFormValuesChange(node?.id); + + useEffect(() => { + if (visible) { + if (node?.id !== previousId.current) { + form.resetFields(); + } + + if (operatorName === Operator.Categorize) { + const items = buildCategorizeListFromObject( + get(node, 'data.form.category_description', {}), + ); + const formData = node?.data?.form; + if (isPlainObject(formData)) { + form.setFieldsValue({ ...formData, items }); + } + } else { + form.setFieldsValue(node?.data?.form); + } + previousId.current = node?.id; + } + }, [visible, form, node?.data?.form, node?.id, node, operatorName]); + + return ( + + + + + + {node?.id === BeginId ? ( + {t(BeginId)} + ) : ( + + )} + + + {needsSingleStepDebugging(operatorName) && ( + + + + )} + + + + {t(`${lowerFirst(operatorName)}Description`)} + + + } + placement="right" + onClose={hideModal} + open={visible} + getContainer={false} + mask={false} + width={getDrawerWidth()} + closeIcon={null} + rootClassName={styles.formDrawer} + > +
+ {visible && ( + + + + )} +
+ {singleDebugDrawerVisible && ( + + )} +
+ ); +}; + +export default FormDrawer; diff --git a/web/src/pages/agent/form-drawer/single-debug-drawer/index.tsx b/web/src/pages/agent/form-drawer/single-debug-drawer/index.tsx new file mode 100644 index 000000000..f9908ef7e --- /dev/null +++ b/web/src/pages/agent/form-drawer/single-debug-drawer/index.tsx @@ -0,0 +1,81 @@ +import CopyToClipboard from '@/components/copy-to-clipboard'; +import { useDebugSingle, useFetchInputElements } from '@/hooks/flow-hooks'; +import { IModalProps } from '@/interfaces/common'; +import { CloseOutlined } from '@ant-design/icons'; +import { Drawer } from 'antd'; +import { isEmpty } from 'lodash'; +import { useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; +import JsonView from 'react18-json-view'; +import 'react18-json-view/src/style.css'; +import DebugContent from '../../debug-content'; + +interface IProps { + componentId?: string; +} + +const SingleDebugDrawer = ({ + componentId, + visible, + hideModal, +}: IModalProps & IProps) => { + const { t } = useTranslation(); + const { data: list } = useFetchInputElements(componentId); + const { debugSingle, data, loading } = useDebugSingle(); + + const onOk = useCallback( + (nextValues: any[]) => { + if (componentId) { + debugSingle({ component_id: componentId, params: nextValues }); + } + }, + [componentId, debugSingle], + ); + + const content = JSON.stringify(data, null, 2); + + return ( + + {t('flow.testRun')} + + + } + width={'100%'} + onClose={hideModal} + open={visible} + getContainer={false} + mask={false} + placement={'bottom'} + height={'95%'} + closeIcon={null} + > +
+ + {!isEmpty(data) ? ( +
+
+ JSON + +
+ +
+ ) : null} +
+
+ ); +}; + +export default SingleDebugDrawer; diff --git a/web/src/pages/agent/form-hooks.ts b/web/src/pages/agent/form-hooks.ts new file mode 100644 index 000000000..372288e6f --- /dev/null +++ b/web/src/pages/agent/form-hooks.ts @@ -0,0 +1,77 @@ +import { useTranslate } from '@/hooks/common-hooks'; +import { useCallback, useMemo } from 'react'; +import { Operator, RestrictedUpstreamMap } from './constant'; +import useGraphStore from './store'; + +export const useBuildFormSelectOptions = ( + operatorName: Operator, + selfId?: string, // exclude the current node +) => { + const nodes = useGraphStore((state) => state.nodes); + + const buildCategorizeToOptions = useCallback( + (toList: string[]) => { + const excludedNodes: Operator[] = [ + Operator.Note, + ...(RestrictedUpstreamMap[operatorName] ?? []), + ]; + return nodes + .filter( + (x) => + excludedNodes.every((y) => y !== x.data.label) && + x.id !== selfId && + !toList.some((y) => y === x.id), // filter out selected values ​​in other to fields from the current drop-down box options + ) + .map((x) => ({ label: x.data.name, value: x.id })); + }, + [nodes, operatorName, selfId], + ); + + return buildCategorizeToOptions; +}; + +/** + * dumped + * @param nodeId + * @returns + */ +export const useHandleFormSelectChange = (nodeId?: string) => { + const { addEdge, deleteEdgeBySourceAndSourceHandle } = useGraphStore( + (state) => state, + ); + const handleSelectChange = useCallback( + (name?: string) => (value?: string) => { + if (nodeId && name) { + if (value) { + addEdge({ + source: nodeId, + target: value, + sourceHandle: name, + targetHandle: null, + }); + } else { + // clear selected value + deleteEdgeBySourceAndSourceHandle({ + source: nodeId, + sourceHandle: name, + }); + } + } + }, + [addEdge, nodeId, deleteEdgeBySourceAndSourceHandle], + ); + + return { handleSelectChange }; +}; + +export const useBuildSortOptions = () => { + const { t } = useTranslate('flow'); + + const options = useMemo(() => { + return ['data', 'relevance'].map((x) => ({ + value: x, + label: t(x), + })); + }, [t]); + return options; +}; diff --git a/web/src/pages/agent/form/akshare-form/index.tsx b/web/src/pages/agent/form/akshare-form/index.tsx new file mode 100644 index 000000000..1f7ce99f1 --- /dev/null +++ b/web/src/pages/agent/form/akshare-form/index.tsx @@ -0,0 +1,21 @@ +import TopNItem from '@/components/top-n-item'; +import { Form } from 'antd'; +import { IOperatorForm } from '../../interface'; +import DynamicInputVariable from '../components/dynamic-input-variable'; + +const AkShareForm = ({ onValuesChange, form, node }: IOperatorForm) => { + return ( +
+ + +
+ ); +}; + +export default AkShareForm; diff --git a/web/src/pages/agent/form/answer-form/index.tsx b/web/src/pages/agent/form/answer-form/index.tsx new file mode 100644 index 000000000..8db015c05 --- /dev/null +++ b/web/src/pages/agent/form/answer-form/index.tsx @@ -0,0 +1,5 @@ +const AnswerForm = () => { + return
; +}; + +export default AnswerForm; diff --git a/web/src/pages/agent/form/arxiv-form/index.tsx b/web/src/pages/agent/form/arxiv-form/index.tsx new file mode 100644 index 000000000..a44592148 --- /dev/null +++ b/web/src/pages/agent/form/arxiv-form/index.tsx @@ -0,0 +1,36 @@ +import TopNItem from '@/components/top-n-item'; +import { useTranslate } from '@/hooks/common-hooks'; +import { Form, Select } from 'antd'; +import { useMemo } from 'react'; +import { IOperatorForm } from '../../interface'; +import DynamicInputVariable from '../components/dynamic-input-variable'; + +const ArXivForm = ({ onValuesChange, form, node }: IOperatorForm) => { + const { t } = useTranslate('flow'); + + const options = useMemo(() => { + return ['submittedDate', 'lastUpdatedDate', 'relevance'].map((x) => ({ + value: x, + label: t(x), + })); + }, [t]); + + return ( +
+ + + + + + +
+ ); +}; + +export default ArXivForm; diff --git a/web/src/pages/agent/form/baidu-fanyi-form/index.tsx b/web/src/pages/agent/form/baidu-fanyi-form/index.tsx new file mode 100644 index 000000000..c4b399026 --- /dev/null +++ b/web/src/pages/agent/form/baidu-fanyi-form/index.tsx @@ -0,0 +1,71 @@ +import { useTranslate } from '@/hooks/common-hooks'; +import { Form, Input, Select } from 'antd'; +import { useMemo } from 'react'; +import { + BaiduFanyiDomainOptions, + BaiduFanyiSourceLangOptions, +} from '../../constant'; +import { IOperatorForm } from '../../interface'; +import DynamicInputVariable from '../components/dynamic-input-variable'; + +const BaiduFanyiForm = ({ onValuesChange, form, node }: IOperatorForm) => { + const { t } = useTranslate('flow'); + const options = useMemo(() => { + return ['translate', 'fieldtranslate'].map((x) => ({ + value: x, + label: t(`baiduSecretKeyOptions.${x}`), + })); + }, [t]); + + const baiduFanyiOptions = useMemo(() => { + return BaiduFanyiDomainOptions.map((x) => ({ + value: x, + label: t(`baiduDomainOptions.${x}`), + })); + }, [t]); + + const baiduFanyiSourceLangOptions = useMemo(() => { + return BaiduFanyiSourceLangOptions.map((x) => ({ + value: x, + label: t(`baiduSourceLangOptions.${x}`), + })); + }, [t]); + + return ( +
+ + + + + + + + + + + + {({ getFieldValue }) => + getFieldValue('trans_type') === 'fieldtranslate' && ( + + + + ) + } + + + + + + + +
+ ); +}; + +export default BaiduFanyiForm; diff --git a/web/src/pages/agent/form/baidu-form/index.tsx b/web/src/pages/agent/form/baidu-form/index.tsx new file mode 100644 index 000000000..0c866e488 --- /dev/null +++ b/web/src/pages/agent/form/baidu-form/index.tsx @@ -0,0 +1,21 @@ +import TopNItem from '@/components/top-n-item'; +import { Form } from 'antd'; +import { IOperatorForm } from '../../interface'; +import DynamicInputVariable from '../components/dynamic-input-variable'; + +const BaiduForm = ({ onValuesChange, form, node }: IOperatorForm) => { + return ( +
+ + +
+ ); +}; + +export default BaiduForm; diff --git a/web/src/pages/agent/form/begin-form/begin-dynamic-options.tsx b/web/src/pages/agent/form/begin-form/begin-dynamic-options.tsx new file mode 100644 index 000000000..bcc9c5788 --- /dev/null +++ b/web/src/pages/agent/form/begin-form/begin-dynamic-options.tsx @@ -0,0 +1,68 @@ +import { MinusCircleOutlined, PlusOutlined } from '@ant-design/icons'; +import { Button, Form, Input } from 'antd'; + +const BeginDynamicOptions = () => { + return ( + { + if (!names || names.length < 1) { + return Promise.reject(new Error('At least 1 option')); + } + }, + }, + ]} + > + {(fields, { add, remove }, { errors }) => ( + <> + {fields.map((field, index) => ( + + + + + {fields.length > 1 ? ( + remove(field.name)} + /> + ) : null} + + ))} + + + + + + )} + + ); +}; + +export default BeginDynamicOptions; diff --git a/web/src/pages/agent/form/begin-form/hooks.ts b/web/src/pages/agent/form/begin-form/hooks.ts new file mode 100644 index 000000000..b045f5dc5 --- /dev/null +++ b/web/src/pages/agent/form/begin-form/hooks.ts @@ -0,0 +1,50 @@ +import { useSetModalState } from '@/hooks/common-hooks'; +import { useSetSelectedRecord } from '@/hooks/logic-hooks'; +import { useCallback, useMemo, useState } from 'react'; +import { BeginQuery, IOperatorForm } from '../../interface'; + +export const useEditQueryRecord = ({ form, onValuesChange }: IOperatorForm) => { + const { setRecord, currentRecord } = useSetSelectedRecord(); + const { visible, hideModal, showModal } = useSetModalState(); + const [index, setIndex] = useState(-1); + + const otherThanCurrentQuery = useMemo(() => { + const query: BeginQuery[] = form?.getFieldValue('query') || []; + return query.filter((item, idx) => idx !== index); + }, [form, index]); + + const handleEditRecord = useCallback( + (record: BeginQuery) => { + const query: BeginQuery[] = form?.getFieldValue('query') || []; + + const nextQuery: BeginQuery[] = + index > -1 ? query.toSpliced(index, 1, record) : [...query, record]; + + onValuesChange?.( + { query: nextQuery }, + { query: nextQuery, prologue: form?.getFieldValue('prologue') }, + ); + hideModal(); + }, + [form, hideModal, index, onValuesChange], + ); + + const handleShowModal = useCallback( + (idx?: number, record?: BeginQuery) => { + setIndex(idx ?? -1); + setRecord(record ?? ({} as BeginQuery)); + showModal(); + }, + [setRecord, showModal], + ); + + return { + ok: handleEditRecord, + currentRecord, + setRecord, + visible, + hideModal, + showModal: handleShowModal, + otherThanCurrentQuery, + }; +}; diff --git a/web/src/pages/agent/form/begin-form/index.less b/web/src/pages/agent/form/begin-form/index.less new file mode 100644 index 000000000..0a03d4743 --- /dev/null +++ b/web/src/pages/agent/form/begin-form/index.less @@ -0,0 +1,24 @@ +.dynamicInputVariable { + background-color: #ebe9e950; + :global(.ant-collapse-content) { + background-color: #f6f6f657; + } + :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; +} diff --git a/web/src/pages/agent/form/begin-form/index.tsx b/web/src/pages/agent/form/begin-form/index.tsx new file mode 100644 index 000000000..8df1181f1 --- /dev/null +++ b/web/src/pages/agent/form/begin-form/index.tsx @@ -0,0 +1,111 @@ +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 } = useTranslation(); + const { + ok, + currentRecord, + visible, + hideModal, + showModal, + otherThanCurrentQuery, + } = useEditQueryRecord({ + form, + onValuesChange, + }); + + const handleDeleteRecord = useCallback( + (idx: number) => { + const query = form?.getFieldValue('query') || []; + const nextQuery = query.filter( + (item: BeginQuery, index: number) => index !== idx, + ); + onValuesChange?.( + { query: nextQuery }, + { query: nextQuery, prologue: form?.getFieldValue('prologue') }, + ); + }, + [form, onValuesChange], + ); + + return ( + { + if (name === 'queryForm') { + ok(values as BeginQuery); + } + }} + > +
+ + name={'prologue'} + label={t('chat.setAnOpener')} + tooltip={t('chat.setAnOpenerTip')} + initialValue={t('chat.setAnOpenerInitial')} + > + + + {/* Create a hidden field to make Form instance record this */} + + + + prevValues.query !== curValues.query + } + > + {({ getFieldValue }) => { + const query: BeginQuery[] = getFieldValue('query') || []; + return ( + + ); + }} + + + + {visible && ( + + )} + +
+ ); +}; + +export default BeginForm; diff --git a/web/src/pages/agent/form/begin-form/paramater-modal.tsx b/web/src/pages/agent/form/begin-form/paramater-modal.tsx new file mode 100644 index 000000000..7d689601b --- /dev/null +++ b/web/src/pages/agent/form/begin-form/paramater-modal.tsx @@ -0,0 +1,124 @@ +import { useResetFormOnCloseModal } from '@/hooks/logic-hooks'; +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 { useTranslation } from 'react-i18next'; +import { BeginQueryType, BeginQueryTypeIconMap } from '../../constant'; +import { BeginQuery } from '../../interface'; +import BeginDynamicOptions from './begin-dynamic-options'; + +export const ModalForm = ({ + visible, + initialValue, + hideModal, + otherThanCurrentQuery, +}: IModalProps & { + initialValue: BeginQuery; + otherThanCurrentQuery: BeginQuery[]; +}) => { + const { t } = useTranslation(); + const [form] = Form.useForm(); + const options = useMemo(() => { + return Object.values(BeginQueryType).reduce( + (pre, cur) => { + const Icon = BeginQueryTypeIconMap[cur]; + + return [ + ...pre, + { + label: ( +
+ + {cur} +
+ ), + value: cur, + }, + ]; + }, + [], + ); + }, []); + + useResetFormOnCloseModal({ + form, + visible: visible, + }); + + useEffect(() => { + form.setFieldsValue(initialValue); + }, [form, initialValue]); + + const onOk = () => { + form.submit(); + }; + + return ( + +
+ + + + + + + + + + + prevValues.type !== curValues.type + } + > + {({ getFieldValue }) => { + const type: BeginQueryType = getFieldValue('type'); + return ( + type === BeginQueryType.Options && ( + + ) + ); + }} + +
+
+ ); +}; diff --git a/web/src/pages/agent/form/begin-form/query-table.tsx b/web/src/pages/agent/form/begin-form/query-table.tsx new file mode 100644 index 000000000..c7614e682 --- /dev/null +++ b/web/src/pages/agent/form/begin-form/query-table.tsx @@ -0,0 +1,92 @@ +import { DeleteOutlined, EditOutlined } from '@ant-design/icons'; +import type { TableProps } 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; + showModal(index: number, record: BeginQuery): void; +} + +const QueryTable = ({ data, deleteRecord, showModal }: IProps) => { + const { t } = useTranslation(); + + const columns: TableProps['columns'] = [ + { + title: 'Key', + dataIndex: 'key', + key: 'key', + ellipsis: { + showTitle: false, + }, + render: (key) => ( + + {key} + + ), + }, + { + title: t('flow.name'), + dataIndex: 'name', + key: 'name', + ellipsis: { + showTitle: false, + }, + render: (name) => ( + + {name} + + ), + }, + { + title: t('flow.type'), + dataIndex: 'type', + key: 'type', + }, + { + title: t('flow.optional'), + dataIndex: 'optional', + key: 'optional', + render: (optional) => (optional ? 'Yes' : 'No'), + }, + { + title: t('common.action'), + key: 'action', + render: (_, record, idx) => ( + + showModal(idx, record)} /> + deleteRecord(idx)} + /> + + ), + }, + ]; + + return ( + {t('flow.input')}, + children: ( + + columns={columns} + dataSource={data} + pagination={false} + /> + ), + }, + ]} + /> + ); +}; + +export default QueryTable; diff --git a/web/src/pages/agent/form/bing-form/index.tsx b/web/src/pages/agent/form/bing-form/index.tsx new file mode 100644 index 000000000..b640f08d0 --- /dev/null +++ b/web/src/pages/agent/form/bing-form/index.tsx @@ -0,0 +1,42 @@ +import TopNItem from '@/components/top-n-item'; +import { useTranslate } from '@/hooks/common-hooks'; +import { Form, Input, Select } from 'antd'; +import { useMemo } from 'react'; +import { BingCountryOptions, BingLanguageOptions } from '../../constant'; +import { IOperatorForm } from '../../interface'; +import DynamicInputVariable from '../components/dynamic-input-variable'; + +const BingForm = ({ onValuesChange, form, node }: IOperatorForm) => { + const { t } = useTranslate('flow'); + + const options = useMemo(() => { + return ['Webpages', 'News'].map((x) => ({ label: x, value: x })); + }, []); + + return ( +
+ + + + + + + + + + + + + + +
+ ); +}; + +export default BingForm; diff --git a/web/src/pages/agent/form/categorize-form/dynamic-categorize.tsx b/web/src/pages/agent/form/categorize-form/dynamic-categorize.tsx new file mode 100644 index 000000000..7d3987971 --- /dev/null +++ b/web/src/pages/agent/form/categorize-form/dynamic-categorize.tsx @@ -0,0 +1,225 @@ +import { useTranslate } from '@/hooks/common-hooks'; +import { CloseOutlined, PlusOutlined } from '@ant-design/icons'; +import { useUpdateNodeInternals } from '@xyflow/react'; +import { + Button, + Collapse, + Flex, + Form, + FormListFieldData, + Input, + Select, +} from 'antd'; +import { FormInstance } from 'antd/lib'; +import { humanId } from 'human-id'; +import trim from 'lodash/trim'; +import { + ChangeEventHandler, + FocusEventHandler, + useCallback, + useEffect, + useState, +} from 'react'; +import { Operator } from '../../constant'; +import { useBuildFormSelectOptions } from '../../form-hooks'; + +import styles from './index.less'; + +interface IProps { + nodeId?: string; +} + +interface INameInputProps { + value?: string; + onChange?: (value: string) => void; + otherNames?: string[]; + validate(errors: string[]): void; +} + +const getOtherFieldValues = ( + form: FormInstance, + formListName: string = 'items', + field: FormListFieldData, + latestField: string, +) => + (form.getFieldValue([formListName]) ?? []) + .map((x: any) => x[latestField]) + .filter( + (x: string) => + x !== form.getFieldValue([formListName, field.name, latestField]), + ); + +const NameInput = ({ + value, + onChange, + otherNames, + validate, +}: INameInputProps) => { + const [name, setName] = useState(); + const { t } = useTranslate('flow'); + + const handleNameChange: ChangeEventHandler = useCallback( + (e) => { + const val = e.target.value; + // trigger validation + if (otherNames?.some((x) => x === val)) { + validate([t('nameRepeatedMsg')]); + } else if (trim(val) === '') { + validate([t('nameRequiredMsg')]); + } else { + validate([]); + } + setName(val); + }, + [otherNames, validate, t], + ); + + const handleNameBlur: FocusEventHandler = useCallback( + (e) => { + const val = e.target.value; + if (otherNames?.every((x) => x !== val) && trim(val) !== '') { + onChange?.(val); + } + }, + [onChange, otherNames], + ); + + useEffect(() => { + setName(value); + }, [value]); + + return ( + + ); +}; + +const FormSet = ({ nodeId, field }: IProps & { field: FormListFieldData }) => { + const form = Form.useFormInstance(); + const { t } = useTranslate('flow'); + const buildCategorizeToOptions = useBuildFormSelectOptions( + Operator.Categorize, + nodeId, + ); + + return ( +
+ + + form.setFields([ + { + name: ['items', field.name, 'name'], + errors, + }, + ]) + } + > + + + + + + + + + + +
+ ); +}; + +const DynamicCategorize = ({ nodeId }: IProps) => { + const updateNodeInternals = useUpdateNodeInternals(); + const form = Form.useFormInstance(); + + const { t } = useTranslate('flow'); + + return ( + <> + + {(fields, { add, remove }) => { + const handleAdd = () => { + const idx = form.getFieldValue([ + 'items', + fields.at(-1)?.name, + 'index', + ]); + add({ + name: humanId(), + index: fields.length === 0 ? 0 : idx + 1, + }); + if (nodeId) updateNodeInternals(nodeId); + }; + + return ( + + {fields.map((field) => ( + + + {form.getFieldValue(['items', field.name, 'name'])} + + { + remove(field.name); + }} + /> + + ), + children: ( + + ), + }, + ]} + > + ))} + + + + ); + }} + + + ); +}; + +export default DynamicCategorize; diff --git a/web/src/pages/agent/form/categorize-form/hooks.ts b/web/src/pages/agent/form/categorize-form/hooks.ts new file mode 100644 index 000000000..a7e8f23f3 --- /dev/null +++ b/web/src/pages/agent/form/categorize-form/hooks.ts @@ -0,0 +1,45 @@ +import { + ICategorizeItem, + ICategorizeItemResult, +} from '@/interfaces/database/flow'; +import omit from 'lodash/omit'; +import { useCallback } from 'react'; +import { IOperatorForm } from '../../interface'; + +/** + * Convert the list in the following form into an object + * { + "items": [ + { + "name": "Categorize 1", + "description": "111", + "examples": "ddd", + "to": "Retrieval:LazyEelsStick" + } + ] + } +*/ +const buildCategorizeObjectFromList = (list: Array) => { + return list.reduce((pre, cur) => { + if (cur?.name) { + pre[cur.name] = omit(cur, 'name'); + } + return pre; + }, {}); +}; + +export const useHandleFormValuesChange = ({ + onValuesChange, +}: IOperatorForm) => { + const handleValuesChange = useCallback( + (changedValues: any, values: any) => { + onValuesChange?.(changedValues, { + ...omit(values, 'items'), + category_description: buildCategorizeObjectFromList(values.items), + }); + }, + [onValuesChange], + ); + + return { handleValuesChange }; +}; diff --git a/web/src/pages/agent/form/categorize-form/index.less b/web/src/pages/agent/form/categorize-form/index.less new file mode 100644 index 000000000..6d78e80f9 --- /dev/null +++ b/web/src/pages/agent/form/categorize-form/index.less @@ -0,0 +1,13 @@ +@lightBackgroundColor: rgba(150, 150, 150, 0.07); +@darkBackgroundColor: rgba(150, 150, 150, 0.12); + +.caseCard { + :global(.ant-collapse-content) { + background-color: @darkBackgroundColor; + } +} + +.addButton { + color: rgb(22, 119, 255); + font-weight: 600; +} diff --git a/web/src/pages/agent/form/categorize-form/index.tsx b/web/src/pages/agent/form/categorize-form/index.tsx new file mode 100644 index 000000000..cb6651a24 --- /dev/null +++ b/web/src/pages/agent/form/categorize-form/index.tsx @@ -0,0 +1,43 @@ +import LLMSelect from '@/components/llm-select'; +import MessageHistoryWindowSizeItem from '@/components/message-history-window-size-item'; +import { useTranslate } from '@/hooks/common-hooks'; +import { Form } from 'antd'; +import { IOperatorForm } from '../../interface'; +import DynamicInputVariable from '../components/dynamic-input-variable'; +import DynamicCategorize from './dynamic-categorize'; +import { useHandleFormValuesChange } from './hooks'; + +const CategorizeForm = ({ form, onValuesChange, node }: IOperatorForm) => { + const { t } = useTranslate('flow'); + const { handleValuesChange } = useHandleFormValuesChange({ + form, + nodeId: node?.id, + onValuesChange, + }); + + return ( +
+ + + + + + +
+ ); +}; + +export default CategorizeForm; diff --git a/web/src/pages/agent/form/components/dynamic-input-variable.tsx b/web/src/pages/agent/form/components/dynamic-input-variable.tsx new file mode 100644 index 000000000..82082e6b8 --- /dev/null +++ b/web/src/pages/agent/form/components/dynamic-input-variable.tsx @@ -0,0 +1,130 @@ +import { RAGFlowNodeType } from '@/interfaces/database/flow'; +import { MinusCircleOutlined, PlusOutlined } from '@ant-design/icons'; +import { Button, Collapse, Flex, Form, Input, Select } from 'antd'; +import { PropsWithChildren, useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useBuildComponentIdSelectOptions } from '../../hooks/use-get-begin-query'; + +import styles from './index.less'; + +interface IProps { + node?: RAGFlowNodeType; +} + +enum VariableType { + Reference = 'reference', + Input = 'input', +} + +const getVariableName = (type: string) => + type === VariableType.Reference ? 'component_id' : 'value'; + +const DynamicVariableForm = ({ node }: IProps) => { + const { t } = useTranslation(); + const valueOptions = useBuildComponentIdSelectOptions( + node?.id, + node?.parentId, + ); + const form = Form.useFormInstance(); + + const options = [ + { value: VariableType.Reference, label: t('flow.reference') }, + { value: VariableType.Input, label: t('flow.text') }, + ]; + + const handleTypeChange = useCallback( + (name: number) => () => { + setTimeout(() => { + form.setFieldValue(['query', name, 'component_id'], undefined); + form.setFieldValue(['query', name, 'value'], undefined); + }, 0); + }, + [form], + ); + + return ( + + {(fields, { add, remove }) => ( + <> + {fields.map(({ key, name, ...restField }) => ( + + + + + + {({ getFieldValue }) => { + const type = getFieldValue(['query', name, 'type']); + return ( + + {type === VariableType.Reference ? ( + + ) : ( + + )} + + ); + }} + + remove(name)} /> + + ))} + + + + + )} + + ); +}; + +export function FormCollapse({ + children, + title, +}: PropsWithChildren<{ title: string }>) { + return ( + {title}, + children, + }, + ]} + /> + ); +} + +const DynamicInputVariable = ({ node }: IProps) => { + const { t } = useTranslation(); + return ( + + + + ); +}; + +export default DynamicInputVariable; diff --git a/web/src/pages/agent/form/components/index.less b/web/src/pages/agent/form/components/index.less new file mode 100644 index 000000000..344514d9e --- /dev/null +++ b/web/src/pages/agent/form/components/index.less @@ -0,0 +1,22 @@ +.dynamicInputVariable { + background-color: #ebe9e950; + :global(.ant-collapse-content) { + background-color: #f6f6f657; + } + margin-bottom: 20px; + .title { + font-weight: 600; + font-size: 16px; + } + .variableType { + width: 30%; + } + .variableValue { + flex: 1; + } + + .addButton { + color: rgb(22, 119, 255); + font-weight: 600; + } +} diff --git a/web/src/pages/agent/form/concentrator-form/index.tsx b/web/src/pages/agent/form/concentrator-form/index.tsx new file mode 100644 index 000000000..d409f3099 --- /dev/null +++ b/web/src/pages/agent/form/concentrator-form/index.tsx @@ -0,0 +1,17 @@ +import { Form } from 'antd'; +import { IOperatorForm } from '../../interface'; + +const ConcentratorForm = ({ onValuesChange, form }: IOperatorForm) => { + return ( +
+ ); +}; + +export default ConcentratorForm; diff --git a/web/src/pages/agent/form/crawler-form/index.tsx b/web/src/pages/agent/form/crawler-form/index.tsx new file mode 100644 index 000000000..8ef5f14d6 --- /dev/null +++ b/web/src/pages/agent/form/crawler-form/index.tsx @@ -0,0 +1,38 @@ +import { useTranslate } from '@/hooks/common-hooks'; +import { Form, Input, Select } from 'antd'; +import { useMemo } from 'react'; +import { CrawlerResultOptions } from '../../constant'; +import { IOperatorForm } from '../../interface'; +import DynamicInputVariable from '../components/dynamic-input-variable'; +const CrawlerForm = ({ onValuesChange, form, node }: IOperatorForm) => { + const { t } = useTranslate('flow'); + const crawlerResultOptions = useMemo(() => { + return CrawlerResultOptions.map((x) => ({ + value: x, + label: t(`crawlerResultOptions.${x}`), + })); + }, [t]); + return ( +
+ + + + + + + +
+ ); +}; + +export default CrawlerForm; diff --git a/web/src/pages/agent/form/deepl-form/index.tsx b/web/src/pages/agent/form/deepl-form/index.tsx new file mode 100644 index 000000000..1fc8cfc26 --- /dev/null +++ b/web/src/pages/agent/form/deepl-form/index.tsx @@ -0,0 +1,36 @@ +import TopNItem from '@/components/top-n-item'; +import { useTranslate } from '@/hooks/common-hooks'; +import { Form, Select } from 'antd'; +import { DeepLSourceLangOptions, DeepLTargetLangOptions } from '../../constant'; +import { useBuildSortOptions } from '../../form-hooks'; +import { IOperatorForm } from '../../interface'; +import DynamicInputVariable from '../components/dynamic-input-variable'; + +const DeepLForm = ({ onValuesChange, form, node }: IOperatorForm) => { + const { t } = useTranslate('flow'); + const options = useBuildSortOptions(); + + return ( +
+ + + + + + + + + + + +
+ ); +}; + +export default DeepLForm; diff --git a/web/src/pages/agent/form/duckduckgo-form/index.tsx b/web/src/pages/agent/form/duckduckgo-form/index.tsx new file mode 100644 index 000000000..53462da31 --- /dev/null +++ b/web/src/pages/agent/form/duckduckgo-form/index.tsx @@ -0,0 +1,38 @@ +import TopNItem from '@/components/top-n-item'; +import { useTranslate } from '@/hooks/common-hooks'; +import { Form, Select } from 'antd'; +import { useMemo } from 'react'; +import { Channel } from '../../constant'; +import { IOperatorForm } from '../../interface'; +import DynamicInputVariable from '../components/dynamic-input-variable'; + +const DuckDuckGoForm = ({ onValuesChange, form, node }: IOperatorForm) => { + const { t } = useTranslate('flow'); + + const options = useMemo(() => { + return Object.values(Channel).map((x) => ({ value: x, label: t(x) })); + }, [t]); + + return ( +
+ + + + + +
+ ); +}; + +export default DuckDuckGoForm; diff --git a/web/src/pages/agent/form/email-form/index.tsx b/web/src/pages/agent/form/email-form/index.tsx new file mode 100644 index 000000000..9bee7997f --- /dev/null +++ b/web/src/pages/agent/form/email-form/index.tsx @@ -0,0 +1,53 @@ +import { useTranslate } from '@/hooks/common-hooks'; +import { Form, Input } from 'antd'; +import { IOperatorForm } from '../../interface'; +import DynamicInputVariable from '../components/dynamic-input-variable'; + +const EmailForm = ({ onValuesChange, form, node }: IOperatorForm) => { + const { t } = useTranslate('flow'); + + return ( +
+ + + {/* SMTP服务器配置 */} + + + + + + + + + + + + + + + + + {/* 动态参数说明 */} +
+

{t('dynamicParameters')}

+
{t('jsonFormatTip')}
+
+          {`{
+  "to_email": "recipient@example.com",  
+  "cc_email": "cc@example.com",
+  "subject": "Email Subject",           
+  "content": "Email Content"            
+}`}
+        
+
+
+ ); +}; + +export default EmailForm; diff --git a/web/src/pages/agent/form/exesql-form/index.tsx b/web/src/pages/agent/form/exesql-form/index.tsx new file mode 100644 index 000000000..f88f593d0 --- /dev/null +++ b/web/src/pages/agent/form/exesql-form/index.tsx @@ -0,0 +1,88 @@ +import LLMSelect from '@/components/llm-select'; +import TopNItem from '@/components/top-n-item'; +import { useTranslate } from '@/hooks/common-hooks'; +import { useTestDbConnect } from '@/hooks/flow-hooks'; +import { Button, Flex, Form, Input, InputNumber, Select } from 'antd'; +import { useCallback } from 'react'; +import { ExeSQLOptions } from '../../constant'; +import { IOperatorForm } from '../../interface'; +import DynamicInputVariable from '../components/dynamic-input-variable'; + +const ExeSQLForm = ({ onValuesChange, form, node }: IOperatorForm) => { + const { t } = useTranslate('flow'); + const { testDbConnect, loading } = useTestDbConnect(); + + const handleTest = useCallback(async () => { + const ret = await form?.validateFields(); + testDbConnect(ret); + }, [form, testDbConnect]); + + return ( +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ ); +}; + +export default ExeSQLForm; diff --git a/web/src/pages/agent/form/generate-form/dynamic-parameters.tsx b/web/src/pages/agent/form/generate-form/dynamic-parameters.tsx new file mode 100644 index 000000000..463437da3 --- /dev/null +++ b/web/src/pages/agent/form/generate-form/dynamic-parameters.tsx @@ -0,0 +1,101 @@ +import { EditableCell, EditableRow } from '@/components/editable-cell'; +import { useTranslate } from '@/hooks/common-hooks'; +import { RAGFlowNodeType } from '@/interfaces/database/flow'; +import { DeleteOutlined } from '@ant-design/icons'; +import { Button, Flex, Select, Table, TableProps } from 'antd'; +import { useBuildComponentIdSelectOptions } from '../../hooks/use-get-begin-query'; +import { IGenerateParameter } from '../../interface'; +import { useHandleOperateParameters } from './hooks'; + +import styles from './index.less'; +interface IProps { + node?: RAGFlowNodeType; +} + +const components = { + body: { + row: EditableRow, + cell: EditableCell, + }, +}; + +const DynamicParameters = ({ node }: IProps) => { + const nodeId = node?.id; + const { t } = useTranslate('flow'); + + const options = useBuildComponentIdSelectOptions(nodeId, node?.parentId); + const { + dataSource, + handleAdd, + handleRemove, + handleSave, + handleComponentIdChange, + } = useHandleOperateParameters(nodeId!); + + const columns: TableProps['columns'] = [ + { + title: t('key'), + dataIndex: 'key', + key: 'key', + width: '40%', + onCell: (record: IGenerateParameter) => ({ + record, + editable: true, + dataIndex: 'key', + title: 'key', + handleSave, + }), + }, + { + title: t('value'), + dataIndex: 'component_id', + key: 'component_id', + align: 'center', + width: '40%', + render(text, record) { + return ( + +
+ + + + + + + + ); +}; + +export default GoogleForm; diff --git a/web/src/pages/agent/form/google-scholar-form/index.tsx b/web/src/pages/agent/form/google-scholar-form/index.tsx new file mode 100644 index 000000000..4e87fac25 --- /dev/null +++ b/web/src/pages/agent/form/google-scholar-form/index.tsx @@ -0,0 +1,75 @@ +import TopNItem from '@/components/top-n-item'; +import { useTranslate } from '@/hooks/common-hooks'; +import { DatePicker, DatePickerProps, Form, Select, Switch } from 'antd'; +import dayjs from 'dayjs'; +import { useCallback, useMemo } from 'react'; +import { useBuildSortOptions } from '../../form-hooks'; +import { IOperatorForm } from '../../interface'; +import DynamicInputVariable from '../components/dynamic-input-variable'; + +const YearPicker = ({ + onChange, + value, +}: { + onChange?: (val: number | undefined) => void; + value?: number | undefined; +}) => { + const handleChange: DatePickerProps['onChange'] = useCallback( + (val: any) => { + const nextVal = val?.format('YYYY'); + onChange?.(nextVal ? Number(nextVal) : undefined); + }, + [onChange], + ); + // The year needs to be converted into a number and saved to the backend + const nextValue = useMemo(() => { + if (value) { + return dayjs(value.toString()); + } + return undefined; + }, [value]); + + return ; +}; + +const GoogleScholarForm = ({ onValuesChange, form, node }: IOperatorForm) => { + const { t } = useTranslate('flow'); + + const options = useBuildSortOptions(); + + return ( +
+ + + + + + + + + + + + + + +
+ ); +}; + +export default GoogleScholarForm; diff --git a/web/src/pages/agent/form/invoke-form/dynamic-variables.tsx b/web/src/pages/agent/form/invoke-form/dynamic-variables.tsx new file mode 100644 index 000000000..3538b8b72 --- /dev/null +++ b/web/src/pages/agent/form/invoke-form/dynamic-variables.tsx @@ -0,0 +1,130 @@ +import { EditableCell, EditableRow } from '@/components/editable-cell'; +import { useTranslate } from '@/hooks/common-hooks'; +import { DeleteOutlined } from '@ant-design/icons'; +import { Button, Collapse, Flex, Input, Select, Table, TableProps } from 'antd'; +import { trim } from 'lodash'; +import { useBuildComponentIdSelectOptions } from '../../hooks/use-get-begin-query'; +import { IInvokeVariable, RAGFlowNodeType } from '../../interface'; +import { useHandleOperateParameters } from './hooks'; + +import styles from './index.less'; + +interface IProps { + node?: RAGFlowNodeType; +} + +const components = { + body: { + row: EditableRow, + cell: EditableCell, + }, +}; + +const DynamicVariablesForm = ({ node }: IProps) => { + const nodeId = node?.id; + const { t } = useTranslate('flow'); + + const options = useBuildComponentIdSelectOptions(nodeId, node?.parentId); + const { + dataSource, + handleAdd, + handleRemove, + handleSave, + handleComponentIdChange, + handleValueChange, + } = useHandleOperateParameters(nodeId!); + + const columns: TableProps['columns'] = [ + { + title: t('key'), + dataIndex: 'key', + key: 'key', + onCell: (record: IInvokeVariable) => ({ + record, + editable: true, + dataIndex: 'key', + title: 'key', + handleSave, + }), + }, + { + title: t('componentId'), + dataIndex: 'component_id', + key: 'component_id', + align: 'center', + width: 140, + render(text, record) { + return ( + + ); + }, + }, + { + title: t('operation'), + dataIndex: 'operation', + width: 20, + key: 'operation', + align: 'center', + fixed: 'right', + render(_, record) { + return ; + }, + }, + ]; + + return ( + + {t('parameter')} + + + ), + children: ( + styles.editableRow} + scroll={{ x: true }} + bordered + /> + ), + }, + ]} + /> + ); +}; + +export default DynamicVariablesForm; diff --git a/web/src/pages/agent/form/invoke-form/hooks.ts b/web/src/pages/agent/form/invoke-form/hooks.ts new file mode 100644 index 000000000..951cd42ae --- /dev/null +++ b/web/src/pages/agent/form/invoke-form/hooks.ts @@ -0,0 +1,97 @@ +import get from 'lodash/get'; +import { + ChangeEventHandler, + MouseEventHandler, + useCallback, + useMemo, +} from 'react'; +import { v4 as uuid } from 'uuid'; +import { IGenerateParameter, IInvokeVariable } from '../../interface'; +import useGraphStore from '../../store'; + +export const useHandleOperateParameters = (nodeId: string) => { + const { getNode, updateNodeForm } = useGraphStore((state) => state); + const node = getNode(nodeId); + const dataSource: IGenerateParameter[] = useMemo( + () => get(node, 'data.form.variables', []) as IGenerateParameter[], + [node], + ); + + const changeValue = useCallback( + (row: IInvokeVariable, field: string, value: string) => { + const newData = [...dataSource]; + const index = newData.findIndex((item) => row.id === item.id); + const item = newData[index]; + newData.splice(index, 1, { + ...item, + [field]: value, + }); + + updateNodeForm(nodeId, { variables: newData }); + }, + [dataSource, nodeId, updateNodeForm], + ); + + const handleComponentIdChange = useCallback( + (row: IInvokeVariable) => (value: string) => { + changeValue(row, 'component_id', value); + }, + [changeValue], + ); + + const handleValueChange = useCallback( + (row: IInvokeVariable): ChangeEventHandler => + (e) => { + changeValue(row, 'value', e.target.value); + }, + [changeValue], + ); + + const handleRemove = useCallback( + (id?: string) => () => { + const newData = dataSource.filter((item) => item.id !== id); + updateNodeForm(nodeId, { variables: newData }); + }, + [updateNodeForm, nodeId, dataSource], + ); + + const handleAdd: MouseEventHandler = useCallback( + (e) => { + e.preventDefault(); + e.stopPropagation(); + updateNodeForm(nodeId, { + variables: [ + ...dataSource, + { + id: uuid(), + key: '', + component_id: undefined, + value: '', + }, + ], + }); + }, + [dataSource, nodeId, updateNodeForm], + ); + + const handleSave = (row: IGenerateParameter) => { + const newData = [...dataSource]; + const index = newData.findIndex((item) => row.id === item.id); + const item = newData[index]; + newData.splice(index, 1, { + ...item, + ...row, + }); + + updateNodeForm(nodeId, { variables: newData }); + }; + + return { + handleAdd, + handleRemove, + handleComponentIdChange, + handleValueChange, + handleSave, + dataSource, + }; +}; diff --git a/web/src/pages/agent/form/invoke-form/index.less b/web/src/pages/agent/form/invoke-form/index.less new file mode 100644 index 000000000..e5bada1ef --- /dev/null +++ b/web/src/pages/agent/form/invoke-form/index.less @@ -0,0 +1,44 @@ +.editableRow { + :global(.editable-cell) { + position: relative; + } + + :global(.editable-cell-value-wrap) { + padding: 5px 12px; + cursor: pointer; + height: 30px !important; + } + &:hover { + :global(.editable-cell-value-wrap) { + padding: 4px 11px; + border: 1px solid #d9d9d9; + border-radius: 2px; + } + } +} + +.dynamicParameterVariable { + background-color: #ebe9e950; + :global(.ant-collapse-content) { + background-color: #f6f6f634; + } + :global(.ant-collapse-content-box) { + padding: 0 !important; + } + margin-bottom: 20px; + .title { + font-weight: 600; + font-size: 16px; + } + .variableType { + width: 30%; + } + .variableValue { + flex: 1; + } + + .addButton { + color: rgb(22, 119, 255); + font-weight: 600; + } +} diff --git a/web/src/pages/agent/form/invoke-form/index.tsx b/web/src/pages/agent/form/invoke-form/index.tsx new file mode 100644 index 000000000..521f1c95f --- /dev/null +++ b/web/src/pages/agent/form/invoke-form/index.tsx @@ -0,0 +1,78 @@ +import Editor, { loader } from '@monaco-editor/react'; +import { Form, Input, InputNumber, Select, Space, Switch } from 'antd'; +import { useTranslation } from 'react-i18next'; +import { IOperatorForm } from '../../interface'; +import DynamicVariablesForm from './dynamic-variables'; + +loader.config({ paths: { vs: '/vs' } }); + +enum Method { + GET = 'GET', + POST = 'POST', + PUT = 'PUT', +} + +const MethodOptions = [Method.GET, Method.POST, Method.PUT].map((x) => ({ + label: x, + value: x, +})); + +interface TimeoutInputProps { + value?: number; + onChange?: (value: number | null) => void; +} + +const TimeoutInput = ({ value, onChange }: TimeoutInputProps) => { + const { t } = useTranslation(); + return ( + + {t('common.s')} + + ); +}; + +const InvokeForm = ({ onValuesChange, form, node }: IOperatorForm) => { + const { t } = useTranslation(); + + return ( + <> +
+ + + + + + + + + + + + + ); +}; + +export default InvokeForm; diff --git a/web/src/pages/agent/form/iteration-from/index.tsx b/web/src/pages/agent/form/iteration-from/index.tsx new file mode 100644 index 000000000..f0f23918a --- /dev/null +++ b/web/src/pages/agent/form/iteration-from/index.tsx @@ -0,0 +1,94 @@ +import { CommaIcon, SemicolonIcon } from '@/assets/icon/Icon'; +import { Form, Select } from 'antd'; +import { + CornerDownLeft, + IndentIncrease, + Minus, + Slash, + Underline, +} from 'lucide-react'; +import { useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { IOperatorForm } from '../../interface'; +import DynamicInputVariable from '../components/dynamic-input-variable'; + +const optionList = [ + { + value: ',', + icon: CommaIcon, + text: 'comma', + }, + { + value: '\n', + icon: CornerDownLeft, + text: 'lineBreak', + }, + { + value: 'tab', + icon: IndentIncrease, + text: 'tab', + }, + { + value: '_', + icon: Underline, + text: 'underline', + }, + { + value: '/', + icon: Slash, + text: 'diagonal', + }, + { + value: '-', + icon: Minus, + text: 'minus', + }, + { + value: ';', + icon: SemicolonIcon, + text: 'semicolon', + }, +]; + +const IterationForm = ({ onValuesChange, form, node }: IOperatorForm) => { + const { t } = useTranslation(); + + const options = useMemo(() => { + return optionList.map((x) => { + let Icon = x.icon; + + return { + value: x.value, + label: ( +
+ + {t(`flow.delimiterOptions.${x.text}`)} +
+ ), + }; + }); + }, [t]); + + return ( +
+ + + + + + ); +}; + +export default IterationForm; diff --git a/web/src/pages/agent/form/jin10-form/index.tsx b/web/src/pages/agent/form/jin10-form/index.tsx new file mode 100644 index 000000000..aa9bb169f --- /dev/null +++ b/web/src/pages/agent/form/jin10-form/index.tsx @@ -0,0 +1,145 @@ +import { useTranslate } from '@/hooks/common-hooks'; +import { Form, Input, Select } from 'antd'; +import { useMemo } from 'react'; +import { + Jin10CalendarDatashapeOptions, + Jin10CalendarTypeOptions, + Jin10FlashTypeOptions, + Jin10SymbolsDatatypeOptions, + Jin10SymbolsTypeOptions, + Jin10TypeOptions, +} from '../../constant'; +import { IOperatorForm } from '../../interface'; +import DynamicInputVariable from '../components/dynamic-input-variable'; + +const Jin10Form = ({ onValuesChange, form, node }: IOperatorForm) => { + const { t } = useTranslate('flow'); + + const jin10TypeOptions = useMemo(() => { + return Jin10TypeOptions.map((x) => ({ + value: x, + label: t(`jin10TypeOptions.${x}`), + })); + }, [t]); + + const jin10FlashTypeOptions = useMemo(() => { + return Jin10FlashTypeOptions.map((x) => ({ + value: x, + label: t(`jin10FlashTypeOptions.${x}`), + })); + }, [t]); + + const jin10CalendarTypeOptions = useMemo(() => { + return Jin10CalendarTypeOptions.map((x) => ({ + value: x, + label: t(`jin10CalendarTypeOptions.${x}`), + })); + }, [t]); + + const jin10CalendarDatashapeOptions = useMemo(() => { + return Jin10CalendarDatashapeOptions.map((x) => ({ + value: x, + label: t(`jin10CalendarDatashapeOptions.${x}`), + })); + }, [t]); + + const jin10SymbolsTypeOptions = useMemo(() => { + return Jin10SymbolsTypeOptions.map((x) => ({ + value: x, + label: t(`jin10SymbolsTypeOptions.${x}`), + })); + }, [t]); + + const jin10SymbolsDatatypeOptions = useMemo(() => { + return Jin10SymbolsDatatypeOptions.map((x) => ({ + value: x, + label: t(`jin10SymbolsDatatypeOptions.${x}`), + })); + }, [t]); + + return ( +
+ + + + + + + + + {({ getFieldValue }) => { + const type = getFieldValue('type'); + switch (type) { + case 'flash': + return ( + <> + + + + + + + + + + + ); + + case 'calendar': + return ( + <> + + + + + + + + ); + + case 'symbols': + return ( + <> + + + + + + + + ); + + case 'news': + return ( + <> + + + + + + + + ); + + default: + return <>; + } + }} + + + ); +}; + +export default Jin10Form; diff --git a/web/src/pages/agent/form/keyword-extract-form/index.tsx b/web/src/pages/agent/form/keyword-extract-form/index.tsx new file mode 100644 index 000000000..089df5eab --- /dev/null +++ b/web/src/pages/agent/form/keyword-extract-form/index.tsx @@ -0,0 +1,32 @@ +import LLMSelect from '@/components/llm-select'; +import TopNItem from '@/components/top-n-item'; +import { useTranslate } from '@/hooks/common-hooks'; +import { Form } from 'antd'; +import { IOperatorForm } from '../../interface'; +import DynamicInputVariable from '../components/dynamic-input-variable'; + +const KeywordExtractForm = ({ onValuesChange, form, node }: IOperatorForm) => { + const { t } = useTranslate('flow'); + + return ( +
+ + + + + + + ); +}; + +export default KeywordExtractForm; diff --git a/web/src/pages/agent/form/message-form/index.less b/web/src/pages/agent/form/message-form/index.less new file mode 100644 index 000000000..9725f89d2 --- /dev/null +++ b/web/src/pages/agent/form/message-form/index.less @@ -0,0 +1,16 @@ +.dynamicDeleteButton { + position: relative; + top: 4px; + margin: 0 8px; + color: #999; + font-size: 24px; + cursor: pointer; + transition: all 0.3s; + &:hover { + color: #777; + } + &[disabled] { + cursor: not-allowed; + opacity: 0.5; + } +} diff --git a/web/src/pages/agent/form/message-form/index.tsx b/web/src/pages/agent/form/message-form/index.tsx new file mode 100644 index 000000000..6040b929d --- /dev/null +++ b/web/src/pages/agent/form/message-form/index.tsx @@ -0,0 +1,87 @@ +import { useTranslate } from '@/hooks/common-hooks'; +import { MinusCircleOutlined, PlusOutlined } from '@ant-design/icons'; +import { Button, Form, Input } from 'antd'; +import { IOperatorForm } from '../../interface'; + +import styles from './index.less'; + +const formItemLayout = { + labelCol: { + sm: { span: 6 }, + }, + wrapperCol: { + sm: { span: 18 }, + }, +}; + +const formItemLayoutWithOutLabel = { + wrapperCol: { + sm: { span: 18, offset: 6 }, + }, +}; + +const MessageForm = ({ onValuesChange, form }: IOperatorForm) => { + const { t } = useTranslate('flow'); + + return ( +
+ + {(fields, { add, remove }, {}) => ( + <> + {fields.map((field, index) => ( + + + + + {fields.length > 1 ? ( + remove(field.name)} + /> + ) : null} + + ))} + + + + + )} + + + ); +}; + +export default MessageForm; diff --git a/web/src/pages/agent/form/pubmed-form/index.tsx b/web/src/pages/agent/form/pubmed-form/index.tsx new file mode 100644 index 000000000..a10962b6e --- /dev/null +++ b/web/src/pages/agent/form/pubmed-form/index.tsx @@ -0,0 +1,32 @@ +import TopNItem from '@/components/top-n-item'; +import { useTranslate } from '@/hooks/common-hooks'; +import { Form, Input } from 'antd'; +import { IOperatorForm } from '../../interface'; +import DynamicInputVariable from '../components/dynamic-input-variable'; + +const PubMedForm = ({ onValuesChange, form, node }: IOperatorForm) => { + const { t } = useTranslate('flow'); + + return ( +
+ + + + + + + ); +}; + +export default PubMedForm; diff --git a/web/src/pages/agent/form/qweather-form/index.tsx b/web/src/pages/agent/form/qweather-form/index.tsx new file mode 100644 index 000000000..b69939826 --- /dev/null +++ b/web/src/pages/agent/form/qweather-form/index.tsx @@ -0,0 +1,88 @@ +import { useTranslate } from '@/hooks/common-hooks'; +import { Form, Input, Select } from 'antd'; +import { useCallback, useMemo } from 'react'; +import { + QWeatherLangOptions, + QWeatherTimePeriodOptions, + QWeatherTypeOptions, + QWeatherUserTypeOptions, +} from '../../constant'; +import { IOperatorForm } from '../../interface'; +import DynamicInputVariable from '../components/dynamic-input-variable'; + +const QWeatherForm = ({ onValuesChange, form, node }: IOperatorForm) => { + const { t } = useTranslate('flow'); + const qWeatherLangOptions = useMemo(() => { + return QWeatherLangOptions.map((x) => ({ + value: x, + label: t(`qWeatherLangOptions.${x}`), + })); + }, [t]); + + const qWeatherTypeOptions = useMemo(() => { + return QWeatherTypeOptions.map((x) => ({ + value: x, + label: t(`qWeatherTypeOptions.${x}`), + })); + }, [t]); + + const qWeatherUserTypeOptions = useMemo(() => { + return QWeatherUserTypeOptions.map((x) => ({ + value: x, + label: t(`qWeatherUserTypeOptions.${x}`), + })); + }, [t]); + + const getQWeatherTimePeriodOptions = useCallback( + (userType: string) => { + let options = QWeatherTimePeriodOptions; + if (userType === 'free') { + options = options.slice(0, 3); + } + return options.map((x) => ({ + value: x, + label: t(`qWeatherTimePeriodOptions.${x}`), + })); + }, + [t], + ); + + return ( +
+ + + + + + + + + + + + + + + {({ getFieldValue }) => + getFieldValue('type') === 'weather' && ( + + + + ) + } + + + ); +}; + +export default QWeatherForm; diff --git a/web/src/pages/agent/form/relevant-form/hooks.ts b/web/src/pages/agent/form/relevant-form/hooks.ts new file mode 100644 index 000000000..6f31550fb --- /dev/null +++ b/web/src/pages/agent/form/relevant-form/hooks.ts @@ -0,0 +1,53 @@ +import { Edge } from '@xyflow/react'; +import pick from 'lodash/pick'; +import { useCallback, useEffect } from 'react'; +import { IOperatorForm } from '../../interface'; +import useGraphStore from '../../store'; + +export const useBuildRelevantOptions = () => { + const nodes = useGraphStore((state) => state.nodes); + + const buildRelevantOptions = useCallback( + (toList: string[]) => { + return nodes + .filter( + (x) => !toList.some((y) => y === x.id), // filter out selected values ​​in other to fields from the current drop-down box options + ) + .map((x) => ({ label: x.data.name, value: x.id })); + }, + [nodes], + ); + + return buildRelevantOptions; +}; + +const getTargetOfEdge = (edges: Edge[], sourceHandle: string) => + edges.find((x) => x.sourceHandle === sourceHandle)?.target; + +/** + * monitor changes in the connection and synchronize the target to the yes and no fields of the form + * similar to the categorize-form's useHandleFormValuesChange method + * @param param0 + */ +export const useWatchConnectionChanges = ({ nodeId, form }: IOperatorForm) => { + const edges = useGraphStore((state) => state.edges); + const getNode = useGraphStore((state) => state.getNode); + const node = getNode(nodeId); + + const watchFormChanges = useCallback(() => { + if (node) { + form?.setFieldsValue(pick(node, ['yes', 'no'])); + } + }, [node, form]); + + const watchConnectionChanges = useCallback(() => { + const edgeList = edges.filter((x) => x.source === nodeId); + const yes = getTargetOfEdge(edgeList, 'yes'); + const no = getTargetOfEdge(edgeList, 'no'); + form?.setFieldsValue({ yes, no }); + }, [edges, nodeId, form]); + + useEffect(() => { + watchFormChanges(); + }, [watchFormChanges]); +}; diff --git a/web/src/pages/agent/form/relevant-form/index.tsx b/web/src/pages/agent/form/relevant-form/index.tsx new file mode 100644 index 000000000..e2366f6f0 --- /dev/null +++ b/web/src/pages/agent/form/relevant-form/index.tsx @@ -0,0 +1,49 @@ +import LLMSelect from '@/components/llm-select'; +import { useTranslate } from '@/hooks/common-hooks'; +import { Form, Select } from 'antd'; +import { Operator } from '../../constant'; +import { useBuildFormSelectOptions } from '../../form-hooks'; +import { IOperatorForm } from '../../interface'; +import { useWatchConnectionChanges } from './hooks'; + +const RelevantForm = ({ onValuesChange, form, node }: IOperatorForm) => { + const { t } = useTranslate('flow'); + const buildRelevantOptions = useBuildFormSelectOptions( + Operator.Relevant, + node?.id, + ); + useWatchConnectionChanges({ nodeId: node?.id, form }); + + return ( +
+ + + + + + + + ); +}; + +export default RelevantForm; diff --git a/web/src/pages/agent/form/retrieval-form/index.tsx b/web/src/pages/agent/form/retrieval-form/index.tsx new file mode 100644 index 000000000..4a92a7f94 --- /dev/null +++ b/web/src/pages/agent/form/retrieval-form/index.tsx @@ -0,0 +1,54 @@ +import KnowledgeBaseItem from '@/components/knowledge-base-item'; +import Rerank from '@/components/rerank'; +import SimilaritySlider from '@/components/similarity-slider'; +import TopNItem from '@/components/top-n-item'; +import { useTranslate } from '@/hooks/common-hooks'; +import type { FormProps } from 'antd'; +import { Form, Input } from 'antd'; +import { IOperatorForm } from '../../interface'; +import DynamicInputVariable from '../components/dynamic-input-variable'; + +type FieldType = { + top_n?: number; +}; + +const onFinish: FormProps['onFinish'] = (values) => { + console.log('Success:', values); +}; + +const onFinishFailed: FormProps['onFinishFailed'] = (errorInfo) => { + console.log('Failed:', errorInfo); +}; + +const RetrievalForm = ({ onValuesChange, form, node }: IOperatorForm) => { + const { t } = useTranslate('flow'); + return ( +
+ + + + + + + + + + ); +}; + +export default RetrievalForm; diff --git a/web/src/pages/agent/form/rewrite-question-form/index.tsx b/web/src/pages/agent/form/rewrite-question-form/index.tsx new file mode 100644 index 000000000..c2b2e8db8 --- /dev/null +++ b/web/src/pages/agent/form/rewrite-question-form/index.tsx @@ -0,0 +1,41 @@ +import LLMSelect from '@/components/llm-select'; +import MessageHistoryWindowSizeItem from '@/components/message-history-window-size-item'; +import { useTranslate } from '@/hooks/common-hooks'; +import { Form, Select } from 'antd'; +import { GoogleLanguageOptions } from '../../constant'; +import { IOperatorForm } from '../../interface'; + +const RewriteQuestionForm = ({ onValuesChange, form }: IOperatorForm) => { + const { t } = useTranslate('chat'); + + return ( +
+ + + + + + + + + ); +}; + +export default RewriteQuestionForm; diff --git a/web/src/pages/agent/form/switch-form/index.less b/web/src/pages/agent/form/switch-form/index.less new file mode 100644 index 000000000..c5e2c7fe8 --- /dev/null +++ b/web/src/pages/agent/form/switch-form/index.less @@ -0,0 +1,21 @@ +@lightBackgroundColor: rgba(150, 150, 150, 0.07); +@darkBackgroundColor: rgba(150, 150, 150, 0.12); + +.caseCard { + background-color: @lightBackgroundColor; +} + +.conditionCard { + background-color: @darkBackgroundColor; +} + +.elseCase { + background-color: @lightBackgroundColor; + padding: 12px; + border-radius: 8px; +} + +.addButton { + color: rgb(22, 119, 255); + font-weight: 600; +} diff --git a/web/src/pages/agent/form/switch-form/index.tsx b/web/src/pages/agent/form/switch-form/index.tsx new file mode 100644 index 000000000..222a2baa3 --- /dev/null +++ b/web/src/pages/agent/form/switch-form/index.tsx @@ -0,0 +1,204 @@ +import { CloseOutlined } from '@ant-design/icons'; +import { Button, Card, Divider, Form, Input, Select } from 'antd'; +import { useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { + Operator, + SwitchElseTo, + SwitchLogicOperatorOptions, + SwitchOperatorOptions, +} from '../../constant'; +import { useBuildFormSelectOptions } from '../../form-hooks'; +import { useBuildComponentIdSelectOptions } from '../../hooks/use-get-begin-query'; +import { IOperatorForm } from '../../interface'; +import { getOtherFieldValues } from '../../utils'; + +import { ISwitchForm } from '@/interfaces/database/flow'; +import styles from './index.less'; + +const SwitchForm = ({ onValuesChange, node, form }: IOperatorForm) => { + const { t } = useTranslation(); + const buildCategorizeToOptions = useBuildFormSelectOptions( + Operator.Switch, + node?.id, + ); + + const getSelectedConditionTos = () => { + const conditions: ISwitchForm['conditions'] = + form?.getFieldValue('conditions'); + + return conditions?.filter((x) => !!x).map((x) => x?.to) ?? []; + }; + + const switchOperatorOptions = useMemo(() => { + return SwitchOperatorOptions.map((x) => ({ + value: x.value, + label: t(`flow.switchOperatorOptions.${x.label}`), + })); + }, [t]); + + const switchLogicOperatorOptions = useMemo(() => { + return SwitchLogicOperatorOptions.map((x) => ({ + value: x, + label: t(`flow.switchLogicOperatorOptions.${x}`), + })); + }, [t]); + + const componentIdOptions = useBuildComponentIdSelectOptions( + node?.id, + node?.parentId, + ); + + return ( +
+ + {(fields, { add, remove }) => ( +
+ {fields.map((field) => { + return ( + { + remove(field.name); + }} + /> + } + > + + {({ getFieldValue }) => + getFieldValue(['conditions', field.name, 'items']) + ?.length > 1 && ( + + + + + + {(subFields, subOpt) => ( +
+ {subFields.map((subField) => ( + { + subOpt.remove(subField.name); + }} + /> + } + > + + + + + + + + ))} + +
+ )} +
+
+
+ ); + })} + + +
+ )} +
+ + + + + + + + + + + + + + + + + + ); +}; + +export default TuShareForm; diff --git a/web/src/pages/agent/form/wencai-form/index.tsx b/web/src/pages/agent/form/wencai-form/index.tsx new file mode 100644 index 000000000..7c44c182f --- /dev/null +++ b/web/src/pages/agent/form/wencai-form/index.tsx @@ -0,0 +1,36 @@ +import TopNItem from '@/components/top-n-item'; +import { useTranslate } from '@/hooks/common-hooks'; +import { Form, Select } from 'antd'; +import { useMemo } from 'react'; +import { WenCaiQueryTypeOptions } from '../../constant'; +import { IOperatorForm } from '../../interface'; +import DynamicInputVariable from '../components/dynamic-input-variable'; + +const WenCaiForm = ({ onValuesChange, form, node }: IOperatorForm) => { + const { t } = useTranslate('flow'); + + const wenCaiQueryTypeOptions = useMemo(() => { + return WenCaiQueryTypeOptions.map((x) => ({ + value: x, + label: t(`wenCaiQueryTypeOptions.${x}`), + })); + }, [t]); + + return ( +
+ + + + + + + ); +}; + +export default WenCaiForm; diff --git a/web/src/pages/agent/form/wikipedia-form/index.tsx b/web/src/pages/agent/form/wikipedia-form/index.tsx new file mode 100644 index 000000000..9e28bf21d --- /dev/null +++ b/web/src/pages/agent/form/wikipedia-form/index.tsx @@ -0,0 +1,28 @@ +import TopNItem from '@/components/top-n-item'; +import { useTranslate } from '@/hooks/common-hooks'; +import { Form, Select } from 'antd'; +import { LanguageOptions } from '../../constant'; +import { IOperatorForm } from '../../interface'; +import DynamicInputVariable from '../components/dynamic-input-variable'; + +const WikipediaForm = ({ onValuesChange, form, node }: IOperatorForm) => { + const { t } = useTranslate('common'); + + return ( +
+ + + + + + + ); +}; + +export default WikipediaForm; diff --git a/web/src/pages/agent/form/yahoo-finance-form/index.tsx b/web/src/pages/agent/form/yahoo-finance-form/index.tsx new file mode 100644 index 000000000..ce7a3e7d2 --- /dev/null +++ b/web/src/pages/agent/form/yahoo-finance-form/index.tsx @@ -0,0 +1,40 @@ +import { useTranslate } from '@/hooks/common-hooks'; +import { Form, Switch } from 'antd'; +import { IOperatorForm } from '../../interface'; +import DynamicInputVariable from '../components/dynamic-input-variable'; + +const YahooFinanceForm = ({ onValuesChange, form, node }: IOperatorForm) => { + const { t } = useTranslate('flow'); + + return ( +
+ + + + + + + + + + + + + + + + + + + + + ); +}; + +export default YahooFinanceForm; diff --git a/web/src/pages/agent/index.tsx b/web/src/pages/agent/index.tsx index 463e58f36..2654418d3 100644 --- a/web/src/pages/agent/index.tsx +++ b/web/src/pages/agent/index.tsx @@ -10,6 +10,7 @@ import { import { SidebarProvider, SidebarTrigger } from '@/components/ui/sidebar'; import { useSetModalState } from '@/hooks/common-hooks'; import { useNavigatePage } from '@/hooks/logic-hooks/navigate-hooks'; +import { ReactFlowProvider } from '@xyflow/react'; import { CodeXml, EllipsisVertical, Forward, Import, Key } from 'lucide-react'; import { ComponentPropsWithoutRef } from 'react'; import { useTranslation } from 'react-i18next'; @@ -95,20 +96,22 @@ export default function Agent() { -
- - -
- -
- + +
+ + +
+ +
+ +
-
- -
+ +
+ {fileUploadVisible && (