// 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, OCTOEVERYWHERE_USAGE_LIMIT, type AiDetection } from '../utils/aiDetection'; import { useAuth } from '../contexts/AuthContext'; import { OctoEverywhereLimitNotice } from './OctoEverywhereLimitNotice'; 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; lastErrorCode?: string | null; onClose: () => void; } export function AiDetectionModal({ printerName, detection, lastError, lastErrorCode, onClose }: AiDetectionModalProps) { const { t } = useTranslation(); const navigate = useNavigate(); const { hasPermission } = useAuth(); 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 hasPrinterError = !!(detection?.error || detection?.error_code); const reason = hasPrinterError ? detection?.error : lastError; const errorCode = hasPrinterError ? detection?.error_code : lastErrorCode; const canSeeError = hasPermission('settings:read'); return (
{/* Header */}

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

{/* Content */}
{t('printers.aiDetection.currentStatus')} {t(`printers.aiDetection.${cls}`)}
{detection && hasVerdict(cls) && ( <>
{t('print_quality' in detection ? 'octoeverywhere.printQuality' : 'printers.aiDetection.score')} {'print_quality' in detection ? detection.print_quality === null ? '—' : `${detection.print_quality}/10` : 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')}

}
{canSeeError && errorCode === OCTOEVERYWHERE_USAGE_LIMIT ? ( ) : canSeeError && reason && (
{t('printers.aiDetection.lastError')}

{reason}

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