Skip to content

Commit

Permalink
Open annotations guide automatically (#7410)
Browse files Browse the repository at this point in the history
  • Loading branch information
bsekachev authored Feb 1, 2024
1 parent bdd002a commit b7cc562
Show file tree
Hide file tree
Showing 13 changed files with 254 additions and 111 deletions.
6 changes: 6 additions & 0 deletions changelog.d/20240130_135306_boris_open_guide_automatically.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
### Changed

- Annotation guide is opened automatically if not seen yet when the job is "new annotation"
(<https://github.com/opencv/cvat/pull/7410>)
- Annotation guide will be opened automatically if this is specified in a link `/tasks/<id>/jobs/<id>?openGuide`
(<https://github.com/opencv/cvat/pull/7410>)
31 changes: 18 additions & 13 deletions cvat-ui/src/actions/annotation-actions.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// Copyright (C) 2020-2022 Intel Corporation
// Copyright (C) 2022-2023 CVAT.ai Corporation
// Copyright (C) 2022-2024 CVAT.ai Corporation
//
// SPDX-License-Identifier: MIT

Expand Down Expand Up @@ -321,7 +321,7 @@ export function fetchAnnotationsAsync(): ThunkAction {
};
}

export function changeAnnotationsFilters(filters: any[]): AnyAction {
export function changeAnnotationsFilters(filters: object[]): AnyAction {
return {
type: AnnotationActionTypes.CHANGE_ANNOTATIONS_FILTERS,
payload: { filters },
Expand Down Expand Up @@ -677,8 +677,6 @@ export function changeFrameAsync(
payload: {},
});

// commit the latest job frame to local storage
localStorage.setItem(`Job_${job.id}_frame`, `${toFrame}`);
const changeFrameLog = await job.logger.log(LogType.changeFrame, {
from: frame,
to: toFrame,
Expand Down Expand Up @@ -867,9 +865,15 @@ export function closeJob(): ThunkAction {
};
}

export function getJobAsync(
tid: number, jid: number, initialFrame: number | null, initialFilters: object[],
): ThunkAction {
export function getJobAsync({
taskID, jobID, initialFrame, initialFilters, initialOpenGuide,
}: {
taskID: number;
jobID: number;
initialFrame: number | null;
initialFilters: object[];
initialOpenGuide: boolean;
}): ThunkAction {
return async (dispatch: ActionCreator<Dispatch>, getState): Promise<void> => {
try {
const state = getState();
Expand All @@ -885,29 +889,29 @@ export function getJobAsync(
dispatch({
type: AnnotationActionTypes.GET_JOB,
payload: {
requestedId: jid,
requestedId: jobID,
},
});

if (!Number.isInteger(tid) || !Number.isInteger(jid)) {
if (!Number.isInteger(taskID) || !Number.isInteger(jobID)) {
throw new Error('Requested resource id is not valid');
}

const loadJobEvent = await logger.log(
LogType.loadJob,
{
task_id: tid,
job_id: jid,
task_id: taskID,
job_id: jobID,
},
true,
);

getCore().config.globalObjectsCounter = 0;
const [job] = await cvat.jobs.get({ jobID: jid });
const [job] = await cvat.jobs.get({ jobID });
let gtJob: Job | null = null;
if (job.type === JobType.ANNOTATION) {
try {
[gtJob] = await cvat.jobs.get({ taskID: tid, type: JobType.GROUND_TRUTH });
[gtJob] = await cvat.jobs.get({ taskID, type: JobType.GROUND_TRUTH });
} catch (e) {
// gtJob is not available for workers
// do nothing
Expand Down Expand Up @@ -956,6 +960,7 @@ export function getJobAsync(
payload: {
openTime,
job,
initialOpenGuide,
groundTruthInstance: gtJob || null,
groundTruthJobFramesMeta,
issues,
Expand Down
65 changes: 34 additions & 31 deletions cvat-ui/src/components/annotation-page/annotation-page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// Copyright (C) 2021-2022 Intel Corporation
// Copyright (C) 2023 CVAT.ai Corporation
// Copyright (C) 2023-2024 CVAT.ai Corporation
//
// SPDX-License-Identifier: MIT

Expand All @@ -9,7 +9,9 @@ import Layout from 'antd/lib/layout';
import Result from 'antd/lib/result';
import Spin from 'antd/lib/spin';
import notification from 'antd/lib/notification';
import Button from 'antd/lib/button';

import './styles.scss';
import AttributeAnnotationWorkspace from 'components/annotation-page/attribute-annotation-workspace/attribute-annotation-workspace';
import ReviewAnnotationsWorkspace from 'components/annotation-page/review-workspace/review-workspace';
import StandardWorkspaceComponent from 'components/annotation-page/standard-workspace/standard-workspace';
Expand All @@ -20,8 +22,7 @@ import StatisticsModalComponent from 'components/annotation-page/top-bar/statist
import AnnotationTopBarContainer from 'containers/annotation-page/top-bar/top-bar';
import { Workspace } from 'reducers';
import { usePrevious } from 'utils/hooks';
import './styles.scss';
import Button from 'antd/lib/button';
import { readLatestFrame } from 'utils/remember-latest-frame';

interface Props {
job: any | null | undefined;
Expand Down Expand Up @@ -69,34 +70,36 @@ export default function AnnotationPageComponent(props: Props): JSX.Element {

useEffect(() => {
if (prevFetching && !fetching && !prevJob && job) {
const latestFrame = localStorage.getItem(`Job_${job.id}_frame`);
if (latestFrame && Number.isInteger(+latestFrame)) {
const parsedFrame = +latestFrame;
if (parsedFrame !== frameNumber && parsedFrame >= job.startFrame && parsedFrame <= job.stopFrame) {
const notificationKey = `cvat-notification-continue-job-${job.id}`;
notification.info({
key: notificationKey,
message: `You finished working on frame ${parsedFrame}`,
description: (
<span>
Press
<Button
className='cvat-notification-continue-job-button'
type='link'
onClick={() => {
changeFrame(parsedFrame);
notification.close(notificationKey);
}}
>
here
</Button>
if you would like to continue
</span>
),
placement: 'topRight',
className: 'cvat-notification-continue-job',
});
}
const latestFrame = readLatestFrame(job.id);

if (typeof latestFrame === 'number' &&
latestFrame !== frameNumber &&
latestFrame >= job.startFrame &&
latestFrame <= job.stopFrame
) {
const notificationKey = `cvat-notification-continue-job-${job.id}`;
notification.info({
key: notificationKey,
message: `You finished working on frame ${latestFrame}`,
description: (
<span>
Press
<Button
className='cvat-notification-continue-job-button'
type='link'
onClick={() => {
changeFrame(latestFrame);
notification.close(notificationKey);
}}
>
here
</Button>
if you would like to continue
</span>
),
placement: 'topRight',
className: 'cvat-notification-continue-job',
});
}

if (!job.labels.length) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// Copyright (C) 2021-2022 Intel Corporation
// Copyright (C) 2023 CVAT.ai Corporation
// Copyright (C) 2023-2024 CVAT.ai Corporation
//
// SPDX-License-Identifier: MIT

Expand Down Expand Up @@ -203,7 +203,7 @@ function FiltersModalComponent(): JSX.Element {
}
}, [visible]);

const applyFilters = (filtersData: any[]): void => {
const applyFilters = (filtersData: object[]): void => {
dispatch(changeAnnotationsFilters(filtersData));
dispatch(fetchAnnotationsAsync());
dispatch(showFilters(false));
Expand Down
110 changes: 75 additions & 35 deletions cvat-ui/src/components/annotation-page/top-bar/right-group.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
// Copyright (C) 2020-2022 Intel Corporation
// Copyright (C) 2023 CVAT.ai Corporation
// Copyright (C) 2023-2024 CVAT.ai Corporation
//
// SPDX-License-Identifier: MIT

import React from 'react';
import { useSelector } from 'react-redux';
import React, { useEffect, useCallback } from 'react';
import { Col } from 'antd/lib/grid';
import Icon from '@ant-design/icons';
import Select from 'antd/lib/select';
Expand All @@ -15,31 +14,97 @@ import notification from 'antd/lib/notification';
import {
FilterIcon, FullscreenIcon, GuideIcon, InfoIcon,
} from 'icons';
import { DimensionType } from 'cvat-core-wrapper';
import { CombinedState, Workspace } from 'reducers';
import config from 'config';
import {
DimensionType, Job, JobStage, JobState,
} from 'cvat-core-wrapper';
import { Workspace } from 'reducers';

import MDEditor from '@uiw/react-md-editor';

interface Props {
workspace: Workspace;
showStatistics(): void;
showFilters(): void;
changeWorkspace(workspace: Workspace): void;
jobInstance: any;
jobInstance: Job;
workspace: Workspace;
annotationFilters: object[];
initialOpenGuide: boolean;
}

function RightGroup(props: Props): JSX.Element {
const {
showStatistics,
changeWorkspace,
showFilters,
workspace,
jobInstance,
showFilters,
annotationFilters,
initialOpenGuide,
} = props;

const annotationFilters = useSelector((state: CombinedState) => state.annotation.annotations.filters);
const filters = annotationFilters.length;

const openGuide = useCallback(() => {
const PADDING = Math.min(window.screen.availHeight, window.screen.availWidth) * 0.4;
jobInstance.guide().then((guide) => {
if (guide?.markdown) {
Modal.info({
icon: null,
width: window.screen.availWidth - PADDING,
className: 'cvat-annotation-view-markdown-guide-modal',
content: (
<MDEditor
visibleDragbar={false}
data-color-mode='light'
height={window.screen.availHeight - PADDING}
preview='preview'
hideToolbar
value={guide.markdown}
/>
),
});
}
}).catch((error: unknown) => {
notification.error({
message: 'Could not receive annotation guide',
description: error instanceof Error ? error.message : console.error('error'),
});
});
}, [jobInstance]);

useEffect(() => {
if (Number.isInteger(jobInstance?.guideId)) {
if (initialOpenGuide) {
openGuide();
} else if (
jobInstance?.stage === JobStage.ANNOTATION &&
jobInstance?.state === JobState.NEW
) {
let seenGuides = [];
try {
seenGuides = JSON.parse(localStorage.getItem('seenGuides') || '[]');
if (!Array.isArray(seenGuides) || seenGuides.some((el) => !Number.isInteger(el))) {
throw new Error('Wrong structure stored in local storage');
}
} catch (error: unknown) {
seenGuides = [];
}

if (!seenGuides.includes(jobInstance.guideId)) {
// open guide if the user have not seen it yet
openGuide();
const updatedSeenGuides = Array
.from(new Set([
jobInstance.guideId,
...seenGuides.slice(0, config.LOCAL_STORAGE_SEEN_GUIDES_MEMORY_LIMIT - 1),
]));
localStorage.setItem('seenGuides', JSON.stringify(updatedSeenGuides));
}
}
}
}, []);

return (
<Col className='cvat-annotation-header-right-group'>
<Button
Expand All @@ -62,32 +127,7 @@ function RightGroup(props: Props): JSX.Element {
<Button
type='link'
className='cvat-annotation-header-guide-button cvat-annotation-header-button'
onClick={async (): Promise<void> => {
const PADDING = Math.min(window.screen.availHeight, window.screen.availWidth) * 0.4;
try {
const guide = await jobInstance.guide();
Modal.info({
icon: null,
width: window.screen.availWidth - PADDING,
className: 'cvat-annotation-view-markdown-guide-modal',
content: (
<MDEditor
visibleDragbar={false}
data-color-mode='light'
height={window.screen.availHeight - PADDING}
preview='preview'
hideToolbar
value={guide.markdown}
/>
),
});
} catch (error: any) {
notification.error({
message: 'Could not receive annotation guide',
description: error.toString(),
});
}
}}
onClick={openGuide}
>
<Icon component={GuideIcon} />
Guide
Expand Down
9 changes: 7 additions & 2 deletions cvat-ui/src/components/annotation-page/top-bar/top-bar.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// Copyright (C) 2020-2022 Intel Corporation
// Copyright (C) 2023 CVAT.ai Corporation
// Copyright (C) 2023-2024 CVAT.ai Corporation
//
// SPDX-License-Identifier: MIT

Expand Down Expand Up @@ -44,6 +44,8 @@ interface Props {
activeControl: ActiveControl;
toolsBlockerState: ToolsBlockerState;
deleteFrameAvailable: boolean;
annotationFilters: object[];
initialOpenGuide: boolean;
changeWorkspace(workspace: Workspace): void;
showStatistics(): void;
showFilters(): void;
Expand Down Expand Up @@ -100,6 +102,8 @@ export default function AnnotationTopBarComponent(props: Props): JSX.Element {
focusFrameInputShortcut,
activeControl,
toolsBlockerState,
annotationFilters,
initialOpenGuide,
showStatistics,
showFilters,
changeWorkspace,
Expand Down Expand Up @@ -143,7 +147,6 @@ export default function AnnotationTopBarComponent(props: Props): JSX.Element {
key='player_buttons'
playing={playing}
playPauseShortcut={playPauseShortcut}
deleteFrameShortcut={deleteFrameShortcut}
nextFrameShortcut={nextFrameShortcut}
previousFrameShortcut={previousFrameShortcut}
forwardShortcut={forwardShortcut}
Expand Down Expand Up @@ -212,6 +215,8 @@ export default function AnnotationTopBarComponent(props: Props): JSX.Element {
<RightGroup
workspace={workspace}
jobInstance={jobInstance}
annotationFilters={annotationFilters}
initialOpenGuide={initialOpenGuide}
changeWorkspace={changeWorkspace}
showStatistics={showStatistics}
showFilters={showFilters}
Expand Down
Loading

0 comments on commit b7cc562

Please sign in to comment.