added analysis progress

This commit is contained in:
Linrador 2026-05-02 20:08:00 +02:00
parent ba4d5ba255
commit fbac1c6327

View File

@ -197,7 +197,7 @@ function TrainingNoticeCard(props: {
return ( return (
<div <div
className={[ className={[
'rounded-2xl border px-4 py-3 shadow-sm', 'rounded-xl border px-3 py-2 shadow-sm',
cls.wrap, cls.wrap,
].join(' ')} ].join(' ')}
role={props.notice.kind === 'error' ? 'alert' : 'status'} role={props.notice.kind === 'error' ? 'alert' : 'status'}
@ -219,7 +219,7 @@ function TrainingNoticeCard(props: {
{props.notice.title} {props.notice.title}
</div> </div>
<div className="mt-0.5 break-words text-sm leading-relaxed"> <div className="mt-0.5 break-words text-xs leading-snug">
{props.notice.message} {props.notice.message}
</div> </div>
@ -896,7 +896,12 @@ function TrainingOverlay(props: { step: string; progress: number }) {
) )
} }
function LoadingImageOverlay(props: { text?: string }) { function LoadingImageOverlay(props: {
text?: string
progress?: number
}) {
const progress = clampPercent(props.progress ?? 0)
return ( return (
<div className="absolute inset-0 z-20 flex items-center justify-center text-center text-white"> <div className="absolute inset-0 z-20 flex items-center justify-center text-center text-white">
<div className="absolute -inset-2 bg-black/45 backdrop-blur-[8px] shadow-[inset_0_0_72px_30px_rgba(0,0,0,0.75)]" /> <div className="absolute -inset-2 bg-black/45 backdrop-blur-[8px] shadow-[inset_0_0_72px_30px_rgba(0,0,0,0.75)]" />
@ -905,16 +910,27 @@ function LoadingImageOverlay(props: { text?: string }) {
<LoadingSpinner <LoadingSpinner
size="lg" size="lg"
className="text-white" className="text-white"
srLabel="Bild wird geladen…" srLabel="Analyse läuft…"
/> />
<div className="mt-3 text-sm font-semibold"> <div className="mt-3 text-sm font-semibold">
Bild wird geladen Analyse läuft
</div> </div>
<div className="mt-1 max-w-[260px] px-4 text-xs text-white/80"> <div className="mt-1 max-w-[260px] px-4 text-xs text-white/80">
{props.text || 'Frame wird erstellt und analysiert. Bitte warten.'} {props.text || 'Frame wird erstellt und analysiert. Bitte warten.'}
</div> </div>
<div className="mt-3 h-2 w-48 overflow-hidden rounded-full bg-white/20">
<div
className="h-full rounded-full bg-indigo-400 transition-all duration-500"
style={{ width: `${progress}%` }}
/>
</div>
<div className="mt-1 text-[11px] text-white/70">
{Math.round(progress)}%
</div>
</div> </div>
</div> </div>
) )
@ -1868,6 +1884,8 @@ export default function TrainingTab(props: {
const [sample, setSample] = useState<TrainingSample | null>(null) const [sample, setSample] = useState<TrainingSample | null>(null)
const [correction, setCorrection] = useState<CorrectionState>(() => predictionToCorrection(null)) const [correction, setCorrection] = useState<CorrectionState>(() => predictionToCorrection(null))
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
const [analysisProgress, setAnalysisProgress] = useState(0)
const [analysisStep, setAnalysisStep] = useState('')
const [saving, setSaving] = useState(false) const [saving, setSaving] = useState(false)
const [training, setTraining] = useState(false) const [training, setTraining] = useState(false)
const [trainingStatus, setTrainingStatus] = useState<TrainingStatus | null>(null) const [trainingStatus, setTrainingStatus] = useState<TrainingStatus | null>(null)
@ -2012,6 +2030,25 @@ export default function TrainingTab(props: {
preserveNotice?: boolean preserveNotice?: boolean
}) => { }) => {
setLoading(true) setLoading(true)
setAnalysisProgress(8)
setAnalysisStep(
opts?.refreshPrediction
? 'Aktuelles Bild wird neu analysiert…'
: opts?.forceNew
? 'Neues Trainingsbild wird gesucht…'
: 'Trainingsbild wird geladen…'
)
let progressTimer: number | undefined
progressTimer = window.setInterval(() => {
setAnalysisProgress((value) => {
if (value < 35) return value + 4
if (value < 65) return value + 2
if (value < 88) return value + 1
return value
})
}, 350)
if (!opts?.preserveNotice) { if (!opts?.preserveNotice) {
setError(null) setError(null)
@ -2026,13 +2063,19 @@ export default function TrainingTab(props: {
const url = `/api/training/next${params.toString() ? `?${params.toString()}` : ''}` const url = `/api/training/next${params.toString() ? `?${params.toString()}` : ''}`
setAnalysisProgress(25)
setAnalysisStep('Frame wird vorbereitet…')
const res = await fetch(url, { cache: 'no-store' }) const res = await fetch(url, { cache: 'no-store' })
const data = await res.json().catch(() => null) const data = await res.json().catch(() => null)
if (!res.ok) { if (!res.ok) {
throw new Error(data?.error || `HTTP ${res.status}`) throw new Error(data?.error || `HTTP ${res.status}`)
} }
setAnalysisProgress(92)
setAnalysisStep('Analyse-Ergebnis wird übernommen…')
const nextCorrection = predictionToCorrection(data) const nextCorrection = predictionToCorrection(data)
setDrawingBox(null) setDrawingBox(null)
@ -2063,7 +2106,18 @@ export default function TrainingTab(props: {
} catch (e) { } catch (e) {
setError(e instanceof Error ? e.message : String(e)) setError(e instanceof Error ? e.message : String(e))
} finally { } finally {
setLoading(false) if (progressTimer !== undefined) {
window.clearInterval(progressTimer)
}
setAnalysisProgress(100)
setAnalysisStep('Analyse abgeschlossen.')
window.setTimeout(() => {
setLoading(false)
setAnalysisProgress(0)
setAnalysisStep('')
}, 150)
} }
}, []) }, [])
@ -2683,6 +2737,24 @@ export default function TrainingTab(props: {
return null return null
}, [error, message, trainingRunning, shownTrainingStep]) }, [error, message, trainingRunning, shownTrainingStep])
useEffect(() => {
if (!message) return
if (error) return
if (trainingRunning) return
const looksPartial =
message.toLowerCase().includes('übersprungen') ||
message.toLowerCase().includes('fehlgeschlagen')
if (looksPartial) return
const timer = window.setTimeout(() => {
setMessage(null)
}, 2500)
return () => window.clearTimeout(timer)
}, [message, error, trainingRunning])
const detectorBoxesPanel = ( const detectorBoxesPanel = (
<div className="rounded-lg bg-gray-50 p-2 ring-1 ring-black/5 dark:bg-white/5 dark:ring-white/10"> <div className="rounded-lg bg-gray-50 p-2 ring-1 ring-black/5 dark:bg-white/5 dark:ring-white/10">
<div className="flex items-center justify-between gap-2"> <div className="flex items-center justify-between gap-2">
@ -2824,22 +2896,6 @@ export default function TrainingTab(props: {
return ( return (
<> <>
{activeNotice ? (
<div className="mb-3">
<TrainingNoticeCard
notice={activeNotice}
onClose={
trainingRunning
? undefined
: () => {
setError(null)
setMessage(null)
}
}
/>
</div>
) : null}
<div className="grid grid-cols-1 items-stretch gap-3 lg:grid-cols-[300px_minmax(0,1fr)_300px] xl:grid-cols-[320px_minmax(0,1fr)_320px]"> <div className="grid grid-cols-1 items-stretch gap-3 lg:grid-cols-[300px_minmax(0,1fr)_300px] xl:grid-cols-[320px_minmax(0,1fr)_320px]">
{/* Sidebar links */} {/* Sidebar links */}
<aside className="max-h-[calc(100dvh-190px)] overflow-y-auto rounded-xl border border-gray-200 bg-white p-3 shadow-sm dark:border-white/10 dark:bg-gray-900/60"> <aside className="max-h-[calc(100dvh-190px)] overflow-y-auto rounded-xl border border-gray-200 bg-white p-3 shadow-sm dark:border-white/10 dark:bg-gray-900/60">
@ -3359,7 +3415,10 @@ export default function TrainingTab(props: {
progress={shownTrainingProgress} progress={shownTrainingProgress}
/> />
) : loading ? ( ) : loading ? (
<LoadingImageOverlay /> <LoadingImageOverlay
text={analysisStep}
progress={analysisProgress}
/>
) : null} ) : null}
</div> </div>
) : trainingRunning ? ( ) : trainingRunning ? (
@ -3368,7 +3427,10 @@ export default function TrainingTab(props: {
progress={shownTrainingProgress} progress={shownTrainingProgress}
/> />
) : loading ? ( ) : loading ? (
<LoadingImageOverlay /> <LoadingImageOverlay
text={analysisStep}
progress={analysisProgress}
/>
) : ( ) : (
<div className="text-sm text-white/80">Kein Frame geladen</div> <div className="text-sm text-white/80">Kein Frame geladen</div>
)} )}
@ -3411,9 +3473,25 @@ export default function TrainingTab(props: {
</Button> </Button>
</div> </div>
<div className="mt-2 text-center text-xs text-gray-500 dark:text-gray-400"> {activeNotice ? (
Prüfe das Bild. Wenn die Erkennung stimmt: Passt so. Wenn nicht: korrigieren und speichern. <div className="mx-auto mt-3 w-full max-w-2xl">
</div> <TrainingNoticeCard
notice={activeNotice}
onClose={
trainingRunning
? undefined
: () => {
setError(null)
setMessage(null)
}
}
/>
</div>
) : (
<div className="mt-2 text-center text-xs text-gray-500 dark:text-gray-400">
Prüfe das Bild. Wenn die Erkennung stimmt: Passt so. Wenn nicht: korrigieren und speichern.
</div>
)}
</section> </section>
{/* Rechte Details/Korrektur */} {/* Rechte Details/Korrektur */}