From 277494085179b73da587afabe579b792e42210d0 Mon Sep 17 00:00:00 2001 From: archer <545436317@qq.com> Date: Sun, 23 Apr 2023 16:06:16 +0800 Subject: [PATCH] feat: history chat --- src/api/chat.ts | 11 ++ src/components/Icon/icons/dbModel.svg | 1 + src/components/Icon/icons/history.svg | 1 + src/components/Icon/index.tsx | 4 +- src/constants/model.ts | 3 + src/pages/api/chat/getHistory.ts | 31 +++++ src/pages/api/chat/removeHistory.ts | 27 +++++ src/pages/api/chat/saveChat.ts | 3 +- src/pages/chat/components/SlideBar.tsx | 151 +++++++++++++------------ src/pages/chat/index.tsx | 23 +--- src/service/models/chat.ts | 4 + src/store/chat.ts | 50 -------- 12 files changed, 162 insertions(+), 147 deletions(-) create mode 100644 src/components/Icon/icons/dbModel.svg create mode 100644 src/components/Icon/icons/history.svg create mode 100644 src/pages/api/chat/getHistory.ts create mode 100644 src/pages/api/chat/removeHistory.ts delete mode 100644 src/store/chat.ts diff --git a/src/api/chat.ts b/src/api/chat.ts index b788ac57f..8367d3614 100644 --- a/src/api/chat.ts +++ b/src/api/chat.ts @@ -26,6 +26,17 @@ export const postGPT3SendPrompt = ({ })) }); +/** + * 获取历史记录 + */ +export const getChatHistory = () => + GET<{ _id: string; title: string; modelId: string }[]>('/chat/getHistory'); + +/** + * 删除一条历史记录 + */ +export const delChatHistoryById = (id: string) => GET(`/chat/removeHistory?id=${id}`); + /** * 存储一轮对话 */ diff --git a/src/components/Icon/icons/dbModel.svg b/src/components/Icon/icons/dbModel.svg new file mode 100644 index 000000000..b0fe76aa6 --- /dev/null +++ b/src/components/Icon/icons/dbModel.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/components/Icon/icons/history.svg b/src/components/Icon/icons/history.svg new file mode 100644 index 000000000..6f9bbaa66 --- /dev/null +++ b/src/components/Icon/icons/history.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/components/Icon/index.tsx b/src/components/Icon/index.tsx index a9ba353d4..7db6b50bd 100644 --- a/src/components/Icon/index.tsx +++ b/src/components/Icon/index.tsx @@ -16,7 +16,9 @@ const map = { chatting: require('./icons/chatting.svg').default, promotion: require('./icons/promotion.svg').default, delete: require('./icons/delete.svg').default, - withdraw: require('./icons/withdraw.svg').default + withdraw: require('./icons/withdraw.svg').default, + dbModel: require('./icons/dbModel.svg').default, + history: require('./icons/history.svg').default }; export type IconName = keyof typeof map; diff --git a/src/constants/model.ts b/src/constants/model.ts index 90610fde8..6956bbbf5 100644 --- a/src/constants/model.ts +++ b/src/constants/model.ts @@ -18,6 +18,7 @@ export const ChatModelNameMap = { }; export type ModelConstantsData = { + icon: 'model' | 'dbModel'; name: string; model: `${ChatModelNameEnum}`; trainName: string; // 空字符串代表不能训练 @@ -29,6 +30,7 @@ export type ModelConstantsData = { export const modelList: ModelConstantsData[] = [ { + icon: 'model', name: 'chatGPT', model: ChatModelNameEnum.GPT35, trainName: '', @@ -38,6 +40,7 @@ export const modelList: ModelConstantsData[] = [ price: 3 }, { + icon: 'dbModel', name: '知识库', model: ChatModelNameEnum.VECTOR_GPT, trainName: 'vector', diff --git a/src/pages/api/chat/getHistory.ts b/src/pages/api/chat/getHistory.ts new file mode 100644 index 000000000..298177a82 --- /dev/null +++ b/src/pages/api/chat/getHistory.ts @@ -0,0 +1,31 @@ +import type { NextApiRequest, NextApiResponse } from 'next'; +import { jsonRes } from '@/service/response'; +import { connectToDatabase, Chat } from '@/service/mongo'; +import { authToken } from '@/service/utils/tools'; + +/* 获取历史记录 */ +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + try { + const userId = await authToken(req.headers.authorization); + + await connectToDatabase(); + + const data = await Chat.find( + { + userId + }, + '_id title modelId' + ) + .sort({ updateTime: -1 }) + .limit(20); + + jsonRes(res, { + data + }); + } catch (err) { + jsonRes(res, { + code: 500, + error: err + }); + } +} diff --git a/src/pages/api/chat/removeHistory.ts b/src/pages/api/chat/removeHistory.ts new file mode 100644 index 000000000..27dca27f0 --- /dev/null +++ b/src/pages/api/chat/removeHistory.ts @@ -0,0 +1,27 @@ +import type { NextApiRequest, NextApiResponse } from 'next'; +import { jsonRes } from '@/service/response'; +import { ChatItemType } from '@/types/chat'; +import { connectToDatabase, Chat } from '@/service/mongo'; +import { authToken } from '@/service/utils/tools'; + +/* 获取历史记录 */ +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + try { + const { id } = req.query; + const userId = await authToken(req.headers.authorization); + + await connectToDatabase(); + + await Chat.findOneAndRemove({ + _id: id, + userId + }); + + jsonRes(res); + } catch (err) { + jsonRes(res, { + code: 500, + error: err + }); + } +} diff --git a/src/pages/api/chat/saveChat.ts b/src/pages/api/chat/saveChat.ts index bb62e144f..1dbc11a24 100644 --- a/src/pages/api/chat/saveChat.ts +++ b/src/pages/api/chat/saveChat.ts @@ -33,7 +33,8 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) const { _id } = await Chat.create({ userId, modelId, - content + content, + title: content[0].value.slice(0, 20) }); return jsonRes(res, { data: _id diff --git a/src/pages/chat/components/SlideBar.tsx b/src/pages/chat/components/SlideBar.tsx index d8ad06513..6aa2c9093 100644 --- a/src/pages/chat/components/SlideBar.tsx +++ b/src/pages/chat/components/SlideBar.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from 'react'; +import React, { useRef, useEffect } from 'react'; import { AddIcon, ChatIcon, DeleteIcon, MoonIcon, SunIcon } from '@chakra-ui/icons'; import { Box, @@ -16,14 +16,13 @@ import { useColorModeValue } from '@chakra-ui/react'; import { useUserStore } from '@/store/user'; -import { useChatStore } from '@/store/chat'; -import { useQuery } from '@tanstack/react-query'; +import { useMutation, useQuery } from '@tanstack/react-query'; import { useRouter } from 'next/router'; import { getToken } from '@/utils/user'; import MyIcon from '@/components/Icon'; -import { useCopyData } from '@/utils/tools'; import WxConcat from '@/components/WxConcat'; -import { useMarkdown } from '@/hooks/useMarkdown'; +import { getChatHistory, delChatHistoryById } from '@/api/chat'; +import { modelList } from '@/constants/model'; const SlideBar = ({ chatId, @@ -38,27 +37,34 @@ const SlideBar = ({ }) => { const router = useRouter(); const { colorMode, toggleColorMode } = useColorMode(); - const { copyData } = useCopyData(); const { myModels, getMyModels } = useUserStore(); - const { chatHistory, removeChatHistoryByWindowId } = useChatStore(); - const [hasReady, setHasReady] = useState(false); - const { isOpen: isOpenShare, onOpen: onOpenShare, onClose: onCloseShare } = useDisclosure(); const { isOpen: isOpenWx, onOpen: onOpenWx, onClose: onCloseWx } = useDisclosure(); - const { data: shareHint } = useMarkdown({ url: '/chatProblem.md' }); + const preChatId = useRef('chatId'); // 用于校验上一次chatId的情况,判断是否需要刷新历史记录 - const { isSuccess } = useQuery(['init'], getMyModels, { + const { isSuccess } = useQuery(['getMyModels'], getMyModels, { cacheTime: 5 * 60 * 1000 }); + const { data: chatHistory = [], mutate: loadChatHistory } = useMutation({ + mutationFn: getChatHistory + }); + useEffect(() => { - setHasReady(true); - }, []); + if (chatId && preChatId.current === '') { + loadChatHistory(); + } + preChatId.current = chatId; + }, [chatId, loadChatHistory]); + + useEffect(() => { + loadChatHistory(); + }, [loadChatHistory]); const RenderHistory = () => ( <> {chatHistory.map((item) => ( { - if (item.chatId === chatId) return; - resetChat(modelId, item.chatId); + if (item._id === chatId) return; + preChatId.current = 'chatId'; + resetChat(item.modelId, item._id); onClose(); }} > @@ -91,12 +98,14 @@ const SlideBar = ({ variant={'unstyled'} aria-label={'edit'} size={'xs'} - onClick={(e) => { - removeChatHistoryByWindowId(item.chatId); - if (item.chatId === chatId) { + onClick={async (e) => { + e.stopPropagation(); + + await delChatHistoryById(item._id); + loadChatHistory(); + if (item._id === chatId) { resetChat(); } - e.stopPropagation(); }} /> @@ -151,53 +160,55 @@ const SlideBar = ({ 新对话 )} - {/* 我的模型 & 历史记录 折叠框*/} - - {isSuccess && ( - - - - 其他模型 - - - - - {myModels.map((item) => ( - { - if (item._id === modelId) return; - resetChat(item._id); - onClose(); - }} - > - - - {item.name} - - - ))} - - - )} + {isSuccess && ( + <> + + {myModels.map((item) => ( + { + if (item._id === modelId) return; + resetChat(item._id); + onClose(); + }} + > + model.model === item.service.modelName)?.icon || + 'model' + } + mr={2} + color={'white'} + w={'16px'} + h={'16px'} + /> + + {item.name} + + + ))} + + + )} + @@ -206,7 +217,7 @@ const SlideBar = ({ - {hasReady && } + @@ -221,12 +232,6 @@ const SlideBar = ({ - {/* - <> - - 分享 - - */} router.push('/number/setting')}> <> diff --git a/src/pages/chat/index.tsx b/src/pages/chat/index.tsx index 44220e597..c2fa7f387 100644 --- a/src/pages/chat/index.tsx +++ b/src/pages/chat/index.tsx @@ -24,7 +24,6 @@ import { useQuery } from '@tanstack/react-query'; import { ChatModelNameEnum } from '@/constants/model'; import dynamic from 'next/dynamic'; import { useGlobalStore } from '@/store/global'; -import { useChatStore } from '@/store/chat'; import { useCopyData } from '@/utils/tools'; import { streamFetch } from '@/api/fetch'; import Icon from '@/components/Icon'; @@ -72,7 +71,6 @@ const Chat = ({ modelId, chatId }: { modelId: string; chatId: string }) => { const { copyData } = useCopyData(); const { isPc, media } = useScreen(); const { setLoading } = useGlobalStore(); - const { pushChatHistory } = useChatStore(); // 滚动到底部 const scrollToBottom = useCallback((behavior: 'smooth' | 'auto' = 'smooth') => { @@ -311,15 +309,6 @@ const Chat = ({ modelId, chatId }: { modelId: string; chatId: string }) => { try { await gptChatPrompt(newChatList[newChatList.length - 2]); - - // 如果是 Human 第一次发送,插入历史记录 - const humanChat = newChatList.filter((item) => item.obj === 'Human'); - if (humanChat.length === 1) { - pushChatHistory({ - chatId, - title: humanChat[0].value - }); - } } catch (err: any) { toast({ title: typeof err === 'string' ? err : err?.message || '聊天出错了~', @@ -335,17 +324,7 @@ const Chat = ({ modelId, chatId }: { modelId: string; chatId: string }) => { history: newChatList.slice(0, newChatList.length - 2) })); } - }, [ - isChatting, - inputVal, - chatData.history, - resetInputVal, - toast, - scrollToBottom, - gptChatPrompt, - pushChatHistory, - chatId - ]); + }, [isChatting, inputVal, chatData.history, resetInputVal, toast, scrollToBottom, gptChatPrompt]); // 删除一句话 const delChatRecord = useCallback( diff --git a/src/service/models/chat.ts b/src/service/models/chat.ts index f2cd829f0..c6fca5d5a 100644 --- a/src/service/models/chat.ts +++ b/src/service/models/chat.ts @@ -26,6 +26,10 @@ const ChatSchema = new Schema({ type: Date, default: () => new Date() }, + title: { + type: String, + default: '历史记录' + }, content: { type: [ { diff --git a/src/store/chat.ts b/src/store/chat.ts deleted file mode 100644 index b06147056..000000000 --- a/src/store/chat.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { create } from 'zustand'; -import { devtools, persist } from 'zustand/middleware'; -import { immer } from 'zustand/middleware/immer'; -import type { HistoryItem } from '@/types/chat'; - -type Props = { - chatHistory: HistoryItem[]; - pushChatHistory: (e: HistoryItem) => void; - updateChatHistory: (chatId: string, title: string) => void; - removeChatHistoryByWindowId: (chatId: string) => void; - clearHistory: () => void; -}; -export const useChatStore = create()( - devtools( - persist( - immer((set, get) => ({ - chatHistory: [], - pushChatHistory(item: HistoryItem) { - set((state) => { - if (state.chatHistory.find((history) => history.chatId === item.chatId)) return; - state.chatHistory = [item, ...state.chatHistory].slice(0, 20); - }); - }, - updateChatHistory(chatId: string, title: string) { - set((state) => { - state.chatHistory = state.chatHistory.map((item) => ({ - ...item, - title: item.chatId === chatId ? title : item.title - })); - }); - }, - removeChatHistoryByWindowId(chatId: string) { - set((state) => { - state.chatHistory = state.chatHistory.filter((item) => item.chatId !== chatId); - }); - }, - clearHistory() { - set((state) => { - state.chatHistory = []; - }); - } - })), - { - name: 'chatHistory' - // serialize: JSON.stringify, - // deserialize: (data) => (data ? JSON.parse(data) : []), - } - ) - ) -);