117 lines
4.8 KiB
TypeScript
117 lines
4.8 KiB
TypeScript
import { useId, useRef, useState, type KeyboardEvent } from 'react'
|
|
import { BadgeCheck, Boxes, Cpu, Image, ScanSearch } from 'lucide-react'
|
|
|
|
interface AiPipelineIllustrationProps {
|
|
hasImagery: boolean
|
|
hasTiles: boolean
|
|
gpuReady: boolean
|
|
hasDetections: boolean
|
|
hasQualityEvidence: boolean
|
|
running: boolean
|
|
}
|
|
|
|
const pipelineStages = [
|
|
{ key: 'imagery', label: 'Orthofoto', title: 'Gegeorefereerd bronbeeld', detail: 'CRS, resolutie en ruimtelijke dekking blijven bij de dataset bewaard.', icon: Image },
|
|
{ key: 'tiles', label: 'Beeldtegels', title: 'Beeld opgedeeld in controleerbare tegels', detail: 'Elke tegel houdt zijn overlap en herkomst bij, zodat elke detectie terug te voeren is op de bronpixel.', icon: Boxes },
|
|
{ key: 'gpu', label: 'Berekening', title: 'De herkenning draait lokaal', detail: 'De analyse gebruikt de GPU van de server. Is die niet beschikbaar, dan stopt de analyse in plaats van een resultaat te maken waarop u niet kunt bouwen.', icon: Cpu },
|
|
{ key: 'detections', label: 'Detecties', title: 'Gegeorefereerde gebouwobjecten', detail: 'Confidence, modelversie, brontegel en geometrie worden als reproduceerbaar resultaat bewaard.', icon: ScanSearch },
|
|
{ key: 'quality', label: 'QA-bewijs', title: 'Controle vóór vrijgave', detail: 'Precision, recall, IoU en foutbewijs bepalen of een resultaat alleen verkennend of operationeel bruikbaar is.', icon: BadgeCheck },
|
|
] as const
|
|
|
|
export function AiPipelineIllustration({
|
|
hasImagery,
|
|
hasTiles,
|
|
gpuReady,
|
|
hasDetections,
|
|
hasQualityEvidence,
|
|
running,
|
|
}: AiPipelineIllustrationProps): JSX.Element {
|
|
const readiness = [hasImagery, hasTiles, gpuReady, hasDetections, hasQualityEvidence]
|
|
const firstIncomplete = readiness.findIndex((ready) => !ready)
|
|
const [selectedIndex, setSelectedIndex] = useState(firstIncomplete === -1 ? 4 : firstIncomplete)
|
|
const tabRefs = useRef<Array<HTMLButtonElement | null>>([])
|
|
const componentId = useId()
|
|
const titleId = `${componentId}-title`
|
|
const panelId = `${componentId}-panel`
|
|
const selected = pipelineStages[selectedIndex]
|
|
|
|
const selectAndFocus = (index: number) => {
|
|
setSelectedIndex(index)
|
|
tabRefs.current[index]?.focus()
|
|
}
|
|
|
|
const handleTabKeyDown = (event: KeyboardEvent<HTMLButtonElement>, index: number) => {
|
|
let nextIndex: number | null = null
|
|
|
|
switch (event.key) {
|
|
case 'ArrowRight':
|
|
nextIndex = (index + 1) % pipelineStages.length
|
|
break
|
|
case 'ArrowLeft':
|
|
nextIndex = (index - 1 + pipelineStages.length) % pipelineStages.length
|
|
break
|
|
case 'Home':
|
|
nextIndex = 0
|
|
break
|
|
case 'End':
|
|
nextIndex = pipelineStages.length - 1
|
|
break
|
|
default:
|
|
return
|
|
}
|
|
|
|
event.preventDefault()
|
|
selectAndFocus(nextIndex)
|
|
}
|
|
|
|
return (
|
|
<section className={running ? 'ai-pipeline ai-pipeline-running' : 'ai-pipeline'} aria-labelledby={titleId}>
|
|
<div className="ai-pipeline-heading">
|
|
<div>
|
|
<p className="eyebrow">Van pixel naar bewijs</p>
|
|
<h3 id={titleId}>Van luchtbeeld naar controleerbare detectie</h3>
|
|
<p>Open een schakel om te zien welke technische context GeoIntel door de volledige analyse bewaart.</p>
|
|
</div>
|
|
<span className={gpuReady ? 'ai-pipeline-gpu ai-pipeline-gpu-ready' : 'ai-pipeline-gpu'}>
|
|
<i /> {gpuReady ? 'GPU gereed' : 'GPU controleren'}
|
|
</span>
|
|
</div>
|
|
|
|
<div className="ai-pipeline-track" role="tablist" aria-label="Stappen in de analyseketen">
|
|
<span className="ai-pipeline-flow" aria-hidden="true" />
|
|
{pipelineStages.map(({ key, label, icon: Icon }, index) => (
|
|
<button
|
|
key={key}
|
|
id={`${componentId}-${key}`}
|
|
ref={(element) => { tabRefs.current[index] = element }}
|
|
type="button"
|
|
role="tab"
|
|
aria-selected={selectedIndex === index}
|
|
aria-controls={panelId}
|
|
tabIndex={selectedIndex === index ? 0 : -1}
|
|
className={readiness[index] ? 'ai-pipeline-stage ai-pipeline-stage-ready' : 'ai-pipeline-stage'}
|
|
onClick={() => setSelectedIndex(index)}
|
|
onKeyDown={(event) => handleTabKeyDown(event, index)}
|
|
>
|
|
<span><Icon aria-hidden="true" /></span>
|
|
<strong>{label}</strong>
|
|
<small>{readiness[index] ? 'gereed' : index === firstIncomplete ? 'volgende stap' : 'wachten'}</small>
|
|
</button>
|
|
))}
|
|
</div>
|
|
|
|
<div
|
|
id={panelId}
|
|
className="ai-pipeline-detail"
|
|
role="tabpanel"
|
|
aria-labelledby={`${componentId}-${selected.key}`}
|
|
tabIndex={0}
|
|
key={selected.key}
|
|
>
|
|
<span>{String(selectedIndex + 1).padStart(2, '0')}</span>
|
|
<div><strong>{selected.title}</strong><p>{selected.detail}</p></div>
|
|
</div>
|
|
</section>
|
|
)
|
|
}
|