import { useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { Loader2, Package, Trash2, Upload } from 'lucide-react'; import { api, type SlicerBundle } from '../api/client'; import { Card, CardContent, CardHeader } from './Card'; import { Button } from './Button'; import { ConfirmModal } from './ConfirmModal'; import { useToast } from '../contexts/ToastContext'; // Settings panel for managing BambuStudio "Printer Preset Bundles" // (.bbscfg) on the slicer sidecar. Sits below the slicer-API URL panel // in SettingsPage and is hidden when use_slicer_api is off — without a // configured sidecar there's nowhere to upload bundles to. // // Backend wiring: backend/app/api/routes/slicer_presets.py exposes // /api/v1/slicer/bundles (POST/GET/DELETE). The list call returns [] // when no sidecar is configured, so an empty render is the natural // "first-run" state for users who haven't enabled the sidecar yet. export function SlicerBundlesPanel() { const { t } = useTranslation(); const queryClient = useQueryClient(); const { showToast } = useToast(); const fileInputRef = useRef(null); const [pendingDelete, setPendingDelete] = useState(null); const { data: bundles, isLoading } = useQuery({ queryKey: ['slicer-bundles'], queryFn: api.listSlicerBundles, }); const importMutation = useMutation({ mutationFn: (file: File) => api.importSlicerBundle(file), onSuccess: (bundle) => { queryClient.invalidateQueries({ queryKey: ['slicer-bundles'] }); showToast( t('settings.slicerBundles.uploadSuccess', { defaultValue: 'Imported {{name}}', name: bundle.printer_preset_name, }), 'success', ); // Reset the file input so the same file can be re-selected after a // failed retry. (Without this, a second click on the same file // doesn't trigger onChange and looks like the panel is broken.) if (fileInputRef.current) fileInputRef.current.value = ''; }, onError: (err: Error) => { showToast( t('settings.slicerBundles.uploadError', { defaultValue: 'Bundle upload failed: {{message}}', message: err.message, }), 'error', ); if (fileInputRef.current) fileInputRef.current.value = ''; }, }); const deleteMutation = useMutation({ mutationFn: (bundleId: string) => api.deleteSlicerBundle(bundleId), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['slicer-bundles'] }); setPendingDelete(null); showToast( t('settings.slicerBundles.deleteSuccess', { defaultValue: 'Bundle removed', }), 'success', ); }, onError: (err: Error) => { showToast( t('settings.slicerBundles.deleteError', { defaultValue: 'Bundle delete failed: {{message}}', message: err.message, }), 'error', ); setPendingDelete(null); }, }); const handleFileChange = (e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (!file) return; importMutation.mutate(file); }; return (

{t('settings.slicerBundles.title', { defaultValue: 'Slicer Bundles' })}

{t('settings.slicerBundles.description', { defaultValue: 'Import a Printer Preset Bundle (.bbscfg) exported from BambuStudio (File → Export → Export Preset Bundle → "Printer preset bundle"). Once imported, slice requests can pick presets from the bundle by name without re-uploading the JSON profile triplet.', })}

{isLoading ? (
{t('settings.slicerBundles.loading', { defaultValue: 'Loading bundles…' })}
) : bundles && bundles.length > 0 ? (
    {bundles.map((b) => (
  • {b.printer_preset_name}

    {t('settings.slicerBundles.summary', { defaultValue: '{{processCount}} process · {{filamentCount}} filament presets', processCount: b.process.length, filamentCount: b.filament.length, })} {b.version && ` · v${b.version}`}

  • ))}
) : (

{t('settings.slicerBundles.empty', { defaultValue: 'No bundles imported yet.', })}

)}
{pendingDelete && ( deleteMutation.mutate(pendingDelete.id)} onCancel={() => setPendingDelete(null)} /> )}
); }