apps/chat/src/components/Marketplace/TabRenderer.tsx (447 lines of code) (raw):
import { IconMessage2 } from '@tabler/icons-react';
import { useCallback, useMemo, useState } from 'react';
import { useTranslation } from 'next-i18next';
import { getApplicationType } from '@/src/utils/app/application';
import { groupModelsAndSaveOrder } from '@/src/utils/app/conversation';
import { getFolderIdFromEntityId } from '@/src/utils/app/folders';
import { doesEntityContainSearchTerm } from '@/src/utils/app/search';
import { translate } from '@/src/utils/app/translation';
import { ApiUtils } from '@/src/utils/server/api';
import {
ApplicationActionType,
ApplicationType,
} from '@/src/types/applications';
import { ScreenState } from '@/src/types/common';
import { DialAIEntityModel } from '@/src/types/models';
import { SharingType } from '@/src/types/share';
import { Translation } from '@/src/types/translation';
import { ApplicationActions } from '@/src/store/application/application.reducers';
import { useAppDispatch, useAppSelector } from '@/src/store/hooks';
import {
MarketplaceActions,
MarketplaceSelectors,
} from '@/src/store/marketplace/marketplace.reducers';
import {
ModelsActions,
ModelsSelectors,
} from '@/src/store/models/models.reducers';
import {
DeleteType,
FilterTypes,
MarketplaceTabs,
} from '@/src/constants/marketplace';
import { PublishModal } from '@/src/components/Chat/Publish/PublishWizard';
import { ApplicationWizard } from '@/src/components/Common/ApplicationWizard/ApplicationWizard';
import { ConfirmDialog } from '@/src/components/Common/ConfirmDialog';
import { ApplicationDetails } from '@/src/components/Marketplace/ApplicationDetails/ApplicationDetails';
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 { PublishActions, ShareEntity } from '@epam/ai-dial-shared';
import intersection from 'lodash-es/intersection';
interface NoAgentsFoundProps {
children: React.ReactNode;
desc: string;
header?: string;
}
const NoAgentsFound = ({ children, desc, header }: NoAgentsFoundProps) => (
<div className="flex grow flex-col items-center justify-center">
{children}
{header && <span className="mt-5 text-lg font-semibold">{header}</span>}
{desc && <span className="mt-4 text-sm font-normal">{desc}</span>}
</div>
);
interface ResultsViewProps {
entities: DialAIEntityModel[];
suggestedResults: DialAIEntityModel[];
selectedTab: MarketplaceTabs;
areAllFiltersEmpty: boolean;
isNotDesktop: boolean;
onCardClick: (entity: DialAIEntityModel, isSuggested?: boolean) => void;
onPublish: (entity: DialAIEntityModel, action: PublishActions) => void;
onDelete: (entity: DialAIEntityModel) => void;
onEdit: (entity: DialAIEntityModel) => void;
onBookmarkClick: (entity: DialAIEntityModel) => void;
}
const ResultsView = ({
entities,
suggestedResults,
areAllFiltersEmpty,
onCardClick,
onPublish,
onDelete,
onEdit,
isNotDesktop,
onBookmarkClick,
}: ResultsViewProps) => {
const { t } = useTranslation(Translation.Marketplace);
const handleSuggestedCardClick = useCallback(
(entity: DialAIEntityModel) => {
onCardClick(entity, true);
},
[onCardClick],
);
if (suggestedResults.length) {
return (
<>
<CardsList
entities={entities}
onCardClick={onCardClick}
onPublish={onPublish}
onDelete={onDelete}
onEdit={onEdit}
isNotDesktop={isNotDesktop}
onBookmarkClick={onBookmarkClick}
/>
{!entities.length && (
<div className="flex items-center gap-1">
<Magnifier
height={32}
width={32}
className="shrink-0 text-secondary"
/>
<span className="text-sm sm:text-base">
{t(
'No results found in My workspace. Look at suggested results from DIAL Marketplace.',
)}
</span>
</div>
)}
<span className="mb-4 mt-5 text-xl md:mt-6 lg:mt-8">
{t('Suggested results from DIAL Marketplace')}
</span>
<CardsList
entities={suggestedResults}
onCardClick={handleSuggestedCardClick}
onPublish={onPublish}
onDelete={onDelete}
onEdit={onEdit}
isNotDesktop={isNotDesktop}
onBookmarkClick={onBookmarkClick}
/>
</>
);
}
if (entities.length) {
return (
<CardsList
entities={entities}
onCardClick={onCardClick}
onPublish={onPublish}
onDelete={onDelete}
onEdit={onEdit}
isNotDesktop={isNotDesktop}
onBookmarkClick={onBookmarkClick}
/>
);
}
if (areAllFiltersEmpty) {
return (
<NoAgentsFound
header={t('No agents') ?? ''}
desc={t("You don't have any agents.") ?? ''}
>
<IconMessage2 size={100} className="stroke-[0.2]" />
</NoAgentsFound>
);
}
return (
<NoAgentsFound
desc={t("Sorry, we couldn't find any results for your search.")}
>
<NoResultsFound iconSize={100} className="gap-5 text-lg font-semibold" />
</NoAgentsFound>
);
};
const getDeleteConfirmationText = (
action: DeleteType,
entity: DialAIEntityModel,
) => {
const translationVariables = {
modelName: entity.name,
modelVersion: entity.version
? translate(' (version {{version}})', { version: entity.version })
: '',
};
const deleteConfirmationText = {
[DeleteType.DELETE]: {
heading: translate('Confirm deleting application'),
description: translate(
'Are you sure you want to delete the {{modelName}}{{modelVersion}}?',
translationVariables,
),
confirmLabel: translate('Delete'),
},
[DeleteType.REMOVE]: {
heading: translate('Confirm removing application'),
description: translate(
'Are you sure you want to remove the {{modelName}}{{modelVersion}} from My workspace?',
translationVariables,
),
confirmLabel: translate('Remove'),
},
};
return deleteConfirmationText[action];
};
interface TabRendererProps {
screenState: ScreenState;
}
export const TabRenderer = ({ screenState }: TabRendererProps) => {
const { t } = useTranslation(Translation.Marketplace);
const dispatch = useAppDispatch();
const installedModelIds = useAppSelector(
ModelsSelectors.selectInstalledModelIds,
);
const selectedTab = useAppSelector(MarketplaceSelectors.selectSelectedTab);
const selectedFilters = useAppSelector(
MarketplaceSelectors.selectSelectedFilters,
);
const searchTerm = useAppSelector(
MarketplaceSelectors.selectTrimmedSearchTerm,
);
const allModels = useAppSelector(ModelsSelectors.selectModels);
const detailsModel = useAppSelector(MarketplaceSelectors.selectDetailsModel);
const modelsMap = useAppSelector(ModelsSelectors.selectModelsMap);
const [suggestedResults, setSuggestedResults] = useState<DialAIEntityModel[]>(
[],
);
const [applicationModel, setApplicationModel] = useState<{
action: ApplicationActionType;
type: ApplicationType;
entity?: DialAIEntityModel;
}>();
const [deleteModel, setDeleteModel] = useState<{
action: DeleteType;
entity: DialAIEntityModel;
}>();
const [publishModel, setPublishModel] = useState<{
entity: ShareEntity & { iconUrl?: string };
action: PublishActions;
}>();
const isSomeFilterNotEmpty =
searchTerm.length ||
selectedFilters[FilterTypes.ENTITY_TYPE].length ||
selectedFilters[FilterTypes.TOPICS].length;
const areAllFiltersEmpty =
!searchTerm.length &&
!selectedFilters[FilterTypes.ENTITY_TYPE].length &&
!selectedFilters[FilterTypes.TOPICS].length;
const displayedEntities = useMemo(() => {
const filteredEntities = allModels.filter(
(entity) =>
(doesEntityContainSearchTerm(entity, searchTerm) ||
(entity.version &&
doesEntityContainSearchTerm(
{ name: entity.version },
searchTerm,
))) &&
(selectedFilters[FilterTypes.ENTITY_TYPE].length
? selectedFilters[FilterTypes.ENTITY_TYPE].includes(entity.type)
: true) &&
(selectedFilters[FilterTypes.TOPICS].length
? intersection(selectedFilters[FilterTypes.TOPICS], entity.topics)
.length
: true),
);
const isInstalledModel = (entity: DialAIEntityModel) =>
installedModelIds.has(entity.reference);
const entitiesForTab =
selectedTab === MarketplaceTabs.MY_APPLICATIONS
? filteredEntities.filter(isInstalledModel)
: filteredEntities;
const shouldSuggest =
selectedTab === MarketplaceTabs.MY_APPLICATIONS && isSomeFilterNotEmpty;
const groupedEntities = groupModelsAndSaveOrder(
entitiesForTab.concat(shouldSuggest ? filteredEntities : []),
);
let orderedEntities = groupedEntities.map(({ entities }) => entities[0]);
if (shouldSuggest) {
const suggestedListWithoutInstalled = orderedEntities.filter(
(entity) => !isInstalledModel(entity),
);
orderedEntities = orderedEntities.filter(isInstalledModel);
setSuggestedResults(suggestedListWithoutInstalled);
} else {
setSuggestedResults([]);
}
return orderedEntities;
}, [
allModels,
selectedTab,
isSomeFilterNotEmpty,
searchTerm,
selectedFilters,
installedModelIds,
]);
const handleAddApplication = useCallback((type: ApplicationType) => {
setApplicationModel({
action: ApplicationActionType.ADD,
type,
});
}, []);
const handleEditApplication = useCallback(
(entity: DialAIEntityModel) => {
dispatch(ApplicationActions.get(entity.id));
setApplicationModel({
entity,
action: ApplicationActionType.EDIT,
type: getApplicationType(entity),
});
},
[dispatch],
);
const handleDeleteClose = useCallback(
(confirm: boolean) => {
if (confirm && deleteModel) {
if (deleteModel.action === DeleteType.REMOVE) {
dispatch(
ModelsActions.removeInstalledModels({
references: [deleteModel.entity.reference],
action: DeleteType.REMOVE,
}),
);
} else if (deleteModel.action === DeleteType.DELETE) {
dispatch(ApplicationActions.delete(deleteModel.entity));
}
dispatch(MarketplaceActions.setDetailsModel());
}
setDeleteModel(undefined);
},
[deleteModel, dispatch],
);
const handleSetPublishEntity = useCallback(
(entity: DialAIEntityModel, action: PublishActions) =>
setPublishModel({
entity: {
name: entity.name,
id: ApiUtils.decodeApiUrl(entity.id),
folderId: getFolderIdFromEntityId(entity.id),
iconUrl: entity.iconUrl,
},
action,
}),
[],
);
const handlePublishClose = useCallback(() => setPublishModel(undefined), []);
const handleDelete = useCallback(
(entity: DialAIEntityModel) => {
setDeleteModel({ entity, action: DeleteType.DELETE });
},
[setDeleteModel],
);
const handleSetDetailsModel = useCallback(
(model: DialAIEntityModel, isSuggested?: boolean) => {
dispatch(
MarketplaceActions.setDetailsModel({
reference: model.reference,
isSuggested: !!isSuggested,
}),
);
},
[dispatch],
);
const handleSetVersion = useCallback(
(model: DialAIEntityModel) => {
if (detailsModel) {
dispatch(
MarketplaceActions.setDetailsModel({
...detailsModel,
reference: model.reference,
}),
);
}
},
[detailsModel, dispatch],
);
const handleCloseApplicationDialog = useCallback(
() => setApplicationModel(undefined),
[setApplicationModel],
);
const handleCloseDetailsDialog = useCallback(
() => dispatch(MarketplaceActions.setDetailsModel()),
[dispatch],
);
const handleBookmarkClick = useCallback(
(entity: DialAIEntityModel) => {
if (installedModelIds.has(entity.reference)) {
setDeleteModel({ entity, action: DeleteType.REMOVE });
} else {
dispatch(
ModelsActions.addInstalledModels({
references: [entity.reference],
showSuccessToast: true,
}),
);
}
},
[dispatch, installedModelIds],
);
const currentDetailsModel = detailsModel && modelsMap[detailsModel.reference];
return (
<>
<header className="mb-5 md:mb-4 xl:mb-6" data-qa="marketplace-header">
<MarketplaceBanner />
<SearchHeader
items={displayedEntities.length}
onAddApplication={handleAddApplication}
/>
</header>
<ResultsView
entities={displayedEntities}
suggestedResults={suggestedResults}
selectedTab={selectedTab}
areAllFiltersEmpty={areAllFiltersEmpty}
onCardClick={handleSetDetailsModel}
onPublish={handleSetPublishEntity}
onDelete={handleDelete}
onEdit={handleEditApplication}
isNotDesktop={screenState !== ScreenState.DESKTOP}
onBookmarkClick={handleBookmarkClick}
/>
{/* MODALS */}
{!!applicationModel && (
<ApplicationWizard
isOpen={!!applicationModel}
onClose={handleCloseApplicationDialog}
isEdit={applicationModel.action === ApplicationActionType.EDIT}
currentReference={applicationModel.entity?.reference}
type={applicationModel.type}
/>
)}
{!!deleteModel && (
<ConfirmDialog
isOpen={!!deleteModel}
{...getDeleteConfirmationText(deleteModel.action, deleteModel.entity)}
onClose={handleDeleteClose}
cancelLabel={t('Cancel')}
/>
)}
{currentDetailsModel && (
<ApplicationDetails
onPublish={handleSetPublishEntity}
isMobileView={screenState === ScreenState.MOBILE}
entity={currentDetailsModel}
onChangeVersion={handleSetVersion}
onClose={handleCloseDetailsDialog}
onDelete={handleDelete}
onEdit={handleEditApplication}
onBookmarkClick={handleBookmarkClick}
allEntities={allModels}
isMyAppsTab={selectedTab === MarketplaceTabs.MY_APPLICATIONS}
isSuggested={detailsModel.isSuggested}
/>
)}
{!!(publishModel && publishModel?.entity?.id) && (
<PublishModal
entity={publishModel.entity}
type={SharingType.Application}
isOpen={!!publishModel}
onClose={handlePublishClose}
publishAction={publishModel.action}
/>
)}
</>
);
};