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

This commit is contained in:
Codex
2026-07-14 21:40:32 +02:00
parent 1693e4ca1f
commit b5bcd9b041
52 changed files with 1183 additions and 617 deletions
@@ -145,6 +145,14 @@ export function DetectionLab({
}: DetectionLabProps): JSX.Element {
const selectedDetectionModel = detectionModels.find((model) => model.model_id === selectedDetectionModelId) ?? null
const selectedModelAsset = modelAssets.find((asset) => asset.model_asset_id === selectedModelAssetId) ?? null
const selectedOperatorProfile = DETECTION_OPERATOR_PROFILES.find(
(profile) => profile.modelAssetId === selectedModelAssetId,
) ?? null
const yoloRuntimeReady = Boolean(
yoloPreflight?.checks.enabled &&
yoloPreflight.checks.dependencies_available &&
yoloPreflight.checks.model_file_exists,
)
const detectionRequiresTileManifest = selectedDetectionModelId === 'yolo-configured'
const detectionHasDataset = selectedDetectionDatasetId.length > 0
const detectionHasModel = selectedDetectionModel !== null
@@ -178,19 +186,19 @@ export function DetectionLab({
detectionHasExplicitModelAsset &&
detectionHasTileManifest
const detectionRunBlockedReason = !selectedProjectId
? 'Select or create a project first'
? 'De regionale werkruimte is nog niet geladen'
: !detectionHasDataset
? 'Select a raster dataset'
? 'Kies een luchtbeeld'
: !detectionHasModel
? 'Select a detection model'
? 'Kies een analysemodel'
: selectedDetectionModelId === 'manual-fixture-detector'
? 'Fixture model is explicit test/demo-only'
? 'Het fixturemodel is alleen bedoeld voor expliciete tests en demo\'s'
: !detectionModelReady
? selectedDetectionModel?.limitation_message ?? 'Selected model is not configured'
? selectedDetectionModel?.limitation_message ?? 'Het gekozen model is niet geconfigureerd'
: !detectionHasExplicitModelAsset
? 'Select a local model asset deliberately'
? 'Kies een lokaal modelbestand onder beheer'
: !detectionHasTileManifest
? 'Provide a raster tile manifest for configured YOLO'
? 'Maak eerst beeldtegels voor het gekozen luchtbeeld'
: null
const calibrationRunReady = detectionRunReady && detectionReferenceDatasetId.length > 0 && calibrationThresholdText.trim().length > 0
@@ -198,18 +206,41 @@ export function DetectionLab({
<section className="workspace-panel ai-lab-shell detection-lab-shell">
<div className="panel-title-row">
<div>
<p className="eyebrow">Object detection</p>
<h2>Detection Lab</h2>
<p className="eyebrow">Analyse van luchtbeelden</p>
<h2>Gebouwen herkennen</h2>
</div>
<button className="secondary-action" type="button" onClick={onLoadModels} disabled={loadingDetectionModels}>
Refresh models
Status vernieuwen
</button>
</div>
<div className="ai-user-summary" aria-label="Status gebouwdetectie">
<div className="ai-user-summary-card ai-user-summary-card-primary">
<span>Actieve analyse</span>
<strong>Gebouwdetectie</strong>
<p>{selectedOperatorProfile?.displayName ?? selectedModelAsset?.display_name ?? 'Lokaal YOLO-model'}</p>
</div>
<div className={yoloRuntimeReady ? 'ai-user-summary-card ai-user-summary-card-ready' : 'ai-user-summary-card'}>
<span>PyTorch-runtime</span>
<strong>{yoloRuntimeReady ? 'Gereed' : loadingDetectionModels ? 'Controleren...' : 'Niet gereed'}</strong>
<p>{yoloRuntimeReady ? `${yoloPreflight?.runtime.cuda_available ? 'GPU' : 'CPU'} · lokaal model gevonden` : 'Controleer de modelconfiguratie onder beheer.'}</p>
</div>
<div className="ai-user-summary-card">
<span>Gevalideerde kwaliteit</span>
<strong>{selectedOperatorProfile ? `F1 ${selectedOperatorProfile.f1.toFixed(3)}` : 'Nog niet gekoppeld'}</strong>
<p>{selectedOperatorProfile ? `${selectedOperatorProfile.positiveSampleCount} testgebieden · resultaten blijven controleplichtig` : 'Kies het goedgekeurde lokale profiel.'}</p>
</div>
<div className={rasterDatasets.length > 0 ? 'ai-user-summary-card ai-user-summary-card-ready' : 'ai-user-summary-card'}>
<span>Beschikbare luchtbeelden</span>
<strong>{rasterDatasets.length}</strong>
<p>{rasterDatasets.length > 0 ? 'Klaar om een beeld te kiezen.' : 'Laad eerst een gegeorefereerd luchtbeeld in.'}</p>
</div>
</div>
<details className="ai-lab-model-surface" aria-label="Detection model capabilities">
<summary>
<span>Model registry</span>
<strong>{detectionModels.length} models</strong>
<span>Technische modelinformatie</span>
<strong>{detectionModels.length} registraties</strong>
</summary>
<div className="ai-lab-disclosure-body">
<div className="ai-lab-state-stack">
@@ -257,21 +288,25 @@ export function DetectionLab({
</details>
{selectedDetectionModelId === 'yolo-configured' ? (
<div className="ai-lab-model-surface" aria-label="Local model asset selection">
<details className="ai-lab-model-surface" aria-label="Local model asset selection">
<summary>
<span>Modelkeuze voor beheerders</span>
<strong>{selectedOperatorProfile?.displayName ?? selectedModelAsset?.display_name ?? 'Geen lokaal model'}</strong>
</summary>
<div className="ai-lab-disclosure-body">
<div className="ai-lab-section-header">
<div>
<h3>Explicit model asset</h3>
<p>Local model assets are read-only. No model file is selected automatically. Choose an existing local model asset before submitting configured YOLO.</p>
<h3>Lokaal modelbestand</h3>
<p>GeoIntel kiest automatisch het actieve lokale model. Een beheerder kan hier bewust een ander reeds aanwezig, alleen-lezen modelbestand kiezen.</p>
</div>
<span className={selectedModelAsset ? 'status-badge status-badge-ready' : 'status-badge'}>
{selectedModelAsset ? 'asset selected' : 'no explicit asset'}
{selectedModelAsset ? 'model gekozen' : 'geen model gekozen'}
</span>
</div>
<div className="model-asset-guidance">
<strong>Operator profiles</strong>
<strong>Gevalideerde profielen</strong>
<p>
Candidate profiles apply a local model asset and confidence threshold only after an explicit click.
Promoted profiles still require explicit operator action and do not mutate the runtime environment.
Een profiel koppelt een lokaal model aan een gemeten zekerheidsdrempel. Een andere keuze geldt alleen voor de huidige analyse en wijzigt de serverconfiguratie niet.
</p>
</div>
<div className="operator-profile-grid" aria-label="Configured YOLO operator profiles">
@@ -288,22 +323,22 @@ export function DetectionLab({
<div className="operator-profile-card-header">
<strong>{profile.displayName}</strong>
<span className={profile.defaultApproved ? 'status-badge status-badge-ready' : 'status-badge'}>
{profile.defaultApproved ? 'default-approved' : 'Candidate only - not default-approved'}
{profile.defaultApproved ? 'standaardprofiel' : 'kandidaat · extra controle vereist'}
</span>
</div>
<p>{profile.description}</p>
<div className="operator-profile-metrics">
<span>threshold {profile.confidenceThreshold.toFixed(2)}</span>
<span>drempel {profile.confidenceThreshold.toFixed(2)}</span>
<span>precision {profile.precision.toFixed(3)}</span>
<span>recall {profile.recall.toFixed(3)}</span>
<span>F1 {profile.f1.toFixed(3)}</span>
<span>positive AOIs {profile.positiveSampleCount}</span>
<span>max background FP {profile.maxBackgroundDetections}</span>
<span>testgebieden {profile.positiveSampleCount}</span>
<span>max. achtergrondfouten {profile.maxBackgroundDetections}</span>
</div>
<div className="entity-meta">
<span>asset: {profile.modelAssetId}</span>
<span>promotionRecommendation: {profile.promotionRecommendation}</span>
<span>available: {profileAsset ? 'yes' : 'not mounted'}</span>
<span>modelbestand: {profile.modelAssetId}</span>
<span>beoordeling: {profile.promotionRecommendation}</span>
<span>beschikbaar: {profileAsset ? 'ja' : 'niet gekoppeld'}</span>
</div>
<p className="field-guidance">{profile.limitationMessage}</p>
<button
@@ -312,7 +347,7 @@ export function DetectionLab({
onClick={() => onApplyOperatorProfile(profile)}
disabled={!profileAsset}
>
Apply profile
Profiel gebruiken
</button>
</div>
)
@@ -320,17 +355,16 @@ export function DetectionLab({
</div>
{selectedModelAsset ? (
<div className="model-asset-guidance">
<strong>Selected model asset status</strong>
<strong>Status gekozen model</strong>
<p>
{selectedModelAsset.display_name} is selected for this browser-run request. Runtime default activation
remains a separate guarded operator action backed by a promotion report.
{selectedModelAsset.display_name} wordt voor deze analyse gebruikt. De standaard serverconfiguratie blijft ongewijzigd.
</p>
</div>
) : null}
<label>
Local model file
Lokaal modelbestand
<select value={selectedModelAssetId} onChange={(event) => onSelectModelAsset(event.target.value)}>
<option value="">Select explicit local model asset</option>
<option value="">Kies een lokaal modelbestand</option>
{modelAssets.map((asset) => (
<option key={asset.model_asset_id} value={asset.model_asset_id}>
{asset.display_name} {asset.active ? '(active)' : ''}
@@ -340,8 +374,8 @@ export function DetectionLab({
</label>
{modelAssets.length === 0 && !loadingDetectionModels ? (
<div className="result-state result-state-empty">
<strong>No local model assets found.</strong>
<p>Mount model files into the backend model directory or continue with the configured YOLO_MODEL_PATH.</p>
<strong>Geen lokaal modelbestand gevonden.</strong>
<p>Plaats een gecontroleerd model in de modelmap of configureer het bestaande YOLO-modelpad.</p>
</div>
) : null}
{selectedModelAsset ? (
@@ -356,13 +390,14 @@ export function DetectionLab({
<p>{selectedModelAsset.limitation_message}</p>
</div>
) : null}
</div>
</div>
</details>
) : null}
<details className="ai-lab-model-surface" aria-label="YOLO runtime preflight">
<summary>
<span>YOLO runtime preflight</span>
<strong>{yoloPreflight?.status ?? 'not loaded'}</strong>
<span>Technische runtimecontrole</span>
<strong>{yoloRuntimeReady ? 'gereed' : yoloPreflight?.status ?? 'niet geladen'}</strong>
</summary>
<div className="ai-lab-disclosure-body">
<div className="ai-lab-section-header">
@@ -447,72 +482,72 @@ export function DetectionLab({
<div className="lab-block">
<div className="ai-lab-run-surface" aria-label="Detection run controls">
<h3>Run detection</h3>
<h3>Nieuwe beeldanalyse</h3>
<div
className={detectionRunReady ? 'lab-readiness-panel lab-readiness-panel-ready' : 'lab-readiness-panel'}
aria-label="Detection run readiness"
>
<div className="ai-lab-section-header">
<div>
<h3>Run readiness</h3>
<p>Checks the selected dataset, model and tile manifest before submitting a detection job.</p>
<h3>Wat is nog nodig?</h3>
<p>De analyse start zodra een luchtbeeld en de bijbehorende beeldtegels beschikbaar zijn.</p>
</div>
<span className={detectionRunReady ? 'status-badge status-badge-ready' : 'status-badge'}>
{detectionRunReady ? 'Ready to submit' : 'Blocked'}
{detectionRunReady ? 'Klaar om te starten' : 'Nog niet startklaar'}
</span>
</div>
<div className="lab-readiness-grid">
<div className={detectionHasDataset ? 'lab-readiness-item lab-readiness-item-ready' : 'lab-readiness-item'}>
<span>Raster dataset</span>
<strong>{detectionHasDataset ? 'Selected' : 'Select a raster dataset'}</strong>
<span>Luchtbeeld</span>
<strong>{detectionHasDataset ? 'Geselecteerd' : 'Nog geen luchtbeeld'}</strong>
</div>
<div className={detectionModelReady ? 'lab-readiness-item lab-readiness-item-ready' : 'lab-readiness-item'}>
<span>Model availability</span>
<span>Analysemodel</span>
<strong>
{detectionModelReady
? `${selectedDetectionModel?.display_name ?? selectedDetectionModelId} is configured`
: selectedDetectionModel?.limitation_message ?? 'Select a configured model'}
? 'Lokaal gebouwmodel beschikbaar'
: 'Model niet beschikbaar'}
</strong>
</div>
<div className={detectionHasTileManifest ? 'lab-readiness-item lab-readiness-item-ready' : 'lab-readiness-item'}>
<span>Tile manifest</span>
<span>Beeldtegels</span>
<strong>
{detectionRequiresTileManifest
? detectionHasTileManifest
? 'Provided for configured YOLO'
: 'Required for configured YOLO'
: 'Not required for this model'}
? 'Beschikbaar'
: 'Maak eerst tegels vanuit het luchtbeeld'
: 'Niet vereist'}
</strong>
</div>
<div className={detectionHasExplicitModelAsset ? 'lab-readiness-item lab-readiness-item-ready' : 'lab-readiness-item'}>
<span>Local model asset</span>
<span>Lokale modelkeuze</span>
<strong>
{selectedDetectionModelId === 'yolo-configured'
? selectedModelAsset
? selectedModelAsset.display_name
? selectedOperatorProfile?.displayName ?? selectedModelAsset.display_name
: modelAssets.length > 0
? 'Select a local model asset deliberately'
: 'No local assets reported; backend configured path only'
: 'Not required for this model'}
? 'Kies een lokaal model onder beheer'
: 'Geen lokaal model gevonden'
: 'Niet vereist'}
</strong>
</div>
</div>
</div>
<div className={detectionRunReady ? 'lab-action-guardrail lab-action-guardrail-ready' : 'lab-action-guardrail'}>
<span>Run action</span>
<strong>{detectionRunReady ? 'Ready to submit a detection job' : detectionRunBlockedReason}</strong>
<span>Analyse</span>
<strong>{detectionRunReady ? 'Klaar om gebouwen te zoeken' : detectionRunBlockedReason}</strong>
</div>
{rasterDatasets.length === 0 ? (
<div className="result-state result-state-empty">
<strong>No raster datasets available for detection.</strong>
<p>Upload or select a raster dataset in Data before running object detection.</p>
<strong>Geen luchtbeeld beschikbaar in deze werkruimte.</strong>
<p>Voeg onder Bronnen een gegeorefereerde GeoTIFF toe. Daarna kan GeoIntel er beeldtegels en een detectierun van maken.</p>
</div>
) : null}
<div className="lab-form-grid">
<label>
Raster dataset
Luchtbeeld
<select value={selectedDetectionDatasetId} onChange={(event) => onSelectDataset(event.target.value)}>
<option value="">Select raster dataset</option>
<option value="">Kies een luchtbeeld</option>
{rasterDatasets.map((dataset) => (
<option key={dataset.id} value={dataset.id}>
{dataset.name}
@@ -521,7 +556,7 @@ export function DetectionLab({
</select>
</label>
<label>
Model
Analysemodel
<select value={selectedDetectionModelId} onChange={(event) => onSelectModel(event.target.value)}>
{detectionModels.map((model) => (
<option key={model.model_id} value={model.model_id}>
@@ -531,7 +566,7 @@ export function DetectionLab({
</select>
</label>
<label>
Min confidence
Minimale zekerheid
<input
type="number"
min="0"
@@ -542,14 +577,14 @@ export function DetectionLab({
/>
{selectedDetectionModelId === 'yolo-configured' ? (
<span className="field-guidance">
Use a validated operator profile, or enter a threshold manually for calibration.
Het actieve gevalideerde profiel gebruikt standaard 0,15 voor een evenwichtige gebouwcontrole.
</span>
) : null}
</label>
</div>
{selectedDetectionModelId === 'yolo-configured' ? (
<label>
Tile manifest
Beeldtegelbestand
<input
type="text"
placeholder="Raster tile manifest path"
@@ -560,13 +595,13 @@ export function DetectionLab({
) : null}
{selectedDetectionModelId === 'yolo-configured' && detectionTileManifestPath.trim() ? (
<div className="linked-manifest-card">
<strong>Linked tile manifest</strong>
<strong>Gekoppelde beeldtegels</strong>
<p>{detectionTileManifestPath}</p>
<span>Refresh preflight after changing model asset or manifest path.</span>
<span>De technische controle wordt vernieuwd wanneer het model of tegelbestand wijzigt.</span>
</div>
) : null}
<button className="primary-action" type="button" onClick={onRunDetection} disabled={runningDetection || !detectionRunReady}>
Run detection
Zoek gebouwen
</button>
</div>
</div>
@@ -590,7 +625,12 @@ export function DetectionLab({
) : null}
</div>
<div className="ai-lab-run-surface guided-calibration-surface" aria-label="Guided calibration runner">
<details className="ai-lab-model-surface guided-calibration-surface" aria-label="Guided calibration runner">
<summary>
<span>Modelkalibratie voor beheerders</span>
<strong>{detectionCalibrationRows.length > 0 ? `${detectionCalibrationRows.length} drempels getest` : 'gesloten'}</strong>
</summary>
<div className="ai-lab-disclosure-body">
<div className="ai-lab-section-header">
<div>
<h3>Guided calibration runner</h3>
@@ -705,23 +745,24 @@ export function DetectionLab({
<p>Choose a reference dataset and threshold set, then start the explicit sweep.</p>
</div>
)}
</div>
</div>
</details>
<div className="ai-lab-results-surface" aria-label="Detection results">
<div className="panel-title-row">
<div>
<h3>Detection results</h3>
<p className="muted">Load persisted detections and filter by class or confidence.</p>
<h3>Gevonden objecten</h3>
<p className="muted">Bekijk eerder bewaarde analyses en filter op type of zekerheid.</p>
</div>
<button className="secondary-action" type="button" onClick={onLoadRuns} disabled={!selectedProjectId}>
Refresh runs
Analyses vernieuwen
</button>
</div>
<div className="lab-form-grid">
<label>
Run
Analyse
<select value={selectedDetectionRunId} onChange={(event) => onSelectRun(event.target.value)}>
<option value="">Select detection run</option>
<option value="">Kies een bewaarde analyse</option>
{detectionRuns.map((run) => (
<option key={run.id} value={run.id}>
{run.model_name || 'detection'} - {run.status} - {run.id}
@@ -730,16 +771,16 @@ export function DetectionLab({
</select>
</label>
<label>
Class
Type object
<input
type="text"
placeholder="Class filter"
placeholder="bijvoorbeeld building"
value={detectionClassFilter}
onChange={(event) => onSetClassFilter(event.target.value)}
/>
</label>
<label>
Min confidence
Minimale zekerheid
<input
type="number"
min="0"
@@ -751,18 +792,18 @@ export function DetectionLab({
</label>
</div>
<button className="primary-action" type="button" onClick={onLoadResults} disabled={!selectedDetectionRunId || loadingDetectionResults}>
Load detections
Resultaten laden
</button>
{loadingDetectionResults ? (
<div className="result-state result-state-loading">
<strong>Loading detection results.</strong>
<p>Retrieving persisted detections for the selected run.</p>
<strong>Resultaten laden.</strong>
<p>De bewaarde objecten van deze analyse worden opgehaald.</p>
</div>
) : null}
<div className="ai-lab-state-stack">
<div className="result-state result-state-ready">
<strong>Detections loaded: {detectionItems.length}</strong>
<p>{selectedDetectionRunId ? 'Loaded from persisted detection records.' : 'Select a detection run before loading results.'}</p>
<strong>{detectionItems.length} objecten geladen</strong>
<p>{selectedDetectionRunId ? 'Deze resultaten zijn bewaard in de database.' : 'Kies eerst een bewaarde analyse.'}</p>
</div>
</div>
{detectionItems.length > 0 ? (