308 lines
15 KiB
TypeScript
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>
|
|
)
|
|
}
|