import { useEffect, useRef, useState } from 'react' import type { DatasetCreateResponse, DetectionModelCapability, DetectionQaResult, DetectionRead, DetectionRunRead, DetectionRunResponse, JobRead, ModelAssetRead, QualityCheckRead, YoloPreflightResponse, } from '../../types' import type { DetectionCalibrationRunRow, DetectionWorkflowStage } from '../../hooks/useDetectionWorkflow' import { DETECTION_OPERATOR_PROFILES, type DetectionOperatorProfile } from './detectionProfiles' import { DetectionModelManagement, detectionModelLabel } from './DetectionModelManagement' import { AiPipelineIllustration } from './AiPipelineIllustration' import { ModelSelector } from '../models/ModelSelector' import { analysisModelAvailabilityMessage, toAnalysisModelOption } from '../models/modelOptions' const DETECTION_PAGE_SIZE_OPTIONS = [25, 50, 100] as const const DEFAULT_DETECTION_PAGE_SIZE = 50 function detectionQualityInterpretation(f1: number | null | undefined): string { if (typeof f1 !== 'number' || !Number.isFinite(f1)) return 'Nog geen onafhankelijke kwaliteitsmeting beschikbaar.' return `Historische F1 ${f1.toFixed(3)} is alleen kalibratiecontext. Een actuele, ruimtelijk onafhankelijke QA-run bepaalt of dit resultaat lokaal bruikbaar is.` } function detectionStatusLabel(status: string): string { if (status === 'completed') return 'afgerond' if (status === 'success') return 'geslaagd' if (status === 'failed') return 'mislukt' if (status === 'running') return 'bezig' if (status === 'queued') return 'in wachtrij' return status.replace(/_/g, ' ') } function persistedDetectionModelLabel(modelName: string | null | undefined): string { if (!modelName) return 'Gebouwdetectie' if (modelName === 'yolo-configured') return 'Lokaal gebouwmodel' if (modelName === 'manual-fixture-detector') return 'Testmodel' if (modelName === 'yolo-placeholder') return 'Niet-geconfigureerd gebouwmodel' return modelName } function detectionClassLabel(className: string): string { if (className.toLowerCase() === 'building') return 'Gebouw' return className } function formatDetectionRunLabel(run: DetectionRunRead): string { const timestamp = run.finished_at ?? run.created_at const dateLabel = timestamp ? new Intl.DateTimeFormat('nl-BE', { dateStyle: 'short', timeStyle: 'short', }).format(new Date(timestamp)) : 'datum onbekend' return `${persistedDetectionModelLabel(run.model_name)} · ${detectionStatusLabel(run.status)} · ${dateLabel}` } 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 { managementLocked?: boolean detectionModels: DetectionModelCapability[] modelAssets: ModelAssetRead[] loadingDetectionModels: boolean detectionModelError: string | null modelAssetError: string | null selectedDetectionDatasetId: string selectedDetectionModelId: string selectedModelAssetId: string detectionTileManifestPath: string detectionConfidenceThreshold: number runningDetection: boolean detectionJob: JobRead | null 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[] /** Complete population behind the returned page. */ detectionTotal?: number detectionTruncated?: boolean 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({ managementLocked = false, detectionModels, modelAssets, loadingDetectionModels, detectionModelError, modelAssetError, selectedDetectionDatasetId, selectedDetectionModelId, selectedModelAssetId, detectionTileManifestPath, detectionConfidenceThreshold, runningDetection, detectionJob, detectionRunResult, detectionRunError, detectionRuns, qualityChecks, calibrationThresholdText, runningDetectionCalibration, detectionCalibrationRows, detectionCalibrationError, detectionWorkflowStage, selectedDetectionRunId, detectionItems, detectionTotal, detectionTruncated = false, 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?.accelerator_ready === true && yoloPreflight.checks?.model_file_exists, ) const detectionRequiresTileManifest = selectedDetectionModelId === 'yolo-configured' const detectionJobActive = detectionJob?.status === 'queued' || detectionJob?.status === 'running' const detectionHasDataset = selectedDetectionDatasetId.length > 0 const detectionHasModel = selectedDetectionModel !== null const detectionModelReady = Boolean(selectedDetectionModel?.configured) const selectedDetectionModelAvailability = selectedDetectionModel ? analysisModelAvailabilityMessage(selectedDetectionModel) : 'Het gekozen model is niet geconfigureerd' 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 ? selectedDetectionModelAvailability : !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 ? selectedDetectionModelAvailability : !detectionHasExplicitModelAsset ? 'Kies een lokaal modelbestand onder beheer' : null return (

Analyse van luchtbeelden

Gebouwen herkennen

0 || detectionRuns.some((run) => run.status === 'completed')} hasQualityEvidence={Boolean(detectionQaResult) || qualityChecks.length > 0} running={runningDetection || runningDetectionQa || runningDetectionCalibration} />
Actieve analyse Gebouwdetectie

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

Rekenomgeving {yoloRuntimeReady ? 'Gereed' : loadingDetectionModels ? 'Controleren...' : 'Niet gereed'}

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

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

{selectedOperatorProfile ? selectedOperatorProfile.validationScope : 'Kies een modelprofiel met gedocumenteerd evaluatiebewijs.'}

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.'}

{detectionQualityInterpretation(selectedOperatorProfile?.f1)}

{selectedOperatorProfile && selectedDetectionModel?.nationally_validated !== true ? (
Nog niet nationaal gevalideerd

Dit model is operationeel voor gecontroleerde beeldanalyse, maar de gemeten kwaliteit geldt alleen voor {selectedDetectionModel?.validation_scope ?? selectedOperatorProfile.validationScope}. Resultaten elders in Belgie of op zee vereisen lokale referentiedata en QA voordat ze als betrouwbaar kunnen worden vrijgegeven.

) : null} {!managementLocked ? : 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} {!managementLocked ?
Eigen luchtbeeld toevoegen

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

: null}
{!managementLocked ?
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}
: null}
{detectionJob && (detectionJob.status === 'queued' || detectionJob.status === 'running') ? (
{detectionJob.status === 'queued' ? 'GPU-taak staat in de wachtrij.' : 'GPU-analyse wordt uitgevoerd.'}

{detectionJob.status === 'queued' ? 'De server heeft de aanvraag veilig bewaard en start ze zodra de NVIDIA-worker beschikbaar is.' : 'Het model verwerkt de beeldtegels op de server. Dit scherm volgt de bewaarde taak automatisch.'}

Taak-ID: {detectionJob.id}
) : null} {detectionRunError ? (
{detectionJobActive ? 'Het volgen van de servertaak is onderbroken.' : 'De beeldanalyse is mislukt.'}

{detectionRunError}

) : null} {detectionRunResult ? (

Status: {detectionStatusLabel(detectionRunResult.status)}

{detectionRunResult.message}

Gevonden objecten: {detectionRunResult.detection_count}

{detectionRunResult.error_code ?

Code: {detectionRunResult.error_code}

: null}
Technische verwerking
Analyserun-ID: {detectionRunResult.analysis_run_id} Taak-ID: {detectionRunResult.job_id}
) : null}
{!managementLocked ?
Modelkalibratie voor beheerders {detectionCalibrationRows.length > 0 ? `${detectionCalibrationRows.length} drempels getest` : 'gesloten'}

Zekerheidsdrempels vergelijken

Voert het lokale model en een kwaliteitscontrole uit voor iedere drempel. Modelbestanden worden niet gewijzigd.

{calibrationRunReady ? 'startklaar' : 'luchtbeeld, model, beeldtegels en referentie vereist'}
{detectionCalibrationError ? (
De kalibratievergelijking is mislukt.

{detectionCalibrationError}

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

Voortgang modelkalibratie

Iedere geslaagde rij is gekoppeld aan een bewaarde beeldanalyse en kwaliteitscontrole.

{detectionCalibrationRows.length} drempels
{detectionCalibrationRows.map((row) => ( ))}
Drempel Status Objecten Precisie Herkenningsgraad F1 Onterecht gevonden Gemist Kaartbewijs
{row.threshold.toFixed(2)} {row.best_f1 ? beste F1 : null} {row.status} {row.detection_count ?? 'n.v.t.'} {formatNullableNumber(row.precision ?? null, 3)} {formatNullableNumber(row.recall ?? null, 3)} {formatNullableNumber(row.f1_score ?? null, 3)} {row.false_positives ?? 'n.v.t.'} {row.false_negatives ?? 'n.v.t.'}
) : (
In deze sessie zijn nog geen drempels vergeleken.

Kies een referentielaag en drempelreeks en start daarna de vergelijking.

)}
: null}
{detectionTruncated ? (

Deze run leverde {(detectionTotal ?? detectionItems.length).toLocaleString('nl-BE')} detecties op; hieronder en op de kaart staan de {detectionItems.length.toLocaleString('nl-BE')} met de hoogste zekerheid. De tellingen in de kwaliteitscontrole gebruiken de volledige run.

) : null}

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 Herkomst
{detectionClassLabel(detection.class_name)} {detection.confidence.toFixed(2)} {persistedDetectionModelLabel(detection.model_name)} Luchtbeeldtegel
) : 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 Herkenningsgraad F1 Fout positief Fout negatief Kwaliteitscontrole
{formatNullableNumber(row.threshold, 2)} {row.modelName} {row.modelAssetId ?? 'geconfigureerd lokaal model'} {row.detectionCount ?? 'n.v.t.'} {formatNullableNumber(row.precision, 3)} {formatNullableNumber(row.recall, 3)} {formatNullableNumber(row.f1, 3)} {row.falsePositives ?? 'n.v.t.'} {row.falseNegatives ?? 'n.v.t.'}
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: {detectionStatusLabel(detectionQaResult.status)}

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

