From ece4778f88ba2d3efe5c8714ce7ce323ddda75e4 Mon Sep 17 00:00:00 2001 From: Jing Hua Date: Fri, 21 Apr 2023 17:20:38 +0800 Subject: [PATCH] feat: import openai chatgpt export --- .../ImportExportChat/ExportChat.tsx | 35 +++ .../ImportExportChat/ImportChat.tsx | 173 +++++++++++++++ .../ImportExportChat/ImportChatOpenAI.tsx | 78 +++++++ .../ImportExportChat/ImportExportChat.tsx | 200 +----------------- src/types/export.ts | 22 +- src/utils/import.ts | 44 +++- 6 files changed, 357 insertions(+), 195 deletions(-) create mode 100644 src/components/ImportExportChat/ExportChat.tsx create mode 100644 src/components/ImportExportChat/ImportChat.tsx create mode 100644 src/components/ImportExportChat/ImportChatOpenAI.tsx diff --git a/src/components/ImportExportChat/ExportChat.tsx b/src/components/ImportExportChat/ExportChat.tsx new file mode 100644 index 0000000..c4ef881 --- /dev/null +++ b/src/components/ImportExportChat/ExportChat.tsx @@ -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 ( +
+
+ {t('export')} (JSON) +
+ +
+ ); +}; +export default ExportChat; diff --git a/src/components/ImportExportChat/ImportChat.tsx b/src/components/ImportExportChat/ImportChat.tsx new file mode 100644 index 0000000..6562dd5 --- /dev/null +++ b/src/components/ImportExportChat/ImportChat.tsx @@ -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(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 = {}; + 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 ( + <> + + + + {alert && ( +
+ {alert.message} +
+ )} + + ); +}; + +export default ImportChat; diff --git a/src/components/ImportExportChat/ImportChatOpenAI.tsx b/src/components/ImportExportChat/ImportChatOpenAI.tsx new file mode 100644 index 0000000..734aadb --- /dev/null +++ b/src/components/ImportExportChat/ImportChatOpenAI.tsx @@ -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>; +}) => { + const { t } = useTranslation(); + + const inputRef = useRef(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 ( + <> +
+ {t('import')} OpenAI ChatGPT {t('export')} +
+ + + + + ); +}; + +export default ImportChatOpenAI; diff --git a/src/components/ImportExportChat/ImportExportChat.tsx b/src/components/ImportExportChat/ImportExportChat.tsx index bdad4cd..8426fe6 100644 --- a/src/components/ImportExportChat/ImportExportChat.tsx +++ b/src/components/ImportExportChat/ImportExportChat.tsx @@ -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 = () => {
+
+
)} @@ -46,185 +41,4 @@ const ImportExportChat = () => { ); }; -const ImportChat = () => { - const { t } = useTranslation(); - const setChats = useStore.getState().setChats; - const setFolders = useStore.getState().setFolders; - const inputRef = useRef(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 = {}; - 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 ( - <> - - - - {alert && ( -
- {alert.message} -
- )} - - ); -}; - -const ExportChat = () => { - const { t } = useTranslation(); - - return ( -
-
- {t('export')} (JSON) -
- -
- ); -}; - export default ImportExportChat; diff --git a/src/types/export.ts b/src/types/export.ts index 0c93b74..e32b75f 100644 --- a/src/types/export.ts +++ b/src/types/export.ts @@ -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; diff --git a/src/utils/import.ts b/src/utils/import.ts index c5dc8d2..8a43bf1 100644 --- a/src/utils/import.ts +++ b/src/utils/import.ts @@ -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); +};