Files
bambuddy/frontend/src/components/AiDetectionModal.tsx
T
maziggy 06e5114a03 Do not report a printer as Safe when nothing is checking it (issue #2952)
The printer card's AI badge collapsed every class that was not Warning or
Failure into green Safe, and the service reported `safe` whenever it had no
verdict. The state entry is created when a monitored print is first seen --
before the first snapshot, let alone the first inference -- so a rejected ML
API token, an unreachable ML API, a failed capture and an unset External URL
all rendered as a healthy watched print: green Safe at score 0.000.

For a safety feature that is the worst failure mode available: it asserts the
print is being watched exactly when it is not. The reporter read that badge and
concluded the loop had never started. It had been calling the ML API every ten
seconds and being turned away with a 401 -- invisible because Obico's auth
layer rejects a bad token before its request log sees it, and because
successful checks log nothing there either.

Add two honest states. Not checking (amber) when the last poll produced no
result, carrying the reason; Starting while a monitored print waits for its
first result. Score and frame count are withheld while not checking, since
0.000 beside "Not checking" reads as a measurement rather than its absence.
The reason is per printer, so a card names its own problem rather than
whichever printer failed most recently, and stays behind settings:read because
it can quote configured URLs -- the badge state does not, because whether a
print is watched is not configuration. An unrecognised class now falls back to
Starting, not Safe.

Test Connection saves the form before probing, so a green result describes the
configuration the loop actually runs with rather than what is typed in the
boxes.
2026-08-25 08:49:24 +02:00

121 lines
5.3 KiB
TypeScript

// AI Failure Detection modal (#1546) — opened from the printer card's AI badge.
// Shows the live classification for this printer and the detection service's
// last error, without leaving the Printers page (mirrors the HMS error modal).
import { useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { useNavigate } from 'react-router-dom';
import { X, ScanEye, AlertCircle, Settings } from 'lucide-react';
import { aiDetectionClass, hasVerdict, type AiDetection } from '../utils/aiDetection';
interface AiDetectionModalProps {
printerName: string;
detection?: AiDetection;
// null = no error, or the viewer lacks settings:read (the backend withholds
// error strings from non-settings users because they can embed config URLs)
lastError: string | null;
onClose: () => void;
}
export function AiDetectionModal({ printerName, detection, lastError, onClose }: AiDetectionModalProps) {
const { t } = useTranslation();
const navigate = useNavigate();
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Escape') onClose();
};
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, [onClose]);
const cls = aiDetectionClass(detection);
const statusColor =
cls === 'failure'
? 'text-status-error'
: cls === 'warning'
? 'text-status-warning'
: cls === 'safe'
? 'text-status-ok'
: cls === 'error'
? 'text-amber-600 dark:text-amber-400'
: 'text-bambu-gray';
// This printer's own reason beats the service-wide one: with several printers
// monitored, the global string is whichever failed most recently and may be
// about someone else's printer entirely.
const reason = detection?.error ?? lastError;
return (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
<div className="bg-bambu-dark-secondary rounded-lg shadow-xl max-w-lg w-full max-h-[80vh] flex flex-col">
{/* Header */}
<div className="flex items-center justify-between p-4 border-b border-bambu-dark-tertiary">
<div className="flex items-center gap-2">
<ScanEye className="w-5 h-5 text-bambu-green" />
<h2 className="text-lg font-semibold text-white">
{t('printers.aiDetection.modalTitle', { name: printerName })}
</h2>
</div>
<button
onClick={onClose}
className="p-1 hover:bg-bambu-dark-tertiary rounded-lg transition-colors"
aria-label={t('common.close')}
>
<X className="w-5 h-5 text-bambu-gray" />
</button>
</div>
{/* Content */}
<div className="flex-1 overflow-y-auto p-4 space-y-4 text-sm">
<div className="space-y-2">
<div className="flex justify-between">
<span className="text-bambu-gray">{t('printers.aiDetection.currentStatus')}</span>
<span className={`font-medium ${statusColor}`}>{t(`printers.aiDetection.${cls}`)}</span>
</div>
{detection && hasVerdict(cls) && (
<>
<div className="flex justify-between">
<span className="text-bambu-gray">{t('printers.aiDetection.score')}</span>
<span className="text-white font-mono">{detection.score.toFixed(3)}</span>
</div>
<div className="flex justify-between">
<span className="text-bambu-gray">{t('printers.aiDetection.framesAnalyzed')}</span>
<span className="text-white font-mono">{detection.frame_count}</span>
</div>
</>
)}
{/* A score of 0.000 next to "Not checking" reads as a reassuring
measurement rather than the absence of one, so it is withheld
until an inference has actually produced it (#2952). */}
{cls === 'error' && <p className="text-bambu-gray">{t('printers.aiDetection.errorHint')}</p>}
{!detection && <p className="text-bambu-gray">{t('printers.aiDetection.idleHint')}</p>}
</div>
{reason && (
<div className="flex items-start gap-2 p-3 rounded-lg bg-red-50 dark:bg-red-500/10 border border-red-300 dark:border-red-500/30">
<AlertCircle className="w-4 h-4 mt-0.5 flex-shrink-0 text-red-700 dark:text-red-400" />
<div className="min-w-0">
<div className="font-medium text-red-700 dark:text-red-400">
{t('printers.aiDetection.lastError')}
</div>
<p className="text-red-700/80 dark:text-red-300/80 break-words mt-1">{reason}</p>
</div>
</div>
)}
</div>
{/* Footer */}
<div className="p-4 border-t border-bambu-dark-tertiary flex items-center justify-end">
<button
onClick={() => navigate('/settings?tab=failure-detection')}
className="flex items-center gap-1.5 px-3 py-1.5 text-sm font-medium rounded-lg bg-bambu-dark-tertiary text-bambu-gray hover:text-white transition-colors"
>
<Settings className="w-4 h-4" />
{t('printers.aiDetection.openSettings')}
</button>
</div>
</div>
</div>
);
}