mirror of
https://github.com/jhbruhn/respira.git
synced 2026-01-27 02:13:41 +00:00
Loading State Improvements: - Add SkeletonLoader component with pattern info, canvas, and connection skeletons - Show loading spinner on file selection and during pattern upload - Display upload progress with enhanced progress bar and percentage - Add success confirmation message when upload completes - Show thread color preview dots inline with pattern info (up to 5 colors) Visual Polish & Animations: - Add custom animations: fadeIn, slideInRight, pulseGlow, skeleton-loading - Enhance all cards with subtle hover shadow effects - Improve header with richer gradient (blue-600 → blue-700 → blue-800) - Polish error messages with icons and improved layouts - Enhance empty state with decorative patterns and feature highlights - Add smooth transitions to all NextStepGuide states - Current color block pulses with blue glow animation - Color blocks have hover states for better interactivity Pattern Upload & Lock Functionality: - Hide upload button after pattern is uploaded (patternUploaded && uploadProgress === 100) - Disable pattern dragging when uploaded with visual lock indicator - Pattern position overlay shows amber background with lock icon when locked - Pattern remains in canvas after deletion for re-editing and re-upload - Delete pattern from cache when deleting from machine to prevent auto-resume - Add LockClosedIcon to indicate locked pattern state Pattern Management: - Keep pattern data in UI after deletion for repositioning and re-uploading - Clear machine-related state but preserve pattern visualization - Reset upload progress and pattern uploaded state on deletion 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
268 lines
13 KiB
TypeScript
268 lines
13 KiB
TypeScript
import { useState, useEffect, useCallback } from 'react';
|
|
import { useBrotherMachine } from './hooks/useBrotherMachine';
|
|
import { MachineConnection } from './components/MachineConnection';
|
|
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 type { PesPatternData } from './utils/pystitchConverter';
|
|
import { pyodideLoader } from './utils/pyodideLoader';
|
|
import { MachineStatus } from './types/machine';
|
|
import { hasError } from './utils/errorCodeHelpers';
|
|
import './App.css';
|
|
|
|
function App() {
|
|
const machine = useBrotherMachine();
|
|
const [pesData, setPesData] = useState<PesPatternData | null>(null);
|
|
const [pyodideReady, setPyodideReady] = useState(false);
|
|
const [pyodideError, setPyodideError] = useState<string | null>(null);
|
|
const [patternOffset, setPatternOffset] = useState<{ x: number; y: number }>({ x: 0, y: 0 });
|
|
const [patternUploaded, setPatternUploaded] = useState(false);
|
|
|
|
// 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);
|
|
});
|
|
}, []);
|
|
|
|
// Auto-load cached pattern when available
|
|
useEffect(() => {
|
|
if (machine.resumedPattern && !pesData) {
|
|
console.log('[App] Loading resumed pattern:', machine.resumeFileName, 'Offset:', machine.resumedPattern.patternOffset);
|
|
setPesData(machine.resumedPattern.pesData);
|
|
// Restore the cached pattern offset
|
|
if (machine.resumedPattern.patternOffset) {
|
|
setPatternOffset(machine.resumedPattern.patternOffset);
|
|
}
|
|
}
|
|
}, [machine.resumedPattern, pesData, machine.resumeFileName]);
|
|
|
|
const handlePatternLoaded = useCallback((data: PesPatternData) => {
|
|
setPesData(data);
|
|
// 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
|
|
useEffect(() => {
|
|
if (!machine.isConnected) {
|
|
setPatternUploaded(false);
|
|
return;
|
|
}
|
|
|
|
// Pattern is uploaded if machine has pattern info
|
|
if (machine.patternInfo !== null) {
|
|
setPatternUploaded(true);
|
|
} else {
|
|
// No pattern info means no pattern on machine
|
|
setPatternUploaded(false);
|
|
}
|
|
}, [machine.machineStatus, machine.patternInfo, machine.isConnected]);
|
|
|
|
return (
|
|
<div className="min-h-screen flex flex-col bg-gray-50">
|
|
<header className="bg-gradient-to-r from-blue-600 via-blue-700 to-blue-800 px-8 py-3 shadow-lg border-b-2 border-blue-900/20">
|
|
<div className="max-w-[1600px] mx-auto flex items-center gap-8">
|
|
<h1 className="text-xl font-bold text-white whitespace-nowrap">SKiTCH Controller</h1>
|
|
|
|
{/* Workflow Stepper - Integrated in header when connected */}
|
|
{machine.isConnected && (
|
|
<div className="flex-1">
|
|
<WorkflowStepper
|
|
machineStatus={machine.machineStatus}
|
|
isConnected={machine.isConnected}
|
|
hasPattern={pesData !== null}
|
|
patternUploaded={patternUploaded}
|
|
/>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</header>
|
|
|
|
<div className="flex-1 p-6 max-w-[1600px] w-full mx-auto">
|
|
{/* Global errors */}
|
|
{machine.error && (
|
|
<div className="bg-red-50 text-red-900 px-6 py-4 rounded-lg border-l-4 border-red-600 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 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<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>
|
|
<strong className="font-semibold">Error:</strong> {machine.error}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
{pyodideError && (
|
|
<div className="bg-red-50 text-red-900 px-6 py-4 rounded-lg border-l-4 border-red-600 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 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 text-blue-900 px-6 py-4 rounded-lg border-l-4 border-blue-600 mb-6 shadow-md animate-fadeIn">
|
|
<div className="flex items-center gap-3">
|
|
<svg className="w-5 h-5 animate-spin text-blue-600" 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="grid grid-cols-1 lg:grid-cols-[400px_1fr] gap-6">
|
|
{/* Left Column - Controls */}
|
|
<div className="flex flex-col gap-6">
|
|
{/* Next Step Guide - Always visible */}
|
|
<NextStepGuide
|
|
machineStatus={machine.machineStatus}
|
|
isConnected={machine.isConnected}
|
|
hasPattern={pesData !== null}
|
|
patternUploaded={patternUploaded}
|
|
hasError={hasError(machine.machineError)}
|
|
errorMessage={machine.error || undefined}
|
|
errorCode={machine.machineError}
|
|
/>
|
|
|
|
{/* Machine Connection - Always visible */}
|
|
<MachineConnection
|
|
isConnected={machine.isConnected}
|
|
machineInfo={machine.machineInfo}
|
|
machineStatus={machine.machineStatus}
|
|
machineStatusName={machine.machineStatusName}
|
|
machineError={machine.machineError}
|
|
isPolling={machine.isPolling}
|
|
onConnect={machine.connect}
|
|
onDisconnect={machine.disconnect}
|
|
onRefresh={machine.refreshStatus}
|
|
/>
|
|
|
|
{/* Pattern File - Only show when connected */}
|
|
{machine.isConnected && (
|
|
<FileUpload
|
|
isConnected={machine.isConnected}
|
|
machineStatus={machine.machineStatus}
|
|
uploadProgress={machine.uploadProgress}
|
|
onPatternLoaded={handlePatternLoaded}
|
|
onUpload={handleUpload}
|
|
pyodideReady={pyodideReady}
|
|
patternOffset={patternOffset}
|
|
patternUploaded={patternUploaded}
|
|
resumeAvailable={machine.resumeAvailable}
|
|
resumeFileName={machine.resumeFileName}
|
|
pesData={pesData}
|
|
/>
|
|
)}
|
|
</div>
|
|
|
|
{/* Right Column - Pattern Preview */}
|
|
<div className="flex flex-col gap-6">
|
|
{pesData ? (
|
|
<PatternCanvas
|
|
pesData={pesData}
|
|
sewingProgress={machine.sewingProgress}
|
|
machineInfo={machine.machineInfo}
|
|
initialPatternOffset={patternOffset}
|
|
onPatternOffsetChange={handlePatternOffsetChange}
|
|
patternUploaded={patternUploaded}
|
|
/>
|
|
) : (
|
|
<div className="bg-white p-6 rounded-lg shadow-md animate-fadeIn">
|
|
<h2 className="text-xl font-semibold mb-4 pb-2 border-b-2 border-gray-300">Pattern Preview</h2>
|
|
<div className="flex items-center justify-center h-[600px] bg-gradient-to-br from-gray-50 to-gray-100 rounded-lg border-2 border-dashed border-gray-300 relative overflow-hidden">
|
|
{/* Decorative background pattern */}
|
|
<div className="absolute inset-0 opacity-5">
|
|
<div className="absolute top-10 left-10 w-32 h-32 border-4 border-gray-400 rounded-full"></div>
|
|
<div className="absolute bottom-10 right-10 w-40 h-40 border-4 border-gray-400 rounded-full"></div>
|
|
<div className="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 w-48 h-48 border-4 border-gray-400 rounded-full"></div>
|
|
</div>
|
|
|
|
<div className="text-center relative z-10">
|
|
<div className="relative inline-block mb-6">
|
|
<svg className="w-28 h-28 mx-auto text-gray-300" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z" />
|
|
</svg>
|
|
<div className="absolute -top-2 -right-2 w-8 h-8 bg-blue-100 rounded-full flex items-center justify-center">
|
|
<svg className="w-5 h-5 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
|
|
</svg>
|
|
</div>
|
|
</div>
|
|
<h3 className="text-gray-700 text-xl font-semibold mb-2">No Pattern Loaded</h3>
|
|
<p className="text-gray-500 text-sm mb-4 max-w-sm mx-auto">
|
|
Connect to your machine and choose a PES embroidery file to see your design preview
|
|
</p>
|
|
<div className="flex items-center justify-center gap-6 text-xs text-gray-400">
|
|
<div className="flex items-center gap-1">
|
|
<div className="w-2 h-2 bg-blue-400 rounded-full"></div>
|
|
<span>Drag to Position</span>
|
|
</div>
|
|
<div className="flex items-center gap-1">
|
|
<div className="w-2 h-2 bg-green-400 rounded-full"></div>
|
|
<span>Zoom & Pan</span>
|
|
</div>
|
|
<div className="flex items-center gap-1">
|
|
<div className="w-2 h-2 bg-purple-400 rounded-full"></div>
|
|
<span>Real-time Preview</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Progress Monitor - Wide section below pattern preview */}
|
|
{machine.isConnected && patternUploaded && (
|
|
<ProgressMonitor
|
|
machineStatus={machine.machineStatus}
|
|
patternInfo={machine.patternInfo}
|
|
sewingProgress={machine.sewingProgress}
|
|
pesData={pesData}
|
|
onStartMaskTrace={machine.startMaskTrace}
|
|
onStartSewing={machine.startSewing}
|
|
onResumeSewing={machine.resumeSewing}
|
|
onDeletePattern={handleDeletePattern}
|
|
/>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default App;
|