import React, { useCallback, useState, useRef, useMemo, useEffect } from 'react'; import { useRouter } from 'next/router'; import Image from 'next/image'; import { getInitChatSiteInfo, getChatSiteId, postGPT3SendPrompt, delLastMessage, postSaveChat } from '@/api/chat'; import type { InitChatResponse } from '@/api/response/chat'; import { ChatSiteItemType } from '@/types/chat'; import { Textarea, Box, Flex, Button, useDisclosure, Drawer, DrawerOverlay, DrawerContent, useColorModeValue } from '@chakra-ui/react'; import { useToast } from '@/hooks/useToast'; import Icon from '@/components/Iconfont'; import { useScreen } from '@/hooks/useScreen'; 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 { streamFetch } from '@/api/fetch'; import SlideBar from './components/SlideBar'; import Empty from './components/Empty'; import { getToken } from '@/utils/user'; import MyIcon from '@/components/Icon'; const Markdown = dynamic(() => import('@/components/Markdown')); const textareaMinH = '22px'; 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({ chatId: '', modelId: '', name: '', avatar: '', intro: '', secret: {}, chatModel: '', history: [], isExpiredTime: false }); // 聊天框整体数据 const ChatBox = useRef(null); const TextareaDom = useRef(null); const [inputVal, setInputVal] = useState(''); // 输入的内容 const { isOpen: isOpenSlider, onClose: onCloseSlider, onOpen: onOpenSlider } = useDisclosure(); 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(); // 中断请求 const controller = useRef(new AbortController()); useEffect(() => { controller.current = new AbortController(); return () => { console.log('close========'); // eslint-disable-next-line react-hooks/exhaustive-deps controller.current?.abort(); }; }, [chatId]); // 滚动到底部 const scrollToBottom = useCallback(() => { setTimeout(() => { ChatBox.current && ChatBox.current.scrollTo({ top: ChatBox.current.scrollHeight, behavior: 'smooth' }); }, 100); }, []); // 初始化聊天框 useQuery( ['init', chatId], () => { setLoading(true); return getInitChatSiteInfo(chatId); }, { onSuccess(res) { setChatData({ ...res, history: res.history.map((item) => ({ ...item, status: 'finish' })) }); if (res.history.length > 0) { setTimeout(() => { scrollToBottom(); }, 500); } }, onError(e: any) { toast({ title: e?.message || '初始化异常,请检查地址', status: 'error', isClosable: true, duration: 5000 }); }, onSettled() { setLoading(false); } } ); // 重置输入内容 const resetInputVal = useCallback((val: string) => { setInputVal(val); setTimeout(() => { /* 回到最小高度 */ if (TextareaDom.current) { TextareaDom.current.style.height = val === '' ? textareaMinH : `${TextareaDom.current.scrollHeight}px`; } }, 100); }, []); // 重载对话 const resetChat = useCallback(async () => { if (!chatData) return; try { router.replace(`/chat?chatId=${await getChatSiteId(chatData.modelId)}`); } catch (error: any) { toast({ title: error?.message || '生成新对话失败', status: 'warning' }); } onCloseSlider(); }, [chatData, onCloseSlider, router, toast]); // gpt3 方法 const gpt3ChatPrompt = useCallback( async (newChatList: ChatSiteItemType[]) => { // 请求内容 const response = await postGPT3SendPrompt({ prompt: newChatList, chatId: chatId as string }); // 更新 AI 的内容 setChatData((state) => ({ ...state, history: state.history.map((item, index) => { if (index !== state.history.length - 1) return item; return { ...item, status: 'finish', value: response }; }) })); }, [chatId] ); // gpt 对话 const gptChatPrompt = useCallback( async (prompts: ChatSiteItemType) => { const urlMap: Record = { [ChatModelNameEnum.GPT35]: '/api/chat/chatGpt', [ChatModelNameEnum.GPT3]: '/api/chat/gpt3' }; if (!urlMap[chatData.chatModel]) return Promise.reject('找不到模型'); const prompt = { obj: prompts.obj, value: prompts.value }; // 流请求,获取数据 const res = await streamFetch({ url: urlMap[chatData.chatModel], data: { prompt, chatId }, onMessage: (text: string) => { setChatData((state) => ({ ...state, history: state.history.map((item, index) => { if (index !== state.history.length - 1) return item; return { ...item, value: item.value + text }; }) })); }, abortSignal: controller.current }); // 保存对话信息 try { await postSaveChat({ chatId, prompts: [ prompt, { obj: 'AI', value: res as string } ] }); } catch (err) { toast({ title: '对话出现异常, 继续对话会导致上下文丢失,请刷新页面', status: 'warning', duration: 3000, isClosable: true }); } // 设置完成状态 setChatData((state) => ({ ...state, history: state.history.map((item, index) => { if (index !== state.history.length - 1) return item; return { ...item, status: 'finish' }; }) })); }, [chatData.chatModel, chatId, toast] ); /** * 发送一个内容 */ const sendPrompt = useCallback(async () => { const storeInput = inputVal; // 去除空行 const val = inputVal .trim() .split('\n') .filter((val) => val) .join('\n'); if (!chatData?.modelId || !val || !ChatBox.current || isChatting) { return; } const newChatList: ChatSiteItemType[] = [ ...chatData.history, { obj: 'Human', value: val, status: 'finish' }, { obj: 'AI', value: '', status: 'loading' } ]; // 插入内容 setChatData((state) => ({ ...state, history: newChatList })); // 清空输入内容 resetInputVal(''); scrollToBottom(); 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 || '聊天出错了~', status: 'warning', duration: 5000, isClosable: true }); resetInputVal(storeInput); setChatData((state) => ({ ...state, history: newChatList.slice(0, newChatList.length - 2) })); } }, [ inputVal, chatData?.modelId, chatData.history, isChatting, resetInputVal, scrollToBottom, gptChatPrompt, pushChatHistory, chatId, toast ]); // 重新编辑 const reEdit = useCallback(async () => { if (chatData.history[chatData.history.length - 1]?.obj !== 'Human') return; // 删除数据库最后一句 await delLastMessage(chatId); const val = chatData.history[chatData.history.length - 1].value; resetInputVal(val); setChatData((state) => ({ ...state, history: state.history.slice(0, -1) })); }, [chatData.history, chatId, resetInputVal]); return ( {isPc ? ( ) : ( {chatData?.name} )} {/* 聊天内容 */} {chatData.history.map((item, index) => ( /icon/logo.png {item.obj === 'AI' ? ( ) : ( {item.value} )} ))} {chatData.history.length === 0 && } {/* 发送区 */} {!!chatWindowError ? ( {chatWindowError.text} {getToken() && } {chatWindowError.canDelete && ( )} ) : ( {/* 输入框 */}