Files
FastGPT/src/pages/chat/components/History.tsx
2023-05-29 22:24:49 +08:00

301 lines
9.4 KiB
TypeScript

import React, { useCallback, useRef, useState, useMemo } from 'react';
import type { MouseEvent } from 'react';
import { AddIcon } from '@chakra-ui/icons';
import {
Box,
Button,
Flex,
useTheme,
Menu,
MenuList,
MenuItem,
useOutsideClick
} from '@chakra-ui/react';
import { ChatIcon } from '@chakra-ui/icons';
import { useQuery } from '@tanstack/react-query';
import { useRouter } from 'next/router';
import { useLoading } from '@/hooks/useLoading';
import { useUserStore } from '@/store/user';
import MyIcon from '@/components/Icon';
import type { HistoryItemType, ExportChatType } from '@/types/chat';
import { useChatStore } from '@/store/chat';
import ModelList from './ModelList';
import { useGlobalStore } from '@/store/global';
import styles from '../index.module.scss';
import { useEditTitle } from './useEditTitle';
import { putChatHistory } from '@/api/chat';
import { useToast } from '@/hooks/useToast';
import { formatTimeToChatTime, getErrText } from '@/utils/tools';
const PcSliderBar = ({
onclickDelHistory,
onclickExportChat
}: {
onclickDelHistory: (historyId: string) => Promise<void>;
onclickExportChat: (type: ExportChatType) => void;
}) => {
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 { Loading, setIsLoading } = useLoading();
const [contextMenuData, setContextMenuData] = useState<{
left: number;
top: number;
history: HistoryItemType;
}>();
const { history, loadHistory } = useChatStore();
const { myModels, myCollectionModels, loadMyModels } = useUserStore();
const models = useMemo(
() => [...myModels, ...myCollectionModels],
[myCollectionModels, myModels]
);
// custom title edit
const { onOpenModal, EditModal: EditTitleModal } = useEditTitle({
title: '自定义历史记录标题',
placeholder: '如果设置为空,会自动跟随聊天记录。'
});
// close contextMenu
useOutsideClick({
ref: ContextMenuRef,
handler: () =>
setTimeout(() => {
setContextMenuData(undefined);
}, 10)
});
const onclickContextMenu = useCallback(
(e: MouseEvent<HTMLDivElement>, history: HistoryItemType) => {
e.preventDefault(); // 阻止默认右键菜单
if (!isPc) return;
setContextMenuData({
left: e.clientX + 15,
top: e.clientY + 10,
history
});
},
[isPc]
);
useQuery(['loadModels'], () => loadMyModels(false));
const { isLoading: isLoadingHistory } = useQuery(['loadingHistory'], () =>
loadHistory({ pageNum: 1 })
);
return (
<Flex
position={'relative'}
flexDirection={'column'}
w={'100%'}
h={'100%'}
bg={'white'}
borderRight={['', theme.borders.base]}
>
{/* 新对话 */}
{isPc && (
<Box
className={styles.newChat}
zIndex={1001}
w={'90%'}
h={'40px'}
my={5}
mx={'auto'}
position={'relative'}
>
<Button
variant={'base'}
w={'100%'}
h={'100%'}
leftIcon={<AddIcon />}
onClick={() => router.replace(`/chat?modelId=${modelId}`)}
>
</Button>
{models.length > 1 && (
<Box
className={styles.modelListContainer}
position={'absolute'}
w={'115%'}
left={0}
top={'40px'}
transition={'0.15s ease-out'}
bg={'white'}
>
<Box
className={styles.modelList}
mt={'6px'}
h={'calc(100% - 6px)'}
overflow={'overlay'}
>
<ModelList models={models} modelId={modelId} />
</Box>
</Box>
)}
</Box>
)}
{/* chat history */}
<Box flex={'1 0 0'} h={0} overflow={'overlay'}>
{history.map((item) => (
<Flex
position={'relative'}
key={item._id}
alignItems={'center'}
py={3}
pr={[0, 3]}
pl={[6, 3]}
cursor={'pointer'}
transition={'background-color .2s ease-in'}
borderLeft={['none', '5px solid transparent']}
userSelect={'none'}
_hover={{
bg: ['', '#dee0e3']
}}
{...(item._id === chatId
? {
bg: 'myGray.100 !important',
borderLeftColor: 'myBlue.600 !important'
}
: {
bg: item.top ? 'myBlue.200' : ''
})}
onClick={() => {
if (item._id === chatId) return;
if (isPc) {
router.replace(`/chat?modelId=${item.modelId}&chatId=${item._id}`);
} else {
router.push(`/chat?modelId=${item.modelId}&chatId=${item._id}`);
}
}}
onContextMenu={(e) => onclickContextMenu(e, item)}
>
<ChatIcon fontSize={'16px'} color={'myGray.500'} />
<Box flex={'1 0 0'} w={0} ml={3}>
<Flex alignItems={'center'}>
<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 || '……'}
</Box>
</Box>
{/* phone quick delete */}
{!isPc && (
<MyIcon
px={3}
name={'delete'}
w={'16px'}
onClickCapture={async (e) => {
e.stopPropagation();
setIsLoading(true);
try {
await onclickDelHistory(item._id);
} catch (error) {
console.log(error);
}
setIsLoading(false);
}}
/>
)}
</Flex>
))}
{!isLoadingHistory && history.length === 0 && (
<Flex h={'100%'} flexDirection={'column'} alignItems={'center'} pt={'30vh'}>
<MyIcon name="empty" w={'48px'} h={'48px'} color={'transparent'} />
<Box mt={2} color={'myGray.500'}>
</Box>
</Flex>
)}
</Box>
{/* context menu */}
{contextMenuData && (
<Box zIndex={10} position={'fixed'} top={contextMenuData.top} left={contextMenuData.left}>
<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);
try {
await onclickDelHistory(contextMenuData.history._id);
if (contextMenuData.history._id === chatId) {
router.replace(`/chat?modelId=${modelId}`);
}
} catch (error) {
console.log(error);
}
setIsLoading(false);
}}
>
</MenuItem>
<MenuItem
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>
<MenuItem onClick={() => onclickExportChat('md')}>Markdown格式</MenuItem>
</MenuList>
</Menu>
</Box>
)}
<EditTitleModal />
<Loading loading={isLoadingHistory} fixed={false} />
</Flex>
);
};
export default PcSliderBar;