import { useEffect, useRef, useState } from 'react' import type { DatasetCreateResponse, DetectionModelCapability, DetectionQaResult, DetectionRead, DetectionRunRead, DetectionRunResponse, ModelAssetRead, QualityCheckRead, YoloPreflightResponse, } from '../../types' import type { DetectionCalibrationRunRow, DetectionWorkflowStage } from '../../hooks/useDetectionWorkflow' import { DETECTION_OPERATOR_PROFILES, type DetectionOperatorProfile } from './detectionProfiles' const DETECTION_PAGE_SIZE_OPTIONS = [25, 50, 100] as const 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 { analysisRunId: string qualityCheckId: string threshold: number | null modelName: string modelAssetId: string | null detectionCount: number | null precision: number | null recall: number | null f1: number | null falsePositives: number | null falseNegatives: number | null score: number | null createdAt: string | null } interface DetectionLabProps { detectionModels: DetectionModelCapability[] modelAssets: ModelAssetRead[] loadingDetectionModels: boolean detectionModelError: string | null modelAssetError: string | null selectedDetectionDatasetId: string selectedDetectionModelId: string selectedModelAssetId: string detectionTileManifestPath: string detectionConfidenceThreshold: number runningDetection: boolean detectionRunResult: DetectionRunResponse | null detectionRunError: string | null detectionRuns: DetectionRunRead[] qualityChecks: QualityCheckRead[] calibrationThresholdText: string runningDetectionCalibration: boolean detectionCalibrationRows: DetectionCalibrationRunRow[] detectionCalibrationError: string | null detectionWorkflowStage: DetectionWorkflowStage selectedDetectionRunId: string detectionItems: DetectionRead[] detectionClassFilter: string detectionMinConfidenceFilter: number loadingDetectionResults: boolean detectionReferenceDatasetId: string detectionQaResult: DetectionQaResult | null detectionQaError: string | null runningDetectionQa: boolean yoloPreflight: YoloPreflightResponse | null loadingYoloPreflight: boolean yoloPreflightError: string | null selectedProjectId: string | null rasterDatasets: DatasetCreateResponse[] referenceDatasets: DatasetCreateResponse[] onLoadModels: () => void onRefreshYoloPreflight: () => void onSelectDataset: (datasetId: string) => void onSelectModel: (modelId: string) => void onSelectModelAsset: (modelAssetId: string) => void onSetConfidenceThreshold: (value: number) => void onSetTileManifestPath: (value: string) => void onRunDetection: () => void onUploadRaster: (file: File) => Promise onPrepareAndRunDetection: () => Promise onOpenResultsOnMap: () => void onLoadRuns: () => void onSelectRun: (runId: string) => void onSetClassFilter: (value: string) => void onSetMinConfidenceFilter: (value: number) => void onLoadResults: () => void onSelectReferenceDataset: (datasetId: string) => void onRunQa: () => void onSetCalibrationThresholdText: (value: string) => void onRunCalibration: () => void onOpenCalibrationEvidence: (qualityCheckId: string) => void onApplyOperatorProfile: (profile: DetectionOperatorProfile) => void } export function DetectionLab({ detectionModels, modelAssets, loadingDetectionModels, detectionModelError, modelAssetError, selectedDetectionDatasetId, selectedDetectionModelId, selectedModelAssetId, detectionTileManifestPath, detectionConfidenceThreshold, runningDetection, detectionRunResult, detectionRunError, detectionRuns, qualityChecks, calibrationThresholdText, runningDetectionCalibration, detectionCalibrationRows, detectionCalibrationError, detectionWorkflowStage, selectedDetectionRunId, detectionItems, detectionClassFilter, detectionMinConfidenceFilter, loadingDetectionResults, detectionReferenceDatasetId, detectionQaResult, detectionQaError, runningDetectionQa, yoloPreflight, loadingYoloPreflight, yoloPreflightError, selectedProjectId, rasterDatasets, referenceDatasets, onLoadModels, onRefreshYoloPreflight, onSelectDataset, onSelectModel, onSelectModelAsset, onSetConfidenceThreshold, onSetTileManifestPath, onRunDetection, onUploadRaster, onPrepareAndRunDetection, onOpenResultsOnMap, onLoadRuns, onSelectRun, onSetClassFilter, onSetMinConfidenceFilter, onLoadResults, onSelectReferenceDataset, onRunQa, onSetCalibrationThresholdText, onRunCalibration, onOpenCalibrationEvidence, onApplyOperatorProfile, }: 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 const detectionModelReady = Boolean(selectedDetectionModel?.configured) const detectionModelUiRunnable = detectionModelReady && selectedDetectionModelId !== 'manual-fixture-detector' const detectionHasExplicitModelAsset = selectedDetectionModelId !== 'yolo-configured' || modelAssets.length === 0 || selectedModelAssetId.length > 0 const calibrationRows = buildCalibrationRows(detectionRuns, qualityChecks) const bestF1Candidate = bestCalibrationRow(calibrationRows, 'f1') const bestPrecisionCandidate = bestCalibrationRow(calibrationRows, 'precision') const lowestFalsePositivePressureCandidate = bestLowestCalibrationRow(calibrationRows, 'falsePositives') const [detectionResultPage, setDetectionResultPage] = useState(1) const [detectionPageSize, setDetectionPageSize] = useState(DEFAULT_DETECTION_PAGE_SIZE) const [pendingRasterFile, setPendingRasterFile] = useState(null) const rasterFileInputRef = useRef(null) const detectionPageCount = Math.max(1, Math.ceil(detectionItems.length / detectionPageSize)) const currentDetectionPage = Math.min(detectionResultPage, detectionPageCount) const detectionPageStart = (currentDetectionPage - 1) * detectionPageSize const detectionPageEnd = Math.min(detectionPageStart + detectionPageSize, detectionItems.length) const visibleDetectionItems = detectionItems.slice(detectionPageStart, detectionPageEnd) useEffect(() => { setDetectionResultPage(1) }, [selectedDetectionRunId, detectionClassFilter, detectionMinConfidenceFilter, detectionItems]) const detectionHasTileManifest = !detectionRequiresTileManifest || detectionTileManifestPath.trim().length > 0 const detectionRunReady = Boolean(selectedProjectId) && detectionHasDataset && detectionHasModel && detectionModelUiRunnable && detectionHasExplicitModelAsset && detectionHasTileManifest const guidedDetectionReady = Boolean(selectedProjectId) && detectionHasDataset && detectionHasModel && detectionModelUiRunnable && detectionHasExplicitModelAsset const detectionRunBlockedReason = !selectedProjectId ? 'De regionale werkruimte is nog niet geladen' : !detectionHasDataset ? 'Kies een luchtbeeld' : !detectionHasModel ? 'Kies een analysemodel' : selectedDetectionModelId === 'manual-fixture-detector' ? 'Het fixturemodel is alleen bedoeld voor expliciete tests en demo\'s' : !detectionModelReady ? selectedDetectionModel?.limitation_message ?? 'Het gekozen model is niet geconfigureerd' : !detectionHasExplicitModelAsset ? 'Kies een lokaal modelbestand onder beheer' : !detectionHasTileManifest ? 'Maak eerst beeldtegels voor het gekozen luchtbeeld' : null const calibrationRunReady = detectionRunReady && detectionReferenceDatasetId.length > 0 && calibrationThresholdText.trim().length > 0 const guidedDetectionBlockedReason = !selectedProjectId ? 'De regionale werkruimte is nog niet geladen' : !detectionHasDataset ? 'Kies of voeg een gegeorefereerd luchtbeeld toe' : !detectionHasModel ? 'Kies een analysemodel' : selectedDetectionModelId === 'manual-fixture-detector' ? 'Het fixturemodel is alleen bedoeld voor expliciete tests en demo\'s' : !detectionModelReady ? selectedDetectionModel?.limitation_message ?? 'Het gekozen model is niet geconfigureerd' : !detectionHasExplicitModelAsset ? 'Kies een lokaal modelbestand onder beheer' : null return (

Analyse van luchtbeelden

Gebouwen herkennen

Actieve analyse Gebouwdetectie

{selectedOperatorProfile?.displayName ?? selectedModelAsset?.display_name ?? 'Lokaal YOLO-model'}

PyTorch-runtime {yoloRuntimeReady ? 'Gereed' : loadingDetectionModels ? 'Controleren...' : 'Niet gereed'}

{yoloRuntimeReady ? `${yoloPreflight?.runtime.cuda_available ? 'GPU' : 'CPU'} · lokaal model gevonden` : 'Controleer de modelconfiguratie onder beheer.'}

Gevalideerde kwaliteit {selectedOperatorProfile ? `F1 ${selectedOperatorProfile.f1.toFixed(3)}` : 'Nog niet gekoppeld'}

{selectedOperatorProfile ? `${selectedOperatorProfile.positiveSampleCount} testgebieden · resultaten blijven controleplichtig` : 'Kies het goedgekeurde lokale profiel.'}

0 ? 'ai-user-summary-card ai-user-summary-card-ready' : 'ai-user-summary-card'}> Beschikbare luchtbeelden {rasterDatasets.length}

{rasterDatasets.length > 0 ? 'Klaar om een beeld te kiezen.' : 'Laad eerst een gegeorefereerd luchtbeeld in.'}

Technische modelinformatie {detectionModels.length} registraties
{loadingDetectionModels ? (
Loading detection models.

Checking backend model registry availability.

) : null} {detectionModelError ? (
Detection model registry unavailable.

{detectionModelError}

) : null} {modelAssetError ? (
Local model assets unavailable.

{modelAssetError}

) : null} {detectionModels.length === 0 && !loadingDetectionModels ? (
No detection models reported by backend.

Refresh models after the backend is reachable.

) : null}
    {detectionModels.map((model) => (
  • {detectionModelLabel(model)} {model.status}
    {model.model_id} {model.framework} {model.task_type}

    classes: {model.supported_classes.join(', ')}

    {model.limitation_message}

  • ))}
{selectedDetectionModelId === 'yolo-configured' ? (
Modelkeuze voor beheerders {selectedOperatorProfile?.displayName ?? selectedModelAsset?.display_name ?? 'Geen lokaal model'}

Lokaal modelbestand

GeoIntel kiest automatisch het actieve lokale model. Een beheerder kan hier bewust een ander reeds aanwezig, alleen-lezen modelbestand kiezen.

{selectedModelAsset ? 'model gekozen' : 'geen model gekozen'}
Gevalideerde profielen

Een profiel koppelt een lokaal model aan een gemeten zekerheidsdrempel. Een andere keuze geldt alleen voor de huidige analyse en wijzigt de serverconfiguratie niet.

{DETECTION_OPERATOR_PROFILES.map((profile) => { const profileAsset = modelAssets.find((asset) => asset.model_asset_id === profile.modelAssetId) const profileSelected = selectedModelAssetId === profile.modelAssetId && Math.abs(detectionConfidenceThreshold - profile.confidenceThreshold) < 0.0001 return (
{profile.displayName} {profile.defaultApproved ? 'standaardprofiel' : 'kandidaat · extra controle vereist'}

{profile.description}

drempel {profile.confidenceThreshold.toFixed(2)} precision {profile.precision.toFixed(3)} recall {profile.recall.toFixed(3)} F1 {profile.f1.toFixed(3)} testgebieden {profile.positiveSampleCount} max. achtergrondfouten {profile.maxBackgroundDetections}
modelbestand: {profile.modelAssetId} beoordeling: {profile.promotionRecommendation} beschikbaar: {profileAsset ? 'ja' : 'niet gekoppeld'}

{profile.limitationMessage}

) })}
{selectedModelAsset ? (
Status gekozen model

{selectedModelAsset.display_name} wordt voor deze analyse gebruikt. De standaard serverconfiguratie blijft ongewijzigd.

) : null} {modelAssets.length === 0 && !loadingDetectionModels ? (
Geen lokaal modelbestand gevonden.

Plaats een gecontroleerd model in de modelmap of configureer het bestaande YOLO-modelpad.

) : null} {selectedModelAsset ? (

File: {selectedModelAsset.filename}

Status: {selectedModelAsset.status}

Active runtime env model: {selectedModelAsset.active ? 'yes' : 'no'}

will_download_models: {selectedModelAsset.will_download_models ? 'yes' : 'no'}

Size: {formatModelAssetSize(selectedModelAsset.size_bytes)}

SHA-256: {selectedModelAsset.sha256.slice(0, 12)}

Path: {selectedModelAsset.model_path}

{selectedModelAsset.limitation_message}

) : null}
) : null}
Technische runtimecontrole {yoloRuntimeReady ? 'gereed' : yoloPreflight?.status ?? 'niet geladen'}

YOLO runtime preflight

Read-only runtime status. This does not load a model, run inference or download weights.

{loadingYoloPreflight ? (
Loading YOLO preflight.

Checking backend runtime configuration and optional dependency visibility.

) : null} {yoloPreflightError ? (
YOLO preflight unavailable.

{yoloPreflightError}

) : null} {!yoloPreflight && !loadingYoloPreflight && !yoloPreflightError ? (
No YOLO preflight loaded.

Refresh preflight to inspect the live backend AI runtime before running configured YOLO.

) : null}
{yoloPreflight ? (

Status: {yoloPreflight.status}

{yoloPreflight.message}

{yoloPreflight.checks.dependencies_available ? 'dependencies visible' : 'not ready'}
YOLO enabled {yoloPreflight.checks.enabled ? 'true' : 'false'}
Dependencies {yoloPreflight.checks.dependencies_available === true ? 'available' : yoloPreflight.checks.dependencies_available === false ? 'unavailable' : 'not checked'}
Local model file {yoloPreflight.checks.model_file_exists === true ? 'found' : yoloPreflight.checks.model_path_set ? 'missing' : 'not configured'}
CUDA {yoloPreflight.runtime.cuda_available === true ? 'available' : yoloPreflight.runtime.cuda_available === false ? 'not available' : 'not checked'}
Tile manifest validation {yoloPreflight.checks.manifest_valid === true ? 'valid' : yoloPreflight.checks.manifest_path_set ? 'not valid' : 'not provided'}
0 ? 'lab-readiness-item lab-readiness-item-ready' : 'lab-readiness-item'}> Tile count {yoloPreflight.tile_count} / {yoloPreflight.max_tiles}
torch_version: {yoloPreflight.runtime.torch_version ?? 'n/a'} ultralytics_version: {yoloPreflight.runtime.ultralytics_version ?? 'n/a'} cuda_available: {String(yoloPreflight.runtime.cuda_available ?? 'unknown')} will_run_inference: {String(yoloPreflight.will_run_inference)} YOLO_CONFIG_DIR: {yoloPreflight.runtime.yolo_config_dir ?? 'n/a'} model directory: {yoloPreflight.runtime.model_directory ?? 'n/a'} model_asset_id: {yoloPreflight.model_asset_id ?? 'n/a'}
) : null}

