mirror of
https://github.com/NovaOSS/nova-betterchat.git
synced 2024-11-25 17:24:00 +01:00
feat: import openai chatgpt export
This commit is contained in:
parent
b3f421cde9
commit
ece4778f88
35
src/components/ImportExportChat/ExportChat.tsx
Normal file
35
src/components/ImportExportChat/ExportChat.tsx
Normal file
|
@ -0,0 +1,35 @@
|
|||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import useStore from '@store/store';
|
||||
|
||||
import downloadFile from '@utils/downloadFile';
|
||||
import { getToday } from '@utils/date';
|
||||
|
||||
import Export from '@type/export';
|
||||
|
||||
const ExportChat = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div className='mt-6'>
|
||||
<div className='block mb-2 text-sm font-medium text-gray-900 dark:text-gray-300'>
|
||||
{t('export')} (JSON)
|
||||
</div>
|
||||
<button
|
||||
className='btn btn-small btn-primary'
|
||||
onClick={() => {
|
||||
const fileData: Export = {
|
||||
chats: useStore.getState().chats,
|
||||
folders: useStore.getState().folders,
|
||||
version: 1,
|
||||
};
|
||||
downloadFile(fileData, getToday());
|
||||
}}
|
||||
>
|
||||
{t('export')}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export default ExportChat;
|
173
src/components/ImportExportChat/ImportChat.tsx
Normal file
173
src/components/ImportExportChat/ImportChat.tsx
Normal file
|
@ -0,0 +1,173 @@
|
|||
import React, { useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import useStore from '@store/store';
|
||||
|
||||
import {
|
||||
isLegacyImport,
|
||||
validateAndFixChats,
|
||||
validateExportV1,
|
||||
} from '@utils/import';
|
||||
|
||||
import { ChatInterface, Folder, FolderCollection } from '@type/chat';
|
||||
import { ExportBase } from '@type/export';
|
||||
|
||||
const ImportChat = () => {
|
||||
const { t } = useTranslation();
|
||||
const setChats = useStore.getState().setChats;
|
||||
const setFolders = useStore.getState().setFolders;
|
||||
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 (isLegacyImport(parsedData)) {
|
||||
if (validateAndFixChats(parsedData)) {
|
||||
// import new folders
|
||||
const folderNameToIdMap: Record<string, string> = {};
|
||||
const parsedFolders: string[] = [];
|
||||
|
||||
parsedData.forEach((data) => {
|
||||
const folder = data.folder;
|
||||
if (folder) {
|
||||
if (!parsedFolders.includes(folder)) {
|
||||
parsedFolders.push(folder);
|
||||
folderNameToIdMap[folder] = uuidv4();
|
||||
}
|
||||
data.folder = folderNameToIdMap[folder];
|
||||
}
|
||||
});
|
||||
|
||||
const newFolders: FolderCollection = parsedFolders.reduce(
|
||||
(acc, curr, index) => {
|
||||
const id = folderNameToIdMap[curr];
|
||||
const _newFolder: Folder = {
|
||||
id,
|
||||
name: curr,
|
||||
expanded: false,
|
||||
order: index,
|
||||
};
|
||||
return { [id]: _newFolder, ...acc };
|
||||
},
|
||||
{}
|
||||
);
|
||||
|
||||
// increment the order of existing folders
|
||||
const offset = parsedFolders.length;
|
||||
|
||||
const updatedFolders = useStore.getState().folders;
|
||||
Object.values(updatedFolders).forEach((f) => (f.order += offset));
|
||||
|
||||
setFolders({ ...newFolders, ...updatedFolders });
|
||||
|
||||
// import chats
|
||||
const prevChats = useStore.getState().chats;
|
||||
if (prevChats) {
|
||||
const updatedChats: ChatInterface[] = JSON.parse(
|
||||
JSON.stringify(prevChats)
|
||||
);
|
||||
setChats(parsedData.concat(updatedChats));
|
||||
} else {
|
||||
setChats(parsedData);
|
||||
}
|
||||
setAlert({ message: 'Succesfully imported!', success: true });
|
||||
} else {
|
||||
setAlert({
|
||||
message: 'Invalid chats data format',
|
||||
success: false,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
switch ((parsedData as ExportBase).version) {
|
||||
case 1:
|
||||
if (validateExportV1(parsedData)) {
|
||||
// import folders
|
||||
parsedData.folders;
|
||||
// increment the order of existing folders
|
||||
const offset = Object.keys(parsedData.folders).length;
|
||||
|
||||
const updatedFolders = useStore.getState().folders;
|
||||
Object.values(updatedFolders).forEach(
|
||||
(f) => (f.order += offset)
|
||||
);
|
||||
|
||||
setFolders({ ...parsedData.folders, ...updatedFolders });
|
||||
|
||||
// import chats
|
||||
const prevChats = useStore.getState().chats;
|
||||
if (parsedData.chats) {
|
||||
if (prevChats) {
|
||||
const updatedChats: ChatInterface[] = JSON.parse(
|
||||
JSON.stringify(prevChats)
|
||||
);
|
||||
setChats(parsedData.chats.concat(updatedChats));
|
||||
} else {
|
||||
setChats(parsedData.chats);
|
||||
}
|
||||
}
|
||||
|
||||
setAlert({ message: 'Succesfully imported!', success: true });
|
||||
} else {
|
||||
setAlert({
|
||||
message: 'Invalid format',
|
||||
success: false,
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
} 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'>
|
||||
{t('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}
|
||||
>
|
||||
{t('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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ImportChat;
|
78
src/components/ImportExportChat/ImportChatOpenAI.tsx
Normal file
78
src/components/ImportExportChat/ImportChatOpenAI.tsx
Normal file
|
@ -0,0 +1,78 @@
|
|||
import React, { useRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import useStore from '@store/store';
|
||||
|
||||
import { importOpenAIChatExport } from '@utils/import';
|
||||
|
||||
import { ChatInterface } from '@type/chat';
|
||||
|
||||
const ImportChatOpenAI = ({
|
||||
setIsModalOpen,
|
||||
}: {
|
||||
setIsModalOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const setToastStatus = useStore((state) => state.setToastStatus);
|
||||
const setToastMessage = useStore((state) => state.setToastMessage);
|
||||
const setToastShow = useStore((state) => state.setToastShow);
|
||||
const setChats = useStore.getState().setChats;
|
||||
|
||||
const handleFileUpload = () => {
|
||||
if (!inputRef || !inputRef.current) return;
|
||||
const file = inputRef.current.files?.[0];
|
||||
if (!file) return;
|
||||
|
||||
const reader = new FileReader();
|
||||
|
||||
reader.onload = (event) => {
|
||||
const data = event.target?.result as string;
|
||||
|
||||
try {
|
||||
const parsedData = JSON.parse(data);
|
||||
const chats = importOpenAIChatExport(parsedData);
|
||||
const prevChats: ChatInterface[] = JSON.parse(
|
||||
JSON.stringify(useStore.getState().chats)
|
||||
);
|
||||
setChats(chats.concat(prevChats));
|
||||
|
||||
setToastStatus('success');
|
||||
setToastMessage('Imported successfully!');
|
||||
setIsModalOpen(false);
|
||||
} catch (error: unknown) {
|
||||
setToastStatus('error');
|
||||
setToastMessage(`Invalid format! ${(error as Error).message}`);
|
||||
}
|
||||
setToastShow(true);
|
||||
};
|
||||
|
||||
reader.readAsText(file);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className='text-lg font-bold text-gray-900 dark:text-gray-300 text-center mb-3'>
|
||||
{t('import')} OpenAI ChatGPT {t('export')}
|
||||
</div>
|
||||
<label className='block mb-2 text-sm font-medium text-gray-900 dark:text-gray-300'>
|
||||
{t('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}
|
||||
>
|
||||
{t('import')}
|
||||
</button>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ImportChatOpenAI;
|
|
@ -1,19 +1,12 @@
|
|||
import React, { useRef, useState } from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
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 {
|
||||
isLegacyImport,
|
||||
validateAndFixChats,
|
||||
validateExportV1,
|
||||
} from '@utils/import';
|
||||
import { ChatInterface, Folder, FolderCollection } from '@type/chat';
|
||||
import Export, { ExportBase, ExportV1 } from '@type/export';
|
||||
|
||||
import ImportChat from './ImportChat';
|
||||
import ExportChat from './ExportChat';
|
||||
import ImportChatOpenAI from './ImportChatOpenAI';
|
||||
|
||||
const ImportExportChat = () => {
|
||||
const { t } = useTranslation();
|
||||
|
@ -39,6 +32,8 @@ const ImportExportChat = () => {
|
|||
<div className='p-6 border-b border-gray-200 dark:border-gray-600'>
|
||||
<ImportChat />
|
||||
<ExportChat />
|
||||
<div className='border-t my-3 border-gray-200 dark:border-gray-600' />
|
||||
<ImportChatOpenAI setIsModalOpen={setIsModalOpen} />
|
||||
</div>
|
||||
</PopupModal>
|
||||
)}
|
||||
|
@ -46,185 +41,4 @@ const ImportExportChat = () => {
|
|||
);
|
||||
};
|
||||
|
||||
const ImportChat = () => {
|
||||
const { t } = useTranslation();
|
||||
const setChats = useStore.getState().setChats;
|
||||
const setFolders = useStore.getState().setFolders;
|
||||
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 (isLegacyImport(parsedData)) {
|
||||
if (validateAndFixChats(parsedData)) {
|
||||
// import new folders
|
||||
const folderNameToIdMap: Record<string, string> = {};
|
||||
const parsedFolders: string[] = [];
|
||||
|
||||
parsedData.forEach((data) => {
|
||||
const folder = data.folder;
|
||||
if (folder) {
|
||||
if (!parsedFolders.includes(folder)) {
|
||||
parsedFolders.push(folder);
|
||||
folderNameToIdMap[folder] = uuidv4();
|
||||
}
|
||||
data.folder = folderNameToIdMap[folder];
|
||||
}
|
||||
});
|
||||
|
||||
const newFolders: FolderCollection = parsedFolders.reduce(
|
||||
(acc, curr, index) => {
|
||||
const id = folderNameToIdMap[curr];
|
||||
const _newFolder: Folder = {
|
||||
id,
|
||||
name: curr,
|
||||
expanded: false,
|
||||
order: index,
|
||||
};
|
||||
return { [id]: _newFolder, ...acc };
|
||||
},
|
||||
{}
|
||||
);
|
||||
|
||||
// increment the order of existing folders
|
||||
const offset = parsedFolders.length;
|
||||
|
||||
const updatedFolders = useStore.getState().folders;
|
||||
Object.values(updatedFolders).forEach((f) => (f.order += offset));
|
||||
|
||||
setFolders({ ...newFolders, ...updatedFolders });
|
||||
|
||||
// import chats
|
||||
const prevChats = useStore.getState().chats;
|
||||
if (prevChats) {
|
||||
const updatedChats: ChatInterface[] = JSON.parse(
|
||||
JSON.stringify(prevChats)
|
||||
);
|
||||
setChats(parsedData.concat(updatedChats));
|
||||
} else {
|
||||
setChats(parsedData);
|
||||
}
|
||||
setAlert({ message: 'Succesfully imported!', success: true });
|
||||
} else {
|
||||
setAlert({
|
||||
message: 'Invalid chats data format',
|
||||
success: false,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
switch ((parsedData as ExportBase).version) {
|
||||
case 1:
|
||||
if (validateExportV1(parsedData)) {
|
||||
// import folders
|
||||
parsedData.folders;
|
||||
// increment the order of existing folders
|
||||
const offset = Object.keys(parsedData.folders).length;
|
||||
|
||||
const updatedFolders = useStore.getState().folders;
|
||||
Object.values(updatedFolders).forEach(
|
||||
(f) => (f.order += offset)
|
||||
);
|
||||
|
||||
setFolders({ ...parsedData.folders, ...updatedFolders });
|
||||
|
||||
// import chats
|
||||
const prevChats = useStore.getState().chats;
|
||||
if (parsedData.chats) {
|
||||
if (prevChats) {
|
||||
const updatedChats: ChatInterface[] = JSON.parse(
|
||||
JSON.stringify(prevChats)
|
||||
);
|
||||
setChats(parsedData.chats.concat(updatedChats));
|
||||
} else {
|
||||
setChats(parsedData.chats);
|
||||
}
|
||||
}
|
||||
|
||||
setAlert({ message: 'Succesfully imported!', success: true });
|
||||
} else {
|
||||
setAlert({
|
||||
message: 'Invalid format',
|
||||
success: false,
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
} 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'>
|
||||
{t('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}
|
||||
>
|
||||
{t('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 { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div className='mt-6'>
|
||||
<div className='block mb-2 text-sm font-medium text-gray-900 dark:text-gray-300'>
|
||||
{t('export')} (JSON)
|
||||
</div>
|
||||
<button
|
||||
className='btn btn-small btn-primary'
|
||||
onClick={() => {
|
||||
const fileData: Export = {
|
||||
chats: useStore.getState().chats,
|
||||
folders: useStore.getState().folders,
|
||||
version: 1,
|
||||
};
|
||||
downloadFile(fileData, getToday());
|
||||
}}
|
||||
>
|
||||
{t('export')}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ImportExportChat;
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { ChatInterface, FolderCollection } from './chat';
|
||||
import { ChatInterface, FolderCollection, Role } from './chat';
|
||||
|
||||
export interface ExportBase {
|
||||
version: number;
|
||||
|
@ -9,4 +9,24 @@ export interface ExportV1 extends ExportBase {
|
|||
folders: FolderCollection;
|
||||
}
|
||||
|
||||
export type OpenAIChat = {
|
||||
title: string;
|
||||
mapping: {
|
||||
[key: string]: {
|
||||
id: string;
|
||||
message: {
|
||||
author: {
|
||||
role: Role;
|
||||
};
|
||||
content: {
|
||||
parts: string[];
|
||||
};
|
||||
} | null;
|
||||
parent: string | null;
|
||||
children: string[];
|
||||
};
|
||||
};
|
||||
current_node: string;
|
||||
};
|
||||
|
||||
export default ExportV1;
|
||||
|
|
|
@ -12,7 +12,7 @@ import {
|
|||
modelOptions,
|
||||
_defaultChatConfig,
|
||||
} from '@constants/chat';
|
||||
import { ExportV1 } from '@type/export';
|
||||
import { ExportV1, OpenAIChat } from '@type/export';
|
||||
|
||||
export const validateAndFixChats = (chats: any): chats is ChatInterface[] => {
|
||||
if (!Array.isArray(chats)) return false;
|
||||
|
@ -88,3 +88,45 @@ export const validateFolders = (
|
|||
export const validateExportV1 = (data: ExportV1): data is ExportV1 => {
|
||||
return validateAndFixChats(data.chats) && validateFolders(data.folders);
|
||||
};
|
||||
|
||||
// Convert OpenAI chat format to BetterChatGPT format
|
||||
export const convertOpenAIToBetterChatGPTFormat = (
|
||||
openAIChat: OpenAIChat
|
||||
): ChatInterface => {
|
||||
const messages: MessageInterface[] = [];
|
||||
|
||||
// Traverse the chat tree and collect messages
|
||||
const traverseTree = (id: string) => {
|
||||
const node = openAIChat.mapping[id];
|
||||
|
||||
// Extract message if it exists
|
||||
if (node.message) {
|
||||
const { role } = node.message.author;
|
||||
const content = node.message.content.parts.join('');
|
||||
if (content.length > 0) messages.push({ role, content });
|
||||
}
|
||||
|
||||
// Traverse the last child node if any children exist
|
||||
if (node.children.length > 0) {
|
||||
traverseTree(node.children[node.children.length - 1]);
|
||||
}
|
||||
};
|
||||
|
||||
// Start traversing the tree from the root node
|
||||
const rootNode = openAIChat.mapping[Object.keys(openAIChat.mapping)[0]].id;
|
||||
traverseTree(rootNode);
|
||||
|
||||
// Return the chat interface object
|
||||
return {
|
||||
id: uuidv4(),
|
||||
title: openAIChat.title,
|
||||
messages,
|
||||
config: _defaultChatConfig,
|
||||
titleSet: true,
|
||||
};
|
||||
};
|
||||
|
||||
// Import OpenAI chat data and convert it to BetterChatGPT format
|
||||
export const importOpenAIChatExport = (openAIChatExport: OpenAIChat[]) => {
|
||||
return openAIChatExport.map(convertOpenAIToBetterChatGPTFormat);
|
||||
};
|
||||
|
|
Loading…
Reference in a new issue