Check debug (#4384)

* feat : Added support for interactive nodes in the debugging interface (#4339)

* feat: add VSCode launch configuration and enhance debug API handler

* feat: refactor debug API handler to streamline workflow processing and enhance interactive chat features

* feat: enhance debug API handler with structured input forms and improved query handling

* feat: enhance debug API handler to support optional query and histories parameters

* feat: simplify query and histories initialization in debug API handler

* feat: add realmode parameter to workflow dispatch and update interactive handling

* feat: add optional query parameter to PostWorkflowDebugProps and remove realmode from ModuleDispatchProps

* feat: add history parameter to PostWorkflowDebugProps and update related components

* feat: remove realmode

* feat: simplify handler parameter destructuring in debug.ts

* feat: remove unused interactive prop from WholeResponseContent component

* feat: refactor onNextNodeDebug to use parameter object for better readability

* feat: Merge selections and next actions to remove unused state management

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

* feat: Simplify the import statements in InteractiveComponents.tsx

* feat: Update the handler function to use default parameters to simplify the code

* feat: Add optional workflowInteractiveResponse field to PostWorkflowDebugResponse type

* feat: Add the workflowInteractiveResponse field in the debugging handler to enhance response capabilities

* feat: Added workflowInteractiveResponse field in FlowNodeItemType to enhance responsiveness

* feat: Refactor NodeDebugResponse to utilize workflowInteractiveResponse for improved interactivity

* feat: Extend UserSelectInteractive and UserInputInteractive types to inherit from InteractiveBasicType

* feat: Refactor NodeDebugResponse to streamline interactive handling and improve code clarity

* feat: 重构交互式调试逻辑,创建共用 Hook 以简化用户选择和输入处理

* fix: type error

* feat: 重构 AIResponseBox 组件,简化用户交互逻辑并引入共用表单组件

* feat: 清理 AIResponseBox 和表单组件代码,移除冗余注释和未使用的导入

* fix: type error

* feat: 重构 AIResponseBox 组件,简化类型定义并优化代码结构

* refactor: 将 FormItem 接口更改为类型定义,优化代码结构

* refactor: 将 NodeDebugResponseProps 接口更改为类型定义,优化代码结构

* refactor: 移除不必要的入口节点检查,简化调试处理逻辑

* feat: 移动调试交互组件位置

* refactor: 将 InteractiveBasicType 中的属性设为可选,简化数据结构

* refactor: 优化类型定义

* refactor: 移除未使用的 ChatItemType 和 UserChatItemValueItemType 导入

* refactor: 将接口定义更改为类型别名,简化代码结构

* refactor: 更新类型定义,使用类型别名简化代码结构

* refactor: 使用类型导入简化代码结构,重构 AIResponseBox 组件

* refactor: 提取描述框和表单项标签组件,简化代码结构

* refactor: 移除多余的空行

* refactor: 移除多余的空行和注释

* refactor: 移除多余的空行,简化 AIResponseBox 组件代码

* refactor: 重构组件,移动 FormComponents 到 InteractiveComponents,简化代码结构

* refactor: 移除多余的空行,简化 NodeDebugResponse 组件代码

* refactor: 更新导入语句,使用 type 关键字优化类型导入

* refactor: 在 tsconfig.json 中启用 verbatimModuleSyntax 选项

* Revert "refactor: 在 tsconfig.json 中启用 verbatimModuleSyntax 选项"

This reverts commit 2b335a9938.

* revert: rendertool

* refactor: Remove unused imports and functions to simplify code

* perf: debug interactive

---------

Co-authored-by: Theresa <63280168+sd0ric4@users.noreply.github.com>
This commit is contained in:
Archer
2025-03-28 17:09:08 +08:00
committed by GitHub
parent 2d3ae7f944
commit 0ed99d8c9a
16 changed files with 792 additions and 503 deletions

39
.vscode/launch.json vendored Normal file
View File

@@ -0,0 +1,39 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "Next.js: debug server-side",
"type": "node-terminal",
"request": "launch",
"command": "pnpm run dev",
"cwd": "${workspaceFolder}/projects/app"
},
{
"name": "Next.js: debug client-side",
"type": "chrome",
"request": "launch",
"url": "http://localhost:3000"
},
{
"name": "Next.js: debug client-side (Edge)",
"type": "msedge",
"request": "launch",
"url": "http://localhost:3000"
},
{
"name": "Next.js: debug full stack",
"type": "node-terminal",
"request": "launch",
"command": "pnpm run dev",
"cwd": "${workspaceFolder}/projects/app",
"skipFiles": ["<node_internals>/**"],
"serverReadyAction": {
"action": "debugWithEdge",
"killOnServerStop": true,
"pattern": "- Local:.+(https?://.+)",
"uriFormat": "%s",
"webRoot": "${workspaceFolder}/projects/app"
}
}
]
}

View File

