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 ? (
+
+ ) : (
+
+ )}
+
+
+
+
+
+
+
+
+
+
);
};
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,
+ };
+ }
+};