Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(chat): fix migration issues (Issue #668) #717

Merged
merged 16 commits into from
Feb 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
141 changes: 109 additions & 32 deletions apps/chat/src/components/Chat/Migration/MigrationFailedModal.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
import { IconBulb, IconCheck, IconMinus } from '@tabler/icons-react';
import {
IconBulb,
IconCheck,
IconCircleCheck,
IconDownload,
IconMinus,
} from '@tabler/icons-react';
import { ReactElement, useCallback, useEffect, useState } from 'react';
import { useDispatch } from 'react-redux';

Expand All @@ -7,15 +13,25 @@ import { useRouter } from 'next/router';

import classNames from 'classnames';

import { BrowserStorage } from '@/src/utils/app/data/storages/browser-storage';
import { isOnlySmallScreen } from '@/src/utils/app/mobile';

import { Conversation } from '@/src/types/chat';
import { Prompt } from '@/src/types/prompt';
import { MigrationStorageKeys } from '@/src/types/storage';
import { Translation } from '@/src/types/translation';

import { ConversationsActions } from '@/src/store/conversations/conversations.reducers';
import {
ConversationsActions,
ConversationsSelectors,
} from '@/src/store/conversations/conversations.reducers';
import { useAppSelector } from '@/src/store/hooks';
import { ImportExportActions } from '@/src/store/import-export/importExport.reducers';
import { ModelsSelectors } from '@/src/store/models/models.reducers';
import { PromptsActions } from '@/src/store/prompts/prompts.reducers';
import {
PromptsActions,
PromptsSelectors,
} from '@/src/store/prompts/prompts.reducers';
import { SettingsSelectors } from '@/src/store/settings/settings.reducers';

import { ReportIssueDialog } from '@/src/components/Chat/ReportIssueDialog';
Expand Down Expand Up @@ -152,13 +168,19 @@ export const MigrationFailedWindow = ({
string[]
>([]);
const [promptsToRetryIds, setPromptsToRetryIds] = useState<string[]>([]);

const [isReportIssueDialogOpen, setIsReportIssueDialogOpen] = useState(false);
const [dontWantBackup, setDontWantBackup] = useState(false);

const enabledFeatures = useAppSelector(
SettingsSelectors.selectEnabledFeatures,
);
const modelsMap = useAppSelector(ModelsSelectors.selectModelsMap);
const isPromptsBackedUp = useAppSelector(
PromptsSelectors.selectIsPromptsBackedUp,
);
const isChatsBackedUp = useAppSelector(
ConversationsSelectors.selectIsChatsBackedUp,
);

useEffect(() => {
setConversationsToRetryIds(
Expand Down Expand Up @@ -203,23 +225,24 @@ export const MigrationFailedWindow = ({
promptsToRetryIds,
]);

const onRetryWithoutBackup = useCallback(() => {
retryMigration();
}, [retryMigration]);
const handleBackupPrompts = useCallback(() => {
dispatch(ImportExportActions.exportLocalStoragePrompts());
BrowserStorage.setEntityBackedUp(MigrationStorageKeys.PromptsBackedUp);
}, [dispatch]);

const onRetryWithBackup = useCallback(() => {
dispatch(ImportExportActions.exportLocalStorageEntities());
retryMigration();
}, [dispatch, retryMigration]);
const handleBackupChats = useCallback(() => {
dispatch(ImportExportActions.exportLocalStorageChats());
BrowserStorage.setEntityBackedUp(MigrationStorageKeys.ChatsBackedUp);
}, [dispatch]);

const onSelectAll = useCallback(() => {
const handleSelectAll = useCallback(() => {
setConversationsToRetryIds(
failedMigratedConversations.map((conv) => conv.id),
);
setPromptsToRetryIds(failedMigratedPrompts.map((prompt) => prompt.id));
}, [failedMigratedConversations, failedMigratedPrompts]);

const onUnselectAll = () => {
const handleUnselectAll = () => {
setConversationsToRetryIds([]);
setPromptsToRetryIds([]);
};
Expand All @@ -230,8 +253,11 @@ export const MigrationFailedWindow = ({
const isSomeItemsSelected =
!!conversationsToRetryIds.length || !!promptsToRetryIds.length;
const isNothingSelected =
conversationsToRetryIds.length === 0 &&
conversationsToRetryIds.length === 0;
!conversationsToRetryIds.length && !promptsToRetryIds.length;
const isNextButtonEnabled =
dontWantBackup ||
((isChatsBackedUp || !failedMigratedConversations.length) &&
(isPromptsBackedUp || !failedMigratedPrompts.length));

return (
<div className="flex size-full flex-col items-center justify-center">
Expand Down Expand Up @@ -266,13 +292,13 @@ export const MigrationFailedWindow = ({
isChecked={isAllItemsSelected || isSomeItemsSelected}
isCheckIcon={isAllItemsSelected}
isMinusIcon={isSomeItemsSelected}
onSelectHandler={onSelectAll}
onSelectHandler={handleSelectAll}
/>
<AllItemsCheckboxes
isChecked={!isAllItemsSelected || isNothingSelected}
isCheckIcon={isNothingSelected}
isMinusIcon={!isAllItemsSelected}
onSelectHandler={onUnselectAll}
onSelectHandler={handleUnselectAll}
/>
</div>
</div>
Expand Down Expand Up @@ -310,21 +336,72 @@ export const MigrationFailedWindow = ({
/>
</div>
</div>
<footer className="flex items-center justify-end px-6 pt-4">
<button
className="button button-secondary mr-3 flex h-[38px] min-w-[73px] items-center"
data-qa="skip-migration"
onClick={onRetryWithoutBackup}
>
{t('Continue without backup')}
</button>
<button
className="button button-primary flex h-[38px] items-center"
data-qa="try-migration-again"
onClick={onRetryWithBackup}
>
{t('Backup to disk and continue')}
</button>
<footer className="flex flex-col items-center justify-end px-6 pt-4">
<div className="flex items-center gap-4">
<div className="relative flex size-[18px] group-hover/file-item:flex">
<input
className="checkbox peer size-[18px] bg-transparent"
type="checkbox"
onClick={() => setDontWantBackup((prev) => !prev)}
readOnly
checked={dontWantBackup}
/>
{dontWantBackup && (
<IconCheck
size={18}
className="pointer-events-none invisible absolute text-accent-primary peer-checked:visible"
/>
)}
</div>
<p className="text-secondary">
{t("I don't want to backup conversations/prompts and I’m ready ")}
<span className="font-semibold">{t('TO LOSE DATA')}</span>
</p>
</div>
<div className="mt-3 flex w-full justify-end">
{!!failedMigratedPrompts.length && (
<button
className="button button-secondary mr-3 flex h-[38px] min-w-[73px] items-center capitalize md:normal-case"
data-qa="skip-migration"
onClick={handleBackupPrompts}
>
{isPromptsBackedUp ? (
<IconCircleCheck
size={18}
className="mr-3 text-accent-secondary"
/>
) : (
<IconDownload size={18} className="mr-3 text-secondary" />
)}
{!isOnlySmallScreen() && t('Backup')} {t('prompts')}
</button>
)}
{!!failedMigratedConversations.length && (
<button
className="button button-secondary mr-3 flex h-[38px] min-w-[73px] items-center capitalize md:normal-case"
data-qa="skip-migration"
onClick={handleBackupChats}
>
{isChatsBackedUp ? (
<IconCircleCheck
size={18}
className="mr-3 text-accent-secondary"
/>
) : (
<IconDownload size={18} className="mr-3 text-secondary" />
)}
{!isOnlySmallScreen() && t('Backup')} {t('chats')}
</button>
)}
<button
className="button button-primary mr-3 flex h-[38px] items-center"
data-qa="skip-migration"
onClick={retryMigration}
disabled={!isNextButtonEnabled}
>
{t('Next')}
</button>
</div>
</footer>
</div>
<p className="mt-6 text-secondary">
Expand Down
21 changes: 14 additions & 7 deletions apps/chat/src/store/conversations/conversations.epics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -818,6 +818,9 @@ const migrateConversationsIfRequiredEpic: AppEpic = (action$, state$) => {
BrowserStorage.getFailedMigratedEntityIds(
MigrationStorageKeys.FailedMigratedConversationIds,
),
isChatsBackedUp: BrowserStorage.getEntityBackedUp(
MigrationStorageKeys.ChatsBackedUp,
),
}),
),
switchMap(
Expand All @@ -826,6 +829,7 @@ const migrateConversationsIfRequiredEpic: AppEpic = (action$, state$) => {
conversationsFolders,
migratedConversationIds,
failedMigratedConversationIds,
isChatsBackedUp,
}) => {
const notMigratedConversations = filterMigratedEntities(
conversations,
Expand All @@ -839,13 +843,16 @@ const migrateConversationsIfRequiredEpic: AppEpic = (action$, state$) => {
!notMigratedConversations.length
) {
if (failedMigratedConversationIds.length) {
return of(
ConversationsActions.setFailedMigratedConversations({
failedMigratedConversations: filterMigratedEntities(
conversations,
failedMigratedConversationIds,
),
}),
return concat(
of(ConversationsActions.setIsChatsBackedUp({ isChatsBackedUp })),
of(
ConversationsActions.setFailedMigratedConversations({
failedMigratedConversations: filterMigratedEntities(
conversations,
failedMigratedConversationIds,
),
}),
),
);
}

Expand Down
11 changes: 11 additions & 0 deletions apps/chat/src/store/conversations/conversations.reducers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export { ConversationsSelectors };
const initialState: ConversationsState = {
conversationsToMigrateCount: 0,
migratedConversationsCount: 0,
isChatsBackedUp: false,
failedMigratedConversations: [],
conversations: [],
selectedConversationsIds: [],
Expand Down Expand Up @@ -81,6 +82,16 @@ export const conversationsSlice = createSlice({
) => {
state.failedMigratedConversations = payload.failedMigratedConversations;
},
setIsChatsBackedUp: (
state,
{
payload,
}: PayloadAction<{
isChatsBackedUp: boolean;
}>,
) => {
state.isChatsBackedUp = payload.isChatsBackedUp;
},
skipFailedMigratedConversations: (
state,
{ payload: _ }: PayloadAction<{ idsToMarkAsMigrated: string[] }>,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -588,3 +588,8 @@ export const selectIsCompareLoading = createSelector(
return state.compareLoading;
},
);

export const selectIsChatsBackedUp = createSelector(
[rootSelector],
(state) => state.isChatsBackedUp,
);
1 change: 1 addition & 0 deletions apps/chat/src/store/conversations/conversations.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { SearchFilters } from '@/src/types/search';
export interface ConversationsState {
conversationsToMigrateCount: number;
migratedConversationsCount: number;
isChatsBackedUp: boolean;
failedMigratedConversations: Conversation[];
conversations: (ConversationInfo | Conversation)[];
selectedConversationsIds: string[];
Expand Down
46 changes: 30 additions & 16 deletions apps/chat/src/store/import-export/importExport.epics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ import { FolderType } from '@/src/types/folder';
import { LatestExportFormat } from '@/src/types/import-export';
import { AppEpic } from '@/src/types/store';

import { PromptsActions } from '@/src/store/prompts/prompts.reducers';

import { errorsMessages } from '@/src/constants/errors';

import {
Expand All @@ -60,7 +62,6 @@ import {
} from '../conversations/conversations.reducers';
import { getUniqueAttachments } from '../conversations/conversations.selectors';
import { FilesActions } from '../files/files.reducers';
import { PromptsActions } from '../prompts/prompts.reducers';
import { selectFolders } from '../prompts/prompts.selectors';
import { SettingsSelectors } from '../settings/settings.reducers';
import {
Expand Down Expand Up @@ -178,35 +179,47 @@ const exportConversationsEpic: AppEpic = (action$, state$) =>
ignoreElements(),
);

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

return action$.pipe(
filter(ImportExportActions.exportLocalStorageEntities.match),
filter(ImportExportActions.exportLocalStorageChats.match),
switchMap(() =>
forkJoin({
conversations: browserStorage
.getConversations()
.pipe(map(filterOnlyMyEntities)),
conversationFolders: browserStorage.getConversationsFolders(),
appName: SettingsSelectors.selectAppName(state$.value),
}),
),
tap(({ conversations, conversationFolders, appName }) => {
exportConversations(conversations, conversationFolders, appName, 4);
}),
switchMap(() =>
of(ConversationsActions.setIsChatsBackedUp({ isChatsBackedUp: true })),
),
);
};

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

return action$.pipe(
filter(ImportExportActions.exportLocalStoragePrompts.match),
switchMap(() =>
forkJoin({
prompts: browserStorage.getPrompts().pipe(map(filterOnlyMyEntities)),
promptFolders: browserStorage.getPromptsFolders(),
appName: SettingsSelectors.selectAppName(state$.value),
}),
),
tap(
({
conversations,
conversationFolders,
prompts,
promptFolders,
appName,
}) => {
exportConversations(conversations, conversationFolders, appName, 4);
exportPrompts(prompts, promptFolders);
},
tap(({ prompts, promptFolders, appName }) => {
exportPrompts(prompts, promptFolders, appName);
}),
switchMap(() =>
of(PromptsActions.setIsPromptsBackedUp({ isPromptsBackedUp: true })),
),
ignoreElements(),
);
};

Expand Down Expand Up @@ -618,5 +631,6 @@ export const ImportExportEpics = combineEpics(
importFailEpic,
exportFailEpic,
checkImportFailEpic,
exportLocalStorageEntitiesEpic,
exportLocalStorageChatsEpic,
exportLocalStoragePromptsEpic,
);
Loading
Loading