Nieuwe beeldanalyse

Wat is nog nodig?

Kies een luchtbeeld en model. GeoIntel maakt de beeldtegels en laadt het resultaat daarna automatisch op de kaart.

{guidedDetectionReady ? 'Klaar om te starten' : 'Nog niet startklaar'}
Luchtbeeld {detectionHasDataset ? 'Geselecteerd' : 'Nog geen luchtbeeld'}
Analysemodel {detectionModelReady ? 'Lokaal gebouwmodel beschikbaar' : 'Model niet beschikbaar'}
Beeldtegels {detectionRequiresTileManifest ? detectionHasTileManifest ? 'Beschikbaar' : detectionHasDataset ? 'Worden automatisch voorbereid' : 'Wachten op een luchtbeeld' : 'Niet vereist'}
Lokale modelkeuze {selectedDetectionModelId === 'yolo-configured' ? selectedModelAsset ? selectedOperatorProfile?.displayName ?? selectedModelAsset.display_name : modelAssets.length > 0 ? 'Kies een lokaal model onder beheer' : 'Geen lokaal model gevonden' : 'Niet vereist'}
Analyse {guidedDetectionReady ? 'Klaar om gebouwen te zoeken' : guidedDetectionBlockedReason}
{rasterDatasets.length === 0 ? (
Geen luchtbeeld beschikbaar in deze werkruimte.

Voeg hieronder een gegeorefereerde GeoTIFF toe. GeoIntel controleert de projectie en bewaart het bronbestand als dataset.

) : null}
Eigen luchtbeeld toevoegen

