feat: simplify regional workbench language
This commit is contained in:
+3
-1
@@ -16,7 +16,9 @@
|
|||||||
- Preserved separate Mol/regional series keys and honest partial-sector population and 10 m forest-area limitations.
|
- Preserved separate Mol/regional series keys and honest partial-sector population and 10 m forest-area limitations.
|
||||||
- Replaced internal provider identifiers with readable source labels in the primary map.
|
- Replaced internal provider identifiers with readable source labels in the primary map.
|
||||||
- Added a provenance-gated full-Area query path for official pre-clipped datasets, avoiding redundant intersection of every feature against the same detailed municipal/regional boundary while preserving exact rectangle selection behavior.
|
- Added a provenance-gated full-Area query path for official pre-clipped datasets, avoiding redundant intersection of every feature against the same detailed municipal/regional boundary while preserving exact rectangle selection behavior.
|
||||||
- Added focused scope filtering, command construction, boundary resolution, multi-NIS provenance, packaging and frontend-label tests.
|
- Reduced a live complete-Kempen population/forest analysis from roughly 92 seconds to 2.8 seconds of API work while retaining the exact official totals.
|
||||||
|
- Grouped dated population and forest snapshots into one current source plus an optional historical disclosure, translated remaining user-facing status/download/model-evaluation text and made municipality selection explicitly optional.
|
||||||
|
- Added focused scope filtering, command construction, boundary resolution, multi-NIS provenance, full-Area correctness, temporal-language and frontend-label tests; the full readiness gate now passes with 571 tests.
|
||||||
|
|
||||||
## Sprint 193 End-user regional workbench simplification (2026-07-14)
|
## Sprint 193 End-user regional workbench simplification (2026-07-14)
|
||||||
|
|
||||||
|
|||||||
@@ -200,7 +200,7 @@ class TemporalAnalysisService:
|
|||||||
return (
|
return (
|
||||||
TemporalObjectChanges(available=False),
|
TemporalObjectChanges(available=False),
|
||||||
{"type": "FeatureCollection", "features": []},
|
{"type": "FeatureCollection", "features": []},
|
||||||
["Object-level changes are unavailable because the source does not guarantee stable feature identifiers."],
|
["Wijzigingen van individuele objecten kunnen voor deze bron niet betrouwbaar worden gevolgd."],
|
||||||
)
|
)
|
||||||
|
|
||||||
normalized_bbox = VectorFeatureService._normalize_selection_bbox(bbox)
|
normalized_bbox = VectorFeatureService._normalize_selection_bbox(bbox)
|
||||||
|
|||||||
@@ -15,19 +15,19 @@ def test_detection_lab_exposes_persisted_threshold_calibration_comparison() -> N
|
|||||||
|
|
||||||
assert "qualityChecks: QualityCheckRead[]" in source
|
assert "qualityChecks: QualityCheckRead[]" in source
|
||||||
assert "buildCalibrationRows(detectionRuns, qualityChecks)" in source
|
assert "buildCalibrationRows(detectionRuns, qualityChecks)" in source
|
||||||
assert "Calibration comparison" in source
|
assert "Kalibraties vergelijken" in source
|
||||||
assert "Compare persisted detection runs by confidence threshold" in source
|
assert "Vergelijk bewaarde analyseruns per zekerheidsdrempel" in source
|
||||||
assert "Best F1 candidate" in source
|
assert "Beste F1-score" in source
|
||||||
assert "Best precision candidate" in source
|
assert "Beste precisie" in source
|
||||||
assert "Lowest false-positive pressure" in source
|
assert "Minste foutieve meldingen" in source
|
||||||
assert "Threshold" in source
|
assert "Drempel" in source
|
||||||
assert "Precision" in source
|
assert "Precisie" in source
|
||||||
assert "Recall" in source
|
assert "Recall" in source
|
||||||
assert "F1" in source
|
assert "F1" in source
|
||||||
assert "False positives" in source
|
assert "Fout positief" in source
|
||||||
assert "False negatives" in source
|
assert "Fout negatief" in source
|
||||||
assert "Promote only after checking evidence across AOIs" in source
|
assert "Keur pas goed nadat meerdere gebieden" in source
|
||||||
assert "No calibration comparison available yet" in source
|
assert "Nog geen kalibratievergelijking beschikbaar" in source
|
||||||
assert "metricValue(check, 'f1')" in source
|
assert "metricValue(check, 'f1')" in source
|
||||||
assert "metricValue(check, 'precision')" in source
|
assert "metricValue(check, 'precision')" in source
|
||||||
assert "metricValue(check, 'recall')" in source
|
assert "metricValue(check, 'recall')" in source
|
||||||
|
|||||||
@@ -100,8 +100,8 @@ def test_detection_results_table_uses_bounded_local_pagination() -> None:
|
|||||||
assert "visibleDetectionItems" in lab
|
assert "visibleDetectionItems" in lab
|
||||||
assert "detectionItems.slice" in lab
|
assert "detectionItems.slice" in lab
|
||||||
assert "Detection result pagination" in lab
|
assert "Detection result pagination" in lab
|
||||||
assert "Previous detection results page" in lab
|
assert "Vorige resultatenpagina" in lab
|
||||||
assert "Next detection results page" in lab
|
assert "Volgende resultatenpagina" in lab
|
||||||
assert "formatSourceTilePath(detection.source_tile_path)" in lab
|
assert "formatSourceTilePath(detection.source_tile_path)" in lab
|
||||||
assert 'title={detection.source_tile_path ?? undefined}' in lab
|
assert 'title={detection.source_tile_path ?? undefined}' in lab
|
||||||
assert "pagination-toolbar" in styles
|
assert "pagination-toolbar" in styles
|
||||||
|
|||||||
@@ -274,6 +274,26 @@ def test_temporal_compare_returns_delta_and_canonical_change_payload(monkeypatch
|
|||||||
assert result.geojson["type"] == "FeatureCollection"
|
assert result.geojson["type"] == "FeatureCollection"
|
||||||
|
|
||||||
|
|
||||||
|
def test_unstable_temporal_identity_returns_clear_end_user_warning() -> None:
|
||||||
|
project_id = uuid4()
|
||||||
|
earlier = temporal_dataset(project_id=project_id, observed_year=2021)
|
||||||
|
later = temporal_dataset(project_id=project_id, observed_year=2025)
|
||||||
|
earlier.source_metadata["identity_stable"] = False
|
||||||
|
later.source_metadata["identity_stable"] = False
|
||||||
|
|
||||||
|
changes, geojson, warnings = TemporalAnalysisService._compare_identity_features(
|
||||||
|
SimpleNamespace(),
|
||||||
|
earlier=earlier,
|
||||||
|
later=later,
|
||||||
|
bbox={"min_x": 5.0, "min_y": 51.1, "max_x": 5.2, "max_y": 51.3},
|
||||||
|
preview_limit=100,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert changes.available is False
|
||||||
|
assert geojson == {"type": "FeatureCollection", "features": []}
|
||||||
|
assert warnings == ["Wijzigingen van individuele objecten kunnen voor deze bron niet betrouwbaar worden gevolgd."]
|
||||||
|
|
||||||
|
|
||||||
def test_temporal_frontend_and_official_operator_contracts_exist() -> None:
|
def test_temporal_frontend_and_official_operator_contracts_exist() -> None:
|
||||||
workspace = (ROOT / "frontend/src/components/map/MapWorkspace.tsx").read_text(encoding="utf-8")
|
workspace = (ROOT / "frontend/src/components/map/MapWorkspace.tsx").read_text(encoding="utf-8")
|
||||||
temporal_api = (ROOT / "frontend/src/services/api/temporal.ts").read_text(encoding="utf-8")
|
temporal_api = (ROOT / "frontend/src/services/api/temporal.ts").read_text(encoding="utf-8")
|
||||||
|
|||||||
@@ -131,7 +131,7 @@ def test_kempen_scope_operator_is_packaged_and_exposed_in_map_flow() -> None:
|
|||||||
assert "Kempen (28 gemeenten)" in workspace
|
assert "Kempen (28 gemeenten)" in workspace
|
||||||
assert 'aria-label="Regio"' not in workspace
|
assert 'aria-label="Regio"' not in workspace
|
||||||
assert 'aria-label="Ingeladen regiobereik"' in workspace
|
assert 'aria-label="Ingeladen regiobereik"' in workspace
|
||||||
assert "Gemeente of volledige regio" in workspace
|
assert "Snel naar een gemeente (optioneel)" in workspace
|
||||||
assert "projects={projects}" in app
|
assert "projects={projects}" in app
|
||||||
map_props = app.split("<MapWorkspace", maxsplit=1)[1].split("/>", maxsplit=1)[0]
|
map_props = app.split("<MapWorkspace", maxsplit=1)[1].split("/>", maxsplit=1)[0]
|
||||||
assert "onSelectProject={selectProject}" not in map_props
|
assert "onSelectProject={selectProject}" not in map_props
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ def test_regional_workspace_is_automatic_and_map_has_one_scope_selector() -> Non
|
|||||||
assert regional_check < municipality_check
|
assert regional_check < municipality_check
|
||||||
assert 'aria-label="Regio"' not in map_workspace
|
assert 'aria-label="Regio"' not in map_workspace
|
||||||
assert 'aria-label="Ingeladen regiobereik"' in map_workspace
|
assert 'aria-label="Ingeladen regiobereik"' in map_workspace
|
||||||
assert "Gemeente of volledige regio" in map_workspace
|
assert "Snel naar een gemeente (optioneel)" in map_workspace
|
||||||
assert 'aria-label="Werkgebied"' in map_workspace
|
assert 'aria-label="Werkgebied"' in map_workspace
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -184,11 +184,22 @@ def test_regional_timeseries_operator_is_packaged_and_release_checked() -> None:
|
|||||||
def test_end_user_dataset_sources_are_human_readable() -> None:
|
def test_end_user_dataset_sources_are_human_readable() -> None:
|
||||||
display = (ROOT / "frontend/src/lib/datasetDisplay.ts").read_text(encoding="utf-8")
|
display = (ROOT / "frontend/src/lib/datasetDisplay.ts").read_text(encoding="utf-8")
|
||||||
workspace = (ROOT / "frontend/src/components/map/MapWorkspace.tsx").read_text(encoding="utf-8")
|
workspace = (ROOT / "frontend/src/components/map/MapWorkspace.tsx").read_text(encoding="utf-8")
|
||||||
|
catalog = (ROOT / "frontend/src/components/datasets/DatasetPanel.tsx").read_text(encoding="utf-8")
|
||||||
|
status = (ROOT / "frontend/src/components/WorkbenchStatusStrip.tsx").read_text(encoding="utf-8")
|
||||||
|
detection = (ROOT / "frontend/src/components/detection/DetectionLab.tsx").read_text(encoding="utf-8")
|
||||||
|
exports = (ROOT / "frontend/src/components/exports/ExportCenter.tsx").read_text(encoding="utf-8")
|
||||||
|
|
||||||
assert "department_omgeving_land_use: 'Departement Omgeving'" in display
|
assert "department_omgeving_land_use: 'Departement Omgeving'" in display
|
||||||
assert "statbel: 'Statbel'" in display
|
assert "statbel: 'Statbel'" in display
|
||||||
assert "getDatasetSourceDisplayName(activeThemeDataset)" in workspace
|
assert "getDatasetSourceDisplayName(activeThemeDataset)" in workspace
|
||||||
assert "dataset ? getDatasetSourceDisplayName(dataset)" in workspace
|
assert "dataset ? getDatasetSourceDisplayName(dataset)" in workspace
|
||||||
|
assert "Snel naar een gemeente (optioneel)" in workspace
|
||||||
|
assert "latestDatasetBySeries" in catalog
|
||||||
|
assert "Historische meetmomenten" in catalog
|
||||||
|
assert "getDatasetSourceDisplayName(dataset)" in catalog
|
||||||
|
assert "statusLabel(item.state)" in status
|
||||||
|
assert "Technische modelevaluatie" in detection
|
||||||
|
assert "Nog geen downloads gemaakt." in exports
|
||||||
|
|
||||||
|
|
||||||
def test_full_area_fast_path_requires_matching_area_and_clipped_operator_provenance() -> None:
|
def test_full_area_fast_path_requires_matching_area_and_clipped_operator_provenance() -> None:
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ def test_frontend_wires_v1_workbench_status_strip() -> None:
|
|||||||
assert "activeLayerFeatureCount={mapFeatureCount}" in app
|
assert "activeLayerFeatureCount={mapFeatureCount}" in app
|
||||||
assert "selectedAreaHasGeometry={Boolean(areaFeatureCollection)}" in app
|
assert "selectedAreaHasGeometry={Boolean(areaFeatureCollection)}" in app
|
||||||
assert "Platformstatus" in component
|
assert "Platformstatus" in component
|
||||||
assert "Workbench status:" in component
|
assert "Status werkruimte:" in component
|
||||||
assert "Kaartwerkruimte is gebruiksklaar" 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
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ def test_detection_lab_exposes_structured_surfaces() -> None:
|
|||||||
assert 'className="ai-lab-results-surface"' in lab
|
assert 'className="ai-lab-results-surface"' in lab
|
||||||
assert 'aria-label="Detection results"' in lab
|
assert 'aria-label="Detection results"' in lab
|
||||||
assert 'className="ai-lab-qa-surface"' in lab
|
assert 'className="ai-lab-qa-surface"' in lab
|
||||||
assert 'aria-label="Detection QA controls and results"' in lab
|
assert 'aria-label="Kwaliteitscontrole gebouwdetectie"' in lab
|
||||||
|
|
||||||
|
|
||||||
def test_segmentation_lab_exposes_structured_surfaces() -> None:
|
def test_segmentation_lab_exposes_structured_surfaces() -> None:
|
||||||
@@ -53,7 +53,7 @@ def test_ai_lab_preserves_existing_detection_and_segmentation_controls() -> None
|
|||||||
assert "onRunQa" in detection
|
assert "onRunQa" in detection
|
||||||
assert "detectionTileManifestPath" in detection
|
assert "detectionTileManifestPath" in detection
|
||||||
assert "{detectionItems.length} objecten geladen" in detection
|
assert "{detectionItems.length} objecten geladen" in detection
|
||||||
assert "Compare detections to reference" in detection
|
assert "Vergelijk met referentielaag" in detection
|
||||||
|
|
||||||
assert "onRunSegmentation" in segmentation
|
assert "onRunSegmentation" in segmentation
|
||||||
assert "onLoadResults" in segmentation
|
assert "onLoadResults" in segmentation
|
||||||
|
|||||||
@@ -1,3 +1,29 @@
|
|||||||
|
## Sprint 194 Regional official time series and full-Area performance (2026-07-14)
|
||||||
|
|
||||||
|
Changed:
|
||||||
|
- Generalized the explicit Statbel operator to the approved 28-municipality scope and added one coordinator for official 2021-2025 population plus 2013-2025 forest snapshots.
|
||||||
|
- Added municipality-partitioned official 10 m WCS retrieval after the upstream response-size limit rejected a single full-region request; source partitions are resumable and mosaicked locally before exact region clipping.
|
||||||
|
- Added a provenance-gated full-Area selection path for trusted pre-clipped operator datasets. Free rectangles, unrelated Areas and ordinary uploads continue to execute the normal PostGIS intersection path.
|
||||||
|
- Grouped older dated snapshots behind `Historische meetmomenten`, replaced raw provider/type labels in the primary catalog and translated the remaining visible temporal, download, platform-status and detection-evaluation language.
|
||||||
|
|
||||||
|
Live regional evidence:
|
||||||
|
- Population datasets: five exact dated snapshots, 3,317 persisted `vector_features`; 2021 `489,927` inhabitants and 2025 `506,473`, a measured increase of `16,546` (`3.377%`).
|
||||||
|
- Forest datasets: five exact dated snapshots, 273,669 persisted `vector_features`; 2013 `27,659.976 ha` and 2025 `27,410.687 ha`, a source-resolution change of `-249.289 ha` (`-0.9013%`).
|
||||||
|
- All ten snapshots have one immutable dataset version and zero invalid, empty, non-4326 or source-ID-missing geometries. A repeated synchronization reused the same ten Dataset IDs without duplicates.
|
||||||
|
- Complete-Kempen current analysis returns 506,473 inhabitants, 27,410.69 ha forest, 466,078 buildings, 88,332 water features, 84,504 roads and 415,288 parcels over 1,399.25 km2.
|
||||||
|
|
||||||
|
Performance and runtime evidence:
|
||||||
|
- Population full-Area API selection completed in `1.738 s`; forest completed in `1.083 s`. The browser rendered the full six-theme analysis and 2021-2025 comparison within the 3.5-second verification window, replacing the previous roughly 92-second forest-heavy workflow.
|
||||||
|
- Tower health, PostGIS 3.6, required schema objects, Alembic head `202607140001`, frontend, API proxy and icon checks passed after deployment.
|
||||||
|
- PyTorch `2.13.0+cpu`, Ultralytics `8.4.95` and the active local model file load successfully. The catalog exposes 24 local assets; the active seven-AOI profile remains review-required at F1 `0.5825` and was not blindly retrained.
|
||||||
|
|
||||||
|
Tested:
|
||||||
|
- `bash scripts/run_readiness_check.sh` passed with `571 passed`, backend compile, one Alembic head, frontend typecheck/build and live-smoke syntax validation.
|
||||||
|
- Focused regional, temporal and end-user contract checks passed. The browser confirmed the complete regional default, six current themes, five population moments, exact temporal totals and no required project/region selection step.
|
||||||
|
|
||||||
|
Next:
|
||||||
|
- Complete the 48 false-positive and 48 false-negative operator review decisions, then use those decisions to build a justified next training corpus and expose the existing raster-to-tiles-to-detection workflow as one guided end-user action.
|
||||||
|
|
||||||
## Sprint 193 End-user regional workbench simplification (2026-07-14)
|
## Sprint 193 End-user regional workbench simplification (2026-07-14)
|
||||||
|
|
||||||
Changed:
|
Changed:
|
||||||
|
|||||||
+1
-1
@@ -14,7 +14,7 @@
|
|||||||
- [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] 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] 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.
|
- [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.
|
- [x] 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`.
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { AreaRead, DatasetCreateResponse, ExportRead, ProjectRead, QualityCheckRead } from '../types'
|
import type { AreaRead, DatasetCreateResponse, ExportRead, ProjectRead, QualityCheckRead } from '../types'
|
||||||
|
import { REGIONAL_WORKSPACE_LABEL, REGIONAL_WORKSPACE_PROJECT_NAME } from '../config/primaryFocus'
|
||||||
|
|
||||||
interface StatusItem {
|
interface StatusItem {
|
||||||
key: string
|
key: string
|
||||||
@@ -26,6 +27,17 @@ function countReferenceDatasets(datasets: DatasetCreateResponse[]): number {
|
|||||||
return datasets.filter((dataset) => dataset.dataset_role === 'reference').length
|
return datasets.filter((dataset) => dataset.dataset_role === 'reference').length
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function statusLabel(state: StatusItem['state']): string {
|
||||||
|
if (state === 'ready') return 'gereed'
|
||||||
|
if (state === 'warning') return 'aandacht'
|
||||||
|
return 'nog open'
|
||||||
|
}
|
||||||
|
|
||||||
|
function projectLabel(project: ProjectRead | null): string {
|
||||||
|
if (!project) return 'Geen werkruimte'
|
||||||
|
return project.name === REGIONAL_WORKSPACE_PROJECT_NAME ? REGIONAL_WORKSPACE_LABEL : project.name
|
||||||
|
}
|
||||||
|
|
||||||
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 'De regionale werkruimte kon niet worden geopend.'
|
return 'De regionale werkruimte kon niet worden geopend.'
|
||||||
@@ -65,7 +77,7 @@ export function WorkbenchStatusStrip({
|
|||||||
{
|
{
|
||||||
key: 'project',
|
key: 'project',
|
||||||
label: 'Werkruimte',
|
label: 'Werkruimte',
|
||||||
value: selectedProject ? selectedProject.name : 'Geen werkruimte',
|
value: projectLabel(selectedProject),
|
||||||
detail: selectedProject ? selectedProject.region : 'Niet geladen',
|
detail: selectedProject ? selectedProject.region : 'Niet geladen',
|
||||||
state: selectedProject ? 'ready' : 'waiting',
|
state: selectedProject ? 'ready' : 'waiting',
|
||||||
},
|
},
|
||||||
@@ -114,12 +126,12 @@ export function WorkbenchStatusStrip({
|
|||||||
<div>
|
<div>
|
||||||
<p className="eyebrow">Platformstatus</p>
|
<p className="eyebrow">Platformstatus</p>
|
||||||
<h2>
|
<h2>
|
||||||
<span className="sr-only">Workbench status: </span>
|
<span className="sr-only">Status werkruimte: </span>
|
||||||
{coreReady ? 'Kaartwerkruimte is gebruiksklaar' : 'Kaartwerkruimte vraagt aandacht'}
|
{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} van ${items.length} onderdelen gereed`}>
|
||||||
<strong>{readyCount}/{items.length}</strong>
|
<strong>{readyCount}/{items.length}</strong>
|
||||||
<span>onderdelen gereed</span>
|
<span>onderdelen gereed</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -129,7 +141,7 @@ export function WorkbenchStatusStrip({
|
|||||||
<div className={`status-tile status-tile-${item.state}`} key={item.key}>
|
<div className={`status-tile status-tile-${item.state}`} key={item.key}>
|
||||||
<div className="status-tile-topline">
|
<div className="status-tile-topline">
|
||||||
<span>{item.label}</span>
|
<span>{item.label}</span>
|
||||||
<span className="status-pill">{item.state}</span>
|
<span className="status-pill">{statusLabel(item.state)}</span>
|
||||||
</div>
|
</div>
|
||||||
<strong title={item.value}>{item.value}</strong>
|
<strong title={item.value}>{item.value}</strong>
|
||||||
<p>{item.detail}</p>
|
<p>{item.detail}</p>
|
||||||
|
|||||||
@@ -1,6 +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'
|
import { getDatasetDisplayName, getDatasetSourceDisplayName } from '../../lib/datasetDisplay'
|
||||||
|
|
||||||
interface DatasetFormState {
|
interface DatasetFormState {
|
||||||
datasetType: string
|
datasetType: string
|
||||||
@@ -63,6 +63,9 @@ function normalizeDatasetRole(dataset: DatasetCreateResponse): 'reference' | 'ca
|
|||||||
if (dataset.dataset_role === 'reference') {
|
if (dataset.dataset_role === 'reference') {
|
||||||
return 'reference'
|
return 'reference'
|
||||||
}
|
}
|
||||||
|
if (dataset.dataset_role === 'source') {
|
||||||
|
return 'source'
|
||||||
|
}
|
||||||
if (dataset.dataset_role === 'derived' || isVectorDatasetType(dataset.dataset_type)) {
|
if (dataset.dataset_role === 'derived' || isVectorDatasetType(dataset.dataset_type)) {
|
||||||
return 'candidate'
|
return 'candidate'
|
||||||
}
|
}
|
||||||
@@ -76,7 +79,7 @@ function datasetRoleLabel(role: string): string {
|
|||||||
if (role === 'candidate') {
|
if (role === 'candidate') {
|
||||||
return 'Analyse-resultaat'
|
return 'Analyse-resultaat'
|
||||||
}
|
}
|
||||||
return 'Eigen bron'
|
return 'Basisbron'
|
||||||
}
|
}
|
||||||
|
|
||||||
function datasetActionHint(dataset: DatasetCreateResponse, role: string): string {
|
function datasetActionHint(dataset: DatasetCreateResponse, role: string): string {
|
||||||
@@ -92,6 +95,19 @@ function datasetActionHint(dataset: DatasetCreateResponse, role: string): string
|
|||||||
return 'Ingeladen gegevensbron voor verdere analyse.'
|
return 'Ingeladen gegevensbron voor verdere analyse.'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatObservationDate(value: string | null | undefined): string {
|
||||||
|
if (!value) return 'huidige toestand'
|
||||||
|
const date = new Date(value)
|
||||||
|
if (Number.isNaN(date.getTime())) return value
|
||||||
|
return new Intl.DateTimeFormat('nl-BE', { day: 'numeric', month: 'short', year: 'numeric' }).format(date)
|
||||||
|
}
|
||||||
|
|
||||||
|
function datasetKindLabel(dataset: DatasetCreateResponse): string {
|
||||||
|
if (dataset.dataset_type === 'raster') return 'Luchtbeeld'
|
||||||
|
if (isVectorDatasetType(dataset.dataset_type)) return 'Kaartlaag'
|
||||||
|
return 'Gegevensbron'
|
||||||
|
}
|
||||||
|
|
||||||
export function DatasetPanel({
|
export function DatasetPanel({
|
||||||
selectedProjectId,
|
selectedProjectId,
|
||||||
selectedDatasetId,
|
selectedDatasetId,
|
||||||
@@ -107,8 +123,24 @@ export function DatasetPanel({
|
|||||||
onOpenDatasetInMap,
|
onOpenDatasetInMap,
|
||||||
onOpenDatasetExport,
|
onOpenDatasetExport,
|
||||||
}: DatasetPanelProps) {
|
}: DatasetPanelProps) {
|
||||||
const readyDatasets = datasets.filter((dataset) => dataset.status === 'ready').length
|
|
||||||
const selectedDataset = datasets.find((dataset) => dataset.id === selectedDatasetId)
|
const selectedDataset = datasets.find((dataset) => dataset.id === selectedDatasetId)
|
||||||
|
const latestDatasetBySeries = new Map<string, DatasetCreateResponse>()
|
||||||
|
datasets.forEach((dataset) => {
|
||||||
|
if (!dataset.temporal_series_key) return
|
||||||
|
const current = latestDatasetBySeries.get(dataset.temporal_series_key)
|
||||||
|
const currentTimestamp = current?.observed_at ? Date.parse(current.observed_at) : Number.NEGATIVE_INFINITY
|
||||||
|
const candidateTimestamp = dataset.observed_at ? Date.parse(dataset.observed_at) : Number.NEGATIVE_INFINITY
|
||||||
|
if (!current || candidateTimestamp > currentTimestamp) {
|
||||||
|
latestDatasetBySeries.set(dataset.temporal_series_key, dataset)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const primaryDatasets = datasets.filter(
|
||||||
|
(dataset) => !dataset.temporal_series_key || latestDatasetBySeries.get(dataset.temporal_series_key)?.id === dataset.id,
|
||||||
|
)
|
||||||
|
const historicalDatasets = datasets.filter(
|
||||||
|
(dataset) => dataset.temporal_series_key && latestDatasetBySeries.get(dataset.temporal_series_key)?.id !== dataset.id,
|
||||||
|
)
|
||||||
|
const readyDatasets = primaryDatasets.filter((dataset) => dataset.status === 'ready').length
|
||||||
const roleSummaries = [
|
const roleSummaries = [
|
||||||
{
|
{
|
||||||
key: 'selected',
|
key: 'selected',
|
||||||
@@ -120,21 +152,21 @@ export function DatasetPanel({
|
|||||||
{
|
{
|
||||||
key: 'reference',
|
key: 'reference',
|
||||||
label: 'Referentie',
|
label: 'Referentie',
|
||||||
count: datasets.filter((dataset) => normalizeDatasetRole(dataset) === 'reference').length,
|
count: primaryDatasets.filter((dataset) => normalizeDatasetRole(dataset) === 'reference').length,
|
||||||
hint: 'Officieel',
|
hint: 'Officieel',
|
||||||
className: 'dataset-role-reference',
|
className: 'dataset-role-reference',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'candidate',
|
key: 'candidate',
|
||||||
label: 'Resultaat',
|
label: 'Resultaat',
|
||||||
count: datasets.filter((dataset) => normalizeDatasetRole(dataset) === 'candidate').length,
|
count: primaryDatasets.filter((dataset) => normalizeDatasetRole(dataset) === 'candidate').length,
|
||||||
hint: 'Afgeleid',
|
hint: 'Afgeleid',
|
||||||
className: 'dataset-role-candidate',
|
className: 'dataset-role-candidate',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'source',
|
key: 'source',
|
||||||
label: 'Eigen bron',
|
label: 'Basisbron',
|
||||||
count: datasets.filter((dataset) => normalizeDatasetRole(dataset) === 'source').length,
|
count: primaryDatasets.filter((dataset) => normalizeDatasetRole(dataset) === 'source').length,
|
||||||
hint: 'Ingeladen',
|
hint: 'Ingeladen',
|
||||||
className: 'dataset-role-source',
|
className: 'dataset-role-source',
|
||||||
},
|
},
|
||||||
@@ -147,7 +179,7 @@ export function DatasetPanel({
|
|||||||
<p className="eyebrow">Ingeladen gegevens</p>
|
<p className="eyebrow">Ingeladen gegevens</p>
|
||||||
<h2>Bronnen</h2>
|
<h2>Bronnen</h2>
|
||||||
</div>
|
</div>
|
||||||
<span className="count-pill">{readyDatasets}/{datasets.length} beschikbaar</span>
|
<span className="count-pill">{readyDatasets}/{primaryDatasets.length} bronnen beschikbaar</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="data-selection-summary data-selection-summary-dataset">
|
<div className="data-selection-summary data-selection-summary-dataset">
|
||||||
@@ -165,17 +197,17 @@ export function DatasetPanel({
|
|||||||
value={datasetForm.datasetType}
|
value={datasetForm.datasetType}
|
||||||
onChange={(event) => onDatasetFormChange((previous) => ({ ...previous, datasetType: event.target.value }))}
|
onChange={(event) => onDatasetFormChange((previous) => ({ ...previous, datasetType: event.target.value }))}
|
||||||
>
|
>
|
||||||
<option value="vector">vector</option>
|
<option value="vector">Kaartlaag</option>
|
||||||
<option value="geojson">geojson</option>
|
<option value="geojson">GeoJSON-kaartlaag</option>
|
||||||
<option value="raster">raster</option>
|
<option value="raster">Luchtbeeld of raster</option>
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
<label>
|
<label>
|
||||||
Bronnaam
|
Naam van de bron
|
||||||
<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 }))}
|
||||||
placeholder="user_upload"
|
placeholder="bijvoorbeeld eigen luchtbeeld"
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
<label>
|
<label>
|
||||||
@@ -226,7 +258,7 @@ export function DatasetPanel({
|
|||||||
<div className="data-panel-list-block dataset-catalog-block">
|
<div className="data-panel-list-block dataset-catalog-block">
|
||||||
<p className="data-section-label">Beschikbare bronnen</p>
|
<p className="data-section-label">Beschikbare bronnen</p>
|
||||||
<ul className="dataset-list">
|
<ul className="dataset-list">
|
||||||
{datasets.map((dataset) => {
|
{primaryDatasets.map((dataset) => {
|
||||||
const datasetRole = normalizeDatasetRole(dataset)
|
const datasetRole = normalizeDatasetRole(dataset)
|
||||||
const roleLabel = datasetRoleLabel(datasetRole)
|
const roleLabel = datasetRoleLabel(datasetRole)
|
||||||
return (
|
return (
|
||||||
@@ -235,12 +267,12 @@ export function DatasetPanel({
|
|||||||
<div>
|
<div>
|
||||||
<div className="dataset-card-kicker">
|
<div className="dataset-card-kicker">
|
||||||
<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>{datasetKindLabel(dataset)}</span>
|
||||||
</div>
|
</div>
|
||||||
<strong className="dataset-card-title">{getDatasetDisplayName(dataset)}</strong>
|
<strong className="dataset-card-title">{getDatasetDisplayName(dataset)}</strong>
|
||||||
<div className="entity-meta dataset-card-primary-meta">
|
<div className="entity-meta dataset-card-primary-meta">
|
||||||
<span>{(dataset.feature_count ?? dataset.vector_summary?.feature_count ?? 0).toLocaleString('nl-BE')} objecten</span>
|
<span>{(dataset.feature_count ?? dataset.vector_summary?.feature_count ?? 0).toLocaleString('nl-BE')} objecten</span>
|
||||||
<span>{dataset.source_name ?? dataset.source}</span>
|
<span>{getDatasetSourceDisplayName(dataset)} · {formatObservationDate(dataset.observed_at)}</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'}>
|
||||||
@@ -305,6 +337,28 @@ export function DatasetPanel({
|
|||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
</ul>
|
</ul>
|
||||||
|
{historicalDatasets.length > 0 ? (
|
||||||
|
<details className="dataset-history-disclosure">
|
||||||
|
<summary>
|
||||||
|
<span>Historische meetmomenten</span>
|
||||||
|
<strong>{historicalDatasets.length} oudere lagen</strong>
|
||||||
|
</summary>
|
||||||
|
<ul className="dataset-history-list">
|
||||||
|
{historicalDatasets.map((dataset) => (
|
||||||
|
<li key={dataset.id}>
|
||||||
|
<div>
|
||||||
|
<strong>{getDatasetDisplayName(dataset)}</strong>
|
||||||
|
<span>{formatObservationDate(dataset.observed_at)} · {(dataset.feature_count ?? dataset.vector_summary?.feature_count ?? 0).toLocaleString('nl-BE')} objecten</span>
|
||||||
|
</div>
|
||||||
|
<div className="panel-action-row">
|
||||||
|
<button className="secondary-action" type="button" onClick={() => onOpenDatasetInMap(dataset)}>Kaart</button>
|
||||||
|
<button className="secondary-action" type="button" onClick={() => onOpenDatasetExport(dataset)}>Downloaden</button>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</details>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -16,6 +16,13 @@ import { DETECTION_OPERATOR_PROFILES, type DetectionOperatorProfile } from './de
|
|||||||
const DETECTION_PAGE_SIZE_OPTIONS = [25, 50, 100] as const
|
const DETECTION_PAGE_SIZE_OPTIONS = [25, 50, 100] as const
|
||||||
const DEFAULT_DETECTION_PAGE_SIZE = 50
|
const DEFAULT_DETECTION_PAGE_SIZE = 50
|
||||||
|
|
||||||
|
function detectionModelLabel(model: DetectionModelCapability): string {
|
||||||
|
if (model.model_id === 'yolo-configured') return 'Lokaal gebouwmodel'
|
||||||
|
if (model.model_id === 'manual-fixture-detector') return 'Testmodel (alleen voor demo)'
|
||||||
|
if (model.model_id === 'yolo-placeholder') return 'Gebouwmodel nog niet geconfigureerd'
|
||||||
|
return model.display_name
|
||||||
|
}
|
||||||
|
|
||||||
interface CalibrationRow {
|
interface CalibrationRow {
|
||||||
analysisRunId: string
|
analysisRunId: string
|
||||||
qualityCheckId: string
|
qualityCheckId: string
|
||||||
@@ -272,7 +279,7 @@ export function DetectionLab({
|
|||||||
<ul className="model-list">
|
<ul className="model-list">
|
||||||
{detectionModels.map((model) => (
|
{detectionModels.map((model) => (
|
||||||
<li className={model.configured ? 'model-card model-card-ready' : 'model-card'} key={model.model_id}>
|
<li className={model.configured ? 'model-card model-card-ready' : 'model-card'} key={model.model_id}>
|
||||||
<strong>{model.display_name}</strong>
|
<strong>{detectionModelLabel(model)}</strong>
|
||||||
<span className={model.configured ? 'status-badge status-badge-ready' : 'status-badge'}>{model.status}</span>
|
<span className={model.configured ? 'status-badge status-badge-ready' : 'status-badge'}>{model.status}</span>
|
||||||
<div className="entity-meta">
|
<div className="entity-meta">
|
||||||
<span>{model.model_id}</span>
|
<span>{model.model_id}</span>
|
||||||
@@ -560,7 +567,7 @@ export function DetectionLab({
|
|||||||
<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}>
|
||||||
{model.display_name}
|
{detectionModelLabel(model)}
|
||||||
</option>
|
</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
@@ -587,7 +594,7 @@ export function DetectionLab({
|
|||||||
Beeldtegelbestand
|
Beeldtegelbestand
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Raster tile manifest path"
|
placeholder="Pad naar de aangemaakte beeldtegels"
|
||||||
value={detectionTileManifestPath}
|
value={detectionTileManifestPath}
|
||||||
onChange={(event) => onSetTileManifestPath(event.target.value)}
|
onChange={(event) => onSetTileManifestPath(event.target.value)}
|
||||||
/>
|
/>
|
||||||
@@ -609,17 +616,17 @@ export function DetectionLab({
|
|||||||
<div className="ai-lab-state-stack">
|
<div className="ai-lab-state-stack">
|
||||||
{detectionRunError ? (
|
{detectionRunError ? (
|
||||||
<div className="result-state result-state-error">
|
<div className="result-state result-state-error">
|
||||||
<strong>Detection run failed.</strong>
|
<strong>De beeldanalyse is mislukt.</strong>
|
||||||
<p>{detectionRunError}</p>
|
<p>{detectionRunError}</p>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
{detectionRunResult ? (
|
{detectionRunResult ? (
|
||||||
<div className="result-summary-card">
|
<div className="result-summary-card">
|
||||||
<p>Status: {detectionRunResult.status}</p>
|
<p>Status: {detectionRunResult.status}</p>
|
||||||
<p>Message: {detectionRunResult.message}</p>
|
<p>Uitleg: {detectionRunResult.message}</p>
|
||||||
<p>Analysis run: {detectionRunResult.analysis_run_id}</p>
|
<p>Analyse: {detectionRunResult.analysis_run_id}</p>
|
||||||
<p>Job: {detectionRunResult.job_id}</p>
|
<p>Verwerking: {detectionRunResult.job_id}</p>
|
||||||
<p>Detections: {detectionRunResult.detection_count}</p>
|
<p>Gevonden objecten: {detectionRunResult.detection_count}</p>
|
||||||
{detectionRunResult.error_code ? <p className="error">Code: {detectionRunResult.error_code}</p> : null}
|
{detectionRunResult.error_code ? <p className="error">Code: {detectionRunResult.error_code}</p> : null}
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
@@ -765,7 +772,7 @@ export function DetectionLab({
|
|||||||
<option value="">Kies een bewaarde analyse</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 || 'Gebouwdetectie'} · {run.status} · {run.id}
|
||||||
</option>
|
</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
@@ -774,7 +781,7 @@ export function DetectionLab({
|
|||||||
Type object
|
Type object
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="bijvoorbeeld building"
|
placeholder="bijvoorbeeld gebouw"
|
||||||
value={detectionClassFilter}
|
value={detectionClassFilter}
|
||||||
onChange={(event) => onSetClassFilter(event.target.value)}
|
onChange={(event) => onSetClassFilter(event.target.value)}
|
||||||
/>
|
/>
|
||||||
@@ -811,10 +818,10 @@ export function DetectionLab({
|
|||||||
<div className="pagination-toolbar" aria-label="Detection result pagination">
|
<div className="pagination-toolbar" aria-label="Detection result pagination">
|
||||||
<p className="pagination-summary" aria-live="polite">
|
<p className="pagination-summary" aria-live="polite">
|
||||||
<strong>{detectionPageStart + 1}-{detectionPageEnd}</strong>
|
<strong>{detectionPageStart + 1}-{detectionPageEnd}</strong>
|
||||||
<span>of {detectionItems.length}</span>
|
<span>van {detectionItems.length}</span>
|
||||||
</p>
|
</p>
|
||||||
<label className="pagination-page-size">
|
<label className="pagination-page-size">
|
||||||
Rows
|
Rijen
|
||||||
<select
|
<select
|
||||||
value={detectionPageSize}
|
value={detectionPageSize}
|
||||||
onChange={(event) => {
|
onChange={(event) => {
|
||||||
@@ -831,19 +838,19 @@ export function DetectionLab({
|
|||||||
<button
|
<button
|
||||||
className="secondary-action pagination-button"
|
className="secondary-action pagination-button"
|
||||||
type="button"
|
type="button"
|
||||||
aria-label="Previous detection results page"
|
aria-label="Vorige resultatenpagina"
|
||||||
title="Previous page"
|
title="Vorige pagina"
|
||||||
disabled={currentDetectionPage <= 1}
|
disabled={currentDetectionPage <= 1}
|
||||||
onClick={() => setDetectionResultPage(currentDetectionPage - 1)}
|
onClick={() => setDetectionResultPage(currentDetectionPage - 1)}
|
||||||
>
|
>
|
||||||
{'<'}
|
{'<'}
|
||||||
</button>
|
</button>
|
||||||
<span>Page {currentDetectionPage} of {detectionPageCount}</span>
|
<span>Pagina {currentDetectionPage} van {detectionPageCount}</span>
|
||||||
<button
|
<button
|
||||||
className="secondary-action pagination-button"
|
className="secondary-action pagination-button"
|
||||||
type="button"
|
type="button"
|
||||||
aria-label="Next detection results page"
|
aria-label="Volgende resultatenpagina"
|
||||||
title="Next page"
|
title="Volgende pagina"
|
||||||
disabled={currentDetectionPage >= detectionPageCount}
|
disabled={currentDetectionPage >= detectionPageCount}
|
||||||
onClick={() => setDetectionResultPage(currentDetectionPage + 1)}
|
onClick={() => setDetectionResultPage(currentDetectionPage + 1)}
|
||||||
>
|
>
|
||||||
@@ -855,10 +862,10 @@ export function DetectionLab({
|
|||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Class</th>
|
<th>Type</th>
|
||||||
<th>Confidence</th>
|
<th>Zekerheid</th>
|
||||||
<th>Model</th>
|
<th>Model</th>
|
||||||
<th>Source tile</th>
|
<th>Beeldtegel</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@@ -879,34 +886,40 @@ export function DetectionLab({
|
|||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="ai-lab-results-surface calibration-comparison-surface" aria-label="Detection calibration comparison">
|
<details className="secondary-analysis-disclosure detection-technical-evaluation">
|
||||||
|
<summary>
|
||||||
|
<span>Technische modelevaluatie</span>
|
||||||
|
<strong>{calibrationRows.length > 0 || detectionQaResult ? 'resultaten beschikbaar' : 'optioneel'}</strong>
|
||||||
|
</summary>
|
||||||
|
<div className="ai-lab-disclosure-body">
|
||||||
|
<div className="ai-lab-results-surface calibration-comparison-surface" aria-label="Vergelijking modelkalibratie">
|
||||||
<div className="panel-title-row">
|
<div className="panel-title-row">
|
||||||
<div>
|
<div>
|
||||||
<h3>Calibration comparison</h3>
|
<h3>Kalibraties vergelijken</h3>
|
||||||
<p className="muted">Compare persisted detection runs by confidence threshold before promoting a model setting.</p>
|
<p className="muted">Vergelijk bewaarde analyseruns per zekerheidsdrempel voordat een modelinstelling wordt goedgekeurd.</p>
|
||||||
</div>
|
</div>
|
||||||
<span className="count-pill">{calibrationRows.length} rows</span>
|
<span className="count-pill">{calibrationRows.length} resultaten</span>
|
||||||
</div>
|
</div>
|
||||||
{calibrationRows.length > 0 ? (
|
{calibrationRows.length > 0 ? (
|
||||||
<>
|
<>
|
||||||
<div className="calibration-summary-grid" aria-label="Calibration comparison winners">
|
<div className="calibration-summary-grid" aria-label="Calibration comparison winners">
|
||||||
<CalibrationSummaryCard title="Best F1 candidate" row={bestF1Candidate} metric="f1" />
|
<CalibrationSummaryCard title="Beste F1-score" row={bestF1Candidate} metric="f1" />
|
||||||
<CalibrationSummaryCard title="Best precision candidate" row={bestPrecisionCandidate} metric="precision" />
|
<CalibrationSummaryCard title="Beste precisie" row={bestPrecisionCandidate} metric="precision" />
|
||||||
<CalibrationSummaryCard title="Lowest false-positive pressure" row={lowestFalsePositivePressureCandidate} metric="falsePositives" />
|
<CalibrationSummaryCard title="Minste foutieve meldingen" row={lowestFalsePositivePressureCandidate} metric="falsePositives" />
|
||||||
</div>
|
</div>
|
||||||
<div className="table-scroll">
|
<div className="table-scroll">
|
||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Threshold</th>
|
<th>Drempel</th>
|
||||||
<th>Model</th>
|
<th>Model</th>
|
||||||
<th>Detections</th>
|
<th>Objecten</th>
|
||||||
<th>Precision</th>
|
<th>Precisie</th>
|
||||||
<th>Recall</th>
|
<th>Recall</th>
|
||||||
<th>F1</th>
|
<th>F1</th>
|
||||||
<th>False positives</th>
|
<th>Fout positief</th>
|
||||||
<th>False negatives</th>
|
<th>Fout negatief</th>
|
||||||
<th>Quality check</th>
|
<th>Kwaliteitscontrole</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@@ -915,7 +928,7 @@ export function DetectionLab({
|
|||||||
<td>{formatNullableNumber(row.threshold, 2)}</td>
|
<td>{formatNullableNumber(row.threshold, 2)}</td>
|
||||||
<td>
|
<td>
|
||||||
<strong>{row.modelName}</strong>
|
<strong>{row.modelName}</strong>
|
||||||
<span className="table-subtle">{row.modelAssetId ?? 'runtime configured path'}</span>
|
<span className="table-subtle">{row.modelAssetId ?? 'geconfigureerd lokaal model'}</span>
|
||||||
</td>
|
</td>
|
||||||
<td>{row.detectionCount ?? 'n/a'}</td>
|
<td>{row.detectionCount ?? 'n/a'}</td>
|
||||||
<td>{formatNullableNumber(row.precision, 3)}</td>
|
<td>{formatNullableNumber(row.precision, 3)}</td>
|
||||||
@@ -930,24 +943,24 @@ export function DetectionLab({
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
<div className="lab-action-guardrail">
|
<div className="lab-action-guardrail">
|
||||||
<span>Promotion guardrail</span>
|
<span>Voorwaarde voor goedkeuring</span>
|
||||||
<strong>Promote only after checking evidence across AOIs, false positives and false negatives.</strong>
|
<strong>Keur pas goed nadat meerdere gebieden en de foutieve positieve en negatieve resultaten zijn gecontroleerd.</strong>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<div className="result-state result-state-empty">
|
<div className="result-state result-state-empty">
|
||||||
<strong>No calibration comparison available yet.</strong>
|
<strong>Nog geen kalibratievergelijking beschikbaar.</strong>
|
||||||
<p>Run configured YOLO at multiple confidence thresholds, then compare each persisted detection run against the same reference dataset.</p>
|
<p>Voer het lokale model met meerdere zekerheidsdrempels uit en vergelijk de resultaten met dezelfde referentielaag.</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="ai-lab-qa-surface" aria-label="Detection QA controls and results">
|
<div className="ai-lab-qa-surface" aria-label="Kwaliteitscontrole gebouwdetectie">
|
||||||
<h3>Detection QA</h3>
|
<h3>Kwaliteitscontrole gebouwdetectie</h3>
|
||||||
<label>
|
<label>
|
||||||
Reference dataset
|
Referentielaag
|
||||||
<select value={detectionReferenceDatasetId} onChange={(event) => onSelectReferenceDataset(event.target.value)}>
|
<select value={detectionReferenceDatasetId} onChange={(event) => onSelectReferenceDataset(event.target.value)}>
|
||||||
<option value="">Select reference dataset</option>
|
<option value="">Kies een referentielaag</option>
|
||||||
{referenceDatasets.map((dataset) => (
|
{referenceDatasets.map((dataset) => (
|
||||||
<option key={dataset.id} value={dataset.id}>
|
<option key={dataset.id} value={dataset.id}>
|
||||||
{dataset.name}
|
{dataset.name}
|
||||||
@@ -956,24 +969,24 @@ export function DetectionLab({
|
|||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
<button className="primary-action" type="button" onClick={onRunQa} disabled={runningDetectionQa || !selectedDetectionRunId || !detectionReferenceDatasetId}>
|
<button className="primary-action" type="button" onClick={onRunQa} disabled={runningDetectionQa || !selectedDetectionRunId || !detectionReferenceDatasetId}>
|
||||||
Compare detections to reference
|
Vergelijk met referentielaag
|
||||||
</button>
|
</button>
|
||||||
{detectionQaError ? (
|
{detectionQaError ? (
|
||||||
<div className="result-state result-state-error">
|
<div className="result-state result-state-error">
|
||||||
<strong>Detection QA failed.</strong>
|
<strong>De kwaliteitscontrole is mislukt.</strong>
|
||||||
<p>{detectionQaError}</p>
|
<p>{detectionQaError}</p>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
{detectionQaResult ? (
|
{detectionQaResult ? (
|
||||||
<div className="result-summary-card">
|
<div className="result-summary-card">
|
||||||
<p>Status: {detectionQaResult.status}</p>
|
<p>Status: {detectionQaResult.status}</p>
|
||||||
<p>Quality check: {detectionQaResult.quality_check_id}</p>
|
<p>Kwaliteitscontrole: {detectionQaResult.quality_check_id}</p>
|
||||||
<p>Precision: {detectionQaResult.precision?.toFixed(3) ?? 'n/a'}</p>
|
<p>Precisie: {detectionQaResult.precision?.toFixed(3) ?? 'n.v.t.'}</p>
|
||||||
<p>Recall: {detectionQaResult.recall?.toFixed(3) ?? 'n/a'}</p>
|
<p>Recall: {detectionQaResult.recall?.toFixed(3) ?? 'n/a'}</p>
|
||||||
<p>F1: {detectionQaResult.f1_score?.toFixed(3) ?? 'n/a'}</p>
|
<p>F1: {detectionQaResult.f1_score?.toFixed(3) ?? 'n/a'}</p>
|
||||||
<p>Mean IoU: {detectionQaResult.mean_iou?.toFixed(3) ?? 'n/a'}</p>
|
<p>Gemiddelde overlap: {detectionQaResult.mean_iou?.toFixed(3) ?? 'n.v.t.'}</p>
|
||||||
<p>False positives: {detectionQaResult.false_positives}</p>
|
<p>Fout positief: {detectionQaResult.false_positives}</p>
|
||||||
<p>False negatives: {detectionQaResult.false_negatives}</p>
|
<p>Fout negatief: {detectionQaResult.false_negatives}</p>
|
||||||
{detectionQaResult.coverage ? (
|
{detectionQaResult.coverage ? (
|
||||||
<div className="detection-qa-diagnostic">
|
<div className="detection-qa-diagnostic">
|
||||||
<span>Inference coverage</span>
|
<span>Inference coverage</span>
|
||||||
@@ -1004,6 +1017,8 @@ export function DetectionLab({
|
|||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
</section>
|
</section>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -326,27 +326,27 @@ export function ExportCenter({
|
|||||||
<div className="export-state-stack">
|
<div className="export-state-stack">
|
||||||
{loadingExports ? (
|
{loadingExports ? (
|
||||||
<div className="result-state result-state-loading">
|
<div className="result-state result-state-loading">
|
||||||
<strong>Loading export registry.</strong>
|
<strong>Downloads laden.</strong>
|
||||||
<p>Retrieving persisted artifacts for the selected project.</p>
|
<p>De bewaarde bestanden van deze werkruimte worden opgehaald.</p>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
{exportError ? (
|
{exportError ? (
|
||||||
<div className="result-state result-state-error">
|
<div className="result-state result-state-error">
|
||||||
<strong>Export action failed.</strong>
|
<strong>Download kon niet worden gemaakt.</strong>
|
||||||
<p>{exportError}</p>
|
<p>{exportError}</p>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
{latestExport ? (
|
{latestExport ? (
|
||||||
<div className="latest-export-card">
|
<div className="latest-export-card">
|
||||||
<span>Latest export</span>
|
<span>Laatste download</span>
|
||||||
<strong>{formatExportType(latestExport.export_type)}</strong>
|
<strong>{formatExportType(latestExport.export_type)}</strong>
|
||||||
<p>{latestExport.path}</p>
|
<p>{latestExport.path}</p>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
{exports.length === 0 ? (
|
{exports.length === 0 ? (
|
||||||
<div className="result-state result-state-empty">
|
<div className="result-state result-state-empty">
|
||||||
<strong>No exports registered yet.</strong>
|
<strong>Nog geen downloads gemaakt.</strong>
|
||||||
<p>Create metadata, GeoJSON or HTML report artifacts once the active project has data to hand off.</p>
|
<p>Kies hierboven een leesbaar rapport, projectoverzicht of kaartlaag.</p>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1049,7 +1049,7 @@ export function MapWorkspace({
|
|||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
<label className="geo-scope-select">
|
<label className="geo-scope-select">
|
||||||
Gemeente of volledige regio
|
Snel naar een gemeente (optioneel)
|
||||||
<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}>
|
||||||
{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>
|
||||||
|
|||||||
@@ -1529,6 +1529,7 @@ details.ai-lab-model-surface > summary strong {
|
|||||||
|
|
||||||
.technical-run-list,
|
.technical-run-list,
|
||||||
.area-catalog-disclosure,
|
.area-catalog-disclosure,
|
||||||
|
.dataset-history-disclosure,
|
||||||
.dataset-technical-details,
|
.dataset-technical-details,
|
||||||
.quality-advanced-disclosure,
|
.quality-advanced-disclosure,
|
||||||
.provider-technical-details,
|
.provider-technical-details,
|
||||||
@@ -1543,6 +1544,7 @@ details.ai-lab-model-surface > summary strong {
|
|||||||
|
|
||||||
.technical-run-list > summary,
|
.technical-run-list > summary,
|
||||||
.area-catalog-disclosure > summary,
|
.area-catalog-disclosure > summary,
|
||||||
|
.dataset-history-disclosure > summary,
|
||||||
.dataset-technical-details > summary,
|
.dataset-technical-details > summary,
|
||||||
.quality-advanced-disclosure > summary,
|
.quality-advanced-disclosure > summary,
|
||||||
.provider-technical-details > summary,
|
.provider-technical-details > summary,
|
||||||
@@ -1564,6 +1566,7 @@ details.ai-lab-model-surface > summary strong {
|
|||||||
|
|
||||||
.technical-run-list > summary::-webkit-details-marker,
|
.technical-run-list > summary::-webkit-details-marker,
|
||||||
.area-catalog-disclosure > summary::-webkit-details-marker,
|
.area-catalog-disclosure > summary::-webkit-details-marker,
|
||||||
|
.dataset-history-disclosure > summary::-webkit-details-marker,
|
||||||
.dataset-technical-details > summary::-webkit-details-marker,
|
.dataset-technical-details > summary::-webkit-details-marker,
|
||||||
.quality-advanced-disclosure > summary::-webkit-details-marker,
|
.quality-advanced-disclosure > summary::-webkit-details-marker,
|
||||||
.provider-technical-details > summary::-webkit-details-marker,
|
.provider-technical-details > summary::-webkit-details-marker,
|
||||||
@@ -1575,6 +1578,7 @@ details.ai-lab-model-surface > summary strong {
|
|||||||
|
|
||||||
.technical-run-list > summary::after,
|
.technical-run-list > summary::after,
|
||||||
.area-catalog-disclosure > summary::after,
|
.area-catalog-disclosure > summary::after,
|
||||||
|
.dataset-history-disclosure > summary::after,
|
||||||
.dataset-technical-details > summary::after,
|
.dataset-technical-details > summary::after,
|
||||||
.quality-advanced-disclosure > summary::after,
|
.quality-advanced-disclosure > summary::after,
|
||||||
.provider-technical-details > summary::after,
|
.provider-technical-details > summary::after,
|
||||||
@@ -1589,6 +1593,7 @@ details.ai-lab-model-surface > summary strong {
|
|||||||
|
|
||||||
.technical-run-list[open] > summary::after,
|
.technical-run-list[open] > summary::after,
|
||||||
.area-catalog-disclosure[open] > summary::after,
|
.area-catalog-disclosure[open] > summary::after,
|
||||||
|
.dataset-history-disclosure[open] > summary::after,
|
||||||
.dataset-technical-details[open] > summary::after,
|
.dataset-technical-details[open] > summary::after,
|
||||||
.quality-advanced-disclosure[open] > summary::after,
|
.quality-advanced-disclosure[open] > summary::after,
|
||||||
.provider-technical-details[open] > summary::after,
|
.provider-technical-details[open] > summary::after,
|
||||||
@@ -1645,6 +1650,56 @@ details.ai-lab-model-surface > summary strong {
|
|||||||
margin-top: 0.25rem;
|
margin-top: 0.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.dataset-history-disclosure {
|
||||||
|
margin-top: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dataset-history-disclosure > summary strong {
|
||||||
|
margin-right: 0.25rem;
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dataset-history-list {
|
||||||
|
display: grid;
|
||||||
|
max-height: 24rem;
|
||||||
|
margin: 0;
|
||||||
|
overflow: auto;
|
||||||
|
padding: 0.5rem;
|
||||||
|
list-style: none;
|
||||||
|
background: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dataset-history-list li {
|
||||||
|
display: flex;
|
||||||
|
min-width: 0;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.75rem;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
border-top: 1px solid var(--line);
|
||||||
|
padding: 0.65rem 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dataset-history-list li:first-child {
|
||||||
|
border-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dataset-history-list li > div:first-child {
|
||||||
|
display: grid;
|
||||||
|
min-width: 0;
|
||||||
|
gap: 0.15rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dataset-history-list li span {
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 0.76rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detection-technical-evaluation {
|
||||||
|
margin-top: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
.ai-user-summary {
|
.ai-user-summary {
|
||||||
order: 0;
|
order: 0;
|
||||||
display: grid;
|
display: grid;
|
||||||
|
|||||||
Reference in New Issue
Block a user