feat: 复制和删除对话功能

This commit is contained in:
archer
2023-03-26 13:14:50 +08:00
parent 936e36205e
commit 41b6401c13
18 changed files with 146 additions and 116 deletions

View File

@@ -39,6 +39,7 @@ export const postSaveChat = (data: { chatId: string; prompts: ChatItemType[] })
POST('/chat/saveChat', data); 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}`);

View File

@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1679805359001" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1328" xmlns:xlink="http://www.w3.org/1999/xlink" width="48" height="48"><path d="M416.583186 1022.194004c-5.417989 0-10.835979-1.203998-16.253968-3.611993-15.049971-6.621987-24.681952-21.069959-24.681952-37.323927l0-299.795414c0-12.641975 5.417989-24.079953 15.651969-31.905938 9.631981-7.825985 22.273956-10.23398 34.915932-7.825985l417.787184 99.931805 84.279835-599.590829c1.203998-9.631981-8.427984-16.253968-16.855967-11.437978L147.489712 573.102881l139.061728 35.517931c19.865961 4.815991 34.313933 22.875955 32.507937 43.343915-2.407995 25.885949-26.487948 42.139918-50.567901 36.119929L30.70194 627.282775c-16.253968-4.213992-27.691946-17.457966-30.099941-33.711934-2.407995-16.253968 5.417989-32.507937 19.865961-40.93592L962.59612 6.621987c13.243974-7.825985 30.099941-7.223986 43.343915 1.203998 12.641975 8.427984 19.865961 24.079953 17.457966 39.129924l-105.349794 750.090535c-1.805996 11.437978-7.825985 21.671958-17.457966 28.293945-9.631981 6.621987-21.069959 8.427984-32.507937 6.019988l-411.165197-98.125808 0 154.111699 81.87184-76.453851c15.049971-13.845973 37.925926-16.855967 54.179894-4.213992 20.46796 15.651969 21.069959 45.149912 3.009994 62.005879L444.275132 1011.358025C436.449148 1018.582011 426.817166 1022.194004 416.583186 1022.194004L416.583186 1022.194004z" p-id="1329"></path><path d="M416.583186 722.398589c-9.631981 0-19.263962-3.611993-27.089947-10.23398-16.855967-15.049971-18.059965-40.93592-3.009994-57.791887l216.117578-242.003527c15.049971-16.855967 40.93592-18.059965 57.791887-3.009994 16.855967 15.049971 18.059965 40.93592 3.009994 57.791887l-216.117578 242.003527C438.857143 718.184597 427.419165 722.398589 416.583186 722.398589L416.583186 722.398589z" p-id="1330"></path></svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1679805221456" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1173" xmlns:xlink="http://www.w3.org/1999/xlink" width="48" height="48"><path d="M267.3 834.6h-96.5c-27.4 0-49.7-22.3-49.7-49.7V115.2c0-27.4 22.3-49.7 49.7-49.7H727c27.4 0 49.7 22.3 49.7 49.7v96.5h-42.6v-96.5c0-3.9-3.2-7.1-7.1-7.1H170.8c-3.9 0-7.1 3.2-7.1 7.1v669.7c0 3.9 3.2 7.1 7.1 7.1h96.5v42.6z" p-id="1174"></path><path d="M851.9 959.5H295.7c-27.4 0-49.7-22.3-49.7-49.7V240.1c0-27.4 22.3-49.7 49.7-49.7h556.2c27.4 0 49.7 22.3 49.7 49.7v669.7c-0.1 27.4-22.3 49.7-49.7 49.7zM295.7 233c-3.9 0-7.1 3.2-7.1 7.1v669.7c0 3.9 3.2 7.1 7.1 7.1h556.2c3.9 0 7.1-3.2 7.1-7.1V240.1c0-3.9-3.2-7.1-7.1-7.1H295.7z" p-id="1175"></path></svg>

After

Width:  |  Height:  |  Size: 878 B

View File

@@ -8,7 +8,9 @@ const map = {
share: require('./icons/share.svg').default, share: require('./icons/share.svg').default,
home: require('./icons/home.svg').default, home: require('./icons/home.svg').default,
menu: require('./icons/menu.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; export type IconName = keyof typeof map;

View File

@@ -3,7 +3,7 @@ import ReactMarkdown from 'react-markdown';
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'; import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
import { Box, Flex, useColorModeValue } from '@chakra-ui/react'; import { Box, Flex, useColorModeValue } from '@chakra-ui/react';
import { useCopyData } from '@/utils/tools'; import { useCopyData } from '@/utils/tools';
import Icon from '@/components/Iconfont'; import Icon from '@/components/Icon';
import remarkGfm from 'remark-gfm'; import remarkGfm from 'remark-gfm';
import remarkMath from 'remark-math'; import remarkMath from 'remark-math';
import rehypeKatex from 'rehype-katex'; import rehypeKatex from 'rehype-katex';
@@ -41,7 +41,7 @@ const Markdown = ({ source, isChatting = false }: { source: string; isChatting?:
> >
<Box flex={1}>{match?.[1]}</Box> <Box flex={1}>{match?.[1]}</Box>
<Flex cursor={'pointer'} onClick={() => copyData(code)} alignItems={'center'}> <Flex cursor={'pointer'} onClick={() => copyData(code)} alignItems={'center'}>
<Icon name={'icon-fuzhi'} width={15} height={15} color={'#fff'}></Icon> <Icon name={'copy'} width={15} height={15} fill={'#fff'}></Icon>
<Box ml={1}></Box> <Box ml={1}></Box>
</Flex> </Flex>
</Flex> </Flex>

View File

@@ -53,6 +53,7 @@ export const chatProblem = `
export const versionIntro = ` export const versionIntro = `
## Fast GPT V2.0 ## Fast GPT V2.0
* 删除和复制功能:点击对话头像,可以选择复制或删除该条内容。
* 优化记账模式: 不再根据文本长度进行记账,而是根据实际消耗 tokens 数量进行记账。 * 优化记账模式: 不再根据文本长度进行记账,而是根据实际消耗 tokens 数量进行记账。
* 文本 QA 拆分: 可以在[数据]模块,使用 QA 拆分功能,粘贴文字或者选择文件均可以实现自动生成 QA。可以一键导出用于微调模型。 * 文本 QA 拆分: 可以在[数据]模块,使用 QA 拆分功能,粘贴文字或者选择文件均可以实现自动生成 QA。可以一键导出用于微调模型。
`; `;

