import { useState, useEffect, useCallback, useRef } from 'react'; import { useBrotherMachine } from './hooks/useBrotherMachine'; import { FileUpload } from './components/FileUpload'; import { PatternCanvas } from './components/PatternCanvas'; import { ProgressMonitor } from './components/ProgressMonitor'; import { WorkflowStepper } from './components/WorkflowStepper'; import { PatternSummaryCard } from './components/PatternSummaryCard'; import { BluetoothDevicePicker } from './components/BluetoothDevicePicker'; import type { PesPatternData } from './utils/pystitchConverter'; import { pyodideLoader } from './utils/pyodideLoader'; import { hasError, getErrorDetails } from './utils/errorCodeHelpers'; import { canDeletePattern, getStateVisualInfo } from './utils/machineStateHelpers'; import { CheckCircleIcon, BoltIcon, PauseCircleIcon, ExclamationTriangleIcon, ArrowPathIcon, XMarkIcon, InformationCircleIcon } from '@heroicons/react/24/solid'; import './App.css'; function App() { const machine = useBrotherMachine(); const [pesData, setPesData] = useState(null); const [pyodideReady, setPyodideReady] = useState(false); const [pyodideError, setPyodideError] = useState(null); const [patternOffset, setPatternOffset] = useState<{ x: number; y: number }>({ x: 0, y: 0 }); const [patternUploaded, setPatternUploaded] = useState(false); const [currentFileName, setCurrentFileName] = useState(''); // Track current pattern filename const [showErrorPopover, setShowErrorPopover] = useState(false); const errorPopoverRef = useRef(null); const errorButtonRef = useRef(null); // Initialize Pyodide on mount useEffect(() => { pyodideLoader .initialize() .then(() => { setPyodideReady(true); console.log('[App] Pyodide initialized successfully'); }) .catch((err) => { setPyodideError(err instanceof Error ? err.message : 'Failed to initialize Python environment'); console.error('[App] Failed to initialize Pyodide:', err); }); }, []); // Close error popover when clicking outside useEffect(() => { const handleClickOutside = (event: MouseEvent) => { if ( errorPopoverRef.current && !errorPopoverRef.current.contains(event.target as Node) && errorButtonRef.current && !errorButtonRef.current.contains(event.target as Node) ) { setShowErrorPopover(false); } }; if (showErrorPopover) { document.addEventListener('mousedown', handleClickOutside); return () => document.removeEventListener('mousedown', handleClickOutside); } }, [showErrorPopover]); // Auto-load cached pattern when available const resumedPattern = machine.resumedPattern; const resumeFileName = machine.resumeFileName; if (resumedPattern && !pesData) { console.log('[App] Loading resumed pattern:', resumeFileName, 'Offset:', resumedPattern.patternOffset); setPesData(resumedPattern.pesData); // Restore the cached pattern offset if (resumedPattern.patternOffset) { setPatternOffset(resumedPattern.patternOffset); } // Preserve the filename from cache if (resumeFileName) { setCurrentFileName(resumeFileName); } } const handlePatternLoaded = useCallback((data: PesPatternData, fileName: string) => { setPesData(data); setCurrentFileName(fileName); // Reset pattern offset when new pattern is loaded setPatternOffset({ x: 0, y: 0 }); setPatternUploaded(false); }, []); const handlePatternOffsetChange = useCallback((offsetX: number, offsetY: number) => { setPatternOffset({ x: offsetX, y: offsetY }); console.log('[App] Pattern offset changed:', { x: offsetX, y: offsetY }); }, []); const handleUpload = useCallback(async (penData: Uint8Array, pesData: PesPatternData, fileName: string, patternOffset?: { x: number; y: number }) => { await machine.uploadPattern(penData, pesData, fileName, patternOffset); setPatternUploaded(true); }, [machine]); const handleDeletePattern = useCallback(async () => { await machine.deletePattern(); setPatternUploaded(false); // NOTE: We intentionally DON'T clear setPesData(null) here // so the pattern remains visible in the canvas for re-editing and re-uploading }, [machine]); // Track pattern uploaded state based on machine status const isConnected = machine.isConnected; const patternInfo = machine.patternInfo; if (!isConnected) { if (patternUploaded) { setPatternUploaded(false); } } else { // Pattern is uploaded if machine has pattern info const shouldBeUploaded = patternInfo !== null; if (patternUploaded !== shouldBeUploaded) { setPatternUploaded(shouldBeUploaded); } } // Get state visual info for header status badge const stateVisual = getStateVisualInfo(machine.machineStatus); const stateIcons = { ready: CheckCircleIcon, active: BoltIcon, waiting: PauseCircleIcon, complete: CheckCircleIcon, interrupted: PauseCircleIcon, error: ExclamationTriangleIcon, }; const StatusIcon = stateIcons[stateVisual.iconName]; return (
{/* Machine Connection Status - Responsive width column */}

Respira

{machine.isConnected && machine.machineInfo?.serialNumber && ( • {machine.machineInfo.serialNumber} )} {machine.isPolling && ( )}
{machine.isConnected ? ( <> {machine.machineStatusName} ) : (

Not Connected

)} {/* Error indicator - always render to prevent layout shift */}
{/* Error popover */} {showErrorPopover && (machine.error || pyodideError) && (
{(() => { const errorDetails = getErrorDetails(machine.machineError); const isPairingError = machine.isPairingError; const errorMsg = pyodideError || machine.error || ''; const isInfo = isPairingError || errorDetails?.isInformational; const bgColor = isInfo ? 'bg-blue-50 dark:bg-blue-900/95 border-blue-600 dark:border-blue-500' : 'bg-red-50 dark:bg-red-900/95 border-red-600 dark:border-red-500'; const iconColor = isInfo ? 'text-blue-600 dark:text-blue-400' : 'text-red-600 dark:text-red-400'; const textColor = isInfo ? 'text-blue-900 dark:text-blue-200' : 'text-red-900 dark:text-red-200'; const descColor = isInfo ? 'text-blue-800 dark:text-blue-300' : 'text-red-800 dark:text-red-300'; const listColor = isInfo ? 'text-blue-700 dark:text-blue-300' : 'text-red-700 dark:text-red-300'; const Icon = isInfo ? InformationCircleIcon : ExclamationTriangleIcon; const title = errorDetails?.title || (isPairingError ? 'Pairing Required' : 'Error'); return (

{title}

{errorDetails?.description || errorMsg}

{errorDetails?.solutions && errorDetails.solutions.length > 0 && ( <>

{isInfo ? 'Steps:' : 'How to Fix:'}

    {errorDetails.solutions.map((solution, index) => (
  1. {solution}
  2. ))}
)} {machine.machineError !== undefined && !errorDetails?.isInformational && (

Error Code: 0x{machine.machineError.toString(16).toUpperCase().padStart(2, '0')}

)}
); })()}
)}
{/* Workflow Stepper - Flexible width column */}
{/* Left Column - Controls */}
{/* Connect Button - Show when disconnected */} {!machine.isConnected && (

Get Started

Connect to your embroidery machine

)} {/* Pattern File - Show during upload stage (before pattern is uploaded) */} {machine.isConnected && !patternUploaded && ( )} {/* Compact Pattern Summary - Show after upload (during sewing stages) */} {machine.isConnected && patternUploaded && pesData && ( )} {/* Progress Monitor - Show when pattern is uploaded */} {machine.isConnected && patternUploaded && (
)}
{/* Right Column - Pattern Preview */}
{pesData ? ( 0 && machine.uploadProgress < 100} /> ) : (

Pattern Preview

{/* Decorative background pattern */}

No Pattern Loaded

Connect to your machine and choose a PES embroidery file to see your design preview

Drag to Position
Zoom & Pan
Real-time Preview
)}
{/* Bluetooth Device Picker (Electron only) */}
); } export default App;