Add one-click GIS workflow run
GeoIntel CI / docs-smoke (push) Has been cancelled
GeoIntel CI / contract-smoke (push) Has been cancelled

This commit is contained in:
Codex
2026-07-05 00:11:03 +02:00
parent a9c108a4d4
commit 533135885c
12 changed files with 140 additions and 25 deletions
+67 -6
View File
@@ -218,12 +218,12 @@ interface MapWorkspaceProps {
onSetMapLayerOpacity: (opacity: number) => void
onSelectMapFeature: (feature: GeoJSON.Feature | null) => void
onSetMapSelectionBbox: (bbox: VectorSelectionBBox | null) => void
onRunMapSelectionExtract: (bbox: VectorSelectionBBox) => void
onRunMapSelectionExtract: (bbox: VectorSelectionBBox) => Promise<VectorSelectionResponse | null>
onClearMapSelectionExtract: () => void
onExportMapSelection: (bbox: VectorSelectionBBox) => void
onDeriveMapSelectionDataset: (bbox: VectorSelectionBBox) => void
onExportMapSelection: (bbox: VectorSelectionBBox) => Promise<unknown>
onDeriveMapSelectionDataset: (bbox: VectorSelectionBBox) => Promise<DatasetCreateResponse | null>
onSelectMapQaReferenceDataset: (datasetId: string) => void
onRunMapSelectionQa: () => void
onRunMapSelectionQa: (candidateDataset?: DatasetCreateResponse | null) => Promise<QaComparisonResult | null>
onOpenMapSelectionQualityEvidence: () => void
onClearQualityEvidence?: () => void
}
@@ -287,6 +287,9 @@ export function MapWorkspace({
const [bboxSelectionMode, setBboxSelectionMode] = useState(false)
const [firstSelectionCorner, setFirstSelectionCorner] = useState<[number, number] | null>(null)
const [bboxInput, setBboxInput] = useState(bboxToInputState(mapSelectionBbox))
const [fullWorkflowRunning, setFullWorkflowRunning] = useState(false)
const [fullWorkflowStatus, setFullWorkflowStatus] = useState('Ready to run persisted GIS workflow.')
const [fullWorkflowError, setFullWorkflowError] = useState<string | null>(null)
const selectedMapArea = areas.find((area) => area.id === selectedMapAreaId)
const featureProperties = selectedMapFeature?.properties ?? null
const featureSummaryEntries = featureProperties
@@ -404,6 +407,51 @@ export function MapWorkspace({
onRunMapSelectionExtract(bbox)
}
const runFullGisWorkflow = async () => {
const bbox = currentSelectionBbox ?? selectedAreaBbox ?? activeLayerBbox
if (!selectedMapDataset || !bbox) {
setFullWorkflowError('Select a database layer and AOI/layer extent before running the full workflow.')
return
}
setFullWorkflowRunning(true)
setFullWorkflowError(null)
try {
setFullWorkflowStatus('1/4 Querying persisted vector_features...')
setSelectionBbox(bbox)
const selection = await onRunMapSelectionExtract(bbox)
if (!selection) {
setFullWorkflowError('Persisted vector query did not complete.')
setFullWorkflowStatus('Stopped at query.')
return
}
setFullWorkflowStatus('2/4 Saving derived result dataset...')
const derived = await onDeriveMapSelectionDataset(bbox)
if (!derived) {
setFullWorkflowError('Derived result dataset was not created.')
setFullWorkflowStatus('Stopped at dataset save.')
return
}
setFullWorkflowStatus('3/4 Saving GeoJSON export artifact...')
await onExportMapSelection(bbox)
if (selectedMapQaReferenceDatasetId) {
setFullWorkflowStatus('4/4 Running QA/QC against selected reference...')
const qaResult = await onRunMapSelectionQa(derived)
setFullWorkflowStatus(qaResult ? 'Full GIS workflow complete with QA/QC result.' : 'Dataset/export complete; QA/QC did not complete.')
} else {
setFullWorkflowStatus('Dataset/export complete. Select a reference dataset to add QA/QC.')
}
} catch (error) {
setFullWorkflowError(error instanceof Error ? error.message : 'Full GIS workflow failed.')
setFullWorkflowStatus('Workflow stopped.')
} finally {
setFullWorkflowRunning(false)
}
}
return (
<section className="map-workspace-shell" data-testid="map-workspace">
<div className="panel-title-row">
@@ -705,6 +753,18 @@ export function MapWorkspace({
</div>
</div>
<div className="guided-gis-actions">
<button
className="primary-action guided-gis-full-run"
disabled={!selectedMapDataset || !mapFeatureCollection || (!currentSelectionBbox && !selectedAreaBbox && !activeLayerBbox) || fullWorkflowRunning}
type="button"
onClick={runFullGisWorkflow}
>
{fullWorkflowRunning ? 'Running full workflow...' : 'Run full GIS workflow'}
</button>
<div className="guided-gis-batch-status" aria-live="polite">
<strong>Query, save, QA and export</strong>
<span>{fullWorkflowStatus}</span>
</div>
<button
className="secondary-action"
disabled={!mapSelectionResult || !currentSelectionBbox || selectionDatasetSaving}
@@ -740,7 +800,7 @@ export function MapWorkspace({
className="primary-action"
disabled={!latestSelectionDatasetName || !selectedMapQaReferenceDatasetId || mapSelectionQaRunning}
type="button"
onClick={onRunMapSelectionQa}
onClick={() => onRunMapSelectionQa()}
>
{mapSelectionQaRunning ? 'Running QA...' : 'Run QA/QC'}
</button>
@@ -756,6 +816,7 @@ export function MapWorkspace({
{selectionDatasetError ? <p className="error">{selectionDatasetError}</p> : null}
{selectionExportError ? <p className="error">{selectionExportError}</p> : null}
{mapSelectionQaError ? <p className="error">{mapSelectionQaError}</p> : null}
{fullWorkflowError ? <p className="error">{fullWorkflowError}</p> : null}
</div>
</div>
<div className="bbox-select-surface" aria-label="Area selection and extract">
@@ -915,7 +976,7 @@ export function MapWorkspace({
className="primary-action"
disabled={!selectedMapQaReferenceDatasetId || mapSelectionQaRunning}
type="button"
onClick={onRunMapSelectionQa}
onClick={() => onRunMapSelectionQa()}
>
{mapSelectionQaRunning ? 'Running QA...' : 'Run QA on saved dataset'}
</button>
+3 -1
View File
@@ -112,7 +112,7 @@ export function useExportWorkflow({
const exportMapSelectionGeoJson = async (bbox: VectorSelectionBBox) => {
if (!selectedDataset || !isVectorDatasetType(selectedDataset.dataset_type)) {
setSelectionExportError('Select a vector dataset before saving an area export.')
return
return null
}
setSelectionExporting(true)
setSelectionExportError(null)
@@ -127,8 +127,10 @@ export function useExportWorkflow({
setLatestExport(response)
setLatestSelectionExport(response)
await loadExports(selectedDataset.project_id)
return response
} catch (error) {
setSelectionExportError(formatError(error, 'Failed to save area export'))
return null
} finally {
setSelectionExporting(false)
}
+3 -1
View File
@@ -27,7 +27,7 @@ export function useMapSelectionDataset({
const deriveMapSelectionDataset = async (bbox: VectorSelectionBBox) => {
if (!selectedProjectId || !selectedDataset || !isVectorDatasetType(selectedDataset.dataset_type)) {
setSelectionDatasetError('Select a vector dataset before saving the area as a dataset.')
return
return null
}
setSelectionDatasetSaving(true)
setSelectionDatasetError(null)
@@ -41,8 +41,10 @@ export function useMapSelectionDataset({
await loadProjectData(selectedProjectId)
await loadDatasetDetails(selectedProjectId, derived)
setMapLayerVisible(true)
return derived
} catch (error) {
setSelectionDatasetError(formatError(error, 'Failed to save area as dataset'))
return null
} finally {
setSelectionDatasetSaving(false)
}
+4 -2
View File
@@ -28,11 +28,11 @@ export function useMapSelectionExtract({
const runMapSelectionExtract = async (bbox: VectorSelectionBBox) => {
if (!selectedProjectId || !selectedDataset) {
setMapSelectionError('Open a vector dataset before extracting a map area.')
return
return null
}
if (!isVectorDatasetType(selectedDataset.dataset_type)) {
setMapSelectionError('Area extraction requires an active vector dataset.')
return
return null
}
setMapSelectionLoading(true)
@@ -44,9 +44,11 @@ export function useMapSelectionExtract({
limit: 250,
})
setMapSelectionResult(response)
return response
} catch (error) {
setMapSelectionResult(null)
setMapSelectionError(formatError(error, 'Area extraction failed'))
return null
} finally {
setMapSelectionLoading(false)
}
+15 -13
View File
@@ -22,56 +22,58 @@ export function useMapSelectionQa({
const [mapSelectionQaResult, setMapQaResult] = useState<QaComparisonResult | null>(null)
const [latestMapSelectionQualityCheckId, setLatestMapSelectionQualityCheckId] = useState<string | null>(null)
const runMapSelectionQa = async () => {
const runMapSelectionQa = async (candidateDataset = latestSelectionDataset) => {
if (!selectedProjectId) {
setMapSelectionQaError('Select a project before running QA/QC.')
return
return null
}
if (!latestSelectionDataset) {
if (!candidateDataset) {
setMapSelectionQaError('Save the map selection as a dataset before running QA/QC.')
return
return null
}
if (!selectedMapQaReferenceDatasetId) {
setMapSelectionQaError('Select a reference dataset for QA/QC.')
return
return null
}
if (latestSelectionDataset.id === selectedMapQaReferenceDatasetId) {
if (candidateDataset.id === selectedMapQaReferenceDatasetId) {
setMapSelectionQaError('Candidate and reference datasets must be different.')
return
return null
}
setMapSelectionQaRunning(true)
setMapSelectionQaError(null)
setMapQaResult(null)
setLatestMapSelectionQualityCheckId(null)
setLatestMapSelectionQualityCheckId(null)
try {
const request: QaComparisonRequest = {
candidate_dataset_id: latestSelectionDataset.id,
candidate_dataset_id: candidateDataset.id,
reference_dataset_id: selectedMapQaReferenceDatasetId,
iou_threshold: 0.5,
area_id: latestSelectionDataset.area_id ?? null,
area_id: candidateDataset.area_id ?? null,
}
const job: JobRead = await qaApi.runQa(request)
if (job.status === 'failed') {
setMapSelectionQaError(job.error_message || 'Map selection QA/QC failed')
return
return null
}
const payload = job.result_json
if (!payload || typeof payload !== 'object') {
setMapSelectionQaError('Map selection QA/QC result was not available')
return
return null
}
const parsed = payload as unknown as QaComparisonResult
if (!parsed || typeof parsed.status !== 'string') {
setMapSelectionQaError('Map selection QA/QC result format was unexpected')
return
return null
}
setMapQaResult(parsed)
setLatestMapSelectionQualityCheckId(parsed.quality_check_id ?? null)
await loadQualityChecks(selectedProjectId)
await loadProjectData(selectedProjectId)
return parsed
} catch (error) {
setMapSelectionQaError(formatError(error, 'Map selection QA/QC failed'))
return null
} finally {
setMapSelectionQaRunning(false)
}
+34
View File
@@ -4873,6 +4873,40 @@ section {
font-size: 0.78rem;
}
.guided-gis-full-run {
grid-column: span 1;
}
.guided-gis-batch-status {
display: grid;
gap: 0.12rem;
min-width: 0;
border: 1px solid rgba(15, 118, 110, 0.22);
border-radius: 6px;
padding: 0.44rem 0.52rem;
background: #f8fbf9;
}
.guided-gis-batch-status strong {
overflow: hidden;
color: #0f766e;
font-size: 0.68rem;
font-weight: 850;
letter-spacing: 0.045em;
text-overflow: ellipsis;
text-transform: uppercase;
white-space: nowrap;
}
.guided-gis-batch-status span {
overflow: hidden;
color: var(--muted);
font-size: 0.76rem;
line-height: 1.25;
text-overflow: ellipsis;
white-space: nowrap;
}
.bbox-select-surface {
border-left-width: 3px;
}