Fix Image Processing
This commit is contained in:
259
frontend/src/components/ImageProcessingProgress.tsx
Normal file
259
frontend/src/components/ImageProcessingProgress.tsx
Normal file
@@ -0,0 +1,259 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { ImageProcessingProgressTracker, ImageProcessingProgress } from '../utils/imageProcessingProgress';
|
||||
|
||||
interface ImageProcessingProgressProps {
|
||||
storyId: string;
|
||||
autoStart?: boolean;
|
||||
onComplete?: () => void;
|
||||
onError?: (error: string) => void;
|
||||
}
|
||||
|
||||
export const ImageProcessingProgressComponent: React.FC<ImageProcessingProgressProps> = ({
|
||||
storyId,
|
||||
autoStart = false,
|
||||
onComplete,
|
||||
onError
|
||||
}) => {
|
||||
const [progress, setProgress] = useState<ImageProcessingProgress | null>(null);
|
||||
const [isTracking, setIsTracking] = useState(false);
|
||||
const [tracker, setTracker] = useState<ImageProcessingProgressTracker | null>(null);
|
||||
|
||||
const startTracking = () => {
|
||||
if (tracker) {
|
||||
tracker.stop();
|
||||
}
|
||||
|
||||
const newTracker = new ImageProcessingProgressTracker(storyId);
|
||||
|
||||
newTracker.onProgress((progress) => {
|
||||
setProgress(progress);
|
||||
});
|
||||
|
||||
newTracker.onComplete((finalProgress) => {
|
||||
setProgress(finalProgress);
|
||||
setIsTracking(false);
|
||||
onComplete?.();
|
||||
});
|
||||
|
||||
newTracker.onError((error) => {
|
||||
console.error('Image processing error:', error);
|
||||
setIsTracking(false);
|
||||
onError?.(error);
|
||||
});
|
||||
|
||||
setTracker(newTracker);
|
||||
setIsTracking(true);
|
||||
newTracker.start();
|
||||
};
|
||||
|
||||
const stopTracking = () => {
|
||||
if (tracker) {
|
||||
tracker.stop();
|
||||
setIsTracking(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (autoStart) {
|
||||
startTracking();
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (tracker) {
|
||||
tracker.stop();
|
||||
}
|
||||
};
|
||||
}, [storyId, autoStart]);
|
||||
|
||||
if (!progress && !isTracking) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!progress?.isProcessing && !progress?.completed) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="image-processing-progress">
|
||||
<div className="progress-header">
|
||||
<h4>Processing Images</h4>
|
||||
{isTracking && (
|
||||
<button onClick={stopTracking} className="btn btn-sm btn-secondary">
|
||||
Cancel
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{progress && (
|
||||
<div className="progress-content">
|
||||
{progress.error ? (
|
||||
<div className="alert alert-danger">
|
||||
<strong>Error:</strong> {progress.error}
|
||||
</div>
|
||||
) : progress.completed ? (
|
||||
<div className="alert alert-success">
|
||||
<strong>Completed:</strong> {progress.status}
|
||||
</div>
|
||||
) : (
|
||||
<div className="progress-info">
|
||||
<div className="status-text">
|
||||
<strong>Status:</strong> {progress.status}
|
||||
</div>
|
||||
|
||||
<div className="progress-stats">
|
||||
Processing {progress.processedImages} of {progress.totalImages} images
|
||||
({progress.progressPercentage.toFixed(1)}%)
|
||||
</div>
|
||||
|
||||
{progress.currentImageUrl && (
|
||||
<div className="current-image">
|
||||
<strong>Current:</strong>
|
||||
<span className="image-url" title={progress.currentImageUrl}>
|
||||
{progress.currentImageUrl.length > 60
|
||||
? `...${progress.currentImageUrl.slice(-60)}`
|
||||
: progress.currentImageUrl
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Progress bar */}
|
||||
<div className="progress-bar-container">
|
||||
<div className="progress-bar">
|
||||
<div
|
||||
className="progress-bar-fill"
|
||||
style={{ width: `${progress.progressPercentage}%` }}
|
||||
/>
|
||||
</div>
|
||||
<span className="progress-percentage">
|
||||
{progress.progressPercentage.toFixed(1)}%
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<style jsx>{`
|
||||
.image-processing-progress {
|
||||
background: #f8f9fa;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 4px;
|
||||
padding: 1rem;
|
||||
margin: 1rem 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||
}
|
||||
|
||||
.progress-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.progress-header h4 {
|
||||
margin: 0;
|
||||
font-size: 1.1rem;
|
||||
color: #495057;
|
||||
}
|
||||
|
||||
.progress-content {
|
||||
space-y: 0.5rem;
|
||||
}
|
||||
|
||||
.status-text {
|
||||
font-size: 0.9rem;
|
||||
color: #6c757d;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.progress-stats {
|
||||
font-weight: 500;
|
||||
color: #495057;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.current-image {
|
||||
font-size: 0.85rem;
|
||||
color: #6c757d;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.image-url {
|
||||
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
||||
background: #e9ecef;
|
||||
padding: 0.1rem 0.3rem;
|
||||
border-radius: 2px;
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
|
||||
.progress-bar-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
flex: 1;
|
||||
height: 8px;
|
||||
background: #e9ecef;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.progress-bar-fill {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, #007bff, #0056b3);
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
.progress-percentage {
|
||||
font-size: 0.85rem;
|
||||
font-weight: 500;
|
||||
color: #495057;
|
||||
min-width: 3rem;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 3px;
|
||||
border: 1px solid transparent;
|
||||
cursor: pointer;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: #6c757d;
|
||||
color: white;
|
||||
border-color: #6c757d;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: #5a6268;
|
||||
border-color: #545b62;
|
||||
}
|
||||
|
||||
.alert {
|
||||
padding: 0.75rem;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.alert-danger {
|
||||
background: #f8d7da;
|
||||
color: #721c24;
|
||||
border: 1px solid #f5c6cb;
|
||||
}
|
||||
|
||||
.alert-success {
|
||||
background: #d4edda;
|
||||
color: #155724;
|
||||
border: 1px solid #c3e6cb;
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ImageProcessingProgressComponent;
|
||||
246
frontend/src/utils/imageProcessingProgress.ts
Normal file
246
frontend/src/utils/imageProcessingProgress.ts
Normal file
@@ -0,0 +1,246 @@
|
||||
/**
|
||||
* Utility for tracking image processing progress
|
||||
*
|
||||
* Usage example:
|
||||
*
|
||||
* // After saving a story, start polling for progress
|
||||
* const progressTracker = new ImageProcessingProgressTracker(storyId);
|
||||
*
|
||||
* progressTracker.onProgress((progress) => {
|
||||
* console.log(`Processing ${progress.processedImages}/${progress.totalImages} images`);
|
||||
* console.log(`Current: ${progress.currentImageUrl}`);
|
||||
* console.log(`Status: ${progress.status}`);
|
||||
* });
|
||||
*
|
||||
* progressTracker.onComplete((finalProgress) => {
|
||||
* console.log('Image processing completed!');
|
||||
* });
|
||||
*
|
||||
* progressTracker.onError((error) => {
|
||||
* console.error('Image processing failed:', error);
|
||||
* });
|
||||
*
|
||||
* progressTracker.start();
|
||||
*/
|
||||
|
||||
export interface ImageProcessingProgress {
|
||||
isProcessing: boolean;
|
||||
totalImages: number;
|
||||
processedImages: number;
|
||||
currentImageUrl: string;
|
||||
status: string;
|
||||
progressPercentage: number;
|
||||
completed: boolean;
|
||||
error: string;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
export type ProgressCallback = (progress: ImageProcessingProgress) => void;
|
||||
export type CompleteCallback = (finalProgress: ImageProcessingProgress) => void;
|
||||
export type ErrorCallback = (error: string) => void;
|
||||
|
||||
export class ImageProcessingProgressTracker {
|
||||
private storyId: string;
|
||||
private pollInterval: number;
|
||||
private timeoutMs: number;
|
||||
private isPolling: boolean = false;
|
||||
private pollTimer: NodeJS.Timeout | null = null;
|
||||
private startTime: number = 0;
|
||||
|
||||
private progressCallbacks: ProgressCallback[] = [];
|
||||
private completeCallbacks: CompleteCallback[] = [];
|
||||
private errorCallbacks: ErrorCallback[] = [];
|
||||
|
||||
constructor(
|
||||
storyId: string,
|
||||
pollInterval: number = 1000, // Poll every 1 second
|
||||
timeoutMs: number = 300000 // 5 minute timeout
|
||||
) {
|
||||
this.storyId = storyId;
|
||||
this.pollInterval = pollInterval;
|
||||
this.timeoutMs = timeoutMs;
|
||||
}
|
||||
|
||||
public onProgress(callback: ProgressCallback): void {
|
||||
this.progressCallbacks.push(callback);
|
||||
}
|
||||
|
||||
public onComplete(callback: CompleteCallback): void {
|
||||
this.completeCallbacks.push(callback);
|
||||
}
|
||||
|
||||
public onError(callback: ErrorCallback): void {
|
||||
this.errorCallbacks.push(callback);
|
||||
}
|
||||
|
||||
public async start(): Promise<void> {
|
||||
if (this.isPolling) {
|
||||
console.warn('Progress tracking already started');
|
||||
return;
|
||||
}
|
||||
|
||||
this.isPolling = true;
|
||||
this.startTime = Date.now();
|
||||
|
||||
console.log(`Starting image processing progress tracking for story ${this.storyId}`);
|
||||
this.poll();
|
||||
}
|
||||
|
||||
public stop(): void {
|
||||
this.isPolling = false;
|
||||
if (this.pollTimer) {
|
||||
clearTimeout(this.pollTimer);
|
||||
this.pollTimer = null;
|
||||
}
|
||||
console.log(`Stopped progress tracking for story ${this.storyId}`);
|
||||
}
|
||||
|
||||
private async poll(): Promise<void> {
|
||||
if (!this.isPolling) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for timeout
|
||||
const elapsed = Date.now() - this.startTime;
|
||||
if (elapsed > this.timeoutMs) {
|
||||
this.handleError('Image processing timed out');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/stories/${this.storyId}/image-processing-progress`);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const progress: ImageProcessingProgress = await response.json();
|
||||
|
||||
// Call progress callbacks
|
||||
this.progressCallbacks.forEach(callback => {
|
||||
try {
|
||||
callback(progress);
|
||||
} catch (error) {
|
||||
console.error('Error in progress callback:', error);
|
||||
}
|
||||
});
|
||||
|
||||
// Check if processing is complete
|
||||
if (progress.completed) {
|
||||
this.handleComplete(progress);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for errors
|
||||
if (progress.error) {
|
||||
this.handleError(progress.error);
|
||||
return;
|
||||
}
|
||||
|
||||
// Continue polling if still processing
|
||||
if (progress.isProcessing) {
|
||||
this.pollTimer = setTimeout(() => this.poll(), this.pollInterval);
|
||||
} else {
|
||||
// No active processing - might have finished or never started
|
||||
this.handleComplete(progress);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
this.handleError(`Failed to fetch progress: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||
}
|
||||
}
|
||||
|
||||
private handleComplete(finalProgress: ImageProcessingProgress): void {
|
||||
this.stop();
|
||||
console.log(`Image processing completed for story ${this.storyId}`);
|
||||
|
||||
this.completeCallbacks.forEach(callback => {
|
||||
try {
|
||||
callback(finalProgress);
|
||||
} catch (error) {
|
||||
console.error('Error in complete callback:', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private handleError(error: string): void {
|
||||
this.stop();
|
||||
console.error(`Image processing error for story ${this.storyId}:`, error);
|
||||
|
||||
this.errorCallbacks.forEach(callback => {
|
||||
try {
|
||||
callback(error);
|
||||
} catch (error) {
|
||||
console.error('Error in error callback:', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* React hook for image processing progress
|
||||
*
|
||||
* Note: This hook requires React to be imported in the file where it's used.
|
||||
* To use this hook, import React in your component file:
|
||||
*
|
||||
* import React from 'react';
|
||||
* import { useImageProcessingProgress } from '../utils/imageProcessingProgress';
|
||||
*
|
||||
* Usage:
|
||||
* const { progress, isTracking, startTracking } = useImageProcessingProgress(storyId);
|
||||
*/
|
||||
import React from 'react';
|
||||
|
||||
export function useImageProcessingProgress(storyId: string) {
|
||||
const [progress, setProgress] = React.useState<ImageProcessingProgress | null>(null);
|
||||
const [isTracking, setIsTracking] = React.useState(false);
|
||||
const [tracker, setTracker] = React.useState<ImageProcessingProgressTracker | null>(null);
|
||||
|
||||
const startTracking = React.useCallback(() => {
|
||||
if (tracker) {
|
||||
tracker.stop();
|
||||
}
|
||||
|
||||
const newTracker = new ImageProcessingProgressTracker(storyId);
|
||||
|
||||
newTracker.onProgress((progress) => {
|
||||
setProgress(progress);
|
||||
});
|
||||
|
||||
newTracker.onComplete((finalProgress) => {
|
||||
setProgress(finalProgress);
|
||||
setIsTracking(false);
|
||||
});
|
||||
|
||||
newTracker.onError((error) => {
|
||||
console.error('Image processing error:', error);
|
||||
setIsTracking(false);
|
||||
});
|
||||
|
||||
setTracker(newTracker);
|
||||
setIsTracking(true);
|
||||
newTracker.start();
|
||||
}, [storyId, tracker]);
|
||||
|
||||
const stopTracking = React.useCallback(() => {
|
||||
if (tracker) {
|
||||
tracker.stop();
|
||||
setIsTracking(false);
|
||||
}
|
||||
}, [tracker]);
|
||||
|
||||
React.useEffect(() => {
|
||||
return () => {
|
||||
if (tracker) {
|
||||
tracker.stop();
|
||||
}
|
||||
};
|
||||
}, [tracker]);
|
||||
|
||||
return {
|
||||
progress,
|
||||
isTracking,
|
||||
startTracking,
|
||||
stopTracking
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user