View File

@@ -4,18 +4,20 @@ import { connectToDatabase, Chat } from '@/service/mongo';
export default async function handler(req: NextApiRequest, res: NextApiResponse) { export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try { 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('缺少参数'); throw new Error('缺少参数');
} }
console.log(index);
await connectToDatabase(); await connectToDatabase();
// 删除最一条数据库记录, 也就是预发送的那一条 // 删除最一条数据库记录, 也就是预发送的那一条
await Chat.findByIdAndUpdate(chatId, { await Chat.findByIdAndUpdate(chatId, {
$pop: { content: 1 }, $set: {
[`content.${index}.deleted`]: true,
updateTime: Date.now() updateTime: Date.now()
}
}); });
jsonRes(res); jsonRes(res);

View File

@@ -42,7 +42,6 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
modelId, modelId,
expiredTime: Date.now() + model.security.expiredTime, expiredTime: Date.now() + model.security.expiredTime,
loadAmount: model.security.maxLoadAmount, loadAmount: model.security.maxLoadAmount,
updateTime: Date.now(),
isShare: isShare === 'true', isShare: isShare === 'true',
content: [] content: []
}); });

View File

@@ -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; const model = chat.modelId;
jsonRes<InitChatResponse>(res, { jsonRes<InitChatResponse>(res, {
code: 201, code: 201,

View File

@@ -27,7 +27,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
})) }))
} }
}, },
updateTime: Date.now() updateTime: new Date()
}); });
jsonRes(res); jsonRes(res);

View File