Herkenningsgraad: {detectionQaResult.recall?.toFixed(3) ?? 'n.v.t.'}

F1: {detectionQaResult.f1_score?.toFixed(3) ?? 'n.v.t.'}

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

Minimale IoU voor een match: {detectionQaResult.iou_threshold.toFixed(2)}

Fout positief: {detectionQaResult.false_positives}

Fout negatief: {detectionQaResult.false_negatives}

Technische referentie Kwaliteitscontrole-ID: {detectionQaResult.quality_check_id}
{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, herkenningsgraad en F1 hierboven blijven gebaseerd op de strikte geometrische overlap.

{detectionQaResult.box_to_footprint_diagnostics.interpretation ? (

{detectionQaResult.box_to_footprint_diagnostics.interpretation}

) : null}
) : null} {detectionQaResult.precision_recall_curve ? (
Drempelonafhankelijke kwaliteit AP {formatNullableNumber(detectionQaResult.precision_recall_curve.average_precision, 3)} · beste F1{' '} {formatNullableNumber(detectionQaResult.precision_recall_curve.best_f1, 3)}

{detectionQaResult.precision_recall_curve.best_f1_threshold === null ? 'Geen kandidaten om een werkpunt uit af te leiden.' : `De beste F1 ligt bij drempel ${formatNullableNumber( detectionQaResult.precision_recall_curve.best_f1_threshold, 2, )}. Precisie en herkenningsgraad hierboven gelden alleen voor de gekozen drempel; AP vat de volledige curve samen en maakt vergelijking tussen modellen mogelijk.`}

) : 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.v.t.' : formatNullableNumber(row[metric], 3)}

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

) : ( <> n.v.t.

Persisted QA metrics are required.

)}
) } 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, jobStatus?: string): string { if (stage === 'tiling') return 'Beeldtegels voorbereiden...' if (stage === 'validating') return 'Model en beeld controleren...' if (stage === 'detecting' && jobStatus === 'queued') return 'Wachten op NVIDIA GPU...' if (stage === 'detecting') return 'Gebouwen zoeken op NVIDIA GPU...' 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 ?? 'geconfigureerde detectie', 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.v.t.' }