import { useEffect, useState } from 'react';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { api } from '../../api/client';
import type { Printer, PrinterStatus } from '../../api/client';
import { X, Loader2, AlertTriangle, ChevronDown } from 'lucide-react';
import { Card, CardContent } from '../Card';
interface PrintOptionsModalProps {
printer: Printer;
status: PrinterStatus | null | undefined;
onClose: () => void;
}
// Checkbox component matching Bambu Studio style
function Checkbox({
checked,
onChange,
disabled,
loading,
}: {
checked: boolean;
onChange: (checked: boolean) => void;
disabled?: boolean;
loading?: boolean;
}) {
return (
!disabled && !loading && onChange(!checked)}
disabled={disabled || loading}
className={`w-5 h-5 rounded border-2 flex items-center justify-center transition-colors ${
checked
? 'bg-bambu-green border-bambu-green'
: 'bg-transparent border-bambu-gray'
} ${disabled || loading ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer'}`}
>
{loading ? (
) : checked ? (
) : null}
);
}
// Sensitivity dropdown component
function SensitivityDropdown({
value,
onChange,
disabled,
loading,
}: {
value: string;
onChange: (value: string) => void;
disabled?: boolean;
loading?: boolean;
}) {
const [isOpen, setIsOpen] = useState(false);
const options = ['Low', 'Medium', 'High'];
const displayValue = value.charAt(0).toUpperCase() + value.slice(1).toLowerCase();
const isDisabled = disabled || loading;
return (
!isDisabled && setIsOpen(!isOpen)}
disabled={isDisabled}
className={`flex items-center gap-2 px-3 py-1.5 bg-bambu-dark-secondary border border-bambu-dark-tertiary rounded text-sm text-white min-w-[100px] justify-between ${
isDisabled ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer hover:border-bambu-gray'
}`}
>
{loading ? (
) : (
{displayValue}
)}
{isOpen && !isDisabled && (
<>
setIsOpen(false)} />
{options.map((option) => (
{
onChange(option.toLowerCase());
setIsOpen(false);
}}
className={`w-full px-3 py-2 text-left text-sm hover:bg-bambu-dark-tertiary ${
value.toLowerCase() === option.toLowerCase() ? 'text-bambu-green' : 'text-white'
}`}
>
{option}
))}
>
)}
);
}
export function PrintOptionsModal({ printer, status, onClose }: PrintOptionsModalProps) {
const queryClient = useQueryClient();
const printOptions = status?.print_options;
const isConnected = status?.connected ?? false;
// Close on Escape key
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Escape') onClose();
};
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, [onClose]);
// Set print option mutation - track both UI and API module names
const setOptionMutation = useMutation({
mutationFn: ({
apiModuleName,
enabled,
printHalt,
sensitivity,
}: {
uiModuleName: string; // The module name from the UI (for loading state) - accessed via variables
apiModuleName: string; // The module name sent to API (may differ due to firmware quirk)
enabled: boolean;
printHalt?: boolean;
sensitivity?: string;
}) => api.setPrintOption(printer.id, apiModuleName, enabled, printHalt, sensitivity),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['printerStatuses'] });
},
});
const handleToggle = (moduleName: string, enabled: boolean, sensitivity?: string) => {
const printHalt = enabled && sensitivity !== undefined && sensitivity !== 'never_halt';
setOptionMutation.mutate({
uiModuleName: moduleName,
apiModuleName: moduleName,
enabled,
printHalt,
sensitivity: sensitivity ?? 'medium'
});
};
const handleSensitivityChange = (moduleName: string, sensitivity: string) => {
// Spaghetti and Pileup sensitivities are linked in firmware
// For pileup, we send spaghetti_detector to set the shared sensitivity
// All other detectors use their own module name
let apiModuleName = moduleName;
if (moduleName === 'pileup_detector') {
// Pileup sensitivity is linked to spaghetti - use spaghetti_detector
apiModuleName = 'spaghetti_detector';
}
setOptionMutation.mutate({
uiModuleName: moduleName,
apiModuleName,
enabled: true,
printHalt: true,
sensitivity
});
};
// Check loading state using the UI module name (not API module name)
const isToggling = (moduleName: string) =>
setOptionMutation.isPending && setOptionMutation.variables?.uiModuleName === moduleName;
// Check if sensitivity is being changed for a specific detector
// This also returns true for linked detectors (e.g., spaghetti loading shows for pileup too)
const isSensitivityLoading = (moduleName: string) => {
if (!setOptionMutation.isPending || !setOptionMutation.variables?.sensitivity) return false;
const uiModule = setOptionMutation.variables.uiModuleName;
// Direct match
if (uiModule === moduleName) return true;
// Linked: spaghetti and pileup share sensitivity
if ((uiModule === 'spaghetti_detector' && moduleName === 'pileup_detector') ||
(uiModule === 'pileup_detector' && moduleName === 'spaghetti_detector')) return true;
return false;
};
return (
e.stopPropagation()}>
{/* Header */}
Print Options
{/* Content - Scrollable */}
{!isConnected && (
Printer not connected. Options cannot be changed.
)}
{!printOptions ? (
Print options not available. Try refreshing.
) : (
<>
{/* AI Detections Section */}
AI Detections
Printer will send assistant message or pause printing if any of the following problem is detected.
{/* Spaghetti Detection */}
handleToggle('spaghetti_detector', checked, printOptions.halt_print_sensitivity)}
disabled={!isConnected}
loading={isToggling('spaghetti_detector')}
/>
Spaghetti Detection
Detects spaghetti failure (scattered loose filament).
Pausing Sensitivity:
handleSensitivityChange('spaghetti_detector', value)}
disabled={!isConnected || !printOptions.spaghetti_detector}
loading={isSensitivityLoading('spaghetti_detector')}
/>
{/* Purge Chute Pile-Up Detection */}
handleToggle('pileup_detector', checked, printOptions.pileup_sensitivity)}
disabled={!isConnected}
loading={isToggling('pileup_detector')}
/>
Purge Chute Pile-Up Detection
Monitors if the waste is piled up in the purge chute.
Pausing Sensitivity:
handleSensitivityChange('pileup_detector', value)}
disabled={!isConnected || !printOptions.pileup_detector}
loading={isSensitivityLoading('pileup_detector')}
/>
{/* Nozzle Clumping Detection */}
handleToggle('clump_detector', checked, printOptions.nozzle_clumping_sensitivity)}
disabled={!isConnected}
loading={isToggling('clump_detector')}
/>
Nozzle Clumping Detection
Checks if the nozzle is clumping by filaments or other foreign objects.
Pausing Sensitivity:
handleSensitivityChange('clump_detector', value)}
disabled={!isConnected || !printOptions.nozzle_clumping_detector}
loading={isSensitivityLoading('clump_detector')}
/>
{/* Air Printing Detection */}
handleToggle('airprint_detector', checked, printOptions.airprint_sensitivity)}
disabled={!isConnected}
loading={isToggling('airprint_detector')}
/>
Air Printing Detection
Detects air printing caused by nozzle clogging or filament grinding.
Pausing Sensitivity:
handleSensitivityChange('airprint_detector', value)}
disabled={!isConnected || !printOptions.airprint_detector}
loading={isSensitivityLoading('airprint_detector')}
/>
{/* Build Plate Detection Section */}
Build Plate Detection
Ensures the build plate type and placement are correct.
handleToggle('buildplate_marker_detector', checked)}
disabled={!isConnected}
loading={isToggling('buildplate_marker_detector')}
/>
Type Detection
Pauses printing when the detected build plate type does not match the selected one.
{/* Other Options */}
{/* Auto-recover from step loss */}
handleToggle('auto_recovery_step_loss', checked)}
disabled={!isConnected}
loading={isToggling('auto_recovery_step_loss')}
/>
Auto-recover from step loss
{/* Store Sent Files on External Storage - Read-only (no MQTT command available) */}
{}}
disabled={true}
/>
Store Sent Files on External Storage
(read-only)
Save the printing files initiated from Bambu Studio, Bambu Handy and MakerWorld on External Storage.
This option can only be changed on the printer's touchscreen.
>
)}
);
}