mirror of
https://github.com/labring/FastGPT.git
synced 2025-07-21 11:43:56 +00:00
feat: custom title and set history top
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { GET, POST, DELETE } from './request';
|
||||
import { GET, POST, DELETE, PUT } from './request';
|
||||
import type { HistoryItemType } from '@/types/chat';
|
||||
import type { InitChatResponse, InitShareChatResponse } from './response/chat';
|
||||
import { RequestPaging } from '../types/index';
|
||||
@@ -6,6 +6,7 @@ import type { ShareChatSchema } from '@/types/mongoSchema';
|
||||
import type { ShareChatEditType } from '@/types/model';
|
||||
import { Obj2Query } from '@/utils/tools';
|
||||
import { QuoteItemType } from '@/pages/api/openapi/kb/appKbSearch';
|
||||
import type { Props as UpdateHistoryProps } from '@/pages/api/chat/history/updateChatHistory';
|
||||
|
||||
/**
|
||||
* 获取初始化聊天内容
|
||||
@@ -17,7 +18,7 @@ export const getInitChatSiteInfo = (modelId: '' | string, chatId: '' | string) =
|
||||
* 获取历史记录
|
||||
*/
|
||||
export const getChatHistory = (data: RequestPaging) =>
|
||||
POST<HistoryItemType[]>('/chat/getHistory', data);
|
||||
POST<HistoryItemType[]>('/chat/history/getHistory', data);
|
||||
|
||||
/**
|
||||
* 删除一条历史记录
|
||||
@@ -28,7 +29,7 @@ export const delChatHistoryById = (id: string) => GET(`/chat/removeHistory?id=${
|
||||
* get history quotes
|
||||
*/
|
||||
export const getHistoryQuote = (params: { chatId: string; historyId: string }) =>
|
||||
GET<(QuoteItemType & { _id: string })[]>(`/chat/getHistoryQuote`, params);
|
||||
GET<(QuoteItemType & { _id: string })[]>(`/chat/history/getHistoryQuote`, params);
|
||||
|
||||
/**
|
||||
* update history quote status
|
||||
@@ -37,7 +38,7 @@ export const updateHistoryQuote = (params: {
|
||||
chatId: string;
|
||||
historyId: string;
|
||||
quoteId: string;
|
||||
}) => GET(`/chat/updateHistoryQuote`, params);
|
||||
}) => GET(`/chat/history/updateHistoryQuote`, params);
|
||||
|
||||
/**
|
||||
* 删除一句对话
|
||||
@@ -46,13 +47,10 @@ 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<string>('/chat/updateChatHistoryTitle', data);
|
||||
export const putChatHistory = (data: UpdateHistoryProps) =>
|
||||
PUT('/chat/history/updateChatHistory', data);
|
||||
|
||||
/**
|
||||
* create a shareChat
|
||||
|
@@ -2,6 +2,7 @@ import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { jsonRes } from '@/service/response';
|
||||
import { connectToDatabase, Chat } from '@/service/mongo';
|
||||
import { authUser } from '@/service/utils/auth';
|
||||
import type { HistoryItemType } from '@/types/chat';
|
||||
|
||||
/* 获取历史记录 */
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
@@ -14,13 +15,20 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
{
|
||||
userId
|
||||
},
|
||||
'_id title modelId updateTime latestChat'
|
||||
'_id title top customTitle modelId updateTime latestChat'
|
||||
)
|
||||
.sort({ updateTime: -1 })
|
||||
.sort({ top: -1, updateTime: -1 })
|
||||
.limit(20);
|
||||
|
||||
jsonRes(res, {
|
||||
data
|
||||
jsonRes<HistoryItemType[]>(res, {
|
||||
data: data.map((item) => ({
|
||||
_id: item._id,
|
||||
updateTime: item.updateTime,
|
||||
modelId: item.modelId,
|
||||
title: item.customTitle || item.title,
|
||||
latestChat: item.latestChat,
|
||||
top: item.top
|
||||
}))
|
||||
});
|
||||
} catch (err) {
|
||||
jsonRes(res, {
|
@@ -1,30 +1,32 @@
|
||||
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 type Props = {
|
||||
chatId: '' | string;
|
||||
customTitle?: string;
|
||||
top?: boolean;
|
||||
};
|
||||
|
||||
/* 更新聊天标题 */
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
try {
|
||||
const { chatId, modelId, newTitle } = req.body as {
|
||||
chatId: '' | string;
|
||||
modelId: '' | string;
|
||||
newTitle: string;
|
||||
};
|
||||
const { chatId, customTitle, top } = req.body as Props;
|
||||
|
||||
const { userId } = await authUser({ req, authToken: true });
|
||||
|
||||
await connectToDatabase();
|
||||
|
||||
await authModel({ modelId, userId, authOwner: false });
|
||||
|
||||
await Chat.findByIdAndUpdate(
|
||||
chatId,
|
||||
await Chat.findOneAndUpdate(
|
||||
{
|
||||
title: newTitle,
|
||||
customTitle: true
|
||||
} // 自定义标题}
|
||||
_id: chatId,
|
||||
userId
|
||||
},
|
||||
{
|
||||
...(customTitle ? { customTitle } : {}),
|
||||
...(top ? { top } : { top: null })
|
||||
}
|
||||
);
|
||||
jsonRes(res);
|
||||
} catch (err) {
|
@@ -77,15 +77,13 @@ export async function saveChat({
|
||||
});
|
||||
return _id;
|
||||
} else {
|
||||
// 已经有记录,追加入库
|
||||
const chat = await Chat.findById(chatId);
|
||||
await Chat.findByIdAndUpdate(chatId, {
|
||||
$push: {
|
||||
content: {
|
||||
$each: content
|
||||
}
|
||||
},
|
||||
...(chat && !chat.customTitle ? { title: content[0].value.slice(0, 20) } : {}),
|
||||
title: content[0].value.slice(0, 20),
|
||||
latestChat: content[1].value,
|
||||
updateTime: new Date()
|
||||
});
|
||||
|
@@ -1,8 +1,7 @@
|
||||
import React, { useCallback, useRef, useState, useMemo, useEffect } from 'react';
|
||||
import React, { useCallback, useRef, useState, useMemo } from 'react';
|
||||
import type { MouseEvent } from 'react';
|
||||
import { AddIcon, EditIcon, CheckIcon, CloseIcon, DeleteIcon } from '@chakra-ui/icons';
|
||||
import { AddIcon } from '@chakra-ui/icons';
|
||||
import {
|
||||
Input,
|
||||
Box,
|
||||
Button,
|
||||
Flex,
|
||||
@@ -20,61 +19,13 @@ import { useUserStore } from '@/store/user';
|
||||
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<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
|
||||
};
|
||||
};
|
||||
import { useEditTitle } from './useEditTitle';
|
||||
import { putChatHistory } from '@/api/chat';
|
||||
import { useToast } from '@/hooks/useToast';
|
||||
import { formatTimeToChatTime, getErrText } from '@/utils/tools';
|
||||
|
||||
const PcSliderBar = ({
|
||||
onclickDelHistory,
|
||||
@@ -83,24 +34,14 @@ const PcSliderBar = ({
|
||||
onclickDelHistory: (historyId: string) => Promise<void>;
|
||||
onclickExportChat: (type: ExportChatType) => void;
|
||||
}) => {
|
||||
// 使用自定义的useEditTitle hook来管理聊天标题的编辑状态
|
||||
const {
|
||||
editingHistoryId,
|
||||
editedTitle,
|
||||
setEditedTitle,
|
||||
inputRef,
|
||||
onEditClick,
|
||||
onSaveClick,
|
||||
onCloseClick
|
||||
} = useEditTitle();
|
||||
|
||||
const router = useRouter();
|
||||
const { toast } = useToast();
|
||||
const { modelId = '', chatId = '' } = router.query as { modelId: string; chatId: string };
|
||||
const ContextMenuRef = useRef(null);
|
||||
|
||||
const theme = useTheme();
|
||||
const { isPc } = useGlobalStore();
|
||||
|
||||
const ContextMenuRef = useRef(null);
|
||||
|
||||
const { Loading, setIsLoading } = useLoading();
|
||||
const [contextMenuData, setContextMenuData] = useState<{
|
||||
left: number;
|
||||
@@ -114,7 +55,12 @@ const PcSliderBar = ({
|
||||
() => [...myModels, ...myCollectionModels],
|
||||
[myCollectionModels, myModels]
|
||||
);
|
||||
useQuery(['loadModels'], () => loadMyModels(false));
|
||||
|
||||
// custom title edit
|
||||
const { onOpenModal, EditModal: EditTitleModal } = useEditTitle({
|
||||
title: '自定义历史记录标题',
|
||||
placeholder: '如果设置为空,会自动跟随聊天记录。'
|
||||
});
|
||||
|
||||
// close contextMenu
|
||||
useOutsideClick({
|
||||
@@ -122,13 +68,9 @@ const PcSliderBar = ({
|
||||
handler: () =>
|
||||
setTimeout(() => {
|
||||
setContextMenuData(undefined);
|
||||
})
|
||||
}, 10)
|
||||
});
|
||||
|
||||
const { isLoading: isLoadingHistory } = useQuery(['loadingHistory'], () =>
|
||||
loadHistory({ pageNum: 1 })
|
||||
);
|
||||
|
||||
const onclickContextMenu = useCallback(
|
||||
(e: MouseEvent<HTMLDivElement>, history: HistoryItemType) => {
|
||||
e.preventDefault(); // 阻止默认右键菜单
|
||||
@@ -144,6 +86,12 @@ const PcSliderBar = ({
|
||||
[isPc]
|
||||
);
|
||||
|
||||
useQuery(['loadModels'], () => loadMyModels(false));
|
||||
|
||||
const { isLoading: isLoadingHistory } = useQuery(['loadingHistory'], () =>
|
||||
loadHistory({ pageNum: 1 })
|
||||
);
|
||||
|
||||
return (
|
||||
<Flex
|
||||
position={'relative'}
|
||||
@@ -211,14 +159,16 @@ const PcSliderBar = ({
|
||||
borderLeft={['none', '5px solid transparent']}
|
||||
userSelect={'none'}
|
||||
_hover={{
|
||||
backgroundColor: ['', '#dee0e3']
|
||||
bg: ['', '#dee0e3']
|
||||
}}
|
||||
{...(item._id === chatId
|
||||
? {
|
||||
backgroundColor: '#eff0f1',
|
||||
bg: 'myGray.100 !important',
|
||||
borderLeftColor: 'myBlue.600 !important'
|
||||
}
|
||||
: {})}
|
||||
: {
|
||||
bg: item.top ? 'myBlue.200' : ''
|
||||
})}
|
||||
onClick={() => {
|
||||
if (item._id === chatId) return;
|
||||
if (isPc) {
|
||||
@@ -232,46 +182,12 @@ const PcSliderBar = ({
|
||||
<ChatIcon fontSize={'16px'} color={'myGray.500'} />
|
||||
<Box flex={'1 0 0'} w={0} ml={3}>
|
||||
<Flex alignItems={'center'}>
|
||||
{editingHistoryId === item._id ? (
|
||||
<Input
|
||||
ref={inputRef}
|
||||
value={editedTitle}
|
||||
onChange={(e: { target: { value: React.SetStateAction<string> } }) =>
|
||||
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}
|
||||
<Box flex={'1 0 0'} w={0} className="textEllipsis" color={'myGray.1000'}>
|
||||
{item.title}
|
||||
</Box>
|
||||
<Box color={'myGray.400'} fontSize={'sm'}>
|
||||
{formatTimeToChatTime(item.updateTime)}
|
||||
</Box>
|
||||
</Flex>
|
||||
<Box className="textEllipsis" mt={1} fontSize={'sm'} color={'myGray.500'}>
|
||||
{item.latestChat || '……'}
|
||||
@@ -312,6 +228,19 @@ const PcSliderBar = ({
|
||||
<Box ref={ContextMenuRef}></Box>
|
||||
<Menu isOpen>
|
||||
<MenuList>
|
||||
<MenuItem
|
||||
onClick={async () => {
|
||||
try {
|
||||
await putChatHistory({
|
||||
chatId: contextMenuData.history._id,
|
||||
top: !contextMenuData.history.top
|
||||
});
|
||||
loadHistory({ pageNum: 1, init: true });
|
||||
} catch (error) {}
|
||||
}}
|
||||
>
|
||||
{contextMenuData.history.top ? '取消置顶' : '置顶'}
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
onClick={async () => {
|
||||
setIsLoading(true);
|
||||
@@ -329,15 +258,31 @@ const PcSliderBar = ({
|
||||
删除记录
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
try {
|
||||
onEditClick(contextMenuData.history._id, contextMenuData.history.title);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
}}
|
||||
onClick={() =>
|
||||
onOpenModal({
|
||||
defaultVal: contextMenuData.history.title,
|
||||
onSuccess: async (val: string) => {
|
||||
await putChatHistory({
|
||||
chatId: contextMenuData.history._id,
|
||||
customTitle: val,
|
||||
top: contextMenuData.history.top
|
||||
});
|
||||
toast({
|
||||
title: '自定义标题成功',
|
||||
status: 'success'
|
||||
});
|
||||
loadHistory({ pageNum: 1, init: true });
|
||||
},
|
||||
onError(err) {
|
||||
toast({
|
||||
title: getErrText(err),
|
||||
status: 'error'
|
||||
});
|
||||
}
|
||||
})
|
||||
}
|
||||
>
|
||||
编辑标题
|
||||
自定义标题
|
||||
</MenuItem>
|
||||
<MenuItem onClick={() => onclickExportChat('html')}>导出HTML格式</MenuItem>
|
||||
<MenuItem onClick={() => onclickExportChat('pdf')}>导出PDF格式</MenuItem>
|
||||
@@ -346,7 +291,7 @@ const PcSliderBar = ({
|
||||
</Menu>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<EditTitleModal />
|
||||
<Loading loading={isLoadingHistory} fixed={false} />
|
||||
</Flex>
|
||||
);
|
||||
|
91
src/pages/chat/components/useEditTitle.tsx
Normal file
91
src/pages/chat/components/useEditTitle.tsx
Normal file
@@ -0,0 +1,91 @@
|
||||
import React, { useCallback, useRef, useState, memo } from 'react';
|
||||
import {
|
||||
Modal,
|
||||
ModalOverlay,
|
||||
ModalContent,
|
||||
ModalHeader,
|
||||
ModalFooter,
|
||||
ModalBody,
|
||||
ModalCloseButton,
|
||||
Input,
|
||||
useDisclosure,
|
||||
Button
|
||||
} from '@chakra-ui/react';
|
||||
|
||||
export const useEditTitle = ({
|
||||
title,
|
||||
placeholder = ''
|
||||
}: {
|
||||
title: string;
|
||||
placeholder?: string;
|
||||
}) => {
|
||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||
|
||||
const inputRef = useRef<HTMLInputElement | null>(null);
|
||||
const onSuccessCb = useRef<(content: string) => void | Promise<void>>();
|
||||
const onErrorCb = useRef<(err: any) => void>();
|
||||
const defaultValue = useRef('');
|
||||
|
||||
const onOpenModal = useCallback(
|
||||
({
|
||||
defaultVal,
|
||||
onSuccess,
|
||||
onError
|
||||
}: {
|
||||
defaultVal: string;
|
||||
onSuccess: (content: string) => any;
|
||||
onError?: (err: any) => void;
|
||||
}) => {
|
||||
onOpen();
|
||||
onSuccessCb.current = onSuccess;
|
||||
onErrorCb.current = onError;
|
||||
defaultValue.current = defaultVal;
|
||||
},
|
||||
[onOpen]
|
||||
);
|
||||
|
||||
const onclickConfirm = useCallback(async () => {
|
||||
if (!inputRef.current) return;
|
||||
try {
|
||||
const val = inputRef.current.value;
|
||||
await onSuccessCb.current?.(val);
|
||||
onClose();
|
||||
} catch (err) {
|
||||
onErrorCb.current?.(err);
|
||||
}
|
||||
}, [onClose]);
|
||||
|
||||
// eslint-disable-next-line react/display-name
|
||||
const EditModal = useCallback(
|
||||
() => (
|
||||
<Modal isOpen={isOpen} onClose={onClose}>
|
||||
<ModalOverlay />
|
||||
<ModalContent>
|
||||
<ModalHeader>{title}</ModalHeader>
|
||||
<ModalCloseButton />
|
||||
<ModalBody>
|
||||
<Input
|
||||
ref={inputRef}
|
||||
defaultValue={defaultValue.current}
|
||||
placeholder={placeholder}
|
||||
autoFocus
|
||||
maxLength={20}
|
||||
/>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button mr={3} variant={'outline'} onClick={onClose}>
|
||||
取消
|
||||
</Button>
|
||||
<Button onClick={onclickConfirm}>确认</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
),
|
||||
[isOpen, onClose, onclickConfirm, placeholder, title]
|
||||
);
|
||||
|
||||
return {
|
||||
onOpenModal,
|
||||
EditModal
|
||||
};
|
||||
};
|
@@ -32,13 +32,16 @@ const ChatSchema = new Schema({
|
||||
default: '历史记录'
|
||||
},
|
||||
customTitle: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
latestChat: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
top: {
|
||||
type: Boolean
|
||||
},
|
||||
content: {
|
||||
type: [
|
||||
{
|
||||
|
@@ -6,13 +6,14 @@ export const connectPg = async () => {
|
||||
return global.pgClient;
|
||||
}
|
||||
|
||||
const maxLink = Number(process.env.VECTOR_MAX_PROCESS || 10);
|
||||
global.pgClient = new Pool({
|
||||
host: process.env.PG_HOST,
|
||||
port: process.env.PG_PORT ? +process.env.PG_PORT : 5432,
|
||||
user: process.env.PG_USER,
|
||||
password: process.env.PG_PASSWORD,
|
||||
database: process.env.PG_DB_NAME,
|
||||
max: 20,
|
||||
max: maxLink,
|
||||
idleTimeoutMillis: 60000,
|
||||
connectionTimeoutMillis: 20000
|
||||
});
|
||||
|
1
src/types/chat.d.ts
vendored
1
src/types/chat.d.ts
vendored
@@ -33,6 +33,7 @@ export type HistoryItemType = {
|
||||
modelId: string;
|
||||
title: string;
|
||||
latestChat: string;
|
||||
top: boolean;
|
||||
};
|
||||
|
||||
export type ShareChatHistoryItemType = {
|
||||
|
6
src/types/mongoSchema.d.ts
vendored
6
src/types/mongoSchema.d.ts
vendored
@@ -85,10 +85,12 @@ export interface ChatSchema {
|
||||
userId: string;
|
||||
modelId: string;
|
||||
expiredTime: number;
|
||||
loadAmount: number;
|
||||
updateTime: Date;
|
||||
title: string;
|
||||
customTitle: string;
|
||||
latestChat: string;
|
||||
top: boolean;
|
||||
content: ChatItemType[];
|
||||
customTitle: Boolean;
|
||||
}
|
||||
export interface ChatPopulate extends ChatSchema {
|
||||
userId: UserModelSchema;
|
||||
|
Reference in New Issue
Block a user