344 lines
13 KiB
TypeScript
344 lines
13 KiB
TypeScript
import type {
|
|
DatasetCreateResponse,
|
|
DetectionModelCapability,
|
|
DetectionQaResult,
|
|
DetectionRead,
|
|
DetectionRunRead,
|
|
DetectionRunResponse,
|
|
} from '../../types'
|
|
|
|
interface DetectionLabProps {
|
|
detectionModels: DetectionModelCapability[]
|
|
loadingDetectionModels: boolean
|
|
detectionModelError: string | null
|
|
selectedDetectionDatasetId: string
|
|
selectedDetectionModelId: string
|
|
detectionTileManifestPath: string
|
|
detectionConfidenceThreshold: number
|
|
runningDetection: boolean
|
|
detectionRunResult: DetectionRunResponse | null
|
|
detectionRunError: string | null
|
|
detectionRuns: DetectionRunRead[]
|
|
selectedDetectionRunId: string
|
|
detectionItems: DetectionRead[]
|
|
detectionClassFilter: string
|
|
detectionMinConfidenceFilter: number
|
|
loadingDetectionResults: boolean
|
|
detectionReferenceDatasetId: string
|
|
detectionQaResult: DetectionQaResult | null
|
|
detectionQaError: string | null
|
|
runningDetectionQa: boolean
|
|
selectedProjectId: string | null
|
|
rasterDatasets: DatasetCreateResponse[]
|
|
referenceDatasets: DatasetCreateResponse[]
|
|
onLoadModels: () => void
|
|
onSelectDataset: (datasetId: string) => void
|
|
onSelectModel: (modelId: 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
|
|
}
|
|
|
|
export function DetectionLab({
|
|
detectionModels,
|
|
loadingDetectionModels,
|
|
detectionModelError,
|
|
selectedDetectionDatasetId,
|
|
selectedDetectionModelId,
|
|
detectionTileManifestPath,
|
|
detectionConfidenceThreshold,
|
|
runningDetection,
|
|
detectionRunResult,
|
|
detectionRunError,
|
|
detectionRuns,
|
|
selectedDetectionRunId,
|
|
detectionItems,
|
|
detectionClassFilter,
|
|
detectionMinConfidenceFilter,
|
|
loadingDetectionResults,
|
|
detectionReferenceDatasetId,
|
|
detectionQaResult,
|
|
detectionQaError,
|
|
runningDetectionQa,
|
|
selectedProjectId,
|
|
rasterDatasets,
|
|
referenceDatasets,
|
|
onLoadModels,
|
|
onSelectDataset,
|
|
onSelectModel,
|
|
onSetConfidenceThreshold,
|
|
onSetTileManifestPath,
|
|
onRunDetection,
|
|
onLoadRuns,
|
|
onSelectRun,
|
|
onSetClassFilter,
|
|
onSetMinConfidenceFilter,
|
|
onLoadResults,
|
|
onSelectReferenceDataset,
|
|
onRunQa,
|
|
}: DetectionLabProps): JSX.Element {
|
|
return (
|
|
<section className="workspace-panel ai-lab-shell detection-lab-shell">
|
|
<div className="panel-title-row">
|
|
<div>
|
|
<p className="eyebrow">Object detection</p>
|
|
<h2>Detection Lab</h2>
|
|
</div>
|
|
<button className="secondary-action" type="button" onClick={onLoadModels} disabled={loadingDetectionModels}>
|
|
Refresh models
|
|
</button>
|
|
</div>
|
|
|
|
<div className="ai-lab-model-surface" aria-label="Detection model capabilities">
|
|
<div className="ai-lab-section-header">
|
|
<div>
|
|
<h3>Model registry</h3>
|
|
<p>Backend-reported detector states and limitations.</p>
|
|
</div>
|
|
</div>
|
|
<div className="ai-lab-state-stack">
|
|
{loadingDetectionModels ? (
|
|
<div className="result-state result-state-loading">
|
|
<strong>Loading detection models.</strong>
|
|
<p>Checking backend model registry availability.</p>
|
|
</div>
|
|
) : null}
|
|
{detectionModelError ? (
|
|
<div className="result-state result-state-error">
|
|
<strong>Detection model registry unavailable.</strong>
|
|
<p>{detectionModelError}</p>
|
|
</div>
|
|
) : null}
|
|
{detectionModels.length === 0 && !loadingDetectionModels ? (
|
|
<div className="result-state result-state-empty">
|
|
<strong>No detection models reported by backend.</strong>
|
|
<p>Refresh models after the backend is reachable.</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>{model.display_name}</strong>
|
|
<span className={model.configured ? 'status-badge status-badge-ready' : 'status-badge'}>{model.status}</span>
|
|
<div className="entity-meta">
|
|
<span>{model.model_id}</span>
|
|
<span>{model.framework}</span>
|
|
<span>{model.task_type}</span>
|
|
</div>
|
|
<p className="muted">classes: {model.supported_classes.join(', ')}</p>
|
|
<p className="muted">{model.limitation_message}</p>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
</div>
|
|
|
|
<div className="lab-block">
|
|
<div className="ai-lab-run-surface" aria-label="Detection run controls">
|
|
<h3>Run detection</h3>
|
|
{rasterDatasets.length === 0 ? (
|
|
<div className="result-state result-state-empty">
|
|
<strong>No raster datasets available for detection.</strong>
|
|
<p>Upload or select a raster dataset in Data before running object detection.</p>
|
|
</div>
|
|
) : null}
|
|
<div className="lab-form-grid">
|
|
<label>
|
|
Raster dataset
|
|
<select value={selectedDetectionDatasetId} onChange={(event) => onSelectDataset(event.target.value)}>
|
|
<option value="">Select raster dataset</option>
|
|
{rasterDatasets.map((dataset) => (
|
|
<option key={dataset.id} value={dataset.id}>
|
|
{dataset.name}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</label>
|
|
<label>
|
|
Model
|
|
<select value={selectedDetectionModelId} onChange={(event) => onSelectModel(event.target.value)}>
|
|
{detectionModels.map((model) => (
|
|
<option key={model.model_id} value={model.model_id}>
|
|
{model.display_name}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</label>
|
|
<label>
|
|
Min confidence
|
|
<input
|
|
type="number"
|
|
min="0"
|
|
max="1"
|
|
step="0.05"
|
|
value={detectionConfidenceThreshold}
|
|
onChange={(event) => onSetConfidenceThreshold(Number(event.target.value))}
|
|
/>
|
|
</label>
|
|
</div>
|
|
{selectedDetectionModelId === 'yolo-configured' ? (
|
|
<label>
|
|
Tile manifest
|
|
<input
|
|
type="text"
|
|
placeholder="Raster tile manifest path"
|
|
value={detectionTileManifestPath}
|
|
onChange={(event) => onSetTileManifestPath(event.target.value)}
|
|
/>
|
|
</label>
|
|
) : null}
|
|
<button className="primary-action" type="button" onClick={onRunDetection} disabled={runningDetection || !selectedProjectId || rasterDatasets.length === 0}>
|
|
Run detection
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="ai-lab-state-stack">
|
|
{detectionRunError ? (
|
|
<div className="result-state result-state-error">
|
|
<strong>Detection run failed.</strong>
|
|
<p>{detectionRunError}</p>
|
|
</div>
|
|
) : null}
|
|
{detectionRunResult ? (
|
|
<div className="result-summary-card">
|
|
<p>Status: {detectionRunResult.status}</p>
|
|
<p>Message: {detectionRunResult.message}</p>
|
|
<p>Analysis run: {detectionRunResult.analysis_run_id}</p>
|
|
<p>Job: {detectionRunResult.job_id}</p>
|
|
<p>Detections: {detectionRunResult.detection_count}</p>
|
|
{detectionRunResult.error_code ? <p className="error">Code: {detectionRunResult.error_code}</p> : null}
|
|
</div>
|
|
) : null}
|
|
</div>
|
|
|
|
<div className="ai-lab-results-surface" aria-label="Detection results">
|
|
<div className="panel-title-row">
|
|
<div>
|
|
<h3>Detection results</h3>
|
|
<p className="muted">Load persisted detections and filter by class or confidence.</p>
|
|
</div>
|
|
<button className="secondary-action" type="button" onClick={onLoadRuns} disabled={!selectedProjectId}>
|
|
Refresh runs
|
|
</button>
|
|
</div>
|
|
<div className="lab-form-grid">
|
|
<label>
|
|
Run
|
|
<select value={selectedDetectionRunId} onChange={(event) => onSelectRun(event.target.value)}>
|
|
<option value="">Select detection run</option>
|
|
{detectionRuns.map((run) => (
|
|
<option key={run.id} value={run.id}>
|
|
{run.model_name || 'detection'} - {run.status} - {run.id}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</label>
|
|
<label>
|
|
Class
|
|
<input
|
|
type="text"
|
|
placeholder="Class filter"
|
|
value={detectionClassFilter}
|
|
onChange={(event) => onSetClassFilter(event.target.value)}
|
|
/>
|
|
</label>
|
|
<label>
|
|
Min confidence
|
|
<input
|
|
type="number"
|
|
min="0"
|
|
max="1"
|
|
step="0.05"
|
|
value={detectionMinConfidenceFilter}
|
|
onChange={(event) => onSetMinConfidenceFilter(Number(event.target.value))}
|
|
/>
|
|
</label>
|
|
</div>
|
|
<button className="primary-action" type="button" onClick={onLoadResults} disabled={!selectedDetectionRunId || loadingDetectionResults}>
|
|
Load detections
|
|
</button>
|
|
{loadingDetectionResults ? (
|
|
<div className="result-state result-state-loading">
|
|
<strong>Loading detection results.</strong>
|
|
<p>Retrieving persisted detections for the selected run.</p>
|
|
</div>
|
|
) : null}
|
|
<div className="ai-lab-state-stack">
|
|
<div className="result-state result-state-ready">
|
|
<strong>Detections loaded: {detectionItems.length}</strong>
|
|
<p>{selectedDetectionRunId ? 'Loaded from persisted detection records.' : 'Select a detection run before loading results.'}</p>
|
|
</div>
|
|
</div>
|
|
{detectionItems.length > 0 ? (
|
|
<div className="table-scroll">
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th>Class</th>
|
|
<th>Confidence</th>
|
|
<th>Model</th>
|
|
<th>Source tile</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{detectionItems.map((detection) => (
|
|
<tr key={detection.id}>
|
|
<td>{detection.class_name}</td>
|
|
<td>{detection.confidence.toFixed(2)}</td>
|
|
<td>{detection.model_name}</td>
|
|
<td>{detection.source_tile_path || 'n/a'}</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
) : null}
|
|
</div>
|
|
|
|
<div className="ai-lab-qa-surface" aria-label="Detection QA controls and results">
|
|
<h3>Detection QA</h3>
|
|
<label>
|
|
Reference dataset
|
|
<select value={detectionReferenceDatasetId} onChange={(event) => onSelectReferenceDataset(event.target.value)}>
|
|
<option value="">Select reference dataset</option>
|
|
{referenceDatasets.map((dataset) => (
|
|
<option key={dataset.id} value={dataset.id}>
|
|
{dataset.name}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</label>
|
|
<button className="primary-action" type="button" onClick={onRunQa} disabled={runningDetectionQa || !selectedDetectionRunId || !detectionReferenceDatasetId}>
|
|
Compare detections to reference
|
|
</button>
|
|
{detectionQaError ? (
|
|
<div className="result-state result-state-error">
|
|
<strong>Detection QA failed.</strong>
|
|
<p>{detectionQaError}</p>
|
|
</div>
|
|
) : null}
|
|
{detectionQaResult ? (
|
|
<div className="result-summary-card">
|
|
<p>Status: {detectionQaResult.status}</p>
|
|
<p>Quality check: {detectionQaResult.quality_check_id}</p>
|
|
<p>Precision: {detectionQaResult.precision?.toFixed(3) ?? 'n/a'}</p>
|
|
<p>Recall: {detectionQaResult.recall?.toFixed(3) ?? 'n/a'}</p>
|
|
<p>F1: {detectionQaResult.f1_score?.toFixed(3) ?? 'n/a'}</p>
|
|
<p>Mean IoU: {detectionQaResult.mean_iou?.toFixed(3) ?? 'n/a'}</p>
|
|
<p>False positives: {detectionQaResult.false_positives}</p>
|
|
<p>False negatives: {detectionQaResult.false_negatives}</p>
|
|
</div>
|
|
) : null}
|
|
</div>
|
|
</section>
|
|
)
|
|
}
|