Skip to content

Commit

Permalink
fix(chat): fix automigration ui and continue without backup button (I…
Browse files Browse the repository at this point in the history
…ssue #265) (#672)
  • Loading branch information
Alexander-Kezik authored Feb 12, 2024
1 parent b21c192 commit 3b3293f
Show file tree
Hide file tree
Showing 9 changed files with 118 additions and 83 deletions.
9 changes: 5 additions & 4 deletions apps/chat/src/components/Chat/Migration/Migration.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,12 @@ export const Migration = ({ total, uploaded }: Props) => {
return (
<div className="flex h-full flex-col items-center justify-center">
<Spinner className="h-auto" size={60} />
<p className="mt-7 text-center text-2xl font-semibold md:text-3xl">
<h1 className="mt-7 text-2xl font-semibold md:text-3xl">
{t('Migration')}
</h1>
<p className="mt-7 text-center text-base md:text-xl">
{uploaded} {t('out of')} {total} <br />
<span className="text-base md:text-xl">
{t('conversations and prompts are loaded')}
</span>
{t('conversations and prompts are loaded')}
</p>
<div className="my-7 h-[1px] w-[80px] bg-controls-disable"></div>
<p className="text-base md:text-xl">
Expand Down
138 changes: 79 additions & 59 deletions apps/chat/src/components/Chat/Migration/MigrationFailedModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,19 +46,21 @@ const ItemsList = <T extends Conversation | Prompt>({
setEntitiesToRetryIds(entitiesToRetryIds.filter((id) => id !== entityId));
};

return failedMigratedEntities.length ? (
if (!failedMigratedEntities.length) return null;

return (
<div className={classNames('mt-2', withPt && 'pt-2')}>
<ul className="flex flex-col gap-0.5">
{failedMigratedEntities.map((entity) => (
<li
className="relative flex h-[30px] items-center justify-between rounded"
className="flex h-[30px] items-center justify-between rounded"
key={entity.id}
>
<div className="flex">
<div className="flex min-w-0">
{getModelIcon(entity)}
<p className="ml-2">{entity.name}</p>
<p className="ml-2 truncate">{entity.name}</p>
</div>
<div className="flex w-[100px] items-center justify-around">
<div className="flex min-w-[100px] items-center justify-around">
<div
onClick={() => handleSelect(entity.id)}
className="relative flex size-[18px] group-hover/file-item:flex"
Expand Down Expand Up @@ -94,14 +96,48 @@ const ItemsList = <T extends Conversation | Prompt>({
))}
</ul>
</div>
) : null;
);
};

interface Props {
failedMigratedConversations: Conversation[];
failedMigratedPrompts: Prompt[];
}

interface AllItemsCheckboxesProps {
isChecked: boolean;
isCheckIcon: boolean;
isMinusIcon: boolean;
onSelectHandler: () => void;
}

const AllItemsCheckboxes = ({
isChecked,
isCheckIcon,
isMinusIcon,
onSelectHandler,
}: AllItemsCheckboxesProps) => {
const Icon = isCheckIcon ? IconCheck : isMinusIcon ? IconMinus : null;

return (
<div className="relative flex size-[18px] group-hover/file-item:flex">
<input
className="checkbox peer size-[18px] bg-transparent"
type="checkbox"
onClick={onSelectHandler}
readOnly
checked={isChecked}
/>
{Icon && (
<Icon
size={18}
className="pointer-events-none invisible absolute text-accent-primary peer-checked:visible"
/>
)}
</div>
);
};

export const MigrationFailedWindow = ({
failedMigratedConversations,
failedMigratedPrompts,
Expand Down Expand Up @@ -134,28 +170,14 @@ export const MigrationFailedWindow = ({
setPromptsToRetryIds(failedMigratedPrompts.map((prompt) => prompt.id));
}, [failedMigratedPrompts]);

const onSkipAll = useCallback(() => {
dispatch(
ConversationsActions.skipFailedMigratedConversations({
idsToMarkAsMigrated: failedMigratedConversations.map((conv) => conv.id),
}),
);
dispatch(
PromptsActions.skipFailedMigratedPrompts({
idsToMarkAsMigrated: failedMigratedPrompts.map((prompt) => prompt.id),
}),
);
}, [dispatch, failedMigratedConversations, failedMigratedPrompts]);

const onRetry = useCallback(() => {
const retryMigration = useCallback(() => {
const failedMigratedConversationIds = failedMigratedConversations.map(
(conv) => conv.id,
);
const failedMigratedPromptIds = failedMigratedPrompts.map(
(conv) => conv.id,
);

dispatch(ImportExportActions.exportLocalStorageEntities());
dispatch(
ConversationsActions.skipFailedMigratedConversations({
idsToMarkAsMigrated: failedMigratedConversationIds.filter(
Expand All @@ -170,8 +192,9 @@ export const MigrationFailedWindow = ({
),
}),
);
dispatch(ConversationsActions.migrateConversations());
dispatch(PromptsActions.migratePrompts());

dispatch(ConversationsActions.migrateConversationsIfRequired());
dispatch(PromptsActions.migratePromptsIfRequired());
}, [
conversationsToRetryIds,
dispatch,
Expand All @@ -180,6 +203,15 @@ export const MigrationFailedWindow = ({
promptsToRetryIds,
]);

const onRetryWithoutBackup = useCallback(() => {
retryMigration();
}, [retryMigration]);

const onRetryWithBackup = useCallback(() => {
dispatch(ImportExportActions.exportLocalStorageEntities());
retryMigration();
}, [dispatch, retryMigration]);

const onSelectAll = useCallback(() => {
setConversationsToRetryIds(
failedMigratedConversations.map((conv) => conv.id),
Expand All @@ -192,6 +224,15 @@ export const MigrationFailedWindow = ({
setPromptsToRetryIds([]);
};

const isAllItemsSelected =
conversationsToRetryIds.length + promptsToRetryIds.length ===
failedMigratedPrompts.length + failedMigratedConversations.length;
const isSomeItemsSelected =
!!conversationsToRetryIds.length || !!promptsToRetryIds.length;
const isNothingSelected =
conversationsToRetryIds.length === 0 &&
conversationsToRetryIds.length === 0;

return (
<div className="flex size-full flex-col items-center justify-center">
<div className="m-2 flex max-w-[523px] flex-col divide-y divide-tertiary rounded bg-layer-2 pb-4 pt-6">
Expand Down Expand Up @@ -220,40 +261,19 @@ export const MigrationFailedWindow = ({
<div className="flex items-center gap-1 py-1 text-xs">
{t('All items')}
</div>
<div className="flex w-[100px] justify-around">
<div className="relative flex size-[18px] group-hover/file-item:flex">
<input
className="checkbox peer size-[18px] bg-transparent"
type="checkbox"
onClick={onUnselectAll}
readOnly
checked={
conversationsToRetryIds.length !== 0 ||
promptsToRetryIds.length !== 0
}
/>
<IconMinus
size={18}
className="pointer-events-none invisible absolute text-accent-primary peer-checked:visible"
/>
</div>
<div className="relative flex size-[18px] group-hover/file-item:flex">
<input
className="checkbox peer size-[18px] bg-transparent"
type="checkbox"
onClick={onSelectAll}
readOnly
checked={
conversationsToRetryIds.length !==
failedMigratedConversations.length ||
promptsToRetryIds.length !== failedMigratedPrompts.length
}
/>
<IconMinus
size={18}
className="pointer-events-none invisible absolute text-accent-primary peer-checked:visible"
/>
</div>
<div className="flex w-[100px] items-center justify-around">
<AllItemsCheckboxes
isChecked={isAllItemsSelected || isSomeItemsSelected}
isCheckIcon={isAllItemsSelected}
isMinusIcon={isSomeItemsSelected}
onSelectHandler={onSelectAll}
/>
<AllItemsCheckboxes
isChecked={!isAllItemsSelected || isNothingSelected}
isCheckIcon={isNothingSelected}
isMinusIcon={!isAllItemsSelected}
onSelectHandler={onUnselectAll}
/>
</div>
</div>
</div>
Expand Down Expand Up @@ -290,14 +310,14 @@ export const MigrationFailedWindow = ({
<button
className="button button-secondary mr-3 flex h-[38px] min-w-[73px] items-center"
data-qa="skip-migration"
onClick={onSkipAll}
onClick={onRetryWithoutBackup}
>
{t('Continue without backup')}
</button>
<button
className="button button-primary flex h-[38px] items-center"
data-qa="try-migration-again"
onClick={onRetry}
onClick={onRetryWithBackup}
>
{t('Backup to disk and continue')}
</button>
Expand Down
11 changes: 6 additions & 5 deletions apps/chat/src/store/conversations/conversations.epics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -705,18 +705,18 @@ const deleteConversationsEpic: AppEpic = (action$, state$) =>
}),
);

const migrateConversationsEpic: AppEpic = (action$, state$) => {
const migrateConversationsIfRequiredEpic: AppEpic = (action$, state$) => {
const browserStorage = new BrowserStorage();

return action$.pipe(
filter(ConversationsActions.migrateConversations.match),
filter(ConversationsActions.migrateConversationsIfRequired.match),
switchMap(() =>
forkJoin({
conversations: browserStorage
.getConversations()
.pipe(map(filterOnlyMyEntities)),
conversationsFolders: browserStorage
.getConversationsFolders()
.getConversationsFolders(undefined, true)
.pipe(map(filterOnlyMyEntities)),
migratedConversationIds: BrowserStorage.getMigratedEntityIds(
MigrationStorageKeys.MigratedConversationIds,
Expand Down Expand Up @@ -769,14 +769,15 @@ const migrateConversationsEpic: AppEpic = (action$, state$) => {
const { path } = getPathToFolderById(
conversationsFolders,
conv.folderId,
true,
);
const newName = conv.name.replace(notAllowedSymbolsRegex, '');

return {
...conv,
id: constructPath(...[path, newName]),
name: newName,
folderId: path.replace(notAllowedSymbolsRegex, ''),
folderId: path,
};
}); // to send conversation with proper parentPath and lastActivityDate order

Expand Down Expand Up @@ -2238,7 +2239,7 @@ const openFolderEpic: AppEpic = (action$, state$) =>
);

export const ConversationsEpics = combineEpics(
migrateConversationsEpic,
migrateConversationsIfRequiredEpic,
skipFailedMigratedConversationsEpic,
// init
initEpic,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ export const conversationsSlice = createSlice({
initialState,
reducers: {
init: (state) => state,
migrateConversations: (state) => state,
migrateConversationsIfRequired: (state) => state,
initConversationsMigration: (
state,
{
Expand Down
16 changes: 10 additions & 6 deletions apps/chat/src/store/prompts/prompts.epics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -402,16 +402,16 @@ const importPromptsEpic: AppEpic = (action$, state$) =>
// ),
// );

const migratePromptsEpic: AppEpic = (action$, state$) => {
const migratePromptsIfRequiredEpic: AppEpic = (action$, state$) => {
const browserStorage = new BrowserStorage();

return action$.pipe(
filter(PromptsActions.migratePrompts.match),
filter(PromptsActions.migratePromptsIfRequired.match),
switchMap(() =>
forkJoin({
prompts: browserStorage.getPrompts().pipe(map(filterOnlyMyEntities)),
promptsFolders: browserStorage
.getPromptsFolders()
.getPromptsFolders(undefined, true)
.pipe(map(filterOnlyMyEntities)),
migratedPromptIds: BrowserStorage.getMigratedEntityIds(
MigrationStorageKeys.MigratedPromptIds,
Expand Down Expand Up @@ -453,14 +453,18 @@ const migratePromptsEpic: AppEpic = (action$, state$) => {
}

const preparedPrompts: Prompt[] = notMigratedPrompts.map((prompt) => {
const { path } = getPathToFolderById(promptsFolders, prompt.folderId);
const { path } = getPathToFolderById(
promptsFolders,
prompt.folderId,
true,
);
const newName = prompt.name.replace(notAllowedSymbolsRegex, '');

return {
...prompt,
id: constructPath(...[path, newName]),
name: newName,
folderId: path.replace(notAllowedSymbolsRegex, ''),
folderId: path,
};
}); // to send prompts with proper parentPath
let migratedPromptsCount = 0;
Expand Down Expand Up @@ -834,7 +838,7 @@ export const uploadPromptEpic: AppEpic = (action$, state$) =>
);

export const PromptsEpics = combineEpics(
migratePromptsEpic,
migratePromptsIfRequiredEpic,
skipFailedMigratedPromptsEpic,
// init
initEpic,
Expand Down
2 changes: 1 addition & 1 deletion apps/chat/src/store/prompts/prompts.reducers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ export const promptsSlice = createSlice({
reducers: {
init: (state) => state,
initPrompts: (state) => state,
migratePrompts: (state) => state,
migratePromptsIfRequired: (state) => state,
skipFailedMigratedPrompts: (
state,
{ payload: _ }: PayloadAction<{ idsToMarkAsMigrated: string[] }>,
Expand Down
4 changes: 2 additions & 2 deletions apps/chat/src/store/settings/settings.epic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,8 @@ const initEpic: AppEpic = (action$, state$) =>
),
switchMap(() =>
concat(
of(ConversationsActions.migrateConversations()),
of(PromptsActions.migratePrompts()),
of(ConversationsActions.migrateConversationsIfRequired()),
of(PromptsActions.migratePromptsIfRequired()),
of(UIActions.init()),
of(ModelsActions.init()),
of(AddonsActions.init()),
Expand Down
10 changes: 6 additions & 4 deletions apps/chat/src/utils/app/data/storages/browser-storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -172,23 +172,25 @@ export class BrowserStorage implements DialStorage {
return BrowserStorage.setData(UIStorageKeys.Prompts, prompts);
}

getConversationsFolders(path?: string) {
getConversationsFolders(path?: string, recursive?: boolean) {
return BrowserStorage.getData(UIStorageKeys.Folders, []).pipe(
map((folders: FolderInterface[]) => {
return folders.filter(
(folder) =>
folder.type === FolderType.Chat && folder.folderId === path,
folder.type === FolderType.Chat &&
(recursive || folder.folderId === path),
);
}),
);
}

getPromptsFolders(path?: string) {
getPromptsFolders(path?: string, recursive?: boolean) {
return BrowserStorage.getData(UIStorageKeys.Folders, []).pipe(
map((folders: FolderInterface[]) => {
return folders.filter(
(folder) =>
folder.type === FolderType.Prompt && folder.folderId === path,
folder.type === FolderType.Prompt &&
(recursive || folder.folderId === path),
);
}),
);
Expand Down
Loading

0 comments on commit 3b3293f

Please sign in to comment.