import { useEffect, useState } from 'react'
import type {
DatasetCreateResponse,
DetectionModelCapability,
DetectionQaResult,
DetectionRead,
DetectionRunRead,
DetectionRunResponse,
ModelAssetRead,
QualityCheckRead,
YoloPreflightResponse,
} from '../../types'
import type { DetectionCalibrationRunRow } 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
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
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
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,
selectedDetectionRunId,
detectionItems,
detectionClassFilter,
detectionMinConfidenceFilter,
loadingDetectionResults,
detectionReferenceDatasetId,
detectionQaResult,
detectionQaError,
runningDetectionQa,
yoloPreflight,
loadingYoloPreflight,
yoloPreflightError,
selectedProjectId,
rasterDatasets,
referenceDatasets,
onLoadModels,
onRefreshYoloPreflight,
onSelectDataset,
onSelectModel,
onSelectModelAsset,
onSetConfidenceThreshold,
onSetTileManifestPath,
onRunDetection,
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 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 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 detectionRunBlockedReason = !selectedProjectId
? 'Select or create a project first'
: !detectionHasDataset
? 'Select a raster dataset'
: !detectionHasModel
? 'Select a detection model'
: selectedDetectionModelId === 'manual-fixture-detector'
? 'Fixture model is explicit test/demo-only'
: !detectionModelReady
? selectedDetectionModel?.limitation_message ?? 'Selected model is not configured'
: !detectionHasExplicitModelAsset
? 'Select a local model asset deliberately'
: !detectionHasTileManifest
? 'Provide a raster tile manifest for configured YOLO'
: null
const calibrationRunReady = detectionRunReady && detectionReferenceDatasetId.length > 0 && calibrationThresholdText.trim().length > 0
return (
Object detection Backend-reported detector states and limitations. Checking backend model registry availability. {detectionModelError} {modelAssetError} Refresh models after the backend is reachable. classes: {model.supported_classes.join(', ')} {model.limitation_message} Local model assets are read-only. No model file is selected automatically. Choose an existing local model asset before submitting configured YOLO.
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.
{profile.description} {profile.limitationMessage}
{selectedModelAsset.display_name} is selected for this browser-run request. Runtime default activation
remains a separate guarded operator action backed by a promotion report.
Mount model files into the backend model directory or continue with the configured YOLO_MODEL_PATH. 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} Read-only runtime status. This does not load a model, run inference or download weights. Checking backend runtime configuration and optional dependency visibility. {yoloPreflightError} Refresh preflight to inspect the live backend AI runtime before running configured YOLO. {yoloPreflight.message} Checks the selected dataset, model and tile manifest before submitting a detection job. Upload or select a raster dataset in Data before running object detection. {detectionTileManifestPath} {detectionRunError} Status: {detectionRunResult.status} Message: {detectionRunResult.message} Analysis run: {detectionRunResult.analysis_run_id} Job: {detectionRunResult.job_id} Detections: {detectionRunResult.detection_count} Code: {detectionRunResult.error_code} This runs real configured YOLO jobs and QA comparisons for each threshold. It does not promote or mutate model files. {detectionCalibrationError} Each row is backed by a persisted detection run and QA check when successful. Choose a reference dataset and threshold set, then start the explicit sweep. Load persisted detections and filter by class or confidence. Retrieving persisted detections for the selected run. {selectedDetectionRunId ? 'Loaded from persisted detection records.' : 'Select a detection run before loading results.'}
{detectionPageStart + 1}-{detectionPageEnd}
of {detectionItems.length}
Compare persisted detection runs by confidence threshold before promoting a model setting. Run configured YOLO at multiple confidence thresholds, then compare each persisted detection run against the same reference dataset. {detectionQaError} Status: {detectionQaResult.status} Quality check: {detectionQaResult.quality_check_id} Precision: {detectionQaResult.precision?.toFixed(3) ?? 'n/a'} Recall: {detectionQaResult.recall?.toFixed(3) ?? 'n/a'} F1: {detectionQaResult.f1_score?.toFixed(3) ?? 'n/a'} Mean IoU: {detectionQaResult.mean_iou?.toFixed(3) ?? 'n/a'} False positives: {detectionQaResult.false_positives} False negatives: {detectionQaResult.false_negatives}Detection Lab
Model registry
{detectionModels.map((model) => (
Explicit model asset
YOLO runtime preflight
Status: {yoloPreflight.status}
Run detection
Run readiness
Guided calibration runner
Calibration run progress
{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'}
Detection results
{visibleDetectionItems.map((detection) => (
Class
Confidence
Model
Source tile
))}
{detection.class_name}
{detection.confidence.toFixed(2)}
{detection.model_name}
{detection.source_tile_path || 'n/a'}
Calibration comparison
{calibrationRows.map((row) => (
Threshold
Model
Detections
Precision
Recall
F1
False positives
False negatives
Quality check
))}
{formatNullableNumber(row.threshold, 2)}
{row.modelName}
{row.modelAssetId ?? 'runtime configured path'}
{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}
Detection QA
{detectionQaError ? (
Threshold {formatNullableNumber(row.threshold, 2)} ยท {row.modelName}
> ) : ( <> n/aPersisted QA metrics are required.
> )}