import React, { useCallback, useState, useRef, useMemo } from 'react'; import { useRouter } from 'next/router'; import Image from 'next/image'; import { getInitChatSiteInfo, postGPT3SendPrompt, getChatGPTSendEvent, postChatGptPrompt, delLastMessage } from '@/api/chat'; import { ChatSiteItemType, ChatSiteType } from '@/types/chat'; import { Textarea, Box, Flex, Button } from '@chakra-ui/react'; import { useToast } from '@/hooks/useToast'; import Icon from '@/components/Icon'; import { useScreen } from '@/hooks/useScreen'; import { useQuery } from '@tanstack/react-query'; import { OpenAiModelEnum } from '@/constants/model'; import dynamic from 'next/dynamic'; import { useGlobalStore } from '@/store/global'; const Markdown = dynamic(() => import('@/components/Markdown')); const textareaMinH = '22px'; const Chat = () => { const { toast } = useToast(); const router = useRouter(); const { isPc, media } = useScreen(); const { chatId, windowId } = router.query as { chatId: string; windowId?: string }; const ChatBox = useRef(null); const TextareaDom = useRef(null); const [chatSiteData, setChatSiteData] = useState(); // 聊天框整体数据 const [chatList, setChatList] = useState([]); // 对话内容 const [inputVal, setInputVal] = useState(''); // 输入的内容 const isChatting = useMemo(() => chatList[chatList.length - 1]?.status === 'loading', [chatList]); const lastWordHuman = useMemo(() => chatList[chatList.length - 1]?.obj === 'Human', [chatList]); const { setLoading } = useGlobalStore(); // 滚动到底部 const scrollToBottom = useCallback(() => { // 滚动到底部 setTimeout(() => { ChatBox.current && ChatBox.current.scrollTo({ top: ChatBox.current.scrollHeight, behavior: 'smooth' }); }, 100); }, []); // 初始化聊天框 useQuery( [chatId, windowId], () => { if (!chatId) return null; setLoading(true); return getInitChatSiteInfo(chatId, windowId); }, { cacheTime: 5 * 60 * 1000, onSuccess(res) { if (!res) return; router.replace(`/chat?chatId=${chatId}&windowId=${res.windowId}`); setChatSiteData(res.chatSite); setChatList( res.history.map((item) => ({ ...item, status: 'finish' })) ); scrollToBottom(); setLoading(false); }, onError(e: any) { toast({ title: e?.message || '初始化异常,请检查地址', status: 'error', isClosable: true, duration: 5000 }); setLoading(false); } } ); // gpt3 方法 const gpt3ChatPrompt = useCallback( async (newChatList: ChatSiteItemType[]) => { // 请求内容 const response = await postGPT3SendPrompt({ prompt: newChatList, chatId: chatId as string }); // 更新 AI 的内容 setChatList((state) => state.map((item, index) => { if (index !== state.length - 1) return item; return { ...item, status: 'finish', value: response }; }) ); }, [chatId] ); // chatGPT const chatGPTPrompt = useCallback( async (newChatList: ChatSiteItemType[]) => { if (!windowId) return; /* 预请求,把消息存入库 */ await postChatGptPrompt({ windowId, prompt: newChatList[newChatList.length - 1], chatId }); return new Promise((resolve, reject) => { const event = getChatGPTSendEvent(chatId, windowId); // 30s 收不到消息就报错 let timer = setTimeout(() => { event.close(); reject('服务器超时'); }, 300000); event.addEventListener('responseData', ({ data }) => { /* 重置定时器 */ clearTimeout(timer); timer = setTimeout(() => { event.close(); reject('服务器超时'); }, 300000); const msg = data.replace(//g, '\n'); setChatList((state) => state.map((item, index) => { if (index !== state.length - 1) return item; return { ...item, value: item.value + msg }; }) ); }); event.addEventListener('done', () => { console.log('done'); clearTimeout(timer); event.close(); setChatList((state) => state.map((item, index) => { if (index !== state.length - 1) return item; return { ...item, status: 'finish' }; }) ); resolve(''); }); event.addEventListener('serviceError', ({ data: err }) => { clearTimeout(timer); event.close(); console.error(err, '==='); reject(typeof err === 'string' ? err : '对话出现不知名错误~'); }); event.onerror = (err) => { clearTimeout(timer); event.close(); console.error(err); reject(typeof err === 'string' ? err : '对话出现不知名错误~'); }; }); }, [chatId, windowId] ); /** * 发送一个内容 */ const sendPrompt = useCallback(async () => { const storeInput = inputVal; // 去除空行 const val = inputVal .trim() .split('\n') .filter((val) => val) .join('\n\n'); if (!chatSiteData?.modelId || !val || !ChatBox.current || isChatting) { return; } const newChatList: ChatSiteItemType[] = [ ...chatList, { obj: 'Human', value: val, status: 'finish' }, { obj: 'AI', value: '', status: 'loading' } ]; // 插入内容 setChatList(newChatList); setInputVal(''); // 滚动到底部 setTimeout(() => { scrollToBottom(); /* 回到最小高度 */ if (TextareaDom.current) { TextareaDom.current.style.height = textareaMinH; } }, 100); const fnMap: { [key: string]: any } = { [OpenAiModelEnum.GPT35]: chatGPTPrompt, [OpenAiModelEnum.GPT3]: gpt3ChatPrompt }; try { /* 对长度进行限制 */ const maxContext = chatSiteData.secret.contextMaxLen; const requestPrompt = newChatList.length > maxContext + 2 ? [newChatList[0], ...newChatList.slice(newChatList.length - maxContext - 1, -1)] : newChatList.slice(0, newChatList.length - 1); if (typeof fnMap[chatSiteData.chatModel] === 'function') { await fnMap[chatSiteData.chatModel](requestPrompt); } } catch (err) { toast({ title: typeof err === 'string' ? err : '聊天已过期', status: 'warning', duration: 5000, isClosable: true }); setInputVal(storeInput); setChatList(newChatList.slice(0, newChatList.length - 2)); } }, [ chatGPTPrompt, chatList, chatSiteData, gpt3ChatPrompt, inputVal, isChatting, scrollToBottom, toast ]); // 重新编辑 const reEdit = useCallback(async () => { if (chatList[chatList.length - 1]?.obj !== 'Human') return; // 删除数据库最后一句 delLastMessage(windowId); const val = chatList[chatList.length - 1].value; setInputVal(val); setChatList(chatList.slice(0, -1)); setTimeout(() => { if (TextareaDom.current) { TextareaDom.current.style.height = val.split('\n').length * 22 + 'px'; } }, 100); }, [chatList, windowId]); return ( {/* 头部 */} {chatSiteData?.name} {/* 重置按键 */} router.replace(`/chat?chatId=${chatId}`)}> {/* 滚动到底部按键 */} {ChatBox.current && ChatBox.current.scrollHeight > 2 * ChatBox.current.clientHeight && ( )} {/* 聊天内容 */} {chatList.map((item, index) => ( /icon/logo.png {item.obj === 'AI' ? ( ) : ( {item.value} )} ))} {/* 空内容提示 */} {/* { chatList.length === 0 && ( <> 内容太长 ) } */} {lastWordHuman ? ( 对话出现了异常 ) : ( {/* 输入框 */}