From e2a92b4c2c8f57f0ce7a58bf8d1021cec8ce3d3f Mon Sep 17 00:00:00 2001 From: Elodie MORIN Date: Thu, 4 Jun 2026 15:59:25 +0200 Subject: [PATCH] fix(frontend.Annotator): display spectrograms --- .../src/components/ui/Modal/modal.hook.tsx | 3 -- .../AnnotationCampaign/CampaignCard.tsx | 6 ++-- .../src/features/AnnotationTask/api/index.ts | 1 - .../Annotator/Analysis/AnalysisSelect.tsx | 1 + .../src/features/Annotator/Canvas/hooks.ts | 6 ++-- frontend/src/features/Annotator/Skeleton.tsx | 34 ++++++++----------- .../features/Annotator/Spectrogram/hooks.ts | 23 +++++++++---- .../spectrogram/$spectrogramID.tsx | 29 +++++++++++++--- 8 files changed, 62 insertions(+), 41 deletions(-) diff --git a/frontend/src/components/ui/Modal/modal.hook.tsx b/frontend/src/components/ui/Modal/modal.hook.tsx index 59bf5a829..f1b0dd600 100644 --- a/frontend/src/components/ui/Modal/modal.hook.tsx +++ b/frontend/src/components/ui/Modal/modal.hook.tsx @@ -6,15 +6,12 @@ export type ModalProps = { onClose: () => void }; export const useModal = (component: React.FC, extraArgs?: object) => { const [ isOpen, setIsOpen ] = useState(false); const toggle = useCallback(() => { - console.log('toggle', component.name) setIsOpen(prev => !prev) }, [ setIsOpen, component ]) const open = useCallback(() => { - console.log('open', component.name) setIsOpen(true) }, [ setIsOpen , component]) const close = useCallback(() => { - console.log('close', component.name) setIsOpen(false) }, [ setIsOpen, component ]) diff --git a/frontend/src/features/AnnotationCampaign/CampaignCard.tsx b/frontend/src/features/AnnotationCampaign/CampaignCard.tsx index 53ec62c85..1c7ac9e81 100644 --- a/frontend/src/features/AnnotationCampaign/CampaignCard.tsx +++ b/frontend/src/features/AnnotationCampaign/CampaignCard.tsx @@ -27,13 +27,11 @@ const Card: React.FC<{ campaign: Campaign }> = React.memo(({ campaign }) => { let color: Color = 'secondary'; let badge: string = 'Open'; + const deadline = campaign.deadline ? new Date(campaign.deadline) : undefined; if (campaign.isArchived) { badge = 'Archived' color = 'medium' - } - - const deadline = campaign.deadline ? new Date(campaign.deadline) : undefined; - if (deadline && (deadline.getTime() - 7 * 24 * 60 * 60 * 1000) <= NOW) { + } else if (deadline && (deadline.getTime() - 7 * 24 * 60 * 60 * 1000) <= NOW) { badge = `Due date: ${ dateToString(deadline) }` color = 'warning' } diff --git a/frontend/src/features/AnnotationTask/api/index.ts b/frontend/src/features/AnnotationTask/api/index.ts index 941a91868..2d7be3675 100644 --- a/frontend/src/features/AnnotationTask/api/index.ts +++ b/frontend/src/features/AnnotationTask/api/index.ts @@ -11,7 +11,6 @@ import { queryKeys } from '@/api/queryKeys'; export const submitMutation = mutationOptions({ mutationFn: (variables: SubmitTaskMutationVariables) => graphqlClient.request(SubmitTaskDocument, variables), onSuccess: (_data, { campaignID, phase, spectrogramID }) => { - console.debug('invalidate', queryKeys.spectrogram.get({ campaignID, phaseType: phase, spectrogramID })) queryClient.invalidateQueries({ type: 'all', queryKey: queryKeys.spectrogram.get({ campaignID, phaseType: phase, spectrogramID }) }) queryClient.invalidateQueries({ queryKey: queryKeys.phase.get({ campaignID, phase }) }) }, diff --git a/frontend/src/features/Annotator/Analysis/AnalysisSelect.tsx b/frontend/src/features/Annotator/Analysis/AnalysisSelect.tsx index ee2ea9bd9..ce2660417 100644 --- a/frontend/src/features/Annotator/Analysis/AnalysisSelect.tsx +++ b/frontend/src/features/Annotator/Analysis/AnalysisSelect.tsx @@ -31,6 +31,7 @@ export const AnalysisSelect: React.FC = () => { } const range = `[${ frequencyToString(min) }Hz-${ frequencyToString(max) }Hz]` label += ` | scale: ${ parts.length > 0 ? parts.length : 1 } ${ range }` + label += ` | ${ a.colormap.name }` return { value: a!.id, label } }) ?? [] }, [ allAnalysis ]); diff --git a/frontend/src/features/Annotator/Canvas/hooks.ts b/frontend/src/features/Annotator/Canvas/hooks.ts index 70a8f6805..37a523fb6 100644 --- a/frontend/src/features/Annotator/Canvas/hooks.ts +++ b/frontend/src/features/Annotator/Canvas/hooks.ts @@ -24,7 +24,7 @@ export const useFocusCanvasOnTime = () => { return useCallback((time: number) => { const left = timeScale.valueToPosition(time) - containerWidth / 2; mainCanvasRef?.current?.parentElement?.scrollTo({ left }) - }, [ timeScale, containerWidth ]) + }, [ timeScale, containerWidth, mainCanvasRef ]) } export const useDrawCanvas = () => { @@ -49,7 +49,7 @@ export const useDrawCanvas = () => { await drawSpectrogram(context) applyColormap(context) drawTempAnnotation(context) - }, [ width, height, drawSpectrogram, applyFilter, applyColormap, drawTempAnnotation ]); + }, [ width, height, drawSpectrogram, applyFilter, applyColormap, drawTempAnnotation, mainCanvasRef ]); } export const useDownloadCanvas = () => { @@ -127,5 +127,5 @@ export const useDownloadCanvas = () => { link.target = '_blank'; link.download = filename; link.click(); - }, [ height, zoom, draw ]) + }, [ height, zoom, draw, mainCanvasRef, xAxisCanvasRef, yAxisCanvasRef ]) } diff --git a/frontend/src/features/Annotator/Skeleton.tsx b/frontend/src/features/Annotator/Skeleton.tsx index 4772683ee..b099a4bee 100644 --- a/frontend/src/features/Annotator/Skeleton.tsx +++ b/frontend/src/features/Annotator/Skeleton.tsx @@ -7,7 +7,7 @@ import styles from './styles.module.scss'; import { IoCheckmarkCircleOutline, IoChevronForwardOutline } from 'react-icons/io5'; import { AnnotationTaskStatus } from '@/api'; import { gqlAPI } from '@/api/baseGqlApi'; -import { useAppDispatch, useAppSelector } from '@/features/App'; +import { useAppDispatch } from '@/features/App'; import { useAnnotatorCanNavigate } from '@/features/Annotator/Navigation'; import { AnnotatorCanvasContextProvider } from '@/features/Annotator/Canvas'; import { PointerProvider } from '@/features/Annotator/Pointer/context'; @@ -15,7 +15,7 @@ import { useLoaderData, useSearch } from '@tanstack/react-router'; import { AnnotatorVisualConfigurationSlice } from '@/features/Annotator/VisualConfiguration'; import type { Colormap } from '@/features/Colormap'; import { AnnotatorConfidenceSlice } from '@/features/Annotator/Confidence'; -import { AnnotatorAnalysisSlice, selectAnalysisID } from '@/features/Annotator/Analysis'; +import { AnnotatorAnalysisSlice } from '@/features/Annotator/Analysis'; import { AnnotatorLabelSlice } from '@/features/Annotator/Label'; import { AnnotatorUXSlice } from '@/features/Annotator/UX'; import { AnnotatorCommentSlice } from '@/features/Annotator/Comment'; @@ -28,11 +28,15 @@ export const AnnotatorSkeleton: React.FC<{ children?: ReactNode }> = ({ children const { campaign, confidences, - analysis, } = useLoaderData({ from: '/_authenticated/annotation-campaign/$campaignID' }) const { phase } = useLoaderData({ from: '/_authenticated/annotation-campaign/$campaignID/phase/$phaseType' }) - const { spectrogram, annotations, info, isEditionAuthorized } = useLoaderData({ from: '/_authenticated/annotation-campaign/$campaignID/phase/$phaseType/spectrogram/$spectrogramID' }) - const analysisID = useAppSelector(selectAnalysisID) + const { + spectrogram, + annotations, + info, + isEditionAuthorized, + defaultAnalysis, + } = useLoaderData({ from: '/_authenticated/annotation-campaign/$campaignID/phase/$phaseType/spectrogram/$spectrogramID' }) const canNavigate = useAnnotatorCanNavigate() const dispatch = useAppDispatch() @@ -55,18 +59,6 @@ export const AnnotatorSkeleton: React.FC<{ children?: ReactNode }> = ({ children default: confidences?.find(c => c?.isDefault) ?? confidences.length ? confidences[0].label : undefined, })) dispatch(AnnotatorLabelSlice.actions.initCampaign()) - - // Select default analysis when none existing is selected - if (analysis.length && !analysis.find(a => a.id === analysisID)) { - const baseScaleAnalysis = analysis.find(a => - !a.frequencyScaleParts || a.frequencyScaleParts.length == 0 || - (a.frequencyScaleParts.length == 1 && a.frequencyScaleParts[0]!.minValue == 0 && a.frequencyScaleParts[0]!.maxValue == a.fft.samplingFrequency / 2), - ); - const minID = Math.min(...analysis.map(a => +a!.id))?.toString(); - if (minID) { - dispatch(AnnotatorAnalysisSlice.actions.setAnalysis(analysis.find(a => a.id === (baseScaleAnalysis?.id ?? minID)))); - } - } }, [ campaign ]); useEffect(() => { @@ -74,7 +66,7 @@ export const AnnotatorSkeleton: React.FC<{ children?: ReactNode }> = ({ children dispatch(AnnotatorUXSlice.actions.initSpectrogram()) const allAnnotations = convertGqlToAnnotations(annotations, phase.phase, user.id) - const defaultAnnotation = [...allAnnotations].pop() + const defaultAnnotation = [ ...allAnnotations ].pop() dispatch(AnnotatorConfidenceSlice.actions.initSpectrogram({ focus: defaultAnnotation?.confidence ?? undefined, })) @@ -86,10 +78,14 @@ export const AnnotatorSkeleton: React.FC<{ children?: ReactNode }> = ({ children })) dispatch(AnnotatorAnnotationSlice.actions.initSpectrogram({ all: allAnnotations, - default: defaultAnnotation + default: defaultAnnotation, })) }, [ spectrogram ]); + useEffect(() => { + dispatch(AnnotatorAnalysisSlice.actions.setAnalysis(defaultAnalysis)); + }, [ defaultAnalysis ]); + return
diff --git a/frontend/src/features/Annotator/Spectrogram/hooks.ts b/frontend/src/features/Annotator/Spectrogram/hooks.ts index cccbe1135..247172828 100644 --- a/frontend/src/features/Annotator/Spectrogram/hooks.ts +++ b/frontend/src/features/Annotator/Spectrogram/hooks.ts @@ -1,4 +1,4 @@ -import { useCallback, useRef } from 'react'; +import { useCallback, useEffect, useRef } from 'react'; import { selectZoom } from '@/features/Annotator/Zoom'; import { useToast } from '@/components/ui'; import { useWindowHeight } from '@/features/Annotator/Canvas'; @@ -16,24 +16,35 @@ export const useDrawSpectrogram = () => { const { data: paths, + refetch, } = useQuery({ ...AnnotationSpectrogram.API.getPathQuery({ spectrogramID: spectrogram.id, analysisID: analysis?.id ?? '', - }), enabled: !!analysis, + }), + enabled: !!analysis, }); const height = useWindowHeight() const timeScale = useTimeScale() const toast = useToast() - const images = useRef>>(new Map); + const images = useRef>>(new Map()); const failedImagesSources = useRef([]) + useEffect(() => { + images.current = new Map(); + }, [analysis]); + const areAllImagesLoaded = useCallback((): boolean => { return images.current.get(zoom)?.filter(i => !!i).length === zoom }, [ zoom ]) const loadImages = useCallback(async () => { - if (!analysis || !paths?.spectrogramPath || !spectrogram) { + let _paths = paths + if (!_paths) { + const { data } = await refetch() + _paths = data + } + if (!analysis || !_paths?.spectrogramPath || !spectrogram) { images.current = new Map(); return; } @@ -42,7 +53,7 @@ export const useDrawSpectrogram = () => { const filename = spectrogram.filename return Promise.all( Array.from(new Array(zoom)).map(async (_, index) => { - let src = paths.spectrogramPath; + let src = _paths.spectrogramPath; if (!src) return; if (analysis.legacy) { src = `${ src.split(filename)[0] }${ filename }_${ zoom }_${ index }${ src.split(filename)[1] }` @@ -69,7 +80,7 @@ export const useDrawSpectrogram = () => { ).then(loadedImages => { images.current.set(zoom, loadedImages) }) - }, [ analysis, zoom, failedImagesSources, areAllImagesLoaded, spectrogram, analysis, paths, toast ]) + }, [ analysis, zoom, failedImagesSources, areAllImagesLoaded, spectrogram, analysis, paths, toast, refetch ]) return useCallback(async (context: CanvasRenderingContext2D) => { if (!areAllImagesLoaded()) await loadImages(); diff --git a/frontend/src/routes/_authenticated/annotation-campaign/$campaignID/phase.$phaseType/spectrogram/$spectrogramID.tsx b/frontend/src/routes/_authenticated/annotation-campaign/$campaignID/phase.$phaseType/spectrogram/$spectrogramID.tsx index 2a4782aa8..b9ecbc4c4 100644 --- a/frontend/src/routes/_authenticated/annotation-campaign/$campaignID/phase.$phaseType/spectrogram/$spectrogramID.tsx +++ b/frontend/src/routes/_authenticated/annotation-campaign/$campaignID/phase.$phaseType/spectrogram/$spectrogramID.tsx @@ -26,7 +26,7 @@ import { AnnotationsBloc } from '@/features/Annotator/Annotation/AnnotationsBloc import styles from './$spectrogramID.module.scss'; import { type AllSpectrogramsFilters } from '@/features/AnnotationSpectrogram'; import { queryClient } from '@/api/queryClient'; -import { AnnotationSpectrogram, User } from '@/features'; +import { AnnotationCampaign, AnnotationSpectrogram, User } from '@/features'; import { useQuery } from '@tanstack/react-query'; const AnnotatorPage: React.FC = () => { @@ -134,11 +134,30 @@ export const Route = createFileRoute( loaderDeps: ({ search }) => search as AllSpectrogramsFilters, loader: async ({ params: { campaignID, phaseType, spectrogramID }, deps }) => { const user = await queryClient.ensureQueryData(User.API.currentQuery) - const { spectrogram, ...data } = await queryClient.ensureQueryData(AnnotationSpectrogram.API.getQuery({ - campaignID, phaseType, spectrogramID, ...deps, annotatorID: user.id, - })) + const [ + { spectrogram, ...data }, + { analysis } + ] = await Promise.all([ + queryClient.ensureQueryData(AnnotationSpectrogram.API.getQuery({ + campaignID, phaseType, spectrogramID, ...deps, annotatorID: user!.id, + })), + queryClient.ensureQueryData(AnnotationCampaign.API.byIdQuery({ id: campaignID })) + ]) if (!spectrogram) throw notFound() - return { spectrogram, ...data } + const baseScaleAnalysis = analysis.find(a => + !a.frequencyScaleParts || a.frequencyScaleParts.length == 0 || + (a.frequencyScaleParts.length == 1 && a.frequencyScaleParts[0]!.minValue == 0 && a.frequencyScaleParts[0]!.maxValue == a.fft.samplingFrequency / 2), + ); + const minID = Math.min(...analysis.map(a => +a!.id))?.toString(); + const defaultAnalysis = minID ? analysis.find(a => a.id === (baseScaleAnalysis?.id ?? minID)) : undefined + + if (defaultAnalysis) { + await queryClient.ensureQueryData(AnnotationSpectrogram.API.getPathQuery({ + spectrogramID: spectrogram.id, + analysisID: defaultAnalysis.id, + })) + } + return { spectrogram, defaultAnalysis, ...data } }, component: AnnotatorPage, })