feat: simplify regional end user workbench
GeoIntel CI / docs-smoke (push) Canceled after 0s
GeoIntel CI / contract-smoke (push) Canceled after 0s

This commit is contained in:
Codex
2026-07-14 21:40:32 +02:00
parent 1693e4ca1f
commit b5bcd9b041
52 changed files with 1183 additions and 617 deletions
+8
View File
@@ -7,6 +7,14 @@
# Changelog # Changelog
## Sprint 193 End-user regional workbench simplification (2026-07-14)
- Made the complete `Kempen Regional Workbench` the automatic fresh-session data context and removed the redundant region selector from the primary map.
- Kept all available regional datasets loaded while treating Mol, the other 27 municipalities and the complete region as spatial work-area filters.
- Replaced technical dataset names with theme/source labels and moved benchmark projects, raw source metadata, provider internals, QA evidence and model diagnostics behind advanced disclosures.
- Defaulted Detection Lab to the configured local YOLO asset, active local model profile and measured operating threshold; model quality remains explicitly review-required rather than presented as production-perfect.
- Simplified Quality, Downloads, Status and Beheer around end-user tasks without changing API contracts, migrations, persistence or inference behavior.
## Sprint 192 Regional map state correctness (2026-07-14) ## Sprint 192 Regional map state correctness (2026-07-14)
- Fixed the map-first work-area selector so changing from the complete Kempen region to a municipality clears the old bbox, theme totals and result geometry before switching Areas. - Fixed the map-first work-area selector so changing from the complete Kempen region to a municipality clears the old bbox, theme totals and result geometry before switching Areas.
@@ -16,11 +16,11 @@ def test_detection_lab_exposes_run_readiness_contract() -> None:
assert "detectionTileManifestPath.trim().length > 0" in lab assert "detectionTileManifestPath.trim().length > 0" in lab
assert "detectionRunReady" in lab assert "detectionRunReady" in lab
assert 'aria-label="Detection run readiness"' in lab assert 'aria-label="Detection run readiness"' in lab
assert "Run readiness" in lab assert "Wat is nog nodig?" in lab
assert "Raster dataset" in lab assert "Luchtbeeld" in lab
assert "Model availability" in lab assert "Analysemodel" in lab
assert "Tile manifest" in lab assert "Beeldtegels" in lab
assert "Ready to submit" in lab assert "Klaar om te starten" in lab
def test_segmentation_lab_exposes_run_readiness_contract() -> None: def test_segmentation_lab_exposes_run_readiness_contract() -> None:
@@ -14,8 +14,8 @@ def test_detection_lab_distinguishes_configured_model_from_ui_runnable_action()
assert "detectionModelUiRunnable" in lab assert "detectionModelUiRunnable" in lab
assert "selectedDetectionModelId !== 'manual-fixture-detector'" in lab assert "selectedDetectionModelId !== 'manual-fixture-detector'" in lab
assert "detectionRunBlockedReason" in lab assert "detectionRunBlockedReason" in lab
assert "Fixture model is explicit test/demo-only" in lab assert "Het fixturemodel is alleen bedoeld voor expliciete tests" in lab
assert "Run action" in lab assert "Klaar om gebouwen te zoeken" in lab
assert "disabled={runningDetection || !detectionRunReady}" in lab assert "disabled={runningDetection || !detectionRunReady}" in lab
@@ -19,9 +19,9 @@ def test_qa_feature_evidence_contract_is_documented_and_rendered() -> None:
assert field_name in api_contracts assert field_name in api_contracts
assert "Feature-level QA/QC evidence" in quality_panel assert "Feature-level QA/QC evidence" in quality_panel
assert "Matched feature ids" in quality_panel assert "Overeenkomende object-ID's" in quality_panel
assert "False positive feature ids" in quality_panel assert "ID's van onterecht gevonden objecten" in quality_panel
assert "False negative feature ids" in quality_panel assert "ID's van gemiste objecten" in quality_panel
assert "evidenceLabel" in quality_panel assert "evidenceLabel" in quality_panel
@@ -47,5 +47,5 @@ def test_detection_lab_surfaces_local_model_asset_selection() -> None:
assert "Local model assets" in lab assert "Local model assets" in lab
assert "onSelectModelAsset" in lab assert "onSelectModelAsset" in lab
assert "modelAssets={modelAssets}" in app assert "modelAssets={modelAssets}" in app
assert "Official reference sources" in provider_panel assert "Officiële referentiebronnen" in provider_panel
assert "not AI model choices" in provider_panel assert "live GRB- en OSM-koppelingen staan uit" in provider_panel
@@ -4,13 +4,15 @@ from pathlib import Path
ROOT = Path(__file__).resolve().parents[2] ROOT = Path(__file__).resolve().parents[2]
def test_detection_workflow_does_not_auto_select_runtime_model_assets() -> None: def test_detection_workflow_selects_only_the_active_runtime_model_asset_automatically() -> None:
hook = (ROOT / "frontend" / "src" / "hooks" / "useDetectionWorkflow.ts").read_text(encoding="utf-8") hook = (ROOT / "frontend" / "src" / "hooks" / "useDetectionWorkflow.ts").read_text(encoding="utf-8")
assert "const activeAsset = assetResponse.items.find" not in hook assert "const activeAsset = assetResponse.items.find((asset) => asset.active) ?? null" in hook
assert "setSelectedModelAssetId(activeAsset?.model_asset_id ?? '')" not in hook assert "const selectedAssetStillAvailable" in hook
assert "activeAsset?.model_asset_id ?? ''" in hook
assert "setSelectedModelAssetId(nextAssetId)" in hook
assert "setSelectedModelAssetId(assetResponse.items[0]" not in hook assert "setSelectedModelAssetId(assetResponse.items[0]" not in hook
assert "setSelectedModelAssetId('')" in hook assert "getYoloPreflight({ model_asset_id: nextAssetId || null })" in hook
def test_detection_lab_explains_explicit_model_asset_and_threshold_selection() -> None: def test_detection_lab_explains_explicit_model_asset_and_threshold_selection() -> None:
@@ -18,11 +20,11 @@ def test_detection_lab_explains_explicit_model_asset_and_threshold_selection() -
encoding="utf-8" encoding="utf-8"
) )
assert "Explicit model asset" in lab assert "Lokaal modelbestand" in lab
assert "No model file is selected automatically" in lab assert "GeoIntel kiest automatisch het actieve lokale model" in lab
assert "Operator profiles" in lab assert "Gevalideerde profielen" in lab
assert "DETECTION_OPERATOR_PROFILES" in lab assert "DETECTION_OPERATOR_PROFILES" in lab
assert "Candidate only - not default-approved" in lab assert "kandidaat · extra controle vereist" in lab
assert "will_download_models" in lab assert "will_download_models" in lab
@@ -32,5 +34,5 @@ def test_detection_run_readiness_requires_explicit_asset_when_local_assets_exist
) )
assert "detectionHasExplicitModelAsset" in lab assert "detectionHasExplicitModelAsset" in lab
assert "Select a local model asset deliberately" in lab assert "Kies een lokaal modelbestand onder beheer" in lab
assert "local model asset" in lab assert "Lokale modelkeuze" in lab
@@ -40,7 +40,7 @@ def test_detection_handoff_opens_ai_lab_preflights_manifest_and_keeps_asset_expl
assert "setDetectionConfidenceThreshold(0.25)" in app assert "setDetectionConfidenceThreshold(0.25)" in app
assert "loadYoloPreflight(manifestPath).catch(() => null)" in app assert "loadYoloPreflight(manifestPath).catch(() => null)" in app
assert "setSelectedModelAssetId(" not in app[app.index("const useRasterTileManifestForDetection"):app.index("const {", app.index("const useRasterTileManifestForDetection"))] assert "setSelectedModelAssetId(" not in app[app.index("const useRasterTileManifestForDetection"):app.index("const {", app.index("const useRasterTileManifestForDetection"))]
assert "Linked tile manifest" in lab assert "Gekoppelde beeldtegels" in lab
assert "Tile manifest validation" in lab assert "Tile manifest validation" in lab
assert "Tile count" in lab assert "Tile count" in lab
assert "will_run_inference" in lab assert "will_run_inference" in lab
@@ -34,17 +34,17 @@ def test_detection_lab_surfaces_profiles_as_deliberate_operator_actions() -> Non
) )
assert "DETECTION_OPERATOR_PROFILES" in lab assert "DETECTION_OPERATOR_PROFILES" in lab
assert "Operator profiles" in lab assert "Gevalideerde profielen" in lab
assert "profile.displayName" in lab assert "profile.displayName" in lab
assert "profile.confidenceThreshold" in lab assert "profile.confidenceThreshold" in lab
assert "Candidate only - not default-approved" in lab assert "kandidaat · extra controle vereist" in lab
assert "default-approved" in lab assert "standaardprofiel" in lab
assert "Apply profile" in lab assert "Profiel gebruiken" in lab
assert "onApplyOperatorProfile(profile)" in lab assert "onApplyOperatorProfile(profile)" in lab
assert "Recommended starting threshold: 0.25" not in lab assert "Recommended starting threshold: 0.25" not in lab
def test_detection_workflow_applies_profiles_without_auto_selecting_assets() -> None: def test_detection_workflow_applies_profiles_without_selecting_the_first_arbitrary_asset() -> None:
hook = (ROOT / "frontend" / "src" / "hooks" / "useDetectionWorkflow.ts").read_text(encoding="utf-8") hook = (ROOT / "frontend" / "src" / "hooks" / "useDetectionWorkflow.ts").read_text(encoding="utf-8")
app = (ROOT / "frontend" / "src" / "App.tsx").read_text(encoding="utf-8") app = (ROOT / "frontend" / "src" / "App.tsx").read_text(encoding="utf-8")
@@ -13,8 +13,9 @@ def test_long_context_names_remain_compact_and_inspectable() -> None:
encoding="utf-8" encoding="utf-8"
) )
assert "const projectContextLabel = selectedProject?.name ?? 'No project'" in app assert "selectedProject?.name === REGIONAL_WORKSPACE_PROJECT_NAME" in app
assert "const datasetContextLabel = selectedDataset?.name ?? (datasets.length > 0 ? 'Select dataset' : 'No dataset')" in app assert "? REGIONAL_WORKSPACE_LABEL" in app
assert "const datasetContextLabel = selectedDataset ? getDatasetDisplayName(selectedDataset)" in app
assert "<strong title={projectContextLabel}>{projectContextLabel}</strong>" in app assert "<strong title={projectContextLabel}>{projectContextLabel}</strong>" in app
assert "<strong title={datasetContextLabel}>{datasetContextLabel}</strong>" in app assert "<strong title={datasetContextLabel}>{datasetContextLabel}</strong>" in app
assert "<strong title={item.value}>{item.value}</strong>" in status assert "<strong title={item.value}>{item.value}</strong>" in status
@@ -29,7 +29,7 @@ def test_frontend_declares_mol_as_primary_operating_focus() -> None:
assert "PRIMARY_FOCUS_AREA_GEOJSON" in project_hook assert "PRIMARY_FOCUS_AREA_GEOJSON" in project_hook
assert "4.35,51.28" not in project_hook assert "4.35,51.28" not in project_hook
assert "center: PRIMARY_FOCUS_CENTER" in map_source assert "center: PRIMARY_FOCUS_CENTER" in map_source
assert "GeoAI Workbench · Mol / Kempen" in app assert "GeoAI-werkruimte · Mol en de Kempen" in app
def test_operator_workflows_put_mol_first_and_name_future_projects() -> None: def test_operator_workflows_put_mol_first_and_name_future_projects() -> None:
@@ -16,8 +16,9 @@ def test_workbench_uses_grouped_navigation_and_optional_inspector() -> None:
assert "import './styles/premium.css'" in app assert "import './styles/premium.css'" in app
assert "const workspaceNavGroups" in app assert "const workspaceNavGroups" in app
assert "['overview', 'data', 'map']" in app assert "['map', 'data']" in app
assert "['analysis', 'ai']" in app assert "['analysis', 'ai']" in app
assert "['overview', 'system']" in app
assert "const [inspectorOpen, setInspectorOpen] = useState(false)" in app assert "const [inspectorOpen, setInspectorOpen] = useState(false)" in app
assert "{inspectorOpen ? (" in app assert "{inspectorOpen ? (" in app
assert 'id="workbench-inspector"' in app assert 'id="workbench-inspector"' in app
@@ -31,12 +32,12 @@ def test_data_creation_forms_are_progressively_disclosed() -> None:
dataset = read("frontend/src/components/datasets/DatasetPanel.tsx") dataset = read("frontend/src/components/datasets/DatasetPanel.tsx")
css = read("frontend/src/styles/premium.css") css = read("frontend/src/styles/premium.css")
assert '<details className="data-panel-form-block">' in project assert '<details className="data-panel-form-block technical-management-block">' in project
assert '<summary>Create project</summary>' in project assert '<summary>Geavanceerd projectbeheer</summary>' in project
assert '<details className="data-panel-form-block">' in area assert '<details className="data-panel-form-block">' in area
assert '<summary>Create spatial scope</summary>' in area assert '<summary>Eigen gebied toevoegen (GeoJSON)</summary>' in area
assert '<details className="dataset-upload-block data-panel-form-block">' in dataset assert '<details className="dataset-upload-block data-panel-form-block">' in dataset
assert '<summary>Upload source data</summary>' in dataset assert '<summary>Eigen bronbestand toevoegen</summary>' in dataset
assert "details.data-panel-form-block > summary" in css assert "details.data-panel-form-block > summary" in css
assert ".workspace-grid-data > section:nth-child(n)" in css assert ".workspace-grid-data > section:nth-child(n)" in css
assert "max-height: calc(100dvh - 10.5rem);" in css assert "max-height: calc(100dvh - 10.5rem);" in css
+6 -2
View File
@@ -129,9 +129,12 @@ def test_kempen_scope_operator_is_packaged_and_exposed_in_map_flow() -> None:
assert "py_compile scripts/provision_geographic_scope.py" in readiness assert "py_compile scripts/provision_geographic_scope.py" in readiness
assert "Immutable scope dataset" in (ROOT / "scripts/provision_geographic_scope.py").read_text(encoding="utf-8") assert "Immutable scope dataset" in (ROOT / "scripts/provision_geographic_scope.py").read_text(encoding="utf-8")
assert "Kempen (28 gemeenten)" in workspace assert "Kempen (28 gemeenten)" in workspace
assert 'aria-label="Regio"' in workspace assert 'aria-label="Regio"' not in workspace
assert 'aria-label="Ingeladen regiobereik"' in workspace
assert "Gemeente of volledige regio" in workspace
assert "projects={projects}" in app assert "projects={projects}" in app
assert "onSelectProject={selectProject}" in app map_props = app.split("<MapWorkspace", maxsplit=1)[1].split("/>", maxsplit=1)[0]
assert "onSelectProject={selectProject}" not in map_props
def test_project_switches_reset_scoped_state_and_prefer_the_regional_context() -> None: def test_project_switches_reset_scoped_state_and_prefer_the_regional_context() -> None:
@@ -144,5 +147,6 @@ def test_project_switches_reset_scoped_state_and_prefer_the_regional_context() -
assert "resetProjectData()" in selected_project_branch assert "resetProjectData()" in selected_project_branch
assert "resetDatasetForProject()" in selected_project_branch assert "resetDatasetForProject()" in selected_project_branch
assert "projectDataRequestSequence" in project_workspace assert "projectDataRequestSequence" in project_workspace
assert "REGIONAL_WORKSPACE_PROJECT_NAME" in project_workspace
assert "vervoerregio|operationele grens" in map_state assert "vervoerregio|operationele grens" in map_state
assert "datasets.find(isOperationalScopeBoundaryDataset)" in dataset_workflow assert "datasets.find(isOperationalScopeBoundaryDataset)" in dataset_workflow
@@ -0,0 +1,55 @@
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
def read(path: str) -> str:
return (ROOT / path).read_text(encoding="utf-8")
def test_regional_workspace_is_automatic_and_map_has_one_scope_selector() -> None:
project_hook = read("frontend/src/hooks/useProjectWorkspace.ts")
map_workspace = read("frontend/src/components/map/MapWorkspace.tsx")
regional_check = project_hook.index("const regionalProject")
municipality_check = project_hook.index("const municipalityProject")
assert regional_check < municipality_check
assert 'aria-label="Regio"' not in map_workspace
assert 'aria-label="Ingeladen regiobereik"' in map_workspace
assert "Gemeente of volledige regio" in map_workspace
assert 'aria-label="Werkgebied"' in map_workspace
def test_primary_navigation_uses_end_user_language_and_keeps_management_secondary() -> None:
app = read("frontend/src/App.tsx")
for label in ("Kaart", "Bronnen", "Kwaliteit", "Beeldanalyse", "Downloads"):
assert f"label: '{label}'" in app
assert "{ label: 'Beheer', keys: ['overview', 'system'] }" in app
assert 'className="secondary-analysis-disclosure segmentation-disclosure"' in app
def test_technical_projects_and_metadata_are_progressively_disclosed() -> None:
projects = read("frontend/src/components/project/ProjectPanel.tsx")
areas = read("frontend/src/components/project/AreaPanel.tsx")
datasets = read("frontend/src/components/datasets/DatasetPanel.tsx")
assert "TECHNICAL_PROJECT_PATTERN" in projects
assert "technische test- en benchmarkruns" in projects
assert "area-catalog-disclosure" in areas
assert "dataset-technical-details" in datasets
assert "getDatasetDisplayName" in datasets
def test_configured_yolo_and_active_asset_are_selected_without_hiding_limitations() -> None:
hook = read("frontend/src/hooks/useDetectionWorkflow.ts")
lab = read("frontend/src/components/detection/DetectionLab.tsx")
assert "useState('yolo-configured')" in hook
assert "useState(0.15)" in hook
assert "asset.active" in hook
assert "getYoloPreflight" in hook
assert 'aria-label="Status gebouwdetectie"' in lab
assert "resultaten blijven controleplichtig" in lab
assert "Modelkalibratie voor beheerders" in lab
@@ -58,4 +58,4 @@ def test_frontend_wires_selected_area_map_overlay_contract() -> None:
assert "area-fill" in geomap assert "area-fill" in geomap
assert "area-line" in geomap assert "area-line" in geomap
assert "onSelectMapArea" in area_panel assert "onSelectMapArea" in area_panel
assert "Show on map" in area_panel assert "Toon op kaart" in area_panel
@@ -16,9 +16,9 @@ def test_frontend_wires_v1_workbench_status_strip() -> None:
assert "qualityChecks={qualityChecks}" in app assert "qualityChecks={qualityChecks}" in app
assert "activeLayerFeatureCount={mapFeatureCount}" in app assert "activeLayerFeatureCount={mapFeatureCount}" in app
assert "selectedAreaHasGeometry={Boolean(areaFeatureCollection)}" in app assert "selectedAreaHasGeometry={Boolean(areaFeatureCollection)}" in app
assert "V1 readiness" in component assert "Platformstatus" in component
assert "Workbench status" in component assert "Workbench status:" in component
assert "Workbench has the core V1 loop populated." in component assert "Kaartwerkruimte is gebruiksklaar" in component
assert "workbench-status-strip" in css assert "workbench-status-strip" in css
assert "status-tile-ready" in css assert "status-tile-ready" in css
@@ -47,7 +47,7 @@ def test_app_still_wires_quality_results_and_export_center() -> None:
assert "<QualityResultsPanel" in app assert "<QualityResultsPanel" in app
assert "onRefresh={() => loadQualityChecks()}" in app assert "onRefresh={() => loadQualityChecks()}" in app
assert "Refresh QA/QC results" in quality_panel assert "Kwaliteitsresultaten vernieuwen" in quality_panel
assert "onClick={onRefresh}" in quality_panel assert "onClick={onRefresh}" in quality_panel
assert "<ExportCenter" in app assert "<ExportCenter" in app
assert "onExportDataset={exportSelectedDatasetGeoJson}" in app assert "onExportDataset={exportSelectedDatasetGeoJson}" in app
@@ -26,9 +26,9 @@ def test_app_uses_dataset_presentational_components() -> None:
def test_dataset_panel_owns_upload_and_list_markup() -> None: def test_dataset_panel_owns_upload_and_list_markup() -> None:
panel = (ROOT / "frontend" / "src" / "components" / "datasets" / "DatasetPanel.tsx").read_text(encoding="utf-8") panel = (ROOT / "frontend" / "src" / "components" / "datasets" / "DatasetPanel.tsx").read_text(encoding="utf-8")
assert "Upload dataset" in panel assert "Eigen bronbestand toevoegen" in panel
assert "Refresh metadata" in panel assert "Metadata vernieuwen" in panel
assert "Select / details" in panel assert "Details en acties" in panel
assert "onLoadDatasetDetails" in panel assert "onLoadDatasetDetails" in panel
assert "onRefreshMetadata" in panel assert "onRefreshMetadata" in panel
@@ -38,8 +38,8 @@ def test_quality_results_panel_owns_persisted_quality_check_markup() -> None:
).read_text(encoding="utf-8") ).read_text(encoding="utf-8")
assert "QualityCheckRead" in quality_panel assert "QualityCheckRead" in quality_panel
assert "Refresh QA/QC results" in quality_panel assert "Kwaliteitsresultaten vernieuwen" in quality_panel
assert "No persisted QA/QC results yet" in quality_panel assert "Nog geen bewaarde kwaliteitsresultaten" in quality_panel
assert "check.metrics.map" in quality_panel assert "check.metrics.map" in quality_panel
assert "fetch(" not in quality_panel assert "fetch(" not in quality_panel
assert "api" not in quality_panel.lower() assert "api" not in quality_panel.lower()
@@ -19,7 +19,7 @@ def test_data_workspace_panels_use_operator_friendly_cards_and_forms() -> None:
assert "compact-form" in project_panel assert "compact-form" in project_panel
assert "entity-card-active" in project_panel assert "entity-card-active" in project_panel
assert "AOI manager" in area_panel assert "<h2>Gebieden</h2>" in area_panel
assert "compact-form" in area_panel assert "compact-form" in area_panel
assert "entity-card-active" in area_panel assert "entity-card-active" in area_panel
assert "dataset-upload-form" in dataset_panel assert "dataset-upload-form" in dataset_panel
@@ -9,11 +9,11 @@ def test_quality_panel_uses_workbench_summary_and_cards() -> None:
ROOT / "frontend" / "src" / "components" / "quality" / "QualityResultsPanel.tsx" ROOT / "frontend" / "src" / "components" / "quality" / "QualityResultsPanel.tsx"
).read_text(encoding="utf-8") ).read_text(encoding="utf-8")
assert 'className="quality-results-panel"' in quality_panel assert "'quality-results-panel quality-results-panel-empty' : 'quality-results-panel'" in quality_panel
assert 'className="quality-summary-grid"' in quality_panel assert 'className="quality-summary-grid"' in quality_panel
assert 'className="quality-check-list"' in quality_panel assert 'className="quality-check-list"' in quality_panel
assert 'className="quality-check-card"' in quality_panel assert 'className="quality-check-card"' in quality_panel
assert "No persisted QA/QC results yet" in quality_panel assert "Nog geen bewaarde kwaliteitsresultaten" in quality_panel
assert "check.metrics.map" in quality_panel assert "check.metrics.map" in quality_panel
assert "fetch(" not in quality_panel assert "fetch(" not in quality_panel
@@ -13,8 +13,8 @@ def test_dataset_panel_exposes_map_and_export_quick_actions() -> None:
assert "dataset-card-active" in panel assert "dataset-card-active" in panel
assert "onOpenDatasetInMap" in panel assert "onOpenDatasetInMap" in panel
assert "onOpenDatasetExport" in panel assert "onOpenDatasetExport" in panel
assert "Open in map" in panel assert "Open op kaart" in panel
assert "Export / QA" in panel assert "Downloaden" in panel
assert "disabled={!(dataset.dataset_type === 'vector' || dataset.dataset_type === 'geojson')}" in panel assert "disabled={!(dataset.dataset_type === 'vector' || dataset.dataset_type === 'geojson')}" in panel
@@ -13,12 +13,12 @@ def test_export_center_surfaces_handoff_readiness_context() -> None:
assert 'className="handoff-readiness-grid"' in export_center assert 'className="handoff-readiness-grid"' in export_center
assert 'className="handoff-action-grid"' in export_center assert 'className="handoff-action-grid"' in export_center
assert 'className="export-type-badge"' in export_center assert 'className="export-type-badge"' in export_center
assert "Latest handoff artifact" in export_center assert "Laatste bestand" in export_center
assert "Project report" in export_center assert "Leesbaar rapport" in export_center
assert "Detection run" in export_center assert "Gebouwanalyse" in export_center
assert "Segmentation run" in export_center assert "Segmentatieanalyse" in export_center
assert "selectedDetectionRunId || 'none'" in export_center assert "selectedDetectionRunId || 'geen'" in export_center
assert "selectedSegmentationRunId || 'none'" in export_center assert "selectedSegmentationRunId || 'geen'" in export_center
def test_export_handoff_polish_has_responsive_styles() -> None: def test_export_handoff_polish_has_responsive_styles() -> None:
@@ -11,10 +11,10 @@ def test_dataset_panel_surfaces_role_summary_and_badges() -> None:
assert "roleSummaries" in dataset_panel assert "roleSummaries" in dataset_panel
assert "dataset-role-summary-grid" in dataset_panel assert "dataset-role-summary-grid" in dataset_panel
assert "Selected" in dataset_panel assert "Geselecteerd" in dataset_panel
assert "Reference" in dataset_panel assert "Referentie" in dataset_panel
assert "Candidate" in dataset_panel assert "Resultaat" in dataset_panel
assert "Source" in dataset_panel assert "Eigen bron" in dataset_panel
assert "dataset-role-badge" in dataset_panel assert "dataset-role-badge" in dataset_panel
assert "dataset-role-selected" in dataset_panel assert "dataset-role-selected" in dataset_panel
assert "dataset-role-reference" in dataset_panel assert "dataset-role-reference" in dataset_panel
@@ -28,9 +28,9 @@ def test_dataset_cards_expose_scan_friendly_source_and_crs_context() -> None:
) )
assert "dataset-card-kicker" in dataset_panel assert "dataset-card-kicker" in dataset_panel
assert "Source: {dataset.source_name ?? dataset.source}" in dataset_panel assert "Bron: {dataset.source_name ?? dataset.source}" in dataset_panel
assert "Layer: {dataset.reference_layer_name}" in dataset_panel assert "Laag: {dataset.reference_layer_name}" in dataset_panel
assert "CRS: {dataset.crs ?? dataset.vector_summary?.crs ?? 'n/a'}" in dataset_panel assert "CRS: {dataset.crs ?? dataset.vector_summary?.crs ?? 'onbekend'}" in dataset_panel
assert "dataset-card-title" in dataset_panel assert "dataset-card-title" in dataset_panel
@@ -11,9 +11,9 @@ def test_dataset_panel_explains_recommended_actions() -> None:
assert "datasetActionHint" in dataset_panel assert "datasetActionHint" in dataset_panel
assert "dataset-action-hint" in dataset_panel assert "dataset-action-hint" in dataset_panel
assert "QA reference layer" in dataset_panel assert "Officiële laag voor kaartanalyse en kwaliteitscontrole." in dataset_panel
assert "QA candidate layer" in dataset_panel assert "Bewaard resultaat dat opnieuw op de kaart" in dataset_panel
assert "Open on the map or send to Export / QA." in dataset_panel assert "Ingeladen gegevensbron voor verdere analyse." in dataset_panel
def test_dataset_buttons_have_scan_friendly_action_copy() -> None: def test_dataset_buttons_have_scan_friendly_action_copy() -> None:
@@ -22,12 +22,12 @@ def test_dataset_buttons_have_scan_friendly_action_copy() -> None:
) )
assert "dataset-action-grid" in dataset_panel assert "dataset-action-grid" in dataset_panel
assert "Inspect" in dataset_panel assert "Bekijken" in dataset_panel
assert "Map" in dataset_panel assert "Kaart" in dataset_panel
assert "Export / QA" in dataset_panel assert "Downloaden" in dataset_panel
assert "Metadata" in dataset_panel assert "Metadata" in dataset_panel
assert "Vector/GeoJSON only" in dataset_panel assert "Enkel vectorlagen" in dataset_panel
assert "Raster metadata is automatic" in dataset_panel assert "disabled={dataset.dataset_type === 'raster'}" in dataset_panel
def test_dataset_action_styles_keep_buttons_dense_and_responsive() -> None: def test_dataset_action_styles_keep_buttons_dense_and_responsive() -> None:
@@ -23,11 +23,11 @@ def test_quality_panel_surfaces_candidate_reference_handoff_summary() -> None:
).read_text(encoding="utf-8") ).read_text(encoding="utf-8")
assert "quality-handoff-grid" in quality_panel assert "quality-handoff-grid" in quality_panel
assert "Candidate layers" in quality_panel assert "Te controleren lagen" in quality_panel
assert "Reference layers" in quality_panel assert "Referentielagen" in quality_panel
assert "Latest comparison" in quality_panel assert "Laatste vergelijking" in quality_panel
assert "Candidate" in quality_panel assert "Te controleren:" in quality_panel
assert "Reference" in quality_panel assert "referentie:" in quality_panel
assert "quality-dataset-name" in quality_panel assert "quality-dataset-name" in quality_panel
@@ -28,8 +28,8 @@ def test_quality_panel_renders_metric_evidence_cards_and_raw_metrics() -> None:
assert "quality-metric-grid" in quality_panel assert "quality-metric-grid" in quality_panel
assert "quality-metric-card" in quality_panel assert "quality-metric-card" in quality_panel
assert "quality-metric-card-critical" in quality_panel assert "quality-metric-card-critical" in quality_panel
assert "Metric evidence" in quality_panel assert "Gemeten kwaliteit" in quality_panel
assert "Raw metrics" in quality_panel assert "Alle meetwaarden" in quality_panel
assert "check.metrics.map" in quality_panel assert "check.metrics.map" in quality_panel
@@ -17,12 +17,12 @@ def test_quality_panel_adds_result_filters_and_density_limit() -> None:
assert "filteredQualityChecks = useMemo" in quality_panel assert "filteredQualityChecks = useMemo" in quality_panel
assert "visibleQualityChecks = showAllQualityChecks ? filteredQualityChecks : filteredQualityChecks.slice(0, 8)" in quality_panel assert "visibleQualityChecks = showAllQualityChecks ? filteredQualityChecks : filteredQualityChecks.slice(0, 8)" in quality_panel
assert "quality-history-controls" in quality_panel assert "quality-history-controls" in quality_panel
assert "Search QA/QC results" in quality_panel assert "Kwaliteitsresultaten zoeken" in quality_panel
assert "All statuses" in quality_panel assert "Alle statussen" in quality_panel
assert "All check types" in quality_panel assert "Alle types" in quality_panel
assert "Reset view" in quality_panel assert "Filters wissen" in quality_panel
assert "Show all QA/QC results" in quality_panel assert "Alle kwaliteitsresultaten tonen" in quality_panel
assert "No QA/QC results match the current filters." in quality_panel assert "Geen kwaliteitsresultaten voldoen aan de filters." in quality_panel
def test_quality_filter_styles_reuse_dense_history_patterns() -> None: def test_quality_filter_styles_reuse_dense_history_patterns() -> None:
@@ -16,12 +16,12 @@ def test_project_and_area_panels_expose_selected_summary_regions() -> None:
assert "const selectedProject = projects.find" in project_panel assert "const selectedProject = projects.find" in project_panel
assert 'className="data-selection-summary data-selection-summary-project"' in project_panel assert 'className="data-selection-summary data-selection-summary-project"' in project_panel
assert 'className="data-panel-form-block"' in project_panel assert 'className="data-panel-form-block technical-management-block"' in project_panel
assert 'className="data-panel-list-block"' in project_panel assert 'className="data-panel-list-block"' in project_panel
assert "const selectedArea = areas.find" in area_panel assert "const selectedArea = areas.find" in area_panel
assert 'className="data-selection-summary data-selection-summary-area"' in area_panel assert 'className="data-selection-summary data-selection-summary-area"' in area_panel
assert 'className="data-panel-form-block"' in area_panel assert 'className="data-panel-form-block"' in area_panel
assert 'className="data-panel-list-block"' in area_panel assert 'className="data-panel-list-block area-catalog-disclosure"' in area_panel
def test_dataset_panel_separates_selected_upload_and_catalog_regions() -> None: def test_dataset_panel_separates_selected_upload_and_catalog_regions() -> None:
@@ -11,7 +11,7 @@ def test_quality_results_panel_exposes_structured_surfaces() -> None:
encoding="utf-8" encoding="utf-8"
) )
assert 'className="quality-results-panel"' in panel assert "'quality-results-panel quality-results-panel-empty' : 'quality-results-panel'" in panel
assert 'className="quality-results-shell"' in panel assert 'className="quality-results-shell"' in panel
assert 'className="quality-summary-surface"' in panel assert 'className="quality-summary-surface"' in panel
assert 'aria-label="QA/QC result summary"' in panel assert 'aria-label="QA/QC result summary"' in panel
@@ -22,6 +22,7 @@ def test_quality_results_panel_exposes_structured_surfaces() -> None:
assert 'className="quality-result-state-stack"' in panel assert 'className="quality-result-state-stack"' in panel
assert 'className="quality-history-surface"' in panel assert 'className="quality-history-surface"' in panel
assert 'aria-label="QA/QC result history"' in panel assert 'aria-label="QA/QC result history"' in panel
assert 'className="quality-advanced-disclosure"' in panel
def test_quality_results_panel_preserves_existing_refresh_filter_and_history_state() -> None: def test_quality_results_panel_preserves_existing_refresh_filter_and_history_state() -> None:
@@ -34,8 +35,8 @@ def test_quality_results_panel_preserves_existing_refresh_filter_and_history_sta
assert "qualityTypeFilter" in panel assert "qualityTypeFilter" in panel
assert "qualitySearchQuery" in panel assert "qualitySearchQuery" in panel
assert "visibleQualityChecks.map" in panel assert "visibleQualityChecks.map" in panel
assert "Show all QA/QC results" in panel assert "Alle kwaliteitsresultaten tonen" in panel
assert "No QA/QC results match the current filters." in panel assert "Geen kwaliteitsresultaten voldoen aan de filters." in panel
def test_quality_workspace_density_css_contracts() -> None: def test_quality_workspace_density_css_contracts() -> None:
@@ -52,7 +52,7 @@ def test_ai_lab_preserves_existing_detection_and_segmentation_controls() -> None
assert "onLoadResults" in detection assert "onLoadResults" in detection
assert "onRunQa" in detection assert "onRunQa" in detection
assert "detectionTileManifestPath" in detection assert "detectionTileManifestPath" in detection
assert "Detections loaded: {detectionItems.length}" in detection assert "{detectionItems.length} objecten geladen" in detection
assert "Compare detections to reference" in detection assert "Compare detections to reference" in detection
assert "onRunSegmentation" in segmentation assert "onRunSegmentation" in segmentation
@@ -20,7 +20,7 @@ def test_export_center_exposes_final_handoff_surfaces() -> None:
assert 'className="export-actions-surface"' in export_center assert 'className="export-actions-surface"' in export_center
assert 'aria-label="Export artifact actions"' in export_center assert 'aria-label="Export artifact actions"' in export_center
assert 'className="export-state-stack"' in export_center assert 'className="export-state-stack"' in export_center
assert 'className="export-history-surface"' in export_center assert 'className="export-history-surface export-history-disclosure"' in export_center
assert 'aria-label="Export history"' in export_center assert 'aria-label="Export history"' in export_center
@@ -11,14 +11,14 @@ def test_export_center_groups_latest_handoff_artifacts_by_type() -> None:
assert "latestHandoffArtifacts" in export_center assert "latestHandoffArtifacts" in export_center
assert "Latest handoff artifacts" in export_center assert "Latest handoff artifacts" in export_center
assert "Project report" in export_center assert "Leesbaar rapport" in export_center
assert "Project metadata" in export_center assert "Werkruimtedata" in export_center
assert "Dataset GeoJSON" in export_center assert "Kaartlaag (GeoJSON)" in export_center
assert "Detection GeoJSON" in export_center assert "Herkende gebouwen (GeoJSON)" in export_center
assert "Segmentation GeoJSON" in export_center assert "Segmentaties (GeoJSON)" in export_center
assert "latest-artifact-grid" in export_center assert "latest-artifact-grid" in export_center
assert "latest-artifact-card" in export_center assert "latest-artifact-card" in export_center
assert "No artifact yet" in export_center assert "Nog niet gemaakt" in export_center
def test_export_center_latest_artifact_cards_keep_existing_actions() -> None: def test_export_center_latest_artifact_cards_keep_existing_actions() -> None:
@@ -12,14 +12,14 @@ def test_quality_results_panel_exposes_selected_check_drilldown() -> None:
assert "selectedQualityCheckId" in panel assert "selectedQualityCheckId" in panel
assert "selectedQualityCheck" in panel assert "selectedQualityCheck" in panel
assert "QA/QC evidence drilldown" in panel assert "QA/QC evidence drilldown" in panel
assert "Selected check" in panel assert "Geselecteerde controle" in panel
assert "Candidate layer" in panel assert "Te controleren laag" in panel
assert "Reference layer" in panel assert "Referentielaag" in panel
assert "Analysis run" in panel assert "Analyserun" in panel
assert "Job" in panel assert "Job" in panel
assert "Completed" in panel assert "Afgerond" in panel
assert "Inspect latest check" in panel assert "Laatste controle bekijken" in panel
assert "Inspect check" in panel assert "Controle bekijken" in panel
def test_quality_results_panel_surfaces_false_positive_negative_evidence() -> None: def test_quality_results_panel_surfaces_false_positive_negative_evidence() -> None:
@@ -27,9 +27,9 @@ def test_quality_results_panel_surfaces_false_positive_negative_evidence() -> No
encoding="utf-8" encoding="utf-8"
) )
assert "False positive evidence" in panel assert "Onterecht gevonden" in panel
assert "False negative evidence" in panel assert "Gemiste objecten" in panel
assert "Map evidence handoff" in panel assert "Controle op de kaart" in panel
assert "metricByKey(selectedQualityCheck, 'false_positive_count')" in panel assert "metricByKey(selectedQualityCheck, 'false_positive_count')" in panel
assert "metricByKey(selectedQualityCheck, 'false_negative_count')" in panel assert "metricByKey(selectedQualityCheck, 'false_negative_count')" in panel
assert "selectedQualityCheck.parameters_json" in panel assert "selectedQualityCheck.parameters_json" in panel
@@ -22,8 +22,8 @@ def test_ai_labs_explain_missing_raster_input_before_disabled_runs() -> None:
encoding="utf-8" encoding="utf-8"
) )
assert "No raster datasets available for detection." in detection_lab assert "Geen luchtbeeld beschikbaar in deze werkruimte." in detection_lab
assert "Upload or select a raster dataset in Data before running object detection." in detection_lab assert "Voeg onder Bronnen een gegeorefereerde GeoTIFF toe." in detection_lab
assert "rasterDatasets.length === 0" in detection_lab assert "rasterDatasets.length === 0" in detection_lab
assert "No raster datasets available for segmentation." in segmentation_lab assert "No raster datasets available for segmentation." in segmentation_lab
assert "Upload or select a raster dataset in Data before running segmentation." in segmentation_lab assert "Upload or select a raster dataset in Data before running segmentation." in segmentation_lab
+23
View File
@@ -1,3 +1,26 @@
## Sprint 193 End-user regional workbench simplification (2026-07-14)
Changed:
- Made `Kempen Regional Workbench` the automatic complete data context before the municipality fallback and removed the primary map's project-level region selector.
- Kept the 28 municipality Areas plus the complete region available as one spatial work-area selector over the already loaded regional datasets.
- Simplified the visible shell and moved technical project runs, raw source metadata, QA internals, provider capabilities, export history and AI diagnostics into explicit advanced disclosures.
- Defaulted Detection Lab to `yolo-configured`, threshold `0.15`, the active local asset and automatic non-mutating preflight checks.
- Added end-user workbench contract tests without changing backend API contracts, migrations, persistence or model execution.
AI evidence:
- The configured Tower runtime reports PyTorch `2.13.0+cpu`, Ultralytics `8.4.95`, a valid local active model file and a successful model-load preflight; CUDA is not available.
- The retained active profile records precision `0.5898`, recall `0.5770` and F1 `0.5825` over seven positive AOIs with zero detections in the empty-background control.
- No automatic download or blind retraining was introduced. The 48 false-positive and 48 false-negative review decisions remain a required gate before constructing another training corpus.
Tested before deployment:
- `bash scripts/run_readiness_check.sh` passed with `560 passed`, backend compile, one Alembic head (`202607140001`), frontend typecheck/build and live-smoke syntax validation.
- `python -m alembic upgrade head --sql` generated a 30,166-byte offline migration plan successfully; `bash -n scripts/live_migration_smoke.sh` passed.
- The false-positive review validator returned the expected `review_required` gate with all 48 selected decisions still `unreviewed`; no unreviewed QA mismatch was promoted into training data.
- Local Docker validation was unavailable because the Windows workstation has no Docker CLI; live container/PostGIS validation remains part of the Tower deployment pass.
Next:
- Extend official population and modern land-use time series from Mol to all 28 municipalities, then expose a guided raster-import and configured-detection run over a drawn map selection.
## Sprint 191 Regional Kempen GRB context foundation (2026-07-14) ## Sprint 191 Regional Kempen GRB context foundation (2026-07-14)
Changed: Changed:
+3
View File
@@ -11,6 +11,9 @@
- [x] Define and provision the official 28-municipality Vervoerregio Kempen as the explicit operational regional scope. - [x] Define and provision the official 28-municipality Vervoerregio Kempen as the explicit operational regional scope.
- [x] Partition and provision the complete current GRB building theme for all 28 Kempen transport-region municipalities, with resumable source artifacts and one queryable regional PostGIS dataset. - [x] Partition and provision the complete current GRB building theme for all 28 Kempen transport-region municipalities, with resumable source artifacts and one queryable regional PostGIS dataset.
- [x] Provision the remaining regional GRB context themes in bounded partitions: roads, water and parcels. - [x] Provision the remaining regional GRB context themes in bounded partitions: roads, water and parcels.
- [x] Make the complete regional workspace the automatic data context and use municipality/full-region Areas as one spatial filter instead of requiring a region dropdown.
- [x] Reduce end-user noise by moving technical projects, source metadata, QA evidence, provider internals and model diagnostics behind explicit advanced disclosures.
- [x] Default Detection Lab to the configured local YOLO asset and present measured model quality and control requirements honestly.
- [ ] Extend official population and land-use time series from Mol to the approved 28-municipality regional scope. - [ ] Extend official population and land-use time series from Mol to the approved 28-municipality regional scope.
This file now starts with the current implementation status. Older preparation/backlog sections are preserved below as historical planning context and should not be treated as the live sprint board without checking `docs/CODEX_EXECUTION_LOG.md`. This file now starts with the current implementation status. Older preparation/backlog sections are preserved below as historical planning context and should not be treated as the live sprint board without checking `docs/CODEX_EXECUTION_LOG.md`.
+7 -3
View File
@@ -1,8 +1,12 @@
# GeoIntel Frontend (Sprint 4) # GeoIntel Frontend (Sprint 4)
React + TypeScript + MapLibre foundation for project/area/dataset workflow. React + TypeScript + MapLibre workbench for regional geographic analysis.
Mol is the primary operating context. On a fresh session the application opens the map-first geographic explorer, prefers the persisted `Mol Municipality Workbench`, selects the official NIS `13025` municipality boundary and activates the largest available authoritative building layer. A compact `Regio` selector switches between this workspace and the explicitly provisioned `Kempen Regional Workbench`, whose scope is the official 28-municipality Vlaamse vervoerregio. Explicit project and dataset selections remain authoritative. The persisted `Kempen Regional Workbench` is the automatic operational data context. Its current GRB layers are loaded once for the complete official 28-municipality Vlaamse vervoerregio; the operator chooses Mol, another municipality or the complete region as a spatial work-area filter. The primary map no longer asks the user to choose a technical project or region before data becomes usable. Mol remains the primary validation and historical-data focus, while explicit project selection stays available under advanced management.
The user-facing shell is task based: `Kaart`, `Bronnen`, `Kwaliteit`, `Beeldanalyse`, `Downloads`, `Status` and `Beheer`. Internal benchmark projects, raw dataset metadata, provider capabilities, model registry details and QA evidence remain accessible through labelled advanced disclosures instead of competing with the normal workflow.
Detection defaults to the configured local YOLO asset and automatically selects an available raster and active model asset where possible. The model registry and preflight remain honest when PyTorch, Ultralytics, a local model file or a tile manifest is unavailable. The active building profile is operational but remains review-required: its recorded coverage-aware benchmark is approximately precision 0.590, recall 0.577 and F1 0.582 over seven positive AOIs, with zero detections in the empty-background control. Another training pass is intentionally blocked until the generated false-positive and false-negative review decisions are completed.
The map-first explorer has two deliberate modes. `Latest state` selects the The map-first explorer has two deliberate modes. `Latest state` selects the
latest explicitly dated source snapshot without claiming an old edition is latest explicitly dated source snapshot without claiming an old edition is
@@ -21,7 +25,7 @@ estimates clearly marked and land-cover sources show intersected hectares. The
advanced workbench remains available but is not required for the primary advanced workbench remains available but is not required for the primary
choose-theme, draw-area, read-result flow. choose-theme, draw-area, read-result flow.
The primary workflow is deliberately short: choose Mol or Kempen, choose a data theme, drag a rectangle on the MapLibre map and read the resulting PostGIS evidence. Releasing the drag runs the active theme query and every other available theme query for the same EPSG:4326 bbox. The result panel shows selection area, exact intersection totals, active-theme density, source identity and bounded feature properties. Map rendering remains capped at 1,000 features while `total_feature_count` reports the exact database count. The primary workflow is deliberately short: choose a municipality or the complete region, choose a data theme, drag a rectangle on the MapLibre map and read the resulting PostGIS evidence. Releasing the drag runs the active theme query and every other available theme query for the same EPSG:4326 bbox. The result panel shows selection area, exact intersection totals, active-theme density, source identity and bounded feature properties. Map rendering remains capped at 1,000 features while `total_feature_count` reports the exact database count.
The theme catalog currently recognizes buildings, population, forest/green, water, roads and parcels from dataset names and canonical `reference_layer_name` metadata. A theme is enabled only when a ready persisted vector dataset exists; otherwise it states `Bron nog niet ingeladen`. This prevents missing population or land-cover sources from appearing as zero-valued observations. The previous technical Map workspace remains available through `Geavanceerde werkbank` for derived datasets, QA/QC evidence and export operations. The theme catalog currently recognizes buildings, population, forest/green, water, roads and parcels from dataset names and canonical `reference_layer_name` metadata. A theme is enabled only when a ready persisted vector dataset exists; otherwise it states `Bron nog niet ingeladen`. This prevents missing population or land-cover sources from appearing as zero-valued observations. The previous technical Map workspace remains available through `Geavanceerde werkbank` for derived datasets, QA/QC evidence and export operations.
+56 -35
View File
@@ -30,6 +30,8 @@ import { useQualityWorkflow } from './hooks/useQualityWorkflow'
import { useSegmentationWorkflow } from './hooks/useSegmentationWorkflow' import { useSegmentationWorkflow } from './hooks/useSegmentationWorkflow'
import { useWorkbenchBootstrap } from './hooks/useWorkbenchBootstrap' import { useWorkbenchBootstrap } from './hooks/useWorkbenchBootstrap'
import { useViewportVectorLayer } from './hooks/useViewportVectorLayer' import { useViewportVectorLayer } from './hooks/useViewportVectorLayer'
import { getDatasetDisplayName } from './lib/datasetDisplay'
import { REGIONAL_WORKSPACE_LABEL, REGIONAL_WORKSPACE_PROJECT_NAME } from './config/primaryFocus'
function isVectorDatasetType(datasetType: string): boolean { function isVectorDatasetType(datasetType: string): boolean {
return datasetType === 'vector' || datasetType === 'geojson' return datasetType === 'vector' || datasetType === 'geojson'
@@ -38,19 +40,20 @@ function isVectorDatasetType(datasetType: string): boolean {
type WorkspaceKey = 'overview' | 'data' | 'map' | 'analysis' | 'ai' | 'exports' | 'system' type WorkspaceKey = 'overview' | 'data' | 'map' | 'analysis' | 'ai' | 'exports' | 'system'
const workspaceNavItems: Array<{ key: WorkspaceKey; label: string; description: string }> = [ const workspaceNavItems: Array<{ key: WorkspaceKey; label: string; description: string }> = [
{ key: 'overview', label: 'Overview', description: 'V1 readiness and next action' }, { key: 'overview', label: 'Status', description: 'Beschikbaarheid en aandachtspunten' },
{ key: 'data', label: 'Data', description: 'Projects, AOI and datasets' }, { key: 'data', label: 'Bronnen', description: 'Gebieden en ingeladen gegevens' },
{ key: 'map', label: 'Map', description: 'Layer review and inspection' }, { key: 'map', label: 'Kaart', description: 'Selecteren, uitlezen en vergelijken' },
{ key: 'analysis', label: 'QA/QC', description: 'Quality checks and change detection' }, { key: 'analysis', label: 'Kwaliteit', description: 'Resultaten controleren' },
{ key: 'ai', label: 'AI Labs', description: 'Detection and segmentation' }, { key: 'ai', label: 'Beeldanalyse', description: 'Gebouwen herkennen op luchtbeelden' },
{ key: 'exports', label: 'Exports', description: 'Artifacts and downloads' }, { key: 'exports', label: 'Downloads', description: 'Resultaten bewaren en delen' },
{ key: 'system', label: 'System', description: 'Provider capabilities' }, { key: 'system', label: 'Beheer', description: 'Databronnen en systeemstatus' },
] ]
const workspaceNavGroups: Array<{ label: string; keys: WorkspaceKey[] }> = [ const workspaceNavGroups: Array<{ label: string; keys: WorkspaceKey[] }> = [
{ label: 'Workspace', keys: ['overview', 'data', 'map'] }, { label: 'Verkennen', keys: ['map', 'data'] },
{ label: 'Analyze', keys: ['analysis', 'ai'] }, { label: 'Analyseren', keys: ['analysis', 'ai'] },
{ label: 'Deliver', keys: ['exports', 'system'] }, { label: 'Afronden', keys: ['exports'] },
{ label: 'Beheer', keys: ['overview', 'system'] },
] ]
function App(): JSX.Element { function App(): JSX.Element {
@@ -659,43 +662,45 @@ function App(): JSX.Element {
target: 'exports', target: 'exports',
}, },
] ]
const projectContextLabel = selectedProject?.name ?? 'No project' const projectContextLabel = selectedProject?.name === REGIONAL_WORKSPACE_PROJECT_NAME
const areaContextLabel = selectedArea?.name ?? (areas.length > 0 ? 'Select area' : 'No AOI') ? REGIONAL_WORKSPACE_LABEL
const datasetContextLabel = selectedDataset?.name ?? (datasets.length > 0 ? 'Select dataset' : 'No dataset') : selectedProject?.name ?? 'Geen werkruimte'
const areaContextLabel = selectedArea?.name ?? (areas.length > 0 ? 'Kies een gebied' : 'Geen gebied')
const datasetContextLabel = selectedDataset ? getDatasetDisplayName(selectedDataset) : datasets.length > 0 ? 'Kies een bron' : 'Geen bron'
const layerContextLabel = mapFeatureCollection const layerContextLabel = mapFeatureCollection
? `${mapFeatureCount} features` ? `${mapFeatureCount} features`
: viewportVectorLayerActive : viewportVectorLayerActive
? 'Viewport layer selected' ? 'Kaartlaag gekozen'
: 'No active layer' : 'Geen actieve laag'
return ( return (
<div className="app-shell workbench-shell"> <div className="app-shell workbench-shell">
<a className="skip-link" href="#workspace-main"> <a className="skip-link" href="#workspace-main">
Skip to workspace Ga naar de werkruimte
</a> </a>
<header className="workbench-topbar"> <header className="workbench-topbar">
<div className="brand-block"> <div className="brand-block">
<span className="brand-mark" aria-hidden="true">GI</span> <span className="brand-mark" aria-hidden="true">GI</span>
<div> <div>
<h1>GeoIntel</h1> <h1>GeoIntel</h1>
<p>GeoAI Workbench · Mol / Kempen</p> <p>GeoAI-werkruimte · Mol en de Kempen</p>
</div> </div>
</div> </div>
<div className="context-bar" aria-label="Active workbench context"> <div className="context-bar" aria-label="Active workbench context">
<div> <div>
<span>Project</span> <span>Werkruimte</span>
<strong title={projectContextLabel}>{projectContextLabel}</strong> <strong title={projectContextLabel}>{projectContextLabel}</strong>
</div> </div>
<div> <div>
<span>AOI</span> <span>Gebied</span>
<strong title={areaContextLabel}>{areaContextLabel}</strong> <strong title={areaContextLabel}>{areaContextLabel}</strong>
</div> </div>
<div> <div>
<span>Dataset</span> <span>Bron</span>
<strong title={datasetContextLabel}>{datasetContextLabel}</strong> <strong title={datasetContextLabel}>{datasetContextLabel}</strong>
</div> </div>
<div> <div>
<span>Layer</span> <span>Kaartlaag</span>
<strong title={layerContextLabel}>{layerContextLabel}</strong> <strong title={layerContextLabel}>{layerContextLabel}</strong>
</div> </div>
</div> </div>
@@ -770,7 +775,7 @@ function App(): JSX.Element {
</button> </button>
))} ))}
</div> </div>
<span>{selectedProject ? `Active project: ${selectedProject.name}` : 'Start with a project or load the demo workflow.'}</span> <span>{selectedProject ? `Actief bereik: ${projectContextLabel}` : 'De regionale gegevens worden ingeladen.'}</span>
</div> </div>
{activeWorkspace === 'overview' ? ( {activeWorkspace === 'overview' ? (
@@ -784,6 +789,11 @@ function App(): JSX.Element {
activeLayerFeatureCount={mapFeatureCount} activeLayerFeatureCount={mapFeatureCount}
selectedAreaHasGeometry={Boolean(areaFeatureCollection)} selectedAreaHasGeometry={Boolean(areaFeatureCollection)}
/> />
<details className="status-details-disclosure">
<summary>
<span>Volledige workflowstatus</span>
<strong>{workflowGuidanceComplete ? 'voltooid' : 'optionele stappen open'}</strong>
</summary>
<section className="workflow-guidance-panel" aria-label="V1 workflow guidance"> <section className="workflow-guidance-panel" aria-label="V1 workflow guidance">
<div className="panel-title-row"> <div className="panel-title-row">
<div> <div>
@@ -860,6 +870,7 @@ function App(): JSX.Element {
</button> </button>
</div> </div>
</section> </section>
</details>
</div> </div>
) : null} ) : null}
@@ -958,7 +969,6 @@ function App(): JSX.Element {
selectedMapDatasetId={selectedDataset && isVectorDatasetType(selectedDataset.dataset_type) ? selectedDataset.id : ''} selectedMapDatasetId={selectedDataset && isVectorDatasetType(selectedDataset.dataset_type) ? selectedDataset.id : ''}
selectedFeature={selectedMapFeature} selectedFeature={selectedMapFeature}
onSelectMapArea={setSelectedMapAreaId} onSelectMapArea={setSelectedMapAreaId}
onSelectProject={selectProject}
onOpenDatasetInMap={openDatasetInMap} onOpenDatasetInMap={openDatasetInMap}
onSetAreaLayerVisible={setAreaLayerVisible} onSetAreaLayerVisible={setAreaLayerVisible}
onSetAreaLayerOpacity={setAreaLayerOpacity} onSetAreaLayerOpacity={setAreaLayerOpacity}
@@ -981,6 +991,22 @@ function App(): JSX.Element {
{activeWorkspace === 'analysis' ? ( {activeWorkspace === 'analysis' ? (
<div className="workspace-grid workspace-grid-analysis"> <div className="workspace-grid workspace-grid-analysis">
<QualityResultsPanel
selectedProjectId={selectedProjectId}
qualityChecks={qualityChecks}
qualityChecksError={qualityChecksError}
candidateDatasets={candidateDatasets}
referenceDatasets={referenceDatasets}
onRefresh={() => loadQualityChecks()}
onOpenEvidenceMap={openQualityEvidenceOnMap}
evidenceLoading={qualityEvidenceLoading}
evidenceError={qualityEvidenceError}
/>
<details className="secondary-analysis-disclosure">
<summary>
<span>Historische vectorlagen vergelijken</span>
<strong>Geavanceerd</strong>
</summary>
<ChangeDetectionPanel <ChangeDetectionPanel
vectorDatasets={availableVectorDatasets} vectorDatasets={availableVectorDatasets}
sourceDatasetId={changeSourceDatasetId} sourceDatasetId={changeSourceDatasetId}
@@ -996,18 +1022,7 @@ function App(): JSX.Element {
onIncludeUnchangedChange={setChangeIncludeUnchanged} onIncludeUnchangedChange={setChangeIncludeUnchanged}
onRun={runChangeDetection} onRun={runChangeDetection}
/> />
</details>
<QualityResultsPanel
selectedProjectId={selectedProjectId}
qualityChecks={qualityChecks}
qualityChecksError={qualityChecksError}
candidateDatasets={candidateDatasets}
referenceDatasets={referenceDatasets}
onRefresh={() => loadQualityChecks()}
onOpenEvidenceMap={openQualityEvidenceOnMap}
evidenceLoading={qualityEvidenceLoading}
evidenceError={qualityEvidenceError}
/>
</div> </div>
) : null} ) : null}
@@ -1069,6 +1084,11 @@ function App(): JSX.Element {
onApplyOperatorProfile={applyDetectionOperatorProfile} onApplyOperatorProfile={applyDetectionOperatorProfile}
/> />
<details className="secondary-analysis-disclosure segmentation-disclosure">
<summary>
<span>Segmentatie van beeldvlakken</span>
<strong>Nog niet geconfigureerd</strong>
</summary>
<SegmentationLab <SegmentationLab
segmentationModels={segmentationModels} segmentationModels={segmentationModels}
loadingSegmentationModels={loadingSegmentationModels} loadingSegmentationModels={loadingSegmentationModels}
@@ -1109,6 +1129,7 @@ function App(): JSX.Element {
onSelectReferenceDataset={setSegmentationReferenceDatasetId} onSelectReferenceDataset={setSegmentationReferenceDatasetId}
onRunQa={runSegmentationQa} onRunQa={runSegmentationQa}
/> />
</details>
</div> </div>
) : null} ) : null}
@@ -28,21 +28,21 @@ function countReferenceDatasets(datasets: DatasetCreateResponse[]): number {
function nextAction(items: StatusItem[]): string { function nextAction(items: StatusItem[]): string {
if (!items.some((item) => item.key === 'project' && item.state === 'ready')) { if (!items.some((item) => item.key === 'project' && item.state === 'ready')) {
return 'Create or open a project.' return 'De regionale werkruimte kon niet worden geopend.'
} }
if (!items.some((item) => item.key === 'area' && item.state === 'ready')) { if (!items.some((item) => item.key === 'area' && item.state === 'ready')) {
return 'Create an AOI or select an area with geometry.' return 'Kies een gemeente of de volledige regio.'
} }
if (!items.some((item) => item.key === 'datasets' && item.state === 'ready')) { if (!items.some((item) => item.key === 'datasets' && item.state === 'ready')) {
return 'Upload a vector or raster dataset, or load the offline demo workflow.' return 'Laad een kaartlaag of luchtbeeld in.'
} }
if (!items.some((item) => item.key === 'qa' && item.state === 'ready')) { if (!items.some((item) => item.key === 'qa' && item.state === 'ready')) {
return 'Run QA/QC against a reference dataset when candidate and reference data are available.' return 'Kaart en bronnen zijn klaar. Kwaliteitscontrole is beschikbaar zodra er een analyseresultaat is.'
} }
if (!items.some((item) => item.key === 'exports' && item.state === 'ready')) { if (!items.some((item) => item.key === 'exports' && item.state === 'ready')) {
return 'Create a metadata, report or GeoJSON export for handoff.' return 'De analyse is klaar; maak eventueel een rapport of download.'
} }
return 'Workbench has the core V1 loop populated.' return 'Alle onderdelen van de werkruimte bevatten bewaarde resultaten.'
} }
export function WorkbenchStatusStrip({ export function WorkbenchStatusStrip({
@@ -64,63 +64,64 @@ export function WorkbenchStatusStrip({
const items: StatusItem[] = [ const items: StatusItem[] = [
{ {
key: 'project', key: 'project',
label: 'Project', label: 'Werkruimte',
value: selectedProject ? selectedProject.name : 'No project', value: selectedProject ? selectedProject.name : 'Geen werkruimte',
detail: selectedProject ? selectedProject.region : 'Open or create a project', detail: selectedProject ? selectedProject.region : 'Niet geladen',
state: selectedProject ? 'ready' : 'waiting', state: selectedProject ? 'ready' : 'waiting',
}, },
{ {
key: 'area', key: 'area',
label: 'AOI', label: 'Gebieden',
value: `${areas.length} area${areas.length === 1 ? '' : 's'}`, value: `${areas.length} beschikbaar`,
detail: selectedAreaHasGeometry ? 'selected area has map geometry' : 'no selected map geometry', detail: selectedAreaHasGeometry ? 'Kaartgrens beschikbaar' : 'Geen kaartgrens geselecteerd',
state: selectedAreaHasGeometry ? 'ready' : areas.length > 0 ? 'warning' : 'waiting', state: selectedAreaHasGeometry ? 'ready' : areas.length > 0 ? 'warning' : 'waiting',
}, },
{ {
key: 'datasets', key: 'datasets',
label: 'Datasets', label: 'Bronnen',
value: `${readyDatasets}/${datasets.length} ready`, value: `${readyDatasets}/${datasets.length} beschikbaar`,
detail: `${vectorDatasets} vector, ${rasterDatasets} raster, ${referenceDatasets} reference`, detail: `${vectorDatasets} kaartlagen, ${rasterDatasets} luchtbeelden, ${referenceDatasets} officiële referenties`,
state: datasets.length === 0 ? 'waiting' : readyDatasets === datasets.length ? 'ready' : 'warning', state: datasets.length === 0 ? 'waiting' : readyDatasets === datasets.length ? 'ready' : 'warning',
}, },
{ {
key: 'map', key: 'map',
label: 'Map', label: 'Kaart',
value: `${activeLayerFeatureCount} feature${activeLayerFeatureCount === 1 ? '' : 's'}`, value: `${activeLayerFeatureCount} zichtbare objecten`,
detail: selectedAreaHasGeometry ? 'AOI overlay available' : 'AOI overlay missing', detail: selectedAreaHasGeometry ? 'Gebiedsgrens beschikbaar' : 'Gebiedsgrens ontbreekt',
state: selectedAreaHasGeometry || activeLayerFeatureCount > 0 ? 'ready' : 'waiting', state: selectedAreaHasGeometry || activeLayerFeatureCount > 0 ? 'ready' : 'waiting',
}, },
{ {
key: 'qa', key: 'qa',
label: 'QA/QC', label: 'Kwaliteit',
value: `${qualityChecks.length} check${qualityChecks.length === 1 ? '' : 's'}`, value: `${qualityChecks.length} controles`,
detail: latestQualityCheck ? `${latestQualityCheck.check_type}: ${latestQualityCheck.status}` : 'no persisted QA/QC result', detail: latestQualityCheck ? `${latestQualityCheck.check_type}: ${latestQualityCheck.status}` : 'Nog geen controle uitgevoerd',
state: qualityChecks.length > 0 ? 'ready' : 'waiting', state: qualityChecks.length > 0 ? 'ready' : 'waiting',
}, },
{ {
key: 'exports', key: 'exports',
label: 'Exports', label: 'Downloads',
value: `${exports.length} export${exports.length === 1 ? '' : 's'}`, value: `${exports.length} bestanden`,
detail: latestExport ? `${latestExport.export_type}: ${latestExport.status}` : 'no handoff artifact yet', detail: latestExport ? `${latestExport.export_type}: ${latestExport.status}` : 'Nog geen bestand gemaakt',
state: exports.length > 0 ? 'ready' : 'waiting', state: exports.length > 0 ? 'ready' : 'waiting',
}, },
] ]
const readyCount = items.filter((item) => item.state === 'ready').length const readyCount = items.filter((item) => item.state === 'ready').length
const coreReady = items.slice(0, 4).every((item) => item.state === 'ready')
return ( return (
<section className="workbench-status-strip" aria-label="Workbench readiness"> <section className="workbench-status-strip" aria-label="Workbench readiness">
<div className="status-strip-header"> <div className="status-strip-header">
<div> <div>
<p className="eyebrow">V1 readiness</p> <p className="eyebrow">Platformstatus</p>
<h2> <h2>
<span className="sr-only">Workbench status: </span> <span className="sr-only">Workbench status: </span>
{readyCount === items.length ? 'Mol workbench is ready' : 'Mol workbench needs attention'} {coreReady ? 'Kaartwerkruimte is gebruiksklaar' : 'Kaartwerkruimte vraagt aandacht'}
</h2> </h2>
<p className="status-next-action">{nextAction(items)}</p> <p className="status-next-action">{nextAction(items)}</p>
</div> </div>
<div className="status-readiness" aria-label={`${readyCount} of ${items.length} workbench stages ready`}> <div className="status-readiness" aria-label={`${readyCount} of ${items.length} workbench stages ready`}>
<strong>{readyCount}/{items.length}</strong> <strong>{readyCount}/{items.length}</strong>
<span>stages ready</span> <span>onderdelen gereed</span>
</div> </div>
</div> </div>
<div className="status-strip-grid"> <div className="status-strip-grid">
@@ -1,5 +1,6 @@
import type { Dispatch, FormEvent, SetStateAction } from 'react' import type { Dispatch, FormEvent, SetStateAction } from 'react'
import type { AreaRead, DatasetCreateResponse } from '../../types' import type { AreaRead, DatasetCreateResponse } from '../../types'
import { getDatasetDisplayName } from '../../lib/datasetDisplay'
interface DatasetFormState { interface DatasetFormState {
datasetType: string datasetType: string
@@ -70,25 +71,25 @@ function normalizeDatasetRole(dataset: DatasetCreateResponse): 'reference' | 'ca
function datasetRoleLabel(role: string): string { function datasetRoleLabel(role: string): string {
if (role === 'reference') { if (role === 'reference') {
return 'Reference' return 'Officiële referentie'
} }
if (role === 'candidate') { if (role === 'candidate') {
return 'Candidate' return 'Analyse-resultaat'
} }
return 'Source' return 'Eigen bron'
} }
function datasetActionHint(dataset: DatasetCreateResponse, role: string): string { function datasetActionHint(dataset: DatasetCreateResponse, role: string): string {
if (role === 'reference') { if (role === 'reference') {
return 'QA reference layer. Select it when comparing candidates against trusted geometry.' return 'Officiële laag voor kaartanalyse en kwaliteitscontrole.'
} }
if (role === 'candidate') { if (role === 'candidate') {
return 'QA candidate layer. Open on the map or send to Export / QA.' return 'Bewaard resultaat dat opnieuw op de kaart, in kwaliteit of als download kan worden gebruikt.'
} }
if (dataset.dataset_type === 'raster') { if (dataset.dataset_type === 'raster') {
return 'Raster source. Use the raster tools in the inspector after selecting it.' return 'Luchtbeeld of rasterbron voor kaart- en beeldanalyse.'
} }
return 'Source dataset. Inspect details before using it in downstream workflows.' return 'Ingeladen gegevensbron voor verdere analyse.'
} }
export function DatasetPanel({ export function DatasetPanel({
@@ -111,30 +112,30 @@ export function DatasetPanel({
const roleSummaries = [ const roleSummaries = [
{ {
key: 'selected', key: 'selected',
label: 'Selected', label: 'Geselecteerd',
count: selectedDatasetId ? 1 : 0, count: selectedDatasetId ? 1 : 0,
hint: 'Active', hint: 'Actief',
className: 'dataset-role-selected', className: 'dataset-role-selected',
}, },
{ {
key: 'reference', key: 'reference',
label: 'Reference', label: 'Referentie',
count: datasets.filter((dataset) => normalizeDatasetRole(dataset) === 'reference').length, count: datasets.filter((dataset) => normalizeDatasetRole(dataset) === 'reference').length,
hint: 'QA truth', hint: 'Officieel',
className: 'dataset-role-reference', className: 'dataset-role-reference',
}, },
{ {
key: 'candidate', key: 'candidate',
label: 'Candidate', label: 'Resultaat',
count: datasets.filter((dataset) => normalizeDatasetRole(dataset) === 'candidate').length, count: datasets.filter((dataset) => normalizeDatasetRole(dataset) === 'candidate').length,
hint: 'QA candidate', hint: 'Afgeleid',
className: 'dataset-role-candidate', className: 'dataset-role-candidate',
}, },
{ {
key: 'source', key: 'source',
label: 'Source', label: 'Eigen bron',
count: datasets.filter((dataset) => normalizeDatasetRole(dataset) === 'source').length, count: datasets.filter((dataset) => normalizeDatasetRole(dataset) === 'source').length,
hint: 'Uploads', hint: 'Ingeladen',
className: 'dataset-role-source', className: 'dataset-role-source',
}, },
] ]
@@ -143,23 +144,23 @@ export function DatasetPanel({
<section className="workspace-panel" data-testid="dataset-panel"> <section className="workspace-panel" data-testid="dataset-panel">
<div className="panel-title-row"> <div className="panel-title-row">
<div> <div>
<p className="eyebrow">Data catalog</p> <p className="eyebrow">Ingeladen gegevens</p>
<h2>Datasets</h2> <h2>Bronnen</h2>
</div> </div>
<span className="count-pill">{readyDatasets}/{datasets.length} ready</span> <span className="count-pill">{readyDatasets}/{datasets.length} beschikbaar</span>
</div> </div>
<div className="data-selection-summary data-selection-summary-dataset"> <div className="data-selection-summary data-selection-summary-dataset">
<span>Selected dataset</span> <span>Actieve bron</span>
<strong>{selectedDataset?.name ?? 'No dataset selected'}</strong> <strong>{selectedDataset ? getDatasetDisplayName(selectedDataset) : 'Geen bron geselecteerd'}</strong>
<small>{selectedDataset ? `${datasetRoleLabel(normalizeDatasetRole(selectedDataset))} / ${selectedDataset.status}` : 'Inspect a dataset to load details and tools.'}</small> <small>{selectedDataset ? `${datasetRoleLabel(normalizeDatasetRole(selectedDataset))} · ${selectedDataset.status === 'ready' ? 'beschikbaar' : selectedDataset.status}` : 'Kies een bron om deze op de kaart te openen.'}</small>
</div> </div>
<details className="dataset-upload-block data-panel-form-block"> <details className="dataset-upload-block data-panel-form-block">
<summary>Upload source data</summary> <summary>Eigen bronbestand toevoegen</summary>
<form className="dataset-upload-form" onSubmit={onUploadDataset}> <form className="dataset-upload-form" onSubmit={onUploadDataset}>
<label> <label>
Type Bestandstype
<select <select
value={datasetForm.datasetType} value={datasetForm.datasetType}
onChange={(event) => onDatasetFormChange((previous) => ({ ...previous, datasetType: event.target.value }))} onChange={(event) => onDatasetFormChange((previous) => ({ ...previous, datasetType: event.target.value }))}
@@ -170,7 +171,7 @@ export function DatasetPanel({
</select> </select>
</label> </label>
<label> <label>
Source Bronnaam
<input <input
value={datasetForm.source} value={datasetForm.source}
onChange={(event) => onDatasetFormChange((previous) => ({ ...previous, source: event.target.value }))} onChange={(event) => onDatasetFormChange((previous) => ({ ...previous, source: event.target.value }))}
@@ -178,12 +179,12 @@ export function DatasetPanel({
/> />
</label> </label>
<label> <label>
AOI Gebied
<select <select
value={datasetForm.areaId} value={datasetForm.areaId}
onChange={(event) => onDatasetFormChange((previous) => ({ ...previous, areaId: event.target.value }))} onChange={(event) => onDatasetFormChange((previous) => ({ ...previous, areaId: event.target.value }))}
> >
<option value="">No area</option> <option value="">Volledige bron</option>
{areas.map((area) => ( {areas.map((area) => (
<option key={area.id} value={area.id}> <option key={area.id} value={area.id}>
{area.name} {area.name}
@@ -192,7 +193,7 @@ export function DatasetPanel({
</select> </select>
</label> </label>
<label className="file-input-label"> <label className="file-input-label">
File Bestand
<input <input
type="file" type="file"
accept=".geojson,.json,.tif,.tiff,.geotiff" accept=".geojson,.json,.tif,.tiff,.geotiff"
@@ -200,7 +201,7 @@ export function DatasetPanel({
/> />
</label> </label>
<button className="primary-action" type="submit" disabled={!selectedProjectId}> <button className="primary-action" type="submit" disabled={!selectedProjectId}>
Upload dataset Bron inladen
</button> </button>
</form> </form>
</details> </details>
@@ -215,15 +216,15 @@ export function DatasetPanel({
))} ))}
</div> </div>
{loadingDatasets ? <p>Loading datasets...</p> : null} {loadingDatasets ? <p>Bronnen laden...</p> : null}
{datasets.length === 0 ? ( {datasets.length === 0 ? (
<div className="empty-state"> <div className="empty-state">
<strong>No datasets yet</strong> <strong>Nog geen bronnen beschikbaar</strong>
<p>Upload vector, GeoJSON or raster data after selecting a project.</p> <p>Voeg een vector-, GeoJSON- of rasterbestand toe aan deze werkruimte.</p>
</div> </div>
) : null} ) : null}
<div className="data-panel-list-block dataset-catalog-block"> <div className="data-panel-list-block dataset-catalog-block">
<p className="data-section-label">Available datasets</p> <p className="data-section-label">Beschikbare bronnen</p>
<ul className="dataset-list"> <ul className="dataset-list">
{datasets.map((dataset) => { {datasets.map((dataset) => {
const datasetRole = normalizeDatasetRole(dataset) const datasetRole = normalizeDatasetRole(dataset)
@@ -236,22 +237,16 @@ export function DatasetPanel({
<span className={`dataset-role-badge dataset-role-${datasetRole}`}>{roleLabel}</span> <span className={`dataset-role-badge dataset-role-${datasetRole}`}>{roleLabel}</span>
<span>{dataset.dataset_type}</span> <span>{dataset.dataset_type}</span>
</div> </div>
<strong className="dataset-card-title">{dataset.name}</strong> <strong className="dataset-card-title">{getDatasetDisplayName(dataset)}</strong>
<div className="entity-meta"> <div className="entity-meta dataset-card-primary-meta">
<span>Source: {dataset.source_name ?? dataset.source}</span> <span>{(dataset.feature_count ?? dataset.vector_summary?.feature_count ?? 0).toLocaleString('nl-BE')} objecten</span>
{dataset.reference_layer_name ? <span>Layer: {dataset.reference_layer_name}</span> : null} <span>{dataset.source_name ?? dataset.source}</span>
<span>CRS: {dataset.crs ?? dataset.vector_summary?.crs ?? 'n/a'}</span>
</div> </div>
</div> </div>
<span className={dataset.status === 'ready' ? 'status-badge status-badge-ready' : 'status-badge'}> <span className={dataset.status === 'ready' ? 'status-badge status-badge-ready' : 'status-badge'}>
{dataset.status === 'ready' ? 'ready' : dataset.status === 'failed' ? 'failed' : 'pending'} {dataset.status === 'ready' ? 'beschikbaar' : dataset.status === 'failed' ? 'mislukt' : 'bezig'}
</span> </span>
</div> </div>
<div className="dataset-metrics data-density-grid">
<span>size: {formatBytes(dataset.size_bytes)}</span>
<span>features: {dataset.feature_count ?? dataset.vector_summary?.feature_count ?? 'n/a'}</span>
<span>bbox: {formatBounds(dataset.bounds_json ?? dataset.vector_summary?.bounds_json)}</span>
</div>
<p className="dataset-action-hint">{datasetActionHint(dataset, datasetRole)}</p> <p className="dataset-action-hint">{datasetActionHint(dataset, datasetRole)}</p>
<div className="dataset-action-grid"> <div className="dataset-action-grid">
<button <button
@@ -261,17 +256,17 @@ export function DatasetPanel({
onClick={() => onLoadDatasetDetails(selectedProjectId ?? '', dataset)} onClick={() => onLoadDatasetDetails(selectedProjectId ?? '', dataset)}
data-testid={`dataset-select-${dataset.id}`} data-testid={`dataset-select-${dataset.id}`}
> >
<span>Inspect</span> <span>Bekijken</span>
<small>Select / details</small> <small>Details en acties</small>
</button> </button>
<button <button
className="secondary-action dataset-action-button" className="secondary-action dataset-action-button"
type="button" type="button"
onClick={() => onOpenDatasetInMap(dataset)} onClick={() => onOpenDatasetInMap(dataset)}
aria-label="Open in map" aria-label="Open op kaart"
> >
<span>Map</span> <span>Kaart</span>
<small>Open layer</small> <small>Laag openen</small>
</button> </button>
<button <button
className="secondary-action dataset-action-button" className="secondary-action dataset-action-button"
@@ -279,20 +274,33 @@ export function DatasetPanel({
onClick={() => onOpenDatasetExport(dataset)} onClick={() => onOpenDatasetExport(dataset)}
disabled={!(dataset.dataset_type === 'vector' || dataset.dataset_type === 'geojson')} disabled={!(dataset.dataset_type === 'vector' || dataset.dataset_type === 'geojson')}
> >
<span>Export / QA</span> <span>Downloaden</span>
<small>{dataset.dataset_type === 'vector' || dataset.dataset_type === 'geojson' ? 'Compare or export' : 'Vector/GeoJSON only'}</small> <small>{dataset.dataset_type === 'vector' || dataset.dataset_type === 'geojson' ? 'GeoJSON of rapport' : 'Enkel vectorlagen'}</small>
</button> </button>
</div>
<details className="dataset-technical-details">
<summary>Broninformatie</summary>
<div className="entity-meta">
<span>Bestand: {dataset.name}</span>
<span>Bron: {dataset.source_name ?? dataset.source}</span>
{dataset.reference_layer_name ? <span>Laag: {dataset.reference_layer_name}</span> : null}
<span>CRS: {dataset.crs ?? dataset.vector_summary?.crs ?? 'onbekend'}</span>
</div>
<div className="dataset-metrics data-density-grid">
<span>Grootte: {formatBytes(dataset.size_bytes)}</span>
<span>Objecten: {dataset.feature_count ?? dataset.vector_summary?.feature_count ?? 'onbekend'}</span>
<span>Begrenzing: {formatBounds(dataset.bounds_json ?? dataset.vector_summary?.bounds_json)}</span>
</div>
<button <button
className="secondary-action dataset-action-button" className="secondary-action"
type="button" type="button"
onClick={() => onRefreshMetadata(dataset.id)} onClick={() => onRefreshMetadata(dataset.id)}
disabled={dataset.dataset_type === 'raster'} disabled={dataset.dataset_type === 'raster'}
aria-label="Refresh metadata" aria-label="Bronmetadata vernieuwen"
> >
<span>Metadata</span> Metadata vernieuwen
<small>{dataset.dataset_type === 'raster' ? 'Raster metadata is automatic' : 'Refresh vector stats'}</small>
</button> </button>
</div> </details>
</li> </li>
) )
})} })}
@@ -145,6 +145,14 @@ export function DetectionLab({
}: DetectionLabProps): JSX.Element { }: DetectionLabProps): JSX.Element {
const selectedDetectionModel = detectionModels.find((model) => model.model_id === selectedDetectionModelId) ?? null const selectedDetectionModel = detectionModels.find((model) => model.model_id === selectedDetectionModelId) ?? null
const selectedModelAsset = modelAssets.find((asset) => asset.model_asset_id === selectedModelAssetId) ?? null const selectedModelAsset = modelAssets.find((asset) => asset.model_asset_id === selectedModelAssetId) ?? null
const selectedOperatorProfile = DETECTION_OPERATOR_PROFILES.find(
(profile) => profile.modelAssetId === selectedModelAssetId,
) ?? null
const yoloRuntimeReady = Boolean(
yoloPreflight?.checks.enabled &&
yoloPreflight.checks.dependencies_available &&
yoloPreflight.checks.model_file_exists,
)
const detectionRequiresTileManifest = selectedDetectionModelId === 'yolo-configured' const detectionRequiresTileManifest = selectedDetectionModelId === 'yolo-configured'
const detectionHasDataset = selectedDetectionDatasetId.length > 0 const detectionHasDataset = selectedDetectionDatasetId.length > 0
const detectionHasModel = selectedDetectionModel !== null const detectionHasModel = selectedDetectionModel !== null
@@ -178,19 +186,19 @@ export function DetectionLab({
detectionHasExplicitModelAsset && detectionHasExplicitModelAsset &&
detectionHasTileManifest detectionHasTileManifest
const detectionRunBlockedReason = !selectedProjectId const detectionRunBlockedReason = !selectedProjectId
? 'Select or create a project first' ? 'De regionale werkruimte is nog niet geladen'
: !detectionHasDataset : !detectionHasDataset
? 'Select a raster dataset' ? 'Kies een luchtbeeld'
: !detectionHasModel : !detectionHasModel
? 'Select a detection model' ? 'Kies een analysemodel'
: selectedDetectionModelId === 'manual-fixture-detector' : selectedDetectionModelId === 'manual-fixture-detector'
? 'Fixture model is explicit test/demo-only' ? 'Het fixturemodel is alleen bedoeld voor expliciete tests en demo\'s'
: !detectionModelReady : !detectionModelReady
? selectedDetectionModel?.limitation_message ?? 'Selected model is not configured' ? selectedDetectionModel?.limitation_message ?? 'Het gekozen model is niet geconfigureerd'
: !detectionHasExplicitModelAsset : !detectionHasExplicitModelAsset
? 'Select a local model asset deliberately' ? 'Kies een lokaal modelbestand onder beheer'
: !detectionHasTileManifest : !detectionHasTileManifest
? 'Provide a raster tile manifest for configured YOLO' ? 'Maak eerst beeldtegels voor het gekozen luchtbeeld'
: null : null
const calibrationRunReady = detectionRunReady && detectionReferenceDatasetId.length > 0 && calibrationThresholdText.trim().length > 0 const calibrationRunReady = detectionRunReady && detectionReferenceDatasetId.length > 0 && calibrationThresholdText.trim().length > 0
@@ -198,18 +206,41 @@ export function DetectionLab({
<section className="workspace-panel ai-lab-shell detection-lab-shell"> <section className="workspace-panel ai-lab-shell detection-lab-shell">
<div className="panel-title-row"> <div className="panel-title-row">
<div> <div>
<p className="eyebrow">Object detection</p> <p className="eyebrow">Analyse van luchtbeelden</p>
<h2>Detection Lab</h2> <h2>Gebouwen herkennen</h2>
</div> </div>
<button className="secondary-action" type="button" onClick={onLoadModels} disabled={loadingDetectionModels}> <button className="secondary-action" type="button" onClick={onLoadModels} disabled={loadingDetectionModels}>
Refresh models Status vernieuwen
</button> </button>
</div> </div>
<div className="ai-user-summary" aria-label="Status gebouwdetectie">
<div className="ai-user-summary-card ai-user-summary-card-primary">
<span>Actieve analyse</span>
<strong>Gebouwdetectie</strong>
<p>{selectedOperatorProfile?.displayName ?? selectedModelAsset?.display_name ?? 'Lokaal YOLO-model'}</p>
</div>
<div className={yoloRuntimeReady ? 'ai-user-summary-card ai-user-summary-card-ready' : 'ai-user-summary-card'}>
<span>PyTorch-runtime</span>
<strong>{yoloRuntimeReady ? 'Gereed' : loadingDetectionModels ? 'Controleren...' : 'Niet gereed'}</strong>
<p>{yoloRuntimeReady ? `${yoloPreflight?.runtime.cuda_available ? 'GPU' : 'CPU'} · lokaal model gevonden` : 'Controleer de modelconfiguratie onder beheer.'}</p>
</div>
<div className="ai-user-summary-card">
<span>Gevalideerde kwaliteit</span>
<strong>{selectedOperatorProfile ? `F1 ${selectedOperatorProfile.f1.toFixed(3)}` : 'Nog niet gekoppeld'}</strong>
<p>{selectedOperatorProfile ? `${selectedOperatorProfile.positiveSampleCount} testgebieden · resultaten blijven controleplichtig` : 'Kies het goedgekeurde lokale profiel.'}</p>
</div>
<div className={rasterDatasets.length > 0 ? 'ai-user-summary-card ai-user-summary-card-ready' : 'ai-user-summary-card'}>
<span>Beschikbare luchtbeelden</span>
<strong>{rasterDatasets.length}</strong>
<p>{rasterDatasets.length > 0 ? 'Klaar om een beeld te kiezen.' : 'Laad eerst een gegeorefereerd luchtbeeld in.'}</p>
</div>
</div>
<details className="ai-lab-model-surface" aria-label="Detection model capabilities"> <details className="ai-lab-model-surface" aria-label="Detection model capabilities">
<summary> <summary>
<span>Model registry</span> <span>Technische modelinformatie</span>
<strong>{detectionModels.length} models</strong> <strong>{detectionModels.length} registraties</strong>
</summary> </summary>
<div className="ai-lab-disclosure-body"> <div className="ai-lab-disclosure-body">
<div className="ai-lab-state-stack"> <div className="ai-lab-state-stack">
@@ -257,21 +288,25 @@ export function DetectionLab({
</details> </details>
{selectedDetectionModelId === 'yolo-configured' ? ( {selectedDetectionModelId === 'yolo-configured' ? (
<div className="ai-lab-model-surface" aria-label="Local model asset selection"> <details className="ai-lab-model-surface" aria-label="Local model asset selection">
<summary>
<span>Modelkeuze voor beheerders</span>
<strong>{selectedOperatorProfile?.displayName ?? selectedModelAsset?.display_name ?? 'Geen lokaal model'}</strong>
</summary>
<div className="ai-lab-disclosure-body">
<div className="ai-lab-section-header"> <div className="ai-lab-section-header">
<div> <div>
<h3>Explicit model asset</h3> <h3>Lokaal modelbestand</h3>
<p>Local model assets are read-only. No model file is selected automatically. Choose an existing local model asset before submitting configured YOLO.</p> <p>GeoIntel kiest automatisch het actieve lokale model. Een beheerder kan hier bewust een ander reeds aanwezig, alleen-lezen modelbestand kiezen.</p>
</div> </div>
<span className={selectedModelAsset ? 'status-badge status-badge-ready' : 'status-badge'}> <span className={selectedModelAsset ? 'status-badge status-badge-ready' : 'status-badge'}>
{selectedModelAsset ? 'asset selected' : 'no explicit asset'} {selectedModelAsset ? 'model gekozen' : 'geen model gekozen'}
</span> </span>
</div> </div>
<div className="model-asset-guidance"> <div className="model-asset-guidance">
<strong>Operator profiles</strong> <strong>Gevalideerde profielen</strong>
<p> <p>
Candidate profiles apply a local model asset and confidence threshold only after an explicit click. Een profiel koppelt een lokaal model aan een gemeten zekerheidsdrempel. Een andere keuze geldt alleen voor de huidige analyse en wijzigt de serverconfiguratie niet.
Promoted profiles still require explicit operator action and do not mutate the runtime environment.
</p> </p>
</div> </div>
<div className="operator-profile-grid" aria-label="Configured YOLO operator profiles"> <div className="operator-profile-grid" aria-label="Configured YOLO operator profiles">
@@ -288,22 +323,22 @@ export function DetectionLab({
<div className="operator-profile-card-header"> <div className="operator-profile-card-header">
<strong>{profile.displayName}</strong> <strong>{profile.displayName}</strong>
<span className={profile.defaultApproved ? 'status-badge status-badge-ready' : 'status-badge'}> <span className={profile.defaultApproved ? 'status-badge status-badge-ready' : 'status-badge'}>
{profile.defaultApproved ? 'default-approved' : 'Candidate only - not default-approved'} {profile.defaultApproved ? 'standaardprofiel' : 'kandidaat · extra controle vereist'}
</span> </span>
</div> </div>
<p>{profile.description}</p> <p>{profile.description}</p>
<div className="operator-profile-metrics"> <div className="operator-profile-metrics">
<span>threshold {profile.confidenceThreshold.toFixed(2)}</span> <span>drempel {profile.confidenceThreshold.toFixed(2)}</span>
<span>precision {profile.precision.toFixed(3)}</span> <span>precision {profile.precision.toFixed(3)}</span>
<span>recall {profile.recall.toFixed(3)}</span> <span>recall {profile.recall.toFixed(3)}</span>
<span>F1 {profile.f1.toFixed(3)}</span> <span>F1 {profile.f1.toFixed(3)}</span>
<span>positive AOIs {profile.positiveSampleCount}</span> <span>testgebieden {profile.positiveSampleCount}</span>
<span>max background FP {profile.maxBackgroundDetections}</span> <span>max. achtergrondfouten {profile.maxBackgroundDetections}</span>
</div> </div>
<div className="entity-meta"> <div className="entity-meta">
<span>asset: {profile.modelAssetId}</span> <span>modelbestand: {profile.modelAssetId}</span>
<span>promotionRecommendation: {profile.promotionRecommendation}</span> <span>beoordeling: {profile.promotionRecommendation}</span>
<span>available: {profileAsset ? 'yes' : 'not mounted'}</span> <span>beschikbaar: {profileAsset ? 'ja' : 'niet gekoppeld'}</span>
</div> </div>
<p className="field-guidance">{profile.limitationMessage}</p> <p className="field-guidance">{profile.limitationMessage}</p>
<button <button
@@ -312,7 +347,7 @@ export function DetectionLab({
onClick={() => onApplyOperatorProfile(profile)} onClick={() => onApplyOperatorProfile(profile)}
disabled={!profileAsset} disabled={!profileAsset}
> >
Apply profile Profiel gebruiken
</button> </button>
</div> </div>
) )
@@ -320,17 +355,16 @@ export function DetectionLab({
</div> </div>
{selectedModelAsset ? ( {selectedModelAsset ? (
<div className="model-asset-guidance"> <div className="model-asset-guidance">
<strong>Selected model asset status</strong> <strong>Status gekozen model</strong>
<p> <p>
{selectedModelAsset.display_name} is selected for this browser-run request. Runtime default activation {selectedModelAsset.display_name} wordt voor deze analyse gebruikt. De standaard serverconfiguratie blijft ongewijzigd.
remains a separate guarded operator action backed by a promotion report.
</p> </p>
</div> </div>
) : null} ) : null}
<label> <label>
Local model file Lokaal modelbestand
<select value={selectedModelAssetId} onChange={(event) => onSelectModelAsset(event.target.value)}> <select value={selectedModelAssetId} onChange={(event) => onSelectModelAsset(event.target.value)}>
<option value="">Select explicit local model asset</option> <option value="">Kies een lokaal modelbestand</option>
{modelAssets.map((asset) => ( {modelAssets.map((asset) => (
<option key={asset.model_asset_id} value={asset.model_asset_id}> <option key={asset.model_asset_id} value={asset.model_asset_id}>
{asset.display_name} {asset.active ? '(active)' : ''} {asset.display_name} {asset.active ? '(active)' : ''}
@@ -340,8 +374,8 @@ export function DetectionLab({
</label> </label>
{modelAssets.length === 0 && !loadingDetectionModels ? ( {modelAssets.length === 0 && !loadingDetectionModels ? (
<div className="result-state result-state-empty"> <div className="result-state result-state-empty">
<strong>No local model assets found.</strong> <strong>Geen lokaal modelbestand gevonden.</strong>
<p>Mount model files into the backend model directory or continue with the configured YOLO_MODEL_PATH.</p> <p>Plaats een gecontroleerd model in de modelmap of configureer het bestaande YOLO-modelpad.</p>
</div> </div>
) : null} ) : null}
{selectedModelAsset ? ( {selectedModelAsset ? (
@@ -357,12 +391,13 @@ export function DetectionLab({
</div> </div>
) : null} ) : null}
</div> </div>
</details>
) : null} ) : null}
<details className="ai-lab-model-surface" aria-label="YOLO runtime preflight"> <details className="ai-lab-model-surface" aria-label="YOLO runtime preflight">
<summary> <summary>
<span>YOLO runtime preflight</span> <span>Technische runtimecontrole</span>
<strong>{yoloPreflight?.status ?? 'not loaded'}</strong> <strong>{yoloRuntimeReady ? 'gereed' : yoloPreflight?.status ?? 'niet geladen'}</strong>
</summary> </summary>
<div className="ai-lab-disclosure-body"> <div className="ai-lab-disclosure-body">
<div className="ai-lab-section-header"> <div className="ai-lab-section-header">
@@ -447,72 +482,72 @@ export function DetectionLab({
<div className="lab-block"> <div className="lab-block">
<div className="ai-lab-run-surface" aria-label="Detection run controls"> <div className="ai-lab-run-surface" aria-label="Detection run controls">
<h3>Run detection</h3> <h3>Nieuwe beeldanalyse</h3>
<div <div
className={detectionRunReady ? 'lab-readiness-panel lab-readiness-panel-ready' : 'lab-readiness-panel'} className={detectionRunReady ? 'lab-readiness-panel lab-readiness-panel-ready' : 'lab-readiness-panel'}
aria-label="Detection run readiness" aria-label="Detection run readiness"
> >
<div className="ai-lab-section-header"> <div className="ai-lab-section-header">
<div> <div>
<h3>Run readiness</h3> <h3>Wat is nog nodig?</h3>
<p>Checks the selected dataset, model and tile manifest before submitting a detection job.</p> <p>De analyse start zodra een luchtbeeld en de bijbehorende beeldtegels beschikbaar zijn.</p>
</div> </div>
<span className={detectionRunReady ? 'status-badge status-badge-ready' : 'status-badge'}> <span className={detectionRunReady ? 'status-badge status-badge-ready' : 'status-badge'}>
{detectionRunReady ? 'Ready to submit' : 'Blocked'} {detectionRunReady ? 'Klaar om te starten' : 'Nog niet startklaar'}
</span> </span>
</div> </div>
<div className="lab-readiness-grid"> <div className="lab-readiness-grid">
<div className={detectionHasDataset ? 'lab-readiness-item lab-readiness-item-ready' : 'lab-readiness-item'}> <div className={detectionHasDataset ? 'lab-readiness-item lab-readiness-item-ready' : 'lab-readiness-item'}>
<span>Raster dataset</span> <span>Luchtbeeld</span>
<strong>{detectionHasDataset ? 'Selected' : 'Select a raster dataset'}</strong> <strong>{detectionHasDataset ? 'Geselecteerd' : 'Nog geen luchtbeeld'}</strong>
</div> </div>
<div className={detectionModelReady ? 'lab-readiness-item lab-readiness-item-ready' : 'lab-readiness-item'}> <div className={detectionModelReady ? 'lab-readiness-item lab-readiness-item-ready' : 'lab-readiness-item'}>
<span>Model availability</span> <span>Analysemodel</span>
<strong> <strong>
{detectionModelReady {detectionModelReady
? `${selectedDetectionModel?.display_name ?? selectedDetectionModelId} is configured` ? 'Lokaal gebouwmodel beschikbaar'
: selectedDetectionModel?.limitation_message ?? 'Select a configured model'} : 'Model niet beschikbaar'}
</strong> </strong>
</div> </div>
<div className={detectionHasTileManifest ? 'lab-readiness-item lab-readiness-item-ready' : 'lab-readiness-item'}> <div className={detectionHasTileManifest ? 'lab-readiness-item lab-readiness-item-ready' : 'lab-readiness-item'}>
<span>Tile manifest</span> <span>Beeldtegels</span>
<strong> <strong>
{detectionRequiresTileManifest {detectionRequiresTileManifest
? detectionHasTileManifest ? detectionHasTileManifest
? 'Provided for configured YOLO' ? 'Beschikbaar'
: 'Required for configured YOLO' : 'Maak eerst tegels vanuit het luchtbeeld'
: 'Not required for this model'} : 'Niet vereist'}
</strong> </strong>
</div> </div>
<div className={detectionHasExplicitModelAsset ? 'lab-readiness-item lab-readiness-item-ready' : 'lab-readiness-item'}> <div className={detectionHasExplicitModelAsset ? 'lab-readiness-item lab-readiness-item-ready' : 'lab-readiness-item'}>
<span>Local model asset</span> <span>Lokale modelkeuze</span>
<strong> <strong>
{selectedDetectionModelId === 'yolo-configured' {selectedDetectionModelId === 'yolo-configured'
? selectedModelAsset ? selectedModelAsset
? selectedModelAsset.display_name ? selectedOperatorProfile?.displayName ?? selectedModelAsset.display_name
: modelAssets.length > 0 : modelAssets.length > 0
? 'Select a local model asset deliberately' ? 'Kies een lokaal model onder beheer'
: 'No local assets reported; backend configured path only' : 'Geen lokaal model gevonden'
: 'Not required for this model'} : 'Niet vereist'}
</strong> </strong>
</div> </div>
</div> </div>
</div> </div>
<div className={detectionRunReady ? 'lab-action-guardrail lab-action-guardrail-ready' : 'lab-action-guardrail'}> <div className={detectionRunReady ? 'lab-action-guardrail lab-action-guardrail-ready' : 'lab-action-guardrail'}>
<span>Run action</span> <span>Analyse</span>
<strong>{detectionRunReady ? 'Ready to submit a detection job' : detectionRunBlockedReason}</strong> <strong>{detectionRunReady ? 'Klaar om gebouwen te zoeken' : detectionRunBlockedReason}</strong>
</div> </div>
{rasterDatasets.length === 0 ? ( {rasterDatasets.length === 0 ? (
<div className="result-state result-state-empty"> <div className="result-state result-state-empty">
<strong>No raster datasets available for detection.</strong> <strong>Geen luchtbeeld beschikbaar in deze werkruimte.</strong>
<p>Upload or select a raster dataset in Data before running object detection.</p> <p>Voeg onder Bronnen een gegeorefereerde GeoTIFF toe. Daarna kan GeoIntel er beeldtegels en een detectierun van maken.</p>
</div> </div>
) : null} ) : null}
<div className="lab-form-grid"> <div className="lab-form-grid">
<label> <label>
Raster dataset Luchtbeeld
<select value={selectedDetectionDatasetId} onChange={(event) => onSelectDataset(event.target.value)}> <select value={selectedDetectionDatasetId} onChange={(event) => onSelectDataset(event.target.value)}>
<option value="">Select raster dataset</option> <option value="">Kies een luchtbeeld</option>
{rasterDatasets.map((dataset) => ( {rasterDatasets.map((dataset) => (
<option key={dataset.id} value={dataset.id}> <option key={dataset.id} value={dataset.id}>
{dataset.name} {dataset.name}
@@ -521,7 +556,7 @@ export function DetectionLab({
</select> </select>
</label> </label>
<label> <label>
Model Analysemodel
<select value={selectedDetectionModelId} onChange={(event) => onSelectModel(event.target.value)}> <select value={selectedDetectionModelId} onChange={(event) => onSelectModel(event.target.value)}>
{detectionModels.map((model) => ( {detectionModels.map((model) => (
<option key={model.model_id} value={model.model_id}> <option key={model.model_id} value={model.model_id}>
@@ -531,7 +566,7 @@ export function DetectionLab({
</select> </select>
</label> </label>
<label> <label>
Min confidence Minimale zekerheid
<input <input
type="number" type="number"
min="0" min="0"
@@ -542,14 +577,14 @@ export function DetectionLab({
/> />
{selectedDetectionModelId === 'yolo-configured' ? ( {selectedDetectionModelId === 'yolo-configured' ? (
<span className="field-guidance"> <span className="field-guidance">
Use a validated operator profile, or enter a threshold manually for calibration. Het actieve gevalideerde profiel gebruikt standaard 0,15 voor een evenwichtige gebouwcontrole.
</span> </span>
) : null} ) : null}
</label> </label>
</div> </div>
{selectedDetectionModelId === 'yolo-configured' ? ( {selectedDetectionModelId === 'yolo-configured' ? (
<label> <label>
Tile manifest Beeldtegelbestand
<input <input
type="text" type="text"
placeholder="Raster tile manifest path" placeholder="Raster tile manifest path"
@@ -560,13 +595,13 @@ export function DetectionLab({
) : null} ) : null}
{selectedDetectionModelId === 'yolo-configured' && detectionTileManifestPath.trim() ? ( {selectedDetectionModelId === 'yolo-configured' && detectionTileManifestPath.trim() ? (
<div className="linked-manifest-card"> <div className="linked-manifest-card">
<strong>Linked tile manifest</strong> <strong>Gekoppelde beeldtegels</strong>
<p>{detectionTileManifestPath}</p> <p>{detectionTileManifestPath}</p>
<span>Refresh preflight after changing model asset or manifest path.</span> <span>De technische controle wordt vernieuwd wanneer het model of tegelbestand wijzigt.</span>
</div> </div>
) : null} ) : null}
<button className="primary-action" type="button" onClick={onRunDetection} disabled={runningDetection || !detectionRunReady}> <button className="primary-action" type="button" onClick={onRunDetection} disabled={runningDetection || !detectionRunReady}>
Run detection Zoek gebouwen
</button> </button>
</div> </div>
</div> </div>
@@ -590,7 +625,12 @@ export function DetectionLab({
) : null} ) : null}
</div> </div>
<div className="ai-lab-run-surface guided-calibration-surface" aria-label="Guided calibration runner"> <details className="ai-lab-model-surface guided-calibration-surface" aria-label="Guided calibration runner">
<summary>
<span>Modelkalibratie voor beheerders</span>
<strong>{detectionCalibrationRows.length > 0 ? `${detectionCalibrationRows.length} drempels getest` : 'gesloten'}</strong>
</summary>
<div className="ai-lab-disclosure-body">
<div className="ai-lab-section-header"> <div className="ai-lab-section-header">
<div> <div>
<h3>Guided calibration runner</h3> <h3>Guided calibration runner</h3>
@@ -706,22 +746,23 @@ export function DetectionLab({
</div> </div>
)} )}
</div> </div>
</details>
<div className="ai-lab-results-surface" aria-label="Detection results"> <div className="ai-lab-results-surface" aria-label="Detection results">
<div className="panel-title-row"> <div className="panel-title-row">
<div> <div>
<h3>Detection results</h3> <h3>Gevonden objecten</h3>
<p className="muted">Load persisted detections and filter by class or confidence.</p> <p className="muted">Bekijk eerder bewaarde analyses en filter op type of zekerheid.</p>
</div> </div>
<button className="secondary-action" type="button" onClick={onLoadRuns} disabled={!selectedProjectId}> <button className="secondary-action" type="button" onClick={onLoadRuns} disabled={!selectedProjectId}>
Refresh runs Analyses vernieuwen
</button> </button>
</div> </div>
<div className="lab-form-grid"> <div className="lab-form-grid">
<label> <label>
Run Analyse
<select value={selectedDetectionRunId} onChange={(event) => onSelectRun(event.target.value)}> <select value={selectedDetectionRunId} onChange={(event) => onSelectRun(event.target.value)}>
<option value="">Select detection run</option> <option value="">Kies een bewaarde analyse</option>
{detectionRuns.map((run) => ( {detectionRuns.map((run) => (
<option key={run.id} value={run.id}> <option key={run.id} value={run.id}>
{run.model_name || 'detection'} - {run.status} - {run.id} {run.model_name || 'detection'} - {run.status} - {run.id}
@@ -730,16 +771,16 @@ export function DetectionLab({
</select> </select>
</label> </label>
<label> <label>
Class Type object
<input <input
type="text" type="text"
placeholder="Class filter" placeholder="bijvoorbeeld building"
value={detectionClassFilter} value={detectionClassFilter}
onChange={(event) => onSetClassFilter(event.target.value)} onChange={(event) => onSetClassFilter(event.target.value)}
/> />
</label> </label>
<label> <label>
Min confidence Minimale zekerheid
<input <input
type="number" type="number"
min="0" min="0"
@@ -751,18 +792,18 @@ export function DetectionLab({
</label> </label>
</div> </div>
<button className="primary-action" type="button" onClick={onLoadResults} disabled={!selectedDetectionRunId || loadingDetectionResults}> <button className="primary-action" type="button" onClick={onLoadResults} disabled={!selectedDetectionRunId || loadingDetectionResults}>
Load detections Resultaten laden
</button> </button>
{loadingDetectionResults ? ( {loadingDetectionResults ? (
<div className="result-state result-state-loading"> <div className="result-state result-state-loading">
<strong>Loading detection results.</strong> <strong>Resultaten laden.</strong>
<p>Retrieving persisted detections for the selected run.</p> <p>De bewaarde objecten van deze analyse worden opgehaald.</p>
</div> </div>
) : null} ) : null}
<div className="ai-lab-state-stack"> <div className="ai-lab-state-stack">
<div className="result-state result-state-ready"> <div className="result-state result-state-ready">
<strong>Detections loaded: {detectionItems.length}</strong> <strong>{detectionItems.length} objecten geladen</strong>
<p>{selectedDetectionRunId ? 'Loaded from persisted detection records.' : 'Select a detection run before loading results.'}</p> <p>{selectedDetectionRunId ? 'Deze resultaten zijn bewaard in de database.' : 'Kies eerst een bewaarde analyse.'}</p>
</div> </div>
</div> </div>
{detectionItems.length > 0 ? ( {detectionItems.length > 0 ? (
@@ -98,44 +98,45 @@ export function ExportCenter({
const canExportDataset = Boolean(selectedDataset && isVectorDatasetType(selectedDataset.dataset_type)) const canExportDataset = Boolean(selectedDataset && isVectorDatasetType(selectedDataset.dataset_type))
const geojsonExports = exports.filter((item) => item.export_type.includes('geojson')).length const geojsonExports = exports.filter((item) => item.export_type.includes('geojson')).length
const reportExports = exports.filter((item) => item.export_type === 'project_report_html').length const reportExports = exports.filter((item) => item.export_type === 'project_report_html').length
const handoffStatus = selectedProjectId ? 'project selected' : 'select project' const handoffStatus = selectedProjectId ? 'werkruimte gekozen' : 'kies een werkruimte'
const exportTypes = useMemo(() => Array.from(new Set(exports.map((item) => item.export_type))).sort(), [exports]) const exportTypes = useMemo(() => Array.from(new Set(exports.map((item) => item.export_type))).sort(), [exports])
const exportStatuses = useMemo(() => Array.from(new Set(exports.map((item) => item.status))).sort(), [exports]) const exportStatuses = useMemo(() => Array.from(new Set(exports.map((item) => item.status))).sort(), [exports])
const latestHandoffArtifacts = useMemo( const latestHandoffArtifacts = useMemo(
() => [ () => [
{ {
key: 'project_report_html', key: 'project_report_html',
label: 'Project report', label: 'Leesbaar rapport',
detail: 'HTML report for human review', detail: 'HTML-rapport om te bekijken en te delen',
item: getLatestExportByType(exports, 'project_report_html'), item: getLatestExportByType(exports, 'project_report_html'),
}, },
{ {
key: 'project_metadata_json', key: 'project_metadata_json',
label: 'Project metadata', label: 'Werkruimtedata',
detail: 'Machine-readable project inventory', detail: 'Technische inventaris in JSON-formaat',
item: getLatestExportByType(exports, 'project_metadata_json'), item: getLatestExportByType(exports, 'project_metadata_json'),
}, },
{ {
key: 'dataset_geojson', key: 'dataset_geojson',
label: 'Dataset GeoJSON', label: 'Kaartlaag (GeoJSON)',
detail: 'Selected vector/reference layer', detail: 'De geselecteerde vector- of referentielaag',
item: getLatestExportByType(exports, 'dataset_geojson'), item: getLatestExportByType(exports, 'dataset_geojson'),
}, },
{ {
key: 'detection_geojson', key: 'detection_geojson',
label: 'Detection GeoJSON', label: 'Herkende gebouwen (GeoJSON)',
detail: 'Persisted detection geometries', detail: 'Bewaarde geometrieën uit beeldanalyse',
item: getLatestExportByType(exports, 'detection_geojson'), item: getLatestExportByType(exports, 'detection_geojson'),
}, },
{ {
key: 'segmentation_geojson', key: 'segmentation_geojson',
label: 'Segmentation GeoJSON', label: 'Segmentaties (GeoJSON)',
detail: 'Persisted segmentation polygons', detail: 'Bewaarde segmentatiepolygonen',
item: getLatestExportByType(exports, 'segmentation_geojson'), item: getLatestExportByType(exports, 'segmentation_geojson'),
}, },
], ],
[exports], [exports],
) )
const availableLatestArtifacts = latestHandoffArtifacts.filter((artifact) => artifact.item)
const filteredExports = useMemo( const filteredExports = useMemo(
() => () =>
exports.filter((item) => { exports.filter((item) => {
@@ -154,10 +155,10 @@ export function ExportCenter({
<div className="export-center-shell"> <div className="export-center-shell">
<div className="panel-title-row"> <div className="panel-title-row">
<div> <div>
<h2>Export Center</h2> <h2>Downloads en rapporten</h2>
<p className="muted">Create handoff artifacts from persisted datasets, AI runs and project metadata.</p> <p className="muted">Bewaar de geselecteerde kaartlaag, analyseresultaten of een leesbaar projectrapport.</p>
</div> </div>
<span className="count-pill">{exports.length} exports</span> <span className="count-pill">{exports.length} bestanden</span>
</div> </div>
<div className="export-summary-surface" aria-label="Export summary"> <div className="export-summary-surface" aria-label="Export summary">
@@ -167,12 +168,12 @@ export function ExportCenter({
<strong>{geojsonExports}</strong> <strong>{geojsonExports}</strong>
</div> </div>
<div> <div>
<span>Reports</span> <span>Rapporten</span>
<strong>{reportExports}</strong> <strong>{reportExports}</strong>
</div> </div>
<div> <div>
<span>Selected dataset</span> <span>Geselecteerde kaartlaag</span>
<strong>{selectedDataset?.name ?? 'none'}</strong> <strong>{selectedDataset?.name ?? 'geen'}</strong>
</div> </div>
</div> </div>
</div> </div>
@@ -188,20 +189,20 @@ export function ExportCenter({
</div> </div>
<div className="handoff-readiness-grid"> <div className="handoff-readiness-grid">
<div> <div>
<span>Vector dataset</span> <span>Vectorkaartlaag</span>
<strong>{canExportDataset ? selectedDataset?.name : 'none ready'}</strong> <strong>{canExportDataset ? selectedDataset?.name : 'geen beschikbaar'}</strong>
</div> </div>
<div> <div>
<span>Detection run</span> <span>Gebouwanalyse</span>
<strong>{selectedDetectionRunId || 'none'}</strong> <strong>{selectedDetectionRunId || 'geen'}</strong>
</div> </div>
<div> <div>
<span>Segmentation run</span> <span>Segmentatieanalyse</span>
<strong>{selectedSegmentationRunId || 'none'}</strong> <strong>{selectedSegmentationRunId || 'geen'}</strong>
</div> </div>
<div> <div>
<span>Latest handoff artifact</span> <span>Laatste bestand</span>
<strong>{latestExport ? formatExportType(latestExport.export_type) : 'none'}</strong> <strong>{latestExport ? formatExportType(latestExport.export_type) : 'geen'}</strong>
</div> </div>
</div> </div>
</div> </div>
@@ -209,16 +210,16 @@ export function ExportCenter({
<div className="latest-artifact-surface" aria-label="Latest handoff artifacts"> <div className="latest-artifact-surface" aria-label="Latest handoff artifacts">
<div> <div>
<h3>Latest handoff artifacts</h3> <h3>Laatste downloads</h3>
<p className="muted">Open the newest artifact of each handoff type without searching the full export history.</p> <p className="muted">Open de recentste bestanden zonder in de volledige geschiedenis te zoeken.</p>
</div> </div>
<div className="latest-artifact-grid"> <div className="latest-artifact-grid">
{latestHandoffArtifacts.map((artifact) => { {availableLatestArtifacts.map((artifact) => {
const item = artifact.item const item = artifact.item
return ( return (
<div className={item ? 'latest-artifact-card' : 'latest-artifact-card latest-artifact-card-empty'} key={artifact.key}> <div className={item ? 'latest-artifact-card' : 'latest-artifact-card latest-artifact-card-empty'} key={artifact.key}>
<span>{artifact.label}</span> <span>{artifact.label}</span>
<strong>{item ? item.status : 'No artifact yet'}</strong> <strong>{item ? (item.status === 'ready' ? 'Beschikbaar' : item.status) : 'Nog niet gemaakt'}</strong>
<p>{item ? compactPath(item.storage_path) : artifact.detail}</p> <p>{item ? compactPath(item.storage_path) : artifact.detail}</p>
{item ? ( {item ? (
<> <>
@@ -226,13 +227,13 @@ export function ExportCenter({
<div className="latest-artifact-actions"> <div className="latest-artifact-actions">
{canPreviewJson(item) ? ( {canPreviewJson(item) ? (
<button type="button" className="secondary-action" onClick={() => onPreviewContent(item.id)}> <button type="button" className="secondary-action" onClick={() => onPreviewContent(item.id)}>
Preview Bekijken
</button> </button>
) : ( ) : (
<span className="status-badge">download-only</span> <span className="status-badge">alleen downloaden</span>
)} )}
<button type="button" className="primary-action" onClick={() => onDownload(item.id)}> <button type="button" className="primary-action" onClick={() => onDownload(item.id)}>
Download Downloaden
</button> </button>
</div> </div>
</> </>
@@ -240,19 +241,25 @@ export function ExportCenter({
</div> </div>
) )
})} })}
{availableLatestArtifacts.length === 0 ? (
<div className="result-state result-state-empty latest-download-empty">
<strong>Nog geen downloads gemaakt</strong>
<p>Kies hieronder een rapport of de actieve kaartlaag.</p>
</div>
) : null}
</div> </div>
</div> </div>
<div className="export-actions-surface" aria-label="Export artifact actions"> <div className="export-actions-surface" aria-label="Export artifact actions">
<div className="panel-title-row"> <div className="panel-title-row">
<div> <div>
<h3>Artifact actions</h3> <h3>Wat wil je bewaren?</h3>
<p className="muted">Create review artifacts from the current project, dataset or selected AI analysis run.</p> <p className="muted">Maak een leesbaar rapport of download gegevens voor verder gebruik.</p>
</div> </div>
</div> </div>
<div className="export-action-grid"> <div className="export-action-grid">
<div className="handoff-action-grid"> <div className="handoff-action-grid">
<div className="handoff-action-card"> <div className="handoff-action-card handoff-action-card-technical">
<span>Refresh</span> <span>Refresh</span>
<strong>Export registry</strong> <strong>Export registry</strong>
<p>Reload the persisted artifact list for the selected project.</p> <p>Reload the persisted artifact list for the selected project.</p>
@@ -267,17 +274,17 @@ export function ExportCenter({
</button> </button>
</div> </div>
<div className="handoff-action-card handoff-action-card-primary"> <div className="handoff-action-card handoff-action-card-primary">
<span>Project report</span> <span>Rapport</span>
<strong>HTML summary</strong> <strong>Leesbaar projectoverzicht</strong>
<p>Generate the human-readable V1 handoff report from persisted project state.</p> <p>Maak een HTML-rapport met de beschikbare bronnen, controles en resultaten.</p>
<button type="button" className="primary-action" onClick={onExportProjectReport} disabled={!selectedProjectId || exporting}> <button type="button" className="primary-action" onClick={onExportProjectReport} disabled={!selectedProjectId || exporting}>
Export project report HTML Projectrapport maken
</button> </button>
</div> </div>
<div className="handoff-action-card"> <div className="handoff-action-card">
<span>Metadata</span> <span>Technische gegevens</span>
<strong>Project JSON</strong> <strong>Projectgegevens (JSON)</strong>
<p>Export project, dataset, QA and artifact metadata for machine-readable review.</p> <p>Download de volledige projectinventaris voor geautomatiseerde verwerking.</p>
<button <button
type="button" type="button"
className="secondary-action" className="secondary-action"
@@ -285,33 +292,33 @@ export function ExportCenter({
disabled={!selectedProjectId || exporting} disabled={!selectedProjectId || exporting}
data-testid="export-project-metadata" data-testid="export-project-metadata"
> >
Export project metadata JSON Projectgegevens downloaden
</button> </button>
</div> </div>
<div className="handoff-action-card"> <div className="handoff-action-card">
<span>Vector dataset</span> <span>Kaartlaag</span>
<strong>Selected GeoJSON</strong> <strong>Geselecteerde bron (GeoJSON)</strong>
<p>Export the active vector/reference dataset when it is selected and ready.</p> <p>Download de actieve vector- of referentielaag.</p>
<button type="button" className="secondary-action" onClick={onExportDataset} disabled={!canExportDataset || exporting}> <button type="button" className="secondary-action" onClick={onExportDataset} disabled={!canExportDataset || exporting}>
Export selected vector GeoJSON Kaartlaag downloaden
</button> </button>
</div> </div>
<div className="handoff-action-card"> {selectedDetectionRunId ? <div className="handoff-action-card">
<span>Detection run</span> <span>Detection run</span>
<strong>Detections GeoJSON</strong> <strong>Detections GeoJSON</strong>
<p>Export persisted detection geometries for the selected analysis run.</p> <p>Export persisted detection geometries for the selected analysis run.</p>
<button type="button" className="secondary-action" onClick={onExportDetectionRun} disabled={!selectedDetectionRunId || exporting}> <button type="button" className="secondary-action" onClick={onExportDetectionRun} disabled={!selectedDetectionRunId || exporting}>
Export selected detection run GeoJSON Export selected detection run GeoJSON
</button> </button>
</div> </div> : null}
<div className="handoff-action-card"> {selectedSegmentationRunId ? <div className="handoff-action-card">
<span>Segmentation run</span> <span>Segmentation run</span>
<strong>Segmentations GeoJSON</strong> <strong>Segmentations GeoJSON</strong>
<p>Export persisted segmentation polygons for the selected analysis run.</p> <p>Export persisted segmentation polygons for the selected analysis run.</p>
<button type="button" className="secondary-action" onClick={onExportSegmentationRun} disabled={!selectedSegmentationRunId || exporting}> <button type="button" className="secondary-action" onClick={onExportSegmentationRun} disabled={!selectedSegmentationRunId || exporting}>
Export selected segmentation run GeoJSON Export selected segmentation run GeoJSON
</button> </button>
</div> </div> : null}
</div> </div>
</div> </div>
</div> </div>
@@ -344,7 +351,12 @@ export function ExportCenter({
) : null} ) : null}
</div> </div>
<div className="export-history-surface" aria-label="Export history"> <details className="export-history-surface export-history-disclosure" aria-label="Export history">
<summary>
<span>Downloadgeschiedenis</span>
<strong>{exports.length} bestanden</strong>
</summary>
<div className="export-history-body">
<div> <div>
<h3>Export history</h3> <h3>Export history</h3>
<p className="muted">Review persisted artifacts and open JSON previews where supported.</p> <p className="muted">Review persisted artifacts and open JSON previews where supported.</p>
@@ -445,6 +457,7 @@ export function ExportCenter({
))} ))}
</ul> </ul>
</div> </div>
</details>
</div> </div>
</section> </section>
) )
+10 -27
View File
@@ -4,6 +4,7 @@ import type { AreaRead, DatasetCreateResponse, MapViewportState, ProjectRead, Qa
import { featureCollectionBounds } from '../../lib/geojsonBounds' import { featureCollectionBounds } from '../../lib/geojsonBounds'
import { useMapThemeSelectionInsights } from '../../hooks/useMapThemeSelectionInsights' import { useMapThemeSelectionInsights } from '../../hooks/useMapThemeSelectionInsights'
import { useTemporalComparison } from '../../hooks/useTemporalComparison' import { useTemporalComparison } from '../../hooks/useTemporalComparison'
import { getDatasetDisplayName } from '../../lib/datasetDisplay'
const DEFAULT_SELECTED_FEATURE_FILENAME = 'selected-feature.geojson' const DEFAULT_SELECTED_FEATURE_FILENAME = 'selected-feature.geojson'
const DEFAULT_AREA_SELECTION_FILENAME = 'area-selection.geojson' const DEFAULT_AREA_SELECTION_FILENAME = 'area-selection.geojson'
@@ -443,7 +444,6 @@ interface MapWorkspaceProps {
availableMapDatasets: DatasetCreateResponse[] availableMapDatasets: DatasetCreateResponse[]
selectedMapDatasetId: string selectedMapDatasetId: string
onSelectMapArea: (areaId: string) => void onSelectMapArea: (areaId: string) => void
onSelectProject: (projectId: string) => void
onOpenDatasetInMap: (dataset: DatasetCreateResponse) => void onOpenDatasetInMap: (dataset: DatasetCreateResponse) => void
onSetAreaLayerVisible: (visible: boolean) => void onSetAreaLayerVisible: (visible: boolean) => void
onSetAreaLayerOpacity: (opacity: number) => void onSetAreaLayerOpacity: (opacity: number) => void
@@ -512,7 +512,6 @@ export function MapWorkspace({
availableMapDatasets, availableMapDatasets,
selectedMapDatasetId, selectedMapDatasetId,
onSelectMapArea, onSelectMapArea,
onSelectProject,
onOpenDatasetInMap, onOpenDatasetInMap,
onSetAreaLayerVisible, onSetAreaLayerVisible,
onSetAreaLayerOpacity, onSetAreaLayerOpacity,
@@ -590,16 +589,9 @@ export function MapWorkspace({
const activeTheme = DATA_THEMES.find((theme) => theme.id === activeThemeId) ?? DATA_THEMES[0] const activeTheme = DATA_THEMES.find((theme) => theme.id === activeThemeId) ?? DATA_THEMES[0]
const activeThemeMapStyle = DATA_THEME_MAP_STYLES[activeTheme.id] const activeThemeMapStyle = DATA_THEME_MAP_STYLES[activeTheme.id]
const activeThemeDataset = themeDatasetMap[activeTheme.id] const activeThemeDataset = themeDatasetMap[activeTheme.id]
const operationalScopeProjects = useMemo(() => {
const scoped = projects.filter((project) => project.name === MOL_PROJECT_NAME || project.name === KEMPEN_PROJECT_NAME)
const selected = projects.find((project) => project.id === selectedProjectId)
if (selected && !scoped.some((project) => project.id === selected.id)) {
return [selected, ...scoped]
}
return scoped
}, [projects, selectedProjectId])
const activeScopeProject = projects.find((project) => project.id === selectedProjectId) ?? null const activeScopeProject = projects.find((project) => project.id === selectedProjectId) ?? null
const activeScopeLabel = activeScopeProject ? operationalScopeProjectLabel(activeScopeProject) : 'Werkgebied' const activeScopeLabel = activeScopeProject ? operationalScopeProjectLabel(activeScopeProject) : 'Werkgebied'
const municipalityAreaCount = areas.filter((area) => /^Gemeente\s/i.test(area.name)).length
const activeTemporalSeriesGroups = useMemo( const activeTemporalSeriesGroups = useMemo(
() => listThemeTemporalSeries(availableMapDatasets, activeTheme), () => listThemeTemporalSeries(availableMapDatasets, activeTheme),
[activeTheme, availableMapDatasets], [activeTheme, availableMapDatasets],
@@ -964,19 +956,11 @@ export function MapWorkspace({
<p>Dit zijn databronnen, geen AI-modellen.</p> <p>Dit zijn databronnen, geen AI-modellen.</p>
</div> </div>
</div> </div>
<label className="geo-project-scope-select"> <div className="geo-loaded-scope" aria-label="Ingeladen regiobereik">
Regio <span>Ingeladen bereik</span>
<select <strong>{activeScopeLabel}</strong>
aria-label="Regio" <small>{municipalityAreaCount > 0 ? `${municipalityAreaCount} gemeenten en de volledige regio beschikbaar` : 'Regionale gegevens worden geladen'}</small>
value={selectedProjectId ?? ''} </div>
onChange={(event) => onSelectProject(event.target.value)}
disabled={operationalScopeProjects.length < 2}
>
{operationalScopeProjects.map((project) => (
<option key={project.id} value={project.id}>{operationalScopeProjectLabel(project)}</option>
))}
</select>
</label>
<div className="geo-theme-list"> <div className="geo-theme-list">
{DATA_THEMES.map((theme) => { {DATA_THEMES.map((theme) => {
const dataset = themeDatasetMap[theme.id] const dataset = themeDatasetMap[theme.id]
@@ -1006,7 +990,7 @@ export function MapWorkspace({
<strong> <strong>
{analysisMode === 'evolution' {analysisMode === 'evolution'
? activeTemporalSeriesGroup?.label ?? 'Geen tijdreeks beschikbaar' ? activeTemporalSeriesGroup?.label ?? 'Geen tijdreeks beschikbaar'
: activeThemeDataset?.name ?? 'Geen databron beschikbaar'} : activeThemeDataset ? getDatasetDisplayName(activeThemeDataset) : 'Geen databron beschikbaar'}
</strong> </strong>
<small> <small>
{analysisMode === 'evolution' {analysisMode === 'evolution'
@@ -1065,9 +1049,8 @@ export function MapWorkspace({
) : null} ) : null}
<label className="geo-scope-select"> <label className="geo-scope-select">
Werkgebied Gemeente of volledige regio
<select aria-label="Werkgebied" value={selectedMapAreaId} onChange={(event) => handleSelectMapArea(event.target.value)} disabled={areas.length === 0}> <select aria-label="Werkgebied" value={selectedMapAreaId} onChange={(event) => handleSelectMapArea(event.target.value)} disabled={areas.length === 0}>
<option value="">Geen werkgebied</option>
{areas.map((area) => ( {areas.map((area) => (
<option key={area.id} value={area.id}>{area.name}</option> <option key={area.id} value={area.id}>{area.name}</option>
))} ))}
@@ -1321,7 +1304,7 @@ export function MapWorkspace({
{analysisMode === 'evolution' {analysisMode === 'evolution'
? activeTemporalSeriesGroup?.label ?? 'geen vergelijkbare tijdreeks' ? activeTemporalSeriesGroup?.label ?? 'geen vergelijkbare tijdreeks'
: activeThemeDataset : activeThemeDataset
? `${activeThemeDataset.source_name ?? activeThemeDataset.source} · ${activeThemeDataset.name}` ? getDatasetDisplayName(activeThemeDataset)
: 'niet beschikbaar'} : 'niet beschikbaar'}
</span> </span>
{usesDefaultOsmBasemap ? <span><strong>Ondergrond:</strong> OpenStreetMap</span> : null} {usesDefaultOsmBasemap ? <span><strong>Ondergrond:</strong> OpenStreetMap</span> : null}
+19 -19
View File
@@ -36,28 +36,28 @@ export function AreaPanel({
<section data-testid="area-panel"> <section data-testid="area-panel">
<div className="panel-title-row"> <div className="panel-title-row">
<div> <div>
<p className="eyebrow">Spatial scope</p> <p className="eyebrow">Geografisch bereik</p>
<h2>AOI manager</h2> <h2>Gebieden</h2>
</div> </div>
<span className="count-pill">{areas.length}</span> <span className="count-pill">{areas.length}</span>
</div> </div>
<p className="muted">{selectedProject ? `Project: ${selectedProject.name}` : 'Select a project first'}</p> <p className="muted">{selectedProject ? selectedProject.name : 'Geen werkruimte actief'}</p>
<div className="data-selection-summary data-selection-summary-area"> <div className="data-selection-summary data-selection-summary-area">
<span>Shown on map</span> <span>Getoond op de kaart</span>
<strong>{selectedArea?.name ?? 'No AOI selected'}</strong> <strong>{selectedArea?.name ?? 'Geen gebied geselecteerd'}</strong>
<small>{selectedArea?.area_m2 ? `${selectedArea.area_m2.toFixed(2)} m2` : 'Select an AOI with geometry for spatial review.'}</small> <small>{selectedArea?.area_m2 ? `${(selectedArea.area_m2 / 1_000_000).toLocaleString('nl-BE', { maximumFractionDigits: 2 })} km2` : 'Kies een gemeente of de volledige regio op de kaart.'}</small>
</div> </div>
<details className="data-panel-form-block"> <details className="data-panel-form-block">
<summary>Create spatial scope</summary> <summary>Eigen gebied toevoegen (GeoJSON)</summary>
<form className="compact-form" onSubmit={onCreateArea}> <form className="compact-form" onSubmit={onCreateArea}>
<label> <label>
AOI name Naam
<input <input
value={areaForm.name} value={areaForm.name}
onChange={(event) => onUpdateAreaForm({ ...areaForm, name: event.target.value })} onChange={(event) => onUpdateAreaForm({ ...areaForm, name: event.target.value })}
placeholder="AOI name" placeholder="Naam van het gebied"
/> />
</label> </label>
<label> <label>
@@ -69,7 +69,7 @@ export function AreaPanel({
/> />
</label> </label>
<label> <label>
GeoJSON geometry GeoJSON-geometrie
<textarea <textarea
value={areaForm.geometry} value={areaForm.geometry}
onChange={(event) => onUpdateAreaForm({ ...areaForm, geometry: event.target.value })} onChange={(event) => onUpdateAreaForm({ ...areaForm, geometry: event.target.value })}
@@ -77,22 +77,22 @@ export function AreaPanel({
/> />
</label> </label>
<button className="primary-action" type="submit" disabled={!selectedProjectId}> <button className="primary-action" type="submit" disabled={!selectedProjectId}>
Create area Gebied toevoegen
</button> </button>
</form> </form>
</details> </details>
{loadingAreas ? <p>Loading areas...</p> : null} {loadingAreas ? <p>Gebieden laden...</p> : null}
{areas.length === 0 ? <p>No areas yet</p> : null} {areas.length === 0 ? <p>Nog geen gebieden beschikbaar.</p> : null}
<div className="data-panel-list-block"> <details className="data-panel-list-block area-catalog-disclosure">
<p className="data-section-label">Saved AOIs</p> <summary>{areas.length} beschikbare gemeenten en regiogrenzen</summary>
<ul className="entity-list"> <ul className="entity-list">
{areas.map((area) => ( {areas.map((area) => (
<li className={selectedMapAreaId === area.id ? 'entity-card entity-card-active' : 'entity-card'} key={area.id}> <li className={selectedMapAreaId === area.id ? 'entity-card entity-card-active' : 'entity-card'} key={area.id}>
<strong>{area.name}</strong> <strong>{area.name}</strong>
<div className="entity-meta"> <div className="entity-meta">
<span>{area.area_m2 ? `${area.area_m2.toFixed(2)} m2` : 'n/a'}</span> <span>{area.area_m2 ? `${(area.area_m2 / 1_000_000).toLocaleString('nl-BE', { maximumFractionDigits: 2 })} km2` : 'Oppervlakte onbekend'}</span>
<span>{area.geometry?.type ?? area.geometry_type ?? 'n/a'}</span> <span>{area.geometry?.type ?? area.geometry_type ?? 'Geometrie onbekend'}</span>
</div> </div>
<button <button
className="secondary-action" className="secondary-action"
@@ -101,12 +101,12 @@ export function AreaPanel({
disabled={!area.geometry} disabled={!area.geometry}
data-testid={`area-show-${area.id}`} data-testid={`area-show-${area.id}`}
> >
{selectedMapAreaId === area.id ? 'Shown on map' : 'Show on map'} {selectedMapAreaId === area.id ? 'Op kaart' : 'Toon op kaart'}
</button> </button>
</li> </li>
))} ))}
</ul> </ul>
</div> </details>
</section> </section>
) )
} }
@@ -1,6 +1,12 @@
import type { FormEvent } from 'react' import type { FormEvent } from 'react'
import type { ProjectCreate, ProjectRead } from '../../types' import type { ProjectCreate, ProjectRead } from '../../types'
const TECHNICAL_PROJECT_PATTERN = /^(GeoIntel Detection Quality Matrix|GeoIntel hard-negative|GeoIntel training|Mol Building QA)/i
function isTechnicalProject(project: ProjectRead): boolean {
return TECHNICAL_PROJECT_PATTERN.test(project.name)
}
interface ProjectPanelProps { interface ProjectPanelProps {
projects: ProjectRead[] projects: ProjectRead[]
selectedProjectId: string | null selectedProjectId: string | null
@@ -27,68 +33,33 @@ export function ProjectPanel({
onLoadDemoWorkflow, onLoadDemoWorkflow,
}: ProjectPanelProps): JSX.Element { }: ProjectPanelProps): JSX.Element {
const selectedProject = projects.find((project) => project.id === selectedProjectId) const selectedProject = projects.find((project) => project.id === selectedProjectId)
const regularProjects = projects.filter((project) => !isTechnicalProject(project))
const technicalProjects = projects.filter(isTechnicalProject)
const visibleProjects = selectedProject && isTechnicalProject(selectedProject)
? [selectedProject, ...regularProjects]
: regularProjects
return ( return (
<section className="workspace-panel" data-testid="project-panel"> <section className="workspace-panel" data-testid="project-panel">
<div className="panel-title-row"> <div className="panel-title-row">
<div> <div>
<p className="eyebrow">Context</p> <p className="eyebrow">Gegevenscontext</p>
<h2>Projects</h2> <h2>Werkruimtes</h2>
</div> </div>
<span className="count-pill">{projects.length}</span> <span className="count-pill">{regularProjects.length} beschikbaar</span>
</div> </div>
{loadingProjects ? <p>Loading projects...</p> : null} {loadingProjects ? <p>Werkruimtes laden...</p> : null}
<div className="data-selection-summary data-selection-summary-project"> <div className="data-selection-summary data-selection-summary-project">
<span>Selected project</span> <span>Actieve werkruimte</span>
<strong>{selectedProject?.name ?? 'No active project'}</strong> <strong>{selectedProject?.name ?? 'Geen werkruimte actief'}</strong>
<small>{selectedProject?.region ?? 'Create or load a project to start the workbench.'}</small> <small>{selectedProject?.region ?? 'De regionale werkruimte wordt automatisch gekozen.'}</small>
</div>
<details className="data-panel-form-block">
<summary>Create project</summary>
<form className="compact-form" onSubmit={onCreateProject}>
<label>
Name
<input
value={projectForm.name}
onChange={(event) => onUpdateProjectForm({ ...projectForm, name: event.target.value })}
placeholder="Project name"
/>
</label>
<label>
Description
<input
value={projectForm.description ?? ''}
onChange={(event) => onUpdateProjectForm({ ...projectForm, description: event.target.value })}
placeholder="Description"
/>
</label>
<label>
Region
<input
value={projectForm.region ?? 'Kempen'}
onChange={(event) => onUpdateProjectForm({ ...projectForm, region: event.target.value })}
placeholder="Region"
/>
</label>
<button className="primary-action" type="submit">
Create project
</button>
</form>
</details>
<div className="demo-actions">
<button className="secondary-action" type="button" onClick={onLoadDemoWorkflow} disabled={loadingDemoWorkflow} data-testid="load-demo-workflow">
{loadingDemoWorkflow ? 'Loading demo...' : 'Load demo workflow'}
</button>
{demoWorkflowMessage ? <p>{demoWorkflowMessage}</p> : null}
</div> </div>
<div className="data-panel-list-block"> <div className="data-panel-list-block">
<p className="data-section-label">Saved projects</p> <p className="data-section-label">Beschikbare werkruimtes</p>
<ul className="entity-list"> <ul className="entity-list">
{projects.map((project) => ( {visibleProjects.map((project) => (
<li key={project.id}> <li key={project.id}>
<button <button
className={project.id === selectedProjectId ? 'entity-card entity-card-active' : 'entity-card'} className={project.id === selectedProjectId ? 'entity-card entity-card-active' : 'entity-card'}
@@ -98,18 +69,78 @@ export function ProjectPanel({
data-testid={`project-select-${project.id}`} data-testid={`project-select-${project.id}`}
> >
<strong>{project.name}</strong> <strong>{project.name}</strong>
<span>{project.region || 'No region'}</span> <span>{project.region || 'Regio niet opgegeven'}</span>
</button> </button>
</li> </li>
))} ))}
{projects.length === 0 ? ( {regularProjects.length === 0 ? (
<li className="empty-state"> <li className="empty-state">
<strong>No projects yet</strong> <strong>Nog geen werkruimte beschikbaar</strong>
<p>Create a project or load the offline demo workflow to populate the workbench.</p> <p>Maak een werkruimte aan of laad de demonstratiegegevens.</p>
</li> </li>
) : null} ) : null}
</ul> </ul>
</div> </div>
<details className="data-panel-form-block technical-management-block">
<summary>Geavanceerd projectbeheer</summary>
<div className="technical-management-body">
<form className="compact-form" onSubmit={onCreateProject}>
<label>
Naam
<input
value={projectForm.name}
onChange={(event) => onUpdateProjectForm({ ...projectForm, name: event.target.value })}
placeholder="Naam van de werkruimte"
/>
</label>
<label>
Beschrijving
<input
value={projectForm.description ?? ''}
onChange={(event) => onUpdateProjectForm({ ...projectForm, description: event.target.value })}
placeholder="Korte beschrijving"
/>
</label>
<label>
Regio
<input
value={projectForm.region ?? 'Kempen'}
onChange={(event) => onUpdateProjectForm({ ...projectForm, region: event.target.value })}
placeholder="Regio"
/>
</label>
<button className="primary-action" type="submit">Nieuwe werkruimte</button>
</form>
<div className="demo-actions">
<button className="secondary-action" type="button" onClick={onLoadDemoWorkflow} disabled={loadingDemoWorkflow} data-testid="load-demo-workflow">
{loadingDemoWorkflow ? 'Demo laden...' : 'Demonstratiegegevens laden'}
</button>
{demoWorkflowMessage ? <p>{demoWorkflowMessage}</p> : null}
</div>
{technicalProjects.length > 0 ? (
<details className="technical-run-list">
<summary>{technicalProjects.length} technische test- en benchmarkruns</summary>
<ul className="entity-list">
{technicalProjects.map((project) => (
<li key={project.id}>
<button
className={project.id === selectedProjectId ? 'entity-card entity-card-active' : 'entity-card'}
type="button"
onClick={() => onSelectProject(project.id)}
aria-pressed={project.id === selectedProjectId}
data-testid={`project-select-${project.id}`}
>
<strong>{project.name}</strong>
<span>{project.region || 'Technische run'}</span>
</button>
</li>
))}
</ul>
</details>
) : null}
</div>
</details>
</section> </section>
) )
} }
@@ -7,6 +7,23 @@ interface ProviderPanelProps {
onRefresh: () => void onRefresh: () => void
} }
function providerStatusLabel(provider: ProviderCapability): string {
if (provider.configured) {
return 'Beschikbaar'
}
return 'Live koppeling uit'
}
function authorityLabel(value: string): string {
const labels: Record<string, string> = {
authoritative: 'Officieel',
contextual: 'Aanvullend',
manual: 'Eigen upload',
fixture: 'Testgegevens',
}
return labels[value] ?? value
}
export function ProviderPanel({ export function ProviderPanel({
providers, providers,
loadingCapabilities, loadingCapabilities,
@@ -18,40 +35,40 @@ export function ProviderPanel({
<div className="system-provider-shell"> <div className="system-provider-shell">
<div className="panel-title-row"> <div className="panel-title-row">
<div> <div>
<h2>Provider Capabilities</h2> <h2>Databronnen</h2>
<p className="muted">Review configured local providers and honest not-configured external sources.</p> <p className="muted">Beschikbaarheid en herkomst van officiële, aanvullende en eigen bronnen.</p>
</div> </div>
<button type="button" className="secondary-action" onClick={onRefresh} disabled={loadingCapabilities}> <button type="button" className="secondary-action" onClick={onRefresh} disabled={loadingCapabilities}>
Refresh providers Status vernieuwen
</button> </button>
</div> </div>
<div className="system-provider-state-stack"> <div className="system-provider-state-stack">
{loadingCapabilities ? ( {loadingCapabilities ? (
<div className="result-state result-state-loading"> <div className="result-state result-state-loading">
<strong>Loading provider capabilities.</strong> <strong>Databronnen laden.</strong>
<p>Checking local, fixture and external provider registry states.</p> <p>De lokale en externe bronstatus wordt gecontroleerd.</p>
</div> </div>
) : null} ) : null}
{capabilitiesError ? ( {capabilitiesError ? (
<div className="result-state result-state-error"> <div className="result-state result-state-error">
<strong>Provider registry unavailable.</strong> <strong>Bronregister niet bereikbaar.</strong>
<p>{capabilitiesError}</p> <p>{capabilitiesError}</p>
</div> </div>
) : null} ) : null}
{providers.length === 0 && !loadingCapabilities ? ( {providers.length === 0 && !loadingCapabilities ? (
<div className="result-state result-state-empty"> <div className="result-state result-state-empty">
<strong>No providers reported by backend.</strong> <strong>Geen databronnen gemeld.</strong>
<p>Refresh provider capabilities after the backend is reachable.</p> <p>Vernieuw de status zodra de backend bereikbaar is.</p>
</div> </div>
) : null} ) : null}
</div> </div>
<div className="system-provider-capability-surface" aria-label="Provider capability registry"> <div className="system-provider-capability-surface" aria-label="Provider capability registry">
<div className="provider-detail-stack"> <div className="provider-detail-stack">
<strong>Official reference sources</strong> <strong>Officiële referentiebronnen</strong>
<div> <div>
GRB and OSM are reference-data provider capabilities, not AI model choices. Manual upload is the configured path for real reference datasets today; fixtures remain test/demo only. Reeds ingeladen GRB-lagen blijven lokaal beschikbaar. De live GRB- en OSM-koppelingen staan uit en halen dus niet ongemerkt externe gegevens op.
</div> </div>
</div> </div>
<ul className="system-provider-list"> <ul className="system-provider-list">
@@ -62,24 +79,16 @@ export function ProviderPanel({
<strong>{provider.display_name}</strong> <strong>{provider.display_name}</strong>
<span>{provider.provider_name}</span> <span>{provider.provider_name}</span>
</div> </div>
<span className={provider.configured ? 'status-badge status-badge-ready' : 'status-badge'}>{provider.status}</span> <span className={provider.configured ? 'status-badge status-badge-ready' : 'status-badge'}>{providerStatusLabel(provider)}</span>
</div> </div>
<div className="system-provider-grid"> <div className="system-provider-grid">
<div> <div>
<span>Authority</span> <span>Herkomst</span>
<strong>{provider.authority_level}</strong> <strong>{authorityLabel(provider.authority_level)}</strong>
</div> </div>
<div> <div>
<span>Configured</span> <span>Status</span>
<strong>{provider.configured ? 'yes' : 'no'}</strong> <strong>{providerStatusLabel(provider)}</strong>
</div>
<div>
<span>Geometry</span>
<strong>{provider.supported_geometry_types.join(', ')}</strong>
</div>
<div>
<span>Query modes</span>
<strong>{provider.supported_query_modes.join(', ')}</strong>
</div> </div>
</div> </div>
<div className="provider-layer-list" aria-label={`${provider.provider_name} supported layers`}> <div className="provider-layer-list" aria-label={`${provider.provider_name} supported layers`}>
@@ -89,22 +98,29 @@ export function ProviderPanel({
</span> </span>
))} ))}
</div> </div>
<details className="provider-technical-details">
<summary>Herkomst en technische mogelijkheden</summary>
<div className="provider-detail-stack"> <div className="provider-detail-stack">
<div>limitation: {provider.limitation_message}</div> <div>{provider.limitation_message}</div>
{!provider.configured && provider.not_configured_reason ? ( {!provider.configured && provider.not_configured_reason ? (
<div>reason: {provider.not_configured_reason}</div> <div>{provider.not_configured_reason}</div>
) : null} ) : null}
</div> </div>
<div className="provider-provenance-grid"> <div className="provider-provenance-grid">
<div className="provider-provenance-card"> <div className="provider-provenance-card">
<span>Attribution</span> <span>Naamsvermelding</span>
<strong>{provider.attribution}</strong> <strong>{provider.attribution}</strong>
</div> </div>
<div className="provider-provenance-card"> <div className="provider-provenance-card">
<span>License</span> <span>Licentie</span>
<strong>{provider.license_note}</strong> <strong>{provider.license_note}</strong>
</div> </div>
</div> </div>
<div className="entity-meta">
<span>Geometrie: {provider.supported_geometry_types.join(', ')}</span>
<span>Zoekwijzen: {provider.supported_query_modes.join(', ')}</span>
</div>
</details>
</li> </li>
))} ))}
</ul> </ul>
@@ -147,50 +147,63 @@ export function QualityResultsPanel({
const hasActiveQualityFilters = qualityStatusFilter !== 'all' || qualityTypeFilter !== 'all' || qualitySearchQuery.trim().length > 0 const hasActiveQualityFilters = qualityStatusFilter !== 'all' || qualityTypeFilter !== 'all' || qualitySearchQuery.trim().length > 0
return ( return (
<section className="quality-results-panel" data-testid="quality-results-panel"> <section className={qualityChecks.length === 0 ? 'quality-results-panel quality-results-panel-empty' : 'quality-results-panel'} data-testid="quality-results-panel">
<div className="quality-results-shell"> <div className="quality-results-shell">
<div className="panel-title-row"> <div className="panel-title-row">
<div> <div>
<h2>QA/QC Results</h2> <h2>Kwaliteitsresultaten</h2>
<p className="muted">Persisted quality checks from dataset, detection and segmentation comparisons.</p> <p className="muted">Bewaarde controles van kaartlagen en beeldanalyses.</p>
</div> </div>
<span className="count-pill">{qualityChecks.length} checks</span> <span className="count-pill">{qualityChecks.length} controles</span>
</div> </div>
<div className="quality-summary-surface" aria-label="QA/QC result summary"> <div className="quality-summary-surface" aria-label="QA/QC result summary">
<div className="quality-summary-grid"> <div className="quality-summary-grid">
<div> <div>
<span>Completed</span> <span>Afgerond</span>
<strong>{completedChecks}</strong> <strong>{completedChecks}</strong>
</div> </div>
<div> <div>
<span>Latest score</span> <span>Laatste score</span>
<strong>{latestCheck?.score ?? 'n/a'}</strong> <strong>{latestCheck?.score ?? 'n/a'}</strong>
</div> </div>
<div> <div>
<span>Latest status</span> <span>Laatste status</span>
<strong>{latestCheck?.status ?? 'waiting'}</strong> <strong>{latestCheck?.status ?? 'waiting'}</strong>
</div> </div>
</div> </div>
</div> </div>
{qualityChecks.length === 0 ? (
<div className="result-state result-state-empty quality-user-empty-state">
<strong>Nog geen kwaliteitscontrole uitgevoerd</strong>
<p>Selecteer een gebied op de kaart en vergelijk een bewaard resultaat met een officiële referentielaag.</p>
</div>
) : null}
{qualityChecks.length > 0 ? (
<details className="quality-advanced-disclosure">
<summary>
<span>Details, kaartbewijs en historiek</span>
<strong>{qualityChecks.length} bewaarde controles</strong>
</summary>
<div className="quality-evidence-surface" aria-label="QA/QC dataset evidence"> <div className="quality-evidence-surface" aria-label="QA/QC dataset evidence">
<div className="quality-handoff-grid" aria-label="QA/QC dataset handoff context"> <div className="quality-handoff-grid" aria-label="QA/QC dataset handoff context">
<div> <div>
<span>Candidate layers</span> <span>Te controleren lagen</span>
<strong>{candidateDatasets.length}</strong> <strong>{candidateDatasets.length}</strong>
<p className="quality-dataset-name">{candidateDatasets[0]?.name ?? 'No candidate layer selected yet'}</p> <p className="quality-dataset-name">{candidateDatasets[0]?.name ?? 'Nog geen te controleren laag gekozen'}</p>
</div> </div>
<div> <div>
<span>Reference layers</span> <span>Referentielagen</span>
<strong>{referenceDatasets.length}</strong> <strong>{referenceDatasets.length}</strong>
<p className="quality-dataset-name">{referenceDatasets[0]?.name ?? 'No reference layer available yet'}</p> <p className="quality-dataset-name">{referenceDatasets[0]?.name ?? 'Nog geen referentielaag beschikbaar'}</p>
</div> </div>
<div> <div>
<span>Latest comparison</span> <span>Laatste vergelijking</span>
<strong>{latestCheck?.status ?? 'waiting'}</strong> <strong>{latestCheck?.status ?? 'waiting'}</strong>
<p className="quality-dataset-name"> <p className="quality-dataset-name">
Candidate {latestCandidateName ?? latestCheck?.candidate_dataset_id ?? 'n/a'} / Reference {latestReferenceName ?? latestCheck?.reference_dataset_id ?? 'n/a'} Te controleren: {latestCandidateName ?? latestCheck?.candidate_dataset_id ?? 'n.v.t.'} / referentie: {latestReferenceName ?? latestCheck?.reference_dataset_id ?? 'n.v.t.'}
</p> </p>
</div> </div>
</div> </div>
@@ -199,8 +212,8 @@ export function QualityResultsPanel({
<div className="quality-drilldown-surface" aria-label="QA/QC evidence drilldown"> <div className="quality-drilldown-surface" aria-label="QA/QC evidence drilldown">
<div className="panel-title-row"> <div className="panel-title-row">
<div> <div>
<h3>QA/QC evidence drilldown</h3> <h3>Bewijs van de kwaliteitscontrole</h3>
<p className="muted">Inspect the persisted check, metric evidence and dataset handoff context before exporting or sharing results.</p> <p className="muted">Bekijk de bewaarde vergelijking, scores en kaartlagen voordat je resultaten deelt.</p>
</div> </div>
<button <button
type="button" type="button"
@@ -208,7 +221,7 @@ export function QualityResultsPanel({
onClick={() => setSelectedQualityCheckId(latestCheck?.id ?? null)} onClick={() => setSelectedQualityCheckId(latestCheck?.id ?? null)}
disabled={!latestCheck} disabled={!latestCheck}
> >
Inspect latest check Laatste controle bekijken
</button> </button>
<button <button
type="button" type="button"
@@ -216,12 +229,12 @@ export function QualityResultsPanel({
onClick={() => latestCheck?.id && onOpenEvidenceMap?.(latestCheck.id)} onClick={() => latestCheck?.id && onOpenEvidenceMap?.(latestCheck.id)}
disabled={!latestCheck || evidenceLoading || !onOpenEvidenceMap} disabled={!latestCheck || evidenceLoading || !onOpenEvidenceMap}
> >
{evidenceLoading ? 'Loading map evidence...' : 'Show latest on map'} {evidenceLoading ? 'Kaartbewijs laden...' : 'Laatste op kaart tonen'}
</button> </button>
</div> </div>
{evidenceError ? ( {evidenceError ? (
<div className="result-state result-state-error"> <div className="result-state result-state-error">
<strong>QA/QC evidence overlay could not be loaded.</strong> <strong>Het bewijs kon niet op de kaart worden geladen.</strong>
<p>{evidenceError}</p> <p>{evidenceError}</p>
</div> </div>
) : null} ) : null}
@@ -229,22 +242,22 @@ export function QualityResultsPanel({
<> <>
<div className="quality-drilldown-grid"> <div className="quality-drilldown-grid">
<div> <div>
<span>Selected check</span> <span>Geselecteerde controle</span>
<strong>{selectedQualityCheck.check_type}</strong> <strong>{selectedQualityCheck.check_type}</strong>
<p>{selectedQualityCheck.id}</p> <p>{selectedQualityCheck.id}</p>
</div> </div>
<div> <div>
<span>Candidate layer</span> <span>Te controleren laag</span>
<strong>{selectedCandidateName}</strong> <strong>{selectedCandidateName}</strong>
<p>{selectedQualityCheck.candidate_dataset_id ?? 'candidate not stored'}</p> <p>{selectedQualityCheck.candidate_dataset_id ?? 'candidate not stored'}</p>
</div> </div>
<div> <div>
<span>Reference layer</span> <span>Referentielaag</span>
<strong>{selectedReferenceName}</strong> <strong>{selectedReferenceName}</strong>
<p>{selectedQualityCheck.reference_dataset_id}</p> <p>{selectedQualityCheck.reference_dataset_id}</p>
</div> </div>
<div> <div>
<span>Analysis run</span> <span>Analyserun</span>
<strong>{selectedQualityCheck.analysis_run_id ?? 'n/a'}</strong> <strong>{selectedQualityCheck.analysis_run_id ?? 'n/a'}</strong>
<p>Job: {selectedQualityCheck.job_id ?? 'n/a'}</p> <p>Job: {selectedQualityCheck.job_id ?? 'n/a'}</p>
</div> </div>
@@ -254,39 +267,39 @@ export function QualityResultsPanel({
<p>Score: {selectedQualityCheck.score ?? 'n/a'}</p> <p>Score: {selectedQualityCheck.score ?? 'n/a'}</p>
</div> </div>
<div> <div>
<span>Completed</span> <span>Afgerond</span>
<strong>{formatQualityTimestamp(selectedQualityCheck.completed_at)}</strong> <strong>{formatQualityTimestamp(selectedQualityCheck.completed_at)}</strong>
<p>Created: {formatQualityTimestamp(selectedQualityCheck.created_at)}</p> <p>Created: {formatQualityTimestamp(selectedQualityCheck.created_at)}</p>
</div> </div>
</div> </div>
<div className="quality-evidence-token-grid"> <div className="quality-evidence-token-grid">
<div> <div>
<span>False positive evidence</span> <span>Onterecht gevonden</span>
<strong>{qualityMetricValue(metricByKey(selectedQualityCheck, 'false_positive_count'))}</strong> <strong>{qualityMetricValue(metricByKey(selectedQualityCheck, 'false_positive_count'))}</strong>
<p>Candidate geometries not matched to the reference layer.</p> <p>Gevonden vormen zonder overeenkomst in de referentielaag.</p>
</div> </div>
<div> <div>
<span>False negative evidence</span> <span>Gemiste objecten</span>
<strong>{qualityMetricValue(metricByKey(selectedQualityCheck, 'false_negative_count'))}</strong> <strong>{qualityMetricValue(metricByKey(selectedQualityCheck, 'false_negative_count'))}</strong>
<p>Reference geometries not matched by the candidate layer.</p> <p>Objecten uit de referentielaag die niet werden gevonden.</p>
</div> </div>
<div className="quality-evidence-map-handoff"> <div className="quality-evidence-map-handoff">
<span>Map evidence handoff</span> <span>Controle op de kaart</span>
<strong>{selectedCandidateName} / {selectedReferenceName}</strong> <strong>{selectedCandidateName} / {selectedReferenceName}</strong>
<p>Use the candidate and reference datasets as map layers for spatial review.</p> <p>Toon beide lagen samen op de kaart voor een ruimtelijke controle.</p>
<button <button
type="button" type="button"
className="secondary-action" className="secondary-action"
onClick={() => onOpenEvidenceMap?.(selectedQualityCheck.id)} onClick={() => onOpenEvidenceMap?.(selectedQualityCheck.id)}
disabled={evidenceLoading || !onOpenEvidenceMap} disabled={evidenceLoading || !onOpenEvidenceMap}
> >
{evidenceLoading ? 'Loading overlay...' : 'Show evidence overlay'} {evidenceLoading ? 'Lagen laden...' : 'Bewijs op kaart tonen'}
</button> </button>
</div> </div>
</div> </div>
<div className="quality-feature-evidence-grid" aria-label="Feature-level QA/QC evidence"> <div className="quality-feature-evidence-grid" aria-label="Feature-level QA/QC evidence">
<div> <div>
<span>Matched feature ids</span> <span>Overeenkomende object-ID's</span>
{selectedMatchEvidence.length > 0 ? ( {selectedMatchEvidence.length > 0 ? (
<ul> <ul>
{selectedMatchEvidence.slice(0, 6).map((item, index) => ( {selectedMatchEvidence.slice(0, 6).map((item, index) => (
@@ -294,11 +307,11 @@ export function QualityResultsPanel({
))} ))}
</ul> </ul>
) : ( ) : (
<p>No matched feature ids persisted for this check.</p> <p>Voor deze controle zijn geen overeenkomende object-ID's bewaard.</p>
)} )}
</div> </div>
<div> <div>
<span>False positive feature ids</span> <span>ID's van onterecht gevonden objecten</span>
{selectedFalsePositiveEvidence.length > 0 ? ( {selectedFalsePositiveEvidence.length > 0 ? (
<ul> <ul>
{selectedFalsePositiveEvidence.slice(0, 6).map((item, index) => ( {selectedFalsePositiveEvidence.slice(0, 6).map((item, index) => (
@@ -306,11 +319,11 @@ export function QualityResultsPanel({
))} ))}
</ul> </ul>
) : ( ) : (
<p>No false-positive feature ids persisted for this check.</p> <p>Er zijn geen ID's van onterecht gevonden objecten bewaard.</p>
)} )}
</div> </div>
<div> <div>
<span>False negative feature ids</span> <span>ID's van gemiste objecten</span>
{selectedFalseNegativeEvidence.length > 0 ? ( {selectedFalseNegativeEvidence.length > 0 ? (
<ul> <ul>
{selectedFalseNegativeEvidence.slice(0, 6).map((item, index) => ( {selectedFalseNegativeEvidence.slice(0, 6).map((item, index) => (
@@ -318,19 +331,19 @@ export function QualityResultsPanel({
))} ))}
</ul> </ul>
) : ( ) : (
<p>No false-negative feature ids persisted for this check.</p> <p>Er zijn geen ID's van gemiste objecten bewaard.</p>
)} )}
</div> </div>
</div> </div>
<div className="quality-provenance-grid"> <div className="quality-provenance-grid">
<div> <div>
<span>Parameters</span> <span>Instellingen</span>
<pre className="quality-provenance-pre"> <pre className="quality-provenance-pre">
{JSON.stringify(selectedQualityCheck.parameters_json ?? {}, null, 2)} {JSON.stringify(selectedQualityCheck.parameters_json ?? {}, null, 2)}
</pre> </pre>
</div> </div>
<div> <div>
<span>Findings</span> <span>Bevindingen</span>
<pre className="quality-provenance-pre"> <pre className="quality-provenance-pre">
{JSON.stringify(selectedQualityCheck.findings_json ?? {}, null, 2)} {JSON.stringify(selectedQualityCheck.findings_json ?? {}, null, 2)}
</pre> </pre>
@@ -339,8 +352,8 @@ export function QualityResultsPanel({
</> </>
) : ( ) : (
<div className="result-state result-state-empty"> <div className="result-state result-state-empty">
<strong>No QA/QC check selected.</strong> <strong>Geen kwaliteitscontrole geselecteerd.</strong>
<p>Run or load a persisted QA/QC check to inspect metric and map evidence.</p> <p>Kies een bewaarde controle om scores en kaartbewijs te bekijken.</p>
</div> </div>
)} )}
</div> </div>
@@ -354,21 +367,21 @@ export function QualityResultsPanel({
disabled={!selectedProjectId} disabled={!selectedProjectId}
data-testid="refresh-quality-results" data-testid="refresh-quality-results"
> >
Refresh QA/QC results Kwaliteitsresultaten vernieuwen
</button> </button>
</div> </div>
<div className="quality-result-state-stack"> <div className="quality-result-state-stack">
{qualityChecksError ? ( {qualityChecksError ? (
<div className="result-state result-state-error"> <div className="result-state result-state-error">
<strong>QA/QC results could not be loaded.</strong> <strong>De kwaliteitsresultaten konden niet worden geladen.</strong>
<p>{qualityChecksError}</p> <p>{qualityChecksError}</p>
</div> </div>
) : null} ) : null}
{qualityChecks.length === 0 ? ( {qualityChecks.length === 0 ? (
<div className="result-state result-state-empty"> <div className="result-state result-state-empty">
<strong>No persisted QA/QC results yet</strong> <strong>Nog geen bewaarde kwaliteitsresultaten</strong>
<p>Run dataset, detection or segmentation QA against a reference layer to populate this workspace.</p> <p>Vergelijk een kaartlaag of beeldanalyse met een referentielaag om deze werkruimte te vullen.</p>
</div> </div>
) : null} ) : null}
</div> </div>
@@ -376,7 +389,7 @@ export function QualityResultsPanel({
{qualityChecks.length > 0 ? ( {qualityChecks.length > 0 ? (
<div className="quality-history-controls" aria-label="QA/QC result filters"> <div className="quality-history-controls" aria-label="QA/QC result filters">
<label> <label>
Search QA/QC results Kwaliteitsresultaten zoeken
<input <input
type="search" type="search"
placeholder="Type, id or dataset" placeholder="Type, id or dataset"
@@ -387,7 +400,7 @@ export function QualityResultsPanel({
<label> <label>
Status Status
<select value={qualityStatusFilter} onChange={(event) => setQualityStatusFilter(event.target.value)}> <select value={qualityStatusFilter} onChange={(event) => setQualityStatusFilter(event.target.value)}>
<option value="all">All statuses</option> <option value="all">Alle statussen</option>
{qualityStatuses.map((status) => ( {qualityStatuses.map((status) => (
<option key={status} value={status}> <option key={status} value={status}>
{status} {status}
@@ -396,9 +409,9 @@ export function QualityResultsPanel({
</select> </select>
</label> </label>
<label> <label>
Check type Type controle
<select value={qualityTypeFilter} onChange={(event) => setQualityTypeFilter(event.target.value)}> <select value={qualityTypeFilter} onChange={(event) => setQualityTypeFilter(event.target.value)}>
<option value="all">All check types</option> <option value="all">Alle types</option>
{qualityTypes.map((checkType) => ( {qualityTypes.map((checkType) => (
<option key={checkType} value={checkType}> <option key={checkType} value={checkType}>
{checkType} {checkType}
@@ -417,33 +430,33 @@ export function QualityResultsPanel({
}} }}
disabled={!hasActiveQualityFilters && !showAllQualityChecks} disabled={!hasActiveQualityFilters && !showAllQualityChecks}
> >
Reset view Filters wissen
</button> </button>
</div> </div>
) : null} ) : null}
{filteredQualityChecks.length > 8 ? ( {filteredQualityChecks.length > 8 ? (
<div className="list-limit-banner"> <div className="list-limit-banner">
<span className="quality-list-count"> <span className="quality-list-count">
Showing {visibleQualityChecks.length} of {filteredQualityChecks.length} matching QA/QC results. {visibleQualityChecks.length} van {filteredQualityChecks.length} kwaliteitsresultaten getoond.
{hiddenQualityCheckCount > 0 ? ` ${hiddenQualityCheckCount} older checks hidden.` : ' All matching checks shown.'} {hiddenQualityCheckCount > 0 ? ` ${hiddenQualityCheckCount} oudere controles verborgen.` : ' Alle resultaten zijn zichtbaar.'}
</span> </span>
<button type="button" className="secondary-action" onClick={() => setShowAllQualityChecks((value) => !value)}> <button type="button" className="secondary-action" onClick={() => setShowAllQualityChecks((value) => !value)}>
{showAllQualityChecks ? 'Show latest 8' : 'Show all QA/QC results'} {showAllQualityChecks ? 'Alleen laatste 8 tonen' : 'Alle kwaliteitsresultaten tonen'}
</button> </button>
</div> </div>
) : null} ) : null}
{qualityChecks.length > 0 && filteredQualityChecks.length === 0 ? ( {qualityChecks.length > 0 && filteredQualityChecks.length === 0 ? (
<div className="result-state result-state-empty"> <div className="result-state result-state-empty">
<strong>No QA/QC results match the current filters.</strong> <strong>Geen kwaliteitsresultaten voldoen aan de filters.</strong>
<p>Clear the search, status or check type filter to return to the latest quality results.</p> <p>Wis de zoek-, status- of typefilter om de laatste resultaten opnieuw te tonen.</p>
</div> </div>
) : null} ) : null}
</div> </div>
<div className="quality-history-surface" aria-label="QA/QC result history"> <div className="quality-history-surface" aria-label="QA/QC result history">
<div className="panel-title-row"> <div className="panel-title-row">
<h3>Result history</h3> <h3>Historiek</h3>
<span className="count-pill">{visibleQualityChecks.length} shown</span> <span className="count-pill">{visibleQualityChecks.length} getoond</span>
</div> </div>
<ul className="quality-check-list"> <ul className="quality-check-list">
{visibleQualityChecks.map((check) => ( {visibleQualityChecks.map((check) => (
@@ -466,7 +479,7 @@ export function QualityResultsPanel({
</div> </div>
<div className="quality-check-actions"> <div className="quality-check-actions">
<button type="button" className="secondary-action" onClick={() => setSelectedQualityCheckId(check.id)}> <button type="button" className="secondary-action" onClick={() => setSelectedQualityCheckId(check.id)}>
Inspect check Controle bekijken
</button> </button>
<button <button
type="button" type="button"
@@ -474,7 +487,7 @@ export function QualityResultsPanel({
onClick={() => onOpenEvidenceMap?.(check.id)} onClick={() => onOpenEvidenceMap?.(check.id)}
disabled={evidenceLoading || !onOpenEvidenceMap} disabled={evidenceLoading || !onOpenEvidenceMap}
> >
Show evidence on map Bewijs op kaart tonen
</button> </button>
</div> </div>
<div className="quality-score-row"> <div className="quality-score-row">
@@ -483,16 +496,16 @@ export function QualityResultsPanel({
<strong>{check.score ?? 'n/a'}</strong> <strong>{check.score ?? 'n/a'}</strong>
</div> </div>
<div> <div>
<span>Metrics</span> <span>Meetwaarden</span>
<strong>{check.metrics.length}</strong> <strong>{check.metrics.length}</strong>
</div> </div>
<div> <div>
<span>Quality check</span> <span>Controle-ID</span>
<strong>{check.id}</strong> <strong>{check.id}</strong>
</div> </div>
</div> </div>
<div className="quality-metric-section"> <div className="quality-metric-section">
<span>Metric evidence</span> <span>Gemeten kwaliteit</span>
<div className="quality-metric-grid"> <div className="quality-metric-grid">
{CORE_METRIC_ORDER.map((metricKey) => { {CORE_METRIC_ORDER.map((metricKey) => {
const metric = check.metrics.find((item) => item.metric_key === metricKey) const metric = check.metrics.find((item) => item.metric_key === metricKey)
@@ -506,7 +519,7 @@ export function QualityResultsPanel({
})} })}
</div> </div>
</div> </div>
<span className="metric-list-label">Raw metrics</span> <span className="metric-list-label">Alle meetwaarden</span>
<ul className="metric-list"> <ul className="metric-list">
{check.metrics.map((metric) => ( {check.metrics.map((metric) => (
<li key={metric.id}> <li key={metric.id}>
@@ -519,6 +532,8 @@ export function QualityResultsPanel({
))} ))}
</ul> </ul>
</div> </div>
</details>
) : null}
</div> </div>
</section> </section>
) )
+2
View File
@@ -2,6 +2,8 @@ import type { DatasetCreateResponse, ProjectRead } from '../types'
export const PRIMARY_FOCUS_LABEL = 'Mol' export const PRIMARY_FOCUS_LABEL = 'Mol'
export const PRIMARY_FOCUS_REGION = 'Mol, Kempen' export const PRIMARY_FOCUS_REGION = 'Mol, Kempen'
export const REGIONAL_WORKSPACE_PROJECT_NAME = 'Kempen Regional Workbench'
export const REGIONAL_WORKSPACE_LABEL = 'Kempen (28 gemeenten)'
export const PRIMARY_FOCUS_CENTER: [number, number] = [5.1167, 51.1919] export const PRIMARY_FOCUS_CENTER: [number, number] = [5.1167, 51.1919]
export const PRIMARY_FOCUS_AREA_NAME = 'Mol primary AOI' export const PRIMARY_FOCUS_AREA_NAME = 'Mol primary AOI'
export const PRIMARY_FOCUS_MUNICIPALITY_PROJECT_NAME = 'Mol Municipality Workbench' export const PRIMARY_FOCUS_MUNICIPALITY_PROJECT_NAME = 'Mol Municipality Workbench'
+25 -7
View File
@@ -1,4 +1,4 @@
import { useState } from 'react' import { useEffect, useState } from 'react'
import { detectionApi } from '../services/api' import { detectionApi } from '../services/api'
import type { import type {
DatasetCreateResponse, DatasetCreateResponse,
@@ -72,10 +72,10 @@ export function useDetectionWorkflow({
const [detectionModelError, setDetectionModelError] = useState<string | null>(null) const [detectionModelError, setDetectionModelError] = useState<string | null>(null)
const [modelAssetError, setModelAssetError] = useState<string | null>(null) const [modelAssetError, setModelAssetError] = useState<string | null>(null)
const [selectedDetectionDatasetId, setSelectedDetectionDatasetId] = useState('') const [selectedDetectionDatasetId, setSelectedDetectionDatasetId] = useState('')
const [selectedDetectionModelId, setSelectedDetectionModelId] = useState('yolo-placeholder') const [selectedDetectionModelId, setSelectedDetectionModelId] = useState('yolo-configured')
const [selectedModelAssetId, setSelectedModelAssetId] = useState('') const [selectedModelAssetId, setSelectedModelAssetId] = useState('')
const [detectionTileManifestPath, setDetectionTileManifestPath] = useState('') const [detectionTileManifestPath, setDetectionTileManifestPath] = useState('')
const [detectionConfidenceThreshold, setDetectionConfidenceThreshold] = useState(0.5) const [detectionConfidenceThreshold, setDetectionConfidenceThreshold] = useState(0.15)
const [runningDetection, setRunningDetection] = useState(false) const [runningDetection, setRunningDetection] = useState(false)
const [detectionRunResult, setDetectionRunResult] = useState<DetectionRunResponse | null>(null) const [detectionRunResult, setDetectionRunResult] = useState<DetectionRunResponse | null>(null)
const [detectionRunError, setDetectionRunError] = useState<string | null>(null) const [detectionRunError, setDetectionRunError] = useState<string | null>(null)
@@ -98,6 +98,12 @@ export function useDetectionWorkflow({
const [detectionCalibrationRows, setDetectionCalibrationRows] = useState<DetectionCalibrationRunRow[]>([]) const [detectionCalibrationRows, setDetectionCalibrationRows] = useState<DetectionCalibrationRunRow[]>([])
const [detectionCalibrationError, setDetectionCalibrationError] = useState<string | null>(null) const [detectionCalibrationError, setDetectionCalibrationError] = useState<string | null>(null)
useEffect(() => {
if (!selectedDetectionDatasetId && rasterDatasets.length > 0) {
setSelectedDetectionDatasetId(rasterDatasets[0].id)
}
}, [rasterDatasets, selectedDetectionDatasetId])
const loadDetectionModels = async () => { const loadDetectionModels = async () => {
setLoadingDetectionModels(true) setLoadingDetectionModels(true)
setDetectionModelError(null) setDetectionModelError(null)
@@ -105,8 +111,12 @@ export function useDetectionWorkflow({
try { try {
const response = await detectionApi.listModels() const response = await detectionApi.listModels()
setDetectionModels(response.models) setDetectionModels(response.models)
if (!response.models.some((model) => model.model_id === selectedDetectionModelId) && response.models.length > 0) { const selectedModelStillAvailable = response.models.some((model) => model.model_id === selectedDetectionModelId)
setSelectedDetectionModelId(response.models[0].model_id) if (!selectedModelStillAvailable && response.models.length > 0) {
const configuredModel = response.models.find(
(model) => model.configured && model.model_id !== 'manual-fixture-detector',
)
setSelectedDetectionModelId(configuredModel?.model_id ?? response.models[0].model_id)
} }
} catch (error) { } catch (error) {
setDetectionModelError(formatError(error, 'Failed to load detection models')) setDetectionModelError(formatError(error, 'Failed to load detection models'))
@@ -114,8 +124,16 @@ export function useDetectionWorkflow({
try { try {
const assetResponse = await detectionApi.listModelAssets() const assetResponse = await detectionApi.listModelAssets()
setModelAssets(assetResponse.items) setModelAssets(assetResponse.items)
if (!assetResponse.items.some((asset) => asset.model_asset_id === selectedModelAssetId)) { const activeAsset = assetResponse.items.find((asset) => asset.active) ?? null
setSelectedModelAssetId('') const selectedAssetStillAvailable = assetResponse.items.some((asset) => asset.model_asset_id === selectedModelAssetId)
const nextAssetId = selectedAssetStillAvailable ? selectedModelAssetId : activeAsset?.model_asset_id ?? ''
setSelectedModelAssetId(nextAssetId)
try {
const preflight = await detectionApi.getYoloPreflight({ model_asset_id: nextAssetId || null })
setYoloPreflight(preflight)
setYoloPreflightError(null)
} catch (error) {
setYoloPreflightError(formatError(error, 'Failed to load YOLO preflight status'))
} }
} catch (error) { } catch (error) {
setModelAssets([]) setModelAssets([])
+12
View File
@@ -3,6 +3,7 @@ import {
PRIMARY_FOCUS_AREA_GEOJSON, PRIMARY_FOCUS_AREA_GEOJSON,
PRIMARY_FOCUS_AREA_NAME, PRIMARY_FOCUS_AREA_NAME,
PRIMARY_FOCUS_REGION, PRIMARY_FOCUS_REGION,
REGIONAL_WORKSPACE_PROJECT_NAME,
isPrimaryFocusMunicipalityBoundaryDataset, isPrimaryFocusMunicipalityBoundaryDataset,
isPrimaryFocusMunicipalityProject, isPrimaryFocusMunicipalityProject,
isPrimaryFocusProject, isPrimaryFocusProject,
@@ -79,6 +80,17 @@ export function useProjectWorkspace() {
if (selectedProjectId && items.some((project) => project.id === selectedProjectId)) { if (selectedProjectId && items.some((project) => project.id === selectedProjectId)) {
return selectedProjectId return selectedProjectId
} }
const regionalProject = items.find((project) => project.name === REGIONAL_WORKSPACE_PROJECT_NAME)
if (regionalProject) {
try {
const data = await fetchProjectData(regionalProject.id)
if (data.areas.length > 0 && data.datasets.some((dataset) => dataset.status === 'ready')) {
return regionalProject.id
}
} catch {
// Continue with the municipality and completeness fallbacks when regional data is unavailable.
}
}
const municipalityProject = items.find(isPrimaryFocusMunicipalityProject) const municipalityProject = items.find(isPrimaryFocusMunicipalityProject)
if (municipalityProject) { if (municipalityProject) {
try { try {
+24
View File
@@ -0,0 +1,24 @@
import type { DatasetCreateResponse } from '../types'
const DATASET_LABEL_BY_LAYER: Record<string, string> = {
buildings: 'Bebouwing',
roads: 'Wegen',
water: 'Water',
parcels: 'Percelen',
population: 'Bevolking',
forest: 'Bos en groen',
regional_boundary: 'Grens vervoerregio Kempen',
municipality_boundaries: 'Gemeentegrenzen Kempen',
}
export function getDatasetDisplayName(dataset: DatasetCreateResponse): string {
const layer = (dataset.reference_layer_name ?? dataset.source_metadata?.layer_name ?? '')
.toString()
.toLowerCase()
const label = DATASET_LABEL_BY_LAYER[layer]
if (!label) {
return dataset.name
}
const source = (dataset.source_name ?? dataset.source).toUpperCase()
return `${label} · ${source}`
}
+279
View File
@@ -1493,6 +1493,285 @@ details.ai-lab-model-surface > summary strong {
} }
} }
/* End-user workbench simplification */
.geo-loaded-scope {
display: grid;
gap: 0.2rem;
border: 1px solid var(--line);
border-radius: 6px;
padding: 0.65rem 0.7rem;
background: #f7faf9;
}
.geo-loaded-scope span,
.geo-loaded-scope small {
color: var(--muted);
font-size: 0.72rem;
}
.geo-loaded-scope strong {
color: var(--text);
font-size: 0.88rem;
}
.technical-management-body {
display: grid;
gap: 0.8rem;
border-top: 1px solid var(--line);
padding: 0.8rem;
background: #ffffff;
}
.technical-management-body > .compact-form {
padding: 0;
}
.technical-run-list,
.area-catalog-disclosure,
.dataset-technical-details,
.quality-advanced-disclosure,
.provider-technical-details,
.secondary-analysis-disclosure,
.status-details-disclosure,
.export-history-disclosure {
overflow: hidden;
border: 1px solid var(--line);
border-radius: 6px;
background: #f8fafb;
}
.technical-run-list > summary,
.area-catalog-disclosure > summary,
.dataset-technical-details > summary,
.quality-advanced-disclosure > summary,
.provider-technical-details > summary,
.secondary-analysis-disclosure > summary,
.status-details-disclosure > summary,
.export-history-disclosure > summary {
display: flex;
min-height: 2.7rem;
cursor: pointer;
gap: 0.75rem;
align-items: center;
justify-content: space-between;
padding: 0.62rem 0.75rem;
color: #33424d;
font-size: 0.8rem;
font-weight: 750;
list-style: none;
}
.technical-run-list > summary::-webkit-details-marker,
.area-catalog-disclosure > summary::-webkit-details-marker,
.dataset-technical-details > summary::-webkit-details-marker,
.quality-advanced-disclosure > summary::-webkit-details-marker,
.provider-technical-details > summary::-webkit-details-marker,
.secondary-analysis-disclosure > summary::-webkit-details-marker,
.status-details-disclosure > summary::-webkit-details-marker,
.export-history-disclosure > summary::-webkit-details-marker {
display: none;
}
.technical-run-list > summary::after,
.area-catalog-disclosure > summary::after,
.dataset-technical-details > summary::after,
.quality-advanced-disclosure > summary::after,
.provider-technical-details > summary::after,
.secondary-analysis-disclosure > summary::after,
.status-details-disclosure > summary::after,
.export-history-disclosure > summary::after {
content: '+';
margin-left: auto;
color: var(--accent-strong);
font-size: 1rem;
}
.technical-run-list[open] > summary::after,
.area-catalog-disclosure[open] > summary::after,
.dataset-technical-details[open] > summary::after,
.quality-advanced-disclosure[open] > summary::after,
.provider-technical-details[open] > summary::after,
.secondary-analysis-disclosure[open] > summary::after,
.status-details-disclosure[open] > summary::after,
.export-history-disclosure[open] > summary::after {
content: '-';
}
.technical-run-list .entity-list,
.area-catalog-disclosure .entity-list {
max-height: 28rem;
overflow: auto;
border-top: 1px solid var(--line);
padding: 0.65rem;
background: #ffffff;
}
.quality-advanced-disclosure {
margin-top: 0.75rem;
}
.quality-advanced-disclosure > summary strong {
margin-right: 0.25rem;
color: var(--ink);
font-size: 0.76rem;
}
.quality-advanced-disclosure > div {
margin: 0;
border-right: 0;
border-bottom: 0;
border-left: 0;
border-radius: 0;
}
.dataset-technical-details {
margin-top: 0.65rem;
}
.dataset-technical-details > div,
.dataset-technical-details > button,
.provider-technical-details > div {
margin: 0 0.7rem 0.7rem;
}
.dataset-technical-details > summary,
.provider-technical-details > summary {
min-height: 2.3rem;
padding-block: 0.45rem;
}
.dataset-card-primary-meta {
margin-top: 0.25rem;
}
.ai-user-summary {
order: 0;
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 0.65rem;
}
.ai-user-summary-card {
display: grid;
min-width: 0;
gap: 0.25rem;
border: 1px solid var(--line);
border-radius: 6px;
padding: 0.75rem;
background: #ffffff;
}
.ai-user-summary-card-primary {
border-color: #bfd8d2;
background: var(--accent-soft);
}
.ai-user-summary-card-ready {
border-left: 3px solid var(--success);
}
.ai-user-summary-card span,
.ai-user-summary-card p {
margin: 0;
color: var(--muted);
font-size: 0.72rem;
line-height: 1.35;
}
.ai-user-summary-card strong {
overflow-wrap: anywhere;
color: var(--text);
font-size: 0.95rem;
}
.secondary-analysis-disclosure {
align-self: start;
}
.secondary-analysis-disclosure > summary strong,
.status-details-disclosure > summary strong,
.export-history-disclosure > summary strong {
margin-left: auto;
color: var(--muted);
font-size: 0.72rem;
}
.secondary-analysis-disclosure > section,
.secondary-analysis-disclosure > .workspace-panel {
border: 0;
border-top: 1px solid var(--line);
border-radius: 0;
}
.status-details-disclosure > section {
margin: 0;
border: 0;
border-top: 1px solid var(--line);
border-radius: 0;
}
.quality-results-panel-empty .quality-evidence-surface,
.quality-results-panel-empty .quality-drilldown-surface,
.quality-results-panel-empty .quality-control-surface,
.quality-results-panel-empty .quality-history-surface {
display: none;
}
.quality-user-empty-state {
margin-top: 0.75rem;
}
.export-summary-surface,
.export-handoff-surface,
.handoff-action-card-technical {
display: none;
}
.latest-download-empty {
grid-column: 1 / -1;
}
.export-history-body {
display: grid;
gap: 0.75rem;
border-top: 1px solid var(--line);
padding: 0.75rem;
background: #ffffff;
}
.export-history-body > div:first-child {
display: none;
}
@media (min-width: 1100px) {
.workspace-grid-analysis {
grid-template-columns: minmax(0, 1fr);
}
.workspace-grid-analysis > section:nth-child(n),
.workspace-grid-analysis > details:nth-child(n) {
max-height: none;
overflow: visible;
}
}
@media (max-width: 1180px) {
.ai-user-summary {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
@media (max-width: 620px) {
.ai-user-summary {
grid-template-columns: minmax(0, 1fr);
}
.context-bar > div:nth-child(n + 3) {
display: none;
}
}
@media (min-width: 621px) { @media (min-width: 621px) {
.status-strip-grid { .status-strip-grid {
grid-template-columns: repeat(6, minmax(0, 1fr)); grid-template-columns: repeat(6, minmax(0, 1fr));