From 3a389c3771ae19605e218d4327856237d012ce87 Mon Sep 17 00:00:00 2001 From: Mateo Date: Fri, 1 May 2026 15:54:44 +0200 Subject: [PATCH 1/2] feat(sequences-review): add stage selector with seq_annotation_done default Route /sequences/review now exposes a stage dropdown covering seq_annotation_done (default), in_review, annotated, needs_manual. The selected stage is persisted across reloads via localStorage. Review-specific UI (model accuracy, smoke/FP filters, unsure, legend, row coloring) stays gated on the annotated stage; the page behaves like /sequences/annotate for the other three stages. --- frontend/src/pages/SequencesPage.tsx | 33 +++++++------- frontend/src/pages/SequencesPageWrapper.tsx | 48 ++++++++++++++++++++- 2 files changed, 64 insertions(+), 17 deletions(-) diff --git a/frontend/src/pages/SequencesPage.tsx b/frontend/src/pages/SequencesPage.tsx index 0e37df5..d8e9b5a 100644 --- a/frontend/src/pages/SequencesPage.tsx +++ b/frontend/src/pages/SequencesPage.tsx @@ -1,4 +1,4 @@ -import { useMemo } from 'react'; +import { ReactNode, useMemo } from 'react'; import { useQuery } from '@tanstack/react-query'; import { useNavigate } from 'react-router-dom'; import { apiClient } from '@/services/api'; @@ -9,6 +9,7 @@ import { } from '@/types/api'; import { QUERY_KEYS } from '@/utils/constants'; import { analyzeSequenceAccuracy } from '@/utils/modelAccuracy'; +import { getProcessingStageLabel } from '@/utils/processingStage'; import TabbedFilters from '@/components/filters/TabbedFilters'; import { SequencesTableHeader, @@ -26,19 +27,20 @@ import { hasActiveUserFilters } from '@/utils/filterHelpers'; interface SequencesPageProps { defaultProcessingStage?: ProcessingStageStatus; + stageSelector?: ReactNode; } export default function SequencesPage({ defaultProcessingStage = 'ready_to_annotate', + stageSelector, }: SequencesPageProps = {}) { const navigate = useNavigate(); const { startAnnotationWorkflow } = useSequenceStore(); - // Determine storage key based on processing stage to separate annotate vs review filters - const storageKey = - defaultProcessingStage === 'annotated' - ? 'filters-sequences-review' - : 'filters-sequences-annotate'; + const isReviewPage = stageSelector !== undefined; + + // Storage key separates review vs annotate filters; review filters are shared across stages. + const storageKey = isReviewPage ? 'filters-sequences-review' : 'filters-sequences-annotate'; // Use persisted filters hook const { @@ -152,7 +154,7 @@ export default function SequencesPage({ } // Navigate to annotation interface with context about source page - const queryParam = defaultProcessingStage === 'annotated' ? '?from=review' : ''; + const queryParam = isReviewPage ? '?from=review' : ''; navigate(`/sequences/${clickedSequence.id}/annotate${queryParam}`); }; @@ -200,6 +202,7 @@ export default function SequencesPage({

Sequences

Manage and annotate wildfire detection sequences

+ {stageSelector} {/* Filters */} @@ -243,16 +246,15 @@ export default function SequencesPage({

No matching sequences found

-

- {defaultProcessingStage === 'annotated' - ? 'No completed sequences match your current filters.' - : 'No sequences match your current filters.'} -

+

No sequences match your current filters.

Try adjusting your search criteria above.

- ) : defaultProcessingStage === 'annotated' ? ( - // Review page - simple message without celebration -

No completed sequences to review at the moment.

+ ) : isReviewPage ? ( + // Review page - simple message scoped to the selected stage +

+ No sequences in "{getProcessingStageLabel(defaultProcessingStage)}" at the + moment. +

) : ( // Annotation page - celebratory message <> @@ -277,6 +279,7 @@ export default function SequencesPage({

Sequences

Manage and annotate wildfire detection sequences

+ {stageSelector} {/* Filters */} diff --git a/frontend/src/pages/SequencesPageWrapper.tsx b/frontend/src/pages/SequencesPageWrapper.tsx index c0b00e6..1de73d0 100644 --- a/frontend/src/pages/SequencesPageWrapper.tsx +++ b/frontend/src/pages/SequencesPageWrapper.tsx @@ -1,12 +1,56 @@ import SequencesPage from './SequencesPage'; -import { ProcessingStageStatus } from '@/types/api'; +import { ProcessingStage, ProcessingStageStatus } from '@/types/api'; +import { usePersistedTabState } from '@/hooks/usePersistedTabState'; +import { getProcessingStageLabel } from '@/utils/processingStage'; interface SequencesPageWrapperProps { defaultProcessingStage?: ProcessingStageStatus; } +const REVIEW_STAGES: ProcessingStage[] = [ + 'seq_annotation_done', + 'in_review', + 'annotated', + 'needs_manual', +]; + export default function SequencesPageWrapper({ defaultProcessingStage, }: SequencesPageWrapperProps) { - return ; + const isReview = defaultProcessingStage === 'annotated'; + + const [stage, setStage] = usePersistedTabState( + 'sequences-review-stage', + 'seq_annotation_done' + ); + + if (!isReview) { + return ; + } + + return ( + + + + + } + /> + ); } From 1ad130411cdde2ac5a52523379deacae29c949b5 Mon Sep 17 00:00:00 2001 From: Mateo Date: Fri, 1 May 2026 16:01:01 +0200 Subject: [PATCH 2/2] refactor(sequences-review): explicit isReviewPage prop, sync stage via effect - Replace implicit isReviewPage = (stageSelector !== undefined) with an explicit boolean prop so behavior is decoupled from the UI prop. - Drop the key={stage} remount; sync filters.processing_stage via a useEffect that resets pagination on stage change. Avoids losing in-flight query state on stage switches. --- frontend/src/pages/SequencesPage.tsx | 15 ++++++++++++--- frontend/src/pages/SequencesPageWrapper.tsx | 2 +- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/frontend/src/pages/SequencesPage.tsx b/frontend/src/pages/SequencesPage.tsx index d8e9b5a..feac060 100644 --- a/frontend/src/pages/SequencesPage.tsx +++ b/frontend/src/pages/SequencesPage.tsx @@ -1,4 +1,4 @@ -import { ReactNode, useMemo } from 'react'; +import { ReactNode, useEffect, useMemo } from 'react'; import { useQuery } from '@tanstack/react-query'; import { useNavigate } from 'react-router-dom'; import { apiClient } from '@/services/api'; @@ -27,18 +27,18 @@ import { hasActiveUserFilters } from '@/utils/filterHelpers'; interface SequencesPageProps { defaultProcessingStage?: ProcessingStageStatus; + isReviewPage?: boolean; stageSelector?: ReactNode; } export default function SequencesPage({ defaultProcessingStage = 'ready_to_annotate', + isReviewPage = false, stageSelector, }: SequencesPageProps = {}) { const navigate = useNavigate(); const { startAnnotationWorkflow } = useSequenceStore(); - const isReviewPage = stageSelector !== undefined; - // Storage key separates review vs annotate filters; review filters are shared across stages. const storageKey = isReviewPage ? 'filters-sequences-review' : 'filters-sequences-annotate'; @@ -61,6 +61,15 @@ export default function SequencesPage({ resetFilters, } = usePersistedFilters(storageKey, createDefaultFilterState(defaultProcessingStage)); + // Keep filters.processing_stage in sync with the parent-controlled stage prop + // (used by the review page stage selector). Reset to page 1 on stage change. + useEffect(() => { + if (filters.processing_stage !== defaultProcessingStage) { + setFilters({ ...filters, processing_stage: defaultProcessingStage, page: 1 }); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [defaultProcessingStage]); + // Fetch cameras, organizations, and source APIs for dropdown options const { data: cameras = [], isLoading: camerasLoading } = useCameras(); const { data: organizations = [], isLoading: organizationsLoading } = useOrganizations(); diff --git a/frontend/src/pages/SequencesPageWrapper.tsx b/frontend/src/pages/SequencesPageWrapper.tsx index 1de73d0..5b07af0 100644 --- a/frontend/src/pages/SequencesPageWrapper.tsx +++ b/frontend/src/pages/SequencesPageWrapper.tsx @@ -30,8 +30,8 @@ export default function SequencesPageWrapper({ return (