diff --git a/src/App.tsx b/src/App.tsx index c6a3166..56fc0d5 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,17 +1,16 @@ -import { useState, useEffect, useCallback } from 'react'; +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 { NextStepGuide } from './components/NextStepGuide'; import { PatternSummaryCard } from './components/PatternSummaryCard'; import { BluetoothDevicePicker } from './components/BluetoothDevicePicker'; import type { PesPatternData } from './utils/pystitchConverter'; import { pyodideLoader } from './utils/pyodideLoader'; -import { hasError } from './utils/errorCodeHelpers'; +import { hasError, getErrorDetails } from './utils/errorCodeHelpers'; import { canDeletePattern, getStateVisualInfo } from './utils/machineStateHelpers'; -import { CheckCircleIcon, BoltIcon, PauseCircleIcon, ExclamationTriangleIcon, ArrowPathIcon, XMarkIcon } from '@heroicons/react/24/solid'; +import { CheckCircleIcon, BoltIcon, PauseCircleIcon, ExclamationTriangleIcon, ArrowPathIcon, XMarkIcon, InformationCircleIcon } from '@heroicons/react/24/solid'; import './App.css'; function App() { @@ -22,6 +21,9 @@ function App() { 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(() => { @@ -37,6 +39,25 @@ function App() { }); }, []); + // 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; @@ -142,7 +163,7 @@ function App() { )} -
+
{machine.isConnected ? ( <> + + {/* 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')} +

+ )} +
+
+
+ ); + })()} +
+ )} +
@@ -173,58 +310,15 @@ function App() { isConnected={machine.isConnected} hasPattern={pesData !== null} patternUploaded={patternUploaded} + hasError={hasError(machine.machineError)} + errorMessage={machine.error || undefined} + errorCode={machine.machineError} />
- {/* Global errors */} - {machine.error && ( -
-
- - {machine.isPairingError ? ( - - ) : ( - - )} - -
-
{machine.isPairingError ? 'Pairing Required' : 'Error'}
-
{machine.error}
-
-
-
- )} - {pyodideError && ( -
-
- - - -
- Python Error: {pyodideError} -
-
-
- )} - {!pyodideReady && !pyodideError && ( -
-
- - - - - Initializing Python environment... -
-
- )} -
{/* Left Column - Controls */}
@@ -362,17 +456,6 @@ function App() { {/* Bluetooth Device Picker (Electron only) */}
- - {/* Next Step Guide - Fixed floating overlay */} -
); } diff --git a/src/components/NextStepGuide.tsx b/src/components/NextStepGuide.tsx deleted file mode 100644 index fc07e89..0000000 --- a/src/components/NextStepGuide.tsx +++ /dev/null @@ -1,354 +0,0 @@ -import { useState, useEffect } from 'react'; -import { InformationCircleIcon, ExclamationTriangleIcon, XMarkIcon } from '@heroicons/react/24/solid'; -import { MachineStatus } from '../types/machine'; -import { getErrorDetails } from '../utils/errorCodeHelpers'; - -interface NextStepGuideProps { - machineStatus: MachineStatus; - isConnected: boolean; - hasPattern: boolean; - patternUploaded: boolean; - hasError: boolean; - errorMessage?: string; - errorCode?: number; -} - -export function NextStepGuide({ - machineStatus, - isConnected, - hasPattern, - patternUploaded, - hasError, - errorMessage, - errorCode -}: NextStepGuideProps) { - const [isExpanded, setIsExpanded] = useState(true); - - // Expand when state changes - useEffect(() => { - setIsExpanded(true); - }, [machineStatus, isConnected, hasPattern, patternUploaded, hasError]); - - // Render guide content based on state - const renderContent = () => { - // Don't show if there's an error - show detailed error guidance instead - if (hasError) { - const errorDetails = getErrorDetails(errorCode); - - // Check if this is informational (like initialization steps) vs a real error - if (errorDetails?.isInformational) { - return ( -
-
- -
-

- {errorDetails.title} -

-

- {errorDetails.description} -

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

Steps:

-
    - {errorDetails.solutions.map((solution, index) => ( -
  1. {solution}
  2. - ))} -
- - )} -
-
-
- ); - } - - // Regular error display for actual errors - return ( -
-
- -
-

- {errorDetails?.title || 'Error Occurred'} -

-

- {errorDetails?.description || errorMessage || 'An error occurred. Please check the machine and try again.'} -

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

