AiDetectionModal.tsx 6.2 KB

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