Extract detection and segmentation workflow hooks
This commit is contained in:
@@ -7,6 +7,14 @@
|
|||||||
|
|
||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## Sprint 26 frontend workflow hook hardening (2026-06-17)
|
||||||
|
|
||||||
|
- Moved Detection Lab orchestration state and API calls from `App.tsx` into `useDetectionWorkflow`.
|
||||||
|
- Moved Segmentation Lab orchestration state and API calls from `App.tsx` into `useSegmentationWorkflow`.
|
||||||
|
- Added shared frontend `formatError` helper.
|
||||||
|
- Added regression coverage to verify `App.tsx` wires the workflow hooks and panels without direct detection/segmentation API ownership.
|
||||||
|
- No API contracts, backend behavior, migrations, product features, provider fetching, AI behavior or UI redesign were introduced.
|
||||||
|
|
||||||
## Sprint 25 YOLO compatibility smoke hardening (2026-06-17)
|
## Sprint 25 YOLO compatibility smoke hardening (2026-06-17)
|
||||||
|
|
||||||
- Added an explicit `--check-model-load` mode to `scripts/yolo_preflight.py`.
|
- Added an explicit `--check-model-load` mode to `scripts/yolo_preflight.py`.
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[2]
|
||||||
|
|
||||||
|
|
||||||
|
def test_app_uses_detection_and_segmentation_workflow_hooks() -> None:
|
||||||
|
app = (ROOT / "frontend" / "src" / "App.tsx").read_text(encoding="utf-8")
|
||||||
|
|
||||||
|
assert "useDetectionWorkflow" in app
|
||||||
|
assert "useSegmentationWorkflow" in app
|
||||||
|
assert "from './hooks/useDetectionWorkflow'" in app
|
||||||
|
assert "from './hooks/useSegmentationWorkflow'" in app
|
||||||
|
assert "detectionApi" not in app
|
||||||
|
assert "segmentationApi" not in app
|
||||||
|
|
||||||
|
|
||||||
|
def test_detection_workflow_hook_owns_detection_api_calls() -> None:
|
||||||
|
hook = (ROOT / "frontend" / "src" / "hooks" / "useDetectionWorkflow.ts").read_text(encoding="utf-8")
|
||||||
|
|
||||||
|
assert "detectionApi.listModels" in hook
|
||||||
|
assert "detectionApi.listRuns" in hook
|
||||||
|
assert "detectionApi.run" in hook
|
||||||
|
assert "detectionApi.compareWithReference" in hook
|
||||||
|
assert "resetDetectionForProject" in hook
|
||||||
|
|
||||||
|
|
||||||
|
def test_segmentation_workflow_hook_owns_segmentation_api_calls() -> None:
|
||||||
|
hook = (ROOT / "frontend" / "src" / "hooks" / "useSegmentationWorkflow.ts").read_text(encoding="utf-8")
|
||||||
|
|
||||||
|
assert "segmentationApi.listModels" in hook
|
||||||
|
assert "segmentationApi.listRuns" in hook
|
||||||
|
assert "segmentationApi.run" in hook
|
||||||
|
assert "segmentationApi.compareWithReference" in hook
|
||||||
|
assert "resetSegmentationForProject" in hook
|
||||||
|
|
||||||
|
|
||||||
|
def test_app_still_wires_detection_and_segmentation_panels() -> None:
|
||||||
|
app = (ROOT / "frontend" / "src" / "App.tsx").read_text(encoding="utf-8")
|
||||||
|
|
||||||
|
assert "<DetectionLab" in app
|
||||||
|
assert "onRunDetection={runDetection}" in app
|
||||||
|
assert "onRunQa={runDetectionQa}" in app
|
||||||
|
assert "<SegmentationLab" in app
|
||||||
|
assert "onRunSegmentation={runSegmentation}" in app
|
||||||
|
assert "onRunQa={runSegmentationQa}" in app
|
||||||
@@ -1,3 +1,23 @@
|
|||||||
|
## Sprint 26 frontend workflow hook hardening (2026-06-17)
|
||||||
|
|
||||||
|
Changed:
|
||||||
|
- Moved Detection Lab orchestration state and API calls from `frontend/src/App.tsx` into `frontend/src/hooks/useDetectionWorkflow.ts`.
|
||||||
|
- Moved Segmentation Lab orchestration state and API calls from `frontend/src/App.tsx` into `frontend/src/hooks/useSegmentationWorkflow.ts`.
|
||||||
|
- Added shared frontend `formatError` helper under `frontend/src/lib/formatError.ts`.
|
||||||
|
- Added regression tests to verify App uses workflow hooks and still wires DetectionLab/SegmentationLab callbacks.
|
||||||
|
- Updated frontend README, TODO and changelog docs.
|
||||||
|
|
||||||
|
Validation:
|
||||||
|
- `python -m pytest backend/tests/test_sprint26_frontend_workflow_hooks.py backend/tests/test_sprint22_workbench_status_strip.py` passed: 6 tests.
|
||||||
|
- `cd frontend && npm run typecheck` passed.
|
||||||
|
- `cd frontend && npm run build` passed.
|
||||||
|
- `bash scripts/run_readiness_check.sh` passed: 160 backend tests, frontend typecheck/build, Alembic head check and script syntax checks.
|
||||||
|
- `python -m compileall backend/app` passed.
|
||||||
|
- `cd backend && python -m alembic upgrade head --sql` passed.
|
||||||
|
- `bash -n scripts/live_migration_smoke.sh` passed.
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
- No API contracts, backend behavior, migrations, product features, provider fetching, AI behavior or UI redesign changed.
|
||||||
## Sprint 25 YOLO compatibility smoke hardening (2026-06-17)
|
## Sprint 25 YOLO compatibility smoke hardening (2026-06-17)
|
||||||
|
|
||||||
Changed:
|
Changed:
|
||||||
|
|||||||
+2
-1
@@ -36,7 +36,8 @@ This file now starts with the current implementation status. Older preparation/b
|
|||||||
- [x] Dry-run-first demo export artifact cleanup tooling.
|
- [x] Dry-run-first demo export artifact cleanup tooling.
|
||||||
- [x] Live Docker/PostGIS validation on Tower/Unraid.
|
- [x] Live Docker/PostGIS validation on Tower/Unraid.
|
||||||
- [x] Real YOLO compatibility smoke with optional AI extras and local model file.
|
- [x] Real YOLO compatibility smoke with optional AI extras and local model file.
|
||||||
- [ ] Further frontend state/module decomposition beyond Sprint 10 extraction.
|
- [x] Detection and segmentation workflow hook extraction beyond Sprint 10.
|
||||||
|
- [ ] Further frontend state/module decomposition for datasets, exports and QA/QC.
|
||||||
|
|
||||||
## Sprint 8 status
|
## Sprint 8 status
|
||||||
|
|
||||||
|
|||||||
@@ -170,6 +170,13 @@ React + TypeScript + MapLibre foundation for project/area/dataset workflow.
|
|||||||
- The report summary covers project, AOI, datasets, QA/QC, exports and known limitations.
|
- The report summary covers project, AOI, datasets, QA/QC, exports and known limitations.
|
||||||
- Export Center behavior is unchanged; the richer handoff content is produced by the existing project report export action.
|
- Export Center behavior is unchanged; the richer handoff content is produced by the existing project report export action.
|
||||||
|
|
||||||
|
## Sprint 26 maintainability updates
|
||||||
|
|
||||||
|
- Detection workflow orchestration moved from `src/App.tsx` into `src/hooks/useDetectionWorkflow.ts`.
|
||||||
|
- Segmentation workflow orchestration moved from `src/App.tsx` into `src/hooks/useSegmentationWorkflow.ts`.
|
||||||
|
- Shared frontend API error formatting now lives in `src/lib/formatError.ts`.
|
||||||
|
- Detection Lab and Segmentation Lab UI behavior is unchanged; `App.tsx` still wires the same panel props and shared project/map state.
|
||||||
|
|
||||||
## Release hardening updates
|
## Release hardening updates
|
||||||
|
|
||||||
- Production builds split application code, React vendor code and MapLibre vendor code into separate chunks.
|
- Production builds split application code, React vendor code and MapLibre vendor code into separate chunks.
|
||||||
|
|||||||
+94
-325
@@ -4,7 +4,7 @@ import GeoMap from './components/GeoMap'
|
|||||||
import { areasApi } from './services/api/areas'
|
import { areasApi } from './services/api/areas'
|
||||||
import { datasetsApi } from './services/api/datasets'
|
import { datasetsApi } from './services/api/datasets'
|
||||||
import { projectsApi } from './services/api/projects'
|
import { projectsApi } from './services/api/projects'
|
||||||
import { analysisApi, demoApi, detectionApi, jobsApi, externalApi, exportsApi, qaApi, segmentationApi } from './services/api'
|
import { analysisApi, demoApi, jobsApi, externalApi, exportsApi, qaApi } from './services/api'
|
||||||
import { ChangeDetectionPanel } from './components/analysis/ChangeDetectionPanel'
|
import { ChangeDetectionPanel } from './components/analysis/ChangeDetectionPanel'
|
||||||
import { DetectionLab } from './components/detection/DetectionLab'
|
import { DetectionLab } from './components/detection/DetectionLab'
|
||||||
import { ExportCenter } from './components/exports/ExportCenter'
|
import { ExportCenter } from './components/exports/ExportCenter'
|
||||||
@@ -16,11 +16,6 @@ import type {
|
|||||||
ChangeDetectionSummary,
|
ChangeDetectionSummary,
|
||||||
DatasetCreateResponse,
|
DatasetCreateResponse,
|
||||||
DatasetListResponse,
|
DatasetListResponse,
|
||||||
DetectionQaResult,
|
|
||||||
DetectionRead,
|
|
||||||
DetectionModelCapability,
|
|
||||||
DetectionRunRead,
|
|
||||||
DetectionRunResponse,
|
|
||||||
JobRead,
|
JobRead,
|
||||||
QaComparisonRequest,
|
QaComparisonRequest,
|
||||||
RasterMetadataResponse,
|
RasterMetadataResponse,
|
||||||
@@ -38,14 +33,12 @@ import type {
|
|||||||
AreaRead,
|
AreaRead,
|
||||||
ProviderCapability,
|
ProviderCapability,
|
||||||
QaComparisonResult,
|
QaComparisonResult,
|
||||||
SegmentationModelCapability,
|
|
||||||
SegmentationQaResult,
|
|
||||||
SegmentationRead,
|
|
||||||
SegmentationRunRead,
|
|
||||||
SegmentationRunResponse,
|
|
||||||
} from './types'
|
} from './types'
|
||||||
import { ProviderPanel } from './components/providers/ProviderPanel'
|
import { ProviderPanel } from './components/providers/ProviderPanel'
|
||||||
import { SegmentationLab } from './components/segmentation/SegmentationLab'
|
import { SegmentationLab } from './components/segmentation/SegmentationLab'
|
||||||
|
import { useDetectionWorkflow } from './hooks/useDetectionWorkflow'
|
||||||
|
import { useSegmentationWorkflow } from './hooks/useSegmentationWorkflow'
|
||||||
|
import { formatError } from './lib/formatError'
|
||||||
|
|
||||||
function isVectorDatasetType(datasetType: string): boolean {
|
function isVectorDatasetType(datasetType: string): boolean {
|
||||||
return datasetType === 'vector' || datasetType === 'geojson'
|
return datasetType === 'vector' || datasetType === 'geojson'
|
||||||
@@ -76,14 +69,6 @@ function formatBounds(bounds: Record<string, number> | null | undefined): string
|
|||||||
return `${bounds.min_x?.toFixed(4)}, ${bounds.min_y?.toFixed(4)} -> ${bounds.max_x?.toFixed(4)}, ${bounds.max_y?.toFixed(4)}`
|
return `${bounds.min_x?.toFixed(4)}, ${bounds.min_y?.toFixed(4)} -> ${bounds.max_x?.toFixed(4)}, ${bounds.max_y?.toFixed(4)}`
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatError(error: unknown, fallback: string): string {
|
|
||||||
if (error instanceof Error) {
|
|
||||||
const code = (error as { code?: string }).code
|
|
||||||
return code ? `${error.message} (${code})` : error.message
|
|
||||||
}
|
|
||||||
return fallback
|
|
||||||
}
|
|
||||||
|
|
||||||
function App(): JSX.Element {
|
function App(): JSX.Element {
|
||||||
const [projects, setProjects] = useState<ProjectRead[]>([])
|
const [projects, setProjects] = useState<ProjectRead[]>([])
|
||||||
const [selectedProjectId, setSelectedProjectId] = useState<string | null>(null)
|
const [selectedProjectId, setSelectedProjectId] = useState<string | null>(null)
|
||||||
@@ -112,47 +97,6 @@ function App(): JSX.Element {
|
|||||||
const [runningChangeDetection, setRunningChangeDetection] = useState(false)
|
const [runningChangeDetection, setRunningChangeDetection] = useState(false)
|
||||||
const [changeDetectionResult, setChangeDetectionResult] = useState<ChangeDetectionSummary | null>(null)
|
const [changeDetectionResult, setChangeDetectionResult] = useState<ChangeDetectionSummary | null>(null)
|
||||||
const [changeDetectionError, setChangeDetectionError] = useState<string | null>(null)
|
const [changeDetectionError, setChangeDetectionError] = useState<string | null>(null)
|
||||||
const [detectionModels, setDetectionModels] = useState<DetectionModelCapability[]>([])
|
|
||||||
const [loadingDetectionModels, setLoadingDetectionModels] = useState(false)
|
|
||||||
const [detectionModelError, setDetectionModelError] = useState<string | null>(null)
|
|
||||||
const [selectedDetectionDatasetId, setSelectedDetectionDatasetId] = useState('')
|
|
||||||
const [selectedDetectionModelId, setSelectedDetectionModelId] = useState('yolo-placeholder')
|
|
||||||
const [detectionTileManifestPath, setDetectionTileManifestPath] = useState('')
|
|
||||||
const [detectionConfidenceThreshold, setDetectionConfidenceThreshold] = useState(0.5)
|
|
||||||
const [runningDetection, setRunningDetection] = useState(false)
|
|
||||||
const [detectionRunResult, setDetectionRunResult] = useState<DetectionRunResponse | null>(null)
|
|
||||||
const [detectionRunError, setDetectionRunError] = useState<string | null>(null)
|
|
||||||
const [detectionRuns, setDetectionRuns] = useState<DetectionRunRead[]>([])
|
|
||||||
const [selectedDetectionRunId, setSelectedDetectionRunId] = useState('')
|
|
||||||
const [detectionItems, setDetectionItems] = useState<DetectionRead[]>([])
|
|
||||||
const [detectionGeoJson, setDetectionGeoJson] = useState<GeoJSON.FeatureCollection | null>(null)
|
|
||||||
const [detectionClassFilter, setDetectionClassFilter] = useState('')
|
|
||||||
const [detectionMinConfidenceFilter, setDetectionMinConfidenceFilter] = useState(0)
|
|
||||||
const [loadingDetectionResults, setLoadingDetectionResults] = useState(false)
|
|
||||||
const [detectionReferenceDatasetId, setDetectionReferenceDatasetId] = useState('')
|
|
||||||
const [detectionQaResult, setDetectionQaResult] = useState<DetectionQaResult | null>(null)
|
|
||||||
const [detectionQaError, setDetectionQaError] = useState<string | null>(null)
|
|
||||||
const [runningDetectionQa, setRunningDetectionQa] = useState(false)
|
|
||||||
const [segmentationModels, setSegmentationModels] = useState<SegmentationModelCapability[]>([])
|
|
||||||
const [loadingSegmentationModels, setLoadingSegmentationModels] = useState(false)
|
|
||||||
const [segmentationModelError, setSegmentationModelError] = useState<string | null>(null)
|
|
||||||
const [selectedSegmentationDatasetId, setSelectedSegmentationDatasetId] = useState('')
|
|
||||||
const [selectedSegmentationModelId, setSelectedSegmentationModelId] = useState('segmentation-placeholder')
|
|
||||||
const [segmentationConfidenceThreshold, setSegmentationConfidenceThreshold] = useState(0.5)
|
|
||||||
const [runningSegmentation, setRunningSegmentation] = useState(false)
|
|
||||||
const [segmentationRunResult, setSegmentationRunResult] = useState<SegmentationRunResponse | null>(null)
|
|
||||||
const [segmentationRunError, setSegmentationRunError] = useState<string | null>(null)
|
|
||||||
const [segmentationRuns, setSegmentationRuns] = useState<SegmentationRunRead[]>([])
|
|
||||||
const [selectedSegmentationRunId, setSelectedSegmentationRunId] = useState('')
|
|
||||||
const [segmentationItems, setSegmentationItems] = useState<SegmentationRead[]>([])
|
|
||||||
const [segmentationGeoJson, setSegmentationGeoJson] = useState<GeoJSON.FeatureCollection | null>(null)
|
|
||||||
const [segmentationClassFilter, setSegmentationClassFilter] = useState('')
|
|
||||||
const [segmentationMinConfidenceFilter, setSegmentationMinConfidenceFilter] = useState(0)
|
|
||||||
const [loadingSegmentationResults, setLoadingSegmentationResults] = useState(false)
|
|
||||||
const [segmentationReferenceDatasetId, setSegmentationReferenceDatasetId] = useState('')
|
|
||||||
const [segmentationQaResult, setSegmentationQaResult] = useState<SegmentationQaResult | null>(null)
|
|
||||||
const [segmentationQaError, setSegmentationQaError] = useState<string | null>(null)
|
|
||||||
const [runningSegmentationQa, setRunningSegmentationQa] = useState(false)
|
|
||||||
const [rasterPreview, setRasterPreview] = useState<RasterPreviewResponse | null>(null)
|
const [rasterPreview, setRasterPreview] = useState<RasterPreviewResponse | null>(null)
|
||||||
const [selectedIntersectTargetId, setSelectedIntersectTargetId] = useState('')
|
const [selectedIntersectTargetId, setSelectedIntersectTargetId] = useState('')
|
||||||
const [selectedClipAreaId, setSelectedClipAreaId] = useState('')
|
const [selectedClipAreaId, setSelectedClipAreaId] = useState('')
|
||||||
@@ -243,10 +187,91 @@ function App(): JSX.Element {
|
|||||||
const candidateDatasets = availableVectorDatasets
|
const candidateDatasets = availableVectorDatasets
|
||||||
const providers = useMemo(() => providerCapabilities, [providerCapabilities])
|
const providers = useMemo(() => providerCapabilities, [providerCapabilities])
|
||||||
const rasterDatasets = useMemo(() => datasets.filter((item) => item.dataset_type === 'raster'), [datasets])
|
const rasterDatasets = useMemo(() => datasets.filter((item) => item.dataset_type === 'raster'), [datasets])
|
||||||
const selectedSegmentationModel = useMemo(
|
const {
|
||||||
() => segmentationModels.find((model) => model.model_id === selectedSegmentationModelId) ?? null,
|
detectionModels,
|
||||||
[segmentationModels, selectedSegmentationModelId],
|
loadingDetectionModels,
|
||||||
)
|
detectionModelError,
|
||||||
|
selectedDetectionDatasetId,
|
||||||
|
selectedDetectionModelId,
|
||||||
|
detectionTileManifestPath,
|
||||||
|
detectionConfidenceThreshold,
|
||||||
|
runningDetection,
|
||||||
|
detectionRunResult,
|
||||||
|
detectionRunError,
|
||||||
|
detectionRuns,
|
||||||
|
selectedDetectionRunId,
|
||||||
|
detectionItems,
|
||||||
|
detectionGeoJson,
|
||||||
|
detectionClassFilter,
|
||||||
|
detectionMinConfidenceFilter,
|
||||||
|
loadingDetectionResults,
|
||||||
|
detectionReferenceDatasetId,
|
||||||
|
detectionQaResult,
|
||||||
|
detectionQaError,
|
||||||
|
runningDetectionQa,
|
||||||
|
loadDetectionModels,
|
||||||
|
loadDetectionRuns,
|
||||||
|
loadDetectionResults,
|
||||||
|
runDetection,
|
||||||
|
runDetectionQa,
|
||||||
|
resetDetectionForProject,
|
||||||
|
setSelectedDetectionDatasetId,
|
||||||
|
setSelectedDetectionModelId,
|
||||||
|
setDetectionTileManifestPath,
|
||||||
|
setDetectionConfidenceThreshold,
|
||||||
|
setSelectedDetectionRunId,
|
||||||
|
setDetectionClassFilter,
|
||||||
|
setDetectionMinConfidenceFilter,
|
||||||
|
setDetectionReferenceDatasetId,
|
||||||
|
} = useDetectionWorkflow({
|
||||||
|
selectedProjectId,
|
||||||
|
rasterDatasets,
|
||||||
|
qaIouThreshold,
|
||||||
|
loadProjectData,
|
||||||
|
loadQualityChecks,
|
||||||
|
})
|
||||||
|
const {
|
||||||
|
segmentationModels,
|
||||||
|
loadingSegmentationModels,
|
||||||
|
segmentationModelError,
|
||||||
|
selectedSegmentationDatasetId,
|
||||||
|
selectedSegmentationModelId,
|
||||||
|
selectedSegmentationModel,
|
||||||
|
segmentationConfidenceThreshold,
|
||||||
|
runningSegmentation,
|
||||||
|
segmentationRunResult,
|
||||||
|
segmentationRunError,
|
||||||
|
segmentationRuns,
|
||||||
|
selectedSegmentationRunId,
|
||||||
|
segmentationItems,
|
||||||
|
segmentationGeoJson,
|
||||||
|
segmentationClassFilter,
|
||||||
|
segmentationMinConfidenceFilter,
|
||||||
|
loadingSegmentationResults,
|
||||||
|
segmentationReferenceDatasetId,
|
||||||
|
segmentationQaResult,
|
||||||
|
segmentationQaError,
|
||||||
|
runningSegmentationQa,
|
||||||
|
loadSegmentationModels,
|
||||||
|
loadSegmentationRuns,
|
||||||
|
loadSegmentationResults,
|
||||||
|
runSegmentation,
|
||||||
|
runSegmentationQa,
|
||||||
|
resetSegmentationForProject,
|
||||||
|
setSelectedSegmentationDatasetId,
|
||||||
|
setSelectedSegmentationModelId,
|
||||||
|
setSegmentationConfidenceThreshold,
|
||||||
|
setSelectedSegmentationRunId,
|
||||||
|
setSegmentationClassFilter,
|
||||||
|
setSegmentationMinConfidenceFilter,
|
||||||
|
setSegmentationReferenceDatasetId,
|
||||||
|
} = useSegmentationWorkflow({
|
||||||
|
selectedProjectId,
|
||||||
|
rasterDatasets,
|
||||||
|
qaIouThreshold,
|
||||||
|
loadProjectData,
|
||||||
|
loadQualityChecks,
|
||||||
|
})
|
||||||
const selectedMapArea = useMemo(
|
const selectedMapArea = useMemo(
|
||||||
() => areas.find((area) => area.id === selectedMapAreaId) ?? null,
|
() => areas.find((area) => area.id === selectedMapAreaId) ?? null,
|
||||||
[areas, selectedMapAreaId],
|
[areas, selectedMapAreaId],
|
||||||
@@ -329,7 +354,7 @@ function App(): JSX.Element {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const loadProjectData = async (projectId: string) => {
|
async function loadProjectData(projectId: string) {
|
||||||
setLoadingAreas(true)
|
setLoadingAreas(true)
|
||||||
setLoadingDatasets(true)
|
setLoadingDatasets(true)
|
||||||
setErrorMessage(null)
|
setErrorMessage(null)
|
||||||
@@ -373,123 +398,7 @@ function App(): JSX.Element {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const loadDetectionModels = async () => {
|
async function loadQualityChecks(projectId = selectedProjectId) {
|
||||||
setLoadingDetectionModels(true)
|
|
||||||
setDetectionModelError(null)
|
|
||||||
try {
|
|
||||||
const response = await detectionApi.listModels()
|
|
||||||
setDetectionModels(response.models)
|
|
||||||
if (!response.models.some((model) => model.model_id === selectedDetectionModelId) && response.models.length > 0) {
|
|
||||||
setSelectedDetectionModelId(response.models[0].model_id)
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
setDetectionModelError(formatError(error, 'Failed to load detection models'))
|
|
||||||
} finally {
|
|
||||||
setLoadingDetectionModels(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const loadDetectionRuns = async (projectId = selectedProjectId) => {
|
|
||||||
if (!projectId) {
|
|
||||||
setDetectionRuns([])
|
|
||||||
return
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const response = await detectionApi.listRuns({ project_id: projectId })
|
|
||||||
setDetectionRuns(response.items)
|
|
||||||
if (!selectedDetectionRunId && response.items.length > 0) {
|
|
||||||
setSelectedDetectionRunId(response.items[0].id)
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
setDetectionRunError(formatError(error, 'Failed to load detection runs'))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const loadDetectionResults = async (analysisRunId = selectedDetectionRunId) => {
|
|
||||||
if (!analysisRunId) {
|
|
||||||
setDetectionItems([])
|
|
||||||
setDetectionGeoJson(null)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
setLoadingDetectionResults(true)
|
|
||||||
setDetectionRunError(null)
|
|
||||||
try {
|
|
||||||
const params = {
|
|
||||||
class_name: detectionClassFilter || null,
|
|
||||||
min_confidence: detectionMinConfidenceFilter > 0 ? detectionMinConfidenceFilter : null,
|
|
||||||
}
|
|
||||||
const [detectionsResponse, geoJsonResponse] = await Promise.all([
|
|
||||||
detectionApi.listDetections(analysisRunId, params),
|
|
||||||
detectionApi.getRunGeoJson(analysisRunId, params),
|
|
||||||
])
|
|
||||||
setDetectionItems(detectionsResponse.items)
|
|
||||||
setDetectionGeoJson(geoJsonResponse)
|
|
||||||
} catch (error) {
|
|
||||||
setDetectionRunError(formatError(error, 'Failed to load detection results'))
|
|
||||||
} finally {
|
|
||||||
setLoadingDetectionResults(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const loadSegmentationModels = async () => {
|
|
||||||
setLoadingSegmentationModels(true)
|
|
||||||
setSegmentationModelError(null)
|
|
||||||
try {
|
|
||||||
const response = await segmentationApi.listModels()
|
|
||||||
setSegmentationModels(response.models)
|
|
||||||
if (!response.models.some((model) => model.model_id === selectedSegmentationModelId) && response.models.length > 0) {
|
|
||||||
setSelectedSegmentationModelId(response.models[0].model_id)
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
setSegmentationModelError(formatError(error, 'Failed to load segmentation models'))
|
|
||||||
} finally {
|
|
||||||
setLoadingSegmentationModels(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const loadSegmentationRuns = async (projectId = selectedProjectId) => {
|
|
||||||
if (!projectId) {
|
|
||||||
setSegmentationRuns([])
|
|
||||||
return
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const response = await segmentationApi.listRuns({ project_id: projectId })
|
|
||||||
setSegmentationRuns(response.items)
|
|
||||||
if (!selectedSegmentationRunId && response.items.length > 0) {
|
|
||||||
setSelectedSegmentationRunId(response.items[0].id)
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
setSegmentationRunError(formatError(error, 'Failed to load segmentation runs'))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const loadSegmentationResults = async (analysisRunId = selectedSegmentationRunId) => {
|
|
||||||
if (!analysisRunId) {
|
|
||||||
setSegmentationItems([])
|
|
||||||
setSegmentationGeoJson(null)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
setLoadingSegmentationResults(true)
|
|
||||||
setSegmentationRunError(null)
|
|
||||||
try {
|
|
||||||
const params = {
|
|
||||||
class_name: segmentationClassFilter || null,
|
|
||||||
min_confidence: segmentationMinConfidenceFilter > 0 ? segmentationMinConfidenceFilter : null,
|
|
||||||
}
|
|
||||||
const [segmentationsResponse, geoJsonResponse] = await Promise.all([
|
|
||||||
segmentationApi.listSegmentations(analysisRunId, params),
|
|
||||||
segmentationApi.getRunGeoJson(analysisRunId, params),
|
|
||||||
])
|
|
||||||
setSegmentationItems(segmentationsResponse.items)
|
|
||||||
setSegmentationGeoJson(geoJsonResponse)
|
|
||||||
} catch (error) {
|
|
||||||
setSegmentationRunError(formatError(error, 'Failed to load segmentation results'))
|
|
||||||
} finally {
|
|
||||||
setLoadingSegmentationResults(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const loadQualityChecks = async (projectId = selectedProjectId) => {
|
|
||||||
if (!projectId) {
|
if (!projectId) {
|
||||||
setQualityChecks([])
|
setQualityChecks([])
|
||||||
return
|
return
|
||||||
@@ -498,6 +407,7 @@ function App(): JSX.Element {
|
|||||||
try {
|
try {
|
||||||
const response = await qaApi.listQualityChecks(projectId)
|
const response = await qaApi.listQualityChecks(projectId)
|
||||||
setQualityChecks(response.items)
|
setQualityChecks(response.items)
|
||||||
|
return response.items
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setQualityChecksError(formatError(error, 'Failed to load QA/QC results'))
|
setQualityChecksError(formatError(error, 'Failed to load QA/QC results'))
|
||||||
}
|
}
|
||||||
@@ -697,18 +607,8 @@ function App(): JSX.Element {
|
|||||||
setSelectedRasterMetadata(null)
|
setSelectedRasterMetadata(null)
|
||||||
setSelectedRasterStats(null)
|
setSelectedRasterStats(null)
|
||||||
setJobs([])
|
setJobs([])
|
||||||
setSelectedDetectionDatasetId('')
|
resetDetectionForProject()
|
||||||
setDetectionRuns([])
|
resetSegmentationForProject()
|
||||||
setSelectedDetectionRunId('')
|
|
||||||
setDetectionItems([])
|
|
||||||
setDetectionGeoJson(null)
|
|
||||||
setDetectionRunResult(null)
|
|
||||||
setSelectedSegmentationDatasetId('')
|
|
||||||
setSegmentationRuns([])
|
|
||||||
setSelectedSegmentationRunId('')
|
|
||||||
setSegmentationItems([])
|
|
||||||
setSegmentationGeoJson(null)
|
|
||||||
setSegmentationRunResult(null)
|
|
||||||
setExports([])
|
setExports([])
|
||||||
setLatestExport(null)
|
setLatestExport(null)
|
||||||
setExportPreview(null)
|
setExportPreview(null)
|
||||||
@@ -1166,40 +1066,6 @@ function App(): JSX.Element {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const runDetection = async () => {
|
|
||||||
if (!selectedProjectId) {
|
|
||||||
setDetectionRunError('Select a project first')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const datasetId = selectedDetectionDatasetId || rasterDatasets[0]?.id
|
|
||||||
if (!datasetId) {
|
|
||||||
setDetectionRunError('Select a raster dataset')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
setDetectionRunError(null)
|
|
||||||
setDetectionRunResult(null)
|
|
||||||
setRunningDetection(true)
|
|
||||||
try {
|
|
||||||
const result = await detectionApi.run({
|
|
||||||
project_id: selectedProjectId,
|
|
||||||
dataset_id: datasetId,
|
|
||||||
model_id: selectedDetectionModelId,
|
|
||||||
confidence_threshold: detectionConfidenceThreshold,
|
|
||||||
tile_manifest_path: detectionTileManifestPath.trim() || null,
|
|
||||||
parameters_json: {},
|
|
||||||
})
|
|
||||||
setDetectionRunResult(result)
|
|
||||||
setSelectedDetectionRunId(result.analysis_run_id)
|
|
||||||
await loadDetectionRuns(selectedProjectId)
|
|
||||||
await loadDetectionResults(result.analysis_run_id)
|
|
||||||
await loadProjectData(selectedProjectId)
|
|
||||||
} catch (error) {
|
|
||||||
setDetectionRunError(formatError(error, 'Detection run failed'))
|
|
||||||
} finally {
|
|
||||||
setRunningDetection(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const runChangeDetection = async () => {
|
const runChangeDetection = async () => {
|
||||||
const sourceDatasetId = changeSourceDatasetId || availableVectorDatasets[0]?.id
|
const sourceDatasetId = changeSourceDatasetId || availableVectorDatasets[0]?.id
|
||||||
const targetDatasetId =
|
const targetDatasetId =
|
||||||
@@ -1245,103 +1111,6 @@ function App(): JSX.Element {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const runDetectionQa = async () => {
|
|
||||||
if (!selectedDetectionRunId) {
|
|
||||||
setDetectionQaError('Select a detection run')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (!detectionReferenceDatasetId) {
|
|
||||||
setDetectionQaError('Select a reference dataset')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
setDetectionQaError(null)
|
|
||||||
setDetectionQaResult(null)
|
|
||||||
setRunningDetectionQa(true)
|
|
||||||
try {
|
|
||||||
const result = await detectionApi.compareWithReference(selectedDetectionRunId, {
|
|
||||||
reference_dataset_id: detectionReferenceDatasetId,
|
|
||||||
iou_threshold: qaIouThreshold,
|
|
||||||
class_name: detectionClassFilter || null,
|
|
||||||
min_confidence: detectionMinConfidenceFilter > 0 ? detectionMinConfidenceFilter : null,
|
|
||||||
})
|
|
||||||
setDetectionQaResult(result)
|
|
||||||
await loadQualityChecks(selectedProjectId)
|
|
||||||
} catch (error) {
|
|
||||||
setDetectionQaError(formatError(error, 'Detection QA failed'))
|
|
||||||
} finally {
|
|
||||||
setRunningDetectionQa(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const runSegmentation = async () => {
|
|
||||||
if (!selectedProjectId) {
|
|
||||||
setSegmentationRunError('Select a project first')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const datasetId = selectedSegmentationDatasetId || rasterDatasets[0]?.id
|
|
||||||
if (!datasetId) {
|
|
||||||
setSegmentationRunError('Select a raster dataset')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (!selectedSegmentationModel?.configured) {
|
|
||||||
setSegmentationRunError('Selected segmentation model is not configured')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
setSegmentationRunError(null)
|
|
||||||
setSegmentationRunResult(null)
|
|
||||||
setRunningSegmentation(true)
|
|
||||||
try {
|
|
||||||
const parameters =
|
|
||||||
selectedSegmentationModelId === 'fixture-segmenter'
|
|
||||||
? { fixture_mode: true, fixture_segmentations: [] }
|
|
||||||
: {}
|
|
||||||
const result = await segmentationApi.run({
|
|
||||||
project_id: selectedProjectId,
|
|
||||||
dataset_id: datasetId,
|
|
||||||
model_id: selectedSegmentationModelId,
|
|
||||||
confidence_threshold: segmentationConfidenceThreshold,
|
|
||||||
parameters_json: parameters,
|
|
||||||
})
|
|
||||||
setSegmentationRunResult(result)
|
|
||||||
setSelectedSegmentationRunId(result.analysis_run_id)
|
|
||||||
await loadSegmentationRuns(selectedProjectId)
|
|
||||||
await loadSegmentationResults(result.analysis_run_id)
|
|
||||||
await loadProjectData(selectedProjectId)
|
|
||||||
} catch (error) {
|
|
||||||
setSegmentationRunError(formatError(error, 'Segmentation run failed'))
|
|
||||||
} finally {
|
|
||||||
setRunningSegmentation(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const runSegmentationQa = async () => {
|
|
||||||
if (!selectedSegmentationRunId) {
|
|
||||||
setSegmentationQaError('Select a segmentation run')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (!segmentationReferenceDatasetId) {
|
|
||||||
setSegmentationQaError('Select a reference dataset')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
setSegmentationQaError(null)
|
|
||||||
setSegmentationQaResult(null)
|
|
||||||
setRunningSegmentationQa(true)
|
|
||||||
try {
|
|
||||||
const result = await segmentationApi.compareWithReference(selectedSegmentationRunId, {
|
|
||||||
reference_dataset_id: segmentationReferenceDatasetId,
|
|
||||||
iou_threshold: qaIouThreshold,
|
|
||||||
class_name: segmentationClassFilter || null,
|
|
||||||
min_confidence: segmentationMinConfidenceFilter > 0 ? segmentationMinConfidenceFilter : null,
|
|
||||||
})
|
|
||||||
setSegmentationQaResult(result)
|
|
||||||
await loadQualityChecks(selectedProjectId)
|
|
||||||
} catch (error) {
|
|
||||||
setSegmentationQaError(formatError(error, 'Segmentation QA failed'))
|
|
||||||
} finally {
|
|
||||||
setRunningSegmentationQa(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const pickDerivedDataset = async (datasetId: string) => {
|
const pickDerivedDataset = async (datasetId: string) => {
|
||||||
if (!selectedProjectId) {
|
if (!selectedProjectId) {
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -0,0 +1,217 @@
|
|||||||
|
import { useState } from 'react'
|
||||||
|
import { detectionApi } from '../services/api'
|
||||||
|
import type {
|
||||||
|
DatasetCreateResponse,
|
||||||
|
DetectionModelCapability,
|
||||||
|
DetectionQaResult,
|
||||||
|
DetectionRead,
|
||||||
|
DetectionRunRead,
|
||||||
|
DetectionRunResponse,
|
||||||
|
QualityCheckRead,
|
||||||
|
} from '../types'
|
||||||
|
import { formatError } from '../lib/formatError'
|
||||||
|
|
||||||
|
interface DetectionWorkflowOptions {
|
||||||
|
selectedProjectId: string | null
|
||||||
|
rasterDatasets: DatasetCreateResponse[]
|
||||||
|
qaIouThreshold: number
|
||||||
|
loadProjectData: (projectId: string) => Promise<unknown>
|
||||||
|
loadQualityChecks: (projectId?: string | null) => Promise<QualityCheckRead[] | void>
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useDetectionWorkflow({
|
||||||
|
selectedProjectId,
|
||||||
|
rasterDatasets,
|
||||||
|
qaIouThreshold,
|
||||||
|
loadProjectData,
|
||||||
|
loadQualityChecks,
|
||||||
|
}: DetectionWorkflowOptions) {
|
||||||
|
const [detectionModels, setDetectionModels] = useState<DetectionModelCapability[]>([])
|
||||||
|
const [loadingDetectionModels, setLoadingDetectionModels] = useState(false)
|
||||||
|
const [detectionModelError, setDetectionModelError] = useState<string | null>(null)
|
||||||
|
const [selectedDetectionDatasetId, setSelectedDetectionDatasetId] = useState('')
|
||||||
|
const [selectedDetectionModelId, setSelectedDetectionModelId] = useState('yolo-placeholder')
|
||||||
|
const [detectionTileManifestPath, setDetectionTileManifestPath] = useState('')
|
||||||
|
const [detectionConfidenceThreshold, setDetectionConfidenceThreshold] = useState(0.5)
|
||||||
|
const [runningDetection, setRunningDetection] = useState(false)
|
||||||
|
const [detectionRunResult, setDetectionRunResult] = useState<DetectionRunResponse | null>(null)
|
||||||
|
const [detectionRunError, setDetectionRunError] = useState<string | null>(null)
|
||||||
|
const [detectionRuns, setDetectionRuns] = useState<DetectionRunRead[]>([])
|
||||||
|
const [selectedDetectionRunId, setSelectedDetectionRunId] = useState('')
|
||||||
|
const [detectionItems, setDetectionItems] = useState<DetectionRead[]>([])
|
||||||
|
const [detectionGeoJson, setDetectionGeoJson] = useState<GeoJSON.FeatureCollection | null>(null)
|
||||||
|
const [detectionClassFilter, setDetectionClassFilter] = useState('')
|
||||||
|
const [detectionMinConfidenceFilter, setDetectionMinConfidenceFilter] = useState(0)
|
||||||
|
const [loadingDetectionResults, setLoadingDetectionResults] = useState(false)
|
||||||
|
const [detectionReferenceDatasetId, setDetectionReferenceDatasetId] = useState('')
|
||||||
|
const [detectionQaResult, setDetectionQaResult] = useState<DetectionQaResult | null>(null)
|
||||||
|
const [detectionQaError, setDetectionQaError] = useState<string | null>(null)
|
||||||
|
const [runningDetectionQa, setRunningDetectionQa] = useState(false)
|
||||||
|
|
||||||
|
const loadDetectionModels = async () => {
|
||||||
|
setLoadingDetectionModels(true)
|
||||||
|
setDetectionModelError(null)
|
||||||
|
try {
|
||||||
|
const response = await detectionApi.listModels()
|
||||||
|
setDetectionModels(response.models)
|
||||||
|
if (!response.models.some((model) => model.model_id === selectedDetectionModelId) && response.models.length > 0) {
|
||||||
|
setSelectedDetectionModelId(response.models[0].model_id)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
setDetectionModelError(formatError(error, 'Failed to load detection models'))
|
||||||
|
} finally {
|
||||||
|
setLoadingDetectionModels(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadDetectionRuns = async (projectId = selectedProjectId) => {
|
||||||
|
if (!projectId) {
|
||||||
|
setDetectionRuns([])
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const response = await detectionApi.listRuns({ project_id: projectId })
|
||||||
|
setDetectionRuns(response.items)
|
||||||
|
if (!selectedDetectionRunId && response.items.length > 0) {
|
||||||
|
setSelectedDetectionRunId(response.items[0].id)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
setDetectionRunError(formatError(error, 'Failed to load detection runs'))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadDetectionResults = async (analysisRunId = selectedDetectionRunId) => {
|
||||||
|
if (!analysisRunId) {
|
||||||
|
setDetectionItems([])
|
||||||
|
setDetectionGeoJson(null)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setLoadingDetectionResults(true)
|
||||||
|
setDetectionRunError(null)
|
||||||
|
try {
|
||||||
|
const params = {
|
||||||
|
class_name: detectionClassFilter || null,
|
||||||
|
min_confidence: detectionMinConfidenceFilter > 0 ? detectionMinConfidenceFilter : null,
|
||||||
|
}
|
||||||
|
const [detectionsResponse, geoJsonResponse] = await Promise.all([
|
||||||
|
detectionApi.listDetections(analysisRunId, params),
|
||||||
|
detectionApi.getRunGeoJson(analysisRunId, params),
|
||||||
|
])
|
||||||
|
setDetectionItems(detectionsResponse.items)
|
||||||
|
setDetectionGeoJson(geoJsonResponse)
|
||||||
|
} catch (error) {
|
||||||
|
setDetectionRunError(formatError(error, 'Failed to load detection results'))
|
||||||
|
} finally {
|
||||||
|
setLoadingDetectionResults(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const runDetection = async () => {
|
||||||
|
if (!selectedProjectId) {
|
||||||
|
setDetectionRunError('Select a project first')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const datasetId = selectedDetectionDatasetId || rasterDatasets[0]?.id
|
||||||
|
if (!datasetId) {
|
||||||
|
setDetectionRunError('Select a raster dataset')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setDetectionRunError(null)
|
||||||
|
setDetectionRunResult(null)
|
||||||
|
setRunningDetection(true)
|
||||||
|
try {
|
||||||
|
const result = await detectionApi.run({
|
||||||
|
project_id: selectedProjectId,
|
||||||
|
dataset_id: datasetId,
|
||||||
|
model_id: selectedDetectionModelId,
|
||||||
|
confidence_threshold: detectionConfidenceThreshold,
|
||||||
|
tile_manifest_path: detectionTileManifestPath.trim() || null,
|
||||||
|
parameters_json: {},
|
||||||
|
})
|
||||||
|
setDetectionRunResult(result)
|
||||||
|
setSelectedDetectionRunId(result.analysis_run_id)
|
||||||
|
await loadDetectionRuns(selectedProjectId)
|
||||||
|
await loadDetectionResults(result.analysis_run_id)
|
||||||
|
await loadProjectData(selectedProjectId)
|
||||||
|
} catch (error) {
|
||||||
|
setDetectionRunError(formatError(error, 'Detection run failed'))
|
||||||
|
} finally {
|
||||||
|
setRunningDetection(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const runDetectionQa = async () => {
|
||||||
|
if (!selectedDetectionRunId) {
|
||||||
|
setDetectionQaError('Select a detection run')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!detectionReferenceDatasetId) {
|
||||||
|
setDetectionQaError('Select a reference dataset')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setDetectionQaError(null)
|
||||||
|
setDetectionQaResult(null)
|
||||||
|
setRunningDetectionQa(true)
|
||||||
|
try {
|
||||||
|
const result = await detectionApi.compareWithReference(selectedDetectionRunId, {
|
||||||
|
reference_dataset_id: detectionReferenceDatasetId,
|
||||||
|
iou_threshold: qaIouThreshold,
|
||||||
|
class_name: detectionClassFilter || null,
|
||||||
|
min_confidence: detectionMinConfidenceFilter > 0 ? detectionMinConfidenceFilter : null,
|
||||||
|
})
|
||||||
|
setDetectionQaResult(result)
|
||||||
|
await loadQualityChecks(selectedProjectId)
|
||||||
|
} catch (error) {
|
||||||
|
setDetectionQaError(formatError(error, 'Detection QA failed'))
|
||||||
|
} finally {
|
||||||
|
setRunningDetectionQa(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const resetDetectionForProject = () => {
|
||||||
|
setSelectedDetectionDatasetId('')
|
||||||
|
setDetectionRuns([])
|
||||||
|
setSelectedDetectionRunId('')
|
||||||
|
setDetectionItems([])
|
||||||
|
setDetectionGeoJson(null)
|
||||||
|
setDetectionRunResult(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
detectionModels,
|
||||||
|
loadingDetectionModels,
|
||||||
|
detectionModelError,
|
||||||
|
selectedDetectionDatasetId,
|
||||||
|
selectedDetectionModelId,
|
||||||
|
detectionTileManifestPath,
|
||||||
|
detectionConfidenceThreshold,
|
||||||
|
runningDetection,
|
||||||
|
detectionRunResult,
|
||||||
|
detectionRunError,
|
||||||
|
detectionRuns,
|
||||||
|
selectedDetectionRunId,
|
||||||
|
detectionItems,
|
||||||
|
detectionGeoJson,
|
||||||
|
detectionClassFilter,
|
||||||
|
detectionMinConfidenceFilter,
|
||||||
|
loadingDetectionResults,
|
||||||
|
detectionReferenceDatasetId,
|
||||||
|
detectionQaResult,
|
||||||
|
detectionQaError,
|
||||||
|
runningDetectionQa,
|
||||||
|
loadDetectionModels,
|
||||||
|
loadDetectionRuns,
|
||||||
|
loadDetectionResults,
|
||||||
|
runDetection,
|
||||||
|
runDetectionQa,
|
||||||
|
resetDetectionForProject,
|
||||||
|
setSelectedDetectionDatasetId,
|
||||||
|
setSelectedDetectionModelId,
|
||||||
|
setDetectionTileManifestPath,
|
||||||
|
setDetectionConfidenceThreshold,
|
||||||
|
setSelectedDetectionRunId,
|
||||||
|
setDetectionClassFilter,
|
||||||
|
setDetectionMinConfidenceFilter,
|
||||||
|
setDetectionReferenceDatasetId,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,227 @@
|
|||||||
|
import { useMemo, useState } from 'react'
|
||||||
|
import { segmentationApi } from '../services/api'
|
||||||
|
import type {
|
||||||
|
DatasetCreateResponse,
|
||||||
|
QualityCheckRead,
|
||||||
|
SegmentationModelCapability,
|
||||||
|
SegmentationQaResult,
|
||||||
|
SegmentationRead,
|
||||||
|
SegmentationRunRead,
|
||||||
|
SegmentationRunResponse,
|
||||||
|
} from '../types'
|
||||||
|
import { formatError } from '../lib/formatError'
|
||||||
|
|
||||||
|
interface SegmentationWorkflowOptions {
|
||||||
|
selectedProjectId: string | null
|
||||||
|
rasterDatasets: DatasetCreateResponse[]
|
||||||
|
qaIouThreshold: number
|
||||||
|
loadProjectData: (projectId: string) => Promise<unknown>
|
||||||
|
loadQualityChecks: (projectId?: string | null) => Promise<QualityCheckRead[] | void>
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useSegmentationWorkflow({
|
||||||
|
selectedProjectId,
|
||||||
|
rasterDatasets,
|
||||||
|
qaIouThreshold,
|
||||||
|
loadProjectData,
|
||||||
|
loadQualityChecks,
|
||||||
|
}: SegmentationWorkflowOptions) {
|
||||||
|
const [segmentationModels, setSegmentationModels] = useState<SegmentationModelCapability[]>([])
|
||||||
|
const [loadingSegmentationModels, setLoadingSegmentationModels] = useState(false)
|
||||||
|
const [segmentationModelError, setSegmentationModelError] = useState<string | null>(null)
|
||||||
|
const [selectedSegmentationDatasetId, setSelectedSegmentationDatasetId] = useState('')
|
||||||
|
const [selectedSegmentationModelId, setSelectedSegmentationModelId] = useState('segmentation-placeholder')
|
||||||
|
const [segmentationConfidenceThreshold, setSegmentationConfidenceThreshold] = useState(0.5)
|
||||||
|
const [runningSegmentation, setRunningSegmentation] = useState(false)
|
||||||
|
const [segmentationRunResult, setSegmentationRunResult] = useState<SegmentationRunResponse | null>(null)
|
||||||
|
const [segmentationRunError, setSegmentationRunError] = useState<string | null>(null)
|
||||||
|
const [segmentationRuns, setSegmentationRuns] = useState<SegmentationRunRead[]>([])
|
||||||
|
const [selectedSegmentationRunId, setSelectedSegmentationRunId] = useState('')
|
||||||
|
const [segmentationItems, setSegmentationItems] = useState<SegmentationRead[]>([])
|
||||||
|
const [segmentationGeoJson, setSegmentationGeoJson] = useState<GeoJSON.FeatureCollection | null>(null)
|
||||||
|
const [segmentationClassFilter, setSegmentationClassFilter] = useState('')
|
||||||
|
const [segmentationMinConfidenceFilter, setSegmentationMinConfidenceFilter] = useState(0)
|
||||||
|
const [loadingSegmentationResults, setLoadingSegmentationResults] = useState(false)
|
||||||
|
const [segmentationReferenceDatasetId, setSegmentationReferenceDatasetId] = useState('')
|
||||||
|
const [segmentationQaResult, setSegmentationQaResult] = useState<SegmentationQaResult | null>(null)
|
||||||
|
const [segmentationQaError, setSegmentationQaError] = useState<string | null>(null)
|
||||||
|
const [runningSegmentationQa, setRunningSegmentationQa] = useState(false)
|
||||||
|
|
||||||
|
const selectedSegmentationModel = useMemo(
|
||||||
|
() => segmentationModels.find((model) => model.model_id === selectedSegmentationModelId) ?? null,
|
||||||
|
[segmentationModels, selectedSegmentationModelId],
|
||||||
|
)
|
||||||
|
|
||||||
|
const loadSegmentationModels = async () => {
|
||||||
|
setLoadingSegmentationModels(true)
|
||||||
|
setSegmentationModelError(null)
|
||||||
|
try {
|
||||||
|
const response = await segmentationApi.listModels()
|
||||||
|
setSegmentationModels(response.models)
|
||||||
|
if (!response.models.some((model) => model.model_id === selectedSegmentationModelId) && response.models.length > 0) {
|
||||||
|
setSelectedSegmentationModelId(response.models[0].model_id)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
setSegmentationModelError(formatError(error, 'Failed to load segmentation models'))
|
||||||
|
} finally {
|
||||||
|
setLoadingSegmentationModels(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadSegmentationRuns = async (projectId = selectedProjectId) => {
|
||||||
|
if (!projectId) {
|
||||||
|
setSegmentationRuns([])
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const response = await segmentationApi.listRuns({ project_id: projectId })
|
||||||
|
setSegmentationRuns(response.items)
|
||||||
|
if (!selectedSegmentationRunId && response.items.length > 0) {
|
||||||
|
setSelectedSegmentationRunId(response.items[0].id)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
setSegmentationRunError(formatError(error, 'Failed to load segmentation runs'))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadSegmentationResults = async (analysisRunId = selectedSegmentationRunId) => {
|
||||||
|
if (!analysisRunId) {
|
||||||
|
setSegmentationItems([])
|
||||||
|
setSegmentationGeoJson(null)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setLoadingSegmentationResults(true)
|
||||||
|
setSegmentationRunError(null)
|
||||||
|
try {
|
||||||
|
const params = {
|
||||||
|
class_name: segmentationClassFilter || null,
|
||||||
|
min_confidence: segmentationMinConfidenceFilter > 0 ? segmentationMinConfidenceFilter : null,
|
||||||
|
}
|
||||||
|
const [segmentationsResponse, geoJsonResponse] = await Promise.all([
|
||||||
|
segmentationApi.listSegmentations(analysisRunId, params),
|
||||||
|
segmentationApi.getRunGeoJson(analysisRunId, params),
|
||||||
|
])
|
||||||
|
setSegmentationItems(segmentationsResponse.items)
|
||||||
|
setSegmentationGeoJson(geoJsonResponse)
|
||||||
|
} catch (error) {
|
||||||
|
setSegmentationRunError(formatError(error, 'Failed to load segmentation results'))
|
||||||
|
} finally {
|
||||||
|
setLoadingSegmentationResults(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const runSegmentation = async () => {
|
||||||
|
if (!selectedProjectId) {
|
||||||
|
setSegmentationRunError('Select a project first')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const datasetId = selectedSegmentationDatasetId || rasterDatasets[0]?.id
|
||||||
|
if (!datasetId) {
|
||||||
|
setSegmentationRunError('Select a raster dataset')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!selectedSegmentationModel?.configured) {
|
||||||
|
setSegmentationRunError('Selected segmentation model is not configured')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setSegmentationRunError(null)
|
||||||
|
setSegmentationRunResult(null)
|
||||||
|
setRunningSegmentation(true)
|
||||||
|
try {
|
||||||
|
const parameters =
|
||||||
|
selectedSegmentationModelId === 'fixture-segmenter'
|
||||||
|
? { fixture_mode: true, fixture_segmentations: [] }
|
||||||
|
: {}
|
||||||
|
const result = await segmentationApi.run({
|
||||||
|
project_id: selectedProjectId,
|
||||||
|
dataset_id: datasetId,
|
||||||
|
model_id: selectedSegmentationModelId,
|
||||||
|
confidence_threshold: segmentationConfidenceThreshold,
|
||||||
|
parameters_json: parameters,
|
||||||
|
})
|
||||||
|
setSegmentationRunResult(result)
|
||||||
|
setSelectedSegmentationRunId(result.analysis_run_id)
|
||||||
|
await loadSegmentationRuns(selectedProjectId)
|
||||||
|
await loadSegmentationResults(result.analysis_run_id)
|
||||||
|
await loadProjectData(selectedProjectId)
|
||||||
|
} catch (error) {
|
||||||
|
setSegmentationRunError(formatError(error, 'Segmentation run failed'))
|
||||||
|
} finally {
|
||||||
|
setRunningSegmentation(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const runSegmentationQa = async () => {
|
||||||
|
if (!selectedSegmentationRunId) {
|
||||||
|
setSegmentationQaError('Select a segmentation run')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!segmentationReferenceDatasetId) {
|
||||||
|
setSegmentationQaError('Select a reference dataset')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setSegmentationQaError(null)
|
||||||
|
setSegmentationQaResult(null)
|
||||||
|
setRunningSegmentationQa(true)
|
||||||
|
try {
|
||||||
|
const result = await segmentationApi.compareWithReference(selectedSegmentationRunId, {
|
||||||
|
reference_dataset_id: segmentationReferenceDatasetId,
|
||||||
|
iou_threshold: qaIouThreshold,
|
||||||
|
class_name: segmentationClassFilter || null,
|
||||||
|
min_confidence: segmentationMinConfidenceFilter > 0 ? segmentationMinConfidenceFilter : null,
|
||||||
|
})
|
||||||
|
setSegmentationQaResult(result)
|
||||||
|
await loadQualityChecks(selectedProjectId)
|
||||||
|
} catch (error) {
|
||||||
|
setSegmentationQaError(formatError(error, 'Segmentation QA failed'))
|
||||||
|
} finally {
|
||||||
|
setRunningSegmentationQa(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const resetSegmentationForProject = () => {
|
||||||
|
setSelectedSegmentationDatasetId('')
|
||||||
|
setSegmentationRuns([])
|
||||||
|
setSelectedSegmentationRunId('')
|
||||||
|
setSegmentationItems([])
|
||||||
|
setSegmentationGeoJson(null)
|
||||||
|
setSegmentationRunResult(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
segmentationModels,
|
||||||
|
loadingSegmentationModels,
|
||||||
|
segmentationModelError,
|
||||||
|
selectedSegmentationDatasetId,
|
||||||
|
selectedSegmentationModelId,
|
||||||
|
selectedSegmentationModel,
|
||||||
|
segmentationConfidenceThreshold,
|
||||||
|
runningSegmentation,
|
||||||
|
segmentationRunResult,
|
||||||
|
segmentationRunError,
|
||||||
|
segmentationRuns,
|
||||||
|
selectedSegmentationRunId,
|
||||||
|
segmentationItems,
|
||||||
|
segmentationGeoJson,
|
||||||
|
segmentationClassFilter,
|
||||||
|
segmentationMinConfidenceFilter,
|
||||||
|
loadingSegmentationResults,
|
||||||
|
segmentationReferenceDatasetId,
|
||||||
|
segmentationQaResult,
|
||||||
|
segmentationQaError,
|
||||||
|
runningSegmentationQa,
|
||||||
|
loadSegmentationModels,
|
||||||
|
loadSegmentationRuns,
|
||||||
|
loadSegmentationResults,
|
||||||
|
runSegmentation,
|
||||||
|
runSegmentationQa,
|
||||||
|
resetSegmentationForProject,
|
||||||
|
setSelectedSegmentationDatasetId,
|
||||||
|
setSelectedSegmentationModelId,
|
||||||
|
setSegmentationConfidenceThreshold,
|
||||||
|
setSelectedSegmentationRunId,
|
||||||
|
setSegmentationClassFilter,
|
||||||
|
setSegmentationMinConfidenceFilter,
|
||||||
|
setSegmentationReferenceDatasetId,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
export function formatError(error: unknown, fallback: string): string {
|
||||||
|
if (error instanceof Error) {
|
||||||
|
const code = (error as { code?: string }).code
|
||||||
|
return code ? `${error.message} (${code})` : error.message
|
||||||
|
}
|
||||||
|
return fallback
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user