diff --git a/apps/chat/src/pages/api/[entitytype]/[...slug].ts b/apps/chat/src/pages/api/[entitytype]/[...slug].ts index be0d06080b..7bbe42c5d1 100644 --- a/apps/chat/src/pages/api/[entitytype]/[...slug].ts +++ b/apps/chat/src/pages/api/[entitytype]/[...slug].ts @@ -2,11 +2,12 @@ import { NextApiRequest, NextApiResponse } from 'next'; import { getServerSession } from 'next-auth'; import { JWT, getToken } from 'next-auth/jwt'; +import { constructPath } from '@/src/utils/app/file'; import { validateServerSession } from '@/src/utils/auth/session'; import { OpenAIError } from '@/src/utils/server'; import { + encodeSlugs, getEntityTypeFromPath, - getEntityUrlFromSlugs, isValidEntityApiType, } from '@/src/utils/server/api'; import { getApiHeaders } from '@/src/utils/server/get-headers'; @@ -19,6 +20,22 @@ import { authOptions } from '@/src/pages/api/auth/[...nextauth]'; import fetch from 'node-fetch'; import { Readable } from 'stream'; +const getEntityUrlFromSlugs = ( + dialApiHost: string, + req: NextApiRequest, +): string => { + const entityType = getEntityTypeFromPath(req); + const slugs = Array.isArray(req.query.slug) + ? req.query.slug + : [req.query.slug]; + + if (!slugs || slugs.length === 0) { + throw new OpenAIError(`No ${entityType} path provided`, '', '', '400'); + } + + return constructPath(dialApiHost, 'v1', entityType, encodeSlugs(slugs)); +}; + const handler = async (req: NextApiRequest, res: NextApiResponse) => { const entityType = getEntityTypeFromPath(req); if (!entityType || !isValidEntityApiType(entityType)) { diff --git a/apps/chat/src/pages/api/[entitytype]/listing.ts b/apps/chat/src/pages/api/listing/[...listing].ts similarity index 74% rename from apps/chat/src/pages/api/[entitytype]/listing.ts rename to apps/chat/src/pages/api/listing/[...listing].ts index 0f0b98113d..82c9d9dde6 100644 --- a/apps/chat/src/pages/api/[entitytype]/listing.ts +++ b/apps/chat/src/pages/api/listing/[...listing].ts @@ -2,14 +2,10 @@ import { NextApiRequest, NextApiResponse } from 'next'; import { getToken } from 'next-auth/jwt'; import { getServerSession } from 'next-auth/next'; +import { constructPath } from '@/src/utils/app/file'; import { validateServerSession } from '@/src/utils/auth/session'; import { OpenAIError } from '@/src/utils/server'; -import { - ApiKeys, - encodeApiUrl, - getEntityTypeFromPath, - isValidEntityApiType, -} from '@/src/utils/server/api'; +import { encodeSlugs } from '@/src/utils/server/api'; import { getApiHeaders } from '@/src/utils/server/get-headers'; import { logger } from '@/src/utils/server/logger'; @@ -27,11 +23,6 @@ import { authOptions } from '@/src/pages/api/auth/[...nextauth]'; import fetch from 'node-fetch'; const handler = async (req: NextApiRequest, res: NextApiResponse) => { - const entityType = getEntityTypeFromPath(req); - if (!entityType || !isValidEntityApiType(entityType)) { - return res.status(400).json(errorsMessages.notValidEntityType); - } - const session = await getServerSession(req, res, authOptions); const isSessionValid = validateServerSession(session, req, res); if (!isSessionValid) { @@ -40,22 +31,28 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => { try { const { - path = '', filter, - bucket, recursive = false, + limit = 1000, } = req.query as { - path: string; filter?: BackendDataNodeType; - bucket: string; recursive?: string; + limit?: number; }; - const token = await getToken({ req }); + const slugs = Array.isArray(req.query.listing) + ? req.query.listing + : [req.query.listing]; + + if (!slugs || slugs.length === 0) { + throw new OpenAIError(`No path provided`, '', '', '400'); + } - const url = `${ - process.env.DIAL_API_HOST - }/v1/metadata/${path ? `${encodeApiUrl(path)}` : `${entityType}/${bucket}`}/?limit=1000${recursive ? '&recursive=true' : ''}`; + const url = `${constructPath( + process.env.DIAL_API_HOST, + 'v1/metadata', + encodeSlugs(slugs), + )}/?limit=${limit}&recursive=${recursive}`; const response = await fetch(url, { headers: getApiHeaders({ jwt: token?.access_token as string }), @@ -78,8 +75,7 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => { | BackendChatFolder )[] = json.items || []; - const filterableEntityTypes: string[] = Object.values(ApiKeys); - if (filter && filterableEntityTypes.includes(entityType)) { + if (filter) { result = result.filter((item) => item.nodeType === filter); } diff --git a/apps/chat/src/store/conversations/conversations.epics.ts b/apps/chat/src/store/conversations/conversations.epics.ts index 537247c25c..6e3b27e3d9 100644 --- a/apps/chat/src/store/conversations/conversations.epics.ts +++ b/apps/chat/src/store/conversations/conversations.epics.ts @@ -132,7 +132,14 @@ const initSelectedConversationsEpic: AppEpic = (action$) => return forkJoin({ selectedConversations: zip( selectedIds.map((id) => - ConversationService.getConversation(getConversationInfoFromId(id)), + ConversationService.getConversation( + getConversationInfoFromId(id), + ).pipe( + catchError((err) => { + console.error('The selected conversation was not found:', err); + return of(null); + }), + ), ), ), selectedIds: of(selectedIds), @@ -237,10 +244,26 @@ const createNewConversationsEpic: AppEpic = (action$, state$) => names: of(names), lastConversation: lastConversation && lastConversation.status !== UploadStatus.LOADED - ? ConversationService.getConversation(lastConversation) + ? ConversationService.getConversation(lastConversation).pipe( + catchError((err) => { + console.error( + 'The last used conversation was not found:', + err, + ); + return of(null); + }), + ) : (of(lastConversation) as Observable), conversations: shouldUploadConversationsForCompare - ? ConversationService.getConversations() + ? ConversationService.getConversations().pipe( + catchError((err) => { + console.error( + 'The conversations were not upload successfully:', + err, + ); + return of([]); + }), + ) : of(conversations), }), ), @@ -298,7 +321,16 @@ const createNewConversationsEpic: AppEpic = (action$, state$) => }), ), ), - catchError(() => EMPTY), + catchError((err) => { + console.error("New conversation wasn't created:", err); + return of( + UIActions.showErrorToast( + translate( + 'An error occurred while creating a new conversation. Most likely the conversation already exists. Please refresh the page.', + ), + ), + ); + }), ); }), ); @@ -318,7 +350,14 @@ const createNewReplayConversationEpic: AppEpic = (action$, state$) => ), switchMap(({ conversationAndPayload, conversations }) => { const { conversation } = conversationAndPayload; - if (!conversation) return EMPTY; // TODO: handle? + if (!conversation) + return of( + UIActions.showErrorToast( + translate( + 'It looks like this conversation has been deleted. Please reload the page', + ), + ), + ); const folderId = ConversationsSelectors.hasExternalParent( state$.value, @@ -361,7 +400,7 @@ const createNewReplayConversationEpic: AppEpic = (action$, state$) => }); return of( - ConversationsActions.createNewConversationSuccess({ + ConversationsActions.saveNewConversation({ newConversation, }), ); @@ -381,7 +420,14 @@ const createNewPlaybackConversationEpic: AppEpic = (action$, state$) => ), switchMap(({ conversationAndPayload, conversations }) => { const { conversation } = conversationAndPayload; - if (!conversation) return EMPTY; // TODO: handle? + if (!conversation) + return of( + UIActions.showErrorToast( + translate( + 'It looks like this conversation has been deleted. Please reload the page', + ), + ), + ); const folderId = ConversationsSelectors.hasExternalParent( state$.value, @@ -421,7 +467,7 @@ const createNewPlaybackConversationEpic: AppEpic = (action$, state$) => }); return of( - ConversationsActions.createNewConversationSuccess({ + ConversationsActions.saveNewConversation({ newConversation, }), ); @@ -433,11 +479,19 @@ const duplicateConversationEpic: AppEpic = (action$, state$) => filter(ConversationsActions.duplicateConversation.match), switchMap(({ payload }) => forkJoin({ - conversation: ConversationService.getConversation(payload), + conversationAndPayload: getOrUploadConversation(payload, state$.value), }), ), - switchMap(({ conversation }) => { - if (!conversation) return EMPTY; + switchMap(({ conversationAndPayload: { conversation } }) => { + if (!conversation) { + return of( + UIActions.showErrorToast( + translate( + 'It looks like this conversation has been deleted. Please reload the page', + ), + ), + ); + } const newConversation: Conversation = regenerateConversationId({ ...conversation, @@ -453,7 +507,7 @@ const duplicateConversationEpic: AppEpic = (action$, state$) => }); return of( - ConversationsActions.createNewConversationSuccess({ + ConversationsActions.saveNewConversation({ newConversation, }), ); @@ -468,14 +522,24 @@ const createNewConversationsSuccessEpic: AppEpic = (action$) => ), ); -const createNewConversationSuccessEpic: AppEpic = (action$) => +const saveNewConversationEpic: AppEpic = (action$) => action$.pipe( - filter((action) => - ConversationsActions.createNewConversationSuccess.match(action), - ), + filter((action) => ConversationsActions.saveNewConversation.match(action)), switchMap(({ payload }) => ConversationService.createConversation(payload.newConversation).pipe( - switchMap(() => EMPTY), // TODO: handle error it in https://github.com/epam/ai-dial-chat/issues/663 + switchMap(() => + of(ConversationsActions.saveNewConversationSuccess(payload)), + ), + catchError((err) => { + console.error(err); + return of( + UIActions.showErrorToast( + translate( + 'An error occurred while saving the conversation. Most likely the conversation already exists. Please refresh the page.', + ), + ), + ); + }), ), ), ); @@ -489,6 +553,11 @@ const deleteFolderEpic: AppEpic = (action$, state$) => conversations: ConversationService.getConversations( payload.folderId, true, + ).pipe( + catchError((err) => { + console.error('Error during delete folder:', err); + return of([]); + }), ), folders: of(ConversationsSelectors.selectFolders(state$.value)), }), @@ -617,6 +686,10 @@ const updateFolderEpic: AppEpic = (action$, state$) => return concat(...actions); }), + catchError((err) => { + console.error('Error during upload conversations and folders', err); + return of(ConversationsActions.uploadConversationsFail()); + }), ); }), ); @@ -650,13 +723,7 @@ const deleteConversationsEpic: AppEpic = (action$, state$) => (id) => !deleteIds.has(id), ); - const actions: Observable[] = [ - of( - ConversationsActions.deleteConversationsSuccess({ - deleteIds, - }), - ), - ]; + const actions: Observable[] = []; if (otherConversations.length === 0) { actions.push( @@ -694,9 +761,37 @@ const deleteConversationsEpic: AppEpic = (action$, state$) => Array.from(deleteIds).map((id) => ConversationService.deleteConversation( getConversationInfoFromId(id), + ).pipe( + switchMap(() => of(null)), + catchError((err) => { + const { name } = getConversationInfoFromId(id); + console.error(`Error during deleting "${name}"`, err); + return name; + }), ), ), - ).pipe(switchMap(() => EMPTY)), // TODO: handle error it in https://github.com/epam/ai-dial-chat/issues/663 + ).pipe( + switchMap((failedNames) => + concat( + iif( + () => failedNames.filter(Boolean).length > 0, + of( + UIActions.showErrorToast( + translate( + `An error occurred while saving the prompt(s): "${failedNames.filter(Boolean).join('", "')}"`, + ), + ), + ), + EMPTY, + ), + of( + ConversationsActions.deleteConversationsComplete({ + deleteIds, + }), + ), + ), + ), + ), ); }), ); @@ -1363,12 +1458,7 @@ const streamMessageFailEpic: AppEpic = (action$, state$) => }), ), isReplay ? of(ConversationsActions.stopReplayConversation()) : EMPTY, - of( - UIActions.showToast({ - message: message, - type: 'error', - }), - ), + of(UIActions.showErrorToast(translate(message))), ); }), ); @@ -1644,7 +1734,23 @@ const saveFoldersEpic: AppEpic = (action$, state$) => conversationsFolders: ConversationsSelectors.selectFolders(state$.value), })), switchMap(({ conversationsFolders }) => { - return ConversationService.setConversationFolders(conversationsFolders); + return ConversationService.setConversationFolders( + conversationsFolders, + ).pipe( + catchError((err) => { + console.error( + 'An error occurred during the saving conversation folders: ', + err, + ); + return of( + UIActions.showErrorToast( + translate( + 'An error occurred during the saving conversation folders', + ), + ), + ); + }), + ); }), ignoreElements(), ); @@ -1657,9 +1763,9 @@ const selectConversationsEpic: AppEpic = (action$, state$) => ConversationsActions.selectConversations.match(action) || ConversationsActions.unselectConversations.match(action) || ConversationsActions.updateConversationSuccess.match(action) || - ConversationsActions.createNewConversationSuccess.match(action) || + ConversationsActions.saveNewConversationSuccess.match(action) || ConversationsActions.importConversationsSuccess.match(action) || - ConversationsActions.deleteConversationsSuccess.match(action) || + ConversationsActions.deleteConversationsComplete.match(action) || ConversationsActions.addConversations.match(action) || ConversationsActions.duplicateConversation.match(action) || ConversationsActions.importConversationsSuccess.match(action), @@ -1721,12 +1827,11 @@ const compareConversationsEpic: AppEpic = (action$, state$) => if (isInvalid) { actions.push( of( - UIActions.showToast({ - message: translate( + UIActions.showErrorToast( + translate( 'Incorrect conversation was chosen for comparison. Please choose another one.\r\nOnly conversations containing the same number of messages can be compared.', ), - type: 'error', - }), + ), ), ); } else { @@ -1972,6 +2077,11 @@ const uploadConversationsByIdsEpic: AppEpic = (action$, state$) => payload.conversationIds.map((id) => ConversationService.getConversation( ConversationsSelectors.selectConversation(state$.value, id)!, + ).pipe( + catchError((err) => { + console.error('The selected conversation was not found:', err); + return of(null); + }), ), ), ), @@ -2023,7 +2133,17 @@ const saveConversationEpic: AppEpic = (action$) => ), switchMap(({ payload: newConversation }) => { return ConversationService.updateConversation(newConversation).pipe( - switchMap(() => EMPTY), // TODO: handle error it in https://github.com/epam/ai-dial-chat/issues/663 + switchMap(() => EMPTY), + catchError((err) => { + console.error(err); + return of( + UIActions.showErrorToast( + translate( + 'An error occurred while saving the conversation. Please refresh the page.', + ), + ), + ); + }), ); }), ); @@ -2032,13 +2152,23 @@ const recreateConversationEpic: AppEpic = (action$) => action$.pipe( filter(ConversationsActions.recreateConversation.match), mergeMap(({ payload }) => { - return concat( - ConversationService.createConversation(payload.new).pipe( - switchMap(() => EMPTY), // TODO: handle error it in https://github.com/epam/ai-dial-chat/issues/663 - ), + return zip( + ConversationService.createConversation(payload.new), ConversationService.deleteConversation( getConversationInfoFromId(payload.old.id), - ).pipe(switchMap(() => EMPTY)), // TODO: handle error it in https://github.com/epam/ai-dial-chat/issues/663 + ), + ).pipe( + switchMap(() => EMPTY), + catchError((err) => { + console.error(err); + return of( + UIActions.showErrorToast( + translate( + 'An error occurred while saving the conversation. Please refresh the page.', + ), + ), + ); + }), ); }), ); @@ -2055,7 +2185,13 @@ const updateConversationEpic: AppEpic = (action$, state$) => values: Partial; }; if (!conversation) { - return EMPTY; // TODO: handle? + return of( + UIActions.showErrorToast( + translate( + 'It looks like this conversation has been deleted. Please reload the page', + ), + ), + ); } const newConversation: Conversation = regenerateConversationId({ ...(conversation as Conversation), @@ -2124,16 +2260,29 @@ const uploadConversationsWithFoldersEpic: AppEpic = (action$) => ), ); }), - catchError(() => - concat( + catchError((err) => { + console.error('Error during upload conversations and folders', err); + return concat( of( ConversationsActions.uploadFoldersFail({ paths: new Set(payload.paths), }), ), of(ConversationsActions.uploadConversationsFail()), - ), - ), // TODO: handle error it in https://github.com/epam/ai-dial-chat/issues/663 + ); + }), + ), + ), + ); + +const uploadConversationsFailEpic: AppEpic = (action$) => + action$.pipe( + filter(ConversationsActions.uploadConversationsFail.match), + map(() => + UIActions.showErrorToast( + translate( + 'An error occurred while loading conversations and folders. Most likely the conversation already exists. Please refresh the page.', + ), ), ), ); @@ -2172,7 +2321,10 @@ const uploadConversationsWithFoldersRecursiveEpic: AppEpic = (action$) => of(ConversationsActions.initFoldersAndConversationsSuccess()), ); }), - catchError(() => of(ConversationsActions.uploadConversationsFail())), // TODO: handle error it in https://github.com/epam/ai-dial-chat/issues/663 + catchError((err) => { + console.error('Error during upload conversations and folders', err); + return of(ConversationsActions.uploadConversationsFail()); + }), ), ), ); @@ -2239,7 +2391,7 @@ export const ConversationsEpics = combineEpics( selectConversationsEpic, uploadSelectedConversationsEpic, - createNewConversationSuccessEpic, + saveNewConversationEpic, createNewConversationsSuccessEpic, saveFoldersEpic, deleteFolderEpic, @@ -2271,6 +2423,7 @@ export const ConversationsEpics = combineEpics( uploadConversationsWithFoldersEpic, uploadConversationsWithFoldersRecursiveEpic, + uploadConversationsFailEpic, toggleFolderEpic, openFolderEpic, compareConversationsEpic, diff --git a/apps/chat/src/store/conversations/conversations.reducers.ts b/apps/chat/src/store/conversations/conversations.reducers.ts index cbf3e01acc..43d4f62e75 100644 --- a/apps/chat/src/store/conversations/conversations.reducers.ts +++ b/apps/chat/src/store/conversations/conversations.reducers.ts @@ -215,7 +215,7 @@ export const conversationsSlice = createSlice({ state, _action: PayloadAction<{ conversationIds: string[] }>, ) => state, - deleteConversationsSuccess: ( + deleteConversationsComplete: ( state, { payload }: PayloadAction<{ deleteIds: Set }>, ) => { @@ -261,7 +261,11 @@ export const conversationsSlice = createSlice({ state, _action: PayloadAction, ) => state, - createNewConversationSuccess: ( + saveNewConversation: ( + state, + _action: PayloadAction<{ newConversation: Conversation }>, + ) => state, + saveNewConversationSuccess: ( state, { payload: { newConversation }, @@ -651,7 +655,6 @@ export const conversationsSlice = createSlice({ uploadConversationsWithFoldersRecursive: (state) => { state.conversationsStatus = UploadStatus.LOADING; }, - uploadConversationsSuccess: ( state, { diff --git a/apps/chat/src/store/prompts/prompts.epics.ts b/apps/chat/src/store/prompts/prompts.epics.ts index e2143fc850..1a38937b5c 100644 --- a/apps/chat/src/store/prompts/prompts.epics.ts +++ b/apps/chat/src/store/prompts/prompts.epics.ts @@ -86,21 +86,24 @@ const createNewPromptEpic: AppEpic = (action$, state$) => content: '', folderId: getRootId({ apiKey: ApiKeys.Prompts }), }); - - return of(PromptsActions.createNewPromptSuccess({ newPrompt })); + return PromptService.createPrompt(newPrompt).pipe( + switchMap(() => + of(PromptsActions.createNewPromptSuccess({ newPrompt })), + ), + catchError((err) => { + console.error("New prompt wasn't created:", err); + return of( + UIActions.showErrorToast( + translate( + 'An error occurred while creating a new prompt. Most likely the prompt already exists. Please refresh the page.', + ), + ), + ); + }), + ); }), ); -const createNewPromptSuccessEpic: AppEpic = (action$) => - action$.pipe( - filter(PromptsActions.createNewPromptSuccess.match), - switchMap(({ payload }) => - PromptService.createPrompt(payload.newPrompt).pipe( - switchMap(() => EMPTY), // TODO: handle error it in https://github.com/epam/ai-dial-chat/issues/663 - ), - ), - ); - const saveFoldersEpic: AppEpic = (action$, state$) => action$.pipe( filter( @@ -117,7 +120,16 @@ const saveFoldersEpic: AppEpic = (action$, state$) => promptsFolders: PromptsSelectors.selectFolders(state$.value), })), switchMap(({ promptsFolders }) => { - return PromptService.setPromptFolders(promptsFolders); + return PromptService.setPromptFolders(promptsFolders).pipe( + catchError((err) => { + console.error('An error occurred during the saving folders', err); + return of( + UIActions.showErrorToast( + translate('An error occurred during the saving folders'), + ), + ); + }), + ); }), ignoreElements(), ); @@ -139,7 +151,12 @@ const getOrUploadPrompt = ( }); return forkJoin({ - prompt: PromptService.getPrompt(prompt), + prompt: PromptService.getPrompt(prompt).pipe( + catchError((err) => { + console.error('The prompt was not found:', err); + return of(null); + }), + ), payload: of(payload), }); } else { @@ -154,8 +171,16 @@ const savePromptEpic: AppEpic = (action$) => action$.pipe( filter(PromptsActions.savePrompt.match), switchMap(({ payload: newPrompt }) => { - return PromptService.updatePrompt(newPrompt).pipe( - switchMap(() => EMPTY), // TODO: handle error it in https://github.com/epam/ai-dial-chat/issues/663 + return PromptService.updatePrompt(newPrompt).pipe(switchMap(() => EMPTY)); + }), + catchError((err) => { + console.error(err); + return of( + UIActions.showErrorToast( + translate( + 'An error occurred while saving the prompt. Please refresh the page.', + ), + ), ); }), ); @@ -165,15 +190,25 @@ const recreatePromptEpic: AppEpic = (action$) => filter(PromptsActions.recreatePrompt.match), mergeMap(({ payload }) => { const { parentPath } = splitEntityId(payload.old.id); - return concat( - PromptService.createPrompt(payload.new).pipe( - switchMap(() => EMPTY), // TODO: handle error it in https://github.com/epam/ai-dial-chat/issues/663 - ), + return zip( + PromptService.createPrompt(payload.new), PromptService.deletePrompt({ id: payload.old.id, folderId: parentPath || getRootId({ apiKey: ApiKeys.Prompts }), name: payload.old.name, - }).pipe(switchMap(() => EMPTY)), // TODO: handle error it in https://github.com/epam/ai-dial-chat/issues/663 + }), + ).pipe( + switchMap(() => EMPTY), + catchError((err) => { + console.error(err); + return of( + UIActions.showErrorToast( + translate( + 'An error occurred while saving the prompt. Please refresh the page.', + ), + ), + ); + }), ); }), ); @@ -189,7 +224,13 @@ const updatePromptEpic: AppEpic = (action$, state$) => }; if (!prompt) { - return EMPTY; // TODO: handle? + return of( + UIActions.showErrorToast( + translate( + 'It looks like this prompt has been deleted. Please reload the page', + ), + ), + ); } const newPrompt: Prompt = { @@ -217,7 +258,17 @@ export const deletePromptEpic: AppEpic = (action$) => filter(PromptsActions.deletePrompt.match), switchMap(({ payload }) => { return PromptService.deletePrompt(payload.prompt).pipe( - switchMap(() => EMPTY), // TODO: handle error it in https://github.com/epam/ai-dial-chat/issues/663 + switchMap(() => EMPTY), + catchError((err) => { + console.error(err); + return of( + UIActions.showErrorToast( + translate( + `An error occurred while saving the prompt "${payload.prompt.name}"`, + ), + ), + ); + }), ); }), ); @@ -240,14 +291,39 @@ const deletePromptsEpic: AppEpic = (action$) => deletePrompts: payload.promptsToRemove, })), switchMap(({ deletePrompts }) => - concat( - of( - PromptsActions.deletePromptsSuccess({ - deletePrompts, - }), + zip( + deletePrompts.map((info) => + PromptService.deletePrompt(info).pipe( + switchMap(() => of(null)), + catchError((err) => { + console.error( + `An error occurred while deleting the prompt "${info.name}"`, + err, + ); + return info.name; + }), + ), ), - zip(deletePrompts.map((id) => PromptService.deletePrompt(id))).pipe( - switchMap(() => EMPTY), // TODO: handle error it in https://github.com/epam/ai-dial-chat/issues/663 + ).pipe( + switchMap((failedNames) => + concat( + iif( + () => failedNames.filter(Boolean).length > 0, + of( + UIActions.showErrorToast( + translate( + `An error occurred while saving the prompt(s): "${failedNames.filter(Boolean).join('", "')}"`, + ), + ), + ), + EMPTY, + ), + of( + PromptsActions.deletePromptsComplete({ + deletePrompts, + }), + ), + ), ), ), ), @@ -334,6 +410,14 @@ const updateFolderEpic: AppEpic = (action$, state$) => return concat(...actions); }), + catchError((err) => { + console.error('An error occurred while updating the folder:', err); + return of( + UIActions.showErrorToast( + translate('An error occurred while updating the folder.'), + ), + ); + }), ); }), ); @@ -344,7 +428,15 @@ const deleteFolderEpic: AppEpic = (action$, state$) => switchMap(({ payload }) => forkJoin({ folderId: of(payload.folderId), - promptsToRemove: PromptService.getPrompts(payload.folderId, true), + promptsToRemove: PromptService.getPrompts(payload.folderId, true).pipe( + catchError((err) => { + console.error( + 'An error occurred while uploading prompts and folders:', + err, + ); + return []; + }), + ), folders: of(PromptsSelectors.selectFolders(state$.value)), }), ), @@ -379,8 +471,17 @@ const deleteFolderEpic: AppEpic = (action$, state$) => const exportPromptsEpic: AppEpic = (action$, state$) => action$.pipe( filter(PromptsActions.exportPrompts.match), - switchMap( - () => PromptService.getPrompts(undefined, true), //listing of all entities + switchMap(() => + //listing of all entities + PromptService.getPrompts(undefined, true).pipe( + catchError((err) => { + console.error( + 'An error occurred while uploading prompts and folders:', + err, + ); + return []; + }), + ), ), switchMap((promptsListing) => { const foldersIds = Array.from( @@ -400,6 +501,11 @@ const exportPromptsEpic: AppEpic = (action$, state$) => //get all prompts from api prompts: zip( promptsListing.map((info) => PromptService.getPrompt(info)), + ).pipe( + catchError((err) => { + console.error('An error occurred while uploading prompts:', err); + return []; + }), ), folders: of(folders), }); @@ -441,7 +547,15 @@ const importPromptsEpic: AppEpic = (action$) => filter(PromptsActions.importPrompts.match), switchMap(({ payload }) => forkJoin({ - promptsListing: PromptService.getPrompts(undefined, true), //listing of all entities + promptsListing: PromptService.getPrompts(undefined, true).pipe( + catchError((err) => { + console.error( + 'An error occurred while uploading prompts and folders:', + err, + ); + return []; + }), + ), //listing of all entities promptsHistory: of(payload.promptsHistory), }), ), @@ -469,6 +583,11 @@ const importPromptsEpic: AppEpic = (action$) => //get all prompts from api const currentPrompts = zip( promptsListing.map((info) => PromptService.getPrompt(info)), + ).pipe( + catchError((err) => { + console.error('An error occurred while uploading prompts:', err); + return []; + }), ); return forkJoin({ @@ -481,12 +600,11 @@ const importPromptsEpic: AppEpic = (action$) => const filteredPrompts = currentPrompts.filter(Boolean) as Prompt[]; if (!isPromptsFormat(promptsHistory)) { return of( - UIActions.showToast({ - message: translate(errorsMessages.unsupportedDataFormat, { + UIActions.showErrorToast( + translate(errorsMessages.unsupportedDataFormat, { ns: 'common', }), - type: 'error', - }), + ), ); } const preparedPrompts: Prompt[] = getPreparedPrompts({ @@ -509,12 +627,11 @@ const importPromptsEpic: AppEpic = (action$) => if (isError) { return of( - UIActions.showToast({ - message: translate(errorsMessages.unsupportedDataFormat, { + UIActions.showErrorToast( + translate(errorsMessages.unsupportedDataFormat, { ns: 'common', }), - type: 'error', - }), + ), ); } return of(PromptsActions.importPromptsSuccess({ prompts, folders })); @@ -684,17 +801,19 @@ const initPromptsEpic: AppEpic = (action$) => of(PromptsActions.initPromptsSuccess()), ); }), + catchError((err) => { + console.error( + 'An error occurred while uploading prompts and folders:', + err, + ); + return []; + }), ); const initEpic: AppEpic = (action$) => action$.pipe( filter((action) => PromptsActions.init.match(action)), - switchMap(() => - concat( - // of(PromptsActions.initFolders()), - of(PromptsActions.initPrompts()), - ), - ), + switchMap(() => concat(of(PromptsActions.initPrompts()))), ); export const uploadPromptEpic: AppEpic = (action$, state$) => @@ -716,6 +835,14 @@ export const uploadPromptEpic: AppEpic = (action$, state$) => originalPromptId: originalPrompt.id, }); }), + catchError((err) => { + console.error('An error occurred while uploading the prompt:', err); + return of( + UIActions.showErrorToast( + translate('An error occurred while uploading the prompt'), + ), + ); + }), ); export const PromptsEpics = combineEpics( @@ -738,7 +865,6 @@ export const PromptsEpics = combineEpics( deletePromptsEpic, updateFolderEpic, createNewPromptEpic, - createNewPromptSuccessEpic, uploadPromptEpic, ); diff --git a/apps/chat/src/store/prompts/prompts.reducers.ts b/apps/chat/src/store/prompts/prompts.reducers.ts index c70bf357fa..d259ec876e 100644 --- a/apps/chat/src/store/prompts/prompts.reducers.ts +++ b/apps/chat/src/store/prompts/prompts.reducers.ts @@ -103,7 +103,7 @@ export const promptsSlice = createSlice({ (p) => !promptToDeleteIds.includes(p.id), ); }, - deletePromptsSuccess: ( + deletePromptsComplete: ( state, { payload }: PayloadAction<{ deletePrompts: PromptInfo[] }>, ) => { diff --git a/apps/chat/src/store/ui/ui.epics.ts b/apps/chat/src/store/ui/ui.epics.ts index 082ea8f8a5..2810c383f9 100644 --- a/apps/chat/src/store/ui/ui.epics.ts +++ b/apps/chat/src/store/ui/ui.epics.ts @@ -102,7 +102,31 @@ const saveShowPromptbarEpic: AppEpic = (action$) => ignoreElements(), ); -const showToastErrorEpic: AppEpic = (action$) => +const showErrorToastEpic: AppEpic = (action$) => + action$.pipe( + filter(UIActions.showErrorToast.match), + switchMap(({ payload }) => + of(UIActions.showToast({ message: payload, type: 'error' })), + ), + ); + +const showLoadingToastEpic: AppEpic = (action$) => + action$.pipe( + filter(UIActions.showLoadingToast.match), + switchMap(({ payload }) => + of(UIActions.showToast({ message: payload, type: 'loading' })), + ), + ); + +const showSuccessToastEpic: AppEpic = (action$) => + action$.pipe( + filter(UIActions.showSuccessToast.match), + switchMap(({ payload }) => + of(UIActions.showToast({ message: payload, type: 'success' })), + ), + ); + +const showToastEpic: AppEpic = (action$) => action$.pipe( filter(UIActions.showToast.match), switchMap(({ payload }) => { @@ -177,7 +201,10 @@ const UIEpics = combineEpics( saveThemeEpic, saveShowChatbarEpic, saveShowPromptbarEpic, - showToastErrorEpic, + showToastEpic, + showErrorToastEpic, + showLoadingToastEpic, + showSuccessToastEpic, closeAnnouncementEpic, saveChatbarWidthEpic, savePromptbarWidthEpic, diff --git a/apps/chat/src/store/ui/ui.reducers.ts b/apps/chat/src/store/ui/ui.reducers.ts index 07b36d51b7..7ffc6bdcfd 100644 --- a/apps/chat/src/store/ui/ui.reducers.ts +++ b/apps/chat/src/store/ui/ui.reducers.ts @@ -104,6 +104,9 @@ export const uiSlice = createSlice({ response?: Response; }>, ) => state, + showErrorToast: (state, _action: PayloadAction) => state, + showLoadingToast: (state, _action: PayloadAction) => state, + showSuccessToast: (state, _action: PayloadAction) => state, setOpenedFoldersIds: ( state, { diff --git a/apps/chat/src/utils/app/data/file-service.ts b/apps/chat/src/utils/app/data/file-service.ts index 48a3eb3f7f..49e37da180 100644 --- a/apps/chat/src/utils/app/data/file-service.ts +++ b/apps/chat/src/utils/app/data/file-service.ts @@ -81,6 +81,19 @@ export class FileService { ); } + private static getListingUrl = ({ + path, + resultQuery, + }: { + path?: string; + resultQuery?: string; + }): string => { + const listingUrl = encodeApiUrl( + constructPath('api/listing', path || getRootId()), + ); + return resultQuery ? `${listingUrl}?${resultQuery}` : listingUrl; + }; + public static getFileFolders( parentPath?: string, ): Observable { @@ -88,14 +101,12 @@ export class FileService { const query = new URLSearchParams({ filter, - bucket: BucketService.getBucket(), - ...(parentPath && { - path: parentPath, - }), }); const resultQuery = query.toString(); - return ApiUtils.request(`api/${ApiKeys.Files}/listing?${resultQuery}`).pipe( + return ApiUtils.request( + this.getListingUrl({ path: parentPath, resultQuery }), + ).pipe( map((folders: BackendFileFolder[]) => { return folders.map((folder): FileFolderInterface => { const relativePath = folder.parentPath @@ -146,14 +157,12 @@ export class FileService { const query = new URLSearchParams({ filter, - bucket: BucketService.getBucket(), - ...(parentPath && { - path: parentPath, - }), }); const resultQuery = query.toString(); - return ApiUtils.request(`api/${ApiKeys.Files}/listing?${resultQuery}`).pipe( + return ApiUtils.request( + this.getListingUrl({ path: parentPath, resultQuery }), + ).pipe( map((files: BackendFile[]) => { return files.map((file): DialFile => { const relativePath = file.parentPath diff --git a/apps/chat/src/utils/app/data/storages/api-storage.ts b/apps/chat/src/utils/app/data/storages/api-storage.ts index e4ebe3b876..c4a932e9c9 100644 --- a/apps/chat/src/utils/app/data/storages/api-storage.ts +++ b/apps/chat/src/utils/app/data/storages/api-storage.ts @@ -56,7 +56,9 @@ export class ApiStorage implements DialStorage { const newName = generateNextName( defaultName, entity.name, - entities.filter((e) => e.folderId === entity.folderId), + entities.filter( + (e) => e.folderId === entity.folderId && e.id !== entity.id, + ), ); const updatedEntity = { ...entity, 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 17b987ff39..3adbd9050f 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 @@ -1,4 +1,4 @@ -import { EMPTY, Observable, catchError, map, of } from 'rxjs'; +import { Observable, map } from 'rxjs'; import { ApiKeys, @@ -20,7 +20,7 @@ import { EntityStorage } from '@/src/types/storage'; import { constructPath } from '../../../file'; import { splitEntityId } from '../../../folders'; -import { BucketService } from '../../bucket-service'; +import { getRootId } from '../../../id'; export abstract class ApiEntityStorage< TEntityInfo extends Entity, @@ -57,23 +57,26 @@ export abstract class ApiEntityStorage< private getEntityUrl = (entity: TEntityInfo): string => encodeApiUrl(constructPath('api', entity.id)); - private getListingUrl = (resultQuery: string): string => { + private getListingUrl = ({ + path, + resultQuery, + }: { + path?: string; + resultQuery?: string; + }): string => { const listingUrl = encodeApiUrl( - constructPath('api', this.getStorageKey(), 'listing'), + constructPath( + 'api/listing', + path || getRootId({ apiKey: this.getStorageKey() }), + ), ); - return `${listingUrl}?${resultQuery}`; + return resultQuery ? `${listingUrl}?${resultQuery}` : listingUrl; }; getFoldersAndEntities( path?: string | undefined, ): Observable> { - const query = new URLSearchParams({ - bucket: BucketService.getBucket(), - ...(path && { path }), - }); - const resultQuery = query.toString(); - - return ApiUtils.request(this.getListingUrl(resultQuery)).pipe( + return ApiUtils.request(this.getListingUrl({ path })).pipe( map((items: (BackendChatFolder | BackendChatEntity)[]) => { const folders = items.filter( (item) => item.nodeType === BackendDataNodeType.FOLDER, @@ -87,12 +90,6 @@ export abstract class ApiEntityStorage< folders: folders.map((folder) => this.mapFolder(folder)), }; }), - catchError(() => - of({ - entities: [], - folders: [], - }), - ), // TODO: handle error it in https://github.com/epam/ai-dial-chat/issues/663 ); } @@ -101,16 +98,13 @@ export abstract class ApiEntityStorage< const query = new URLSearchParams({ filter, - bucket: BucketService.getBucket(), - ...(path && { path }), }); const resultQuery = query.toString(); - return ApiUtils.request(this.getListingUrl(resultQuery)).pipe( + return ApiUtils.request(this.getListingUrl({ path, resultQuery })).pipe( map((folders: BackendChatFolder[]) => { return folders.map((folder) => this.mapFolder(folder)); }), - catchError(() => of([])), // TODO: handle error it in https://github.com/epam/ai-dial-chat/issues/663 ); } @@ -119,17 +113,14 @@ export abstract class ApiEntityStorage< const query = new URLSearchParams({ filter, - bucket: BucketService.getBucket(), - ...(path && { path }), ...(recursive && { recursive: String(recursive) }), }); const resultQuery = query.toString(); - return ApiUtils.request(this.getListingUrl(resultQuery)).pipe( + return ApiUtils.request(this.getListingUrl({ path, resultQuery })).pipe( map((entities: BackendChatEntity[]) => { return entities.map((entity) => this.mapEntity(entity)); }), - catchError(() => of([])), // TODO: handle error it in https://github.com/epam/ai-dial-chat/issues/663 ); } @@ -141,7 +132,6 @@ export abstract class ApiEntityStorage< status: UploadStatus.LOADED, }; }), - catchError(() => of(null)), // TODO: handle error it in https://github.com/epam/ai-dial-chat/issues/663 ); } @@ -152,7 +142,7 @@ export abstract class ApiEntityStorage< 'Content-Type': 'application/json', }, body: JSON.stringify(this.cleanUpEntity(entity)), - }); // TODO: handle error it in https://github.com/epam/ai-dial-chat/issues/663 + }); } updateEntity(entity: TEntity): Observable { @@ -162,7 +152,7 @@ export abstract class ApiEntityStorage< 'Content-Type': 'application/json', }, body: JSON.stringify(this.cleanUpEntity(entity)), - }).pipe(catchError(() => EMPTY)); // TODO: handle error it in https://github.com/epam/ai-dial-chat/issues/663 + }); } deleteEntity(info: TEntityInfo): Observable { @@ -171,7 +161,7 @@ export abstract class ApiEntityStorage< headers: { 'Content-Type': 'application/json', }, - }).pipe(catchError(() => EMPTY)); // TODO: handle error it in https://github.com/epam/ai-dial-chat/issues/663 + }); } abstract getEntityKey(info: TEntityInfo): string; diff --git a/apps/chat/src/utils/app/data/storages/api/conversation-api-storage.ts b/apps/chat/src/utils/app/data/storages/api/conversation-api-storage.ts index d70d5e63d2..59ede29d4a 100644 --- a/apps/chat/src/utils/app/data/storages/api/conversation-api-storage.ts +++ b/apps/chat/src/utils/app/data/storages/api/conversation-api-storage.ts @@ -1,4 +1,4 @@ -import { Observable, forkJoin, of } from 'rxjs'; +import { Observable, catchError, forkJoin, of } from 'rxjs'; import { getRootId } from '@/src/utils/app/id'; import { @@ -66,7 +66,12 @@ export const getOrUploadConversation = ( if (conversation?.status !== UploadStatus.LOADED) { return forkJoin({ - conversation: ConversationService.getConversation(conversation), + conversation: ConversationService.getConversation(conversation).pipe( + catchError((err) => { + console.error('The conversation was not found:', err); + return of(null); + }), + ), payload: of(payload), }); } else { diff --git a/apps/chat/src/utils/server/api.ts b/apps/chat/src/utils/server/api.ts index 6b4b1c43ec..1267f517aa 100644 --- a/apps/chat/src/utils/server/api.ts +++ b/apps/chat/src/utils/server/api.ts @@ -11,7 +11,6 @@ import { PromptInfo } from '@/src/types/prompt'; import { EMPTY_MODEL_ID } from '@/src/constants/default-settings'; import { constructPath } from '../app/file'; -import { OpenAIError } from './error'; export enum ApiKeys { Files = 'files', @@ -67,7 +66,7 @@ export const getEntityTypeFromPath = ( return Array.isArray(req.query.entitytype) ? '' : req.query.entitytype; }; -const encodeSlugs = (slugs: (string | undefined)[]): string => +export const encodeSlugs = (slugs: (string | undefined)[]): string => constructPath( ...slugs.filter(Boolean).map((part) => encodeURIComponent(part as string)), ); @@ -78,22 +77,6 @@ export const encodeApiUrl = (path: string): string => export const decodeApiUrl = (path: string): string => constructPath(...path.split('/').map((part) => decodeURIComponent(part))); -export const getEntityUrlFromSlugs = ( - dialApiHost: string, - req: NextApiRequest, -): string => { - const entityType = getEntityTypeFromPath(req); - const slugs = Array.isArray(req.query.slug) - ? req.query.slug - : [req.query.slug]; - - if (!slugs || slugs.length === 0) { - throw new OpenAIError(`No ${entityType} path provided`, '', '', '404'); - } - - return `${dialApiHost}/v1/${entityType}/${encodeSlugs(slugs)}`; -}; - const pathKeySeparator = '__'; const encodedKeySeparator = '%5F%5F';