Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open annotations guide automatically #7410

Merged
merged 25 commits into from
Feb 1, 2024
Merged
Show file tree
Hide file tree
Changes from 21 commits
Commits
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
@@ -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
107 changes: 72 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,94 @@ 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 {
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 = [];
}
klakhov marked this conversation as resolved.
Show resolved Hide resolved

if (!seenGuides.includes(jobInstance.guideId)) {
// open guide if the user have not seen it yet
openGuide();
const limit = 10;
bsekachev marked this conversation as resolved.
Show resolved Hide resolved
const updatedSeenGuides = Array
.from(new Set([jobInstance.guideId, ...seenGuides.slice(0, limit - 1)]));
localStorage.setItem('seenGuides', JSON.stringify(updatedSeenGuides));
}
}
}
}, []);

return (
<Col className='cvat-annotation-header-right-group'>
<Button
Expand All @@ -62,32 +124,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
Loading