feat: simplify regional workbench language
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 23:15:50 +02:00
parent 0381fdd636
commit b50b250f72
18 changed files with 292 additions and 97 deletions
+3 -1
View File
@@ -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")
+1 -1
View File
@@ -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
+26
View File
@@ -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
View File
@@ -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>
+1 -1
View File
@@ -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>
+55
View File
@@ -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;