import { useRef, useEffect, useState, useMemo } from "react"; import { useShallow } from 'zustand/react/shallow'; import { useMachineStore } from '../stores/useMachineStore'; import { usePatternStore } from '../stores/usePatternStore'; import { CheckCircleIcon, ArrowRightIcon, CircleStackIcon, PlayIcon, CheckBadgeIcon, ClockIcon, PauseCircleIcon, ExclamationCircleIcon, ChartBarIcon, ArrowPathIcon, } from "@heroicons/react/24/solid"; import { MachineStatus } from "../types/machine"; import { canStartSewing, canStartMaskTrace, canResumeSewing, getStateVisualInfo, } from "../utils/machineStateHelpers"; export function ProgressMonitor() { // Machine store const { machineStatus, patternInfo, sewingProgress, isDeleting, startMaskTrace, startSewing, resumeSewing, } = useMachineStore( useShallow((state) => ({ machineStatus: state.machineStatus, patternInfo: state.patternInfo, sewingProgress: state.sewingProgress, isDeleting: state.isDeleting, startMaskTrace: state.startMaskTrace, startSewing: state.startSewing, resumeSewing: state.resumeSewing, })) ); // Pattern store const pesData = usePatternStore((state) => state.pesData); const currentBlockRef = useRef(null); const colorBlocksScrollRef = useRef(null); const [showGradient, setShowGradient] = useState(true); // State indicators const isMaskTraceComplete = machineStatus === MachineStatus.MASK_TRACE_COMPLETE; const stateVisual = getStateVisualInfo(machineStatus); // Use PEN stitch count as fallback when machine reports 0 total stitches const totalStitches = patternInfo ? (patternInfo.totalStitches === 0 && pesData?.penStitches ? pesData.penStitches.stitches.length : patternInfo.totalStitches) : 0; const progressPercent = totalStitches > 0 ? ((sewingProgress?.currentStitch || 0) / totalStitches) * 100 : 0; // Calculate color block information from decoded penStitches const colorBlocks = useMemo(() => { if (!pesData || !pesData.penStitches) return []; const blocks: Array<{ colorIndex: number; threadHex: string; startStitch: number; endStitch: number; stitchCount: number; threadCatalogNumber: string | null; threadBrand: string | null; threadDescription: string | null; threadChart: string | null; }> = []; // Use the pre-computed color blocks from decoded PEN data for (const penBlock of pesData.penStitches.colorBlocks) { const thread = pesData.threads[penBlock.colorIndex]; blocks.push({ colorIndex: penBlock.colorIndex, threadHex: thread?.hex || "#000000", threadCatalogNumber: thread?.catalogNumber ?? null, threadBrand: thread?.brand ?? null, threadDescription: thread?.description ?? null, threadChart: thread?.chart ?? null, startStitch: penBlock.startStitchIndex, endStitch: penBlock.endStitchIndex, stitchCount: penBlock.endStitchIndex - penBlock.startStitchIndex, }); } return blocks; }, [pesData]); // Determine current color block based on current stitch const currentStitch = sewingProgress?.currentStitch || 0; const currentBlockIndex = colorBlocks.findIndex( (block) => currentStitch >= block.startStitch && currentStitch < block.endStitch, ); // Auto-scroll to current block useEffect(() => { if (currentBlockRef.current) { currentBlockRef.current.scrollIntoView({ behavior: "smooth", block: "nearest", }); } }, [currentBlockIndex]); // Handle scroll to detect if at bottom const handleColorBlocksScroll = () => { if (colorBlocksScrollRef.current) { const { scrollTop, scrollHeight, clientHeight } = colorBlocksScrollRef.current; const isAtBottom = scrollTop + clientHeight >= scrollHeight - 5; // 5px threshold setShowGradient(!isAtBottom); } }; // Check initial scroll state and update on resize useEffect(() => { const checkScrollable = () => { if (colorBlocksScrollRef.current) { const { scrollHeight, clientHeight } = colorBlocksScrollRef.current; const isScrollable = scrollHeight > clientHeight; setShowGradient(isScrollable); } }; checkScrollable(); window.addEventListener('resize', checkScrollable); return () => window.removeEventListener('resize', checkScrollable); }, [colorBlocks]); const stateIndicatorColors = { idle: "bg-blue-50 dark:bg-blue-900/20 border-blue-600", info: "bg-blue-50 dark:bg-blue-900/20 border-blue-600", active: "bg-yellow-50 dark:bg-yellow-900/20 border-yellow-500", waiting: "bg-yellow-50 dark:bg-yellow-900/20 border-yellow-500", warning: "bg-yellow-50 dark:bg-yellow-900/20 border-yellow-500", complete: "bg-green-50 dark:bg-green-900/20 border-green-600", success: "bg-green-50 dark:bg-green-900/20 border-green-600", interrupted: "bg-red-50 dark:bg-red-900/20 border-red-600", error: "bg-red-50 dark:bg-red-900/20 border-red-600", danger: "bg-red-50 dark:bg-red-900/20 border-red-600", }; return (

Sewing Progress

{sewingProgress && (

{progressPercent.toFixed(1)}% complete

)}
{/* Pattern Info */} {patternInfo && (
Total Stitches {totalStitches.toLocaleString()}
Est. Time {Math.floor(patternInfo.totalTime / 60)}: {String(patternInfo.totalTime % 60).padStart(2, "0")}
Speed {patternInfo.speed} spm
)} {/* Progress Bar */} {sewingProgress && (
Current Stitch {sewingProgress.currentStitch.toLocaleString()} /{" "} {totalStitches.toLocaleString()}
Time Elapsed {Math.floor(sewingProgress.currentTime / 60)}: {String(sewingProgress.currentTime % 60).padStart(2, "0")}
)} {/* State Visual Indicator */} {patternInfo && (() => { const iconMap = { ready: ( ), active: ( ), waiting: ( ), complete: ( ), interrupted: ( ), error: ( ), }; return (
{iconMap[stateVisual.iconName]}
{stateVisual.label}
{stateVisual.description}
); })()} {/* Color Blocks */} {colorBlocks.length > 0 && (

Color Blocks

{colorBlocks.map((block, index) => { const isCompleted = currentStitch >= block.endStitch; const isCurrent = index === currentBlockIndex; // Calculate progress within current block let blockProgress = 0; if (isCurrent) { blockProgress = ((currentStitch - block.startStitch) / block.stitchCount) * 100; } else if (isCompleted) { blockProgress = 100; } return (
{/* Color swatch */}
{/* Thread info */}
Thread {block.colorIndex + 1} {(block.threadBrand || block.threadChart || block.threadDescription || block.threadCatalogNumber) && ( {" "} ( {(() => { // Primary metadata: brand and catalog number const primaryMetadata = [ block.threadBrand, block.threadCatalogNumber ? `#${block.threadCatalogNumber}` : null ].filter(Boolean).join(" "); // Secondary metadata: chart and description const secondaryMetadata = [ block.threadChart, block.threadDescription ].filter(Boolean).join(" "); return [primaryMetadata, secondaryMetadata].filter(Boolean).join(" • "); })()} ) )}
{block.stitchCount.toLocaleString()} stitches
{/* Status icon */} {isCompleted ? ( ) : isCurrent ? ( ) : ( )}
{/* Progress bar for current block */} {isCurrent && (
)}
); })}
{/* Gradient overlay to indicate more content below - only on desktop and when not at bottom */} {showGradient && (
)}
)} {/* Action buttons */}
{/* Resume has highest priority when available */} {canResumeSewing(machineStatus) && ( )} {/* Start Sewing - primary action, takes more space */} {canStartSewing(machineStatus) && !canResumeSewing(machineStatus) && ( )} {/* Start Mask Trace - secondary action */} {canStartMaskTrace(machineStatus) && ( )}
); }