From aed6f374c41fcf5ac39f3ce992a80fe075f9d815 Mon Sep 17 00:00:00 2001 From: Ilya Bondar Date: Wed, 14 Feb 2024 11:28:42 +0100 Subject: [PATCH] fix(chat): fix decoding url (Issue #165, #680) (#699) --- .../src/components/Chatbar/ChatFolders.tsx | 14 +-- .../Promptbar/components/PromptFolders.tsx | 6 +- .../src/pages/api/[entitytype]/listing.ts | 3 +- .../conversations/conversations.epics.ts | 88 ++++++++----------- .../conversations/conversations.reducers.ts | 9 +- apps/chat/src/utils/app/data/file-service.ts | 25 ++++-- .../data/storages/api/api-entity-storage.ts | 13 ++- apps/chat/src/utils/app/folders.ts | 3 +- apps/chat/src/utils/server/api.ts | 6 ++ 9 files changed, 83 insertions(+), 84 deletions(-) diff --git a/apps/chat/src/components/Chatbar/ChatFolders.tsx b/apps/chat/src/components/Chatbar/ChatFolders.tsx index 78a309ae77..ca966493f6 100644 --- a/apps/chat/src/components/Chatbar/ChatFolders.tsx +++ b/apps/chat/src/components/Chatbar/ChatFolders.tsx @@ -329,7 +329,6 @@ export function ChatFolders() { const isFilterEmpty = useAppSelector( ConversationsSelectors.selectIsEmptySearchFilter, ); - const searchTerm = useAppSelector(ConversationsSelectors.selectSearchTerm); const commonItemFilter = useAppSelector( ConversationsSelectors.selectMyItemsFilters, ); @@ -351,7 +350,7 @@ export function ChatFolders() { filters: PublishedWithMeFilter, displayRootFiles: true, dataQa: 'published-with-me', - openByDefault: !!searchTerm.length, + openByDefault: true, }, { hidden: !isSharingEnabled || !isFilterEmpty, @@ -359,7 +358,7 @@ export function ChatFolders() { filters: SharedWithMeFilter, displayRootFiles: true, dataQa: 'shared-with-me', - openByDefault: !!searchTerm.length, + openByDefault: true, }, { name: t('Pinned chats'), @@ -369,14 +368,7 @@ export function ChatFolders() { dataQa: 'pinned-chats', }, ].filter(({ hidden }) => !hidden), - [ - commonItemFilter, - isFilterEmpty, - isPublishingEnabled, - isSharingEnabled, - searchTerm.length, - t, - ], + [commonItemFilter, isFilterEmpty, isPublishingEnabled, isSharingEnabled, t], ); return ( diff --git a/apps/chat/src/components/Promptbar/components/PromptFolders.tsx b/apps/chat/src/components/Promptbar/components/PromptFolders.tsx index b23a577487..4779b366e6 100644 --- a/apps/chat/src/components/Promptbar/components/PromptFolders.tsx +++ b/apps/chat/src/components/Promptbar/components/PromptFolders.tsx @@ -314,7 +314,6 @@ export function PromptFolders() { const isFilterEmpty = useAppSelector( PromptsSelectors.selectIsEmptySearchFilter, ); - const searchTerm = useAppSelector(PromptsSelectors.selectSearchTerm); const commonSearchFilter = useAppSelector( PromptsSelectors.selectMyItemsFilters, ); @@ -335,7 +334,7 @@ export function PromptFolders() { filters: PublishedWithMeFilter, displayRootFiles: true, dataQa: 'published-with-me', - openByDefault: !!searchTerm.length, + openByDefault: true, }, { hidden: !isSharingEnabled || !isFilterEmpty, @@ -343,7 +342,7 @@ export function PromptFolders() { filters: SharedWithMeFilter, displayRootFiles: true, dataQa: 'shared-with-me', - openByDefault: !!searchTerm.length, + openByDefault: true, }, { name: t('Pinned prompts'), @@ -358,7 +357,6 @@ export function PromptFolders() { isFilterEmpty, isPublishingEnabled, isSharingEnabled, - searchTerm.length, t, ], ); diff --git a/apps/chat/src/pages/api/[entitytype]/listing.ts b/apps/chat/src/pages/api/[entitytype]/listing.ts index 22e8376b51..0f0b98113d 100644 --- a/apps/chat/src/pages/api/[entitytype]/listing.ts +++ b/apps/chat/src/pages/api/[entitytype]/listing.ts @@ -6,6 +6,7 @@ import { validateServerSession } from '@/src/utils/auth/session'; import { OpenAIError } from '@/src/utils/server'; import { ApiKeys, + encodeApiUrl, getEntityTypeFromPath, isValidEntityApiType, } from '@/src/utils/server/api'; @@ -54,7 +55,7 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => { const url = `${ process.env.DIAL_API_HOST - }/v1/metadata/${path ? `${encodeURI(path)}` : `${entityType}/${bucket}`}/?limit=1000${recursive ? '&recursive=true' : ''}`; + }/v1/metadata/${path ? `${encodeApiUrl(path)}` : `${entityType}/${bucket}`}/?limit=1000${recursive ? '&recursive=true' : ''}`; const response = await fetch(url, { headers: getApiHeaders({ jwt: token?.access_token as string }), diff --git a/apps/chat/src/store/conversations/conversations.epics.ts b/apps/chat/src/store/conversations/conversations.epics.ts index de32d8053f..4349528929 100644 --- a/apps/chat/src/store/conversations/conversations.epics.ts +++ b/apps/chat/src/store/conversations/conversations.epics.ts @@ -34,11 +34,9 @@ import { combineEpics } from 'redux-observable'; import { clearStateForMessages } from '@/src/utils/app/clear-messages-state'; import { combineEntities, - updateEntitiesFoldersAndIds, -} from '@/src/utils/app/common'; -import { filterMigratedEntities, filterOnlyMyEntities, + updateEntitiesFoldersAndIds, } from '@/src/utils/app/common'; import { compareConversationsByDate, @@ -161,6 +159,7 @@ const initSelectedConversationsEpic: AppEpic = (action$) => }), switchMap(({ conversations, selectedConversationsIds }) => { const actions: Observable[] = []; + if (conversations.length) { actions.push( of( @@ -170,6 +169,17 @@ const initSelectedConversationsEpic: AppEpic = (action$) => }), ), ); + const paths = selectedConversationsIds.flatMap((id) => + getParentFolderIdsFromEntityId(id), + ); + actions.push( + of( + UIActions.setOpenedFoldersIds({ + openedFolderIds: paths, + featureType: FeatureType.Chat, + }), + ), + ); } actions.push( of( @@ -178,18 +188,16 @@ const initSelectedConversationsEpic: AppEpic = (action$) => }), ), ); - if (!conversations.length || !selectedConversationsIds.length) { + if (!conversations.length) { actions.push( of( ConversationsActions.createNewConversations({ names: [translate(DEFAULT_CONVERSATION_NAME)], + shouldUploadConversationsForCompare: true, }), ), ); } - actions.push( - of(ConversationsActions.uploadConversationsWithFoldersRecursive()), - ); return concat(...actions); }), @@ -200,41 +208,9 @@ const initFoldersAndConversationsEpic: AppEpic = (action$) => filter((action) => ConversationsActions.initFoldersAndConversations.match(action), ), - switchMap(() => ConversationService.getSelectedConversationsIds()), - switchMap((selectedIds) => { - const paths = selectedIds.flatMap((id) => - getParentFolderIdsFromEntityId(id), - ); - const uploadPaths = [undefined, ...paths]; - return zip( - uploadPaths.map((path) => - ConversationService.getConversationsAndFolders(path), - ), - ).pipe( - switchMap((foldersAndEntities) => { - const folders = foldersAndEntities.flatMap((f) => f.folders); - const conversations = foldersAndEntities.flatMap((f) => f.entities); - return concat( - of( - ConversationsActions.setFolders({ - folders, - }), - ), - of( - ConversationsActions.setConversations({ - conversations, - }), - ), - of( - UIActions.setOpenedFoldersIds({ - openedFolderIds: paths, - featureType: FeatureType.Chat, - }), - ), - ); - }), - ); - }), + switchMap(() => + of(ConversationsActions.uploadConversationsWithFoldersRecursive()), + ), ); const createNewConversationsEpic: AppEpic = (action$, state$) => @@ -246,16 +222,26 @@ const createNewConversationsEpic: AppEpic = (action$, state$) => state$.value, ), conversations: ConversationsSelectors.selectConversations(state$.value), + shouldUploadConversationsForCompare: + payload.shouldUploadConversationsForCompare, })), - switchMap(({ names, lastConversation, conversations }) => - forkJoin({ - names: of(names), - lastConversation: - lastConversation && lastConversation.status !== UploadStatus.LOADED - ? ConversationService.getConversation(lastConversation) - : (of(lastConversation) as Observable), - conversations: of(conversations), - }), + switchMap( + ({ + names, + lastConversation, + conversations, + shouldUploadConversationsForCompare, + }) => + forkJoin({ + names: of(names), + lastConversation: + lastConversation && lastConversation.status !== UploadStatus.LOADED + ? ConversationService.getConversation(lastConversation) + : (of(lastConversation) as Observable), + conversations: shouldUploadConversationsForCompare + ? ConversationService.getConversations() + : of(conversations), + }), ), switchMap(({ names, lastConversation, conversations }) => { return state$.pipe( diff --git a/apps/chat/src/store/conversations/conversations.reducers.ts b/apps/chat/src/store/conversations/conversations.reducers.ts index 1a644c3777..2f2883496c 100644 --- a/apps/chat/src/store/conversations/conversations.reducers.ts +++ b/apps/chat/src/store/conversations/conversations.reducers.ts @@ -146,7 +146,10 @@ export const conversationsSlice = createSlice({ }, createNewConversations: ( state, - _action: PayloadAction<{ names: string[] }>, + _action: PayloadAction<{ + names: string[]; + shouldUploadConversationsForCompare?: boolean; + }>, ) => state, publishConversation: ( state, @@ -265,6 +268,7 @@ export const conversationsSlice = createSlice({ ) => { state.conversations = state.conversations.concat(newConversation); state.selectedConversationsIds = [newConversation.id]; + state.areSelectedConversationsLoaded = true; }, createNewPlaybackConversation: ( state, @@ -621,6 +625,9 @@ export const conversationsSlice = createSlice({ } : f, ); + if (payload.allLoaded) { + state.conversationsLoaded = true; + } state.foldersStatus = payload.allLoaded ? UploadStatus.ALL_LOADED : UploadStatus.LOADED; diff --git a/apps/chat/src/utils/app/data/file-service.ts b/apps/chat/src/utils/app/data/file-service.ts index 058e41b205..48a3eb3f7f 100644 --- a/apps/chat/src/utils/app/data/file-service.ts +++ b/apps/chat/src/utils/app/data/file-service.ts @@ -9,7 +9,12 @@ import { } from '@/src/types/files'; import { FolderType } from '@/src/types/folder'; -import { ApiKeys, ApiUtils } from '../../server/api'; +import { + ApiKeys, + ApiUtils, + decodeApiUrl, + encodeApiUrl, +} from '../../server/api'; import { constructPath } from '../file'; import { getRootId } from '../id'; import { BucketService } from './bucket-service'; @@ -20,7 +25,7 @@ export class FileService { relativePath: string | undefined, fileName: string, ): Observable<{ percent?: number; result?: DialFile }> { - const resultPath = encodeURI( + const resultPath = encodeApiUrl( constructPath(BucketService.getBucket(), relativePath, fileName), ); @@ -47,11 +52,13 @@ export class FileService { } const typedResult = result as BackendFile; - const relativePath = typedResult.parentPath || undefined; + const relativePath = typedResult.parentPath + ? decodeApiUrl(typedResult.parentPath) + : undefined; return { result: { - id: decodeURI(typedResult.url), + id: decodeApiUrl(typedResult.url), name: typedResult.name, absolutePath: constructPath( ApiKeys.Files, @@ -91,7 +98,9 @@ export class FileService { return ApiUtils.request(`api/${ApiKeys.Files}/listing?${resultQuery}`).pipe( map((folders: BackendFileFolder[]) => { return folders.map((folder): FileFolderInterface => { - const relativePath = folder.parentPath || undefined; + const relativePath = folder.parentPath + ? decodeApiUrl(folder.parentPath) + : undefined; return { id: constructPath( @@ -120,7 +129,7 @@ export class FileService { } public static removeFile(filePath: string): Observable { - const resultPath = encodeURI( + const resultPath = encodeApiUrl( constructPath(BucketService.getBucket(), filePath), ); @@ -147,7 +156,9 @@ export class FileService { return ApiUtils.request(`api/${ApiKeys.Files}/listing?${resultQuery}`).pipe( map((files: BackendFile[]) => { return files.map((file): DialFile => { - const relativePath = file.parentPath || undefined; + const relativePath = file.parentPath + ? decodeApiUrl(file.parentPath) + : undefined; return { id: constructPath( diff --git a/apps/chat/src/utils/app/data/storages/api/api-entity-storage.ts b/apps/chat/src/utils/app/data/storages/api/api-entity-storage.ts index 9bd3d3181b..4a5f0d685e 100644 --- a/apps/chat/src/utils/app/data/storages/api/api-entity-storage.ts +++ b/apps/chat/src/utils/app/data/storages/api/api-entity-storage.ts @@ -3,6 +3,8 @@ import { EMPTY, Observable, catchError, map, of } from 'rxjs'; import { ApiKeys, ApiUtils, + decodeApiUrl, + encodeApiUrl, getFolderTypeByApiKey, } from '@/src/utils/server/api'; @@ -26,7 +28,7 @@ export abstract class ApiEntityStorage< > implements EntityStorage { private mapFolder(folder: BackendChatFolder): FolderInterface { - const id = decodeURI(folder.url.slice(0, folder.url.length - 1)); + const id = decodeApiUrl(folder.url.slice(0, folder.url.length - 1)); const { apiKey, bucket, parentPath } = splitEntityId(id); return { @@ -39,7 +41,7 @@ export abstract class ApiEntityStorage< private mapEntity(entity: BackendChatEntity): TEntityInfo { const info = this.parseEntityKey(entity.name); - const id = decodeURI(entity.url); + const id = decodeApiUrl(entity.url); const { apiKey, bucket, parentPath } = splitEntityId(id); return { @@ -50,14 +52,11 @@ export abstract class ApiEntityStorage< } as unknown as TEntityInfo; } - private encodePath = (path: string): string => - constructPath(...path.split('/').map((part) => encodeURIComponent(part))); - private getEntityUrl = (entity: TEntityInfo): string => - this.encodePath(constructPath('api', entity.id)); + encodeApiUrl(constructPath('api', entity.id)); private getListingUrl = (resultQuery: string): string => { - const listingUrl = this.encodePath( + const listingUrl = encodeApiUrl( constructPath('api', this.getStorageKey(), 'listing'), ); return `${listingUrl}?${resultQuery}`; diff --git a/apps/chat/src/utils/app/folders.ts b/apps/chat/src/utils/app/folders.ts index 5dc7e884db..c4e29e7570 100644 --- a/apps/chat/src/utils/app/folders.ts +++ b/apps/chat/src/utils/app/folders.ts @@ -418,8 +418,7 @@ export const getParentFolderIdsFromFolderId = (path?: string): string[] => { }; export const getParentFolderIdsFromEntityId = (id: string): string[] => { - const { parentPath } = splitEntityId(id); - return getParentFolderIdsFromFolderId(parentPath); + return getParentFolderIdsFromFolderId(id); }; export const getFolderFromId = ( diff --git a/apps/chat/src/utils/server/api.ts b/apps/chat/src/utils/server/api.ts index e3f68de8cb..6b4b1c43ec 100644 --- a/apps/chat/src/utils/server/api.ts +++ b/apps/chat/src/utils/server/api.ts @@ -72,6 +72,12 @@ const encodeSlugs = (slugs: (string | undefined)[]): string => ...slugs.filter(Boolean).map((part) => encodeURIComponent(part as string)), ); +export const encodeApiUrl = (path: string): string => + constructPath(...path.split('/').map((part) => encodeURIComponent(part))); + +export const decodeApiUrl = (path: string): string => + constructPath(...path.split('/').map((part) => decodeURIComponent(part))); + export const getEntityUrlFromSlugs = ( dialApiHost: string, req: NextApiRequest,