Commit b0d414ac by archer

feat: export chat

parent 89a67ca9
This source diff could not be displayed because it is too large. You can view the blob instead.
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1682957006954" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2644" xmlns:xlink="http://www.w3.org/1999/xlink" width="48" height="48"><path d="M942.1 593.9c-22.6 0-41 18.3-41 41v204.8c0 22.6-18.4 41-41 41H163.8c-22.6 0-41-18.4-41-41V634.9c0-22.6-18.3-41-41-41s-41 18.3-41 41v204.8c0 67.8 55.1 122.9 122.9 122.9H860c67.8 0 122.9-55.1 122.9-122.9V634.9c0.1-22.6-18.2-41-40.8-41z" p-id="2645"></path><path d="M309.3 363L471 201.3v515.5c0 22.5 18.4 41 41 41 22.5 0 41-18.4 41-41V201.3L714.7 363c15.9 15.9 42 15.9 57.9 0 15.9-15.9 15.9-42 0-57.9L541.5 73.9c-0.2-0.2-0.3-0.4-0.4-0.5-5.7-5.7-12.7-9.3-20.1-10.9-0.2-0.1-0.5-0.2-0.7-0.2-2.7-0.5-5.4-0.8-8.1-0.8-2.7 0-5.5 0.3-8.1 0.8-0.3 0.1-0.5 0.2-0.7 0.2-7.4 1.6-14.4 5.2-20.1 10.9-0.2 0.2-0.3 0.4-0.4 0.5L251.4 305.1c-15.9 15.9-15.9 42 0 57.9 15.9 15.9 42 15.9 57.9 0z" p-id="2646"></path></svg>
\ No newline at end of file
......@@ -21,7 +21,8 @@ const map = {
stop: require('./icons/stop.svg').default,
shareMarket: require('./icons/shareMarket.svg').default,
collectionLight: require('./icons/collectionLight.svg').default,
collectionSolid: require('./icons/collectionSolid.svg').default
collectionSolid: require('./icons/collectionSolid.svg').default,
export: require('./icons/export.svg').default
};
export type IconName = keyof typeof map;
......
......@@ -17,7 +17,7 @@ const Markdown = ({ source, isChatting = false }: { source: string; isChatting?:
return (
<ReactMarkdown
className={`${styles.markdown} ${
className={`markdown ${styles.markdown} ${
isChatting ? (source === '' ? styles.waitingAnimation : styles.animation) : ''
}`}
remarkPlugins={[remarkMath]}
......@@ -31,6 +31,7 @@ const Markdown = ({ source, isChatting = false }: { source: string; isChatting?:
return !inline || match ? (
<Box my={3} borderRadius={'md'} overflow={'hidden'} backgroundColor={'#222'}>
<Flex
className="code-header"
py={2}
px={5}
backgroundColor={useColorModeValue('#323641', 'gray.600')}
......
......@@ -51,8 +51,9 @@ export default function App({ Component, pageProps }: AppProps) {
/>
<link rel="icon" href="/favicon.ico" />
</Head>
<Script src="/js/qrcode.min.js" strategy="afterInteractive"></Script>
<Script src="/js/pdf.js" strategy="afterInteractive"></Script>
<Script src="/js/qrcode.min.js" strategy="lazyOnload"></Script>
<Script src="/js/pdf.js" strategy="lazyOnload"></Script>
<Script src="/js/html2pdf.bundle.min.js" strategy="lazyOnload"></Script>
<QueryClientProvider client={queryClient}>
<ChakraProvider theme={theme}>
<ColorModeScript initialColorMode={theme.config.initialColorMode} />
......
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('/')}>
<>
......
......@@ -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 && (
......
......@@ -75,7 +75,7 @@ export async function generateQA(next = false): Promise<any> {
role: 'system',
content: `你是出题人
${dataItem.prompt || '下面是"一段长文本"'}
从中选出5至20个题目和答案,题目包含问答题,计算题,代码题等.答案要详细.按格式返回: Q1:
从中选出5至20个题目和答案.答案详细.按格式返回: Q1:
A1:
Q2:
A2:
......
......@@ -90,7 +90,7 @@ export const openaiCreateEmbedding = async ({
)
.then((res) => ({
tokenLen: res.data.usage.total_tokens || 0,
vector: res?.data?.data?.[0]?.embedding || []
vector: res.data.data?.[0]?.embedding || []
}));
pushGenerateVectorBill({
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment