From 1e770088d0dde1e9336d9b70695fda3614b8c33d Mon Sep 17 00:00:00 2001 From: Archer <545436317@qq.com> Date: Thu, 16 Mar 2023 23:38:43 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=A2=9E=E5=8A=A0=E8=81=8A=E5=A4=A9nav?= =?UTF-8?q?bar?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/api/chat/chatGpt.ts | 8 +- src/pages/chat/components/SlideBar.tsx | 197 ++++++++++++++++++++++++- src/pages/chat/index.tsx | 87 ++++++++++- src/store/chat.ts | 48 ++++++ src/styles/reset.scss | 12 ++ src/types/chat.d.ts | 6 + 6 files changed, 341 insertions(+), 17 deletions(-) create mode 100644 src/store/chat.ts diff --git a/src/pages/api/chat/chatGpt.ts b/src/pages/api/chat/chatGpt.ts index cd2f3af9a..662c6c481 100644 --- a/src/pages/api/chat/chatGpt.ts +++ b/src/pages/api/chat/chatGpt.ts @@ -59,6 +59,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) // 获取 chatAPI const chatAPI = getOpenAIApi(userApiKey); + let startTime = Date.now(); // 发出请求 const chatResponse = await chatAPI.createChatCompletion( { @@ -69,14 +70,15 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) stream: true }, { - timeout: 20000, + timeout: 40000, responseType: 'stream', httpsAgent } ); console.log( - formatPrompts.reduce((sum, item) => sum + item.content.length, 0), - 'response success' + 'response success', + `${(Date.now() - startTime) / 1000}s`, + formatPrompts.reduce((sum, item) => sum + item.content.length, 0) ); // 创建响应流 diff --git a/src/pages/chat/components/SlideBar.tsx b/src/pages/chat/components/SlideBar.tsx index 502f129cd..776a0b129 100644 --- a/src/pages/chat/components/SlideBar.tsx +++ b/src/pages/chat/components/SlideBar.tsx @@ -1,17 +1,198 @@ -import React from 'react'; +import React, { useState, useEffect } from 'react'; import { Box, Button } from '@chakra-ui/react'; -import { AddIcon } from '@chakra-ui/icons'; +import { AddIcon, ChatIcon, EditIcon, DeleteIcon } from '@chakra-ui/icons'; +import { + Accordion, + AccordionItem, + AccordionButton, + AccordionPanel, + AccordionIcon, + Flex, + Input, + IconButton +} from '@chakra-ui/react'; +import { useUserStore } from '@/store/user'; +import { useChatStore } from '@/store/chat'; +import { useQuery } from '@tanstack/react-query'; +import { useRouter } from 'next/router'; +import { useScreen } from '@/hooks/useScreen'; + +const SlideBar = ({ + name, + windowId, + chatId, + resetChat, + onClose +}: { + resetChat: () => void; + name?: string; + windowId?: string; + chatId: string; + onClose: () => void; +}) => { + const router = useRouter(); + const { isPc } = useScreen(); + const { myModels, getMyModels } = useUserStore(); + const { chatHistory, removeChatHistoryByWindowId, generateChatWindow, updateChatHistory } = + useChatStore(); + const { isSuccess } = useQuery(['init'], getMyModels); + const [hasReady, setHasReady] = useState(false); + const [editHistoryId, setEditHistoryId] = useState(); + + useEffect(() => { + setHasReady(true); + }, []); + + const RenderHistory = () => ( + <> + {chatHistory.map((item) => ( + { + if ( + (item.chatId === chatId && item.windowId === windowId) || + editHistoryId === item.windowId + ) + return; + router.push( + `/chat?chatId=${item.chatId}&windowId=${item.windowId}&timeStamp=${Date.now()}` + ); + onClose(); + }} + > + + + {item.title} + + {/* { + updateChatHistory(item.windowId, e.target.value); + }} + /> */} + + {/* } + variant={'unstyled'} + aria-label={'edit'} + size={'xs'} + onClick={(e) => { + console.log(e); + setEditHistoryId(item.windowId); + }} + /> */} + } + variant={'unstyled'} + aria-label={'edit'} + size={'xs'} + onClick={(e) => { + removeChatHistoryByWindowId(item.windowId); + e.stopPropagation(); + }} + /> + + + ))} + + ); -const SlideBar = ({ resetChat }: { resetChat: () => void }) => { return ( - + {/* 新对话 */} - - {/* 我的模型 */} - - {/* 历史记录 */} + {/* 我的模型 & 历史记录 折叠框*/} + {isSuccess ? ( + + + + + 历史记录 + + + + + {hasReady && } + + + + + + 其他模型 + + + + + {myModels.map((item) => ( + { + if (item.name === name) return; + router.push( + `/chat?chatId=${await generateChatWindow(item._id)}&timeStamp=${Date.now()}` + ); + onClose(); + }} + > + + + {item.name} + + + ))} + + + + ) : ( + + )} ); }; diff --git a/src/pages/chat/index.tsx b/src/pages/chat/index.tsx index f470cb466..f366d6fc5 100644 --- a/src/pages/chat/index.tsx +++ b/src/pages/chat/index.tsx @@ -3,7 +3,17 @@ 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 { Textarea, Box, Flex, Button } from '@chakra-ui/react'; +import { + Textarea, + Box, + Flex, + Button, + useDisclosure, + Drawer, + DrawerFooter, + DrawerOverlay, + DrawerContent +} from '@chakra-ui/react'; import { useToast } from '@/hooks/useToast'; import Icon from '@/components/Icon'; import { useScreen } from '@/hooks/useScreen'; @@ -11,6 +21,7 @@ import { useQuery } from '@tanstack/react-query'; import { OpenAiModelEnum } 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'; @@ -36,10 +47,12 @@ const Chat = ({ const [chatSiteData, setChatSiteData] = useState(); // 聊天框整体数据 const [chatList, setChatList] = useState([]); // 对话内容 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 { pushChatHistory } = useChatStore(); // 滚动到底部 const scrollToBottom = useCallback(() => { @@ -102,7 +115,8 @@ const Chat = ({ // 重载对话 const resetChat = useCallback(() => { router.push(`/chat?chatId=${chatId}&timeStamp=${Date.now()}`); - }, [chatId, router]); + onCloseSlider(); + }, [chatId, router, onCloseSlider]); // gpt3 方法 const gpt3ChatPrompt = useCallback( @@ -242,6 +256,16 @@ const Chat = ({ if (typeof fnMap[chatSiteData.chatModel] === 'function') { await fnMap[chatSiteData.chatModel](requestPrompt); } + + // 如果是 Human 第一次发送,插入历史记录 + const humanChat = newChatList.filter((item) => item.obj === 'Human'); + if (windowId && humanChat.length === 1) { + pushChatHistory({ + chatId, + windowId, + title: humanChat[0].value + }); + } } catch (err: any) { toast({ title: typeof err === 'string' ? err : err?.message || '聊天出错了~', @@ -263,7 +287,10 @@ const Chat = ({ isChatting, resetInputVal, scrollToBottom, - toast + toast, + chatId, + windowId, + pushChatHistory ]); // 重新编辑 @@ -279,9 +306,57 @@ const Chat = ({ }, [chatList, resetInputVal, windowId]); return ( - - - + + {isPc ? ( + + + + ) : ( + + + + + + {chatSiteData?.name} + + + + + + + + + + + + )} + + {/* 聊天内容 */} {chatList.map((item, index) => ( diff --git a/src/store/chat.ts b/src/store/chat.ts new file mode 100644 index 000000000..9f8a9f557 --- /dev/null +++ b/src/store/chat.ts @@ -0,0 +1,48 @@ +import { create } from 'zustand'; +import { devtools, persist } from 'zustand/middleware'; +import { immer } from 'zustand/middleware/immer'; +import type { HistoryItem } from '@/types/chat'; +import { getChatSiteId } from '@/api/chat'; + +type Props = { + chatHistory: HistoryItem[]; + pushChatHistory: (e: HistoryItem) => void; + updateChatHistory: (windowId: string, title: string) => void; + removeChatHistoryByWindowId: (windowId: string) => void; + generateChatWindow: (modelId: string) => Promise; +}; +export const useChatStore = create()( + devtools( + persist( + immer((set, get) => ({ + chatHistory: [], + pushChatHistory(item: HistoryItem) { + set((state) => { + state.chatHistory = [item, ...state.chatHistory]; + }); + }, + updateChatHistory(windowId: string, title: string) { + set((state) => { + state.chatHistory = state.chatHistory.map((item) => ({ + ...item, + title: item.windowId === windowId ? title : item.title + })); + }); + }, + removeChatHistoryByWindowId(windowId: string) { + set((state) => { + state.chatHistory = state.chatHistory.filter((item) => item.windowId !== windowId); + }); + }, + generateChatWindow(modelId: string) { + return getChatSiteId(modelId); + } + })), + { + name: 'chatHistory' + // serialize: JSON.stringify, + // deserialize: (data) => (data ? JSON.parse(data) : []), + } + ) + ) +); diff --git a/src/styles/reset.scss b/src/styles/reset.scss index feb2a8810..685dc39c0 100644 --- a/src/styles/reset.scss +++ b/src/styles/reset.scss @@ -59,3 +59,15 @@ svg { height: 2px; } } +.textEllipsis { + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; +} + +* { + -moz-outline-style: none; + -webkit-tap-highlight-color: rgba(0, 0, 0, 0); + -webkit-focus-ring-color: rgba(0, 0, 0, 0); + outline: none; +} diff --git a/src/types/chat.d.ts b/src/types/chat.d.ts index 19de5a519..2f9d8cd41 100644 --- a/src/types/chat.d.ts +++ b/src/types/chat.d.ts @@ -14,3 +14,9 @@ export type ChatItemType = { export type ChatSiteItemType = { status: 'loading' | 'finish'; } & ChatItemType; + +export type HistoryItem = { + chatId: string; + windowId: string; + title: string; +};