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)'}
>
-
-
-
-
+
+
{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[];
}