Extract detection and segmentation workflow hooks
GeoIntel CI / docs-smoke (push) Has been cancelled
GeoIntel CI / contract-smoke (push) Has been cancelled

This commit is contained in:
Codex
2026-06-17 02:13:57 +02:00
parent 019dc8da7a
commit 6c32f29245
9 changed files with 630 additions and 326 deletions
+7
View File
@@ -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.
- 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
- Production builds split application code, React vendor code and MapLibre vendor code into separate chunks.
+94 -325
View File
@@ -4,7 +4,7 @@ import GeoMap from './components/GeoMap'
import { areasApi } from './services/api/areas'
import { datasetsApi } from './services/api/datasets'
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 { DetectionLab } from './components/detection/DetectionLab'
import { ExportCenter } from './components/exports/ExportCenter'
@@ -16,11 +16,6 @@ import type {
ChangeDetectionSummary,
DatasetCreateResponse,
DatasetListResponse,
DetectionQaResult,
DetectionRead,
DetectionModelCapability,
DetectionRunRead,
DetectionRunResponse,
JobRead,
QaComparisonRequest,
RasterMetadataResponse,
@@ -38,14 +33,12 @@ import type {
AreaRead,
ProviderCapability,
QaComparisonResult,
SegmentationModelCapability,
SegmentationQaResult,
SegmentationRead,
SegmentationRunRead,
SegmentationRunResponse,
} from './types'
import { ProviderPanel } from './components/providers/ProviderPanel'
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 {
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)}`
}
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 {
const [projects, setProjects] = useState<ProjectRead[]>([])
const [selectedProjectId, setSelectedProjectId] = useState<string | null>(null)
@@ -112,47 +97,6 @@ function App(): JSX.Element {
const [runningChangeDetection, setRunningChangeDetection] = useState(false)
const [changeDetectionResult, setChangeDetectionResult] = useState<ChangeDetectionSummary | 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 [selectedIntersectTargetId, setSelectedIntersectTargetId] = useState('')
const [selectedClipAreaId, setSelectedClipAreaId] = useState('')
@@ -243,10 +187,91 @@ function App(): JSX.Element {
const candidateDatasets = availableVectorDatasets
const providers = useMemo(() => providerCapabilities, [providerCapabilities])
const rasterDatasets = useMemo(() => datasets.filter((item) => item.dataset_type === 'raster'), [datasets])
const selectedSegmentationModel = useMemo(
() => segmentationModels.find((model) => model.model_id === selectedSegmentationModelId) ?? null,
[segmentationModels, selectedSegmentationModelId],
)
const {
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,
} = 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(
() => areas.find((area) => area.id === selectedMapAreaId) ?? null,
[areas, selectedMapAreaId],
@@ -329,7 +354,7 @@ function App(): JSX.Element {
}
}
const loadProjectData = async (projectId: string) => {
async function loadProjectData(projectId: string) {
setLoadingAreas(true)
setLoadingDatasets(true)
setErrorMessage(null)
@@ -373,123 +398,7 @@ function App(): JSX.Element {
}
}
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 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) => {
async function loadQualityChecks(projectId = selectedProjectId) {
if (!projectId) {
setQualityChecks([])
return
@@ -498,6 +407,7 @@ function App(): JSX.Element {
try {
const response = await qaApi.listQualityChecks(projectId)
setQualityChecks(response.items)
return response.items
} catch (error) {
setQualityChecksError(formatError(error, 'Failed to load QA/QC results'))
}
@@ -697,18 +607,8 @@ function App(): JSX.Element {
setSelectedRasterMetadata(null)
setSelectedRasterStats(null)
setJobs([])
setSelectedDetectionDatasetId('')
setDetectionRuns([])
setSelectedDetectionRunId('')
setDetectionItems([])
setDetectionGeoJson(null)
setDetectionRunResult(null)
setSelectedSegmentationDatasetId('')
setSegmentationRuns([])
setSelectedSegmentationRunId('')
setSegmentationItems([])
setSegmentationGeoJson(null)
setSegmentationRunResult(null)
resetDetectionForProject()
resetSegmentationForProject()
setExports([])
setLatestExport(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 sourceDatasetId = changeSourceDatasetId || availableVectorDatasets[0]?.id
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) => {
if (!selectedProjectId) {
return
+217
View File
@@ -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,
}
}
+7
View File
@@ -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
}