diff --git a/web_ui/src/pages/project-details/components/project-dataset/dataset-tab-panel.component.tsx b/web_ui/src/pages/project-details/components/project-dataset/dataset-tab-panel.component.tsx index 5555ac26b3..a7c955a0f5 100644 --- a/web_ui/src/pages/project-details/components/project-dataset/dataset-tab-panel.component.tsx +++ b/web_ui/src/pages/project-details/components/project-dataset/dataset-tab-panel.component.tsx @@ -1,7 +1,7 @@ // Copyright (C) 2022-2025 Intel Corporation // LIMITED EDGE SOFTWARE DISTRIBUTION LICENSE -import { Key } from 'react'; +import { Key, useRef } from 'react'; import { useNavigateToAnnotatorRoute } from '@geti/core/src/services/use-navigate-to-annotator-route.hook'; import { Button, Flex, Item, TabList, TabPanels, Tabs, View } from '@geti/ui'; @@ -11,9 +11,8 @@ import { useOverlayTriggerState } from 'react-stately'; import { Dataset } from '../../../../core/projects/dataset.interface'; import { isAnomalyDomain } from '../../../../core/projects/domains'; -import { FUX_NOTIFICATION_KEYS } from '../../../../core/user-settings/dtos/user-settings.interface'; -import { CoachMark } from '../../../../shared/components/coach-mark/coach-mark.component'; import { TooltipWithDisableButton } from '../../../../shared/components/custom-tooltip/tooltip-with-disable-button'; +import { AnnotateInteractivelyNotification } from '../../../../shared/components/fux-notification/notifications/annotate-interactively-notification.component'; import { TabItem } from '../../../../shared/components/tabs/tabs.interface'; import { TruncatedText } from '../../../../shared/components/truncated-text/truncated-text.component'; import { useActiveTab } from '../../../../shared/hooks/use-active-tab.hook'; @@ -62,6 +61,9 @@ export const DatasetTabPanel = ({ dataset }: { dataset: Dataset }) => { }); }; + const triggerRef = useRef(null); + const fuxState = useOverlayTriggerState({}); + useOpenNotificationToast(); return ( @@ -99,21 +101,9 @@ export const DatasetTabPanel = ({ dataset }: { dataset: Dataset }) => { {isAnomalyProject && } - {annotateButtonText === 'Annotate interactively' && !isAnnotatorDisabled && ( - - )} - + {annotateButtonText === 'Annotate interactively' && !isAnnotatorDisabled && ( + + )} diff --git a/web_ui/src/shared/components/coach-mark/fux-notifications/successfully-auto-trained-notification.component.tsx b/web_ui/src/shared/components/coach-mark/fux-notifications/successfully-auto-trained-notification.component.tsx index d374217b92..87b6711b21 100644 --- a/web_ui/src/shared/components/coach-mark/fux-notifications/successfully-auto-trained-notification.component.tsx +++ b/web_ui/src/shared/components/coach-mark/fux-notifications/successfully-auto-trained-notification.component.tsx @@ -18,8 +18,8 @@ import { useUserGlobalSettings } from '../../../../core/user-settings/hooks/use- import { useFuxNotifications } from '../../../../hooks/use-fux-notifications/use-fux-notifications.hook'; import { useProjectIdentifier } from '../../../../hooks/use-project-identifier/use-project-identifier'; import { useProject } from '../../../../pages/project-details/providers/project-provider/project-provider.component'; +import { onFirstSuccessfulAutoTrainingJob } from '../../fux-notification/notifications/utils'; import { CoachMark } from '../coach-mark.component'; -import { onFirstSuccessfulAutoTrainingJob } from '../utils'; const useSuccessfullyAutotrainedNotificationJobs = ({ enabled, diff --git a/web_ui/src/shared/components/coach-mark/utils.ts b/web_ui/src/shared/components/coach-mark/utils.ts index a85879fbd2..bae70f9b74 100644 --- a/web_ui/src/shared/components/coach-mark/utils.ts +++ b/web_ui/src/shared/components/coach-mark/utils.ts @@ -1,13 +1,7 @@ // Copyright (C) 2022-2025 Intel Corporation // LIMITED EDGE SOFTWARE DISTRIBUTION LICENSE -import { InfiniteData } from '@tanstack/react-query'; - -import { GETI_SYSTEM_AUTHOR_ID, JobState } from '../../../core/jobs/jobs.const'; -import { JobTask } from '../../../core/jobs/jobs.interface'; -import { JobsResponse } from '../../../core/jobs/services/jobs-service.interface'; -import { FUX_NOTIFICATION_KEYS, FUX_SETTINGS_KEYS } from '../../../core/user-settings/dtos/user-settings.interface'; -import { UserGlobalSettings, UseSettings } from '../../../core/user-settings/services/user-settings.interface'; +import { FUX_NOTIFICATION_KEYS } from '../../../core/user-settings/dtos/user-settings.interface'; import { DocsUrl } from '../../../shared/components/tutorials/utils'; export enum TipPosition { @@ -155,33 +149,3 @@ export const getStepInfo = (fuxNotificationId: FUX_NOTIFICATION_KEYS) => { }; } }; - -export const onFirstSuccessfulAutoTrainingJob = - (settings: UseSettings, callback: (modelId: string) => void) => - ({ pages }: InfiniteData) => { - if (!pages[0]) { - return; - } - - const { jobsCount, jobs } = pages[0]; - const totalFinishedJobs = Number(jobsCount.numberOfFinishedJobs); - const hasFinishedJobs = totalFinishedJobs > 0; - const neverSuccessfullyAutoTrained = settings.config[FUX_SETTINGS_KEYS.NEVER_SUCCESSFULLY_AUTOTRAINED].value; - const firstScheduledAutoTrainingJobId = settings.config[FUX_SETTINGS_KEYS.FIRST_AUTOTRAINING_JOB_ID].value; - const desiredJob = jobs.find((job): job is JobTask => { - return ( - job.state === JobState.FINISHED && - job.authorId === GETI_SYSTEM_AUTHOR_ID && - job.id === firstScheduledAutoTrainingJobId - ); - }); - const trainedModelId = desiredJob?.metadata?.trainedModel?.modelId; - - if (trainedModelId === undefined) { - return; - } - - if (trainedModelId && hasFinishedJobs && neverSuccessfullyAutoTrained && desiredJob) { - callback(trainedModelId); - } - }; diff --git a/web_ui/src/shared/components/fux-notification/fux-notification.component.tsx b/web_ui/src/shared/components/fux-notification/fux-notification.component.tsx index ad21105ca9..9a0ce60b28 100644 --- a/web_ui/src/shared/components/fux-notification/fux-notification.component.tsx +++ b/web_ui/src/shared/components/fux-notification/fux-notification.component.tsx @@ -3,15 +3,29 @@ import { ComponentProps, MutableRefObject, ReactNode } from 'react'; -import { ActionButton, Button, CustomPopover, Divider, Flex, Popover, Text, View } from '@geti/ui'; -import { Close } from '@geti/ui/icons'; -import { isFunction } from 'lodash-es'; +import { + ActionButton, + Button, + ButtonGroup, + CustomPopover, + Divider, + Flex, + Item, + Menu, + MenuTrigger, + Popover, + Text, + View, +} from '@geti/ui'; +import { ChevronLeft, Close, MoreMenu } from '@geti/ui/icons'; +import { isEmpty, isFunction } from 'lodash-es'; import { FUX_NOTIFICATION_KEYS } from '../../../core/user-settings/dtos/user-settings.interface'; +import { useUserGlobalSettings } from '../../../core/user-settings/hooks/use-global-settings.hook'; import { useDocsUrl } from '../../../hooks/use-docs-url/use-docs-url.hook'; -import { openNewTab } from '../../utils'; +import { useTutorialEnablement } from '../../hooks/use-tutorial-enablement.hook'; import { onPressLearnMore } from '../tutorials/utils'; -import { getFuxNotificationData } from './utils'; +import { getFuxNotificationData, getStepInfo } from './utils'; import classes from './fux-notification.module.scss'; @@ -32,10 +46,18 @@ export const FuxNotification = ({ onClose, children, }: CustomPopoverProps) => { - const { description, showDismissAll, docUrl } = getFuxNotificationData(settingsKey); + const settings = useUserGlobalSettings(); + const { header, description, docUrl, nextStepId, previousStepId, showDismissAll } = + getFuxNotificationData(settingsKey); + const { dismissAll, changeTutorial } = useTutorialEnablement(settingsKey); const message = children ? children : description; const url = useDocsUrl(); const newDocUrl = customDocUrl ?? (docUrl && `${url}${docUrl}`) ?? undefined; + + if (isEmpty(message)) { + return <>; + } + if (!showDismissAll) { return ( ); } - // todo: not implemented anywhere yet, to do in next PR + + const onPressNext = () => { + nextStepId && changeTutorial(settingsKey, nextStepId); + }; + + const onPressPrevious = () => { + previousStepId && changeTutorial(settingsKey, previousStepId); + }; + + const stepInfo = getStepInfo(settingsKey); + return ( - - {children} - - {newDocUrl && ( - - )} - - - - { - state.close(); - isFunction(onClose) && onClose(); - }} - aria-label={'close first user experience notification'} - UNSAFE_className={classes.close} - > - - - + + + {header && {header}} + {stepInfo.stepNumber && stepInfo.totalCount && ( + + {stepInfo.stepNumber} of {stepInfo.totalCount} + + )} + + + {message} + + + + {previousStepId && ( + + )} + {docUrl && ( + + )} + {nextStepId ? ( + + ) : ( + + )} + + + + + + + + + Dismiss all + + + + + ); }; diff --git a/web_ui/src/shared/components/fux-notification/notifications/annotate-interactively-notification.component.tsx b/web_ui/src/shared/components/fux-notification/notifications/annotate-interactively-notification.component.tsx new file mode 100644 index 0000000000..cbbeeb2366 --- /dev/null +++ b/web_ui/src/shared/components/fux-notification/notifications/annotate-interactively-notification.component.tsx @@ -0,0 +1,48 @@ +// Copyright (C) 2022-2025 Intel Corporation +// LIMITED EDGE SOFTWARE DISTRIBUTION LICENSE + +import { MutableRefObject, useEffect } from 'react'; + +import { OverlayTriggerState } from 'react-stately'; + +import { FUX_NOTIFICATION_KEYS } from '../../../../core/user-settings/dtos/user-settings.interface'; +import { useUserGlobalSettings } from '../../../../core/user-settings/hooks/use-global-settings.hook'; +import { usePrevious } from '../../../../hooks/use-previous/use-previous.hook'; +import { FuxNotification } from '../fux-notification.component'; + +interface AnnotateInteractivelyNotificationProps { + triggerRef: MutableRefObject; + state: OverlayTriggerState; +} + +export const AnnotateInteractivelyNotification = ({ triggerRef, state }: AnnotateInteractivelyNotificationProps) => { + const settings = useUserGlobalSettings(); + const isFuxNotificationEnabled = settings.config[FUX_NOTIFICATION_KEYS.ANNOTATE_INTERACTIVELY]?.isEnabled; + const prevFuxEnabled = usePrevious(isFuxNotificationEnabled); + + useEffect(() => { + if (isFuxNotificationEnabled && prevFuxEnabled !== isFuxNotificationEnabled) { + state.open(); + } else if (!isFuxNotificationEnabled && prevFuxEnabled !== isFuxNotificationEnabled) { + state.close(); + } + }, [state, isFuxNotificationEnabled, prevFuxEnabled]); + + const handleCloseNotification = () => { + isFuxNotificationEnabled && + settings.saveConfig({ + ...settings.config, + [FUX_NOTIFICATION_KEYS.ANNOTATE_INTERACTIVELY]: { isEnabled: false }, + }); + }; + + return ( + + ); +}; diff --git a/web_ui/src/shared/components/coach-mark/utils.test.ts b/web_ui/src/shared/components/fux-notification/notifications/utils.test.ts similarity index 91% rename from web_ui/src/shared/components/coach-mark/utils.test.ts rename to web_ui/src/shared/components/fux-notification/notifications/utils.test.ts index 471b9b0407..099a736e60 100644 --- a/web_ui/src/shared/components/coach-mark/utils.test.ts +++ b/web_ui/src/shared/components/fux-notification/notifications/utils.test.ts @@ -1,14 +1,14 @@ // Copyright (C) 2022-2025 Intel Corporation // LIMITED EDGE SOFTWARE DISTRIBUTION LICENSE -import { GETI_SYSTEM_AUTHOR_ID, JobState } from '../../../core/jobs/jobs.const'; -import { Job, JobCount } from '../../../core/jobs/jobs.interface'; -import { FUX_SETTINGS_KEYS } from '../../../core/user-settings/dtos/user-settings.interface'; -import { getMockedJob } from '../../../test-utils/mocked-items-factory/mocked-jobs'; +import { GETI_SYSTEM_AUTHOR_ID, JobState } from '../../../../core/jobs/jobs.const'; +import { Job, JobCount } from '../../../../core/jobs/jobs.interface'; +import { FUX_SETTINGS_KEYS } from '../../../../core/user-settings/dtos/user-settings.interface'; +import { getMockedJob } from '../../../../test-utils/mocked-items-factory/mocked-jobs'; import { getMockedUserGlobalSettings, getMockedUserGlobalSettingsObject, -} from '../../../test-utils/mocked-items-factory/mocked-settings'; +} from '../../../../test-utils/mocked-items-factory/mocked-settings'; import { onFirstSuccessfulAutoTrainingJob } from './utils'; const getJobResponse = (jobCount: Partial = {}, mockedJobs: Job[] = []) => ({ diff --git a/web_ui/src/shared/components/fux-notification/notifications/utils.ts b/web_ui/src/shared/components/fux-notification/notifications/utils.ts new file mode 100644 index 0000000000..0cb54f1f4c --- /dev/null +++ b/web_ui/src/shared/components/fux-notification/notifications/utils.ts @@ -0,0 +1,40 @@ +// Copyright (C) 2022-2025 Intel Corporation +// LIMITED EDGE SOFTWARE DISTRIBUTION LICENSE + +import { InfiniteData } from '@tanstack/react-query'; + +import { GETI_SYSTEM_AUTHOR_ID, JobState } from '../../../../core/jobs/jobs.const'; +import { JobTask } from '../../../../core/jobs/jobs.interface'; +import { JobsResponse } from '../../../../core/jobs/services/jobs-service.interface'; +import { FUX_SETTINGS_KEYS } from '../../../../core/user-settings/dtos/user-settings.interface'; +import { UserGlobalSettings, UseSettings } from '../../../../core/user-settings/services/user-settings.interface'; + +export const onFirstSuccessfulAutoTrainingJob = + (settings: UseSettings, callback: (modelId: string) => void) => + ({ pages }: InfiniteData) => { + if (!pages[0]) { + return; + } + + const { jobsCount, jobs } = pages[0]; + const totalFinishedJobs = Number(jobsCount.numberOfFinishedJobs); + const hasFinishedJobs = totalFinishedJobs > 0; + const neverSuccessfullyAutoTrained = settings.config[FUX_SETTINGS_KEYS.NEVER_SUCCESSFULLY_AUTOTRAINED].value; + const firstScheduledAutoTrainingJobId = settings.config[FUX_SETTINGS_KEYS.FIRST_AUTOTRAINING_JOB_ID].value; + const desiredJob = jobs.find((job): job is JobTask => { + return ( + job.state === JobState.FINISHED && + job.authorId === GETI_SYSTEM_AUTHOR_ID && + job.id === firstScheduledAutoTrainingJobId + ); + }); + const trainedModelId = desiredJob?.metadata?.trainedModel?.modelId; + + if (trainedModelId === undefined) { + return; + } + + if (trainedModelId && hasFinishedJobs && neverSuccessfullyAutoTrained && desiredJob) { + callback(trainedModelId); + } + }; diff --git a/web_ui/src/shared/components/fux-notification/utils.ts b/web_ui/src/shared/components/fux-notification/utils.ts index d83b87fc5a..d21b88f6e7 100644 --- a/web_ui/src/shared/components/fux-notification/utils.ts +++ b/web_ui/src/shared/components/fux-notification/utils.ts @@ -13,7 +13,7 @@ interface FuxNotificationData { showDismissAll: boolean; } -export const getFuxNotificationData = (fuxNotificationId: string): FuxNotificationData => { +export const getFuxNotificationData = (fuxNotificationId: FUX_NOTIFICATION_KEYS): FuxNotificationData => { switch (fuxNotificationId) { case FUX_NOTIFICATION_KEYS.ANNOTATE_INTERACTIVELY: return { @@ -113,3 +113,33 @@ export const getFuxNotificationData = (fuxNotificationId: string): FuxNotificati }; } }; + +export const getStepInfo = (fuxNotificationId: FUX_NOTIFICATION_KEYS) => { + switch (fuxNotificationId) { + case FUX_NOTIFICATION_KEYS.ANNOTATOR_TOOLS: + return { + stepNumber: 1, + totalCount: 2, + }; + case FUX_NOTIFICATION_KEYS.ANNOTATOR_ACTIVE_SET: + return { + stepNumber: 2, + totalCount: 2, + }; + case FUX_NOTIFICATION_KEYS.ANNOTATOR_SUCCESSFULLY_TRAINED: + return { + stepNumber: 1, + totalCount: 2, + }; + case FUX_NOTIFICATION_KEYS.ANNOTATOR_CHECK_PREDICTIONS: + return { + stepNumber: 2, + totalCount: 2, + }; + default: + return { + stepNumber: undefined, + totalCount: undefined, + }; + } +};