mirror of
https://github.com/NovaOSS/nova-betterchat.git
synced 2024-11-29 09:53:59 +01:00
parent
bdc385158b
commit
df82b62e38
23
src/assets/icons/ExportIcon.tsx
Normal file
23
src/assets/icons/ExportIcon.tsx
Normal 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;
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
|
124
src/components/ImportExportChat/ImportExportChat.tsx
Normal file
124
src/components/ImportExportChat/ImportExportChat.tsx
Normal 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;
|
1
src/components/ImportExportChat/index.ts
Normal file
1
src/components/ImportExportChat/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export { default } from './ImportExportChat';
|
|
@ -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 /> */}
|
||||
</>
|
||||
|
|
|
@ -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
24
src/utils/chat.ts
Normal 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
10
src/utils/date.ts
Normal 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
11
src/utils/downloadFile.ts
Normal 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;
|
Loading…
Reference in a new issue