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)
This commit is contained in:
balibabu 2024-12-13 14:43:24 +08:00 committed by GitHub
parent 0bca46ac3a
commit 1defe0b19b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
23 changed files with 645 additions and 262 deletions

View File

@ -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 };
};

View File

@ -0,0 +1,4 @@
export interface IDebugSingleRequestBody {
component_id: string;
params: any[];
}

View File

@ -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 models 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:

View File

@ -414,7 +414,7 @@ export default {
partialTitle: '部分嵌入',
extensionTitle: 'Chrome 插件',
tokenError: '請先創建 API Token!',
betaError: 'API Token的beta欄位不可以為空',
betaError: '請先在系統設定中申請API密鑰。',
searching: '搜索中',
parsing: '解析中',
uploading: '上傳中',

View File

@ -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:

View File

@ -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) {
<ControlButton onClick={handleImportJson}>
<TooltipProvider>
<Tooltip>
<TooltipTrigger>
<TooltipTrigger asChild>
<FolderInput />
</TooltipTrigger>
<TooltipContent>Import</TooltipContent>
@ -224,7 +238,7 @@ function FlowCanvas({ drawerVisible, hideDrawer }: IProps) {
<ControlButton onClick={handleExportJson}>
<TooltipProvider>
<Tooltip>
<TooltipTrigger>
<TooltipTrigger asChild>
<FolderOutput />
</TooltipTrigger>
<TooltipContent>Export</TooltipContent>
@ -238,6 +252,9 @@ function FlowCanvas({ drawerVisible, hideDrawer }: IProps) {
node={clickedNode}
visible={formDrawerVisible}
hideModal={hideFormDrawer}
singleDebugDrawerVisible={singleDebugDrawerVisible}
hideSingleDebugDrawer={hideSingleDebugDrawer}
showSingleDebugDrawer={showSingleDebugDrawer}
></FormDrawer>
)}
{chatVisible && (

View File

@ -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<IProps, 'label'>) {
export function RunStatus({ id, name, label }: IProps) {
const { t } = useTranslate('flow');
return (
<section className="flex justify-end items-center pb-1 ">
<section className="flex justify-end items-center pb-1 gap-2 text-blue-600">
{needsSingleStepDebugging(label) && (
<RunTooltip>
<Play className="size-3 cursor-pointer" data-play />
</RunTooltip> // data-play is used to trigger single step debugging
)}
<NextNodePopover nodeId={id} name={name}>
<span className="text-blue-600 cursor-pointer text-[10px]">
<span className="cursor-pointer text-[10px]">
{t('operationResults')}
</span>
</NextNodePopover>
@ -30,8 +37,10 @@ export function RunStatus({ id, name }: Omit<IProps, 'label'>) {
const NodeHeader = ({ label, id, name, gap = 4, className }: IProps) => {
return (
<section className="haha">
{label !== Operator.Answer && <RunStatus id={id} name={name}></RunStatus>}
<section>
{label !== Operator.Answer && (
<RunStatus id={id} name={name} label={label}></RunStatus>
)}
<Flex
flex={1}
align="center"

View File

@ -2931,3 +2931,13 @@ export const BeginQueryTypeIconMap = {
[BeginQueryType.Integer]: ListOrdered,
[BeginQueryType.Boolean]: ToggleLeft,
};
export const NoDebugOperatorsList = [
Operator.Begin,
Operator.Answer,
Operator.Concentrator,
Operator.Template,
Operator.Message,
Operator.RewriteQuestion,
Operator.Switch,
];

View File

@ -0,0 +1,5 @@
.formWrapper {
:global(.ant-form-item-label) {
font-weight: 600 !important;
}
}

View File

@ -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<number>();
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<UploadFile>) => {
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]: (
<Form.Item {...props}>
<Input></Input>
</Form.Item>
),
[BeginQueryType.Paragraph]: (
<Form.Item {...props}>
<Input.TextArea rows={1}></Input.TextArea>
</Form.Item>
),
[BeginQueryType.Options]: (
<Form.Item {...props}>
<Select
allowClear
options={q.options?.map((x) => ({ label: x, value: x })) ?? []}
></Select>
</Form.Item>
),
[BeginQueryType.File]: (
<React.Fragment key={idx}>
<Form.Item label={q.name ?? q.key} required={!q.optional}>
<div className="relative">
<Form.Item
{...props}
valuePropName="fileList"
getValueFromEvent={normFile}
noStyle
>
<Upload
name="file"
action={api.parse}
multiple
headers={{ [Authorization]: getAuthorization() }}
onChange={onChange(q.optional)}
>
<Button icon={<UploadOutlined />}>
{t('common.upload')}
</Button>
</Upload>
</Form.Item>
<Form.Item
{...pick(props, ['key', 'label', 'rules'])}
required={!q.optional}
className={urlList.length > 0 ? 'mb-1' : ''}
noStyle
>
<PopoverForm visible={visible} switchVisible={switchVisible}>
<Button
onClick={handleShowPopover(idx)}
className="absolute left-1/2 top-0"
icon={<Link className="size-3" />}
>
{t('flow.pasteFileLink')}
</Button>
</PopoverForm>
</Form.Item>
</div>
</Form.Item>
<Form.Item name={idx} noStyle {...pick(props, ['rules'])} />
</React.Fragment>
),
[BeginQueryType.Integer]: (
<Form.Item {...props}>
<InputNumber></InputNumber>
</Form.Item>
),
[BeginQueryType.Boolean]: (
<Form.Item valuePropName={'checked'} {...props}>
<Switch></Switch>
</Form.Item>
),
};
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 (
<>
<section className={styles.formWrapper}>
<Form.Provider
onFormFinish={(name, { values, forms }) => {
if (name === 'urlForm') {
const { basicForm } = forms;
const urlInfo = basicForm.getFieldValue(currentRecord) || [];
basicForm.setFieldsValue({
[currentRecord]: [...urlInfo, { ...values, name: values.url }],
});
hidePopover();
}
}}
>
<Form
name="basicForm"
autoComplete="off"
layout={'vertical'}
form={form}
>
{parameters.map((x, idx) => {
return renderWidget(x, idx);
})}
</Form>
</Form.Provider>
</section>
<Button
type={'primary'}
block
onClick={onOk}
loading={loading}
disabled={!submittable || isUploading || submitButtonDisabled}
>
{t(isNext ? 'common.next' : 'flow.run')}
</Button>
</>
);
};
export default DebugContent;

View File

@ -0,0 +1,74 @@
import { useParseDocument } from '@/hooks/document-hooks';
import { useResetFormOnCloseModal } from '@/hooks/logic-hooks';
import { IModalProps } from '@/interfaces/common';
import { Button, Form, Input, Popover } from 'antd';
import { PropsWithChildren } from 'react';
import { useTranslation } from 'react-i18next';
const reg =
/^(((ht|f)tps?):\/\/)?([^!@#$%^&*?.\s-]([^!@#$%^&*?.\s]{0,63}[^!@#$%^&*?.\s])?\.)+[a-z]{2,6}\/?/;
export const PopoverForm = ({
children,
visible,
switchVisible,
}: PropsWithChildren<IModalProps<any>>) => {
const [form] = Form.useForm();
const { parseDocument, loading } = useParseDocument();
const { t } = useTranslation();
useResetFormOnCloseModal({
form,
visible,
});
const onOk = async () => {
const values = await form.validateFields();
const val = values.url;
if (reg.test(val)) {
const ret = await parseDocument(val);
if (ret?.data?.code === 0) {
form.setFieldValue('result', ret?.data?.data);
form.submit();
}
}
};
const content = (
<Form form={form} name="urlForm">
<Form.Item
name="url"
rules={[{ required: true, type: 'url' }]}
className="m-0"
>
<Input
onPressEnter={(e) => e.preventDefault()}
placeholder={t('flow.pasteFileLink')}
suffix={
<Button
type="primary"
onClick={onOk}
size={'small'}
loading={loading}
>
{t('common.submit')}
</Button>
}
/>
</Form.Item>
<Form.Item name={'result'} noStyle />
</Form>
);
return (
<Popover
content={content}
open={visible}
trigger={'click'}
onOpenChange={switchVisible}
>
{children}
</Popover>
);
};

View File

@ -13,3 +13,9 @@
padding-top: 16px;
font-weight: normal;
}
.formDrawer {
:global(.ant-drawer-content-wrapper) {
transform: translateX(0) !important;
}
}

View File

@ -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<any>['visible'];
hideSingleDebugDrawer: IModalProps<any>['hideModal'];
showSingleDebugDrawer: IModalProps<any>['showModal'];
}
const FormMap = {
@ -91,6 +97,9 @@ const FormDrawer = ({
visible,
hideModal,
node,
singleDebugDrawerVisible,
hideSingleDebugDrawer,
showSingleDebugDrawer,
}: IModalProps<any> & 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}
></Input>
</Flex>
{needsSingleStepDebugging(operatorName) && (
<RunTooltip>
<Play
className="size-5 cursor-pointer"
onClick={showSingleDebugDrawer}
/>
</RunTooltip>
)}
<CloseOutlined onClick={hideModal} />
</Flex>
<span className={styles.operatorDescription}>
@ -142,6 +161,7 @@ const FormDrawer = ({
mask={false}
width={getDrawerWidth()}
closeIcon={null}
rootClassName={styles.formDrawer}
>
<section className={styles.formWrapper}>
{visible && (
@ -152,6 +172,13 @@ const FormDrawer = ({
></OperatorForm>
)}
</section>
{singleDebugDrawerVisible && (
<SingleDebugDrawer
visible={singleDebugDrawerVisible}
hideModal={hideSingleDebugDrawer}
componentId={node?.id}
></SingleDebugDrawer>
)}
</Drawer>
);
};

View File

@ -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<any> & 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 (
<Drawer
title={
<div className="flex justify-between">
{t('flow.testRun')}
<CloseOutlined onClick={hideModal} />
</div>
}
width={'100%'}
onClose={hideModal}
open={visible}
getContainer={false}
mask={false}
placement={'bottom'}
height={'95%'}
closeIcon={null}
>
<section className="overflow-y-auto">
<DebugContent
parameters={list}
ok={onOk}
isNext={false}
loading={loading}
submitButtonDisabled={list.length === 0}
></DebugContent>
{!isEmpty(data) ? (
<div className="mt-4 rounded-md bg-slate-200 border border-neutral-200">
<div className="flex justify-between p-2">
<span>JSON</span>
<CopyToClipboard text={content}></CopyToClipboard>
</div>
<JsonView
src={data}
displaySize
collapseStringsAfterLength={100000000000}
className="w-full h-[800px] break-words overflow-auto p-2 bg-slate-100"
/>
</div>
) : null}
</section>
</Drawer>
);
};
export default SingleDebugDrawer;

View File

@ -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 (
<TooltipProvider>
<Tooltip>
<TooltipTrigger>{children}</TooltipTrigger>
<TooltipContent>
<p>{t('flow.testRun')}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
);
};

View File

@ -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',

View File

@ -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,
};
};

View File

@ -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<any>) => {
const { t } = useTranslation();
const [form] = Form.useForm();
const updateNodeForm = useGraphStore((state) => state.updateNodeForm);
const {
visible,
hideModal: hidePopover,
switchVisible,
showModal: showPopover,
} = useSetModalState();
const { setRecord, currentRecord } = useSetSelectedRecord<number>();
const { submittable } = useHandleSubmittable(form);
const [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<UploadFile>) => {
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]: (
<Form.Item {...props}>
<Input></Input>
</Form.Item>
),
[BeginQueryType.Paragraph]: (
<Form.Item {...props}>
<Input.TextArea rows={4}></Input.TextArea>
</Form.Item>
),
[BeginQueryType.Options]: (
<Form.Item {...props}>
<Select
allowClear
options={q.options?.map((x) => ({ label: x, value: x })) ?? []}
></Select>
</Form.Item>
),
[BeginQueryType.File]: (
<React.Fragment key={idx}>
<Form.Item label={q.name} required={!q.optional}>
<div className="relative">
<Form.Item
{...props}
valuePropName="fileList"
getValueFromEvent={normFile}
noStyle
>
<Upload
name="file"
action={api.parse}
multiple
headers={{ [Authorization]: getAuthorization() }}
onChange={onChange(q.optional)}
>
<Button icon={<UploadOutlined />}>
{t('common.upload')}
</Button>
</Upload>
</Form.Item>
<Form.Item
{...pick(props, ['key', 'label', 'rules'])}
required={!q.optional}
className={urlList.length > 0 ? 'mb-1' : ''}
noStyle
>
<PopoverForm visible={visible} switchVisible={switchVisible}>
<Button
onClick={handleShowPopover(idx)}
className="absolute left-1/2 top-0"
icon={<Link className="size-3" />}
>
{t('flow.pasteFileLink')}
</Button>
</PopoverForm>
</Form.Item>
</div>
</Form.Item>
<Form.Item name={idx} noStyle {...pick(props, ['rules'])} />
</React.Fragment>
),
[BeginQueryType.Integer]: (
<Form.Item {...props}>
<InputNumber></InputNumber>
</Form.Item>
),
[BeginQueryType.Boolean]: (
<Form.Item valuePropName={'checked'} {...props}>
<Switch></Switch>
</Form.Item>
),
};
return BeginQueryTypeMap[q.type as BeginQueryType];
},
[form, handleShowPopover, onChange, switchVisible, t, visible],
);
const { handleRun } = useSaveGraphBeforeOpeningDebugDrawer(showChatModal!);
const handleRunAgent = useCallback(
(nextValues: Record<string, any>) => {
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 (
<Drawer
@ -213,39 +52,11 @@ const RunDrawer = ({
width={getDrawerWidth()}
mask={false}
>
<section className={styles.formWrapper}>
<Form.Provider
onFormFinish={(name, { values, forms }) => {
if (name === 'urlForm') {
const { basicForm } = forms;
const urlInfo = basicForm.getFieldValue(currentRecord) || [];
basicForm.setFieldsValue({
[currentRecord]: [...urlInfo, { ...values, name: values.url }],
});
hidePopover();
}
}}
>
<Form
name="basicForm"
autoComplete="off"
layout={'vertical'}
form={form}
>
{query.map((x, idx) => {
return renderWidget(x, idx);
})}
</Form>
</Form.Provider>
</section>
<Button
type={'primary'}
block
onClick={onOk}
disabled={!submittable || isUploading}
>
{t('common.next')}
</Button>
<DebugContent
ok={onOk}
parameters={query}
loading={loading}
></DebugContent>
</Drawer>
);
};

View File

@ -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);
};

View File

@ -168,7 +168,7 @@ const Login = () => {
onClick={toGoogle}
style={{ marginTop: 15 }}
>
<div>
<div className="flex items-center">
<Icon
icon="local:github"
style={{ verticalAlign: 'middle', marginRight: 5 }}

View File

@ -11,6 +11,8 @@ const {
runCanvas,
listTemplates,
testDbConnect,
getInputElements,
debug,
} = api;
const methods = {
@ -46,6 +48,14 @@ const methods = {
url: testDbConnect,
method: 'post',
},
getInputElements: {
url: getInputElements,
method: 'get',
},
debugSingle: {
url: debug,
method: 'post',
},
} as const;
const chatService = registerServer<keyof typeof methods>(methods, request);

View File

@ -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`,
};

View File

@ -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;