mirror of
https://github.com/jhbruhn/respira.git
synced 2026-01-27 10:23:41 +00:00
Move error display to header with clickable popover
- Replace content-pushing error banners with header status indicator - Add categorized error labels (Python Error, Connection Error, etc.) - Show detailed error info with solutions in 600px popover on click - Fix layout shift by always rendering error button (invisible when no error) - Clear machineError state on disconnect - Fix TypeScript error in WorkflowStepper ref callback 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
bf3e397ddb
commit
efc712995b
4 changed files with 467 additions and 419 deletions
207
src/App.tsx
207
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 { useBrotherMachine } from './hooks/useBrotherMachine';
|
||||||
import { FileUpload } from './components/FileUpload';
|
import { FileUpload } from './components/FileUpload';
|
||||||
import { PatternCanvas } from './components/PatternCanvas';
|
import { PatternCanvas } from './components/PatternCanvas';
|
||||||
import { ProgressMonitor } from './components/ProgressMonitor';
|
import { ProgressMonitor } from './components/ProgressMonitor';
|
||||||
import { WorkflowStepper } from './components/WorkflowStepper';
|
import { WorkflowStepper } from './components/WorkflowStepper';
|
||||||
import { NextStepGuide } from './components/NextStepGuide';
|
|
||||||
import { PatternSummaryCard } from './components/PatternSummaryCard';
|
import { PatternSummaryCard } from './components/PatternSummaryCard';
|
||||||
import { BluetoothDevicePicker } from './components/BluetoothDevicePicker';
|
import { BluetoothDevicePicker } from './components/BluetoothDevicePicker';
|
||||||
import type { PesPatternData } from './utils/pystitchConverter';
|
import type { PesPatternData } from './utils/pystitchConverter';
|
||||||
import { pyodideLoader } from './utils/pyodideLoader';
|
import { pyodideLoader } from './utils/pyodideLoader';
|
||||||
import { hasError } from './utils/errorCodeHelpers';
|
import { hasError, getErrorDetails } from './utils/errorCodeHelpers';
|
||||||
import { canDeletePattern, getStateVisualInfo } from './utils/machineStateHelpers';
|
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';
|
import './App.css';
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
|
|
@ -22,6 +21,9 @@ function App() {
|
||||||
const [patternOffset, setPatternOffset] = useState<{ x: number; y: number }>({ x: 0, y: 0 });
|
const [patternOffset, setPatternOffset] = useState<{ x: number; y: number }>({ x: 0, y: 0 });
|
||||||
const [patternUploaded, setPatternUploaded] = useState(false);
|
const [patternUploaded, setPatternUploaded] = useState(false);
|
||||||
const [currentFileName, setCurrentFileName] = useState<string>(''); // Track current pattern filename
|
const [currentFileName, setCurrentFileName] = useState<string>(''); // Track current pattern filename
|
||||||
|
const [showErrorPopover, setShowErrorPopover] = useState(false);
|
||||||
|
const errorPopoverRef = useRef<HTMLDivElement>(null);
|
||||||
|
const errorButtonRef = useRef<HTMLButtonElement>(null);
|
||||||
|
|
||||||
// Initialize Pyodide on mount
|
// Initialize Pyodide on mount
|
||||||
useEffect(() => {
|
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
|
// Auto-load cached pattern when available
|
||||||
const resumedPattern = machine.resumedPattern;
|
const resumedPattern = machine.resumedPattern;
|
||||||
const resumeFileName = machine.resumeFileName;
|
const resumeFileName = machine.resumeFileName;
|
||||||
|
|
@ -142,7 +163,7 @@ function App() {
|
||||||
<ArrowPathIcon className="w-3.5 h-3.5 text-blue-200 animate-spin" title="Auto-refreshing status" />
|
<ArrowPathIcon className="w-3.5 h-3.5 text-blue-200 animate-spin" title="Auto-refreshing status" />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2 mt-1">
|
<div className="flex items-center gap-2 mt-1 min-h-[32px]">
|
||||||
{machine.isConnected ? (
|
{machine.isConnected ? (
|
||||||
<>
|
<>
|
||||||
<button
|
<button
|
||||||
|
|
@ -162,6 +183,122 @@ function App() {
|
||||||
) : (
|
) : (
|
||||||
<p className="text-xs text-blue-200">Not Connected</p>
|
<p className="text-xs text-blue-200">Not Connected</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Error indicator - always render to prevent layout shift */}
|
||||||
|
<div className="relative">
|
||||||
|
<button
|
||||||
|
ref={errorButtonRef}
|
||||||
|
onClick={() => setShowErrorPopover(!showErrorPopover)}
|
||||||
|
className={`inline-flex items-center gap-1.5 px-2.5 py-1.5 sm:py-1 rounded text-sm font-medium bg-red-500/90 hover:bg-red-600 text-white border border-red-400 transition-all flex-shrink-0 ${
|
||||||
|
(machine.error || pyodideError)
|
||||||
|
? 'cursor-pointer animate-pulse hover:animate-none'
|
||||||
|
: 'invisible pointer-events-none'
|
||||||
|
}`}
|
||||||
|
title="Click to view error details"
|
||||||
|
aria-label="View error details"
|
||||||
|
disabled={!(machine.error || pyodideError)}
|
||||||
|
>
|
||||||
|
<ExclamationTriangleIcon className="w-3.5 h-3.5 flex-shrink-0" />
|
||||||
|
<span>
|
||||||
|
{(() => {
|
||||||
|
if (pyodideError) return 'Python Error';
|
||||||
|
if (machine.isPairingError) return 'Pairing Required';
|
||||||
|
|
||||||
|
const errorMsg = machine.error || '';
|
||||||
|
|
||||||
|
// Categorize by error message content
|
||||||
|
if (errorMsg.toLowerCase().includes('bluetooth') || errorMsg.toLowerCase().includes('connection')) {
|
||||||
|
return 'Connection Error';
|
||||||
|
}
|
||||||
|
if (errorMsg.toLowerCase().includes('upload')) {
|
||||||
|
return 'Upload Error';
|
||||||
|
}
|
||||||
|
if (errorMsg.toLowerCase().includes('pattern')) {
|
||||||
|
return 'Pattern Error';
|
||||||
|
}
|
||||||
|
if (machine.machineError !== undefined) {
|
||||||
|
return `Machine Error`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default fallback
|
||||||
|
return 'Error';
|
||||||
|
})()}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Error popover */}
|
||||||
|
{showErrorPopover && (machine.error || pyodideError) && (
|
||||||
|
<div
|
||||||
|
ref={errorPopoverRef}
|
||||||
|
className="absolute top-full mt-2 left-0 w-[600px] z-50 animate-fadeIn"
|
||||||
|
role="dialog"
|
||||||
|
aria-label="Error details"
|
||||||
|
>
|
||||||
|
{(() => {
|
||||||
|
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 (
|
||||||
|
<div className={`${bgColor} border-l-4 p-4 rounded-lg shadow-xl backdrop-blur-sm`}>
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<Icon className={`w-6 h-6 ${iconColor} flex-shrink-0 mt-0.5`} />
|
||||||
|
<div className="flex-1">
|
||||||
|
<h3 className={`text-base font-semibold ${textColor} mb-2`}>
|
||||||
|
{title}
|
||||||
|
</h3>
|
||||||
|
<p className={`text-sm ${descColor} mb-3`}>
|
||||||
|
{errorDetails?.description || errorMsg}
|
||||||
|
</p>
|
||||||
|
{errorDetails?.solutions && errorDetails.solutions.length > 0 && (
|
||||||
|
<>
|
||||||
|
<h4 className={`text-sm font-semibold ${textColor} mb-2`}>
|
||||||
|
{isInfo ? 'Steps:' : 'How to Fix:'}
|
||||||
|
</h4>
|
||||||
|
<ol className={`list-decimal list-inside text-sm ${listColor} space-y-1.5`}>
|
||||||
|
{errorDetails.solutions.map((solution, index) => (
|
||||||
|
<li key={index} className="pl-2">{solution}</li>
|
||||||
|
))}
|
||||||
|
</ol>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{machine.machineError !== undefined && !errorDetails?.isInformational && (
|
||||||
|
<p className={`text-xs ${descColor} mt-3 font-mono`}>
|
||||||
|
Error Code: 0x{machine.machineError.toString(16).toUpperCase().padStart(2, '0')}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})()}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -173,58 +310,15 @@ function App() {
|
||||||
isConnected={machine.isConnected}
|
isConnected={machine.isConnected}
|
||||||
hasPattern={pesData !== null}
|
hasPattern={pesData !== null}
|
||||||
patternUploaded={patternUploaded}
|
patternUploaded={patternUploaded}
|
||||||
|
hasError={hasError(machine.machineError)}
|
||||||
|
errorMessage={machine.error || undefined}
|
||||||
|
errorCode={machine.machineError}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div className="flex-1 p-4 sm:p-5 lg:p-6 w-full overflow-y-auto lg:overflow-hidden flex flex-col">
|
<div className="flex-1 p-4 sm:p-5 lg:p-6 w-full overflow-y-auto lg:overflow-hidden flex flex-col">
|
||||||
{/* Global errors */}
|
|
||||||
{machine.error && (
|
|
||||||
<div className={`px-6 py-4 rounded-lg border-l-4 mb-6 shadow-md hover:shadow-lg transition-shadow animate-fadeIn ${
|
|
||||||
machine.isPairingError
|
|
||||||
? 'bg-blue-50 dark:bg-blue-900/20 text-blue-900 dark:text-blue-200 border-blue-600 dark:border-blue-500'
|
|
||||||
: 'bg-red-50 dark:bg-red-900/20 text-red-900 dark:text-red-200 border-red-600 dark:border-red-500'
|
|
||||||
}`}>
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<svg className={`w-5 h-5 flex-shrink-0 ${machine.isPairingError ? 'text-blue-600 dark:text-blue-400' : 'text-red-600 dark:text-red-400'}`} fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
||||||
{machine.isPairingError ? (
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
||||||
) : (
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
|
|
||||||
)}
|
|
||||||
</svg>
|
|
||||||
<div className="flex-1">
|
|
||||||
<div className="font-semibold mb-1">{machine.isPairingError ? 'Pairing Required' : 'Error'}</div>
|
|
||||||
<div className="text-sm">{machine.error}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{pyodideError && (
|
|
||||||
<div className="bg-red-50 dark:bg-red-900/20 text-red-900 dark:text-red-200 px-6 py-4 rounded-lg border-l-4 border-red-600 dark:border-red-500 mb-6 shadow-md hover:shadow-lg transition-shadow animate-fadeIn">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<svg className="w-5 h-5 text-red-600 dark:text-red-400 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
||||||
</svg>
|
|
||||||
<div>
|
|
||||||
<strong className="font-semibold">Python Error:</strong> {pyodideError}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{!pyodideReady && !pyodideError && (
|
|
||||||
<div className="bg-blue-50 dark:bg-blue-900/20 text-blue-900 dark:text-blue-200 px-6 py-4 rounded-lg border-l-4 border-blue-600 dark:border-blue-500 mb-6 shadow-md animate-fadeIn">
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<svg className="w-5 h-5 animate-spin text-blue-600 dark:text-blue-400" fill="none" viewBox="0 0 24 24">
|
|
||||||
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
|
|
||||||
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
|
||||||
</svg>
|
|
||||||
<span className="font-medium">Initializing Python environment...</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="flex-1 grid grid-cols-1 lg:grid-cols-[400px_1fr] gap-4 md:gap-5 lg:gap-6 lg:overflow-hidden">
|
<div className="flex-1 grid grid-cols-1 lg:grid-cols-[400px_1fr] gap-4 md:gap-5 lg:gap-6 lg:overflow-hidden">
|
||||||
{/* Left Column - Controls */}
|
{/* Left Column - Controls */}
|
||||||
<div className="flex flex-col gap-4 md:gap-5 lg:gap-6 lg:overflow-hidden">
|
<div className="flex flex-col gap-4 md:gap-5 lg:gap-6 lg:overflow-hidden">
|
||||||
|
|
@ -362,17 +456,6 @@ function App() {
|
||||||
{/* Bluetooth Device Picker (Electron only) */}
|
{/* Bluetooth Device Picker (Electron only) */}
|
||||||
<BluetoothDevicePicker />
|
<BluetoothDevicePicker />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Next Step Guide - Fixed floating overlay */}
|
|
||||||
<NextStepGuide
|
|
||||||
machineStatus={machine.machineStatus}
|
|
||||||
isConnected={machine.isConnected}
|
|
||||||
hasPattern={pesData !== null}
|
|
||||||
patternUploaded={patternUploaded}
|
|
||||||
hasError={hasError(machine.machineError)}
|
|
||||||
errorMessage={machine.error || undefined}
|
|
||||||
errorCode={machine.machineError}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 (
|
|
||||||
<div className="bg-blue-50 dark:bg-blue-900/95 border-l-4 border-blue-600 dark:border-blue-500 p-4 rounded-lg shadow-lg backdrop-blur-sm">
|
|
||||||
<div className="flex items-start gap-3">
|
|
||||||
<InformationCircleIcon className="w-6 h-6 text-blue-600 dark:text-blue-400 flex-shrink-0 mt-0.5" />
|
|
||||||
<div className="flex-1">
|
|
||||||
<h3 className="text-base font-semibold text-blue-900 dark:text-blue-200 mb-2">
|
|
||||||
{errorDetails.title}
|
|
||||||
</h3>
|
|
||||||
<p className="text-sm text-blue-800 dark:text-blue-300 mb-3">
|
|
||||||
{errorDetails.description}
|
|
||||||
</p>
|
|
||||||
{errorDetails.solutions && errorDetails.solutions.length > 0 && (
|
|
||||||
<>
|
|
||||||
<h4 className="text-sm font-semibold text-blue-900 dark:text-blue-200 mb-2">Steps:</h4>
|
|
||||||
<ol className="list-decimal list-inside text-sm text-blue-700 dark:text-blue-300 space-y-1.5">
|
|
||||||
{errorDetails.solutions.map((solution, index) => (
|
|
||||||
<li key={index} className="pl-2">{solution}</li>
|
|
||||||
))}
|
|
||||||
</ol>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Regular error display for actual errors
|
|
||||||
return (
|
|
||||||
<div className="bg-red-50 dark:bg-red-900/95 border-l-4 border-red-600 dark:border-red-500 p-4 rounded-lg shadow-lg backdrop-blur-sm">
|
|
||||||
<div className="flex items-start gap-3">
|
|
||||||
<ExclamationTriangleIcon className="w-6 h-6 text-red-600 dark:text-red-400 flex-shrink-0 mt-0.5" />
|
|
||||||
<div className="flex-1">
|
|
||||||
<h3 className="text-base font-semibold text-red-900 dark:text-red-200 mb-2">
|
|
||||||
{errorDetails?.title || 'Error Occurred'}
|
|
||||||
</h3>
|
|
||||||
<p className="text-sm text-red-800 dark:text-red-300 mb-3">
|
|
||||||
{errorDetails?.description || errorMessage || 'An error occurred. Please check the machine and try again.'}
|
|
||||||
</p>
|
|
||||||
{errorDetails?.solutions && errorDetails.solutions.length > 0 && (
|
|
||||||
<>
|
|
||||||
<h4 className="text-sm font-semibold text-red-900 dark:text-red-200 mb-2">How to Fix:</h4>
|
|
||||||
<ol className="list-decimal list-inside text-sm text-red-700 dark:text-red-300 space-y-1.5">
|
|
||||||
{errorDetails.solutions.map((solution, index) => (
|
|
||||||
<li key={index} className="pl-2">{solution}</li>
|
|
||||||
))}
|
|
||||||
</ol>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{errorCode !== undefined && (
|
|
||||||
<p className="text-xs text-red-600 dark:text-red-400 mt-3 font-mono">
|
|
||||||
Error Code: 0x{errorCode.toString(16).toUpperCase().padStart(2, '0')}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Determine what to show based on current state
|
|
||||||
if (!isConnected) {
|
|
||||||
return (
|
|
||||||
<div className="bg-blue-50 dark:bg-blue-900/95 border-l-4 border-blue-600 dark:border-blue-500 p-4 rounded-lg shadow-lg backdrop-blur-sm">
|
|
||||||
<div className="flex items-start gap-3">
|
|
||||||
<InformationCircleIcon className="w-6 h-6 text-blue-600 dark:text-blue-400 flex-shrink-0 mt-0.5" />
|
|
||||||
<div className="flex-1">
|
|
||||||
<h3 className="text-base font-semibold text-blue-900 dark:text-blue-200 mb-2">Step 1: Connect to Machine</h3>
|
|
||||||
<p className="text-sm text-blue-800 dark:text-blue-300 mb-3">To get started, connect to your Brother embroidery machine via Bluetooth.</p>
|
|
||||||
<ul className="list-disc list-inside text-sm text-blue-700 dark:text-blue-300 space-y-1">
|
|
||||||
<li>Make sure your machine is powered on</li>
|
|
||||||
<li>Enable Bluetooth on your machine</li>
|
|
||||||
<li>Click the "Connect to Machine" button below</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!hasPattern) {
|
|
||||||
return (
|
|
||||||
<div className="bg-blue-50 dark:bg-blue-900/95 border-l-4 border-blue-600 dark:border-blue-500 p-4 rounded-lg shadow-lg backdrop-blur-sm">
|
|
||||||
<div className="flex items-start gap-3">
|
|
||||||
<InformationCircleIcon className="w-6 h-6 text-blue-600 dark:text-blue-400 flex-shrink-0 mt-0.5" />
|
|
||||||
<div className="flex-1">
|
|
||||||
<h3 className="text-base font-semibold text-blue-900 dark:text-blue-200 mb-2">Step 2: Load Your Pattern</h3>
|
|
||||||
<p className="text-sm text-blue-800 dark:text-blue-300 mb-3">Choose a PES embroidery file from your computer to preview and upload.</p>
|
|
||||||
<ul className="list-disc list-inside text-sm text-blue-700 dark:text-blue-300 space-y-1">
|
|
||||||
<li>Click "Choose PES File" in the Pattern File section</li>
|
|
||||||
<li>Select your embroidery design (.pes file)</li>
|
|
||||||
<li>Review the pattern preview on the right</li>
|
|
||||||
<li>You can drag the pattern to adjust its position</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!patternUploaded) {
|
|
||||||
return (
|
|
||||||
<div className="bg-blue-50 dark:bg-blue-900/95 border-l-4 border-blue-600 dark:border-blue-500 p-4 rounded-lg shadow-lg backdrop-blur-sm">
|
|
||||||
<div className="flex items-start gap-3">
|
|
||||||
<InformationCircleIcon className="w-6 h-6 text-blue-600 dark:text-blue-400 flex-shrink-0 mt-0.5" />
|
|
||||||
<div className="flex-1">
|
|
||||||
<h3 className="text-base font-semibold text-blue-900 dark:text-blue-200 mb-2">Step 3: Upload Pattern to Machine</h3>
|
|
||||||
<p className="text-sm text-blue-800 dark:text-blue-300 mb-3">Send your pattern to the embroidery machine to prepare for sewing.</p>
|
|
||||||
<ul className="list-disc list-inside text-sm text-blue-700 dark:text-blue-300 space-y-1">
|
|
||||||
<li>Review the pattern preview to ensure it's positioned correctly</li>
|
|
||||||
<li>Check the pattern size matches your hoop</li>
|
|
||||||
<li>Click "Upload to Machine" when ready</li>
|
|
||||||
<li>Wait for the upload to complete (this may take a minute)</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pattern is uploaded, guide based on machine status
|
|
||||||
switch (machineStatus) {
|
|
||||||
case MachineStatus.IDLE:
|
|
||||||
return (
|
|
||||||
<div className="bg-blue-50 dark:bg-blue-900/95 border-l-4 border-blue-600 dark:border-blue-500 p-4 rounded-lg shadow-lg backdrop-blur-sm">
|
|
||||||
<div className="flex items-start gap-3">
|
|
||||||
<InformationCircleIcon className="w-6 h-6 text-blue-600 dark:text-blue-400 flex-shrink-0 mt-0.5" />
|
|
||||||
<div className="flex-1">
|
|
||||||
<h3 className="text-base font-semibold text-blue-900 dark:text-blue-200 mb-2">Step 4: Start Mask Trace</h3>
|
|
||||||
<p className="text-sm text-blue-800 dark:text-blue-300 mb-3">The mask trace helps the machine understand the pattern boundaries.</p>
|
|
||||||
<ul className="list-disc list-inside text-sm text-blue-700 dark:text-blue-300 space-y-1">
|
|
||||||
<li>Click "Start Mask Trace" button in the Sewing Progress section</li>
|
|
||||||
<li>The machine will trace the pattern outline</li>
|
|
||||||
<li>This ensures the hoop is positioned correctly</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
case MachineStatus.MASK_TRACE_LOCK_WAIT:
|
|
||||||
return (
|
|
||||||
<div className="bg-yellow-50 dark:bg-yellow-900/95 border-l-4 border-yellow-600 dark:border-yellow-500 p-4 rounded-lg shadow-lg backdrop-blur-sm">
|
|
||||||
<div className="flex items-start gap-3">
|
|
||||||
<InformationCircleIcon className="w-6 h-6 text-yellow-600 dark:text-yellow-400 flex-shrink-0 mt-0.5" />
|
|
||||||
<div className="flex-1">
|
|
||||||
<h3 className="text-base font-semibold text-yellow-900 dark:text-yellow-200 mb-2">Machine Action Required</h3>
|
|
||||||
<p className="text-sm text-yellow-800 dark:text-yellow-300 mb-3">The machine is ready to trace the pattern outline.</p>
|
|
||||||
<ul className="list-disc list-inside text-sm text-yellow-700 dark:text-yellow-300 space-y-1">
|
|
||||||
<li><strong>Press the button on your machine</strong> to confirm and start the mask trace</li>
|
|
||||||
<li>Ensure the hoop is properly attached</li>
|
|
||||||
<li>Make sure the needle area is clear</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
case MachineStatus.MASK_TRACING:
|
|
||||||
return (
|
|
||||||
<div className="bg-cyan-50 dark:bg-cyan-900/95 border-l-4 border-cyan-600 dark:border-cyan-500 p-4 rounded-lg shadow-lg backdrop-blur-sm">
|
|
||||||
<div className="flex items-start gap-3">
|
|
||||||
<InformationCircleIcon className="w-6 h-6 text-cyan-600 dark:text-cyan-400 flex-shrink-0 mt-0.5" />
|
|
||||||
<div className="flex-1">
|
|
||||||
<h3 className="text-base font-semibold text-cyan-900 dark:text-cyan-200 mb-2">Mask Trace In Progress</h3>
|
|
||||||
<p className="text-sm text-cyan-800 dark:text-cyan-300 mb-3">The machine is tracing the pattern boundary. Please wait...</p>
|
|
||||||
<ul className="list-disc list-inside text-sm text-cyan-700 dark:text-cyan-300 space-y-1">
|
|
||||||
<li>Watch the machine trace the outline</li>
|
|
||||||
<li>Verify the pattern fits within your hoop</li>
|
|
||||||
<li>Do not interrupt the machine</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
case MachineStatus.MASK_TRACE_COMPLETE:
|
|
||||||
case MachineStatus.SEWING_WAIT:
|
|
||||||
return (
|
|
||||||
<div className="bg-green-50 dark:bg-green-900/95 border-l-4 border-green-600 dark:border-green-500 p-4 rounded-lg shadow-lg backdrop-blur-sm">
|
|
||||||
<div className="flex items-start gap-3">
|
|
||||||
<InformationCircleIcon className="w-6 h-6 text-green-600 dark:text-green-400 flex-shrink-0 mt-0.5" />
|
|
||||||
<div className="flex-1">
|
|
||||||
<h3 className="text-base font-semibold text-green-900 dark:text-green-200 mb-2">Step 5: Ready to Sew!</h3>
|
|
||||||
<p className="text-sm text-green-800 dark:text-green-300 mb-3">The machine is ready to begin embroidering your pattern.</p>
|
|
||||||
<ul className="list-disc list-inside text-sm text-green-700 dark:text-green-300 space-y-1">
|
|
||||||
<li>Verify your thread colors are correct</li>
|
|
||||||
<li>Ensure the fabric is properly hooped</li>
|
|
||||||
<li>Click "Start Sewing" when ready</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
case MachineStatus.SEWING:
|
|
||||||
return (
|
|
||||||
<div className="bg-cyan-50 dark:bg-cyan-900/95 border-l-4 border-cyan-600 dark:border-cyan-500 p-4 rounded-lg shadow-lg backdrop-blur-sm">
|
|
||||||
<div className="flex items-start gap-3">
|
|
||||||
<InformationCircleIcon className="w-6 h-6 text-cyan-600 dark:text-cyan-400 flex-shrink-0 mt-0.5" />
|
|
||||||
<div className="flex-1">
|
|
||||||
<h3 className="text-base font-semibold text-cyan-900 dark:text-cyan-200 mb-2">Step 6: Sewing In Progress</h3>
|
|
||||||
<p className="text-sm text-cyan-800 dark:text-cyan-300 mb-3">Your embroidery is being stitched. Monitor the progress below.</p>
|
|
||||||
<ul className="list-disc list-inside text-sm text-cyan-700 dark:text-cyan-300 space-y-1">
|
|
||||||
<li>Watch the progress bar and current stitch count</li>
|
|
||||||
<li>The machine will pause when a color change is needed</li>
|
|
||||||
<li>Do not leave the machine unattended</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
case MachineStatus.COLOR_CHANGE_WAIT:
|
|
||||||
return (
|
|
||||||
<div className="bg-yellow-50 dark:bg-yellow-900/95 border-l-4 border-yellow-600 dark:border-yellow-500 p-4 rounded-lg shadow-lg backdrop-blur-sm">
|
|
||||||
<div className="flex items-start gap-3">
|
|
||||||
<InformationCircleIcon className="w-6 h-6 text-yellow-600 dark:text-yellow-400 flex-shrink-0 mt-0.5" />
|
|
||||||
<div className="flex-1">
|
|
||||||
<h3 className="text-base font-semibold text-yellow-900 dark:text-yellow-200 mb-2">Thread Change Required</h3>
|
|
||||||
<p className="text-sm text-yellow-800 dark:text-yellow-300 mb-3">The machine needs a different thread color to continue.</p>
|
|
||||||
<ul className="list-disc list-inside text-sm text-yellow-700 dark:text-yellow-300 space-y-1">
|
|
||||||
<li>Check the color blocks section to see which thread is needed</li>
|
|
||||||
<li>Change to the correct thread color</li>
|
|
||||||
<li><strong>Press the button on your machine</strong> to resume sewing</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
case MachineStatus.PAUSE:
|
|
||||||
case MachineStatus.STOP:
|
|
||||||
case MachineStatus.SEWING_INTERRUPTION:
|
|
||||||
return (
|
|
||||||
<div className="bg-yellow-50 dark:bg-yellow-900/95 border-l-4 border-yellow-600 dark:border-yellow-500 p-4 rounded-lg shadow-lg backdrop-blur-sm">
|
|
||||||
<div className="flex items-start gap-3">
|
|
||||||
<InformationCircleIcon className="w-6 h-6 text-yellow-600 dark:text-yellow-400 flex-shrink-0 mt-0.5" />
|
|
||||||
<div className="flex-1">
|
|
||||||
<h3 className="text-base font-semibold text-yellow-900 dark:text-yellow-200 mb-2">Sewing Paused</h3>
|
|
||||||
<p className="text-sm text-yellow-800 dark:text-yellow-300 mb-3">The embroidery has been paused or interrupted.</p>
|
|
||||||
<ul className="list-disc list-inside text-sm text-yellow-700 dark:text-yellow-300 space-y-1">
|
|
||||||
<li>Check if everything is okay with the machine</li>
|
|
||||||
<li>Click "Resume Sewing" when ready to continue</li>
|
|
||||||
<li>The machine will pick up where it left off</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
case MachineStatus.SEWING_COMPLETE:
|
|
||||||
return (
|
|
||||||
<div className="bg-green-50 dark:bg-green-900/95 border-l-4 border-green-600 dark:border-green-500 p-4 rounded-lg shadow-lg backdrop-blur-sm">
|
|
||||||
<div className="flex items-start gap-3">
|
|
||||||
<InformationCircleIcon className="w-6 h-6 text-green-600 dark:text-green-400 flex-shrink-0 mt-0.5" />
|
|
||||||
<div className="flex-1">
|
|
||||||
<h3 className="text-base font-semibold text-green-900 dark:text-green-200 mb-2">Step 7: Embroidery Complete!</h3>
|
|
||||||
<p className="text-sm text-green-800 dark:text-green-300 mb-3">Your embroidery is finished. Great work!</p>
|
|
||||||
<ul className="list-disc list-inside text-sm text-green-700 dark:text-green-300 space-y-1">
|
|
||||||
<li>Remove the hoop from the machine</li>
|
|
||||||
<li>Press the Accept button on the machine</li>
|
|
||||||
<li>Carefully remove your finished embroidery</li>
|
|
||||||
<li>Trim any jump stitches or loose threads</li>
|
|
||||||
<li>Click "Delete Pattern" to start a new project</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
default:
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Render floating overlay
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{/* Collapsed state - small info button in bottom-right */}
|
|
||||||
{!isExpanded && (
|
|
||||||
<div className="fixed bottom-4 right-4 z-50 animate-fadeIn">
|
|
||||||
<button
|
|
||||||
onClick={() => setIsExpanded(true)}
|
|
||||||
className="w-12 h-12 bg-blue-600 dark:bg-blue-700 hover:bg-blue-700 dark:hover:bg-blue-600 text-white rounded-full shadow-lg flex items-center justify-center transition-all hover:scale-110 animate-pulse"
|
|
||||||
aria-label="Show next step guide"
|
|
||||||
title="Show next step guide"
|
|
||||||
>
|
|
||||||
<InformationCircleIcon className="w-7 h-7" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Expanded state - floating overlay */}
|
|
||||||
{isExpanded && (
|
|
||||||
<div className="fixed bottom-4 right-4 z-50 w-[90vw] max-w-[400px] max-h-[40vh] overflow-y-auto animate-slideInRight">
|
|
||||||
<div className="relative rounded-lg overflow-hidden shadow-xl">
|
|
||||||
{/* Close button */}
|
|
||||||
<button
|
|
||||||
onClick={() => setIsExpanded(false)}
|
|
||||||
className="absolute top-2 right-2 z-10 w-6 h-6 bg-white/90 dark:bg-gray-800/90 hover:bg-white dark:hover:bg-gray-700 rounded-full flex items-center justify-center transition-colors shadow-md"
|
|
||||||
aria-label="Minimize guide"
|
|
||||||
title="Minimize guide"
|
|
||||||
>
|
|
||||||
<XMarkIcon className="w-4 h-4 text-gray-700 dark:text-gray-300" />
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{/* Content */}
|
|
||||||
{renderContent()}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -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 { MachineStatus } from '../types/machine';
|
||||||
|
import { getErrorDetails } from '../utils/errorCodeHelpers';
|
||||||
|
|
||||||
interface WorkflowStepperProps {
|
interface WorkflowStepperProps {
|
||||||
machineStatus: MachineStatus;
|
machineStatus: MachineStatus;
|
||||||
isConnected: boolean;
|
isConnected: boolean;
|
||||||
hasPattern: boolean;
|
hasPattern: boolean;
|
||||||
patternUploaded: boolean;
|
patternUploaded: boolean;
|
||||||
|
hasError?: boolean;
|
||||||
|
errorMessage?: string;
|
||||||
|
errorCode?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Step {
|
interface Step {
|
||||||
|
|
@ -24,6 +29,183 @@ const steps: Step[] = [
|
||||||
{ id: 7, label: 'Complete', description: 'Finish and remove' },
|
{ 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 {
|
function getCurrentStep(machineStatus: MachineStatus, isConnected: boolean, hasPattern: boolean, patternUploaded: boolean): number {
|
||||||
if (!isConnected) return 1;
|
if (!isConnected) return 1;
|
||||||
if (!hasPattern) return 2;
|
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 currentStep = getCurrentStep(machineStatus, isConnected, hasPattern, patternUploaded);
|
||||||
|
const [showPopover, setShowPopover] = useState(false);
|
||||||
|
const [popoverStep, setPopoverStep] = useState<number | null>(null);
|
||||||
|
const popoverRef = useRef<HTMLDivElement>(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 (
|
return (
|
||||||
<div className="relative max-w-5xl mx-auto mt-2 lg:mt-4" role="navigation" aria-label="Workflow progress">
|
<div className="relative max-w-5xl mx-auto mt-2 lg:mt-4" role="navigation" aria-label="Workflow progress">
|
||||||
|
|
@ -94,13 +321,19 @@ export function WorkflowStepper({ machineStatus, isConnected, hasPattern, patter
|
||||||
>
|
>
|
||||||
{/* Step circle */}
|
{/* Step circle */}
|
||||||
<div
|
<div
|
||||||
|
ref={(el) => { stepRefs.current[step.id] = el; }}
|
||||||
|
onClick={() => handleStepClick(step.id)}
|
||||||
className={`
|
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
|
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' : ''}
|
${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' : ''}
|
${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' : ''}
|
${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 ? (
|
{isComplete ? (
|
||||||
<CheckCircleIcon className="w-5 h-5 lg:w-6 lg:h-6" aria-hidden="true" />
|
<CheckCircleIcon className="w-5 h-5 lg:w-6 lg:h-6" aria-hidden="true" />
|
||||||
|
|
@ -121,6 +354,91 @@ export function WorkflowStepper({ machineStatus, isConnected, hasPattern, patter
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Popover */}
|
||||||
|
{showPopover && popoverStep !== null && stepRefs.current[popoverStep] && (
|
||||||
|
<div
|
||||||
|
ref={popoverRef}
|
||||||
|
className="absolute top-full mt-4 left-1/2 transform -translate-x-1/2 w-full max-w-xl z-50 animate-fadeIn"
|
||||||
|
role="dialog"
|
||||||
|
aria-label="Step guidance"
|
||||||
|
>
|
||||||
|
{(() => {
|
||||||
|
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 (
|
||||||
|
<div className={`${colorClasses[content.type]} border-l-4 p-4 rounded-lg shadow-xl backdrop-blur-sm`}>
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<Icon className={`w-6 h-6 ${iconColorClasses[content.type]} flex-shrink-0 mt-0.5`} />
|
||||||
|
<div className="flex-1">
|
||||||
|
<h3 className={`text-base font-semibold ${textColorClasses[content.type]} mb-2`}>
|
||||||
|
{content.title}
|
||||||
|
</h3>
|
||||||
|
<p className={`text-sm ${descColorClasses[content.type]} mb-3`}>
|
||||||
|
{content.description}
|
||||||
|
</p>
|
||||||
|
{content.items && content.items.length > 0 && (
|
||||||
|
<ul className={`list-disc list-inside text-sm ${listColorClasses[content.type]} space-y-1`}>
|
||||||
|
{content.items.map((item, index) => (
|
||||||
|
<li key={index} className="pl-2" dangerouslySetInnerHTML={{ __html: item.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>') }} />
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
|
{content.type === 'error' && content.errorCode !== undefined && (
|
||||||
|
<p className={`text-xs ${descColorClasses[content.type]} mt-3 font-mono`}>
|
||||||
|
Error Code: 0x{content.errorCode.toString(16).toUpperCase().padStart(2, '0')}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})()}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -152,6 +152,7 @@ export function useBrotherMachine() {
|
||||||
setPatternInfo(null);
|
setPatternInfo(null);
|
||||||
setSewingProgress(null);
|
setSewingProgress(null);
|
||||||
setError(null);
|
setError(null);
|
||||||
|
setMachineError(SewingMachineError.None);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err instanceof Error ? err.message : "Failed to disconnect");
|
setError(err instanceof Error ? err.message : "Failed to disconnect");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue