Extract detection and segmentation workflow hooks
This commit is contained in:
@@ -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
@@ -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
|
||||
|
||||
@@ -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