Gebruik een GeoTIFF met geldige CRS en georeferentie. Een bestaand luchtbeeld kan meteen in de keuzelijst worden gebruikt.

Technische tegelinstellingen {detectionHasTileManifest ? 'manifest beschikbaar' : 'automatisch'}

De normale actie gebruikt automatisch 512 px-tegels met 64 px overlap. Alleen beheerders hoeven hier een bestaand manifest te koppelen.

{selectedDetectionModelId === 'yolo-configured' ? ( ) : null} {selectedDetectionModelId === 'yolo-configured' && detectionTileManifestPath.trim() ? (
Gekoppelde beeldtegels

{detectionTileManifestPath}

De technische controle wordt vernieuwd wanneer het model of tegelbestand wijzigt.
) : null}
{detectionRunError ? (
De beeldanalyse is mislukt.

{detectionRunError}

) : null} {detectionRunResult ? (

Status: {detectionRunResult.status}

Uitleg: {detectionRunResult.message}

Analyse: {detectionRunResult.analysis_run_id}

Verwerking: {detectionRunResult.job_id}

Gevonden objecten: {detectionRunResult.detection_count}

{detectionRunResult.error_code ?

Code: {detectionRunResult.error_code}

: null}
) : null}
Modelkalibratie voor beheerders {detectionCalibrationRows.length > 0 ? `${detectionCalibrationRows.length} drempels getest` : 'gesloten'}

