Refactor folder (#188)

* refactor folders

* add chat id
This commit is contained in:
Jing Hua 2023-04-02 16:27:19 +08:00 committed by GitHub
parent f6c2976cbd
commit 9956d254f4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 390 additions and 193 deletions

View file

@ -1,13 +1,19 @@
import React, { useRef, 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 { validateAndFixChats } from '@utils/chat';
import { ChatInterface } from '@type/chat';
import {
isLegacyImport,
validateAndFixChats,
validateExportV1,
} from '@utils/import';
import { ChatInterface, Folder, FolderCollection } from '@type/chat';
import Export, { ExportBase, ExportV1 } from '@type/export';
const ImportExportChat = () => {
const { t } = useTranslation();
@ -43,8 +49,7 @@ const ImportExportChat = () => {
const ImportChat = () => {
const { t } = useTranslation();
const setChats = useStore.getState().setChats;
const setFoldersName = useStore.getState().setFoldersName;
const setFoldersExpanded = useStore.getState().setFoldersExpanded;
const setFolders = useStore.getState().setFolders;
const inputRef = useRef<HTMLInputElement>(null);
const [alert, setAlert] = useState<{
message: string;
@ -63,21 +68,46 @@ const ImportChat = () => {
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) => {
if (data.folder && !parsedFolders.includes(data.folder))
parsedFolders.push(data.folder);
});
setFoldersName([
...parsedFolders,
...useStore.getState().foldersName,
]);
setFoldersExpanded([
...new Array(parsedFolders.length).fill(false),
...useStore.getState().foldersExpanded,
]);
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(
@ -89,7 +119,49 @@ const ImportChat = () => {
}
setAlert({ message: 'Succesfully imported!', success: true });
} else {
setAlert({ message: 'Invalid chats data format', success: false });
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 });
@ -132,7 +204,7 @@ const ImportChat = () => {
const ExportChat = () => {
const { t } = useTranslation();
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'>
@ -141,7 +213,12 @@ const ExportChat = () => {
<button
className='btn btn-small btn-primary'
onClick={() => {
if (chats) downloadFile(chats, getToday());
const fileData: Export = {
chats: useStore.getState().chats,
folders: useStore.getState().folders,
version: 1,
};
downloadFile(fileData, getToday());
}}
>
{t('export')}

View file

@ -1,9 +1,13 @@
import React, { useRef, useState } from 'react';
import React, { useEffect, useRef, useState } from 'react';
import useStore from '@store/store';
import DownChevronArrow from '@icon/DownChevronArrow';
import FolderIcon from '@icon/FolderIcon';
import { ChatHistoryInterface, ChatInterface } from '@type/chat';
import {
ChatHistoryInterface,
ChatInterface,
FolderCollection,
} from '@type/chat';
import ChatHistory from './ChatHistory';
import EditIcon from '@icon/EditIcon';
@ -12,18 +16,17 @@ import CrossIcon from '@icon/CrossIcon';
import TickIcon from '@icon/TickIcon';
const ChatFolder = ({
folderName,
folderChats,
folderIndex,
folderId,
}: {
folderName: string;
folderChats: ChatHistoryInterface[];
folderIndex: number;
folderId: string;
}) => {
const folderName = useStore((state) => state.folders[folderId].name);
const isExpanded = useStore((state) => state.folders[folderId].expanded);
const setChats = useStore((state) => state.setChats);
const setFoldersName = useStore((state) => state.setFoldersName);
const setFoldersExpanded = useStore((state) => state.setFoldersExpanded);
const foldersExpanded = useStore((state) => state.foldersExpanded);
const setFolders = useStore((state) => state.setFolders);
const inputRef = useRef<HTMLInputElement>(null);
@ -33,20 +36,11 @@ const ChatFolder = ({
const [isHover, setIsHover] = useState<boolean>(false);
const editTitle = () => {
const updatedChats: ChatInterface[] = JSON.parse(
JSON.stringify(useStore.getState().chats)
const updatedFolders: FolderCollection = JSON.parse(
JSON.stringify(useStore.getState().folders)
);
updatedChats.forEach((chat) => {
if (chat.folder === folderName) chat.folder = _folderName;
});
setChats(updatedChats);
const updatedFolderNames = [...useStore.getState().foldersName];
const pos = updatedFolderNames.indexOf(folderName);
if (pos !== -1) updatedFolderNames[pos] = _folderName;
setFoldersName(updatedFolderNames);
updatedFolders[folderId].name = _folderName;
setFolders(updatedFolders);
setIsEdit(false);
};
@ -55,19 +49,16 @@ const ChatFolder = ({
JSON.stringify(useStore.getState().chats)
);
updatedChats.forEach((chat) => {
if (chat.folder === folderName) delete chat.folder;
if (chat.folder === folderId) delete chat.folder;
});
setChats(updatedChats);
const updatedFoldersName = [...useStore.getState().foldersName];
const updatedFoldersExpanded = [...useStore.getState().foldersExpanded];
const updatedFolders: FolderCollection = JSON.parse(
JSON.stringify(useStore.getState().folders)
);
delete updatedFolders[folderId];
setFolders(updatedFolders);
const i = updatedFoldersName.findIndex((name) => name === folderName);
updatedFoldersName.splice(i, 1);
updatedFoldersExpanded.splice(i, 1);
setFoldersName(updatedFoldersName);
setFoldersExpanded(updatedFoldersExpanded);
setIsDelete(false);
};
@ -95,15 +86,19 @@ const ChatFolder = ({
e.stopPropagation();
setIsHover(false);
const updatedFoldersExpanded = [...foldersExpanded];
updatedFoldersExpanded[folderIndex] = true;
setFoldersExpanded(updatedFoldersExpanded);
// expand folder on drop
const updatedFolders: FolderCollection = JSON.parse(
JSON.stringify(useStore.getState().folders)
);
updatedFolders[folderId].expanded = true;
setFolders(updatedFolders);
// update chat folderId to new folderId
const chatIndex = Number(e.dataTransfer.getData('chatIndex'));
const updatedChats: ChatInterface[] = JSON.parse(
JSON.stringify(useStore.getState().chats)
);
updatedChats[chatIndex].folder = folderName;
updatedChats[chatIndex].folder = folderId;
setChats(updatedChats);
}
};
@ -119,11 +114,17 @@ const ChatFolder = ({
};
const toggleExpanded = () => {
const updatedFoldersExpanded = [...foldersExpanded];
updatedFoldersExpanded[folderIndex] = !updatedFoldersExpanded[folderIndex];
setFoldersExpanded(updatedFoldersExpanded);
const updatedFolders: FolderCollection = JSON.parse(
JSON.stringify(useStore.getState().folders)
);
updatedFolders[folderId].expanded = !updatedFolders[folderId].expanded;
setFolders(updatedFolders);
};
useEffect(() => {
if (inputRef && inputRef.current) inputRef.current.focus();
}, [isEdit]);
return (
<div
className={`w-full transition-colors ${isHover ? 'bg-gray-800/40' : ''}`}
@ -154,7 +155,7 @@ const ChatFolder = ({
)}
</div>
<div
className='absolute flex right-1 z-10 text-gray-300 visible'
className='flex text-gray-300'
onClick={(e) => e.stopPropagation()}
>
{isDelete || isEdit ? (
@ -183,7 +184,7 @@ const ChatFolder = ({
<button className='p-1 hover:text-white' onClick={toggleExpanded}>
<DownChevronArrow
className={`${
foldersExpanded[folderIndex] ? 'rotate-180' : ''
isExpanded ? 'rotate-180' : ''
} transition-transform`}
/>
</button>
@ -192,7 +193,7 @@ const ChatFolder = ({
</div>
</div>
<div className='ml-3 pl-1 border-l-2 border-gray-700 flex flex-col gap-1'>
{foldersExpanded[folderIndex] &&
{isExpanded &&
folderChats.map((chat) => (
<ChatHistory
title={chat.title}

View file

@ -11,54 +11,71 @@ import {
ChatHistoryInterface,
ChatHistoryFolderInterface,
ChatInterface,
FolderCollection,
} from '@type/chat';
const ChatHistoryList = () => {
const currentChatIndex = useStore((state) => state.currentChatIndex);
const setChats = useStore((state) => state.setChats);
const setFolders = useStore((state) => state.setFolders);
const chatTitles = useStore(
(state) => state.chats?.map((chat) => chat.title),
shallow
);
const [isHover, setIsHover] = useState<boolean>(false);
const [folders, setFolders] = useState<ChatHistoryFolderInterface>({});
const [noFolders, setNoFolders] = useState<ChatHistoryInterface[]>([]);
const [chatFolders, setChatFolders] = useState<ChatHistoryFolderInterface>(
{}
);
const [noChatFolders, setNoChatFolders] = useState<ChatHistoryInterface[]>(
[]
);
const [filter, setFilter] = useState<string>('');
const chatsRef = useRef<ChatInterface[]>(useStore.getState().chats || []);
const foldersNameRef = useRef<string[]>(useStore.getState().foldersName);
const foldersRef = useRef<FolderCollection>(useStore.getState().folders);
const filterRef = useRef<string>(filter);
const updateFolders = useRef(() => {
const _folders: ChatHistoryFolderInterface = {};
const _noFolders: ChatHistoryInterface[] = [];
const chats = useStore.getState().chats;
const foldersName = useStore.getState().foldersName;
const folders = useStore.getState().folders;
foldersName.forEach((f) => (_folders[f] = []));
Object.values(folders)
.sort((a, b) => a.order - b.order)
.forEach((f) => (_folders[f.id] = []));
if (chats) {
chats.forEach((chat, index) => {
const filterLowerCase = filterRef.current.toLowerCase();
const _filterLowerCase = filterRef.current.toLowerCase();
const _chatTitle = chat.title.toLowerCase();
const _chatFolderName = chat.folder
? folders[chat.folder].name.toLowerCase()
: '';
if (
!chat.title.toLocaleLowerCase().includes(filterLowerCase) &&
!chat.folder?.toLowerCase().includes(filterLowerCase) &&
!_chatTitle.includes(_filterLowerCase) &&
!_chatFolderName.includes(_filterLowerCase) &&
index !== useStore.getState().currentChatIndex
)
return;
if (!chat.folder) {
_noFolders.push({ title: chat.title, index: index });
_noFolders.push({ title: chat.title, index: index, id: chat.id });
} else {
if (!_folders[chat.folder]) _folders[chat.folder] = [];
_folders[chat.folder].push({ title: chat.title, index: index });
if (!_folders[chat.folder]) _folders[_chatFolderName] = [];
_folders[chat.folder].push({
title: chat.title,
index: index,
id: chat.id,
});
}
});
}
setFolders(_folders);
setNoFolders(_noFolders);
setChatFolders(_folders);
setNoChatFolders(_noFolders);
}).current;
useEffect(() => {
@ -72,9 +89,9 @@ const ChatHistoryList = () => {
) {
updateFolders();
chatsRef.current = state.chats;
} else if (state.foldersName !== foldersNameRef.current) {
} else if (state.folders !== foldersRef.current) {
updateFolders();
foldersNameRef.current = state.foldersName;
foldersRef.current = state.folders;
}
});
}, []);
@ -85,20 +102,21 @@ const ChatHistoryList = () => {
currentChatIndex >= 0 &&
currentChatIndex < chatTitles.length
) {
// set title
document.title = chatTitles[currentChatIndex];
// expand folder of current chat
const chats = useStore.getState().chats;
if (chats) {
const folderIndex = useStore
.getState()
.foldersName.findIndex((f) => f === chats[currentChatIndex].folder);
const folderId = chats[currentChatIndex].folder;
if (folderIndex) {
const updatedFolderExpanded = [
...useStore.getState().foldersExpanded,
];
updatedFolderExpanded[folderIndex] = true;
useStore.getState().setFoldersExpanded(updatedFolderExpanded);
if (folderId) {
const updatedFolders: FolderCollection = JSON.parse(
JSON.stringify(useStore.getState().folders)
);
updatedFolders[folderId].expanded = true;
setFolders(updatedFolders);
}
}
}
@ -149,20 +167,15 @@ const ChatHistoryList = () => {
<NewFolder />
<ChatSearch filter={filter} setFilter={setFilter} />
<div className='flex flex-col gap-2 text-gray-100 text-sm'>
{Object.keys(folders).map((folderName, folderIndex) => (
{Object.keys(chatFolders).map((folderId) => (
<ChatFolder
folderName={folderName}
folderChats={folders[folderName]}
folderIndex={folderIndex}
key={folderName}
folderChats={chatFolders[folderId]}
folderId={folderId}
key={folderId}
/>
))}
{noFolders.map(({ title, index }) => (
<ChatHistory
title={title}
key={`${title}-${index}`}
chatIndex={index}
/>
{noChatFolders.map(({ title, index, id }) => (
<ChatHistory title={title} key={`${title}-${id}`} chatIndex={index} />
))}
</div>
<div className='w-full h-10' />

View file

@ -10,15 +10,13 @@ const ClearConversation = () => {
const { t } = useTranslation();
const initialiseNewChat = useInitialiseNewChat();
const setFoldersName = useStore((state) => state.setFoldersName);
const setFoldersExpanded = useStore((state) => state.setFoldersExpanded);
const setFolders = useStore((state) => state.setFolders);
const [isModalOpen, setIsModalOpen] = useState<boolean>(false);
const handleConfirm = () => {
setIsModalOpen(false);
setFoldersName([]);
setFoldersExpanded([]);
initialiseNewChat();
setFolders({});
};
return (

View file

@ -1,30 +1,44 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import { v4 as uuidv4 } from 'uuid';
import useStore from '@store/store';
import NewFolderIcon from '@icon/NewFolderIcon';
import { Folder, FolderCollection } from '@type/chat';
const NewFolder = () => {
const { t } = useTranslation();
const generating = useStore((state) => state.generating);
const setFoldersName = useStore((state) => state.setFoldersName);
const setFoldersExpanded = useStore((state) => state.setFoldersExpanded);
const setFolders = useStore((state) => state.setFolders);
const addFolder = () => {
let folderIndex = 1;
let name = `New Folder ${folderIndex}`;
while (
useStore
.getState()
.foldersName.some((_folderName) => _folderName === name)
) {
const folders = useStore.getState().folders;
while (Object.values(folders).some((folder) => folder.name === name)) {
folderIndex += 1;
name = `New Folder ${folderIndex}`;
}
setFoldersName([name, ...useStore.getState().foldersName]);
setFoldersExpanded([false, ...useStore.getState().foldersExpanded]);
const updatedFolders: FolderCollection = JSON.parse(
JSON.stringify(folders)
);
const id = uuidv4();
const newFolder: Folder = {
id,
name,
expanded: false,
order: 0,
};
Object.values(updatedFolders).forEach((folder) => {
folder.order += 1;
});
setFolders({ [id]: newFolder, ...updatedFolders });
};
return (

View file

@ -1,3 +1,4 @@
import { v4 as uuidv4 } from 'uuid';
import { ChatInterface, ConfigInterface, ModelOptions } from '@type/chat';
import useStore from '@store/store';
@ -55,6 +56,7 @@ export const _defaultChatConfig: ConfigInterface = {
};
export const generateDefaultChat = (title?: string): ChatInterface => ({
id: uuidv4(),
title: title ? title : 'New Chat',
messages:
useStore.getState().defaultSystemMessage.length > 0

View file

@ -1,5 +1,5 @@
import { StoreSlice } from './store';
import { ChatInterface, MessageInterface } from '@type/chat';
import { ChatInterface, FolderCollection, MessageInterface } from '@type/chat';
export interface ChatSlice {
messages: MessageInterface[];
@ -7,15 +7,13 @@ export interface ChatSlice {
currentChatIndex: number;
generating: boolean;
error: string;
foldersName: string[];
foldersExpanded: boolean[];
folders: FolderCollection;
setMessages: (messages: MessageInterface[]) => void;
setChats: (chats: ChatInterface[]) => void;
setCurrentChatIndex: (currentChatIndex: number) => void;
setGenerating: (generating: boolean) => void;
setError: (error: string) => void;
setFoldersName: (foldersName: string[]) => void;
setFoldersExpanded: (foldersExpanded: boolean[]) => void;
setFolders: (folders: FolderCollection) => void;
}
export const createChatSlice: StoreSlice<ChatSlice> = (set, get) => ({
@ -23,8 +21,7 @@ export const createChatSlice: StoreSlice<ChatSlice> = (set, get) => ({
currentChatIndex: -1,
generating: false,
error: '',
foldersName: [],
foldersExpanded: [],
folders: {},
setMessages: (messages: MessageInterface[]) => {
set((prev: ChatSlice) => ({
...prev,
@ -55,16 +52,10 @@ export const createChatSlice: StoreSlice<ChatSlice> = (set, get) => ({
error: error,
}));
},
setFoldersName: (foldersName: string[]) => {
setFolders: (folders: FolderCollection) => {
set((prev: ChatSlice) => ({
...prev,
foldersName: foldersName,
}));
},
setFoldersExpanded: (foldersExpanded: boolean[]) => {
set((prev: ChatSlice) => ({
...prev,
foldersExpanded: foldersExpanded,
folders: folders,
}));
},
});

View file

@ -1,4 +1,8 @@
import { v4 as uuidv4 } from 'uuid';
import {
Folder,
FolderCollection,
LocalStorageInterfaceV0ToV1,
LocalStorageInterfaceV1ToV2,
LocalStorageInterfaceV2ToV3,
@ -6,6 +10,7 @@ import {
LocalStorageInterfaceV4ToV5,
LocalStorageInterfaceV5ToV6,
LocalStorageInterfaceV6ToV7,
LocalStorageInterfaceV7oV8,
} from '@type/chat';
import {
_defaultChatConfig,
@ -73,3 +78,29 @@ export const migrateV6 = (persistedState: LocalStorageInterfaceV6ToV7) => {
if (!persistedState.apiKey || persistedState.apiKey.length === 0)
persistedState.apiKey = '';
};
export const migrateV7 = (persistedState: LocalStorageInterfaceV7oV8) => {
let folders: FolderCollection = {};
const folderNameToIdMap: Record<string, string> = {};
// convert foldersExpanded and foldersName to folders
persistedState.foldersName.forEach((name, index) => {
const id = uuidv4();
const folder: Folder = {
id,
name,
expanded: persistedState.foldersExpanded[index],
order: index,
};
folders = { [id]: folder, ...folders };
folderNameToIdMap[name] = id;
});
persistedState.folders = folders;
// change the chat.folder from name to id
persistedState.chats.forEach((chat) => {
if (chat.folder) chat.folder = folderNameToIdMap[chat.folder];
chat.id = uuidv4();
});
};

View file

@ -13,6 +13,7 @@ import {
LocalStorageInterfaceV4ToV5,
LocalStorageInterfaceV5ToV6,
LocalStorageInterfaceV6ToV7,
LocalStorageInterfaceV7oV8,
} from '@type/chat';
import {
migrateV0,
@ -22,6 +23,7 @@ import {
migrateV4,
migrateV5,
migrateV6,
migrateV7,
} from './migrate';
export type StoreState = ChatSlice &
@ -59,11 +61,10 @@ const useStore = create<StoreState>()(
hideMenuOptions: state.hideMenuOptions,
firstVisit: state.firstVisit,
hideSideMenu: state.hideSideMenu,
foldersName: state.foldersName,
foldersExpanded: state.foldersExpanded,
folders: state.folders,
enterToSubmit: state.enterToSubmit,
}),
version: 7,
version: 8,
migrate: (persistedState, version) => {
switch (version) {
case 0:
@ -80,6 +81,8 @@ const useStore = create<StoreState>()(
migrateV5(persistedState as LocalStorageInterfaceV5ToV6);
case 6:
migrateV6(persistedState as LocalStorageInterfaceV6ToV7);
case 7:
migrateV7(persistedState as LocalStorageInterfaceV7oV8);
break;
}
return persistedState as StoreState;

View file

@ -10,6 +10,7 @@ export interface MessageInterface {
}
export interface ChatInterface {
id: string;
title: string;
folder?: string;
messages: MessageInterface[];
@ -29,10 +30,23 @@ export interface ConfigInterface {
export interface ChatHistoryInterface {
title: string;
index: number;
id: string;
}
export interface ChatHistoryFolderInterface {
[folderName: string]: ChatHistoryInterface[];
[folderId: string]: ChatHistoryInterface[];
}
export interface FolderCollection {
[folderId: string]: Folder;
}
export interface Folder {
id: string;
name: string;
expanded: boolean;
order: number;
color?: string;
}
export type ModelOptions = 'gpt-4' | 'gpt-4-32k' | 'gpt-3.5-turbo';
@ -120,3 +134,10 @@ export interface LocalStorageInterfaceV6ToV7 {
firstVisit: boolean;
hideSideMenu: boolean;
}
export interface LocalStorageInterfaceV7oV8
extends LocalStorageInterfaceV6ToV7 {
foldersName: string[];
foldersExpanded: boolean[];
folders: FolderCollection;
}

12
src/types/export.ts Normal file
View file

@ -0,0 +1,12 @@
import { ChatInterface, FolderCollection } from './chat';
export interface ExportBase {
version: number;
}
export interface ExportV1 extends ExportBase {
chats?: ChatInterface[];
folders: FolderCollection;
}
export default ExportV1;

View file

@ -1,63 +1,7 @@
import html2canvas from 'html2canvas';
// import jsPDF from 'jspdf';
import { ChatInterface, ConfigInterface, MessageInterface } from '@type/chat';
import { roles } from '@type/chat';
import { ChatInterface } from '@type/chat';
import { Theme } from '@type/theme';
import {
defaultModel,
modelOptions,
_defaultChatConfig,
} from '@constants/chat';
export const validateAndFixChats = (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 (chat.titleSet === undefined) chat.titleSet = false;
if (!(typeof chat.titleSet === 'boolean')) return false;
if (!validateMessage(chat.messages)) return false;
if (!validateAndFixChatConfig(chat.config)) return false;
}
return true;
};
const validateMessage = (messages: MessageInterface[]) => {
if (!Array.isArray(messages)) return false;
for (const message of messages) {
if (!(typeof message.content === 'string')) return false;
if (!(typeof message.role === 'string')) return false;
if (!roles.includes(message.role)) return false;
}
return true;
};
const validateAndFixChatConfig = (config: ConfigInterface) => {
if (config === undefined) config = _defaultChatConfig;
if (!(typeof config === 'object')) return false;
if (!config.temperature) config.temperature = _defaultChatConfig.temperature;
if (!(typeof config.temperature === 'number')) return false;
if (!config.presence_penalty)
config.presence_penalty = _defaultChatConfig.presence_penalty;
if (!(typeof config.presence_penalty === 'number')) return false;
if (!config.top_p) config.top_p = _defaultChatConfig.top_p;
if (!(typeof config.top_p === 'number')) return false;
if (!config.frequency_penalty)
config.frequency_penalty = _defaultChatConfig.frequency_penalty;
if (!(typeof config.frequency_penalty === 'number')) return false;
if (!config.model) config.model = defaultModel;
if (!modelOptions.includes(config.model)) return false;
return true;
};
export const htmlToImg = async (html: HTMLDivElement) => {
const needResize = window.innerWidth >= 1024;

90
src/utils/import.ts Normal file
View file

@ -0,0 +1,90 @@
import { v4 as uuidv4 } from 'uuid';
import {
ChatInterface,
ConfigInterface,
FolderCollection,
MessageInterface,
} from '@type/chat';
import { roles } from '@type/chat';
import {
defaultModel,
modelOptions,
_defaultChatConfig,
} from '@constants/chat';
import { ExportV1 } from '@type/export';
export const validateAndFixChats = (chats: any): chats is ChatInterface[] => {
if (!Array.isArray(chats)) return false;
for (const chat of chats) {
if (!(typeof chat.id === 'string')) chat.id = uuidv4();
if (!(typeof chat.title === 'string') || chat.title === '') return false;
if (chat.titleSet === undefined) chat.titleSet = false;
if (!(typeof chat.titleSet === 'boolean')) return false;
if (!validateMessage(chat.messages)) return false;
if (!validateAndFixChatConfig(chat.config)) return false;
}
return true;
};
const validateMessage = (messages: MessageInterface[]) => {
if (!Array.isArray(messages)) return false;
for (const message of messages) {
if (!(typeof message.content === 'string')) return false;
if (!(typeof message.role === 'string')) return false;
if (!roles.includes(message.role)) return false;
}
return true;
};
const validateAndFixChatConfig = (config: ConfigInterface) => {
if (config === undefined) config = _defaultChatConfig;
if (!(typeof config === 'object')) return false;
if (!config.temperature) config.temperature = _defaultChatConfig.temperature;
if (!(typeof config.temperature === 'number')) return false;
if (!config.presence_penalty)
config.presence_penalty = _defaultChatConfig.presence_penalty;
if (!(typeof config.presence_penalty === 'number')) return false;
if (!config.top_p) config.top_p = _defaultChatConfig.top_p;
if (!(typeof config.top_p === 'number')) return false;
if (!config.frequency_penalty)
config.frequency_penalty = _defaultChatConfig.frequency_penalty;
if (!(typeof config.frequency_penalty === 'number')) return false;
if (!config.model) config.model = defaultModel;
if (!modelOptions.includes(config.model)) return false;
return true;
};
export const isLegacyImport = (importedData: any) => {
if (Array.isArray(importedData)) return true;
return false;
};
export const validateFolders = (
folders: FolderCollection
): folders is FolderCollection => {
if (typeof folders !== 'object') return false;
for (const folderId in folders) {
if (typeof folders[folderId].id !== 'string') return false;
if (typeof folders[folderId].name !== 'string') return false;
if (typeof folders[folderId].order !== 'number') return false;
if (typeof folders[folderId].expanded !== 'boolean') return false;
}
return true;
};
export const validateExportV1 = (data: ExportV1): data is ExportV1 => {
return validateAndFixChats(data.chats) && validateFolders(data.folders);
};