mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-04 05:01:37 +02:00
The live Obico classification was only visible under Settings -> Failure Detection, so tracking how detection matched an ongoing print meant flipping between the Printers screen and Settings. Each printer card's badge row now shows an AI badge whenever detection is enabled for that printer, like the other health badges: gray Idle while no print is being watched, then green Safe, amber Warning, or red Failure while a print is actively monitored. The tooltip carries the current smoothed score; clicking jumps to the full detection status and history in Settings. Printers excluded from the monitored subset show no badge. Served by a new lightweight /obico/printer-status endpoint readable with printer permissions alone - it exposes only the enabled flag, the monitored-printer set, and per-printer classification, keeping ML URL and other configuration behind the existing settings-gated endpoint.
112 lines
4.7 KiB
TypeScript
112 lines
4.7 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';
|
|
|
|
interface AiDetectionModalProps {
|
|
printerName: string;
|
|
detection?: { class: string; frame_count: number; score: number };
|
|
// 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 = detection
|
|
? (detection.class === 'failure' || detection.class === 'warning' ? detection.class : 'safe')
|
|
: 'idle';
|
|
const statusColor =
|
|
cls === 'failure'
|
|
? 'text-status-error'
|
|
: cls === 'warning'
|
|
? 'text-status-warning'
|
|
: cls === 'safe'
|
|
? 'text-status-ok'
|
|
: 'text-bambu-gray';
|
|
|
|
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 ? (
|
|
<>
|
|
<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>
|
|
</>
|
|
) : (
|
|
<p className="text-bambu-gray">{t('printers.aiDetection.idleHint')}</p>
|
|
)}
|
|
</div>
|
|
|
|
{lastError && (
|
|
<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">{lastError}</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>
|
|
);
|
|
}
|