diff --git a/src/api/chat.ts b/src/api/chat.ts index 2c61ea76a..72355d815 100644 --- a/src/api/chat.ts +++ b/src/api/chat.ts @@ -45,6 +45,15 @@ export const updateHistoryQuote = (params: { export const delChatRecordByIndex = (chatId: string, contentId: string) => DELETE(`/chat/delChatRecordByContentId?chatId=${chatId}&contentId=${contentId}`); +/** + * 修改历史记录标题 + */ +export const updateChatHistoryTitle = (data: { + chatId: string; + modelId: string; + newTitle: string; +}) => POST('/chat/updateChatHistoryTitle', data); + /** * create a shareChat */ diff --git a/src/pages/api/chat/saveChat.ts b/src/pages/api/chat/saveChat.ts index 5f397b6f3..03eba4d94 100644 --- a/src/pages/api/chat/saveChat.ts +++ b/src/pages/api/chat/saveChat.ts @@ -78,13 +78,14 @@ export async function saveChat({ return _id; } else { // 已经有记录,追加入库 + const chat = await Chat.findById(chatId); await Chat.findByIdAndUpdate(chatId, { $push: { content: { $each: content } }, - title: content[0].value.slice(0, 20), + ...(chat && !chat.customTitle ? { title: content[0].value.slice(0, 20) } : {}), latestChat: content[1].value, updateTime: new Date() }); diff --git a/src/pages/api/chat/updateChatHistoryTitle.ts b/src/pages/api/chat/updateChatHistoryTitle.ts new file mode 100644 index 000000000..c9d1dd9be --- /dev/null +++ b/src/pages/api/chat/updateChatHistoryTitle.ts @@ -0,0 +1,36 @@ +import type { NextApiRequest, NextApiResponse } from 'next'; +import { jsonRes } from '@/service/response'; +import { connectToDatabase, Chat } from '@/service/mongo'; +import { authModel } from '@/service/utils/auth'; +import { authUser } from '@/service/utils/auth'; + +/* 更新聊天标题 */ +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + try { + const { chatId, modelId, newTitle } = req.body as { + chatId: '' | string; + modelId: '' | string; + newTitle: string; + }; + + const { userId } = await authUser({ req, authToken: true }); + + await connectToDatabase(); + + await authModel({ modelId, userId, authOwner: false }); + + await Chat.findByIdAndUpdate( + chatId, + { + title: newTitle, + customTitle: true + } // 自定义标题} + ); + jsonRes(res); + } catch (err) { + jsonRes(res, { + code: 500, + error: err + }); + } +} diff --git a/src/pages/chat/components/History.tsx b/src/pages/chat/components/History.tsx index 9e81f1c11..f9ca57df5 100644 --- a/src/pages/chat/components/History.tsx +++ b/src/pages/chat/components/History.tsx @@ -1,7 +1,8 @@ -import React, { useCallback, useRef, useState, useMemo } from 'react'; +import React, { useCallback, useRef, useState, useMemo, useEffect } from 'react'; import type { MouseEvent } from 'react'; -import { AddIcon } from '@chakra-ui/icons'; +import { AddIcon, EditIcon, CheckIcon, CloseIcon, DeleteIcon } from '@chakra-ui/icons'; import { + Input, Box, Button, Flex, @@ -16,15 +17,65 @@ import { useQuery } from '@tanstack/react-query'; import { useRouter } from 'next/router'; import { useLoading } from '@/hooks/useLoading'; import { useUserStore } from '@/store/user'; -import { formatTimeToChatTime } from '@/utils/tools'; import MyIcon from '@/components/Icon'; import type { HistoryItemType, ExportChatType } from '@/types/chat'; import { useChatStore } from '@/store/chat'; +import { updateChatHistoryTitle } from '@/api/chat'; import ModelList from './ModelList'; import { useGlobalStore } from '@/store/global'; - import styles from '../index.module.scss'; +type UseEditTitleReturnType = { + editingHistoryId: string | null; + setEditingHistoryId: React.Dispatch>; + editedTitle: string; + setEditedTitle: React.Dispatch>; + inputRef: React.RefObject; + onEditClick: (id: string, title: string) => void; + onSaveClick: (chatId: string, modelId: string, editedTitle: string) => Promise; + onCloseClick: () => void; +}; + +const useEditTitle = (): UseEditTitleReturnType => { + const [editingHistoryId, setEditingHistoryId] = useState(null); + const [editedTitle, setEditedTitle] = useState(''); + + const inputRef = useRef(null); + + const onEditClick = (id: string, title: string) => { + setEditingHistoryId(id); + setEditedTitle(title); + inputRef.current && inputRef.current.focus(); + }; + + const onSaveClick = async (chatId: string, modelId: string, editedTitle: string) => { + setEditingHistoryId(null); + + await updateChatHistoryTitle({ chatId: chatId, modelId: modelId, newTitle: editedTitle }); + }; + + const onCloseClick = () => { + setEditingHistoryId(null); + }; + + useEffect(() => { + if (editingHistoryId) { + inputRef.current && inputRef.current.focus(); + } + }, [editingHistoryId]); + + return { + editingHistoryId, + setEditingHistoryId, + editedTitle, + setEditedTitle, + inputRef, + onEditClick, + onSaveClick, + onCloseClick + }; +}; + const PcSliderBar = ({ onclickDelHistory, onclickExportChat @@ -32,6 +83,17 @@ const PcSliderBar = ({ onclickDelHistory: (historyId: string) => Promise; onclickExportChat: (type: ExportChatType) => void; }) => { + // 使用自定义的useEditTitle hook来管理聊天标题的编辑状态 + const { + editingHistoryId, + editedTitle, + setEditedTitle, + inputRef, + onEditClick, + onSaveClick, + onCloseClick + } = useEditTitle(); + const router = useRouter(); const { modelId = '', chatId = '' } = router.query as { modelId: string; chatId: string }; const theme = useTheme(); @@ -170,12 +232,46 @@ const PcSliderBar = ({ - - {item.title} - - - {formatTimeToChatTime(item.updateTime)} - + {editingHistoryId === item._id ? ( + } }) => + setEditedTitle(e.target.value) + } + onBlur={onCloseClick} + height={'1.5em'} + paddingLeft={'0.5'} + style={{ width: '65%' }} // 设置输入框宽度为父元素宽度的一半 + /> + ) : ( + + {item.title} + + )} + + {/* 编辑状态下显示确认和取消按钮 */} + {editingHistoryId === item._id ? ( + <> + + { + event.preventDefault(); + setIsLoading(true); + try { + await onSaveClick(item._id, item.modelId, editedTitle); + await loadHistory({ pageNum: 1, init: true }); + } catch (error) { + console.log(error); + } + setIsLoading(false); + }} + _hover={{ color: 'blue.500' }} + paddingLeft={'1'} + /> + + + ) : null} {item.latestChat || '……'} @@ -232,6 +328,17 @@ const PcSliderBar = ({ > 删除记录 + { + try { + onEditClick(contextMenuData.history._id, contextMenuData.history.title); + } catch (error) { + console.log(error); + } + }} + > + 编辑标题 + onclickExportChat('html')}>导出HTML格式 onclickExportChat('pdf')}>导出PDF格式 onclickExportChat('md')}>导出Markdown格式 diff --git a/src/service/models/chat.ts b/src/service/models/chat.ts index a2474f0e5..9a6f030ad 100644 --- a/src/service/models/chat.ts +++ b/src/service/models/chat.ts @@ -31,6 +31,10 @@ const ChatSchema = new Schema({ type: String, default: '历史记录' }, + customTitle: { + type: Boolean, + default: false + }, latestChat: { type: String, default: '' diff --git a/src/types/mongoSchema.d.ts b/src/types/mongoSchema.d.ts index 8bb144368..74c266a21 100644 --- a/src/types/mongoSchema.d.ts +++ b/src/types/mongoSchema.d.ts @@ -88,6 +88,7 @@ export interface ChatSchema { loadAmount: number; updateTime: Date; content: ChatItemType[]; + customTitle: Boolean; } export interface ChatPopulate extends ChatSchema { userId: UserModelSchema;