Files
FastGPT/projects/app/src/pages/chat/share.tsx
2024-12-11 14:53:07 +08:00

389 lines
12 KiB
TypeScript

import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useRouter } from 'next/router';
import { Box, Flex, Drawer, DrawerOverlay, DrawerContent } from '@chakra-ui/react';
import { streamFetch } from '@/web/common/api/fetch';
import SideBar from '@/components/SideBar';
import { GPTMessages2Chats } from '@fastgpt/global/core/chat/adapt';
import ChatBox from '@/components/core/chat/ChatContainer/ChatBox';
import type { StartChatFnProps } from '@/components/core/chat/ChatContainer/type';
import PageContainer from '@/components/PageContainer';
import ChatHeader from './components/ChatHeader';
import ChatHistorySlider from './components/ChatHistorySlider';
import { serviceSideProps } from '@/web/common/utils/i18n';
import { useTranslation } from 'next-i18next';
import { getInitOutLinkChatInfo } from '@/web/core/chat/api';
import { getChatTitleFromChatMessage } from '@fastgpt/global/core/chat/utils';
import { MongoOutLink } from '@fastgpt/service/support/outLink/schema';
import { OutLinkWithAppType } from '@fastgpt/global/support/outLink/type';
import { addLog } from '@fastgpt/service/common/system/log';
import { connectToDatabase } from '@/service/mongo';
import NextHead from '@/components/common/NextHead';
import { useContextSelector } from 'use-context-selector';
import ChatContextProvider, { ChatContext } from '@/web/core/chat/context/chatContext';
import { InitChatResponse } from '@/global/core/chat/api';
import { defaultChatData, GetChatTypeEnum } from '@/global/core/chat/constants';
import { useMount } from 'ahooks';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { getNanoid } from '@fastgpt/global/common/string/tools';
import dynamic from 'next/dynamic';
import { useSystem } from '@fastgpt/web/hooks/useSystem';
import { useShareChatStore } from '@/web/core/chat/storeShareChat';
import ChatItemContextProvider, { ChatItemContext } from '@/web/core/chat/context/chatItemContext';
import ChatRecordContextProvider, {
ChatRecordContext
} from '@/web/core/chat/context/chatRecordContext';
import { useChatStore } from '@/web/core/chat/context/useChatStore';
import { ChatSourceEnum } from '@fastgpt/global/core/chat/constants';
const CustomPluginRunBox = dynamic(() => import('./components/CustomPluginRunBox'));
type Props = {
appId: string;
appName: string;
appIntro: string;
appAvatar: string;
shareId: string;
authToken: string;
customUid: string;
showRawSource: boolean;
showNodeStatus: boolean;
};
const OutLink = (props: Props) => {
const { t } = useTranslation();
const router = useRouter();
const { showRawSource, showNodeStatus } = props;
const {
shareId = '',
showHistory = '1',
showHead = '1',
authToken,
customUid,
...customVariables
} = router.query as {
shareId: string;
showHistory: '0' | '1';
showHead: '0' | '1';
authToken: string;
[key: string]: string;
};
const { isPc } = useSystem();
const { outLinkAuthData, appId, chatId } = useChatStore();
const isOpenSlider = useContextSelector(ChatContext, (v) => v.isOpenSlider);
const onCloseSlider = useContextSelector(ChatContext, (v) => v.onCloseSlider);
const forbidLoadChat = useContextSelector(ChatContext, (v) => v.forbidLoadChat);
const onChangeChatId = useContextSelector(ChatContext, (v) => v.onChangeChatId);
const onUpdateHistoryTitle = useContextSelector(ChatContext, (v) => v.onUpdateHistoryTitle);
const resetVariables = useContextSelector(ChatItemContext, (v) => v.resetVariables);
const isPlugin = useContextSelector(ChatItemContext, (v) => v.isPlugin);
const setChatBoxData = useContextSelector(ChatItemContext, (v) => v.setChatBoxData);
const chatRecords = useContextSelector(ChatRecordContext, (v) => v.chatRecords);
const totalRecordsCount = useContextSelector(ChatRecordContext, (v) => v.totalRecordsCount);
const isChatRecordsLoaded = useContextSelector(ChatRecordContext, (v) => v.isChatRecordsLoaded);
const initSign = useRef(false);
const { data, loading } = useRequest2(
async () => {
const shareId = outLinkAuthData.shareId;
const outLinkUid = outLinkAuthData.outLinkUid;
if (!outLinkUid || !shareId || forbidLoadChat.current) return;
const res = await getInitOutLinkChatInfo({
chatId,
shareId,
outLinkUid
});
setChatBoxData(res);
resetVariables({
variables: res.variables
});
return res;
},
{
manual: false,
refreshDeps: [shareId, outLinkAuthData, chatId],
onError(e: any) {
if (chatId) {
onChangeChatId('');
}
},
onFinally() {
forbidLoadChat.current = false;
}
}
);
useEffect(() => {
if (initSign.current === false && data && isChatRecordsLoaded) {
initSign.current = true;
if (window !== top) {
window.top?.postMessage({ type: 'shareChatReady' }, '*');
}
}
}, [data, isChatRecordsLoaded]);
const startChat = useCallback(
async ({
messages,
controller,
generatingMessage,
variables,
responseChatItemId
}: StartChatFnProps) => {
const completionChatId = chatId || getNanoid();
const histories = messages.slice(-1);
//post message to report chat start
window.top?.postMessage(
{
type: 'shareChatStart',
data: {
question: histories[0]?.content
}
},
'*'
);
const { responseText, responseData } = await streamFetch({
data: {
messages: histories,
variables: {
...variables,
...customVariables
},
responseChatItemId,
chatId: completionChatId,
...outLinkAuthData
},
onMessage: generatingMessage,
abortCtrl: controller
});
const newTitle = getChatTitleFromChatMessage(GPTMessages2Chats(histories)[0]);
// new chat
if (completionChatId !== chatId) {
onChangeChatId(completionChatId, true);
}
onUpdateHistoryTitle({ chatId: completionChatId, newTitle });
// update chat window
setChatBoxData((state) => ({
...state,
title: newTitle
}));
// hook message
window.top?.postMessage(
{
type: 'shareChatFinish',
data: {
question: histories[0]?.content,
answer: responseText
}
},
'*'
);
return { responseText, responseData, isNewChat: forbidLoadChat.current };
},
[
chatId,
customVariables,
outLinkAuthData,
onUpdateHistoryTitle,
setChatBoxData,
forbidLoadChat,
onChangeChatId
]
);
// window init
const [isEmbed, setIdEmbed] = useState(true);
useMount(() => {
setIdEmbed(window !== top);
});
const RenderHistoryList = useMemo(() => {
const Children = (
<ChatHistorySlider
confirmClearText={t('common:core.chat.Confirm to clear share chat history')}
/>
);
if (showHistory !== '1') return null;
return isPc ? (
<SideBar>{Children}</SideBar>
) : (
<Drawer
isOpen={isOpenSlider}
placement="left"
autoFocus={false}
size={'xs'}
onClose={onCloseSlider}
>
<DrawerOverlay backgroundColor={'rgba(255,255,255,0.5)'} />
<DrawerContent maxWidth={'75vw'} boxShadow={'2px 0 10px rgba(0,0,0,0.15)'}>
{Children}
</DrawerContent>
</Drawer>
);
}, [isOpenSlider, isPc, onCloseSlider, showHistory, t]);
return (
<>
<NextHead title={props.appName || 'AI'} desc={props.appIntro} icon={props.appAvatar} />
<PageContainer
isLoading={loading}
{...(isEmbed
? { p: '0 !important', insertProps: { borderRadius: '0', boxShadow: 'none' } }
: { p: [0, 5] })}
>
<Flex h={'100%'} flexDirection={['column', 'row']}>
{RenderHistoryList}
{/* chat container */}
<Flex
position={'relative'}
h={[0, '100%']}
w={['100%', 0]}
flex={'1 0 0'}
flexDirection={'column'}
>
{/* header */}
{showHead === '1' ? (
<ChatHeader
history={chatRecords}
totalRecordsCount={totalRecordsCount}
showHistory={showHistory === '1'}
/>
) : null}
{/* chat box */}
<Box flex={1} bg={'white'}>
{isPlugin ? (
<CustomPluginRunBox
appId={appId}
chatId={chatId}
outLinkAuthData={outLinkAuthData}
onNewChat={() => onChangeChatId(getNanoid())}
onStartChat={startChat}
/>
) : (
<ChatBox
appId={appId}
chatId={chatId}
outLinkAuthData={outLinkAuthData}
feedbackType={'user'}
onStartChat={startChat}
chatType="share"
showRawSource={showRawSource}
showNodeStatus={showNodeStatus}
/>
)}
</Box>
</Flex>
</Flex>
</PageContainer>
</>
);
};
const Render = (props: Props) => {
const { shareId, authToken, customUid, appId } = props;
const { localUId, loaded } = useShareChatStore();
const { source, chatId, setSource, setAppId, setOutLinkAuthData } = useChatStore();
const chatHistoryProviderParams = useMemo(() => {
return { shareId, outLinkUid: authToken || customUid || localUId };
}, [authToken, customUid, localUId, shareId]);
const chatRecordProviderParams = useMemo(() => {
return {
appId,
shareId,
outLinkUid: chatHistoryProviderParams.outLinkUid,
chatId,
type: GetChatTypeEnum.outLink
};
}, [appId, chatHistoryProviderParams.outLinkUid, chatId, shareId]);
useMount(() => {
setSource('share');
});
// Set outLinkAuthData
useEffect(() => {
setOutLinkAuthData({
shareId,
outLinkUid: chatHistoryProviderParams.outLinkUid
});
return () => {
setOutLinkAuthData({});
};
}, [chatHistoryProviderParams.outLinkUid, setOutLinkAuthData, shareId]);
// Watch appId
useEffect(() => {
setAppId(appId);
}, [appId, setAppId]);
return source === ChatSourceEnum.share ? (
<ChatContextProvider params={chatHistoryProviderParams}>
<ChatItemContextProvider>
<ChatRecordContextProvider params={chatRecordProviderParams}>
<OutLink {...props} />
</ChatRecordContextProvider>
</ChatItemContextProvider>
</ChatContextProvider>
) : (
<NextHead title={props.appName} desc={props.appIntro} icon={props.appAvatar} />
);
};
export default React.memo(Render);
export async function getServerSideProps(context: any) {
const shareId = context?.query?.shareId || '';
const authToken = context?.query?.authToken || '';
const customUid = context?.query?.customUid || '';
const app = await (async () => {
try {
await connectToDatabase();
const app = (await MongoOutLink.findOne(
{
shareId
},
'appId showRawSource showNodeStatus'
)
.populate('appId', 'name avatar intro')
.lean()) as OutLinkWithAppType;
return app;
} catch (error) {
addLog.error('getServerSideProps', error);
return undefined;
}
})();
return {
props: {
appId: String(app?.appId?._id) ?? '',
appName: app?.appId?.name ?? 'AI',
appAvatar: app?.appId?.avatar ?? '',
appIntro: app?.appId?.intro ?? 'AI',
showRawSource: app?.showRawSource ?? false,
showNodeStatus: app?.showNodeStatus ?? false,
shareId: shareId ?? '',
authToken: authToken ?? '',
customUid,
...(await serviceSideProps(context, ['file', 'app', 'chat', 'workflow']))
}
};
}