Guided calibration runner

This runs real configured YOLO jobs and QA comparisons for each threshold. It does not promote or mutate model files.

{calibrationRunReady ? 'ready' : 'needs dataset, model, manifest and reference'}
{detectionCalibrationError ? (
Calibration sweep failed.

{detectionCalibrationError}

) : null} {detectionCalibrationRows.length > 0 ? (

Calibration run progress

Each row is backed by a persisted detection run and QA check when successful.

{detectionCalibrationRows.length} thresholds
{detectionCalibrationRows.map((row) => ( ))}
Threshold Status Detections Precision Recall F1 False positives False negatives Evidence
{row.threshold.toFixed(2)} {row.status} {row.detection_count ?? 'n/a'} {formatNullableNumber(row.precision ?? null, 3)} {formatNullableNumber(row.recall ?? null, 3)} {formatNullableNumber(row.f1_score ?? null, 3)} {row.false_positives ?? 'n/a'} {row.false_negatives ?? 'n/a'}
) : (
No calibration sweep has been run in this session.

Choose a reference dataset and threshold set, then start the explicit sweep.

)}

Gevonden objecten

Bekijk eerder bewaarde analyses en filter op type of zekerheid.

{loadingDetectionResults ? (
Resultaten laden.

De bewaarde objecten van deze analyse worden opgehaald.

) : null}
{detectionItems.length} objecten geladen

