feat: custom title and set history top

This commit is contained in:
archer
2023-05-29 22:24:49 +08:00
parent 7fe39c2515
commit 2fce76202a
11 changed files with 210 additions and 161 deletions

View File

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

View File

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

View File

@@ -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) {

View File

@@ -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()
});

View File

@@ -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>
);

View 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
};
};

View File

@@ -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: [
{

View File

@@ -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
View File

@@ -33,6 +33,7 @@ export type HistoryItemType = {
modelId: string;
title: string;
latestChat: string;
top: boolean;
};
export type ShareChatHistoryItemType = {

View File

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