Extract frontend orchestration hooks
This commit is contained in:
+54
-129
@@ -3,7 +3,7 @@ import './styles/app.css'
|
||||
import { areasApi } from './services/api/areas'
|
||||
import { datasetsApi } from './services/api/datasets'
|
||||
import { projectsApi } from './services/api/projects'
|
||||
import { analysisApi, demoApi, externalApi } from './services/api'
|
||||
import { demoApi } from './services/api'
|
||||
import { ChangeDetectionPanel } from './components/analysis/ChangeDetectionPanel'
|
||||
import { DatasetDetailPanel } from './components/datasets/DatasetDetailPanel'
|
||||
import { DatasetPanel } from './components/datasets/DatasetPanel'
|
||||
@@ -16,8 +16,6 @@ import { ProjectPanel } from './components/project/ProjectPanel'
|
||||
import { QualityResultsPanel } from './components/quality/QualityResultsPanel'
|
||||
import { WorkbenchStatusStrip } from './components/WorkbenchStatusStrip'
|
||||
import type {
|
||||
ApiError,
|
||||
ChangeDetectionSummary,
|
||||
DatasetCreateResponse,
|
||||
DatasetListResponse,
|
||||
ProjectRead,
|
||||
@@ -25,13 +23,15 @@ import type {
|
||||
AreaCreate,
|
||||
AreaListResponse,
|
||||
AreaRead,
|
||||
ProviderCapability,
|
||||
} from './types'
|
||||
import { ProviderPanel } from './components/providers/ProviderPanel'
|
||||
import { SegmentationLab } from './components/segmentation/SegmentationLab'
|
||||
import { useChangeDetectionWorkflow } from './hooks/useChangeDetectionWorkflow'
|
||||
import { useDetectionWorkflow } from './hooks/useDetectionWorkflow'
|
||||
import { useDatasetWorkflow } from './hooks/useDatasetWorkflow'
|
||||
import { useExportWorkflow } from './hooks/useExportWorkflow'
|
||||
import { useMapWorkspaceState } from './hooks/useMapWorkspaceState'
|
||||
import { useProviderCapabilities } from './hooks/useProviderCapabilities'
|
||||
import { useQualityWorkflow } from './hooks/useQualityWorkflow'
|
||||
import { useSegmentationWorkflow } from './hooks/useSegmentationWorkflow'
|
||||
import { formatError } from './lib/formatError'
|
||||
@@ -45,22 +45,6 @@ function App(): JSX.Element {
|
||||
const [selectedProjectId, setSelectedProjectId] = useState<string | null>(null)
|
||||
const [areas, setAreas] = useState<AreaRead[]>([])
|
||||
const [datasets, setDatasets] = useState<DatasetCreateResponse[]>([])
|
||||
const [mapLayerVisible, setMapLayerVisible] = useState(true)
|
||||
const [mapLayerOpacity, setMapLayerOpacity] = useState(0.4)
|
||||
const [selectedMapAreaId, setSelectedMapAreaId] = useState('')
|
||||
const [areaLayerVisible, setAreaLayerVisible] = useState(true)
|
||||
const [areaLayerOpacity, setAreaLayerOpacity] = useState(0.18)
|
||||
const [selectedMapFeature, setSelectedMapFeature] = useState<GeoJSON.Feature | null>(null)
|
||||
const [providerCapabilities, setProviderCapabilities] = useState<ProviderCapability[]>([])
|
||||
const [loadingCapabilities, setLoadingCapabilities] = useState(false)
|
||||
const [capabilitiesError, setCapabilitiesError] = useState<string | null>(null)
|
||||
const [changeSourceDatasetId, setChangeSourceDatasetId] = useState('')
|
||||
const [changeTargetDatasetId, setChangeTargetDatasetId] = useState('')
|
||||
const [changeIouThreshold, setChangeIouThreshold] = useState(0.8)
|
||||
const [changeIncludeUnchanged, setChangeIncludeUnchanged] = useState(true)
|
||||
const [runningChangeDetection, setRunningChangeDetection] = useState(false)
|
||||
const [changeDetectionResult, setChangeDetectionResult] = useState<ChangeDetectionSummary | null>(null)
|
||||
const [changeDetectionError, setChangeDetectionError] = useState<string | null>(null)
|
||||
const [loadingProjects, setLoadingProjects] = useState(false)
|
||||
const [loadingDemoWorkflow, setLoadingDemoWorkflow] = useState(false)
|
||||
const [demoWorkflowMessage, setDemoWorkflowMessage] = useState<string | null>(null)
|
||||
@@ -171,8 +155,31 @@ function App(): JSX.Element {
|
||||
[availableVectorDatasets],
|
||||
)
|
||||
const candidateDatasets = availableVectorDatasets
|
||||
const providers = useMemo(() => providerCapabilities, [providerCapabilities])
|
||||
const rasterDatasets = useMemo(() => datasets.filter((item) => item.dataset_type === 'raster'), [datasets])
|
||||
const {
|
||||
providers,
|
||||
loadingCapabilities,
|
||||
capabilitiesError,
|
||||
loadCapabilities,
|
||||
} = useProviderCapabilities()
|
||||
const {
|
||||
changeSourceDatasetId,
|
||||
changeTargetDatasetId,
|
||||
changeIouThreshold,
|
||||
changeIncludeUnchanged,
|
||||
runningChangeDetection,
|
||||
changeDetectionResult,
|
||||
changeDetectionError,
|
||||
runChangeDetection,
|
||||
setChangeSourceDatasetId,
|
||||
setChangeTargetDatasetId,
|
||||
setChangeIouThreshold,
|
||||
setChangeIncludeUnchanged,
|
||||
} = useChangeDetectionWorkflow({
|
||||
selectedProjectId,
|
||||
availableVectorDatasets,
|
||||
loadDatasetJobs,
|
||||
})
|
||||
const {
|
||||
qaCandidateDatasetId,
|
||||
qaReferenceDatasetId,
|
||||
@@ -301,52 +308,32 @@ function App(): JSX.Element {
|
||||
selectedSegmentationRunId,
|
||||
isVectorDatasetType,
|
||||
})
|
||||
const selectedMapArea = useMemo(
|
||||
() => areas.find((area) => area.id === selectedMapAreaId) ?? null,
|
||||
[areas, selectedMapAreaId],
|
||||
)
|
||||
const areaFeatureCollection = useMemo<GeoJSON.FeatureCollection | null>(() => {
|
||||
if (!selectedMapArea?.geometry) {
|
||||
return null
|
||||
}
|
||||
return {
|
||||
type: 'FeatureCollection',
|
||||
features: [
|
||||
{
|
||||
type: 'Feature',
|
||||
geometry: selectedMapArea.geometry,
|
||||
properties: {
|
||||
layer_type: 'project_area',
|
||||
area_id: selectedMapArea.id,
|
||||
name: selectedMapArea.name,
|
||||
area_m2: selectedMapArea.area_m2 ?? null,
|
||||
original_crs: selectedMapArea.original_crs ?? null,
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
}, [selectedMapArea])
|
||||
const mapFeatureCollection = useMemo(
|
||||
() => changeDetectionResult?.geojson ?? segmentationGeoJson ?? detectionGeoJson ?? datasetContent,
|
||||
[changeDetectionResult, segmentationGeoJson, detectionGeoJson, datasetContent],
|
||||
)
|
||||
const mapLayerLabel = useMemo(() => {
|
||||
if (changeDetectionResult) {
|
||||
return 'Change detection result'
|
||||
}
|
||||
if (segmentationGeoJson) {
|
||||
return 'Segmentation result'
|
||||
}
|
||||
if (detectionGeoJson) {
|
||||
return 'Detection result'
|
||||
}
|
||||
if (datasetContent && selectedDataset) {
|
||||
return selectedDataset.name
|
||||
}
|
||||
return 'No active vector layer'
|
||||
}, [changeDetectionResult, datasetContent, detectionGeoJson, segmentationGeoJson, selectedDataset])
|
||||
const mapFeatureCount = mapFeatureCollection?.features.length ?? 0
|
||||
const areaFeatureCount = areaFeatureCollection?.features.length ?? 0
|
||||
const {
|
||||
mapLayerVisible,
|
||||
mapLayerOpacity,
|
||||
selectedMapAreaId,
|
||||
areaLayerVisible,
|
||||
areaLayerOpacity,
|
||||
selectedMapFeature,
|
||||
areaFeatureCollection,
|
||||
mapFeatureCollection,
|
||||
mapLayerLabel,
|
||||
mapFeatureCount,
|
||||
areaFeatureCount,
|
||||
setMapLayerVisible,
|
||||
setMapLayerOpacity,
|
||||
setSelectedMapAreaId,
|
||||
setAreaLayerVisible,
|
||||
setAreaLayerOpacity,
|
||||
setSelectedMapFeature,
|
||||
} = useMapWorkspaceState({
|
||||
areas,
|
||||
changeDetectionGeoJson: changeDetectionResult?.geojson ?? null,
|
||||
segmentationGeoJson,
|
||||
detectionGeoJson,
|
||||
datasetContent,
|
||||
selectedDataset,
|
||||
})
|
||||
const loadProjects = async () => {
|
||||
setLoadingProjects(true)
|
||||
setErrorMessage(null)
|
||||
@@ -394,19 +381,6 @@ function App(): JSX.Element {
|
||||
}
|
||||
}
|
||||
|
||||
const loadCapabilities = async () => {
|
||||
setLoadingCapabilities(true)
|
||||
setCapabilitiesError(null)
|
||||
try {
|
||||
const providerResponse = await externalApi.listProviders()
|
||||
setProviderCapabilities(providerResponse.providers)
|
||||
} catch (error) {
|
||||
setCapabilitiesError(error instanceof Error ? error.message : 'Failed to load external capabilities')
|
||||
} finally {
|
||||
setLoadingCapabilities(false)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
loadProjects().catch(() => null)
|
||||
loadCapabilities().catch(() => null)
|
||||
@@ -414,10 +388,6 @@ function App(): JSX.Element {
|
||||
loadSegmentationModels().catch(() => null)
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedMapFeature(null)
|
||||
}, [mapFeatureCollection, areaFeatureCollection])
|
||||
|
||||
useEffect(() => {
|
||||
if (!selectedProjectId) {
|
||||
setAreas([])
|
||||
@@ -524,51 +494,6 @@ function App(): JSX.Element {
|
||||
}
|
||||
}
|
||||
|
||||
const runChangeDetection = async () => {
|
||||
const sourceDatasetId = changeSourceDatasetId || availableVectorDatasets[0]?.id
|
||||
const targetDatasetId =
|
||||
changeTargetDatasetId || availableVectorDatasets.find((dataset) => dataset.id !== sourceDatasetId)?.id
|
||||
if (!sourceDatasetId || !targetDatasetId) {
|
||||
setChangeDetectionError('Select two vector datasets')
|
||||
return
|
||||
}
|
||||
if (sourceDatasetId === targetDatasetId) {
|
||||
setChangeDetectionError('Source and target datasets must differ')
|
||||
return
|
||||
}
|
||||
if (changeIouThreshold < 0 || changeIouThreshold > 1) {
|
||||
setChangeDetectionError('IoU threshold must be between 0 and 1')
|
||||
return
|
||||
}
|
||||
setChangeDetectionError(null)
|
||||
setChangeDetectionResult(null)
|
||||
setRunningChangeDetection(true)
|
||||
try {
|
||||
const job = await analysisApi.runChangeDetection({
|
||||
source_dataset_id: sourceDatasetId,
|
||||
target_dataset_id: targetDatasetId,
|
||||
iou_threshold: changeIouThreshold,
|
||||
include_unchanged: changeIncludeUnchanged,
|
||||
})
|
||||
if (job.status !== 'success') {
|
||||
throw new Error(job.error_message || 'Change detection job failed')
|
||||
}
|
||||
if (!job.result_json) {
|
||||
throw new Error('Change detection completed without result payload')
|
||||
}
|
||||
setChangeSourceDatasetId(sourceDatasetId)
|
||||
setChangeTargetDatasetId(targetDatasetId)
|
||||
setChangeDetectionResult(job.result_json)
|
||||
if (selectedProjectId) {
|
||||
await loadDatasetJobs(selectedProjectId, sourceDatasetId)
|
||||
}
|
||||
} catch (error) {
|
||||
setChangeDetectionError(formatError(error, 'Change detection failed'))
|
||||
} finally {
|
||||
setRunningChangeDetection(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="app-shell">
|
||||
<header>
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
import { useState } from 'react'
|
||||
import { analysisApi } from '../services/api'
|
||||
import type { ChangeDetectionSummary, DatasetCreateResponse } from '../types'
|
||||
import { formatError } from '../lib/formatError'
|
||||
|
||||
interface ChangeDetectionWorkflowOptions {
|
||||
selectedProjectId: string | null
|
||||
availableVectorDatasets: DatasetCreateResponse[]
|
||||
loadDatasetJobs: (projectId: string, datasetId: string) => Promise<void>
|
||||
}
|
||||
|
||||
export function useChangeDetectionWorkflow({
|
||||
selectedProjectId,
|
||||
availableVectorDatasets,
|
||||
loadDatasetJobs,
|
||||
}: ChangeDetectionWorkflowOptions) {
|
||||
const [changeSourceDatasetId, setChangeSourceDatasetId] = useState('')
|
||||
const [changeTargetDatasetId, setChangeTargetDatasetId] = useState('')
|
||||
const [changeIouThreshold, setChangeIouThreshold] = useState(0.8)
|
||||
const [changeIncludeUnchanged, setChangeIncludeUnchanged] = useState(true)
|
||||
const [runningChangeDetection, setRunningChangeDetection] = useState(false)
|
||||
const [changeDetectionResult, setChangeDetectionResult] = useState<ChangeDetectionSummary | null>(null)
|
||||
const [changeDetectionError, setChangeDetectionError] = useState<string | null>(null)
|
||||
|
||||
const runChangeDetection = async () => {
|
||||
const sourceDatasetId = changeSourceDatasetId || availableVectorDatasets[0]?.id
|
||||
const targetDatasetId =
|
||||
changeTargetDatasetId || availableVectorDatasets.find((dataset) => dataset.id !== sourceDatasetId)?.id
|
||||
if (!sourceDatasetId || !targetDatasetId) {
|
||||
setChangeDetectionError('Select two vector datasets')
|
||||
return
|
||||
}
|
||||
if (sourceDatasetId === targetDatasetId) {
|
||||
setChangeDetectionError('Source and target datasets must differ')
|
||||
return
|
||||
}
|
||||
if (changeIouThreshold < 0 || changeIouThreshold > 1) {
|
||||
setChangeDetectionError('IoU threshold must be between 0 and 1')
|
||||
return
|
||||
}
|
||||
setChangeDetectionError(null)
|
||||
setChangeDetectionResult(null)
|
||||
setRunningChangeDetection(true)
|
||||
try {
|
||||
const job = await analysisApi.runChangeDetection({
|
||||
source_dataset_id: sourceDatasetId,
|
||||
target_dataset_id: targetDatasetId,
|
||||
iou_threshold: changeIouThreshold,
|
||||
include_unchanged: changeIncludeUnchanged,
|
||||
})
|
||||
if (job.status !== 'success') {
|
||||
throw new Error(job.error_message || 'Change detection job failed')
|
||||
}
|
||||
if (!job.result_json) {
|
||||
throw new Error('Change detection completed without result payload')
|
||||
}
|
||||
setChangeSourceDatasetId(sourceDatasetId)
|
||||
setChangeTargetDatasetId(targetDatasetId)
|
||||
setChangeDetectionResult(job.result_json)
|
||||
if (selectedProjectId) {
|
||||
await loadDatasetJobs(selectedProjectId, sourceDatasetId)
|
||||
}
|
||||
} catch (error) {
|
||||
setChangeDetectionError(formatError(error, 'Change detection failed'))
|
||||
} finally {
|
||||
setRunningChangeDetection(false)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
changeSourceDatasetId,
|
||||
changeTargetDatasetId,
|
||||
changeIouThreshold,
|
||||
changeIncludeUnchanged,
|
||||
runningChangeDetection,
|
||||
changeDetectionResult,
|
||||
changeDetectionError,
|
||||
runChangeDetection,
|
||||
setChangeSourceDatasetId,
|
||||
setChangeTargetDatasetId,
|
||||
setChangeIouThreshold,
|
||||
setChangeIncludeUnchanged,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import type { AreaRead, DatasetCreateResponse } from '../types'
|
||||
|
||||
interface MapWorkspaceStateOptions {
|
||||
areas: AreaRead[]
|
||||
changeDetectionGeoJson: GeoJSON.FeatureCollection | null
|
||||
segmentationGeoJson: GeoJSON.FeatureCollection | null
|
||||
detectionGeoJson: GeoJSON.FeatureCollection | null
|
||||
datasetContent: GeoJSON.FeatureCollection | null
|
||||
selectedDataset: DatasetCreateResponse | null
|
||||
}
|
||||
|
||||
export function useMapWorkspaceState({
|
||||
areas,
|
||||
changeDetectionGeoJson,
|
||||
segmentationGeoJson,
|
||||
detectionGeoJson,
|
||||
datasetContent,
|
||||
selectedDataset,
|
||||
}: MapWorkspaceStateOptions) {
|
||||
const [mapLayerVisible, setMapLayerVisible] = useState(true)
|
||||
const [mapLayerOpacity, setMapLayerOpacity] = useState(0.4)
|
||||
const [selectedMapAreaId, setSelectedMapAreaId] = useState('')
|
||||
const [areaLayerVisible, setAreaLayerVisible] = useState(true)
|
||||
const [areaLayerOpacity, setAreaLayerOpacity] = useState(0.18)
|
||||
const [selectedMapFeature, setSelectedMapFeature] = useState<GeoJSON.Feature | null>(null)
|
||||
|
||||
const selectedMapArea = useMemo(
|
||||
() => areas.find((area) => area.id === selectedMapAreaId) ?? null,
|
||||
[areas, selectedMapAreaId],
|
||||
)
|
||||
const areaFeatureCollection = useMemo<GeoJSON.FeatureCollection | null>(() => {
|
||||
if (!selectedMapArea?.geometry) {
|
||||
return null
|
||||
}
|
||||
return {
|
||||
type: 'FeatureCollection',
|
||||
features: [
|
||||
{
|
||||
type: 'Feature',
|
||||
geometry: selectedMapArea.geometry,
|
||||
properties: {
|
||||
layer_type: 'project_area',
|
||||
area_id: selectedMapArea.id,
|
||||
name: selectedMapArea.name,
|
||||
area_m2: selectedMapArea.area_m2 ?? null,
|
||||
original_crs: selectedMapArea.original_crs ?? null,
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
}, [selectedMapArea])
|
||||
const mapFeatureCollection = useMemo(
|
||||
() => changeDetectionGeoJson ?? segmentationGeoJson ?? detectionGeoJson ?? datasetContent,
|
||||
[changeDetectionGeoJson, segmentationGeoJson, detectionGeoJson, datasetContent],
|
||||
)
|
||||
const mapLayerLabel = useMemo(() => {
|
||||
if (changeDetectionGeoJson) {
|
||||
return 'Change detection result'
|
||||
}
|
||||
if (segmentationGeoJson) {
|
||||
return 'Segmentation result'
|
||||
}
|
||||
if (detectionGeoJson) {
|
||||
return 'Detection result'
|
||||
}
|
||||
if (datasetContent && selectedDataset) {
|
||||
return selectedDataset.name
|
||||
}
|
||||
return 'No active vector layer'
|
||||
}, [changeDetectionGeoJson, datasetContent, detectionGeoJson, segmentationGeoJson, selectedDataset])
|
||||
const mapFeatureCount = mapFeatureCollection?.features.length ?? 0
|
||||
const areaFeatureCount = areaFeatureCollection?.features.length ?? 0
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedMapFeature(null)
|
||||
}, [mapFeatureCollection, areaFeatureCollection])
|
||||
|
||||
return {
|
||||
mapLayerVisible,
|
||||
mapLayerOpacity,
|
||||
selectedMapAreaId,
|
||||
areaLayerVisible,
|
||||
areaLayerOpacity,
|
||||
selectedMapFeature,
|
||||
areaFeatureCollection,
|
||||
mapFeatureCollection,
|
||||
mapLayerLabel,
|
||||
mapFeatureCount,
|
||||
areaFeatureCount,
|
||||
setMapLayerVisible,
|
||||
setMapLayerOpacity,
|
||||
setSelectedMapAreaId,
|
||||
setAreaLayerVisible,
|
||||
setAreaLayerOpacity,
|
||||
setSelectedMapFeature,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import { useMemo, useState } from 'react'
|
||||
import { externalApi } from '../services/api'
|
||||
import type { ProviderCapability } from '../types'
|
||||
|
||||
export function useProviderCapabilities() {
|
||||
const [providerCapabilities, setProviderCapabilities] = useState<ProviderCapability[]>([])
|
||||
const [loadingCapabilities, setLoadingCapabilities] = useState(false)
|
||||
const [capabilitiesError, setCapabilitiesError] = useState<string | null>(null)
|
||||
|
||||
const providers = useMemo(() => providerCapabilities, [providerCapabilities])
|
||||
|
||||
const loadCapabilities = async () => {
|
||||
setLoadingCapabilities(true)
|
||||
setCapabilitiesError(null)
|
||||
try {
|
||||
const providerResponse = await externalApi.listProviders()
|
||||
setProviderCapabilities(providerResponse.providers)
|
||||
} catch (error) {
|
||||
setCapabilitiesError(error instanceof Error ? error.message : 'Failed to load external capabilities')
|
||||
} finally {
|
||||
setLoadingCapabilities(false)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
providers,
|
||||
loadingCapabilities,
|
||||
capabilitiesError,
|
||||
loadCapabilities,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user