mirror of
https://github.com/NovaOSS/nova-betterchat.git
synced 2024-11-25 17:43:58 +01:00
parent
f6c2976cbd
commit
9956d254f4
|
@ -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,33 +68,100 @@ const ImportChat = () => {
|
|||
|
||||
try {
|
||||
const parsedData = JSON.parse(data);
|
||||
if (validateAndFixChats(parsedData)) {
|
||||
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,
|
||||
]);
|
||||
if (isLegacyImport(parsedData)) {
|
||||
if (validateAndFixChats(parsedData)) {
|
||||
// import new folders
|
||||
const folderNameToIdMap: Record<string, string> = {};
|
||||
const parsedFolders: string[] = [];
|
||||
|
||||
const prevChats = useStore.getState().chats;
|
||||
if (prevChats) {
|
||||
const updatedChats: ChatInterface[] = JSON.parse(
|
||||
JSON.stringify(prevChats)
|
||||
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 };
|
||||
},
|
||||
{}
|
||||
);
|
||||
setChats(parsedData.concat(updatedChats));
|
||||
|
||||
// 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 {
|
||||
setChats(parsedData);
|
||||
setAlert({
|
||||
message: 'Invalid chats data format',
|
||||
success: false,
|
||||
});
|
||||
}
|
||||
setAlert({ message: 'Succesfully imported!', success: true });
|
||||
} else {
|
||||
setAlert({ message: 'Invalid chats data format', success: false });
|
||||
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')}
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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' />
|
||||
|
|
|
@ -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 (
|
||||
|
|
|
@ -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 (
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
}));
|
||||
},
|
||||
});
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
12
src/types/export.ts
Normal 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;
|
|
@ -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
90
src/utils/import.ts
Normal 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);
|
||||
};
|
Loading…
Reference in a new issue