Commit 2534cdc6 by DigHuang Committed by GitHub

fix(skill): resolve debug chat state mismatch & simplify test container (#7146)

parent 313066b9
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { Box } from '@chakra-ui/react';
import { Box, Flex } from '@chakra-ui/react';
import { useContextSelector } from 'use-context-selector';
import { SkillDetailContext } from '../context';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import ChatItemContextProvider from '@/web/core/chat/context/chatItemContext';
import ChatItemContextProvider, { ChatItemContext } from '@/web/core/chat/context/chatItemContext';
import ChatRecordContextProvider from '@/web/core/chat/context/chatRecordContext';
import { useSkillChatTest } from './useSkillChatTest';
import { getSkillDebugRecords } from '@/web/core/skill/api';
import {
delSkillDebugChatItem,
getSkillDebugRecords,
postStopSkillDebugChat,
streamSkillDebugChat
} from '@/web/core/skill/api';
import type { LinkedPaginationProps } from '@fastgpt/global/openapi/api';
import type { GetPaginationRecordsBodyType } from '@fastgpt/global/openapi/core/chat/record/api';
import ChatAIModelSelector from '@/pageComponents/chat/ChatWindow/ChatAIModelSelector';
import ChatBox from '@/components/core/chat/ChatContainer/ChatBox';
import { ChatTypeEnum } from '@/components/core/chat/ChatContainer/ChatBox/constants';
import { useTranslation } from 'next-i18next';
import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
import type { AppFileSelectConfigType } from '@fastgpt/global/core/app/type/config.schema';
import type { StartChatFnProps } from '@/components/core/chat/ChatContainer/type';
import { useMemoizedFn } from 'ahooks';
const fileSelectConfig: AppFileSelectConfigType = {
maxFiles: 10,
canSelectFile: false,
canSelectImg: false,
customPdfParse: false,
canSelectVideo: false,
canSelectAudio: false,
canSelectCustomFileExtension: false,
customFileExtensionList: []
};
const SkillPreview = () => {
const { t } = useTranslation(['skill', 'common']);
const { skillId, sandboxState, chatId } = useContextSelector(SkillDetailContext, (v) => ({
skillId: v.skillId,
sandboxState: v.sandboxState,
......@@ -19,6 +42,7 @@ const SkillPreview = () => {
}));
const { llmModelList, defaultModels } = useSystemStore();
const setChatBoxData = useContextSelector(ChatItemContext, (v) => v.setChatBoxData);
const defaultModel = defaultModels.llm?.model || llmModelList[0]?.model || '';
const [selectedModel, setSelectedModel] = useState('');
const userSelectedModelRef = useRef(false);
......@@ -31,6 +55,28 @@ const SkillPreview = () => {
const isReady = sandboxState === 'ready';
useEffect(() => {
setChatBoxData((prev) => {
const isSameChat = prev.appId === skillId && prev.chatId === chatId;
return {
...prev,
appId: skillId,
chatId,
title: isSameChat ? prev.title : undefined,
chatGenerateStatus: isSameChat ? prev.chatGenerateStatus : undefined,
hasBeenRead: isSameChat ? prev.hasBeenRead : undefined,
app: {
chatConfig: { fileSelectConfig },
name: 'Skill Preview',
avatar: '',
type: AppTypeEnum.simple,
pluginInputs: []
}
};
});
}, [skillId, chatId, setChatBoxData]);
useEffect(() => {
if (!userSelectedModelRef.current && defaultModel && selectedModel !== defaultModel) {
setSelectedModel(defaultModel);
}
......@@ -54,18 +100,72 @@ const SkillPreview = () => {
);
}, [selectedModel, modelSelectList]);
const { ChatContainer } = useSkillChatTest({
skillId,
model: selectedModel,
chatId,
isReady,
disabledSendTip: isReady ? undefined : 'skill:sandbox_debug_loading_toast',
InputLeftComponent: ModelSelectorInput
const onStartChat = useMemoizedFn(
async ({ messages, responseChatItemId, controller, generatingMessage }: StartChatFnProps) => {
const histories = messages.slice(-1);
const { responseText } = await streamSkillDebugChat({
data: {
skillId,
chatId,
messages: histories,
model: selectedModel,
responseChatItemId
},
onMessage: generatingMessage,
abortCtrl: controller
});
return { responseText };
}
);
// 使用 skill 专属的删除接口,避免走 /api/core/chat/item/delete 时用 skillId 查 App 报错
const onDeleteChatItem = useMemoizedFn((contentId: string) =>
delSkillDebugChatItem({ skillId, chatId, contentId })
);
const onStopChat = useMemoizedFn(async () => {
const result = await postStopSkillDebugChat({ skillId, chatId });
return {
chatGenerateStatus: result.chatGenerateStatus,
completed: result.completed
};
});
return (
<Box h={'100%'} w={'100%'} overflow={'hidden'}>
<ChatContainer />
<ChatBox
isReady={isReady}
appId={skillId}
chatId={chatId}
chatType={ChatTypeEnum.test}
enableMarkChatRead={false}
onStartChat={onStartChat}
onDeleteChatItem={onDeleteChatItem}
onStopChat={onStopChat}
InputLeftComponent={ModelSelectorInput}
disabledSendTip={isReady ? undefined : t('sandbox_lazy_init')}
dialogTips={t('common:core.chat.Type a message')}
pl={'16px'}
pr={0}
maxW={'100%'}
boxBodyProps={{ px: 0, pr: '8px', maxW: '100%', mx: 0 }}
inputBodyProps={{ maxW: '100%', mx: 0, px: 0, pl: 0, pr: '8px' }}
EmptyState={
<Flex
flex={1}
alignItems="center"
justifyContent="center"
color="myGray.500"
fontSize="sm"
textAlign="center"
lineHeight="20px"
whiteSpace="pre-wrap"
>
{t('empty_state_tip')}
</Flex>
}
/>
</Box>
);
};
......
import { useCallback, useEffect } from 'react';
import { useMemoizedFn } from 'ahooks';
import { useContextSelector } from 'use-context-selector';
import { streamFetch } from '@/web/common/api/fetch';
import {
SKILL_DEBUG_CHAT_URL,
delSkillDebugChatItem,
postStopSkillDebugChat
} from '@/web/core/skill/api';
import { ChatItemContext } from '@/web/core/chat/context/chatItemContext';
import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
import type { StartChatFnProps } from '@/components/core/chat/ChatContainer/type';
import ChatBox from '@/components/core/chat/ChatContainer/ChatBox';
import React from 'react';
import { ChatTypeEnum } from '@/components/core/chat/ChatContainer/ChatBox/constants';
import type { AppFileSelectConfigType } from '@fastgpt/global/core/app/type/config.schema';
import { Flex } from '@chakra-ui/react';
import { useTranslation } from 'next-i18next';
const fileSelectConfig: AppFileSelectConfigType = {
maxFiles: 10,
canSelectFile: false,
canSelectImg: false,
customPdfParse: false,
canSelectVideo: false,
canSelectAudio: false,
canSelectCustomFileExtension: false,
customFileExtensionList: []
};
export const useSkillChatTest = ({
skillId,
model,
chatId,
isReady,
disabledSendTip,
InputLeftComponent
}: {
skillId: string;
model: string;
chatId: string;
isReady: boolean;
disabledSendTip?: string;
InputLeftComponent?: React.ReactNode;
}) => {
const { t } = useTranslation(['skill', 'common']);
const setChatBoxData = useContextSelector(ChatItemContext, (v) => v.setChatBoxData);
// Set chat box data
useEffect(() => {
setChatBoxData({
appId: skillId,
app: {
chatConfig: { fileSelectConfig },
name: 'Skill Preview',
avatar: '',
type: AppTypeEnum.simple,
pluginInputs: []
}
});
}, [skillId, setChatBoxData]);
const startChat = useMemoizedFn(
async ({ messages, responseChatItemId, controller, generatingMessage }: StartChatFnProps) => {
const histories = messages.slice(-1);
const { responseText } = await streamFetch({
url: SKILL_DEBUG_CHAT_URL,
data: {
skillId,
chatId,
messages: histories,
model,
responseChatItemId
},
onMessage: generatingMessage,
abortCtrl: controller
});
return { responseText };
}
);
// 使用 skill 专属的删除接口,避免走 /api/core/chat/item/delete 时用 skillId 查 App 报错
const handleDeleteChatItem = useMemoizedFn((contentId: string) =>
delSkillDebugChatItem({ skillId, chatId, contentId })
);
const handleStopChat = useMemoizedFn(async () => {
const result = await postStopSkillDebugChat({ skillId, chatId });
return {
chatGenerateStatus: result.chatGenerateStatus,
completed: result.completed
};
});
const ChatContainer = useCallback(
() => (
<ChatBox
isReady={isReady}
appId={skillId}
chatId={chatId}
chatType={ChatTypeEnum.test}
enableMarkChatRead={false}
onStartChat={startChat}
onDeleteChatItem={handleDeleteChatItem}
onStopChat={handleStopChat}
InputLeftComponent={InputLeftComponent}
disabledSendTip={disabledSendTip ? t(disabledSendTip as any) : undefined}
dialogTips={t('common:core.chat.Type a message')}
pl={'16px'}
pr={0}
maxW={'100%'}
boxBodyProps={{ px: 0, pr: '8px', maxW: '100%', mx: 0 }}
inputBodyProps={{ maxW: '100%', mx: 0, px: 0, pl: 0, pr: '8px' }}
EmptyState={
<Flex
flex={1}
alignItems="center"
justifyContent="center"
color="myGray.500"
fontSize="sm"
textAlign="center"
lineHeight="20px"
whiteSpace="pre-wrap"
>
{t('empty_state_tip')}
</Flex>
}
/>
),
[
skillId,
chatId,
isReady,
startChat,
handleDeleteChatItem,
handleStopChat,
InputLeftComponent,
disabledSendTip,
t
]
);
return {
ChatContainer
};
};
import { GET, DELETE, POST } from '@/web/common/api/request';
import { streamFetch, type StreamResponseType } from '@/web/common/api/fetch';
import { downloadFetch } from '@/web/common/system/utils';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import { EventStreamContentType, fetchEventSource } from '@fortaine/fetch-event-source';
......@@ -21,6 +22,7 @@ import type {
CreateEditDebugSandboxBody,
CreateEditDebugSandboxResponse,
CreateSkillFolderBody,
SkillDebugChatBody,
SkillDebugRecordsBody,
SkillDebugSessionControlBody,
SkillDebugSessionStopResponse,
......@@ -34,6 +36,7 @@ import type { SkillDebugDeleteChatItemBody } from '@fastgpt/global/core/ai/skill
import type { GetResourceFolderListProps } from '@fastgpt/global/common/parentFolder/type';
import { AgentSkillTypeEnum } from '@fastgpt/global/core/ai/skill/constants';
import type { GetRecordsV2ResponseType } from '@fastgpt/global/openapi/core/chat/record/api';
import type { StartChatFnProps } from '@/components/core/chat/ChatContainer/type';
/** 获取 Skill 列表(支持分页、搜索、分类、文件夹过滤) */
export const getSkillList = (data: ListSkillsQuery) =>
......@@ -140,8 +143,22 @@ export const streamCreateEditDebugSandbox = ({
});
});
/** Skill 调试对话 SSE 接口 URL */
export const SKILL_DEBUG_CHAT_URL = '/api/core/ai/skill/debugChat';
/** 发起 Skill 调试对话,使用 Skill 专属鉴权与编辑沙箱运行态。 */
export const streamSkillDebugChat = ({
data,
onMessage,
abortCtrl
}: {
data: SkillDebugChatBody;
onMessage: StartChatFnProps['generatingMessage'];
abortCtrl: AbortController;
}): Promise<StreamResponseType> =>
streamFetch({
url: '/api/core/ai/skill/debugChat',
data,
onMessage,
abortCtrl
});
/** 创建 Skill 文件夹 */
export const postCreateSkillFolder = (data: CreateSkillFolderBody) =>
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment