Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
42 changes: 27 additions & 15 deletions frontend/src/pages/SequencesPage.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { 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';
Expand All @@ -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,
Expand All @@ -26,19 +27,20 @@ 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();

// Determine storage key based on processing stage to separate annotate vs review filters
const storageKey =
defaultProcessingStage === 'annotated'
? 'filters-sequences-review'
: 'filters-sequences-annotate';
// 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 {
Expand All @@ -59,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();
Expand Down Expand Up @@ -152,7 +163,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}`);
};

Expand Down Expand Up @@ -200,6 +211,7 @@ export default function SequencesPage({
<h1 className="text-2xl font-bold text-gray-900">Sequences</h1>
<p className="text-gray-600">Manage and annotate wildfire detection sequences</p>
</div>
{stageSelector}
</div>

{/* Filters */}
Expand Down Expand Up @@ -243,16 +255,15 @@ export default function SequencesPage({
<h3 className="text-lg font-semibold text-gray-900 mb-2">
No matching sequences found
</h3>
<p className="text-gray-500 mb-4">
{defaultProcessingStage === 'annotated'
? 'No completed sequences match your current filters.'
: 'No sequences match your current filters.'}
</p>
<p className="text-gray-500 mb-4">No sequences match your current filters.</p>
<p className="text-gray-400 text-sm">Try adjusting your search criteria above.</p>
</>
) : defaultProcessingStage === 'annotated' ? (
// Review page - simple message without celebration
<p className="text-gray-500">No completed sequences to review at the moment.</p>
) : isReviewPage ? (
// Review page - simple message scoped to the selected stage
<p className="text-gray-500">
No sequences in &quot;{getProcessingStageLabel(defaultProcessingStage)}&quot; at the
moment.
</p>
) : (
// Annotation page - celebratory message
<>
Expand All @@ -277,6 +288,7 @@ export default function SequencesPage({
<h1 className="text-2xl font-bold text-gray-900">Sequences</h1>
<p className="text-gray-600">Manage and annotate wildfire detection sequences</p>
</div>
{stageSelector}
</div>

{/* Filters */}
Expand Down
48 changes: 46 additions & 2 deletions frontend/src/pages/SequencesPageWrapper.tsx
Original file line number Diff line number Diff line change
@@ -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 <SequencesPage defaultProcessingStage={defaultProcessingStage} />;
const isReview = defaultProcessingStage === 'annotated';

const [stage, setStage] = usePersistedTabState<ProcessingStage>(
'sequences-review-stage',
'seq_annotation_done'
);

if (!isReview) {
return <SequencesPage defaultProcessingStage={defaultProcessingStage} />;
}

return (
<SequencesPage
defaultProcessingStage={stage}
isReviewPage
stageSelector={
<div className="flex items-center space-x-2">
<label htmlFor="review-stage" className="text-sm text-gray-700">
Stage:
</label>
<select
id="review-stage"
value={stage}
onChange={e => setStage(e.target.value as ProcessingStage)}
className="border border-gray-300 rounded px-2 py-1 text-sm"
>
{REVIEW_STAGES.map(s => (
<option key={s} value={s}>
{getProcessingStageLabel(s)}
</option>
))}
</select>
</div>
}
/>
);
}
Loading