// 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 (
{/* Header */}

{t('printers.aiDetection.modalTitle', { name: printerName })}

{/* Content */}
{t('printers.aiDetection.currentStatus')} {t(`printers.aiDetection.${cls}`)}
{detection && hasVerdict(cls) && ( <>
{t('printers.aiDetection.score')} {detection.score.toFixed(3)}
{t('printers.aiDetection.framesAnalyzed')} {detection.frame_count}
)} {/* 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' &&

{t('printers.aiDetection.errorHint')}

} {!detection &&

{t('printers.aiDetection.idleHint')}

}
{reason && (
{t('printers.aiDetection.lastError')}

{reason}

)}
{/* Footer */}
); }