feat: 增加聊天navbar

This commit is contained in:
Archer
2023-03-16 23:38:43 +08:00
parent 7529f51e72
commit 1e770088d0
6 changed files with 341 additions and 17 deletions

View File

@@ -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)
);
// 创建响应流

View File

@@ -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<string>();
useEffect(() => {
setHasReady(true);
}, []);
const RenderHistory = () => (
<>
{chatHistory.map((item) => (
<Flex
key={item.windowId}
alignItems={'center'}
p={3}
borderRadius={'md'}
mb={2}
cursor={'pointer'}
_hover={{
backgroundColor: 'rgba(255,255,255,0.1)'
}}
fontSize={'xs'}
border={'1px solid transparent'}
{...(item.chatId === chatId && item.windowId === windowId
? {
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()}`
);
onClose();
}}
>
<ChatIcon mr={2} />
<Box flex={'1 0 0'} w={0} className="textEllipsis">
{item.title}
</Box>
{/* <Input
flex={'1 0 0'}
w={0}
value={item.title}
variant={'unstyled'}
disabled={editHistoryId !== item.windowId}
opacity={'1 !important'}
cursor={`${editHistoryId !== item.windowId ? 'pointer' : 'text'} !important`}
onChange={(e) => {
updateChatHistory(item.windowId, e.target.value);
}}
/> */}
<Box>
{/* <IconButton
icon={<EditIcon />}
variant={'unstyled'}
aria-label={'edit'}
size={'xs'}
onClick={(e) => {
console.log(e);
setEditHistoryId(item.windowId);
}}
/> */}
<IconButton
icon={<DeleteIcon />}
variant={'unstyled'}
aria-label={'edit'}
size={'xs'}
onClick={(e) => {
removeChatHistoryByWindowId(item.windowId);
e.stopPropagation();
}}
/>
</Box>
</Flex>
))}
</>
);
const SlideBar = ({ resetChat }: { resetChat: () => void }) => {
return (
<Box flex={'0 0 250px'} p={3} backgroundColor={'blackAlpha.800'} color={'white'}>
<Box w={'100%'} h={'100%'} p={3} backgroundColor={'blackAlpha.800'} color={'white'}>
{/* 新对话 */}
<Button w={'100%'} variant={'white'} h={'40px'} leftIcon={<AddIcon />} onClick={resetChat}>
<Button
w={'100%'}
variant={'white'}
h={'40px'}
mb={4}
leftIcon={<AddIcon />}
onClick={resetChat}
>
</Button>
{/* 我的模型 */}
{/* 历史记录 */}
{/* 我的模型 & 历史记录 折叠框*/}
{isSuccess ? (
<Accordion defaultIndex={[0]} allowToggle>
<AccordionItem borderTop={0} borderBottom={0}>
<AccordionButton borderRadius={'md'} pl={1}>
<Box as="span" flex="1" textAlign="left">
</Box>
<AccordionIcon />
</AccordionButton>
<AccordionPanel pb={0} px={0}>
{hasReady && <RenderHistory />}
</AccordionPanel>
</AccordionItem>
<AccordionItem borderTop={0} borderBottom={0}>
<AccordionButton borderRadius={'md'} pl={1}>
<Box as="span" flex="1" textAlign="left">
</Box>
<AccordionIcon />
</AccordionButton>
<AccordionPanel pb={4} px={0}>
{myModels.map((item) => (
<Flex
key={item._id}
alignItems={'center'}
p={3}
borderRadius={'md'}
mb={2}
cursor={'pointer'}
_hover={{
backgroundColor: 'rgba(255,255,255,0.1)'
}}
fontSize={'xs'}
border={'1px solid transparent'}
{...(item.name === name
? {
borderColor: 'rgba(255,255,255,0.5)',
backgroundColor: 'rgba(255,255,255,0.1)'
}
: {})}
onClick={async () => {
if (item.name === name) return;
router.push(
`/chat?chatId=${await generateChatWindow(item._id)}&timeStamp=${Date.now()}`
);
onClose();
}}
>
<ChatIcon mr={2} />
<Box className={'textEllipsis'} flex={'1 0 0'} w={0}>
{item.name}
</Box>
</Flex>
))}
</AccordionPanel>
</AccordionItem>
</Accordion>
) : (
<RenderHistory />
)}
</Box>
);
};

View File

@@ -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<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 { 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 (
<Flex h={'100%'}>
<SlideBar resetChat={resetChat} />
<Flex flex={1} h={'100%'} flexDirection={'column'}>
<Flex h={'100%'} flexDirection={media('row', 'column')}>
{isPc ? (
<Box flex={'0 0 250px'} w={0} h={'100%'}>
<SlideBar
resetChat={resetChat}
name={chatSiteData?.name}
windowId={windowId}
chatId={chatId}
onClose={onCloseSlider}
/>
</Box>
) : (
<Box h={'60px'} borderBottom={'1px solid rgba(0,0,0,0.1)'}>
<Flex
alignItems={'center'}
h={'100%'}
justifyContent={'space-between'}
backgroundColor={'white'}
position={'relative'}
px={7}
>
<Box onClick={onOpenSlider}>
<Icon name="icon-caidan" width={20} height={20}></Icon>
</Box>
<Box>{chatSiteData?.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}
chatId={chatId}
onClose={onCloseSlider}
/>
<DrawerFooter px={2} backgroundColor={'blackAlpha.800'}>
<Button variant="white" onClick={onCloseSlider}>
Cancel
</Button>
</DrawerFooter>
</DrawerContent>
</Drawer>
</Box>
)}
<Flex
{...media({ h: '100%', w: 0 }, { h: 0, w: '100%' })}
flex={'1 0 0'}
flexDirection={'column'}
>
{/* 聊天内容 */}
<Box ref={ChatBox} flex={'1 0 0'} h={0} w={'100%'} overflowY={'auto'}>
{chatList.map((item, index) => (

48
src/store/chat.ts Normal file
View File

@@ -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<string>;
};
export const useChatStore = create<Props>()(
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) : []),
}
)
)
);

View File

@@ -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;
}

6
src/types/chat.d.ts vendored
View File

@@ -14,3 +14,9 @@ export type ChatItemType = {
export type ChatSiteItemType = {
status: 'loading' | 'finish';
} & ChatItemType;
export type HistoryItem = {
chatId: string;
windowId: string;
title: string;
};