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: '',