From 610358372bb823c39465a26fd6ec5ac09b19f61e Mon Sep 17 00:00:00 2001 From: Alexander <98586297+Alexander-Kezik@users.noreply.github.com> Date: Wed, 14 Feb 2024 10:09:58 +0100 Subject: [PATCH 1/5] fix(chat): fix unique name validation (Issue #633) (#698) --- .../src/components/Chatbar/ChatFolders.tsx | 41 +++++++++++++++---- apps/chat/src/components/Chatbar/Chatbar.tsx | 39 ++++++++++++++++-- .../src/components/Chatbar/Conversation.tsx | 37 +++++++++++++---- .../src/components/Promptbar/Promptbar.tsx | 40 +++++++++++++++--- .../Promptbar/components/Prompt.tsx | 36 +++++++++++++++- .../Promptbar/components/PromptFolders.tsx | 39 +++++++++++++++--- .../components/Sidebar/BetweenFoldersLine.tsx | 11 ++--- apps/chat/src/utils/app/common.ts | 1 + 8 files changed, 204 insertions(+), 40 deletions(-) diff --git a/apps/chat/src/components/Chatbar/ChatFolders.tsx b/apps/chat/src/components/Chatbar/ChatFolders.tsx index e0ccaffd29..78a309ae77 100644 --- a/apps/chat/src/components/Chatbar/ChatFolders.tsx +++ b/apps/chat/src/components/Chatbar/ChatFolders.tsx @@ -2,14 +2,16 @@ import { DragEvent, useCallback, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'next-i18next'; +import { isEntityNameOnSameLevelUnique } from '@/src/utils/app/common'; import { compareEntitiesByName } from '@/src/utils/app/folders'; -import { isRootId } from '@/src/utils/app/id'; +import { getRootId, isRootId } from '@/src/utils/app/id'; import { MoveType } from '@/src/utils/app/move'; import { PublishedWithMeFilter, SharedWithMeFilter, } from '@/src/utils/app/search'; import { isEntityOrParentsExternal } from '@/src/utils/app/share'; +import { ApiKeys } from '@/src/utils/server/api'; import { Conversation } from '@/src/types/chat'; import { FeatureType } from '@/src/types/common'; @@ -23,7 +25,7 @@ import { } from '@/src/store/conversations/conversations.reducers'; import { useAppDispatch, useAppSelector } from '@/src/store/hooks'; import { SettingsSelectors } from '@/src/store/settings/settings.reducers'; -import { UISelectors } from '@/src/store/ui/ui.reducers'; +import { UIActions, UISelectors } from '@/src/store/ui/ui.reducers'; import { MAX_CHAT_AND_PROMPT_FOLDERS_DEPTH, @@ -51,6 +53,8 @@ const ChatFolderTemplate = ({ filters, includeEmpty = false, }: ChatFolderProps) => { + const { t } = useTranslation(Translation.SideBar); + const dispatch = useAppDispatch(); const searchTerm = useAppSelector(ConversationsSelectors.selectSearchTerm); @@ -122,15 +126,40 @@ const ChatFolderTemplate = ({ [dispatch], ); const onDropBetweenFolders = useCallback( - (folder: FolderInterface, parentFolderId: string | undefined) => { + (folder: FolderInterface) => { + const folderId = getRootId({ apiKey: ApiKeys.Conversations }); + + if ( + !isEntityNameOnSameLevelUnique( + folder.name, + { ...folder, folderId }, + allFolders, + ) + ) { + dispatch( + UIActions.showToast({ + message: t( + 'Folder with name "{{name}}" already exists at the root.', + { + ns: 'folder', + name: folder.name, + }, + ), + type: 'error', + }), + ); + + return; + } + dispatch( ConversationsActions.updateFolder({ folderId: folder.id, - values: { folderId: parentFolderId }, + values: { folderId }, }), ); }, - [dispatch], + [allFolders, dispatch, t], ); const handleFolderClick = useCallback( @@ -145,7 +174,6 @@ const ChatFolderTemplate = ({ @@ -181,7 +209,6 @@ const ChatFolderTemplate = ({ diff --git a/apps/chat/src/components/Chatbar/Chatbar.tsx b/apps/chat/src/components/Chatbar/Chatbar.tsx index c72b23f1fa..6fde253dae 100644 --- a/apps/chat/src/components/Chatbar/Chatbar.tsx +++ b/apps/chat/src/components/Chatbar/Chatbar.tsx @@ -2,7 +2,10 @@ import { DragEvent, useCallback } from 'react'; import { useTranslation } from 'next-i18next'; +import { isEntityNameOnSameLevelUnique } from '@/src/utils/app/common'; +import { getRootId } from '@/src/utils/app/id'; import { MoveType } from '@/src/utils/app/move'; +import { ApiKeys } from '@/src/utils/server/api'; import { ConversationInfo } from '@/src/types/chat'; import { FeatureType } from '@/src/types/common'; @@ -14,7 +17,7 @@ import { ConversationsSelectors, } from '@/src/store/conversations/conversations.reducers'; import { useAppDispatch, useAppSelector } from '@/src/store/hooks'; -import { UISelectors } from '@/src/store/ui/ui.reducers'; +import { UIActions, UISelectors } from '@/src/store/ui/ui.reducers'; import { DEFAULT_CONVERSATION_NAME } from '@/src/constants/default-settings'; @@ -54,10 +57,15 @@ const ChatActionsBlock = () => { }; export const Chatbar = () => { + const { t } = useTranslation(Translation.Chat); + const dispatch = useAppDispatch(); const showChatbar = useAppSelector(UISelectors.selectShowChatbar); const searchTerm = useAppSelector(ConversationsSelectors.selectSearchTerm); + const allConversations = useAppSelector( + ConversationsSelectors.selectConversations, + ); const areEntitiesUploaded = useAppSelector( ConversationsSelectors.areConversationsUploaded, ); @@ -82,17 +90,42 @@ export const Chatbar = () => { const conversationData = e.dataTransfer.getData(MoveType.Conversation); if (conversationData) { const conversation = JSON.parse(conversationData); + const folderId = getRootId({ apiKey: ApiKeys.Conversations }); + + if ( + !isEntityNameOnSameLevelUnique( + conversation.name, + { ...conversation, folderId }, + allConversations, + ) + ) { + dispatch( + UIActions.showToast({ + message: t( + 'Prompt with name "{{name}}" already exists at the root.', + { + ns: 'prompt', + name: conversation.name, + }, + ), + type: 'error', + }), + ); + + return; + } + dispatch( ConversationsActions.updateConversation({ id: conversation.id, - values: { folderId: undefined }, + values: { folderId }, }), ); dispatch(ConversationsActions.resetSearch()); } } }, - [dispatch], + [allConversations, dispatch, t], ); return ( diff --git a/apps/chat/src/components/Chatbar/Conversation.tsx b/apps/chat/src/components/Chatbar/Conversation.tsx index 3bd3e92e43..72363270ef 100644 --- a/apps/chat/src/components/Chatbar/Conversation.tsx +++ b/apps/chat/src/components/Chatbar/Conversation.tsx @@ -34,6 +34,7 @@ import { FeatureType, isNotLoaded, } from '@/src/types/common'; +import { MoveToFolderProps } from '@/src/types/folder'; import { SharingType } from '@/src/types/share'; import { Translation } from '@/src/types/translation'; @@ -372,14 +373,32 @@ export const ConversationComponent = ({ item: conversation, level }: Props) => { }, []); const handleMoveToFolder = useCallback( - ({ - folderId, - isNewFolder, - }: { - folderId?: string; - isNewFolder?: boolean; - }) => { - const folderPath = isNewFolder ? newFolderName : folderId; + ({ folderId, isNewFolder }: MoveToFolderProps) => { + const folderPath = (isNewFolder ? newFolderName : folderId) as string; + + if ( + !isEntityNameOnSameLevelUnique( + conversation.name, + { ...conversation, folderId: folderPath }, + allConversations, + ) + ) { + dispatch( + UIActions.showToast({ + message: t( + 'Conversation with name "{{name}}" already exists in this folder.', + { + ns: 'chat', + name: conversation.name, + }, + ), + type: 'error', + }), + ); + + return; + } + if (isNewFolder) { dispatch( ConversationsActions.createFolder({ @@ -402,7 +421,7 @@ export const ConversationComponent = ({ item: conversation, level }: Props) => { }), ); }, - [conversation.id, dispatch, newFolderName], + [allConversations, conversation, dispatch, newFolderName, t], ); const handleOpenExportModal = useCallback(() => { setIsShowExportModal(true); diff --git a/apps/chat/src/components/Promptbar/Promptbar.tsx b/apps/chat/src/components/Promptbar/Promptbar.tsx index 8fca211f3e..f99048363d 100644 --- a/apps/chat/src/components/Promptbar/Promptbar.tsx +++ b/apps/chat/src/components/Promptbar/Promptbar.tsx @@ -2,7 +2,10 @@ import { DragEvent, useCallback } from 'react'; import { useTranslation } from 'next-i18next'; +import { isEntityNameOnSameLevelUnique } from '@/src/utils/app/common'; +import { getRootId } from '@/src/utils/app/id'; import { MoveType } from '@/src/utils/app/move'; +import { ApiKeys } from '@/src/utils/server/api'; import { FeatureType } from '@/src/types/common'; import { PromptInfo } from '@/src/types/prompt'; @@ -14,7 +17,7 @@ import { PromptsActions, PromptsSelectors, } from '@/src/store/prompts/prompts.reducers'; -import { UISelectors } from '@/src/store/ui/ui.reducers'; +import { UIActions, UISelectors } from '@/src/store/ui/ui.reducers'; import { PromptFolders } from './components/PromptFolders'; import { PromptbarSettings } from './components/PromptbarSettings'; @@ -46,8 +49,11 @@ const PromptActionsBlock = () => { }; const Promptbar = () => { + const { t } = useTranslation(Translation.PromptBar); + const dispatch = useAppDispatch(); const showPromptbar = useAppSelector(UISelectors.selectShowPromptbar); + const allPrompts = useAppSelector(PromptsSelectors.selectPrompts); const searchTerm = useAppSelector(PromptsSelectors.selectSearchTerm); const myItemsFilters = useAppSelector(PromptsSelectors.selectMyItemsFilters); const areEntitiesUploaded = useAppSelector( @@ -64,20 +70,44 @@ const Promptbar = () => { (e: DragEvent) => { if (e.dataTransfer) { const promptData = e.dataTransfer.getData(MoveType.Prompt); + const folderId = getRootId({ apiKey: ApiKeys.Prompts }); + if (promptData) { const prompt = JSON.parse(promptData); + + if ( + !isEntityNameOnSameLevelUnique( + prompt.name, + { ...prompt, folderId }, + allPrompts, + ) + ) { + dispatch( + UIActions.showToast({ + message: t( + 'Prompt with name "{{name}}" already exists at the root.', + { + ns: 'prompt', + name: prompt.name, + }, + ), + type: 'error', + }), + ); + + return; + } + dispatch( PromptsActions.updatePrompt({ id: prompt.id, - values: { - folderId: e.currentTarget.dataset.folderId, - }, + values: { folderId }, }), ); } } }, - [dispatch], + [allPrompts, dispatch, t], ); return ( diff --git a/apps/chat/src/components/Promptbar/components/Prompt.tsx b/apps/chat/src/components/Promptbar/components/Prompt.tsx index f150cf2d12..75110ff37c 100644 --- a/apps/chat/src/components/Promptbar/components/Prompt.tsx +++ b/apps/chat/src/components/Promptbar/components/Prompt.tsx @@ -8,8 +8,11 @@ import { useState, } from 'react'; +import { useTranslation } from 'next-i18next'; + import classNames from 'classnames'; +import { isEntityNameOnSameLevelUnique } from '@/src/utils/app/common'; import { constructPath } from '@/src/utils/app/file'; import { getRootId } from '@/src/utils/app/id'; import { hasParentWithFloatingOverlay } from '@/src/utils/app/modals'; @@ -26,6 +29,7 @@ import { import { MoveToFolderProps } from '@/src/types/folder'; import { Prompt, PromptInfo } from '@/src/types/prompt'; import { SharingType } from '@/src/types/share'; +import { Translation } from '@/src/types/translation'; import { useAppDispatch, useAppSelector } from '@/src/store/hooks'; import { @@ -33,6 +37,7 @@ import { PromptsSelectors, } from '@/src/store/prompts/prompts.reducers'; import { ShareActions } from '@/src/store/share/share.reducers'; +import { UIActions } from '@/src/store/ui/ui.reducers'; import { stopBubbling } from '@/src/constants/chat'; @@ -53,6 +58,8 @@ interface Props { export const PromptComponent = ({ item: prompt, level }: Props) => { const dispatch = useAppDispatch(); + const { t } = useTranslation(Translation.Chat); + const folders = useAppSelector((state) => PromptsSelectors.selectFilteredFolders( state, @@ -80,6 +87,7 @@ export const PromptComponent = ({ item: prompt, level }: Props) => { const newFolderName = useAppSelector((state) => PromptsSelectors.selectNewFolderName(state, prompt.folderId), ); + const allPrompts = useAppSelector(PromptsSelectors.selectPrompts); const { refs, context } = useFloating({ open: isContextMenu, @@ -207,7 +215,31 @@ export const PromptComponent = ({ item: prompt, level }: Props) => { const handleMoveToFolder = useCallback( ({ folderId, isNewFolder }: MoveToFolderProps) => { - const folderPath = isNewFolder ? newFolderName : folderId; + const folderPath = (isNewFolder ? newFolderName : folderId) as string; + + if ( + !isEntityNameOnSameLevelUnique( + prompt.name, + { ...prompt, folderId: folderPath }, + allPrompts, + ) + ) { + dispatch( + UIActions.showToast({ + message: t( + 'Prompt with name "{{name}}" already exists in this folder.', + { + ns: 'prompt', + name: prompt.name, + }, + ), + type: 'error', + }), + ); + + return; + } + if (isNewFolder) { dispatch( PromptsActions.createFolder({ @@ -231,7 +263,7 @@ export const PromptComponent = ({ item: prompt, level }: Props) => { ); setIsContextMenu(false); }, - [dispatch, newFolderName, prompt.id], + [allPrompts, dispatch, newFolderName, prompt, t], ); const handleClose = useCallback(() => { diff --git a/apps/chat/src/components/Promptbar/components/PromptFolders.tsx b/apps/chat/src/components/Promptbar/components/PromptFolders.tsx index cf9b1e0089..b23a577487 100644 --- a/apps/chat/src/components/Promptbar/components/PromptFolders.tsx +++ b/apps/chat/src/components/Promptbar/components/PromptFolders.tsx @@ -2,14 +2,16 @@ import { DragEvent, useCallback, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'next-i18next'; +import { isEntityNameOnSameLevelUnique } from '@/src/utils/app/common'; import { compareEntitiesByName } from '@/src/utils/app/folders'; -import { isRootId } from '@/src/utils/app/id'; +import { getRootId, isRootId } from '@/src/utils/app/id'; import { MoveType } from '@/src/utils/app/move'; import { PublishedWithMeFilter, SharedWithMeFilter, } from '@/src/utils/app/search'; import { isEntityOrParentsExternal } from '@/src/utils/app/share'; +import { ApiKeys } from '@/src/utils/server/api'; import { FeatureType } from '@/src/types/common'; import { FolderInterface, FolderSectionProps } from '@/src/types/folder'; @@ -49,6 +51,8 @@ const PromptFolderTemplate = ({ filters, includeEmpty = false, }: promptFolderProps) => { + const { t } = useTranslation(Translation.SideBar); + const dispatch = useAppDispatch(); const searchTerm = useAppSelector(PromptsSelectors.selectSearchTerm); @@ -112,15 +116,40 @@ const PromptFolderTemplate = ({ ); const onDropBetweenFolders = useCallback( - (folder: FolderInterface, parentFolderId: string | undefined) => { + (folder: FolderInterface) => { + const folderId = getRootId({ apiKey: ApiKeys.Prompts }); + + if ( + !isEntityNameOnSameLevelUnique( + folder.name, + { ...folder, folderId }, + allFolders, + ) + ) { + dispatch( + UIActions.showToast({ + message: t( + 'Folder with name "{{name}}" already exists at the root.', + { + ns: 'folder', + name: folder.name, + }, + ), + type: 'error', + }), + ); + + return; + } + dispatch( PromptsActions.updateFolder({ folderId: folder.id, - values: { folderId: parentFolderId }, + values: { folderId }, }), ); }, - [dispatch], + [allFolders, dispatch, t], ); const handleFolderClick = useCallback( @@ -140,7 +169,6 @@ const PromptFolderTemplate = ({ @@ -174,7 +202,6 @@ const PromptFolderTemplate = ({ diff --git a/apps/chat/src/components/Sidebar/BetweenFoldersLine.tsx b/apps/chat/src/components/Sidebar/BetweenFoldersLine.tsx index babf498a07..904d5fa259 100644 --- a/apps/chat/src/components/Sidebar/BetweenFoldersLine.tsx +++ b/apps/chat/src/components/Sidebar/BetweenFoldersLine.tsx @@ -12,11 +12,7 @@ import { FolderInterface } from '@/src/types/folder'; interface BetweenFoldersLineProps { level: number; - parentFolderId: string | undefined; - onDrop: ( - folderData: FolderInterface, - parentFolderId: string | undefined, - ) => void; + onDrop: (folderData: FolderInterface) => void; onDraggingOver?: (isDraggingOver: boolean) => void; featureType: FeatureType; denyDrop?: boolean; @@ -24,7 +20,6 @@ interface BetweenFoldersLineProps { export const BetweenFoldersLine = ({ level, - parentFolderId, onDrop, onDraggingOver, featureType, @@ -47,10 +42,10 @@ export const BetweenFoldersLine = ({ const folderData = e.dataTransfer.getData(getFolderMoveType(featureType)); if (folderData) { - onDrop(JSON.parse(folderData), parentFolderId); + onDrop(JSON.parse(folderData)); } }, - [denyDrop, featureType, onDrop, parentFolderId], + [denyDrop, featureType, onDrop], ); const allowDrop = useCallback( diff --git a/apps/chat/src/utils/app/common.ts b/apps/chat/src/utils/app/common.ts index d9ef790837..f4b36f9a10 100644 --- a/apps/chat/src/utils/app/common.ts +++ b/apps/chat/src/utils/app/common.ts @@ -38,6 +38,7 @@ export const isEntityNameOnSameLevelUnique = < const sameLevelEntities = entities.filter( (e) => entity.id !== e.id && e.folderId === entity.folderId, ); + return !sameLevelEntities.some((e) => nameToBeUnique === e.name); }; From 25571bb97c2d12a3ab6ae9c6d6e33cb09b589259 Mon Sep 17 00:00:00 2001 From: Alexander <98586297+Alexander-Kezik@users.noreply.github.com> Date: Wed, 14 Feb 2024 10:51:48 +0100 Subject: [PATCH 2/5] fix(chat): fix ids (Issue #265) (#702) --- .../src/store/conversations/conversations.epics.ts | 1 + apps/chat/src/store/prompts/prompts.epics.ts | 1 + apps/chat/src/utils/app/data/prompt-service.ts | 12 ++++++++++-- .../app/data/storages/api/api-entity-storage.ts | 2 +- .../data/storages/api/conversation-api-storage.ts | 14 ++++++++++++-- 5 files changed, 25 insertions(+), 5 deletions(-) diff --git a/apps/chat/src/store/conversations/conversations.epics.ts b/apps/chat/src/store/conversations/conversations.epics.ts index 0db8718cdf..de32d8053f 100644 --- a/apps/chat/src/store/conversations/conversations.epics.ts +++ b/apps/chat/src/store/conversations/conversations.epics.ts @@ -777,6 +777,7 @@ const migrateConversationsIfRequiredEpic: AppEpic = (action$, state$) => { const preparedConversations = getPreparedConversations({ conversations: notMigratedConversations, conversationsFolders, + addRoot: true, }); let migratedConversationsCount = 0; diff --git a/apps/chat/src/store/prompts/prompts.epics.ts b/apps/chat/src/store/prompts/prompts.epics.ts index a88d012952..0ce29e7c7b 100644 --- a/apps/chat/src/store/prompts/prompts.epics.ts +++ b/apps/chat/src/store/prompts/prompts.epics.ts @@ -574,6 +574,7 @@ const migratePromptsIfRequiredEpic: AppEpic = (action$, state$) => { const preparedPrompts: Prompt[] = getPreparedPrompts({ prompts: notMigratedPrompts, folders: promptsFolders, + addRoot: true, }); // to send prompts with proper parentPath let migratedPromptsCount = 0; diff --git a/apps/chat/src/utils/app/data/prompt-service.ts b/apps/chat/src/utils/app/data/prompt-service.ts index 3768e2ef06..d2a17dc69a 100644 --- a/apps/chat/src/utils/app/data/prompt-service.ts +++ b/apps/chat/src/utils/app/data/prompt-service.ts @@ -1,6 +1,8 @@ import { Observable } from 'rxjs'; import { constructPath, notAllowedSymbolsRegex } from '@/src/utils/app/file'; +import { getRootId } from '@/src/utils/app/id'; +import { ApiKeys } from '@/src/utils/server/api'; import { FolderInterface } from '@/src/types/folder'; import { Prompt, PromptInfo } from '@/src/types/prompt'; @@ -48,9 +50,11 @@ export class PromptService { export const getPreparedPrompts = ({ prompts, folders, + addRoot = false, }: { prompts: Prompt[]; folders: FolderInterface[]; + addRoot?: boolean; }) => prompts.map((prompt) => { const { path } = getPathToFolderById(folders, prompt.folderId, true); @@ -58,8 +62,12 @@ export const getPreparedPrompts = ({ return { ...prompt, - id: constructPath(...[path, newName]), + id: addRoot + ? constructPath(getRootId({ apiKey: ApiKeys.Prompts }), path, newName) + : constructPath(path, newName), name: newName, - folderId: path, + folderId: addRoot + ? constructPath(getRootId({ apiKey: ApiKeys.Prompts }), path) + : path, }; }); // to send prompts with proper parentPath 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 76d9a208cb..9bd3d3181b 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 @@ -151,7 +151,7 @@ export abstract class ApiEntityStorage< 'Content-Type': 'application/json', }, body: JSON.stringify(this.cleanUpEntity(entity)), - }).pipe(catchError(() => of())); // TODO: handle error it in https://github.com/epam/ai-dial-chat/issues/663 + }) // TODO: handle error it in https://github.com/epam/ai-dial-chat/issues/663 } updateEntity(entity: TEntity): Observable { 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 f98f3efa61..d70d5e63d2 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,5 +1,6 @@ import { Observable, forkJoin, of } from 'rxjs'; +import { getRootId } from '@/src/utils/app/id'; import { ApiKeys, getConversationApiKey, @@ -14,7 +15,7 @@ import { ConversationsSelectors } from '@/src/store/conversations/conversations. import { cleanConversation } from '../../../clean'; import { getGeneratedConversationId } from '../../../conversation'; -import { notAllowedSymbolsRegex } from '../../../file'; +import { constructPath, notAllowedSymbolsRegex } from '../../../file'; import { getPathToFolderById } from '../../../folders'; import { ConversationService } from '../../conversation-service'; import { ApiEntityStorage } from './api-entity-storage'; @@ -33,15 +34,19 @@ export class ConversationApiStorage extends ApiEntityStorage< model: entity.model, }; } + cleanUpEntity(conversation: Conversation): Conversation { return cleanConversation(conversation); } + getEntityKey(info: ConversationInfo): string { return getConversationApiKey(info); } + parseEntityKey(key: string): Omit { return parseConversationApiKey(key); } + getStorageKey(): ApiKeys { return ApiKeys.Conversations; } @@ -75,9 +80,11 @@ export const getOrUploadConversation = ( export const getPreparedConversations = ({ conversations, conversationsFolders, + addRoot = false, }: { conversations: Conversation[]; conversationsFolders: FolderInterface[]; + addRoot?: boolean; }) => conversations.map((conv) => { const { path } = getPathToFolderById( @@ -85,6 +92,7 @@ export const getPreparedConversations = ({ conv.folderId, true, ); + const newName = conv.name.replace(notAllowedSymbolsRegex, ''); return { @@ -95,6 +103,8 @@ export const getPreparedConversations = ({ folderId: path, }), name: newName, - folderId: path, + folderId: addRoot + ? constructPath(getRootId({ apiKey: ApiKeys.Conversations }), path) + : path, }; }); // to send conversation with proper parentPath and lastActivityDate order From a45ccf9722bab3cbebc3796c493669e9a7b8cb8a Mon Sep 17 00:00:00 2001 From: Ilya Bondar Date: Wed, 14 Feb 2024 11:28:42 +0100 Subject: [PATCH 3/5] 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, From 9ded656c811b82168d470f523cf90d6262114f43 Mon Sep 17 00:00:00 2001 From: Alexander <98586297+Alexander-Kezik@users.noreply.github.com> Date: Wed, 14 Feb 2024 12:53:42 +0100 Subject: [PATCH 4/5] fix(chat): fix automigration icons (Issue #265) (#708) --- .../utils/app/data/storages/api-storage.ts | 21 ++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) 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 eb7fd5e419..e4ebe3b876 100644 --- a/apps/chat/src/utils/app/data/storages/api-storage.ts +++ b/apps/chat/src/utils/app/data/storages/api-storage.ts @@ -7,12 +7,13 @@ import { throwError, } from 'rxjs'; +import { regenerateConversationId } from '@/src/utils/app/conversation'; import { ApiEntityStorage } from '@/src/utils/app/data/storages/api/api-entity-storage'; -import { constructPath } from '@/src/utils/app/file'; import { generateNextName } from '@/src/utils/app/folders'; +import { addGeneratedPromptId } from '@/src/utils/app/prompts'; import { Conversation, ConversationInfo } from '@/src/types/chat'; -import { Entity } from '@/src/types/common'; +import { BackendResourceType, Entity } from '@/src/types/common'; import { FolderInterface, FoldersAndEntities } from '@/src/types/folder'; import { Prompt, PromptInfo } from '@/src/types/prompt'; import { DialStorage } from '@/src/types/storage'; @@ -35,12 +36,12 @@ export class ApiStorage implements DialStorage { entity: T, entities: T[], apiStorage: ApiEntityStorage, + entityType: BackendResourceType, ): Observable { let retries = 0; const retry = ( entity: T, - entities: T[], apiStorage: ApiEntityStorage, ): Observable => apiStorage.createEntity(entity).pipe( @@ -49,7 +50,7 @@ export class ApiStorage implements DialStorage { retries++; const defaultName = - 'messages' in entity + entityType === BackendResourceType.CONVERSATION ? DEFAULT_CONVERSATION_NAME : DEFAULT_PROMPT_NAME; const newName = generateNextName( @@ -59,18 +60,22 @@ export class ApiStorage implements DialStorage { ); const updatedEntity = { ...entity, - id: constructPath(entity.folderId, newName), name: newName, }; - return retry(updatedEntity, entities, apiStorage); + const updatedEntityWithRegeneratedId = + entityType === BackendResourceType.CONVERSATION + ? regenerateConversationId(updatedEntity as Conversation) + : addGeneratedPromptId(updatedEntity as Prompt); + + return retry(updatedEntityWithRegeneratedId as T, apiStorage); } return throwError(() => err); }), ); - return retry(entity, entities, apiStorage); + return retry(entity, apiStorage); } getConversationsFolders(path?: string): Observable { @@ -127,6 +132,7 @@ export class ApiStorage implements DialStorage { conv, [...conversations, ...apiConversations], this._conversationApiStorage, + BackendResourceType.CONVERSATION, ), ), ), @@ -169,6 +175,7 @@ export class ApiStorage implements DialStorage { prompt, [...prompts, ...apiPrompts], this._promptApiStorage, + BackendResourceType.PROMPT, ), ), ), From 5e6c70542ac6453b1f7d304dbee16f38b13b60b3 Mon Sep 17 00:00:00 2001 From: Alexander <98586297+Alexander-Kezik@users.noreply.github.com> Date: Wed, 14 Feb 2024 13:19:20 +0100 Subject: [PATCH 5/5] fix(chat): fix prompt counter when creating new prompt (Issue #705) (#710) --- apps/chat/src/store/prompts/prompts.epics.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/chat/src/store/prompts/prompts.epics.ts b/apps/chat/src/store/prompts/prompts.epics.ts index 0ce29e7c7b..e2143fc850 100644 --- a/apps/chat/src/store/prompts/prompts.epics.ts +++ b/apps/chat/src/store/prompts/prompts.epics.ts @@ -42,7 +42,7 @@ import { splitEntityId, updateMovedFolderId, } from '@/src/utils/app/folders'; -import { getRootId } from '@/src/utils/app/id'; +import { getRootId, isRootId } from '@/src/utils/app/id'; import { exportPrompt, exportPrompts, @@ -80,7 +80,7 @@ const createNewPromptEpic: AppEpic = (action$, state$) => const newPrompt: Prompt = addGeneratedPromptId({ name: getNextDefaultName( DEFAULT_PROMPT_NAME, - prompts.filter((prompt) => !prompt.folderId), + prompts.filter((prompt) => isRootId(prompt.folderId)), ), description: '', content: '',