diff --git a/apps/chat-e2e/src/assertions/api/apiAssertion.ts b/apps/chat-e2e/src/assertions/api/apiAssertion.ts index 08d3a9d169..86def932fb 100644 --- a/apps/chat-e2e/src/assertions/api/apiAssertion.ts +++ b/apps/chat-e2e/src/assertions/api/apiAssertion.ts @@ -70,10 +70,19 @@ export class ApiAssertion { .toBe(expectedTemperature); } - public async assertRequestPrompt(request: ChatBody, expectedPrompt: string) { - expect - .soft(request.prompt, ExpectedMessages.chatRequestPromptIsValid) - .toBe(expectedPrompt); + public async assertRequestPrompt( + request: ChatBody, + expectedPrompt: string | undefined, + ) { + if (request.prompt === undefined) { + expect + .soft(request.prompt, ExpectedMessages.chatRequestPromptIsValid) + .toBeUndefined(); + } else { + expect + .soft(request.prompt, ExpectedMessages.chatRequestPromptIsValid) + .toBe(expectedPrompt); + } } public async assertRequestAddons( diff --git a/apps/chat-e2e/src/assertions/baseAssertion.ts b/apps/chat-e2e/src/assertions/baseAssertion.ts index 3cc909acc4..64f56111ab 100644 --- a/apps/chat-e2e/src/assertions/baseAssertion.ts +++ b/apps/chat-e2e/src/assertions/baseAssertion.ts @@ -139,10 +139,42 @@ export class BaseAssertion { .toHaveText(expectedText); } + public async assertElementAttribute( + element: BaseElement | Locator, + attribute: string, + expectedValue: string, + expectedMessage?: string, + ) { + const elementLocator = + element instanceof BaseElement + ? element.getElementLocator() + : (element as Locator); + await expect + .soft( + elementLocator, + expectedMessage ?? ExpectedMessages.elementAttributeValueIsValid, + ) + .toHaveAttribute(attribute, expectedValue); + } + public async assertElementColor(element: BaseElement, expectedColor: string) { const style = await element.getComputedStyleProperty(Styles.color); expect .soft(style[0], ExpectedMessages.elementColorIsValid) .toBe(expectedColor); } + + public async assertElementsCount( + element: BaseElement | Locator, + expectedCount: number, + ) { + const elementsCount = + element instanceof BaseElement + ? await element.getElementsCount() + : await element.count(); + + expect + .soft(elementsCount, ExpectedMessages.elementsCountIsValid) + .toBe(expectedCount); + } } diff --git a/apps/chat-e2e/src/assertions/chatMessagesAssertion.ts b/apps/chat-e2e/src/assertions/chatMessagesAssertion.ts index c4f89eb71d..01675738d1 100644 --- a/apps/chat-e2e/src/assertions/chatMessagesAssertion.ts +++ b/apps/chat-e2e/src/assertions/chatMessagesAssertion.ts @@ -105,16 +105,14 @@ export class ChatMessagesAssertion extends BaseAssertion { message: string | number, expectedState: ElementState, ) { - const chatMessage = this.chatMessages.getChatMessage(message); - await chatMessage.scrollIntoViewIfNeeded(); - await chatMessage.hover(); - const editIcon = this.chatMessages.setMessageTemplateIcon(chatMessage); + const chatMessage = await this.chatMessages.hoverOverMessage(message); + const templateIcon = this.chatMessages.setMessageTemplateIcon(chatMessage); expectedState === 'visible' ? await expect - .soft(editIcon, ExpectedMessages.buttonIsVisible) + .soft(templateIcon, ExpectedMessages.buttonIsVisible) .toBeVisible() : await expect - .soft(editIcon, ExpectedMessages.buttonIsNotVisible) + .soft(templateIcon, ExpectedMessages.buttonIsNotVisible) .toBeHidden(); } diff --git a/apps/chat-e2e/src/assertions/folderAssertion.ts b/apps/chat-e2e/src/assertions/folderAssertion.ts index ceeabc5362..cc1754e47e 100644 --- a/apps/chat-e2e/src/assertions/folderAssertion.ts +++ b/apps/chat-e2e/src/assertions/folderAssertion.ts @@ -290,6 +290,20 @@ export class FolderAssertion extends BaseAssertion { .toBe(expectedColor); } + public async assertFolderEntityColor( + folder: TreeEntity, + folderEntity: TreeEntity, + expectedColor: string, + ) { + const folderEntityElement = this.folder.getFolderEntityNameElement( + folder.name, + folderEntity.name, + folder.index, + folderEntity.index, + ); + await this.assertElementColor(folderEntityElement, expectedColor); + } + public async assertFolderEditInputState(expectedState: ElementState) { const editInputLocator = this.folder .getEditFolderInput() @@ -352,6 +366,20 @@ export class FolderAssertion extends BaseAssertion { await this.assertElementState(entityArrowIcon, expectedState); } + public async assertFolderEntityIcon( + folder: TreeEntity, + folderEntity: TreeEntity, + expectedIcon: string, + ) { + const folderEntityIcon = this.folder.getFolderEntityIcon( + folder.name, + folderEntity.name, + folder.index, + folderEntity.index, + ); + await this.assertEntityIcon(folderEntityIcon, expectedIcon); + } + public async assertFoldersCount(expectedCount: number) { const actualFoldersCount = await this.folder.getFoldersCount(); expect diff --git a/apps/chat-e2e/src/assertions/index.ts b/apps/chat-e2e/src/assertions/index.ts index a4c2e4c361..65ff770f43 100644 --- a/apps/chat-e2e/src/assertions/index.ts +++ b/apps/chat-e2e/src/assertions/index.ts @@ -39,3 +39,4 @@ export * from './addonsDialogAssertion'; export * from './marketplaceAgentsAssertion'; export * from './conversationToCompareAssertion'; export * from './conversationToPublishAssertion'; +export * from './publishFolderAssertion'; diff --git a/apps/chat-e2e/src/assertions/manageAttachmentsAssertion.ts b/apps/chat-e2e/src/assertions/manageAttachmentsAssertion.ts index dfb55b06b2..aaf837d334 100644 --- a/apps/chat-e2e/src/assertions/manageAttachmentsAssertion.ts +++ b/apps/chat-e2e/src/assertions/manageAttachmentsAssertion.ts @@ -79,10 +79,15 @@ export class ManageAttachmentsAssertion { state: ElementCaretState, ) { const sectionElement = this.attachFilesModal.getSectionElement(section); - const isExpanded = - await this.attachFilesModal.isSectionExpanded(sectionElement); + const filesSection = this.attachFilesModal.getFilesSection(sectionElement); state === 'expanded' - ? expect(isExpanded, `Section "${section}" is ${state}`).toBeTruthy() - : expect(isExpanded, `Section "${section}" is ${state}`).toBeFalsy(); + ? await expect( + filesSection, + `Section "${section}" is ${state}`, + ).toBeVisible() + : await expect( + filesSection, + `Section "${section}" is ${state}`, + ).toBeHidden(); } } diff --git a/apps/chat-e2e/src/assertions/messageTemplateModalAssertion.ts b/apps/chat-e2e/src/assertions/messageTemplateModalAssertion.ts new file mode 100644 index 0000000000..c7fa8e17ac --- /dev/null +++ b/apps/chat-e2e/src/assertions/messageTemplateModalAssertion.ts @@ -0,0 +1,11 @@ +import { BaseAssertion } from '@/src/assertions/baseAssertion'; +import { MessageTemplateModal } from '@/src/ui/webElements'; + +export class MessageTemplateModalAssertion extends BaseAssertion { + readonly messageTemplateModal: MessageTemplateModal; + + constructor(messageTemplateModal: MessageTemplateModal) { + super(); + this.messageTemplateModal = messageTemplateModal; + } +} diff --git a/apps/chat-e2e/src/assertions/publishFolderAssertion.ts b/apps/chat-e2e/src/assertions/publishFolderAssertion.ts new file mode 100644 index 0000000000..dc9ea1181c --- /dev/null +++ b/apps/chat-e2e/src/assertions/publishFolderAssertion.ts @@ -0,0 +1,47 @@ +import { FolderAssertion } from '@/src/assertions/folderAssertion'; +import { PublishingExpectedMessages, TreeEntity } from '@/src/testData'; +import { PublishFolder } from '@/src/ui/webElements/entityTree'; + +export class PublishFolderAssertion< + T extends PublishFolder, +> extends FolderAssertion { + readonly publishFolder: T; + + constructor(publishFolder: T) { + super(publishFolder); + this.publishFolder = publishFolder; + } + + public async assertFolderEntityVersion( + folder: TreeEntity, + folderEntity: TreeEntity, + expectedVersion: string, + ) { + await this.assertElementText( + this.publishFolder.getFolderEntityVersion( + folder.name, + folderEntity.name, + folder.index, + folderEntity.index, + ), + expectedVersion, + PublishingExpectedMessages.entityVersionIsValid, + ); + } + + public async assertFolderEntityVersionColor( + folder: TreeEntity, + folderEntity: TreeEntity, + expectedColor: string, + ) { + await this.assertElementColor( + this.publishFolder.getFolderEntityVersionElement( + folder.name, + folderEntity.name, + folder.index, + folderEntity.index, + ), + expectedColor, + ); + } +} diff --git a/apps/chat-e2e/src/assertions/renameConversationModalAssertion.ts b/apps/chat-e2e/src/assertions/renameConversationModalAssertion.ts new file mode 100644 index 0000000000..777296e8ab --- /dev/null +++ b/apps/chat-e2e/src/assertions/renameConversationModalAssertion.ts @@ -0,0 +1,56 @@ +import { BaseAssertion } from '@/src/assertions/baseAssertion'; +import { RenameConversationModal } from '@/src/ui/webElements/renameConversationModal'; +import { expect } from '@playwright/test'; + +export class RenameConversationModalAssertion extends BaseAssertion { + readonly renameModal: RenameConversationModal; + + constructor(renameModal: RenameConversationModal) { + super(); + this.renameModal = renameModal; + } + + async assertModalIsVisible() { + await this.assertElementState( + this.renameModal.getElementLocator(), + 'visible', + 'Rename Conversation Modal should be visible', + ); + } + + async assertModalTitle(expectedTitle: string) { + await expect + .soft( + this.renameModal.title.getElementLocator(), + 'Rename Conversation Modal title should match', + ) + .toHaveText(expectedTitle); + } + + async assertInputValue(expectedValue: string) { + await expect + .soft( + this.renameModal.nameInput.getElementLocator(), + 'Rename Conversation Modal input value should match', + ) + .toHaveText(expectedValue); + } + + async assertSaveButtonIsEnabled() { + await expect + .soft( + this.renameModal.saveButton.getElementLocator(), + 'Save button should be enabled', + ) + .toBeEnabled(); + } + + async assertSaveButtonIsDisabled() { + await expect + .soft( + this.renameModal.saveButton.getElementLocator(), + 'Save button should be disabled', + ) + .toBeDisabled(); + } +} diff --git a/apps/chat-e2e/src/core/dialAdminFixtures.ts b/apps/chat-e2e/src/core/dialAdminFixtures.ts index adf2c3a17b..31770cc37e 100644 --- a/apps/chat-e2e/src/core/dialAdminFixtures.ts +++ b/apps/chat-e2e/src/core/dialAdminFixtures.ts @@ -15,6 +15,7 @@ import { ChatHeaderAssertion, ChatMessagesAssertion, MenuAssertion, + PublishFolderAssertion, TooltipAssertion, } from '@/src/assertions'; import { ConversationToApproveAssertion } from '@/src/assertions/conversationToApproveAssertion'; @@ -28,11 +29,11 @@ import { ApproveRequiredConversationsTree, ConversationsToApproveTree, ConversationsTree, + FolderConversationsToApprove, FolderPrompts, Folders, OrganizationConversationsTree, PromptsTree, - PublishFolder, } from '@/src/ui/webElements/entityTree'; import { Tooltip } from '@/src/ui/webElements/tooltip'; import { Page } from '@playwright/test'; @@ -56,6 +57,7 @@ const dialAdminTest = dialTest.extend<{ adminOrganizationFolderConversationAssertions: FolderAssertion; adminPublishingApprovalModalAssertion: PublishingApprovalModalAssertion; adminConversationToApproveAssertion: ConversationToApproveAssertion; + adminFolderToApproveAssertion: PublishFolderAssertion; adminPublicationReviewControl: PublicationReviewControl; adminChatHeader: ChatHeader; adminChatMessages: ChatMessages; @@ -63,7 +65,6 @@ const dialAdminTest = dialTest.extend<{ adminApproveRequiredConversationDropdownMenu: DropdownMenu; adminTooltip: Tooltip; adminOrganizationConversations: OrganizationConversationsTree; - adminPublishingApprovalFolderConversationsAssertion: FolderAssertion; adminChatHeaderAssertion: ChatHeaderAssertion; adminChatMessagesAssertion: ChatMessagesAssertion; adminOrganizationFolderDropdownMenuAssertion: MenuAssertion; @@ -176,16 +177,6 @@ const dialAdminTest = dialTest.extend<{ adminChatBar.getOrganizationConversationsTree(); await use(adminOrganizationConversations); }, - adminPublishingApprovalFolderConversationsAssertion: async ( - { adminPublishingApprovalModal }, - use, - ) => { - const adminPublishingApprovalFolderConversationsAssertion = - new FolderAssertion( - adminPublishingApprovalModal.getFolderConversationsToApprove(), - ); - await use(adminPublishingApprovalFolderConversationsAssertion); - }, adminChatHeaderAssertion: async ({ adminChatHeader }, use) => { const adminChatHeaderAssertion = new ChatHeaderAssertion(adminChatHeader); await use(adminChatHeaderAssertion); @@ -231,6 +222,15 @@ const dialAdminTest = dialTest.extend<{ new ConversationToApproveAssertion(adminConversationsToApprove); await use(adminConversationToApproveAssertion); }, + adminFolderToApproveAssertion: async ( + { adminPublishingApprovalModal }, + use, + ) => { + const adminFolderToApproveAssertion = new PublishFolderAssertion( + adminPublishingApprovalModal.getFolderConversationsToApprove(), + ); + await use(adminFolderToApproveAssertion); + }, adminOrganizationFolderDropdownMenuAssertion: async ( { adminOrganizationFolderDropdownMenu }, use, diff --git a/apps/chat-e2e/src/core/dialFixtures.ts b/apps/chat-e2e/src/core/dialFixtures.ts index b5daac0d17..405bed34ca 100644 --- a/apps/chat-e2e/src/core/dialFixtures.ts +++ b/apps/chat-e2e/src/core/dialFixtures.ts @@ -10,7 +10,9 @@ import { ChatNotFound, ConversationSettingsModal, ConversationToCompare, + MessageTemplateModal, PromptBar, + PublishingRules, SelectFolderModal, SendMessage, } from '../ui/webElements'; @@ -38,6 +40,7 @@ import { PromptAssertion, PromptListAssertion, PromptModalAssertion, + PublishFolderAssertion, PublishingRequestModalAssertion, SendMessageAssertion, ShareApiAssertion, @@ -50,6 +53,8 @@ import { import { AddonsDialogAssertion } from '@/src/assertions/addonsDialogAssertion'; import { ConversationToPublishAssertion } from '@/src/assertions/conversationToPublishAssertion'; import { ManageAttachmentsAssertion } from '@/src/assertions/manageAttachmentsAssertion'; +import { MessageTemplateModalAssertion } from '@/src/assertions/messageTemplateModalAssertion'; +import { RenameConversationModalAssertion } from '@/src/assertions/renameConversationModalAssertion'; import { SelectFolderModalAssertion } from '@/src/assertions/selectFolderModalAssertion'; import { SettingsModalAssertion } from '@/src/assertions/settingsModalAssertion'; import { SideBarEntityAssertion } from '@/src/assertions/sideBarEntityAssertion'; @@ -106,6 +111,7 @@ import { ModelInfoTooltip } from '@/src/ui/webElements/modelInfoTooltip'; import { PlaybackControl } from '@/src/ui/webElements/playbackControl'; import { PromptModalDialog } from '@/src/ui/webElements/promptModalDialog'; import { PublishingRequestModal } from '@/src/ui/webElements/publishingRequestModal'; +import { RenameConversationModal } from '@/src/ui/webElements/renameConversationModal'; import { Search } from '@/src/ui/webElements/search'; import { SettingsModal } from '@/src/ui/webElements/settingsModal'; import { ShareModal } from '@/src/ui/webElements/shareModal'; @@ -114,6 +120,7 @@ import { TemperatureSlider } from '@/src/ui/webElements/temperatureSlider'; import { Tooltip } from '@/src/ui/webElements/tooltip'; import { UploadFromDeviceModal } from '@/src/ui/webElements/uploadFromDeviceModal'; import { VariableModalDialog } from '@/src/ui/webElements/variableModalDialog'; +import { BucketUtil } from '@/src/utils'; import { allure } from 'allure-playwright'; import path from 'path'; import { APIRequestContext } from 'playwright-core'; @@ -158,6 +165,7 @@ const dialTest = test.extend< folderConversations: FolderConversations; folderPrompts: FolderPrompts; organizationConversations: OrganizationConversationsTree; + organizationFolderConversations: Folders; conversationSettingsModal: ConversationSettingsModal; talkToAgentDialog: TalkToAgentDialog; talkToAgents: MarketplaceAgents; @@ -173,6 +181,8 @@ const dialTest = test.extend< promptDropdownMenu: DropdownMenu; confirmationDialog: ConfirmationDialog; promptModalDialog: PromptModalDialog; + renameConversationModal: RenameConversationModal; + renameConversationModalAssertion: RenameConversationModalAssertion; variableModalDialog: VariableModalDialog; chatHeader: ChatHeader; modelInfoTooltip: ModelInfoTooltip; @@ -215,6 +225,7 @@ const dialTest = test.extend< selectFolderModal: SelectFolderModal; selectFolders: Folders; attachedAllFiles: Folders; + messageTemplateModal: MessageTemplateModal; manageAttachmentsAssertion: ManageAttachmentsAssertion; settingsModal: SettingsModal; publishingRequestModal: PublishingRequestModal; @@ -223,6 +234,7 @@ const dialTest = test.extend< publicationApiHelper: PublicationApiHelper; adminPublicationApiHelper: PublicationApiHelper; publishRequestBuilder: PublishRequestBuilder; + publishingRules: PublishingRules; conversationAssertion: ConversationAssertion; chatBarFolderAssertion: FolderAssertion; organizationConversationAssertion: SideBarEntityAssertion; @@ -266,6 +278,9 @@ const dialTest = test.extend< publishingRequestFolderConversationAssertion: FolderAssertion; talkToAgentDialogAssertion: TalkToAgentDialogAssertion; conversationToPublishAssertion: ConversationToPublishAssertion; + folderToPublishAssertion: PublishFolderAssertion; + organizationFolderConversationAssertions: FolderAssertion; + messageTemplateModalAssertion: MessageTemplateModalAssertion; } >({ // eslint-disable-next-line no-empty-pattern @@ -440,6 +455,11 @@ const dialTest = test.extend< const conversationSettingsModal = new ConversationSettingsModal(page); await use(conversationSettingsModal); }, + organizationFolderConversations: async ({ chatBar }, use) => { + const organizationFolderConversations = + chatBar.getOrganizationFolderConversations(); + await use(organizationFolderConversations); + }, talkToAgentDialog: async ({ page }, use) => { const talkToAgentDialog = new TalkToAgentDialog(page); await use(talkToAgentDialog); @@ -488,6 +508,18 @@ const dialTest = test.extend< const promptModalDialog = new PromptModalDialog(page); await use(promptModalDialog); }, + renameConversationModal: async ({ page }, use) => { + const renameConversationModal = new RenameConversationModal(page); + await use(renameConversationModal); + }, + renameConversationModalAssertion: async ( + { renameConversationModal }, + use, + ) => { + const renameConversationModalAssertion = + new RenameConversationModalAssertion(renameConversationModal); + await use(renameConversationModalAssertion); + }, variableModalDialog: async ({ page }, use) => { const variableModalDialog = new VariableModalDialog(page); await use(variableModalDialog); @@ -564,6 +596,7 @@ const dialTest = test.extend< ) => { const additionalSecondShareUserFileApiHelper = new FileApiHelper( additionalSecondShareUserRequestContext, + BucketUtil.getAdditionalSecondShareUserBucket(), ); await use(additionalSecondShareUserFileApiHelper); }, @@ -683,6 +716,10 @@ const dialTest = test.extend< const attachedAllFiles = attachFilesModal.getAllFolderFiles(); await use(attachedAllFiles); }, + messageTemplateModal: async ({ page }, use) => { + const messageTemplateModal = new MessageTemplateModal(page); + await use(messageTemplateModal); + }, settingsModal: async ({ page }, use) => { const settingsModal = new SettingsModal(page); await use(settingsModal); @@ -716,6 +753,10 @@ const dialTest = test.extend< const publishRequestBuilder = new PublishRequestBuilder(); await use(publishRequestBuilder); }, + publishingRules: async ({ publishingRequestModal }, use) => { + const publishingRules = publishingRequestModal.getPublishingRules(); + await use(publishingRules); + }, conversationAssertion: async ({ conversations }, use) => { const conversationAssertion = new ConversationAssertion(conversations); await use(conversationAssertion); @@ -930,6 +971,21 @@ const dialTest = test.extend< ); await use(conversationToPublishAssertion); }, + folderToPublishAssertion: async ({ publishingRequestModal }, use) => { + const folderToPublishAssertion = new PublishFolderAssertion( + publishingRequestModal.getFolderConversationsToPublish(), + ); + await use(folderToPublishAssertion); + }, + organizationFolderConversationAssertions: async ( + { organizationFolderConversations }, + use, + ) => { + const organizationFolderConversationAssertions = new FolderAssertion( + organizationFolderConversations, + ); + await use(organizationFolderConversationAssertions); + }, // eslint-disable-next-line no-empty-pattern apiAssertion: async ({}, use) => { const apiAssertion = new ApiAssertion(); @@ -940,6 +996,12 @@ const dialTest = test.extend< const shareApiAssertion = new ShareApiAssertion(); await use(shareApiAssertion); }, + messageTemplateModalAssertion: async ({ messageTemplateModal }, use) => { + const messageTemplateModalAssertion = new MessageTemplateModalAssertion( + messageTemplateModal, + ); + await use(messageTemplateModalAssertion); + }, }); export default dialTest; diff --git a/apps/chat-e2e/src/core/dialSharedWithMeFixtures.ts b/apps/chat-e2e/src/core/dialSharedWithMeFixtures.ts index c8ba4f6ac2..36c2eb7418 100644 --- a/apps/chat-e2e/src/core/dialSharedWithMeFixtures.ts +++ b/apps/chat-e2e/src/core/dialSharedWithMeFixtures.ts @@ -198,6 +198,7 @@ const dialSharedWithMeTest = dialTest.extend<{ ) => { const additionalShareUserFileApiHelper = new FileApiHelper( additionalShareUserRequestContext, + BucketUtil.getAdditionalShareUserBucket(), ); await use(additionalShareUserFileApiHelper); }, diff --git a/apps/chat-e2e/src/testData/api/fileApiHelper.ts b/apps/chat-e2e/src/testData/api/fileApiHelper.ts index 96893dfa84..ec649df0a1 100644 --- a/apps/chat-e2e/src/testData/api/fileApiHelper.ts +++ b/apps/chat-e2e/src/testData/api/fileApiHelper.ts @@ -6,8 +6,16 @@ import { BucketUtil, ItemUtil } from '@/src/utils'; import { expect } from '@playwright/test'; import * as fs from 'fs'; import path from 'path'; +import { APIRequestContext } from 'playwright-core'; export class FileApiHelper extends BaseApiHelper { + private readonly userBucket?: string; + + constructor(request: APIRequestContext, userBucket?: string) { + super(request); + this.userBucket = userBucket; + } + public async putFile(filename: string, parentPath?: string) { const encodedFilename = encodeURIComponent(filename); const encodedParentPath = parentPath @@ -15,7 +23,7 @@ export class FileApiHelper extends BaseApiHelper { : undefined; const filePath = path.join(Attachment.attachmentPath, filename); const bufferedFile = fs.readFileSync(filePath); - const baseUrl = `${API.fileHost}/${BucketUtil.getBucket()}`; + const baseUrl = `${API.fileHost}/${this.userBucket ?? BucketUtil.getBucket()}`; const url = parentPath ? `${baseUrl}/${encodedParentPath}/${encodedFilename}` : `${baseUrl}/${encodedFilename}`; @@ -65,7 +73,7 @@ export class FileApiHelper extends BaseApiHelper { public async listEntities(nodeType: BackendDataNodeType, url?: string) { const host = url ? `${API.listingHost}/${url.substring(0, url.length - 1)}` - : `${API.filesListingHost()}/${BucketUtil.getBucket()}`; + : `${API.filesListingHost()}/${this.userBucket ?? BucketUtil.getBucket()}`; const response = await this.request.get(host, { params: { filter: nodeType, diff --git a/apps/chat-e2e/src/testData/api/itemApiHelper.ts b/apps/chat-e2e/src/testData/api/itemApiHelper.ts index a6f76a2de6..3d676bf8e9 100644 --- a/apps/chat-e2e/src/testData/api/itemApiHelper.ts +++ b/apps/chat-e2e/src/testData/api/itemApiHelper.ts @@ -10,10 +10,12 @@ import { APIRequestContext } from 'playwright-core'; export class ItemApiHelper extends BaseApiHelper { private readonly userBucket?: string; + constructor(request: APIRequestContext, userBucket?: string) { super(request); this.userBucket = userBucket; } + public async deleteAllData(bucket?: string, isOverlay = false) { const bucketToUse = this.userBucket ?? bucket; const conversations = await this.listItems( diff --git a/apps/chat-e2e/src/testData/api/publicationApiHelper.ts b/apps/chat-e2e/src/testData/api/publicationApiHelper.ts index 43bc472be7..0cff397e4a 100644 --- a/apps/chat-e2e/src/testData/api/publicationApiHelper.ts +++ b/apps/chat-e2e/src/testData/api/publicationApiHelper.ts @@ -81,7 +81,9 @@ export class PublicationApiHelper extends BaseApiHelper { return JSON.parse(responseText) as Publication; } - public async createUnpublishRequest(publicationRequest: Publication) { + public async createUnpublishRequest( + publicationRequest: Publication | PublicationRequestModel, + ) { const unpublishResources = []; for (const resource of publicationRequest.resources) { unpublishResources.push({ diff --git a/apps/chat-e2e/src/testData/conversationHistory/conversationData.ts b/apps/chat-e2e/src/testData/conversationHistory/conversationData.ts index 08886b7d14..623e7069c8 100644 --- a/apps/chat-e2e/src/testData/conversationHistory/conversationData.ts +++ b/apps/chat-e2e/src/testData/conversationHistory/conversationData.ts @@ -8,7 +8,13 @@ import { FolderData } from '@/src/testData/folders/folderData'; import { ItemUtil } from '@/src/utils'; import { DateUtil } from '@/src/utils/dateUtil'; import { GeneratorUtil } from '@/src/utils/generatorUtil'; -import { Message, MessageSettings, Role, Stage } from '@epam/ai-dial-shared'; +import { + Message, + MessageSettings, + Role, + Stage, + TemplateMapping, +} from '@epam/ai-dial-shared'; export interface FolderConversation { conversations: Conversation[]; @@ -96,8 +102,8 @@ export class ConversationData extends FolderData { } public prepareModelConversationBasedOnRequests( - model: DialAIEntityModel | string, requests: string[], + model?: DialAIEntityModel | string, name?: string, ) { const basicConversation = this.prepareEmptyConversation(model, name); @@ -136,8 +142,8 @@ export class ConversationData extends FolderData { requests[i] = `${i} + ${i + 1} =`; } const basicConversation = this.prepareModelConversationBasedOnRequests( - models[models.length - 1], requests, + models[models.length - 1], ); const messages = basicConversation.messages; for (let i = 0; i < models.length; i++) { @@ -158,12 +164,12 @@ export class ConversationData extends FolderData { } public prepareConversationBasedOnPrompt( - prompt: Prompt, + prompt: Prompt | string, params?: Map, model?: DialAIEntityModel | string, name?: string, ) { - let promptContent = prompt.content!; + let promptContent = typeof prompt === 'string' ? prompt : prompt.content!; const paramRegex = (param: string) => new RegExp('\\{\\{' + `(${param}.*?)` + '\\}\\}'); const defaultParamValueRegex = '(?<=\\|)(.*?)(?=\\}})'; @@ -178,19 +184,19 @@ export class ConversationData extends FolderData { } } //set default prompt parameters if absent in params map - const matchedDefaultValue = promptContent.match(defaultParamValueRegex); - if (matchedDefaultValue) { - promptContent = promptContent.replace( - paramRegex(''), - matchedDefaultValue[0], - ); - } + const matchedDefaultValues = promptContent.matchAll( + new RegExp(defaultParamValueRegex, 'g'), + ); + Array.from(matchedDefaultValues, (match) => { + promptContent = promptContent.replace(paramRegex(''), match[0]); + }); + const conversation = this.prepareDefaultConversation(model, name); const userMessages = conversation.messages.filter((m) => m.role === 'user'); userMessages.forEach((m) => { (m.templateMapping! as Record)[promptContent] = - prompt.content!; + typeof prompt === 'string' ? prompt : prompt.content!; m.content = promptContent; }); return conversation; @@ -788,4 +794,18 @@ export class ConversationData extends FolderData { replayConversation.replay.replayAsIs = true; return replayConversation; } + + public prepareConversationBasedOnMessageTemplate( + request: string, + templateMap: Map, + ) { + const conversation = this.prepareModelConversationBasedOnRequests([ + request, + ]); + const userMessage = conversation.messages.find((m) => m.role === 'user')!; + const templateMapping: TemplateMapping[] = []; + templateMap.forEach((k, v) => templateMapping.push([v, k])); + userMessage.templateMapping = templateMapping; + return conversation; + } } diff --git a/apps/chat-e2e/src/testData/expectedConstants.ts b/apps/chat-e2e/src/testData/expectedConstants.ts index cff81a54e9..429cb45769 100644 --- a/apps/chat-e2e/src/testData/expectedConstants.ts +++ b/apps/chat-e2e/src/testData/expectedConstants.ts @@ -15,6 +15,7 @@ export const ExpectedConstants = { `${ExpectedConstants.newFolderTitle} ${index}`, newPromptFolderWithIndexTitle: (index: number) => `${ExpectedConstants.newFolderTitle} ${index}`, + renameConversationModalTitle: 'Rename conversation', emptyString: '', defaultTemperature: '1', signInButtonTitle: 'Sign in with Credentials', @@ -192,8 +193,23 @@ export const ExpectedConstants = { continueReviewButtonTitle: 'Continue review', goToReviewButtonTitle: 'Go to a review', reviewResourcesTooltip: `It's required to review all resources`, - duplicatedUnpublishingError: (name: string) => - `"${name}" have already been unpublished. You can't approve this request.`, + duplicatedUnpublishingError: (...names: string[]) => { + const namesString = names.map((name) => `"${name}"`).join(', '); + return `${namesString} have already been unpublished. You can't approve this request.`; + }, + messageTemplateModalTitle: 'Message template', + messageTemplateModalDescription: + 'Copy a part of the message into the first input and provide a template with template variables into the second input', + messageTemplateModalOriginalMessageLabel: 'Original message:', + messageTemplateContentPlaceholder: 'A part of the message', + messageTemplateValuePlaceholder: + 'Your template. Use {{}} to denote a variable', + originalMessageTemplateErrorMessage: + 'This part was not found in the original message', + messageTemplateMissingVarErrorMessage: + 'Template must have at least one variable', + messageTemplateRequiredField: 'Please fill in this required field', + messageTemplateMismatchTextErrorMessage: `Template doesn't match the message text`, }; export enum Types { diff --git a/apps/chat-e2e/src/testData/publishing/publishRequestBuilder.ts b/apps/chat-e2e/src/testData/publishing/publishRequestBuilder.ts index 0ad6c76dc6..16734e0cd0 100644 --- a/apps/chat-e2e/src/testData/publishing/publishRequestBuilder.ts +++ b/apps/chat-e2e/src/testData/publishing/publishRequestBuilder.ts @@ -6,6 +6,12 @@ import { import { ExpectedConstants } from '@/src/testData'; import { Attachment, PublishActions } from '@epam/ai-dial-shared'; +export interface PublicationResource { + action: PublishActions; + sourceUrl?: string; + targetUrl: string; +} + export class PublishRequestBuilder { private publishRequest: PublicationRequestModel; @@ -45,14 +51,21 @@ export class PublishRequestBuilder { withConversationResource( conversation: Conversation, + action: PublishActions, version?: string, ): PublishRequestBuilder { const targetResource = conversation.id.split('/').slice(2).join('/'); - const resource = { - action: PublishActions.ADD, - sourceUrl: conversation.id, - targetUrl: `conversations/${this.getPublishRequest().targetFolder}${targetResource}__${version ?? ExpectedConstants.defaultAppVersion}`, + const targetUrl = `conversations/${this.getPublishRequest().targetFolder}${targetResource}__${version ?? ExpectedConstants.defaultAppVersion}`; + let resource: PublicationResource = { + action: action, + targetUrl: targetUrl, }; + if (action === 'ADD' || action === 'ADD_IF_ABSENT') { + resource = { + ...resource, + sourceUrl: conversation.id, + }; + } this.publishRequest.resources.push(resource); return this; } diff --git a/apps/chat-e2e/src/tests/abortedReplay.test.ts b/apps/chat-e2e/src/tests/abortedReplay.test.ts index 9c42cebf0f..3593b909be 100644 --- a/apps/chat-e2e/src/tests/abortedReplay.test.ts +++ b/apps/chat-e2e/src/tests/abortedReplay.test.ts @@ -75,14 +75,14 @@ dialTest( async () => { firstConversation = conversationData.prepareModelConversationBasedOnRequests( - firstRandomModel, [firstUserRequest], + firstRandomModel, ); conversationData.resetData(); secondConversation = conversationData.prepareModelConversationBasedOnRequests( - secondRandomModel, [secondUserRequest], + secondRandomModel, ); conversationData.resetData(); historyConversation = conversationData.prepareHistoryConversation( @@ -392,10 +392,7 @@ dialTest( requests.push(GeneratorUtil.randomString(200)); } const conversation = - conversationData.prepareModelConversationBasedOnRequests( - defaultModel, - requests, - ); + conversationData.prepareModelConversationBasedOnRequests(requests); replayConversation = conversationData.prepareDefaultReplayConversation(conversation); await dataInjector.createConversations([ diff --git a/apps/chat-e2e/src/tests/accountSettings.test.ts b/apps/chat-e2e/src/tests/accountSettings.test.ts index d15c2f157e..a05b4d4708 100644 --- a/apps/chat-e2e/src/tests/accountSettings.test.ts +++ b/apps/chat-e2e/src/tests/accountSettings.test.ts @@ -112,8 +112,8 @@ dialTest( const request = GeneratorUtil.randomString(170); const name = GeneratorUtil.randomString(170); conversation = conversationData.prepareModelConversationBasedOnRequests( - ModelsUtil.getDefaultModel()!, [request], + ModelsUtil.getDefaultModel()!, name, ); await dataInjector.createConversations([conversation]); diff --git a/apps/chat-e2e/src/tests/chatApi/entitySimpleRequest.test.ts b/apps/chat-e2e/src/tests/chatApi/entitySimpleRequest.test.ts index d84129f87c..32e48445ba 100644 --- a/apps/chat-e2e/src/tests/chatApi/entitySimpleRequest.test.ts +++ b/apps/chat-e2e/src/tests/chatApi/entitySimpleRequest.test.ts @@ -16,8 +16,8 @@ for (const entity of entitySimpleRequests) { dialTest.skip(process.env.E2E_HOST === undefined, skipReason); const conversation = conversationData.prepareModelConversationBasedOnRequests( - entity.entityId, [entity.request], + entity.entityId, ); if (entity.systemPrompt) { conversation.prompt = entity.systemPrompt; @@ -48,8 +48,8 @@ dialTest( ); const conversation = conversationData.prepareModelConversationBasedOnRequests( - replayEntity.entityId, [replayEntity.request], + replayEntity.entityId, ); conversationData.resetData(); const replayConversation = diff --git a/apps/chat-e2e/src/tests/chatBarConversation.test.ts b/apps/chat-e2e/src/tests/chatBarConversation.test.ts index 3992d2c7fc..d7c32abe39 100644 --- a/apps/chat-e2e/src/tests/chatBarConversation.test.ts +++ b/apps/chat-e2e/src/tests/chatBarConversation.test.ts @@ -13,7 +13,6 @@ import { } from '@/src/testData'; import { Colors, Overflow, Styles } from '@/src/ui/domData'; import { ChatBarSelectors } from '@/src/ui/selectors'; -import { EditInput } from '@/src/ui/webElements'; import { GeneratorUtil } from '@/src/utils'; import { ModelsUtil } from '@/src/utils/modelsUtil'; import { expect } from '@playwright/test'; @@ -102,6 +101,8 @@ dialTest( conversationData, dataInjector, setTestIds, + renameConversationModal, + renameConversationModalAssertion, }) => { setTestIds('EPMRTC-588', 'EPMRTC-816', 'EPMRTC-1494'); const newName = 'new name to cancel'; @@ -149,20 +150,25 @@ dialTest( async () => { await conversations.openEntityDropdownMenu(conversationName); await conversationDropdownMenu.selectMenuOption(MenuOptions.rename); - const chatNameOverflow = await conversations - .getEntityName(conversationName) - .getComputedStyleProperty(Styles.text_overflow); + await renameConversationModalAssertion.assertModalIsVisible(); + await renameConversationModalAssertion.assertModalTitle( + ExpectedConstants.renameConversationModalTitle, + ); + const modalInputValue = await renameConversationModal.getInputValue(); expect - .soft(chatNameOverflow[0], ExpectedMessages.chatNameIsTruncated) - .toBe(undefined); + .soft( + modalInputValue, + 'Modal input should contain the initial conversation name', + ) + .toBe(conversationName); }, ); await dialTest.step( 'Set new conversation name, cancel edit and verify conversation with initial name shown', async () => { - await conversations.openEditEntityNameMode(newName); - await conversations.getEditInputActions().clickCancelButton(); + await renameConversationModal.nameInput.fillInInput(newName); + await renameConversationModal.cancelButton.click(); await expect .soft( conversations.getEntityByName(newName), @@ -199,6 +205,7 @@ dialTest( conversationData, dataInjector, setTestIds, + renameConversationModal, }) => { setTestIds('EPMRTC-584', 'EPMRTC-819'); const conversation = conversationData.prepareDefaultConversation(); @@ -209,7 +216,7 @@ dialTest( await dialHomePage.waitForPageLoaded(); await conversations.openEntityDropdownMenu(conversation.name); await conversationDropdownMenu.selectMenuOption(MenuOptions.rename); - await conversations.editConversationNameWithTick(newName, { + await renameConversationModal.editConversationNameWithSaveButton(newName, { isHttpMethodTriggered: false, }); await expect @@ -258,6 +265,7 @@ dialTest( setTestIds, errorPopup, errorToast, + renameConversationModal, }) => { setTestIds( 'EPMRTC-585', @@ -281,7 +289,7 @@ dialTest( await conversations.selectConversation(conversation.name); await conversations.openEntityDropdownMenu(conversation.name); await conversationDropdownMenu.selectMenuOption(MenuOptions.rename); - await conversations.editConversationNameWithEnter( + await renameConversationModal.editConversationNameWithEnter( newLongNameWithMiddleSpacesEndDot, ); @@ -374,6 +382,7 @@ dialTest( conversationData, dataInjector, setTestIds, + renameConversationModal, }) => { setTestIds( 'EPMRTC-595', @@ -383,7 +392,6 @@ dialTest( 'EPMRTC-1574', 'EPMRTC-1276', ); - let editInputContainer: EditInput; const newNameWithEndDot = 'updated folder name.'; let conversation: Conversation; @@ -399,52 +407,54 @@ dialTest( await dialHomePage.waitForPageLoaded(); await conversations.openEntityDropdownMenu(conversation.name); await conversationDropdownMenu.selectMenuOption(MenuOptions.rename); - editInputContainer = - await conversations.openEditEntityNameMode(newNameWithEndDot); - await conversations.getEditInputActions().clickTickButton(); + await renameConversationModal.editConversationNameWithSaveButton( + newNameWithEndDot, + { isHttpMethodTriggered: false }, + ); const errorMessage = await errorToast.getElementContent(); expect .soft(errorMessage, ExpectedMessages.notAllowedNameErrorShown) .toBe(ExpectedConstants.nameWithDotErrorMessage); + await errorToast.closeToast(); }, ); await dialTest.step( 'Start typing prohibited symbols and verify they are not displayed in text input', async () => { - await editInputContainer.editInput.click(); - await editInputContainer.editValue( + await renameConversationModal.editInputValue( ExpectedConstants.restrictedNameChars, ); - const inputContent = await editInputContainer.getEditInputValue(); + const inputContent = await renameConversationModal.getInputValue(); expect .soft(inputContent, ExpectedMessages.charactersAreNotDisplayed) .toBe(''); + await renameConversationModal.cancelButton.click(); }, ); - await dialTest.step( - 'Set empty conversation name or spaces and verify initial name is preserved', - async () => { - const name = GeneratorUtil.randomArrayElement(['', ' ']); - editInputContainer = await conversations.openEditEntityNameMode(name); - await conversations.getEditInputActions().clickTickButton(); - await expect - .soft( - conversations.getEntityByName(conversation.name), - ExpectedMessages.conversationNameNotUpdated, - ) - .toBeVisible(); - }, - ); + //TODO decide if that is a correct behavior - to delete this step since the new modal doesn't let us press the "save" button with spaces in the input + // await dialTest.step( + // 'Set empty conversation name or spaces and verify initial name is preserved', + // async () => { + // const name = GeneratorUtil.randomArrayElement(['', ' ']); + // await renameConversationModal.editConversationNameWithEnter(name); + // await expect + // .soft( + // conversations.getEntityByName(conversation.name), + // ExpectedMessages.conversationNameNotUpdated, + // ) + // .toBeVisible(); + // }, + // ); await dialTest.step( 'Verify renaming conversation to the name with special symbols is successful', async () => { await conversations.openEntityDropdownMenu(conversation.name); await conversationDropdownMenu.selectMenuOption(MenuOptions.rename); - await conversations.editConversationNameWithTick( + await renameConversationModal.editConversationNameWithSaveButton( ExpectedConstants.allowedSpecialChars, { isHttpMethodTriggered: false }, ); @@ -1200,10 +1210,7 @@ dialTest( conversationData.resetData(); const firstConversation = - conversationData.prepareModelConversationBasedOnRequests( - defaultModel, - [request], - ); + conversationData.prepareModelConversationBasedOnRequests([request]); firstConversation.folderId = firstFolder.id; firstConversation.id = `${firstConversation.folderId}/${firstConversation.id}`; conversationData.resetData(); @@ -1221,8 +1228,8 @@ dialTest( const thirdConversation = conversationData.prepareModelConversationBasedOnRequests( - defaultModel, [request], + defaultModel, specialSymbolsName(), ); thirdConversation.folderId = secondFolder.id; @@ -1310,6 +1317,7 @@ dialTest( chatMessages, chat, setTestIds, + renameConversationModal, }) => { setTestIds('EPMRTC-2849', 'EPMRTC-2959'); const updatedConversationName = `😂👍🥳 😷 🤧 🤠 🥴😇 😈 ⭐あおㅁㄹñ¿äß`; @@ -1328,7 +1336,7 @@ dialTest( await conversations.selectConversation(conversation.name); await conversations.openEntityDropdownMenu(conversation.name); await conversationDropdownMenu.selectMenuOption(MenuOptions.rename); - await conversations.editConversationNameWithTick( + await renameConversationModal.editConversationNameWithSaveButton( updatedConversationName, ); await expect diff --git a/apps/chat-e2e/src/tests/chatExportImport.test.ts b/apps/chat-e2e/src/tests/chatExportImport.test.ts index e291bee314..fd8022085d 100644 --- a/apps/chat-e2e/src/tests/chatExportImport.test.ts +++ b/apps/chat-e2e/src/tests/chatExportImport.test.ts @@ -406,8 +406,8 @@ dialTest( async () => { importedRootConversation = conversationData.prepareModelConversationBasedOnRequests( - simpleRequestModel!, requests, + simpleRequestModel!, ); threeConversationsData = ImportConversation.prepareConversationFile( importedRootConversation, diff --git a/apps/chat-e2e/src/tests/chatHeader.test.ts b/apps/chat-e2e/src/tests/chatHeader.test.ts index d7e2e15d9d..309fb530e2 100644 --- a/apps/chat-e2e/src/tests/chatHeader.test.ts +++ b/apps/chat-e2e/src/tests/chatHeader.test.ts @@ -179,10 +179,11 @@ dialTest( setTestIds('EPMRTC-490', 'EPMRTC-491'); let conversation: Conversation; await dialTest.step('Prepare conversation with history', async () => { - conversation = conversationData.prepareModelConversationBasedOnRequests( - defaultModel, - ['first request', 'second request', 'third request'], - ); + conversation = conversationData.prepareModelConversationBasedOnRequests([ + 'first request', + 'second request', + 'third request', + ]); await dataInjector.createConversations([conversation]); }); diff --git a/apps/chat-e2e/src/tests/chatSelectionFunctionality.test.ts b/apps/chat-e2e/src/tests/chatSelectionFunctionality.test.ts index 0895242f11..d665a55b49 100644 --- a/apps/chat-e2e/src/tests/chatSelectionFunctionality.test.ts +++ b/apps/chat-e2e/src/tests/chatSelectionFunctionality.test.ts @@ -33,6 +33,7 @@ dialTest( shareModal, conversationDropdownMenu, downloadAssertion, + renameConversationModal, }) => { setTestIds( 'EPMRTC-934', @@ -106,7 +107,9 @@ dialTest( await dialTest.step('Click on Rename, rename and confirm', async () => { await conversationDropdownMenu.selectMenuOption(MenuOptions.rename); firstConversation.name = 'Renamed chat'; - await conversations.editConversationNameWithTick(firstConversation.name); + await renameConversationModal.editConversationNameWithSaveButton( + firstConversation.name, + ); await conversations.getEntityByName(firstConversation.name).waitFor(); await conversationAssertion.assertSelectedConversation( secondConversation.name, diff --git a/apps/chat-e2e/src/tests/compareMode.test.ts b/apps/chat-e2e/src/tests/compareMode.test.ts index e36358fbcc..5fd6fd0930 100644 --- a/apps/chat-e2e/src/tests/compareMode.test.ts +++ b/apps/chat-e2e/src/tests/compareMode.test.ts @@ -25,7 +25,9 @@ dialTest.beforeAll(async () => { allModels = ModelsUtil.getModels().filter((m) => m.iconUrl !== undefined); defaultModel = ModelsUtil.getDefaultModel()!; aModel = GeneratorUtil.randomArrayElement( - allModels.filter((m) => m.id !== defaultModel.id), + allModels.filter( + (m) => m.id !== defaultModel.id && m.features?.systemPrompt, + ), ); bModel = GeneratorUtil.randomArrayElement( allModels.filter((m) => m.id !== defaultModel.id && m.id !== aModel.id), @@ -105,8 +107,8 @@ dialTest( conversationData.resetData(); secondModelConversation = conversationData.prepareModelConversationBasedOnRequests( - aModel, [request!], + aModel, conversationName, ); conversationData.resetData(); @@ -393,36 +395,36 @@ dialTest( async () => { firstConversation = conversationData.prepareModelConversationBasedOnRequests( - defaultModel, [firstRequest, secondRequest], + defaultModel, 'firstConv', ); conversationData.resetData(); secondConversation = conversationData.prepareModelConversationBasedOnRequests( - defaultModel, [secondRequest, firstRequest], + defaultModel, 'secondConv', ); conversationData.resetData(); thirdConversation = conversationData.prepareModelConversationBasedOnRequests( - defaultModel, [firstRequest], + defaultModel, 'thirdConv', ); conversationData.resetData(); forthConversation = conversationData.prepareModelConversationBasedOnRequests( - defaultModel, [firstRequest, thirdRequest], + defaultModel, 'forthConv', ); conversationData.resetData(); fifthConversation = conversationData.prepareModelConversationBasedOnRequests( - defaultModel, [firstRequest.toLowerCase(), secondRequest], + defaultModel, 'fifthConv', ); @@ -644,15 +646,15 @@ dialTest( await dialTest.step('Prepare two conversations for comparing', async () => { firstConversation = conversationData.prepareModelConversationBasedOnRequests( - aModel, request, + aModel, conversationName, ); conversationData.resetData(); secondConversation = conversationData.prepareModelConversationBasedOnRequests( - bModel, request, + bModel, conversationName2, ); await dataInjector.createConversations([ @@ -1043,29 +1045,29 @@ dialTest( async () => { firstConversation = conversationData.prepareModelConversationBasedOnRequests( - defaultModel, [request], + defaultModel, request, ); conversationData.resetData(); secondConversation = conversationData.prepareModelConversationBasedOnRequests( - defaultModel, [request], + defaultModel, 'When was epam officially founded', ); conversationData.resetData(); thirdConversation = conversationData.prepareModelConversationBasedOnRequests( - defaultModel, [request], + defaultModel, 'Renamed epam systems', ); conversationData.resetData(); fourthConversation = conversationData.prepareModelConversationBasedOnRequests( - defaultModel, [request], + defaultModel, 'epam_systems', ); @@ -1492,6 +1494,7 @@ dialTest( conversationDropdownMenu, compare, compareConversation, + renameConversationModal, }) => { setTestIds( 'EPMRTC-560', @@ -1511,15 +1514,14 @@ dialTest( async () => { firstConversation = conversationData.prepareModelConversationBasedOnRequests( - defaultModel, firstConversationRequests, ); conversationData.resetData(); secondConversation = conversationData.prepareModelConversationBasedOnRequests( - aModel, secondConversationRequests, + aModel, ); await dataInjector.createConversations([ @@ -1612,7 +1614,9 @@ dialTest( const newLeftChatName = GeneratorUtil.randomString(7); await conversations.openEntityDropdownMenu(updatedRequestContent, 1); await conversationDropdownMenu.selectMenuOption(MenuOptions.rename); - await conversations.editConversationNameWithTick(newLeftChatName); + await renameConversationModal.editConversationNameWithSaveButton( + newLeftChatName, + ); const chatTitle = await leftChatHeader.chatTitle.getElementContent(); expect diff --git a/apps/chat-e2e/src/tests/conversationNameNumeration.test.ts b/apps/chat-e2e/src/tests/conversationNameNumeration.test.ts index bd84b27fa5..438e9b5f70 100644 --- a/apps/chat-e2e/src/tests/conversationNameNumeration.test.ts +++ b/apps/chat-e2e/src/tests/conversationNameNumeration.test.ts @@ -79,6 +79,7 @@ dialTest.skip( dataInjector, conversationDropdownMenu, setTestIds, + renameConversationModal, }) => { setTestIds('EPMRTC-1625'); let firstConversation: Conversation; @@ -136,7 +137,7 @@ dialTest.skip( async () => { await conversations.openEntityDropdownMenu(thirdConversationName); await conversationDropdownMenu.selectMenuOption(MenuOptions.rename); - await conversations.editConversationNameWithTick( + await renameConversationModal.editConversationNameWithSaveButton( GeneratorUtil.randomString(7), { isHttpMethodTriggered: false }, ); @@ -366,6 +367,7 @@ dialTest( localStorageManager, errorToast, setTestIds, + renameConversationModal, }) => { setTestIds('EPMRTC-2915', 'EPMRTC-2956', 'EPMRTC-2931'); const duplicatedName = GeneratorUtil.randomString(7); @@ -421,12 +423,8 @@ dialTest( secondFolderConversation.name, ); await conversationDropdownMenu.selectMenuOption(MenuOptions.rename); - const editFolderConversationInputActions = - folderConversations.getEditFolderEntityInputActions(); - await folderConversations - .getEditFolderEntityInput() - .editValue(duplicatedName); - await editFolderConversationInputActions.clickTickButton(); + await renameConversationModal.editInputValue(duplicatedName); + await renameConversationModal.saveButton.click(); await expect .soft( @@ -443,7 +441,7 @@ dialTest( ), ); await errorToast.closeToast(); - await editFolderConversationInputActions.clickCancelButton(); + await renameConversationModal.cancelButton.click(); }, ); @@ -527,6 +525,7 @@ dialTest( errorToast, conversationDropdownMenu, setTestIds, + renameConversationModal, }) => { setTestIds('EPMRTC-2933'); let firstConversation: Conversation; @@ -549,8 +548,8 @@ dialTest( await dialHomePage.waitForPageLoaded(); await conversations.openEntityDropdownMenu(secondConversation.name); await conversationDropdownMenu.selectMenuOption(MenuOptions.rename); - await conversations.openEditEntityNameMode(firstConversation.name); - await conversations.getEditInputActions().clickTickButton(); + await renameConversationModal.editInputValue(firstConversation.name); + await renameConversationModal.saveButton.click(); await expect .soft( diff --git a/apps/chat-e2e/src/tests/duplicateConversation.test.ts b/apps/chat-e2e/src/tests/duplicateConversation.test.ts index ac31f8cf33..325aeebd15 100644 --- a/apps/chat-e2e/src/tests/duplicateConversation.test.ts +++ b/apps/chat-e2e/src/tests/duplicateConversation.test.ts @@ -1,5 +1,4 @@ import { Conversation } from '@/chat/types/chat'; -import { DialAIEntityModel } from '@/chat/types/models'; import dialTest from '@/src/core/dialFixtures'; import { CollapsedSections, @@ -8,15 +7,8 @@ import { FolderConversation, MenuOptions, } from '@/src/testData'; -import { ModelsUtil } from '@/src/utils'; import { expect } from '@playwright/test'; -let defaultModel: DialAIEntityModel; - -dialTest.beforeAll(async () => { - defaultModel = ModelsUtil.getDefaultModel()!; -}); - dialTest( 'Duplicate chat located in today.\n' + 'Duplicate chat located in today several times to check postfixes', @@ -35,10 +27,10 @@ dialTest( const secondRequest = 'second request'; await dialTest.step('Prepare conversation with some history', async () => { - conversation = conversationData.prepareModelConversationBasedOnRequests( - defaultModel, - [firstRequest, secondRequest], - ); + conversation = conversationData.prepareModelConversationBasedOnRequests([ + firstRequest, + secondRequest, + ]); await dataInjector.createConversations([conversation]); }); diff --git a/apps/chat-e2e/src/tests/messageTemplate.test.ts b/apps/chat-e2e/src/tests/messageTemplate.test.ts new file mode 100644 index 0000000000..bbcab2e2a7 --- /dev/null +++ b/apps/chat-e2e/src/tests/messageTemplate.test.ts @@ -0,0 +1,852 @@ +import { Conversation } from '@/chat/types/chat'; +import { Prompt } from '@/chat/types/prompt'; +import dialTest from '@/src/core/dialFixtures'; +import { ExpectedConstants, MockedChatApiResponseBodies } from '@/src/testData'; +import { Attributes, ThemeColorAttributes } from '@/src/ui/domData'; +import { keys } from '@/src/ui/keyboard'; +import { GeneratorUtil } from '@/src/utils'; + +const requestContent = + 'Spanish is the 2nd most widely spoken language in the world.\n' + + 'Spanish has a royal family.\n' + + 'Spanish people do not consider paella as Spain’s national dish.'; +const firstRowFirstVar = '{{Language}}'; +const firstRowSecondVar = '{{number of place}}'; +const secondRowFirstVar = '{{Country}}'; +const secondRowSecondVar = '{{adjective}}'; +const thirdRowFirstVar = '{{Language2}}'; +const firstRowContent = 'Spanish is the 2nd'; +const firstRowTemplate = `${firstRowFirstVar} is the ${firstRowSecondVar}`; +const secondRowContent = 'Spain’s national dish'; +const secondRowTemplate = `${secondRowFirstVar}’s ${secondRowSecondVar} dish`; +const thirdRowContent = 'Spanish'; +const thirdRowTemplate = thirdRowFirstVar; + +dialTest( + 'Message template: Show more/less, Original message, tips.\n' + + 'Message template: new row appears if to type anything in "a part of the message" when \'your template\' is empty (and vice versa).\n' + + 'Message template: the changes are not saved if to close the window on X.\n' + + 'Message template: Delete is not available for the initial row, other rows can be deleted.\n' + + 'Message template: the window is not closed if to click on any area outside the window.\n' + + "Message template: the order of the 'part of the messages' is set by user, no auto-sorting", + async ({ + dialHomePage, + conversations, + messageTemplateModal, + page, + messageTemplateModalAssertion, + chatMessages, + conversationData, + dataInjector, + setTestIds, + }) => { + setTestIds( + 'EPMRTC-4251', + 'EPMRTC-4271', + 'EPMRTC-4268', + 'EPMRTC-4272', + 'EPMRTC-4269', + 'EPMRTC-4274', + ); + const requestContent = GeneratorUtil.randomString(40) + .concat(' ') + .repeat(10); + const truncatedRequestContent = requestContent + .substring(0, 160) + .concat('...'); + let conversation: Conversation; + + await dialTest.step('Prepare conversation with long request', async () => { + conversation = conversationData.prepareModelConversationBasedOnRequests([ + requestContent, + ]); + await dataInjector.createConversations([conversation]); + }); + + await dialTest.step( + 'Open "Message Template" modal and verify its content', + async () => { + await dialHomePage.openHomePage(); + await dialHomePage.waitForPageLoaded(); + await conversations.selectConversation(conversation.name); + await chatMessages.openMessageTemplateModal(1); + await messageTemplateModalAssertion.assertElementText( + messageTemplateModal.title, + ExpectedConstants.messageTemplateModalTitle, + ); + await messageTemplateModalAssertion.assertElementText( + messageTemplateModal.description, + ExpectedConstants.messageTemplateModalDescription, + ); + await messageTemplateModalAssertion.assertElementAttribute( + messageTemplateModal.getTemplateRowContent(1), + Attributes.placeholder, + ExpectedConstants.messageTemplateContentPlaceholder, + ); + await messageTemplateModalAssertion.assertElementAttribute( + messageTemplateModal.getTemplateRowValue(1), + Attributes.placeholder, + ExpectedConstants.messageTemplateValuePlaceholder, + ); + }, + ); + + await dialTest.step( + 'Verify original message is cut with dots', + async () => { + await messageTemplateModalAssertion.assertElementText( + messageTemplateModal.originalMessageContent, + truncatedRequestContent, + ); + }, + ); + + await dialTest.step( + 'Click on "Show more" button and verify full message is displayed', + async () => { + await messageTemplateModal.showMoreButton.click(); + await messageTemplateModalAssertion.assertElementText( + messageTemplateModal.originalMessageContent, + requestContent, + ); + await messageTemplateModal.showLessButton.click(); + await messageTemplateModalAssertion.assertElementText( + messageTemplateModal.originalMessageContent, + truncatedRequestContent, + ); + }, + ); + + await dialTest.step( + 'Set cursor in the first row and verify no new row is added', + async () => { + await messageTemplateModal.getTemplateRowContent(1).click(); + await messageTemplateModal.getTemplateRowValue(1).click(); + await messageTemplateModalAssertion.assertElementsCount( + messageTemplateModal.templateRows, + 1, + ); + }, + ); + + await dialTest.step( + 'Close the modal, reopen it again and verify changes are not saved', + async () => { + await messageTemplateModal.cancelButton.click(); + await chatMessages.openMessageTemplateModal(1); + await messageTemplateModalAssertion.assertElementsCount( + messageTemplateModal.templateRows, + 1, + ); + }, + ); + + await dialTest.step( + 'Click outside the modal and verify it is not closed', + async () => { + await page.mouse.click(0, 0); + await messageTemplateModalAssertion.assertElementState( + messageTemplateModal, + 'visible', + ); + }, + ); + + await dialTest.step( + 'Type request chars in the first row and verify new row is added, delete button is available for the first row', + async () => { + const matchContent = requestContent.split(' ')[0].split(''); + const values = [ + GeneratorUtil.randomArrayElement(matchContent), + GeneratorUtil.randomArrayElement(matchContent), + GeneratorUtil.randomArrayElement(matchContent), + ]; + for (let i = 0; i < values.length; i++) { + await messageTemplateModal + .getTemplateRowContent(i + 1) + .fill(values[i]); + await messageTemplateModalAssertion.assertElementsCount( + messageTemplateModal.templateRows, + i + 2, + ); + await messageTemplateModalAssertion.assertElementState( + messageTemplateModal.getTemplateRowDeleteButton(i + 1), + 'visible', + ); + await messageTemplateModalAssertion.assertElementState( + messageTemplateModal.getTemplateRowDeleteButton(i + 2), + 'hidden', + ); + await messageTemplateModal + .getTemplateRowValue(i + 1) + .fill(`{{${values[i]}}}`); + } + await messageTemplateModal.saveChanges(); + await chatMessages.openMessageTemplateModal(1); + + for (let i = 0; i < values.length; i++) { + await messageTemplateModalAssertion.assertElementText( + messageTemplateModal.getTemplateRowContent(i + 1), + values[i], + ); + } + }, + ); + }, +); + +dialTest( + 'Message template: Preview mode based on three variables.\n' + + 'Message template: changes are saved: add a row, delete the row, update values', + async ({ + dialHomePage, + conversations, + messageTemplateModal, + messageTemplateModalAssertion, + chatMessages, + conversationData, + dataInjector, + setTestIds, + }) => { + setTestIds('EPMRTC-4270', 'EPMRTC-4276'); + const updatedSecondRowContent = secondRowContent.substring(0, 3); + const updatedSecondRowTemplate = `{{${secondRowContent.substring(0, 3)}}}`; + const rowsMap = new Map([ + [firstRowContent, firstRowTemplate], + [secondRowContent, secondRowTemplate], + [thirdRowContent, thirdRowTemplate], + ]); + const expectedPreviewContent = `${firstRowFirstVar} is the ${firstRowSecondVar} most widely spoken language in the world.\n${thirdRowFirstVar} has a royal family.\n${thirdRowFirstVar} people do not consider paella as ${secondRowFirstVar}’s ${secondRowSecondVar} dish.`; + let conversation: Conversation; + + await dialTest.step( + 'Prepare conversation with specified request', + async () => { + conversation = conversationData.prepareModelConversationBasedOnRequests( + [requestContent], + ); + await dataInjector.createConversations([conversation]); + }, + ); + + await dialTest.step( + 'Open "Message Template" modal and fill in rows with values', + async () => { + await dialHomePage.openHomePage(); + await dialHomePage.waitForPageLoaded(); + await conversations.selectConversation(conversation.name); + await chatMessages.openMessageTemplateModal(1); + for (const [key, value] of rowsMap.entries()) { + const index = Array.from(rowsMap.keys()).indexOf(key); + await messageTemplateModal.getTemplateRowContent(index + 1).fill(key); + await messageTemplateModal.getTemplateRowValue(index + 1).fill(value); + } + }, + ); + + await dialTest.step( + 'Switch to Preview tab and verify the content', + async () => { + await messageTemplateModal.previewTab.click(); + await messageTemplateModalAssertion.assertElementText( + messageTemplateModal.templatePreview, + expectedPreviewContent, + ); + for (const variable of [ + firstRowFirstVar, + firstRowSecondVar, + secondRowFirstVar, + secondRowSecondVar, + thirdRowFirstVar, + ]) { + await messageTemplateModalAssertion.assertElementAttribute( + messageTemplateModal.templatePreviewVar(variable), + Attributes.class, + ThemeColorAttributes.textAccentTertiary, + ); + } + }, + ); + + await dialTest.step( + 'Save changes, reopen "Message Template" modal and verify the changes are saved', + async () => { + await messageTemplateModal.saveChanges(); + await chatMessages.openMessageTemplateModal(1); + await messageTemplateModalAssertion.assertElementsCount( + messageTemplateModal.templateRows, + rowsMap.size + 1, + ); + for (const [key, value] of rowsMap.entries()) { + const index = Array.from(rowsMap.keys()).indexOf(key); + await messageTemplateModalAssertion.assertElementText( + messageTemplateModal.getTemplateRowContent(index + 1), + key, + ); + await messageTemplateModalAssertion.assertElementText( + messageTemplateModal.getTemplateRowValue(index + 1), + value, + ); + } + }, + ); + + await dialTest.step( + 'Delete the first row, update the second one and save', + async () => { + await messageTemplateModal + .getTemplateRowContent(2) + .fill(updatedSecondRowContent); + await messageTemplateModal + .getTemplateRowValue(2) + .fill(updatedSecondRowTemplate); + await messageTemplateModal.getTemplateRowDeleteButton(1).click(); + await messageTemplateModal.saveChanges(); + }, + ); + + await dialTest.step( + 'Reopen "Message Template" modal and verify the changes are saved', + async () => { + await chatMessages.openMessageTemplateModal(1); + await messageTemplateModalAssertion.assertElementText( + messageTemplateModal.getTemplateRowContent(1), + updatedSecondRowContent, + ); + await messageTemplateModalAssertion.assertElementText( + messageTemplateModal.getTemplateRowValue(1), + updatedSecondRowTemplate, + ); + await messageTemplateModalAssertion.assertElementText( + messageTemplateModal.getTemplateRowContent(2), + thirdRowContent, + ); + await messageTemplateModalAssertion.assertElementText( + messageTemplateModal.getTemplateRowValue(2), + thirdRowTemplate, + ); + }, + ); + }, +); + +dialTest( + 'Message template: updated user-message influences on the template: original text is updated, deleted text is removed from the template', + async ({ + dialHomePage, + conversations, + messageTemplateModal, + messageTemplateModalAssertion, + chatMessages, + conversationData, + dataInjector, + setTestIds, + }) => { + setTestIds('EPMRTC-4273'); + const request = requestContent.split('\n').slice(0, 2).join('\n'); + const rowsMap = new Map([ + [firstRowContent, firstRowTemplate], + [thirdRowContent, thirdRowTemplate], + ]); + const updatedRequest = GeneratorUtil.randomString(5).concat( + request.split('\n')[1], + ); + let conversation: Conversation; + + await dialTest.step( + 'Prepare conversation with message template', + async () => { + conversation = + conversationData.prepareConversationBasedOnMessageTemplate( + request, + rowsMap, + ); + await dataInjector.createConversations([conversation]); + }, + ); + + await dialTest.step( + 'Edit conversation request by removing the first line', + async () => { + await dialHomePage.openHomePage(); + await dialHomePage.waitForPageLoaded(); + await conversations.selectConversation(conversation.name); + await chatMessages.openEditMessageMode(1); + await dialHomePage.mockChatTextResponse( + MockedChatApiResponseBodies.simpleTextBody, + ); + await chatMessages.editMessage(request, updatedRequest); + }, + ); + + await dialTest.step( + 'Open "Message Template" modal and verify template rows are updated', + async () => { + await chatMessages.openMessageTemplateModal(1); + await messageTemplateModalAssertion.assertElementText( + messageTemplateModal.originalMessageContent, + updatedRequest, + ); + await messageTemplateModalAssertion.assertElementsCount( + messageTemplateModal.templateRows, + 2, + ); + await messageTemplateModalAssertion.assertElementText( + messageTemplateModal.getTemplateRowContent(1), + thirdRowContent, + ); + await messageTemplateModalAssertion.assertElementText( + messageTemplateModal.getTemplateRowValue(1), + thirdRowTemplate, + ); + }, + ); + + await dialTest.step( + 'Open template preview and verify it is updated according to changes', + async () => { + await messageTemplateModal.previewTab.click(); + await messageTemplateModalAssertion.assertElementText( + messageTemplateModal.templatePreview, + updatedRequest.replace(thirdRowContent, thirdRowFirstVar), + ); + }, + ); + }, +); + +dialTest( + 'Message template: error "This part was not found in the original message" disappears if to correct the message after it was copy-pasted.\n' + + 'Message template: error "This part was not found in the original message".\n' + + 'Message template: error "Template must have at least one variable".\n' + + 'Message template: Preview and Save buttons are disabled if there is any error.\n' + + 'Message template: error "Please fill in this required field".\n' + + `Message template: fill in 'template' field when 'a part of the message' is empty` + + `Message template: error "Template doesn't match the message text".\n` + + 'Message template: errors are not applied to other fields from the row below, when the row is deleted', + async ({ + dialHomePage, + conversations, + messageTemplateModal, + page, + messageTemplateModalAssertion, + chatMessages, + conversationData, + dataInjector, + setTestIds, + }) => { + setTestIds( + 'EPMRTC-4297', + 'EPMRTC-4260', + 'EPMRTC-4262', + 'EPMRTC-4304', + 'EPMRTC-4264', + 'EPMRTC-4265', + 'EPMRTC-4263', + 'EPMRTC-4266', + ); + const request = requestContent.split('\n')[0]; + const mismatchWord = 'food'; + const firstRowMismatchContent = `Spanish is the 2nd most widely spoken ${mismatchWord}`; + let conversation: Conversation; + + await dialTest.step( + 'Prepare conversation with specified request', + async () => { + conversation = conversationData.prepareModelConversationBasedOnRequests( + [request], + ); + await dataInjector.createConversations([conversation]); + }, + ); + + await dialTest.step( + 'Open "Message Template" modal, paste the text that does not match the request and verify error appears', + async () => { + await dialHomePage.openHomePage(); + await dialHomePage.waitForPageLoaded(); + await conversations.selectConversation(conversation.name); + await chatMessages.openMessageTemplateModal(1); + await messageTemplateModal.getTemplateRowContent(1).click(); + await dialHomePage.copyToClipboard(firstRowMismatchContent); + await dialHomePage.pasteFromClipboard(); + for (let i = 1; i <= 2; i++) { + await page.keyboard.press(keys.backspace); + } + await messageTemplateModalAssertion.assertElementText( + messageTemplateModal.getFieldBottomMessage( + messageTemplateModal.getTemplateRowContent(1), + ), + ExpectedConstants.originalMessageTemplateErrorMessage, + ); + await messageTemplateModalAssertion.assertElementActionabilityState( + messageTemplateModal.saveTemplate, + 'disabled', + ); + await messageTemplateModalAssertion.assertElementActionabilityState( + messageTemplateModal.previewTab, + 'disabled', + ); + }, + ); + + await dialTest.step( + 'Correct the text to match the request and verify error disappears', + async () => { + for (let i = 1; i <= mismatchWord.length; i++) { + await page.keyboard.press(keys.backspace); + } + await messageTemplateModalAssertion.assertElementState( + messageTemplateModal.getFieldBottomMessage( + messageTemplateModal.getTemplateRowContent(1), + ), + 'hidden', + ); + await messageTemplateModalAssertion.assertElementActionabilityState( + messageTemplateModal.saveTemplate, + 'disabled', + ); + await messageTemplateModalAssertion.assertElementActionabilityState( + messageTemplateModal.previewTab, + 'disabled', + ); + }, + ); + + await dialTest.step( + 'Type the text that match the request but in capitals and verify error appears', + async () => { + await messageTemplateModal + .getTemplateRowContent(1) + .fill(request.toUpperCase()); + await messageTemplateModalAssertion.assertElementText( + messageTemplateModal.getFieldBottomMessage( + messageTemplateModal.getTemplateRowContent(1), + ), + ExpectedConstants.originalMessageTemplateErrorMessage, + ); + }, + ); + + await dialTest.step( + 'Type incorrect value into template field and verify error appears', + async () => { + for (const variable of [GeneratorUtil.randomString(5), '{{}}']) { + await messageTemplateModal.getTemplateRowValue(1).fill(variable); + await messageTemplateModalAssertion.assertElementText( + messageTemplateModal.getFieldBottomMessage( + messageTemplateModal.getTemplateRowValue(1), + ), + ExpectedConstants.messageTemplateMissingVarErrorMessage, + ); + } + }, + ); + + await dialTest.step( + 'Set correct value into template field and verify error disappears', + async () => { + await messageTemplateModal + .getTemplateRowValue(1) + .fill(secondRowFirstVar); + await messageTemplateModalAssertion.assertElementState( + messageTemplateModal.getFieldBottomMessage( + messageTemplateModal.getTemplateRowValue(1), + ), + 'hidden', + ); + }, + ); + + await dialTest.step( + 'Delete the row and verify Save and Preview buttons are enabled', + async () => { + await messageTemplateModal.getTemplateRowDeleteButton(1).click(); + await messageTemplateModalAssertion.assertElementActionabilityState( + messageTemplateModal.saveTemplate, + 'enabled', + ); + await messageTemplateModalAssertion.assertElementActionabilityState( + messageTemplateModal.previewTab, + 'enabled', + ); + }, + ); + + await dialTest.step( + 'Set the correct value in the template field and verify error is displayed', + async () => { + await messageTemplateModal + .getTemplateRowValue(1) + .fill(firstRowFirstVar); + await messageTemplateModal.getTemplateRowContent(1).click(); + await messageTemplateModal.getTemplateRowValue(1).click(); + await messageTemplateModalAssertion.assertElementText( + messageTemplateModal.getFieldBottomMessage( + messageTemplateModal.getTemplateRowContent(1), + ), + ExpectedConstants.messageTemplateRequiredField, + ); + }, + ); + + await dialTest.step( + 'Set correct value into message, clear the row fields and verify error appears', + async () => { + await messageTemplateModal.getTemplateRowContent(1).fill(request); + + await messageTemplateModal.getTemplateRowContent(1).fill(''); + await messageTemplateModal.getTemplateRowValue(1).fill(''); + + await messageTemplateModalAssertion.assertElementText( + messageTemplateModal.getFieldBottomMessage( + messageTemplateModal.getTemplateRowContent(1), + ), + ExpectedConstants.messageTemplateRequiredField, + ); + await messageTemplateModalAssertion.assertElementText( + messageTemplateModal.getFieldBottomMessage( + messageTemplateModal.getTemplateRowValue(1), + ), + ExpectedConstants.messageTemplateRequiredField, + ); + }, + ); + + await dialTest.step( + 'Set correct values into row fields and verify errors disappear', + async () => { + await messageTemplateModal.getTemplateRowContent(1).fill(request); + await messageTemplateModal + .getTemplateRowValue(1) + .fill(firstRowFirstVar); + await messageTemplateModalAssertion.assertElementState( + messageTemplateModal.getFieldBottomMessage( + messageTemplateModal.getTemplateRowContent(1), + ), + 'hidden', + ); + await messageTemplateModalAssertion.assertElementState( + messageTemplateModal.getFieldBottomMessage( + messageTemplateModal.getTemplateRowValue(1), + ), + 'hidden', + ); + }, + ); + + await dialTest.step( + 'Set the template that does not match the message and verify error is displayed', + async () => { + await messageTemplateModal + .getTemplateRowContent(1) + .fill(firstRowContent); + await messageTemplateModal + .getTemplateRowValue(1) + .fill( + firstRowTemplate.substring( + 0, + firstRowTemplate.indexOf(firstRowSecondVar), + ), + ); + await messageTemplateModalAssertion.assertElementText( + messageTemplateModal.getFieldBottomMessage( + messageTemplateModal.getTemplateRowValue(1), + ), + ExpectedConstants.messageTemplateMismatchTextErrorMessage, + ); + }, + ); + + await dialTest.step( + 'Correct the template value and verify error disappears', + async () => { + await messageTemplateModal + .getTemplateRowValue(1) + .fill(firstRowTemplate); + await messageTemplateModalAssertion.assertElementState( + messageTemplateModal.getFieldBottomMessage( + messageTemplateModal.getTemplateRowContent(1), + ), + 'hidden', + ); + await messageTemplateModalAssertion.assertElementState( + messageTemplateModal.getFieldBottomMessage( + messageTemplateModal.getTemplateRowValue(1), + ), + 'hidden', + ); + await messageTemplateModalAssertion.assertElementActionabilityState( + messageTemplateModal.saveTemplate, + 'enabled', + ); + await messageTemplateModalAssertion.assertElementActionabilityState( + messageTemplateModal.previewTab, + 'enabled', + ); + }, + ); + + await dialTest.step( + 'Fill in one more row with the correct values, clear the 1st row and verify errors are displayed', + async () => { + await messageTemplateModal + .getTemplateRowContent(2) + .fill(thirdRowContent); + await messageTemplateModal + .getTemplateRowValue(2) + .fill(thirdRowTemplate); + + await messageTemplateModal.getTemplateRowContent(1).fill(''); + await messageTemplateModal.getTemplateRowValue(1).fill(''); + await messageTemplateModalAssertion.assertElementText( + messageTemplateModal.getFieldBottomMessage( + messageTemplateModal.getTemplateRowContent(1), + ), + ExpectedConstants.messageTemplateRequiredField, + ); + await messageTemplateModalAssertion.assertElementText( + messageTemplateModal.getFieldBottomMessage( + messageTemplateModal.getTemplateRowValue(1), + ), + ExpectedConstants.messageTemplateRequiredField, + ); + }, + ); + + await dialTest.step( + 'Delete row with errors and verify no errors are displayed for the valid row', + async () => { + await messageTemplateModal.getTemplateRowDeleteButton(1).click(); + await messageTemplateModalAssertion.assertElementState( + messageTemplateModal.getFieldBottomMessage( + messageTemplateModal.getTemplateRowContent(1), + ), + 'hidden', + ); + await messageTemplateModalAssertion.assertElementState( + messageTemplateModal.getFieldBottomMessage( + messageTemplateModal.getTemplateRowValue(1), + ), + 'hidden', + ); + await messageTemplateModalAssertion.assertElementActionabilityState( + messageTemplateModal.saveTemplate, + 'enabled', + ); + await messageTemplateModalAssertion.assertElementActionabilityState( + messageTemplateModal.previewTab, + 'enabled', + ); + }, + ); + }, +); + +dialTest( + 'Message template created for the user-message with parametrized prompt', + async ({ + dialHomePage, + messageTemplateModal, + messageTemplateModalAssertion, + chatMessages, + promptData, + sendMessage, + dataInjector, + variableModalDialog, + setTestIds, + }) => { + setTestIds('EPMRTC-4298'); + const aVar = 'A'; + const aVarPlaceholder = `{{${aVar}}}`; + const aValue = '1'; + const bVar = 'B'; + const bVarPlaceholder = `{{${bVar}}}`; + const bValue = '2'; + const cVar = 'C'; + const cValue = '10'; + const cVarPlaceholder = `{{${cVar}|${cValue}}}`; + const dVar = 'D'; + const dValue = '5'; + const dVarPlaceholder = `{{${dVar}|${dValue}}}`; + const firstPromptContent = (a: string, b: string) => + `Calculate ${aVar} + ${bVar}, where ${aVar} = ${a} and ${bVar} = ${b}`; + const secondPromptContent = (c: string, d: string) => + `Calculate ${cVar} - ${dVar}, where ${cVar} = ${c} and ${dVar} = ${d}`; + const fullRequest = `${firstPromptContent(aValue, bValue)} AND ${secondPromptContent(cValue, dValue)}`; + let firstPrompt: Prompt; + let secondPrompt: Prompt; + + await dialTest.step( + 'Prepare prompt with params and default values', + async () => { + firstPrompt = promptData.preparePrompt( + firstPromptContent(aVarPlaceholder, bVarPlaceholder), + ); + promptData.resetData(); + secondPrompt = promptData.preparePrompt( + secondPromptContent(cVarPlaceholder, dVarPlaceholder), + ); + await dataInjector.createPrompts([firstPrompt, secondPrompt]); + }, + ); + + await dialTest.step( + 'Create new conversation based on both prompts', + async () => { + await dialHomePage.openHomePage(); + await dialHomePage.waitForPageLoaded(); + await sendMessage.messageInput.fillInInput(`/${firstPrompt.name}`); + await sendMessage + .getPromptList() + .selectPromptWithMouse(firstPrompt.name, { + triggeredHttpMethod: 'GET', + }); + await variableModalDialog.setVariableValue(aVar, aValue); + await variableModalDialog.setVariableValue(bVar, bValue); + await variableModalDialog.submitButton.click(); + await sendMessage.typeInInput(' AND '); + await sendMessage.messageInput.typeInInput(`/${secondPrompt.name}`); + await sendMessage + .getPromptList() + .selectPromptWithMouse(secondPrompt.name, { + triggeredHttpMethod: 'GET', + }); + await variableModalDialog.submitButton.click(); + await dialHomePage.mockChatTextResponse( + MockedChatApiResponseBodies.simpleTextBody, + ); + await sendMessage.sendMessageButton.click(); + }, + ); + + await dialTest.step( + 'Open request message template and verify prompts are displayed in the rows', + async () => { + await chatMessages.openMessageTemplateModal(1); + await messageTemplateModalAssertion.assertElementText( + messageTemplateModal.originalMessageContent, + fullRequest, + ); + + await messageTemplateModalAssertion.assertElementText( + messageTemplateModal.getTemplateRowContent(1), + firstPromptContent(aValue, bValue), + ); + await messageTemplateModalAssertion.assertElementText( + messageTemplateModal.getTemplateRowContent(2), + secondPromptContent(cValue, dValue), + ); + + await messageTemplateModalAssertion.assertElementText( + messageTemplateModal.getTemplateRowValue(1), + firstPromptContent(aVarPlaceholder, bVarPlaceholder), + ); + await messageTemplateModalAssertion.assertElementText( + messageTemplateModal.getTemplateRowValue(2), + secondPromptContent(cVarPlaceholder, dVarPlaceholder), + ); + }, + ); + }, +); diff --git a/apps/chat-e2e/src/tests/monitoring/renameConversation.test.ts b/apps/chat-e2e/src/tests/monitoring/renameConversation.test.ts index 27f7d4bdad..657dccb391 100644 --- a/apps/chat-e2e/src/tests/monitoring/renameConversation.test.ts +++ b/apps/chat-e2e/src/tests/monitoring/renameConversation.test.ts @@ -12,6 +12,7 @@ dialTest( conversationData, dataInjector, conversationAssertion, + renameConversationModal, }) => { const updatedConversationName = GeneratorUtil.randomString(5); let conversation: Conversation; @@ -27,7 +28,9 @@ dialTest( await conversations.selectConversation(conversation.name); await conversations.openEntityDropdownMenu(conversation.name); await conversationDropdownMenu.selectMenuOption(MenuOptions.rename); - await conversations.editConversationNameWithTick(updatedConversationName); + await renameConversationModal.editConversationNameWithSaveButton( + updatedConversationName, + ); await conversationAssertion.assertEntityState( { name: updatedConversationName }, 'visible', diff --git a/apps/chat-e2e/src/tests/playBack.test.ts b/apps/chat-e2e/src/tests/playBack.test.ts index e8c22952c0..e73b74f213 100644 --- a/apps/chat-e2e/src/tests/playBack.test.ts +++ b/apps/chat-e2e/src/tests/playBack.test.ts @@ -420,7 +420,6 @@ dialTest( 'Prepare playback conversation based on 2 requests', async () => { conversation = conversationData.prepareModelConversationBasedOnRequests( - defaultModel, ['1st request', '2nd request'], ); conversationData.resetData(); @@ -672,7 +671,6 @@ dialTest( 'Prepare playback conversation based on 2 requests and played back till the last message', async () => { conversation = conversationData.prepareModelConversationBasedOnRequests( - defaultModel, ['1+2=', '2+3='], ); conversationData.resetData(); @@ -751,7 +749,6 @@ dialTest( 'Prepare playback conversation based on several long requests', async () => { conversation = conversationData.prepareModelConversationBasedOnRequests( - defaultModel, [GeneratorUtil.randomString(3000)], ); conversationData.resetData(); diff --git a/apps/chat-e2e/src/tests/promptUsage.test.ts b/apps/chat-e2e/src/tests/promptUsage.test.ts index 85613c0acc..3d9d7884c0 100644 --- a/apps/chat-e2e/src/tests/promptUsage.test.ts +++ b/apps/chat-e2e/src/tests/promptUsage.test.ts @@ -121,26 +121,31 @@ dialTest( dialTest( 'Prompt text without parameters appears one by one in Input message box.\n' + - 'The text entered by user remains if to use prompt with parameters in Input message box', + 'The text entered by user remains if to use prompt with parameters in Input message box.\n' + + 'Message template created for the user-message with prompt without parameters', async ({ dialHomePage, promptData, dataInjector, sendMessage, + chatMessages, + messageTemplateModal, + messageTemplateModalAssertion, sendMessageAssertion, variableModalDialog, setTestIds, }) => { - setTestIds('EPMRTC-3823', 'EPMRTC-3803'); + setTestIds('EPMRTC-3823', 'EPMRTC-3803', 'EPMRTC-4371'); let simplePrompt: Prompt; let promptWithVariable: Prompt; + const simplePromptContent = GeneratorUtil.randomString(10); const promptVariable = 'A'; const promptWithVariableContent = (variable: string) => `Calculate ${variable} * 100`; await dialTest.step('Prepare 2 prompts', async () => { simplePrompt = promptData.preparePrompt( - GeneratorUtil.randomString(10), + simplePromptContent, undefined, ExpectedConstants.newPromptTitle(1), ); @@ -184,6 +189,22 @@ dialTest( ); }, ); + + await dialTest.step( + 'Send the request, open request message template and verify prompt w/o params is not displayed in the rows', + async () => { + await dialHomePage.mockChatTextResponse( + MockedChatApiResponseBodies.simpleTextBody, + ); + await sendMessage.sendMessageButton.click(); + await chatMessages.openMessageTemplateModal(1); + + await messageTemplateModalAssertion.assertElementState( + messageTemplateModal.getTemplateRowContent(simplePromptContent), + 'hidden', + ); + }, + ); }, ); diff --git a/apps/chat-e2e/src/tests/publishConversation.test.ts b/apps/chat-e2e/src/tests/publishConversation.test.ts index 93274ae34f..a99330ef4e 100644 --- a/apps/chat-e2e/src/tests/publishConversation.test.ts +++ b/apps/chat-e2e/src/tests/publishConversation.test.ts @@ -11,6 +11,7 @@ import { } from '@/src/testData'; import { UploadDownloadData } from '@/src/ui/pages'; import { GeneratorUtil, ModelsUtil } from '@/src/utils'; +import { PublishActions } from '@epam/ai-dial-shared'; const publicationsToUnpublish: Publication[] = []; @@ -428,7 +429,7 @@ dialAdminTest( await dataInjector.createConversations([conversation]); const publishRequest = publishRequestBuilder .withName(publicationName) - .withConversationResource(conversation) + .withConversationResource(conversation, PublishActions.ADD) .build(); await publicationApiHelper.createPublishRequest(publishRequest); conversationData.resetData(); diff --git a/apps/chat-e2e/src/tests/publishConversationToOrganisation.test.ts b/apps/chat-e2e/src/tests/publishConversationToOrganisation.test.ts index bef7a70672..5b6bf4f05d 100644 --- a/apps/chat-e2e/src/tests/publishConversationToOrganisation.test.ts +++ b/apps/chat-e2e/src/tests/publishConversationToOrganisation.test.ts @@ -10,6 +10,7 @@ import { PublishPath, } from '@/src/testData'; import { GeneratorUtil } from '@/src/utils'; +import { PublishActions } from '@epam/ai-dial-shared'; const publicationsToUnpublish: Publication[] = []; @@ -75,7 +76,10 @@ dialAdminTest( const publishRequest = publishRequestBuilder .withName(GeneratorUtil.randomPublicationRequestName()) .withTargetFolder(organizationFolderNames[i - 1]) - .withConversationResource(publishRequestConversations[i - 1]) + .withConversationResource( + publishRequestConversations[i - 1], + PublishActions.ADD, + ) .build(); const publication = await publicationApiHelper.createPublishRequest(publishRequest); @@ -258,7 +262,8 @@ dialAdminTest( const requestName = GeneratorUtil.randomPublicationRequestName(); const newFolderName = GeneratorUtil.randomString(maxNameLength * 1.5); const cutNewFolderName = newFolderName.substring(0, maxNameLength); - const publicationPath = `${PublishPath.Organization}/${cutNewFolderName}/${ExpectedConstants.newFolderWithIndexTitle(1)}/${ExpectedConstants.newFolderWithIndexTitle(2)}/${ExpectedConstants.newFolderWithIndexTitle(3)}`; + const defaultFolderName = ExpectedConstants.newFolderWithIndexTitle(1); + const publicationPath = `${PublishPath.Organization}/${cutNewFolderName}/${defaultFolderName}/${defaultFolderName}/${defaultFolderName}`; await dialTest.step('Prepare a new conversation to publish', async () => { conversationToPublish = conversationData.prepareDefaultConversation(); @@ -279,11 +284,11 @@ dialAdminTest( await selectFolderModal.newFolderButton.click(); await selectFoldersAssertion.assertFolderEditInputState('visible'); await selectFoldersAssertion.assertFolderEditInputValue( - ExpectedConstants.newFolderWithIndexTitle(1), + defaultFolderName, ); await selectFolders.getEditFolderInputActions().clickTickButton(); await selectFoldersAssertion.assertFolderState( - { name: ExpectedConstants.newFolderWithIndexTitle(1) }, + { name: defaultFolderName }, 'visible', ); }, @@ -292,9 +297,7 @@ dialAdminTest( await dialTest.step( 'Open folder dropdown menu and verify available options', async () => { - await selectFolders.openFolderDropdownMenu( - ExpectedConstants.newFolderWithIndexTitle(1), - ); + await selectFolders.openFolderDropdownMenu(defaultFolderName); await folderDropdownMenuAssertion.assertMenuOptions([ MenuOptions.rename, MenuOptions.delete, @@ -318,11 +321,11 @@ dialAdminTest( await selectFolders.openFolderDropdownMenu(cutNewFolderName); await folderDropdownMenu.selectMenuOption(MenuOptions.addNewFolder); await selectFoldersAssertion.assertFolderEditInputValue( - ExpectedConstants.newFolderWithIndexTitle(1), + defaultFolderName, ); await selectFolders.getEditFolderInputActions().clickTickButton(); await selectFoldersAssertion.assertFolderState( - { name: ExpectedConstants.newFolderWithIndexTitle(1) }, + { name: defaultFolderName }, 'visible', ); }); @@ -331,9 +334,7 @@ dialAdminTest( 'Verify error message appears on adding more than 3 sub-folders', async () => { for (let i = 1; i <= maxNestedLevel - 1; i++) { - await selectFolders.openFolderDropdownMenu( - ExpectedConstants.newFolderWithIndexTitle(i), - ); + await selectFolders.openFolderDropdownMenu(defaultFolderName, i); await folderDropdownMenu.selectMenuOption(MenuOptions.addNewFolder); if (i === maxNestedLevel - 1) { await errorToastAssertion.assertToastMessage( @@ -343,7 +344,10 @@ dialAdminTest( } else { await selectFolders.getEditFolderInputActions().clickTickButton(); await selectFoldersAssertion.assertFolderState( - { name: ExpectedConstants.newFolderWithIndexTitle(i + 1) }, + { + name: defaultFolderName, + index: i + 1, + }, 'visible', ); } @@ -371,16 +375,12 @@ dialAdminTest( if (i === maxNestedLevel - 1) { await selectFolders.getEditFolderInputActions().clickTickButton(); } - await selectFolderModal.selectFolder( - ExpectedConstants.newFolderWithIndexTitle(i), - ); + await selectFolderModal.selectFolder(defaultFolderName, i); await selectFoldersAssertion.assertFolderSelectedState( - { name: ExpectedConstants.newFolderWithIndexTitle(i) }, + { name: defaultFolderName, index: i }, true, ); - await selectFolders.expandFolder( - ExpectedConstants.newFolderWithIndexTitle(i), - ); + await selectFolders.expandFolder(defaultFolderName, undefined, i); } }, ); @@ -445,8 +445,6 @@ dialAdminTest( await adminPublicationReviewControl.backToPublicationRequest(); await adminPublishingApprovalModal.approveRequest(); - await dialHomePage.reloadPage(); - await dialHomePage.waitForPageLoaded(); await adminOrganizationFolderConversationAssertions.assertFolderState( { name: cutNewFolderName }, 'visible', @@ -457,17 +455,18 @@ dialAdminTest( ); for (let i = 1; i <= maxNestedLevel - 1; i++) { await adminOrganizationFolderConversationAssertions.assertFolderState( - { name: ExpectedConstants.newFolderWithIndexTitle(i) }, + { name: defaultFolderName, index: i }, 'visible', ); await adminOrganizationFolderConversations.expandFolder( - ExpectedConstants.newFolderWithIndexTitle(i), - { httpHost: ExpectedConstants.newFolderWithIndexTitle(i) }, + defaultFolderName, + { httpHost: defaultFolderName }, + i, ); } await adminOrganizationFolderConversationAssertions.assertFolderEntityState( - { name: ExpectedConstants.newFolderWithIndexTitle(3) }, + { name: defaultFolderName, index: 3 }, { name: conversationToPublish.name }, 'visible', ); diff --git a/apps/chat-e2e/src/tests/publishFolderWithConversation.test.ts b/apps/chat-e2e/src/tests/publishFolderWithConversation.test.ts index 90b0ac4baf..e62cdde276 100644 --- a/apps/chat-e2e/src/tests/publishFolderWithConversation.test.ts +++ b/apps/chat-e2e/src/tests/publishFolderWithConversation.test.ts @@ -12,6 +12,7 @@ import { PublishPath, } from '@/src/testData'; import { GeneratorUtil } from '@/src/utils'; +import { PublishActions } from '@epam/ai-dial-shared'; const publicationsToUnpublish: Publication[] = []; const levelsCount = 4; @@ -40,7 +41,7 @@ dialAdminTest( adminTooltip, adminApproveRequiredConversationsAssertion, adminPublishingApprovalModalAssertion, - adminPublishingApprovalFolderConversationsAssertion, + adminFolderToApproveAssertion, adminChatHeaderAssertion, adminChatMessagesAssertion, adminOrganizationFolderConversations, @@ -188,7 +189,7 @@ dialAdminTest( 'Verify folders hierarchy with non empty conversations is displayed on "Publication approval" modal, "Approve" button is disabled', async () => { for (const conversation of allConversations) { - await adminPublishingApprovalFolderConversationsAssertion.assertFolderEntityState( + await adminFolderToApproveAssertion.assertFolderEntityState( { name: nestedFolders[0].name }, { name: conversation.name }, conversation.name === emptyConversation.name || @@ -401,7 +402,7 @@ dialAdminTest( adminPublicationReviewControl, adminApproveRequiredConversations, adminPublishingApprovalModalAssertion, - adminPublishingApprovalFolderConversationsAssertion, + adminFolderToApproveAssertion, adminPublishingApprovalModal, adminOrganizationFolderConversations, adminOrganizationFolderConversationAssertions, @@ -435,6 +436,7 @@ dialAdminTest( .withName(GeneratorUtil.randomPublicationRequestName()) .withConversationResource( publishedFolderConversation.conversations[0], + PublishActions.ADD, ) .build(); const publication = @@ -491,14 +493,16 @@ dialAdminTest( for (let i = 1; i <= levelsCount - 2; i++) { await selectFolders.openFolderDropdownMenu( - ExpectedConstants.newFolderWithIndexTitle(i), + ExpectedConstants.newFolderWithIndexTitle(1), + i, ); await folderDropdownMenu.selectMenuOption(MenuOptions.addNewFolder); await selectFolders.getEditFolderInputActions().clickTickButton(); } await selectFolderModal.selectFolder( - ExpectedConstants.newFolderWithIndexTitle(levelsCount - 1), + ExpectedConstants.newFolderWithIndexTitle(1), + levelsCount - 1, ); await selectFolderModal.clickSelectFolderButton(); await errorToastAssertion.assertToastMessage( @@ -571,7 +575,7 @@ dialAdminTest( await dialAdminTest.step( 'Verify folders hierarchy is displayed on "Publication approval" modal and "Publish to" path', async () => { - await adminPublishingApprovalFolderConversationsAssertion.assertFolderEntityState( + await adminFolderToApproveAssertion.assertFolderEntityState( { name: folderConversationToPublish.folders.name }, { name: folderConversationToPublish.conversations[0].name }, 'visible', diff --git a/apps/chat-e2e/src/tests/replay.test.ts b/apps/chat-e2e/src/tests/replay.test.ts index 4c8e055bf4..5b4e526f90 100644 --- a/apps/chat-e2e/src/tests/replay.test.ts +++ b/apps/chat-e2e/src/tests/replay.test.ts @@ -21,7 +21,9 @@ let aModel: DialAIEntityModel; let bModel: DialAIEntityModel; dialTest.beforeAll(async () => { - allModels = ModelsUtil.getModels().filter((m) => m.iconUrl != undefined); + allModels = ModelsUtil.getLatestModels().filter( + (m) => m.iconUrl != undefined, + ); defaultModel = ModelsUtil.getDefaultModel()!; aModel = GeneratorUtil.randomArrayElement( allModels.filter((m) => m.id !== defaultModel.id), @@ -595,6 +597,7 @@ dialTest( chatMessages, setTestIds, conversationDropdownMenu, + renameConversationModal, }) => { setTestIds('EPMRTC-505', 'EPMRTC-506', 'EPMRTC-515', 'EPMRTC-516'); let conversation: Conversation; @@ -604,7 +607,6 @@ dialTest( 'Prepare conversation to replay with updated name', async () => { conversation = conversationData.prepareModelConversationBasedOnRequests( - defaultModel, ['1+2'], ); replayConversation = @@ -626,7 +628,7 @@ dialTest( await conversations.openEntityDropdownMenu(replayConversation.name); await conversationDropdownMenu.selectMenuOption(MenuOptions.rename); replayConversation.name = GeneratorUtil.randomString(7); - await conversations.editConversationNameWithTick( + await renameConversationModal.editConversationNameWithSaveButton( replayConversation.name, ); diff --git a/apps/chat-e2e/src/tests/scrolling.test.ts b/apps/chat-e2e/src/tests/scrolling.test.ts index c6e40194e3..f11e5c7aa5 100644 --- a/apps/chat-e2e/src/tests/scrolling.test.ts +++ b/apps/chat-e2e/src/tests/scrolling.test.ts @@ -37,10 +37,9 @@ dialTest( let conversation: Conversation; await dialTest.step('Prepare conversation with long response', async () => { - conversation = conversationData.prepareModelConversationBasedOnRequests( - defaultModel, - [GeneratorUtil.randomString(3000)], - ); + conversation = conversationData.prepareModelConversationBasedOnRequests([ + GeneratorUtil.randomString(3000), + ]); await dataInjector.createConversations([conversation]); }); @@ -146,10 +145,9 @@ dialTest( let conversation: Conversation; await dialTest.step('Prepare conversation with long response', async () => { - conversation = conversationData.prepareModelConversationBasedOnRequests( - defaultModel, - [GeneratorUtil.randomString(3000)], - ); + conversation = conversationData.prepareModelConversationBasedOnRequests([ + GeneratorUtil.randomString(3000), + ]); await dataInjector.createConversations([conversation]); }); @@ -203,16 +201,14 @@ dialTest( 'Prepare two conversations with long responses', async () => { firstConversation = - conversationData.prepareModelConversationBasedOnRequests( - defaultModel, - [GeneratorUtil.randomString(3000)], - ); + conversationData.prepareModelConversationBasedOnRequests([ + GeneratorUtil.randomString(3000), + ]); conversationData.resetData(); secondConversation = - conversationData.prepareModelConversationBasedOnRequests( - defaultModel, - [GeneratorUtil.randomString(3000)], - ); + conversationData.prepareModelConversationBasedOnRequests([ + GeneratorUtil.randomString(3000), + ]); await dataInjector.createConversations([ firstConversation, secondConversation, @@ -335,21 +331,21 @@ dialTest( async () => { firstConversation = conversationData.prepareModelConversationBasedOnRequests( - defaultModel, [ GeneratorUtil.randomString(2000), GeneratorUtil.randomString(2000), ], + defaultModel, firstConversationName, ); conversationData.resetData(); secondConversation = conversationData.prepareModelConversationBasedOnRequests( - defaultModel, [ GeneratorUtil.randomString(2000), GeneratorUtil.randomString(2000), ], + defaultModel, secondConversationName, ); await dataInjector.createConversations([ @@ -519,10 +515,10 @@ dialTest( await dialTest.step( 'Prepare conversation with 3 long requests', async () => { - conversation = conversationData.prepareModelConversationBasedOnRequests( - defaultModel, - userRequests, - ); + conversation = + conversationData.prepareModelConversationBasedOnRequests( + userRequests, + ); await dataInjector.createConversations([conversation]); }, ); diff --git a/apps/chat-e2e/src/tests/selectUploadFolder.test.ts b/apps/chat-e2e/src/tests/selectUploadFolder.test.ts index 14ded2c9d4..62942374b6 100644 --- a/apps/chat-e2e/src/tests/selectUploadFolder.test.ts +++ b/apps/chat-e2e/src/tests/selectUploadFolder.test.ts @@ -91,7 +91,7 @@ dialTest( await dialTest.step( 'Select created folder and verify correct path is displayed in "Upload to" field, the field is highlighted and has text_overflow=ellipsis property', async () => { - await selectFolderModal.selectFolder(updatedFolderName, { + await selectFolderModal.selectFolder(updatedFolderName, 1, { triggeredApiHost: API.listingHost, }); await selectFolderModal.selectFolderButton.click(); diff --git a/apps/chat-e2e/src/tests/sharedChatIcons.test.ts b/apps/chat-e2e/src/tests/sharedChatIcons.test.ts index f92d1a2a9e..7e97c2ce15 100644 --- a/apps/chat-e2e/src/tests/sharedChatIcons.test.ts +++ b/apps/chat-e2e/src/tests/sharedChatIcons.test.ts @@ -321,6 +321,7 @@ dialTest( conversationSettingsModal, chat, setTestIds, + renameConversationModal, }) => { setTestIds( 'EPMRTC-1514', @@ -402,8 +403,8 @@ dialTest( secondConversationToShare.name, ); await conversationDropdownMenu.selectMenuOption(MenuOptions.rename); - await conversations.getEditEntityInput().editValue(newName); - await conversations.getEditInputActions().clickTickButton(); + await renameConversationModal.editInputValue(newName); + await renameConversationModal.saveButton.click(); await confirmationDialogAssertion.assertConfirmationDialogTitle( ExpectedConstants.renameSharedConversationDialogTitle, ); diff --git a/apps/chat-e2e/src/tests/sharedFilesAttachments.test.ts b/apps/chat-e2e/src/tests/sharedFilesAttachments.test.ts index e7d0fb2944..047ab7e1f6 100644 --- a/apps/chat-e2e/src/tests/sharedFilesAttachments.test.ts +++ b/apps/chat-e2e/src/tests/sharedFilesAttachments.test.ts @@ -67,6 +67,7 @@ dialSharedWithMeTest( additionalShareUserFileApiHelper, errorToast, additionalShareUserManageAttachmentsAssertion, + renameConversationModal, }) => { dialSharedWithMeTest.slow(); setTestIds( @@ -362,10 +363,10 @@ dialSharedWithMeTest( ); conversationWithTwoResponses.name = GeneratorUtil.randomString(10); await conversationDropdownMenu.selectMenuOption(MenuOptions.rename); - await conversations - .getEditEntityInput() - .editValue(conversationWithTwoResponses.name); - await conversations.getEditInputActions().clickTickButton(); + await renameConversationModal.editInputValue( + conversationWithTwoResponses.name, + ); + await renameConversationModal.saveButton.click(); await confirmationDialog.confirm({ triggeredHttpMethod: 'DELETE', }); @@ -504,6 +505,7 @@ dialSharedWithMeTest( conversationData, dataInjector, fileApiHelper, + additionalShareUserFileApiHelper, mainUserShareApiHelper, additionalUserShareApiHelper, additionalShareUserSendMessage, @@ -580,6 +582,11 @@ dialSharedWithMeTest( await fileApiHelper.putFile( user1ConversationInFolderImageInResponse1, ); + + //upload file into 'All files' section to have it visible + await additionalShareUserFileApiHelper.putFile( + Attachment.heartImageName, + ); }, ); diff --git a/apps/chat-e2e/src/tests/unpublishConversation.test.ts b/apps/chat-e2e/src/tests/unpublishConversation.test.ts index 89995df967..c5d78aa3d0 100644 --- a/apps/chat-e2e/src/tests/unpublishConversation.test.ts +++ b/apps/chat-e2e/src/tests/unpublishConversation.test.ts @@ -11,6 +11,7 @@ import { import { PublicationProps } from '@/src/testData/api'; import { Colors } from '@/src/ui/domData'; import { GeneratorUtil, ModelsUtil } from '@/src/utils'; +import { PublishActions } from '@epam/ai-dial-shared'; dialAdminTest( 'Unpublish single chat without attachments.\n' + @@ -65,7 +66,7 @@ dialAdminTest( const publishRequest = publishRequestBuilder .withName(GeneratorUtil.randomPublicationRequestName()) - .withConversationResource(publishedConversation) + .withConversationResource(publishedConversation, PublishActions.ADD) .build(); const publication = await publicationApiHelper.createPublishRequest(publishRequest); diff --git a/apps/chat-e2e/src/tests/unpublishFolderWithConversations.test.ts b/apps/chat-e2e/src/tests/unpublishFolderWithConversations.test.ts new file mode 100644 index 0000000000..aa6b656e4d --- /dev/null +++ b/apps/chat-e2e/src/tests/unpublishFolderWithConversations.test.ts @@ -0,0 +1,1093 @@ +import { Conversation } from '@/chat/types/chat'; +import { FolderInterface } from '@/chat/types/folder'; +import { Publication, PublicationRequestModel } from '@/chat/types/publication'; +import dialAdminTest from '@/src/core/dialAdminFixtures'; +import dialTest from '@/src/core/dialFixtures'; +import { + CheckboxState, + ExpectedConstants, + FolderConversation, + MenuOptions, + PublishPath, +} from '@/src/testData'; +import { PublicationProps } from '@/src/testData/api'; +import { Colors } from '@/src/ui/domData'; +import { GeneratorUtil, ModelsUtil } from '@/src/utils'; +import { PublishActions } from '@epam/ai-dial-shared'; + +let expectedConversationIcon: string; +let folderConversationToUnpublish: Conversation; + +dialTest.beforeAll(async ({ iconApiHelper }) => { + const defaultModel = ModelsUtil.getDefaultModel()!; + expectedConversationIcon = iconApiHelper.getEntityIcon(defaultModel); +}); + +dialAdminTest( + 'Unpublish chat inside folder.\n' + + 'Unpublish request for folder structure where one chat was already unpublished.\n' + + 'Unpublish all chats from folder: folder is deleted from Organization', + async ({ + dialHomePage, + conversationData, + publishRequestBuilder, + publicationApiHelper, + adminPublicationApiHelper, + dataInjector, + organizationFolderConversations, + conversationDropdownMenu, + publishingRequestModal, + conversationToPublishAssertion, + baseAssertion, + publishingRules, + publishingRequestModalAssertion, + adminDialHomePage, + adminApproveRequiredConversations, + adminPublishingApprovalModal, + adminPublicationReviewControl, + adminOrganizationFolderConversations, + adminApproveRequiredConversationsAssertion, + adminOrganizationFolderConversationAssertions, + adminPublishingApprovalModalAssertion, + adminFolderToApproveAssertion, + organizationFolderConversationAssertions, + setTestIds, + }) => { + setTestIds('EPMRTC-3386', 'EPMRTC-3802', 'EPMRTC-3389'); + let firstConversation: Conversation; + let secondConversation: Conversation; + let conversationsFolder: FolderConversation; + let publishedFolderName: string; + const firstConversationUnpublishingRequestName = + GeneratorUtil.randomUnpublishRequestName(); + const secondConversationUnpublishingRequestName = + GeneratorUtil.randomUnpublishRequestName(); + let firstUnpublishApiModels: { + request: PublicationRequestModel; + response: Publication; + }; + let folderPublicationRequest: Publication; + let publishPath: string; + let unpublishFolderResponse: PublicationProps; + + await dialTest.step( + 'Create and approve publishing of folder with 2 conversations inside', + async () => { + firstConversation = conversationData.prepareDefaultConversation(); + conversationData.resetData(); + secondConversation = conversationData.prepareDefaultConversation(); + conversationData.resetData(); + conversationsFolder = conversationData.prepareConversationsInFolder([ + firstConversation, + secondConversation, + ]); + await dataInjector.createConversations( + conversationsFolder.conversations, + conversationsFolder.folders, + ); + publishedFolderName = conversationsFolder.folders.name; + publishPath = `${PublishPath.Organization}/${publishedFolderName}`; + + const publishRequest = publishRequestBuilder + .withName(GeneratorUtil.randomPublicationRequestName()) + .withConversationResource(firstConversation, PublishActions.ADD) + .withConversationResource(secondConversation, PublishActions.ADD) + .build(); + folderPublicationRequest = + await publicationApiHelper.createPublishRequest(publishRequest); + await adminPublicationApiHelper.approveRequest( + folderPublicationRequest, + ); + }, + ); + + await dialTest.step( + 'Select "Unpublish" menu option for the 1st conversation and verify "Publish request" modal is opened', + async () => { + await dialHomePage.openHomePage(); + await dialHomePage.waitForPageLoaded(); + await organizationFolderConversations.expandFolder(publishedFolderName); + await organizationFolderConversations.openFolderEntityDropdownMenu( + publishedFolderName, + firstConversation.name, + ); + await conversationDropdownMenu.selectMenuOption(MenuOptions.unpublish); + await publishingRequestModalAssertion.assertElementState( + publishingRequestModal, + 'visible', + ); + await baseAssertion.assertElementText( + publishingRequestModal.getChangePublishToPath().path, + publishPath, + ); + + await conversationToPublishAssertion.assertEntityState( + { name: firstConversation.name }, + 'visible', + ); + await conversationToPublishAssertion.assertEntityColor( + { name: firstConversation.name }, + Colors.textError, + ); + await conversationToPublishAssertion.assertEntityCheckboxState( + { name: firstConversation.name }, + CheckboxState.checked, + ); + + await conversationToPublishAssertion.assertEntityVersion( + { name: firstConversation.name }, + ExpectedConstants.defaultAppVersion, + ); + await conversationToPublishAssertion.assertEntityVersionColor( + { name: firstConversation.name }, + Colors.textError, + ); + await conversationToPublishAssertion.assertTreeEntityIcon( + { name: firstConversation.name }, + expectedConversationIcon, + ); + + await conversationToPublishAssertion.assertEntityState( + { name: secondConversation.name }, + 'hidden', + ); + + await baseAssertion.assertElementText( + publishingRules.publishingPath, + publishedFolderName, + ); + await baseAssertion.assertElementState( + publishingRules.addRuleButton, + 'visible', + ); + await baseAssertion.assertElementsCount(publishingRules.allRules, 0); + }, + ); + + await dialTest.step('Set a valid request name and submit', async () => { + await publishingRequestModal.requestName.fillInInput( + firstConversationUnpublishingRequestName, + ); + firstUnpublishApiModels = + await publishingRequestModal.sendPublicationRequest(); + await publishingRequestModalAssertion.assertElementState( + publishingRequestModal, + 'hidden', + ); + }); + + await dialTest.step( + 'Create unpublish request for the 2nd folder conversation', + async () => { + await organizationFolderConversations.openFolderEntityDropdownMenu( + publishedFolderName, + secondConversation.name, + ); + await conversationDropdownMenu.selectMenuOption(MenuOptions.unpublish); + await publishingRequestModalAssertion.assertElementState( + publishingRequestModal, + 'visible', + ); + await publishingRequestModal.requestName.fillInInput( + secondConversationUnpublishingRequestName, + ); + await publishingRequestModal.sendPublicationRequest(); + }, + ); + + await dialTest.step( + 'Create one more unpublish request for the whole folder', + async () => { + const unpublishFolderRequestModel = publishRequestBuilder + .withConversationResource(firstConversation, PublishActions.DELETE) + .withConversationResource(secondConversation, PublishActions.DELETE) + .build(); + unpublishFolderResponse = + await publicationApiHelper.createUnpublishRequest( + unpublishFolderRequestModel, + ); + }, + ); + + await dialAdminTest.step( + 'Login as admin and verify conversation unpublishing request is displayed under "Approve required" section', + async () => { + await adminDialHomePage.openHomePage(); + await adminDialHomePage.waitForPageLoaded(); + await adminApproveRequiredConversationsAssertion.assertFolderState( + { name: firstConversationUnpublishingRequestName }, + 'visible', + ); + }, + ); + + await dialAdminTest.step( + 'Expand request folder and verify "Publication approval" modal is displayed', + async () => { + await adminApproveRequiredConversations.expandApproveRequiredFolder( + firstConversationUnpublishingRequestName, + ); + await adminApproveRequiredConversations.expandApproveRequiredFolder( + publishedFolderName, + { isHttpMethodTriggered: false }, + ); + await adminApproveRequiredConversationsAssertion.assertFolderEntityState( + { name: firstConversationUnpublishingRequestName }, + { name: firstConversation.name }, + 'visible', + ); + await adminApproveRequiredConversationsAssertion.assertFolderEntityColor( + { name: firstConversationUnpublishingRequestName }, + { name: firstConversation.name }, + Colors.textError, + ); + await adminApproveRequiredConversationsAssertion.assertFolderEntityState( + { name: firstConversationUnpublishingRequestName }, + { name: secondConversation.name }, + 'hidden', + ); + await adminPublishingApprovalModalAssertion.assertElementState( + adminPublishingApprovalModal, + 'visible', + ); + }, + ); + + await dialAdminTest.step( + 'Verify only 1t conversation is displayed on "Publication approval" modal', + async () => { + await adminPublishingApprovalModalAssertion.assertElementText( + adminPublishingApprovalModal.publishToPath, + publishPath, + ); + await adminPublishingApprovalModalAssertion.assertRequestCreationDate( + firstUnpublishApiModels.response, + ); + await adminPublishingApprovalModalAssertion.assertAvailabilityLabelState( + 'visible', + ); + await adminFolderToApproveAssertion.assertFolderEntityState( + { name: publishedFolderName }, + { name: firstConversation.name }, + 'visible', + ); + await adminFolderToApproveAssertion.assertFolderEntityColor( + { name: publishedFolderName }, + { name: firstConversation.name }, + Colors.textError, + ); + await adminFolderToApproveAssertion.assertFolderEntityVersion( + { name: publishedFolderName }, + { name: firstConversation.name }, + ExpectedConstants.defaultAppVersion, + ); + await adminFolderToApproveAssertion.assertFolderEntityVersionColor( + { name: publishedFolderName }, + { name: firstConversation.name }, + Colors.textError, + ); + await adminFolderToApproveAssertion.assertFolderEntityIcon( + { name: publishedFolderName }, + { name: firstConversation.name }, + expectedConversationIcon, + ); + await adminFolderToApproveAssertion.assertFolderEntityState( + { name: publishedFolderName }, + { name: secondConversation.name }, + 'hidden', + ); + }, + ); + + await dialAdminTest.step( + 'Admins reviews and approves the request and verify publication disappears from "Approve required", only one conversation left in "Organization" section', + async () => { + await adminPublishingApprovalModal.goToEntityReview(); + await adminPublicationReviewControl.backToPublicationRequest(); + await adminPublishingApprovalModal.approveRequest(); + await adminApproveRequiredConversationsAssertion.assertFolderState( + { name: firstConversationUnpublishingRequestName }, + 'hidden', + ); + await adminOrganizationFolderConversations.expandFolder( + publishedFolderName, + ); + await adminOrganizationFolderConversationAssertions.assertFolderEntityState( + { name: publishedFolderName }, + { name: firstConversation.name }, + 'hidden', + ); + await adminOrganizationFolderConversationAssertions.assertFolderEntityState( + { name: publishedFolderName }, + { name: secondConversation.name }, + 'visible', + ); + + await dialHomePage.reloadPage(); + await dialHomePage.waitForPageLoaded(); + await organizationFolderConversations.expandFolder(publishedFolderName); + await organizationFolderConversationAssertions.assertFolderEntityState( + { name: publishedFolderName }, + { name: firstConversation.name }, + 'hidden', + ); + await organizationFolderConversationAssertions.assertFolderEntityState( + { name: publishedFolderName }, + { name: secondConversation.name }, + 'visible', + ); + }, + ); + + await dialAdminTest.step( + 'Admin expands the folder unpublishing request and verify error message is displayed instead of "Go to a review" link', + async () => { + await adminApproveRequiredConversations.expandApproveRequiredFolder( + unpublishFolderResponse.name!, + ); + await adminPublishingApprovalModalAssertion.assertElementActionabilityState( + adminPublishingApprovalModal.approveButton, + 'disabled', + ); + await adminPublishingApprovalModalAssertion.assertElementState( + adminPublishingApprovalModal.goToReviewButton, + 'hidden', + ); + await adminPublishingApprovalModalAssertion.assertElementText( + adminPublishingApprovalModal.duplicatedUnpublishingError, + ExpectedConstants.duplicatedUnpublishingError(firstConversation.name), + ); + }, + ); + + await dialAdminTest.step( + 'Verify unpublished conversation is marked as grey under the request and on side panel', + async () => { + await adminFolderToApproveAssertion.assertFolderEntityState( + { name: publishedFolderName }, + { name: firstConversation.name }, + 'visible', + ); + await adminFolderToApproveAssertion.assertFolderEntityColor( + { name: publishedFolderName }, + { name: firstConversation.name }, + Colors.controlsBackgroundDisable, + ); + + await adminFolderToApproveAssertion.assertFolderEntityState( + { name: publishedFolderName }, + { name: firstConversation.name }, + 'visible', + ); + await adminFolderToApproveAssertion.assertFolderEntityColor( + { name: publishedFolderName }, + { name: firstConversation.name }, + Colors.controlsBackgroundDisable, + ); + }, + ); + + await dialAdminTest.step( + 'Admins reviews and approves the second request and verifies published folder disappears from "Organization" section', + async () => { + await adminApproveRequiredConversations.expandApproveRequiredFolder( + secondConversationUnpublishingRequestName, + ); + await adminPublishingApprovalModal.goToEntityReview(); + await adminPublicationReviewControl.backToPublicationRequest(); + await adminPublishingApprovalModal.approveRequest(); + await adminApproveRequiredConversationsAssertion.assertFolderState( + { name: secondConversationUnpublishingRequestName }, + 'hidden', + ); + //TODO: enable when the issue is fixed https://github.com/epam/ai-dial-chat/issues/2727 + // await adminOrganizationFolderConversationAssertions.assertFolderState( + // { name: publishedFolderName }, + // 'hidden', + // ); + + await dialHomePage.reloadPage(); + await dialHomePage.waitForPageLoaded(); + await organizationFolderConversationAssertions.assertFolderState( + { name: publishedFolderName }, + 'hidden', + ); + }, + ); + }, +); + +dialAdminTest( + 'Unpublish request for folder with more than one chat.\n' + + '2 Unpublish requests for folder structure.\n' + + 'Admin review 2 Unpublish requests for chat from folder', + async ({ + dialHomePage, + conversationData, + publishRequestBuilder, + publicationApiHelper, + adminPublicationApiHelper, + dataInjector, + organizationFolderConversations, + conversationDropdownMenu, + publishingRequestModal, + conversationToPublishAssertion, + publishingRequestModalAssertion, + adminDialHomePage, + adminApproveRequiredConversations, + adminPublishingApprovalModal, + baseAssertion, + adminPublicationReviewControl, + adminApproveRequiredConversationsAssertion, + adminOrganizationFolderConversationAssertions, + adminPublishingApprovalModalAssertion, + adminFolderToApproveAssertion, + organizationFolderConversationAssertions, + setTestIds, + }) => { + setTestIds('EPMRTC-3429', 'EPMRTC-3800', 'EPMRTC-3801'); + let firstConversation: Conversation; + let secondConversation: Conversation; + let conversationsFolder: FolderConversation; + let publishedFolderName: string; + const firstFolderUnpublishingRequestName = + GeneratorUtil.randomUnpublishRequestName(); + const secondFolderUnpublishingRequestName = + GeneratorUtil.randomUnpublishRequestName(); + let firstUnpublishApiModels: { + request: PublicationRequestModel; + response: Publication; + }; + let folderPublicationRequest: Publication; + let folderConversations: string[]; + + await dialTest.step( + 'Create and approve publishing of folder with 2 conversations inside', + async () => { + firstConversation = conversationData.prepareDefaultConversation(); + conversationData.resetData(); + secondConversation = conversationData.prepareDefaultConversation(); + conversationData.resetData(); + conversationsFolder = conversationData.prepareConversationsInFolder([ + firstConversation, + secondConversation, + ]); + await dataInjector.createConversations( + conversationsFolder.conversations, + conversationsFolder.folders, + ); + publishedFolderName = conversationsFolder.folders.name; + + const publishRequest = publishRequestBuilder + .withName(GeneratorUtil.randomPublicationRequestName()) + .withConversationResource(firstConversation, PublishActions.ADD) + .withConversationResource(secondConversation, PublishActions.ADD) + .build(); + folderPublicationRequest = + await publicationApiHelper.createPublishRequest(publishRequest); + await adminPublicationApiHelper.approveRequest( + folderPublicationRequest, + ); + folderConversations = [firstConversation.name, secondConversation.name]; + }, + ); + + await dialTest.step( + 'Select "Unpublish" menu option for the folder and verify "Publish request" modal is opened', + async () => { + await dialHomePage.openHomePage(); + await dialHomePage.waitForPageLoaded(); + await organizationFolderConversations.openFolderDropdownMenu( + publishedFolderName, + ); + await conversationDropdownMenu.selectMenuOption(MenuOptions.unpublish); + await publishingRequestModalAssertion.assertElementState( + publishingRequestModal, + 'visible', + ); + for (const conversation of folderConversations) { + await conversationToPublishAssertion.assertEntityState( + { name: conversation }, + 'visible', + ); + await conversationToPublishAssertion.assertEntityColor( + { name: conversation }, + Colors.textError, + ); + await conversationToPublishAssertion.assertEntityCheckboxState( + { name: conversation }, + CheckboxState.checked, + ); + await conversationToPublishAssertion.assertEntityVersion( + { name: conversation }, + ExpectedConstants.defaultAppVersion, + ); + await conversationToPublishAssertion.assertEntityVersionColor( + { name: conversation }, + Colors.textError, + ); + await conversationToPublishAssertion.assertTreeEntityIcon( + { name: conversation }, + expectedConversationIcon, + ); + } + }, + ); + + await dialTest.step('Set a valid request name and submit', async () => { + await publishingRequestModal.requestName.fillInInput( + firstFolderUnpublishingRequestName, + ); + firstUnpublishApiModels = + await publishingRequestModal.sendPublicationRequest(); + await publishingRequestModalAssertion.assertElementState( + publishingRequestModal, + 'hidden', + ); + }); + + await dialTest.step( + 'Create one more unpublish request for the folder conversation', + async () => { + await organizationFolderConversations.openFolderDropdownMenu( + publishedFolderName, + ); + await conversationDropdownMenu.selectMenuOption(MenuOptions.unpublish); + await publishingRequestModalAssertion.assertElementState( + publishingRequestModal, + 'visible', + ); + await publishingRequestModal.requestName.fillInInput( + secondFolderUnpublishingRequestName, + ); + await publishingRequestModal.sendPublicationRequest(); + }, + ); + + await dialAdminTest.step( + 'Login as admin and verify both folder unpublishing requests are displayed under "Approve required" section', + async () => { + await adminDialHomePage.openHomePage(); + await adminDialHomePage.waitForPageLoaded(); + await adminApproveRequiredConversationsAssertion.assertFolderState( + { name: firstFolderUnpublishingRequestName }, + 'visible', + ); + await adminApproveRequiredConversationsAssertion.assertFolderState( + { name: secondFolderUnpublishingRequestName }, + 'visible', + ); + }, + ); + + await dialAdminTest.step( + 'Expand the first request folder and verify "Publication approval" modal is displayed', + async () => { + await adminApproveRequiredConversations.expandApproveRequiredFolder( + firstFolderUnpublishingRequestName, + ); + await adminApproveRequiredConversations.expandApproveRequiredFolder( + publishedFolderName, + { isHttpMethodTriggered: false }, + ); + for (const conversation of folderConversations) { + await adminApproveRequiredConversationsAssertion.assertFolderEntityState( + { name: firstFolderUnpublishingRequestName }, + { name: conversation }, + 'visible', + ); + await adminApproveRequiredConversationsAssertion.assertFolderEntityColor( + { name: firstFolderUnpublishingRequestName }, + { name: conversation }, + Colors.textError, + ); + } + await adminPublishingApprovalModalAssertion.assertElementState( + adminPublishingApprovalModal, + 'visible', + ); + }, + ); + + await dialAdminTest.step( + 'Verify both conversations are displayed on "Publication approval" modal', + async () => { + await adminPublishingApprovalModalAssertion.assertElementText( + adminPublishingApprovalModal.publishToPath, + PublishPath.Organization, + ); + await adminPublishingApprovalModalAssertion.assertRequestCreationDate( + firstUnpublishApiModels.response, + ); + await adminPublishingApprovalModalAssertion.assertAvailabilityLabelState( + 'visible', + ); + for (const conversation of folderConversations) { + await adminFolderToApproveAssertion.assertFolderEntityState( + { name: publishedFolderName }, + { name: conversation }, + 'visible', + ); + await adminFolderToApproveAssertion.assertFolderEntityColor( + { name: publishedFolderName }, + { name: conversation }, + Colors.textError, + ); + await adminFolderToApproveAssertion.assertFolderEntityVersion( + { name: publishedFolderName }, + { name: conversation }, + ExpectedConstants.defaultAppVersion, + ); + await adminFolderToApproveAssertion.assertFolderEntityVersionColor( + { name: publishedFolderName }, + { name: conversation }, + Colors.textError, + ); + await adminFolderToApproveAssertion.assertFolderEntityIcon( + { name: publishedFolderName }, + { name: conversation }, + expectedConversationIcon, + ); + } + }, + ); + + await dialAdminTest.step( + 'Admins reviews the 1st request conversation, back to publication and checks the 1st unpublishing modal is opened', + async () => { + await adminPublishingApprovalModal.goToEntityReview(); + await adminPublicationReviewControl.backToPublicationRequest(); + await adminPublishingApprovalModalAssertion.assertElementText( + adminPublishingApprovalModal.publishName, + firstFolderUnpublishingRequestName, + ); + await adminPublishingApprovalModal.goToEntityReview(); + await adminPublicationReviewControl.backToPublicationRequest(); + }, + ); + + await dialAdminTest.step( + 'Admins approves the 1st request conversation and verifies folder disappears from "Organization" section ', + async () => { + await adminPublishingApprovalModal.approveRequest(); + await adminApproveRequiredConversationsAssertion.assertFolderState( + { name: firstFolderUnpublishingRequestName }, + 'hidden', + ); + await adminOrganizationFolderConversationAssertions.assertFolderState( + { name: publishedFolderName }, + 'hidden', + ); + + await dialHomePage.reloadPage(); + await dialHomePage.waitForPageLoaded(); + await organizationFolderConversationAssertions.assertFolderState( + { name: publishedFolderName }, + 'hidden', + ); + }, + ); + + await dialAdminTest.step( + 'Admin expands the 2nd folder unpublishing request and verifies error message is displayed instead of "Go to a review" link', + async () => { + await adminApproveRequiredConversations.expandApproveRequiredFolder( + secondFolderUnpublishingRequestName, + ); + await adminPublishingApprovalModalAssertion.assertElementActionabilityState( + adminPublishingApprovalModal.approveButton, + 'disabled', + ); + await adminPublishingApprovalModalAssertion.assertElementState( + adminPublishingApprovalModal.goToReviewButton, + 'hidden', + ); + await adminPublishingApprovalModalAssertion.assertElementText( + adminPublishingApprovalModal.duplicatedUnpublishingError, + ExpectedConstants.duplicatedUnpublishingError( + ...baseAssertion.sortStringsArray( + folderConversations, + (f) => f.toLowerCase(), + 'asc', + ), + ), + ); + }, + ); + + await dialAdminTest.step( + 'Verify unpublished conversations are marked as grey under the request and on side panel', + async () => { + for (const conversation of folderConversations) { + await adminFolderToApproveAssertion.assertFolderEntityState( + { name: publishedFolderName }, + { name: conversation }, + 'visible', + ); + await adminFolderToApproveAssertion.assertFolderEntityColor( + { name: publishedFolderName }, + { name: conversation }, + Colors.controlsBackgroundDisable, + ); + + await adminFolderToApproveAssertion.assertFolderEntityState( + { name: publishedFolderName }, + { name: conversation }, + 'visible', + ); + await adminFolderToApproveAssertion.assertFolderEntityColor( + { name: publishedFolderName }, + { name: conversation }, + Colors.controlsBackgroundDisable, + ); + } + }, + ); + }, +); + +dialAdminTest( + 'Unpublish folder from folder structure', + async ({ + dialHomePage, + conversationData, + publishRequestBuilder, + publicationApiHelper, + adminPublicationApiHelper, + dataInjector, + organizationFolderConversations, + conversationDropdownMenu, + publishingRequestModal, + publishingRules, + folderToPublishAssertion, + publishingRequestModalAssertion, + adminDialHomePage, + adminApproveRequiredConversations, + adminPublishingApprovalModal, + adminPublicationReviewControl, + adminApproveRequiredConversationsAssertion, + adminOrganizationFolderConversations, + adminOrganizationFolderConversationAssertions, + adminPublishingApprovalModalAssertion, + adminFolderToApproveAssertion, + organizationFolderConversationAssertions, + setTestIds, + }) => { + setTestIds('EPMRTC-3808'); + let nestedFolders: FolderInterface[]; + let nestedConversations: Conversation[]; + const nestedFolderLevel = 2; + const folderUnpublishingRequestName = + GeneratorUtil.randomUnpublishRequestName(); + let unpublishApiModels: { + request: PublicationRequestModel; + response: Publication; + }; + let publishPath: string; + let rootFolderName: string; + let rootFolderConversationName: string; + let innerFolderName: string; + let innerFolderConversationName: string; + + await dialTest.step( + 'Create and approve publishing of 2 nested folders with 2 conversations inside', + async () => { + nestedFolders = conversationData.prepareNestedFolder(nestedFolderLevel); + nestedConversations = + conversationData.prepareConversationsForNestedFolders(nestedFolders); + await dataInjector.createConversations( + nestedConversations, + ...nestedFolders, + ); + + const publishRequest = publishRequestBuilder + .withName(GeneratorUtil.randomPublicationRequestName()) + .withConversationResource(nestedConversations[0], PublishActions.ADD) + .withConversationResource(nestedConversations[1], PublishActions.ADD) + .build(); + const folderPublicationRequest = + await publicationApiHelper.createPublishRequest(publishRequest); + await adminPublicationApiHelper.approveRequest( + folderPublicationRequest, + ); + + rootFolderName = nestedFolders[0].name; + rootFolderConversationName = nestedConversations[0].name; + innerFolderName = nestedFolders[1].name; + innerFolderConversationName = nestedConversations[1].name; + publishPath = `${PublishPath.Organization}/${rootFolderName}`; + folderConversationToUnpublish = nestedConversations[0]; + }, + ); + + await dialTest.step( + 'Select "Unpublish" menu option for the nested folder and verify "Publish request" modal is opened', + async () => { + await dialHomePage.openHomePage(); + await dialHomePage.waitForPageLoaded(); + await organizationFolderConversations.expandFolder(rootFolderName); + await organizationFolderConversations.openFolderDropdownMenu( + innerFolderName, + ); + await conversationDropdownMenu.selectMenuOption(MenuOptions.unpublish); + await publishingRequestModalAssertion.assertElementState( + publishingRequestModal, + 'visible', + ); + await folderToPublishAssertion.assertFolderState( + { name: innerFolderName }, + 'visible', + ); + await folderToPublishAssertion.assertFolderEntityState( + { name: innerFolderName }, + { name: innerFolderConversationName }, + 'visible', + ); + await folderToPublishAssertion.assertFolderState( + { name: rootFolderName }, + 'hidden', + ); + await folderToPublishAssertion.assertFolderEntityColor( + { name: innerFolderName }, + { name: innerFolderConversationName }, + Colors.textError, + ); + await folderToPublishAssertion.assertFolderCheckboxState( + { name: innerFolderName }, + CheckboxState.checked, + ); + await folderToPublishAssertion.assertFolderEntityCheckboxState( + { name: innerFolderName }, + { name: innerFolderConversationName }, + CheckboxState.checked, + ); + await folderToPublishAssertion.assertFolderEntityVersion( + { name: innerFolderName }, + { name: innerFolderConversationName }, + ExpectedConstants.defaultAppVersion, + ); + await folderToPublishAssertion.assertFolderEntityVersionColor( + { name: innerFolderName }, + { name: innerFolderConversationName }, + Colors.textError, + ); + await folderToPublishAssertion.assertFolderEntityIcon( + { name: innerFolderName }, + { name: innerFolderConversationName }, + expectedConversationIcon, + ); + await publishingRequestModalAssertion.assertElementText( + publishingRequestModal.getChangePublishToPath().path, + publishPath, + ); + await publishingRequestModalAssertion.assertElementText( + publishingRules.publishingPath, + rootFolderName, + ); + await publishingRequestModalAssertion.assertElementState( + publishingRules.addRuleButton, + 'visible', + ); + await publishingRequestModalAssertion.assertElementsCount( + publishingRules.allRules, + 0, + ); + }, + ); + + await dialTest.step('Set a valid request name and submit', async () => { + await publishingRequestModal.requestName.fillInInput( + folderUnpublishingRequestName, + ); + unpublishApiModels = + await publishingRequestModal.sendPublicationRequest(); + await publishingRequestModalAssertion.assertElementState( + publishingRequestModal, + 'hidden', + ); + }); + + await dialAdminTest.step( + 'Login as admin and verify inner folder unpublishing request is displayed under "Approve required" section', + async () => { + await adminDialHomePage.openHomePage(); + await adminDialHomePage.waitForPageLoaded(); + await adminApproveRequiredConversationsAssertion.assertFolderState( + { name: folderUnpublishingRequestName }, + 'visible', + ); + }, + ); + + await dialAdminTest.step( + 'Expand the request folder and verify "Publication approval" modal is displayed', + async () => { + await adminApproveRequiredConversations.expandApproveRequiredFolder( + folderUnpublishingRequestName, + ); + await adminApproveRequiredConversations.expandApproveRequiredFolder( + rootFolderName, + { isHttpMethodTriggered: false }, + ); + await adminApproveRequiredConversations.expandApproveRequiredFolder( + innerFolderName, + { isHttpMethodTriggered: false }, + ); + await adminApproveRequiredConversationsAssertion.assertFolderEntityState( + { name: innerFolderName }, + { name: innerFolderConversationName }, + 'visible', + ); + await adminApproveRequiredConversationsAssertion.assertFolderEntityColor( + { name: innerFolderName }, + { name: innerFolderConversationName }, + Colors.textError, + ); + await adminApproveRequiredConversationsAssertion.assertFolderState( + { name: rootFolderName }, + 'visible', + ); + await adminApproveRequiredConversationsAssertion.assertFolderEntityState( + { name: rootFolderName }, + { name: rootFolderConversationName }, + 'hidden', + ); + await adminPublishingApprovalModalAssertion.assertElementState( + adminPublishingApprovalModal, + 'visible', + ); + }, + ); + + await dialAdminTest.step( + 'Verify only inner folder with content is displayed on "Publication approval" modal', + async () => { + await adminPublishingApprovalModalAssertion.assertElementText( + adminPublishingApprovalModal.publishToPath, + publishPath, + ); + await adminPublishingApprovalModalAssertion.assertRequestCreationDate( + unpublishApiModels.response, + ); + await adminPublishingApprovalModalAssertion.assertAvailabilityLabelState( + 'visible', + ); + await adminFolderToApproveAssertion.assertFolderState( + { name: innerFolderName }, + 'visible', + ); + await adminFolderToApproveAssertion.assertFolderEntityState( + { name: innerFolderName }, + { name: innerFolderConversationName }, + 'visible', + ); + await adminFolderToApproveAssertion.assertFolderEntityColor( + { name: innerFolderName }, + { name: innerFolderConversationName }, + Colors.textError, + ); + await adminFolderToApproveAssertion.assertFolderEntityVersion( + { name: innerFolderName }, + { name: innerFolderConversationName }, + ExpectedConstants.defaultAppVersion, + ); + await adminFolderToApproveAssertion.assertFolderEntityVersionColor( + { name: innerFolderName }, + { name: innerFolderConversationName }, + Colors.textError, + ); + await adminFolderToApproveAssertion.assertFolderEntityIcon( + { name: innerFolderName }, + { name: innerFolderConversationName }, + expectedConversationIcon, + ); + await adminFolderToApproveAssertion.assertFolderState( + { name: rootFolderName }, + 'visible', + ); + await adminFolderToApproveAssertion.assertFolderEntityState( + { name: rootFolderName }, + { name: rootFolderConversationName }, + 'hidden', + ); + }, + ); + + await dialAdminTest.step( + 'Admins reviews and approves the request conversation and checks only root folder with content is displayed in the "Organization" section', + async () => { + await adminPublishingApprovalModal.goToEntityReview(); + await adminPublicationReviewControl.backToPublicationRequest(); + await adminPublishingApprovalModal.approveRequest(); + await adminApproveRequiredConversationsAssertion.assertFolderState( + { name: folderUnpublishingRequestName }, + 'hidden', + ); + await adminOrganizationFolderConversationAssertions.assertFolderState( + { name: rootFolderName }, + 'visible', + ); + await adminOrganizationFolderConversations.expandFolder(rootFolderName); + await adminOrganizationFolderConversationAssertions.assertFolderState( + { name: innerFolderName }, + 'hidden', + ); + await adminOrganizationFolderConversationAssertions.assertFolderEntityState( + { name: rootFolderName }, + { name: rootFolderConversationName }, + 'visible', + ); + await adminOrganizationFolderConversationAssertions.assertFolderEntityState( + { name: rootFolderName }, + { name: innerFolderConversationName }, + 'hidden', + ); + }, + ); + + await dialAdminTest.step( + 'Main user refreshes the page and verifies only root folder remains in the "Organization" section', + async () => { + await dialHomePage.reloadPage(); + await dialHomePage.waitForPageLoaded(); + await organizationFolderConversationAssertions.assertFolderState( + { name: rootFolderName }, + 'visible', + ); + await organizationFolderConversations.expandFolder(rootFolderName); + await organizationFolderConversationAssertions.assertFolderState( + { name: innerFolderName }, + 'hidden', + ); + await organizationFolderConversationAssertions.assertFolderEntityState( + { name: rootFolderName }, + { name: rootFolderConversationName }, + 'visible', + ); + await organizationFolderConversationAssertions.assertFolderEntityState( + { name: rootFolderName }, + { name: innerFolderConversationName }, + 'hidden', + ); + }, + ); + }, +); + +dialTest.afterAll( + async ({ + publicationApiHelper, + adminPublicationApiHelper, + publishRequestBuilder, + }) => { + const publishRequest = publishRequestBuilder + .withConversationResource( + folderConversationToUnpublish, + PublishActions.DELETE, + ) + .build(); + const folderPublicationRequest = + await publicationApiHelper.createUnpublishRequest(publishRequest); + await adminPublicationApiHelper.approveRequest(folderPublicationRequest); + }, +); diff --git a/apps/chat-e2e/src/tests/workWithModels.test.ts b/apps/chat-e2e/src/tests/workWithModels.test.ts index cdb6d96c2f..e213899152 100644 --- a/apps/chat-e2e/src/tests/workWithModels.test.ts +++ b/apps/chat-e2e/src/tests/workWithModels.test.ts @@ -21,11 +21,9 @@ const requestTerm = 'qwer'; const request = 'write cinderella story'; const expectedResponse = 'The sky is blue.'; const promptContent = `Type: "${expectedResponse}" if user types ${requestTerm}`; -let defaultModel: DialAIEntityModel; let simpleRequestModel: DialAIEntityModel | undefined; dialTest.beforeAll(async () => { - defaultModel = ModelsUtil.getDefaultModel()!; simpleRequestModel = ModelsUtil.getModelForSimpleRequest(); }); @@ -47,10 +45,8 @@ dialTest( 'write down 100 adjectives', ]; await dialTest.step('Prepare model conversation', async () => { - conversation = conversationData.prepareModelConversationBasedOnRequests( - defaultModel, - userRequests, - ); + conversation = + conversationData.prepareModelConversationBasedOnRequests(userRequests); await dataInjector.createConversations([conversation]); }); @@ -192,10 +188,8 @@ dialTest( let conversation: Conversation; const userRequests = ['1+2=', '2+3=', '3+4=']; await dialTest.step('Prepare conversation with 3 requests', async () => { - conversation = conversationData.prepareModelConversationBasedOnRequests( - defaultModel, - userRequests, - ); + conversation = + conversationData.prepareModelConversationBasedOnRequests(userRequests); await dataInjector.createConversations([conversation]); }); @@ -283,10 +277,7 @@ dialTest( }) => { setTestIds('EPMRTC-488', 'EPMRTC-489'); const conversation = - conversationData.prepareModelConversationBasedOnRequests( - defaultModel, - userRequests, - ); + conversationData.prepareModelConversationBasedOnRequests(userRequests); await dialTest.step('Prepare conversation with 3 requests', async () => { await dataInjector.createConversations([conversation]); }); diff --git a/apps/chat-e2e/src/ui/domData/colors.ts b/apps/chat-e2e/src/ui/domData/colors.ts index 1efcb0d9d3..201ebc2617 100644 --- a/apps/chat-e2e/src/ui/domData/colors.ts +++ b/apps/chat-e2e/src/ui/domData/colors.ts @@ -40,3 +40,7 @@ export function removeAlpha(color: string): string { export const ColorsWithoutAlpha = Object.fromEntries( Object.entries(Colors).map(([key, value]) => [key, removeAlpha(value)]), ) as typeof Colors; + +export enum ThemeColorAttributes { + textAccentTertiary = 'text-accent-tertiary', +} diff --git a/apps/chat-e2e/src/ui/selectors/chatSelectors.ts b/apps/chat-e2e/src/ui/selectors/chatSelectors.ts index e3e479cc69..2dd0364e64 100644 --- a/apps/chat-e2e/src/ui/selectors/chatSelectors.ts +++ b/apps/chat-e2e/src/ui/selectors/chatSelectors.ts @@ -155,3 +155,18 @@ export const PublicationReviewControls = { nextButton: '[data-qa="next-chat-review-button"]', backToPublication: '[data-qa="back-to-publication"]', }; + +export const RenameConversationModalSelectors = { + modal: '[data-qa="rename-conversation-modal"]', + saveButton: '[data-qa="save"]', + cancelButton: '[data-qa="cancel"]', + title: '[data-qa="title"]', +}; + +export const PublishingRulesSelectors = { + rulesContainer: '[data-qa="rules-container"]', + path: '[data-qa="published-path"]', + rulesList: '[data-qa="rules-list"]', + rule: '[data-qa="rule"]', + addRuleButton: '[data-qa="add-rule"]', +}; diff --git a/apps/chat-e2e/src/ui/selectors/dialogSelectors.ts b/apps/chat-e2e/src/ui/selectors/dialogSelectors.ts index 29247bfe5a..22aed06b6f 100644 --- a/apps/chat-e2e/src/ui/selectors/dialogSelectors.ts +++ b/apps/chat-e2e/src/ui/selectors/dialogSelectors.ts @@ -197,3 +197,21 @@ export const TalkToAgentDialogSelectors = { searchAgent: '[data-qa="search-agents"]', goToMyWorkspaceButton: '[data-qa="go-to-my-workspace"]', }; + +export const MessageTemplateModalSelectors = { + messageTemplateModal: '[data-qa="message-templates-dialog"]', + modalTitle: '[data-qa="modal-entity-name"]', + description: '[data-qa="description"]', + originalMessageLabel: '[data-qa="original-message-label"]', + setTemplateTab: '[data-qa="set-template-tab"]', + previewTab: '[data-qa="preview-tab"]', + originalMessageContent: '[data-qa="original-message-content"]', + templateRow: '[data-qa="template-row"]', + templateRowContent: '[data-qa="template-content"]', + templateRowValue: '[data-qa="template-value"]', + deleteRow: '[name="delete-row"]', + saveButton: '[data-qa="save-button"]', + templatePreview: '[data-qa="result-message-template"]', + showMoreButton: '[data-qa="show-more"]', + showLessButton: '[data-qa="show-less"]', +}; diff --git a/apps/chat-e2e/src/ui/webElements/attachFilesModal.ts b/apps/chat-e2e/src/ui/webElements/attachFilesModal.ts index 3ebe32ce87..d0472b2335 100644 --- a/apps/chat-e2e/src/ui/webElements/attachFilesModal.ts +++ b/apps/chat-e2e/src/ui/webElements/attachFilesModal.ts @@ -163,6 +163,11 @@ export class AttachFilesModal extends BaseElement { SelectFolderModalSelectors.newFolderButton, ); + public getFilesSection = (sectionElement: BaseElement) => + sectionElement + .getChildElementBySelector(AttachFilesModalSelectors.fileSection) + .getElementLocator(); + public closeButton = this.getChildElementBySelector(IconSelectors.cancelIcon); public async checkAttachedFile( @@ -209,12 +214,4 @@ export class AttachFilesModal extends BaseElement { ErrorLabelSelectors.errorText, ).getElementContent(); } - - public async isSectionExpanded( - sectionElement: BaseElement, - ): Promise { - return sectionElement - .getChildElementBySelector(AttachFilesModalSelectors.fileSection) - .isVisible(); - } } diff --git a/apps/chat-e2e/src/ui/webElements/chatMessages.ts b/apps/chat-e2e/src/ui/webElements/chatMessages.ts index 9a14c0c308..debb781091 100644 --- a/apps/chat-e2e/src/ui/webElements/chatMessages.ts +++ b/apps/chat-e2e/src/ui/webElements/chatMessages.ts @@ -521,10 +521,15 @@ export class ChatMessages extends BaseElement { await editIcon.click(); } - public async waitForEditMessageIcon(message: string | number) { + public async hoverOverMessage(message: string | number) { const chatMessage = this.getChatMessage(message); await chatMessage.scrollIntoViewIfNeeded(); await chatMessage.hover(); + return chatMessage; + } + + public async waitForEditMessageIcon(message: string | number) { + const chatMessage = await this.hoverOverMessage(message); const editIcon = this.messageEditIcon(chatMessage); await editIcon.waitFor(); return editIcon; @@ -570,4 +575,9 @@ export class ChatMessages extends BaseElement { await this.waitForResponseReceived(); } } + + public async openMessageTemplateModal(message: string | number) { + const chatMessage = await this.hoverOverMessage(message); + await this.setMessageTemplateIcon(chatMessage).click(); + } } diff --git a/apps/chat-e2e/src/ui/webElements/entityTree/folders.ts b/apps/chat-e2e/src/ui/webElements/entityTree/folders.ts index 1939da79c6..a65fbca58c 100644 --- a/apps/chat-e2e/src/ui/webElements/entityTree/folders.ts +++ b/apps/chat-e2e/src/ui/webElements/entityTree/folders.ts @@ -314,6 +314,37 @@ export class Folders extends BaseElement { .nth(entityIndex ? entityIndex - 1 : 0); } + public getFolderEntityNameElement( + folderName: string, + entityName: string, + folderIndex?: number, + entityIndex?: number, + ) { + return this.createElementFromLocator( + this.getFolderEntity( + folderName, + entityName, + folderIndex, + entityIndex, + ).locator(EntitySelectors.entityName), + ); + } + + getFolderEntityIcon( + folderName: string, + entityName: string, + folderIndex?: number, + entityIndex?: number, + ) { + const folderEntity = this.getFolderEntity( + folderName, + entityName, + folderIndex, + entityIndex, + ); + return this.getElementIcon(folderEntity); + } + public folderEntityDotsMenu = (folderName: string, entityName: string) => { return this.getFolderEntity(folderName, entityName).locator( MenuSelectors.dotsMenu, @@ -400,13 +431,11 @@ export class Folders extends BaseElement { folderIndex?: number, entityIndex?: number, ) { - const folderEntityColor = await this.createElementFromLocator( - this.getFolderEntity( - folderName, - entityName, - folderIndex, - entityIndex, - ).locator(EntitySelectors.entityName), + const folderEntityColor = await this.getFolderEntityNameElement( + folderName, + entityName, + folderIndex, + entityIndex, ).getComputedStyleProperty(Styles.color); return folderEntityColor[0]; } diff --git a/apps/chat-e2e/src/ui/webElements/entityTree/publishFolder.ts b/apps/chat-e2e/src/ui/webElements/entityTree/publishFolder.ts index a2787c57d6..18de241617 100644 --- a/apps/chat-e2e/src/ui/webElements/entityTree/publishFolder.ts +++ b/apps/chat-e2e/src/ui/webElements/entityTree/publishFolder.ts @@ -13,8 +13,24 @@ export class PublishFolder extends Folders { entityName, folderIndex, entityIndex, - ) - .locator('~*') - .locator(PublishEntitySelectors.version); + ).locator( + `~*${PublishEntitySelectors.version}, ~* > ${PublishEntitySelectors.version}`, + ); + } + + public getFolderEntityVersionElement( + folderName: string, + entityName: string, + folderIndex?: number, + entityIndex?: number, + ) { + return this.createElementFromLocator( + this.getFolderEntityVersion( + folderName, + entityName, + folderIndex, + entityIndex, + ), + ); } } diff --git a/apps/chat-e2e/src/ui/webElements/entityTree/sidebar/baseSideBarConversationTree.ts b/apps/chat-e2e/src/ui/webElements/entityTree/sidebar/baseSideBarConversationTree.ts index 9dd0606a57..1c2c81fc2b 100644 --- a/apps/chat-e2e/src/ui/webElements/entityTree/sidebar/baseSideBarConversationTree.ts +++ b/apps/chat-e2e/src/ui/webElements/entityTree/sidebar/baseSideBarConversationTree.ts @@ -1,5 +1,4 @@ import { isApiStorageType } from '@/src/hooks/global-setup'; -import { keys } from '@/src/ui/keyboard'; import { ChatBarSelectors } from '@/src/ui/selectors'; import { SideBarEntitiesTree } from '@/src/ui/webElements/entityTree/sidebar/sideBarEntitiesTree'; @@ -25,32 +24,4 @@ export class BaseSideBarConversationTree extends SideBarEntitiesTree { ChatBarSelectors.selectedEntity, ); } - - public async editConversationNameWithTick( - newName: string, - { isHttpMethodTriggered = true }: { isHttpMethodTriggered?: boolean } = {}, - ) { - await this.openEditEntityNameMode(newName); - const editInputActions = this.getEditInputActions(); - if (isApiStorageType && isHttpMethodTriggered) { - const respPromise = this.page.waitForResponse( - (resp) => resp.request().method() === 'DELETE', - ); - await editInputActions.clickTickButton(); - return respPromise; - } - await editInputActions.clickTickButton(); - } - - public async editConversationNameWithEnter(newName: string) { - await this.openEditEntityNameMode(newName); - if (isApiStorageType) { - const respPromise = this.page.waitForResponse( - (resp) => resp.request().method() === 'DELETE', - ); - await this.page.keyboard.press(keys.enter); - return respPromise; - } - await this.page.keyboard.press(keys.enter); - } } diff --git a/apps/chat-e2e/src/ui/webElements/index.ts b/apps/chat-e2e/src/ui/webElements/index.ts index 6f030e7650..d83f30d15e 100644 --- a/apps/chat-e2e/src/ui/webElements/index.ts +++ b/apps/chat-e2e/src/ui/webElements/index.ts @@ -45,6 +45,7 @@ export * from './publishingRequestModal'; export * from './changePath'; export * from './publishingApprovalModal'; export * from './publicationReviewControl'; +export * from './publishingRules'; export * from './baseLayoutContainer'; export * from './marketplace/agentDetailsModal'; export * from './marketplace/marketplaceAgents'; @@ -54,3 +55,4 @@ export * from './marketplace/marketplaceFilter'; export * from './marketplace/marketplaceHeader'; export * from './marketplace/marketplaceSidebar'; export * from './talkToAgentDialog'; +export * from './messageTemplateModal'; diff --git a/apps/chat-e2e/src/ui/webElements/marketplace/marketplaceAgents.ts b/apps/chat-e2e/src/ui/webElements/marketplace/marketplaceAgents.ts index 708018c06c..c87abd74a7 100644 --- a/apps/chat-e2e/src/ui/webElements/marketplace/marketplaceAgents.ts +++ b/apps/chat-e2e/src/ui/webElements/marketplace/marketplaceAgents.ts @@ -38,9 +38,7 @@ export class MarketplaceAgents extends BaseElement { public getAgent = (entity: DialAIEntityModel | string) => { let agent; if (typeof entity === 'string') { - agent = agent = this.rootLocator - .filter({ has: this.agentName(entity) }) - .first(); + agent = this.rootLocator.filter({ has: this.agentName(entity) }).first(); } else { //if agent has version in the config if (entity.version) { diff --git a/apps/chat-e2e/src/ui/webElements/messageTemplateModal.ts b/apps/chat-e2e/src/ui/webElements/messageTemplateModal.ts new file mode 100644 index 0000000000..9ac0c6b067 --- /dev/null +++ b/apps/chat-e2e/src/ui/webElements/messageTemplateModal.ts @@ -0,0 +1,98 @@ +import { Tags } from '@/src/ui/domData'; +import { + ErrorLabelSelectors, + IconSelectors, + MessageTemplateModalSelectors, +} from '@/src/ui/selectors'; +import { BaseElement } from '@/src/ui/webElements/baseElement'; +import { Locator, Page } from '@playwright/test'; + +export class MessageTemplateModal extends BaseElement { + constructor(page: Page) { + super(page, MessageTemplateModalSelectors.messageTemplateModal); + } + + public title = this.getChildElementBySelector( + MessageTemplateModalSelectors.modalTitle, + ); + public description = this.getChildElementBySelector( + MessageTemplateModalSelectors.description, + ); + public originalMessageLabel = this.getChildElementBySelector( + MessageTemplateModalSelectors.originalMessageLabel, + ); + public setTemplateTab = this.getChildElementBySelector( + MessageTemplateModalSelectors.setTemplateTab, + ); + public previewTab = this.getChildElementBySelector( + MessageTemplateModalSelectors.previewTab, + ); + public originalMessageContent = this.getChildElementBySelector( + MessageTemplateModalSelectors.originalMessageContent, + ); + public templateRows = this.getChildElementBySelector( + MessageTemplateModalSelectors.templateRow, + ); + public templatePreview = this.getChildElementBySelector( + MessageTemplateModalSelectors.templatePreview, + ); + public templatePreviewVar = (variable: string) => + this.templatePreview + .getChildElementBySelector(Tags.span) + .getElementLocatorByText(variable); + public saveTemplate = this.getChildElementBySelector( + MessageTemplateModalSelectors.saveButton, + ); + public showMoreButton = this.getChildElementBySelector( + MessageTemplateModalSelectors.showMoreButton, + ); + public showLessButton = this.getChildElementBySelector( + MessageTemplateModalSelectors.showLessButton, + ); + cancelButton = this.getChildElementBySelector(IconSelectors.cancelIcon); + public getFieldBottomMessage = (field: Locator) => + field.locator(`~${ErrorLabelSelectors.fieldError}`); + + public getTemplateRowContent = (rowContent: string | number) => { + if (typeof rowContent === 'string') { + return new BaseElement( + this.page, + `${MessageTemplateModalSelectors.templateRowContent}:text-is('${rowContent}')`, + ).getElementLocator(); + } else { + return this.getChildElementBySelector( + MessageTemplateModalSelectors.templateRowContent, + ).getNthElement(rowContent); + } + }; + + public getTemplateRow = (rowContent: string | number) => { + if (typeof rowContent === 'string') { + return this.rootLocator + .filter({ has: this.getTemplateRowContent(rowContent) }) + .first(); + } else { + return this.templateRows.getNthElement(rowContent); + } + }; + + public getTemplateRowValue = (rowContent: string | number) => { + return this.getTemplateRow(rowContent).locator( + MessageTemplateModalSelectors.templateRowValue, + ); + }; + + public getTemplateRowDeleteButton = (rowContent: string | number) => { + return this.getTemplateRow(rowContent).locator( + MessageTemplateModalSelectors.deleteRow, + ); + }; + + public async saveChanges() { + const responsePromise = this.page.waitForResponse( + (r) => r.request().method() === 'PUT', + ); + await this.saveTemplate.click(); + await responsePromise; + } +} diff --git a/apps/chat-e2e/src/ui/webElements/publishingRequestModal.ts b/apps/chat-e2e/src/ui/webElements/publishingRequestModal.ts index 60d0602732..ce44542709 100644 --- a/apps/chat-e2e/src/ui/webElements/publishingRequestModal.ts +++ b/apps/chat-e2e/src/ui/webElements/publishingRequestModal.ts @@ -14,6 +14,7 @@ import { FolderPromptsToPublish, PromptsToPublishTree, } from '@/src/ui/webElements/entityTree'; +import { PublishingRules } from '@/src/ui/webElements/publishingRules'; import { Page } from '@playwright/test'; export class PublishingRequestModal extends BaseElement { @@ -40,6 +41,7 @@ export class PublishingRequestModal extends BaseElement { private applicationsToPublishTree!: ApplicationsToPublishTree; //change publish path element private changePublishToPath!: ChangePath; + private publishingRules!: PublishingRules; getConversationsToPublishTree(): ConversationsToPublishTree { if (!this.conversationsToPublishTree) { @@ -108,6 +110,13 @@ export class PublishingRequestModal extends BaseElement { return this.changePublishToPath; } + getPublishingRules(): PublishingRules { + if (!this.publishingRules) { + this.publishingRules = new PublishingRules(this.page, this.rootLocator); + } + return this.publishingRules; + } + public requestName = this.getChildElementBySelector( PublishingModalSelectors.requestName, ); diff --git a/apps/chat-e2e/src/ui/webElements/publishingRules.ts b/apps/chat-e2e/src/ui/webElements/publishingRules.ts new file mode 100644 index 0000000000..b6b3e00028 --- /dev/null +++ b/apps/chat-e2e/src/ui/webElements/publishingRules.ts @@ -0,0 +1,22 @@ +import { PublishingRulesSelectors } from '@/src/ui/selectors'; +import { BaseElement } from '@/src/ui/webElements/baseElement'; +import { Locator, Page } from '@playwright/test'; + +export class PublishingRules extends BaseElement { + constructor(page: Page, parentLocator: Locator) { + super(page, PublishingRulesSelectors.rulesContainer, parentLocator); + } + + public publishingPath = this.getChildElementBySelector( + PublishingRulesSelectors.path, + ); + public rulesList = this.getChildElementBySelector( + PublishingRulesSelectors.rulesList, + ); + public addRuleButton = this.rulesList.getChildElementBySelector( + PublishingRulesSelectors.addRuleButton, + ); + public allRules = this.rulesList.getChildElementBySelector( + PublishingRulesSelectors.rule, + ); +} diff --git a/apps/chat-e2e/src/ui/webElements/renameConversationModal.ts b/apps/chat-e2e/src/ui/webElements/renameConversationModal.ts new file mode 100644 index 0000000000..af78f85929 --- /dev/null +++ b/apps/chat-e2e/src/ui/webElements/renameConversationModal.ts @@ -0,0 +1,76 @@ +import { BaseElement } from './baseElement'; + +import { isApiStorageType } from '@/src/hooks/global-setup'; +import { Attributes, Tags } from '@/src/ui/domData'; +import { keys } from '@/src/ui/keyboard'; +import { RenameConversationModalSelectors } from '@/src/ui/selectors'; +import { Page } from '@playwright/test'; + +export class RenameConversationModal extends BaseElement { + constructor(page: Page) { + super(page, RenameConversationModalSelectors.modal); + } + + public cancelButton = this.getChildElementBySelector( + RenameConversationModalSelectors.cancelButton, + ); + public saveButton = this.getChildElementBySelector( + RenameConversationModalSelectors.saveButton, + ); + public nameInput = this.getChildElementBySelector(Tags.input); + public title = this.getChildElementBySelector( + RenameConversationModalSelectors.title, + ); + + private async editConversationName( + newName: string, + confirmationAction: () => Promise, + { isHttpMethodTriggered = true }: { isHttpMethodTriggered?: boolean } = {}, + ) { + await this.nameInput.fillInInput(newName); + if (isApiStorageType && isHttpMethodTriggered) { + const respPromise = this.page.waitForResponse( + (resp) => resp.request().method() === 'DELETE', + ); + await confirmationAction(); + await respPromise; + } else { + await confirmationAction(); + } + } + + async editConversationNameWithSaveButton( + newName: string, + options?: { isHttpMethodTriggered?: boolean }, + ) { + await this.editConversationName( + newName, + () => this.saveButton.click(), + options, + ); + } + + async editConversationNameWithEnter( + newName: string, + options?: { isHttpMethodTriggered?: boolean }, + ) { + await this.editConversationName( + newName, + () => this.page.keyboard.press(keys.enter), + options, + ); + } + + async close() { + await this.cancelButton.click(); + } + + async getInputValue() { + return this.nameInput.getAttribute(Attributes.value); + } + + public async editInputValue(newValue: string) { + await this.page.keyboard.press(keys.ctrlPlusA); + await this.nameInput.fillInInput(newValue); + } +} diff --git a/apps/chat-e2e/src/ui/webElements/selectFolderModal.ts b/apps/chat-e2e/src/ui/webElements/selectFolderModal.ts index 129f3675e8..b2cb5cd54e 100644 --- a/apps/chat-e2e/src/ui/webElements/selectFolderModal.ts +++ b/apps/chat-e2e/src/ui/webElements/selectFolderModal.ts @@ -54,9 +54,13 @@ export class SelectFolderModal extends BaseElement { public async selectFolder( folderName: string, + index?: number, { triggeredApiHost }: { triggeredApiHost?: string } = {}, ) { - const folderToSelect = this.getSelectFolders().getFolderName(folderName); + const folderToSelect = this.getSelectFolders().getFolderName( + folderName, + index, + ); let respPremise: Promise; if (triggeredApiHost) { respPremise = this.page.waitForResponse((r) => diff --git a/apps/chat/src/components/Chat/ChangePathDialog.tsx b/apps/chat/src/components/Chat/ChangePathDialog.tsx index ae49d43211..5497466bb8 100644 --- a/apps/chat/src/components/Chat/ChangePathDialog.tsx +++ b/apps/chat/src/components/Chat/ChangePathDialog.tsx @@ -2,6 +2,7 @@ import { ChangeEvent, useCallback, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'next-i18next'; +import { updateEntitiesFoldersAndIds } from '@/src/utils/app/common'; import { constructPath } from '@/src/utils/app/file'; import { getChildAndCurrentFoldersIdsById, @@ -9,6 +10,7 @@ import { getNextDefaultName, getPathToFolderById, sortByName, + updateMovedFolderId, validateFolderRenaming, } from '@/src/utils/app/folders'; @@ -160,10 +162,17 @@ export const ChangePathDialog = ({ setErrorMessage(t(error) as string); return; } + const { updatedOpenedFoldersIds } = updateEntitiesFoldersAndIds( + [], + [], + (id) => updateMovedFolderId(folderId, newFolderId, id), + openedFoldersIds, + ); dispatch(actions.renameTemporaryFolder({ folderId, name: newName })); + setOpenedFoldersIds(updatedOpenedFoldersIds); }, - [actions, dispatch, folders, t], + [actions, dispatch, folders, t, openedFoldersIds, setOpenedFoldersIds], ); const handleAddFolder = useCallback( @@ -175,12 +184,15 @@ export const ChangePathDialog = ({ false, true, ); + const id = constructPath(parentFolderId, folderName); - setSelectedFolderId(constructPath(parentFolderId, folderName)); + setSelectedFolderId(id); dispatch( actions.createTemporaryFolder({ - relativePath: parentFolderId, + folderId: parentFolderId, + name: folderName, + id, }), ); diff --git a/apps/chat/src/components/Chat/Chat.tsx b/apps/chat/src/components/Chat/Chat.tsx index 8e313add86..3af1b22c8c 100644 --- a/apps/chat/src/components/Chat/Chat.tsx +++ b/apps/chat/src/components/Chat/Chat.tsx @@ -834,9 +834,8 @@ export function Chat() { const isolatedModelId = useAppSelector( SettingsSelectors.selectIsolatedModelId, ); - const activeModel = useAppSelector((state) => - ModelsSelectors.selectModel(state, isolatedModelId || ''), - ); + const modelsMap = useAppSelector(ModelsSelectors.selectModelsMap); + const activeModel = modelsMap[isolatedModelId || '']; const selectedPublication = useAppSelector( PublicationSelectors.selectSelectedPublication, ); diff --git a/apps/chat/src/components/Chat/ChatHeader/Header.tsx b/apps/chat/src/components/Chat/ChatHeader/Header.tsx index 271f53815a..f526fbbccc 100644 --- a/apps/chat/src/components/Chat/ChatHeader/Header.tsx +++ b/apps/chat/src/components/Chat/ChatHeader/Header.tsx @@ -90,12 +90,21 @@ export const ChatHeader = ({ const isSelectMode = useAppSelector( ConversationsSelectors.selectIsSelectMode, ); - const isTopChatModelSettingsEnabled = useAppSelector((state) => - SettingsSelectors.isFeatureEnabled(state, Feature.TopChatModelSettings), + + const enabledFeatures = useAppSelector( + SettingsSelectors.selectEnabledFeatures, + ); + const isTopChatModelSettingsEnabled = enabledFeatures.has( + Feature.TopChatModelSettings, ); - const isChatbarEnabled = useAppSelector((state) => - SettingsSelectors.isFeatureEnabled(state, Feature.ConversationsSection), + const isTopContextMenuHidden = enabledFeatures.has( + Feature.HideTopContextMenu, ); + const isChangeAgentDisallowed = enabledFeatures.has( + Feature.DisallowChangeAgent, + ); + const isChatbarEnabled = enabledFeatures.has(Feature.ConversationsSection); + const selectedConversations = useAppSelector( ConversationsSelectors.selectSelectedConversations, ); @@ -111,14 +120,6 @@ export const ChatHeader = ({ const screenState = useScreenState(); - const isTopContextMenuHidden = useAppSelector((state) => - SettingsSelectors.isFeatureEnabled(state, Feature.HideTopContextMenu), - ); - - const isChangeAgentDisallowed = useAppSelector((state) => - SettingsSelectors.isFeatureEnabled(state, Feature.DisallowChangeAgent), - ); - const isContextMenuVisible = isChatbarEnabled && !isSelectMode && !isTopContextMenuHidden; diff --git a/apps/chat/src/components/Chat/ChatMessage/ChatMessageTemplatesModal/ChatMessageTemplatesModal.tsx b/apps/chat/src/components/Chat/ChatMessage/ChatMessageTemplatesModal/ChatMessageTemplatesModal.tsx index cb95dfa8e4..832ca9673a 100644 --- a/apps/chat/src/components/Chat/ChatMessage/ChatMessageTemplatesModal/ChatMessageTemplatesModal.tsx +++ b/apps/chat/src/components/Chat/ChatMessage/ChatMessageTemplatesModal/ChatMessageTemplatesModal.tsx @@ -147,14 +147,14 @@ export const ChatMessageTemplatesModal = ({ setPreviewMode(false)} - dataQA="save-button" + dataQA="set-template-tab" > {t('Set template')} setPreviewMode(true)} - dataQA="save-button" + dataQA="preview-tab" disabled={isInvalid} > {t('Preview')} @@ -182,11 +182,8 @@ export const ChatMessageTemplatesModal = ({ > {t('Original message:')}

-
- +
+ {collapsed ? `${message.content .trim() @@ -203,7 +200,7 @@ export const ChatMessageTemplatesModal = ({ diff --git a/apps/chat/src/components/Chat/ChatMessage/ChatMessageTemplatesModal/TemplateRow.tsx b/apps/chat/src/components/Chat/ChatMessage/ChatMessageTemplatesModal/TemplateRow.tsx index 781d9e13a5..992bf5b3ce 100644 --- a/apps/chat/src/components/Chat/ChatMessage/ChatMessageTemplatesModal/TemplateRow.tsx +++ b/apps/chat/src/components/Chat/ChatMessage/ChatMessageTemplatesModal/TemplateRow.tsx @@ -139,7 +139,10 @@ export const TemplateRow = ({ ); return ( -
+
); diff --git a/apps/chat/src/components/Chat/ChatSettings/AddonsDialog.tsx b/apps/chat/src/components/Chat/ChatSettings/AddonsDialog.tsx index 18138ea1b1..b5bfc91394 100644 --- a/apps/chat/src/components/Chat/ChatSettings/AddonsDialog.tsx +++ b/apps/chat/src/components/Chat/ChatSettings/AddonsDialog.tsx @@ -14,6 +14,8 @@ import { Translation } from '@/src/types/translation'; import { AddonsSelectors } from '@/src/store/addons/addons.reducers'; import { useAppSelector } from '@/src/store/hooks'; +import { OUTSIDE_PRESS } from '@/src/constants/modal'; + import Modal from '@/src/components/Common/Modal'; import { ModelIcon } from '../../Chatbar/ModelIcon'; @@ -181,7 +183,7 @@ export const AddonsDialog: FC = ({ state={isOpen ? ModalState.OPENED : ModalState.CLOSED} hideClose containerClassName="flex h-fit max-h-full h-[700px] w-full grow justify-between flex-col gap-4 divide-tertiary py-4 md:grow-0 xl:max-w-[720px] 2xl:max-w-[780px]" - dismissProps={{ outsidePress: true }} + dismissProps={OUTSIDE_PRESS} >
{t('Addons (max 10)')} diff --git a/apps/chat/src/components/Chat/ConversationContextMenu.tsx b/apps/chat/src/components/Chat/ConversationContextMenu.tsx index 11a3d7c4f5..77a68a730c 100644 --- a/apps/chat/src/components/Chat/ConversationContextMenu.tsx +++ b/apps/chat/src/components/Chat/ConversationContextMenu.tsx @@ -1,8 +1,16 @@ import { useDismiss, useFloating, useInteractions } from '@floating-ui/react'; -import { MouseEventHandler, useCallback, useEffect, useState } from 'react'; +import { + MouseEventHandler, + useCallback, + useEffect, + useMemo, + useState, +} from 'react'; import { useTranslation } from 'next-i18next'; +import { useScreenState } from '@/src/hooks/useScreenState'; + import { isEntityNameOnSameLevelUnique } from '@/src/utils/app/common'; import { constructPath } from '@/src/utils/app/file'; import { getNextDefaultName } from '@/src/utils/app/folders'; @@ -15,7 +23,7 @@ import { defaultMyItemsFilters } from '@/src/utils/app/search'; import { translate } from '@/src/utils/app/translation'; import { Conversation } from '@/src/types/chat'; -import { FeatureType, isNotLoaded } from '@/src/types/common'; +import { FeatureType, ScreenState, isNotLoaded } from '@/src/types/common'; import { MoveToFolderProps } from '@/src/types/folder'; import { ContextMenuProps } from '@/src/types/menu'; import { SharingType } from '@/src/types/share'; @@ -29,9 +37,10 @@ import { useAppDispatch, useAppSelector } from '@/src/store/hooks'; import { ImportExportActions } from '@/src/store/import-export/importExport.reducers'; import { SettingsSelectors } from '@/src/store/settings/settings.reducers'; import { ShareActions } from '@/src/store/share/share.reducers'; -import { UIActions } from '@/src/store/ui/ui.reducers'; +import { UIActions, UISelectors } from '@/src/store/ui/ui.reducers'; import { DEFAULT_FOLDER_NAME } from '@/src/constants/default-ui-settings'; +import { PINNED_CONVERSATIONS_SECTION_NAME } from '@/src/constants/sections'; import { PublishModal } from '@/src/components/Chat/Publish/PublishWizard'; import { ExportModal } from '@/src/components/Chatbar/ExportModal'; @@ -51,7 +60,6 @@ interface ConversationContextMenuProps { setIsOpen: (v: boolean) => void; isHeaderMenu?: boolean; publicationUrl?: string; - onStartRename?: () => void; className?: string; TriggerIcon?: ContextMenuProps['TriggerIcon']; disabledState?: boolean; @@ -60,7 +68,6 @@ interface ConversationContextMenuProps { export const ConversationContextMenu = ({ conversation, publicationUrl, - onStartRename, isOpen, setIsOpen, className, @@ -71,15 +78,17 @@ export const ConversationContextMenu = ({ const { t } = useTranslation(Translation.Chat); const dispatch = useAppDispatch(); - - const folders = useAppSelector((state) => - ConversationsSelectors.selectFilteredFolders( - state, - defaultMyItemsFilters, - '', - true, - ), + const selectFilteredFoldersSelector = useMemo( + () => + ConversationsSelectors.selectFilteredFolders( + defaultMyItemsFilters, + '', + true, + ), + [], ); + + const folders = useAppSelector(selectFilteredFoldersSelector); const allConversations = useAppSelector( ConversationsSelectors.selectConversations, ); @@ -87,6 +96,13 @@ export const ConversationContextMenu = ({ SettingsSelectors.selectIsPublishingEnabled(state, FeatureType.Chat), ); + const collapsedSectionsSelector = useMemo( + () => UISelectors.selectCollapsedSections(FeatureType.Chat), + [], + ); + + const collapsedSections = useAppSelector(collapsedSectionsSelector); + const [isShowMoveToModal, setIsShowMoveToModal] = useState(false); const [isDeleting, setIsDeleting] = useState(false); const [isShowExportModal, setIsShowExportModal] = useState(false); @@ -94,6 +110,8 @@ export const ConversationContextMenu = ({ const [isPublishing, setIsPublishing] = useState(false); const [isUnpublishing, setIsUnpublishing] = useState(false); + const screenState = useScreenState(); + const { refs, context } = useFloating({ open: isOpen, onOpenChange: setIsOpen, @@ -126,6 +144,13 @@ export const ConversationContextMenu = ({ setIsShowExportModal(false); }, []); + useEffect(() => { + if (screenState !== ScreenState.MOBILE) { + setIsShowMoveToModal(false); + handleCloseExportModal(); + } + }, [handleCloseExportModal, screenState]); + const handleOpenDeleteModal: MouseEventHandler = useCallback((e) => { e.stopPropagation(); @@ -194,6 +219,15 @@ export const ConversationContextMenu = ({ }), ); } + + dispatch( + UIActions.setCollapsedSections({ + featureType: FeatureType.Chat, + collapsedSections: collapsedSections.filter( + (section) => section !== PINNED_CONVERSATIONS_SECTION_NAME, + ), + }), + ); dispatch( ConversationsActions.updateConversation({ id: conversation.id, @@ -205,7 +239,7 @@ export const ConversationContextMenu = ({ }), ); }, - [allConversations, conversation, dispatch, folders, t], + [allConversations, collapsedSections, conversation, dispatch, folders, t], ); const handleCompare: MouseEventHandler = @@ -303,6 +337,10 @@ export const ConversationContextMenu = ({ setIsDeleting(false); }, [conversation.id, conversation.sharedWithMe, dispatch]); + const handleOpenRenameModal = useCallback(() => { + dispatch(ConversationsActions.setRenamingConversationId(conversation.id)); + }, [conversation, dispatch]); + return ( <> -
- {isShowMoveToModal && ( - { - setIsShowMoveToModal(false); - }} - folders={folders} - onMoveToFolder={handleMoveToFolder} - /> - )} -
- -
- {isShowExportModal && ( - - )} -
+ {isShowMoveToModal && ( + { + setIsShowMoveToModal(false); + }} + folders={folders} + onMoveToFolder={handleMoveToFolder} + /> + )} + + {isShowExportModal && ( + + )} {isPublishingEnabled && (isPublishing || isUnpublishing) && ( )} - { - setIsDeleting(false); - if (result) handleDelete(); - }} - /> + {isDeleting && ( + { + setIsDeleting(false); + if (result) handleDelete(); + }} + /> + )} ); }; diff --git a/apps/chat/src/components/Chat/EmptyChatDescription.tsx b/apps/chat/src/components/Chat/EmptyChatDescription.tsx index bbd96ca59e..6e97cbd179 100644 --- a/apps/chat/src/components/Chat/EmptyChatDescription.tsx +++ b/apps/chat/src/components/Chat/EmptyChatDescription.tsx @@ -42,19 +42,20 @@ const EmptyChatDescriptionView = ({ const dispatch = useAppDispatch(); const { t } = useTranslation(Translation.Chat); - - const model = useAppSelector((state) => - ModelsSelectors.selectModel(state, conversation.model.id), - ); + const modelsMap = useAppSelector(ModelsSelectors.selectModelsMap); + const model = modelsMap[conversation.model.id]; const installedModelIds = useAppSelector( ModelsSelectors.selectInstalledModelIds, ); const models = useAppSelector(ModelsSelectors.selectModels); - const isEmptyChatChangeAgentHidden = useAppSelector((state) => - SettingsSelectors.isFeatureEnabled(state, Feature.HideEmptyChatChangeAgent), + const enabledFeatures = useAppSelector( + SettingsSelectors.selectEnabledFeatures, + ); + const isEmptyChatChangeAgentHidden = enabledFeatures.has( + Feature.HideEmptyChatChangeAgent, ); - const isEmptyChatSettingsEnabled = useAppSelector((state) => - SettingsSelectors.isFeatureEnabled(state, Feature.EmptyChatSettings), + const isEmptyChatSettingsEnabled = enabledFeatures.has( + Feature.EmptyChatSettings, ); const isExternal = isEntityIdExternal(conversation); diff --git a/apps/chat/src/components/Chat/MainModalManager.jsx b/apps/chat/src/components/Chat/MainModalManager.jsx new file mode 100644 index 0000000000..ff0109c99e --- /dev/null +++ b/apps/chat/src/components/Chat/MainModalManager.jsx @@ -0,0 +1,13 @@ +import { ReplaceConfirmationModal } from '../Common/ReplaceConfirmationModal/ReplaceConfirmationModal'; +import { RenameConversationModal } from './RenameConversationModal'; +import ShareModal from './ShareModal'; + +export const MainModalManager = () => { + return ( + <> + + + + + ); +}; diff --git a/apps/chat/src/components/Chat/MessageAttachment.tsx b/apps/chat/src/components/Chat/MessageAttachment.tsx index 308d7f6fb9..6e737ac7e2 100644 --- a/apps/chat/src/components/Chat/MessageAttachment.tsx +++ b/apps/chat/src/components/Chat/MessageAttachment.tsx @@ -189,9 +189,12 @@ const AttachmentUrlRendererComponent = ({ const mappedVisualizers = useAppSelector( SettingsSelectors.selectMappedVisualizers, ); - - const isCustomAttachmentType = useAppSelector((state) => - SettingsSelectors.selectIsCustomAttachmentType(state, attachmentType), + const selectIsCustomAttachmentTypeSelector = useMemo( + () => SettingsSelectors.selectIsCustomAttachmentType(attachmentType), + [attachmentType], + ); + const isCustomAttachmentType = useAppSelector( + selectIsCustomAttachmentTypeSelector, ); if (mappedVisualizers && isCustomAttachmentType) { @@ -223,8 +226,12 @@ export const MessageAttachment = ({ attachment, isInner }: Props) => { const [isExpanded, setIsExpanded] = useState(false); const anchorRef = useRef(null); - const isCustomAttachmentType = useAppSelector((state) => - SettingsSelectors.selectIsCustomAttachmentType(state, attachment.type), + const selectIsCustomAttachmentTypeSelector = useMemo( + () => SettingsSelectors.selectIsCustomAttachmentType(attachment.type), + [attachment.type], + ); + const isCustomAttachmentType = useAppSelector( + selectIsCustomAttachmentTypeSelector, ); useEffect(() => { diff --git a/apps/chat/src/components/Chat/ModelVersionSelect.tsx b/apps/chat/src/components/Chat/ModelVersionSelect.tsx index 62ecb64f6a..4eaa090a90 100644 --- a/apps/chat/src/components/Chat/ModelVersionSelect.tsx +++ b/apps/chat/src/components/Chat/ModelVersionSelect.tsx @@ -85,7 +85,7 @@ export const ModelVersionSelect = ({ { const dispatch = useAppDispatch(); - const openedFoldersIds = useAppSelector((state) => - UISelectors.selectOpenedFoldersIds(state, FeatureType.Prompt), + const openedFolderIdsSelector = useMemo( + () => UISelectors.selectOpenedFoldersIds(FeatureType.Prompt), + [], ); + + const openedFoldersIds = useAppSelector(openedFolderIdsSelector); const searchTerm = useAppSelector(PromptsSelectors.selectSearchTerm); const prompts = useAppSelector(PromptsSelectors.selectPrompts); const highlightedFolders = useAppSelector( @@ -235,9 +238,12 @@ export const ConversationPublicationResources = ({ }: PublicationResources) => { const dispatch = useAppDispatch(); - const openedFoldersIds = useAppSelector((state) => - UISelectors.selectOpenedFoldersIds(state, FeatureType.Chat), + const openedFolderIdsSelector = useMemo( + () => UISelectors.selectOpenedFoldersIds(FeatureType.Chat), + [], ); + + const openedFoldersIds = useAppSelector(openedFolderIdsSelector); const searchTerm = useAppSelector(ConversationsSelectors.selectSearchTerm); const conversations = useAppSelector( ConversationsSelectors.selectConversations, @@ -372,9 +378,12 @@ export const FilePublicationResources = ({ }: PublicationResources) => { const dispatch = useAppDispatch(); - const openedFoldersIds = useAppSelector((state) => - UISelectors.selectOpenedFoldersIds(state, FeatureType.File), + const openedFolderIdsSelector = useMemo( + () => UISelectors.selectOpenedFoldersIds(FeatureType.File), + [], ); + + const openedFoldersIds = useAppSelector(openedFolderIdsSelector); const files = useAppSelector(FilesSelectors.selectFiles); const allFolders = useAppSelector(FilesSelectors.selectFolders); diff --git a/apps/chat/src/components/Chat/Publish/PublishWizard.tsx b/apps/chat/src/components/Chat/Publish/PublishWizard.tsx index e9a48541f9..ec6766777b 100644 --- a/apps/chat/src/components/Chat/Publish/PublishWizard.tsx +++ b/apps/chat/src/components/Chat/Publish/PublishWizard.tsx @@ -515,13 +515,23 @@ export function PublishModal< )) )} {!isRulesLoading && path && ( -
-
+
+
{path.split('/').pop()}
-
+
{otherTargetAudienceFilters.map((item) => ( -
+
@@ -559,6 +569,7 @@ export function PublishModal< diff --git a/apps/chat/src/components/Chat/Publish/ReviewApplicationDialog.tsx b/apps/chat/src/components/Chat/Publish/ReviewApplicationDialog.tsx index fa6849fd0b..db1136d20c 100644 --- a/apps/chat/src/components/Chat/Publish/ReviewApplicationDialog.tsx +++ b/apps/chat/src/components/Chat/Publish/ReviewApplicationDialog.tsx @@ -4,6 +4,8 @@ import { ApplicationSelectors } from '@/src/store/application/application.reduce import { useAppDispatch, useAppSelector } from '@/src/store/hooks'; import { PublicationActions } from '@/src/store/publication/publication.reducers'; +import { MOUSE_OUTSIDE_PRESS_EVENT } from '@/src/constants/modal'; + import Modal from '../../Common/Modal'; import { Spinner } from '../../Common/Spinner'; import { ReviewApplicationDialogView } from './ReviewApplicationDialogView'; @@ -26,7 +28,7 @@ export function ReviewApplicationDialog() { overlayClassName="fixed inset-0 top-[48px]" state={ModalState.OPENED} containerClassName="flex flex-col gap-4 sm:w-[600px] w-full" - dismissProps={{ outsidePressEvent: 'mousedown' }} + dismissProps={MOUSE_OUTSIDE_PRESS_EVENT} > {isLoading ? (
diff --git a/apps/chat/src/components/Chat/RenameConversationModal.tsx b/apps/chat/src/components/Chat/RenameConversationModal.tsx new file mode 100644 index 0000000000..2da3b4be9c --- /dev/null +++ b/apps/chat/src/components/Chat/RenameConversationModal.tsx @@ -0,0 +1,232 @@ +import { + KeyboardEvent, + useCallback, + useEffect, + useMemo, + useRef, + useState, +} from 'react'; + +import { useTranslation } from 'next-i18next'; + +import { + doesHaveDotsInTheEnd, + isEntityNameOnSameLevelUnique, + prepareEntityName, +} from '@/src/utils/app/common'; +import { notAllowedSymbolsRegex } from '@/src/utils/app/file'; + +import { ModalState } from '@/src/types/modal'; +import { Translation } from '@/src/types/translation'; + +import { + ConversationsActions, + ConversationsSelectors, +} from '@/src/store/conversations/conversations.reducers'; +import { useAppDispatch, useAppSelector } from '@/src/store/hooks'; +import { UIActions } from '@/src/store/ui/ui.reducers'; + +import { DISALLOW_INTERACTIONS } from '@/src/constants/modal'; + +import { ConfirmDialog } from '../Common/ConfirmDialog'; +import Modal from '../Common/Modal'; + +export const RenameConversationModal = () => { + const renamingConversation = useAppSelector( + ConversationsSelectors.selectRenamingConversation, + ); + if (renamingConversation) { + return ; + } +}; + +const RenameConversationView = () => { + const { t } = useTranslation(Translation.Chat); + const dispatch = useAppDispatch(); + const inputRef = useRef(null); + + const allConversations = useAppSelector( + ConversationsSelectors.selectConversations, + ); + + const renamingConversation = useAppSelector( + ConversationsSelectors.selectRenamingConversation, + ); + + const [newConversationName, setNewConversationName] = useState(''); + const [isConfirmRenaming, setIsConfirmRenaming] = useState(false); + const [originConversationName, setOriginConversationName] = useState(''); + + useEffect(() => { + if (renamingConversation) { + setNewConversationName(renamingConversation.name || ''); + setOriginConversationName(renamingConversation.name || ''); + setTimeout(() => { + inputRef.current?.focus(); + inputRef.current?.select(); + }); + } else { + setNewConversationName(''); + setOriginConversationName(''); + setIsConfirmRenaming(false); + } + }, [renamingConversation]); + + const newName = useMemo( + () => prepareEntityName(newConversationName, { forRenaming: true }), + [newConversationName], + ); + + const performRename = useCallback( + (name: string) => { + if (!name.trim()) return; + if (name.length > 0 && renamingConversation) { + dispatch( + ConversationsActions.updateConversation({ + id: renamingConversation.id, + values: { + name, + isNameChanged: true, + isShared: false, + }, + }), + ); + dispatch(ConversationsActions.setRenamingConversationId(null)); + } + }, + [renamingConversation, dispatch], + ); + + const handleRename = useCallback(() => { + if (!renamingConversation) return; + + if ( + !isEntityNameOnSameLevelUnique( + newName, + renamingConversation, + allConversations, + ) + ) { + dispatch( + UIActions.showErrorToast( + t( + 'Conversation with name "{{newName}}" already exists in this folder.', + { + ns: 'chat', + newName, + }, + ), + ), + ); + + return; + } + + if (doesHaveDotsInTheEnd(newName)) { + dispatch( + UIActions.showErrorToast( + t('Using a dot at the end of a name is not permitted.'), + ), + ); + return; + } + + if ( + renamingConversation.isShared && + newName !== renamingConversation.name + ) { + setIsConfirmRenaming(true); + return; + } + + performRename(newName); + }, [ + newName, + renamingConversation, + allConversations, + performRename, + dispatch, + t, + ]); + + const handleEnterDown = useCallback( + (e: KeyboardEvent) => { + if (e.key === 'Enter') { + e.preventDefault(); + e.stopPropagation(); + handleRename(); + } + }, + [handleRename], + ); + + const handleClose = useCallback(() => { + dispatch(ConversationsActions.setRenamingConversationId(null)); + }, [dispatch]); + + return ( + +

+ {t('Rename conversation')} +

+ e.target.select()} + onChange={(e) => + setNewConversationName( + e.target.value.replaceAll(notAllowedSymbolsRegex, ''), + ) + } + onKeyDown={handleEnterDown} + className="w-full rounded border border-primary bg-transparent px-3 py-2.5 leading-4 outline-none placeholder:text-secondary focus-visible:border-accent-primary" + /> +
+ + +
+ { + setIsConfirmRenaming(false); + if (result) performRename(newName); + handleClose(); + }} + /> +
+ ); +}; diff --git a/apps/chat/src/components/Chat/ShareModal.tsx b/apps/chat/src/components/Chat/ShareModal.tsx index d9568a4837..d521bb23d7 100644 --- a/apps/chat/src/components/Chat/ShareModal.tsx +++ b/apps/chat/src/components/Chat/ShareModal.tsx @@ -19,10 +19,21 @@ import { Translation } from '@/src/types/translation'; import { useAppDispatch, useAppSelector } from '@/src/store/hooks'; import { ShareActions, ShareSelectors } from '@/src/store/share/share.reducers'; +import { OUTSIDE_PRESS_AND_MOUSE_EVENT } from '@/src/constants/modal'; + import Modal from '../Common/Modal'; import Tooltip from '../Common/Tooltip'; -export default function ShareModal() { +export const ShareModal = () => { + const isShareModalClosed = useAppSelector( + ShareSelectors.selectShareModalClosed, + ); + if (!isShareModalClosed) { + return ; + } +}; + +export default function ShareModalView() { const { t } = useTranslation(Translation.SideBar); const dispatch = useAppDispatch(); @@ -84,7 +95,7 @@ export default function ShareModal() { state={modalState} onClose={handleClose} heading={`${t('Share')}: ${shareResourceName?.trim()}`} - dismissProps={{ outsidePress: true }} + dismissProps={OUTSIDE_PRESS_AND_MOUSE_EVENT} >

diff --git a/apps/chat/src/components/Chat/TalkTo/TalkToCard.tsx b/apps/chat/src/components/Chat/TalkTo/TalkToCard.tsx index aeef93a7d5..28705f470f 100644 --- a/apps/chat/src/components/Chat/TalkTo/TalkToCard.tsx +++ b/apps/chat/src/components/Chat/TalkTo/TalkToCard.tsx @@ -1,4 +1,5 @@ import { + IconFileDescription, IconPencilMinus, IconPlayerPlay, IconPlaystationSquare, @@ -18,8 +19,10 @@ import { getApplicationSimpleStatus, getModelShortDescription, isApplicationStatusUpdating, + isExecutableApp, } from '@/src/utils/app/application'; import { getRootId } from '@/src/utils/app/id'; +import { isEntityIdPublic } from '@/src/utils/app/publications'; import { PseudoModel, isPseudoModel } from '@/src/utils/server/api'; import { @@ -50,7 +53,8 @@ import { ApplicationTopic } from '@/src/components/Marketplace/ApplicationTopic' import { FunctionStatusIndicator } from '@/src/components/Marketplace/FunctionStatusIndicator'; import LoaderIcon from '@/public/images/icons/loader.svg'; -import { Feature } from '@epam/ai-dial-shared'; +import UnpublishIcon from '@/public/images/icons/unpublish.svg'; +import { Feature, PublishActions } from '@epam/ai-dial-shared'; const DESKTOP_ICON_SIZE = 80; const TABLET_ICON_SIZE = 48; @@ -78,10 +82,11 @@ interface ApplicationCardProps { disabled: boolean; isUnavailableModel: boolean; onClick: (entity: DialAIEntityModel) => void; - onPublish: (entity: DialAIEntityModel) => void; + onPublish: (entity: DialAIEntityModel, action: PublishActions) => void; onDelete: (entity: DialAIEntityModel) => void; onEdit: (entity: DialAIEntityModel) => void; onSelectVersion: (entity: DialAIEntityModel) => void; + onOpenLogs: (entity: DialAIEntityModel) => void; } export const TalkToCard = ({ @@ -95,6 +100,7 @@ export const TalkToCard = ({ onEdit, onPublish, onSelectVersion, + onOpenLogs, }: ApplicationCardProps) => { const { t } = useTranslation(Translation.Marketplace); @@ -109,6 +115,10 @@ export const TalkToCard = ({ ); const isAdmin = useAppSelector(AuthSelectors.selectIsAdmin); + const isMyApp = entity.id.startsWith( + getRootId({ featureType: FeatureType.Application }), + ); + const isExecutable = isExecutableApp(entity) && (isMyApp || isAdmin); const screenState = useScreenState(); const versionsToSelect = useMemo(() => { @@ -205,7 +215,29 @@ export const TalkToCard = ({ Icon: IconWorldShare, onClick: (e: React.MouseEvent) => { e.stopPropagation(); - onPublish(entity); + onPublish?.(entity, PublishActions.ADD); + }, + }, + { + name: t('Unpublish'), + dataQa: 'unpublish', + display: isEntityIdPublic(entity) && !!onPublish, + Icon: UnpublishIcon, + onClick: (e: React.MouseEvent) => { + e.stopPropagation(); + onPublish?.(entity, PublishActions.DELETE); + }, + }, + { + name: t('Logs'), + dataQa: 'app-logs', + display: + isExecutable && playerStatus === SimpleApplicationStatus.UNDEPLOY, + Icon: IconFileDescription, + onClick: (e: React.MouseEvent) => { + e.preventDefault(); + e.stopPropagation(); + onOpenLogs(entity); }, }, { @@ -230,10 +262,12 @@ export const TalkToCard = ({ isCodeAppsEnabled, PlayerIcon, onEdit, - isModifyDisabled, onPublish, + isExecutable, onDelete, + isModifyDisabled, handleUpdateFunctionStatus, + onOpenLogs, ], ); diff --git a/apps/chat/src/components/Chat/TalkTo/TalkToModal.tsx b/apps/chat/src/components/Chat/TalkTo/TalkToModal.tsx index b904a3c61a..d9434e8b79 100644 --- a/apps/chat/src/components/Chat/TalkTo/TalkToModal.tsx +++ b/apps/chat/src/components/Chat/TalkTo/TalkToModal.tsx @@ -7,7 +7,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useDispatch } from 'react-redux'; import { useTranslation } from 'next-i18next'; -import { useRouter } from 'next/router'; +import Link from 'next/link'; import classNames from 'classnames'; @@ -35,6 +35,7 @@ import { ApplicationActions } from '@/src/store/application/application.reducers import { ConversationsActions } from '@/src/store/conversations/conversations.reducers'; import { useAppSelector } from '@/src/store/hooks'; import { ModelsSelectors } from '@/src/store/models/models.reducers'; +import { SettingsSelectors } from '@/src/store/settings/settings.reducers'; import { REPLAY_AS_IS_MODEL } from '@/src/constants/chat'; import { MarketplaceQueryParams } from '@/src/constants/marketplace'; @@ -45,9 +46,10 @@ import { ConfirmDialog } from '@/src/components/Common/ConfirmDialog'; import Modal from '@/src/components/Common/Modal'; import { NoResultsFound } from '@/src/components/Common/NoResultsFound'; +import { ApplicationLogs } from '../../Marketplace/ApplicationLogs'; import { TalkToCard } from './TalkToCard'; -import { PublishActions, ShareEntity } from '@epam/ai-dial-shared'; +import { Feature, PublishActions, ShareEntity } from '@epam/ai-dial-shared'; import chunk from 'lodash-es/chunk'; import orderBy from 'lodash-es/orderBy'; import range from 'lodash-es/range'; @@ -80,8 +82,12 @@ interface SliderModelsGroupProps { rowsCount: number; onEditApplication: (entity: DialAIEntityModel) => void; onDeleteApplication: (entity: DialAIEntityModel) => void; - onSetPublishEntity: (entity: DialAIEntityModel) => void; + onSetPublishEntity: ( + entity: DialAIEntityModel, + action: PublishActions, + ) => void; onSelectModel: (entity: DialAIEntityModel) => void; + onOpenLogs: (entity: DialAIEntityModel) => void; } const SliderModelsGroup = ({ modelsGroup, @@ -92,6 +98,7 @@ const SliderModelsGroup = ({ onDeleteApplication, onSetPublishEntity, onSelectModel, + onOpenLogs, }: SliderModelsGroupProps) => { const config = getMaxChunksCountConfig(); @@ -144,6 +151,7 @@ const SliderModelsGroup = ({ onPublish={onSetPublishEntity} onSelectVersion={onSelectModel} onClick={onSelectModel} + onOpenLogs={onOpenLogs} /> ); })} @@ -176,8 +184,6 @@ const TalkToModalView = ({ }: TalkToModalViewProps) => { const { t } = useTranslation(Translation.Chat); - const router = useRouter(); - const dispatch = useDispatch(); const allModels = useAppSelector(ModelsSelectors.selectModels); @@ -192,12 +198,15 @@ const TalkToModalView = ({ const [activeSlide, setActiveSlide] = useState(0); const [editModel, setEditModel] = useState(); const [deleteModel, setDeleteModel] = useState(); - const [publishModel, setPublishModel] = useState< - ShareEntity & { iconUrl?: string } - >(); + const [logModel, setLogModel] = useState(); + const [publishModel, setPublishModel] = useState<{ + entity: ShareEntity & { iconUrl?: string }; + action: PublishActions; + }>(); const [sliderHeight, setSliderHeight] = useState(0); const [sharedConversationNewModel, setSharedConversationNewModel] = useState(); + const [isOpenLogs, setIsOpenLogs] = useState(); const sliderRef = useRef(null); @@ -206,6 +215,9 @@ const TalkToModalView = ({ const isPlayback = conversation.playback?.isPlayback; const isReplay = conversation.replay?.isReplay; const config = getMaxChunksCountConfig(); + const isMarketplaceEnabled = useAppSelector((state) => + SettingsSelectors.isFeatureEnabled(state, Feature.Marketplace), + ); const displayedModels = useMemo(() => { const currentModel = modelsMap[conversation.model.id]; @@ -428,17 +440,32 @@ const TalkToModalView = ({ [deleteModel, dispatch], ); - const handleSetPublishEntity = useCallback((entity: DialAIEntityModel) => { - setPublishModel({ - name: entity.name, - id: ApiUtils.decodeApiUrl(entity.id), - folderId: getFolderIdFromEntityId(entity.id), - iconUrl: entity.iconUrl, - }); - }, []); + const handleSetPublishEntity = useCallback( + (entity: DialAIEntityModel, action: PublishActions) => + setPublishModel({ + entity: { + name: entity.name, + id: ApiUtils.decodeApiUrl(entity.id), + folderId: getFolderIdFromEntityId(entity.id), + iconUrl: entity.iconUrl, + }, + action, + }), + [], + ); const handlePublishClose = useCallback(() => setPublishModel(undefined), []); + const handleCloseApplicationLogs = useCallback( + () => setIsOpenLogs(false), + [setIsOpenLogs], + ); + + const handleOpenApplicationLogs = useCallback((entity: DialAIEntityModel) => { + setIsOpenLogs(true); + setLogModel(entity); + }, []); + const handleDeleteApplication = useCallback( (entity: DialAIEntityModel) => { setDeleteModel(entity); @@ -522,6 +549,7 @@ const TalkToModalView = ({ onDeleteApplication={handleDeleteApplication} onSetPublishEntity={handleSetPublishEntity} onSelectModel={handleSelectModel} + onOpenLogs={handleOpenApplicationLogs} /> )) ) : ( @@ -577,21 +605,22 @@ const TalkToModalView = ({ )}

- + {isMarketplaceEnabled && ( + + conversation.playback?.isPlayback ? e.preventDefault() : null + } + className={classNames( + 'mt-4 text-accent-primary md:mt-0', + conversation.playback?.isPlayback && 'cursor-not-allowed', + )} + data-qa="go-to-my-workspace" + > + {t('Go to My workspace')} + + )}
@@ -628,11 +657,18 @@ const TalkToModalView = ({ )} {publishModel && ( + )} + {logModel && isOpenLogs && ( + )} diff --git a/apps/chat/src/components/Chatbar/ChatFolders.tsx b/apps/chat/src/components/Chatbar/ChatFolders.tsx index 47503e503d..d715311b71 100644 --- a/apps/chat/src/components/Chatbar/ChatFolders.tsx +++ b/apps/chat/src/components/Chatbar/ChatFolders.tsx @@ -66,31 +66,37 @@ const ChatFolderTemplate = ({ const dispatch = useAppDispatch(); const searchTerm = useAppSelector(ConversationsSelectors.selectSearchTerm); - const conversations = useAppSelector((state) => - ConversationsSelectors.selectFilteredConversations( - state, - filters, - searchTerm, - ), + const selectFilteredConversationsSelector = useMemo( + () => + ConversationsSelectors.selectFilteredConversations(filters, searchTerm), + [filters, searchTerm], ); + const conversations = useAppSelector(selectFilteredConversationsSelector); const allConversations = useAppSelector( ConversationsSelectors.selectConversations, ); const allFolders = useAppSelector(ConversationsSelectors.selectFolders); - const conversationFolders = useAppSelector((state) => - ConversationsSelectors.selectFilteredFolders( - state, - filters, - searchTerm, - includeEmpty, - ), + const selectFilteredFoldersSelector = useMemo( + () => + ConversationsSelectors.selectFilteredFolders( + filters, + searchTerm, + includeEmpty, + ), + [filters, includeEmpty, searchTerm], ); + + const conversationFolders = useAppSelector(selectFilteredFoldersSelector); const highlightedFolders = useAppSelector( ConversationsSelectors.selectSelectedConversationsFoldersIds, ); - const openedFoldersIds = useAppSelector((state) => - UISelectors.selectOpenedFoldersIds(state, FeatureType.Chat), + + const openedFolderIdsSelector = useMemo( + () => UISelectors.selectOpenedFoldersIds(FeatureType.Chat), + [], ); + + const openedFoldersIds = useAppSelector(openedFolderIdsSelector); const loadingFolderIds = useAppSelector( ConversationsSelectors.selectLoadingFolderIds, ); @@ -100,9 +106,14 @@ const ChatFolderTemplate = ({ const isConversationsStreaming = useAppSelector( ConversationsSelectors.selectIsConversationsStreaming, ); + + const chosenFolderIdsSelector = useMemo( + () => ConversationsSelectors.selectChosenFolderIds(conversations), + [conversations], + ); + const { fullyChosenFolderIds, partialChosenFolderIds } = useAppSelector( - (state) => - ConversationsSelectors.selectChosenFolderIds(state, conversations), + chosenFolderIdsSelector, ); const selectedConversations = useAppSelector( ConversationsSelectors.selectSelectedItems, @@ -321,21 +332,22 @@ export const ChatSection = ({ const [isSectionHighlighted, setIsSectionHighlighted] = useState(false); const searchTerm = useAppSelector(ConversationsSelectors.selectSearchTerm); - const rootFolders = useAppSelector((state) => - ConversationsSelectors.selectFilteredFolders( - state, - filters, - searchTerm, - showEmptyFolders, - ), - ); - const rootConversations = useAppSelector((state) => - ConversationsSelectors.selectFilteredConversations( - state, - filters, - searchTerm, - ), + const selectFilteredFoldersSelector = useMemo( + () => + ConversationsSelectors.selectFilteredFolders( + filters, + searchTerm, + showEmptyFolders, + ), + [filters, searchTerm, showEmptyFolders], + ); + const rootFolders = useAppSelector(selectFilteredFoldersSelector); + const selectFilteredConversationsSelector = useMemo( + () => + ConversationsSelectors.selectFilteredConversations(filters, searchTerm), + [filters, searchTerm], ); + const rootConversations = useAppSelector(selectFilteredConversationsSelector); const selectedFoldersIds = useAppSelector( ConversationsSelectors.selectSelectedConversationsFoldersIds, ); @@ -443,13 +455,18 @@ export function ChatFolders() { const isSharingEnabled = useAppSelector((state) => SettingsSelectors.isSharingEnabled(state, FeatureType.Chat), ); - const publicationItems = useAppSelector((state) => - PublicationSelectors.selectFilteredPublications( - state, - publicationFeatureTypes, - ), + + const publicationItemsSelector = useMemo( + () => + PublicationSelectors.selectFilteredPublications( + publicationFeatureTypes, + true, + ), + [], ); + const publicationItems = useAppSelector(publicationItemsSelector); + const toApproveFolderItem = { hidden: !publicationItems.length, name: APPROVE_REQUIRED_SECTION_NAME, diff --git a/apps/chat/src/components/Chatbar/Chatbar.tsx b/apps/chat/src/components/Chatbar/Chatbar.tsx index fc43e9d227..3689b4b8d8 100644 --- a/apps/chat/src/components/Chatbar/Chatbar.tsx +++ b/apps/chat/src/components/Chatbar/Chatbar.tsx @@ -1,8 +1,10 @@ import { IconApps } from '@tabler/icons-react'; -import { DragEvent, useCallback } from 'react'; +import { DragEvent, useCallback, useMemo } from 'react'; import { useTranslation } from 'next-i18next'; -import { useRouter } from 'next/router'; +import Link from 'next/link'; + +import classNames from 'classnames'; import { isEntityNameOnSameLevelUnique } from '@/src/utils/app/common'; import { getConversationRootId } from '@/src/utils/app/id'; @@ -20,6 +22,8 @@ import { useAppDispatch, useAppSelector } from '@/src/store/hooks'; import { SettingsSelectors } from '@/src/store/settings/settings.reducers'; import { UIActions, UISelectors } from '@/src/store/ui/ui.reducers'; +import { CONVERSATIONS_DATE_SECTIONS } from '@/src/constants/sections'; + import Tooltip from '../Common/Tooltip'; import Sidebar from '../Sidebar'; import { ChatFolders } from './ChatFolders'; @@ -29,19 +33,19 @@ import { Conversations } from './Conversations'; import { ConversationInfo, Feature } from '@epam/ai-dial-shared'; const ChatActionsBlock = () => { - const router = useRouter(); const { t } = useTranslation(Translation.SideBar); const messageIsStreaming = useAppSelector( ConversationsSelectors.selectIsConversationsStreaming, ); - const isNewConversationDisabled = useAppSelector((state) => - SettingsSelectors.isFeatureEnabled(state, Feature.HideNewConversation), + const enabledFeatures = useAppSelector( + SettingsSelectors.selectEnabledFeatures, ); - - const isMarketplaceEnabled = useAppSelector((state) => - SettingsSelectors.isFeatureEnabled(state, Feature.Marketplace), + const isNewConversationDisabled = enabledFeatures.has( + Feature.HideNewConversation, ); + const isMarketplaceEnabled = enabledFeatures.has(Feature.Marketplace); + if (isNewConversationDisabled) { return null; } @@ -50,17 +54,23 @@ const ChatActionsBlock = () => { <> {isMarketplaceEnabled && (
- +
)} @@ -87,20 +97,30 @@ export const Chatbar = () => { ConversationsSelectors.selectMyItemsFilters, ); - const filteredConversations = useAppSelector((state) => - ConversationsSelectors.selectFilteredConversations( - state, - myItemsFilters, - searchTerm, - ), + const collapsedSectionsSelector = useMemo( + () => UISelectors.selectCollapsedSections(FeatureType.Chat), + [], + ); + + const collapsedSections = useAppSelector(collapsedSectionsSelector); + + const selectFilteredConversationsSelector = useMemo( + () => + ConversationsSelectors.selectFilteredConversations( + myItemsFilters, + searchTerm, + ), + [myItemsFilters, searchTerm], + ); + const filteredConversations = useAppSelector( + selectFilteredConversationsSelector, ); - const filteredFolders = useAppSelector((state) => - ConversationsSelectors.selectFilteredFolders( - state, - myItemsFilters, - searchTerm, - ), + const selectFilteredFoldersSelector = useMemo( + () => + ConversationsSelectors.selectFilteredFolders(myItemsFilters, searchTerm), + [myItemsFilters, searchTerm], ); + const filteredFolders = useAppSelector(selectFilteredFoldersSelector); const handleDrop = useCallback( (e: DragEvent) => { @@ -132,6 +152,14 @@ export const Chatbar = () => { return; } + dispatch( + UIActions.setCollapsedSections({ + featureType: FeatureType.Chat, + collapsedSections: collapsedSections.filter( + (section) => section !== CONVERSATIONS_DATE_SECTIONS.today, + ), + }), + ); dispatch( ConversationsActions.updateConversation({ id: conversation.id, @@ -142,7 +170,7 @@ export const Chatbar = () => { } } }, - [allConversations, dispatch, t], + [allConversations, collapsedSections, dispatch, t], ); const handleSearchTerm = useCallback( @@ -173,9 +201,9 @@ export const Chatbar = () => { filteredFolders={filteredFolders} searchTerm={searchTerm} searchFilters={searchFilters} - handleSearchTerm={handleSearchTerm} - handleSearchFilters={handleSearchFilters} - handleDrop={handleDrop} + onSearchTerm={handleSearchTerm} + onSearchFilters={handleSearchFilters} + onDrop={handleDrop} footerComponent={} areEntitiesUploaded={areEntitiesUploaded} /> diff --git a/apps/chat/src/components/Chatbar/ChatbarSettings.tsx b/apps/chat/src/components/Chatbar/ChatbarSettings.tsx index 1e6cc1fc5b..ec0e620136 100644 --- a/apps/chat/src/components/Chatbar/ChatbarSettings.tsx +++ b/apps/chat/src/components/Chatbar/ChatbarSettings.tsx @@ -62,10 +62,14 @@ export const ChatbarSettings = () => { const isSelectMode = useAppSelector( ConversationsSelectors.selectIsSelectMode, ); - const collapsedSections = useAppSelector((state) => - UISelectors.selectCollapsedSections(state, FeatureType.Chat), + + const collapsedSectionsSelector = useMemo( + () => UISelectors.selectCollapsedSections(FeatureType.Chat), + [], ); + const collapsedSections = useAppSelector(collapsedSectionsSelector); + const handleToggleCompare = useCallback(() => { dispatch( ConversationsActions.createNewConversations({ diff --git a/apps/chat/src/components/Chatbar/Conversation.tsx b/apps/chat/src/components/Chatbar/Conversation.tsx index f4bfe46ad1..caf63e85bc 100644 --- a/apps/chat/src/components/Chatbar/Conversation.tsx +++ b/apps/chat/src/components/Chatbar/Conversation.tsx @@ -1,11 +1,8 @@ -import { IconCheck, IconX } from '@tabler/icons-react'; +import { IconCheck } from '@tabler/icons-react'; import { DragEvent, - KeyboardEvent, MouseEvent, - MouseEventHandler, useCallback, - useEffect, useMemo, useRef, useState, @@ -16,16 +13,11 @@ import { useTranslation } from 'next-i18next'; import classNames from 'classnames'; import { - doesHaveDotsInTheEnd, hasInvalidNameInPath, isEntityNameInvalid, - isEntityNameOnSameLevelUnique, isEntityNameOrPathInvalid, - prepareEntityName, - trimEndDots, } from '@/src/utils/app/common'; import { getEntityNameError } from '@/src/utils/app/errors'; -import { notAllowedSymbolsRegex } from '@/src/utils/app/file'; import { isEntityIdExternal } from '@/src/utils/app/id'; import { hasParentWithFloatingOverlay } from '@/src/utils/app/modals'; import { MoveType, getDragImage } from '@/src/utils/app/move'; @@ -43,16 +35,13 @@ import { PublicationActions, PublicationSelectors, } from '@/src/store/publication/publication.reducers'; -import { UIActions } from '@/src/store/ui/ui.reducers'; -import SidebarActionButton from '@/src/components/Buttons/SidebarActionButton'; import { ConversationContextMenu } from '@/src/components/Chat/ConversationContextMenu'; import { PlaybackIcon } from '@/src/components/Chat/Playback/PlaybackIcon'; import { ReplayAsIsIcon } from '@/src/components/Chat/ReplayAsIsIcon'; import ShareIcon from '@/src/components/Common/ShareIcon'; import { ReviewDot } from '../Chat/Publish/ReviewDot'; -import { ConfirmDialog } from '../Common/ConfirmDialog'; import Tooltip from '../Common/Tooltip'; import { ModelIcon } from './ModelIcon'; @@ -180,10 +169,7 @@ export function ConversationView({ /> )} -
+
{conversation.name} @@ -215,11 +202,8 @@ export const ConversationComponent = ({ level, additionalItemData, }: Props) => { - const { t } = useTranslation(Translation.Chat); - const dispatch = useAppDispatch(); - const modelsMap = useAppSelector(ModelsSelectors.selectModelsMap); const selectedConversationIds = useAppSelector( ConversationsSelectors.selectSelectedConversationsIds, ); @@ -227,16 +211,9 @@ export const ConversationComponent = ({ const messageIsStreaming = useAppSelector( ConversationsSelectors.selectIsConversationsStreaming, ); - const allConversations = useAppSelector( - ConversationsSelectors.selectConversations, - ); - const [isRenaming, setIsRenaming] = useState(false); - const [renameValue, setRenameValue] = useState(''); const buttonRef = useRef(null); - const inputRef = useRef(null); const [isContextMenu, setIsContextMenu] = useState(false); - const [isConfirmRenaming, setIsConfirmRenaming] = useState(false); const isSelected = selectedConversationIds.includes(conversation.id); @@ -261,81 +238,6 @@ export const ConversationComponent = ({ const isExternal = isEntityIdExternal(conversation); - const performRename = useCallback( - (name: string) => { - if (name.length > 0) { - dispatch( - ConversationsActions.updateConversation({ - id: conversation.id, - values: { - name, - isNameChanged: true, - isShared: false, - }, - }), - ); - - setRenameValue(''); - setIsContextMenu(false); - } - - setIsRenaming(false); - }, - [conversation.id, dispatch], - ); - - const handleRename = useCallback(() => { - const newName = prepareEntityName(renameValue, { forRenaming: true }); - setRenameValue(newName); - - if ( - !isEntityNameOnSameLevelUnique(newName, conversation, allConversations) - ) { - dispatch( - UIActions.showErrorToast( - t( - 'Conversation with name "{{newName}}" already exists in this folder.', - { - ns: 'chat', - newName, - }, - ), - ), - ); - - return; - } - - if (doesHaveDotsInTheEnd(newName)) { - dispatch( - UIActions.showErrorToast( - t('Using a dot at the end of a name is not permitted.'), - ), - ); - return; - } - - if (conversation.isShared && newName !== conversation.name) { - setIsConfirmRenaming(true); - setIsContextMenu(false); - setIsRenaming(false); - return; - } - - performRename(trimEndDots(newName)); - }, [allConversations, conversation, dispatch, performRename, renameValue, t]); - - const handleEnterDown = useCallback( - (e: KeyboardEvent) => { - e.stopPropagation(); - if (e.key === 'Enter') { - e.preventDefault(); - handleRename(); - } - }, - [handleRename], - ); - const handleDragStart = useCallback( (e: DragEvent, conversation: ConversationInfo) => { if ( @@ -354,28 +256,6 @@ export const ConversationComponent = ({ [isConversationsStreaming, isExternal, isSelectMode], ); - const handleCancelRename: MouseEventHandler = useCallback( - (e) => { - e.stopPropagation(); - setIsRenaming(false); - }, - [], - ); - - const handleStartRename = useCallback(() => { - setIsRenaming(true); - setRenameValue(conversation.name); - }, [conversation.name]); - - useEffect(() => { - if (isRenaming) { - setTimeout(() => { - inputRef.current?.focus(); - inputRef.current?.select(); - }); // set auto-focus - } - }, [isRenaming]); - const handleContextMenuOpen = (e: MouseEvent) => { if (hasParentWithFloatingOverlay(e.target as Element)) { return; @@ -386,22 +266,12 @@ export const ConversationComponent = ({ }; const isHighlighted = !isSelectMode - ? (isSelected && - (!additionalItemData?.publicationUrl || - selectedPublicationUrl === additionalItemData.publicationUrl)) || - isRenaming + ? isSelected && + (!additionalItemData?.publicationUrl || + selectedPublicationUrl === additionalItemData.publicationUrl) : isChosen; const isNameOrPathInvalid = isEntityNameOrPathInvalid(conversation); - useEffect(() => { - if (isSelectMode) { - setIsRenaming(false); - } - }, [isSelectMode]); - - const iconSize = additionalItemData?.isSidePanelItem ? 24 : 18; - const strokeWidth = additionalItemData?.isSidePanelItem ? 1.5 : 2; - return (
- {isRenaming ? ( -
- - {conversation.isReplay && ( - - - - )} - - {conversation.isPlayback && ( - - - - )} - - {!conversation.isReplay && !conversation.isPlayback && ( - - )} - - - setRenameValue( - e.target.value.replaceAll(notAllowedSymbolsRegex, ''), - ) - } - onKeyDown={handleEnterDown} - autoFocus - ref={inputRef} - /> -
- ) : ( - - )} + }} + disabled={messageIsStreaming || (isSelectMode && isExternal)} + draggable={ + !isExternal && + !isNameOrPathInvalid && + !isSelectMode && + !isConversationsStreaming + } + onDragStart={(e) => handleDragStart(e, conversation)} + ref={buttonRef} + data-qa={isSelected ? 'selected' : null} + > + + - {!isSelectMode && !isRenaming && !messageIsStreaming && ( + {!isSelectMode && !messageIsStreaming && (
)} - - {isRenaming && ( -
- handleRename()} - dataQA="confirm-edit" - > - - - - - -
- )} - { - setIsConfirmRenaming(false); - - if (result) { - performRename( - prepareEntityName(renameValue, { forRenaming: true }), - ); - } - - setIsContextMenu(false); - setIsRenaming(false); - }} - />
); }; diff --git a/apps/chat/src/components/Chatbar/Conversations.tsx b/apps/chat/src/components/Chatbar/Conversations.tsx index af5406a92e..d626347197 100644 --- a/apps/chat/src/components/Chatbar/Conversations.tsx +++ b/apps/chat/src/components/Chatbar/Conversations.tsx @@ -1,4 +1,4 @@ -import { useEffect, useMemo, useState } from 'react'; +import { memo, useEffect, useMemo, useState } from 'react'; import { sortByDateAndName } from '@/src/utils/app/conversation'; import { getConversationRootId } from '@/src/utils/app/id'; @@ -33,7 +33,7 @@ interface SortedConversations { other: SortedBlock; } -export const Conversations = ({ conversations }: Props) => { +const _Conversations = ({ conversations }: Props) => { const [sortedConversations, setSortedConversations] = useState(); @@ -141,3 +141,5 @@ export const Conversations = ({ conversations }: Props) => {
); }; + +export const Conversations = memo(_Conversations); diff --git a/apps/chat/src/components/Chatbar/ConversationsRenderer.tsx b/apps/chat/src/components/Chatbar/ConversationsRenderer.tsx index 820e356c6f..7d900f5ef3 100644 --- a/apps/chat/src/components/Chatbar/ConversationsRenderer.tsx +++ b/apps/chat/src/components/Chatbar/ConversationsRenderer.tsx @@ -1,4 +1,4 @@ -import { useEffect, useMemo, useState } from 'react'; +import { useEffect, useState } from 'react'; import { useSectionToggle } from '@/src/hooks/useSectionToggle'; @@ -16,6 +16,10 @@ interface ConversationsRendererProps { label: string; } +const additionalConvData = { + isSidePanelItem: true, +}; + export const ConversationsRenderer = ({ conversations, label, @@ -31,41 +35,34 @@ export const ConversationsRenderer = ({ FeatureType.Chat, ); - const additionalConvData = useMemo( - () => ({ - isSidePanelItem: true, - }), - [], - ); - useEffect(() => { setIsSectionHighlighted( conversations.some((conv) => selectedConversationsIds.includes(conv.id)), ); }, [selectedConversationsIds, conversations]); + if (!conversations.length) { + return null; + } + return ( - <> - {conversations.length > 0 && ( - -
- {conversations.map((conversation) => ( - - ))} -
-
- )} - + +
+ {conversations.map((conversation) => ( + + ))} +
+
); }; diff --git a/apps/chat/src/components/Chatbar/ExportModal.tsx b/apps/chat/src/components/Chatbar/ExportModal.tsx index fa6ca6e521..0e45896f2f 100644 --- a/apps/chat/src/components/Chatbar/ExportModal.tsx +++ b/apps/chat/src/components/Chatbar/ExportModal.tsx @@ -3,23 +3,25 @@ import { useTranslation } from 'next-i18next'; import { ModalState } from '@/src/types/modal'; import { Translation } from '@/src/types/translation'; +import { OUTSIDE_PRESS } from '@/src/constants/modal'; + import Modal from '../Common/Modal'; interface Props { onExport: (args?: { withAttachments?: boolean }) => void; onClose: () => void; - isOpen: boolean; } -export const ExportModal = ({ onExport, onClose, isOpen }: Props) => { +export const ExportModal = ({ onExport, onClose }: Props) => { const { t } = useTranslation(Translation.SideBar); + return (

{t('Export')}

diff --git a/apps/chat/src/components/Common/ApplicationWizard/ApplicationWizard.tsx b/apps/chat/src/components/Common/ApplicationWizard/ApplicationWizard.tsx index 03a1ec2563..88c05f32f9 100644 --- a/apps/chat/src/components/Common/ApplicationWizard/ApplicationWizard.tsx +++ b/apps/chat/src/components/Common/ApplicationWizard/ApplicationWizard.tsx @@ -1,4 +1,3 @@ -import { UseDismissProps } from '@floating-ui/react'; import React, { useCallback, useMemo } from 'react'; import { ApplicationType } from '@/src/types/applications'; @@ -7,6 +6,8 @@ import { ModalState } from '@/src/types/modal'; import { ApplicationSelectors } from '@/src/store/application/application.reducers'; import { useAppSelector } from '@/src/store/hooks'; +import { MOUSE_OUTSIDE_PRESS_EVENT } from '@/src/constants/modal'; + import { ApplicationWizardHeader } from '@/src/components/Common/ApplicationWizard/ApplicationWizardHeader'; import { CodeAppView } from '@/src/components/Common/ApplicationWizard/CodeAppView/CodeAppView'; import { CustomAppView } from '@/src/components/Common/ApplicationWizard/CustomAppView'; @@ -14,7 +15,6 @@ import { QuickAppView } from '@/src/components/Common/ApplicationWizard/QuickApp import Modal from '@/src/components/Common/Modal'; import { Spinner } from '@/src/components/Common/Spinner'; -const modalDismissProps = { outsidePressEvent: 'mousedown' } as UseDismissProps; interface ApplicationWizardProps { isOpen: boolean; onClose: (value: boolean) => void; @@ -60,7 +60,7 @@ export const ApplicationWizard: React.FC = ({ onClose={handleClose} dataQa="application-dialog" containerClassName="flex w-full flex-col pt-2 md:grow-0 xl:max-w-[720px] 2xl:max-w-[780px] !bg-layer-2" - dismissProps={modalDismissProps} + dismissProps={MOUSE_OUTSIDE_PRESS_EVENT} hideClose > {isLoading ? ( diff --git a/apps/chat/src/components/Common/ApplicationWizard/CodeAppView/CodeEditor.tsx b/apps/chat/src/components/Common/ApplicationWizard/CodeAppView/CodeEditor.tsx index f020861a99..da90187bea 100644 --- a/apps/chat/src/components/Common/ApplicationWizard/CodeAppView/CodeEditor.tsx +++ b/apps/chat/src/components/Common/ApplicationWizard/CodeAppView/CodeEditor.tsx @@ -127,10 +127,11 @@ const editorOptions: monaco.editor.IStandaloneEditorConstructionOptions = { const CodeEditorView = ({ selectedFileId }: CodeEditorViewProps) => { const dispatch = useAppDispatch(); - - const fileContent = useAppSelector((state) => - CodeEditorSelectors.selectFileContent(state, selectedFileId), + const selectFileContentSelector = useMemo( + () => CodeEditorSelectors.selectFileContent(selectedFileId), + [selectedFileId], ); + const fileContent = useAppSelector(selectFileContentSelector); const isContentLoading = useAppSelector( CodeEditorSelectors.selectIsFileContentLoading, ); diff --git a/apps/chat/src/components/Common/ConfirmDialog.tsx b/apps/chat/src/components/Common/ConfirmDialog.tsx index ee37e3a8cd..a0ae5f57c4 100644 --- a/apps/chat/src/components/Common/ConfirmDialog.tsx +++ b/apps/chat/src/components/Common/ConfirmDialog.tsx @@ -2,6 +2,8 @@ import { useId, useRef } from 'react'; import { ModalState } from '@/src/types/modal'; +import { DISALLOW_INTERACTIONS } from '@/src/constants/modal'; + import Modal from '@/src/components/Common/Modal'; interface Props { @@ -34,7 +36,7 @@ export const ConfirmDialog = ({ onClose={() => onClose(false)} dataQa="confirmation-dialog" containerClassName="inline-block w-full min-w-[90%] px-3 py-4 md:p-6 text-center md:min-w-[300px] md:max-w-[500px]" - dismissProps={{ outsidePressEvent: 'mousedown', outsidePress: true }} + dismissProps={DISALLOW_INTERACTIONS} hideClose heading={heading} headingClassName={headingClassName} diff --git a/apps/chat/src/components/Common/ItemContextMenu.tsx b/apps/chat/src/components/Common/ItemContextMenu.tsx index a4cca3e269..f54d5fb3ec 100644 --- a/apps/chat/src/components/Common/ItemContextMenu.tsx +++ b/apps/chat/src/components/Common/ItemContextMenu.tsx @@ -99,6 +99,7 @@ export default function ItemContextMenu({ TriggerIcon, }: ItemContextMenuProps) { const { t } = useTranslation(Translation.SideBar); + const isPublishingEnabled = useAppSelector((state) => SettingsSelectors.selectIsPublishingEnabled(state, featureType), ); diff --git a/apps/chat/src/components/Common/MoveToFolderMobileModal.tsx b/apps/chat/src/components/Common/MoveToFolderMobileModal.tsx index 318f8db744..7fa509bf48 100644 --- a/apps/chat/src/components/Common/MoveToFolderMobileModal.tsx +++ b/apps/chat/src/components/Common/MoveToFolderMobileModal.tsx @@ -1,26 +1,27 @@ -import { FloatingOverlay } from '@floating-ui/react'; -import { IconFolderPlus, IconX } from '@tabler/icons-react'; -import { useCallback, useEffect } from 'react'; +import { IconFolderPlus } from '@tabler/icons-react'; +import { useCallback } from 'react'; import { useTranslation } from 'next-i18next'; import { FolderInterface, MoveToFolderProps } from '@/src/types/folder'; +import { ModalState } from '@/src/types/modal'; import { Translation } from '@/src/types/translation'; +import Modal from './Modal'; + interface MoveToFolderMobileModalProps { folders: FolderInterface[]; - onOpen?: () => void; onClose: () => void; onMoveToFolder: (args: { folderId?: string; isNewFolder?: boolean }) => void; } export const MoveToFolderMobileModal = ({ folders, - onOpen, onMoveToFolder, onClose, }: MoveToFolderMobileModalProps) => { const { t } = useTranslation(Translation.SideBar); + const handleMoveToFolder = useCallback( ({ isNewFolder, folderId }: MoveToFolderProps) => { onMoveToFolder({ isNewFolder, folderId }); @@ -29,18 +30,17 @@ export const MoveToFolderMobileModal = ({ [onMoveToFolder, onClose], ); - useEffect(() => { - onOpen?.(); - }, [onOpen]); - return ( - +
{t('Move to')} - - -
- + ); }; diff --git a/apps/chat/src/components/Common/ReplaceConfirmationModal/ReplaceConfirmationModal.tsx b/apps/chat/src/components/Common/ReplaceConfirmationModal/ReplaceConfirmationModal.tsx index 44aa182e2b..406cc15d2e 100644 --- a/apps/chat/src/components/Common/ReplaceConfirmationModal/ReplaceConfirmationModal.tsx +++ b/apps/chat/src/components/Common/ReplaceConfirmationModal/ReplaceConfirmationModal.tsx @@ -23,19 +23,26 @@ import { ImportExportSelectors, } from '@/src/store/import-export/importExport.reducers'; +import { OUTSIDE_PRESS_AND_MOUSE_EVENT } from '@/src/constants/modal'; + import Modal from '../Modal'; import { ReplaceSelector } from './Components'; import { ConversationsList } from './ConversationsList'; import { FilesList } from './FilesList'; import { PromptsList } from './PromptsList'; -interface Props { - isOpen: boolean; -} - export type OnItemEvent = (actionOption: string, entityId: unknown) => void; -export const ReplaceConfirmationModal = ({ isOpen }: Props) => { +export const ReplaceConfirmationModal = () => { + const isReplaceModalOpened = useAppSelector( + ImportExportSelectors.selectIsShowReplaceDialog, + ); + if (isReplaceModalOpened) { + return ; + } +}; + +export const ReplaceConfirmationModalView = () => { const { t } = useTranslation(Translation.Chat); const dispatch = useAppDispatch(); @@ -171,14 +178,14 @@ export const ReplaceConfirmationModal = ({ isOpen }: Props) => { return ( { return; }} hideClose dataQa="replace-confirmation-modal" containerClassName="flex w-full min-h-[595px] flex-col gap-4 pt-4 sm:w-[525px] md:pt-6" - dismissProps={{ outsidePressEvent: 'mousedown' }} + dismissProps={OUTSIDE_PRESS_AND_MOUSE_EVENT} >

diff --git a/apps/chat/src/components/Common/SelectFolder/SelectFolder.tsx b/apps/chat/src/components/Common/SelectFolder/SelectFolder.tsx index 27137a0168..4e8df52d91 100644 --- a/apps/chat/src/components/Common/SelectFolder/SelectFolder.tsx +++ b/apps/chat/src/components/Common/SelectFolder/SelectFolder.tsx @@ -6,6 +6,8 @@ import { useTranslation } from 'next-i18next'; import { ModalState } from '@/src/types/modal'; import { Translation } from '@/src/types/translation'; +import { OUTSIDE_PRESS_AND_MOUSE_EVENT } from '@/src/constants/modal'; + import Modal from '@/src/components/Common/Modal'; interface Props { @@ -33,7 +35,7 @@ export const SelectFolder = ({ onClose={onClose} dataQa={modalDataQa} containerClassName="flex flex-col gap-4 md:min-w-[425px] w-[525px] sm:w-[525px] max-w-full" - dismissProps={{ outsidePressEvent: 'mousedown', outsidePress: true }} + dismissProps={OUTSIDE_PRESS_AND_MOUSE_EVENT} >
diff --git a/apps/chat/src/components/Common/Tooltip.tsx b/apps/chat/src/components/Common/Tooltip.tsx index 9d212f125c..fcc009efbf 100644 --- a/apps/chat/src/components/Common/Tooltip.tsx +++ b/apps/chat/src/components/Common/Tooltip.tsx @@ -234,7 +234,11 @@ export default function Tooltip({ ...tooltipProps }: TooltipOptions) { if (hideTooltip || !tooltip) - return {children}; + return ( + + {children} + + ); return ( diff --git a/apps/chat/src/components/Files/AttachLinkDialog.tsx b/apps/chat/src/components/Files/AttachLinkDialog.tsx index bd84e0c9a2..b8170d2840 100644 --- a/apps/chat/src/components/Files/AttachLinkDialog.tsx +++ b/apps/chat/src/components/Files/AttachLinkDialog.tsx @@ -9,6 +9,8 @@ import { DialLink } from '@/src/types/files'; import { ModalState } from '@/src/types/modal'; import { Translation } from '@/src/types/translation'; +import { OUTSIDE_PRESS } from '@/src/constants/modal'; + import Modal from '@/src/components/Common/Modal'; import { FieldErrorMessage } from '../Common/Forms/FieldErrorMessage'; @@ -59,7 +61,7 @@ export const AttachLinkDialog = ({ onClose }: Props) => { overlayClassName="fixed inset-0" containerClassName="inline-block w-full overflow-y-auto px-3 py-4 align-bottom transition-all md:p-6 xl:max-h-[800px] xl:max-w-[720px] 2xl:max-w-[780px]" heading={t('Attach link')} - dismissProps={{ outsidePress: true }} + dismissProps={OUTSIDE_PRESS} >
diff --git a/apps/chat/src/components/Files/FileItemContextMenu.tsx b/apps/chat/src/components/Files/FileItemContextMenu.tsx index 6443e313bb..3e681ad1f0 100644 --- a/apps/chat/src/components/Files/FileItemContextMenu.tsx +++ b/apps/chat/src/components/Files/FileItemContextMenu.tsx @@ -55,9 +55,11 @@ export function FileItemContextMenu({ const isPublishingConversationEnabled = useAppSelector((state) => SettingsSelectors.selectIsPublishingEnabled(state, FeatureType.Chat), ); - const isCodeEditorFile = !!useAppSelector((state) => - CodeEditorSelectors.selectFileContent(state, file.id), + const selectFileContentSelector = useMemo( + () => CodeEditorSelectors.selectFileContent(file.id), + [file.id], ); + const isCodeEditorFile = !!useAppSelector(selectFileContentSelector); const menuItems: DisplayMenuItemProps[] = useMemo( () => [ diff --git a/apps/chat/src/components/Files/FileManagerModal.tsx b/apps/chat/src/components/Files/FileManagerModal.tsx index 0df9282a5a..b667445ac9 100644 --- a/apps/chat/src/components/Files/FileManagerModal.tsx +++ b/apps/chat/src/components/Files/FileManagerModal.tsx @@ -39,6 +39,7 @@ import { FilesActions, FilesSelectors } from '@/src/store/files/files.reducers'; import { useAppDispatch, useAppSelector } from '@/src/store/hooks'; import { ShareActions } from '@/src/store/share/share.reducers'; +import { OUTSIDE_PRESS_AND_MOUSE_EVENT } from '@/src/constants/modal'; import { ORGANIZATION_SECTION_NAME, SHARED_WITH_ME_SECTION_NAME, @@ -630,7 +631,7 @@ export const FileManagerModal = ({ onClose={() => onClose(false)} dataQa="file-manager-modal" containerClassName="flex flex-col gap-4 sm:w-[525px] w-full" - dismissProps={{ outsidePressEvent: 'mousedown', outsidePress: true }} + dismissProps={OUTSIDE_PRESS_AND_MOUSE_EVENT} >
diff --git a/apps/chat/src/components/Files/PreUploadModal.tsx b/apps/chat/src/components/Files/PreUploadModal.tsx index a036ad36a3..0c0e702acf 100644 --- a/apps/chat/src/components/Files/PreUploadModal.tsx +++ b/apps/chat/src/components/Files/PreUploadModal.tsx @@ -32,6 +32,8 @@ import { Translation } from '@/src/types/translation'; import { FilesActions, FilesSelectors } from '@/src/store/files/files.reducers'; import { useAppDispatch, useAppSelector } from '@/src/store/hooks'; +import { OUTSIDE_PRESS_AND_MOUSE_EVENT } from '@/src/constants/modal'; + import Modal from '@/src/components/Common/Modal'; import { ErrorMessage } from '../Common/ErrorMessage'; @@ -360,7 +362,7 @@ export const PreUploadDialog = ({ dataQa="pre-upload-modal" state={isOpen ? ModalState.OPENED : ModalState.CLOSED} onClose={() => onClose(false)} - dismissProps={{ outsidePressEvent: 'mousedown', outsidePress: true }} + dismissProps={OUTSIDE_PRESS_AND_MOUSE_EVENT} >
diff --git a/apps/chat/src/components/Header/BackToChat.tsx b/apps/chat/src/components/Header/BackToChat.tsx index 7cd0c34e67..46c8c83da5 100644 --- a/apps/chat/src/components/Header/BackToChat.tsx +++ b/apps/chat/src/components/Header/BackToChat.tsx @@ -1,7 +1,7 @@ import { IconMessage2 } from '@tabler/icons-react'; import { useTranslation } from 'next-i18next'; -import { useRouter } from 'next/router'; +import Link from 'next/link'; import { Translation } from '@/src/types/translation'; @@ -10,23 +10,20 @@ import Tooltip from '../Common/Tooltip'; export const BackToChat = () => { const { t } = useTranslation(Translation.Header); - const router = useRouter(); - return ( - + ); }; diff --git a/apps/chat/src/components/Header/Logo.tsx b/apps/chat/src/components/Header/Logo.tsx index c24333770c..cb316b8346 100644 --- a/apps/chat/src/components/Header/Logo.tsx +++ b/apps/chat/src/components/Header/Logo.tsx @@ -1,5 +1,10 @@ +import { MouseEventHandler } from 'react'; + +import Link from 'next/link'; import { useRouter } from 'next/router'; +import classNames from 'classnames'; + import { ApiUtils } from '@/src/utils/server/api'; import { @@ -23,16 +28,17 @@ export const Logo = () => { ConversationsSelectors.areConversationsUploaded, ); const customLogo = useAppSelector(UISelectors.selectCustomLogo); - const isCustomLogoFeatureEnabled = useAppSelector((state) => - SettingsSelectors.isFeatureEnabled(state, Feature.CustomLogo), + const enabledFeatures = useAppSelector( + SettingsSelectors.selectEnabledFeatures, ); - const messageIsStreaming = useAppSelector( - ConversationsSelectors.selectIsConversationsStreaming, + const isCustomLogoFeatureEnabled = enabledFeatures.has(Feature.CustomLogo); + const isNewConversationDisabled = enabledFeatures.has( + Feature.HideNewConversation, ); - const isNewConversationDisabled = useAppSelector((state) => - SettingsSelectors.isFeatureEnabled(state, Feature.HideNewConversation), + const messageIsStreaming = useAppSelector( + ConversationsSelectors.selectIsConversationsStreaming, ); const customLogoUrl = @@ -50,23 +56,28 @@ export const Logo = () => { dispatch(ConversationsActions.resetSearch()); }; - const handleLogoClick = () => { + const handleLogoClick: MouseEventHandler = (e) => { + if (messageIsStreaming) return e.preventDefault(); if (router.route === '/') createNewConversation(); else { - router.push('/').then(createNewConversation); + createNewConversation(); } }; return ( - + > ); }; diff --git a/apps/chat/src/components/Markdown/CodeBlock.tsx b/apps/chat/src/components/Markdown/CodeBlock.tsx index e089a08cac..aa4752f2e8 100644 --- a/apps/chat/src/components/Markdown/CodeBlock.tsx +++ b/apps/chat/src/components/Markdown/CodeBlock.tsx @@ -10,7 +10,10 @@ import { useTranslation } from 'next-i18next'; import classNames from 'classnames'; -import { programmingLanguages } from '@/src/utils/app/codeblock'; +import { + languageExtensionMapping, + languageNameMapping, +} from '@/src/utils/app/codeblock'; import { Translation } from '@/src/types/translation'; @@ -32,6 +35,10 @@ const codeBlockTheme: Record> = { light: oneLight, }; +export function currentDate() { + return new Date().toISOString().replaceAll(':', '-').replaceAll('.', '-'); +} + export const CodeBlock: FC = memo( ({ language, value, isInner, isLastMessageStreaming }) => { const { t } = useTranslation(Translation.Markdown); @@ -53,9 +60,11 @@ export const CodeBlock: FC = memo( }); }, [value]); + const displayLanguage = languageNameMapping[language] || language; + const downloadAsFile = useCallback(() => { - const fileExtension = programmingLanguages[language] || '.txt'; - const suggestedFileName = `ai-chat-code${fileExtension}`; + const fileExtension = languageExtensionMapping[displayLanguage] || '.txt'; + const suggestedFileName = `ai-chat-code-${currentDate()}${fileExtension}`; const fileName = window.prompt( t('Enter file name') || '', suggestedFileName, @@ -93,7 +102,7 @@ export const CodeBlock: FC = memo( : 'border-secondary bg-layer-1', )} > - {language} + {displayLanguage} {!isLastMessageStreaming && (
= memo(
{ } return ( -
+
{applicationLogs.split('\n').map((log, index) => (

{log}

@@ -128,11 +128,10 @@ export const ApplicationLogs = ({ }: ApplicationLogsProps) => { return ( diff --git a/apps/chat/src/components/Marketplace/SearchHeader.tsx b/apps/chat/src/components/Marketplace/SearchHeader.tsx index ae73347674..1852fa8e5c 100644 --- a/apps/chat/src/components/Marketplace/SearchHeader.tsx +++ b/apps/chat/src/components/Marketplace/SearchHeader.tsx @@ -85,16 +85,14 @@ export const SearchHeader = ({ const { t } = useTranslation(Translation.Marketplace); const dispatch = useAppDispatch(); - - const isCustomApplicationsEnabled = useAppSelector((state) => - SettingsSelectors.isFeatureEnabled(state, Feature.CustomApplications), - ); - const isQuickAppsEnabled = useAppSelector((state) => - SettingsSelectors.isFeatureEnabled(state, Feature.QuickApps), + const enabledFeatures = useAppSelector( + SettingsSelectors.selectEnabledFeatures, ); - const isCodeAppsEnabled = useAppSelector((state) => - SettingsSelectors.isFeatureEnabled(state, Feature.CodeApps), + const isCustomApplicationsEnabled = enabledFeatures.has( + Feature.CustomApplications, ); + const isQuickAppsEnabled = enabledFeatures.has(Feature.QuickApps); + const isCodeAppsEnabled = enabledFeatures.has(Feature.CodeApps); const searchTerm = useAppSelector(MarketplaceSelectors.selectSearchTerm); const selectedTab = useAppSelector(MarketplaceSelectors.selectSelectedTab); diff --git a/apps/chat/src/components/Promptbar/Promptbar.tsx b/apps/chat/src/components/Promptbar/Promptbar.tsx index 3cf22108ad..717ac85ae6 100644 --- a/apps/chat/src/components/Promptbar/Promptbar.tsx +++ b/apps/chat/src/components/Promptbar/Promptbar.tsx @@ -1,4 +1,4 @@ -import { DragEvent, useCallback } from 'react'; +import { DragEvent, useCallback, useMemo } from 'react'; import { useTranslation } from 'next-i18next'; @@ -19,6 +19,8 @@ import { } from '@/src/store/prompts/prompts.reducers'; import { UIActions, UISelectors } from '@/src/store/ui/ui.reducers'; +import { RECENT_PROMPTS_SECTION_NAME } from '@/src/constants/sections'; + import { PromptFolders } from './components/PromptFolders'; import { PromptModal } from './components/PromptModal'; import { PromptbarSettings } from './components/PromptbarSettings'; @@ -98,6 +100,7 @@ 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); @@ -106,13 +109,25 @@ const Promptbar = () => { PromptsSelectors.arePromptsUploaded, ); - const filteredPrompts = useAppSelector((state) => - PromptsSelectors.selectFilteredPrompts(state, myItemsFilters, searchTerm), + const collapsedSectionsSelector = useMemo( + () => UISelectors.selectCollapsedSections(FeatureType.Chat), + [], + ); + + const collapsedSections = useAppSelector(collapsedSectionsSelector); + + const filteredPromptsSelector = useMemo( + () => PromptsSelectors.selectFilteredPrompts(myItemsFilters, searchTerm), + [myItemsFilters, searchTerm], ); - const filteredFolders = useAppSelector((state) => - PromptsSelectors.selectFilteredFolders(state, myItemsFilters, searchTerm), + const filteredFoldersSelector = useMemo( + () => PromptsSelectors.selectFilteredFolders(myItemsFilters, searchTerm), + [myItemsFilters, searchTerm], ); + const filteredPrompts = useAppSelector(filteredPromptsSelector); + const filteredFolders = useAppSelector(filteredFoldersSelector); + const searchFilters = useAppSelector(PromptsSelectors.selectSearchFilters); const handleDrop = useCallback( @@ -143,6 +158,14 @@ const Promptbar = () => { return; } + dispatch( + UIActions.setCollapsedSections({ + featureType: FeatureType.Prompt, + collapsedSections: collapsedSections.filter( + (section) => section !== RECENT_PROMPTS_SECTION_NAME, + ), + }), + ); dispatch( PromptsActions.updatePrompt({ id: prompt.id, @@ -152,7 +175,7 @@ const Promptbar = () => { } } }, - [allPrompts, dispatch, t], + [allPrompts, collapsedSections, dispatch, t], ); const handleSearchTerm = useCallback( @@ -183,9 +206,9 @@ const Promptbar = () => { filteredFolders={filteredFolders} searchTerm={searchTerm} searchFilters={searchFilters} - handleSearchTerm={handleSearchTerm} - handleSearchFilters={handleSearchFilters} - handleDrop={handleDrop} + onSearchTerm={handleSearchTerm} + onSearchFilters={handleSearchFilters} + onDrop={handleDrop} footerComponent={} areEntitiesUploaded={areEntitiesUploaded} /> diff --git a/apps/chat/src/components/Promptbar/components/Prompt.tsx b/apps/chat/src/components/Promptbar/components/Prompt.tsx index f8003ec7f6..7ad72f9b28 100644 --- a/apps/chat/src/components/Promptbar/components/Prompt.tsx +++ b/apps/chat/src/components/Promptbar/components/Prompt.tsx @@ -14,6 +14,8 @@ import { useTranslation } from 'next-i18next'; import classNames from 'classnames'; +import { useScreenState } from '@/src/hooks/useScreenState'; + import { hasInvalidNameInPath, isEntityNameInvalid, @@ -33,7 +35,11 @@ import { MoveType, getDragImage } from '@/src/utils/app/move'; import { defaultMyItemsFilters } from '@/src/utils/app/search'; import { translate } from '@/src/utils/app/translation'; -import { AdditionalItemData, FeatureType } from '@/src/types/common'; +import { + AdditionalItemData, + FeatureType, + ScreenState, +} from '@/src/types/common'; import { MoveToFolderProps } from '@/src/types/folder'; import { Prompt, PromptInfo } from '@/src/types/prompt'; import { SharingType } from '@/src/types/share'; @@ -51,10 +57,11 @@ import { } from '@/src/store/publication/publication.reducers'; import { SettingsSelectors } from '@/src/store/settings/settings.reducers'; import { ShareActions } from '@/src/store/share/share.reducers'; -import { UIActions } from '@/src/store/ui/ui.reducers'; +import { UIActions, UISelectors } from '@/src/store/ui/ui.reducers'; import { stopBubbling } from '@/src/constants/chat'; import { DEFAULT_FOLDER_NAME } from '@/src/constants/default-ui-settings'; +import { PINNED_PROMPTS_SECTION_NAME } from '@/src/constants/sections'; import ItemContextMenu from '@/src/components/Common/ItemContextMenu'; import { MoveToFolderMobileModal } from '@/src/components/Common/MoveToFolderMobileModal'; @@ -83,14 +90,13 @@ export const PromptComponent = ({ const { t } = useTranslation(Translation.Chat); - const folders = useAppSelector((state) => - PromptsSelectors.selectFilteredFolders( - state, - defaultMyItemsFilters, - '', - true, - ), + const filteredFoldersSelector = useMemo( + () => + PromptsSelectors.selectFilteredFolders(defaultMyItemsFilters, '', true), + [], ); + + const folders = useAppSelector(filteredFoldersSelector); const { selectedPromptId, isSelectedPromptApproveRequiredResource } = useAppSelector(PromptsSelectors.selectSelectedPromptId); const selectedPublicationUrl = useAppSelector( @@ -113,6 +119,13 @@ export const PromptComponent = ({ SettingsSelectors.selectIsPublishingEnabled(state, FeatureType.Prompt), ); + const collapsedSectionsSelector = useMemo( + () => UISelectors.selectCollapsedSections(FeatureType.Chat), + [], + ); + + const collapsedSections = useAppSelector(collapsedSectionsSelector); + const isExternal = isEntityIdExternal(prompt); const isApproveRequiredResource = !!additionalItemData?.publicationUrl; const isPartOfSelectedPublication = @@ -135,6 +148,8 @@ export const PromptComponent = ({ const [isContextMenu, setIsContextMenu] = useState(false); const [isUnshareConfirmOpened, setIsUnshareConfirmOpened] = useState(false); + const screenState = useScreenState(); + const isChosen = useMemo( () => chosenPromptIds.includes(prompt.id), [chosenPromptIds, prompt.id], @@ -303,6 +318,15 @@ export const PromptComponent = ({ }), ); } + + dispatch( + UIActions.setCollapsedSections({ + featureType: FeatureType.Prompt, + collapsedSections: collapsedSections.filter( + (section) => section !== PINNED_PROMPTS_SECTION_NAME, + ), + }), + ); dispatch( PromptsActions.updatePrompt({ id: prompt.id, @@ -315,7 +339,7 @@ export const PromptComponent = ({ ); setIsContextMenu(false); }, - [allPrompts, dispatch, folders, prompt, t], + [allPrompts, collapsedSections, dispatch, folders, prompt, t], ); const handleClose = useCallback(() => { @@ -331,7 +355,7 @@ export const PromptComponent = ({ e.stopPropagation(); setIsContextMenu(true); }; - const isHighlited = !isSelectMode + const isHighlighted = !isSelectMode ? isDeleting || isRenaming || (showModal && isSelected) || isContextMenu : isChosen; @@ -360,6 +384,12 @@ export const PromptComponent = ({ } }, [isSelectMode]); + useEffect(() => { + if (screenState !== ScreenState.MOBILE) { + setIsShowMoveToModal(false); + } + }, [screenState]); + const handleToggle = useCallback(() => { PromptsActions.setChosenPrompts({ ids: [prompt.id] }); }, [prompt.id]); @@ -373,10 +403,10 @@ export const PromptComponent = ({ className={classNames( 'group relative flex size-full shrink-0 cursor-pointer items-center rounded border-l-2 pr-3 hover:bg-accent-primary-alpha disabled:cursor-not-allowed', !isSelectMode && '[&:not(:disabled)]:hover:pr-9', - !isSelectMode && isHighlited + !isSelectMode && isHighlighted ? 'border-l-accent-primary ' : 'border-l-transparent', - isHighlited && 'bg-accent-primary-alpha', + isHighlighted && 'bg-accent-primary-alpha', additionalItemData?.isSidePanelItem ? 'h-[34px]' : 'h-[30px]', )} onClick={() => { @@ -428,7 +458,7 @@ export const PromptComponent = ({
)} - { - setIsDeleting(false); - if (result) handleDelete(); - }} - /> + {isDeleting && ( + { + setIsDeleting(false); + if (result) handleDelete(); + }} + /> + )} {isUnshareConfirmOpened && ( - PromptsSelectors.selectFilteredPrompts(state, filters, searchTerm), - ); const allFolders = useAppSelector(PromptsSelectors.selectFolders); - const promptFolders = useAppSelector((state) => - PromptsSelectors.selectFilteredFolders( - state, - filters, - searchTerm, - includeEmpty, - ), - ); - const openedFoldersIds = useAppSelector((state) => - UISelectors.selectOpenedFoldersIds(state, FeatureType.Prompt), - ); - const loadingFolderIds = useAppSelector((state) => - PromptsSelectors.selectLoadingFolderIds(state), + const loadingFolderIds = useAppSelector( + PromptsSelectors.selectLoadingFolderIds, ); const isSelectMode = useAppSelector(PromptsSelectors.selectIsSelectMode); const selectedPrompts = useAppSelector(PromptsSelectors.selectSelectedItems); - const { fullyChosenFolderIds, partialChosenFolderIds } = useAppSelector( - (state) => PromptsSelectors.selectChosenFolderIds(state, prompts), - ); const emptyFoldersIds = useAppSelector(PromptsSelectors.selectEmptyFolderIds); const isFolderEmpty = useAppSelector((state) => PromptsSelectors.selectIsFolderEmpty(state, folder.id), ); + const filteredPromptsSelector = useMemo( + () => PromptsSelectors.selectFilteredPrompts(filters, searchTerm), + [filters, searchTerm], + ); + const prompts = useAppSelector(filteredPromptsSelector); + + const chosenFolderIdsSelector = useMemo( + () => PromptsSelectors.selectChosenFolderIds(prompts), + [prompts], + ); + const { fullyChosenFolderIds, partialChosenFolderIds } = useAppSelector( + chosenFolderIdsSelector, + ); + + const filteredFoldersSelector = useMemo( + () => + PromptsSelectors.selectFilteredFolders(filters, searchTerm, includeEmpty), + [filters, searchTerm, includeEmpty], + ); + const promptFolders = useAppSelector(filteredFoldersSelector); + + const openedFolderIdsSelector = useMemo( + () => UISelectors.selectOpenedFoldersIds(FeatureType.Prompt), + [], + ); + const openedFoldersIds = useAppSelector(openedFolderIdsSelector); + const additionalFolderData = useMemo( () => ({ selectedFolderIds: fullyChosenFolderIds, @@ -285,7 +302,7 @@ const PromptFolderTemplate = ({ ); }; -export const PromptSection = ({ +const _PromptSection = ({ name, filters, hideIfEmpty = true, @@ -297,17 +314,23 @@ export const PromptSection = ({ const [isSectionHighlighted, setIsSectionHighlighted] = useState(false); const searchTerm = useAppSelector(PromptsSelectors.selectSearchTerm); - const rootFolders = useAppSelector((state) => - PromptsSelectors.selectFilteredFolders( - state, - filters, - searchTerm, - showEmptyFolders, - ), + + const filteredPromptsSelector = useMemo( + () => PromptsSelectors.selectFilteredPrompts(filters, searchTerm), + [filters, searchTerm], ); - const prompts = useAppSelector((state) => - PromptsSelectors.selectFilteredPrompts(state, filters, searchTerm), + const filteredFoldersSelector = useMemo( + () => + PromptsSelectors.selectFilteredFolders( + filters, + searchTerm, + showEmptyFolders, + ), + [filters, searchTerm, showEmptyFolders], ); + + const rootFolders = useAppSelector(filteredFoldersSelector); + const prompts = useAppSelector(filteredPromptsSelector); const selectedFoldersIds = useAppSelector( PromptsSelectors.selectSelectedPromptFoldersIds, ); @@ -399,6 +422,8 @@ export const PromptSection = ({ ); }; +export const PromptSection = memo(_PromptSection); + export function PromptFolders() { const isFilterEmpty = useAppSelector( PromptsSelectors.selectIsEmptySearchFilter, @@ -412,14 +437,18 @@ export function PromptFolders() { const isPublishingEnabled = useAppSelector((state) => SettingsSelectors.selectIsPublishingEnabled(state, FeatureType.Prompt), ); - const publicationItems = useAppSelector((state) => - PublicationSelectors.selectFilteredPublications( - state, - publicationFeatureTypes, - true, - ), + + const publicationItemsSelector = useMemo( + () => + PublicationSelectors.selectFilteredPublications( + publicationFeatureTypes, + true, + ), + [], ); + const publicationItems = useAppSelector(publicationItemsSelector); + const toApproveFolderItem = { hidden: !publicationItems.length, name: APPROVE_REQUIRED_SECTION_NAME, diff --git a/apps/chat/src/components/Promptbar/components/PromptbarSettings.tsx b/apps/chat/src/components/Promptbar/components/PromptbarSettings.tsx index 90cddeefc2..e808346f62 100644 --- a/apps/chat/src/components/Promptbar/components/PromptbarSettings.tsx +++ b/apps/chat/src/components/Promptbar/components/PromptbarSettings.tsx @@ -43,10 +43,14 @@ export function PromptbarSettings() { PromptsSelectors.selectDoesAnyMyItemExist, ); const isSelectMode = useAppSelector(PromptsSelectors.selectIsSelectMode); - const collapsedSections = useAppSelector((state) => - UISelectors.selectCollapsedSections(state, FeatureType.Prompt), + + const collapsedSectionsSelector = useMemo( + () => UISelectors.selectCollapsedSections(FeatureType.Prompt), + [], ); + const collapsedSections = useAppSelector(collapsedSectionsSelector); + const deleteTerm = isSelectMode ? 'selected' : 'all'; const menuItems: DisplayMenuItemProps[] = useMemo( diff --git a/apps/chat/src/components/Settings/SettingDialog.tsx b/apps/chat/src/components/Settings/SettingDialog.tsx index ee64db3484..dc828a2a97 100644 --- a/apps/chat/src/components/Settings/SettingDialog.tsx +++ b/apps/chat/src/components/Settings/SettingDialog.tsx @@ -15,6 +15,8 @@ import { useAppDispatch, useAppSelector } from '@/src/store/hooks'; import { SettingsSelectors } from '@/src/store/settings/settings.reducers'; import { UIActions, UISelectors } from '@/src/store/ui/ui.reducers'; +import { OUTSIDE_PRESS_AND_MOUSE_EVENT } from '@/src/constants/modal'; + import Modal from '@/src/components/Common/Modal'; import { ToggleSwitchLabeled } from '../Common/ToggleSwitch/ToggleSwitchLabeled'; @@ -130,7 +132,7 @@ export const SettingDialog: FC = ({ open, onClose }) => { state={open ? ModalState.OPENED : ModalState.CLOSED} onClose={handleClose} initialFocus={saveBtnRef} - dismissProps={{ outsidePressEvent: 'mousedown', outsidePress: true }} + dismissProps={OUTSIDE_PRESS_AND_MOUSE_EVENT} >
- ) : null; + ); }; export default Sidebar; diff --git a/apps/chat/src/constants/modal.ts b/apps/chat/src/constants/modal.ts new file mode 100644 index 0000000000..228d3260c4 --- /dev/null +++ b/apps/chat/src/constants/modal.ts @@ -0,0 +1,32 @@ +import { UseDismissProps } from '@floating-ui/react'; + +export const MOUSE_OUTSIDE_PRESS_EVENT: Pick< + UseDismissProps, + 'outsidePressEvent' +> = { outsidePressEvent: 'mousedown' }; + +export const OUTSIDE_PRESS: Pick = { + outsidePress: true, +}; + +export const DISALLOW_OUTSIDE_PRESS: Pick = { + outsidePress: false, +}; + +export const ESCAPE_KEY_PRESS: Pick = { + escapeKey: true, +}; + +export const DISALLOW_ESCAPE_KEY_PRESS: Pick = { + escapeKey: false, +}; + +export const OUTSIDE_PRESS_AND_MOUSE_EVENT = { + ...MOUSE_OUTSIDE_PRESS_EVENT, + ...OUTSIDE_PRESS, +}; + +export const DISALLOW_INTERACTIONS = { + ...DISALLOW_OUTSIDE_PRESS, + ...DISALLOW_ESCAPE_KEY_PRESS, +}; diff --git a/apps/chat/src/hooks/usePromptSelection.ts b/apps/chat/src/hooks/usePromptSelection.ts index 2332faba84..10c87bc232 100644 --- a/apps/chat/src/hooks/usePromptSelection.ts +++ b/apps/chat/src/hooks/usePromptSelection.ts @@ -46,12 +46,16 @@ export const usePromptSelection = ( const dispatch = useDispatch(); const isLoading = useAppSelector(PromptsSelectors.isPromptLoading); - const promptResources = useAppSelector((state) => - PublicationSelectors.selectFilteredPublicationResources( - state, - publicationResourceTypesToFilter, - ), + + const promptResourcesSelector = useMemo( + () => + PublicationSelectors.selectFilteredPublicationResources( + publicationResourceTypesToFilter, + ), + [], ); + + const promptResources = useAppSelector(promptResourcesSelector); const prompts = useAppSelector(PromptsSelectors.selectPrompts); const publicVersionGroups = useAppSelector( PublicationSelectors.selectPublicVersionGroups, diff --git a/apps/chat/src/hooks/useSectionToggle.ts b/apps/chat/src/hooks/useSectionToggle.ts index 4b3cd197b9..de7131a007 100644 --- a/apps/chat/src/hooks/useSectionToggle.ts +++ b/apps/chat/src/hooks/useSectionToggle.ts @@ -1,4 +1,4 @@ -import { useCallback } from 'react'; +import { useCallback, useMemo } from 'react'; import { FeatureType } from '../types/common'; @@ -13,10 +13,13 @@ export const useSectionToggle = ( ) => { const dispatch = useAppDispatch(); - const collapsedSections = useAppSelector((state) => - UISelectors.selectCollapsedSections(state, featureType), + const collapsedSectionsSelector = useMemo( + () => UISelectors.selectCollapsedSections(featureType), + [featureType], ); + const collapsedSections = useAppSelector(collapsedSectionsSelector); + const handleToggle = useCallback( (isOpen: boolean) => { const newCollapsedSections = isOpen diff --git a/apps/chat/src/pages/index.tsx b/apps/chat/src/pages/index.tsx index c25f516949..c90af1231b 100644 --- a/apps/chat/src/pages/index.tsx +++ b/apps/chat/src/pages/index.tsx @@ -2,7 +2,6 @@ import { getCommonPageProps } from '@/src/utils/server/get-common-page-props'; import { ImportExportSelectors } from '../store/import-export/importExport.reducers'; import { MigrationSelectors } from '../store/migration/migration.reducers'; -import { ShareSelectors } from '../store/share/share.reducers'; import { useAppSelector } from '@/src/store/hooks'; import { SettingsSelectors, @@ -15,10 +14,9 @@ import { import { getLayout } from '@/src/pages/_app'; -import ShareModal from '../components/Chat/ShareModal'; +import { MainModalManager } from '../components/Chat/MainModalManager'; import { ImportExportLoader } from '../components/Chatbar/ImportExportLoader'; import { AnnouncementsBanner } from '../components/Common/AnnouncementBanner'; -import { ReplaceConfirmationModal } from '../components/Common/ReplaceConfirmationModal/ReplaceConfirmationModal'; import { Chat } from '@/src/components/Chat/Chat'; import { Migration } from '@/src/components/Chat/Migration/Migration'; import { MigrationFailedWindow } from '@/src/components/Chat/Migration/MigrationFailedModal'; @@ -40,9 +38,7 @@ function Home() { useCustomizations(); const isProfileOpen = useAppSelector(UISelectors.selectIsProfileOpen); - const isShareModalClosed = useAppSelector( - ShareSelectors.selectShareModalClosed, - ); + const enabledFeatures = useAppSelector( SettingsSelectors.selectEnabledFeatures, ); @@ -66,10 +62,6 @@ function Home() { ImportExportSelectors.selectIsLoadingImportExport, ); - const isReplaceModalOpened = useAppSelector( - ImportExportSelectors.selectIsShowReplaceDialog, - ); - if (conversationsToMigrateCount !== 0 || promptsToMigrateCount !== 0) { if ( conversationsToMigrateCount + promptsToMigrateCount === @@ -111,10 +103,7 @@ function Home() {
{enabledFeatures.has(Feature.PromptsSection) && } {isProfileOpen && } - {!isShareModalClosed && } - {isReplaceModalOpened && ( - - )} +
)} diff --git a/apps/chat/src/store/codeEditor/codeEditor.selectors.ts b/apps/chat/src/store/codeEditor/codeEditor.selectors.ts index 1abd250335..040d7218b4 100644 --- a/apps/chat/src/store/codeEditor/codeEditor.selectors.ts +++ b/apps/chat/src/store/codeEditor/codeEditor.selectors.ts @@ -18,12 +18,10 @@ export const selectModifiedFileIds = createSelector( }, ); -export const selectFileContent = createSelector( - [selectFilesContent, (_filesContent, id: string) => id], - (filesContent, id) => { - return filesContent.find((file) => file.id === id); - }, -); +export const selectFileContent = (fileId: string) => + createSelector([selectFilesContent], (filesContents) => { + return filesContents.find((file) => file.id === fileId); + }); export const selectIsFileContentLoading = createSelector( [rootSelector], diff --git a/apps/chat/src/store/conversations/conversations.epics.ts b/apps/chat/src/store/conversations/conversations.epics.ts index 3c8868d75f..614440a6de 100644 --- a/apps/chat/src/store/conversations/conversations.epics.ts +++ b/apps/chat/src/store/conversations/conversations.epics.ts @@ -784,9 +784,8 @@ const updateFolderEpic: AppEpic = (action$, state$) => state$.value, ); const openedFoldersIds = UISelectors.selectOpenedFoldersIds( - state$.value, FeatureType.Chat, - ); + )(state$.value); const selectedConversationsIds = ConversationsSelectors.selectSelectedConversationsIds(state$.value); @@ -1726,8 +1725,9 @@ const replayConversationEpic: AppEpic = (action$, state$) => }; const model = - ModelsSelectors.selectModel(state$.value, activeMessage.model.id) ?? - conv.model; + ModelsSelectors.selectModelsMap(state$.value)[ + activeMessage.model.id + ] ?? conv.model; const messages = conv.model.id !== model.id || @@ -2398,9 +2398,8 @@ const updateLocalConversationEpic: AppEpic = (action$, state$) => } const collapsedSections = UISelectors.selectCollapsedSections( - state$.value, FeatureType.Chat, - ); + )(state$.value); return concat( of( @@ -2545,9 +2544,8 @@ const uploadConversationsFromMultipleFoldersEpic: AppEpic = (action$, state$) => if (!!payload?.pathToSelectFrom && !!conversations.length) { const openedFolders = UISelectors.selectOpenedFoldersIds( - state$.value, FeatureType.Chat, - ); + )(state$.value); const topLevelConversation = conversations .filter((conv) => @@ -2755,9 +2753,8 @@ const toggleFolderEpic: AppEpic = (action$, state$) => filter(ConversationsActions.toggleFolder.match), switchMap(({ payload }) => { const openedFoldersIds = UISelectors.selectOpenedFoldersIds( - state$.value, FeatureType.Chat, - ); + )(state$.value); const isOpened = openedFoldersIds.includes(payload.id); const action = isOpened ? UIActions.closeFolder : UIActions.openFolder; @@ -2845,9 +2842,8 @@ const deleteChosenConversationsEpic: AppEpic = (action$, state$) => state$.value, ); const { fullyChosenFolderIds } = - ConversationsSelectors.selectChosenFolderIds( + ConversationsSelectors.selectChosenFolderIds(conversations)( state$.value, - conversations, ); const conversationIds = ConversationsSelectors.selectConversations( state$.value, diff --git a/apps/chat/src/store/conversations/conversations.reducers.ts b/apps/chat/src/store/conversations/conversations.reducers.ts index a6b7c1d850..c7c91d83d1 100644 --- a/apps/chat/src/store/conversations/conversations.reducers.ts +++ b/apps/chat/src/store/conversations/conversations.reducers.ts @@ -3,11 +3,10 @@ import { PlotParams } from 'react-plotly.js'; import { PayloadAction, createSlice } from '@reduxjs/toolkit'; import { combineEntities } from '@/src/utils/app/common'; -import { constructPath } from '@/src/utils/app/file'; import { addGeneratedFolderId, - getNextDefaultName, isFolderEmpty, + renameFolderWithChildren, } from '@/src/utils/app/folders'; import { getConversationRootId, @@ -15,15 +14,12 @@ import { isEntityIdLocal, } from '@/src/utils/app/id'; import { doesEntityContainSearchTerm } from '@/src/utils/app/search'; -import { translate } from '@/src/utils/app/translation'; import { Conversation } from '@/src/types/chat'; import { FolderInterface, FolderType } from '@/src/types/folder'; import { SearchFilters } from '@/src/types/search'; import { LastConversationSettings } from '@/src/types/settings'; -import { DEFAULT_FOLDER_NAME } from '@/src/constants/default-ui-settings'; - import * as ConversationsSelectors from './conversations.selectors'; import { ConversationsState } from './conversations.types'; @@ -65,6 +61,7 @@ const initialState: ConversationsState = { customAttachmentDataLoading: false, chosenConversationIds: [], chosenEmptyFoldersIds: [], + renamingConversationId: null, }; export const conversationsSlice = createSlice({ @@ -394,32 +391,19 @@ export const conversationsSlice = createSlice({ { payload, }: PayloadAction<{ - relativePath?: string; + name: string; + id: string; + folderId?: string; }>, ) => { - const folderName = getNextDefaultName( - translate(DEFAULT_FOLDER_NAME), - [ - ...state.temporaryFolders, - ...state.folders.filter((folder) => folder.publishedWithMe), - ], - 0, - false, - true, - ); - const id = constructPath( - payload.relativePath || getConversationRootId(), - folderName, - ); - state.temporaryFolders.push({ - id, - name: folderName, + id: payload.id, + name: payload.name, type: FolderType.Chat, - folderId: payload.relativePath || getConversationRootId(), + folderId: payload.folderId || getConversationRootId(), temporary: true, }); - state.newAddedFolderId = id; + state.newAddedFolderId = payload.id; }, deleteFolder: (state, _action: PayloadAction<{ folderId?: string }>) => state, @@ -439,13 +423,11 @@ export const conversationsSlice = createSlice({ { payload }: PayloadAction<{ folderId: string; name: string }>, ) => { state.newAddedFolderId = undefined; - const name = payload.name.trim(); - - state.temporaryFolders = state.temporaryFolders.map((folder) => - folder.id !== payload.folderId - ? folder - : { ...folder, name, id: constructPath(folder.folderId, name) }, - ); + state.temporaryFolders = renameFolderWithChildren({ + folderId: payload.folderId, + newName: payload.name, + folders: state.temporaryFolders, + }); }, resetNewFolderId: (state) => { state.newAddedFolderId = undefined; @@ -872,6 +854,12 @@ export const conversationsSlice = createSlice({ ) => { state.lastConversationSettings = payload; }, + setRenamingConversationId: ( + state, + { payload }: PayloadAction, + ) => { + state.renamingConversationId = payload; + }, }, }); diff --git a/apps/chat/src/store/conversations/conversations.selectors.ts b/apps/chat/src/store/conversations/conversations.selectors.ts index 0e7e598d48..791ab6513a 100644 --- a/apps/chat/src/store/conversations/conversations.selectors.ts +++ b/apps/chat/src/store/conversations/conversations.selectors.ts @@ -64,10 +64,8 @@ import uniqBy from 'lodash-es/uniqBy'; const rootSelector = (state: RootState): ConversationsState => state.conversations; -export const selectConversations = createSelector( - [rootSelector], - (state) => state.conversations, -); +export const selectConversations = (state: RootState): ConversationInfo[] => + state.conversations.conversations; export const selectNotExternalConversations = createSelector( [selectConversations], @@ -89,41 +87,38 @@ export const selectPublishedOrSharedByMeConversations = createSelector( (conversations) => conversations.filter((c) => c.isShared || c.isPublished), ); -export const selectFilteredConversations = createSelector( - [ - selectConversations, - (state) => PublicationSelectors.selectPublicVersionGroups(state), - (_state, filters: EntityFilters) => filters, - (_state, _filters: EntityFilters, searchTerm?: string) => searchTerm, - ( - _state, - _filters, - _searchTerm?: string, - ignoreFilters?: Partial<{ - ignoreSectionFilter: boolean; - ignoreVersionFilter: boolean; - }>, - ) => ignoreFilters, - ], - (conversations, versionGroups, filters, searchTerm, ignoreFilters) => { - return conversations.filter( - (conversation) => - isSearchTermMatched(conversation, searchTerm) && - isSearchFilterMatched(conversation, filters) && - isSectionFilterMatched( - conversation, - filters, - ignoreFilters?.ignoreSectionFilter, - ) && - isVersionFilterMatched( - conversation, - filters, - versionGroups, - ignoreFilters?.ignoreVersionFilter, - ), - ); - }, -); +export const selectFilteredConversations = ( + filters: EntityFilters, + searchTerm?: string, + ignoreFilters?: Partial<{ + ignoreSectionFilter: boolean; + ignoreVersionFilter: boolean; + }>, +) => + createSelector( + [ + selectConversations, + (state) => PublicationSelectors.selectPublicVersionGroups(state), + ], + (conversations, versionGroups) => { + return conversations.filter( + (conversation) => + isSearchTermMatched(conversation, searchTerm) && + isSearchFilterMatched(conversation, filters) && + isSectionFilterMatched( + conversation, + filters, + ignoreFilters?.ignoreSectionFilter, + ) && + isVersionFilterMatched( + conversation, + filters, + versionGroups, + ignoreFilters?.ignoreVersionFilter, + ), + ); + }, + ); export const selectFolders = createSelector( [rootSelector], @@ -149,47 +144,31 @@ export const selectEmptyFolderIds = createSelector( .map(({ id }) => id); }, ); - -export const selectFilteredFolders = createSelector( - [ - selectFolders, - selectEmptyFolderIds, - (_state, filters: EntityFilters) => filters, - (_state, _filters: EntityFilters, searchTerm?: string) => searchTerm, - ( - _state, - _filters: EntityFilters, - _searchTerm?: string, - includeEmptyFolders?: boolean, - ) => includeEmptyFolders, - ( - state, - filters: EntityFilters, - searchTerm?: string, - _includeEmptyFolders?: boolean, - ) => - selectFilteredConversations(state, filters, searchTerm, { - ignoreSectionFilter: true, - ignoreVersionFilter: true, +const ignoreFilters = { + ignoreSectionFilter: true, + ignoreVersionFilter: true, +}; +export const selectFilteredFolders = ( + filters: EntityFilters, + searchTerm?: string, + includeEmptyFolders?: boolean, +) => + createSelector( + [ + selectFolders, + selectEmptyFolderIds, + selectFilteredConversations(filters, searchTerm, ignoreFilters), + ], + (allFolders, emptyFolderIds, filteredConversations) => + getFilteredFolders({ + allFolders, + emptyFolderIds, + filters, + entities: filteredConversations, + searchTerm, + includeEmptyFolders, }), - ], - ( - allFolders, - emptyFolderIds, - filters, - searchTerm, - includeEmptyFolders, - filteredConversations, - ) => - getFilteredFolders({ - allFolders, - emptyFolderIds, - filters, - entities: filteredConversations, - searchTerm, - includeEmptyFolders, - }), -); + ); export const selectLastConversation = createSelector( [selectNotExternalConversations], @@ -505,11 +484,9 @@ export const selectMaximumAttachmentsAmount = createSelector( ); export const selectCanAttachLink = createSelector( - [ - (state) => SettingsSelectors.isFeatureEnabled(state, Feature.InputLinks), - selectSelectedConversationsModels, - ], - (inputLinksEnabled, models) => { + [SettingsSelectors.selectEnabledFeatures, selectSelectedConversationsModels], + (enabledFeatures, models) => { + const inputLinksEnabled = enabledFeatures.has(Feature.InputLinks); if (!inputLinksEnabled || models.length === 0) { return false; } @@ -530,11 +507,9 @@ export const selectCanAttachFolders = createSelector( ); export const selectCanAttachFile = createSelector( - [ - (state) => SettingsSelectors.isFeatureEnabled(state, Feature.InputFiles), - selectSelectedConversationsModels, - ], - (inputFilesEnabled, models) => { + [SettingsSelectors.selectEnabledFeatures, selectSelectedConversationsModels], + (enabledFeatures, models) => { + const inputFilesEnabled = enabledFeatures.has(Feature.InputFiles); if (!inputFilesEnabled || models.length === 0) { return false; } @@ -791,53 +766,47 @@ export const selectIsFolderEmpty = createSelector( }, ); -export const selectChosenFolderIds = createSelector( - [ - selectSelectedItems, - selectFolders, - selectEmptyFolderIds, - selectChosenEmptyFolderIds, - (_state, itemsShouldBeChosen: ShareEntity[]) => itemsShouldBeChosen, - ], - ( - selectedItems, - folders, - emptyFolderIds, - chosenEmptyFolderIds, - itemsShouldBeChosen, - ) => { - const fullyChosenFolderIds = folders - .map((folder) => `${folder.id}/`) - .filter( - (folderId) => - itemsShouldBeChosen.some((item) => item.id.startsWith(folderId)) || - chosenEmptyFolderIds.some((id) => id.startsWith(folderId)), - ) - .filter( - (folderId) => - itemsShouldBeChosen - .filter((item) => item.id.startsWith(folderId)) - .every((item) => selectedItems.includes(item.id)) && - emptyFolderIds - .filter((id) => id.startsWith(folderId)) - .every((id) => chosenEmptyFolderIds.includes(`${id}/`)), - ); - - const partialChosenFolderIds = folders - .map((folder) => `${folder.id}/`) - .filter( - (folderId) => - !selectedItems.some((chosenId) => folderId.startsWith(chosenId)) && - (selectedItems.some((chosenId) => chosenId.startsWith(folderId)) || - fullyChosenFolderIds.some((entityId) => - entityId.startsWith(folderId), - )) && - !fullyChosenFolderIds.includes(folderId), - ); - - return { fullyChosenFolderIds, partialChosenFolderIds }; - }, -); +export const selectChosenFolderIds = (itemsShouldBeChosen: ShareEntity[]) => + createSelector( + [ + selectSelectedItems, + selectFolders, + selectEmptyFolderIds, + selectChosenEmptyFolderIds, + ], + (selectedItems, folders, emptyFolderIds, chosenEmptyFolderIds) => { + const fullyChosenFolderIds = folders + .map((folder) => `${folder.id}/`) + .filter( + (folderId) => + itemsShouldBeChosen.some((item) => item.id.startsWith(folderId)) || + chosenEmptyFolderIds.some((id) => id.startsWith(folderId)), + ) + .filter( + (folderId) => + itemsShouldBeChosen + .filter((item) => item.id.startsWith(folderId)) + .every((item) => selectedItems.includes(item.id)) && + emptyFolderIds + .filter((id) => id.startsWith(folderId)) + .every((id) => chosenEmptyFolderIds.includes(`${id}/`)), + ); + + const partialChosenFolderIds = folders + .map((folder) => `${folder.id}/`) + .filter( + (folderId) => + !selectedItems.some((chosenId) => folderId.startsWith(chosenId)) && + (selectedItems.some((chosenId) => chosenId.startsWith(folderId)) || + fullyChosenFolderIds.some((entityId) => + entityId.startsWith(folderId), + )) && + !fullyChosenFolderIds.includes(folderId), + ); + + return { fullyChosenFolderIds, partialChosenFolderIds }; + }, + ); export const selectIsNewConversationUpdating = createSelector( [rootSelector], @@ -855,3 +824,14 @@ export const selectLastConversationSettings = createSelector( [rootSelector], (state) => state.lastConversationSettings, ); + +const selectRenamingConversationId = createSelector( + [rootSelector], + (state) => state.renamingConversationId, +); + +export const selectRenamingConversation = createSelector( + [selectConversations, selectRenamingConversationId], + (conversations, renamingConversationId) => + conversations.find((conv) => conv.id === renamingConversationId), +); diff --git a/apps/chat/src/store/conversations/conversations.types.ts b/apps/chat/src/store/conversations/conversations.types.ts index ca2fa1e0dd..c15c648c1c 100644 --- a/apps/chat/src/store/conversations/conversations.types.ts +++ b/apps/chat/src/store/conversations/conversations.types.ts @@ -39,4 +39,5 @@ export interface ConversationsState { chosenConversationIds: string[]; chosenEmptyFoldersIds: string[]; lastConversationSettings?: LastConversationSettings; + renamingConversationId?: string | null; } diff --git a/apps/chat/src/store/import-export/importExport.epics.ts b/apps/chat/src/store/import-export/importExport.epics.ts index abf6297f18..6e2fff00af 100644 --- a/apps/chat/src/store/import-export/importExport.epics.ts +++ b/apps/chat/src/store/import-export/importExport.epics.ts @@ -505,9 +505,8 @@ const uploadImportedConversationsEpic: AppEpic = (action$, state$) => ); const openedFolderIds = UISelectors.selectOpenedFoldersIds( - state$.value, FeatureType.Chat, - ); + )(state$.value); const isShowReplaceDialog = ImportExportSelectors.selectIsShowReplaceDialog(state$.value); @@ -594,9 +593,8 @@ const uploadImportedPromptsEpic: AppEpic = (action$, state$) => ), ); const openedFolderIds = UISelectors.selectOpenedFoldersIds( - state$.value, FeatureType.Prompt, - ); + )(state$.value); const isShowReplaceDialog = ImportExportSelectors.selectIsShowReplaceDialog(state$.value); diff --git a/apps/chat/src/store/models/models.reducers.ts b/apps/chat/src/store/models/models.reducers.ts index 25dfe1a922..fd0a74f3eb 100644 --- a/apps/chat/src/store/models/models.reducers.ts +++ b/apps/chat/src/store/models/models.reducers.ts @@ -330,13 +330,6 @@ const selectRecentModelsIds = createSelector([rootSelector], (state) => { return state.recentModelsIds; }); -const selectModel = createSelector( - [selectModelsMap, (_state, modelId: string) => modelId], - (modelsMap, modelId) => { - return modelsMap[modelId]; - }, -); - const selectRecentModels = createSelector( [selectRecentModelsIds, selectModelsMap], (recentModelsIds, modelsMap) => { @@ -395,7 +388,6 @@ export const ModelsSelectors = { selectRecentModelsIds, selectRecentModels, selectIsRecentModelsLoaded, - selectModel, selectModelsOnly, selectPublishRequestModels, selectPublishedApplicationIds, diff --git a/apps/chat/src/store/prompts/prompts.epics.ts b/apps/chat/src/store/prompts/prompts.epics.ts index 025174599f..53b522feab 100644 --- a/apps/chat/src/store/prompts/prompts.epics.ts +++ b/apps/chat/src/store/prompts/prompts.epics.ts @@ -106,9 +106,8 @@ const createNewPromptEpic: AppEpic = (action$, state$) => return PromptService.createPrompt(newPrompt).pipe( switchMap((apiPrompt) => { const collapsedSections = UISelectors.selectCollapsedSections( - state$.value, FeatureType.Prompt, - ); + )(state$.value); return concat( iif( @@ -392,9 +391,8 @@ const updateFolderEpic: AppEpic = (action$, state$) => const folders = PromptsSelectors.selectFolders(state$.value); const allPrompts = PromptsSelectors.selectPrompts(state$.value); const openedFoldersIds = UISelectors.selectOpenedFoldersIds( - state$.value, FeatureType.Prompt, - ); + )(state$.value); const { updatedFolders, updatedOpenedFoldersIds } = updateEntitiesFoldersAndIds( @@ -514,9 +512,8 @@ const toggleFolderEpic: AppEpic = (action$, state$) => filter(PromptsActions.toggleFolder.match), switchMap(({ payload }) => { const openedFoldersIds = UISelectors.selectOpenedFoldersIds( - state$.value, FeatureType.Prompt, - ); + )(state$.value); const isOpened = openedFoldersIds.includes(payload.id); const action = isOpened ? UIActions.closeFolder : UIActions.openFolder; return of( @@ -599,9 +596,8 @@ const uploadPromptsFromMultipleFoldersEpic: AppEpic = (action$, state$) => if (!!payload?.pathToSelectFrom && !!prompts.length) { const openedFolders = UISelectors.selectOpenedFoldersIds( - state$.value, FeatureType.Prompt, - ); + )(state$.value); const topLevelPrompt = prompts .filter((prompt) => prompt.id.startsWith(`${payload.pathToSelectFrom}/`), @@ -844,9 +840,8 @@ const deleteChosenPromptsEpic: AppEpic = (action$, state$) => state$.value, ); const { fullyChosenFolderIds } = PromptsSelectors.selectChosenFolderIds( - state$.value, prompts, - ); + )(state$.value); const promptIds = PromptsSelectors.selectPrompts(state$.value).map( (prompt) => prompt.id, ); diff --git a/apps/chat/src/store/prompts/prompts.reducers.ts b/apps/chat/src/store/prompts/prompts.reducers.ts index 08711f16cc..9b466fd516 100644 --- a/apps/chat/src/store/prompts/prompts.reducers.ts +++ b/apps/chat/src/store/prompts/prompts.reducers.ts @@ -1,23 +1,19 @@ import { PayloadAction, createSlice } from '@reduxjs/toolkit'; import { combineEntities } from '@/src/utils/app/common'; -import { constructPath } from '@/src/utils/app/file'; import { addGeneratedFolderId, - getNextDefaultName, isFolderEmpty, + renameFolderWithChildren, } from '@/src/utils/app/folders'; import { getPromptRootId, isEntityIdExternal } from '@/src/utils/app/id'; import { doesEntityContainSearchTerm } from '@/src/utils/app/search'; -import { translate } from '@/src/utils/app/translation'; import { FolderInterface, FolderType } from '@/src/types/folder'; import { Prompt, PromptInfo } from '@/src/types/prompt'; import { SearchFilters } from '@/src/types/search'; import '@/src/types/share'; -import { DEFAULT_FOLDER_NAME } from '@/src/constants/default-ui-settings'; - import * as PromptsSelectors from './prompts.selectors'; import { PromptsState } from './prompts.types'; @@ -205,32 +201,19 @@ export const promptsSlice = createSlice({ { payload, }: PayloadAction<{ - relativePath: string; + name: string; + id: string; + folderId?: string; }>, ) => { - const folderName = getNextDefaultName( - translate(DEFAULT_FOLDER_NAME), - [ - ...state.temporaryFolders, - ...state.folders.filter((folder) => folder.publishedWithMe), - ], - 0, - false, - true, - ); - const id = constructPath( - payload.relativePath || getPromptRootId(), - folderName, - ); - state.temporaryFolders.push({ - id, - name: folderName, + id: payload.id, + name: payload.name, type: FolderType.Prompt, - folderId: payload.relativePath || getPromptRootId(), + folderId: payload.folderId || getPromptRootId(), temporary: true, }); - state.newAddedFolderId = id; + state.newAddedFolderId = payload.id; }, deleteFolder: ( state, @@ -254,13 +237,11 @@ export const promptsSlice = createSlice({ { payload }: PayloadAction<{ folderId: string; name: string }>, ) => { state.newAddedFolderId = undefined; - const name = payload.name.trim(); - - state.temporaryFolders = state.temporaryFolders.map((folder) => - folder.id !== payload.folderId - ? folder - : { ...folder, name, id: constructPath(folder.folderId, name) }, - ); + state.temporaryFolders = renameFolderWithChildren({ + folderId: payload.folderId, + newName: payload.name, + folders: state.temporaryFolders, + }); }, resetNewFolderId: (state) => { state.newAddedFolderId = undefined; diff --git a/apps/chat/src/store/prompts/prompts.selectors.ts b/apps/chat/src/store/prompts/prompts.selectors.ts index ac80220abf..332e9d29ba 100644 --- a/apps/chat/src/store/prompts/prompts.selectors.ts +++ b/apps/chat/src/store/prompts/prompts.selectors.ts @@ -46,41 +46,42 @@ export const selectPrompts = createSelector([rootSelector], (state) => { return state.prompts; }); -export const selectFilteredPrompts = createSelector( - [ - selectPrompts, - PublicationSelectors.selectPublicVersionGroups, - (_state, filters: EntityFilters) => filters, - (_state, _filters: EntityFilters, searchTerm?: string) => searchTerm, - ( - _state, - _filters, - _searchTerm?: string, - ignoreFilters?: Partial<{ - ignoreSectionFilter: boolean; - ignoreVersionFilter: boolean; - }>, - ) => ignoreFilters, - ], - (prompts, versionGroups, filters, searchTerm, ignoreFilters) => { - return prompts.filter( - (prompt) => - isSearchTermMatched(prompt, searchTerm) && - isSearchFilterMatched(prompt, filters) && - isSectionFilterMatched( - prompt, - filters, - ignoreFilters?.ignoreSectionFilter, - ) && - isVersionFilterMatched( - prompt, - filters, - versionGroups, - ignoreFilters?.ignoreVersionFilter, - ), - ); - }, -); +export const selectSearchTerm = createSelector([rootSelector], (state) => { + return state.searchTerm; +}); + +export const selectFilteredPrompts = ( + filters: EntityFilters, + searchTerm?: string, + ignoreFilters?: Partial<{ + ignoreSectionFilter: boolean; + ignoreVersionFilter: boolean; + }>, +) => + createSelector( + [ + selectPrompts, + (state) => PublicationSelectors.selectPublicVersionGroups(state), + ], + (prompts, versionGroups) => { + return prompts.filter( + (prompt) => + isSearchTermMatched(prompt, searchTerm) && + isSearchFilterMatched(prompt, filters) && + isSectionFilterMatched( + prompt, + filters, + ignoreFilters?.ignoreSectionFilter, + ) && + isVersionFilterMatched( + prompt, + filters, + versionGroups, + ignoreFilters?.ignoreVersionFilter, + ), + ); + }, + ); export const selectPrompt = createSelector( [selectPrompts, (_state, promptId: string) => promptId], @@ -102,46 +103,30 @@ export const selectEmptyFolderIds = createSelector( }, ); -export const selectFilteredFolders = createSelector( - [ - selectFolders, - selectEmptyFolderIds, - (_state, filters: EntityFilters) => filters, - (_state, _filters: EntityFilters, searchTerm?: string) => searchTerm, - ( - _state, - _filters: EntityFilters, - _searchTerm?: string, - includeEmptyFolders?: boolean, - ) => includeEmptyFolders, - ( - state, - filters: EntityFilters, - searchTerm?: string, - _includeEmptyFolders?: boolean, - ) => - selectFilteredPrompts(state, filters, searchTerm, { +export const selectFilteredFolders = ( + filters: EntityFilters, + searchTerm?: string, + includeEmptyFolders?: boolean, +) => + createSelector( + [ + selectFolders, + selectEmptyFolderIds, + selectFilteredPrompts(filters, searchTerm, { ignoreSectionFilter: true, ignoreVersionFilter: true, }), - ], - ( - allFolders, - emptyFolderIds, - filters, - searchTerm, - includeEmptyFolders, - filteredPrompts, - ) => - getFilteredFolders({ - allFolders, - emptyFolderIds, - filters, - entities: filteredPrompts, - searchTerm, - includeEmptyFolders, - }), -); + ], + (allFolders, emptyFolderIds, filteredPrompts) => + getFilteredFolders({ + allFolders, + emptyFolderIds, + filters, + entities: filteredPrompts, + searchTerm, + includeEmptyFolders, + }), + ); export const selectParentFolders = createSelector( [selectFolders, (_state, folderId: string | undefined) => folderId], @@ -167,10 +152,6 @@ export const selectParentFoldersIds = createSelector( }, ); -export const selectSearchTerm = createSelector([rootSelector], (state) => { - return state.searchTerm; -}); - export const selectSearchFilters = createSelector( [rootSelector], (state) => state.searchFilters, @@ -225,15 +206,13 @@ export const selectSelectedPrompt = createSelector( ); export const selectSelectedPromptFoldersIds = createSelector( - [selectSelectedPrompt, (state) => state], - (prompt, state) => { - let selectedFolders: string[] = []; - - selectedFolders = prompt - ? selectedFolders.concat(selectParentFoldersIds(state, prompt.folderId)) - : []; + [selectSelectedPrompt, selectFolders], + (prompt, folders) => { + if (!prompt) return []; - return selectedFolders; + return getParentAndCurrentFoldersById(folders, prompt.folderId).map( + ({ id }) => id, + ); }, ); @@ -400,53 +379,47 @@ export const selectIsFolderEmpty = createSelector( }, ); -export const selectChosenFolderIds = createSelector( - [ - selectSelectedItems, - selectFolders, - selectEmptyFolderIds, - selectChosenEmptyFolderIds, - (_state, itemsShouldBeChosen: ShareEntity[]) => itemsShouldBeChosen, - ], - ( - selectedItems, - folders, - emptyFolderIds, - chosenEmptyFolderIds, - itemsShouldBeChosen, - ) => { - const fullyChosenFolderIds = folders - .map((folder) => `${folder.id}/`) - .filter( - (folderId) => - itemsShouldBeChosen.some((item) => item.id.startsWith(folderId)) || - chosenEmptyFolderIds.some((id) => id.startsWith(folderId)), - ) - .filter( - (folderId) => - itemsShouldBeChosen - .filter((item) => item.id.startsWith(folderId)) - .every((item) => selectedItems.includes(item.id)) && - emptyFolderIds - .filter((id) => id.startsWith(folderId)) - .every((id) => chosenEmptyFolderIds.includes(`${id}/`)), - ); - - const partialChosenFolderIds = folders - .map((folder) => `${folder.id}/`) - .filter( - (folderId) => - !selectedItems.some((chosenId) => folderId.startsWith(chosenId)) && - (selectedItems.some((chosenId) => chosenId.startsWith(folderId)) || - fullyChosenFolderIds.some((entityId) => - entityId.startsWith(folderId), - )) && - !fullyChosenFolderIds.includes(folderId), - ); - - return { fullyChosenFolderIds, partialChosenFolderIds }; - }, -); +export const selectChosenFolderIds = (itemsShouldBeChosen: ShareEntity[]) => + createSelector( + [ + selectSelectedItems, + selectFolders, + selectEmptyFolderIds, + selectChosenEmptyFolderIds, + ], + (selectedItems, folders, emptyFolderIds, chosenEmptyFolderIds) => { + const fullyChosenFolderIds = folders + .map((folder) => `${folder.id}/`) + .filter( + (folderId) => + itemsShouldBeChosen.some((item) => item.id.startsWith(folderId)) || + chosenEmptyFolderIds.some((id) => id.startsWith(folderId)), + ) + .filter( + (folderId) => + itemsShouldBeChosen + .filter((item) => item.id.startsWith(folderId)) + .every((item) => selectedItems.includes(item.id)) && + emptyFolderIds + .filter((id) => id.startsWith(folderId)) + .every((id) => chosenEmptyFolderIds.includes(`${id}/`)), + ); + + const partialChosenFolderIds = folders + .map((folder) => `${folder.id}/`) + .filter( + (folderId) => + !selectedItems.some((chosenId) => folderId.startsWith(chosenId)) && + (selectedItems.some((chosenId) => chosenId.startsWith(folderId)) || + fullyChosenFolderIds.some((entityId) => + entityId.startsWith(folderId), + )) && + !fullyChosenFolderIds.includes(folderId), + ); + + return { fullyChosenFolderIds, partialChosenFolderIds }; + }, + ); export const selectInitialized = createSelector( [rootSelector], diff --git a/apps/chat/src/store/publication/publication.selectors.ts b/apps/chat/src/store/publication/publication.selectors.ts index b94faabd1a..885a7ccf4d 100644 --- a/apps/chat/src/store/publication/publication.selectors.ts +++ b/apps/chat/src/store/publication/publication.selectors.ts @@ -18,17 +18,11 @@ export const selectPublications = createSelector([rootSelector], (state) => { return state.publications; }); -export const selectFilteredPublications = createSelector( - [ - selectPublications, - (_state, featureTypes: FeatureType[]) => featureTypes, - ( - _state, - _featureTypes: FeatureType[], - includeEmptyResourceTypes?: boolean, - ) => includeEmptyResourceTypes, - ], - (publications, featureTypes, includeEmptyResourceTypes) => { +export const selectFilteredPublications = ( + featureTypes: FeatureType[], + includeEmptyResourceTypes?: boolean, +) => + createSelector([selectPublications], (publications) => { return publications.filter( (publication) => publication.resourceTypes.some((resourceType) => @@ -40,17 +34,21 @@ export const selectFilteredPublications = createSelector( ) || (includeEmptyResourceTypes && !publication.resourceTypes.length), ); - }, -); - -export const selectFilteredPublicationResources = createSelector( - [selectFilteredPublications], - (filteredPublications) => { - return filteredPublications - .filter((publication) => publication.resources) - .flatMap((publication) => publication.resources) as PublicationResource[]; - }, -); + }); + +export const selectFilteredPublicationResources = ( + featureTypes: FeatureType[], +) => + createSelector( + [selectFilteredPublications(featureTypes)], + (filteredPublications) => { + return filteredPublications + .filter((publication) => publication.resources) + .flatMap( + (publication) => publication.resources, + ) as PublicationResource[]; + }, + ); export const selectSelectedPublicationUrl = createSelector( [rootSelector], diff --git a/apps/chat/src/store/settings/settings.reducers.ts b/apps/chat/src/store/settings/settings.reducers.ts index ccb8ead3e3..16f8c7c5b2 100644 --- a/apps/chat/src/store/settings/settings.reducers.ts +++ b/apps/chat/src/store/settings/settings.reducers.ts @@ -190,42 +190,37 @@ const selectIsolatedModelId = createSelector([rootSelector], (state) => { return state.isolatedModelId; }); -const isFeatureEnabled = createSelector( - [selectEnabledFeatures, (_, featureName: Feature) => featureName], - (enabledFeatures, featureName) => { - return enabledFeatures.has(featureName); - }, -); - -const selectIsPublishingEnabled = createSelector( - [selectEnabledFeatures, (_, featureType: FeatureType) => featureType], - (enabledFeatures, featureType) => { - switch (featureType) { - case FeatureType.Chat: - case FeatureType.File: - return enabledFeatures.has(Feature.ConversationsPublishing); - case FeatureType.Prompt: - return enabledFeatures.has(Feature.PromptsPublishing); - default: - return false; - } - }, -); +const isFeatureEnabled = (state: RootState, featureName: Feature) => + selectEnabledFeatures(state).has(featureName); + +const selectIsPublishingEnabled = ( + state: RootState, + featureType: FeatureType, +) => { + const enabledFeatures = SettingsSelectors.selectEnabledFeatures(state); + switch (featureType) { + case FeatureType.Chat: + case FeatureType.File: + return enabledFeatures.has(Feature.ConversationsPublishing); + case FeatureType.Prompt: + return enabledFeatures.has(Feature.PromptsPublishing); + default: + return false; + } +}; -const isSharingEnabled = createSelector( - [selectEnabledFeatures, (_, featureType: FeatureType) => featureType], - (enabledFeatures, featureType) => { - switch (featureType) { - case FeatureType.Chat: - return enabledFeatures.has(Feature.ConversationsSharing); - case FeatureType.Prompt: - return enabledFeatures.has(Feature.PromptsSharing); - - default: - return false; - } - }, -); +const isSharingEnabled = (state: RootState, featureType: FeatureType) => { + const enabledFeatures = SettingsSelectors.selectEnabledFeatures(state); + switch (featureType) { + case FeatureType.Chat: + return enabledFeatures.has(Feature.ConversationsSharing); + case FeatureType.Prompt: + return enabledFeatures.has(Feature.PromptsSharing); + + default: + return false; + } +}; const selectCodeWarning = createSelector([rootSelector], (state) => { return state.codeWarning; @@ -290,15 +285,13 @@ const selectMappedVisualizers = createSelector( }, ); -const selectIsCustomAttachmentType = createSelector( - [selectMappedVisualizers, (_state, attachmentType: string) => attachmentType], - (mappedVisualizers, attachmentType) => { +const selectIsCustomAttachmentType = (attachmentType: string) => + createSelector([selectMappedVisualizers], (mappedVisualizers) => { return ( mappedVisualizers && Object.prototype.hasOwnProperty.call(mappedVisualizers, attachmentType) ); - }, -); + }); const selectPublicationFilters = createSelector([rootSelector], (state) => { return state.publicationFilters; diff --git a/apps/chat/src/store/ui/ui.epics.ts b/apps/chat/src/store/ui/ui.epics.ts index 85986deba7..b630fed3d5 100644 --- a/apps/chat/src/store/ui/ui.epics.ts +++ b/apps/chat/src/store/ui/ui.epics.ts @@ -350,7 +350,9 @@ const setCollapsedSectionsEpic: AppEpic = (action$) => } if (payload.featureType === FeatureType.Prompt) { - DataService.setPromptCollapsedSections(payload.collapsedSections); + return DataService.setPromptCollapsedSections( + payload.collapsedSections, + ); } return DataService.setFileCollapsedSections(payload.collapsedSections); diff --git a/apps/chat/src/store/ui/ui.reducers.ts b/apps/chat/src/store/ui/ui.reducers.ts index 29f8d1c5b5..c4e029cd41 100644 --- a/apps/chat/src/store/ui/ui.reducers.ts +++ b/apps/chat/src/store/ui/ui.reducers.ts @@ -273,25 +273,14 @@ const selectAllOpenedFoldersIds = createSelector([rootSelector], (state) => { return state.openedFoldersIds; }); -const selectOpenedFoldersIds = createSelector( - [ - selectAllOpenedFoldersIds, - (_state, featureType: FeatureType) => featureType, - ], - (openedFoldersIds, featureType) => { +const selectOpenedFoldersIds = (featureType: FeatureType) => + createSelector([selectAllOpenedFoldersIds], (openedFoldersIds) => { return openedFoldersIds[featureType]; - }, -); -const selectIsFolderOpened = createSelector( - [ - (state, featureType: FeatureType) => - selectOpenedFoldersIds(state, featureType), - (_state, _featureType: FeatureType, id: string) => id, - ], - (ids, id): boolean => { + }); +const selectIsFolderOpened = (featureType: FeatureType, id: string) => + createSelector([selectOpenedFoldersIds(featureType)], (ids): boolean => { return ids.includes(id); - }, -); + }); const selectTextOfClosedAnnouncement = createSelector( [rootSelector], (state) => { @@ -327,12 +316,10 @@ export const selectIsAnyMenuOpen = createSelector( state.isProfileOpen, ); -export const selectCollapsedSections = createSelector( - [rootSelector, (_state, featureType: FeatureType) => featureType], - (state, featureType) => { +export const selectCollapsedSections = (featureType: FeatureType) => + createSelector([rootSelector], (state) => { return state.collapsedSections[featureType]; - }, -); + }); export const selectPreviousRoute = createSelector( [rootSelector], diff --git a/apps/chat/src/utils/app/codeblock.ts b/apps/chat/src/utils/app/codeblock.ts index 1bcd2c36cf..d26db4c898 100644 --- a/apps/chat/src/utils/app/codeblock.ts +++ b/apps/chat/src/utils/app/codeblock.ts @@ -1,6 +1,6 @@ type languageMap = Record; -export const programmingLanguages: languageMap = { +export const languageExtensionMapping: languageMap = { javascript: '.js', python: '.py', java: '.java', @@ -24,5 +24,10 @@ export const programmingLanguages: languageMap = { sql: '.sql', html: '.html', css: '.css', + bash: '.bash', // add more file extensions here, make sure the key is same as language prop in CodeBlock.tsx component }; + +export const languageNameMapping: languageMap = { + sh: 'shell', +}; diff --git a/apps/chat/src/utils/app/data/file-service.ts b/apps/chat/src/utils/app/data/file-service.ts index 7394dd986e..856e0e7403 100644 --- a/apps/chat/src/utils/app/data/file-service.ts +++ b/apps/chat/src/utils/app/data/file-service.ts @@ -10,6 +10,8 @@ import { import { FolderType } from '@/src/types/folder'; import { HTTPMethod } from '@/src/types/http'; +import { CLIENTDATA_PATH } from '@/src/constants/client-data'; + import { ApiUtils } from '../../server/api'; import { constructPath } from '../file'; import { getFileRootId } from '../id'; @@ -121,30 +123,37 @@ export class FileService { this.getListingUrl({ path: parentPath, resultQuery }), ).pipe( map((folders: BackendFileFolder[]) => { - return folders.map((folder): FileFolderInterface => { - const relativePath = folder.parentPath - ? ApiUtils.decodeApiUrl(folder.parentPath) - : undefined; - - return { - id: constructPath( - ApiKeys.Files, - folder.bucket, - relativePath, - folder.name, - ), - name: folder.name, - type: FolderType.File, - absolutePath: constructPath( - ApiKeys.Files, - folder.bucket, - relativePath, - ), - relativePath: relativePath, - folderId: constructPath(getFileRootId(folder.bucket), relativePath), - serverSynced: true, - }; - }); + return folders + .filter( + (folder) => !!folder.parentPath || folder.name !== CLIENTDATA_PATH, + ) + .map((folder): FileFolderInterface => { + const relativePath = folder.parentPath + ? ApiUtils.decodeApiUrl(folder.parentPath) + : undefined; + + return { + id: constructPath( + ApiKeys.Files, + folder.bucket, + relativePath, + folder.name, + ), + name: folder.name, + type: FolderType.File, + absolutePath: constructPath( + ApiKeys.Files, + folder.bucket, + relativePath, + ), + relativePath: relativePath, + folderId: constructPath( + getFileRootId(folder.bucket), + relativePath, + ), + serverSynced: true, + }; + }); }), ); } diff --git a/apps/chat/src/utils/app/folders.ts b/apps/chat/src/utils/app/folders.ts index 19b73bbd0c..188dc94f9c 100644 --- a/apps/chat/src/utils/app/folders.ts +++ b/apps/chat/src/utils/app/folders.ts @@ -14,7 +14,11 @@ import { EntityFilters } from '@/src/types/search'; import { DEFAULT_FOLDER_NAME } from '@/src/constants/default-ui-settings'; -import { doesHaveDotsInTheEnd, prepareEntityName } from './common'; +import { + doesHaveDotsInTheEnd, + prepareEntityName, + updateEntitiesFoldersAndIds, +} from './common'; import { isRootId } from './id'; import { @@ -25,6 +29,7 @@ import { UploadStatus, } from '@epam/ai-dial-shared'; import escapeRegExp from 'lodash-es/escapeRegExp'; +import groupBy from 'lodash-es/groupBy'; import sortBy from 'lodash-es/sortBy'; import uniq from 'lodash-es/uniq'; @@ -564,3 +569,33 @@ export const isFolderEmpty = ({ !entities.some((entity) => entity.folderId === id) ); }; + +export const renameFolderWithChildren = ({ + folderId, + newName, + folders, +}: { + folderId: string; + newName: string; + folders: FolderInterface[]; +}) => { + const { + target: [targetFolder], + otherFolders = [], + } = groupBy(folders, (f) => (f.id === folderId ? 'target' : 'otherFolders')); + + if (!targetFolder) return folders; + + const newFolder = addGeneratedFolderId({ + ...targetFolder, + name: newName.trim(), + }); + const { updatedFolders } = updateEntitiesFoldersAndIds( + [], + otherFolders, + (id) => updateMovedFolderId(folderId, newFolder.id, id), + [], + ); + + return updatedFolders.concat(newFolder); +}; diff --git a/package-lock.json b/package-lock.json index 4f53a15072..e91e20674c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "ai-dial-chat", - "version": "0.22.0-rc", + "version": "0.23.0-rc", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "ai-dial-chat", - "version": "0.22.0-rc", + "version": "0.23.0-rc", "hasInstallScript": true, "dependencies": { "@floating-ui/react": "^0.26.7", @@ -7620,6 +7620,11 @@ } } }, + "node_modules/@reduxjs/toolkit/node_modules/reselect": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-4.1.8.tgz", + "integrity": "sha512-ab9EmR80F/zQTMNeneUr4cv+jSwPJgIlvEmVwLerwrWVbpLlBuls9XHzIeTFy4cegU2NHBp3va0LKOzU5qFEYQ==" + }, "node_modules/@rollup/plugin-babel": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz", @@ -9800,9 +9805,9 @@ } }, "node_modules/@vitest/coverage-v8/node_modules/magic-string": { - "version": "0.30.15", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.15.tgz", - "integrity": "sha512-zXeaYRgZ6ldS1RJJUrMrYgNJ4fdwnyI6tVqoiIhyCyv5IVTK9BU8Ic2l253GGETQHxI4HNUwhJ3fjDhKqEoaAw==", + "version": "0.30.17", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", + "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==", "dev": true, "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0" @@ -9896,9 +9901,9 @@ } }, "node_modules/@vitest/snapshot/node_modules/magic-string": { - "version": "0.30.15", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.15.tgz", - "integrity": "sha512-zXeaYRgZ6ldS1RJJUrMrYgNJ4fdwnyI6tVqoiIhyCyv5IVTK9BU8Ic2l253GGETQHxI4HNUwhJ3fjDhKqEoaAw==", + "version": "0.30.17", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", + "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==", "dev": true, "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0" @@ -17072,10 +17077,9 @@ } }, "node_modules/is-core-module": { - "version": "2.15.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz", - "integrity": "sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==", - "license": "MIT", + "version": "2.16.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.0.tgz", + "integrity": "sha512-urTSINYfAYgcbLb0yDQ6egFm6h3Mo1DcF9EkyXSRjjzdHbsulg01qhwWuXdOoUBuTkbQ80KDboXa0vFJ+BDH+g==", "dependencies": { "hasown": "^2.0.2" }, @@ -19660,10 +19664,9 @@ "license": "BSD-2-Clause" }, "node_modules/maplibre-gl/node_modules/earcut": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/earcut/-/earcut-3.0.0.tgz", - "integrity": "sha512-41Fs7Q/PLq1SDbqjsgcY7GA42T0jvaCNGXgGtsNdvg+Yv8eIu06bxv4/PoREkZ9nMDNwnUSG9OFB9+yv8eKhDg==", - "license": "ISC" + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/earcut/-/earcut-3.0.1.tgz", + "integrity": "sha512-0l1/0gOjESMeQyYaK5IDiPNvFeu93Z/cO0TjZh9eZ1vyCtZnA7KMZ8rQggpsJHIbGSdrqYq9OhuveadOVHCshw==" }, "node_modules/maplibre-gl/node_modules/geojson-vt": { "version": "4.0.2", @@ -24699,19 +24702,12 @@ "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", "license": "MIT" }, - "node_modules/reselect": { - "version": "4.1.8", - "resolved": "https://registry.npmjs.org/reselect/-/reselect-4.1.8.tgz", - "integrity": "sha512-ab9EmR80F/zQTMNeneUr4cv+jSwPJgIlvEmVwLerwrWVbpLlBuls9XHzIeTFy4cegU2NHBp3va0LKOzU5qFEYQ==", - "license": "MIT" - }, "node_modules/resolve": { - "version": "1.22.8", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", - "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", - "license": "MIT", + "version": "1.22.9", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.9.tgz", + "integrity": "sha512-QxrmX1DzraFIi9PxdG5VkRfRwIgjwyud+z/iBwfRRrVmHc+P9Q7u2lSSpQ6bjr2gy5lrqIiU9vb6iAeGf2400A==", "dependencies": { - "is-core-module": "^2.13.0", + "is-core-module": "^2.16.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, @@ -28206,9 +28202,9 @@ } }, "node_modules/vitest/node_modules/magic-string": { - "version": "0.30.15", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.15.tgz", - "integrity": "sha512-zXeaYRgZ6ldS1RJJUrMrYgNJ4fdwnyI6tVqoiIhyCyv5IVTK9BU8Ic2l253GGETQHxI4HNUwhJ3fjDhKqEoaAw==", + "version": "0.30.17", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", + "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==", "dev": true, "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0" diff --git a/package.json b/package.json index 2b8cfc2c70..a42052bdd5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ai-dial-chat", - "version": "0.22.0-rc", + "version": "0.23.0-rc", "private": true, "scripts": { "nx": "nx",