feat: Import and export chats

fixes #8, #26
This commit is contained in:
Jing Hua 2023-03-10 17:03:46 +08:00
parent bdc385158b
commit df82b62e38
10 changed files with 205 additions and 4 deletions

View file

@ -0,0 +1,23 @@
import React from 'react';
const ExportIcon = (props: React.SVGProps<SVGSVGElement>) => {
return (
<svg
fill='none'
stroke='currentColor'
strokeLinecap='round'
strokeLinejoin='round'
strokeWidth={2}
viewBox='0 0 24 24'
height='1em'
width='1em'
{...props}
>
<path stroke='none' d='M0 0h24v24H0z' />
<path d='M14 3v4a1 1 0 001 1h4' />
<path d='M11.5 21H7a2 2 0 01-2-2V5a2 2 0 012-2h7l5 5v5m-5 6h7m-3-3l3 3-3 3' />
</svg>
);
};
export default ExportIcon;

View file

@ -1,6 +1,7 @@
import React, { useState } from 'react';
import PopupModal from '@components/PopupModal';
import AboutIcon from '@icon/AboutIcon';
import Updates from '@components/Menu/MenuOptions/Updates';
const AboutMenu = () => {
const [isModalOpen, setIsModalOpen] = useState<boolean>(false);
@ -27,6 +28,7 @@ const AboutMenu = () => {
<div className='p-6 border-b border-gray-200 dark:border-gray-600'>
<div className='min-w-fit text-gray-900 dark:text-gray-300 text-sm flex flex-col gap-2'>
<p>Free ChatGPT is an amazing open-source web app that allows you to play with OpenAI's ChatGPT API for free!</p>
<Updates isButton />
<h2 className='text-lg font-bold'>Discord Server</h2>
<p>We invite you to join our Discord community! Our Discord server is a great place to exchange ChatGPT ideas and tips, and submit feature requests for Free ChatGPT. You'll have the opportunity to interact with the developers behind Free ChatGPT as well as other AI enthusiasts who share your passion.</p>

View file

@ -69,7 +69,7 @@ const ChatContent = () => {
{error !== '' && (
<div className='relative py-2 px-3 w-3/5 mt-3 max-md:w-11/12 border rounded-md border-red-500 bg-red-500/10'>
<div className='text-gray-600 dark:text-gray-100 text-sm whitespace-pre-line'>
<div className='text-gray-600 dark:text-gray-100 text-sm whitespace-pre-wrap'>
{error}
</div>
<div

View file

@ -0,0 +1,124 @@
import React, { useRef, useState } from 'react';
import useStore from '@store/store';
import ExportIcon from '@icon/ExportIcon';
import downloadFile from '@utils/downloadFile';
import { getToday } from '@utils/date';
import PopupModal from '@components/PopupModal';
import { isChats } from '@utils/chat';
const ImportExportChat = () => {
const [isModalOpen, setIsModalOpen] = useState<boolean>(false);
return (
<>
<a
className='flex py-3 px-3 items-center gap-3 rounded-md hover:bg-gray-500/10 transition-colors duration-200 text-white cursor-pointer text-sm'
onClick={() => {
setIsModalOpen(true);
}}
>
<ExportIcon className='w-4 h-4' />
Import / Export
</a>
{isModalOpen && (
<PopupModal
title='Import / Export'
setIsModalOpen={setIsModalOpen}
cancelButton={false}
>
<div className='p-6 border-b border-gray-200 dark:border-gray-600'>
<ImportChat />
<ExportChat />
</div>
</PopupModal>
)}
</>
);
};
const ImportChat = () => {
const setChats = useStore.getState().setChats;
const inputRef = useRef<HTMLInputElement>(null);
const [alert, setAlert] = useState<{
message: string;
success: boolean;
} | null>(null);
const handleFileUpload = () => {
if (!inputRef || !inputRef.current) return;
const file = inputRef.current.files?.[0];
if (file) {
const reader = new FileReader();
reader.onload = (event) => {
const data = event.target?.result as string;
try {
const parsedData = JSON.parse(data);
if (isChats(parsedData)) {
setChats(parsedData);
setAlert({ message: 'Succesfully imported!', success: true });
} else {
setAlert({ message: 'Invalid chats data format', success: false });
}
} catch (error: unknown) {
setAlert({ message: (error as Error).message, success: false });
}
};
reader.readAsText(file);
}
};
return (
<>
<label className='block mb-2 text-sm font-medium text-gray-900 dark:text-gray-300'>
Import (JSON)
</label>
<input
className='w-full text-sm file:p-2 text-gray-800 file:text-gray-700 dark:text-gray-300 dark:file:text-gray-200 rounded-md cursor-pointer focus:outline-none bg-gray-50 file:bg-gray-100 dark:bg-gray-800 dark:file:bg-gray-700 file:border-0 border border-gray-300 dark:border-gray-600 placeholder-gray-900 dark:placeholder-gray-300 file:cursor-pointer'
type='file'
ref={inputRef}
/>
<button
className='btn btn-small btn-primary mt-3'
onClick={handleFileUpload}
>
Import
</button>
{alert && (
<div
className={`relative py-2 px-3 w-full mt-3 border rounded-md text-gray-600 dark:text-gray-100 text-sm whitespace-pre-wrap ${
alert.success
? 'border-green-500 bg-green-500/10'
: 'border-red-500 bg-red-500/10'
}`}
>
{alert.message}
</div>
)}
</>
);
};
const ExportChat = () => {
const chats = useStore.getState().chats;
return (
<div className='mt-6'>
<div className='block mb-2 text-sm font-medium text-gray-900 dark:text-gray-300'>
Export (JSON)
</div>
<button
className='btn btn-small btn-primary'
onClick={() => {
if (chats) downloadFile(chats, getToday());
}}
>
Export
</button>
</div>
);
};
export default ImportExportChat;

View file

@ -0,0 +1 @@
export { default } from './ImportExportChat';

View file

@ -8,16 +8,18 @@ import Me from './Me';
import ThemeSwitcher from './ThemeSwitcher';
import Updates from './Updates';
import AboutMenu from '@components/AboutMenu';
import ImportExportChat from '@components/ImportExportChat';
const MenuOptions = () => {
return (
<>
<AboutMenu />
<ClearConversation />
<ImportExportChat />
<Api />
<ThemeSwitcher />
{/* <Account /> */}
<Updates />
{/* <Updates /> */}
<Me />
{/* <Logout /> */}
</>

View file

@ -1,12 +1,16 @@
import React from 'react';
import LinkIcon from '@icon/LinkIcon';
const Updates = () => {
const Updates = ({ isButton = false }: { isButton?: boolean }) => {
return (
<a
href='https://github.com/ztjhz/chatgpt-free-app'
target='_blank'
className='flex py-3 px-3 items-center gap-3 rounded-md hover:bg-gray-500/10 transition-colors duration-200 text-white cursor-pointer text-sm'
className={
isButton
? 'flex py-3 px-3 items-center gap-3 transition-colors duration-200 btn btn-neutral text-sm justify-center'
: 'flex py-3 px-3 items-center gap-3 rounded-md hover:bg-gray-500/10 transition-colors duration-200 text-white cursor-pointer text-sm'
}
>
<LinkIcon />
Source Code

24
src/utils/chat.ts Normal file
View file

@ -0,0 +1,24 @@
import { ChatInterface } from '@type/chat';
import { roles } from '@type/chat';
export const isChats = (chats: any): chats is ChatInterface[] => {
if (!Array.isArray(chats)) return false;
for (const chat of chats) {
if (!(typeof chat.title === 'string') || chat.title === '') return false;
if (!(typeof chat.titleSet === 'boolean')) return false;
if (!Array.isArray(chat.messages)) return false;
for (const message of chat.messages) {
if (!(typeof message.content === 'string')) return false;
if (!(typeof message.role === 'string')) return false;
if (!roles.includes(message.role)) return false;
}
if (!(typeof chat.config === 'object')) return false;
if (!(typeof chat.config.temperature === 'number')) return false;
if (!(typeof chat.config.presence_penalty === 'number')) return false;
}
return true;
};

10
src/utils/date.ts Normal file
View file

@ -0,0 +1,10 @@
export const getToday = () => {
const date = new Date();
const dateString =
date.getFullYear() +
'-' +
('0' + (date.getMonth() + 1)).slice(-2) +
'-' +
('0' + date.getDate()).slice(-2);
return dateString;
};

11
src/utils/downloadFile.ts Normal file
View file

@ -0,0 +1,11 @@
const downloadFile = (data: object, filename: string) => {
const blob = new Blob([JSON.stringify(data)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = filename;
link.click();
link.remove();
};
export default downloadFile;