How to Fix:

-
    - {errorDetails.solutions.map((solution, index) => ( -
  1. {solution}
  2. - ))} -
- - )} - {errorCode !== undefined && ( -

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

- )} -
-
-
- ); - } - - // Determine what to show based on current state - if (!isConnected) { - return ( -
-
- -
-

Step 1: Connect to Machine

-

To get started, connect to your Brother embroidery machine via Bluetooth.

-
    -
  • Make sure your machine is powered on
  • -
  • Enable Bluetooth on your machine
  • -
  • Click the "Connect to Machine" button below
  • -
-
-
-
- ); - } - - if (!hasPattern) { - return ( -
-
- -
-

Step 2: Load Your Pattern

-

Choose a PES embroidery file from your computer to preview and upload.

-
    -
  • Click "Choose PES File" in the Pattern File section
  • -
  • Select your embroidery design (.pes file)
  • -
  • Review the pattern preview on the right
  • -
  • You can drag the pattern to adjust its position
  • -
-
-
-
- ); - } - - if (!patternUploaded) { - return ( -
-
- -
-

Step 3: Upload Pattern to Machine

-

Send your pattern to the embroidery machine to prepare for sewing.

-
    -
  • Review the pattern preview to ensure it's positioned correctly
  • -
  • Check the pattern size matches your hoop
  • -
  • Click "Upload to Machine" when ready
  • -
  • Wait for the upload to complete (this may take a minute)
  • -
-
-
-
- ); - } - - // Pattern is uploaded, guide based on machine status - switch (machineStatus) { - case MachineStatus.IDLE: - return ( -
-
- -
-

Step 4: Start Mask Trace

-

The mask trace helps the machine understand the pattern boundaries.

-
    -
  • Click "Start Mask Trace" button in the Sewing Progress section
  • -
  • The machine will trace the pattern outline
  • -
  • This ensures the hoop is positioned correctly
  • -
