Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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';
Expand Down Expand Up @@ -62,6 +61,9 @@ export const DatasetTabPanel = ({ dataset }: { dataset: Dataset }) => {
});
};

const triggerRef = useRef(null);
const fuxState = useOverlayTriggerState({});

useOpenNotificationToast();

return (
Expand Down Expand Up @@ -99,21 +101,9 @@ export const DatasetTabPanel = ({ dataset }: { dataset: Dataset }) => {
<Flex gap='size-100'>
{isAnomalyProject && <ExportImportDatasetButtons hasMediaItems={hasMediaItems} />}
<View>
{annotateButtonText === 'Annotate interactively' && !isAnnotatorDisabled && (
<CoachMark
settingsKey={FUX_NOTIFICATION_KEYS.ANNOTATE_INTERACTIVELY}
styles={{
position: 'absolute',
zIndex: '5',
top: '-58px',
right: '55px',
maxWidth: '100%',
}}
/>
)}

<TooltipWithDisableButton activeTooltip={tooltipText} disabledTooltip={tooltipText}>
<Button
ref={triggerRef}
id={'annotate-button-id'}
variant={'accent'}
onPress={handleGoToAnnotator}
Expand All @@ -123,6 +113,9 @@ export const DatasetTabPanel = ({ dataset }: { dataset: Dataset }) => {
<TruncatedText>{annotateButtonText}</TruncatedText>
</Button>
</TooltipWithDisableButton>
{annotateButtonText === 'Annotate interactively' && !isAnnotatorDisabled && (
<AnnotateInteractivelyNotification triggerRef={triggerRef} state={fuxState} />
)}
</View>
</Flex>
</Flex>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
38 changes: 1 addition & 37 deletions web_ui/src/shared/components/coach-mark/utils.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -155,33 +149,3 @@ export const getStepInfo = (fuxNotificationId: FUX_NOTIFICATION_KEYS) => {
};
}
};

export const onFirstSuccessfulAutoTrainingJob =
(settings: UseSettings<UserGlobalSettings>, callback: (modelId: string) => void) =>
({ pages }: InfiniteData<JobsResponse>) => {
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);
}
};
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -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 (
<CustomPopover
Expand Down Expand Up @@ -78,7 +100,17 @@ export const FuxNotification = ({
</CustomPopover>
);
}
// 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 (
<CustomPopover
ref={triggerRef}
Expand All @@ -88,35 +120,77 @@ export const FuxNotification = ({
UNSAFE_className={classes.container}
isKeyboardDismissDisabled
>
<Flex direction={'row'} gap={'size-200'} alignItems={'center'}>
<Text order={1}>{children}</Text>

{newDocUrl && (
<Button
order={2}
variant='primary'
UNSAFE_style={{ border: 'none' }}
onPress={() => openNewTab(newDocUrl)}
>
Learn more
</Button>
)}

<Divider order={3} orientation='vertical' size='S' UNSAFE_className={classes.divider} />

<ActionButton
isQuiet
order={4}
onPress={() => {
state.close();
isFunction(onClose) && onClose();
}}
aria-label={'close first user experience notification'}
UNSAFE_className={classes.close}
>
<Close />
</ActionButton>
</Flex>
<View UNSAFE_className={classes.dialogWrapper}>
<Flex UNSAFE_className={classes.coachMarkHeader}>
{header && <View UNSAFE_className={classes.header}>{header}</View>}
{stepInfo.stepNumber && stepInfo.totalCount && (
<Text UNSAFE_className={classes.steps}>
{stepInfo.stepNumber} of {stepInfo.totalCount}
</Text>
)}
</Flex>

<Text UNSAFE_className={classes.dialogDescription}>{message}</Text>

<ButtonGroup UNSAFE_className={classes.dialogButtonGroup}>
<Flex gap={'size-100'}>
{previousStepId && (
<Button
variant='primary'
aria-label='Back button'
id={`${settingsKey}-previous-button-id`}
onPress={onPressPrevious}
UNSAFE_className={classes.backButton}
>
<ChevronLeft />
</Button>
)}
{docUrl && (
<Button
variant='primary'
id={`${settingsKey}-learn-more-button-id`}
onPress={() => {
onPressLearnMore(newDocUrl);
}}
>
Learn more
</Button>
)}
{nextStepId ? (
<Button variant='primary' id='next-button-id' onPress={onPressNext}>
Next
</Button>
) : (
<Button
variant='primary'
isPending={settings.isSavingConfig}
id='dismiss-button-id'
onPress={close}
aria-label='Dismiss help dialog'
>
Dismiss
</Button>
)}
</Flex>

<MenuTrigger>
<ActionButton
isQuiet
id={`${settingsKey}-more-btn-id`}
aria-label='Open to dismiss all help dialogs'
data-testid={`${settingsKey}-more-btn-id`}
UNSAFE_className={classes.moreMenu}
>
<MoreMenu />
</ActionButton>
<Menu id={`${settingsKey}-tutorial-card-menu-id`} onAction={dismissAll}>
<Item key={settingsKey} test-id={`${settingsKey}-dismiss-all-id`} textValue='Dismiss all'>
Dismiss all
</Item>
</Menu>
</MenuTrigger>
</ButtonGroup>
</View>
</CustomPopover>
);
};
Original file line number Diff line number Diff line change
@@ -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<null>;
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) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (isFuxNotificationEnabled && prevFuxEnabled !== isFuxNotificationEnabled) {
if (prevFuxEnabled !== isFuxNotificationEnabled) {
isFuxNotificationEnabled? state.open(): state.close()
}

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 (
<FuxNotification
settingsKey={FUX_NOTIFICATION_KEYS.ANNOTATE_INTERACTIVELY}
placement='top right'
triggerRef={triggerRef}
state={state}
onClose={handleCloseNotification}
/>
);
};
Original file line number Diff line number Diff line change
@@ -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<JobCount> = {}, mockedJobs: Job[] = []) => ({
Expand Down
Loading
Loading