{selectedDetectionRunId ? 'Deze resultaten zijn bewaard in de database.' : 'Kies eerst een bewaarde analyse.'}

{detectionItems.length > 0 ? ( <>

{detectionPageStart + 1}-{detectionPageEnd} van {detectionItems.length}

Pagina {currentDetectionPage} van {detectionPageCount}
{visibleDetectionItems.map((detection) => ( ))}
Type Zekerheid Model Beeldtegel
{detection.class_name} {detection.confidence.toFixed(2)} {detection.model_name} {formatSourceTilePath(detection.source_tile_path)}
) : null}
Technische modelevaluatie {calibrationRows.length > 0 || detectionQaResult ? 'resultaten beschikbaar' : 'optioneel'}

Kalibraties vergelijken

Vergelijk bewaarde analyseruns per zekerheidsdrempel voordat een modelinstelling wordt goedgekeurd.

{calibrationRows.length} resultaten
{calibrationRows.length > 0 ? ( <>
{calibrationRows.map((row) => ( ))}
Drempel Model Objecten Precisie Recall F1 Fout positief Fout negatief Kwaliteitscontrole
{formatNullableNumber(row.threshold, 2)} {row.modelName} {row.modelAssetId ?? 'geconfigureerd lokaal model'} {row.detectionCount ?? 'n/a'} {formatNullableNumber(row.precision, 3)} {formatNullableNumber(row.recall, 3)} {formatNullableNumber(row.f1, 3)} {row.falsePositives ?? 'n/a'} {row.falseNegatives ?? 'n/a'} {row.qualityCheckId}
Voorwaarde voor goedkeuring Keur pas goed nadat meerdere gebieden en de foutieve positieve en negatieve resultaten zijn gecontroleerd.
) : (
Nog geen kalibratievergelijking beschikbaar.

Voer het lokale model met meerdere zekerheidsdrempels uit en vergelijk de resultaten met dezelfde referentielaag.

)}

Kwaliteitscontrole gebouwdetectie

Vergelijk de gevonden gebouwen met een bewaarde officiële referentielaag. De uitkomst wordt als kwaliteitscontrole in de database bewaard.

