feat: export chat

This commit is contained in:
archer
2023-05-02 11:45:10 +08:00
parent 89a67ca9c0
commit b0d414ac12
10 changed files with 951 additions and 15 deletions

View File

@@ -1,4 +1,4 @@
import React, { useRef, useEffect, useMemo } from 'react';
import React, { useRef, useEffect, useMemo, useCallback } from 'react';
import { AddIcon, ChatIcon, DeleteIcon, MoonIcon, SunIcon } from '@chakra-ui/icons';
import {
Box,
@@ -13,7 +13,11 @@ import {
IconButton,
useDisclosure,
useColorMode,
useColorModeValue
useColorModeValue,
Menu,
MenuButton,
MenuList,
MenuItem
} from '@chakra-ui/react';
import { useUserStore } from '@/store/user';
import { useMutation, useQuery } from '@tanstack/react-query';
@@ -23,16 +27,20 @@ import MyIcon from '@/components/Icon';
import WxConcat from '@/components/WxConcat';
import { getChatHistory, delChatHistoryById } from '@/api/chat';
import { getCollectionModels } from '@/api/model';
import { modelList } from '@/constants/model';
import type { ChatSiteItemType } from '../index';
import { fileDownload } from '@/utils/file';
import { htmlTemplate } from '@/constants/common';
const SlideBar = ({
chatId,
modelId,
history,
resetChat,
onClose
}: {
chatId: string;
modelId: string;
history: ChatSiteItemType[];
resetChat: (modelId?: string, chatId?: string) => void;
onClose: () => void;
}) => {
@@ -84,6 +92,68 @@ const SlideBar = ({
}, 1000);
}, [loadChatHistory]);
/**
* export md
*/
const onclickExportMd = useCallback(() => {
fileDownload({
text: history.map((item) => item.value).join('\n'),
type: 'text/markdown',
filename: 'chat.md'
});
}, [history]);
const getHistoryHtml = useCallback(() => {
const historyDom = document.getElementById('history');
if (!historyDom) return;
const dom = Array.from(historyDom.children).map((child, i) => {
const avatar = `<img src="${
child.querySelector<HTMLImageElement>('.avatar')?.src
}" alt="" />`;
const chatContent = child.querySelector<HTMLDivElement>('.markdown');
if (!chatContent) {
return '';
}
const chatContentClone = chatContent.cloneNode(true) as HTMLDivElement;
const codeHeader = chatContentClone.querySelectorAll('.code-header');
codeHeader.forEach((childElement: any) => {
childElement.remove();
});
return `<div class="chat-item">
${avatar}
${chatContentClone.outerHTML}
</div>`;
});
const html = htmlTemplate.replace('{{CHAT_CONTENT}}', dom.join('\n'));
return html;
}, []);
const onclickExportHtml = useCallback(() => {
const html = getHistoryHtml();
html &&
fileDownload({
text: html,
type: 'text/html',
filename: '聊天记录.html'
});
}, [getHistoryHtml]);
const onclickExportPdf = useCallback(() => {
const html = getHistoryHtml();
html &&
// @ts-ignore
html2pdf(html, {
margin: 0,
filename: `聊天记录.pdf`
});
}, [getHistoryHtml]);
const RenderHistory = () => (
<>
{chatHistory.map((item) => (
@@ -145,7 +215,7 @@ const SlideBar = ({
onClick: () => void;
children: JSX.Element | string;
}) => (
<Box px={3} mb={3}>
<Box px={3} mb={2}>
<Flex
alignItems={'center'}
p={2}
@@ -176,7 +246,7 @@ const SlideBar = ({
w={'90%'}
variant={'white'}
h={'40px'}
mb={4}
mb={2}
mx={'auto'}
leftIcon={<AddIcon />}
onClick={() => resetChat()}
@@ -238,7 +308,33 @@ const SlideBar = ({
</Accordion>
</Box>
<Divider my={4} colorScheme={useColorModeValue('gray', 'white')} />
<Divider my={3} colorScheme={useColorModeValue('gray', 'white')} />
{history.length > 0 && (
<Menu autoSelect={false}>
<MenuButton
mx={3}
mb={2}
p={2}
display={'flex'}
alignItems={'center'}
cursor={'pointer'}
borderRadius={'md'}
textAlign={'left'}
_hover={{
backgroundColor: 'rgba(255,255,255,0.2)'
}}
>
<MyIcon name="export" fill={'white'} w={'18px'} h={'18px'} mr={4} />
</MenuButton>
<MenuList fontSize={'sm'} color={'blackAlpha.800'}>
<MenuItem onClick={onclickExportHtml}>HTML格式</MenuItem>
<MenuItem onClick={onclickExportPdf}>PDF格式</MenuItem>
<MenuItem onClick={onclickExportMd}>Markdown格式</MenuItem>
</MenuList>
</Menu>
)}
<RenderButton onClick={() => router.push('/')}>
<>

View File

@@ -403,6 +403,7 @@ const Chat = ({ modelId, chatId }: { modelId: string; chatId: string }) => {
resetChat={resetChat}
chatId={chatId}
modelId={modelId}
history={chatData.history}
onClose={onCloseSlider}
/>
</Box>
@@ -434,6 +435,7 @@ const Chat = ({ modelId, chatId }: { modelId: string; chatId: string }) => {
resetChat={resetChat}
chatId={chatId}
modelId={modelId}
history={chatData.history}
onClose={onCloseSlider}
/>
</DrawerContent>
@@ -447,7 +449,15 @@ const Chat = ({ modelId, chatId }: { modelId: string; chatId: string }) => {
flexDirection={'column'}
>
{/* 聊天内容 */}
<Box ref={ChatBox} pb={[4, 0]} flex={'1 0 0'} h={0} w={'100%'} overflowY={'auto'}>
<Box
id={'history'}
ref={ChatBox}
pb={[4, 0]}
flex={'1 0 0'}
h={0}
w={'100%'}
overflowY={'auto'}
>
{chatData.history.map((item, index) => (
<Box
key={item._id}
@@ -463,6 +473,7 @@ const Chat = ({ modelId, chatId }: { modelId: string; chatId: string }) => {
<Menu autoSelect={false}>
<MenuButton as={Box} mr={media(4, 1)} cursor={'pointer'}>
<Image
className="avatar"
src={
item.obj === 'Human'
? '/icon/human.png'
@@ -479,14 +490,16 @@ const Chat = ({ modelId, chatId }: { modelId: string; chatId: string }) => {
<MenuItem onClick={() => delChatRecord(index, item._id)}></MenuItem>
</MenuList>
</Menu>
<Box flex={'1 0 0'} w={0} overflow={'hidden'} id={`chat${index}`}>
<Box flex={'1 0 0'} w={0} overflow={'hidden'}>
{item.obj === 'AI' ? (
<Markdown
source={item.value}
isChatting={isChatting && index === chatData.history.length - 1}
/>
) : (
<Box whiteSpace={'pre-wrap'}>{item.value}</Box>
<Box className="markdown" whiteSpace={'pre-wrap'}>
<Box as={'p'}>{item.value}</Box>
</Box>
)}
</Box>
{isPc && (