feat: 添加 NodeDebugResponse 组件以增强调试功能

This commit is contained in:
sd0ric4
2025-03-26 18:47:53 +08:00
parent fb24fddb60
commit 7dd8bc9fb8
2 changed files with 249 additions and 239 deletions

View File

@@ -23,22 +23,12 @@ import { moduleTemplatesFlat } from '@fastgpt/global/core/workflow/template/cons
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { useWorkflowUtils } from '../../hooks/useUtils';
import { WholeResponseContent } from '@/components/core/chat/components/WholeResponseModal';
import { WorkflowNodeEdgeContext } from '../../../context/workflowInitContext';
import { WorkflowEventContext } from '../../../context/workflowEventContext';
import MyImage from '@fastgpt/web/components/common/Image/MyImage';
import MyIconButton from '@fastgpt/web/components/common/Icon/button';
import UseGuideModal from '@/components/common/Modal/UseGuideModal';
import {
RenderUserSelectInteractive,
RenderUserFormInteractive
} from '@/components/core/chat/components/InteractiveComponents';
import {
InteractiveBasicType,
UserInputInteractive,
UserSelectInteractive,
WorkflowInteractiveResponseType
} from '@fastgpt/global/core/workflow/template/system/interactive/type';
import NodeDebugResponse from './RenderDebug/NodeDebugResponse';
type Props = FlowNodeItemType & {
children?: React.ReactNode | React.ReactNode[] | string;
@@ -672,231 +662,3 @@ const NodeIntro = React.memo(function NodeIntro({
return Render;
});
const NodeDebugResponse = React.memo(function NodeDebugResponse({
nodeId,
debugResult
}: {
nodeId: string;
debugResult: FlowNodeItemType['debugResult'];
}) {
const { t } = useTranslation();
const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList);
// 获取当前节点
const node = useMemo(() => nodeList.find((node) => node.nodeId === nodeId), [nodeList, nodeId]);
const firstInteractive = useMemo(() => {
if (
node &&
node.flowNodeType === FlowNodeTypeEnum.userSelect &&
!node.debugResult?.response?.userSelectResult
) {
return true;
}
if (
node &&
node.flowNodeType === FlowNodeTypeEnum.formInput &&
!node.debugResult?.response?.formInputResult
) {
return true;
}
return false; // 明确返回值
}, [node]);
const { onChangeNode, onStopNodeDebug, onNextNodeDebug, workflowDebugData } = useContextSelector(
WorkflowContext,
(v) => v
);
const interactive: UserSelectInteractive | UserInputInteractive | undefined = useMemo(() => {
const description = node?.inputs?.find((input) => input.key === 'description')?.value;
const userSelectOptions = node?.inputs?.find(
(input) => input.key === 'userSelectOptions'
)?.value;
const formInputForms = node?.inputs?.find((input) => input.key === 'userInputForms')?.value;
if (node?.flowNodeType === FlowNodeTypeEnum.userSelect) {
return {
type: 'userSelect',
params: {
description,
userSelectOptions
}
};
}
if (node?.flowNodeType === FlowNodeTypeEnum.formInput) {
return {
type: 'userInput',
params: {
description,
inputForm: formInputForms
}
};
}
return undefined;
}, [node]);
const { openConfirm, ConfirmModal } = useConfirm({
content: t('common:core.workflow.Confirm stop debug')
});
const RenderStatus = useMemo(() => {
const map = {
running: {
bg: 'primary.50',
text: t('common:core.workflow.Running'),
icon: 'core/workflow/running'
},
success: {
bg: 'green.50',
text: t('common:core.workflow.Success'),
icon: 'core/workflow/runSuccess'
},
failed: {
bg: 'red.50',
text: t('common:core.workflow.Failed'),
icon: 'core/workflow/runError'
},
skipped: {
bg: 'myGray.50',
text: t('common:core.workflow.Skipped'),
icon: 'core/workflow/runSkip'
}
};
const statusData = map[debugResult?.status || 'running'];
const response = debugResult?.response;
const onStop = () => {
openConfirm(onStopNodeDebug)();
};
return !!debugResult && !!statusData ? (
<>
<Flex px={3} bg={statusData.bg} borderTopRadius={'md'} py={3}>
<MyIcon name={statusData.icon as any} w={'16px'} mr={2} />
<Box color={'myGray.900'} fontWeight={'bold'} flex={'1 0 0'}>
{statusData.text}
</Box>
{debugResult.status !== 'running' && (
<Box
color={'primary.700'}
cursor={'pointer'}
fontSize={'sm'}
onClick={() =>
onChangeNode({
nodeId,
type: 'attr',
key: 'debugResult',
value: {
...debugResult,
showResult: !debugResult.showResult
}
})
}
>
{debugResult.showResult
? t('common:core.workflow.debug.Hide result')
: t('common:core.workflow.debug.Show result')}
</Box>
)}
</Flex>
{/* Result card */}
{debugResult.showResult && (
<Card
className="nowheel"
position={'absolute'}
right={'-430px'}
top={0}
zIndex={10}
w={'420px'}
maxH={'max(100%,500px)'}
border={'base'}
>
{/* Status header */}
<Flex h={'54x'} px={3} py={3} alignItems={'center'}>
<MyIcon mr={1} name={'core/workflow/debugResult'} w={'20px'} color={'primary.600'} />
<Box fontWeight={'bold'} flex={'1'}>
{t('common:core.workflow.debug.Run result')}
</Box>
{workflowDebugData?.nextRunNodes.length !== 0 && (
<Button
size={'sm'}
leftIcon={<MyIcon name={'core/chat/stopSpeech'} w={'16px'} />}
variant={'whiteDanger'}
onClick={onStop}
>
{t('common:core.workflow.Stop debug')}
</Button>
)}
{(debugResult.status === 'success' || debugResult.status === 'skipped') &&
!firstInteractive &&
!debugResult.isExpired &&
workflowDebugData?.nextRunNodes &&
workflowDebugData.nextRunNodes.length > 0 && (
<Button
ml={2}
size={'sm'}
leftIcon={<MyIcon name={'core/workflow/debugNext'} w={'16px'} />}
variant={'primary'}
onClick={() => onNextNodeDebug()}
>
{t('common:common.Next Step')}
</Button>
)}
{!firstInteractive &&
workflowDebugData?.nextRunNodes &&
workflowDebugData?.nextRunNodes.length === 0 && (
<Button ml={2} size={'sm'} variant={'primary'} onClick={onStopNodeDebug}>
{t('common:core.workflow.debug.Done')}
</Button>
)}
</Flex>
{/* Response list */}
{debugResult.status !== 'skipped' && (
<Box borderTop={'base'} mt={1} overflowY={'auto'} minH={'250px'}>
{!debugResult.message && !response && !firstInteractive && (
<EmptyTip text={t('common:core.workflow.debug.Not result')} pt={2} pb={5} />
)}
{debugResult.message && (
<Box color={'red.600'} px={3} py={4}>
{debugResult.message}
</Box>
)}
{firstInteractive && interactive && (
<>
{interactive.type === 'userSelect' && (
<RenderUserSelectInteractive interactive={interactive} nodeId={nodeId} />
)}
{interactive.type === 'userInput' && (
<RenderUserFormInteractive interactive={interactive} nodeId={nodeId} />
)}
</>
)}
{response && <WholeResponseContent activeModule={response} />}
</Box>
)}
</Card>
)}
</>
) : null;
}, [
interactive,
firstInteractive,
debugResult,
nodeId,
onChangeNode,
onNextNodeDebug,
onStopNodeDebug,
openConfirm,
t,
workflowDebugData
]);
return (
<>
{RenderStatus}
<ConfirmModal />
</>
);
});

View File

@@ -0,0 +1,248 @@
import React, { useMemo } from 'react';
import { Box, Button, Card, Flex } from '@chakra-ui/react';
import { useTranslation } from 'next-i18next';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { useConfirm } from '@fastgpt/web/hooks/useConfirm';
import { useContextSelector } from 'use-context-selector';
import { WorkflowContext } from '../../../../context';
import { WorkflowEventContext } from '../../../../context/workflowEventContext';
import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
import EmptyTip from '@fastgpt/web/components/common/EmptyTip';
import { WholeResponseContent } from '@/components/core/chat/components/WholeResponseModal';
import type { FlowNodeItemType } from '@fastgpt/global/core/workflow/type/node.d';
import {
RenderUserSelectInteractive,
RenderUserFormInteractive
} from '@/components/core/chat/components/InteractiveComponents';
import {
UserInputInteractive,
UserSelectInteractive
} from '@fastgpt/global/core/workflow/template/system/interactive/type';
interface NodeDebugResponseProps {
nodeId: string;
debugResult: FlowNodeItemType['debugResult'];
}
const NodeDebugResponse = ({ nodeId, debugResult }: NodeDebugResponseProps) => {
const { t } = useTranslation();
const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList);
const node = useMemo(() => nodeList.find((node) => node.nodeId === nodeId), [nodeList, nodeId]);
const firstInteractive = useMemo(() => {
if (
node &&
node.flowNodeType === FlowNodeTypeEnum.userSelect &&
!node.debugResult?.response?.userSelectResult
) {
return true;
}
if (
node &&
node.flowNodeType === FlowNodeTypeEnum.formInput &&
!node.debugResult?.response?.formInputResult
) {
return true;
}
return false;
}, [node]);
const { onChangeNode, onStopNodeDebug, onNextNodeDebug, workflowDebugData } = useContextSelector(
WorkflowContext,
(v) => v
);
const interactive: UserSelectInteractive | UserInputInteractive | undefined = useMemo(() => {
const description = node?.inputs?.find((input) => input.key === 'description')?.value;
const userSelectOptions = node?.inputs?.find(
(input) => input.key === 'userSelectOptions'
)?.value;
const formInputForms = node?.inputs?.find((input) => input.key === 'userInputForms')?.value;
if (node?.flowNodeType === FlowNodeTypeEnum.userSelect) {
return {
type: 'userSelect',
params: {
description,
userSelectOptions
}
};
}
if (node?.flowNodeType === FlowNodeTypeEnum.formInput) {
return {
type: 'userInput',
params: {
description,
inputForm: formInputForms
}
};
}
return undefined;
}, [node]);
const { openConfirm, ConfirmModal } = useConfirm({
content: t('common:core.workflow.Confirm stop debug')
});
const RenderStatus = useMemo(() => {
const map = {
running: {
bg: 'primary.50',
text: t('common:core.workflow.Running'),
icon: 'core/workflow/running'
},
success: {
bg: 'green.50',
text: t('common:core.workflow.Success'),
icon: 'core/workflow/runSuccess'
},
failed: {
bg: 'red.50',
text: t('common:core.workflow.Failed'),
icon: 'core/workflow/runError'
},
skipped: {
bg: 'myGray.50',
text: t('common:core.workflow.Skipped'),
icon: 'core/workflow/runSkip'
}
};
const statusData = map[debugResult?.status || 'running'];
const response = debugResult?.response;
const onStop = () => {
openConfirm(onStopNodeDebug)();
};
return !!debugResult && !!statusData ? (
<>
<Flex px={3} bg={statusData.bg} borderTopRadius={'md'} py={3}>
<MyIcon name={statusData.icon as any} w={'16px'} mr={2} />
<Box color={'myGray.900'} fontWeight={'bold'} flex={'1 0 0'}>
{statusData.text}
</Box>
{debugResult.status !== 'running' && (
<Box
color={'primary.700'}
cursor={'pointer'}
fontSize={'sm'}
onClick={() =>
onChangeNode({
nodeId,
type: 'attr',
key: 'debugResult',
value: {
...debugResult,
showResult: !debugResult.showResult
}
})
}
>
{debugResult.showResult
? t('common:core.workflow.debug.Hide result')
: t('common:core.workflow.debug.Show result')}
</Box>
)}
</Flex>
{/* Result card */}
{debugResult.showResult && (
<Card
className="nowheel"
position={'absolute'}
right={'-430px'}
top={0}
zIndex={10}
w={'420px'}
maxH={'max(100%,500px)'}
border={'base'}
>
{/* Status header */}
<Flex h={'54x'} px={3} py={3} alignItems={'center'}>
<MyIcon mr={1} name={'core/workflow/debugResult'} w={'20px'} color={'primary.600'} />
<Box fontWeight={'bold'} flex={'1'}>
{t('common:core.workflow.debug.Run result')}
</Box>
{workflowDebugData?.nextRunNodes.length !== 0 && (
<Button
size={'sm'}
leftIcon={<MyIcon name={'core/chat/stopSpeech'} w={'16px'} />}
variant={'whiteDanger'}
onClick={onStop}
>
{t('common:core.workflow.Stop debug')}
</Button>
)}
{(debugResult.status === 'success' || debugResult.status === 'skipped') &&
!firstInteractive &&
!debugResult.isExpired &&
workflowDebugData?.nextRunNodes &&
workflowDebugData.nextRunNodes.length > 0 && (
<Button
ml={2}
size={'sm'}
leftIcon={<MyIcon name={'core/workflow/debugNext'} w={'16px'} />}
variant={'primary'}
onClick={() => onNextNodeDebug()}
>
{t('common:common.Next Step')}
</Button>
)}
{!firstInteractive &&
workflowDebugData?.nextRunNodes &&
workflowDebugData?.nextRunNodes.length === 0 && (
<Button ml={2} size={'sm'} variant={'primary'} onClick={onStopNodeDebug}>
{t('common:core.workflow.debug.Done')}
</Button>
)}
</Flex>
{/* Response list */}
{debugResult.status !== 'skipped' && (
<Box borderTop={'base'} mt={1} overflowY={'auto'} minH={'250px'}>
{!debugResult.message && !response && !firstInteractive && (
<EmptyTip text={t('common:core.workflow.debug.Not result')} pt={2} pb={5} />
)}
{debugResult.message && (
<Box color={'red.600'} px={3} py={4}>
{debugResult.message}
</Box>
)}
{firstInteractive && interactive && (
<>
{interactive.type === 'userSelect' && (
<RenderUserSelectInteractive interactive={interactive} nodeId={nodeId} />
)}
{interactive.type === 'userInput' && (
<RenderUserFormInteractive interactive={interactive} nodeId={nodeId} />
)}
</>
)}
{response && <WholeResponseContent activeModule={response} />}
</Box>
)}
</Card>
)}
</>
) : null;
}, [
interactive,
firstInteractive,
debugResult,
nodeId,
onChangeNode,
onNextNodeDebug,
onStopNodeDebug,
openConfirm,
t,
workflowDebugData
]);
return (
<>
{RenderStatus}
<ConfirmModal />
</>
);
};
export default React.memo(NodeDebugResponse);