{detectionQaError ? (
De kwaliteitscontrole is mislukt.

{detectionQaError}

) : null} {detectionQaResult ? (

Status: {detectionQaResult.status}

Kwaliteitscontrole: {detectionQaResult.quality_check_id}

Precisie: {detectionQaResult.precision?.toFixed(3) ?? 'n.v.t.'}

Recall: {detectionQaResult.recall?.toFixed(3) ?? 'n/a'}

F1: {detectionQaResult.f1_score?.toFixed(3) ?? 'n/a'}

Gemiddelde overlap: {detectionQaResult.mean_iou?.toFixed(3) ?? 'n.v.t.'}

Fout positief: {detectionQaResult.false_positives}

Fout negatief: {detectionQaResult.false_negatives}

{detectionQaResult.coverage ? (
Gecontroleerd beeldbereik {detectionQaResult.coverage.applied ? `${detectionQaResult.coverage.reference_evaluated_count} van ${detectionQaResult.coverage.reference_raw_count} referentieobjecten gecontroleerd` : 'De volledige referentielaag is gecontroleerd'}

{detectionQaResult.coverage.applied ? `${detectionQaResult.coverage.reference_excluded_outside_count} buiten beeldbereik, ${detectionQaResult.coverage.reference_clipped_boundary_count} aan de rand begrensd, ${detectionQaResult.coverage.tile_count} beeldtegels.` : 'Deze controle gebruikt alle objecten uit de gekozen referentielaag.'}

) : null} {detectionQaResult.box_to_footprint_diagnostics ? (
Aanvullende vormdiagnose {detectionQaResult.box_to_footprint_diagnostics.envelope_matches} rechthoekmatches tegenover{' '} {detectionQaResult.box_to_footprint_diagnostics.strict_matches} strikte vormmatches

{detectionQaResult.box_to_footprint_diagnostics.possible_box_to_footprint_mismatch_count} mogelijke vormafwijkingen. Precisie, recall en F1 hierboven blijven gebaseerd op de strikte geometrische overlap.

) : null}
) : null}
) } function CalibrationSummaryCard({ title, row, metric, }: { title: string row: CalibrationRow | null metric: 'f1' | 'precision' | 'falsePositives' }): JSX.Element { return (
{title} {row ? ( <> {metric === 'falsePositives' ? row.falsePositives ?? 'n/a' : formatNullableNumber(row[metric], 3)}

Threshold {formatNullableNumber(row.threshold, 2)} · {row.modelName}

) : ( <> n/a

Persisted QA metrics are required.

)}
) } function formatModelAssetSize(sizeBytes: number): string { if (sizeBytes >= 1024 * 1024) { return `${(sizeBytes / (1024 * 1024)).toFixed(1)} MB` } if (sizeBytes >= 1024) { return `${(sizeBytes / 1024).toFixed(1)} KB` } return `${sizeBytes} B` } function DetectionWorkflowStep({ label, complete, active, }: { label: string complete: boolean active: boolean }): JSX.Element { const className = active ? 'guided-detection-step guided-detection-step-active' : complete ? 'guided-detection-step guided-detection-step-complete' : 'guided-detection-step' return (
{label}
) } function detectionWorkflowActionLabel(stage: DetectionWorkflowStage): string { if (stage === 'tiling') return 'Beeldtegels voorbereiden...' if (stage === 'validating') return 'Model en beeld controleren...' if (stage === 'detecting') return 'Gebouwen zoeken...' if (stage === 'loading') return 'Resultaat op kaart laden...' if (stage === 'complete') return 'Analyse opnieuw uitvoeren' return 'Gebouwen zoeken en op kaart tonen' } function buildCalibrationRows(detectionRuns: DetectionRunRead[], qualityChecks: QualityCheckRead[]): CalibrationRow[] { const runById = new Map(detectionRuns.map((run) => [run.id, run])) return qualityChecks .flatMap((check) => { const run = check.analysis_run_id ? runById.get(check.analysis_run_id) : null if (!run || run.analysis_type !== 'detection') { return [] } const threshold = confidenceThresholdForRun(run) return [{ analysisRunId: run.id, qualityCheckId: check.id, threshold, modelName: run.model_name ?? 'configured detection', modelAssetId: stringFromRecord(run.parameters_json, 'model_asset_id'), detectionCount: numberFromRecord(run.result_json, 'detection_count'), precision: metricValue(check, 'precision'), recall: metricValue(check, 'recall'), f1: metricValue(check, 'f1'), falsePositives: metricValue(check, 'false_positives'), falseNegatives: metricValue(check, 'false_negatives'), score: check.score ?? null, createdAt: check.completed_at ?? check.created_at ?? run.finished_at ?? run.created_at ?? null, }] }) .sort((left, right) => { const createdDiff = Date.parse(right.createdAt ?? '') - Date.parse(left.createdAt ?? '') if (Number.isFinite(createdDiff) && createdDiff !== 0) { return createdDiff } return (right.threshold ?? -1) - (left.threshold ?? -1) }) } function confidenceThresholdForRun(run: DetectionRunRead): number | null { return ( numberFromRecord(run.parameters_json, 'confidence_threshold') ?? numberFromRecord(run.result_json, 'confidence_threshold') ?? null ) } function metricValue(check: QualityCheckRead, key: string): number | null { const aliases = key === 'f1' ? ['f1', 'f1_score'] : [key] for (const alias of aliases) { const metric = check.metrics.find((item) => item.metric_key === alias) if (typeof metric?.metric_value === 'number' && Number.isFinite(metric.metric_value)) { return metric.metric_value } const findingValue = numberFromRecord(check.findings_json, alias) if (findingValue !== null) { return findingValue } } return key === 'f1' && typeof check.score === 'number' ? check.score : null } function bestCalibrationRow(rows: CalibrationRow[], metric: 'f1' | 'precision'): CalibrationRow | null { return rows.reduce((best, row) => { const value = row[metric] if (value === null) { return best } if (!best || value > (best[metric] ?? Number.NEGATIVE_INFINITY)) { return row } return best }, null) } function bestLowestCalibrationRow(rows: CalibrationRow[], metric: 'falsePositives'): CalibrationRow | null { return rows.reduce((best, row) => { const value = row[metric] if (value === null) { return best } if (!best || value < (best[metric] ?? Number.POSITIVE_INFINITY)) { return row } return best }, null) } function numberFromRecord(record: Record | null | undefined, key: string): number | null { const value = record?.[key] return typeof value === 'number' && Number.isFinite(value) ? value : null } function stringFromRecord(record: Record | null | undefined, key: string): string | null { const value = record?.[key] return typeof value === 'string' && value.trim().length > 0 ? value : null } function buildCalibrationSummaryExport(projectId: string, rows: DetectionCalibrationRunRow[]): Record { const successfulRows = rows.filter((row) => row.status === 'success') return { export_type: 'detection_calibration_summary', project_id: projectId, created_at: new Date().toISOString(), calibration_thresholds: rows.map((row) => row.threshold), quality_check_ids: successfulRows.map((row) => row.quality_check_id).filter(Boolean), rows: rows.map((row) => ({ threshold: row.threshold, status: row.status, analysis_run_id: row.analysis_run_id ?? null, job_id: row.job_id ?? null, quality_check_id: row.quality_check_id ?? null, evidence_geojson_url: row.quality_check_id ? `/api/v1/projects/${projectId}/quality-checks/${row.quality_check_id}/evidence/geojson` : null, detection_count: row.detection_count ?? null, precision: row.precision ?? null, recall: row.recall ?? null, f1_score: row.f1_score ?? null, false_positives: row.false_positives ?? null, false_negatives: row.false_negatives ?? null, message: row.message ?? null, })), } } function downloadCalibrationSummary(projectId: string | null, rows: DetectionCalibrationRunRow[]): void { if (!projectId || rows.length === 0) { return } downloadJsonFile('detection-calibration-summary.json', buildCalibrationSummaryExport(projectId, rows)) } function downloadJsonFile(filename: string, payload: unknown): void { const blob = new Blob([JSON.stringify(payload, null, 2)], { type: 'application/json' }) const url = URL.createObjectURL(blob) const link = document.createElement('a') link.href = url link.download = filename link.click() URL.revokeObjectURL(url) } function formatNullableNumber(value: number | null, digits: number): string { return typeof value === 'number' && Number.isFinite(value) ? value.toFixed(digits) : 'n/a' } function formatSourceTilePath(path: string | null | undefined): string { if (!path) { return 'n/a' } const parts = path.replace(/\\/g, '/').split('/').filter(Boolean) return parts[parts.length - 1] ?? path }