feat: add error recovery with retry functionality
Error recovery features: - **Retry button**: Appears on failed conversions - **Smart retry logic**: Re-attempts conversion with same settings - **State management**: Properly resets job state before retry - **Progress tracking**: Shows progress during retry attempt - **Toast notifications**: Informs user of retry success/failure - **History updates**: Successful retries added to history - **Visual feedback**: RefreshCw icon with clear button label Implementation: - Added onRetry prop to ConversionPreview component - Implemented handleRetry function in FileConverter - Reuses existing conversion logic for consistency - Maintains all conversion options during retry - Updates job status through proper state flow: error → loading → processing → completed/error User experience: - One-click retry for failed conversions - No need to re-upload file or reconfigure settings - Clear visual indication when retry is in progress - Helpful error messages if retry also fails 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
import { Download, CheckCircle, XCircle, Loader2, Clock, TrendingUp, FileCheck2, ArrowRight } from 'lucide-react';
|
||||
import { Download, CheckCircle, XCircle, Loader2, Clock, TrendingUp, FileCheck2, ArrowRight, RefreshCw } from 'lucide-react';
|
||||
import { cn } from '@/lib/utils/cn';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card';
|
||||
@@ -12,9 +12,10 @@ import type { ConversionJob } from '@/types/conversion';
|
||||
export interface ConversionPreviewProps {
|
||||
job: ConversionJob;
|
||||
onDownload?: () => void;
|
||||
onRetry?: () => void;
|
||||
}
|
||||
|
||||
export function ConversionPreview({ job, onDownload }: ConversionPreviewProps) {
|
||||
export function ConversionPreview({ job, onDownload, onRetry }: ConversionPreviewProps) {
|
||||
const [previewUrl, setPreviewUrl] = React.useState<string | null>(null);
|
||||
const [elapsedTime, setElapsedTime] = React.useState(0);
|
||||
const [estimatedTimeRemaining, setEstimatedTimeRemaining] = React.useState<number | null>(null);
|
||||
@@ -260,6 +261,14 @@ export function ConversionPreview({ job, onDownload }: ConversionPreviewProps) {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Retry button */}
|
||||
{job.status === 'error' && onRetry && (
|
||||
<Button onClick={onRetry} variant="outline" className="w-full gap-2">
|
||||
<RefreshCw className="h-4 w-4" />
|
||||
Retry Conversion
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{/* Preview */}
|
||||
{job.status === 'completed' && renderPreview()}
|
||||
|
||||
|
||||
@@ -244,6 +244,109 @@ export function FileConverter() {
|
||||
addToast(`Downloaded ${files.length} files as ZIP`, 'success');
|
||||
};
|
||||
|
||||
const handleRetry = async (jobId: string) => {
|
||||
const jobIndex = conversionJobs.findIndex(j => j.id === jobId);
|
||||
if (jobIndex === -1 || !outputFormat) return;
|
||||
|
||||
const job = conversionJobs[jobIndex];
|
||||
|
||||
try {
|
||||
// Reset job to loading
|
||||
setConversionJobs((prev) =>
|
||||
prev.map((j, idx) => idx === jobIndex ? {
|
||||
...j,
|
||||
status: 'loading' as const,
|
||||
progress: 0,
|
||||
error: undefined,
|
||||
startTime: Date.now(),
|
||||
} : j)
|
||||
);
|
||||
|
||||
// Update to processing
|
||||
setConversionJobs((prev) =>
|
||||
prev.map((j, idx) => idx === jobIndex ? { ...j, status: 'processing' as const, progress: 10 } : j)
|
||||
);
|
||||
|
||||
// Call appropriate converter
|
||||
let result;
|
||||
|
||||
switch (outputFormat.converter) {
|
||||
case 'ffmpeg':
|
||||
result = await convertWithFFmpeg(job.inputFile, outputFormat.extension, conversionOptions, (progress) => {
|
||||
setConversionJobs((prev) =>
|
||||
prev.map((j, idx) => idx === jobIndex ? { ...j, progress } : j)
|
||||
);
|
||||
});
|
||||
break;
|
||||
|
||||
case 'imagemagick':
|
||||
result = await convertWithImageMagick(
|
||||
job.inputFile,
|
||||
outputFormat.extension,
|
||||
conversionOptions,
|
||||
(progress) => {
|
||||
setConversionJobs((prev) =>
|
||||
prev.map((j, idx) => idx === jobIndex ? { ...j, progress } : j)
|
||||
);
|
||||
}
|
||||
);
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new Error(`Unknown converter: ${outputFormat.converter}`);
|
||||
}
|
||||
|
||||
// Update job with result
|
||||
if (result.success && result.blob) {
|
||||
setConversionJobs((prev) =>
|
||||
prev.map((j, idx) => idx === jobIndex ? {
|
||||
...j,
|
||||
status: 'completed' as const,
|
||||
progress: 100,
|
||||
result: result.blob,
|
||||
endTime: Date.now(),
|
||||
} : j)
|
||||
);
|
||||
|
||||
addToast('Conversion completed successfully!', 'success');
|
||||
|
||||
// Add to history
|
||||
addToHistory({
|
||||
inputFileName: job.inputFile.name,
|
||||
inputFormat: job.inputFormat.name,
|
||||
outputFormat: outputFormat.name,
|
||||
outputFileName: `output.${outputFormat.extension}`,
|
||||
fileSize: result.blob.size,
|
||||
result: result.blob,
|
||||
});
|
||||
} else {
|
||||
setConversionJobs((prev) =>
|
||||
prev.map((j, idx) => idx === jobIndex ? {
|
||||
...j,
|
||||
status: 'error' as const,
|
||||
error: result.error || 'Unknown error',
|
||||
endTime: Date.now(),
|
||||
} : j)
|
||||
);
|
||||
|
||||
addToast(result.error || 'Retry failed', 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
||||
|
||||
setConversionJobs((prev) =>
|
||||
prev.map((j, idx) => idx === jobIndex ? {
|
||||
...j,
|
||||
status: 'error' as const,
|
||||
error: errorMessage,
|
||||
endTime: Date.now(),
|
||||
} : j)
|
||||
);
|
||||
|
||||
addToast(`Retry failed: ${errorMessage}`, 'error');
|
||||
}
|
||||
};
|
||||
|
||||
const isConverting = conversionJobs.some(job => job.status === 'loading' || job.status === 'processing');
|
||||
const isConvertDisabled = selectedFiles.length === 0 || !outputFormat || isConverting;
|
||||
const completedCount = conversionJobs.filter(job => job.status === 'completed').length;
|
||||
@@ -356,7 +459,11 @@ export function FileConverter() {
|
||||
{conversionJobs.length > 0 && (
|
||||
<div className="space-y-4">
|
||||
{conversionJobs.map((job) => (
|
||||
<ConversionPreview key={job.id} job={job} />
|
||||
<ConversionPreview
|
||||
key={job.id}
|
||||
job={job}
|
||||
onRetry={() => handleRetry(job.id)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user