mirror of
https://github.com/labring/FastGPT.git
synced 2025-07-23 13:03:50 +00:00
feat:自定义历史聊天标题 (#41)
* feat:自定义历史聊天标题 * Update chat.ts * perf:自定义聊天标题 * feat: google auth * perf:将修改标题移入右键菜单 * perf:updatetitle --------- Co-authored-by: archer <545436317@qq.com>
This commit is contained in:
@@ -45,6 +45,15 @@ export const updateHistoryQuote = (params: {
|
|||||||
export const delChatRecordByIndex = (chatId: string, contentId: string) =>
|
export const delChatRecordByIndex = (chatId: string, contentId: string) =>
|
||||||
DELETE(`/chat/delChatRecordByContentId?chatId=${chatId}&contentId=${contentId}`);
|
DELETE(`/chat/delChatRecordByContentId?chatId=${chatId}&contentId=${contentId}`);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 修改历史记录标题
|
||||||
|
*/
|
||||||
|
export const updateChatHistoryTitle = (data: {
|
||||||
|
chatId: string;
|
||||||
|
modelId: string;
|
||||||
|
newTitle: string;
|
||||||
|
}) => POST<string>('/chat/updateChatHistoryTitle', data);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* create a shareChat
|
* create a shareChat
|
||||||
*/
|
*/
|
||||||
|
@@ -78,13 +78,14 @@ export async function saveChat({
|
|||||||
return _id;
|
return _id;
|
||||||
} else {
|
} else {
|
||||||
// 已经有记录,追加入库
|
// 已经有记录,追加入库
|
||||||
|
const chat = await Chat.findById(chatId);
|
||||||
await Chat.findByIdAndUpdate(chatId, {
|
await Chat.findByIdAndUpdate(chatId, {
|
||||||
$push: {
|
$push: {
|
||||||
content: {
|
content: {
|
||||||
$each: content
|
$each: content
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
title: content[0].value.slice(0, 20),
|
...(chat && !chat.customTitle ? { title: content[0].value.slice(0, 20) } : {}),
|
||||||
latestChat: content[1].value,
|
latestChat: content[1].value,
|
||||||
updateTime: new Date()
|
updateTime: new Date()
|
||||||
});
|
});
|
||||||
|
36
src/pages/api/chat/updateChatHistoryTitle.ts
Normal file
36
src/pages/api/chat/updateChatHistoryTitle.ts
Normal file
@@ -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
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@@ -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 type { MouseEvent } from 'react';
|
||||||
import { AddIcon } from '@chakra-ui/icons';
|
import { AddIcon, EditIcon, CheckIcon, CloseIcon, DeleteIcon } from '@chakra-ui/icons';
|
||||||
import {
|
import {
|
||||||
|
Input,
|
||||||
Box,
|
Box,
|
||||||
Button,
|
Button,
|
||||||
Flex,
|
Flex,
|
||||||
@@ -16,15 +17,65 @@ import { useQuery } from '@tanstack/react-query';
|
|||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import { useLoading } from '@/hooks/useLoading';
|
import { useLoading } from '@/hooks/useLoading';
|
||||||
import { useUserStore } from '@/store/user';
|
import { useUserStore } from '@/store/user';
|
||||||
import { formatTimeToChatTime } from '@/utils/tools';
|
|
||||||
import MyIcon from '@/components/Icon';
|
import MyIcon from '@/components/Icon';
|
||||||
import type { HistoryItemType, ExportChatType } from '@/types/chat';
|
import type { HistoryItemType, ExportChatType } from '@/types/chat';
|
||||||
import { useChatStore } from '@/store/chat';
|
import { useChatStore } from '@/store/chat';
|
||||||
|
import { updateChatHistoryTitle } from '@/api/chat';
|
||||||
import ModelList from './ModelList';
|
import ModelList from './ModelList';
|
||||||
import { useGlobalStore } from '@/store/global';
|
import { useGlobalStore } from '@/store/global';
|
||||||
|
|
||||||
import styles from '../index.module.scss';
|
import styles from '../index.module.scss';
|
||||||
|
|
||||||
|
type UseEditTitleReturnType = {
|
||||||
|
editingHistoryId: string | null;
|
||||||
|
setEditingHistoryId: React.Dispatch<React.SetStateAction<string | null>>;
|
||||||
|
editedTitle: string;
|
||||||
|
setEditedTitle: React.Dispatch<React.SetStateAction<string>>;
|
||||||
|
inputRef: React.RefObject<HTMLInputElement>;
|
||||||
|
onEditClick: (id: string, title: string) => void;
|
||||||
|
onSaveClick: (chatId: string, modelId: string, editedTitle: string) => Promise<void>;
|
||||||
|
onCloseClick: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const useEditTitle = (): UseEditTitleReturnType => {
|
||||||
|
const [editingHistoryId, setEditingHistoryId] = useState<string | null>(null);
|
||||||
|
const [editedTitle, setEditedTitle] = useState<string>('');
|
||||||
|
|
||||||
|
const inputRef = useRef<HTMLInputElement | null>(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 = ({
|
const PcSliderBar = ({
|
||||||
onclickDelHistory,
|
onclickDelHistory,
|
||||||
onclickExportChat
|
onclickExportChat
|
||||||
@@ -32,6 +83,17 @@ const PcSliderBar = ({
|
|||||||
onclickDelHistory: (historyId: string) => Promise<void>;
|
onclickDelHistory: (historyId: string) => Promise<void>;
|
||||||
onclickExportChat: (type: ExportChatType) => void;
|
onclickExportChat: (type: ExportChatType) => void;
|
||||||
}) => {
|
}) => {
|
||||||
|
// 使用自定义的useEditTitle hook来管理聊天标题的编辑状态
|
||||||
|
const {
|
||||||
|
editingHistoryId,
|
||||||
|
editedTitle,
|
||||||
|
setEditedTitle,
|
||||||
|
inputRef,
|
||||||
|
onEditClick,
|
||||||
|
onSaveClick,
|
||||||
|
onCloseClick
|
||||||
|
} = useEditTitle();
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { modelId = '', chatId = '' } = router.query as { modelId: string; chatId: string };
|
const { modelId = '', chatId = '' } = router.query as { modelId: string; chatId: string };
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
@@ -170,12 +232,46 @@ const PcSliderBar = ({
|
|||||||
<ChatIcon fontSize={'16px'} color={'myGray.500'} />
|
<ChatIcon fontSize={'16px'} color={'myGray.500'} />
|
||||||
<Box flex={'1 0 0'} w={0} ml={3}>
|
<Box flex={'1 0 0'} w={0} ml={3}>
|
||||||
<Flex alignItems={'center'}>
|
<Flex alignItems={'center'}>
|
||||||
<Box flex={'1 0 0'} w={0} className="textEllipsis" color={'myGray.1000'}>
|
{editingHistoryId === item._id ? (
|
||||||
{item.title}
|
<Input
|
||||||
</Box>
|
ref={inputRef}
|
||||||
<Box color={'myGray.400'} fontSize={'sm'}>
|
value={editedTitle}
|
||||||
{formatTimeToChatTime(item.updateTime)}
|
onChange={(e: { target: { value: React.SetStateAction<string> } }) =>
|
||||||
</Box>
|
setEditedTitle(e.target.value)
|
||||||
|
}
|
||||||
|
onBlur={onCloseClick}
|
||||||
|
height={'1.5em'}
|
||||||
|
paddingLeft={'0.5'}
|
||||||
|
style={{ width: '65%' }} // 设置输入框宽度为父元素宽度的一半
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Box flex={'1 0 0'} w={0} className="textEllipsis" color={'myGray.1000'}>
|
||||||
|
{item.title}
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 编辑状态下显示确认和取消按钮 */}
|
||||||
|
{editingHistoryId === item._id ? (
|
||||||
|
<>
|
||||||
|
<Box mx={1}>
|
||||||
|
<CheckIcon
|
||||||
|
onMouseDown={async (event) => {
|
||||||
|
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'}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
</Flex>
|
</Flex>
|
||||||
<Box className="textEllipsis" mt={1} fontSize={'sm'} color={'myGray.500'}>
|
<Box className="textEllipsis" mt={1} fontSize={'sm'} color={'myGray.500'}>
|
||||||
{item.latestChat || '……'}
|
{item.latestChat || '……'}
|
||||||
@@ -232,6 +328,17 @@ const PcSliderBar = ({
|
|||||||
>
|
>
|
||||||
删除记录
|
删除记录
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
|
<MenuItem
|
||||||
|
onClick={() => {
|
||||||
|
try {
|
||||||
|
onEditClick(contextMenuData.history._id, contextMenuData.history.title);
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
编辑标题
|
||||||
|
</MenuItem>
|
||||||
<MenuItem onClick={() => onclickExportChat('html')}>导出HTML格式</MenuItem>
|
<MenuItem onClick={() => onclickExportChat('html')}>导出HTML格式</MenuItem>
|
||||||
<MenuItem onClick={() => onclickExportChat('pdf')}>导出PDF格式</MenuItem>
|
<MenuItem onClick={() => onclickExportChat('pdf')}>导出PDF格式</MenuItem>
|
||||||
<MenuItem onClick={() => onclickExportChat('md')}>导出Markdown格式</MenuItem>
|
<MenuItem onClick={() => onclickExportChat('md')}>导出Markdown格式</MenuItem>
|
||||||
|
@@ -31,6 +31,10 @@ const ChatSchema = new Schema({
|
|||||||
type: String,
|
type: String,
|
||||||
default: '历史记录'
|
default: '历史记录'
|
||||||
},
|
},
|
||||||
|
customTitle: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
latestChat: {
|
latestChat: {
|
||||||
type: String,
|
type: String,
|
||||||
default: ''
|
default: ''
|
||||||
|
1
src/types/mongoSchema.d.ts
vendored
1
src/types/mongoSchema.d.ts
vendored
@@ -88,6 +88,7 @@ export interface ChatSchema {
|
|||||||
loadAmount: number;
|
loadAmount: number;
|
||||||
updateTime: Date;
|
updateTime: Date;
|
||||||
content: ChatItemType[];
|
content: ChatItemType[];
|
||||||
|
customTitle: Boolean;
|
||||||
}
|
}
|
||||||
export interface ChatPopulate extends ChatSchema {
|
export interface ChatPopulate extends ChatSchema {
|
||||||
userId: UserModelSchema;
|
userId: UserModelSchema;
|
||||||
|
Reference in New Issue
Block a user