-
-
-
- ); - - case MachineStatus.MASK_TRACE_LOCK_WAIT: - return ( -
-
- -
-

Machine Action Required

-

The machine is ready to trace the pattern outline.

-
    -
  • Press the button on your machine to confirm and start the mask trace
  • -
  • Ensure the hoop is properly attached
  • -
  • Make sure the needle area is clear
  • -
-
-
-
- ); - - case MachineStatus.MASK_TRACING: - return ( -
-
- -
-

Mask Trace In Progress

-

The machine is tracing the pattern boundary. Please wait...

-
    -
  • Watch the machine trace the outline
  • -
  • Verify the pattern fits within your hoop
  • -
  • Do not interrupt the machine
  • -
-
-
-
- ); - - case MachineStatus.MASK_TRACE_COMPLETE: - case MachineStatus.SEWING_WAIT: - return ( -
-
- -
-

Step 5: Ready to Sew!

-

The machine is ready to begin embroidering your pattern.

-
    -
  • Verify your thread colors are correct
  • -
  • Ensure the fabric is properly hooped
  • -
  • Click "Start Sewing" when ready
  • -
-
-
-
- ); - - case MachineStatus.SEWING: - return ( -
-
- -
-

Step 6: Sewing In Progress

-

Your embroidery is being stitched. Monitor the progress below.

-
    -
  • Watch the progress bar and current stitch count
  • -
  • The machine will pause when a color change is needed
  • -
  • Do not leave the machine unattended
  • -
-
-
-
- ); - - case MachineStatus.COLOR_CHANGE_WAIT: - return ( -
-
- -
-

Thread Change Required

-

The machine needs a different thread color to continue.

-
    -
  • Check the color blocks section to see which thread is needed
  • -
  • Change to the correct thread color
  • -
  • Press the button on your machine to resume sewing
  • -
-
-
-
- ); - - case MachineStatus.PAUSE: - case MachineStatus.STOP: - case MachineStatus.SEWING_INTERRUPTION: - return ( -
-
- -
-

Sewing Paused

-

The embroidery has been paused or interrupted.

-
    -
  • Check if everything is okay with the machine
  • -
  • Click "Resume Sewing" when ready to continue
  • -
  • The machine will pick up where it left off
  • -
-
-
-
- ); - - case MachineStatus.SEWING_COMPLETE: - return ( -
-
- -
-

Step 7: Embroidery Complete!

-

Your embroidery is finished. Great work!

-
    -
  • Remove the hoop from the machine
  • -
  • Press the Accept button on the machine
  • -
  • Carefully remove your finished embroidery
  • -
  • Trim any jump stitches or loose threads
  • -
  • Click "Delete Pattern" to start a new project
  • -
-
-
-
- ); - - default: - return null; - } - }; - - // Render floating overlay - return ( - <> - {/* Collapsed state - small info button in bottom-right */} - {!isExpanded && ( -
- -
- )} - - {/* Expanded state - floating overlay */} - {isExpanded && ( -
-
- {/* Close button */} - - - {/* Content */} - {renderContent()} -
-
- )} - - ); -} diff --git a/src/components/WorkflowStepper.tsx b/src/components/WorkflowStepper.tsx index 1f03ebc..ed4df8c 100644 --- a/src/components/WorkflowStepper.tsx +++ b/src/components/WorkflowStepper.tsx @@ -1,11 +1,16 @@ -import { CheckCircleIcon } from '@heroicons/react/24/solid'; +import { useState, useRef, useEffect } from 'react'; +import { CheckCircleIcon, InformationCircleIcon, ExclamationTriangleIcon } from '@heroicons/react/24/solid'; import { MachineStatus } from '../types/machine'; +import { getErrorDetails } from '../utils/errorCodeHelpers'; interface WorkflowStepperProps { machineStatus: MachineStatus; isConnected: boolean; hasPattern: boolean; patternUploaded: boolean; + hasError?: boolean; + errorMessage?: string; + errorCode?: number; } interface Step { @@ -24,6 +29,183 @@ const steps: Step[] = [ { id: 7, label: 'Complete', description: 'Finish and remove' }, ]; +// Helper function to get guide content for a step +function getGuideContent( + stepId: number, + machineStatus: MachineStatus, + hasError: boolean, + errorCode?: number, + errorMessage?: string +) { + // Check for errors first + if (hasError) { + const errorDetails = getErrorDetails(errorCode); + + if (errorDetails?.isInformational) { + return { + type: 'info' as const, + title: errorDetails.title, + description: errorDetails.description, + items: errorDetails.solutions || [] + }; + } + + return { + type: 'error' as const, + title: errorDetails?.title || 'Error Occurred', + description: errorDetails?.description || errorMessage || 'An error occurred. Please check the machine and try again.', + items: errorDetails?.solutions || [], + errorCode + }; + } + + // Return content based on step + switch (stepId) { + case 1: + return { + type: 'info' as const, + title: 'Step 1: Connect to Machine', + description: 'To get started, connect to your Brother embroidery machine via Bluetooth.', + items: [ + 'Make sure your machine is powered on', + 'Enable Bluetooth on your machine', + 'Click the "Connect to Machine" button below' + ] + }; + + case 2: + return { + type: 'info' as const, + title: 'Step 2: Load Your Pattern', + description: 'Choose a PES embroidery file from your computer to preview and upload.', + items: [ + 'Click "Choose PES File" in the Pattern File section', + 'Select your embroidery design (.pes file)', + 'Review the pattern preview on the right', + 'You can drag the pattern to adjust its position' + ] + }; + + case 3: + return { + type: 'info' as const, + title: 'Step 3: Upload Pattern to Machine', + description: 'Send your pattern to the embroidery machine to prepare for sewing.', + items: [ + 'Review the pattern preview to ensure it\'s positioned correctly', + 'Check the pattern size matches your hoop', + 'Click "Upload to Machine" when ready', + 'Wait for the upload to complete (this may take a minute)' + ] + }; + + case 4: + // Check machine status for substates + if (machineStatus === MachineStatus.MASK_TRACE_LOCK_WAIT) { + return { + type: 'warning' as const, + title: 'Machine Action Required', + description: 'The machine is ready to trace the pattern outline.', + items: [ + 'Press the button on your machine to confirm and start the mask trace', + 'Ensure the hoop is properly attached', + 'Make sure the needle area is clear' + ] + }; + } + if (machineStatus === MachineStatus.MASK_TRACING) { + return { + type: 'progress' as const, + title: 'Mask Trace In Progress', + description: 'The machine is tracing the pattern boundary. Please wait...', + items: [ + 'Watch the machine trace the outline', + 'Verify the pattern fits within your hoop', + 'Do not interrupt the machine' + ] + }; + } + return { + type: 'info' as const, + title: 'Step 4: Start Mask Trace', + description: 'The mask trace helps the machine understand the pattern boundaries.', + items: [ + 'Click "Start Mask Trace" button in the Sewing Progress section', + 'The machine will trace the pattern outline', + 'This ensures the hoop is positioned correctly' + ] + }; + + case 5: + return { + type: 'success' as const, + title: 'Step 5: Ready to Sew!', + description: 'The machine is ready to begin embroidering your pattern.', + items: [ + 'Verify your thread colors are correct', + 'Ensure the fabric is properly hooped', + 'Click "Start Sewing" when ready' + ] + }; + + case 6: + // Check for substates + if (machineStatus === MachineStatus.COLOR_CHANGE_WAIT) { + return { + type: 'warning' as const, + title: 'Thread Change Required', + description: 'The machine needs a different thread color to continue.', + items: [ + 'Check the color blocks section to see which thread is needed', + 'Change to the correct thread color', + 'Press the button on your machine to resume sewing' + ] + }; + } + if (machineStatus === MachineStatus.PAUSE || + machineStatus === MachineStatus.STOP || + machineStatus === MachineStatus.SEWING_INTERRUPTION) { + return { + type: 'warning' as const, + title: 'Sewing Paused', + description: 'The embroidery has been paused or interrupted.', + items: [ + 'Check if everything is okay with the machine', + 'Click "Resume Sewing" when ready to continue', + 'The machine will pick up where it left off' + ] + }; + } + return { + type: 'progress' as const, + title: 'Step 6: Sewing In Progress', + description: 'Your embroidery is being stitched. Monitor the progress below.', + items: [ + 'Watch the progress bar and current stitch count', + 'The machine will pause when a color change is needed', + 'Do not leave the machine unattended' + ] + }; + + case 7: + return { + type: 'success' as const, + title: 'Step 7: Embroidery Complete!', + description: 'Your embroidery is finished. Great work!', + items: [ + 'Remove the hoop from the machine', + 'Press the Accept button on the machine', + 'Carefully remove your finished embroidery', + 'Trim any jump stitches or loose threads', + 'Click "Delete Pattern" to start a new project' + ] + }; + + default: + return null; + } +} + function getCurrentStep(machineStatus: MachineStatus, isConnected: boolean, hasPattern: boolean, patternUploaded: boolean): number { if (!isConnected) return 1; if (!hasPattern) return 2; @@ -55,8 +237,53 @@ function getCurrentStep(machineStatus: MachineStatus, isConnected: boolean, hasP } } -export function WorkflowStepper({ machineStatus, isConnected, hasPattern, patternUploaded }: WorkflowStepperProps) { +export function WorkflowStepper({ + machineStatus, + isConnected, + hasPattern, + patternUploaded, + hasError = false, + errorMessage, + errorCode +}: WorkflowStepperProps) { const currentStep = getCurrentStep(machineStatus, isConnected, hasPattern, patternUploaded); + const [showPopover, setShowPopover] = useState(false); + const [popoverStep, setPopoverStep] = useState(null); + const popoverRef = useRef(null); + const stepRefs = useRef<{ [key: number]: HTMLDivElement | null }>({}); + + // Close popover when clicking outside + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if (popoverRef.current && !popoverRef.current.contains(event.target as Node)) { + // Check if click was on a step circle + const clickedStep = Object.values(stepRefs.current).find(ref => + ref?.contains(event.target as Node) + ); + if (!clickedStep) { + setShowPopover(false); + } + } + }; + + if (showPopover) { + document.addEventListener('mousedown', handleClickOutside); + return () => document.removeEventListener('mousedown', handleClickOutside); + } + }, [showPopover]); + + const handleStepClick = (stepId: number) => { + // Only allow clicking on current step or earlier completed steps + if (stepId <= currentStep) { + if (showPopover && popoverStep === stepId) { + setShowPopover(false); + setPopoverStep(null); + } else { + setPopoverStep(stepId); + setShowPopover(true); + } + } + }; return (
@@ -94,13 +321,19 @@ export function WorkflowStepper({ machineStatus, isConnected, hasPattern, patter > {/* Step circle */}
{ stepRefs.current[step.id] = el; }} + onClick={() => handleStepClick(step.id)} className={` w-8 h-8 lg:w-10 lg:h-10 rounded-full flex items-center justify-center font-bold text-xs transition-all duration-300 border-2 shadow-md + ${step.id <= currentStep ? 'cursor-pointer hover:scale-110' : 'cursor-not-allowed'} ${isComplete ? 'bg-green-500 dark:bg-green-600 border-green-400 dark:border-green-500 text-white shadow-green-500/30 dark:shadow-green-600/30' : ''} ${isCurrent ? 'bg-blue-600 dark:bg-blue-700 border-blue-500 dark:border-blue-600 text-white scale-105 lg:scale-110 shadow-blue-600/40 dark:shadow-blue-700/40 ring-2 ring-blue-300 dark:ring-blue-500 ring-offset-2 dark:ring-offset-gray-900' : ''} ${isUpcoming ? 'bg-blue-700 dark:bg-blue-800 border-blue-500/30 dark:border-blue-600/30 text-blue-200/70 dark:text-blue-300/70' : ''} + ${showPopover && popoverStep === step.id ? 'ring-4 ring-white dark:ring-gray-800' : ''} `} - aria-label={`${step.label}: ${isComplete ? 'completed' : isCurrent ? 'current' : 'upcoming'}`} + aria-label={`${step.label}: ${isComplete ? 'completed' : isCurrent ? 'current' : 'upcoming'}. Click for details.`} + role="button" + tabIndex={step.id <= currentStep ? 0 : -1} > {isComplete ? (
+ + {/* Popover */} + {showPopover && popoverStep !== null && stepRefs.current[popoverStep] && ( +
+ {(() => { + const content = getGuideContent(popoverStep, machineStatus, hasError, errorCode, errorMessage); + if (!content) return null; + + const colorClasses = { + info: 'bg-blue-50 dark:bg-blue-900/95 border-blue-600 dark:border-blue-500', + success: 'bg-green-50 dark:bg-green-900/95 border-green-600 dark:border-green-500', + warning: 'bg-yellow-50 dark:bg-yellow-900/95 border-yellow-600 dark:border-yellow-500', + error: 'bg-red-50 dark:bg-red-900/95 border-red-600 dark:border-red-500', + progress: 'bg-cyan-50 dark:bg-cyan-900/95 border-cyan-600 dark:border-cyan-500' + }; + + const iconColorClasses = { + info: 'text-blue-600 dark:text-blue-400', + success: 'text-green-600 dark:text-green-400', + warning: 'text-yellow-600 dark:text-yellow-400', + error: 'text-red-600 dark:text-red-400', + progress: 'text-cyan-600 dark:text-cyan-400' + }; + + const textColorClasses = { + info: 'text-blue-900 dark:text-blue-200', + success: 'text-green-900 dark:text-green-200', + warning: 'text-yellow-900 dark:text-yellow-200', + error: 'text-red-900 dark:text-red-200', + progress: 'text-cyan-900 dark:text-cyan-200' + }; + + const descColorClasses = { + info: 'text-blue-800 dark:text-blue-300', + success: 'text-green-800 dark:text-green-300', + warning: 'text-yellow-800 dark:text-yellow-300', + error: 'text-red-800 dark:text-red-300', + progress: 'text-cyan-800 dark:text-cyan-300' + }; + + const listColorClasses = { + info: 'text-blue-700 dark:text-blue-300', + success: 'text-green-700 dark:text-green-300', + warning: 'text-yellow-700 dark:text-yellow-300', + error: 'text-red-700 dark:text-red-300', + progress: 'text-cyan-700 dark:text-cyan-300' + }; + + const Icon = content.type === 'error' ? ExclamationTriangleIcon : InformationCircleIcon; + + return ( +
+
+ +
+

+ {content.title} +

+

+ {content.description} +

+ {content.items && content.items.length > 0 && ( +
    + {content.items.map((item, index) => ( +
  • $1') }} /> + ))} +
+ )} + {content.type === 'error' && content.errorCode !== undefined && ( +

+ Error Code: 0x{content.errorCode.toString(16).toUpperCase().padStart(2, '0')} +

+ )} +
+
+
+ ); + })()} +
+ )}
); } diff --git a/src/hooks/useBrotherMachine.ts b/src/hooks/useBrotherMachine.ts index b3e5ad2..096f465 100644 --- a/src/hooks/useBrotherMachine.ts +++ b/src/hooks/useBrotherMachine.ts @@ -152,6 +152,7 @@ export function useBrotherMachine() { setPatternInfo(null); setSewingProgress(null); setError(null); + setMachineError(SewingMachineError.None); } catch (err) { setError(err instanceof Error ? err.message : "Failed to disconnect"); }