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);
/**
* 删除最后一句
* 删除一句对话
*/
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,
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;

View File

@@ -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?:
>
<Box flex={1}>{match?.[1]}</Box>
<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>
</Flex>
</Flex>

View File

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

View File

@@ -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 },
$set: {
[`content.${index}.deleted`]: true,
updateTime: Date.now()
}
});
jsonRes(res);

View File

@@ -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: []
});

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;
jsonRes<InitChatResponse>(res, {
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);

View File

@@ -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<HTMLDivElement>(null);
const TextareaDom = useRef<HTMLTextAreaElement>(null);
// 中断请求
const controller = useRef(new AbortController());
const [chatData, setChatData] = useState<ChatType>({
chatId: '',
modelId: '',
@@ -59,45 +65,27 @@ const Chat = ({ chatId }: { chatId: string }) => {
history: [],
isExpiredTime: false
}); // 聊天框整体数据
const ChatBox = useRef<HTMLDivElement>(null);
const TextareaDom = useRef<HTMLTextAreaElement>(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;
// 删除一句话
const delChatRecord = useCallback(
async (index: number) => {
setLoading(true);
try {
// 删除数据库最后一句
await delLastMessage(chatId);
const val = chatData.history[chatData.history.length - 1].value;
resetInputVal(val);
await delChatRecordByIndex(chatId, index);
setChatData((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 (
<Flex
@@ -389,7 +400,7 @@ const Chat = ({ chatId }: { chatId: string }) => {
px={7}
>
<Box onClick={onOpenSlider}>
<MyIcon
<Icon
name={'menu'}
w={'20px'}
h={'20px'}
@@ -432,15 +443,21 @@ const Chat = ({ chatId }: { chatId: string }) => {
borderBottom={'1px solid rgba(0,0,0,0.1)'}
>
<Flex maxW={'750px'} m={'auto'} alignItems={'flex-start'}>
<Box mr={media(4, 1)}>
<Menu>
<MenuButton as={Box} mr={media(4, 1)} cursor={'pointer'}>
<Image
src={item.obj === 'Human' ? '/icon/human.png' : '/icon/logo.png'}
alt="/icon/logo.png"
width={media(30, 20)}
height={media(30, 20)}
/>
</Box>
<Box flex={'1 0 0'} w={0} overflow={'hidden'}>
</MenuButton>
<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' ? (
<Markdown
source={item.value}
@@ -462,12 +479,6 @@ const Chat = ({ chatId }: { chatId: string }) => {
<Box color={'red'}>{chatWindowError.text}</Box>
<Flex py={5} justifyContent={'center'}>
{getToken() && <Button onClick={resetChat}></Button>}
{chatWindowError.canDelete && (
<Button ml={20} colorScheme={'green'} onClick={reEdit}>
</Button>
)}
</Flex>
</Box>
) : (
@@ -530,10 +541,10 @@ const Chat = ({ chatId }: { chatId: string }) => {
) : (
<Box cursor={'pointer'} onClick={sendPrompt}>
<Icon
name={'icon-fasong'}
width={20}
height={20}
color={useColorModeValue('#718096', 'white')}
name={'chatSend'}
width={'20px'}
height={'20px'}
fill={useColorModeValue('#718096', 'white')}
></Icon>
</Box>
)}

View File

@@ -1,9 +1,10 @@
export const openaiError: Record<string, string> = {
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<string, boolean> = {
ECONNABORTED: true,

View File

@@ -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
}
}
],

View File

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

View File

@@ -28,11 +28,11 @@ export const jsonRes = <T = any>(
} 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({

View File

@@ -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,

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

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

View File

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