From fa6bf20059850d248e2401b6e34bc176f5a5e35b Mon Sep 17 00:00:00 2001 From: Ilya Bondar Date: Mon, 20 Jan 2025 19:24:42 +0100 Subject: [PATCH 01/11] feat(chat): implement direct application links (Issue #2950) --- apps/chat/src/components/Chat/ShareModal.tsx | 8 +-- .../ApplicationCopyLink.tsx | 69 +++++++++++++++++++ .../ApplicationDetails/ApplicationHeader.tsx | 4 ++ apps/chat/src/constants/marketplace.ts | 1 + apps/chat/src/utils/app/application.ts | 4 ++ 5 files changed, 79 insertions(+), 7 deletions(-) create mode 100644 apps/chat/src/components/Marketplace/ApplicationDetails/ApplicationCopyLink.tsx diff --git a/apps/chat/src/components/Chat/ShareModal.tsx b/apps/chat/src/components/Chat/ShareModal.tsx index fff112edab..f1055c77da 100644 --- a/apps/chat/src/components/Chat/ShareModal.tsx +++ b/apps/chat/src/components/Chat/ShareModal.tsx @@ -72,9 +72,7 @@ export default function ShareModalView() { const { t } = useTranslation(Translation.SideBar); const dispatch = useAppDispatch(); - const copyButtonRef = useRef(null); const [urlCopied, setUrlCopied] = useState(false); - const [urlWasCopied, setUrlWasCopied] = useState(false); const timeoutRef = useRef>(); const [editAccess, setEditAccess] = useState(false); @@ -152,12 +150,9 @@ export default function ShareModalView() { timeoutRef.current = setTimeout(() => { setUrlCopied(false); }, 2000); - if (!urlWasCopied) { - setUrlWasCopied(true); - } }); }, - [url, urlWasCopied], + [url], ); useEffect(() => () => clearTimeout(timeoutRef.current), []); @@ -209,7 +204,6 @@ export default function ShareModalView() { )} + {isPublicApp && } ); }; diff --git a/apps/chat/src/constants/marketplace.ts b/apps/chat/src/constants/marketplace.ts index fefe36f308..b301ae73d6 100644 --- a/apps/chat/src/constants/marketplace.ts +++ b/apps/chat/src/constants/marketplace.ts @@ -1,5 +1,6 @@ export enum MarketplaceQueryParams { fromConversation = 'fromConversation', + id = 'id', } export enum FilterTypes { diff --git a/apps/chat/src/utils/app/application.ts b/apps/chat/src/utils/app/application.ts index 203346e5a8..65072e1798 100644 --- a/apps/chat/src/utils/app/application.ts +++ b/apps/chat/src/utils/app/application.ts @@ -28,6 +28,7 @@ import { ApiUtils, getApplicationApiKey } from '../server/api'; import { constructPath } from './file'; import { getFolderIdFromEntityId } from './folders'; import { getApplicationRootId } from './id'; +import { isEntityIdPublic } from './publications'; import { translate } from './translation'; import omit from 'lodash-es/omit'; @@ -229,3 +230,6 @@ export const getSharedTooltip = (context: string) => { { ns: Translation.Marketplace }, ); }; + +export const isApplicationPublic = (entity: DialAIEntityModel) => + isEntityIdPublic(entity) || entity.id === entity.reference; From 1eee9e79ca94a2c46f00797f0db6e1f8ce23d1fe Mon Sep 17 00:00:00 2001 From: Ilya Bondar Date: Mon, 20 Jan 2025 22:21:02 +0100 Subject: [PATCH 02/11] feat(chat): implement direct Application links (issue #2950) --- apps/chat/src/components/Layout.tsx | 2 +- .../ApplicationCopyLink.tsx | 2 +- apps/chat/src/constants/marketplace.ts | 2 +- apps/chat/src/store/index.ts | 2 + .../store/marketplace/marketplace.epics.ts | 67 +++++++++++++++++++ .../store/marketplace/marketplace.reducers.ts | 1 + apps/chat/src/store/models/models.epics.ts | 2 + 7 files changed, 75 insertions(+), 3 deletions(-) create mode 100644 apps/chat/src/store/marketplace/marketplace.epics.ts diff --git a/apps/chat/src/components/Layout.tsx b/apps/chat/src/components/Layout.tsx index 743f717e64..5ba86a7f7a 100644 --- a/apps/chat/src/components/Layout.tsx +++ b/apps/chat/src/components/Layout.tsx @@ -55,7 +55,7 @@ export default function Layout({ ); const shouldOverlayLogin = isOverlay && shouldLogin; - const handleStartRedirecting = useCallback(() => setLoading(true), []); + const handleStartRedirecting = useCallback(() => setLoading(false), []); // TODO: how show loader for page switching? const handleStopRedirecting = useCallback(() => setLoading(false), []); // EFFECTS -------------------------------------------- diff --git a/apps/chat/src/components/Marketplace/ApplicationDetails/ApplicationCopyLink.tsx b/apps/chat/src/components/Marketplace/ApplicationDetails/ApplicationCopyLink.tsx index db852c4a86..99872df2fc 100644 --- a/apps/chat/src/components/Marketplace/ApplicationDetails/ApplicationCopyLink.tsx +++ b/apps/chat/src/components/Marketplace/ApplicationDetails/ApplicationCopyLink.tsx @@ -24,7 +24,7 @@ const ApplicationCopyLink: React.FC = ({ const timeoutRef = useRef>(); const link = useMemo( () => - `${window.location.origin}/marketplace?${MarketplaceQueryParams.id}=${entity.reference}`, + `${window.location.origin}/marketplace?${MarketplaceQueryParams.model}=${entity.reference}`, [entity.reference], ); const handleCopy = useCallback( diff --git a/apps/chat/src/constants/marketplace.ts b/apps/chat/src/constants/marketplace.ts index b301ae73d6..0ee3e85b86 100644 --- a/apps/chat/src/constants/marketplace.ts +++ b/apps/chat/src/constants/marketplace.ts @@ -1,6 +1,6 @@ export enum MarketplaceQueryParams { fromConversation = 'fromConversation', - id = 'id', + model = 'model', } export enum FilterTypes { diff --git a/apps/chat/src/store/index.ts b/apps/chat/src/store/index.ts index 28b63251b8..152cbe13d9 100644 --- a/apps/chat/src/store/index.ts +++ b/apps/chat/src/store/index.ts @@ -31,6 +31,7 @@ import { FilesEpics } from './files/files.epics'; import { filesSlice } from './files/files.reducers'; import { ImportExportEpics } from './import-export/importExport.epics'; import { importExportSlice } from './import-export/importExport.reducers'; +import { MarketplaceEpics } from './marketplace/marketplace.epics'; import { marketplaceSlice } from './marketplace/marketplace.reducers'; import { MigrationEpics } from './migration/migration.epics'; import { migrationSlice } from './migration/migration.reducers'; @@ -68,6 +69,7 @@ export const rootEpic = combineEpics( ApplicationEpics, CodeEditorEpics, ChatEpics, + MarketplaceEpics, ); const reducer = combineReducers({ diff --git a/apps/chat/src/store/marketplace/marketplace.epics.ts b/apps/chat/src/store/marketplace/marketplace.epics.ts new file mode 100644 index 0000000000..d32b9b8d63 --- /dev/null +++ b/apps/chat/src/store/marketplace/marketplace.epics.ts @@ -0,0 +1,67 @@ +import { EMPTY, filter, of, switchMap } from 'rxjs'; + +import { combineEpics } from 'redux-observable'; + +import { AppEpic } from '@/src/types/store'; + +import { MarketplaceQueryParams } from '@/src/constants/marketplace'; + +import { ModelsSelectors } from '../models/models.reducers'; +import { UIActions } from '../ui/ui.reducers'; +import { MarketplaceActions } from './marketplace.reducers'; + +const setDetailsModelEpic: AppEpic = (action$, _, { router }) => + action$.pipe( + filter(MarketplaceActions.setDetailsModel.match), + switchMap(({ payload }) => { + const reference = payload?.reference; + const query = router.query; + if (reference) { + router.push({ + query: { + ...query, + [MarketplaceQueryParams.model]: reference, + }, + }); + } else { + delete query[MarketplaceQueryParams.model]; + router.push({ + query, + }); + } + return EMPTY; + }), + ); + +const initQueryParamsEpic: AppEpic = (action$, state$, { router }) => + action$.pipe( + filter(MarketplaceActions.initQueryParams.match), + switchMap(() => { + const query = router.query; + const modelReference = query[MarketplaceQueryParams.model] as + | string + | undefined; + const modelsMap = ModelsSelectors.selectModelsMap(state$.value); + const model = modelReference ? modelsMap[modelReference] : undefined; + + if (modelReference) { + if (model) { + return of( + MarketplaceActions.setDetailsModel({ + reference: modelReference, + isSuggested: false, + }), + ); + } else { + return of(UIActions.showErrorToast('Agent by this link not found')); + } + } + + return EMPTY; + }), + ); + +export const MarketplaceEpics = combineEpics( + initQueryParamsEpic, + setDetailsModelEpic, +); diff --git a/apps/chat/src/store/marketplace/marketplace.reducers.ts b/apps/chat/src/store/marketplace/marketplace.reducers.ts index 7eb702ce92..beef841c04 100644 --- a/apps/chat/src/store/marketplace/marketplace.reducers.ts +++ b/apps/chat/src/store/marketplace/marketplace.reducers.ts @@ -41,6 +41,7 @@ export const marketplaceSlice = createSlice({ name: 'marketplace', initialState, reducers: { + initQueryParams: (state) => state, setSelectedFilters: ( state, { payload }: PayloadAction<{ filterType: FilterTypes; value: string }>, diff --git a/apps/chat/src/store/models/models.epics.ts b/apps/chat/src/store/models/models.epics.ts index f7f09b73ef..64c2b5f996 100644 --- a/apps/chat/src/store/models/models.epics.ts +++ b/apps/chat/src/store/models/models.epics.ts @@ -38,6 +38,7 @@ import { ApplicationActions } from '@/src/store/application/application.reducers import { DeleteType } from '@/src/constants/marketplace'; +import { MarketplaceActions } from '../marketplace/marketplace.reducers'; import { PublicationActions } from '../publication/publication.reducers'; import { SettingsActions, @@ -156,6 +157,7 @@ const getModelsEpic: AppEpic = (action$, state$) => featureType: FeatureType.Application, }), ), + of(MarketplaceActions.initQueryParams()), ...continueUpdateActions, ); }), From 92c0bf697443f2b76b137b9df5a2ee3f1c2bb951 Mon Sep 17 00:00:00 2001 From: Ilya Bondar Date: Tue, 21 Jan 2025 00:09:46 +0100 Subject: [PATCH 03/11] feat(chat): support url parameters for Marketplace (Issue #2614) --- .../components/Marketplace/Marketplace.tsx | 17 -- apps/chat/src/constants/marketplace.ts | 4 + .../store/marketplace/marketplace.epics.ts | 186 +++++++++++++++--- .../store/marketplace/marketplace.reducers.ts | 16 +- 4 files changed, 169 insertions(+), 54 deletions(-) diff --git a/apps/chat/src/components/Marketplace/Marketplace.tsx b/apps/chat/src/components/Marketplace/Marketplace.tsx index 558943c569..468cef707c 100644 --- a/apps/chat/src/components/Marketplace/Marketplace.tsx +++ b/apps/chat/src/components/Marketplace/Marketplace.tsx @@ -1,7 +1,6 @@ import { FloatingOverlay } from '@floating-ui/react'; import { useEffect } from 'react'; -import { useSearchParams } from 'next/navigation'; import { useRouter } from 'next/router'; import { useScreenState } from '@/src/hooks/useScreenState'; @@ -16,11 +15,6 @@ import { import { ModelsSelectors } from '@/src/store/models/models.reducers'; import { UISelectors } from '@/src/store/ui/ui.reducers'; -import { - MarketplaceQueryParams, - MarketplaceTabs, -} from '@/src/constants/marketplace'; - import { Spinner } from '@/src/components/Common/Spinner'; import { TabRenderer } from '@/src/components/Marketplace/TabRenderer'; @@ -30,7 +24,6 @@ export const Marketplace = () => { const dispatch = useAppDispatch(); const router = useRouter(); - const searchParams = useSearchParams(); const isFilterbarOpen = useAppSelector( UISelectors.selectShowMarketplaceFilterbar, @@ -46,16 +39,6 @@ export const Marketplace = () => { const showOverlay = (isFilterbarOpen || isProfileOpen) && screenState === ScreenState.MOBILE; - useEffect(() => { - dispatch( - MarketplaceActions.setSelectedTab( - searchParams.get(MarketplaceQueryParams.fromConversation) - ? MarketplaceTabs.MY_APPLICATIONS - : MarketplaceTabs.HOME, - ), - ); - }, [dispatch, searchParams]); - useEffect(() => { if (applyModelStatus === UploadStatus.LOADED) { dispatch( diff --git a/apps/chat/src/constants/marketplace.ts b/apps/chat/src/constants/marketplace.ts index 0ee3e85b86..b088086de0 100644 --- a/apps/chat/src/constants/marketplace.ts +++ b/apps/chat/src/constants/marketplace.ts @@ -1,6 +1,10 @@ export enum MarketplaceQueryParams { fromConversation = 'fromConversation', model = 'model', + workspace = 'workspace', + types = 'types', + topics = 'topics', + search = 'search', } export enum FilterTypes { diff --git a/apps/chat/src/store/marketplace/marketplace.epics.ts b/apps/chat/src/store/marketplace/marketplace.epics.ts index d32b9b8d63..0ec522b6fa 100644 --- a/apps/chat/src/store/marketplace/marketplace.epics.ts +++ b/apps/chat/src/store/marketplace/marketplace.epics.ts @@ -1,34 +1,119 @@ -import { EMPTY, filter, of, switchMap } from 'rxjs'; +import { EMPTY, Observable, concat, filter, of, switchMap } from 'rxjs'; + +import { AnyAction } from '@reduxjs/toolkit'; import { combineEpics } from 'redux-observable'; import { AppEpic } from '@/src/types/store'; -import { MarketplaceQueryParams } from '@/src/constants/marketplace'; +import { + FilterTypes, + MarketplaceQueryParams, + MarketplaceTabs, +} from '@/src/constants/marketplace'; import { ModelsSelectors } from '../models/models.reducers'; import { UIActions } from '../ui/ui.reducers'; import { MarketplaceActions } from './marketplace.reducers'; +import { + selectDetailsModel, + selectSearchTerm, + selectSelectedFilters, + selectSelectedTab, +} from './marketplace.selectors'; + +import isNil from 'lodash/isNil'; +import { ParsedUrlQueryInput } from 'querystring'; + +// const setDetailsModelEpic: AppEpic = (action$, _, { router }) => +// action$.pipe( +// filter(MarketplaceActions.setDetailsModel.match), +// switchMap(({ payload }) => { +// const reference = payload?.reference; +// const query = router.query; +// if (reference) { +// router.push({ +// query: { +// ...query, +// [MarketplaceQueryParams.model]: reference, +// }, +// }); +// } else { +// delete query[MarketplaceQueryParams.model]; +// router.push({ +// query, +// }); +// } +// return EMPTY; +// }), +// ); -const setDetailsModelEpic: AppEpic = (action$, _, { router }) => +const addToQuery = ( + query: ParsedUrlQueryInput, + key: string, + value: string | undefined, +) => { + if (value !== undefined) { + query[key] = value; + } else { + delete query[key]; + } +}; + +const setQueryParamsEpic: AppEpic = (action$, state$, { router }) => action$.pipe( - filter(MarketplaceActions.setDetailsModel.match), - switchMap(({ payload }) => { - const reference = payload?.reference; + filter( + (action) => + MarketplaceActions.setSelectedTab.match(action) || + MarketplaceActions.setDetailsModel.match(action) || + MarketplaceActions.setSelectedFilters.match(action) || + MarketplaceActions.setFilters.match(action) || + MarketplaceActions.setSearchTerm.match(action), + ), + switchMap(() => { + const state = state$.value; const query = router.query; - if (reference) { - router.push({ - query: { - ...query, - [MarketplaceQueryParams.model]: reference, - }, - }); - } else { - delete query[MarketplaceQueryParams.model]; - router.push({ + // workspace tab + const selectedTab = selectSelectedTab(state); + addToQuery( + query, + MarketplaceQueryParams.workspace, + selectedTab === MarketplaceTabs.MY_APPLICATIONS ? '1' : undefined, + ); + // application link + const reference = selectDetailsModel(state)?.reference; + addToQuery( + query, + MarketplaceQueryParams.model, + reference ? reference : undefined, + ); + // filters + const filters = selectSelectedFilters(state); + addToQuery( + query, + MarketplaceQueryParams.types, + filters.Type.length ? filters.Type.join(',') : undefined, + ); + addToQuery( + query, + MarketplaceQueryParams.topics, + filters.Topics.length ? filters.Topics.join(',') : undefined, + ); + // search + const searchTerm = selectSearchTerm(state); + addToQuery( + query, + MarketplaceQueryParams.search, + searchTerm ? searchTerm : undefined, + ); + + router.push( + { query, - }); - } + }, + undefined, + { shallow: true }, + ); return EMPTY; }), ); @@ -38,30 +123,69 @@ const initQueryParamsEpic: AppEpic = (action$, state$, { router }) => filter(MarketplaceActions.initQueryParams.match), switchMap(() => { const query = router.query; - const modelReference = query[MarketplaceQueryParams.model] as - | string - | undefined; - const modelsMap = ModelsSelectors.selectModelsMap(state$.value); - const model = modelReference ? modelsMap[modelReference] : undefined; + const actions: Observable[] = []; + // application link + const modelReference = query[MarketplaceQueryParams.model]; + const modelsMap = ModelsSelectors.selectModelsMap(state$.value); + const model = + typeof modelReference === 'string' + ? modelsMap[modelReference] + : undefined; if (modelReference) { if (model) { - return of( - MarketplaceActions.setDetailsModel({ - reference: modelReference, - isSuggested: false, - }), + actions.push( + of( + MarketplaceActions.setDetailsModel({ + reference: modelReference as string, + isSuggested: false, + }), + ), ); } else { - return of(UIActions.showErrorToast('Agent by this link not found')); + actions.push( + of(UIActions.showErrorToast('Agent by this link not found')), + ); } } + // workspace tab + const workSpaceTab = + query[MarketplaceQueryParams.fromConversation] || + !isNil(query[MarketplaceQueryParams.workspace]); + actions.push( + of( + MarketplaceActions.setSelectedTab( + workSpaceTab + ? MarketplaceTabs.MY_APPLICATIONS + : MarketplaceTabs.HOME, + ), + ), + ); + // filters + const types = ((query[MarketplaceQueryParams.types] as string) ?? '') + .split(',') + .filter(Boolean); + const topics = ((query[MarketplaceQueryParams.topics] as string) ?? '') + .split(',') + .filter(Boolean); + actions.push( + of( + MarketplaceActions.setFilters({ + [FilterTypes.ENTITY_TYPE]: types, + [FilterTypes.TOPICS]: topics, + }), + ), + ); + // search + const search = (query[MarketplaceQueryParams.search] as string) ?? ''; + actions.push(of(MarketplaceActions.setSearchTerm(search))); - return EMPTY; + return concat(...actions); }), ); export const MarketplaceEpics = combineEpics( initQueryParamsEpic, - setDetailsModelEpic, + // setDetailsModelEpic, + setQueryParamsEpic, ); diff --git a/apps/chat/src/store/marketplace/marketplace.reducers.ts b/apps/chat/src/store/marketplace/marketplace.reducers.ts index beef841c04..8109a07b50 100644 --- a/apps/chat/src/store/marketplace/marketplace.reducers.ts +++ b/apps/chat/src/store/marketplace/marketplace.reducers.ts @@ -9,13 +9,14 @@ import xor from 'lodash/xor'; export { MarketplaceSelectors }; +interface Filters { + [FilterTypes.ENTITY_TYPE]: string[]; + [FilterTypes.TOPICS]: string[]; + // [FilterTypes.CAPABILITIES]: string[]; + // [FilterTypes.ENVIRONMENT]: string[]; +} export interface MarketplaceState { - selectedFilters: { - [FilterTypes.ENTITY_TYPE]: string[]; - [FilterTypes.TOPICS]: string[]; - // [FilterTypes.CAPABILITIES]: string[]; - // [FilterTypes.ENVIRONMENT]: string[]; - }; + selectedFilters: Filters; searchTerm: string; selectedTab: MarketplaceTabs; applyModelStatus: UploadStatus; @@ -42,6 +43,9 @@ export const marketplaceSlice = createSlice({ initialState, reducers: { initQueryParams: (state) => state, + setFilters: (state, { payload }: PayloadAction) => { + state.selectedFilters = payload; + }, setSelectedFilters: ( state, { payload }: PayloadAction<{ filterType: FilterTypes; value: string }>, From f326bad119ca259f285583c41f1b9c8b0d697498 Mon Sep 17 00:00:00 2001 From: Ilya Bondar Date: Tue, 21 Jan 2025 00:10:23 +0100 Subject: [PATCH 04/11] clean up --- .../store/marketplace/marketplace.epics.ts | 24 ------------------- 1 file changed, 24 deletions(-) diff --git a/apps/chat/src/store/marketplace/marketplace.epics.ts b/apps/chat/src/store/marketplace/marketplace.epics.ts index 0ec522b6fa..4989b5ad82 100644 --- a/apps/chat/src/store/marketplace/marketplace.epics.ts +++ b/apps/chat/src/store/marketplace/marketplace.epics.ts @@ -25,29 +25,6 @@ import { import isNil from 'lodash/isNil'; import { ParsedUrlQueryInput } from 'querystring'; -// const setDetailsModelEpic: AppEpic = (action$, _, { router }) => -// action$.pipe( -// filter(MarketplaceActions.setDetailsModel.match), -// switchMap(({ payload }) => { -// const reference = payload?.reference; -// const query = router.query; -// if (reference) { -// router.push({ -// query: { -// ...query, -// [MarketplaceQueryParams.model]: reference, -// }, -// }); -// } else { -// delete query[MarketplaceQueryParams.model]; -// router.push({ -// query, -// }); -// } -// return EMPTY; -// }), -// ); - const addToQuery = ( query: ParsedUrlQueryInput, key: string, @@ -186,6 +163,5 @@ const initQueryParamsEpic: AppEpic = (action$, state$, { router }) => export const MarketplaceEpics = combineEpics( initQueryParamsEpic, - // setDetailsModelEpic, setQueryParamsEpic, ); From 4bccec01c1ba7051b26c8f55b6fe75725293a317 Mon Sep 17 00:00:00 2001 From: Ilya Bondar Date: Tue, 21 Jan 2025 00:37:53 +0100 Subject: [PATCH 05/11] add validation --- .../Marketplace/MarketplaceFilterbar.tsx | 21 +++++++------------ apps/chat/src/constants/marketplace.ts | 8 +++++++ .../store/marketplace/marketplace.epics.ts | 16 +++++++++----- .../store/marketplace/marketplace.reducers.ts | 12 ++++------- apps/chat/src/types/marketplace.ts | 8 +++++++ 5 files changed, 39 insertions(+), 26 deletions(-) create mode 100644 apps/chat/src/types/marketplace.ts diff --git a/apps/chat/src/components/Marketplace/MarketplaceFilterbar.tsx b/apps/chat/src/components/Marketplace/MarketplaceFilterbar.tsx index d3ba12c557..246c508452 100644 --- a/apps/chat/src/components/Marketplace/MarketplaceFilterbar.tsx +++ b/apps/chat/src/components/Marketplace/MarketplaceFilterbar.tsx @@ -11,7 +11,7 @@ import { useTranslation } from 'next-i18next'; import classNames from 'classnames'; -import { EntityType } from '@/src/types/common'; +import { MarketplaceFilters } from '@/src/types/marketplace'; import { Translation } from '@/src/types/translation'; import { useAppDispatch, useAppSelector } from '@/src/store/hooks'; @@ -22,7 +22,11 @@ import { import { ModelsSelectors } from '@/src/store/models/models.reducers'; import { UISelectors } from '@/src/store/ui/ui.reducers'; -import { FilterTypes, MarketplaceTabs } from '@/src/constants/marketplace'; +import { + ENTITY_TYPES, + FilterTypes, + MarketplaceTabs, +} from '@/src/constants/marketplace'; import Tooltip from '../Common/Tooltip'; @@ -68,10 +72,7 @@ const FilterItem = ({ interface FilterSectionProps { sectionName: string; openedSections: Record; - selectedFilters: { - Type: string[]; - Topics: string[]; - }; + selectedFilters: MarketplaceFilters; filterValues: string[]; filterType: FilterTypes; onToggleFilterSection: (filterType: FilterTypes) => void; @@ -129,12 +130,6 @@ const FilterSection = ({ ); }; -const entityTypes = [ - EntityType.Model, - EntityType.Assistant, - EntityType.Application, -]; - interface ActionButtonProps { isOpen: boolean; onClick: () => void; @@ -262,7 +257,7 @@ export const MarketplaceFilterbar = () => { <> filter(MarketplaceActions.initQueryParams.match), switchMap(() => { const query = router.query; + const state = state$.value; const actions: Observable[] = []; // application link const modelReference = query[MarketplaceQueryParams.model]; - const modelsMap = ModelsSelectors.selectModelsMap(state$.value); + const modelsMap = ModelsSelectors.selectModelsMap(state); const model = typeof modelReference === 'string' ? modelsMap[modelReference] @@ -139,12 +142,15 @@ const initQueryParamsEpic: AppEpic = (action$, state$, { router }) => ), ); // filters - const types = ((query[MarketplaceQueryParams.types] as string) ?? '') - .split(',') - .filter(Boolean); + const existingTopics = ModelsSelectors.selectModelTopics(state); const topics = ((query[MarketplaceQueryParams.topics] as string) ?? '') .split(',') - .filter(Boolean); + .filter((topic) => topic && existingTopics.includes(topic)); + + const types = ((query[MarketplaceQueryParams.types] as string) ?? '') + .split(',') + .filter((type) => type && ENTITY_TYPES.includes(type as EntityType)); + actions.push( of( MarketplaceActions.setFilters({ diff --git a/apps/chat/src/store/marketplace/marketplace.reducers.ts b/apps/chat/src/store/marketplace/marketplace.reducers.ts index 8109a07b50..197952cdd2 100644 --- a/apps/chat/src/store/marketplace/marketplace.reducers.ts +++ b/apps/chat/src/store/marketplace/marketplace.reducers.ts @@ -1,5 +1,7 @@ import { PayloadAction, createSlice } from '@reduxjs/toolkit'; +import { MarketplaceFilters } from '@/src/types/marketplace'; + import { FilterTypes, MarketplaceTabs } from '@/src/constants/marketplace'; import * as MarketplaceSelectors from './marketplace.selectors'; @@ -9,14 +11,8 @@ import xor from 'lodash/xor'; export { MarketplaceSelectors }; -interface Filters { - [FilterTypes.ENTITY_TYPE]: string[]; - [FilterTypes.TOPICS]: string[]; - // [FilterTypes.CAPABILITIES]: string[]; - // [FilterTypes.ENVIRONMENT]: string[]; -} export interface MarketplaceState { - selectedFilters: Filters; + selectedFilters: MarketplaceFilters; searchTerm: string; selectedTab: MarketplaceTabs; applyModelStatus: UploadStatus; @@ -43,7 +39,7 @@ export const marketplaceSlice = createSlice({ initialState, reducers: { initQueryParams: (state) => state, - setFilters: (state, { payload }: PayloadAction) => { + setFilters: (state, { payload }: PayloadAction) => { state.selectedFilters = payload; }, setSelectedFilters: ( diff --git a/apps/chat/src/types/marketplace.ts b/apps/chat/src/types/marketplace.ts new file mode 100644 index 0000000000..0560adf7e4 --- /dev/null +++ b/apps/chat/src/types/marketplace.ts @@ -0,0 +1,8 @@ +import { FilterTypes } from '../constants/marketplace'; + +export interface MarketplaceFilters { + [FilterTypes.ENTITY_TYPE]: string[]; + [FilterTypes.TOPICS]: string[]; + // [FilterTypes.CAPABILITIES]: string[]; + // [FilterTypes.ENVIRONMENT]: string[]; +} From 625f6d03f4ca0b9c7fceced10e0b6f79574719c1 Mon Sep 17 00:00:00 2001 From: Ilya Bondar Date: Tue, 21 Jan 2025 01:07:00 +0100 Subject: [PATCH 06/11] refactoring --- .../Marketplace/MarketplaceBanner.tsx | 8 +++---- .../Marketplace/MarketplaceFilterbar.tsx | 4 ++-- .../components/Marketplace/SearchHeader.tsx | 2 +- .../components/Marketplace/TabRenderer.tsx | 6 +++--- apps/chat/src/constants/marketplace.ts | 6 +++--- .../store/marketplace/marketplace.epics.ts | 21 +++++++++---------- .../store/marketplace/marketplace.reducers.ts | 4 ++++ 7 files changed, 27 insertions(+), 24 deletions(-) diff --git a/apps/chat/src/components/Marketplace/MarketplaceBanner.tsx b/apps/chat/src/components/Marketplace/MarketplaceBanner.tsx index 4ab7095348..1c27c25003 100644 --- a/apps/chat/src/components/Marketplace/MarketplaceBanner.tsx +++ b/apps/chat/src/components/Marketplace/MarketplaceBanner.tsx @@ -16,24 +16,24 @@ import lightBanner from '@/public/images/banners/welcome-light.jpg'; const bannerText = { title: { [MarketplaceTabs.HOME]: 'Welcome to DIAL Marketplace', - [MarketplaceTabs.MY_APPLICATIONS]: 'My workspace', + [MarketplaceTabs.MY_WORKSPACE]: 'My workspace', }, subtitle: { [MarketplaceTabs.HOME]: 'Explore our AI offerings with your data and see how they boost your productivity!', - [MarketplaceTabs.MY_APPLICATIONS]: + [MarketplaceTabs.MY_WORKSPACE]: 'Here you can manage the AI agents used in your daily work', }, }; const getBannerSrc = (theme: string, tab: MarketplaceTabs) => { if (theme === 'dark') { - return tab === MarketplaceTabs.MY_APPLICATIONS + return tab === MarketplaceTabs.MY_WORKSPACE ? darkMyAppsBanner.src : darkBanner.src; } - return tab === MarketplaceTabs.MY_APPLICATIONS + return tab === MarketplaceTabs.MY_WORKSPACE ? lightMyAppsBanner.src : lightBanner.src; }; diff --git a/apps/chat/src/components/Marketplace/MarketplaceFilterbar.tsx b/apps/chat/src/components/Marketplace/MarketplaceFilterbar.tsx index 246c508452..81d49d3c48 100644 --- a/apps/chat/src/components/Marketplace/MarketplaceFilterbar.tsx +++ b/apps/chat/src/components/Marketplace/MarketplaceFilterbar.tsx @@ -215,7 +215,7 @@ export const MarketplaceFilterbar = () => { ); const handleMyAppsClick = useCallback( - () => handleChangeTab(MarketplaceTabs.MY_APPLICATIONS), + () => handleChangeTab(MarketplaceTabs.MY_WORKSPACE), [handleChangeTab], ); @@ -249,7 +249,7 @@ export const MarketplaceFilterbar = () => { onClick={handleMyAppsClick} caption={t('My workspace')} Icon={IconHome2} - selected={selectedTab === MarketplaceTabs.MY_APPLICATIONS} + selected={selectedTab === MarketplaceTabs.MY_WORKSPACE} dataQa="my-applications" /> diff --git a/apps/chat/src/components/Marketplace/SearchHeader.tsx b/apps/chat/src/components/Marketplace/SearchHeader.tsx index 631dbe8e22..fd85754b2a 100644 --- a/apps/chat/src/components/Marketplace/SearchHeader.tsx +++ b/apps/chat/src/components/Marketplace/SearchHeader.tsx @@ -167,7 +167,7 @@ export const SearchHeader = ({ className="w-full rounded border border-primary bg-transparent py-2.5 pl-[38px] pr-3 leading-4 outline-none placeholder:text-secondary focus-visible:border-accent-primary" /> - {selectedTab === MarketplaceTabs.MY_APPLICATIONS && ( + {selectedTab === MarketplaceTabs.MY_WORKSPACE && ( )} diff --git a/apps/chat/src/components/Marketplace/TabRenderer.tsx b/apps/chat/src/components/Marketplace/TabRenderer.tsx index d8eefd47b6..6321329fa3 100644 --- a/apps/chat/src/components/Marketplace/TabRenderer.tsx +++ b/apps/chat/src/components/Marketplace/TabRenderer.tsx @@ -278,12 +278,12 @@ export const TabRenderer = ({ screenState }: TabRendererProps) => { installedModelIds.has(entity.reference); const entitiesForTab = - selectedTab === MarketplaceTabs.MY_APPLICATIONS + selectedTab === MarketplaceTabs.MY_WORKSPACE ? filteredEntities.filter(isInstalledModel) : filteredEntities; const shouldSuggest = - selectedTab === MarketplaceTabs.MY_APPLICATIONS && isSomeFilterNotEmpty; + selectedTab === MarketplaceTabs.MY_WORKSPACE && isSomeFilterNotEmpty; const groupedEntities = groupModelsAndSaveOrder( entitiesForTab.concat(shouldSuggest ? filteredEntities : []), @@ -481,7 +481,7 @@ export const TabRenderer = ({ screenState }: TabRendererProps) => { onEdit={handleEditApplication} onBookmarkClick={handleBookmarkClick} allEntities={allModels} - isMyAppsTab={selectedTab === MarketplaceTabs.MY_APPLICATIONS} + isMyAppsTab={selectedTab === MarketplaceTabs.MY_WORKSPACE} isSuggested={detailsModel.isSuggested} /> )} diff --git a/apps/chat/src/constants/marketplace.ts b/apps/chat/src/constants/marketplace.ts index 025f983faf..a9ba29bdb2 100644 --- a/apps/chat/src/constants/marketplace.ts +++ b/apps/chat/src/constants/marketplace.ts @@ -3,7 +3,7 @@ import { EntityType } from '../types/common'; export enum MarketplaceQueryParams { fromConversation = 'fromConversation', model = 'model', - workspace = 'workspace', + tab = 'tab', types = 'types', topics = 'topics', search = 'search', @@ -17,8 +17,8 @@ export enum FilterTypes { } export enum MarketplaceTabs { - HOME = 'HOME', - MY_APPLICATIONS = 'MY_APPLICATIONS', + HOME = 'marketplace', + MY_WORKSPACE = 'workspace', } export enum DeleteType { diff --git a/apps/chat/src/store/marketplace/marketplace.epics.ts b/apps/chat/src/store/marketplace/marketplace.epics.ts index 83013b76f5..86b0c0bd63 100644 --- a/apps/chat/src/store/marketplace/marketplace.epics.ts +++ b/apps/chat/src/store/marketplace/marketplace.epics.ts @@ -24,8 +24,7 @@ import { selectSelectedTab, } from './marketplace.selectors'; -import isNil from 'lodash/isNil'; -import { ParsedUrlQueryInput } from 'querystring'; +import { ParsedUrlQueryInput, parse } from 'querystring'; const addToQuery = ( query: ParsedUrlQueryInput, @@ -51,13 +50,15 @@ const setQueryParamsEpic: AppEpic = (action$, state$, { router }) => ), switchMap(() => { const state = state$.value; - const query = router.query; + const query = parse(window.location.search.slice(1)); // workspace tab const selectedTab = selectSelectedTab(state); addToQuery( query, - MarketplaceQueryParams.workspace, - selectedTab === MarketplaceTabs.MY_APPLICATIONS ? '1' : undefined, + MarketplaceQueryParams.tab, + selectedTab === MarketplaceTabs.MY_WORKSPACE + ? MarketplaceTabs.MY_WORKSPACE + : undefined, ); // application link const reference = selectDetailsModel(state)?.reference; @@ -97,11 +98,11 @@ const setQueryParamsEpic: AppEpic = (action$, state$, { router }) => }), ); -const initQueryParamsEpic: AppEpic = (action$, state$, { router }) => +const initQueryParamsEpic: AppEpic = (action$, state$) => action$.pipe( filter(MarketplaceActions.initQueryParams.match), switchMap(() => { - const query = router.query; + const query = parse(window.location.search.slice(1)); const state = state$.value; const actions: Observable[] = []; @@ -131,13 +132,11 @@ const initQueryParamsEpic: AppEpic = (action$, state$, { router }) => // workspace tab const workSpaceTab = query[MarketplaceQueryParams.fromConversation] || - !isNil(query[MarketplaceQueryParams.workspace]); + query[MarketplaceQueryParams.tab] === MarketplaceTabs.MY_WORKSPACE; actions.push( of( MarketplaceActions.setSelectedTab( - workSpaceTab - ? MarketplaceTabs.MY_APPLICATIONS - : MarketplaceTabs.HOME, + workSpaceTab ? MarketplaceTabs.MY_WORKSPACE : MarketplaceTabs.HOME, ), ), ); diff --git a/apps/chat/src/store/marketplace/marketplace.reducers.ts b/apps/chat/src/store/marketplace/marketplace.reducers.ts index 197952cdd2..628dc96897 100644 --- a/apps/chat/src/store/marketplace/marketplace.reducers.ts +++ b/apps/chat/src/store/marketplace/marketplace.reducers.ts @@ -16,6 +16,7 @@ export interface MarketplaceState { searchTerm: string; selectedTab: MarketplaceTabs; applyModelStatus: UploadStatus; + applyModelId?: string; detailsModel: { reference: string; isSuggested: boolean } | undefined; } @@ -64,6 +65,9 @@ export const marketplaceSlice = createSlice({ setApplyModelStatus: (state, { payload }: PayloadAction) => { state.applyModelStatus = payload; }, + setApplyModelId: (state, { payload }: PayloadAction) => { + state.applyModelId = payload; + }, setDetailsModel: ( state, { From 5ae87b9dab942ee8c32092afc7e2427b36a092f8 Mon Sep 17 00:00:00 2001 From: Ilya Bondar Date: Tue, 21 Jan 2025 01:22:31 +0100 Subject: [PATCH 07/11] limit for search --- apps/chat/src/store/marketplace/marketplace.reducers.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/chat/src/store/marketplace/marketplace.reducers.ts b/apps/chat/src/store/marketplace/marketplace.reducers.ts index 628dc96897..df48b6708f 100644 --- a/apps/chat/src/store/marketplace/marketplace.reducers.ts +++ b/apps/chat/src/store/marketplace/marketplace.reducers.ts @@ -53,7 +53,7 @@ export const marketplaceSlice = createSlice({ ); }, setSearchTerm: (state, { payload }: PayloadAction) => { - state.searchTerm = payload; + state.searchTerm = payload.slice(0, 120); // limit to 120 characters }, setSelectedTab: (state, { payload }: PayloadAction) => { state.selectedTab = payload; From 842482d1708c6f067cc5f260c6024e4cc417f065 Mon Sep 17 00:00:00 2001 From: Ilya Bondar Date: Tue, 21 Jan 2025 11:49:24 +0100 Subject: [PATCH 08/11] commit code review suggestion Co-authored-by: Alexander <98586297+Alexander-Kezik@users.noreply.github.com> --- .../Marketplace/ApplicationDetails/ApplicationCopyLink.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/chat/src/components/Marketplace/ApplicationDetails/ApplicationCopyLink.tsx b/apps/chat/src/components/Marketplace/ApplicationDetails/ApplicationCopyLink.tsx index 99872df2fc..89ec4830dd 100644 --- a/apps/chat/src/components/Marketplace/ApplicationDetails/ApplicationCopyLink.tsx +++ b/apps/chat/src/components/Marketplace/ApplicationDetails/ApplicationCopyLink.tsx @@ -24,7 +24,7 @@ const ApplicationCopyLink: React.FC = ({ const timeoutRef = useRef>(); const link = useMemo( () => - `${window.location.origin}/marketplace?${MarketplaceQueryParams.model}=${entity.reference}`, + `${window.location.origin}/${PageType.Marketplace}?${MarketplaceQueryParams.model}=${entity.reference}`, [entity.reference], ); const handleCopy = useCallback( From d8c04b2469ccb0b87df54918df55b7c5f11d5567 Mon Sep 17 00:00:00 2001 From: Ilya Bondar Date: Tue, 21 Jan 2025 11:51:21 +0100 Subject: [PATCH 09/11] code review resolving --- .../ApplicationDetails/ApplicationCopyLink.tsx | 8 ++------ .../Marketplace/ApplicationDetails/ApplicationHeader.tsx | 2 +- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/apps/chat/src/components/Marketplace/ApplicationDetails/ApplicationCopyLink.tsx b/apps/chat/src/components/Marketplace/ApplicationDetails/ApplicationCopyLink.tsx index 99872df2fc..1cabdde2ad 100644 --- a/apps/chat/src/components/Marketplace/ApplicationDetails/ApplicationCopyLink.tsx +++ b/apps/chat/src/components/Marketplace/ApplicationDetails/ApplicationCopyLink.tsx @@ -16,9 +16,7 @@ const ICON_SIZE = 20; const TRIGGER_CLASS = 'flex items-center gap-2 whitespace-nowrap px-3 py-1.5 text-sm text-accent-primary outline-none'; -const ApplicationCopyLink: React.FC = ({ - entity, -}) => { +export function ApplicationCopyLink({ entity }: ApplicationCopyLinkProps) { const { t } = useTranslation(Translation.Marketplace); const [urlCopied, setUrlCopied] = useState(false); const timeoutRef = useRef>(); @@ -64,6 +62,4 @@ const ApplicationCopyLink: React.FC = ({ )} ); -}; - -export default ApplicationCopyLink; +} diff --git a/apps/chat/src/components/Marketplace/ApplicationDetails/ApplicationHeader.tsx b/apps/chat/src/components/Marketplace/ApplicationDetails/ApplicationHeader.tsx index 88470621c9..e59bad94ec 100644 --- a/apps/chat/src/components/Marketplace/ApplicationDetails/ApplicationHeader.tsx +++ b/apps/chat/src/components/Marketplace/ApplicationDetails/ApplicationHeader.tsx @@ -20,7 +20,7 @@ import { FunctionStatusIndicator } from '@/src/components/Marketplace/FunctionSt import { ModelIcon } from '../../Chatbar/ModelIcon'; import { ApplicationTopic } from '../ApplicationTopic'; -import ApplicationCopyLink from './ApplicationCopyLink'; +import { ApplicationCopyLink } from './ApplicationCopyLink'; import { Feature } from '@epam/ai-dial-shared'; From dd808352a1f66d009abeca7a1e5618645cf8fdf2 Mon Sep 17 00:00:00 2001 From: Ilya Bondar Date: Tue, 21 Jan 2025 11:51:57 +0100 Subject: [PATCH 10/11] import --- .../Marketplace/ApplicationDetails/ApplicationCopyLink.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/chat/src/components/Marketplace/ApplicationDetails/ApplicationCopyLink.tsx b/apps/chat/src/components/Marketplace/ApplicationDetails/ApplicationCopyLink.tsx index 1cd36d8a69..0c7ab2a824 100644 --- a/apps/chat/src/components/Marketplace/ApplicationDetails/ApplicationCopyLink.tsx +++ b/apps/chat/src/components/Marketplace/ApplicationDetails/ApplicationCopyLink.tsx @@ -3,6 +3,7 @@ import { MouseEvent, useCallback, useMemo, useRef, useState } from 'react'; import { useTranslation } from 'next-i18next'; +import { PageType } from '@/src/types/common'; import { DialAIEntityModel } from '@/src/types/models'; import { Translation } from '@/src/types/translation'; From 0dcec115dd3a6f8a3f016092ebfd2412b74f6531 Mon Sep 17 00:00:00 2001 From: Ilya Bondar Date: Tue, 21 Jan 2025 12:27:04 +0100 Subject: [PATCH 11/11] fix loader when change route --- apps/chat/src/components/Layout.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/apps/chat/src/components/Layout.tsx b/apps/chat/src/components/Layout.tsx index 5ba86a7f7a..d0d399e9bb 100644 --- a/apps/chat/src/components/Layout.tsx +++ b/apps/chat/src/components/Layout.tsx @@ -26,6 +26,8 @@ import { UIActions } from '@/src/store/ui/ui.reducers'; import Loader from './Common/Loader'; +const removeQueryString = (url: string) => url.split('?')[0]; + export default function Layout({ children, settings, @@ -55,7 +57,10 @@ export default function Layout({ ); const shouldOverlayLogin = isOverlay && shouldLogin; - const handleStartRedirecting = useCallback(() => setLoading(false), []); // TODO: how show loader for page switching? + + const handleStartRedirecting = useCallback((url: string) => { + setLoading(removeQueryString(url) !== window.location.pathname); + }, []); const handleStopRedirecting = useCallback(() => setLoading(false), []); // EFFECTS --------------------------------------------