@@ -5,7 +5,7 @@ import {
getInitChatSiteInfo, getInitChatSiteInfo,
getChatSiteId, getChatSiteId,
postGPT3SendPrompt, postGPT3SendPrompt,
delLastMessage, delChatRecordByIndex,
postSaveChat postSaveChat
} from '@/api/chat'; } from '@/api/chat';
import type { InitChatResponse } from '@/api/response/chat'; import type { InitChatResponse } from '@/api/response/chat';
@@ -19,21 +19,25 @@ import {
Drawer, Drawer,
DrawerOverlay, DrawerOverlay,
DrawerContent, DrawerContent,
useColorModeValue useColorModeValue,
Menu,
MenuButton,
MenuList,
MenuItem
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import { useToast } from '@/hooks/useToast'; import { useToast } from '@/hooks/useToast';
import Icon from '@/components/Iconfont';
import { useScreen } from '@/hooks/useScreen'; import { useScreen } from '@/hooks/useScreen';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { ChatModelNameEnum } from '@/constants/model'; import { ChatModelNameEnum } from '@/constants/model';
import dynamic from 'next/dynamic'; import dynamic from 'next/dynamic';
import { useGlobalStore } from '@/store/global'; import { useGlobalStore } from '@/store/global';
import { useChatStore } from '@/store/chat'; import { useChatStore } from '@/store/chat';
import { useCopyData } from '@/utils/tools';
import { streamFetch } from '@/api/fetch'; import { streamFetch } from '@/api/fetch';
import SlideBar from './components/SlideBar'; import SlideBar from './components/SlideBar';
import Empty from './components/Empty'; import Empty from './components/Empty';
import { getToken } from '@/utils/user'; import { getToken } from '@/utils/user';
import MyIcon from '@/components/Icon'; import Icon from '@/components/Icon';
const Markdown = dynamic(() => import('@/components/Markdown')); const Markdown = dynamic(() => import('@/components/Markdown'));
@@ -46,8 +50,10 @@ interface ChatType extends InitChatResponse {
const Chat = ({ chatId }: { chatId: string }) => { const Chat = ({ chatId }: { chatId: string }) => {
const { toast } = useToast(); const { toast } = useToast();
const router = useRouter(); const router = useRouter();
const { isPc, media } = useScreen(); const ChatBox = useRef<HTMLDivElement>(null);
const { setLoading } = useGlobalStore(); const TextareaDom = useRef<HTMLTextAreaElement>(null);
// 中断请求
const controller = useRef(new AbortController());
const [chatData, setChatData] = useState<ChatType>({ const [chatData, setChatData] = useState<ChatType>({
chatId: '', chatId: '',
modelId: '', modelId: '',
@@ -59,45 +65,27 @@ const Chat = ({ chatId }: { chatId: string }) => {
history: [], history: [],
isExpiredTime: false isExpiredTime: false
}); // 聊天框整体数据 }); // 聊天框整体数据
const ChatBox = useRef<HTMLDivElement>(null);
const TextareaDom = useRef<HTMLTextAreaElement>(null);
const [inputVal, setInputVal] = useState(''); // 输入的内容 const [inputVal, setInputVal] = useState(''); // 输入的内容
const { isOpen: isOpenSlider, onClose: onCloseSlider, onOpen: onOpenSlider } = useDisclosure();
const isChatting = useMemo( const isChatting = useMemo(
() => chatData.history[chatData.history.length - 1]?.status === 'loading', () => chatData.history[chatData.history.length - 1]?.status === 'loading',
[chatData.history] [chatData.history]
); );
const chatWindowError = useMemo(() => { const chatWindowError = useMemo(() => {
if (chatData.history[chatData.history.length - 1]?.obj === 'Human') {
return {
text: '内容出现异常',
canDelete: true
};
}
if (chatData.isExpiredTime) { if (chatData.isExpiredTime) {
return { return {
text: '聊天框已过期', text: '聊天框已过期'
canDelete: false
}; };
} }
return ''; return '';
}, [chatData]); }, [chatData]);
const { copyData } = useCopyData();
const { isPc, media } = useScreen();
const { setLoading } = useGlobalStore();
const { isOpen: isOpenSlider, onClose: onCloseSlider, onOpen: onOpenSlider } = useDisclosure();
const { pushChatHistory } = useChatStore(); 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(() => { const scrollToBottom = useCallback(() => {
@@ -110,42 +98,6 @@ const Chat = ({ chatId }: { chatId: string }) => {
}, 100); }, 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) => { const resetInputVal = useCallback((val: string) => {
setInputVal(val); setInputVal(val);
@@ -346,20 +298,79 @@ const Chat = ({ chatId }: { chatId: string }) => {
toast toast
]); ]);
// 重新编辑 // 删除一句话
const reEdit = useCallback(async () => { const delChatRecord = useCallback(
if (chatData.history[chatData.history.length - 1]?.obj !== 'Human') return; async (index: number) => {
setLoading(true);
try {
// 删除数据库最后一句 // 删除数据库最后一句
await delLastMessage(chatId); await delChatRecordByIndex(chatId, index);
const val = chatData.history[chatData.history.length - 1].value;
resetInputVal(val);
setChatData((state) => ({ setChatData((state) => ({
...state, ...state,
history: state.history.slice(0, -1) history: state.history.filter((_, i) => i !== index)
})); }));
}, [chatData.history, chatId, resetInputVal]); } catch (err) {
console.log(err);
}
setLoading(false);
},
[chatId, setLoading]
);
// 复制内容
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 ( return (
<Flex <Flex
@@ -389,7 +400,7 @@ const Chat = ({ chatId }: { chatId: string }) => {
px={7} px={7}
> >
<Box onClick={onOpenSlider}> <Box onClick={onOpenSlider}>
<MyIcon <Icon
name={'menu'} name={'menu'}
w={'20px'} w={'20px'}
h={'20px'} h={'20px'}
@@ -432,15 +443,21 @@ const Chat = ({ chatId }: { chatId: string }) => {
borderBottom={'1px solid rgba(0,0,0,0.1)'} borderBottom={'1px solid rgba(0,0,0,0.1)'}
> >
<Flex maxW={'750px'} m={'auto'} alignItems={'flex-start'}> <Flex maxW={'750px'} m={'auto'} alignItems={'flex-start'}>
<Box mr={media(4, 1)}> <Menu>
<MenuButton as={Box} mr={media(4, 1)} cursor={'pointer'}>
<Image <Image
src={item.obj === 'Human' ? '/icon/human.png' : '/icon/logo.png'} src={item.obj === 'Human' ? '/icon/human.png' : '/icon/logo.png'}
alt="/icon/logo.png" alt="/icon/logo.png"
width={media(30, 20)} width={media(30, 20)}
height={media(30, 20)} height={media(30, 20)}
/> />
</Box> </MenuButton>
<Box flex={'1 0 0'} w={0} overflow={'hidden'}> <MenuList fontSize={'sm'}>
<MenuItem onClick={() => onclickCopy(`chat${index}`)}></MenuItem>
<MenuItem onClick={() => delChatRecord(index)}></MenuItem>
</MenuList>
</Menu>
<Box flex={'1 0 0'} w={0} overflow={'hidden'} id={`chat${index}`}>
{item.obj === 'AI' ? ( {item.obj === 'AI' ? (
<Markdown <Markdown
source={item.value} source={item.value}
@@ -462,12 +479,6 @@ const Chat = ({ chatId }: { chatId: string }) => {
<Box color={'red'}>{chatWindowError.text}</Box> <Box color={'red'}>{chatWindowError.text}</Box>
<Flex py={5} justifyContent={'center'}> <Flex py={5} justifyContent={'center'}>
{getToken() && <Button onClick={resetChat}></Button>} {getToken() && <Button onClick={resetChat}></Button>}
{chatWindowError.canDelete && (
<Button ml={20} colorScheme={'green'} onClick={reEdit}>
</Button>
)}
</Flex> </Flex>
</Box> </Box>
) : ( ) : (
@@ -530,10 +541,10 @@ const Chat = ({ chatId }: { chatId: string }) => {
) : ( ) : (
<Box cursor={'pointer'} onClick={sendPrompt}> <Box cursor={'pointer'} onClick={sendPrompt}>
<Icon <Icon
name={'icon-fasong'} name={'chatSend'}
width={20} width={'20px'}
height={20} height={'20px'}
color={useColorModeValue('#718096', 'white')} fill={useColorModeValue('#718096', 'white')}
></Icon> ></Icon>
</Box> </Box>
)} )}

View File

@@ -1,9 +1,10 @@
export const openaiError: Record<string, string> = { export const openaiError: Record<string, string> = {
context_length_exceeded: '内容超长了,请重置对话', context_length_exceeded: '内容超长了,请重置对话',
Unauthorized: 'API-KEY 不合法', Unauthorized: 'API-KEY 不合法',
rate_limit_reached: '同时访问用户过多,请稍后再试', rate_limit_reached: 'API被限制,请稍后再试',
'Bad Request': 'Bad Request~ 可能内容太多了', 'Bad Request': 'Bad Request~ 可能内容太多了',
'Too Many Requests': '请求次数太多了,请慢点~' 'Too Many Requests': '请求次数太多了,请慢点~',
'Bad Gateway': '网关异常,请重试'
}; };
export const proxyError: Record<string, boolean> = { export const proxyError: Record<string, boolean> = {
ECONNABORTED: true, ECONNABORTED: true,

View File

@@ -23,8 +23,8 @@ const ChatSchema = new Schema({
required: true required: true
}, },
updateTime: { updateTime: {
type: Number, type: Date,
required: true default: () => new Date()
}, },
isShare: { isShare: {
type: Boolean, type: Boolean,
@@ -41,6 +41,10 @@ const ChatSchema = new Schema({
value: { value: {
type: String, type: String,
required: true required: true
},
deleted: {
type: Boolean,
default: false
} }
} }
], ],

View File

@@ -52,7 +52,7 @@ const ModelSchema = new Schema({
trainId: { trainId: {
// 训练时需要的 ID // 训练时需要的 ID
type: String, type: String,
required: true required: false
}, },
chatModel: { chatModel: {
// 聊天时使用的模型 // 聊天时使用的模型

View File

@@ -28,11 +28,11 @@ export const jsonRes = <T = any>(
} else if (openaiError[error?.response?.statusText]) { } else if (openaiError[error?.response?.statusText]) {
msg = openaiError[error.response.statusText]; msg = openaiError[error.response.statusText];
} }
error?.response && console.log('chat err:', error?.response);
console.log('error->'); console.log('error->');
console.log('code:', error.code); console.log('code:', error.code);
console.log('statusText:', error?.response?.statusText); console.log('statusText:', error?.response?.statusText);
console.log('msg:', msg); console.log('msg:', msg);
error?.response && console.log('chat err:', error?.response);
} }
res.json({ res.json({

View File

@@ -51,6 +51,9 @@ export const authChat = async (chatId: string, authorization?: string) => {
return Promise.reject('该账号余额不足'); return Promise.reject('该账号余额不足');
} }
// filter 掉被 deleted 的内容
chat.content = chat.content.filter((item) => item.deleted !== true);
return { return {
userApiKey, userApiKey,
systemKey: process.env.OPENAIKEY as string, systemKey: process.env.OPENAIKEY as string,

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

@@ -1,6 +1,7 @@
export type ChatItemType = { export type ChatItemType = {
obj: 'Human' | 'AI' | 'SYSTEM'; obj: 'Human' | 'AI' | 'SYSTEM';
value: string; value: string;
deleted?: boolean;
}; };
export type ChatSiteItemType = { export type ChatSiteItemType = {

View File

@@ -68,7 +68,7 @@ export interface ChatSchema {
modelId: string; modelId: string;
expiredTime: number; expiredTime: number;
loadAmount: number; loadAmount: number;
updateTime: number; updateTime: Date;
isShare: boolean; isShare: boolean;
content: ChatItemType[]; content: ChatItemType[];
} }