| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313 |
- "use client";
- import { useState, useEffect, useCallback } from "react";
- import { Header } from "@/components/layout";
- import { AppLayout } from "@/components/layout";
- import { Button } from "@/components/ui/button";
- import { Card, CardContent } from "@/components/ui/card";
- import { Badge } from "@/components/ui/badge";
- import { Progress } from "@/components/ui/progress";
- import { apiClient, type JobInfo, type QueueStatus } from "@/lib/api";
- import {
- Loader2,
- RefreshCw,
- X,
- Clock,
- CheckCircle,
- XCircle,
- AlertCircle,
- Play,
- Pause,
- } from "lucide-react";
- function QueuePage() {
- const [queueStatus, setQueueStatus] = useState<QueueStatus | null>(null);
- const [loading, setLoading] = useState(true);
- const [isInitialLoad, setIsInitialLoad] = useState(true);
- const [error, setError] = useState<string | null>(null);
- const loadQueueStatus = useCallback(async () => {
- try {
- setLoading(true);
- setError(null);
- const status = await apiClient.getQueueStatus();
- setQueueStatus(status);
- } catch (err) {
- console.error("Failed to load queue status:", err);
- setError(err instanceof Error ? err.message : "Failed to load queue status");
- } finally {
- setLoading(false);
- setIsInitialLoad(false);
- }
- }, []);
- useEffect(() => {
- loadQueueStatus();
-
- // Set up polling for active jobs
- const interval = setInterval(() => {
- loadQueueStatus();
- }, 2000); // Poll every 2 seconds
- return () => clearInterval(interval);
- }, [loadQueueStatus]);
- const handleCancelJob = async (jobId: string) => {
- try {
- await apiClient.cancelJob(jobId);
- // Refresh queue status after cancellation
- loadQueueStatus();
- } catch (err) {
- console.error("Failed to cancel job:", err);
- setError(err instanceof Error ? err.message : "Failed to cancel job");
- }
- };
- const handleClearQueue = async () => {
- if (!confirm("Are you sure you want to clear the entire queue? This will remove all jobs.")) {
- return;
- }
-
- try {
- await apiClient.clearQueue();
- loadQueueStatus();
- } catch (err) {
- console.error("Failed to clear queue:", err);
- setError(err instanceof Error ? err.message : "Failed to clear queue");
- }
- };
- const getStatusIcon = (status: string) => {
- switch (status) {
- case "completed":
- return <CheckCircle className="h-4 w-4 text-green-500" />;
- case "failed":
- return <XCircle className="h-4 w-4 text-red-500" />;
- case "cancelled":
- return <X className="h-4 w-4 text-gray-500" />;
- case "processing":
- return <Play className="h-4 w-4 text-blue-500" />;
- case "queued":
- case "pending":
- return <Clock className="h-4 w-4 text-yellow-500" />;
- default:
- return <AlertCircle className="h-4 w-4 text-gray-500" />;
- }
- };
- const getStatusBadge = (status: string) => {
- const variants: Record<string, "default" | "secondary" | "destructive" | "outline"> = {
- completed: "default",
- failed: "destructive",
- cancelled: "secondary",
- processing: "default",
- queued: "secondary",
- pending: "secondary",
- };
-
- return (
- <Badge variant={variants[status] || "secondary"} className="capitalize">
- {status}
- </Badge>
- );
- };
- const getProgressValue = (job: JobInfo): number => {
- // For completed jobs, ensure progress is 100%
- if (job.status === "completed") {
- return 100;
- }
- // For failed jobs, show 0% progress
- if (job.status === "failed" || job.status === "cancelled") {
- return 0;
- }
- // For other statuses, use the progress value from the API
- return job.progress || 0;
- };
- const formatDate = (dateString?: string) => {
- if (!dateString) return "N/A";
- const date = new Date(dateString);
- const year = date.getFullYear();
- const month = String(date.getMonth() + 1).padStart(2, '0');
- const day = String(date.getDate()).padStart(2, '0');
- const hours = String(date.getHours()).padStart(2, '0');
- const minutes = String(date.getMinutes()).padStart(2, '0');
- const seconds = String(date.getSeconds()).padStart(2, '0');
- return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
- };
- if (loading && isInitialLoad) {
- return (
- <AppLayout>
- <Header title="Queue" description="Monitor and manage generation jobs" />
- <div className="container mx-auto p-6">
- <div className="flex items-center justify-center h-96">
- <div className="flex items-center gap-2">
- <Loader2 className="h-6 w-6 animate-spin" />
- <span>Loading queue...</span>
- </div>
- </div>
- </div>
- </AppLayout>
- );
- }
- if (error) {
- return (
- <AppLayout>
- <Header title="Queue" description="Monitor and manage generation jobs" />
- <div className="container mx-auto p-6">
- <div className="flex flex-col items-center justify-center h-96 gap-4">
- <div className="text-destructive text-center">
- <p className="text-lg font-medium">Error loading queue</p>
- <p className="text-sm">{error}</p>
- </div>
- <Button onClick={loadQueueStatus} variant="outline">
- <RefreshCw className="h-4 w-4 mr-2" />
- Try Again
- </Button>
- </div>
- </div>
- </AppLayout>
- );
- }
- return (
- <AppLayout>
- <Header title="Queue" description="Monitor and manage generation jobs" />
- <div className="container mx-auto p-6">
- {/* Queue Controls */}
- <div className="flex items-center justify-between mb-6">
- <div className="flex items-center gap-4">
- <h2 className="text-2xl font-bold">
- {queueStatus?.size || 0} Job{(queueStatus?.size || 0) !== 1 ? "s" : ""}
- </h2>
- {queueStatus?.active_generations ? (
- <Badge variant="secondary">
- {queueStatus.active_generations} Active
- </Badge>
- ) : null}
- </div>
-
- <div className="flex items-center gap-2">
- <Button onClick={loadQueueStatus} variant="outline" size="sm">
- <RefreshCw className="h-4 w-4 mr-2" />
- Refresh
- </Button>
- {queueStatus && queueStatus.size > 0 && (
- <Button onClick={handleClearQueue} variant="destructive" size="sm">
- Clear Queue
- </Button>
- )}
- </div>
- </div>
- {/* Queue Status */}
- {!queueStatus || queueStatus.jobs.length === 0 ? (
- <div className="flex flex-col items-center justify-center h-96 border-2 border-dashed border-border rounded-lg">
- <div className="text-center">
- <h3 className="text-lg font-medium text-muted-foreground mb-2">
- No jobs in queue
- </h3>
- <p className="text-sm text-muted-foreground">
- Generate some images using the Text to Image, Image to Image, or Inpainting tools.
- </p>
- </div>
- </div>
- ) : (
- <div className="space-y-4">
- {queueStatus.jobs.map((job, index) => (
- <Card key={job.id || job.request_id || index} className="overflow-hidden">
- <CardContent className="p-6">
- <div className="flex items-start justify-between">
- {/* Job Info */}
- <div className="flex-1 space-y-2">
- <div className="flex items-center gap-3">
- {getStatusIcon(job.status)}
- <h3 className="text-lg font-semibold">
- Job {job.id || job.request_id}
- </h3>
- {getStatusBadge(job.status)}
- </div>
-
- <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 text-sm">
- <div>
- <span className="font-medium">Type:</span>
- <p className="text-muted-foreground">
- {job.prompt ? "Text to Image" : "Unknown"}
- </p>
- </div>
-
- <div>
- <span className="font-medium">Created:</span>
- <p className="text-muted-foreground">
- {formatDate(job.created_at)}
- </p>
- </div>
-
- <div>
- <span className="font-medium">Position:</span>
- <p className="text-muted-foreground">
- {job.position !== undefined ? job.position + 1 : "N/A"}
- </p>
- </div>
- </div>
- {/* Progress Bar */}
- <div className="space-y-2">
- <div className="flex items-center justify-between">
- <span className="font-medium">Progress:</span>
- <span className="text-sm text-muted-foreground">
- {getProgressValue(job)}%
- </span>
- </div>
- <Progress value={getProgressValue(job)} className="h-2" />
- </div>
- {/* Prompt Preview */}
- {job.prompt && (
- <div>
- <span className="font-medium">Prompt:</span>
- <p className="text-sm text-muted-foreground line-clamp-2">
- {job.prompt}
- </p>
- </div>
- )}
- {/* Error Message */}
- {job.error && (
- <div>
- <span className="font-medium text-red-500">Error:</span>
- <p className="text-sm text-red-500">
- {job.error}
- </p>
- </div>
- )}
- </div>
- {/* Actions */}
- <div className="flex flex-col gap-2 ml-4">
- {(job.status === "queued" || job.status === "processing" || job.status === "pending") && (
- <Button
- onClick={() => handleCancelJob(job.id || job.request_id || "")}
- variant="destructive"
- size="sm"
- >
- <X className="h-4 w-4 mr-2" />
- Cancel
- </Button>
- )}
- </div>
- </div>
- </CardContent>
- </Card>
- ))}
- </div>
- )}
- </div>
- </AppLayout>
- );
- }
- export default QueuePage;
|