diff --git a/src/api/chat.ts b/src/api/chat.ts index 72355d815..e57a0253a 100644 --- a/src/api/chat.ts +++ b/src/api/chat.ts @@ -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('/chat/getHistory', data); + POST('/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('/chat/updateChatHistoryTitle', data); +export const putChatHistory = (data: UpdateHistoryProps) => + PUT('/chat/history/updateChatHistory', data); /** * create a shareChat diff --git a/src/pages/api/chat/getHistory.ts b/src/pages/api/chat/history/getHistory.ts similarity index 58% rename from src/pages/api/chat/getHistory.ts rename to src/pages/api/chat/history/getHistory.ts index ac6a9e9a4..a582055de 100644 --- a/src/pages/api/chat/getHistory.ts +++ b/src/pages/api/chat/history/getHistory.ts @@ -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(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, { diff --git a/src/pages/api/chat/updateChatHistoryTitle.ts b/src/pages/api/chat/history/updateChatHistory.ts similarity index 59% rename from src/pages/api/chat/updateChatHistoryTitle.ts rename to src/pages/api/chat/history/updateChatHistory.ts index c9d1dd9be..00835e75c 100644 --- a/src/pages/api/chat/updateChatHistoryTitle.ts +++ b/src/pages/api/chat/history/updateChatHistory.ts @@ -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) { diff --git a/src/pages/api/chat/updateHistoryQuote.ts b/src/pages/api/chat/history/updateHistoryQuote.ts similarity index 100% rename from src/pages/api/chat/updateHistoryQuote.ts rename to src/pages/api/chat/history/updateHistoryQuote.ts diff --git a/src/pages/api/chat/saveChat.ts b/src/pages/api/chat/saveChat.ts index 03eba4d94..19d1246d5 100644 --- a/src/pages/api/chat/saveChat.ts +++ b/src/pages/api/chat/saveChat.ts @@ -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() }); diff --git a/src/pages/chat/components/History.tsx b/src/pages/chat/components/History.tsx index f9ca57df5..0a328109e 100644 --- a/src/pages/chat/components/History.tsx +++ b/src/pages/chat/components/History.tsx @@ -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>; - 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 - }; -}; +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; 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, history: HistoryItemType) => { e.preventDefault(); // 阻止默认右键菜单 @@ -144,6 +86,12 @@ const PcSliderBar = ({ [isPc] ); + useQuery(['loadModels'], () => loadMyModels(false)); + + const { isLoading: isLoadingHistory } = useQuery(['loadingHistory'], () => + loadHistory({ pageNum: 1 }) + ); + return ( { if (item._id === chatId) return; if (isPc) { @@ -232,46 +182,12 @@ const PcSliderBar = ({ - {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.title} + + + {formatTimeToChatTime(item.updateTime)} + {item.latestChat || '……'} @@ -312,6 +228,19 @@ const PcSliderBar = ({ + { + try { + await putChatHistory({ + chatId: contextMenuData.history._id, + top: !contextMenuData.history.top + }); + loadHistory({ pageNum: 1, init: true }); + } catch (error) {} + }} + > + {contextMenuData.history.top ? '取消置顶' : '置顶'} + { setIsLoading(true); @@ -329,15 +258,31 @@ const PcSliderBar = ({ 删除记录 { - 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' + }); + } + }) + } > - 编辑标题 + 自定义标题 onclickExportChat('html')}>导出HTML格式 onclickExportChat('pdf')}>导出PDF格式 @@ -346,7 +291,7 @@ const PcSliderBar = ({ )} - + ); diff --git a/src/pages/chat/components/useEditTitle.tsx b/src/pages/chat/components/useEditTitle.tsx new file mode 100644 index 000000000..94d9d03b4 --- /dev/null +++ b/src/pages/chat/components/useEditTitle.tsx @@ -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(null); + const onSuccessCb = useRef<(content: string) => void | Promise>(); + 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( + () => ( + + + + {title} + + + + + + + + + + + ), + [isOpen, onClose, onclickConfirm, placeholder, title] + ); + + return { + onOpenModal, + EditModal + }; +}; diff --git a/src/service/models/chat.ts b/src/service/models/chat.ts index 9a6f030ad..db2670de5 100644 --- a/src/service/models/chat.ts +++ b/src/service/models/chat.ts @@ -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: [ { diff --git a/src/service/pg.ts b/src/service/pg.ts index 2ac25a193..00e131233 100644 --- a/src/service/pg.ts +++ b/src/service/pg.ts @@ -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 }); diff --git a/src/types/chat.d.ts b/src/types/chat.d.ts index 3b322aaad..71e49812f 100644 --- a/src/types/chat.d.ts +++ b/src/types/chat.d.ts @@ -33,6 +33,7 @@ export type HistoryItemType = { modelId: string; title: string; latestChat: string; + top: boolean; }; export type ShareChatHistoryItemType = { diff --git a/src/types/mongoSchema.d.ts b/src/types/mongoSchema.d.ts index 74c266a21..d727d0d53 100644 --- a/src/types/mongoSchema.d.ts +++ b/src/types/mongoSchema.d.ts @@ -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;