Features: - Image generation (OpenAI, Gemini, Leonardo, Bria, Stability, Flux) - Nano Banana iterative editing - Video generation and upscaling - Audio TTS, STT, sound effects (ElevenLabs) - Text prompt studio and alt text - User authentication with JWT/cookies - Admin panel with voice management - Job queue with Celery - PostgreSQL + Redis backend - Next.js 15 + FastAPI architecture 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 (1M context) <noreply@anthropic.com>
95 lines
2.8 KiB
TypeScript
95 lines
2.8 KiB
TypeScript
'use client';
|
|
|
|
import { useEffect, useState } from 'react';
|
|
import { clsx } from 'clsx';
|
|
import { CheckCircle, XCircle, Loader2 } from 'lucide-react';
|
|
import { jobsApi } from '@/lib/api';
|
|
|
|
interface JobProgressProps {
|
|
jobId: string;
|
|
onComplete?: (job: any) => void;
|
|
onError?: (error: string) => void;
|
|
}
|
|
|
|
export default function JobProgress({ jobId, onComplete, onError }: JobProgressProps) {
|
|
const [job, setJob] = useState<any>(null);
|
|
const [polling, setPolling] = useState(true);
|
|
|
|
useEffect(() => {
|
|
if (!jobId || !polling) return;
|
|
|
|
const pollJob = async () => {
|
|
try {
|
|
const response = await jobsApi.get(jobId);
|
|
const jobData = response.data;
|
|
setJob(jobData);
|
|
|
|
if (jobData.status === 'completed') {
|
|
setPolling(false);
|
|
onComplete?.(jobData);
|
|
} else if (jobData.status === 'failed') {
|
|
setPolling(false);
|
|
onError?.(jobData.error_message || 'Job failed');
|
|
}
|
|
} catch (err) {
|
|
console.error('Failed to poll job:', err);
|
|
}
|
|
};
|
|
|
|
pollJob();
|
|
const interval = setInterval(pollJob, 2000);
|
|
|
|
return () => clearInterval(interval);
|
|
}, [jobId, polling, onComplete, onError]);
|
|
|
|
if (!job) {
|
|
return (
|
|
<div className="bg-forge-dark rounded-xl p-6 border border-gray-700">
|
|
<div className="flex items-center gap-3">
|
|
<Loader2 className="w-5 h-5 text-forge-yellow animate-spin" />
|
|
<span className="text-gray-300">Starting job...</span>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="bg-forge-dark rounded-xl p-6 border border-gray-700">
|
|
<div className="flex items-center justify-between mb-4">
|
|
<div className="flex items-center gap-3">
|
|
{job.status === 'completed' && (
|
|
<CheckCircle className="w-5 h-5 text-green-400" />
|
|
)}
|
|
{job.status === 'failed' && (
|
|
<XCircle className="w-5 h-5 text-red-400" />
|
|
)}
|
|
{(job.status === 'pending' || job.status === 'processing') && (
|
|
<Loader2 className="w-5 h-5 text-forge-yellow animate-spin" />
|
|
)}
|
|
<span className="text-white font-medium capitalize">{job.status}</span>
|
|
</div>
|
|
<span className="text-gray-500 text-sm">{job.progress}%</span>
|
|
</div>
|
|
|
|
<div className="progress-bar">
|
|
<div
|
|
className={clsx(
|
|
'progress-bar-fill',
|
|
job.status === 'failed' && 'bg-red-500'
|
|
)}
|
|
style={{ width: `${job.progress}%` }}
|
|
/>
|
|
</div>
|
|
|
|
{job.api_provider && (
|
|
<p className="mt-3 text-sm text-gray-500">
|
|
Using: {job.api_provider} {job.api_model && `(${job.api_model})`}
|
|
</p>
|
|
)}
|
|
|
|
{job.error_message && (
|
|
<p className="mt-3 text-sm text-red-400">{job.error_message}</p>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|