feat: 修改chat的数据结构

This commit is contained in:
Archer
2023-03-18 00:49:44 +08:00
parent e6c9ca540a
commit 38c093d9ae
33 changed files with 2631 additions and 341 deletions

View File

@@ -8,7 +8,7 @@ import {
AccordionPanel,
AccordionIcon,
Flex,
Input,
Divider,
IconButton
} from '@chakra-ui/react';
import { useUserStore } from '@/store/user';
@@ -16,28 +16,30 @@ import { useChatStore } from '@/store/chat';
import { useQuery } from '@tanstack/react-query';
import { useRouter } from 'next/router';
import { useScreen } from '@/hooks/useScreen';
import { getToken } from '@/utils/user';
import MyIcon from '@/components/Icon';
import { useCopyData } from '@/utils/tools';
const SlideBar = ({
name,
windowId,
chatId,
modelId,
resetChat,
onClose
}: {
resetChat: () => void;
name?: string;
windowId?: string;
chatId: string;
modelId: string;
resetChat: () => void;
onClose: () => void;
}) => {
const router = useRouter();
const { isPc } = useScreen();
const { copyData } = useCopyData();
const { myModels, getMyModels } = useUserStore();
const { chatHistory, removeChatHistoryByWindowId, generateChatWindow, updateChatHistory } =
useChatStore();
const { isSuccess } = useQuery(['init'], getMyModels);
const [hasReady, setHasReady] = useState(false);
const [editHistoryId, setEditHistoryId] = useState<string>();
useEffect(() => {
setHasReady(true);
@@ -47,7 +49,7 @@ const SlideBar = ({
<>
{chatHistory.map((item) => (
<Flex
key={item.windowId}
key={item.chatId}
alignItems={'center'}
p={3}
borderRadius={'md'}
@@ -58,21 +60,15 @@ const SlideBar = ({
}}
fontSize={'xs'}
border={'1px solid transparent'}
{...(item.chatId === chatId && item.windowId === windowId
{...(item.chatId === chatId
? {
borderColor: 'rgba(255,255,255,0.5)',
backgroundColor: 'rgba(255,255,255,0.1)'
}
: {})}
onClick={() => {
if (
(item.chatId === chatId && item.windowId === windowId) ||
editHistoryId === item.windowId
)
return;
router.push(
`/chat?chatId=${item.chatId}&windowId=${item.windowId}&timeStamp=${Date.now()}`
);
if (item.chatId === chatId) return;
router.push(`/chat?chatId=${item.chatId}`);
onClose();
}}
>
@@ -87,7 +83,7 @@ const SlideBar = ({
aria-label={'edit'}
size={'xs'}
onClick={(e) => {
removeChatHistoryByWindowId(item.windowId);
removeChatHistoryByWindowId(item.chatId);
e.stopPropagation();
}}
/>
@@ -107,16 +103,19 @@ const SlideBar = ({
color={'white'}
>
{/* 新对话 */}
<Button
w={'100%'}
variant={'white'}
h={'40px'}
mb={4}
leftIcon={<AddIcon />}
onClick={resetChat}
>
</Button>
{getToken() && (
<Button
w={'100%'}
variant={'white'}
h={'40px'}
mb={4}
leftIcon={<AddIcon />}
onClick={resetChat}
>
</Button>
)}
{/* 我的模型 & 历史记录 折叠框*/}
<Box flex={'1 0 0'} h={0} overflowY={'auto'}>
{isSuccess ? (
@@ -161,13 +160,11 @@ const SlideBar = ({
: {})}
onClick={async () => {
if (item.name === name) return;
router.push(
`/chat?chatId=${await generateChatWindow(item._id)}&timeStamp=${Date.now()}`
);
router.push(`/chat?chatId=${await generateChatWindow(item._id)}`);
onClose();
}}
>
<ChatIcon mr={2} />
<MyIcon name="model" mr={2} fill={'white'} w={'16px'} h={'16px'} />
<Box className={'textEllipsis'} flex={'1 0 0'} w={0}>
{item.name}
</Box>
@@ -177,9 +174,54 @@ const SlideBar = ({
</AccordionItem>
</Accordion>
) : (
<RenderHistory />
<>
<Box mb={4} textAlign={'center'}>
</Box>
<RenderHistory />
</>
)}
</Box>
<Divider my={4} />
{/* 分享 */}
{getToken() && (
<Flex
alignItems={'center'}
p={2}
cursor={'pointer'}
borderRadius={'md'}
_hover={{
backgroundColor: 'rgba(255,255,255,0.2)'
}}
onClick={async () => {
copyData(
`${location.origin}/chat?chatId=${await generateChatWindow(modelId)}`,
'已复制分享链接'
);
}}
>
<MyIcon name="share" fill={'white'} w={'16px'} h={'16px'} mr={4} />
</Flex>
)}
<Flex
mt={4}
alignItems={'center'}
p={2}
cursor={'pointer'}
borderRadius={'md'}
_hover={{
backgroundColor: 'rgba(255,255,255,0.2)'
}}
onClick={async () => {
copyData(`${location.origin}/chat?chatId=${chatId}`, '已复制分享链接');
}}
>
<MyIcon name="share" fill={'white'} w={'16px'} h={'16px'} mr={4} />
</Flex>
</Flex>
);
};

View File

@@ -1,8 +1,15 @@
import React, { useCallback, useState, useRef, useMemo } from 'react';
import { useRouter } from 'next/router';
import Image from 'next/image';
import { getInitChatSiteInfo, postGPT3SendPrompt, delLastMessage, postSaveChat } from '@/api/chat';
import { ChatSiteItemType, ChatSiteType } from '@/types/chat';
import {
getInitChatSiteInfo,
getChatSiteId,
postGPT3SendPrompt,
delLastMessage,
postSaveChat
} from '@/api/chat';
import type { InitChatResponse } from '@/api/response/chat';
import { ChatSiteItemType } from '@/types/chat';
import {
Textarea,
Box,
@@ -15,43 +22,68 @@ import {
DrawerContent
} from '@chakra-ui/react';
import { useToast } from '@/hooks/useToast';
import Icon from '@/components/Icon';
import Icon from '@/components/Iconfont';
import { useScreen } from '@/hooks/useScreen';
import { useQuery } from '@tanstack/react-query';
import { OpenAiModelEnum } from '@/constants/model';
import { ChatModelNameEnum } from '@/constants/model';
import dynamic from 'next/dynamic';
import { useGlobalStore } from '@/store/global';
import { useChatStore } from '@/store/chat';
import { streamFetch } from '@/api/fetch';
import SlideBar from './components/SlideBar';
import { getToken } from '@/utils/user';
const Markdown = dynamic(() => import('@/components/Markdown'));
const textareaMinH = '22px';
const Chat = ({
chatId,
windowId,
timeStamp
}: {
chatId: string;
windowId?: string;
timeStamp: string;
}) => {
interface ChatType extends InitChatResponse {
history: ChatSiteItemType[];
}
const Chat = ({ chatId }: { chatId: string }) => {
const { toast } = useToast();
const router = useRouter();
const { isPc, media } = useScreen();
const { setLoading } = useGlobalStore();
const [chatData, setChatData] = useState<ChatType>({
chatId: '',
modelId: '',
name: '',
avatar: '',
secret: {},
chatModel: '',
history: [],
isExpiredTime: false
}); // 聊天框整体数据
const ChatBox = useRef<HTMLDivElement>(null);
const TextareaDom = useRef<HTMLTextAreaElement>(null);
const [chatSiteData, setChatSiteData] = useState<ChatSiteType>(); // 聊天框整体数据
const [chatList, setChatList] = useState<ChatSiteItemType[]>([]); // 对话内容
const [inputVal, setInputVal] = useState(''); // 输入的内容
const { isOpen: isOpenSlider, onClose: onCloseSlider, onOpen: onOpenSlider } = useDisclosure();
const isChatting = useMemo(() => chatList[chatList.length - 1]?.status === 'loading', [chatList]);
const lastWordHuman = useMemo(() => chatList[chatList.length - 1]?.obj === 'Human', [chatList]);
const { setLoading } = useGlobalStore();
const isChatting = useMemo(
() => chatData.history[chatData.history.length - 1]?.status === 'loading',
[chatData.history]
);
const chatWindowError = useMemo(() => {
if (chatData.history[chatData.history.length - 1]?.obj === 'Human') {
return {
text: '内容出现异常',
canDelete: true
};
}
if (chatData.isExpiredTime) {
return {
text: '聊天框已过期',
canDelete: false
};
}
return '';
}, [chatData]);
const { pushChatHistory } = useChatStore();
// 滚动到底部
@@ -67,23 +99,20 @@ const Chat = ({
// 初始化聊天框
useQuery(
['initData', timeStamp],
['init', chatId],
() => {
setLoading(true);
return getInitChatSiteInfo(chatId, windowId);
return getInitChatSiteInfo(chatId);
},
{
onSuccess(res) {
// 可能没有 windowId给它设置一下
router.replace(`/chat?chatId=${chatId}&windowId=${res.windowId}&timeStamp=${timeStamp}`);
setChatSiteData(res.chatSite);
setChatList(
res.history.map((item) => ({
setChatData({
...res,
history: res.history.map((item) => ({
...item,
status: 'finish'
}))
);
});
scrollToBottom();
},
onError(e: any) {
@@ -113,10 +142,18 @@ const Chat = ({
}, []);
// 重载对话
const resetChat = useCallback(() => {
router.push(`/chat?chatId=${chatId}&timeStamp=${Date.now()}`);
const resetChat = useCallback(async () => {
if (!chatData) return;
try {
router.push(`/chat?chatId=${await getChatSiteId(chatData.modelId)}`);
} catch (error: any) {
toast({
title: error?.message || '生成新对话失败',
status: 'warning'
});
}
onCloseSlider();
}, [chatId, router, onCloseSlider]);
}, [chatData, onCloseSlider, router, toast]);
// gpt3 方法
const gpt3ChatPrompt = useCallback(
@@ -128,16 +165,17 @@ const Chat = ({
});
// 更新 AI 的内容
setChatList((state) =>
state.map((item, index) => {
if (index !== state.length - 1) return item;
setChatData((state) => ({
...state,
history: state.history.map((item, index) => {
if (index !== state.history.length - 1) return item;
return {
...item,
status: 'finish',
value: response
};
})
);
}));
},
[chatId]
);
@@ -145,7 +183,6 @@ const Chat = ({
// chatGPT
const chatGPTPrompt = useCallback(
async (newChatList: ChatSiteItemType[]) => {
if (!windowId) return;
const prompt = {
obj: newChatList[newChatList.length - 1].obj,
value: newChatList[newChatList.length - 1].value
@@ -154,27 +191,27 @@ const Chat = ({
const res = await streamFetch({
url: '/api/chat/chatGpt',
data: {
windowId,
prompt,
chatId
},
onMessage: (text: string) => {
setChatList((state) =>
state.map((item, index) => {
if (index !== state.length - 1) return item;
setChatData((state) => ({
...state,
history: state.history.map((item, index) => {
if (index !== state.history.length - 1) return item;
return {
...item,
value: item.value + text
};
})
);
}));
}
});
// 保存对话信息
try {
await postSaveChat({
windowId,
chatId,
prompts: [
prompt,
{
@@ -193,17 +230,18 @@ const Chat = ({
}
// 设置完成状态
setChatList((state) =>
state.map((item, index) => {
if (index !== state.length - 1) return item;
setChatData((state) => ({
...state,
history: state.history.map((item, index) => {
if (index !== state.history.length - 1) return item;
return {
...item,
status: 'finish'
};
})
);
}));
},
[chatId, toast, windowId]
[chatId, toast]
);
/**
@@ -217,12 +255,12 @@ const Chat = ({
.split('\n')
.filter((val) => val)
.join('\n\n');
if (!chatSiteData?.modelId || !val || !ChatBox.current || isChatting) {
if (!chatData?.modelId || !val || !ChatBox.current || isChatting) {
return;
}
const newChatList: ChatSiteItemType[] = [
...chatList,
...chatData.history,
{
obj: 'Human',
value: val,
@@ -236,33 +274,37 @@ const Chat = ({
];
// 插入内容
setChatList(newChatList);
setChatData((state) => ({
...state,
history: newChatList
}));
// 清空输入内容
resetInputVal('');
scrollToBottom();
const fnMap: { [key: string]: any } = {
[OpenAiModelEnum.GPT35]: chatGPTPrompt,
[OpenAiModelEnum.GPT3]: gpt3ChatPrompt
[ChatModelNameEnum.GPT35]: chatGPTPrompt,
[ChatModelNameEnum.GPT3]: gpt3ChatPrompt
};
try {
/* 对长度进行限制 */
const maxContext = chatSiteData.secret.contextMaxLen;
const maxContext = chatData.secret.contextMaxLen;
const requestPrompt =
newChatList.length > maxContext + 1
? newChatList.slice(newChatList.length - maxContext - 1, -1)
: newChatList.slice(0, -1);
if (typeof fnMap[chatSiteData.chatModel] === 'function') {
await fnMap[chatSiteData.chatModel](requestPrompt);
if (typeof fnMap[chatData.chatModel] === 'function') {
await fnMap[chatData.chatModel](requestPrompt);
}
// 如果是 Human 第一次发送,插入历史记录
const humanChat = newChatList.filter((item) => item.obj === 'Human');
if (windowId && humanChat.length === 1) {
if (humanChat.length === 1) {
pushChatHistory({
chatId,
windowId,
title: humanChat[0].value
});
}
@@ -276,34 +318,41 @@ const Chat = ({
resetInputVal(storeInput);
setChatList(newChatList.slice(0, newChatList.length - 2));
setChatData((state) => ({
...state,
history: newChatList.slice(0, newChatList.length - 2)
}));
}
}, [
chatGPTPrompt,
chatList,
chatSiteData,
gpt3ChatPrompt,
inputVal,
chatData.modelId,
chatData.history,
chatData.secret.contextMaxLen,
chatData.chatModel,
isChatting,
resetInputVal,
scrollToBottom,
toast,
chatGPTPrompt,
gpt3ChatPrompt,
pushChatHistory,
chatId,
windowId,
pushChatHistory
toast
]);
// 重新编辑
const reEdit = useCallback(async () => {
if (chatList[chatList.length - 1]?.obj !== 'Human') return;
if (chatData.history[chatData.history.length - 1]?.obj !== 'Human') return;
// 删除数据库最后一句
await delLastMessage(windowId);
const val = chatList[chatList.length - 1].value;
await delLastMessage(chatId);
const val = chatData.history[chatData.history.length - 1].value;
resetInputVal(val);
setChatList(chatList.slice(0, -1));
}, [chatList, resetInputVal, windowId]);
setChatData((state) => ({
...state,
history: state.history.slice(0, -1)
}));
}, [chatData.history, chatId, resetInputVal]);
return (
<Flex h={'100%'} flexDirection={media('row', 'column')}>
@@ -311,9 +360,9 @@ const Chat = ({
<Box flex={'0 0 250px'} w={0} h={'100%'}>
<SlideBar
resetChat={resetChat}
name={chatSiteData?.name}
windowId={windowId}
name={chatData?.name}
chatId={chatId}
modelId={chatData.modelId}
onClose={onCloseSlider}
/>
</Box>
@@ -330,23 +379,18 @@ const Chat = ({
<Box onClick={onOpenSlider}>
<Icon name="icon-caidan" width={20} height={20}></Icon>
</Box>
<Box>{chatSiteData?.name}</Box>
<Box>{chatData?.name}</Box>
</Flex>
<Drawer isOpen={isOpenSlider} placement="left" size={'xs'} onClose={onCloseSlider}>
<DrawerOverlay backgroundColor={'rgba(255,255,255,0.5)'} />
<DrawerContent maxWidth={'250px'}>
<SlideBar
resetChat={resetChat}
name={chatSiteData?.name}
windowId={windowId}
name={chatData?.name}
chatId={chatId}
modelId={chatData.modelId}
onClose={onCloseSlider}
/>
<DrawerFooter px={2} backgroundColor={'blackAlpha.800'}>
<Button variant="white" onClick={onCloseSlider}>
Cancel
</Button>
</DrawerFooter>
</DrawerContent>
</Drawer>
</Box>
@@ -359,7 +403,7 @@ const Chat = ({
>
{/* 聊天内容 */}
<Box ref={ChatBox} flex={'1 0 0'} h={0} w={'100%'} overflowY={'auto'}>
{chatList.map((item, index) => (
{chatData.history.map((item, index) => (
<Box
key={index}
py={media(9, 6)}
@@ -380,7 +424,7 @@ const Chat = ({
{item.obj === 'AI' ? (
<Markdown
source={item.value}
isChatting={isChatting && index === chatList.length - 1}
isChatting={isChatting && index === chatData.history.length - 1}
/>
) : (
<Box whiteSpace={'pre-wrap'}>{item.value}</Box>
@@ -398,14 +442,17 @@ const Chat = ({
boxShadow={'0 -14px 30px rgba(255,255,255,0.6)'}
borderTop={media('none', '1px solid rgba(0,0,0,0.1)')}
>
{lastWordHuman ? (
{!!chatWindowError ? (
<Box textAlign={'center'}>
<Box color={'red'}></Box>
<Box color={'red'}>{chatWindowError.text}</Box>
<Flex py={5} justifyContent={'center'}>
<Button mr={20} onClick={resetChat} colorScheme={'green'}>
</Button>
<Button onClick={reEdit}></Button>
{getToken() && <Button onClick={resetChat}></Button>}
{chatWindowError.canDelete && (
<Button ml={20} colorScheme={'green'} onClick={reEdit}>
</Button>
)}
</Flex>
</Box>
) : (
@@ -433,7 +480,7 @@ const Chat = ({
height={'22px'}
lineHeight={'22px'}
maxHeight={'150px'}
maxLength={chatSiteData?.secret.contentMaxLen || -1}
maxLength={chatData?.secret.contentMaxLen || -1}
overflowY={'auto'}
onChange={(e) => {
const textarea = e.target;
@@ -480,10 +527,8 @@ export default Chat;
export async function getServerSideProps(context: any) {
const chatId = context.query?.chatId || '';
const windowId = context.query?.windowId || '';
const timeStamp = context.query?.timeStamp || `${Date.now()}`;
return {
props: { chatId, windowId, timeStamp }
props: { chatId }
};
}