diff --git a/src/api/chat.ts b/src/api/chat.ts index c324f0207..912a166d1 100644 --- a/src/api/chat.ts +++ b/src/api/chat.ts @@ -39,6 +39,7 @@ export const postSaveChat = (data: { chatId: string; prompts: ChatItemType[] }) POST('/chat/saveChat', data); /** - * 删除最后一句 + * 删除一句对话 */ -export const delLastMessage = (chatId: string) => DELETE(`/chat/delLastMessage?chatId=${chatId}`); +export const delChatRecordByIndex = (chatId: string, index: number) => + DELETE(`/chat/delChatRecordByIndex?chatId=${chatId}&index=${index}`); diff --git a/src/components/Icon/icons/chatSend.svg b/src/components/Icon/icons/chatSend.svg new file mode 100644 index 000000000..8eac31de2 --- /dev/null +++ b/src/components/Icon/icons/chatSend.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/components/Icon/icons/copy.svg b/src/components/Icon/icons/copy.svg new file mode 100644 index 000000000..7204c2804 --- /dev/null +++ b/src/components/Icon/icons/copy.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 124564f71..aa69b3446 100644 --- a/src/components/Icon/index.tsx +++ b/src/components/Icon/index.tsx @@ -8,7 +8,9 @@ const map = { share: require('./icons/share.svg').default, home: require('./icons/home.svg').default, menu: require('./icons/menu.svg').default, - pay: require('./icons/pay.svg').default + pay: require('./icons/pay.svg').default, + copy: require('./icons/copy.svg').default, + chatSend: require('./icons/chatSend.svg').default }; export type IconName = keyof typeof map; diff --git a/src/components/Markdown/index.tsx b/src/components/Markdown/index.tsx index dc206a628..e5f140a43 100644 --- a/src/components/Markdown/index.tsx +++ b/src/components/Markdown/index.tsx @@ -3,7 +3,7 @@ import ReactMarkdown from 'react-markdown'; import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'; import { Box, Flex, useColorModeValue } from '@chakra-ui/react'; import { useCopyData } from '@/utils/tools'; -import Icon from '@/components/Iconfont'; +import Icon from '@/components/Icon'; import remarkGfm from 'remark-gfm'; import remarkMath from 'remark-math'; import rehypeKatex from 'rehype-katex'; @@ -41,7 +41,7 @@ const Markdown = ({ source, isChatting = false }: { source: string; isChatting?: > {match?.[1]} copyData(code)} alignItems={'center'}> - + 复制代码 diff --git a/src/constants/common.ts b/src/constants/common.ts index 696e32db4..531510e0d 100644 --- a/src/constants/common.ts +++ b/src/constants/common.ts @@ -53,6 +53,7 @@ export const chatProblem = ` export const versionIntro = ` ## Fast GPT V2.0 +* 删除和复制功能:点击对话头像,可以选择复制或删除该条内容。 * 优化记账模式: 不再根据文本长度进行记账,而是根据实际消耗 tokens 数量进行记账。 * 文本 QA 拆分: 可以在[数据]模块,使用 QA 拆分功能,粘贴文字或者选择文件均可以实现自动生成 QA。可以一键导出,用于微调模型。 `; diff --git a/src/pages/api/chat/delLastMessage.ts b/src/pages/api/chat/delChatRecordByIndex.ts similarity index 71% rename from src/pages/api/chat/delLastMessage.ts rename to src/pages/api/chat/delChatRecordByIndex.ts index dcedcab1b..d0596f014 100644 --- a/src/pages/api/chat/delLastMessage.ts +++ b/src/pages/api/chat/delChatRecordByIndex.ts @@ -4,18 +4,20 @@ import { connectToDatabase, Chat } from '@/service/mongo'; export default async function handler(req: NextApiRequest, res: NextApiResponse) { try { - const { chatId } = req.query as { chatId: string }; + const { chatId, index } = req.query as { chatId: string; index: string }; - if (!chatId) { + if (!chatId || !index) { throw new Error('缺少参数'); } - + console.log(index); await connectToDatabase(); // 删除最一条数据库记录, 也就是预发送的那一条 await Chat.findByIdAndUpdate(chatId, { - $pop: { content: 1 }, - updateTime: Date.now() + $set: { + [`content.${index}.deleted`]: true, + updateTime: Date.now() + } }); jsonRes(res); diff --git a/src/pages/api/chat/generate.ts b/src/pages/api/chat/generate.ts index 65aff4186..ca383d514 100644 --- a/src/pages/api/chat/generate.ts +++ b/src/pages/api/chat/generate.ts @@ -42,7 +42,6 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse< modelId, expiredTime: Date.now() + model.security.expiredTime, loadAmount: model.security.maxLoadAmount, - updateTime: Date.now(), isShare: isShare === 'true', content: [] }); diff --git a/src/pages/api/chat/init.ts b/src/pages/api/chat/init.ts index c76b0dea3..7378a111d 100644 --- a/src/pages/api/chat/init.ts +++ b/src/pages/api/chat/init.ts @@ -38,6 +38,9 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) ); } + // filter 掉被 deleted 的内容 + chat.content = chat.content.filter((item) => item.deleted !== true); + const model = chat.modelId; jsonRes(res, { code: 201, diff --git a/src/pages/api/chat/saveChat.ts b/src/pages/api/chat/saveChat.ts index 3b0414011..57c51ed8d 100644 --- a/src/pages/api/chat/saveChat.ts +++ b/src/pages/api/chat/saveChat.ts @@ -27,7 +27,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) })) } }, - updateTime: Date.now() + updateTime: new Date() }); jsonRes(res); diff --git a/src/pages/chat/index.tsx b/src/pages/chat/index.tsx index d4aafc0ae..e5d8098d7 100644 --- a/src/pages/chat/index.tsx +++ b/src/pages/chat/index.tsx @@ -5,7 +5,7 @@ import { getInitChatSiteInfo, getChatSiteId, postGPT3SendPrompt, - delLastMessage, + delChatRecordByIndex, postSaveChat } from '@/api/chat'; import type { InitChatResponse } from '@/api/response/chat'; @@ -19,21 +19,25 @@ import { Drawer, DrawerOverlay, DrawerContent, - useColorModeValue + useColorModeValue, + Menu, + MenuButton, + MenuList, + MenuItem } 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 { useCopyData } from '@/utils/tools'; 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'; +import Icon from '@/components/Icon'; const Markdown = dynamic(() => import('@/components/Markdown')); @@ -46,8 +50,10 @@ interface ChatType extends InitChatResponse { const Chat = ({ chatId }: { chatId: string }) => { const { toast } = useToast(); const router = useRouter(); - const { isPc, media } = useScreen(); - const { setLoading } = useGlobalStore(); + const ChatBox = useRef(null); + const TextareaDom = useRef(null); + // 中断请求 + const controller = useRef(new AbortController()); const [chatData, setChatData] = useState({ chatId: '', modelId: '', @@ -59,45 +65,27 @@ const Chat = ({ chatId }: { chatId: string }) => { 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 + text: '聊天框已过期' }; } return ''; }, [chatData]); + const { copyData } = useCopyData(); + const { isPc, media } = useScreen(); + const { setLoading } = useGlobalStore(); + const { isOpen: isOpenSlider, onClose: onCloseSlider, onOpen: onOpenSlider } = useDisclosure(); 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(() => { @@ -110,42 +98,6 @@ const Chat = ({ chatId }: { chatId: string }) => { }, 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); @@ -346,20 +298,79 @@ const Chat = ({ chatId }: { chatId: string }) => { 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; + // 删除一句话 + const delChatRecord = useCallback( + async (index: number) => { + setLoading(true); + try { + // 删除数据库最后一句 + await delChatRecordByIndex(chatId, index); - resetInputVal(val); + setChatData((state) => ({ + ...state, + history: state.history.filter((_, i) => i !== index) + })); + } catch (err) { + console.log(err); + } + setLoading(false); + }, + [chatId, setLoading] + ); - setChatData((state) => ({ - ...state, - history: state.history.slice(0, -1) - })); - }, [chatData.history, chatId, resetInputVal]); + // 复制内容 + const onclickCopy = useCallback( + (chatId: string) => { + const dom = document.getElementById(chatId); + const innerText = dom?.innerText; + innerText && copyData(innerText); + }, + [copyData] + ); + + useEffect(() => { + controller.current = new AbortController(); + return () => { + // eslint-disable-next-line react-hooks/exhaustive-deps + controller.current?.abort(); + }; + }, [chatId]); + + // 初始化聊天框 + 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); + } + } + ); return ( { px={7} > - { borderBottom={'1px solid rgba(0,0,0,0.1)'} > - - /icon/logo.png - - + + + /icon/logo.png + + + onclickCopy(`chat${index}`)}>复制 + delChatRecord(index)}>删除该行 + + + {item.obj === 'AI' ? ( { {chatWindowError.text} {getToken() && } - - {chatWindowError.canDelete && ( - - )} ) : ( @@ -530,10 +541,10 @@ const Chat = ({ chatId }: { chatId: string }) => { ) : ( )} diff --git a/src/service/errorCode.ts b/src/service/errorCode.ts index ed9ff78ae..81ac1dc58 100644 --- a/src/service/errorCode.ts +++ b/src/service/errorCode.ts @@ -1,9 +1,10 @@ export const openaiError: Record = { context_length_exceeded: '内容超长了,请重置对话', Unauthorized: 'API-KEY 不合法', - rate_limit_reached: '同时访问用户过多,请稍后再试', + rate_limit_reached: 'API被限制,请稍后再试', 'Bad Request': 'Bad Request~ 可能内容太多了', - 'Too Many Requests': '请求次数太多了,请慢点~' + 'Too Many Requests': '请求次数太多了,请慢点~', + 'Bad Gateway': '网关异常,请重试' }; export const proxyError: Record = { ECONNABORTED: true, diff --git a/src/service/models/chat.ts b/src/service/models/chat.ts index 8b8182eb9..513d15087 100644 --- a/src/service/models/chat.ts +++ b/src/service/models/chat.ts @@ -23,8 +23,8 @@ const ChatSchema = new Schema({ required: true }, updateTime: { - type: Number, - required: true + type: Date, + default: () => new Date() }, isShare: { type: Boolean, @@ -41,6 +41,10 @@ const ChatSchema = new Schema({ value: { type: String, required: true + }, + deleted: { + type: Boolean, + default: false } } ], diff --git a/src/service/models/model.ts b/src/service/models/model.ts index 52e0ecec8..5f81bf95c 100644 --- a/src/service/models/model.ts +++ b/src/service/models/model.ts @@ -52,7 +52,7 @@ const ModelSchema = new Schema({ trainId: { // 训练时需要的 ID type: String, - required: true + required: false }, chatModel: { // 聊天时使用的模型 diff --git a/src/service/response.ts b/src/service/response.ts index 850268c0d..edaab5af4 100644 --- a/src/service/response.ts +++ b/src/service/response.ts @@ -28,11 +28,11 @@ export const jsonRes = ( } else if (openaiError[error?.response?.statusText]) { msg = openaiError[error.response.statusText]; } + error?.response && console.log('chat err:', error?.response); console.log('error->'); console.log('code:', error.code); console.log('statusText:', error?.response?.statusText); console.log('msg:', msg); - error?.response && console.log('chat err:', error?.response); } res.json({ diff --git a/src/service/utils/chat.ts b/src/service/utils/chat.ts index e708e14ab..cb7e52348 100644 --- a/src/service/utils/chat.ts +++ b/src/service/utils/chat.ts @@ -51,6 +51,9 @@ export const authChat = async (chatId: string, authorization?: string) => { return Promise.reject('该账号余额不足'); } + // filter 掉被 deleted 的内容 + chat.content = chat.content.filter((item) => item.deleted !== true); + return { userApiKey, systemKey: process.env.OPENAIKEY as string, diff --git a/src/types/chat.d.ts b/src/types/chat.d.ts index 87b281127..b8b7dbf06 100644 --- a/src/types/chat.d.ts +++ b/src/types/chat.d.ts @@ -1,6 +1,7 @@ export type ChatItemType = { obj: 'Human' | 'AI' | 'SYSTEM'; value: string; + deleted?: boolean; }; export type ChatSiteItemType = { diff --git a/src/types/mongoSchema.d.ts b/src/types/mongoSchema.d.ts index 731b53b5e..8bc7e836b 100644 --- a/src/types/mongoSchema.d.ts +++ b/src/types/mongoSchema.d.ts @@ -68,7 +68,7 @@ export interface ChatSchema { modelId: string; expiredTime: number; loadAmount: number; - updateTime: number; + updateTime: Date; isShare: boolean; content: ChatItemType[]; }