mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-06 06:02:05 +02:00
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.
121 lines
5.3 KiB
TypeScript
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>
|
|
);
|
|
}
|