From d941e1c9adc606eba5710f6d4dff9bbe1a4ac697 Mon Sep 17 00:00:00 2001 From: Alexander <98586297+Alexander-Kezik@users.noreply.github.com> Date: Sat, 21 Dec 2024 16:35:52 +0100 Subject: [PATCH] fix(chat): fix slider overload and keyboard arrows navigation (Issue #2825) (#2862) --- .../src/components/Chat/TalkTo/TalkToCard.tsx | 22 +- .../components/Chat/TalkTo/TalkToModal.tsx | 458 +++--------------- .../components/Chat/TalkTo/TalkToSlider.tsx | 419 ++++++++++++++++ 3 files changed, 500 insertions(+), 399 deletions(-) create mode 100644 apps/chat/src/components/Chat/TalkTo/TalkToSlider.tsx diff --git a/apps/chat/src/components/Chat/TalkTo/TalkToCard.tsx b/apps/chat/src/components/Chat/TalkTo/TalkToCard.tsx index 7d6004337..360405d99 100644 --- a/apps/chat/src/components/Chat/TalkTo/TalkToCard.tsx +++ b/apps/chat/src/components/Chat/TalkTo/TalkToCard.tsx @@ -22,7 +22,6 @@ import { isExecutableApp, } from '@/src/utils/app/application'; import { getRootId } from '@/src/utils/app/id'; -import { isEntityIdPublic } from '@/src/utils/app/publications'; import { PseudoModel, isPseudoModel } from '@/src/utils/server/api'; import { @@ -53,8 +52,7 @@ import { ApplicationTopic } from '@/src/components/Marketplace/ApplicationTopic' import { FunctionStatusIndicator } from '@/src/components/Marketplace/FunctionStatusIndicator'; import LoaderIcon from '@/public/images/icons/loader.svg'; -import UnpublishIcon from '@/public/images/icons/unpublish.svg'; -import { Feature, PublishActions } from '@epam/ai-dial-shared'; +import { Feature } from '@epam/ai-dial-shared'; const DESKTOP_ICON_SIZE = 80; const TABLET_ICON_SIZE = 48; @@ -82,7 +80,7 @@ interface ApplicationCardProps { disabled: boolean; isUnavailableModel: boolean; onClick: (entity: DialAIEntityModel) => void; - onPublish: (entity: DialAIEntityModel, action: PublishActions) => void; + onPublish: (entity: DialAIEntityModel) => void; onDelete: (entity: DialAIEntityModel) => void; onEdit: (entity: DialAIEntityModel) => void; onSelectVersion: (entity: DialAIEntityModel) => void; @@ -159,7 +157,7 @@ export const TalkToCard = ({ const handleSelectVersion = useCallback( (model: DialAIEntityModel) => { - onSelectVersion?.(model); + onSelectVersion(model); }, [onSelectVersion], ); @@ -215,17 +213,7 @@ export const TalkToCard = ({ Icon: IconWorldShare, onClick: (e: React.MouseEvent) => { e.stopPropagation(); - onPublish?.(entity, PublishActions.ADD); - }, - }, - { - name: t('Unpublish'), - dataQa: 'unpublish', - display: isEntityIdPublic(entity) && !!onPublish, - Icon: UnpublishIcon, - onClick: (e: React.MouseEvent) => { - e.stopPropagation(); - onPublish?.(entity, PublishActions.DELETE); + onPublish(entity); }, }, { @@ -326,7 +314,7 @@ export const TalkToCard = ({ )} -
+
{!!versionsToSelect.length && (

{t('Version')}:

diff --git a/apps/chat/src/components/Chat/TalkTo/TalkToModal.tsx b/apps/chat/src/components/Chat/TalkTo/TalkToModal.tsx index d9434e8b7..c84951111 100644 --- a/apps/chat/src/components/Chat/TalkTo/TalkToModal.tsx +++ b/apps/chat/src/components/Chat/TalkTo/TalkToModal.tsx @@ -1,9 +1,5 @@ -import { - IconCaretLeftFilled, - IconCaretRightFilled, - IconSearch, -} from '@tabler/icons-react'; -import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { IconSearch } from '@tabler/icons-react'; +import { useCallback, useMemo, useState } from 'react'; import { useDispatch } from 'react-redux'; import { useTranslation } from 'next-i18next'; @@ -11,9 +7,6 @@ import Link from 'next/link'; import classNames from 'classnames'; -import { useScreenState } from '@/src/hooks/useScreenState'; -import { useSwipe } from '@/src/hooks/useSwipe'; - import { getApplicationType } from '@/src/utils/app/application'; import { getConversationModelParams, @@ -21,10 +14,10 @@ import { } from '@/src/utils/app/conversation'; import { getFolderIdFromEntityId } from '@/src/utils/app/folders'; import { doesEntityContainSearchTerm } from '@/src/utils/app/search'; -import { ApiUtils, PseudoModel, isPseudoModel } from '@/src/utils/server/api'; +import { ApiUtils, PseudoModel } from '@/src/utils/server/api'; import { Conversation } from '@/src/types/chat'; -import { EntityType, ScreenState } from '@/src/types/common'; +import { EntityType } from '@/src/types/common'; import { ModalState } from '@/src/types/modal'; import { DialAIEntityModel } from '@/src/types/models'; import { SharingType } from '@/src/types/share'; @@ -44,121 +37,12 @@ import { PublishModal } from '@/src/components/Chat/Publish/PublishWizard'; import { ApplicationWizard } from '@/src/components/Common/ApplicationWizard/ApplicationWizard'; import { ConfirmDialog } from '@/src/components/Common/ConfirmDialog'; import Modal from '@/src/components/Common/Modal'; -import { NoResultsFound } from '@/src/components/Common/NoResultsFound'; import { ApplicationLogs } from '../../Marketplace/ApplicationLogs'; -import { TalkToCard } from './TalkToCard'; +import { TalkToSlider } from './TalkToSlider'; import { Feature, PublishActions, ShareEntity } from '@epam/ai-dial-shared'; -import chunk from 'lodash-es/chunk'; import orderBy from 'lodash-es/orderBy'; -import range from 'lodash-es/range'; - -const COMMON_GRID_TILES_GAP = 16; -const MOBILE_GRID_TILES_GAP = 12; -const getMaxChunksCountConfig = () => { - return { - [ScreenState.DESKTOP]: { - cardHeight: 166, - maxRows: 3, - cols: 3, - }, - [ScreenState.TABLET]: { - cardHeight: 160, - maxRows: 4, - cols: 2, - }, - [ScreenState.MOBILE]: { - cardHeight: 98, - maxRows: 5, - cols: 1, - }, - }; -}; -interface SliderModelsGroupProps { - modelsGroup: DialAIEntityModel[]; - conversation: Conversation; - screenState: ScreenState; - rowsCount: number; - onEditApplication: (entity: DialAIEntityModel) => void; - onDeleteApplication: (entity: DialAIEntityModel) => void; - onSetPublishEntity: ( - entity: DialAIEntityModel, - action: PublishActions, - ) => void; - onSelectModel: (entity: DialAIEntityModel) => void; - onOpenLogs: (entity: DialAIEntityModel) => void; -} -const SliderModelsGroup = ({ - modelsGroup, - conversation, - screenState, - rowsCount, - onEditApplication, - onDeleteApplication, - onSetPublishEntity, - onSelectModel, - onOpenLogs, -}: SliderModelsGroupProps) => { - const config = getMaxChunksCountConfig(); - - const modelsMap = useAppSelector(ModelsSelectors.selectModelsMap); - - return ( -
model.id).join('.')} - className="h-full min-w-full" - > -
- {modelsGroup.map((model) => { - const isNotPseudoModelSelected = - model.reference === conversation.model.id && - !conversation.playback?.isPlayback && - !conversation.replay?.replayAsIs; - const isPseudoModelSelected = - model.reference === PseudoModel.Playback || - (model.reference === REPLAY_AS_IS_MODEL && - !!conversation.replay?.replayAsIs); - - return ( - - ); - })} -
-
- ); -}; interface TalkToModalViewProps { conversation: Conversation; @@ -167,15 +51,6 @@ interface TalkToModalViewProps { onClose: () => void; } -const SLIDES_GAP = 16; -const calculateTranslateX = (activeSlide: number, clientWidth?: number) => { - if (!clientWidth) return 'none'; - - const offset = activeSlide * (clientWidth + SLIDES_GAP); - - return `translateX(-${offset}px)`; -}; - const TalkToModalView = ({ conversation, isCompareMode, @@ -186,6 +61,9 @@ const TalkToModalView = ({ const dispatch = useDispatch(); + const isMarketplaceEnabled = useAppSelector((state) => + SettingsSelectors.isFeatureEnabled(state, Feature.Marketplace), + ); const allModels = useAppSelector(ModelsSelectors.selectModels); const modelsMap = useAppSelector(ModelsSelectors.selectModelsMap); const addonsMap = useAppSelector(AddonsSelectors.selectAddonsMap); @@ -195,29 +73,17 @@ const TalkToModalView = ({ const recentModelIds = useAppSelector(ModelsSelectors.selectRecentModelsIds); const [searchTerm, setSearchTerm] = useState(''); - const [activeSlide, setActiveSlide] = useState(0); const [editModel, setEditModel] = useState(); const [deleteModel, setDeleteModel] = useState(); const [logModel, setLogModel] = useState(); - const [publishModel, setPublishModel] = useState<{ - entity: ShareEntity & { iconUrl?: string }; - action: PublishActions; - }>(); - const [sliderHeight, setSliderHeight] = useState(0); + const [publishModel, setPublishModel] = useState< + ShareEntity & { iconUrl?: string } + >(); const [sharedConversationNewModel, setSharedConversationNewModel] = useState(); - const [isOpenLogs, setIsOpenLogs] = useState(); - - const sliderRef = useRef(null); - - const screenState = useScreenState(); const isPlayback = conversation.playback?.isPlayback; const isReplay = conversation.replay?.isReplay; - const config = getMaxChunksCountConfig(); - const isMarketplaceEnabled = useAppSelector((state) => - SettingsSelectors.isFeatureEnabled(state, Feature.Marketplace), - ); const displayedModels = useMemo(() => { const currentModel = modelsMap[conversation.model.id]; @@ -307,70 +173,6 @@ const TalkToModalView = ({ t, ]); - const sliderRowsCount = useMemo(() => { - const availableRows = - Math.floor(sliderHeight / config[screenState].cardHeight) || 1; - - const finalRows = - availableRows === 1 - ? availableRows - : Math.floor( - (sliderHeight - - (availableRows - 1) * - (screenState === ScreenState.MOBILE - ? MOBILE_GRID_TILES_GAP - : COMMON_GRID_TILES_GAP)) / - config[screenState].cardHeight, - ) || 1; - - return finalRows > config[screenState].maxRows - ? config[screenState].maxRows - : finalRows; - }, [config, screenState, sliderHeight]); - - const sliderGroups = useMemo(() => { - return chunk(displayedModels, sliderRowsCount * config[screenState].cols); - }, [config, displayedModels, screenState, sliderRowsCount]); - - const sliderDotsArray = range(0, sliderGroups.length); - - const swipeHandlers = useSwipe({ - onSwipedLeft: () => { - setActiveSlide((slide) => - slide >= sliderGroups.length - 1 ? sliderGroups.length - 1 : slide + 1, - ); - }, - onSwipedRight: () => { - setActiveSlide((slide) => (slide === 0 ? 0 : slide - 1)); - }, - }); - - useEffect(() => { - const handleResize = () => { - if (sliderRef.current) { - setSliderHeight(sliderRef.current.clientHeight); - } - }; - - const resizeObserver = new ResizeObserver(handleResize); - - if (sliderRef.current) { - resizeObserver.observe(sliderRef.current); - } - - return () => { - resizeObserver.disconnect(); - }; - }, []); - - useEffect(() => { - if (!sliderGroups.length) { - setActiveSlide(0); - } else if (activeSlide !== 0 && activeSlide > sliderGroups.length - 1) { - setActiveSlide(sliderGroups.length - 1); - } - }, [activeSlide, sliderGroups]); - const handleUpdateConversationModel = useCallback( (entity: DialAIEntityModel) => { const model = modelsMap[entity.reference]; @@ -400,6 +202,15 @@ const TalkToModalView = ({ [addonsMap, conversation, dispatch, modelsMap, onClose], ); + const handleCloseApplicationLogs = useCallback( + () => setLogModel(undefined), + [], + ); + + const handleOpenApplicationLogs = useCallback((entity: DialAIEntityModel) => { + setLogModel(entity); + }, []); + const handleSelectModel = useCallback( (entity: DialAIEntityModel) => { if (conversation.isShared && entity.reference !== conversation.model.id) { @@ -440,32 +251,17 @@ const TalkToModalView = ({ [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 handleSetPublishEntity = useCallback((entity: DialAIEntityModel) => { + setPublishModel({ + name: entity.name, + id: ApiUtils.decodeApiUrl(entity.id), + folderId: getFolderIdFromEntityId(entity.id), + iconUrl: entity.iconUrl, + }); + }, []); const handlePublishClose = useCallback(() => setPublishModel(undefined), []); - const handleCloseApplicationLogs = useCallback( - () => setIsOpenLogs(false), - [setIsOpenLogs], - ); - - const handleOpenApplicationLogs = useCallback((entity: DialAIEntityModel) => { - setIsOpenLogs(true); - setLogModel(entity); - }, []); - const handleDeleteApplication = useCallback( (entity: DialAIEntityModel) => { setDeleteModel(entity); @@ -473,32 +269,6 @@ const TalkToModalView = ({ [setDeleteModel], ); - const handleKeyDown = useCallback( - (e: KeyboardEvent) => { - if (e.key === 'ArrowRight') { - setActiveSlide((activeSlide) => - activeSlide === sliderDotsArray.length - 1 - ? activeSlide - : activeSlide + 1, - ); - } else if (e.key === 'ArrowLeft') { - setActiveSlide((activeSlide) => - activeSlide === 0 ? activeSlide : activeSlide - 1, - ); - } - }, - [sliderDotsArray.length], - ); - - useEffect(() => { - if (isPlayback) { - window.addEventListener('keydown', handleKeyDown); - return () => { - window.removeEventListener('keydown', handleKeyDown); - }; - } - }, [handleKeyDown, isPlayback]); - return ( <>

@@ -519,110 +289,33 @@ const TalkToModalView = ({ data-qa="search-agents" />

-
-
+ + {isMarketplaceEnabled && ( + + conversation.playback?.isPlayback ? e.preventDefault() : null + } className={classNames( - 'flex size-full', - sliderGroups.length && 'transition duration-1000 ease-out', + 'm-auto mt-4 text-accent-primary md:absolute md:bottom-6 md:right-6', + conversation.playback?.isPlayback && 'cursor-not-allowed', )} - style={{ - transform: calculateTranslateX( - activeSlide, - sliderRef.current?.clientWidth, - ), - gap: `${SLIDES_GAP}px`, - }} + data-qa="go-to-my-workspace" > - {sliderGroups.length ? ( - sliderGroups.map((modelsGroup) => ( - model.id).join('.')} - modelsGroup={modelsGroup} - conversation={conversation} - screenState={screenState} - rowsCount={sliderRowsCount} - onEditApplication={handleEditApplication} - onDeleteApplication={handleDeleteApplication} - onSetPublishEntity={handleSetPublishEntity} - onSelectModel={handleSelectModel} - onOpenLogs={handleOpenApplicationLogs} - /> - )) - ) : ( -
- -
- )} -
-
-
-
-
- {sliderDotsArray.length <= 1 && - screenState === ScreenState.MOBILE && ( - - )} - {sliderDotsArray.length > 1 && ( - <> - - {sliderDotsArray.map((slideNumber) => ( - - ))} - - - )} -
- {isMarketplaceEnabled && ( - - conversation.playback?.isPlayback ? e.preventDefault() : null - } - className={classNames( - 'mt-4 text-accent-primary md:mt-0', - conversation.playback?.isPlayback && 'cursor-not-allowed', - )} - data-qa="go-to-my-workspace" - > - {t('Go to My workspace')} - - )} -
-
+ {t('Go to My workspace')} + + )} {editModel && ( )} - {logModel && isOpenLogs && ( + {logModel && ( )} - - { - if (result && sharedConversationNewModel) { - handleUpdateConversationModel(sharedConversationNewModel); + {sharedConversationNewModel && ( + { + if (result && sharedConversationNewModel) { + handleUpdateConversationModel(sharedConversationNewModel); + } - setSharedConversationNewModel(undefined); - }} - /> + setSharedConversationNewModel(undefined); + }} + /> + )} ); }; @@ -712,7 +406,7 @@ export const TalkToModal = ({ portalId="theme-main" state={ModalState.OPENED} dataQa="talk-to-agent" - containerClassName="flex xl:h-fit max-h-full flex-col rounded py-4 px-3 md:p-6 w-full grow items-start justify-center !bg-layer-2 md:w-[728px] md:max-w-[728px] xl:w-[1200px] xl:max-w-[1200px]" + containerClassName="flex xl:h-fit relative max-h-full flex-col rounded py-4 px-3 md:p-6 w-full grow items-start justify-center !bg-layer-2 md:w-[728px] md:max-w-[728px] xl:w-[1200px] xl:max-w-[1200px]" onClose={onClose} > { + if (!clientWidth) return 'none'; + + const offset = activeSlide * (clientWidth + SLIDES_GAP); + + return `translateX(-${offset}px)`; +}; + +const getDotSizeClass = ( + slideNumber: number, + activeSlide: number, + slidesCount: number, +) => { + if (slideNumber === activeSlide) { + return 'h-2 w-8'; + } + + if (slidesCount < 7) { + return 'size-2'; + } + + const offsetActive = activeSlide - slideNumber; + const offsetLast = slidesCount - activeSlide; + + if ( + (offsetActive === 3 && activeSlide > 2 && offsetLast > 3) || + (offsetLast === 3 && slideNumber < activeSlide - 3) || + (offsetLast === 2 && slideNumber < activeSlide - 4) || + (offsetLast === 1 && slideNumber < activeSlide - 5) || + (activeSlide <= 3 && slideNumber === 6) || + (activeSlide > 3 && slideNumber > activeSlide + 2) + ) { + return 'size-1'; + } + + if ( + (offsetActive === 2 && activeSlide > 1 && offsetLast > 3) || + (offsetLast === 3 && slideNumber < activeSlide - 2) || + (offsetLast === 2 && slideNumber < activeSlide - 3) || + (offsetLast === 1 && slideNumber < activeSlide - 4) || + (activeSlide <= 3 && slideNumber === 5) || + (activeSlide > 3 && slideNumber > activeSlide + 1) + ) { + return 'size-1.5'; + } + + return 'size-2'; +}; + +interface SliderModelsGroupProps { + modelsGroup: DialAIEntityModel[]; + conversation: Conversation; + screenState: ScreenState; + rowsCount: number; + onEdit: (entity: DialAIEntityModel) => void; + onDelete: (entity: DialAIEntityModel) => void; + onPublish: (entity: DialAIEntityModel) => void; + onSelectModel: (entity: DialAIEntityModel) => void; + onOpenLogs: (entity: DialAIEntityModel) => void; +} + +const SliderModelsGroup = ({ + modelsGroup, + conversation, + screenState, + rowsCount, + onSelectModel, + ...restProps +}: SliderModelsGroupProps) => { + const modelsMap = useAppSelector(ModelsSelectors.selectModelsMap); + + return ( +
model.id).join('.')} + className="h-full min-w-full" + > +
+ {modelsGroup.map((model) => { + const isNotPseudoModelSelected = + model.reference === conversation.model.id && + !conversation.playback?.isPlayback && + !conversation.replay?.replayAsIs; + const isPseudoModelSelected = + model.reference === PseudoModel.Playback || + (model.reference === REPLAY_AS_IS_MODEL && + !!conversation.replay?.replayAsIs); + + return ( + + ); + })} +
+
+ ); +}; + +interface Props { + conversation: Conversation; + items: DialAIEntityModel[]; + onEdit: (entity: DialAIEntityModel) => void; + onDelete: (entity: DialAIEntityModel) => void; + onPublish: (entity: DialAIEntityModel) => void; + onSelectModel: (entity: DialAIEntityModel) => void; + onOpenLogs: (entity: DialAIEntityModel) => void; +} + +export const TalkToSlider = ({ conversation, items, ...restProps }: Props) => { + const sliderRef = useRef(null); + + const [activeSlide, setActiveSlide] = useState(0); + const [sliderHeight, setSliderHeight] = useState(0); + + const screenState = useScreenState(); + + const sliderRowsCount = useMemo(() => { + const availableRows = + Math.floor(sliderHeight / maxChunksCountConfig[screenState].cardHeight) || + 1; + + const finalRows = + availableRows === 1 + ? availableRows + : Math.floor( + (sliderHeight - + (availableRows - 1) * + (screenState === ScreenState.MOBILE + ? MOBILE_GRID_TILES_GAP + : COMMON_GRID_TILES_GAP)) / + maxChunksCountConfig[screenState].cardHeight, + ) || 1; + + return finalRows > maxChunksCountConfig[screenState].maxRows + ? maxChunksCountConfig[screenState].maxRows + : finalRows; + }, [screenState, sliderHeight]); + + const sliderGroups = useMemo(() => { + return chunk( + items, + sliderRowsCount * maxChunksCountConfig[screenState].cols, + ); + }, [items, screenState, sliderRowsCount]); + + const sliderDotsArray = useMemo(() => { + return range(0, sliderGroups.length); + }, [sliderGroups.length]); + + const handleKeyDown = useCallback( + (e: KeyboardEvent) => { + if (e.key === 'ArrowRight') { + setActiveSlide((activeSlide) => + activeSlide === sliderDotsArray.length - 1 + ? activeSlide + : activeSlide + 1, + ); + } else if (e.key === 'ArrowLeft') { + setActiveSlide((activeSlide) => + activeSlide === 0 ? activeSlide : activeSlide - 1, + ); + } + }, + [sliderDotsArray.length], + ); + + useEffect(() => { + window.addEventListener('keydown', handleKeyDown); + + return () => { + window.removeEventListener('keydown', handleKeyDown); + }; + }, [handleKeyDown]); + + const swipeHandlers = useSwipe({ + onSwipedLeft: () => { + setActiveSlide((slide) => + slide >= sliderGroups.length - 1 ? sliderGroups.length - 1 : slide + 1, + ); + }, + onSwipedRight: () => { + setActiveSlide((slide) => (slide === 0 ? 0 : slide - 1)); + }, + }); + + useEffect(() => { + const handleResize = () => { + if (sliderRef.current) { + setSliderHeight(sliderRef.current.clientHeight); + } + }; + + const resizeObserver = new ResizeObserver(handleResize); + + if (sliderRef.current) { + resizeObserver.observe(sliderRef.current); + } + + return () => { + resizeObserver.disconnect(); + }; + }, []); + + useEffect(() => { + const handleResize = () => { + if (sliderRef.current) { + setSliderHeight(sliderRef.current.clientHeight); + } + }; + + const resizeObserver = new ResizeObserver(handleResize); + + if (sliderRef.current) { + resizeObserver.observe(sliderRef.current); + } + + return () => { + resizeObserver.disconnect(); + }; + }, []); + + useEffect(() => { + if (!sliderGroups.length) { + setActiveSlide(0); + } else if (activeSlide !== 0 && activeSlide > sliderGroups.length - 1) { + setActiveSlide(sliderGroups.length - 1); + } + }, [activeSlide, sliderGroups]); + + const excessDots = sliderDotsArray.length - MAX_VISIBLE_SLIDER_DOTS; + const maxDotsTranslate = Math.max(0, excessDots * SLIDER_DOT_SIZE_WITH_GAPS); + const translateXValue = Math.max( + 0, + Math.min(maxDotsTranslate, (activeSlide - 3) * SLIDER_DOT_SIZE_WITH_GAPS), + ); + + return ( + <> +
+
+ {sliderGroups.length ? ( + sliderGroups.map((modelsGroup) => ( + model.id).join('.')} + modelsGroup={modelsGroup} + conversation={conversation} + screenState={screenState} + rowsCount={sliderRowsCount} + {...restProps} + /> + )) + ) : ( +
+ +
+ )} +
+
+
+
+
+ {sliderDotsArray.length <= 1 && + screenState === ScreenState.MOBILE && ( + + )} + {sliderDotsArray.length > 1 && ( + <> + +
+
+ {sliderDotsArray.map((slideNumber) => { + return ( +
setActiveSlide(slideNumber)} + className="flex min-w-2 items-center justify-center" + > +
+ ); + })} +
+
+ + + )} +
+
+
+ + ); +};