AiDetectionModal.tsx 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111
  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. interface AiDetectionModalProps {
  9. printerName: string;
  10. detection?: { class: string; frame_count: number; score: number };
  11. // null = no error, or the viewer lacks settings:read (the backend withholds
  12. // error strings from non-settings users because they can embed config URLs)
  13. lastError: string | null;
  14. onClose: () => void;
  15. }
  16. export function AiDetectionModal({ printerName, detection, lastError, onClose }: AiDetectionModalProps) {
  17. const { t } = useTranslation();
  18. const navigate = useNavigate();
  19. useEffect(() => {
  20. const handleKeyDown = (e: KeyboardEvent) => {
  21. if (e.key === 'Escape') onClose();
  22. };
  23. window.addEventListener('keydown', handleKeyDown);
  24. return () => window.removeEventListener('keydown', handleKeyDown);
  25. }, [onClose]);
  26. const cls = detection
  27. ? (detection.class === 'failure' || detection.class === 'warning' ? detection.class : 'safe')
  28. : 'idle';
  29. const statusColor =
  30. cls === 'failure'
  31. ? 'text-status-error'
  32. : cls === 'warning'
  33. ? 'text-status-warning'
  34. : cls === 'safe'
  35. ? 'text-status-ok'
  36. : 'text-bambu-gray';
  37. return (
  38. <div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
  39. <div className="bg-bambu-dark-secondary rounded-lg shadow-xl max-w-lg w-full max-h-[80vh] flex flex-col">
  40. {/* Header */}
  41. <div className="flex items-center justify-between p-4 border-b border-bambu-dark-tertiary">
  42. <div className="flex items-center gap-2">
  43. <ScanEye className="w-5 h-5 text-bambu-green" />
  44. <h2 className="text-lg font-semibold text-white">
  45. {t('printers.aiDetection.modalTitle', { name: printerName })}
  46. </h2>
  47. </div>
  48. <button
  49. onClick={onClose}
  50. className="p-1 hover:bg-bambu-dark-tertiary rounded-lg transition-colors"
  51. aria-label={t('common.close')}
  52. >
  53. <X className="w-5 h-5 text-bambu-gray" />
  54. </button>
  55. </div>
  56. {/* Content */}
  57. <div className="flex-1 overflow-y-auto p-4 space-y-4 text-sm">
  58. <div className="space-y-2">
  59. <div className="flex justify-between">
  60. <span className="text-bambu-gray">{t('printers.aiDetection.currentStatus')}</span>
  61. <span className={`font-medium ${statusColor}`}>{t(`printers.aiDetection.${cls}`)}</span>
  62. </div>
  63. {detection ? (
  64. <>
  65. <div className="flex justify-between">
  66. <span className="text-bambu-gray">{t('printers.aiDetection.score')}</span>
  67. <span className="text-white font-mono">{detection.score.toFixed(3)}</span>
  68. </div>
  69. <div className="flex justify-between">
  70. <span className="text-bambu-gray">{t('printers.aiDetection.framesAnalyzed')}</span>
  71. <span className="text-white font-mono">{detection.frame_count}</span>
  72. </div>
  73. </>
  74. ) : (
  75. <p className="text-bambu-gray">{t('printers.aiDetection.idleHint')}</p>
  76. )}
  77. </div>
  78. {lastError && (
  79. <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">
  80. <AlertCircle className="w-4 h-4 mt-0.5 flex-shrink-0 text-red-700 dark:text-red-400" />
  81. <div className="min-w-0">
  82. <div className="font-medium text-red-700 dark:text-red-400">
  83. {t('printers.aiDetection.lastError')}
  84. </div>
  85. <p className="text-red-700/80 dark:text-red-300/80 break-words mt-1">{lastError}</p>
  86. </div>
  87. </div>
  88. )}
  89. </div>
  90. {/* Footer */}
  91. <div className="p-4 border-t border-bambu-dark-tertiary flex items-center justify-end">
  92. <button
  93. onClick={() => navigate('/settings?tab=failure-detection')}
  94. 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"
  95. >
  96. <Settings className="w-4 h-4" />
  97. {t('printers.aiDetection.openSettings')}
  98. </button>
  99. </div>
  100. </div>
  101. </div>
  102. );
  103. }