AiDetectionModal.tsx 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120
  1. // AI Failure Detection modal (#1546) — opened from the printer card's AI badge.
  2. // Shows the live classification for this printer and the detection service's
  3. // last error, without leaving the Printers page (mirrors the HMS error modal).
  4. import { useEffect } from 'react';
  5. import { useTranslation } from 'react-i18next';
  6. import { useNavigate } from 'react-router-dom';
  7. import { X, ScanEye, AlertCircle, Settings } from 'lucide-react';
  8. import { aiDetectionClass, hasVerdict, type AiDetection } from '../utils/aiDetection';
  9. interface AiDetectionModalProps {
  10. printerName: string;
  11. detection?: AiDetection;
  12. // null = no error, or the viewer lacks settings:read (the backend withholds
  13. // error strings from non-settings users because they can embed config URLs)
  14. lastError: string | null;
  15. onClose: () => void;
  16. }
  17. export function AiDetectionModal({ printerName, detection, lastError, onClose }: AiDetectionModalProps) {
  18. const { t } = useTranslation();
  19. const navigate = useNavigate();
  20. useEffect(() => {
  21. const handleKeyDown = (e: KeyboardEvent) => {
  22. if (e.key === 'Escape') onClose();
  23. };
  24. window.addEventListener('keydown', handleKeyDown);
  25. return () => window.removeEventListener('keydown', handleKeyDown);
  26. }, [onClose]);
  27. const cls = aiDetectionClass(detection);
  28. const statusColor =
  29. cls === 'failure'
  30. ? 'text-status-error'
  31. : cls === 'warning'
  32. ? 'text-status-warning'
  33. : cls === 'safe'
  34. ? 'text-status-ok'
  35. : cls === 'error'
  36. ? 'text-amber-600 dark:text-amber-400'
  37. : 'text-bambu-gray';
  38. // This printer's own reason beats the service-wide one: with several printers
  39. // monitored, the global string is whichever failed most recently and may be
  40. // about someone else's printer entirely.
  41. const reason = detection?.error ?? lastError;
  42. return (
  43. <div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
  44. <div className="bg-bambu-dark-secondary rounded-lg shadow-xl max-w-lg w-full max-h-[80vh] flex flex-col">
  45. {/* Header */}
  46. <div className="flex items-center justify-between p-4 border-b border-bambu-dark-tertiary">
  47. <div className="flex items-center gap-2">
  48. <ScanEye className="w-5 h-5 text-bambu-green" />
  49. <h2 className="text-lg font-semibold text-white">
  50. {t('printers.aiDetection.modalTitle', { name: printerName })}
  51. </h2>
  52. </div>
  53. <button
  54. onClick={onClose}
  55. className="p-1 hover:bg-bambu-dark-tertiary rounded-lg transition-colors"
  56. aria-label={t('common.close')}
  57. >
  58. <X className="w-5 h-5 text-bambu-gray" />
  59. </button>
  60. </div>
  61. {/* Content */}
  62. <div className="flex-1 overflow-y-auto p-4 space-y-4 text-sm">
  63. <div className="space-y-2">
  64. <div className="flex justify-between">
  65. <span className="text-bambu-gray">{t('printers.aiDetection.currentStatus')}</span>
  66. <span className={`font-medium ${statusColor}`}>{t(`printers.aiDetection.${cls}`)}</span>
  67. </div>
  68. {detection && hasVerdict(cls) && (
  69. <>
  70. <div className="flex justify-between">
  71. <span className="text-bambu-gray">{t('printers.aiDetection.score')}</span>
  72. <span className="text-white font-mono">{detection.score.toFixed(3)}</span>
  73. </div>
  74. <div className="flex justify-between">
  75. <span className="text-bambu-gray">{t('printers.aiDetection.framesAnalyzed')}</span>
  76. <span className="text-white font-mono">{detection.frame_count}</span>
  77. </div>
  78. </>
  79. )}
  80. {/* A score of 0.000 next to "Not checking" reads as a reassuring
  81. measurement rather than the absence of one, so it is withheld
  82. until an inference has actually produced it (#2952). */}
  83. {cls === 'error' && <p className="text-bambu-gray">{t('printers.aiDetection.errorHint')}</p>}
  84. {!detection && <p className="text-bambu-gray">{t('printers.aiDetection.idleHint')}</p>}
  85. </div>
  86. {reason && (
  87. <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">
  88. <AlertCircle className="w-4 h-4 mt-0.5 flex-shrink-0 text-red-700 dark:text-red-400" />
  89. <div className="min-w-0">
  90. <div className="font-medium text-red-700 dark:text-red-400">
  91. {t('printers.aiDetection.lastError')}
  92. </div>
  93. <p className="text-red-700/80 dark:text-red-300/80 break-words mt-1">{reason}</p>
  94. </div>
  95. </div>
  96. )}
  97. </div>
  98. {/* Footer */}
  99. <div className="p-4 border-t border-bambu-dark-tertiary flex items-center justify-end">
  100. <button
  101. onClick={() => navigate('/settings?tab=failure-detection')}
  102. 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"
  103. >
  104. <Settings className="w-4 h-4" />
  105. {t('printers.aiDetection.openSettings')}
  106. </button>
  107. </div>
  108. </div>
  109. </div>
  110. );
  111. }