@@ -10,7 +10,6 @@ import { FlowNodeOutputItemType, ReferenceValueType } from '../type/io';
import { ChatItemType, NodeOutputItemType } from '../../../core/chat/type'; import { ChatItemType, NodeOutputItemType } from '../../../core/chat/type';
import { ChatItemValueTypeEnum, ChatRoleEnum } from '../../../core/chat/constants'; import { ChatItemValueTypeEnum, ChatRoleEnum } from '../../../core/chat/constants';
import { replaceVariable, valToStr } from '../../../common/string/tools'; import { replaceVariable, valToStr } from '../../../common/string/tools';
import { ChatCompletionChunk } from 'openai/resources';
export const getMaxHistoryLimitFromNodes = (nodes: StoreNodeItemType[]): number => { export const getMaxHistoryLimitFromNodes = (nodes: StoreNodeItemType[]): number => {
let limit = 10; let limit = 10;

View File

@@ -5,10 +5,36 @@ import { FlowNodeInputTypeEnum } from 'core/workflow/node/constant';
import { WorkflowIOValueTypeEnum } from 'core/workflow/constants'; import { WorkflowIOValueTypeEnum } from 'core/workflow/constants';
import type { ChatCompletionMessageParam } from '../../../../ai/type'; import type { ChatCompletionMessageParam } from '../../../../ai/type';
type InteractiveBasicType = {
entryNodeIds: string[];
memoryEdges: RuntimeEdgeItemType[];
nodeOutputs: NodeOutputItemType[];
toolParams?: {
entryNodeIds: string[]; // 记录工具中,交互节点的 Id而不是起始工作流的入口
memoryMessages: ChatCompletionMessageParam[]; // 这轮工具中,产生的新的 messages
toolCallId: string; // 记录对应 tool 的id用于后续交互节点可以替换掉 tool 的 response
};
};
type InteractiveNodeType = {
entryNodeIds?: string[];
memoryEdges?: RuntimeEdgeItemType[];
nodeOutputs?: NodeOutputItemType[];
};
export type UserSelectOptionItemType = { export type UserSelectOptionItemType = {
key: string; key: string;
value: string; value: string;
}; };
type UserSelectInteractive = InteractiveNodeType & {
type: 'userSelect';
params: {
description: string;
userSelectOptions: UserSelectOptionItemType[];
userSelectedVal?: string;
};
};
export type UserInputFormItemType = { export type UserInputFormItemType = {
type: FlowNodeInputTypeEnum; type: FlowNodeInputTypeEnum;
@@ -28,29 +54,7 @@ export type UserInputFormItemType = {
// select // select
list?: { label: string; value: string }[]; list?: { label: string; value: string }[];
}; };
type UserInputInteractive = InteractiveNodeType & {
type InteractiveBasicType = {
entryNodeIds: string[];
memoryEdges: RuntimeEdgeItemType[];
nodeOutputs: NodeOutputItemType[];
toolParams?: {
entryNodeIds: string[]; // 记录工具中,交互节点的 Id而不是起始工作流的入口
memoryMessages: ChatCompletionMessageParam[]; // 这轮工具中,产生的新的 messages
toolCallId: string; // 记录对应 tool 的id用于后续交互节点可以替换掉 tool 的 response
};
};
type UserSelectInteractive = {
type: 'userSelect';
params: {
description: string;
userSelectOptions: UserSelectOptionItemType[];
userSelectedVal?: string;
};
};
type UserInputInteractive = {
type: 'userInput'; type: 'userInput';
params: { params: {
description: string; description: string;
@@ -58,6 +62,5 @@ type UserInputInteractive = {
submitted?: boolean; submitted?: boolean;
}; };
}; };
export type InteractiveNodeResponseType = UserSelectInteractive | UserInputInteractive; export type InteractiveNodeResponseType = UserSelectInteractive | UserInputInteractive;
export type WorkflowInteractiveResponseType = InteractiveBasicType & InteractiveNodeResponseType; export type WorkflowInteractiveResponseType = InteractiveBasicType & InteractiveNodeResponseType;

View File

@@ -23,6 +23,7 @@ import { NextApiResponse } from 'next';
import { AppDetailType, AppSchema } from '../../app/type'; import { AppDetailType, AppSchema } from '../../app/type';
import { ParentIdType } from 'common/parentFolder/type'; import { ParentIdType } from 'common/parentFolder/type';
import { AppTypeEnum } from 'core/app/constants'; import { AppTypeEnum } from 'core/app/constants';
import { WorkflowInteractiveResponseType } from '../template/system/interactive/type';
export type FlowNodeCommonType = { export type FlowNodeCommonType = {
parentNodeId?: string; parentNodeId?: string;
@@ -120,6 +121,7 @@ export type FlowNodeItemType = FlowNodeTemplateType & {
showResult?: boolean; // show and hide result modal showResult?: boolean; // show and hide result modal
response?: ChatHistoryItemResType; response?: ChatHistoryItemResType;
isExpired?: boolean; isExpired?: boolean;
workflowInteractiveResponse?: WorkflowInteractiveResponseType;
}; };
isFolded?: boolean; isFolded?: boolean;
}; };

View File

@@ -44,14 +44,14 @@ import {
textAdaptGptResponse, textAdaptGptResponse,
replaceEditorVariable replaceEditorVariable
} from '@fastgpt/global/core/workflow/runtime/utils'; } from '@fastgpt/global/core/workflow/runtime/utils';
import { ChatNodeUsageType } from '@fastgpt/global/support/wallet/bill/type'; import type { ChatNodeUsageType } from '@fastgpt/global/support/wallet/bill/type';
import { dispatchRunTools } from './agent/runTool/index'; import { dispatchRunTools } from './agent/runTool/index';
import { ChatItemValueTypeEnum } from '@fastgpt/global/core/chat/constants'; import { ChatItemValueTypeEnum } from '@fastgpt/global/core/chat/constants';
import { DispatchFlowResponse } from './type'; import type { DispatchFlowResponse } from './type';
import { dispatchStopToolCall } from './agent/runTool/stopTool'; import { dispatchStopToolCall } from './agent/runTool/stopTool';
import { dispatchLafRequest } from './tools/runLaf'; import { dispatchLafRequest } from './tools/runLaf';
import { dispatchIfElse } from './tools/runIfElse'; import { dispatchIfElse } from './tools/runIfElse';
import { RuntimeEdgeItemType } from '@fastgpt/global/core/workflow/type/edge'; import type { RuntimeEdgeItemType } from '@fastgpt/global/core/workflow/type/edge';
import { getReferenceVariableValue } from '@fastgpt/global/core/workflow/runtime/utils'; import { getReferenceVariableValue } from '@fastgpt/global/core/workflow/runtime/utils';
import { dispatchSystemConfig } from './init/systemConfig'; import { dispatchSystemConfig } from './init/systemConfig';
import { dispatchUpdateVariable } from './tools/runUpdateVar'; import { dispatchUpdateVariable } from './tools/runUpdateVar';
@@ -62,7 +62,7 @@ import { dispatchTextEditor } from './tools/textEditor';
import { dispatchCustomFeedback } from './tools/customFeedback'; import { dispatchCustomFeedback } from './tools/customFeedback';
import { dispatchReadFiles } from './tools/readFiles'; import { dispatchReadFiles } from './tools/readFiles';
import { dispatchUserSelect } from './interactive/userSelect'; import { dispatchUserSelect } from './interactive/userSelect';
import { import type {
WorkflowInteractiveResponseType, WorkflowInteractiveResponseType,
InteractiveNodeResponseType InteractiveNodeResponseType
} from '@fastgpt/global/core/workflow/template/system/interactive/type'; } from '@fastgpt/global/core/workflow/template/system/interactive/type';
@@ -451,6 +451,11 @@ export async function dispatchWorkFlow(data: Props): Promise<DispatchFlowRespons
const interactiveResponse = nodeRunResult.result?.[DispatchNodeResponseKeyEnum.interactive]; const interactiveResponse = nodeRunResult.result?.[DispatchNodeResponseKeyEnum.interactive];
if (interactiveResponse) { if (interactiveResponse) {
pushStore(nodeRunResult.node, nodeRunResult.result); pushStore(nodeRunResult.node, nodeRunResult.result);
if (props.mode === 'debug') {
debugNextStepRunNodes = debugNextStepRunNodes.concat([nodeRunResult.node]);
}
nodeInteractiveResponse = { nodeInteractiveResponse = {
entryNodeIds: [nodeRunResult.node.nodeId], entryNodeIds: [nodeRunResult.node.nodeId],
interactiveResponse interactiveResponse

View File

@@ -1,11 +1,11 @@
import { chatValue2RuntimePrompt } from '@fastgpt/global/core/chat/adapt'; import { chatValue2RuntimePrompt } from '@fastgpt/global/core/chat/adapt';
import { NodeInputKeyEnum, NodeOutputKeyEnum } from '@fastgpt/global/core/workflow/constants'; import { NodeInputKeyEnum, NodeOutputKeyEnum } from '@fastgpt/global/core/workflow/constants';
import { DispatchNodeResponseKeyEnum } from '@fastgpt/global/core/workflow/runtime/constants'; import { DispatchNodeResponseKeyEnum } from '@fastgpt/global/core/workflow/runtime/constants';
import { import type {
DispatchNodeResultType, DispatchNodeResultType,
ModuleDispatchProps ModuleDispatchProps
} from '@fastgpt/global/core/workflow/runtime/type'; } from '@fastgpt/global/core/workflow/runtime/type';
import { import type {
UserInputFormItemType, UserInputFormItemType,
UserInputInteractive UserInputInteractive
} from '@fastgpt/global/core/workflow/template/system/interactive/type'; } from '@fastgpt/global/core/workflow/template/system/interactive/type';
@@ -32,7 +32,6 @@ export const dispatchFormInput = async (props: Props): Promise<FormInputResponse
query query
} = props; } = props;
const { isEntry } = node; const { isEntry } = node;
const interactive = getLastInteractiveValue(histories); const interactive = getLastInteractiveValue(histories);
// Interactive node is not the entry node, return interactive result // Interactive node is not the entry node, return interactive result

View File

@@ -1,5 +1,5 @@
import { DispatchNodeResponseKeyEnum } from '@fastgpt/global/core/workflow/runtime/constants'; import { DispatchNodeResponseKeyEnum } from '@fastgpt/global/core/workflow/runtime/constants';
import { import type {
DispatchNodeResultType, DispatchNodeResultType,
ModuleDispatchProps ModuleDispatchProps
} from '@fastgpt/global/core/workflow/runtime/type'; } from '@fastgpt/global/core/workflow/runtime/type';
@@ -30,7 +30,6 @@ export const dispatchUserSelect = async (props: Props): Promise<UserSelectRespon
query query
} = props; } = props;
const { nodeId, isEntry } = node; const { nodeId, isEntry } = node;
const interactive = getLastInteractiveValue(histories); const interactive = getLastInteractiveValue(histories);
// Interactive node is not the entry node, return interactive result // Interactive node is not the entry node, return interactive result

View File

@@ -106,7 +106,7 @@ export const getHistories = (history?: ChatItemType[] | number, histories: ChatI
/* value type format */ /* value type format */
export const valueTypeFormat = (value: any, type?: WorkflowIOValueTypeEnum) => { export const valueTypeFormat = (value: any, type?: WorkflowIOValueTypeEnum) => {
if (value === undefined) return; if (value === undefined) return;
if (!type) return value; if (!type || type === WorkflowIOValueTypeEnum.any) return value;
if (type === 'string') { if (type === 'string') {
if (typeof value !== 'object') return String(value); if (typeof value !== 'object') return String(value);
@@ -118,7 +118,7 @@ export const valueTypeFormat = (value: any, type?: WorkflowIOValueTypeEnum) => {
return Boolean(value); return Boolean(value);
} }
try { try {
if (WorkflowIOValueTypeEnum.arrayString && typeof value === 'string') { if (type === WorkflowIOValueTypeEnum.arrayString && typeof value === 'string') {
return [value]; return [value];
} }
if ( if (

View File

@@ -8,43 +8,80 @@ import {
Box, Box,
Button, Button,
Flex, Flex,
HStack, HStack
Textarea
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import { ChatItemValueTypeEnum } from '@fastgpt/global/core/chat/constants'; import { ChatItemValueTypeEnum } from '@fastgpt/global/core/chat/constants';
import { import type {
AIChatItemValueItemType, AIChatItemValueItemType,
ToolModuleResponseItemType, ToolModuleResponseItemType,
UserChatItemValueItemType UserChatItemValueItemType
} from '@fastgpt/global/core/chat/type'; } from '@fastgpt/global/core/chat/type';
import React, { useCallback, useEffect } from 'react'; import React, { useCallback, useMemo } from 'react';
import MyIcon from '@fastgpt/web/components/common/Icon'; import MyIcon from '@fastgpt/web/components/common/Icon';
import Avatar from '@fastgpt/web/components/common/Avatar'; import Avatar from '@fastgpt/web/components/common/Avatar';
import { import type {
InteractiveBasicType, InteractiveBasicType,
UserInputInteractive, UserInputInteractive,
UserSelectInteractive UserSelectInteractive
} from '@fastgpt/global/core/workflow/template/system/interactive/type'; } from '@fastgpt/global/core/workflow/template/system/interactive/type';
import { isEqual } from 'lodash'; import { isEqual } from 'lodash';
import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel';
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
import { FlowNodeInputTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
import { useTranslation } from 'next-i18next'; import { useTranslation } from 'next-i18next';
import { Controller, useForm } from 'react-hook-form';
import MySelect from '@fastgpt/web/components/common/MySelect';
import MyTextarea from '@/components/common/Textarea/MyTextarea';
import MyNumberInput from '@fastgpt/web/components/common/Input/NumberInput';
import { SendPromptFnType } from '../ChatContainer/ChatBox/type';
import { eventBus, EventNameEnum } from '@/web/common/utils/eventbus'; import { eventBus, EventNameEnum } from '@/web/common/utils/eventbus';
import { SelectOptionsComponent, FormInputComponent } from './Interactive/InteractiveComponents';
type props = { const accordionButtonStyle = {
value: UserChatItemValueItemType | AIChatItemValueItemType; w: 'auto',
isLastResponseValue: boolean; bg: 'white',
isChatting: boolean; borderRadius: 'md',
borderWidth: '1px',
borderColor: 'myGray.200',
boxShadow: '1',
pl: 3,
pr: 2.5,
_hover: {
bg: 'auto'
}
}; };
const onSendPrompt: SendPromptFnType = (e) => eventBus.emit(EventNameEnum.sendQuestion, e); const RenderResoningContent = React.memo(function RenderResoningContent({
content,
isChatting,
isLastResponseValue
}: {
content: string;
isChatting: boolean;
isLastResponseValue: boolean;
}) {
const { t } = useTranslation();
const showAnimation = isChatting && isLastResponseValue;
return (
<Accordion allowToggle defaultIndex={isLastResponseValue ? 0 : undefined}>
<AccordionItem borderTop={'none'} borderBottom={'none'}>
<AccordionButton {...accordionButtonStyle} py={1}>
<HStack mr={2} spacing={1}>
<MyIcon name={'core/chat/think'} w={'0.85rem'} />
<Box fontSize={'sm'}>{t('chat:ai_reasoning')}</Box>
</HStack>
{showAnimation && <MyIcon name={'common/loading'} w={'0.85rem'} />}
<AccordionIcon color={'myGray.600'} ml={5} />
</AccordionButton>
<AccordionPanel
py={0}
pr={0}
pl={3}
mt={2}
borderLeft={'2px solid'}
borderColor={'myGray.300'}
color={'myGray.500'}
>
<Markdown source={content} showAnimation={showAnimation} />
</AccordionPanel>
</AccordionItem>
</Accordion>
);
});
const RenderText = React.memo(function RenderText({ const RenderText = React.memo(function RenderText({
showAnimation, showAnimation,
text text
@@ -58,6 +95,7 @@ const RenderText = React.memo(function RenderText({
return <Markdown source={source} showAnimation={showAnimation} />; return <Markdown source={source} showAnimation={showAnimation} />;
}); });
const RenderTool = React.memo( const RenderTool = React.memo(
function RenderTool({ function RenderTool({
showAnimation, showAnimation,
@@ -69,37 +107,20 @@ const RenderTool = React.memo(
return ( return (
<Box> <Box>
{tools.map((tool) => { {tools.map((tool) => {
const toolParams = (() => { const formatJson = (string: string) => {
try { try {
return JSON.stringify(JSON.parse(tool.params), null, 2); return JSON.stringify(JSON.parse(string), null, 2);
} catch (error) { } catch (error) {
return tool.params; return string;
} }
})(); };
const toolResponse = (() => { const toolParams = formatJson(tool.params);
try { const toolResponse = formatJson(tool.response);
return JSON.stringify(JSON.parse(tool.response), null, 2);
} catch (error) {
return tool.response;
}
})();
return ( return (
<Accordion key={tool.id} allowToggle _notLast={{ mb: 2 }}> <Accordion key={tool.id} allowToggle _notLast={{ mb: 2 }}>
<AccordionItem borderTop={'none'} borderBottom={'none'}> <AccordionItem borderTop={'none'} borderBottom={'none'}>
<AccordionButton <AccordionButton {...accordionButtonStyle}>
w={'auto'}
bg={'white'}
borderRadius={'md'}
borderWidth={'1px'}
borderColor={'myGray.200'}
boxShadow={'1'}
pl={3}
pr={2.5}
_hover={{
bg: 'auto'
}}
>
<Avatar src={tool.toolAvatar} w={'1.25rem'} h={'1.25rem'} borderRadius={'sm'} /> <Avatar src={tool.toolAvatar} w={'1.25rem'} h={'1.25rem'} borderRadius={'sm'} />
<Box mx={2} fontSize={'sm'} color={'myGray.900'}> <Box mx={2} fontSize={'sm'} color={'myGray.900'}>
{tool.toolName} {tool.toolName}
@@ -140,99 +161,24 @@ ${toolResponse}`}
}, },
(prevProps, nextProps) => isEqual(prevProps, nextProps) (prevProps, nextProps) => isEqual(prevProps, nextProps)
); );
const RenderResoningContent = React.memo(function RenderResoningContent({
content,
isChatting,
isLastResponseValue
}: {
content: string;
isChatting: boolean;
isLastResponseValue: boolean;
}) {
const { t } = useTranslation();
const showAnimation = isChatting && isLastResponseValue;
return ( const onSendPrompt = (e: { text: string; isInteractivePrompt: boolean }) =>
<Accordion allowToggle defaultIndex={isLastResponseValue ? 0 : undefined}> eventBus.emit(EventNameEnum.sendQuestion, e);
<AccordionItem borderTop={'none'} borderBottom={'none'}>
<AccordionButton
w={'auto'}
bg={'white'}
borderRadius={'md'}
borderWidth={'1px'}
borderColor={'myGray.200'}
boxShadow={'1'}
pl={3}
pr={2.5}
py={1}
_hover={{
bg: 'auto'
}}
>
<HStack mr={2} spacing={1}>
<MyIcon name={'core/chat/think'} w={'0.85rem'} />
<Box fontSize={'sm'}>{t('chat:ai_reasoning')}</Box>
</HStack>
{showAnimation && <MyIcon name={'common/loading'} w={'0.85rem'} />}
<AccordionIcon color={'myGray.600'} ml={5} />
</AccordionButton>
<AccordionPanel
py={0}
pr={0}
pl={3}
mt={2}
borderLeft={'2px solid'}
borderColor={'myGray.300'}
color={'myGray.500'}
>
<Markdown source={content} showAnimation={showAnimation} />
</AccordionPanel>
</AccordionItem>
</Accordion>
);
});
const RenderUserSelectInteractive = React.memo(function RenderInteractive({ const RenderUserSelectInteractive = React.memo(function RenderInteractive({
interactive interactive
}: { }: {
interactive: InteractiveBasicType & UserSelectInteractive; interactive: InteractiveBasicType & UserSelectInteractive;
}) { }) {
return ( return (
<> <SelectOptionsComponent
{interactive?.params?.description && <Markdown source={interactive.params.description} />} interactiveParams={interactive.params}
<Flex flexDirection={'column'} gap={2} w={'250px'}> onSelect={(value) => {
{interactive.params.userSelectOptions?.map((option) => { onSendPrompt({
const selected = option.value === interactive?.params?.userSelectedVal; text: value,
isInteractivePrompt: true
return ( });
<Button }}
key={option.key} />
variant={'whitePrimary'}
whiteSpace={'pre-wrap'}
isDisabled={interactive?.params?.userSelectedVal !== undefined}
{...(selected
? {
_disabled: {
cursor: 'default',
borderColor: 'primary.300',
bg: 'primary.50 !important',
color: 'primary.600'
}
}
: {})}
onClick={() => {
onSendPrompt({
text: option.value,
isInteractivePrompt: true
});
}}
>
{option.value}
</Button>
);
})}
</Flex>
</>
); );
}); });
const RenderUserFormInteractive = React.memo(function RenderFormInput({ const RenderUserFormInteractive = React.memo(function RenderFormInput({
@@ -241,110 +187,52 @@ const RenderUserFormInteractive = React.memo(function RenderFormInput({
interactive: InteractiveBasicType & UserInputInteractive; interactive: InteractiveBasicType & UserInputInteractive;
}) { }) {
const { t } = useTranslation(); const { t } = useTranslation();
const { register, setValue, handleSubmit: handleSubmitChat, control, reset } = useForm();
const onSubmit = useCallback((data: any) => { const defaultValues = useMemo(() => {
if (interactive.type === 'userInput') {
return interactive.params.inputForm?.reduce((acc: Record<string, any>, item) => {
acc[item.label] = !!item.value ? item.value : item.defaultValue;
return acc;
}, {});
}
return {};
}, [interactive]);
const handleFormSubmit = useCallback((data: Record<string, any>) => {
onSendPrompt({ onSendPrompt({
text: JSON.stringify(data), text: JSON.stringify(data),
isInteractivePrompt: true isInteractivePrompt: true
}); });
}, []); }, []);
useEffect(() => {
if (interactive.type === 'userInput') {
const defaultValues = interactive.params.inputForm?.reduce(
(acc: Record<string, any>, item) => {
acc[item.label] = !!item.value ? item.value : item.defaultValue;
return acc;
},
{}
);
reset(defaultValues);
}
}, []);
return ( return (
<Flex flexDirection={'column'} gap={2} w={'250px'}> <Flex flexDirection={'column'} gap={2} w={'250px'}>
{interactive.params.description && <Markdown source={interactive.params.description} />} <FormInputComponent
{interactive.params.inputForm?.map((input) => ( interactiveParams={interactive.params}
<Box key={input.label}> defaultValues={defaultValues}
<FormLabel mb={1} required={input.required} whiteSpace={'pre-wrap'}> SubmitButton={({ onSubmit }) => (
{input.label} <Button onClick={() => onSubmit(handleFormSubmit)()}>{t('common:Submit')}</Button>
{input.description && <QuestionTip ml={1} label={input.description} />} )}
</FormLabel> />
{input.type === FlowNodeInputTypeEnum.input && (
<MyTextarea
isDisabled={interactive.params.submitted}
{...register(input.label, {
required: input.required
})}
bg={'white'}
autoHeight
minH={40}
maxH={100}
/>
)}
{input.type === FlowNodeInputTypeEnum.textarea && (
<Textarea
isDisabled={interactive.params.submitted}
bg={'white'}
{...register(input.label, {
required: input.required
})}
rows={5}
maxLength={input.maxLength || 4000}
/>
)}
{input.type === FlowNodeInputTypeEnum.numberInput && (
<MyNumberInput
min={input.min}
max={input.max}
defaultValue={input.defaultValue}
isDisabled={interactive.params.submitted}
bg={'white'}
register={register}
name={input.label}
isRequired={input.required}
/>
)}
{input.type === FlowNodeInputTypeEnum.select && (
<Controller
key={input.label}
control={control}
name={input.label}
rules={{ required: input.required }}
render={({ field: { ref, value } }) => {
if (!input.list) return <></>;
return (
<MySelect
ref={ref}
width={'100%'}
list={input.list}
value={value}
isDisabled={interactive.params.submitted}
onChange={(e) => setValue(input.label, e)}
/>
);
}}
/>
)}
</Box>
))}
{!interactive.params.submitted && (
<Flex w={'full'} justifyContent={'end'}>
<Button onClick={handleSubmitChat(onSubmit)}>{t('common:Submit')}</Button>
</Flex>
)}
</Flex> </Flex>
); );
}); });
const AIResponseBox = ({ value, isLastResponseValue, isChatting }: props) => { const AIResponseBox = ({
if (value.type === ChatItemValueTypeEnum.text && value.text) value,
isLastResponseValue,
isChatting
}: {
value: UserChatItemValueItemType | AIChatItemValueItemType;
isLastResponseValue: boolean;
isChatting: boolean;
}) => {
if (value.type === ChatItemValueTypeEnum.text && value.text) {
return ( return (
<RenderText showAnimation={isChatting && isLastResponseValue} text={value.text.content} /> <RenderText showAnimation={isChatting && isLastResponseValue} text={value.text.content} />
); );
if (value.type === ChatItemValueTypeEnum.reasoning && value.reasoning) }
if (value.type === ChatItemValueTypeEnum.reasoning && value.reasoning) {
return ( return (
<RenderResoningContent <RenderResoningContent
isChatting={isChatting} isChatting={isChatting}
@@ -352,14 +240,18 @@ const AIResponseBox = ({ value, isLastResponseValue, isChatting }: props) => {
content={value.reasoning.content} content={value.reasoning.content}
/> />
); );
if (value.type === ChatItemValueTypeEnum.tool && value.tools)
return <RenderTool showAnimation={isChatting} tools={value.tools} />;
if (value.type === ChatItemValueTypeEnum.interactive && value.interactive) {
if (value.interactive.type === 'userSelect')
return <RenderUserSelectInteractive interactive={value.interactive} />;
if (value.interactive?.type === 'userInput')
return <RenderUserFormInteractive interactive={value.interactive} />;
} }
if (value.type === ChatItemValueTypeEnum.tool && value.tools) {
return <RenderTool showAnimation={isChatting} tools={value.tools} />;
}
if (value.type === ChatItemValueTypeEnum.interactive && value.interactive) {
if (value.interactive.type === 'userSelect') {
return <RenderUserSelectInteractive interactive={value.interactive} />;
}
if (value.interactive?.type === 'userInput') {
return <RenderUserFormInteractive interactive={value.interactive} />;
}
}
return null;
}; };
export default React.memo(AIResponseBox); export default React.memo(AIResponseBox);

View File

@@ -0,0 +1,206 @@
import React, { useCallback } from 'react';
import { Box, Button, Flex, Textarea } from '@chakra-ui/react';
import { Controller, useForm, UseFormHandleSubmit } from 'react-hook-form';
import Markdown from '@/components/Markdown';
import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel';
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
import MySelect from '@fastgpt/web/components/common/MySelect';
import MyTextarea from '@/components/common/Textarea/MyTextarea';
import MyNumberInput from '@fastgpt/web/components/common/Input/NumberInput';
import { FlowNodeInputTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
import {
UserInputFormItemType,
UserInputInteractive,
UserSelectInteractive,
UserSelectOptionItemType
} from '@fastgpt/global/core/workflow/template/system/interactive/type';
const DescriptionBox = React.memo(function DescriptionBox({
description
}: {
description?: string;
}) {
if (!description) return null;
return (
<Box mb={4}>
<Markdown source={description} />
</Box>
);
});
export const SelectOptionsComponent = React.memo(function SelectOptionsComponent({
interactiveParams,
onSelect
}: {
interactiveParams: UserSelectInteractive['params'];
onSelect: (value: string) => void;
}) {
const { description, userSelectOptions, userSelectedVal } = interactiveParams;
return (
<Box maxW={'100%'}>
<DescriptionBox description={description} />
<Flex flexDirection={'column'} gap={3} w={'250px'}>
{userSelectOptions.map((option: UserSelectOptionItemType) => {
const selected = option.value === userSelectedVal;
return (
<Button
key={option.key}
variant={'whitePrimary'}
whiteSpace={'pre-wrap'}
isDisabled={!!userSelectedVal}
{...(selected
? {
_disabled: {
cursor: 'default',
borderColor: 'primary.300',
bg: 'primary.50 !important',
color: 'primary.600'
}
}
: {})}
onClick={() => onSelect(option.value)}
>
{option.value}
</Button>
);
})}
</Flex>
</Box>
);
});
export const FormInputComponent = React.memo(function FormInputComponent({
interactiveParams,
defaultValues = {},
SubmitButton
}: {
interactiveParams: UserInputInteractive['params'];
defaultValues?: Record<string, any>;
SubmitButton: (e: { onSubmit: UseFormHandleSubmit<Record<string, any>> }) => React.JSX.Element;
}) {
const { description, inputForm, submitted } = interactiveParams;
const { register, setValue, handleSubmit, control } = useForm({
defaultValues
});
const FormItemLabel = useCallback(
({
label,
required,
description
}: {
label: string;
required?: boolean;
description?: string;
}) => {
return (
<Flex mb={1} alignItems={'center'}>
<FormLabel required={required} mb={0} fontWeight="medium" color="gray.700">
{label}
</FormLabel>
{description && <QuestionTip ml={1} label={description} />}
</Flex>
);
},
[]
);
const RenderFormInput = useCallback(
({ input }: { input: UserInputFormItemType }) => {
const { type, label, required, maxLength, min, max, defaultValue, list } = input;
switch (type) {
case FlowNodeInputTypeEnum.input:
return (
<MyTextarea
isDisabled={submitted}
{...register(label, {
required: required
})}
bg={'white'}
autoHeight
minH={40}
maxH={100}
/>
);
case FlowNodeInputTypeEnum.textarea:
return (
<Textarea
isDisabled={submitted}
bg={'white'}
{...register(label, {
required: required
})}
rows={5}
maxLength={maxLength || 4000}
/>
);
case FlowNodeInputTypeEnum.numberInput:
return (
<MyNumberInput
min={min}
max={max}
defaultValue={defaultValue}
isDisabled={submitted}
bg={'white'}
register={register}
name={label}
isRequired={required}
/>
);
case FlowNodeInputTypeEnum.select:
return (
<Controller
key={label}
control={control}
name={label}
rules={{ required: required }}
render={({ field: { ref, value } }) => {
if (!list) return <></>;
return (
<MySelect
ref={ref}
width={'100%'}
list={list}
value={value}
isDisabled={submitted}
onChange={(e) => setValue(label, e)}
/>
);
}}
/>
);
default:
return null;
}
},
[control, register, setValue, submitted]
);
return (
<Box>
<DescriptionBox description={description} />
<Flex flexDirection={'column'} gap={3}>
{inputForm.map((input) => (
<Box key={input.label}>
<FormItemLabel
label={input.label}
required={input.required}
description={input.description}
/>
<RenderFormInput input={input} />
</Box>
))}
</Flex>
{!submitted && (
<Flex justifyContent={'flex-end'} mt={4}>
<SubmitButton onSubmit={handleSubmit} />
</Flex>
)}
</Box>
);
});

View File

@@ -1,6 +1,7 @@
import { AppSchema } from '@fastgpt/global/core/app/type'; import { AppSchema } from '@fastgpt/global/core/app/type';
import { ChatHistoryItemResType } from '@fastgpt/global/core/chat/type'; import { ChatHistoryItemResType } from '@fastgpt/global/core/chat/type';
import { RuntimeNodeItemType } from '@fastgpt/global/core/workflow/runtime/type'; import { RuntimeNodeItemType } from '@fastgpt/global/core/workflow/runtime/type';
import { WorkflowInteractiveResponseType } from '@fastgpt/global/core/workflow/template/system/interactive/type';
import { StoreNodeItemType } from '@fastgpt/global/core/workflow/type'; import { StoreNodeItemType } from '@fastgpt/global/core/workflow/type';
import { RuntimeEdgeItemType, StoreEdgeItemType } from '@fastgpt/global/core/workflow/type/edge'; import { RuntimeEdgeItemType, StoreEdgeItemType } from '@fastgpt/global/core/workflow/type/edge';
@@ -9,6 +10,8 @@ export type PostWorkflowDebugProps = {
edges: RuntimeEdgeItemType[]; edges: RuntimeEdgeItemType[];
variables: Record<string, any>; variables: Record<string, any>;
appId: string; appId: string;
query?: UserChatItemValueItemType[];
history?: ChatItemType[];
}; };
export type PostWorkflowDebugResponse = { export type PostWorkflowDebugResponse = {
@@ -16,5 +19,6 @@ export type PostWorkflowDebugResponse = {
finishedEdges: RuntimeEdgeItemType[]; finishedEdges: RuntimeEdgeItemType[];
nextStepRunNodes: RuntimeNodeItemType[]; nextStepRunNodes: RuntimeNodeItemType[];
flowResponses: ChatHistoryItemResType[]; flowResponses: ChatHistoryItemResType[];
workflowInteractiveResponse?: WorkflowInteractiveResponseType;
newVariables: Record<string, any>; newVariables: Record<string, any>;
}; };

View File

@@ -1,5 +1,5 @@
import React, { useCallback, useMemo } from 'react'; import React, { useCallback, useMemo } from 'react';
import { Box, Button, Card, Flex, FlexProps } from '@chakra-ui/react'; import { Box, Button, Flex, type FlexProps } from '@chakra-ui/react';
import MyIcon from '@fastgpt/web/components/common/Icon'; import MyIcon from '@fastgpt/web/components/common/Icon';
import Avatar from '@fastgpt/web/components/common/Avatar'; import Avatar from '@fastgpt/web/components/common/Avatar';
import type { FlowNodeItemType } from '@fastgpt/global/core/workflow/type/node.d'; import type { FlowNodeItemType } from '@fastgpt/global/core/workflow/type/node.d';
@@ -13,7 +13,6 @@ import { ToolSourceHandle, ToolTargetHandle } from './Handle/ToolHandle';
import { useEditTextarea } from '@fastgpt/web/hooks/useEditTextarea'; import { useEditTextarea } from '@fastgpt/web/hooks/useEditTextarea';
import { ConnectionSourceHandle, ConnectionTargetHandle } from './Handle/ConnectionHandle'; import { ConnectionSourceHandle, ConnectionTargetHandle } from './Handle/ConnectionHandle';
import { useDebug } from '../../hooks/useDebug'; import { useDebug } from '../../hooks/useDebug';
import EmptyTip from '@fastgpt/web/components/common/EmptyTip';
import { getPreviewPluginNode } from '@/web/core/app/api/plugin'; import { getPreviewPluginNode } from '@/web/core/app/api/plugin';
import { storeNode2FlowNode } from '@/web/core/workflow/utils'; import { storeNode2FlowNode } from '@/web/core/workflow/utils';
import { getNanoid } from '@fastgpt/global/common/string/tools'; import { getNanoid } from '@fastgpt/global/common/string/tools';
@@ -23,12 +22,12 @@ import { moduleTemplatesFlat } from '@fastgpt/global/core/workflow/template/cons
import MyTooltip from '@fastgpt/web/components/common/MyTooltip'; import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest'; import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { useWorkflowUtils } from '../../hooks/useUtils'; import { useWorkflowUtils } from '../../hooks/useUtils';
import { WholeResponseContent } from '@/components/core/chat/components/WholeResponseModal';
import { WorkflowNodeEdgeContext } from '../../../context/workflowInitContext'; import { WorkflowNodeEdgeContext } from '../../../context/workflowInitContext';
import { WorkflowEventContext } from '../../../context/workflowEventContext'; import { WorkflowEventContext } from '../../../context/workflowEventContext';
import MyImage from '@fastgpt/web/components/common/Image/MyImage'; import MyImage from '@fastgpt/web/components/common/Image/MyImage';
import MyIconButton from '@fastgpt/web/components/common/Icon/button'; import MyIconButton from '@fastgpt/web/components/common/Icon/button';
import UseGuideModal from '@/components/common/Modal/UseGuideModal'; import UseGuideModal from '@/components/common/Modal/UseGuideModal';
import NodeDebugResponse from './RenderDebug/NodeDebugResponse';
type Props = FlowNodeItemType & { type Props = FlowNodeItemType & {
children?: React.ReactNode | React.ReactNode[] | string; children?: React.ReactNode | React.ReactNode[] | string;
@@ -62,6 +61,7 @@ const NodeCard = (props: Props) => {
w = 'full', w = 'full',
h = 'full', h = 'full',
nodeId, nodeId,
flowNodeType,
selected, selected,
menuForbid, menuForbid,
isTool = false, isTool = false,
@@ -409,7 +409,7 @@ const NodeCard = (props: Props) => {
})} })}
{...customStyle} {...customStyle}
> >
<NodeDebugResponse nodeId={nodeId} debugResult={debugResult} /> {debugResult && <NodeDebugResponse nodeId={nodeId} debugResult={debugResult} />}
{Header} {Header}
<Flex flexDirection={'column'} flex={1} my={!isFolded ? 3 : 0} gap={2}> <Flex flexDirection={'column'} flex={1} my={!isFolded ? 3 : 0} gap={2}>
{!isFolded ? children : <Box h={4} />} {!isFolded ? children : <Box h={4} />}
@@ -661,168 +661,3 @@ const NodeIntro = React.memo(function NodeIntro({
return Render; return Render;
}); });
const NodeDebugResponse = React.memo(function NodeDebugResponse({
nodeId,
debugResult
}: {
nodeId: string;
debugResult: FlowNodeItemType['debugResult'];
}) {
const { t } = useTranslation();
const { onChangeNode, onStopNodeDebug, onNextNodeDebug, workflowDebugData } = useContextSelector(
WorkflowContext,
(v) => v
);
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') &&
!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>
)}
{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 && (
<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>
)}
{response && <WholeResponseContent activeModule={response} />}
</Box>
)}
</Card>
)}
</>
) : null;
}, [
debugResult,
nodeId,
onChangeNode,
onNextNodeDebug,
onStopNodeDebug,
openConfirm,
t,
workflowDebugData
]);
return (
<>
{RenderStatus}
<ConfirmModal />
</>
);
});

View File

@@ -0,0 +1,269 @@
import React, { useCallback, useMemo, useRef } 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 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 {
FormInputComponent,
SelectOptionsComponent
} from '@/components/core/chat/components/Interactive/InteractiveComponents';
import { UserInputInteractive } from '@fastgpt/global/core/workflow/template/system/interactive/type';
import { initWorkflowEdgeStatus } from '@fastgpt/global/core/workflow/runtime/utils';
import { ChatItemType, UserChatItemValueItemType } from '@fastgpt/global/core/chat/type';
import { ChatItemValueTypeEnum, ChatRoleEnum } from '@fastgpt/global/core/chat/constants';
type NodeDebugResponseProps = {
nodeId: string;
debugResult: FlowNodeItemType['debugResult'];
};
const RenderUserFormInteractive = React.memo(function RenderFormInput({
interactive,
onNext
}: {
interactive: UserInputInteractive;
onNext: (val: string) => void;
}) {
const { t } = useTranslation();
const defaultValues = useMemo(() => {
return interactive.params.inputForm?.reduce((acc: Record<string, any>, item) => {
acc[item.label] = !!item.value ? item.value : item.defaultValue;
return acc;
}, {});
}, [interactive.params.inputForm]);
return (
<Box px={4} py={4} bg="white" borderRadius="md">
<FormInputComponent
defaultValues={defaultValues}
interactiveParams={interactive.params}
SubmitButton={({ onSubmit }) => (
<Button
leftIcon={<MyIcon name="core/workflow/debugNext" />}
onClick={() =>
onSubmit((data) => {
onNext(JSON.stringify(data));
})()
}
>
{t('common:common.Next Step')}
</Button>
)}
/>
</Box>
);
});
const NodeDebugResponse = ({ nodeId, debugResult }: NodeDebugResponseProps) => {
const { t } = useTranslation();
const { onChangeNode, onStopNodeDebug, onNextNodeDebug, workflowDebugData } = useContextSelector(
WorkflowContext,
(v) => v
);
const statusMap = useRef({
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 = statusMap.current[debugResult?.status || 'running'];
const response = debugResult?.response;
const { openConfirm, ConfirmModal } = useConfirm({
content: t('common:core.workflow.Confirm stop debug')
});
const onStop = () => {
openConfirm(onStopNodeDebug)();
};
const interactive = debugResult?.workflowInteractiveResponse;
const onNextInteractive = useCallback(
(userContent: string) => {
if (!workflowDebugData || !workflowDebugData || !interactive) return;
const updatedQuery: UserChatItemValueItemType[] = [
{
type: ChatItemValueTypeEnum.text,
text: { content: userContent }
}
];
const mockHistory: ChatItemType[] = [
{
obj: ChatRoleEnum.AI,
value: [
{
type: ChatItemValueTypeEnum.interactive,
interactive: {
...interactive,
memoryEdges: interactive.memoryEdges || [],
entryNodeIds: interactive.entryNodeIds || [],
nodeOutputs: interactive.nodeOutputs || []
}
}
]
}
];
onNextNodeDebug({
...workflowDebugData,
// Rewrite runtimeEdges
runtimeEdges: initWorkflowEdgeStatus(workflowDebugData.runtimeEdges, mockHistory),
query: updatedQuery,
history: mockHistory
});
},
[workflowDebugData, interactive, onNextNodeDebug]
);
return !!debugResult && !!statusData ? (
<>
{/* Status header */}
<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>
)}
{!interactive && (
<>
{(debugResult.status === 'success' || debugResult.status === 'skipped') &&
!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(workflowDebugData)}
>
{t('common:common.Next Step')}
</Button>
)}
{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 && !interactive && (
<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>
)}
{interactive && onNextInteractive && (
<>
{interactive.type === 'userSelect' && (
<Box px={4} py={3}>
<SelectOptionsComponent
interactiveParams={interactive.params}
onSelect={(val) => {
onNextInteractive(val);
}}
/>
</Box>
)}
{interactive.type === 'userInput' && (
<RenderUserFormInteractive
interactive={interactive}
onNext={onNextInteractive}
/>
)}
</>
)}
{response && <WholeResponseContent activeModule={response} />}
</Box>
)}
</Card>
)}
<ConfirmModal />
</>
) : null;
};
export default React.memo(NodeDebugResponse);

View File

@@ -35,6 +35,8 @@ import WorkflowInitContextProvider, { WorkflowNodeEdgeContext } from './workflow
import WorkflowEventContextProvider from './workflowEventContext'; import WorkflowEventContextProvider from './workflowEventContext';
import { getAppConfigByDiff } from '@/web/core/app/diff'; import { getAppConfigByDiff } from '@/web/core/app/diff';
import WorkflowStatusContextProvider from './workflowStatusContext'; import WorkflowStatusContextProvider from './workflowStatusContext';
import { ChatItemType, UserChatItemValueItemType } from '@fastgpt/global/core/chat/type';
import { WorkflowInteractiveResponseType } from '@fastgpt/global/core/workflow/template/system/interactive/type';
/* /*
Context Context
@@ -156,24 +158,22 @@ type WorkflowContextType = {
| undefined; | undefined;
// debug // debug
workflowDebugData: workflowDebugData?: DebugDataType;
| { onNextNodeDebug: (params: DebugDataType) => Promise<void>;
runtimeNodes: RuntimeNodeItemType[];
runtimeEdges: RuntimeEdgeItemType[];
nextRunNodes: RuntimeNodeItemType[];
}
| undefined;
onNextNodeDebug: () => Promise<void>;
onStartNodeDebug: ({ onStartNodeDebug: ({
entryNodeId, entryNodeId,
runtimeNodes, runtimeNodes,
runtimeEdges, runtimeEdges,
variables variables,
query,
history
}: { }: {
entryNodeId: string; entryNodeId: string;
runtimeNodes: RuntimeNodeItemType[]; runtimeNodes: RuntimeNodeItemType[];
runtimeEdges: RuntimeEdgeItemType[]; runtimeEdges: RuntimeEdgeItemType[];
variables: Record<string, any>; variables: Record<string, any>;
query?: UserChatItemValueItemType[];
history?: ChatItemType[];
}) => Promise<void>; }) => Promise<void>;
onStopNodeDebug: () => void; onStopNodeDebug: () => void;
@@ -189,11 +189,14 @@ type WorkflowContextType = {
>; >;
}; };
type DebugDataType = { export type DebugDataType = {
runtimeNodes: RuntimeNodeItemType[]; runtimeNodes: RuntimeNodeItemType[];
runtimeEdges: RuntimeEdgeItemType[]; runtimeEdges: RuntimeEdgeItemType[];
nextRunNodes: RuntimeNodeItemType[]; nextRunNodes: RuntimeNodeItemType[];
variables: Record<string, any>; variables: Record<string, any>;
history?: ChatItemType[];
query?: UserChatItemValueItemType[];
workflowInteractiveResponse?: WorkflowInteractiveResponseType;
}; };
export const WorkflowContext = createContext<WorkflowContextType>({ export const WorkflowContext = createContext<WorkflowContextType>({
@@ -236,17 +239,25 @@ export const WorkflowContext = createContext<WorkflowContextType>({
throw new Error('Function not implemented.'); throw new Error('Function not implemented.');
}, },
workflowDebugData: undefined, workflowDebugData: undefined,
onNextNodeDebug: function (): Promise<void> { onNextNodeDebug: function (params?: {
history?: ChatItemType[];
query?: UserChatItemValueItemType[];
debugData?: DebugDataType;
}): Promise<void> {
throw new Error('Function not implemented.'); throw new Error('Function not implemented.');
}, },
onStartNodeDebug: function ({ onStartNodeDebug: function ({
entryNodeId, entryNodeId,
runtimeNodes, runtimeNodes,
runtimeEdges runtimeEdges,
query,
history
}: { }: {
entryNodeId: string; entryNodeId: string;
runtimeNodes: RuntimeNodeItemType[]; runtimeNodes: RuntimeNodeItemType[];
runtimeEdges: RuntimeEdgeItemType[]; runtimeEdges: RuntimeEdgeItemType[];
query?: UserChatItemValueItemType[];
history?: ChatItemType[];
}): Promise<void> { }): Promise<void> {
throw new Error('Function not implemented.'); throw new Error('Function not implemented.');
}, },
@@ -551,8 +562,7 @@ const WorkflowContextProvider = ({
/* debug */ /* debug */
const [workflowDebugData, setWorkflowDebugData] = useState<DebugDataType>(); const [workflowDebugData, setWorkflowDebugData] = useState<DebugDataType>();
const onNextNodeDebug = useCallback( const onNextNodeDebug = useCallback(
async (debugData = workflowDebugData) => { async (debugData: DebugDataType) => {
if (!debugData) return;
// 1. Cancel node selected status and debugResult.showStatus // 1. Cancel node selected status and debugResult.showStatus
setNodes((state) => setNodes((state) =>
state.map((node) => ({ state.map((node) => ({
@@ -612,26 +622,35 @@ const WorkflowContextProvider = ({
try { try {
// 4. Run one step // 4. Run one step
const { finishedEdges, finishedNodes, nextStepRunNodes, flowResponses, newVariables } = const {
await postWorkflowDebug({ finishedEdges,
nodes: runtimeNodes, finishedNodes,
edges: debugData.runtimeEdges, nextStepRunNodes,
variables: { flowResponses,
appId, newVariables,
cTime: formatTime2YMDHMW(), workflowInteractiveResponse
...debugData.variables } = await postWorkflowDebug({
}, nodes: runtimeNodes,
appId edges: debugData.runtimeEdges,
}); variables: {
appId,
cTime: formatTime2YMDHMW(),
...debugData.variables
},
query: debugData.query, // 添加 query 参数
history: debugData.history,
appId
});
// 5. Store debug result // 5. Store debug result
const newStoreDebugData = { setWorkflowDebugData({
runtimeNodes: finishedNodes, runtimeNodes: finishedNodes,
// edges need to save status // edges need to save status
runtimeEdges: finishedEdges, runtimeEdges: finishedEdges,
nextRunNodes: nextStepRunNodes, nextRunNodes: nextStepRunNodes,
variables: newVariables variables: newVariables,
}; workflowInteractiveResponse: workflowInteractiveResponse
setWorkflowDebugData(newStoreDebugData); });
// 6. selected entry node and Update entry node debug result // 6. selected entry node and Update entry node debug result
setNodes((state) => setNodes((state) =>
@@ -665,16 +684,21 @@ const WorkflowContextProvider = ({
status: 'success', status: 'success',
response: result, response: result,
showResult: true, showResult: true,
isExpired: false isExpired: false,
workflowInteractiveResponse: workflowInteractiveResponse
} }
} }
}; };
}) })
); );
// Check for an empty response // Check for an empty response(Skip node)
if (flowResponses.length === 0 && nextStepRunNodes.length > 0) { if (
onNextNodeDebug(newStoreDebugData); !workflowInteractiveResponse &&
flowResponses.length === 0 &&
nextStepRunNodes.length > 0
) {
onNextNodeDebug(debugData);
} }
} catch (error) { } catch (error) {
entryNodes.forEach((node) => { entryNodes.forEach((node) => {
@@ -692,7 +716,7 @@ const WorkflowContextProvider = ({
console.log(error); console.log(error);
} }
}, },
[appId, onChangeNode, setNodes, workflowDebugData] [appId, onChangeNode, setNodes]
); );
const onStopNodeDebug = useMemoizedFn(() => { const onStopNodeDebug = useMemoizedFn(() => {
setWorkflowDebugData(undefined); setWorkflowDebugData(undefined);
@@ -712,18 +736,24 @@ const WorkflowContextProvider = ({
entryNodeId, entryNodeId,
runtimeNodes, runtimeNodes,
runtimeEdges, runtimeEdges,
variables variables,
query,
history
}: { }: {
entryNodeId: string; entryNodeId: string;
runtimeNodes: RuntimeNodeItemType[]; runtimeNodes: RuntimeNodeItemType[];
runtimeEdges: RuntimeEdgeItemType[]; runtimeEdges: RuntimeEdgeItemType[];
variables: Record<string, any>; variables: Record<string, any>;
query?: UserChatItemValueItemType[];
history?: ChatItemType[];
}) => { }) => {
const data = { const data: DebugDataType = {
runtimeNodes, runtimeNodes,
runtimeEdges, runtimeEdges,
nextRunNodes: runtimeNodes.filter((node) => node.nodeId === entryNodeId), nextRunNodes: runtimeNodes.filter((node) => node.nodeId === entryNodeId),
variables variables,
query,
history
}; };
onStopNodeDebug(); onStopNodeDebug();
setWorkflowDebugData(data); setWorkflowDebugData(data);

View File

@@ -11,7 +11,7 @@ import type { AIChatItemType, UserChatItemType } from '@fastgpt/global/core/chat
import { authApp } from '@fastgpt/service/support/permission/app/auth'; import { authApp } from '@fastgpt/service/support/permission/app/auth';
import { dispatchWorkFlow } from '@fastgpt/service/core/workflow/dispatch'; import { dispatchWorkFlow } from '@fastgpt/service/core/workflow/dispatch';
import { getUserChatInfoAndAuthTeamPoints } from '@fastgpt/service/support/permission/auth/team'; import { getUserChatInfoAndAuthTeamPoints } from '@fastgpt/service/support/permission/auth/team';
import { StoreEdgeItemType } from '@fastgpt/global/core/workflow/type/edge'; import type { StoreEdgeItemType } from '@fastgpt/global/core/workflow/type/edge';
import { import {
concatHistories, concatHistories,
getChatTitleFromChatMessage, getChatTitleFromChatMessage,
@@ -25,8 +25,8 @@ import {
} from '@fastgpt/global/core/workflow/utils'; } from '@fastgpt/global/core/workflow/utils';
import { NextAPI } from '@/service/middleware/entry'; import { NextAPI } from '@/service/middleware/entry';
import { chatValue2RuntimePrompt, GPTMessages2Chats } from '@fastgpt/global/core/chat/adapt'; import { chatValue2RuntimePrompt, GPTMessages2Chats } from '@fastgpt/global/core/chat/adapt';
import { ChatCompletionMessageParam } from '@fastgpt/global/core/ai/type'; import type { ChatCompletionMessageParam } from '@fastgpt/global/core/ai/type';
import { AppChatConfigType } from '@fastgpt/global/core/app/type'; import type { AppChatConfigType } from '@fastgpt/global/core/app/type';
import { import {
getLastInteractiveValue, getLastInteractiveValue,
getMaxHistoryLimitFromNodes, getMaxHistoryLimitFromNodes,
@@ -36,7 +36,7 @@ import {
storeNodes2RuntimeNodes, storeNodes2RuntimeNodes,
textAdaptGptResponse textAdaptGptResponse
} from '@fastgpt/global/core/workflow/runtime/utils'; } from '@fastgpt/global/core/workflow/runtime/utils';
import { StoreNodeItemType } from '@fastgpt/global/core/workflow/type/node'; import type { StoreNodeItemType } from '@fastgpt/global/core/workflow/type/node';
import { getWorkflowResponseWrite } from '@fastgpt/service/core/workflow/dispatch/utils'; import { getWorkflowResponseWrite } from '@fastgpt/service/core/workflow/dispatch/utils';
import { WORKFLOW_MAX_RUN_TIMES } from '@fastgpt/service/core/workflow/constants'; import { WORKFLOW_MAX_RUN_TIMES } from '@fastgpt/service/core/workflow/constants';
import { getPluginInputsFromStoreNodes } from '@fastgpt/global/core/app/plugin/utils'; import { getPluginInputsFromStoreNodes } from '@fastgpt/global/core/app/plugin/utils';

View File

@@ -5,7 +5,7 @@ import { authApp } from '@fastgpt/service/support/permission/app/auth';
import { dispatchWorkFlow } from '@fastgpt/service/core/workflow/dispatch'; import { dispatchWorkFlow } from '@fastgpt/service/core/workflow/dispatch';
import { authCert } from '@fastgpt/service/support/permission/auth/common'; import { authCert } from '@fastgpt/service/support/permission/auth/common';
import { getUserChatInfoAndAuthTeamPoints } from '@fastgpt/service/support/permission/auth/team'; import { getUserChatInfoAndAuthTeamPoints } from '@fastgpt/service/support/permission/auth/team';
import { PostWorkflowDebugProps, PostWorkflowDebugResponse } from '@/global/core/workflow/api'; import type { PostWorkflowDebugProps, PostWorkflowDebugResponse } from '@/global/core/workflow/api';
import { NextAPI } from '@/service/middleware/entry'; import { NextAPI } from '@/service/middleware/entry';
import { ReadPermissionVal } from '@fastgpt/global/support/permission/constant'; import { ReadPermissionVal } from '@fastgpt/global/support/permission/constant';
import { defaultApp } from '@/web/core/app/constants'; import { defaultApp } from '@/web/core/app/constants';
@@ -15,16 +15,22 @@ async function handler(
req: NextApiRequest, req: NextApiRequest,
res: NextApiResponse res: NextApiResponse
): Promise<PostWorkflowDebugResponse> { ): Promise<PostWorkflowDebugResponse> {
const { nodes = [], edges = [], variables = {}, appId } = req.body as PostWorkflowDebugProps; const {
nodes = [],
edges = [],
variables = {},
appId,
query = [],
history = []
} = req.body as PostWorkflowDebugProps;
if (!nodes) { if (!nodes) {
throw new Error('Prams Error'); return Promise.reject('Prams Error');
} }
if (!Array.isArray(nodes)) { if (!Array.isArray(nodes)) {
throw new Error('Nodes is not array'); return Promise.reject('Nodes is not array');
} }
if (!Array.isArray(edges)) { if (!Array.isArray(edges)) {
throw new Error('Edges is not array'); return Promise.reject('Edges is not array');
} }
/* user auth */ /* user auth */
@@ -40,31 +46,32 @@ async function handler(
const { timezone, externalProvider } = await getUserChatInfoAndAuthTeamPoints(tmbId); const { timezone, externalProvider } = await getUserChatInfoAndAuthTeamPoints(tmbId);
/* start process */ /* start process */
const { flowUsages, flowResponses, debugResponse, newVariables } = await dispatchWorkFlow({ const { flowUsages, flowResponses, debugResponse, newVariables, workflowInteractiveResponse } =
res, await dispatchWorkFlow({
requestOrigin: req.headers.origin, res,
mode: 'debug', requestOrigin: req.headers.origin,
runningAppInfo: { mode: 'debug',
id: app._id, timezone,
teamId: app.teamId, externalProvider,
tmbId: app.tmbId uid: tmbId,
}, runningAppInfo: {
runningUserInfo: { id: app._id,
teamId, teamId: app.teamId,
tmbId tmbId: app.tmbId
}, },
uid: tmbId, runningUserInfo: {
timezone, teamId,
externalProvider, tmbId
runtimeNodes: nodes, },
runtimeEdges: edges, runtimeNodes: nodes,
variables, runtimeEdges: edges,
query: [], variables,
chatConfig: defaultApp.chatConfig, query: query,
histories: [], chatConfig: defaultApp.chatConfig,
stream: false, histories: history,
maxRunTimes: WORKFLOW_MAX_RUN_TIMES stream: false,
}); maxRunTimes: WORKFLOW_MAX_RUN_TIMES
});
createChatUsage({ createChatUsage({
appName: `${app.name}-Debug`, appName: `${app.name}-Debug`,
@@ -78,12 +85,12 @@ async function handler(
return { return {
...debugResponse, ...debugResponse,
newVariables, newVariables,
flowResponses flowResponses,
workflowInteractiveResponse
}; };
} }
export default NextAPI(handler); export default NextAPI(handler);
export const config = { export const config = {
api: { api: {
bodyParser: { bodyParser: {