From 14adf2892dce139b7990625b5c52d6251df013b7 Mon Sep 17 00:00:00 2001 From: balibabu Date: Fri, 13 Dec 2024 14:43:24 +0800 Subject: [PATCH] Feat: Supports to debug single component in Agent. #3993 (#4007) ### What problem does this PR solve? Feat: Supports to debug single component in Agent. #3993 Fix: The github button on the login page is displayed incorrectly #4002 ### Type of change - [x] New Feature (non-breaking change which adds functionality) --- web/src/hooks/flow-hooks.ts | 48 ++++ web/src/interfaces/request/flow.ts | 4 + web/src/locales/en.ts | 13 +- web/src/locales/zh-traditional.ts | 2 +- web/src/locales/zh.ts | 4 +- web/src/pages/flow/canvas/index.tsx | 35 ++- .../pages/flow/canvas/node/node-header.tsx | 25 +- web/src/pages/flow/constant.tsx | 10 + web/src/pages/flow/debug-content/index.less | 5 + web/src/pages/flow/debug-content/index.tsx | 238 ++++++++++++++++++ .../pages/flow/debug-content/popover-form.tsx | 74 ++++++ web/src/pages/flow/flow-drawer/index.less | 6 + web/src/pages/flow/flow-drawer/index.tsx | 37 ++- .../flow-drawer/single-debug-drawer/index.tsx | 81 ++++++ web/src/pages/flow/flow-tooltip.tsx | 22 ++ web/src/pages/flow/form/invoke-form/index.tsx | 4 +- web/src/pages/flow/hooks.tsx | 19 +- web/src/pages/flow/run-drawer/index.tsx | 221 ++-------------- web/src/pages/flow/utils.ts | 13 +- web/src/pages/login/index.tsx | 2 +- web/src/services/flow-service.ts | 10 + web/src/utils/api.ts | 2 + web/src/utils/request.ts | 32 +-- 23 files changed, 645 insertions(+), 262 deletions(-) create mode 100644 web/src/interfaces/request/flow.ts create mode 100644 web/src/pages/flow/debug-content/index.less create mode 100644 web/src/pages/flow/debug-content/index.tsx create mode 100644 web/src/pages/flow/debug-content/popover-form.tsx create mode 100644 web/src/pages/flow/flow-drawer/single-debug-drawer/index.tsx create mode 100644 web/src/pages/flow/flow-tooltip.tsx diff --git a/web/src/hooks/flow-hooks.ts b/web/src/hooks/flow-hooks.ts index bcd582bcb2..8d01a12822 100644 --- a/web/src/hooks/flow-hooks.ts +++ b/web/src/hooks/flow-hooks.ts @@ -1,5 +1,6 @@ import { ResponseType } from '@/interfaces/database/base'; import { DSL, IFlow, IFlowTemplate } from '@/interfaces/database/flow'; +import { IDebugSingleRequestBody } from '@/interfaces/request/flow'; import i18n from '@/locales/config'; import flowService from '@/services/flow-service'; import { buildMessageListWithUuid } from '@/utils/chat'; @@ -220,3 +221,50 @@ export const useTestDbConnect = () => { return { data, loading, testDbConnect: mutateAsync }; }; + +export const useFetchInputElements = (componentId?: string) => { + const { id } = useParams(); + + const { data, isPending: loading } = useQuery({ + queryKey: ['fetchInputElements', id, componentId], + initialData: [], + enabled: !!id && !!componentId, + retryOnMount: false, + refetchOnWindowFocus: false, + refetchOnReconnect: false, + gcTime: 0, + queryFn: async () => { + try { + const { data } = await flowService.getInputElements({ + id, + component_id: componentId, + }); + return data?.data ?? []; + } catch (error) { + console.log('🚀 ~ queryFn: ~ error:', error); + } + }, + }); + + return { data, loading }; +}; + +export const useDebugSingle = () => { + const { id } = useParams(); + const { + data, + isPending: loading, + mutateAsync, + } = useMutation({ + mutationKey: ['debugSingle'], + mutationFn: async (params: IDebugSingleRequestBody) => { + const ret = await flowService.debugSingle({ id, ...params }); + if (ret?.data?.code !== 0) { + message.error(ret?.data?.message); + } + return ret?.data?.data; + }, + }); + + return { data, loading, debugSingle: mutateAsync }; +}; diff --git a/web/src/interfaces/request/flow.ts b/web/src/interfaces/request/flow.ts new file mode 100644 index 0000000000..0ee8bcdd0b --- /dev/null +++ b/web/src/interfaces/request/flow.ts @@ -0,0 +1,4 @@ +export interface IDebugSingleRequestBody { + component_id: string; + params: any[]; +} diff --git a/web/src/locales/en.ts b/web/src/locales/en.ts index 75b45f7d5b..8f6c76e794 100644 --- a/web/src/locales/en.ts +++ b/web/src/locales/en.ts @@ -381,8 +381,7 @@ The above is the content you need to summarize.`, freedomTip: `Set the freedom level to 'Precise' to strictly confine the LLM's response to your selected knowledge base(s). Choose 'Improvise' to grant the LLM greater freedom in its responses, which may lead to hallucinations. 'Balance' is an intermediate level; choose 'Balance' for more balanced responses.`, temperature: 'Temperature', temperatureMessage: 'Temperature is required', - temperatureTip: - `This parameter controls the randomness of the model's predictions. A lower temperature results in more conservative responses, while a higher temperature yields more creative and diverse responses.`, + temperatureTip: `This parameter controls the randomness of the model's predictions. A lower temperature results in more conservative responses, while a higher temperature yields more creative and diverse responses.`, topP: 'Top P', topPMessage: 'Top P is required', topPTip: @@ -397,8 +396,7 @@ The above is the content you need to summarize.`, 'Similar to the presence penalty, this reduces the model’s tendency to repeat the same words frequently.', maxTokens: 'Max tokens', maxTokensMessage: 'Max tokens is required', - maxTokensTip: - `This sets the maximum length of the model's output, measured in the number of tokens (words or pieces of words). If disabled, you lift the maximum token limit, allowing the model to determine the number of tokens in its responses. Defaults to 512.`, + maxTokensTip: `This sets the maximum length of the model's output, measured in the number of tokens (words or pieces of words). If disabled, you lift the maximum token limit, allowing the model to determine the number of tokens in its responses. Defaults to 512.`, maxTokensInvalidMessage: 'Please enter a valid number for Max Tokens.', maxTokensMinMessage: 'Max Tokens cannot be less than 0.', quote: 'Show quote', @@ -430,7 +428,7 @@ The above is the content you need to summarize.`, partialTitle: 'Partial Embed', extensionTitle: 'Chrome Extension', tokenError: 'Please create API Token first!', - betaError: 'The beta field of the API Token cannot be empty!', + betaError: 'Please apply an API key in system setting firstly.', searching: 'Searching...', parsing: 'Parsing', uploading: 'Uploading', @@ -453,8 +451,7 @@ The above is the content you need to summarize.`, profileDescription: 'Update your photo and personal details here.', maxTokens: 'Max Tokens', maxTokensMessage: 'Max Tokens is required', - maxTokensTip: - `This sets the maximum length of the model's output, measured in the number of tokens (words or pieces of words). If disabled, you lift the maximum token limit, allowing the model to determine the number of tokens in its responses. Defaults to 512.`, + maxTokensTip: `This sets the maximum length of the model's output, measured in the number of tokens (words or pieces of words). If disabled, you lift the maximum token limit, allowing the model to determine the number of tokens in its responses. Defaults to 512.`, maxTokensInvalidMessage: 'Please enter a valid number for Max Tokens.', maxTokensMinMessage: 'Max Tokens cannot be less than 0.', password: 'Password', @@ -774,7 +771,7 @@ The above is the content you need to summarize.`, sourceLang: 'Source language', targetLang: 'Target language', gitHub: 'GitHub', - githubDescription: + gitHubDescription: 'This component is used to search the repository from https://github.com/. Top N specifies the number of search results to be adjusted.', baiduFanyi: 'BaiduFanyi', baiduFanyiDescription: diff --git a/web/src/locales/zh-traditional.ts b/web/src/locales/zh-traditional.ts index 3a378bdd3b..06b1c4ae39 100644 --- a/web/src/locales/zh-traditional.ts +++ b/web/src/locales/zh-traditional.ts @@ -414,7 +414,7 @@ export default { partialTitle: '部分嵌入', extensionTitle: 'Chrome 插件', tokenError: '請先創建 API Token!', - betaError: 'API Token的beta欄位不可以為空!', + betaError: '請先在系統設定中申請API密鑰。', searching: '搜索中', parsing: '解析中', uploading: '上傳中', diff --git a/web/src/locales/zh.ts b/web/src/locales/zh.ts index fb8ed55a9e..18668cecc2 100644 --- a/web/src/locales/zh.ts +++ b/web/src/locales/zh.ts @@ -431,7 +431,7 @@ export default { partialTitle: '部分嵌入', extensionTitle: 'Chrome 插件', tokenError: '请先创建 API Token!', - betaError: 'API Token的beta字段不可以为空!', + betaError: '请先在系统设置中申请API密钥。', searching: '搜索中', parsing: '解析中', uploading: '上传中', @@ -760,7 +760,7 @@ export default { sourceLang: '源语言', targetLang: '目标语言', gitHub: 'GitHub', - githubDescription: + gitHubDescription: '该组件用于从 https://github.com/ 搜索仓库。Top N 指定需要调整的搜索结果数量。', baiduFanyi: '百度翻译', baiduFanyiDescription: diff --git a/web/src/pages/flow/canvas/index.tsx b/web/src/pages/flow/canvas/index.tsx index 4e42c0e80e..71ef7e42de 100644 --- a/web/src/pages/flow/canvas/index.tsx +++ b/web/src/pages/flow/canvas/index.tsx @@ -5,6 +5,7 @@ import { TooltipTrigger, } from '@/components/ui/tooltip'; import { useSetModalState } from '@/hooks/common-hooks'; +import { get } from 'lodash'; import { FolderInput, FolderOutput } from 'lucide-react'; import { useCallback, useEffect } from 'react'; import ReactFlow, { @@ -24,6 +25,7 @@ import { useHandleExportOrImportJsonFile, useSelectCanvasData, useShowFormDrawer, + useShowSingleDebugDrawer, useValidateConnection, useWatchNodeFormDataChange, } from '../hooks'; @@ -95,6 +97,11 @@ function FlowCanvas({ drawerVisible, hideDrawer }: IProps) { showModal: showChatModal, hideModal: hideChatModal, } = useSetModalState(); + const { + singleDebugDrawerVisible, + showSingleDebugDrawer, + hideSingleDebugDrawer, + } = useShowSingleDebugDrawer(); const { formDrawerVisible, hideFormDrawer, showFormDrawer, clickedNode } = useShowFormDrawer(); @@ -116,11 +123,24 @@ function FlowCanvas({ drawerVisible, hideDrawer }: IProps) { const onNodeClick: NodeMouseHandler = useCallback( (e, node) => { if (node.data.label !== Operator.Note) { + hideSingleDebugDrawer(); hideRunOrChatDrawer(); showFormDrawer(node); } + // handle single debug icon click + if ( + get(e.target, 'dataset.play') === 'true' || + get(e.target, 'parentNode.dataset.play') === 'true' + ) { + showSingleDebugDrawer(); + } }, - [hideRunOrChatDrawer, showFormDrawer], + [ + hideRunOrChatDrawer, + hideSingleDebugDrawer, + showFormDrawer, + showSingleDebugDrawer, + ], ); const getBeginNodeDataQuery = useGetBeginNodeDataQuery(); @@ -193,12 +213,6 @@ function FlowCanvas({ drawerVisible, hideDrawer }: IProps) { onSelectionChange={onSelectionChange} nodeOrigin={[0.5, 0]} isValidConnection={isValidConnection} - onChangeCapture={(...params) => { - console.info('onChangeCapture:', ...params); - }} - onChange={(...params) => { - console.info('params:', ...params); - }} defaultEdgeOptions={{ type: 'buttonEdge', markerEnd: 'logo', @@ -214,7 +228,7 @@ function FlowCanvas({ drawerVisible, hideDrawer }: IProps) { - + Import @@ -224,7 +238,7 @@ function FlowCanvas({ drawerVisible, hideDrawer }: IProps) { - + Export @@ -238,6 +252,9 @@ function FlowCanvas({ drawerVisible, hideDrawer }: IProps) { node={clickedNode} visible={formDrawerVisible} hideModal={hideFormDrawer} + singleDebugDrawerVisible={singleDebugDrawerVisible} + hideSingleDebugDrawer={hideSingleDebugDrawer} + showSingleDebugDrawer={showSingleDebugDrawer} > )} {chatVisible && ( diff --git a/web/src/pages/flow/canvas/node/node-header.tsx b/web/src/pages/flow/canvas/node/node-header.tsx index dafddf137a..f9bb287060 100644 --- a/web/src/pages/flow/canvas/node/node-header.tsx +++ b/web/src/pages/flow/canvas/node/node-header.tsx @@ -1,12 +1,14 @@ +import { useTranslate } from '@/hooks/common-hooks'; import { Flex } from 'antd'; +import { Play } from 'lucide-react'; import { Operator, operatorMap } from '../../constant'; import OperatorIcon from '../../operator-icon'; +import { needsSingleStepDebugging } from '../../utils'; import NodeDropdown from './dropdown'; - -import { useTranslate } from '@/hooks/common-hooks'; -import styles from './index.less'; import { NextNodePopover } from './popover'; +import { RunTooltip } from '../../flow-tooltip'; +import styles from './index.less'; interface IProps { id: string; label: string; @@ -15,12 +17,17 @@ interface IProps { className?: string; } -export function RunStatus({ id, name }: Omit) { +export function RunStatus({ id, name, label }: IProps) { const { t } = useTranslate('flow'); return ( -
+
+ {needsSingleStepDebugging(label) && ( + + + // data-play is used to trigger single step debugging + )} - + {t('operationResults')} @@ -30,8 +37,10 @@ export function RunStatus({ id, name }: Omit) { const NodeHeader = ({ label, id, name, gap = 4, className }: IProps) => { return ( -
- {label !== Operator.Answer && } +
+ {label !== Operator.Answer && ( + + )} { + 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/flow/debug-content/popover-form.tsx b/web/src/pages/flow/debug-content/popover-form.tsx new file mode 100644 index 0000000000..557e3185bc --- /dev/null +++ b/web/src/pages/flow/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/flow/flow-drawer/index.less b/web/src/pages/flow/flow-drawer/index.less index 6c725fbb57..1348cc66dd 100644 --- a/web/src/pages/flow/flow-drawer/index.less +++ b/web/src/pages/flow/flow-drawer/index.less @@ -13,3 +13,9 @@ padding-top: 16px; font-weight: normal; } + +.formDrawer { + :global(.ant-drawer-content-wrapper) { + transform: translateX(0) !important; + } +} diff --git a/web/src/pages/flow/flow-drawer/index.tsx b/web/src/pages/flow/flow-drawer/index.tsx index f669038a27..bff6c3d998 100644 --- a/web/src/pages/flow/flow-drawer/index.tsx +++ b/web/src/pages/flow/flow-drawer/index.tsx @@ -1,6 +1,9 @@ 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 { lowerFirst } from 'lodash'; +import { Play } from 'lucide-react'; import { useEffect } from 'react'; import { Node } from 'reactflow'; import { Operator, operatorMap } from '../constant'; @@ -15,6 +18,7 @@ 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'; @@ -30,22 +34,24 @@ 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 { getDrawerWidth, needsSingleStepDebugging } from '../utils'; +import SingleDebugDrawer from './single-debug-drawer'; -import { CloseOutlined } from '@ant-design/icons'; -import { lowerFirst } from 'lodash'; -import EmailForm from '../form/email-form'; -import TemplateForm from '../form/template-form'; -import { getDrawerWidth } from '../utils'; +import { RunTooltip } from '../flow-tooltip'; import styles from './index.less'; interface IProps { node?: Node; + singleDebugDrawerVisible: IModalProps['visible']; + hideSingleDebugDrawer: IModalProps['hideModal']; + showSingleDebugDrawer: IModalProps['showModal']; } const FormMap = { @@ -91,6 +97,9 @@ const FormDrawer = ({ visible, hideModal, node, + singleDebugDrawerVisible, + hideSingleDebugDrawer, + showSingleDebugDrawer, }: IModalProps & IProps) => { const operatorName: Operator = node?.data.label; const OperatorForm = FormMap[operatorName] ?? EmptyContent; @@ -99,12 +108,14 @@ const FormDrawer = ({ id: node?.id, data: node?.data, }); + const { t } = useTranslate('flow'); const { handleValuesChange } = useHandleFormValuesChange(node?.id); useEffect(() => { if (visible) { + form.resetFields(); form.setFieldsValue(node?.data?.form); } }, [visible, form, node?.data?.form]); @@ -128,6 +139,14 @@ const FormDrawer = ({ onChange={handleNameChange} >
+ {needsSingleStepDebugging(operatorName) && ( + + + + )} @@ -142,6 +161,7 @@ const FormDrawer = ({ mask={false} width={getDrawerWidth()} closeIcon={null} + rootClassName={styles.formDrawer} >
{visible && ( @@ -152,6 +172,13 @@ const FormDrawer = ({ > )}
+ {singleDebugDrawerVisible && ( + + )} ); }; diff --git a/web/src/pages/flow/flow-drawer/single-debug-drawer/index.tsx b/web/src/pages/flow/flow-drawer/single-debug-drawer/index.tsx new file mode 100644 index 0000000000..f9908ef7e5 --- /dev/null +++ b/web/src/pages/flow/flow-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/flow/flow-tooltip.tsx b/web/src/pages/flow/flow-tooltip.tsx new file mode 100644 index 0000000000..d393932499 --- /dev/null +++ b/web/src/pages/flow/flow-tooltip.tsx @@ -0,0 +1,22 @@ +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from '@/components/ui/tooltip'; +import { PropsWithChildren } from 'react'; +import { useTranslation } from 'react-i18next'; + +export const RunTooltip = ({ children }: PropsWithChildren) => { + const { t } = useTranslation(); + return ( + + + {children} + +

{t('flow.testRun')}

+
+
+
+ ); +}; diff --git a/web/src/pages/flow/form/invoke-form/index.tsx b/web/src/pages/flow/form/invoke-form/index.tsx index 926ca8f2ab..f3f2db5e3e 100644 --- a/web/src/pages/flow/form/invoke-form/index.tsx +++ b/web/src/pages/flow/form/invoke-form/index.tsx @@ -1,9 +1,11 @@ -import Editor from '@monaco-editor/react'; +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', diff --git a/web/src/pages/flow/hooks.tsx b/web/src/pages/flow/hooks.tsx index 21a238483f..9608e3b134 100644 --- a/web/src/pages/flow/hooks.tsx +++ b/web/src/pages/flow/hooks.tsx @@ -620,7 +620,6 @@ export const useWatchNodeFormDataChange = () => { ); useEffect(() => { - console.info('xxx'); nodes.forEach((node) => { const currentNode = getNode(node.id); const form = currentNode?.data.form ?? {}; @@ -856,3 +855,21 @@ export const useHandleExportOrImportJsonFile = () => { onFileUploadOk, }; }; + +export const useShowSingleDebugDrawer = () => { + const { visible, showModal, hideModal } = useSetModalState(); + const { saveGraph } = useSaveGraph(); + + const showSingleDebugDrawer = useCallback(async () => { + const saveRet = await saveGraph(); + if (saveRet?.code === 0) { + showModal(); + } + }, [saveGraph, showModal]); + + return { + singleDebugDrawerVisible: visible, + hideSingleDebugDrawer: hideModal, + showSingleDebugDrawer, + }; +}; diff --git a/web/src/pages/flow/run-drawer/index.tsx b/web/src/pages/flow/run-drawer/index.tsx index dda626e01a..063c5ad47f 100644 --- a/web/src/pages/flow/run-drawer/index.tsx +++ b/web/src/pages/flow/run-drawer/index.tsx @@ -1,26 +1,7 @@ -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 { UploadOutlined } from '@ant-design/icons'; -import { - Button, - Drawer, - Form, - FormItemProps, - Input, - InputNumber, - Select, - Switch, - Upload, -} from 'antd'; -import { pick } from 'lodash'; -import React, { useCallback, useState } from 'react'; +import { Drawer } from 'antd'; +import { useCallback } from 'react'; import { useTranslation } from 'react-i18next'; -import { BeginQueryType } from '../constant'; import { useGetBeginNodeDataQuery, useSaveGraphBeforeOpeningDebugDrawer, @@ -28,152 +9,23 @@ import { import { BeginQuery } from '../interface'; import useGraphStore from '../store'; import { getDrawerWidth } from '../utils'; -import { PopoverForm } from './popover-form'; -import { UploadChangeParam, UploadFile } from 'antd/es/upload'; -import { Link } from 'lucide-react'; -import styles from './index.less'; +import DebugContent from '../debug-content'; const RunDrawer = ({ hideModal, showModal: showChatModal, }: IModalProps) => { 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(); - const { submittable } = useHandleSubmittable(form); - const [isUploading, setIsUploading] = useState(false); - - const handleShowPopover = useCallback( - (idx: number) => () => { - setRecord(idx); - showPopover(); - }, - [setRecord, showPopover], - ); const getBeginNodeDataQuery = useGetBeginNodeDataQuery(); const query: BeginQuery[] = getBeginNodeDataQuery(); - 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 { handleRun, loading } = useSaveGraphBeforeOpeningDebugDrawer( + showChatModal!, ); - 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]: ( - - - - ), - [BeginQueryType.Paragraph]: ( - - - - ), - [BeginQueryType.Options]: ( - - - - ), - [BeginQueryType.File]: ( - - -
- - - - - - 0 ? 'mb-1' : ''} - noStyle - > - - - - -
-
- -
- ), - [BeginQueryType.Integer]: ( - - - - ), - [BeginQueryType.Boolean]: ( - - - - ), - }; - - return BeginQueryTypeMap[q.type as BeginQueryType]; - }, - [form, handleShowPopover, onChange, switchVisible, t, visible], - ); - - const { handleRun } = useSaveGraphBeforeOpeningDebugDrawer(showChatModal!); - const handleRunAgent = useCallback( (nextValues: Record) => { const currentNodes = updateNodeForm('begin', nextValues, ['query']); @@ -183,25 +35,12 @@ const RunDrawer = ({ [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) => { - nextValue += - x?.originFileObj instanceof File - ? `${x.name}\n${x.response?.data}\n----\n` - : `${x.url}\n${x.result}\n----\n`; - }); - } - return { ...item, value: nextValue }; - }); - handleRunAgent(nextValues); - }, [form, handleRunAgent, query]); + const onOk = useCallback( + async (nextValues: any[]) => { + handleRunAgent(nextValues); + }, + [handleRunAgent], + ); return ( -
- { - if (name === 'urlForm') { - const { basicForm } = forms; - const urlInfo = basicForm.getFieldValue(currentRecord) || []; - basicForm.setFieldsValue({ - [currentRecord]: [...urlInfo, { ...values, name: values.url }], - }); - hidePopover(); - } - }} - > -
- {query.map((x, idx) => { - return renderWidget(x, idx); - })} -
-
-
- +
); }; diff --git a/web/src/pages/flow/utils.ts b/web/src/pages/flow/utils.ts index e3b66d5719..a466210955 100644 --- a/web/src/pages/flow/utils.ts +++ b/web/src/pages/flow/utils.ts @@ -7,7 +7,12 @@ import pipe from 'lodash/fp/pipe'; import isObject from 'lodash/isObject'; import { Edge, Node, Position } from 'reactflow'; import { v4 as uuidv4 } from 'uuid'; -import { CategorizeAnchorPointPositions, NodeMap, Operator } from './constant'; +import { + CategorizeAnchorPointPositions, + NoDebugOperatorsList, + NodeMap, + Operator, +} from './constant'; import { ICategorizeItemResult, IPosition, NodeData } from './interface'; const buildEdges = ( @@ -124,7 +129,7 @@ export const buildDslComponentsByGraph = ( const components: DSLComponents = {}; nodes - .filter((x) => x.data.label !== Operator.Note) + ?.filter((x) => x.data.label !== Operator.Note) .forEach((x) => { const id = x.id; const operatorName = x.data.label; @@ -323,3 +328,7 @@ export const duplicateNodeForm = (nodeData?: NodeData) => { export const getDrawerWidth = () => { return window.innerWidth > 1278 ? '40%' : 470; }; + +export const needsSingleStepDebugging = (label: string) => { + return !NoDebugOperatorsList.some((x) => (label as Operator) === x); +}; diff --git a/web/src/pages/login/index.tsx b/web/src/pages/login/index.tsx index 45f8e68f76..0bc24cf047 100644 --- a/web/src/pages/login/index.tsx +++ b/web/src/pages/login/index.tsx @@ -168,7 +168,7 @@ const Login = () => { onClick={toGoogle} style={{ marginTop: 15 }} > -
+
(methods, request); diff --git a/web/src/utils/api.ts b/web/src/utils/api.ts index f2d5d78216..6633490e01 100644 --- a/web/src/utils/api.ts +++ b/web/src/utils/api.ts @@ -118,4 +118,6 @@ export default { resetCanvas: `${api_host}/canvas/reset`, runCanvas: `${api_host}/canvas/completion`, testDbConnect: `${api_host}/canvas/test_db_connect`, + getInputElements: `${api_host}/canvas/input_elements`, + debug: `${api_host}/canvas/debug`, }; diff --git a/web/src/utils/request.ts b/web/src/utils/request.ts index 9668a93c0d..fe15772897 100644 --- a/web/src/utils/request.ts +++ b/web/src/utils/request.ts @@ -107,31 +107,25 @@ request.interceptors.response.use(async (response: any, options) => { return response; } - const data: ResponseType = await response.clone().json(); - - if (data.code === 401 || data.code === 401) { + const data: ResponseType = await response?.clone()?.json(); + if (data?.code === 100) { + message.error(data?.message); + } else if (data?.code === 401) { notification.error({ - message: data.message, - description: data.message, + message: data?.message, + description: data?.message, duration: 3, }); authorizationUtil.removeAll(); history.push('/login'); // Will not jump to the login page - } else if (data.code !== 0) { - if (data.code === 100) { - message.error(data.message); - } else { - notification.error({ - message: `${i18n.t('message.hint')} : ${data.code}`, - description: data.message, - duration: 3, - }); - } - - return response; - } else { - return response; + } else if (data?.code !== 0) { + notification.error({ + message: `${i18n.t('message.hint')} : ${data?.code}`, + description: data?.message, + duration: 3, + }); } + return response; }); export default request;