From 80e87ece0177123a6d2e67557d484af26204bf1a Mon Sep 17 00:00:00 2001 From: Ilya Bondar Date: Wed, 5 Feb 2025 21:16:55 +0100 Subject: [PATCH 1/2] fix(chat): fix modal displaying logic (Issue #2878) (#3088) --- .../Chat/ChatInput/ChatInputFooter.tsx | 12 +++++-- ...ModalManager.jsx => ChatModalsManager.jsx} | 6 ++-- .../Chat/Migration/MigrationFailedModal.tsx | 18 +++++----- .../Chat/RenameConversationModal.tsx | 18 ++++------ .../src/components/Chat/ReportIssueDialog.tsx | 5 ++- .../components/Chat/RequestApiKeyDialog.tsx | 5 ++- apps/chat/src/components/Chat/ShareModal.tsx | 22 +++++------- .../Chat/__tests__/FooterMessage.test.tsx | 11 +++--- .../components/Chatbar/ImportExportLoader.tsx | 14 ++++---- .../src/components/Common/FooterMessage.tsx | 23 ++++++------- .../chat/src/components/Common/RenderWhen.tsx | 34 +++++++++++++++++++ .../ReplaceConfirmationModal.tsx | 18 ++++------ .../src/components/Common/ScreenRender.tsx | 22 ++++++++++++ .../src/components/Common/UnshareDialog.tsx | 17 ++++------ .../src/components/Header/User/UserMobile.tsx | 11 ++++-- apps/chat/src/pages/index.tsx | 22 +++--------- apps/chat/src/pages/marketplace/index.tsx | 10 ++---- apps/chat/src/store/share/share.selectors.ts | 4 +-- 18 files changed, 154 insertions(+), 118 deletions(-) rename apps/chat/src/components/Chat/{MainModalManager.jsx => ChatModalsManager.jsx} (78%) create mode 100644 apps/chat/src/components/Common/RenderWhen.tsx create mode 100644 apps/chat/src/components/Common/ScreenRender.tsx diff --git a/apps/chat/src/components/Chat/ChatInput/ChatInputFooter.tsx b/apps/chat/src/components/Chat/ChatInput/ChatInputFooter.tsx index 8373920e40..44bce515de 100644 --- a/apps/chat/src/components/Chat/ChatInput/ChatInputFooter.tsx +++ b/apps/chat/src/components/Chat/ChatInput/ChatInputFooter.tsx @@ -1,9 +1,17 @@ +import { ScreenState } from '@/src/types/common'; + import { FooterMessage } from '../../Common/FooterMessage'; +import { withRenderForScreen } from '../../Common/ScreenRender'; -export const ChatInputFooter = () => { +function ChatInputFooterView() { return (
); -}; +} + +export const ChatInputFooter = withRenderForScreen([ + ScreenState.TABLET, + ScreenState.DESKTOP, +])(ChatInputFooterView); diff --git a/apps/chat/src/components/Chat/MainModalManager.jsx b/apps/chat/src/components/Chat/ChatModalsManager.jsx similarity index 78% rename from apps/chat/src/components/Chat/MainModalManager.jsx rename to apps/chat/src/components/Chat/ChatModalsManager.jsx index e889897821..dd9640ed25 100644 --- a/apps/chat/src/components/Chat/MainModalManager.jsx +++ b/apps/chat/src/components/Chat/ChatModalsManager.jsx @@ -1,15 +1,17 @@ import { ReplaceConfirmationModal } from '../Common/ReplaceConfirmationModal/ReplaceConfirmationModal'; import { UnshareDialog } from '../Common/UnshareDialog'; +import { UserMobile } from '../Header/User/UserMobile'; import { RenameConversationModal } from './RenameConversationModal'; import { ShareModal } from './ShareModal'; -export const MainModalManager = () => { +export function ChatModalsManager() { return ( <> + ); -}; +} diff --git a/apps/chat/src/components/Chat/Migration/MigrationFailedModal.tsx b/apps/chat/src/components/Chat/Migration/MigrationFailedModal.tsx index b47e7624a3..5610245718 100644 --- a/apps/chat/src/components/Chat/Migration/MigrationFailedModal.tsx +++ b/apps/chat/src/components/Chat/Migration/MigrationFailedModal.tsx @@ -451,15 +451,15 @@ export const MigrationFailedWindow = ({ {t('contact us.')}

- {enabledFeatures.has(Feature.ReportAnIssue) && ( - { - setIsReportIssueDialogOpen(false); - router.replace(router.basePath); - }} - /> - )} + {enabledFeatures.has(Feature.ReportAnIssue) && + isReportIssueDialogOpen && ( + { + setIsReportIssueDialogOpen(false); + router.replace(router.basePath); + }} + /> + )} ); }; diff --git a/apps/chat/src/components/Chat/RenameConversationModal.tsx b/apps/chat/src/components/Chat/RenameConversationModal.tsx index b9f823ba8a..be062c4098 100644 --- a/apps/chat/src/components/Chat/RenameConversationModal.tsx +++ b/apps/chat/src/components/Chat/RenameConversationModal.tsx @@ -30,17 +30,9 @@ import { DISALLOW_INTERACTIONS } from '@/src/constants/modal'; import { ConfirmDialog } from '../Common/ConfirmDialog'; import { Modal } from '../Common/Modal'; +import { withRenderWhen } from '../Common/RenderWhen'; -export const RenameConversationModal = () => { - const renamingConversation = useAppSelector( - ConversationsSelectors.selectRenamingConversation, - ); - if (renamingConversation) { - return ; - } -}; - -const RenameConversationView = () => { +function RenameConversationView() { const { t } = useTranslation(Translation.Chat); const dispatch = useAppDispatch(); const inputRef = useRef(null); @@ -228,4 +220,8 @@ const RenameConversationView = () => { /> ); -}; +} + +export const RenameConversationModal = withRenderWhen( + ConversationsSelectors.selectRenamingConversation, +)(RenameConversationView); diff --git a/apps/chat/src/components/Chat/ReportIssueDialog.tsx b/apps/chat/src/components/Chat/ReportIssueDialog.tsx index 1761adcd17..39403cf5e1 100644 --- a/apps/chat/src/components/Chat/ReportIssueDialog.tsx +++ b/apps/chat/src/components/Chat/ReportIssueDialog.tsx @@ -30,11 +30,10 @@ import { Modal } from '@/src/components/Common/Modal'; import EmptyRequiredInputMessage from '../Common/EmptyRequiredInputMessage'; interface Props { - isOpen: boolean; onClose: () => void; } -export const ReportIssueDialog: FC = ({ isOpen, onClose }) => { +export const ReportIssueDialog: FC = ({ onClose }) => { const { t } = useTranslation(Translation.Settings); const isSuccessfullySent = useAppSelector( @@ -102,7 +101,7 @@ export const ReportIssueDialog: FC = ({ isOpen, onClose }) => { void; } -export const RequestAPIKeyDialog: FC = ({ isOpen, onClose }) => { +export const RequestAPIKeyDialog: FC = ({ onClose }) => { const { t } = useTranslation(Translation.Settings); const dispatch = useAppDispatch(); @@ -218,7 +217,7 @@ export const RequestAPIKeyDialog: FC = ({ isOpen, onClose }) => { { - const isShareModalClosed = useAppSelector( - ShareSelectors.selectShareModalClosed, - ); - if (!isShareModalClosed) { - return ; - } -}; - interface ShareAccessOptionProps { filterValue: string; selected: boolean; onSelect: (value: boolean) => void; } -const ShareAccessOption = ({ +function ShareAccessOption({ filterValue, selected, onSelect, -}: ShareAccessOptionProps) => { +}: ShareAccessOptionProps) { return ( ); -}; +} -export default function ShareModalView() { +export function ShareModalView() { const { t } = useTranslation(Translation.SideBar); const dispatch = useAppDispatch(); @@ -260,3 +252,7 @@ export default function ShareModalView() { ); } + +export const ShareModal = withRenderWhen(ShareSelectors.selectShareModalOpened)( + ShareModalView, +); diff --git a/apps/chat/src/components/Chat/__tests__/FooterMessage.test.tsx b/apps/chat/src/components/Chat/__tests__/FooterMessage.test.tsx index e2c37e05d8..f0e2a6984d 100644 --- a/apps/chat/src/components/Chat/__tests__/FooterMessage.test.tsx +++ b/apps/chat/src/components/Chat/__tests__/FooterMessage.test.tsx @@ -39,12 +39,11 @@ interface DialogProps { function makeMockDialog(dataTestId: string) { // eslint-disable-next-line react/display-name - return ({ isOpen, onClose }: DialogProps) => - isOpen ? ( -
- -
- ) : null; + return ({ onClose }: DialogProps) => ( +
+ +
+ ); } const reportIssueDialogTestId = 'reportIssueDialog'; const requestAPIKeyDialogTestId = 'requestAPIKeyDialog'; diff --git a/apps/chat/src/components/Chatbar/ImportExportLoader.tsx b/apps/chat/src/components/Chatbar/ImportExportLoader.tsx index 87997b2ef3..2ae7fa0272 100644 --- a/apps/chat/src/components/Chatbar/ImportExportLoader.tsx +++ b/apps/chat/src/components/Chatbar/ImportExportLoader.tsx @@ -12,11 +12,9 @@ import { } from '@/src/store/import-export/importExport.reducers'; import { FullPageLoader } from '../Common/FullPageLoader'; +import { withRenderWhen } from '../Common/RenderWhen'; -interface Props { - isOpen: boolean; -} -export const ImportExportLoader = ({ isOpen }: Props) => { +function ImportExportLoaderView() { const { t } = useTranslation(Translation.Chat); const dispatch = useAppDispatch(); const operationName = @@ -38,7 +36,7 @@ export const ImportExportLoader = ({ isOpen }: Props) => { return ( { return; }} @@ -46,4 +44,8 @@ export const ImportExportLoader = ({ isOpen }: Props) => { stopLabel={t(stopLabel)} /> ); -}; +} + +export const ImportExportLoader = withRenderWhen( + ImportExportSelectors.selectIsLoadingImportExport, +)(ImportExportLoaderView); diff --git a/apps/chat/src/components/Common/FooterMessage.tsx b/apps/chat/src/components/Common/FooterMessage.tsx index b861aed622..ffe51f206f 100644 --- a/apps/chat/src/components/Common/FooterMessage.tsx +++ b/apps/chat/src/components/Common/FooterMessage.tsx @@ -42,9 +42,8 @@ export const FooterMessage = () => { dangerouslySetInnerHTML={{ __html: footerHtmlMessage || '' }} > - {enabledFeatures.has(Feature.RequestApiKey) && ( + {enabledFeatures.has(Feature.RequestApiKey) && isRequestAPIDialogOpen && ( { setIsRequestAPIDialogOpen(false); window.location.hash = ''; @@ -52,16 +51,16 @@ export const FooterMessage = () => { }} /> )} - {enabledFeatures.has(Feature.ReportAnIssue) && ( - { - setIsReportIssueDialogOpen(false); - window.location.hash = ''; - resetHash(); - }} - /> - )} + {enabledFeatures.has(Feature.ReportAnIssue) && + isReportIssueDialogOpen && ( + { + setIsReportIssueDialogOpen(false); + window.location.hash = ''; + resetHash(); + }} + /> + )} ) : null; }; diff --git a/apps/chat/src/components/Common/RenderWhen.tsx b/apps/chat/src/components/Common/RenderWhen.tsx new file mode 100644 index 0000000000..4316478707 --- /dev/null +++ b/apps/chat/src/components/Common/RenderWhen.tsx @@ -0,0 +1,34 @@ +import { ComponentType, ReactNode } from 'react'; + +import { useAppSelector } from '@/src/store/hooks'; + +import { RootState } from '@/src/store'; + +interface RenderWhenProps { + children: ReactNode; + selector: (state: RootState) => unknown; +} + +export function RenderWhen({ selector, children }: RenderWhenProps) { + const shouldRender = useAppSelector(selector); + return shouldRender ? children : null; +} + +export function getComponentDisplayName( + WrappedComponent: ComponentType, +) { + return WrappedComponent.displayName || WrappedComponent.name || 'Component'; +} + +export function withRenderWhen(selector: (state: RootState) => unknown) { + return function (WrappedComponent: ComponentType) { + const ComponentWithRenderWhen = (props: T) => { + const shouldRender = useAppSelector(selector); + return shouldRender ? : null; + }; + + ComponentWithRenderWhen.displayName = `withRenderWhen(${getComponentDisplayName(WrappedComponent)})`; + + return ComponentWithRenderWhen; + }; +} diff --git a/apps/chat/src/components/Common/ReplaceConfirmationModal/ReplaceConfirmationModal.tsx b/apps/chat/src/components/Common/ReplaceConfirmationModal/ReplaceConfirmationModal.tsx index 6d6607d4a6..2ace521b14 100644 --- a/apps/chat/src/components/Common/ReplaceConfirmationModal/ReplaceConfirmationModal.tsx +++ b/apps/chat/src/components/Common/ReplaceConfirmationModal/ReplaceConfirmationModal.tsx @@ -26,6 +26,7 @@ import { import { OUTSIDE_PRESS_AND_MOUSE_EVENT } from '@/src/constants/modal'; import { Modal } from '../Modal'; +import { withRenderWhen } from '../RenderWhen'; import { ReplaceSelector } from './Components'; import { ConversationsList } from './ConversationsList'; import { FilesList } from './FilesList'; @@ -33,16 +34,7 @@ import { PromptsList } from './PromptsList'; export type OnItemEvent = (actionOption: string, entityId: unknown) => void; -export const ReplaceConfirmationModal = () => { - const isReplaceModalOpened = useAppSelector( - ImportExportSelectors.selectIsShowReplaceDialog, - ); - if (isReplaceModalOpened) { - return ; - } -}; - -export const ReplaceConfirmationModalView = () => { +export function ReplaceConfirmationModalView() { const { t } = useTranslation(Translation.Chat); const dispatch = useAppDispatch(); @@ -244,4 +236,8 @@ export const ReplaceConfirmationModalView = () => {
); -}; +} + +export const ReplaceConfirmationModal = withRenderWhen( + ImportExportSelectors.selectIsShowReplaceDialog, +)(ReplaceConfirmationModalView); diff --git a/apps/chat/src/components/Common/ScreenRender.tsx b/apps/chat/src/components/Common/ScreenRender.tsx new file mode 100644 index 0000000000..2c70b723cf --- /dev/null +++ b/apps/chat/src/components/Common/ScreenRender.tsx @@ -0,0 +1,22 @@ +import { ComponentType } from 'react'; + +import { useScreenState } from '@/src/hooks/useScreenState'; + +import { ScreenState } from '@/src/types/common'; + +import { getComponentDisplayName } from './RenderWhen'; + +export function withRenderForScreen(screenStates: ScreenState[]) { + return function (WrappedComponent: ComponentType) { + const ComponentWithRenderForScreen = (props: T) => { + const screenState = useScreenState(); + return screenStates.includes(screenState) ? ( + + ) : null; + }; + + ComponentWithRenderForScreen.displayName = `withRenderForScreen(${getComponentDisplayName(WrappedComponent)})`; + + return ComponentWithRenderForScreen; + }; +} diff --git a/apps/chat/src/components/Common/UnshareDialog.tsx b/apps/chat/src/components/Common/UnshareDialog.tsx index 0e6e506525..0e1a564a48 100644 --- a/apps/chat/src/components/Common/UnshareDialog.tsx +++ b/apps/chat/src/components/Common/UnshareDialog.tsx @@ -9,16 +9,9 @@ import { useAppDispatch, useAppSelector } from '@/src/store/hooks'; import { ShareActions, ShareSelectors } from '@/src/store/share/share.reducers'; import { ConfirmDialog } from './ConfirmDialog'; +import { withRenderWhen } from './RenderWhen'; -export const UnshareDialog = () => { - const unshareEntity = useAppSelector(ShareSelectors.selectUnshareModel); - - if (unshareEntity !== undefined) { - return ; - } -}; - -const UnshareDialogView = () => { +function UnshareDialogView() { const { t } = useTranslation(Translation.Common); const dispatch = useAppDispatch(); const unshareEntity = useAppSelector(ShareSelectors.selectUnshareModel); @@ -72,4 +65,8 @@ const UnshareDialogView = () => { onClose={handleConfirmUnshare} /> ); -}; +} + +export const UnshareDialog = withRenderWhen(ShareSelectors.selectUnshareModel)( + UnshareDialogView, +); diff --git a/apps/chat/src/components/Header/User/UserMobile.tsx b/apps/chat/src/components/Header/User/UserMobile.tsx index 64285907de..235208f3d7 100644 --- a/apps/chat/src/components/Header/User/UserMobile.tsx +++ b/apps/chat/src/components/Header/User/UserMobile.tsx @@ -8,17 +8,20 @@ import classNames from 'classnames'; import { useLogout } from '@/src/hooks/useLogout'; import { useTranslation } from '@/src/hooks/useTranslation'; +import { ScreenState } from '@/src/types/common'; import { Translation } from '@/src/types/translation'; import { useAppDispatch, useAppSelector } from '@/src/store/hooks'; import { SettingsSelectors } from '@/src/store/settings/settings.reducers'; -import { UIActions } from '@/src/store/ui/ui.reducers'; +import { UIActions, UISelectors } from '@/src/store/ui/ui.reducers'; import { ConfirmDialog } from '@/src/components/Common/ConfirmDialog'; import { FooterMessage } from '@/src/components/Common/FooterMessage'; import LogOutIcon from '../../../../public/images/icons/log-out.svg'; import UserIcon from '../../../../public/images/icons/user.svg'; +import { withRenderWhen } from '../../Common/RenderWhen'; +import { withRenderForScreen } from '../../Common/ScreenRender'; import { Inversify } from '@epam/ai-dial-modulify-ui'; @@ -117,7 +120,7 @@ const UserMenu = () => { ); }; -export const UserMobile = Inversify.register('UserMobile', () => { +const UserMobileView = Inversify.register('UserMobile', () => { const isOverlay = useAppSelector(SettingsSelectors.selectIsOverlay); return ( @@ -137,3 +140,7 @@ export const UserMobile = Inversify.register('UserMobile', () => { ); }); + +export const UserMobile = withRenderForScreen([ScreenState.MOBILE])( + withRenderWhen(UISelectors.selectIsProfileOpen)(UserMobileView), +); diff --git a/apps/chat/src/pages/index.tsx b/apps/chat/src/pages/index.tsx index c90af1231b..c7dbbec4cc 100644 --- a/apps/chat/src/pages/index.tsx +++ b/apps/chat/src/pages/index.tsx @@ -1,20 +1,16 @@ 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 { useAppSelector } from '@/src/store/hooks'; import { SettingsSelectors, SettingsState, } from '@/src/store/settings/settings.reducers'; -import { - UISelectors, - selectShowSelectToMigrateWindow, -} from '@/src/store/ui/ui.reducers'; +import { selectShowSelectToMigrateWindow } from '@/src/store/ui/ui.reducers'; import { getLayout } from '@/src/pages/_app'; -import { MainModalManager } from '../components/Chat/MainModalManager'; +import { ChatModalsManager } from '../components/Chat/ChatModalsManager'; import { ImportExportLoader } from '../components/Chatbar/ImportExportLoader'; import { AnnouncementsBanner } from '../components/Common/AnnouncementBanner'; import { Chat } from '@/src/components/Chat/Chat'; @@ -22,7 +18,6 @@ import { Migration } from '@/src/components/Chat/Migration/Migration'; import { MigrationFailedWindow } from '@/src/components/Chat/Migration/MigrationFailedModal'; import { Chatbar } from '@/src/components/Chatbar/Chatbar'; import Header from '@/src/components/Header/Header'; -import { UserMobile } from '@/src/components/Header/User/UserMobile'; import Promptbar from '@/src/components/Promptbar'; import { useCustomizations } from '@/src/customizations'; @@ -37,8 +32,6 @@ export interface HomeProps { function Home() { useCustomizations(); - const isProfileOpen = useAppSelector(UISelectors.selectIsProfileOpen); - const enabledFeatures = useAppSelector( SettingsSelectors.selectEnabledFeatures, ); @@ -58,9 +51,6 @@ function Home() { const showSelectToMigrateWindow = useAppSelector( selectShowSelectToMigrateWindow, ); - const isImportingExporting = useAppSelector( - ImportExportSelectors.selectIsLoadingImportExport, - ); if (conversationsToMigrateCount !== 0 || promptsToMigrateCount !== 0) { if ( @@ -96,14 +86,10 @@ function Home() {
- - {isImportingExporting && ( - - )} +
{enabledFeatures.has(Feature.PromptsSection) && } - {isProfileOpen && } - + )} diff --git a/apps/chat/src/pages/marketplace/index.tsx b/apps/chat/src/pages/marketplace/index.tsx index 8c9aa3e84e..96d7a423d8 100644 --- a/apps/chat/src/pages/marketplace/index.tsx +++ b/apps/chat/src/pages/marketplace/index.tsx @@ -6,14 +6,11 @@ import { getCommonPageProps } from '@/src/utils/server/get-common-page-props'; import { useAppSelector } from '@/src/store/hooks'; import { SettingsSelectors } from '@/src/store/settings/settings.reducers'; -import { UISelectors } from '@/src/store/ui/ui.reducers'; import { getLayout } from '@/src/pages/_app'; -import { ShareModal } from '@/src/components/Chat/ShareModal'; +import { ChatModalsManager } from '@/src/components/Chat/ChatModalsManager'; import Loader from '@/src/components/Common/Loader'; -import { UnshareDialog } from '@/src/components/Common/UnshareDialog'; -import { UserMobile } from '@/src/components/Header/User/UserMobile'; import { Marketplace as MarketplaceView } from '@/src/components/Marketplace/Marketplace'; import { MarketplaceFilterbar } from '@/src/components/Marketplace/MarketplaceFilterbar'; import { MarketplaceHeader } from '@/src/components/Marketplace/MarketplaceHeader'; @@ -21,7 +18,6 @@ import { MarketplaceHeader } from '@/src/components/Marketplace/MarketplaceHeade import { Feature } from '@epam/ai-dial-shared'; function Marketplace() { - const isProfileOpen = useAppSelector(UISelectors.selectIsProfileOpen); const isMarketplaceEnabled = useAppSelector((state) => SettingsSelectors.isFeatureEnabled(state, Feature.Marketplace), ); @@ -43,9 +39,7 @@ function Marketplace() { - {isProfileOpen && } - - + ); diff --git a/apps/chat/src/store/share/share.selectors.ts b/apps/chat/src/store/share/share.selectors.ts index 7a5b96110b..786fad39f9 100644 --- a/apps/chat/src/store/share/share.selectors.ts +++ b/apps/chat/src/store/share/share.selectors.ts @@ -21,10 +21,10 @@ export const selectWriteInvitationId = createSelector( export const selectShareModalState = createSelector([rootSelector], (state) => { return state.shareModalState; }); -export const selectShareModalClosed = createSelector( +export const selectShareModalOpened = createSelector( [rootSelector], (state) => { - return state.shareModalState === ModalState.CLOSED; + return state.shareModalState !== ModalState.CLOSED; }, ); From 1b64e07e7de5b238e8c61602a2aaa3bdc81687a1 Mon Sep 17 00:00:00 2001 From: Ilya Bondar Date: Thu, 6 Feb 2025 00:13:46 +0100 Subject: [PATCH 2/2] feat(chat): implement `use prompt` context menu (Issue #3065) (#3100) --- .../src/testData/expectedConstants.ts | 1 + apps/chat-e2e/src/tests/prompts.test.ts | 1 + .../public/images/icons/insert-prompt.svg | 3 + apps/chat/src/components/Chat/Chat.tsx | 11 +- .../src/components/Chat/ChatCompareRotate.tsx | 2 +- .../src/components/Chat/ChatHeader/Header.tsx | 3 +- .../Chat/ChatInput/ChatControls.tsx | 2 +- .../Chat/ChatInput/ChatInputMessage.tsx | 8 +- .../PromptVariablesForApplyDialog.tsx | 42 +++ .../ChatMessageContent/AssistantMessage.tsx | 3 +- .../src/components/Chat/ChatModalsManager.jsx | 2 + .../Chat/ChatSettings/ChatSettingsModal.tsx | 3 +- .../ChatSettings/ConversationSettings.tsx | 3 +- .../Chat/ConversationContextMenu.tsx | 8 +- .../components/Chat/EmptyChatDescription.tsx | 10 +- .../src/components/Chat/MessageAttachment.tsx | 4 +- .../components/Chat/MessageAttachments.tsx | 2 +- .../chat/src/components/Chat/MessageStage.tsx | 4 +- .../src/components/Chat/TalkTo/TalkToCard.tsx | 7 +- .../components/Chat/TalkTo/TalkToModal.tsx | 15 +- .../components/Chat/TalkTo/TalkToSlider.tsx | 12 +- .../src/components/Chatbar/Conversation.tsx | 12 +- apps/chat/src/components/Common/Combobox.tsx | 3 +- .../src/components/Common/ItemContextMenu.tsx | 15 ++ .../src/components/Common/NoResultsFound.tsx | 2 +- .../ReplaceConfirmationModal/Components.tsx | 12 +- .../src/components/Files/FileManagerModal.tsx | 2 +- apps/chat/src/components/Folder/Folder.tsx | 13 +- apps/chat/src/components/Header/Header.tsx | 4 +- .../components/Header/User/ProfileButton.tsx | 2 +- .../components/Header/User/UserDesktop.tsx | 7 +- .../src/components/Markdown/CodeBlock.tsx | 3 +- .../Marketplace/MarketplaceHeader.tsx | 5 +- .../components/Marketplace/TabRenderer.tsx | 2 +- .../src/components/Promptbar/Promptbar.tsx | 3 +- .../Promptbar/components/Prompt.tsx | 61 +++-- apps/chat/src/store/chat/chat.reducer.ts | 3 + apps/chat/src/store/chat/chat.selectors.ts | 24 +- .../conversations/conversations.epics.ts | 13 +- .../conversations/conversations.selectors.ts | 247 +++++++----------- .../store/import-export/importExport.epics.ts | 8 +- apps/chat/src/store/prompts/prompts.epics.ts | 61 +++-- .../src/store/prompts/prompts.reducers.ts | 7 + .../src/store/prompts/prompts.selectors.ts | 101 +++---- apps/chat/src/store/prompts/prompts.types.ts | 3 +- .../store/publication/publication.epics.ts | 8 +- apps/chat/src/utils/app/conversation.ts | 26 +- .../storages/api/conversation-api-storage.ts | 9 +- .../data/storages/api/prompt-api-storage.ts | 9 +- apps/chat/src/utils/app/folders.ts | 3 +- apps/chat/src/utils/server/api.ts | 10 +- 51 files changed, 447 insertions(+), 377 deletions(-) create mode 100644 apps/chat/public/images/icons/insert-prompt.svg create mode 100644 apps/chat/src/components/Chat/ChatInput/PromptVariablesForApplyDialog.tsx diff --git a/apps/chat-e2e/src/testData/expectedConstants.ts b/apps/chat-e2e/src/testData/expectedConstants.ts index c2f4fc78c0..4727308b60 100644 --- a/apps/chat-e2e/src/testData/expectedConstants.ts +++ b/apps/chat-e2e/src/testData/expectedConstants.ts @@ -259,6 +259,7 @@ export enum MenuOptions { attachLink = 'Attach link', select = 'Select', view = 'View', + use = 'Use', } export enum FilterMenuOptions { diff --git a/apps/chat-e2e/src/tests/prompts.test.ts b/apps/chat-e2e/src/tests/prompts.test.ts index 0add2311cb..afc97d13fc 100644 --- a/apps/chat-e2e/src/tests/prompts.test.ts +++ b/apps/chat-e2e/src/tests/prompts.test.ts @@ -301,6 +301,7 @@ dialTest( expect .soft(menuOptions, ExpectedMessages.contextMenuOptionsValid) .toEqual([ + MenuOptions.use, MenuOptions.select, MenuOptions.edit, MenuOptions.duplicate, diff --git a/apps/chat/public/images/icons/insert-prompt.svg b/apps/chat/public/images/icons/insert-prompt.svg new file mode 100644 index 0000000000..eacdac8f9e --- /dev/null +++ b/apps/chat/public/images/icons/insert-prompt.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/chat/src/components/Chat/Chat.tsx b/apps/chat/src/components/Chat/Chat.tsx index f48129893c..0378aa41da 100644 --- a/apps/chat/src/components/Chat/Chat.tsx +++ b/apps/chat/src/components/Chat/Chat.tsx @@ -17,6 +17,8 @@ import { clearStateForMessages } from '@/src/utils/app/clear-messages-state'; import { excludeSystemMessages, getConversationModelParams, + isReplayAsIsConversation, + isReplayConversation, } from '@/src/utils/app/conversation'; import { isConversationWithFormSchema } from '@/src/utils/app/form-schema'; import { isSmallScreen } from '@/src/utils/app/mobile'; @@ -170,11 +172,10 @@ export const ChatView = memo(() => { (models.length === 0 || selectedConversations.some((conv) => { if ( - conv.replay && - conv.replay.isReplay && - conv.replay.replayAsIs && - conv.replay.replayUserMessagesStack && - conv.replay.replayUserMessagesStack[0].model + isReplayConversation(conv) && + isReplayAsIsConversation(conv) && + conv.replay?.replayUserMessagesStack && + conv.replay?.replayUserMessagesStack[0].model ) { return conv.replay.replayUserMessagesStack.some( (message) => diff --git a/apps/chat/src/components/Chat/ChatCompareRotate.tsx b/apps/chat/src/components/Chat/ChatCompareRotate.tsx index 1496119894..748d5d4817 100644 --- a/apps/chat/src/components/Chat/ChatCompareRotate.tsx +++ b/apps/chat/src/components/Chat/ChatCompareRotate.tsx @@ -2,7 +2,7 @@ import { useTranslation } from '@/src/hooks/useTranslation'; import { Translation } from '@/src/types/translation'; -import Rotate from '../../../public/images/icons/rotate.svg'; +import Rotate from '@/public/images/icons/rotate.svg'; export const ChatCompareRotate = () => { const { t } = useTranslation(Translation.Chat); diff --git a/apps/chat/src/components/Chat/ChatHeader/Header.tsx b/apps/chat/src/components/Chat/ChatHeader/Header.tsx index dd3664d2ad..def167cc3b 100644 --- a/apps/chat/src/components/Chat/ChatHeader/Header.tsx +++ b/apps/chat/src/components/Chat/ChatHeader/Header.tsx @@ -16,6 +16,7 @@ import { isEntityNameOrPathInvalid } from '@/src/utils/app/common'; import { getSelectedAddons, getValidEntitiesFromIds, + isReplayAsIsConversation, } from '@/src/utils/app/conversation'; import { doesModelAllowAddons, @@ -180,7 +181,7 @@ export const ChatHeader = Inversify.register( const disallowChangeAgent = isChangeAgentDisallowed || isExternal; const disallowChangeSettings = - conversation.replay?.replayAsIs || isPlayback || isExternal; + isReplayAsIsConversation(conversation) || isPlayback || isExternal; return ( <> diff --git a/apps/chat/src/components/Chat/ChatInput/ChatControls.tsx b/apps/chat/src/components/Chat/ChatInput/ChatControls.tsx index 56e0760c9e..a38f664f2e 100644 --- a/apps/chat/src/components/Chat/ChatInput/ChatControls.tsx +++ b/apps/chat/src/components/Chat/ChatInput/ChatControls.tsx @@ -17,7 +17,7 @@ import { SettingsSelectors } from '@/src/store/settings/settings.reducers'; import { SendMessageButton } from '@/src/components/Chat/ChatInput/SendMessageButton'; import Tooltip from '@/src/components/Common/Tooltip'; -import RefreshCW from '../../../../public/images/icons/refresh-cw.svg'; +import RefreshCW from '@/public/images/icons/refresh-cw.svg'; interface Props { showReplayControls: boolean; diff --git a/apps/chat/src/components/Chat/ChatInput/ChatInputMessage.tsx b/apps/chat/src/components/Chat/ChatInput/ChatInputMessage.tsx index 2ae827072b..2b698c003e 100644 --- a/apps/chat/src/components/Chat/ChatInput/ChatInputMessage.tsx +++ b/apps/chat/src/components/Chat/ChatInput/ChatInputMessage.tsx @@ -138,21 +138,15 @@ export const ChatInputMessage = Inversify.register( ConversationsSelectors.selectSelectedConversationsModels, ); - const isConversationBlocksInput = useAppSelector( + const isChatInputDisabled = useAppSelector( ConversationsSelectors.selectIsSelectedConversationBlocksInput, ); - const isConfigurationBlocksInput = useAppSelector( - ChatSelectors.selectIsConfigurationBlocksInput, - ); const configurationSchema = useAppSelector( ChatSelectors.selectConfigurationSchema, ); const isChatEmpty = !selectedConversations[0]?.messages?.length; - const isChatInputDisabled = - isConversationBlocksInput || (isConfigurationBlocksInput && isChatEmpty); - const modelTokenizer = selectedModels?.length === 1 ? selectedModels[0]?.tokenizer : undefined; const maxTokensLength = diff --git a/apps/chat/src/components/Chat/ChatInput/PromptVariablesForApplyDialog.tsx b/apps/chat/src/components/Chat/ChatInput/PromptVariablesForApplyDialog.tsx new file mode 100644 index 0000000000..6068287282 --- /dev/null +++ b/apps/chat/src/components/Chat/ChatInput/PromptVariablesForApplyDialog.tsx @@ -0,0 +1,42 @@ +import { useCallback } from 'react'; + +import { ChatActions } from '@/src/store/chat/chat.reducer'; +import { useAppDispatch, useAppSelector } from '@/src/store/hooks'; +import { + PromptsActions, + PromptsSelectors, +} from '@/src/store/prompts/prompts.reducers'; + +import { withRenderWhen } from '../../Common/RenderWhen'; +import { PromptVariablesDialog } from './PromptVariablesDialog'; + +function PromptVariablesForApplyDialogView() { + const dispatch = useAppDispatch(); + + const prompt = useAppSelector( + PromptsSelectors.selectPromptWithVariablesForApply, + ); + + const handleClose = useCallback(() => { + dispatch(PromptsActions.setPromptWithVariablesForApply()); + }, [dispatch]); + + const handleSubmit = useCallback( + (updatedContent: string) => { + dispatch(ChatActions.appendInputContent(updatedContent)); + handleClose(); + }, + [dispatch, handleClose], + ); + return ( + + ); +} + +export const PromptVariablesForApplyDialog = withRenderWhen( + PromptsSelectors.selectPromptWithVariablesForApply, +)(PromptVariablesForApplyDialogView); diff --git a/apps/chat/src/components/Chat/ChatMessage/ChatMessageContent/AssistantMessage.tsx b/apps/chat/src/components/Chat/ChatMessage/ChatMessageContent/AssistantMessage.tsx index fda37786b9..49eefb2654 100644 --- a/apps/chat/src/components/Chat/ChatMessage/ChatMessageContent/AssistantMessage.tsx +++ b/apps/chat/src/components/Chat/ChatMessage/ChatMessageContent/AssistantMessage.tsx @@ -5,6 +5,7 @@ import classNames from 'classnames'; import { useTranslation } from '@/src/hooks/useTranslation'; import { isEntityNameOrPathInvalid } from '@/src/utils/app/common'; +import { isPlaybackConversation } from '@/src/utils/app/conversation'; import { Conversation } from '@/src/types/chat'; import { Translation } from '@/src/types/translation'; @@ -83,7 +84,7 @@ export const AssistantMessage = memo(function AssistantMessage({ )} {!( conversation.isMessageStreaming && - conversation.playback?.isPlayback && + isPlaybackConversation(conversation) && isLastMessage ) && ( + ); } diff --git a/apps/chat/src/components/Chat/ChatSettings/ChatSettingsModal.tsx b/apps/chat/src/components/Chat/ChatSettings/ChatSettingsModal.tsx index 9eb4ce99ad..82c049f206 100644 --- a/apps/chat/src/components/Chat/ChatSettings/ChatSettingsModal.tsx +++ b/apps/chat/src/components/Chat/ChatSettings/ChatSettingsModal.tsx @@ -17,6 +17,7 @@ import { ModelsSelectors } from '@/src/store/models/models.reducers'; import { PromptsSelectors } from '@/src/store/prompts/prompts.reducers'; import { FALLBACK_ASSISTANT_SUBMODEL_ID } from '@/src/constants/default-ui-settings'; +import { MOUSE_OUTSIDE_PRESS_EVENT } from '@/src/constants/modal'; import { Modal } from '@/src/components/Common/Modal'; @@ -165,7 +166,7 @@ export const ChatSettings = ({ isCompareMode ? 'md:max-w-[1000px]' : 'md:max-w-[500px]', isSomethingConfigurable ? 'py-3 md:py-4' : 'pt-3 md:pt-4', )} - dismissProps={{ outsidePressEvent: 'mousedown' }} + dismissProps={MOUSE_OUTSIDE_PRESS_EVENT} >
{t('Conversation settings')} diff --git a/apps/chat/src/components/Chat/ChatSettings/ConversationSettings.tsx b/apps/chat/src/components/Chat/ChatSettings/ConversationSettings.tsx index 74d1607763..7972803455 100644 --- a/apps/chat/src/components/Chat/ChatSettings/ConversationSettings.tsx +++ b/apps/chat/src/components/Chat/ChatSettings/ConversationSettings.tsx @@ -2,6 +2,7 @@ import { ReactNode } from 'react'; import { useTranslation } from '@/src/hooks/useTranslation'; +import { isPlaybackConversation } from '@/src/utils/app/conversation'; import { DefaultsService } from '@/src/utils/app/data/defaults-service'; import { doesModelAllowAddons, @@ -97,7 +98,7 @@ export const ConversationSettings = Inversify.register( const modelsMap = useAppSelector(ModelsSelectors.selectModelsMap); const model = modelsMap[conversation.model.id]; - const isPlayback = !!conversation.playback?.isPlayback; + const isPlayback = isPlaybackConversation(conversation); if (!model) { return ( diff --git a/apps/chat/src/components/Chat/ConversationContextMenu.tsx b/apps/chat/src/components/Chat/ConversationContextMenu.tsx index 64accd8369..0722d82d18 100644 --- a/apps/chat/src/components/Chat/ConversationContextMenu.tsx +++ b/apps/chat/src/components/Chat/ConversationContextMenu.tsx @@ -11,6 +11,10 @@ import { useScreenState } from '@/src/hooks/useScreenState'; import { useTranslation } from '@/src/hooks/useTranslation'; import { isEntityNameOnSameLevelUnique } from '@/src/utils/app/common'; +import { + isPlaybackConversation, + isReplayConversation, +} from '@/src/utils/app/conversation'; import { constructPath } from '@/src/utils/app/file'; import { getNextDefaultName } from '@/src/utils/app/folders'; import { @@ -133,8 +137,8 @@ export const ConversationContextMenu = ({ } }, [conversation.id, conversation.status, dispatch, isOpen]); - const isReplay = (conversation as Conversation).replay?.isReplay; - const isPlayback = (conversation as Conversation).playback?.isPlayback; + const isReplay = isReplayConversation(conversation); + const isPlayback = isPlaybackConversation(conversation); const isEmptyConversation = !( (conversation as Conversation).messages?.length > 0 ); diff --git a/apps/chat/src/components/Chat/EmptyChatDescription.tsx b/apps/chat/src/components/Chat/EmptyChatDescription.tsx index ba93bbf51e..6878890646 100644 --- a/apps/chat/src/components/Chat/EmptyChatDescription.tsx +++ b/apps/chat/src/components/Chat/EmptyChatDescription.tsx @@ -9,6 +9,8 @@ import { getModelDescription } from '@/src/utils/app/application'; import { getOpenAIEntityFullName, isOldConversationReplay, + isPlaybackConversation, + isReplayAsIsConversation, } from '@/src/utils/app/conversation'; import { isEntityIdExternal } from '@/src/utils/app/id'; @@ -42,11 +44,11 @@ const getModelName = ( conversation: Conversation, model: DialAIEntityModel | undefined, ) => { - if (conversation.playback?.isPlayback) { + if (isPlaybackConversation(conversation)) { return 'Playback'; } - if (conversation.replay?.replayAsIs) { + if (isReplayAsIsConversation(conversation)) { return 'Replay as is'; } @@ -119,8 +121,8 @@ const EmptyChatDescriptionView = ({ ); } - const isReplayAsIs = conversation.replay?.replayAsIs; - const isPlayback = conversation.playback?.isPlayback; + const isReplayAsIs = isReplayAsIsConversation(conversation); + const isPlayback = isPlaybackConversation(conversation); const isEmptyChatChangeAgentHidden = enabledFeatures.has( Feature.HideEmptyChatChangeAgent, ); diff --git a/apps/chat/src/components/Chat/MessageAttachment.tsx b/apps/chat/src/components/Chat/MessageAttachment.tsx index a83eb52e00..44b487719a 100644 --- a/apps/chat/src/components/Chat/MessageAttachment.tsx +++ b/apps/chat/src/components/Chat/MessageAttachment.tsx @@ -28,12 +28,12 @@ import { FOLDER_ATTACHMENT_CONTENT_TYPE } from '@/src/constants/folders'; import { Spinner } from '@/src/components/Common/Spinner'; import { PlotlyComponent } from '@/src/components/Plotly/Plotly'; -import LinkIcon from '../../../public/images/icons/arrow-up-right-from-square.svg'; -import ChevronDown from '../../../public/images/icons/chevron-down.svg'; import Tooltip from '../Common/Tooltip'; import ChatMDComponent from '../Markdown/ChatMDComponent'; import { VisualizerRenderer } from '../VisualalizerRenderer/VisualizerRenderer'; +import LinkIcon from '@/public/images/icons/arrow-up-right-from-square.svg'; +import ChevronDown from '@/public/images/icons/chevron-down.svg'; import { Attachment, MIMEType } from '@epam/ai-dial-shared'; import { sanitize } from 'isomorphic-dompurify'; diff --git a/apps/chat/src/components/Chat/MessageAttachments.tsx b/apps/chat/src/components/Chat/MessageAttachments.tsx index 434587f845..64e04ffdc3 100644 --- a/apps/chat/src/components/Chat/MessageAttachments.tsx +++ b/apps/chat/src/components/Chat/MessageAttachments.tsx @@ -6,9 +6,9 @@ import { useTranslation } from '@/src/hooks/useTranslation'; import { Translation } from '@/src/types/translation'; -import ChevronDown from '../../../public/images/icons/chevron-down.svg'; import { MessageAttachment } from './MessageAttachment'; +import ChevronDown from '@/public/images/icons/chevron-down.svg'; import { Attachment } from '@epam/ai-dial-shared'; interface Props { diff --git a/apps/chat/src/components/Chat/MessageStage.tsx b/apps/chat/src/components/Chat/MessageStage.tsx index af3af368ee..861a76ef25 100644 --- a/apps/chat/src/components/Chat/MessageStage.tsx +++ b/apps/chat/src/components/Chat/MessageStage.tsx @@ -10,12 +10,12 @@ import { useAppSelector } from '@/src/store/hooks'; import { ModelIcon } from '@/src/components/Chatbar/ModelIcon'; -import ChevronDown from '../../../public/images/icons/chevron-down.svg'; -import CircleCheck from '../../../public/images/icons/circle-check.svg'; import { Spinner } from '../Common/Spinner'; import ChatMDComponent from '../Markdown/ChatMDComponent'; import { MessageAttachments } from './MessageAttachments'; +import ChevronDown from '@/public/images/icons/chevron-down.svg'; +import CircleCheck from '@/public/images/icons/circle-check.svg'; import { Stage } from '@epam/ai-dial-shared'; interface StageTitleProps { diff --git a/apps/chat/src/components/Chat/TalkTo/TalkToCard.tsx b/apps/chat/src/components/Chat/TalkTo/TalkToCard.tsx index 042a65c51e..e2752d5875 100644 --- a/apps/chat/src/components/Chat/TalkTo/TalkToCard.tsx +++ b/apps/chat/src/components/Chat/TalkTo/TalkToCard.tsx @@ -20,7 +20,10 @@ import { isApplicationStatusUpdating, isExecutableApp, } from '@/src/utils/app/application'; -import { isOldConversationReplay } from '@/src/utils/app/conversation'; +import { + isOldConversationReplay, + isPlaybackConversation, +} from '@/src/utils/app/conversation'; import { isMyApplication } from '@/src/utils/app/id'; import { canWriteSharedWithMe } from '@/src/utils/app/share'; import { PseudoModel, isPseudoModel } from '@/src/utils/server/api'; @@ -339,7 +342,7 @@ export const TalkToCard = ({

{t('Version')}:

(); - const isPlayback = conversation.playback?.isPlayback; - const isReplay = conversation.replay?.isReplay; + const isPlayback = isPlaybackConversation(conversation); + const isReplay = isReplayConversation(conversation); const displayedModels = useMemo(() => { const currentModel = modelsMap[conversation.model.id]; @@ -179,7 +182,7 @@ const TalkToModalView = ({ if ( (model || entity.reference === REPLAY_AS_IS_MODEL) && (conversation.model.id !== entity.reference || - conversation.replay?.replayAsIs) + isReplayAsIsConversation(conversation)) ) { dispatch( ConversationsActions.updateConversation({ @@ -270,13 +273,13 @@ const TalkToModalView = ({ const handleGoToWorkspace = useCallback( (e: MouseEvent) => { - if (conversation.playback?.isPlayback) { + if (isPlayback) { e.preventDefault(); } else { dispatch(ConversationsActions.setTalkToConversationId(null)); } }, - [conversation.playback?.isPlayback, dispatch], + [isPlayback, dispatch], ); return ( @@ -317,7 +320,7 @@ const TalkToModalView = ({ onClick={handleGoToWorkspace} className={classNames( 'm-auto mt-4 text-accent-primary md:absolute md:bottom-6 md:right-6', - conversation.playback?.isPlayback && 'cursor-not-allowed', + isPlayback && 'cursor-not-allowed', )} data-qa="go-to-my-workspace" > diff --git a/apps/chat/src/components/Chat/TalkTo/TalkToSlider.tsx b/apps/chat/src/components/Chat/TalkTo/TalkToSlider.tsx index 6b9b04a0b1..7c8a59a86c 100644 --- a/apps/chat/src/components/Chat/TalkTo/TalkToSlider.tsx +++ b/apps/chat/src/components/Chat/TalkTo/TalkToSlider.tsx @@ -6,6 +6,10 @@ import classNames from 'classnames'; import { useScreenState } from '@/src/hooks/useScreenState'; import { useSwipe } from '@/src/hooks/useSwipe'; +import { + isPlaybackConversation, + isReplayAsIsConversation, +} from '@/src/utils/app/conversation'; import { PseudoModel, isPseudoModel } from '@/src/utils/server/api'; import { Conversation } from '@/src/types/chat'; @@ -138,12 +142,12 @@ const SliderModelsGroup = ({ {modelsGroup.map((model) => { const isNotPseudoModelSelected = model.reference === conversation.model.id && - !conversation.playback?.isPlayback && - !conversation.replay?.replayAsIs; + !isPlaybackConversation(conversation) && + !isReplayAsIsConversation(conversation); const isPseudoModelSelected = model.reference === PseudoModel.Playback || (model.reference === REPLAY_AS_IS_MODEL && - !!conversation.replay?.replayAsIs); + isReplayAsIsConversation(conversation)); return ( @@ -149,19 +155,19 @@ export function ConversationView({ )} /> )} - {conversation.isReplay && ( + {isReplay && ( )} - {conversation.isPlayback && ( + {isPlayback && ( )} - {!conversation.isReplay && !conversation.isPlayback && ( + {!isReplay && !isPlayback && ( { diff --git a/apps/chat/src/components/Common/ItemContextMenu.tsx b/apps/chat/src/components/Common/ItemContextMenu.tsx index 78d4c8774a..2a576508c8 100644 --- a/apps/chat/src/components/Common/ItemContextMenu.tsx +++ b/apps/chat/src/components/Common/ItemContextMenu.tsx @@ -40,6 +40,7 @@ import { SettingsSelectors } from '@/src/store/settings/settings.reducers'; import ContextMenu from './ContextMenu'; +import InsertPromptIcon from '@/public/images/icons/insert-prompt.svg'; import UnpublishIcon from '@/public/images/icons/unpublish.svg'; import { ShareEntity } from '@epam/ai-dial-shared'; @@ -68,6 +69,8 @@ interface ItemContextMenuProps { onDuplicate?: MouseEventHandler; onView?: MouseEventHandler; onSelect?: MouseEventHandler; + disableUse?: boolean; + onUse?: MouseEventHandler; isLoading?: boolean; TriggerIcon?: ContextMenuProps['TriggerIcon']; } @@ -98,6 +101,8 @@ export default function ItemContextMenu({ onView, isLoading, onSelect, + disableUse, + onUse, TriggerIcon, }: ItemContextMenuProps) { const { t } = useTranslation(Translation.SideBar); @@ -120,6 +125,14 @@ export default function ItemContextMenu({ const menuItems: DisplayMenuItemProps[] = useMemo( () => [ + { + name: t('Use'), + display: !!onUse, + disabled: disableUse, + dataQa: 'use', + Icon: InsertPromptIcon, + onClick: onUse, + }, { name: t('Select'), display: !isExternal && !!onSelect, @@ -305,6 +318,7 @@ export default function ItemContextMenu({ ], [ disableAll, + disableUse, entity, featureType, folders, @@ -329,6 +343,7 @@ export default function ItemContextMenu({ onShare, onUnpublish, onUnshare, + onUse, onView, t, ], diff --git a/apps/chat/src/components/Common/NoResultsFound.tsx b/apps/chat/src/components/Common/NoResultsFound.tsx index 996a3679cd..2e451fa56c 100644 --- a/apps/chat/src/components/Common/NoResultsFound.tsx +++ b/apps/chat/src/components/Common/NoResultsFound.tsx @@ -4,7 +4,7 @@ import { useTranslation } from '@/src/hooks/useTranslation'; import { Translation } from '@/src/types/translation'; -import Magnifier from '../../../public/images/icons/search-alt.svg'; +import Magnifier from '@/public/images/icons/search-alt.svg'; interface NoResultsFoundProps { iconSize?: number; diff --git a/apps/chat/src/components/Common/ReplaceConfirmationModal/Components.tsx b/apps/chat/src/components/Common/ReplaceConfirmationModal/Components.tsx index 5d7c7bedbc..66f54207a3 100644 --- a/apps/chat/src/components/Common/ReplaceConfirmationModal/Components.tsx +++ b/apps/chat/src/components/Common/ReplaceConfirmationModal/Components.tsx @@ -11,6 +11,10 @@ import classNames from 'classnames'; import { useTranslation } from '@/src/hooks/useTranslation'; +import { + isPlaybackConversation, + isReplayConversation, +} from '@/src/utils/app/conversation'; import { getFolderIdFromEntityId } from '@/src/utils/app/folders'; import { @@ -161,6 +165,8 @@ const ConversationView = ({ featureContainerClassNames, }: ConversationViewProps) => { const modelsMap = useAppSelector(ModelsSelectors.selectModelsMap); + const isReplay = isReplayConversation(conversation); + const isPlayback = isPlaybackConversation(conversation); return ( @@ -191,17 +197,17 @@ const ConversationView = ({ iconWrapperClassName="!bg-layer-2" {...conversation} > - {conversation.isReplay && ( + {isReplay && ( )} - {conversation.isPlayback && ( + {isPlayback && ( )} - {!conversation.isReplay && !conversation.isPlayback && ( + {!isReplay && !isPlayback && ( ({ return (sortedItems as (ConversationInfo & Partial)[]).filter( (item) => - item.isPlayback || - (!item.isReplay && (item.messages?.length || !item.messages)), + isPlaybackConversation(item) || + (!isReplayConversation(item) && + (item.messages?.length || !item.messages)), ); }, [ allItemsWithoutFilters, @@ -1145,8 +1150,8 @@ const Folder = ({ onUnshare={handleUnshare} onPublish={ featureType !== FeatureType.Chat || - !allChildItems.every( - (item) => (item as ConversationInfo).isReplay, + !allChildItems.every((item) => + isReplayConversation(item as ConversationInfo), ) ? handleOpenPublishing : undefined diff --git a/apps/chat/src/components/Header/Header.tsx b/apps/chat/src/components/Header/Header.tsx index 4bac23233a..29dfe9bbc9 100644 --- a/apps/chat/src/components/Header/Header.tsx +++ b/apps/chat/src/components/Header/Header.tsx @@ -20,14 +20,14 @@ import { OVERLAY_HEADER_ICON_SIZE, } from '@/src/constants/default-ui-settings'; -import MoveLeftIcon from '../../../public/images/icons/move-left.svg'; -import MoveRightIcon from '../../../public/images/icons/move-right.svg'; import Tooltip from '../Common/Tooltip'; import { SettingDialog } from '../Settings/SettingDialog'; import { CreateNewConversation } from './CreateNewConversation'; import { Logo } from './Logo'; import { User } from './User/User'; +import MoveLeftIcon from '@/public/images/icons/move-left.svg'; +import MoveRightIcon from '@/public/images/icons/move-right.svg'; import { Inversify } from '@epam/ai-dial-modulify-ui'; import { Feature } from '@epam/ai-dial-shared'; diff --git a/apps/chat/src/components/Header/User/ProfileButton.tsx b/apps/chat/src/components/Header/User/ProfileButton.tsx index 0030cbf7d3..8646316302 100644 --- a/apps/chat/src/components/Header/User/ProfileButton.tsx +++ b/apps/chat/src/components/Header/User/ProfileButton.tsx @@ -13,7 +13,7 @@ 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 UserIcon from '../../../../public/images/icons/user.svg'; +import UserIcon from '@/public/images/icons/user.svg'; export const ProfileButton = () => { const isProfileOpen = useAppSelector(UISelectors.selectIsProfileOpen); diff --git a/apps/chat/src/components/Header/User/UserDesktop.tsx b/apps/chat/src/components/Header/User/UserDesktop.tsx index 681eee06fc..5e4513f89d 100644 --- a/apps/chat/src/components/Header/User/UserDesktop.tsx +++ b/apps/chat/src/components/Header/User/UserDesktop.tsx @@ -13,10 +13,9 @@ import { UIActions } from '@/src/store/ui/ui.reducers'; import { ConfirmDialog } from '@/src/components/Common/ConfirmDialog'; import { Menu, MenuItem } from '@/src/components/Common/DropdownMenu'; -import ChevronDownIcon from '../../../../public/images/icons/chevron-down.svg'; -import LogOutIcon from '../../../../public/images/icons/log-out.svg'; -import UserIcon from '../../../../public/images/icons/user.svg'; - +import ChevronDownIcon from '@/public/images/icons/chevron-down.svg'; +import LogOutIcon from '@/public/images/icons/log-out.svg'; +import UserIcon from '@/public/images/icons/user.svg'; import { Inversify } from '@epam/ai-dial-modulify-ui'; export const UserDesktop = Inversify.register('UserDesktop', () => { diff --git a/apps/chat/src/components/Markdown/CodeBlock.tsx b/apps/chat/src/components/Markdown/CodeBlock.tsx index 0de319406f..275c17c84f 100644 --- a/apps/chat/src/components/Markdown/CodeBlock.tsx +++ b/apps/chat/src/components/Markdown/CodeBlock.tsx @@ -21,9 +21,10 @@ import { Translation } from '@/src/types/translation'; import { useAppSelector } from '@/src/store/hooks'; import { UISelectors } from '@/src/store/ui/ui.reducers'; -import Download from '../../../public/images/icons/download.svg'; import Tooltip from '../Common/Tooltip'; +import Download from '@/public/images/icons/download.svg'; + interface Props { language: string; value: string; diff --git a/apps/chat/src/components/Marketplace/MarketplaceHeader.tsx b/apps/chat/src/components/Marketplace/MarketplaceHeader.tsx index 75a5a9f327..4d7f28036c 100644 --- a/apps/chat/src/components/Marketplace/MarketplaceHeader.tsx +++ b/apps/chat/src/components/Marketplace/MarketplaceHeader.tsx @@ -21,12 +21,13 @@ import { import { Logo } from '@/src/components/Header/Logo'; import { SettingDialog } from '@/src/components/Settings/SettingDialog'; -import MoveLeftIcon from '../../../public/images/icons/move-left.svg'; -import MoveRightIcon from '../../../public/images/icons/move-right.svg'; import Tooltip from '../Common/Tooltip'; import { BackToChat } from '../Header/BackToChat'; import { User } from '../Header/User/User'; +import MoveLeftIcon from '@/public/images/icons/move-left.svg'; +import MoveRightIcon from '@/public/images/icons/move-right.svg'; + export const MarketplaceHeader = () => { const { t } = useTranslation(Translation.Header); const showFilterbar = useAppSelector( diff --git a/apps/chat/src/components/Marketplace/TabRenderer.tsx b/apps/chat/src/components/Marketplace/TabRenderer.tsx index 41a33a053b..9716049718 100644 --- a/apps/chat/src/components/Marketplace/TabRenderer.tsx +++ b/apps/chat/src/components/Marketplace/TabRenderer.tsx @@ -46,9 +46,9 @@ import { CardsList } from '@/src/components/Marketplace/CardsList'; import { MarketplaceBanner } from '@/src/components/Marketplace/MarketplaceBanner'; import { SearchHeader } from '@/src/components/Marketplace/SearchHeader'; -import Magnifier from '../../../public/images/icons/search-alt.svg'; import { NoResultsFound } from '../Common/NoResultsFound'; +import Magnifier from '@/public/images/icons/search-alt.svg'; import { PublishActions, ShareEntity } from '@epam/ai-dial-shared'; interface NoAgentsFoundProps { diff --git a/apps/chat/src/components/Promptbar/Promptbar.tsx b/apps/chat/src/components/Promptbar/Promptbar.tsx index 5d5a687cb2..c1feb2f971 100644 --- a/apps/chat/src/components/Promptbar/Promptbar.tsx +++ b/apps/chat/src/components/Promptbar/Promptbar.tsx @@ -26,10 +26,11 @@ import { PromptModal } from './components/PromptModal'; import { PromptbarSettings } from './components/PromptbarSettings'; import { Prompts } from './components/Prompts'; -import PlusIcon from '../../../public/images/icons/plus-large.svg'; import Tooltip from '../Common/Tooltip'; import Sidebar from '../Sidebar'; +import PlusIcon from '@/public/images/icons/plus-large.svg'; + const PromptActionsBlock = () => { const { t } = useTranslation(Translation.PromptBar); const dispatch = useAppDispatch(); diff --git a/apps/chat/src/components/Promptbar/components/Prompt.tsx b/apps/chat/src/components/Promptbar/components/Prompt.tsx index 12f9405a99..f82fa903fe 100644 --- a/apps/chat/src/components/Promptbar/components/Prompt.tsx +++ b/apps/chat/src/components/Promptbar/components/Prompt.tsx @@ -44,6 +44,7 @@ import { Prompt, PromptInfo } from '@/src/types/prompt'; import { SharingType } from '@/src/types/share'; import { Translation } from '@/src/types/translation'; +import { ConversationsSelectors } from '@/src/store/conversations/conversations.reducers'; import { useAppDispatch, useAppSelector } from '@/src/store/hooks'; import { ImportExportActions } from '@/src/store/import-export/importExport.reducers'; import { @@ -140,7 +141,7 @@ export const PromptComponent = ({ const isNameOrPathInvalid = isNameInvalid || isInvalidPath; const [isDeleting, setIsDeleting] = useState(false); - const [isRenaming, setIsRenaming] = useState(false); + const [isOpened, setIsOpened] = useState(false); const [isShowMoveToModal, setIsShowMoveToModal] = useState(false); const [isPublishing, setIsPublishing] = useState(false); const [isUnpublishing, setIsUnpublishing] = useState(false); @@ -164,7 +165,7 @@ export const PromptComponent = ({ useEffect(() => { if (!showModal) { - setIsRenaming(false); + setIsOpened(false); } }, [showModal]); @@ -198,29 +199,26 @@ export const PromptComponent = ({ }, []); const handleDelete = useCallback(() => { - if (isDeleting) { - if (prompt.sharedWithMe) { - dispatch( - ShareActions.discardSharedWithMe({ - resourceIds: [prompt.id], - featureType: FeatureType.Prompt, - }), - ); - } else { - dispatch(PromptsActions.deletePrompt({ prompt })); - } - dispatch(PromptsActions.resetSearch()); + if (prompt.sharedWithMe) { + dispatch( + ShareActions.discardSharedWithMe({ + resourceIds: [prompt.id], + featureType: FeatureType.Prompt, + }), + ); + } else { + dispatch(PromptsActions.deletePrompt({ prompt })); } + dispatch(PromptsActions.resetSearch()); - setIsDeleting(false); dispatch(PromptsActions.setSelectedPrompt({ promptId: undefined })); - }, [dispatch, isDeleting, prompt]); + }, [dispatch, prompt]); const handleOpenDeleteModal: MouseEventHandler = useCallback((e) => { e.stopPropagation(); e.preventDefault(); - setIsRenaming(false); + setIsOpened(false); setIsDeleting(true); }, []); @@ -238,7 +236,7 @@ export const PromptComponent = ({ (e: MouseEvent, isPreview = false) => { e.stopPropagation(); e.preventDefault(); - setIsRenaming(true); + setIsOpened(true); dispatch( PromptsActions.setSelectedPrompt({ promptId: prompt.id, @@ -355,7 +353,7 @@ export const PromptComponent = ({ setIsContextMenu(true); }; const isHighlighted = !isSelectMode - ? isDeleting || isRenaming || (showModal && isSelected) || isContextMenu + ? isDeleting || isOpened || (showModal && isSelected) || isContextMenu : isChosen; const handleDuplicate: MouseEventHandler = useCallback( @@ -376,9 +374,22 @@ export const PromptComponent = ({ [dispatch, prompt.id], ); + const disableUsePrompt = useAppSelector( + ConversationsSelectors.selectIsSelectedConversationBlocksInput, + ); + + const handleUse: MouseEventHandler = useCallback( + (e) => { + e.stopPropagation(); + setIsContextMenu(false); + dispatch(PromptsActions.applyPrompt(prompt)); + }, + [dispatch, prompt], + ); + useEffect(() => { if (isSelectMode) { - setIsRenaming(false); + setIsOpened(false); setIsDeleting(false); } }, [isSelectMode]); @@ -411,7 +422,7 @@ export const PromptComponent = ({ onClick={() => { if (isSelectMode && !isExternal) { setIsDeleting(false); - setIsRenaming(false); + setIsOpened(false); dispatch(PromptsActions.setChosenPrompts({ ids: [prompt.id] })); } }} @@ -425,7 +436,7 @@ export const PromptComponent = ({
handleDragStart(e, prompt)} @@ -505,7 +516,7 @@ export const PromptComponent = ({
- {!isSelectMode && !isDeleting && !isRenaming && ( + {!isSelectMode && !isDeleting && !isOpened && (
handleOpenEditModal(e, true)} isOpen={isContextMenu} onSelect={handleSelect} + disableUse={disableUsePrompt} + onUse={handleUse} />
)} @@ -599,8 +612,8 @@ export const PromptComponent = ({ confirmLabel={t('Delete')} cancelLabel={t('Cancel')} onClose={(result) => { - setIsDeleting(false); if (result) handleDelete(); + setIsDeleting(false); }} /> )} diff --git a/apps/chat/src/store/chat/chat.reducer.ts b/apps/chat/src/store/chat/chat.reducer.ts index dd2780ab95..dbae9c08f2 100644 --- a/apps/chat/src/store/chat/chat.reducer.ts +++ b/apps/chat/src/store/chat/chat.reducer.ts @@ -25,6 +25,9 @@ export const chatSlice = createSlice({ setInputContent: (state, { payload }: PayloadAction) => { state.inputContent = payload; }, + appendInputContent: (state, { payload }: PayloadAction) => { + state.inputContent = `${state.inputContent} ${payload}`; + }, setFormValue( state, { diff --git a/apps/chat/src/store/chat/chat.selectors.ts b/apps/chat/src/store/chat/chat.selectors.ts index 44d66f14fc..dce97a70ff 100644 --- a/apps/chat/src/store/chat/chat.selectors.ts +++ b/apps/chat/src/store/chat/chat.selectors.ts @@ -7,25 +7,17 @@ import { DialSchemaProperties } from '@epam/ai-dial-shared'; const rootSelector = (state: RootState): ChatState => state.chat; -export const selectInputContent = createSelector( - [rootSelector], - (state) => state.inputContent, -); +export const selectInputContent = (state: RootState) => + rootSelector(state).inputContent; -export const selectChatFormValue = createSelector( - [rootSelector], - (state) => state.formValue, -); +export const selectChatFormValue = (state: RootState) => + rootSelector(state).formValue; -export const selectConfigurationSchema = createSelector( - [rootSelector], - (state) => state.configurationSchema, -); +export const selectConfigurationSchema = (state: RootState) => + rootSelector(state).configurationSchema; -export const selectIsConfigurationSchemaLoading = createSelector( - [rootSelector], - (state) => state.isConfigurationSchemaLoading, -); +export const selectIsConfigurationSchemaLoading = (state: RootState) => + rootSelector(state).isConfigurationSchemaLoading; export const selectIsConfigurationBlocksInput = createSelector( [rootSelector], diff --git a/apps/chat/src/store/conversations/conversations.epics.ts b/apps/chat/src/store/conversations/conversations.epics.ts index 66a1342f84..64d192e531 100644 --- a/apps/chat/src/store/conversations/conversations.epics.ts +++ b/apps/chat/src/store/conversations/conversations.epics.ts @@ -45,6 +45,8 @@ import { getDefaultModelReference, getNewConversationName, isChosenConversationValidForCompare, + isReplayAsIsConversation, + isReplayConversation, isSettingsChanged, regenerateConversationId, } from '@/src/utils/app/conversation'; @@ -223,9 +225,9 @@ const initSelectedConversationsEpic: AppEpic = (action$, state$) => : selectedConversationsIds; if (!selectedIds.length) { - return forkJoin({ - selectedConversations: of([]), - selectedIds: of([]), + return of({ + selectedConversations: [], + selectedIds: [], }); } @@ -1258,7 +1260,7 @@ const sendMessageEpic: AppEpic = (action$, state$) => const conversationRootFolderId = getConversationRootId(); const newConversationName = - payload.conversation.replay?.isReplay || + isReplayConversation(payload.conversation) || updatedMessages.filter((msg) => msg.role === Role.User).length > 1 || payload.conversation.isNameChanged ? payload.conversation.name @@ -1764,7 +1766,8 @@ const replayConversationEpic: AppEpic = (action$, state$) => let updatedConversation: Conversation = conv; if ( - conv.replay?.replayAsIs && + conv.replay && + isReplayAsIsConversation(conv) && activeMessage.model && activeMessage.model.id ) { diff --git a/apps/chat/src/store/conversations/conversations.selectors.ts b/apps/chat/src/store/conversations/conversations.selectors.ts index 11ebcb6f27..438f39c01c 100644 --- a/apps/chat/src/store/conversations/conversations.selectors.ts +++ b/apps/chat/src/store/conversations/conversations.selectors.ts @@ -9,7 +9,11 @@ import { isSectionFilterMatched, isVersionFilterMatched, } from '@/src/utils/app/common'; -import { sortByDateAndName } from '@/src/utils/app/conversation'; +import { + isPlaybackConversation, + isReplayConversation, + sortByDateAndName, +} from '@/src/utils/app/conversation'; import { constructPath } from '@/src/utils/app/file'; import { getChildAndCurrentFoldersById, @@ -51,6 +55,7 @@ import { PublicationSelectors } from '@/src/store/publication/publication.reduce import { DEFAULT_FOLDER_NAME } from '@/src/constants/default-ui-settings'; +import { ChatSelectors } from '../chat/chat.selectors'; import { RootState } from '../index'; import { ModelsSelectors } from '../models/models.reducers'; import { SettingsSelectors } from '../settings/settings.reducers'; @@ -124,12 +129,8 @@ export const selectFilteredConversations = ( }, ); -export const selectFolders = createSelector( - [rootSelector], - (state: ConversationsState) => { - return state.folders || []; - }, -); +export const selectFolders = (state: RootState) => + rootSelector(state).folders || []; export const selectPublicationFolders = createSelector( [rootSelector], @@ -187,18 +188,13 @@ export const selectConversation = createSelector( return conversations.find((conv) => conv.id === id); }, ); -export const selectSelectedConversationsIds = createSelector( - [rootSelector], - (state) => { - return state.selectedConversationsIds; - }, -); -export const selectConversationSignal = createSelector( - [rootSelector], - (state) => { - return state.conversationSignal; - }, -); + +export const selectSelectedConversationsIds = (state: RootState) => + rootSelector(state).selectedConversationsIds; + +export const selectConversationSignal = (state: RootState) => + rootSelector(state).conversationSignal; + export const selectSelectedConversations = createSelector( [selectConversations, selectSelectedConversationsIds], (conversations, selectedConversationIds) => { @@ -214,9 +210,8 @@ export const selectLoadedCharts = createSelector([rootSelector], (state) => { return cloneDeep(state.loadedCharts); }); -export const selectChartLoading = createSelector([rootSelector], (state) => { - return state.chartLoading; -}); +export const selectChartLoading = (state: RootState) => + rootSelector(state).chartLoading; export const selectParentFolders = createSelector( [selectFolders, (_state, folderId: string | undefined) => folderId], @@ -278,19 +273,14 @@ export const selectIsConversationNameOrPathInvalid = createSelector( }, ); -export const selectSearchTerm = createSelector([rootSelector], (state) => { - return state.searchTerm; -}); +export const selectSearchTerm = (state: RootState) => + rootSelector(state).searchTerm; -export const selectSearchFilters = createSelector( - [rootSelector], - (state) => state.searchFilters, -); +export const selectSearchFilters = (state: RootState) => + rootSelector(state).searchFilters; -export const selectIsEmptySearchFilter = createSelector( - [rootSelector], - (state) => state.searchFilters === SearchFilters.None, -); +export const selectIsEmptySearchFilter = (state: RootState) => + selectSearchFilters(state) === SearchFilters.None; export const selectMyItemsFilters = createSelector( [selectSearchFilters], @@ -305,15 +295,12 @@ export const selectSearchedConversations = createSelector( ), ); -export const selectIsReplayPaused = createSelector([rootSelector], (state) => { - return state.isReplayPaused; -}); -export const selectIsReplayRequiresVariables = createSelector( - [rootSelector], - (state) => { - return state.isReplayRequiresVariables; - }, -); +export const selectIsReplayPaused = (state: RootState) => + rootSelector(state).isReplayPaused; + +export const selectIsReplayRequiresVariables = (state: RootState) => + rootSelector(state).isReplayRequiresVariables; + export const selectWillReplayRequireVariables = createSelector( [selectFirstSelectedConversation], (conversation) => { @@ -329,23 +316,22 @@ export const selectWillReplayRequireVariables = createSelector( ); export const selectIsSendMessageAborted = createSelector( [selectConversationSignal], - (state) => { - return state.signal.aborted; + (abortController) => { + return abortController.signal.aborted; }, ); + export const selectIsReplaySelectedConversations = createSelector( [selectSelectedConversations], (conversations) => { - return conversations.some((conv) => conv.replay?.isReplay); + return conversations.some((conv) => isReplayConversation(conv)); }, ); export const selectIsPlaybackSelectedConversations = createSelector( [selectSelectedConversations], (conversations) => { - return conversations.some( - (conv) => conv.playback && conv.playback.isPlayback, - ); + return conversations.some((conv) => isPlaybackConversation(conv)); }, ); @@ -383,12 +369,8 @@ export const selectIsErrorReplayConversations = createSelector( }, ); -export const selectIsPlaybackPaused = createSelector( - [rootSelector], - (state) => { - return state.isPlaybackPaused; - }, -); +export const selectIsPlaybackPaused = (state: RootState) => + rootSelector(state).isPlaybackPaused; export const selectPlaybackActiveMessage = createSelector( [selectSelectedConversations], @@ -526,12 +508,8 @@ export const selectCanAttachFile = createSelector( }, ); -export const selectTemporaryFolders = createSelector( - [rootSelector], - (state: ConversationsState) => { - return state.temporaryFolders; - }, -); +export const selectTemporaryFolders = (state: RootState) => + rootSelector(state).temporaryFolders; export const selectPublishedWithMeFolders = createSelector( [selectFolders], @@ -565,12 +543,8 @@ export const selectTemporaryAndPublishedFolders = createSelector( }, ); -export const selectNewAddedFolderId = createSelector( - [rootSelector], - (state) => { - return state.newAddedFolderId; - }, -); +export const selectNewAddedFolderId = (state: RootState) => + rootSelector(state).newAddedFolderId; export const getUniqueAttachments = (attachments: DialFile[]): DialFile[] => uniqBy(attachments, (file) => constructPath(file.relativePath, file.name)); @@ -619,37 +593,20 @@ export const getAttachments = createSelector( }, ); -export const areConversationsUploaded = createSelector( - [rootSelector], - (state) => { - return state.conversationsLoaded; - }, -); +export const areConversationsUploaded = (state: RootState) => + rootSelector(state).conversationsLoaded; -export const selectFoldersStatus = createSelector([rootSelector], (state) => { - return state.foldersStatus; -}); +export const selectFoldersStatus = (state: RootState) => + rootSelector(state).foldersStatus; -export const selectConversationsStatus = createSelector( - [rootSelector], - (state) => { - return state.conversationsStatus; - }, -); +export const selectConversationsStatus = (state: RootState) => + rootSelector(state).conversationsStatus; -export const selectAreSelectedConversationsLoaded = createSelector( - [rootSelector], - (state) => { - return state.areSelectedConversationsLoaded; - }, -); +export const selectAreSelectedConversationsLoaded = (state: RootState) => + rootSelector(state).areSelectedConversationsLoaded; -export const selectAreConversationsWithContentUploading = createSelector( - [rootSelector], - (state) => { - return state.areConversationsWithContentUploading; - }, -); +export const selectAreConversationsWithContentUploading = (state: RootState) => + rootSelector(state).areConversationsWithContentUploading; // default name with counter export const selectNewFolderName = createSelector( @@ -665,24 +622,14 @@ export const selectNewFolderName = createSelector( }, ); -export const selectLoadingFolderIds = createSelector( - [rootSelector], - (state) => { - return state.loadingFolderIds; - }, -); +export const selectLoadingFolderIds = (state: RootState) => + rootSelector(state).loadingFolderIds; -export const selectIsCompareLoading = createSelector( - [rootSelector], - (state) => { - return state.compareLoading; - }, -); +export const selectIsCompareLoading = (state: RootState) => + rootSelector(state).compareLoading; -export const selectIsMessageSending = createSelector( - [rootSelector], - (state) => state.isMessageSending, -); +export const selectIsMessageSending = (state: RootState) => + rootSelector(state).isMessageSending; export const selectDuplicatedConversation = createSelector( [ @@ -708,19 +655,11 @@ export const selectDuplicatedConversation = createSelector( }, ); -export const selectCustomAttachmentLoading = createSelector( - [rootSelector], - (state) => { - return state.customAttachmentDataLoading; - }, -); +export const selectCustomAttachmentLoading = (state: RootState) => + rootSelector(state).customAttachmentDataLoading; -export const selectLoadedCustomAttachments = createSelector( - [rootSelector], - (state) => { - return state.loadedCustomAttachmentsData; - }, -); +export const selectLoadedCustomAttachments = (state: RootState) => + rootSelector(state).loadedCustomAttachmentsData; export const selectCustomAttachmentData = createSelector( [ @@ -752,16 +691,11 @@ export const selectIsSelectMode = createSelector([rootSelector], (state) => { ); }); -export const selectSelectedItems = createSelector([rootSelector], (state) => { - return state.chosenConversationIds; -}); +export const selectSelectedItems = (state: RootState) => + rootSelector(state).chosenConversationIds; -export const selectChosenEmptyFolderIds = createSelector( - [rootSelector], - (state) => { - return state.chosenEmptyFoldersIds; - }, -); +export const selectChosenEmptyFolderIds = (state: RootState) => + rootSelector(state).chosenEmptyFoldersIds; export const selectIsFolderEmpty = createSelector( [selectEmptyFolderIds, (_state, folderId: string) => folderId], @@ -812,27 +746,17 @@ export const selectChosenFolderIds = (itemsShouldBeChosen: ShareEntity[]) => }, ); -export const selectIsNewConversationUpdating = createSelector( - [rootSelector], - (state) => { - return state.isNewConversationUpdating; - }, -); +export const selectIsNewConversationUpdating = (state: RootState) => + rootSelector(state).isNewConversationUpdating; -export const selectInitialized = createSelector( - [rootSelector], - (state) => state.initialized, -); +export const selectInitialized = (state: RootState) => + rootSelector(state).initialized; -export const selectLastConversationSettings = createSelector( - [rootSelector], - (state) => state.lastConversationSettings, -); +export const selectLastConversationSettings = (state: RootState) => + rootSelector(state).lastConversationSettings; -const selectRenamingConversationId = createSelector( - [rootSelector], - (state) => state.renamingConversationId, -); +const selectRenamingConversationId = (state: RootState) => + rootSelector(state).renamingConversationId; export const selectRenamingConversation = createSelector( [selectConversations, selectRenamingConversationId], @@ -840,19 +764,24 @@ export const selectRenamingConversation = createSelector( conversations.find((conv) => conv.id === renamingConversationId), ); -export const selectTalkToConversationId = createSelector( - [rootSelector], - (state) => state.talkToConversationId, -); +export const selectTalkToConversationId = (state: RootState) => + rootSelector(state).talkToConversationId; export const selectIsSelectedConversationBlocksInput = createSelector( - [selectSelectedConversations], - (conversations) => - conversations.some((conversation) => - isMessageInputDisabled( - conversation.messages.length, - conversation.messages, - ), + [selectSelectedConversations, ChatSelectors.selectIsConfigurationBlocksInput], + (conversations, isConfigurationBlocksInput) => + conversations.some( + (conversation) => + conversation.sharedWithMe || + (!conversation.messages?.length && + (isConfigurationBlocksInput || isReplayConversation(conversation))) || + isPlaybackConversation(conversation) || + isEntityIdExternal(conversation) || + !conversation.messages || + isMessageInputDisabled( + conversation.messages.length, + conversation.messages, + ), ), ); diff --git a/apps/chat/src/store/import-export/importExport.epics.ts b/apps/chat/src/store/import-export/importExport.epics.ts index 64df4884e4..cc78cd804e 100644 --- a/apps/chat/src/store/import-export/importExport.epics.ts +++ b/apps/chat/src/store/import-export/importExport.epics.ts @@ -235,7 +235,7 @@ const exportPromptEpic: AppEpic = (action$, state$) => switchMap(({ payload }) => getOrUploadPrompt(payload, state$.value)), switchMap((promptAndPayload) => { - const { prompt } = promptAndPayload; + const { prompt, wasUploaded } = promptAndPayload; if (!prompt) { return concat( of( @@ -254,7 +254,11 @@ const exportPromptEpic: AppEpic = (action$, state$) => PromptsSelectors.selectParentFolders(state$.value, prompt.folderId), appName, ); - return EMPTY; + return iif( + () => wasUploaded, + of(PromptsActions.updatePromptSuccess({ id: prompt.id, prompt })), + EMPTY, + ); }), ); diff --git a/apps/chat/src/store/prompts/prompts.epics.ts b/apps/chat/src/store/prompts/prompts.epics.ts index 53b522feab..1e09f08cb6 100644 --- a/apps/chat/src/store/prompts/prompts.epics.ts +++ b/apps/chat/src/store/prompts/prompts.epics.ts @@ -25,7 +25,6 @@ import { } from '@/src/utils/app/common'; import { PromptService } from '@/src/utils/app/data/prompt-service'; import { getOrUploadPrompt } from '@/src/utils/app/data/storages/api/prompt-api-storage'; -import { constructPath } from '@/src/utils/app/file'; import { addGeneratedFolderId, generateNextName, @@ -37,6 +36,7 @@ import { import { getPromptRootId, isEntityIdExternal } from '@/src/utils/app/id'; import { getPromptInfoFromId, + parseVariablesFromContent, regeneratePromptId, } from '@/src/utils/app/prompts'; import { @@ -44,7 +44,6 @@ import { mapPublishedItems, } from '@/src/utils/app/publications'; import { translate } from '@/src/utils/app/translation'; -import { getPromptApiKey } from '@/src/utils/server/api'; import { FeatureType } from '@/src/types/common'; import { FolderType } from '@/src/types/folder'; @@ -54,6 +53,7 @@ import { AppEpic } from '@/src/types/store'; import { resetShareEntity } from '@/src/constants/chat'; import { DEFAULT_PROMPT_NAME } from '@/src/constants/default-ui-settings'; +import { ChatActions } from '../chat/chat.reducer'; import { PublicationActions } from '../publication/publication.reducers'; import { ShareActions } from '../share/share.reducers'; import { UIActions, UISelectors } from '../ui/ui.reducers'; @@ -275,14 +275,10 @@ const updatePromptEpic: AppEpic = (action$, state$) => ); } - const newPrompt: Prompt = { + const newPrompt: Prompt = regeneratePromptId({ ...prompt, ...values, - id: constructPath( - values.folderId || prompt.folderId, - getPromptApiKey({ ...prompt, ...values }), - ), - }; + }); return concat( of(PromptsActions.updatePromptSuccess({ prompt: newPrompt, id })), @@ -540,14 +536,8 @@ const openFolderEpic: AppEpic = (action$) => const duplicatePromptEpic: AppEpic = (action$, state$) => action$.pipe( filter(PromptsActions.duplicatePrompt.match), - switchMap(({ payload }) => - forkJoin({ - prompt: getOrUploadPrompt(payload, state$.value).pipe( - map((data) => data.prompt), - ), - }), - ), - switchMap(({ prompt }) => { + switchMap(({ payload }) => getOrUploadPrompt(payload, state$.value)), + switchMap(({ prompt, wasUploaded }) => { if (!prompt) { return of( UIActions.showErrorToast( @@ -574,7 +564,14 @@ const duplicatePromptEpic: AppEpic = (action$, state$) => ), }); - return of(PromptsActions.saveNewPrompt({ newPrompt })); + return concat( + of(PromptsActions.saveNewPrompt({ newPrompt })), + iif( + () => wasUploaded, + of(PromptsActions.updatePromptSuccess({ id: prompt.id, prompt })), + EMPTY, + ), + ); }), ); @@ -883,6 +880,35 @@ const deleteChosenPromptsEpic: AppEpic = (action$, state$) => }), ); +const applyPromptEpic: AppEpic = (action$, state$) => + action$.pipe( + filter(PromptsActions.applyPrompt.match), + switchMap(({ payload }) => getOrUploadPrompt(payload, state$.value)), + switchMap(({ prompt, wasUploaded }) => { + if (!prompt) { + return of( + UIActions.showErrorToast( + translate( + 'It looks like this prompt has been deleted. Please reload the page', + ), + ), + ); + } + + const parsedVariables = parseVariablesFromContent(prompt.content); + + return concat( + parsedVariables.length > 0 + ? of(PromptsActions.setPromptWithVariablesForApply(prompt)) + : of(ChatActions.appendInputContent(prompt.content ?? '')), + // save in state to not upload again + wasUploaded + ? of(PromptsActions.updatePromptSuccess({ id: prompt.id, prompt })) + : EMPTY, + ); + }), + ); + export const PromptsEpics = combineEpics( initEpic, uploadPromptsFromMultipleFoldersEpic, @@ -905,4 +931,5 @@ export const PromptsEpics = combineEpics( duplicatePromptEpic, uploadPromptEpic, deleteChosenPromptsEpic, + applyPromptEpic, ); diff --git a/apps/chat/src/store/prompts/prompts.reducers.ts b/apps/chat/src/store/prompts/prompts.reducers.ts index 9b466fd516..57052bcef6 100644 --- a/apps/chat/src/store/prompts/prompts.reducers.ts +++ b/apps/chat/src/store/prompts/prompts.reducers.ts @@ -144,6 +144,7 @@ export const promptsSlice = createSlice({ }); }, duplicatePrompt: (state, _action: PayloadAction) => state, + applyPrompt: (state, _action: PayloadAction) => state, setPrompts: ( state, { payload }: PayloadAction<{ prompts: PromptInfo[] }>, @@ -437,6 +438,12 @@ export const promptsSlice = createSlice({ payload.ids, ); }, + setPromptWithVariablesForApply: ( + state, + { payload }: PayloadAction, + ) => { + state.promptWithVariablesForApply = payload; + }, }, }); diff --git a/apps/chat/src/store/prompts/prompts.selectors.ts b/apps/chat/src/store/prompts/prompts.selectors.ts index 332e9d29ba..d190737e86 100644 --- a/apps/chat/src/store/prompts/prompts.selectors.ts +++ b/apps/chat/src/store/prompts/prompts.selectors.ts @@ -42,13 +42,10 @@ import { ShareEntity } from '@epam/ai-dial-shared'; const rootSelector = (state: RootState): PromptsState => state.prompts; -export const selectPrompts = createSelector([rootSelector], (state) => { - return state.prompts; -}); +export const selectPrompts = (state: RootState) => rootSelector(state).prompts; -export const selectSearchTerm = createSelector([rootSelector], (state) => { - return state.searchTerm; -}); +export const selectSearchTerm = (state: RootState) => + rootSelector(state).searchTerm; export const selectFilteredPrompts = ( filters: EntityFilters, @@ -90,9 +87,7 @@ export const selectPrompt = createSelector( }, ); -export const selectFolders = createSelector([rootSelector], (state) => { - return state.folders; -}); +export const selectFolders = (state: RootState) => rootSelector(state).folders; export const selectEmptyFolderIds = createSelector( [selectFolders, selectPrompts], @@ -152,15 +147,11 @@ export const selectParentFoldersIds = createSelector( }, ); -export const selectSearchFilters = createSelector( - [rootSelector], - (state) => state.searchFilters, -); +export const selectSearchFilters = (state: RootState) => + rootSelector(state).searchFilters; -export const selectIsEmptySearchFilter = createSelector( - [rootSelector], - (state) => state.searchFilters === SearchFilters.None, -); +export const selectIsEmptySearchFilter = (state: RootState) => + selectSearchFilters(state) === SearchFilters.None; export const selectMyItemsFilters = createSelector( [selectSearchFilters], @@ -227,12 +218,8 @@ export const selectDoesAnyMyItemExist = createSelector( }, ); -export const selectTemporaryFolders = createSelector( - [rootSelector], - (state: PromptsState) => { - return state.temporaryFolders; - }, -); +export const selectTemporaryFolders = (state: RootState) => + rootSelector(state).temporaryFolders; export const selectPublishedWithMeFolders = createSelector( [selectFolders], @@ -266,26 +253,17 @@ export const selectTemporaryAndPublishedFolders = createSelector( }, ); -export const selectNewAddedFolderId = createSelector( - [rootSelector], - (state) => { - return state.newAddedFolderId; - }, -); -export const selectLoadingFolderIds = createSelector( - [rootSelector], - (state) => { - return state.loadingFolderIds; - }, -); +export const selectNewAddedFolderId = (state: RootState) => + rootSelector(state).newAddedFolderId; -export const arePromptsUploaded = createSelector([rootSelector], (state) => { - return state.promptsLoaded; -}); +export const selectLoadingFolderIds = (state: RootState) => + rootSelector(state).loadingFolderIds; -export const isPromptLoading = createSelector([rootSelector], (state) => { - return state.isPromptLoading; -}); +export const arePromptsUploaded = (state: RootState) => + rootSelector(state).promptsLoaded; + +export const isPromptLoading = (state: RootState) => + rootSelector(state).isPromptLoading; // default name with counter export const selectNewFolderName = createSelector( @@ -301,10 +279,8 @@ export const selectNewFolderName = createSelector( }, ); -export const selectIsNewPromptCreating = createSelector( - [rootSelector], - (state) => state.isNewPromptCreating, -); +export const selectIsNewPromptCreating = (state: RootState) => + rootSelector(state).isNewPromptCreating; export const getNewPrompt = createSelector([selectPrompts], (prompts) => { const promptRootId = getPromptRootId(); @@ -349,26 +325,22 @@ export const selectDuplicatedPrompt = createSelector( ); export const selectPublicationFolders = createSelector( - [rootSelector], - (state: PromptsState) => { - return state.folders.filter((f) => f.isPublicationFolder); + [selectFolders], + (folders) => { + return folders.filter((f) => f.isPublicationFolder); }, ); -export const selectIsSelectMode = createSelector([rootSelector], (state) => { - return ( - state.chosenPromptIds.length > 0 || state.chosenEmptyFoldersIds.length > 0 - ); -}); +export const selectSelectedItems = (state: RootState) => + rootSelector(state).chosenPromptIds; -export const selectSelectedItems = createSelector([rootSelector], (state) => { - return state.chosenPromptIds; -}); +export const selectChosenEmptyFolderIds = (state: RootState) => + rootSelector(state).chosenEmptyFoldersIds; -export const selectChosenEmptyFolderIds = createSelector( - [rootSelector], - (state) => { - return state.chosenEmptyFoldersIds; +export const selectIsSelectMode = createSelector( + [selectSelectedItems, selectChosenEmptyFolderIds], + (chosenPromptIds, chosenEmptyFoldersIds) => { + return chosenPromptIds.length > 0 || chosenEmptyFoldersIds.length > 0; }, ); @@ -421,7 +393,8 @@ export const selectChosenFolderIds = (itemsShouldBeChosen: ShareEntity[]) => }, ); -export const selectInitialized = createSelector( - [rootSelector], - (state) => state.initialized, -); +export const selectInitialized = (state: RootState) => + rootSelector(state).initialized; + +export const selectPromptWithVariablesForApply = (state: RootState) => + rootSelector(state).promptWithVariablesForApply; diff --git a/apps/chat/src/store/prompts/prompts.types.ts b/apps/chat/src/store/prompts/prompts.types.ts index 92ab085bfc..8991fd3925 100644 --- a/apps/chat/src/store/prompts/prompts.types.ts +++ b/apps/chat/src/store/prompts/prompts.types.ts @@ -1,5 +1,5 @@ import { FolderInterface } from '@/src/types/folder'; -import { PromptInfo } from '@/src/types/prompt'; +import { Prompt, PromptInfo } from '@/src/types/prompt'; import { SearchFilters } from '@/src/types/search'; export interface PromptsState { @@ -20,4 +20,5 @@ export interface PromptsState { isNewPromptCreating: boolean; chosenPromptIds: string[]; chosenEmptyFoldersIds: string[]; + promptWithVariablesForApply?: Prompt; } diff --git a/apps/chat/src/store/publication/publication.epics.ts b/apps/chat/src/store/publication/publication.epics.ts index 0fd4147c31..37ab39fd1c 100644 --- a/apps/chat/src/store/publication/publication.epics.ts +++ b/apps/chat/src/store/publication/publication.epics.ts @@ -264,10 +264,10 @@ const uploadPublicationEpic: AppEpic = (action$, state$) => }); } - return forkJoin({ - publication: of(publication), - uploadedUnpublishEntities: of([]), - unpublishResources: of([]), + return of({ + publication: publication, + uploadedUnpublishEntities: [], + unpublishResources: [], }); }), switchMap( diff --git a/apps/chat/src/utils/app/conversation.ts b/apps/chat/src/utils/app/conversation.ts index 4d1d215f38..b3297c05e4 100644 --- a/apps/chat/src/utils/app/conversation.ts +++ b/apps/chat/src/utils/app/conversation.ts @@ -181,8 +181,8 @@ export const isValidConversationForCompare = ( dontCompareNames?: boolean, ): boolean => { if ( - candidate.isReplay || - candidate.isPlayback || + isReplayConversation(candidate) || + isPlaybackConversation(candidate) || isEntityIdLocal(candidate) || isEntityNameOrPathInvalid(candidate) ) { @@ -206,8 +206,8 @@ export const isChosenConversationValidForCompare = ( ): boolean => { if ( chosenSelection.status !== UploadStatus.LOADED || - chosenSelection.replay?.isReplay || - chosenSelection.playback?.isPlayback + isReplayConversation(chosenSelection) || + isPlaybackConversation(chosenSelection) ) { return false; } @@ -331,9 +331,8 @@ export const getConversationModelParams = ( replayAsIs: false, }; const updatedAddons = - conversation.replay && - conversation.replay.isReplay && - conversation.replay.replayAsIs && + isReplayConversation(conversation) && + isReplayAsIsConversation(conversation) && !updatedReplay?.replayAsIs ? conversation.selectedAddons.filter((addonId) => addonsMap[addonId]) : conversation.selectedAddons; @@ -379,3 +378,16 @@ export const isOldConversationReplay = (replay: Replay | undefined) => replay.isReplay && replay.replayUserMessagesStack && replay.replayUserMessagesStack.some((message) => !message.model); + +export const isPlaybackConversation = (conversation: ConversationInfo) => + (conversation as Conversation).playback?.isPlayback ?? + conversation.isPlayback ?? + false; + +export const isReplayConversation = (conversation: ConversationInfo) => + (conversation as Conversation).replay?.isReplay ?? + conversation.isReplay ?? + false; + +export const isReplayAsIsConversation = (conversation: ConversationInfo) => + (conversation as Conversation).replay?.replayAsIs ?? false; diff --git a/apps/chat/src/utils/app/data/storages/api/conversation-api-storage.ts b/apps/chat/src/utils/app/data/storages/api/conversation-api-storage.ts index 7cabb836f5..cc6e029728 100644 --- a/apps/chat/src/utils/app/data/storages/api/conversation-api-storage.ts +++ b/apps/chat/src/utils/app/data/storages/api/conversation-api-storage.ts @@ -65,6 +65,7 @@ export const getOrUploadConversation = ( ): Observable<{ conversation: Conversation | null; payload: T; + wasUploaded: boolean; }> => { const conversation = ConversationsSelectors.selectConversation( state, @@ -84,11 +85,13 @@ export const getOrUploadConversation = ( }), ), payload: of(payload), + wasUploaded: of(true), }); } else { - return forkJoin({ - conversation: of((conversation as Conversation) ?? null), - payload: of(payload), + return of({ + conversation: (conversation as Conversation) ?? null, + payload: payload, + wasUploaded: false, }); } }; diff --git a/apps/chat/src/utils/app/data/storages/api/prompt-api-storage.ts b/apps/chat/src/utils/app/data/storages/api/prompt-api-storage.ts index dcfee9e978..f42d657f4f 100644 --- a/apps/chat/src/utils/app/data/storages/api/prompt-api-storage.ts +++ b/apps/chat/src/utils/app/data/storages/api/prompt-api-storage.ts @@ -41,6 +41,7 @@ export const getOrUploadPrompt = ( ): Observable<{ prompt: Prompt | null; payload: { id: string }; + wasUploaded: boolean; }> => { const prompt = PromptsSelectors.selectPrompt(state, payload.id); @@ -53,11 +54,13 @@ export const getOrUploadPrompt = ( }), ), payload: of(payload), + wasUploaded: of(true), }); } else { - return forkJoin({ - prompt: of(prompt ?? null), - payload: of(payload), + return of({ + prompt: prompt ?? null, + payload: payload, + wasUploaded: false, }); } }; diff --git a/apps/chat/src/utils/app/folders.ts b/apps/chat/src/utils/app/folders.ts index 3dd23c835e..4e17ddecc2 100644 --- a/apps/chat/src/utils/app/folders.ts +++ b/apps/chat/src/utils/app/folders.ts @@ -20,6 +20,7 @@ import { prepareEntityName, updateEntitiesFoldersAndIds, } from './common'; +import { isReplayConversation } from './conversation'; import { isRootId } from './id'; import { hasWritePermission } from './share'; @@ -389,7 +390,7 @@ export const getConversationAttachmentWithPath = < ): DialFile[] => { const { path } = getPathToFolderById(folders, conversation.folderId); const isReplay = - 'replay' in conversation ? conversation?.replay?.isReplay : false; + 'replay' in conversation ? isReplayConversation(conversation) : false; const attachments = 'messages' in conversation ? ( diff --git a/apps/chat/src/utils/server/api.ts b/apps/chat/src/utils/server/api.ts index acbf8df840..d272387eb2 100644 --- a/apps/chat/src/utils/server/api.ts +++ b/apps/chat/src/utils/server/api.ts @@ -13,6 +13,10 @@ import { EMPTY_MODEL_ID } from '@/src/constants/default-ui-settings'; import { NA_VERSION } from '@/src/constants/public'; import { validVersionRegEx } from '@/src/constants/versions'; +import { + isPlaybackConversation, + isReplayConversation, +} from '../app/conversation'; import { constructPath } from '../app/file'; import { splitEntityId } from '../app/folders'; @@ -42,10 +46,8 @@ export const isPseudoModel = (modelId: string | undefined) => modelId ? Object.values(PseudoModel).includes(modelId as PseudoModel) : false; const getModelApiIdFromConversation = (conversation: Conversation): string => { - if (conversation.replay?.isReplay ?? conversation.isReplay) - return PseudoModel.Replay; - if (conversation.playback?.isPlayback ?? conversation.isPlayback) - return PseudoModel.Playback; + if (isReplayConversation(conversation)) return PseudoModel.Replay; + if (isPlaybackConversation(conversation)) return PseudoModel.Playback; return conversation.model.id; };