226 lines
10 KiB
TypeScript
226 lines
10 KiB
TypeScript
import React, { useState, useEffect } from 'react';
|
|
import { LoadingSpinner } from './LoadingSpinner';
|
|
import { CheckCircleIcon } from './icons/CheckCircleIcon';
|
|
import { ExclamationTriangleIcon } from './icons/ExclamationTriangleIcon';
|
|
import { StageStatus, ProcessingStage } from '../types';
|
|
|
|
interface ProcessingStatusProps {
|
|
stages: ProcessingStage[];
|
|
estimatedTime: number;
|
|
currentFile?: string | null;
|
|
pageProgress?: {current: number, total: number} | null;
|
|
bulkProgress?: number;
|
|
bulkFileCount?: {current: number, total: number} | null;
|
|
}
|
|
|
|
interface StageIconProps {
|
|
status: StageStatus;
|
|
isCritical: boolean;
|
|
}
|
|
|
|
const StageIcon: React.FC<StageIconProps> = ({ status, isCritical }) => {
|
|
switch (status) {
|
|
case 'in-progress':
|
|
return <div className="w-5 h-5 text-brand-primary"><LoadingSpinner /></div>;
|
|
case 'completed':
|
|
return <CheckCircleIcon className="w-5 h-5 text-green-500" />;
|
|
case 'pending':
|
|
return <div className="w-5 h-5 rounded-full border-2 border-gray-400 dark:border-gray-600"></div>;
|
|
case 'error':
|
|
return isCritical ? (
|
|
<svg xmlns="http://www.w3.org/2000/svg" className="w-5 h-5 text-red-500" viewBox="0 0 20 20" fill="currentColor">
|
|
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clipRule="evenodd" />
|
|
</svg>
|
|
) : (
|
|
<ExclamationTriangleIcon className="w-5 h-5 text-yellow-500" />
|
|
);
|
|
default:
|
|
return null;
|
|
}
|
|
};
|
|
|
|
export const ProcessingStatus: React.FC<ProcessingStatusProps> = ({ stages, estimatedTime, currentFile, pageProgress, bulkProgress, bulkFileCount }) => {
|
|
const [timeRemaining, setTimeRemaining] = useState(estimatedTime);
|
|
|
|
useEffect(() => {
|
|
setTimeRemaining(estimatedTime); // Reset when component gets new props
|
|
const timer = setInterval(() => {
|
|
setTimeRemaining(prevTime => (prevTime > 0 ? prevTime - 1 : 0));
|
|
}, 1000);
|
|
|
|
return () => clearInterval(timer);
|
|
}, [estimatedTime]);
|
|
|
|
const getStatusTextColor = (status: StageStatus, isCritical: boolean) => {
|
|
switch (status) {
|
|
case 'in-progress':
|
|
return 'text-brand-primary font-semibold';
|
|
case 'completed':
|
|
return 'text-gray-700 dark:text-gray-300';
|
|
case 'pending':
|
|
return 'text-gray-400 dark:text-gray-500';
|
|
case 'error':
|
|
return isCritical ? 'text-red-500 font-semibold' : 'text-yellow-600 dark:text-yellow-400';
|
|
default:
|
|
return '';
|
|
}
|
|
}
|
|
|
|
// Render new layout for bulk processing
|
|
if (currentFile) {
|
|
const extractionStage = stages.find(s => s.name === 'Extracting All Items from Flyer' && s.status === 'in-progress' && s.progress);
|
|
|
|
const stageList = (
|
|
<ul className="space-y-3">
|
|
{stages.map((stage, index) => {
|
|
const isCritical = stage.critical ?? true;
|
|
return (
|
|
<li key={index}>
|
|
<div className="flex items-center space-x-3">
|
|
<div className="flex-shrink-0">
|
|
<StageIcon status={stage.status} isCritical={isCritical} />
|
|
</div>
|
|
<span className={`text-sm ${getStatusTextColor(stage.status, isCritical)}`}>
|
|
{stage.name}
|
|
{!isCritical && <span className="text-gray-400 dark:text-gray-500 text-xs italic"> (optional)</span>}
|
|
<span className="text-gray-400 dark:text-gray-500 ml-1">{stage.detail}</span>
|
|
</span>
|
|
</div>
|
|
</li>
|
|
);
|
|
})}
|
|
</ul>
|
|
);
|
|
|
|
return (
|
|
<div className="p-6 bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 min-h-[400px] flex flex-col justify-center">
|
|
<h2 className="text-xl font-bold text-gray-800 dark:text-white mb-6 text-center">
|
|
Processing Steps for: <br/>
|
|
<span className="font-normal text-base text-gray-600 dark:text-gray-300 truncate mt-1 block max-w-sm">{currentFile}</span>
|
|
</h2>
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-8 w-full max-w-4xl mx-auto">
|
|
{/* Left Column: Spinners and Progress Bars */}
|
|
<div className="flex flex-col justify-center items-center space-y-4">
|
|
<div className="w-24 h-24 text-brand-primary">
|
|
<LoadingSpinner />
|
|
</div>
|
|
|
|
{/* Overall Progress */}
|
|
{bulkFileCount && (
|
|
<div className="w-full">
|
|
<p className="text-sm text-center text-gray-500 dark:text-gray-400 mb-1">
|
|
File {bulkFileCount.current} of {bulkFileCount.total}
|
|
</p>
|
|
<div className="w-full bg-gray-200 rounded-full h-2.5 dark:bg-gray-700">
|
|
<div
|
|
className="bg-brand-primary h-2.5 rounded-full"
|
|
style={{ width: `${bulkProgress || 0}%`, transition: 'width 0.5s ease-in-out' }}
|
|
></div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* PDF Conversion Progress */}
|
|
{pageProgress && pageProgress.total > 1 && (
|
|
<div className="w-full">
|
|
<p className="text-xs text-left text-gray-500 dark:text-gray-400 mb-1">
|
|
Converting PDF: Page {pageProgress.current} of {pageProgress.total}
|
|
</p>
|
|
<div className="w-full bg-gray-200 rounded-full h-1.5 dark:bg-gray-700">
|
|
<div
|
|
className="bg-blue-500 h-1.5 rounded-full"
|
|
style={{ width: `${(pageProgress.current / pageProgress.total) * 100}%`, transition: 'width 0.2s ease-in-out' }}
|
|
></div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Item Extraction Progress */}
|
|
{extractionStage && extractionStage.progress && (
|
|
<div className="w-full">
|
|
<p className="text-xs text-left text-gray-500 dark:text-gray-400 mb-1">
|
|
Analyzing page {extractionStage.progress.current} of {extractionStage.progress.total}
|
|
</p>
|
|
<div className="w-full bg-gray-200 rounded-full h-1.5 dark:bg-gray-700">
|
|
<div
|
|
className="bg-purple-500 h-1.5 rounded-full"
|
|
style={{ width: `${(extractionStage.progress.current / extractionStage.progress.total) * 100}%`, transition: 'width 0.5s ease-out' }}
|
|
></div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Right Column: Checklist */}
|
|
<div className="flex items-center">
|
|
<div className="w-full">
|
|
{stageList}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// Original layout for single file processing
|
|
const title = 'Processing Your Flyer...';
|
|
const subTitle = `Estimated time remaining: ${Math.floor(timeRemaining / 60)}m ${timeRemaining % 60}s`;
|
|
|
|
return (
|
|
<div className="text-center p-8 bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 h-full flex flex-col justify-center items-center min-h-[400px]">
|
|
<h2 className="text-xl font-bold mb-2 text-gray-800 dark:text-white">{title}</h2>
|
|
<p className="text-gray-500 dark:text-gray-400 mb-6 font-semibold text-brand-primary truncate max-w-full px-4">
|
|
{subTitle}
|
|
</p>
|
|
|
|
{pageProgress && pageProgress.total > 1 && (
|
|
<div className="w-full max-w-sm mb-6">
|
|
<p className="text-sm text-gray-500 dark:text-gray-400 mb-1 text-left">
|
|
Converting PDF: Page {pageProgress.current} of {pageProgress.total}
|
|
</p>
|
|
<div className="w-full bg-gray-200 rounded-full h-2 dark:bg-gray-700">
|
|
<div
|
|
className="bg-blue-500 h-2 rounded-full"
|
|
style={{ width: `${(pageProgress.current / pageProgress.total) * 100}%`, transition: 'width 0.2s ease-in-out' }}
|
|
></div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
<div className="w-full max-w-sm text-left">
|
|
<ul className="space-y-3">
|
|
{stages.map((stage, index) => {
|
|
const isCritical = stage.critical ?? true;
|
|
return (
|
|
<li key={index}>
|
|
<div className="flex items-center space-x-3">
|
|
<div className="flex-shrink-0">
|
|
<StageIcon status={stage.status} isCritical={isCritical} />
|
|
</div>
|
|
<span className={`text-sm ${getStatusTextColor(stage.status, isCritical)}`}>
|
|
{stage.name}
|
|
{!isCritical && <span className="text-gray-400 dark:text-gray-500 text-xs italic"> (optional)</span>}
|
|
<span className="text-gray-400 dark:text-gray-500 ml-1">{stage.detail}</span>
|
|
</span>
|
|
</div>
|
|
{stage.progress && stage.status === 'in-progress' && stage.progress.total > 1 && (
|
|
<div className="w-full mt-2 pl-8">
|
|
<p className="text-xs text-gray-500 dark:text-gray-400 mb-1">
|
|
Analyzing page {stage.progress.current} of {stage.progress.total}
|
|
</p>
|
|
<div className="w-full bg-gray-200 rounded-full h-1.5 dark:bg-gray-700">
|
|
<div
|
|
className="bg-purple-500 h-1.5 rounded-full"
|
|
style={{ width: `${(stage.progress.current / stage.progress.total) * 100}%`, transition: 'width 0.5s ease-out' }}
|
|
></div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</li>
|
|
);
|
|
})}
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
);
|
|
}; |