diff --git a/changelog.d/20240130_135306_boris_open_guide_automatically.md b/changelog.d/20240130_135306_boris_open_guide_automatically.md new file mode 100644 index 000000000000..3b34ca7cf6be --- /dev/null +++ b/changelog.d/20240130_135306_boris_open_guide_automatically.md @@ -0,0 +1,6 @@ +### Changed + +- Annotation guide is opened automatically if not seen yet when the job is "new annotation" + () +- Annotation guide will be opened automatically if this is specified in a link `/tasks//jobs/?openGuide` + () diff --git a/cvat-ui/src/actions/annotation-actions.ts b/cvat-ui/src/actions/annotation-actions.ts index 791f90c3d9b5..ac3be64331b7 100644 --- a/cvat-ui/src/actions/annotation-actions.ts +++ b/cvat-ui/src/actions/annotation-actions.ts @@ -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 @@ -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 }, @@ -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, @@ -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, getState): Promise => { try { const state = getState(); @@ -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 @@ -956,6 +960,7 @@ export function getJobAsync( payload: { openTime, job, + initialOpenGuide, groundTruthInstance: gtJob || null, groundTruthJobFramesMeta, issues, diff --git a/cvat-ui/src/components/annotation-page/annotation-page.tsx b/cvat-ui/src/components/annotation-page/annotation-page.tsx index 90136383219f..670fd3ce996b 100644 --- a/cvat-ui/src/components/annotation-page/annotation-page.tsx +++ b/cvat-ui/src/components/annotation-page/annotation-page.tsx @@ -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 @@ -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'; @@ -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; @@ -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: ( - - Press - - if you would like to continue - - ), - 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: ( + + Press + + if you would like to continue + + ), + placement: 'topRight', + className: 'cvat-notification-continue-job', + }); } if (!job.labels.length) { diff --git a/cvat-ui/src/components/annotation-page/top-bar/filters-modal.tsx b/cvat-ui/src/components/annotation-page/top-bar/filters-modal.tsx index 3a3373ffa229..b1f7ec1da6bb 100644 --- a/cvat-ui/src/components/annotation-page/top-bar/filters-modal.tsx +++ b/cvat-ui/src/components/annotation-page/top-bar/filters-modal.tsx @@ -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 @@ -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)); diff --git a/cvat-ui/src/components/annotation-page/top-bar/right-group.tsx b/cvat-ui/src/components/annotation-page/top-bar/right-group.tsx index 16a21bfbd932..7bc685b6cc33 100644 --- a/cvat-ui/src/components/annotation-page/top-bar/right-group.tsx +++ b/cvat-ui/src/components/annotation-page/top-bar/right-group.tsx @@ -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'; @@ -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: ( + + ), + }); + } + }).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 (