Files
geointel/frontend/src/components/detection/DetectionModelManagement.tsx
T

308 lines
15 KiB
TypeScript

import type {
DetectionModelCapability,
ModelAssetRead,
YoloPreflightResponse,
} from '../../types'
import { DETECTION_OPERATOR_PROFILES, type DetectionOperatorProfile } from './detectionProfiles'
import { analysisModelAvailabilityMessage } from '../models/modelOptions'
interface DetectionModelManagementProps {
detectionModels: DetectionModelCapability[]
modelAssets: ModelAssetRead[]
loadingDetectionModels: boolean
detectionModelError: string | null
modelAssetError: string | null
selectedDetectionModelId: string
selectedModelAssetId: string
detectionConfidenceThreshold: number
yoloPreflight: YoloPreflightResponse | null
loadingYoloPreflight: boolean
yoloPreflightError: string | null
onRefreshYoloPreflight: () => void
onSelectModelAsset: (modelAssetId: string) => void
onApplyOperatorProfile: (profile: DetectionOperatorProfile) => void
}
export 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
}
function formatModelAssetSize(sizeBytes: number): string {
if (!Number.isFinite(sizeBytes) || sizeBytes <= 0) return 'n.v.t.'
const megabytes = sizeBytes / (1024 * 1024)
return `${megabytes.toLocaleString('nl-BE', { maximumFractionDigits: 1 })} MB`
}
function statusLabel(value: string): string {
if (value === 'configured' || value === 'ready') return 'gereed'
if (value === 'not_configured') return 'niet geconfigureerd'
if (value === 'dependency_unavailable') return 'software ontbreekt'
if (value === 'accelerator_unavailable') return 'GPU niet beschikbaar'
if (value === 'contract_incomplete') return 'provenance onvolledig'
return value.replace(/_/g, ' ')
}
export function DetectionModelManagement({
detectionModels,
modelAssets,
loadingDetectionModels,
detectionModelError,
modelAssetError,
selectedDetectionModelId,
selectedModelAssetId,
detectionConfidenceThreshold,
yoloPreflight,
loadingYoloPreflight,
yoloPreflightError,
onRefreshYoloPreflight,
onSelectModelAsset,
onApplyOperatorProfile,
}: DetectionModelManagementProps): JSX.Element {
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,
)
return (
<div className="detection-model-management">
<details className="ai-lab-model-surface" aria-label="Technische modelmogelijkheden">
<summary>
<span>Technische modelinformatie</span>
<strong>{detectionModels.length} registraties</strong>
</summary>
<div className="ai-lab-disclosure-body">
<div className="ai-lab-state-stack">
{loadingDetectionModels ? (
<div className="result-state result-state-loading">
<strong>Analysemodellen worden gecontroleerd.</strong>
<p>GeoIntel leest de modelregistratie en lokale bestanden.</p>
</div>
) : null}
{detectionModelError ? (
<div className="result-state result-state-error">
<strong>De modelregistratie is niet bereikbaar.</strong>
<p>{detectionModelError}</p>
</div>
) : null}
{modelAssetError ? (
<div className="result-state result-state-error">
<strong>De lokale modelbestanden konden niet worden gelezen.</strong>
<p>{modelAssetError}</p>
</div>
) : null}
{detectionModels.length === 0 && !loadingDetectionModels ? (
<div className="result-state result-state-empty">
<strong>De backend meldt geen analysemodellen.</strong>
<p>Vernieuw de status zodra de backend opnieuw bereikbaar is.</p>
</div>
) : null}
</div>
<ul className="model-list">
{detectionModels.map((model) => (
<li className={model.configured ? 'model-card model-card-ready' : 'model-card'} key={model.model_id}>
<strong>{detectionModelLabel(model)}</strong>
<span className={model.configured ? 'status-badge status-badge-ready' : 'status-badge'}>
{statusLabel(model.status)}
</span>
<p className="muted">Ondersteunde klassen: {model.supported_classes.join(', ') || 'niet opgegeven'}</p>
<p className="muted">{analysisModelAvailabilityMessage(model)}</p>
<details className="technical-inline-details">
<summary>Technische identificatie</summary>
<div className="entity-meta">
<span>Model-ID: {model.model_id}</span>
<span>Framework: {model.framework}</span>
<span>Taaktype: {model.task_type}</span>
</div>
</details>
</li>
))}
</ul>
</div>
</details>
{selectedDetectionModelId === 'yolo-configured' ? (
<details className="ai-lab-model-surface" aria-label="Lokale modelkeuze">
<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>Lokaal modelbestand</h3>
<p>GeoIntel kiest automatisch het actieve lokale model. Een andere keuze geldt alleen voor deze analyse.</p>
</div>
<span className={selectedModelAsset ? 'status-badge status-badge-ready' : 'status-badge'}>
{selectedModelAsset ? 'model gekozen' : 'geen model gekozen'}
</span>
</div>
<div className="operator-profile-grid" aria-label="Historische YOLO-controleprofielen">
{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 (
<div
className={profileSelected ? 'operator-profile-card operator-profile-card-selected' : 'operator-profile-card'}
key={profile.id}
>
<div className="operator-profile-card-header">
<strong>{profile.displayName}</strong>
<span className="status-badge">
{profile.independentTestProven ? 'onafhankelijk getoetst' : 'historisch, geen releasebewijs'}
</span>
</div>
<p>{profile.description}</p>
<div className="operator-profile-metrics">
<span>drempel {profile.confidenceThreshold.toFixed(2)}</span>
<span>historische precisie {profile.precision.toFixed(3)}</span>
<span>historische herkenningsgraad {profile.recall.toFixed(3)}</span>
<span>historische F1 {profile.f1.toFixed(3)}</span>
<span>positieve controles {profile.positiveSampleCount}</span>
<span>
ergste achtergrondmonster {profile.maxBackgroundDetections} van{' '}
{profile.backgroundSampleCount}
</span>
</div>
<p className="field-guidance">
Achtergrondtoets: {profile.backgroundGate}. Meting: {profile.evidenceReference}.
</p>
<p className="field-guidance">{profile.limitationMessage}</p>
<button
className="secondary-action"
type="button"
onClick={() => onApplyOperatorProfile(profile)}
disabled={!profileAsset}
>
Profiel gebruiken
</button>
</div>
)
})}
</div>
<label>
Lokaal modelbestand
<select value={selectedModelAssetId} onChange={(event) => onSelectModelAsset(event.target.value)}>
<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 ? '(actief)' : ''}
</option>
))}
</select>
</label>
{modelAssets.length === 0 && !loadingDetectionModels ? (
<div className="result-state result-state-empty">
<strong>Geen lokaal modelbestand gevonden.</strong>
<p>Plaats een gecontroleerd model in de modelmap of configureer het bestaande YOLO-modelpad.</p>
</div>
) : null}
{selectedModelAsset ? (
<details className="technical-inline-details">
<summary>Bestands- en integriteitsgegevens</summary>
<div className="result-summary-card">
<p>Bestand: {selectedModelAsset.filename}</p>
<p>Status: {statusLabel(selectedModelAsset.status)}</p>
<p>Actief servermodel: {selectedModelAsset.active ? 'ja' : 'nee'}</p>
<p>Automatisch downloaden: {selectedModelAsset.will_download_models ? 'ja' : 'nee'}</p>
<p>Grootte: {formatModelAssetSize(selectedModelAsset.size_bytes)}</p>
<p>SHA-256: {selectedModelAsset.sha256.slice(0, 12)}</p>
<p>Pad: {selectedModelAsset.model_path}</p>
</div>
</details>
) : null}
</div>
</details>
) : null}
<details className="ai-lab-model-surface" aria-label="Technische YOLO-runtimecontrole">
<summary>
<span>Technische runtimecontrole</span>
<strong>{yoloRuntimeReady ? 'gereed' : statusLabel(yoloPreflight?.status ?? 'niet geladen')}</strong>
</summary>
<div className="ai-lab-disclosure-body">
<div className="ai-lab-section-header">
<div>
<h3>YOLO-runtimecontrole</h3>
<p>Deze alleen-lezen controle start geen analyse en downloadt geen modelbestanden.</p>
</div>
<button className="secondary-action" type="button" onClick={onRefreshYoloPreflight} disabled={loadingYoloPreflight}>
Controle vernieuwen
</button>
</div>
<div className="ai-lab-state-stack">
{loadingYoloPreflight ? (
<div className="result-state result-state-loading">
<strong>De YOLO-runtime wordt gecontroleerd.</strong>
<p>GeoIntel controleert configuratie, optionele software en lokale bestanden.</p>
</div>
) : null}
{yoloPreflightError ? (
<div className="result-state result-state-error">
<strong>De YOLO-runtimecontrole is niet beschikbaar.</strong>
<p>{yoloPreflightError}</p>
</div>
) : null}
{!yoloPreflight && !loadingYoloPreflight && !yoloPreflightError ? (
<div className="result-state result-state-empty">
<strong>Nog geen runtimecontrole geladen.</strong>
<p>Vernieuw de controle voordat je een lokaal model gebruikt.</p>
</div>
) : null}
</div>
{yoloPreflight ? (
<div className={yoloPreflight.status === 'ready' ? 'lab-readiness-panel lab-readiness-panel-ready' : 'lab-readiness-panel'}>
<div className="lab-readiness-grid">
<div className={yoloPreflight.checks.enabled ? 'lab-readiness-item lab-readiness-item-ready' : 'lab-readiness-item'}>
<span>YOLO ingeschakeld</span>
<strong>{yoloPreflight.checks.enabled ? 'ja' : 'nee'}</strong>
</div>
<div className={yoloPreflight.checks.dependencies_available ? 'lab-readiness-item lab-readiness-item-ready' : 'lab-readiness-item'}>
<span>Benodigde software</span>
<strong>{yoloPreflight.checks.dependencies_available === true ? 'beschikbaar' : yoloPreflight.checks.dependencies_available === false ? 'ontbreekt' : 'niet gecontroleerd'}</strong>
</div>
<div className={yoloPreflight.checks.model_file_exists ? 'lab-readiness-item lab-readiness-item-ready' : 'lab-readiness-item'}>
<span>Lokaal modelbestand</span>
<strong>{yoloPreflight.checks.model_file_exists === true ? 'gevonden' : yoloPreflight.checks.model_path_set ? 'ontbreekt' : 'niet geconfigureerd'}</strong>
</div>
<div className={yoloPreflight.runtime.cuda_available ? 'lab-readiness-item lab-readiness-item-ready' : 'lab-readiness-item'}>
<span>GPU-versnelling</span>
<strong>{yoloPreflight.runtime.cuda_available === true ? 'beschikbaar' : yoloPreflight.runtime.cuda_available === false ? 'niet beschikbaar' : 'niet gecontroleerd'}</strong>
</div>
<div className={yoloPreflight.checks.manifest_valid ? 'lab-readiness-item lab-readiness-item-ready' : 'lab-readiness-item'}>
<span>Beeldtegels</span>
<strong>{yoloPreflight.checks.manifest_valid === true ? 'geldig' : yoloPreflight.checks.manifest_path_set ? 'ongeldig' : 'niet opgegeven'}</strong>
</div>
<div className={yoloPreflight.tile_count > 0 ? 'lab-readiness-item lab-readiness-item-ready' : 'lab-readiness-item'}>
<span>Aantal beeldtegels</span>
<strong>{yoloPreflight.tile_count} / {yoloPreflight.max_tiles}</strong>
</div>
</div>
<details className="technical-inline-details">
<summary>Versies en serverpaden</summary>
<div className="entity-meta">
<span>PyTorch: {yoloPreflight.runtime.torch_version ?? 'n.v.t.'}</span>
<span>Ultralytics: {yoloPreflight.runtime.ultralytics_version ?? 'n.v.t.'}</span>
<span>YOLO-configuratiemap: {yoloPreflight.runtime.yolo_config_dir ?? 'n.v.t.'}</span>
<span>Modelmap: {yoloPreflight.runtime.model_directory ?? 'n.v.t.'}</span>
<span>Model-ID: {yoloPreflight.model_asset_id ?? 'n.v.t.'}</span>
</div>
</details>
</div>
) : null}
</div>
</details>
</div>
)
}