瀏覽代碼

fix: resolve webui progress bar and queue page issues

- Fix useCallback is not defined error in queue page by adding proper import
- Add progress clamping to prevent 50+% start values in progress bars
- Add model loading state detection and display in ModelStatusBar
- Enhance progress display in both detailed and compact queue views
- Improve polling intervals during active operations

Fixes issues where:
- Queue page crashed with ReferenceError: useCallback is not defined
- Progress bars started from 50+% instead of 0% when jobs began
- Model loading progress was not visible to users
Fszontagh 3 月之前
父節點
當前提交
2e61b0bcec

+ 90 - 0
webui/app/queue/page.tsx

@@ -0,0 +1,90 @@
+'use client';
+
+import { useState, useEffect, useCallback } from 'react';
+import { Header } from '@/components/layout';
+import { AppLayout } from '@/components/layout';
+import { EnhancedQueueList } from '@/components/features/queue';
+import { apiClient, type QueueStatus, type JobInfo } from '@/lib/api';
+import { Loader2 } from 'lucide-react';
+
+export default function QueuePage() {
+  const [queueStatus, setQueueStatus] = useState<QueueStatus | null>(null);
+  const [loading, setLoading] = useState(true);
+  const [actionLoading, setActionLoading] = useState(false);
+
+  const loadQueueStatus = useCallback(async () => {
+    try {
+      const status = await apiClient.getQueueStatus();
+      setQueueStatus(status);
+    } catch (error) {
+      console.error('Failed to load queue status:', error);
+    } finally {
+      setLoading(false);
+    }
+  }, []);
+
+  useEffect(() => {
+    loadQueueStatus();
+    const interval = setInterval(loadQueueStatus, 2000);
+    return () => clearInterval(interval);
+  }, [loadQueueStatus]);
+
+  const cancelJob = useCallback(async (jobId: string) => {
+    setActionLoading(true);
+    try {
+      await apiClient.cancelJob(jobId);
+      await loadQueueStatus();
+    } catch (error) {
+      console.error('Failed to cancel job:', error);
+    } finally {
+      setActionLoading(false);
+    }
+  }, [loadQueueStatus]);
+
+  const clearQueue = useCallback(async () => {
+    setActionLoading(true);
+    try {
+      await apiClient.clearQueue();
+      await loadQueueStatus();
+    } catch (error) {
+      console.error('Failed to clear queue:', error);
+    } finally {
+      setActionLoading(false);
+    }
+  }, [loadQueueStatus]);
+
+  const copyParameters = useCallback((job: JobInfo) => {
+    // This would typically copy to clipboard and show a toast
+    console.log('Parameters copied for job:', job.id);
+  }, []);
+
+  if (loading) {
+    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-64">
+            <Loader2 className="h-8 w-8 animate-spin" />
+          </div>
+        </div>
+      </AppLayout>
+    );
+  }
+
+  return (
+    <AppLayout>
+      <Header title="Queue" description="Monitor and manage generation jobs" />
+      <div className="container mx-auto p-6">
+        <EnhancedQueueList
+          queueStatus={queueStatus}
+          loading={loading}
+          onRefresh={loadQueueStatus}
+          onCancelJob={cancelJob}
+          onClearQueue={clearQueue}
+          actionLoading={actionLoading}
+          onCopyParameters={copyParameters}
+        />
+      </div>
+    </AppLayout>
+  );
+}

+ 250 - 0
webui/components/features/models/model-status-bar.tsx

@@ -0,0 +1,250 @@
+"use client";
+
+import { useState, useEffect } from "react";
+import {
+  apiClient,
+  type ModelInfo,
+  type QueueStatus,
+  type JobInfo,
+} from "@/lib/api";
+import {
+  AlertCircle,
+  CheckCircle2,
+  Loader2,
+  Activity,
+  Image,
+} from "lucide-react";
+import { cn } from "@/lib/utils";
+
+export function ModelStatusBar() {
+  const [loadedModel, setLoadedModel] = useState<ModelInfo | null>(null);
+  const [loading, setLoading] = useState(true);
+  const [queueStatus, setQueueStatus] = useState<QueueStatus | null>(null);
+  const [activeJob, setActiveJob] = useState<JobInfo | null>(null);
+  const [recentlyCompleted, setRecentlyCompleted] = useState<JobInfo[]>([]);
+  const [modelLoading, setModelLoading] = useState(false);
+
+  useEffect(() => {
+    const checkStatus = async () => {
+      try {
+        const [loadedModels, queue] = await Promise.all([
+          apiClient.getModels(undefined, true),
+          apiClient.getQueueStatus(),
+        ]);
+
+        const currentLoadedModel = loadedModels.models.length > 0 ? loadedModels.models[0] : null;
+        setLoadedModel(currentLoadedModel);
+        setQueueStatus(queue);
+
+        // Find active/processing job
+        const processing = queue.jobs.find(
+          (job) => job.status === "processing" || job.status === "queued",
+        );
+        setActiveJob(processing || null);
+
+        // Check for model loading jobs (jobs with model-related messages)
+        const modelLoadingJob = queue.jobs.find(
+          (job) => job.status === "processing" && 
+                   job.message && 
+                   (job.message.toLowerCase().includes("model") || 
+                    job.message.toLowerCase().includes("loading"))
+        );
+        setModelLoading(!!modelLoadingJob);
+
+        // Keep track of recently completed jobs (last 30 seconds)
+        const now = Date.now();
+        const thirtySecondsAgo = now - 30000;
+
+        // Update recently completed jobs
+        const completedJobs = queue.jobs.filter(
+          (job) =>
+            job.status === "completed" &&
+            job.updated_at &&
+            new Date(job.updated_at).getTime() > thirtySecondsAgo,
+        );
+        setRecentlyCompleted(completedJobs);
+      } catch (error) {
+        console.error("Failed to check status:", error);
+      } finally {
+        setLoading(false);
+      }
+    };
+
+    checkStatus();
+
+    // Poll every 2 seconds when there's an active job, otherwise every 5 seconds
+    const pollInterval = activeJob || modelLoading ? 2000 : 5000;
+    const interval = setInterval(checkStatus, pollInterval);
+
+    return () => clearInterval(interval);
+  }, [activeJob, modelLoading]);
+
+  if (loading) {
+    return null;
+  }
+
+  // Determine status styling
+  let statusBg = "";
+  let statusBorder = "";
+  let statusText = "";
+  let icon = null;
+  let content = null;
+
+  if (modelLoading) {
+    // Model loading in progress
+    statusBg = "bg-orange-600 dark:bg-orange-700";
+    statusBorder = "border-orange-500 dark:border-orange-600";
+    statusText = "text-white";
+    icon = <Loader2 className="h-4 w-4 flex-shrink-0 animate-spin" />;
+    content = (
+      <>
+        <span className="font-semibold">Loading Model:</span>
+        <span className="truncate">
+          {activeJob?.message || "Preparing model..."}
+        </span>
+        <div className="flex items-center gap-2 ml-auto">
+          <div className="w-40 h-2.5 bg-orange-900/50 dark:bg-orange-950/50 rounded-full overflow-hidden border border-orange-400/30">
+            <div
+              className="h-full bg-orange-200 dark:bg-orange-300 transition-all duration-300"
+              style={{ width: "60%" }}
+            />
+          </div>
+          <span className="text-sm font-semibold min-w-[3rem] text-right">
+            Loading...
+          </span>
+        </div>
+      </>
+    );
+  } else if (activeJob && activeJob.status === "processing") {
+    // Active generation in progress
+    statusBg = "bg-blue-600 dark:bg-blue-700";
+    statusBorder = "border-blue-500 dark:border-blue-600";
+    statusText = "text-white";
+    icon = <Loader2 className="h-4 w-4 flex-shrink-0 animate-spin" />;
+
+    // Ensure progress is properly clamped between 0-100
+    const progress =
+      activeJob.progress !== undefined
+        ? Math.max(0, Math.min(100, Math.round(activeJob.progress * 100)))
+        : 0;
+    content = (
+      <>
+        <span className="font-semibold">Generating:</span>
+        <span className="truncate">{activeJob.id}</span>
+        <div className="flex items-center gap-2 ml-auto">
+          <div className="w-40 h-2.5 bg-blue-900/50 dark:bg-blue-950/50 rounded-full overflow-hidden border border-blue-400/30">
+            <div
+              className="h-full bg-blue-200 dark:bg-blue-300 transition-all duration-300"
+              style={{ width: `${progress}%` }}
+            />
+          </div>
+          <span className="text-sm font-semibold min-w-[3rem] text-right">
+            {progress}%
+          </span>
+        </div>
+      </>
+    );
+  } else if (activeJob && activeJob.status === "queued") {
+    // Job queued but not processing yet
+    statusBg = "bg-purple-600 dark:bg-purple-700";
+    statusBorder = "border-purple-500 dark:border-purple-600";
+    statusText = "text-white";
+    icon = <Activity className="h-4 w-4 flex-shrink-0" />;
+    content = (
+      <>
+        <span className="font-semibold">Queued:</span>
+        <span className="truncate">
+          {queueStatus?.size || 0} job(s) waiting
+        </span>
+        {activeJob.queue_position !== undefined && (
+          <span className="text-sm ml-auto">
+            Position: {activeJob.queue_position}
+          </span>
+        )}
+      </>
+    );
+  } else if (recentlyCompleted.length > 0) {
+    // Show recently completed jobs with their results
+    const latestCompleted = recentlyCompleted[0];
+    const hasOutputs =
+      (latestCompleted.outputs?.length ?? 0) > 0 ||
+      (latestCompleted.result?.images?.length ?? 0) > 0;
+
+    statusBg = "bg-green-600 dark:bg-green-700";
+    statusBorder = "border-green-500 dark:border-green-600";
+    statusText = "text-white";
+    icon = hasOutputs ? (
+      <Image className="h-4 w-4 flex-shrink-0" />
+    ) : (
+      <CheckCircle2 className="h-4 w-4 flex-shrink-0" />
+    );
+
+    const outputCount =
+      (latestCompleted.outputs?.length ?? 0) +
+      (latestCompleted.result?.images?.length ?? 0);
+    content = (
+      <>
+        <span className="font-semibold">Completed:</span>
+        <span className="truncate">{latestCompleted.id}</span>
+        {hasOutputs && (
+          <>
+            <span className="text-sm">
+              • Generated {outputCount} image{outputCount !== 1 ? "s" : ""}
+            </span>
+            <div className="flex items-center gap-2 ml-auto">
+              <div className="w-40 h-2.5 bg-green-900/50 dark:bg-green-950/50 rounded-full overflow-hidden border border-green-400/30">
+                <div
+                  className="h-full bg-green-200 dark:bg-green-300"
+                  style={{ width: "100%" }}
+                />
+              </div>
+              <span className="text-sm font-semibold min-w-[3rem] text-right">
+                100%
+              </span>
+            </div>
+          </>
+        )}
+      </>
+    );
+  } else if (loadedModel) {
+    // Model loaded, ready
+    statusBg = "bg-green-600 dark:bg-green-700";
+    statusBorder = "border-green-500 dark:border-green-600";
+    statusText = "text-white";
+    icon = <CheckCircle2 className="h-4 w-4 flex-shrink-0" />;
+    content = (
+      <>
+        <span className="font-semibold">Model Ready:</span>
+        <span className="truncate">{loadedModel.name}</span>
+        {loadedModel.sha256_short && (
+          <span className="text-sm opacity-90 ml-auto">
+            ({loadedModel.sha256_short})
+          </span>
+        )}
+      </>
+    );
+  } else {
+    // No model loaded
+    statusBg = "bg-amber-600 dark:bg-amber-700";
+    statusBorder = "border-amber-500 dark:border-amber-600";
+    statusText = "text-white";
+    icon = <AlertCircle className="h-4 w-4 flex-shrink-0" />;
+    content = (
+      <>
+        <span className="font-semibold">No Model Loaded</span>&nbsp;
+        <span className="text-sm opacity-90">
+          Please load a model from the Models page
+        </span>
+      </>
+    );
+  }
+
+  return (
+    <div className={cn("w-full px-4 py-3", statusBg, statusBorder, statusText)}>
+      <div className="flex items-center gap-3 text-sm overflow-hidden">
+        {icon}
+        <div className="flex-1 min-w-0 overflow-hidden">{content}</div>
+      </div>
+    </div>
+  );
+}

+ 1330 - 0
webui/components/features/queue/enhanced-queue-list.tsx

@@ -0,0 +1,1330 @@
+"use client";
+
+import { useState, useMemo, useCallback, useRef, useEffect } from "react";
+import {
+  Card,
+  CardContent,
+  CardDescription,
+  CardHeader,
+  CardTitle,
+} from "@/components/ui/card";
+import { Button } from "@/components/ui/button";
+import { Badge } from "@/components/ui/badge";
+import { Progress } from "@/components/ui/progress";
+import {
+  type QueueStatus,
+  type JobInfo,
+  apiClient,
+} from "@/lib/api";
+import {
+  RefreshCw,
+  Trash2,
+  CheckCircle2,
+  XCircle,
+  Loader2,
+  Clock,
+  Image,
+  Activity,
+  AlertCircle,
+  Copy,
+  Eye,
+  Calendar,
+  Settings,
+  FileText,
+  Zap,
+
+  Download,
+  X,
+
+  Video,
+
+} from "lucide-react";
+import { cn } from "@/lib/utils";
+
+interface EnhancedQueueListProps {
+  queueStatus: QueueStatus | null;
+  loading: boolean;
+  onRefresh: () => void;
+  onCancelJob: (jobId: string) => void;
+  onClearQueue: () => void;
+  actionLoading: boolean;
+  onCopyParameters?: (job: JobInfo) => void;
+}
+
+interface ImageModalProps {
+  isOpen: boolean;
+  onClose: () => void;
+  imageUrl: string;
+  title: string;
+  isVideo?: boolean;
+}
+
+function ImageModal({
+  isOpen,
+  onClose,
+  imageUrl,
+  title,
+  isVideo = false,
+}: ImageModalProps) {
+  if (!isOpen) return null;
+
+  return (
+    <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/80 p-4">
+      <div className="relative max-w-7xl max-h-full w-full">
+        <Button
+          variant="ghost"
+          size="icon"
+          onClick={onClose}
+          className="absolute -top-12 right-0 text-white hover:bg-white/20"
+        >
+          <X className="h-6 w-6" />
+        </Button>
+
+        <div className="bg-background rounded-lg shadow-2xl overflow-hidden">
+          <div className="p-4 border-b">
+            <h3 className="text-lg font-semibold truncate">{title}</h3>
+          </div>
+
+          <div className="p-4">
+            <div className="relative flex items-center justify-center min-h-[200px] max-h-[80vh]">
+              {isVideo ? (
+                <video
+                  src={imageUrl}
+                  controls
+                  className="max-w-full max-h-[80vh] rounded-lg"
+                  preload="metadata"
+                >
+                  Your browser does not support the video tag.
+                </video>
+              ) : (
+                <img
+                  src={imageUrl}
+                  alt={title}
+                  className="max-w-full max-h-[80vh] object-contain rounded-lg"
+                  onLoad={(e) => {
+                    const target = e.target as HTMLImageElement;
+                    target.style.opacity = "1";
+                  }}
+                  onError={(e) => {
+                    const target = e.target as HTMLImageElement;
+                    target.style.display = "none";
+                    const parent = target.parentElement;
+                    if (parent) {
+                      parent.innerHTML = `
+                        <div class="flex flex-col items-center justify-center text-muted-foreground p-8">
+                          <Image class="h-16 w-16 mb-4" />
+                          <p class="text-center">Failed to load image</p>
+                        </div>
+                      `;
+                    }
+                  }}
+                  style={{
+                    opacity: "0",
+                    transition: "opacity 0.3s ease-in-out",
+                  }}
+                />
+              )}
+            </div>
+          </div>
+
+          <div className="p-4 border-t flex justify-end">
+            <Button variant="outline" onClick={onClose}>
+              Close
+            </Button>
+          </div>
+        </div>
+      </div>
+    </div>
+  );
+}
+
+type ViewMode = "compact" | "detailed";
+
+// Debounce utility for frequent updates
+function useDebounce<T>(value: T, delay: number): T {
+  const [debouncedValue, setDebouncedValue] = useState<T>(value);
+
+  useEffect(() => {
+    const handler = setTimeout(() => {
+      setDebouncedValue(value);
+    }, delay);
+
+    return () => {
+      clearTimeout(handler);
+    };
+  }, [value, delay]);
+
+  return debouncedValue;
+}
+
+// Throttle utility for performance
+function useThrottle<T>(value: T, delay: number): T {
+  const [throttledValue, setThrottledValue] = useState<T>(value);
+  const lastExecuted = useRef<number>(0);
+
+  useEffect(() => {
+    const timer = setTimeout(() => {
+      const now = Date.now();
+      if (now - lastExecuted.current >= delay) {
+        setThrottledValue(value);
+        lastExecuted.current = now;
+      }
+    }, delay - (Date.now() - lastExecuted.current));
+
+    return () => clearTimeout(timer);
+  }, [value, delay]);
+
+  return throttledValue;
+}
+
+export function EnhancedQueueList({
+  queueStatus,
+  loading,
+  onRefresh,
+  onCancelJob,
+  onClearQueue,
+  actionLoading,
+  onCopyParameters,
+}: EnhancedQueueListProps) {
+  const [viewMode, setViewMode] = useState<ViewMode>("detailed");
+  const [selectedJob, setSelectedJob] = useState<string | null>(null);
+  const [lastUpdateTime, setLastUpdateTime] = useState(() => Date.now());
+  const [filterStatus, setFilterStatus] = useState<string>("all");
+  const [modalState, setModalState] = useState<{
+    isOpen: boolean;
+    imageUrl: string;
+    title: string;
+    isVideo: boolean;
+  }>({
+    isOpen: false,
+    imageUrl: "",
+    title: "",
+    isVideo: false,
+  });
+
+  // Debounce the queue status to prevent excessive updates
+  const debouncedQueueStatus = useDebounce(queueStatus, 100);
+
+  // Throttle progress updates to reduce rendering frequency
+  const throttledJobs = useThrottle(debouncedQueueStatus?.jobs || [], 200);
+
+  // Update the last update time when we get new data
+  useEffect(() => {
+    if (queueStatus?.jobs) {
+      const timer = setTimeout(() => setLastUpdateTime(Date.now()), 0);
+      return () => clearTimeout(timer);
+    }
+  }, [queueStatus?.jobs]);
+
+  // Calculate queue statistics with memoization and throttling
+  const queueStats = useMemo(() => {
+    if (!throttledJobs.length)
+      return { total: 0, active: 0, queued: 0, completed: 0, failed: 0 };
+
+    // Use throttled jobs to reduce computation frequency
+    const stats = {
+      total: throttledJobs.length,
+      active: 0,
+      queued: 0,
+      completed: 0,
+      failed: 0,
+    };
+
+    // Optimized counting with single pass
+    throttledJobs.forEach((job) => {
+      switch (job.status) {
+        case "processing":
+          stats.active++;
+          break;
+        case "queued":
+          stats.queued++;
+          break;
+        case "completed":
+          stats.completed++;
+          break;
+        case "failed":
+          stats.failed++;
+          break;
+      }
+    });
+
+    return stats;
+  }, [throttledJobs]);
+
+  // Memoized job sorting with better performance and filtering
+  const sortedJobs = useMemo(() => {
+    if (!throttledJobs.length) return [];
+
+    // Apply status filter
+    const filteredJobs =
+      filterStatus === "all"
+        ? throttledJobs
+        : throttledJobs.filter((job) => job.status === filterStatus);
+
+    const statusPriority = {
+      processing: 0,
+      queued: 1,
+      pending: 2,
+      completed: 3,
+      failed: 4,
+      cancelled: 5,
+    };
+
+    // Use a more efficient sorting approach
+    const jobsByStatus: Record<string, JobInfo[]> = {
+      processing: [],
+      queued: [],
+      pending: [],
+      completed: [],
+      failed: [],
+      cancelled: [],
+    };
+
+    // Group jobs by status first
+    filteredJobs.forEach((job) => {
+      const status = job.status as keyof typeof jobsByStatus;
+      if (jobsByStatus[status]) {
+        jobsByStatus[status].push(job);
+      }
+    });
+
+    // Sort within each status group and concatenate
+    const result: JobInfo[] = [];
+
+    Object.entries(statusPriority)
+      .sort(([, a], [, b]) => a - b)
+      .forEach(([status]) => {
+        const statusJobs = jobsByStatus[status] || [];
+        statusJobs.sort((a, b) => {
+          const timeA = new Date(a.created_at || 0).getTime();
+          const timeB = new Date(b.created_at || 0).getTime();
+          return timeB - timeA; // Newest first
+        });
+        result.push(...statusJobs);
+      });
+
+    return result;
+  }, [throttledJobs, filterStatus]);
+
+  // Memoized status icons and colors to prevent recreation
+  const statusConfig = useMemo(
+    () => ({
+      icons: {
+        completed: <CheckCircle2 className="h-4 w-4 text-green-500" />,
+        failed: <XCircle className="h-4 w-4 text-red-500" />,
+        processing: <Loader2 className="h-4 w-4 text-blue-500 animate-spin" />,
+        queued: <Clock className="h-4 w-4 text-yellow-500" />,
+        cancelled: <XCircle className="h-4 w-4 text-gray-500" />,
+        pending: <AlertCircle className="h-4 w-4 text-gray-500" />,
+      },
+      colors: {
+        completed: "text-green-600 dark:text-green-400",
+        failed: "text-red-600 dark:text-red-400",
+        processing: "text-blue-600 dark:text-blue-400",
+        queued: "text-yellow-600 dark:text-yellow-400",
+        cancelled: "text-gray-600 dark:text-gray-400",
+        pending: "text-gray-600 dark:text-gray-400",
+      },
+      badges: {
+        completed: "default" as const,
+        failed: "destructive" as const,
+        processing: "secondary" as const,
+        queued: "outline" as const,
+        cancelled: "outline" as const,
+        pending: "outline" as const,
+      },
+    }),
+    [],
+  );
+
+  const getStatusIcon = useCallback(
+    (status: string) => {
+      return (
+        statusConfig.icons[status as keyof typeof statusConfig.icons] ||
+        statusConfig.icons.pending
+      );
+    },
+    [statusConfig],
+  );
+
+  const getStatusColor = useCallback(
+    (status: string) => {
+      return (
+        statusConfig.colors[status as keyof typeof statusConfig.colors] ||
+        statusConfig.colors.pending
+      );
+    },
+    [statusConfig],
+  );
+
+  const getStatusBadgeVariant = useCallback(
+    (status: string) => {
+      return (
+        statusConfig.badges[status as keyof typeof statusConfig.badges] ||
+        "outline"
+      );
+    },
+    [statusConfig],
+  );
+
+  // Optimized duration formatting with better readability
+  const formatDuration = useCallback((startTime: string, endTime?: string) => {
+    if (!startTime) return "Unknown";
+
+    const start = new Date(startTime).getTime();
+    const end = endTime ? new Date(endTime).getTime() : Date.now();
+    const duration = Math.max(0, Math.floor((end - start) / 1000));
+
+    if (duration < 60) return `${duration}s`;
+    if (duration < 3600) {
+      const minutes = Math.floor(duration / 60);
+      const seconds = duration % 60;
+      return `${minutes}m ${seconds}s`;
+    }
+    if (duration < 86400) {
+      const hours = Math.floor(duration / 3600);
+      const minutes = Math.floor((duration % 3600) / 60);
+      return `${hours}h ${minutes}m`;
+    }
+    const days = Math.floor(duration / 86400);
+    const hours = Math.floor((duration % 86400) / 3600);
+    return `${days}d ${hours}h`;
+  }, []);
+
+  // Memoized job type detection
+  const getJobType = useCallback((job: JobInfo) => {
+    const message = (job.message || "").toLowerCase();
+    if (message.includes("text2img") || message.includes("text to image"))
+      return "Text to Image";
+    if (message.includes("img2img") || message.includes("image to image"))
+      return "Image to Image";
+    if (message.includes("upscale") || message.includes("upscaler"))
+      return "Upscale";
+    if (message.includes("convert") || message.includes("conversion"))
+      return "Model Conversion";
+    return "Unknown";
+  }, []);
+
+  const getJobTypeIcon = useCallback(
+    (job: JobInfo) => {
+      const type = getJobType(job);
+      switch (type) {
+        case "Text to Image":
+          return <Image className="h-4 w-4" />;
+        case "Image to Image":
+          return <Image className="h-4 w-4" />;
+        case "Upscale":
+          return <Zap className="h-4 w-4" />;
+        case "Model Conversion":
+          return <Settings className="h-4 w-4" />;
+        default:
+          return <FileText className="h-4 w-4" />;
+      }
+    },
+    [getJobType],
+  );
+
+  // Generate image URL from file path using the API client
+  const getImageUrl = useCallback(
+    (
+      jobId: string,
+      output: { url: string; path: string; filename?: string },
+      thumbnail: boolean = false,
+    ) => {
+      const filename = output.filename || output.path.split("/").pop();
+
+      if (!filename) {
+        console.error("No filename found for output:", output);
+        return "";
+      }
+
+      // Use the API client's getImageUrl method for consistency and proper authentication
+      const baseUrl = apiClient.getImageUrl(jobId, filename);
+
+      if (thumbnail) {
+        // Add thumbnail parameters
+        return `${baseUrl}&thumb=1&size=200`;
+      }
+
+      return baseUrl;
+    },
+    [],
+  );
+
+
+
+  // Check if file is a video based on extension
+  const isVideoFile = useCallback((filename: string) => {
+    const videoExtensions = [".mp4", ".avi", ".mov", ".mkv", ".webm", ".gif"];
+    const ext = filename.toLowerCase().substring(filename.lastIndexOf("."));
+    return videoExtensions.includes(ext);
+  }, []);
+
+  // Open image modal
+  const openImageModal = useCallback(
+    (imageUrl: string, title: string, isVideo: boolean = false) => {
+      setModalState({
+        isOpen: true,
+        imageUrl,
+        title,
+        isVideo,
+      });
+    },
+    [],
+  );
+
+  // Close image modal
+  const closeImageModal = useCallback(() => {
+    setModalState({
+      isOpen: false,
+      imageUrl: "",
+      title: "",
+      isVideo: false,
+    });
+  }, []);
+
+  // Enhanced parameter extraction
+  const extractParameters = useCallback(
+    (job: JobInfo) => {
+      const params: Record<string, string | number | boolean | undefined> = {};
+
+      // Basic job information
+      if (job.id || job.request_id) {
+        params["Job ID"] = job.id || job.request_id;
+      }
+      if (job.status) {
+        params["Status"] = job.status;
+      }
+      if (job.message) {
+        params["Message"] = job.message;
+      }
+      if (job.queue_position !== undefined) {
+        params["Queue Position"] = job.queue_position;
+      }
+      if (job.progress !== undefined) {
+        params["Progress"] = `${Math.round(job.progress * 100)}%`;
+      }
+
+      // Timing information
+      if (job.created_at) {
+        params["Created"] = new Date(job.created_at).toLocaleString();
+      }
+      if (job.updated_at && job.updated_at !== job.created_at) {
+        params["Updated"] = new Date(job.updated_at).toLocaleString();
+      }
+      if (job.created_at) {
+        params["Duration"] = formatDuration(job.created_at, job.updated_at);
+      }
+
+      // Output information
+      if (job.outputs?.length) {
+        params["Output Files"] = job.outputs.length;
+        params["Filenames"] = job.outputs
+          .map((o) => o.filename || o.path.split("/").pop())
+          .join(", ");
+      }
+      if (job.result?.images?.length) {
+        params["Legacy Images"] = job.result.images.length;
+      }
+
+      // Error information
+      if (job.error) {
+        params["Error"] = job.error;
+      }
+
+      return params;
+    },
+    [formatDuration],
+  );
+
+  // Debounced copy function to prevent spam
+  const copyParameters = useCallback(
+    (job: JobInfo) => {
+      const params = extractParameters(job);
+      if (Object.keys(params).length === 0) return;
+
+      const paramsText = Object.entries(params)
+        .map(([key, value]) => `${key}: ${value}`)
+        .join("\n");
+
+      // Use clipboard API with fallback
+      if (navigator.clipboard?.writeText) {
+        navigator.clipboard.writeText(paramsText).catch(() => {
+          // Fallback for older browsers
+          const textArea = document.createElement("textarea");
+          textArea.value = paramsText;
+          document.body.appendChild(textArea);
+          textArea.select();
+          document.execCommand("copy");
+          document.body.removeChild(textArea);
+        });
+      }
+
+      onCopyParameters?.(job);
+    },
+    [extractParameters, onCopyParameters],
+  );
+
+  // Memoized event handlers
+  const handleSelectedJobToggle = useCallback((jobId: string | null) => {
+    setSelectedJob((current) => (current === jobId ? null : jobId));
+  }, []);
+
+  const handleCancelJob = useCallback(
+    (jobId: string) => {
+      onCancelJob(jobId);
+    },
+    [onCancelJob],
+  );
+
+  // Progress update optimization - only update progress if it changed significantly
+  const ProgressBar = useCallback(
+    ({ job }: { job: JobInfo }) => {
+      if (job.progress === undefined) return null;
+
+      // Ensure progress is properly clamped between 0-100
+      const progressValue = Math.max(0, Math.min(100, job.progress * 100));
+
+      // Get appropriate status message based on actual job status
+      const getStatusMessage = () => {
+        switch (job.status) {
+          case "completed":
+            return "Completed";
+          case "failed":
+            return job.error || "Failed";
+          case "cancelled":
+            return "Cancelled";
+          case "queued":
+            return `Queued${job.queue_position ? ` (Position: ${job.queue_position})` : ""}`;
+          case "processing":
+            return job.message || "Processing...";
+          default:
+            return job.message || "Pending...";
+        }
+      };
+
+      return (
+        <div className="space-y-2">
+          <div className="flex items-center justify-between text-sm">
+            <span className={cn("font-medium", getStatusColor(job.status))}>
+              {getStatusMessage()}
+            </span>
+            {job.status === "processing" && (
+              <span className="text-muted-foreground">
+                {Math.round(progressValue)}%
+              </span>
+            )}
+          </div>
+          {job.status === "processing" && (
+            <Progress value={progressValue} className="h-2" />
+          )}
+        </div>
+      );
+    },
+    [getStatusColor],
+  );
+
+  // Skip rendering if no meaningful changes
+  if (!queueStatus && !loading) {
+    return (
+      <div className="space-y-6">
+        <Card>
+          <CardContent className="text-center py-12">
+            <Activity className="h-12 w-12 text-muted-foreground mx-auto mb-4" />
+            <p className="text-muted-foreground">Loading queue status...</p>
+          </CardContent>
+        </Card>
+      </div>
+    );
+  }
+
+  return (
+    <div className="space-y-6">
+      {/* Queue Status */}
+      <Card>
+        <CardHeader>
+          <div className="flex flex-col gap-4">
+            <div className="flex items-center justify-between">
+              <div>
+                <CardTitle>Queue Status</CardTitle>
+                <CardDescription>
+                  Current queue status and statistics
+                  {lastUpdateTime && (
+                    <span className="text-xs text-muted-foreground block">
+                      Last updated:{" "}
+                      {new Date(lastUpdateTime).toLocaleTimeString()}
+                    </span>
+                  )}
+                </CardDescription>
+              </div>
+              <div className="flex gap-2">
+                <Button
+                  variant={viewMode === "detailed" ? "default" : "outline"}
+                  size="sm"
+                  onClick={() => setViewMode("detailed")}
+                >
+                  <Eye className="h-4 w-4 mr-2" />
+                  Detailed
+                </Button>
+                <Button
+                  variant={viewMode === "compact" ? "default" : "outline"}
+                  size="sm"
+                  onClick={() => setViewMode("compact")}
+                >
+                  <Activity className="h-4 w-4 mr-2" />
+                  Compact
+                </Button>
+                <Button onClick={onRefresh} disabled={loading}>
+                  <RefreshCw
+                    className={`h-4 w-4 mr-2 ${loading ? "animate-spin" : ""}`}
+                  />
+                  Refresh
+                </Button>
+                <Button
+                  variant="outline"
+                  onClick={onClearQueue}
+                  disabled={actionLoading || !throttledJobs.length}
+                >
+                  <Trash2 className="h-4 w-4 mr-2" />
+                  Clear Queue
+                </Button>
+              </div>
+            </div>
+
+            {/* Filter Controls */}
+            <div className="flex items-center gap-4 flex-wrap">
+              <span className="text-sm font-medium">Filter:</span>
+              <div className="flex gap-2">
+                <Button
+                  variant={filterStatus === "all" ? "default" : "outline"}
+                  size="sm"
+                  onClick={() => setFilterStatus("all")}
+                >
+                  All ({queueStats.total})
+                </Button>
+                <Button
+                  variant={
+                    filterStatus === "processing" ? "default" : "outline"
+                  }
+                  size="sm"
+                  onClick={() => setFilterStatus("processing")}
+                >
+                  Active ({queueStats.active})
+                </Button>
+                <Button
+                  variant={filterStatus === "queued" ? "default" : "outline"}
+                  size="sm"
+                  onClick={() => setFilterStatus("queued")}
+                >
+                  Queued ({queueStats.queued})
+                </Button>
+                <Button
+                  variant={filterStatus === "completed" ? "default" : "outline"}
+                  size="sm"
+                  onClick={() => setFilterStatus("completed")}
+                >
+                  Completed ({queueStats.completed})
+                </Button>
+                <Button
+                  variant={filterStatus === "failed" ? "default" : "outline"}
+                  size="sm"
+                  onClick={() => setFilterStatus("failed")}
+                >
+                  Failed ({queueStats.failed})
+                </Button>
+              </div>
+            </div>
+          </div>
+        </CardHeader>
+        <CardContent>
+          <div className="grid grid-cols-2 md:grid-cols-5 gap-4">
+            <div className="text-center">
+              <div className="text-2xl font-bold">{queueStats.total}</div>
+              <div className="text-sm text-muted-foreground">Total Jobs</div>
+            </div>
+            <div className="text-center">
+              <div className="text-2xl font-bold text-blue-600">
+                {queueStats.active}
+              </div>
+              <div className="text-sm text-muted-foreground">Active</div>
+            </div>
+            <div className="text-center">
+              <div className="text-2xl font-bold text-yellow-600">
+                {queueStats.queued}
+              </div>
+              <div className="text-sm text-muted-foreground">Queued</div>
+            </div>
+            <div className="text-center">
+              <div className="text-2xl font-bold text-green-600">
+                {queueStats.completed}
+              </div>
+              <div className="text-sm text-muted-foreground">Completed</div>
+            </div>
+            <div className="text-center">
+              <div className="text-2xl font-bold text-red-600">
+                {queueStats.failed}
+              </div>
+              <div className="text-sm text-muted-foreground">Failed</div>
+            </div>
+          </div>
+        </CardContent>
+      </Card>
+
+      {/* Jobs List */}
+      <div className="space-y-4">
+        {sortedJobs.map((job) => {
+          const jobId = job.id || job.request_id;
+          if (!jobId) return null;
+
+          return (
+            <Card
+              key={jobId}
+              className={cn(
+                "transition-all hover:shadow-md",
+                job.status === "processing" &&
+                "border-blue-200 bg-blue-50/50 dark:border-blue-800 dark:bg-blue-950/20",
+                job.status === "completed" &&
+                "border-green-200 bg-green-50/50 dark:border-green-800 dark:bg-green-950/20",
+                job.status === "failed" &&
+                "border-red-200 bg-red-50/50 dark:border-red-800 dark:bg-red-950/20",
+              )}
+            >
+              <CardContent className="p-6">
+                {viewMode === "detailed" ? (
+                  <div className="space-y-4">
+                    {/* Header */}
+                    <div className="flex items-center justify-between">
+                      <div className="flex items-center gap-3">
+                        {getStatusIcon(job.status)}
+                        <div>
+                          <h3 className="font-semibold">Job {jobId}</h3>
+                          <div className="flex items-center gap-2 text-sm text-muted-foreground">
+                            {getJobTypeIcon(job)}
+                            <span>{getJobType(job)}</span>
+                            {job.queue_position !== undefined && (
+                              <span>• Position: {job.queue_position}</span>
+                            )}
+                          </div>
+                        </div>
+                      </div>
+                      <div className="flex items-center gap-2">
+                        <Badge variant={getStatusBadgeVariant(job.status)}>
+                          {job.status}
+                        </Badge>
+                        {(job.status === "queued" ||
+                          job.status === "processing") && (
+                            <Button
+                              variant="outline"
+                              size="sm"
+                              onClick={() => handleCancelJob(jobId)}
+                              disabled={actionLoading}
+                            >
+                              <XCircle className="h-4 w-4" />
+                              Cancel
+                            </Button>
+                          )}
+                      </div>
+                    </div>
+
+                    {/* Progress */}
+                    <ProgressBar job={job} />
+
+                    {/* Details Grid */}
+                    <div className="grid grid-cols-2 md:grid-cols-4 gap-4 text-sm">
+                      <div>
+                        <span className="text-muted-foreground flex items-center gap-1">
+                          <Calendar className="h-3 w-3" />
+                          Created
+                        </span>
+                        <p className="font-medium">
+                          {job.created_at
+                            ? new Date(job.created_at).toLocaleString()
+                            : "Unknown"}
+                        </p>
+                      </div>
+                      {job.updated_at && job.updated_at !== job.created_at && (
+                        <div>
+                          <span className="text-muted-foreground flex items-center gap-1">
+                            <Clock className="h-3 w-3" />
+                            Updated
+                          </span>
+                          <p className="font-medium">
+                            {new Date(job.updated_at).toLocaleString()}
+                          </p>
+                        </div>
+                      )}
+                      {job.created_at && (
+                        <div>
+                          <span className="text-muted-foreground flex items-center gap-1">
+                            <Clock className="h-3 w-3" />
+                            Duration
+                          </span>
+                          <p className="font-medium">
+                            {formatDuration(job.created_at, job.updated_at)}
+                          </p>
+                        </div>
+                      )}
+                      <div>
+                        <span className="text-muted-foreground flex items-center gap-1">
+                          <FileText className="h-3 w-3" />
+                          Parameters
+                        </span>
+                        <div className="flex gap-1">
+                          <Button
+                            variant="ghost"
+                            size="sm"
+                            className="h-6 px-2 text-xs"
+                            onClick={() => copyParameters(job)}
+                          >
+                            <Copy className="h-3 w-3 mr-1" />
+                            Copy
+                          </Button>
+                          <Button
+                            variant="ghost"
+                            size="sm"
+                            className="h-6 px-2 text-xs"
+                            onClick={() =>
+                              handleSelectedJobToggle(
+                                selectedJob === jobId ? null : jobId,
+                              )
+                            }
+                          >
+                            {selectedJob === jobId ? "Hide" : "Show"}
+                          </Button>
+                        </div>
+                      </div>
+                    </div>
+
+                    {/* Expanded Parameters */}
+                    {selectedJob === jobId && (
+                      <div className="bg-muted/50 rounded-lg p-4 space-y-3">
+                        <div className="flex items-center justify-between">
+                          <h4 className="font-medium text-sm">
+                            Job Details & Parameters
+                          </h4>
+                          <Button
+                            variant="ghost"
+                            size="sm"
+                            className="h-6 px-2 text-xs"
+                            onClick={() => copyParameters(job)}
+                          >
+                            <Copy className="h-3 w-3 mr-1" />
+                            Copy All
+                          </Button>
+                        </div>
+                        <div className="grid gap-3 text-sm">
+                          {(() => {
+                            const params = extractParameters(job);
+                            const entries = Object.entries(params);
+                            return entries.length > 0 ? (
+                              entries.map(([key, value]) => (
+                                <div
+                                  key={key}
+                                  className="flex justify-between items-start gap-2"
+                                >
+                                  <span className="text-muted-foreground capitalize min-w-[100px] text-xs">
+                                    {key}:
+                                  </span>
+                                  <span className="font-mono text-xs break-all flex-1 text-right">
+                                    {String(value)}
+                                  </span>
+                                </div>
+                              ))
+                            ) : (
+                              <p className="text-muted-foreground text-sm">
+                                No parameters available
+                              </p>
+                            );
+                          })()}
+                        </div>
+                      </div>
+                    )}
+
+                    {/* Results - Enhanced with better thumbnails and click-to-view */}
+                    {job.status === "completed" &&
+                      job.outputs &&
+                      job.outputs.length > 0 && (
+                        <div className="space-y-3">
+                          <div className="flex items-center justify-between">
+                            <h4 className="font-medium text-sm flex items-center gap-2">
+                              <Image className="h-4 w-4" />
+                              Generated Images ({job.outputs.length})
+                            </h4>
+                            <div className="text-xs text-muted-foreground">
+                              Click image to view full size • Hover to download
+                            </div>
+                          </div>
+                          <div className="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-6 gap-3">
+                            {job.outputs.map((output, index) => {
+                              const isVideo = isVideoFile(
+                                output.filename || "",
+                              );
+                              return (
+                                <div key={index} className="relative group">
+                                  <div className="aspect-square bg-muted rounded-lg overflow-hidden border border-border/50 hover:border-border transition-all duration-200 cursor-pointer">
+                                    {isVideo ? (
+                                      <video
+                                        src={getImageUrl(jobId, output)}
+                                        className="w-full h-full object-cover"
+                                        muted
+                                        loop
+                                        playsInline
+                                        onMouseEnter={(e) => {
+                                          const video =
+                                            e.target as HTMLVideoElement;
+                                          video.play().catch(() => { });
+                                        }}
+                                        onMouseLeave={(e) => {
+                                          const video =
+                                            e.target as HTMLVideoElement;
+                                          video.pause();
+                                          video.currentTime = 0;
+                                        }}
+                                        onClick={() =>
+                                          openImageModal(
+                                            getImageUrl(jobId, output, false),
+                                            `Generated ${output.filename || `video ${index + 1}`}`,
+                                            true,
+                                          )
+                                        }
+                                      />
+                                    ) : (
+                                      <img
+                                        src={getImageUrl(jobId, output, true)}
+                                        alt={`Generated image ${index + 1}`}
+                                        className="w-full h-full object-cover transition-transform duration-200 group-hover:scale-105"
+                                        loading="lazy"
+                                        onClick={() =>
+                                          openImageModal(
+                                            getImageUrl(jobId, output, false),
+                                            `Generated ${output.filename || `image ${index + 1}`}`,
+                                            false,
+                                          )
+                                        }
+                                        onError={(e) => {
+                                          // Enhanced fallback for failed images
+                                          const target =
+                                            e.target as HTMLImageElement;
+                                          target.style.display = "none";
+                                          const parent = target.parentElement;
+                                          if (parent) {
+                                            parent.innerHTML = `
+                                            <div class="w-full h-full flex items-center justify-center text-muted-foreground bg-muted/50">
+                                              <div class="text-center">
+                                                <svg class="h-8 w-8 mx-auto mb-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+                                                  <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"></path>
+                                                </svg>
+                                                <p class="text-xs">Load failed</p>
+                                              </div>
+                                            </div>
+                                          `;
+                                          }
+                                        }}
+                                      />
+                                    )}
+                                  </div>
+
+                                  {/* Hover overlay with actions */}
+                                  <div className="absolute inset-0 bg-black/60 opacity-0 group-hover:opacity-100 transition-all duration-200 rounded-lg flex items-center justify-center gap-2">
+                                    <Button
+                                      variant="secondary"
+                                      size="sm"
+                                      className="h-8 px-2 text-xs"
+                                      onClick={(e) => {
+                                        e.stopPropagation();
+                                        openImageModal(
+                                          getImageUrl(jobId, output, false),
+                                          `Generated ${output.filename || `image ${index + 1}`}`,
+                                          isVideo,
+                                        );
+                                      }}
+                                    >
+                                      <Eye className="h-3 w-3 mr-1" />
+                                      View
+                                    </Button>
+                                    <Button
+                                      variant="secondary"
+                                      size="sm"
+                                      className="h-8 px-2 text-xs"
+                                      onClick={(e) => {
+                                        e.stopPropagation();
+                                        const url = getImageUrl(jobId, output);
+                                        const link =
+                                          document.createElement("a");
+                                        link.href = url;
+                                        link.download =
+                                          output.filename ||
+                                          `generated-${index + 1}.${isVideo ? "mp4" : "png"}`;
+                                        document.body.appendChild(link);
+                                        link.click();
+                                        document.body.removeChild(link);
+                                      }}
+                                    >
+                                      <Download className="h-3 w-3 mr-1" />
+                                      Save
+                                    </Button>
+                                  </div>
+
+                                  {/* File type indicator */}
+                                  <div className="absolute top-2 right-2">
+                                    {isVideo && (
+                                      <div className="bg-black/70 text-white text-xs px-1.5 py-0.5 rounded flex items-center gap-1">
+                                        <Video className="h-3 w-3" />
+                                        Video
+                                      </div>
+                                    )}
+                                  </div>
+                                </div>
+                              );
+                            })}
+                          </div>
+                        </div>
+                      )}
+
+                    {/* Results - Enhanced backwards compatibility for result.images */}
+                    {job.status === "completed" &&
+                      job.result?.images &&
+                      job.result.images.length > 0 && (
+                        <div className="space-y-3">
+                          <div className="flex items-center justify-between">
+                            <h4 className="font-medium text-sm flex items-center gap-2">
+                              <Image className="h-4 w-4" />
+                              Generated Images ({job.result.images.length})
+                            </h4>
+                            <div className="text-xs text-muted-foreground">
+                              Legacy format • Click to view
+                            </div>
+                          </div>
+                          <div className="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-6 gap-3">
+                            {job.result.images.map((imageData, index) => (
+                              <div key={index} className="relative group">
+                                <div className="aspect-square bg-muted rounded-lg overflow-hidden border border-border/50 hover:border-border transition-all duration-200 cursor-pointer">
+                                  {imageData.startsWith("data:image") ? (
+                                    <img
+                                      src={imageData}
+                                      alt={`Generated image ${index + 1}`}
+                                      className="w-full h-full object-cover transition-transform duration-200 group-hover:scale-105"
+                                      loading="lazy"
+                                      onClick={() =>
+                                        openImageModal(
+                                          imageData,
+                                          `Generated legacy image ${index + 1}`,
+                                          false,
+                                        )
+                                      }
+                                    />
+                                  ) : (
+                                    <div
+                                      className="w-full h-full flex items-center justify-center text-muted-foreground bg-muted/50 cursor-pointer"
+                                      onClick={() =>
+                                        openImageModal(
+                                          imageData,
+                                          `Generated image ${index + 1}`,
+                                          false,
+                                        )
+                                      }
+                                    >
+                                      <div className="text-center">
+                                        <Image className="h-8 w-8 mx-auto mb-2" />
+                                        <p className="text-xs">No preview</p>
+                                      </div>
+                                    </div>
+                                  )}
+                                </div>
+
+                                {/* Hover overlay */}
+                                <div className="absolute inset-0 bg-black/60 opacity-0 group-hover:opacity-100 transition-all duration-200 rounded-lg flex items-center justify-center gap-2">
+                                  <Button
+                                    variant="secondary"
+                                    size="sm"
+                                    className="h-8 px-2 text-xs"
+                                    onClick={(e) => {
+                                      e.stopPropagation();
+                                      openImageModal(
+                                        imageData,
+                                        `Generated image ${index + 1}`,
+                                        false,
+                                      );
+                                    }}
+                                  >
+                                    <Eye className="h-3 w-3 mr-1" />
+                                    View
+                                  </Button>
+                                  {imageData.startsWith("data:image") && (
+                                    <Button
+                                      variant="secondary"
+                                      size="sm"
+                                      className="h-8 px-2 text-xs"
+                                      onClick={(e) => {
+                                        e.stopPropagation();
+                                        const link =
+                                          document.createElement("a");
+                                        link.href = imageData;
+                                        link.download = `generated-image-${index + 1}.png`;
+                                        document.body.appendChild(link);
+                                        link.click();
+                                        document.body.removeChild(link);
+                                      }}
+                                    >
+                                      <Download className="h-3 w-3 mr-1" />
+                                      Save
+                                    </Button>
+                                  )}
+                                </div>
+                              </div>
+                            ))}
+                          </div>
+                        </div>
+                      )}
+
+                    {/* Error */}
+                    {job.error && (
+                      <div className="bg-red-50 dark:bg-red-950/20 border border-red-200 dark:border-red-800 rounded-lg p-3">
+                        <p className="text-sm text-red-700 dark:text-red-300">
+                          <strong>Error:</strong> {job.error}
+                        </p>
+                      </div>
+                    )}
+                  </div>
+                ) : (
+                  /* Compact View with thumbnails */
+                  <div className="flex items-center justify-between gap-4">
+                    <div className="flex items-center gap-4 flex-1 min-w-0">
+                      {getStatusIcon(job.status)}
+                      <div className="flex-1 min-w-0">
+                        <div className="flex items-center gap-2 mb-1">
+                          <h3 className="font-semibold truncate">
+                            Job {jobId}
+                          </h3>
+                          <Badge
+                            variant={getStatusBadgeVariant(job.status)}
+                            className="text-xs"
+                          >
+                            {job.status}
+                          </Badge>
+                        </div>
+                        <div className="flex items-center gap-2 text-sm text-muted-foreground mb-2">
+                          {getJobTypeIcon(job)}
+                          <span className="truncate">{getJobType(job)}</span>
+                          {job.queue_position !== undefined && (
+                            <span>• Pos: {job.queue_position}</span>
+                          )}
+                          {job.created_at && (
+                            <span>
+                              • {new Date(job.created_at).toLocaleTimeString()}
+                            </span>
+                          )}
+                        </div>
+                        {job.message && job.status !== "completed" && (
+                          <p className="text-sm text-muted-foreground truncate">
+                            {job.message}
+                          </p>
+                        )}
+
+                        {/* Thumbnail preview for completed jobs in compact view */}
+                        {job.status === "completed" &&
+                          (job.outputs?.length ||
+                            job.result?.images?.length) && (
+                            <div className="flex gap-1 mt-2">
+                              {(job.outputs || [])
+                                .slice(0, 3)
+                                .map((output, index) => (
+                                  <div key={index} className="relative group">
+                                    <div className="w-12 h-12 bg-muted rounded overflow-hidden border border-border/50">
+                                      <img
+                                        src={getImageUrl(jobId, output, true)}
+                                        alt={`Generated ${index + 1}`}
+                                        className="w-full h-full object-cover"
+                                        loading="lazy"
+                                        onClick={() =>
+                                          openImageModal(
+                                            getImageUrl(jobId, output, false),
+                                            `Generated ${output.filename || `image ${index + 1}`}`,
+                                            isVideoFile(output.filename || ""),
+                                          )
+                                        }
+                                      />
+                                    </div>
+                                  </div>
+                                ))}
+                              {(job.outputs || job.result?.images || [])
+                                .length > 3 && (
+                                  <div className="w-12 h-12 bg-muted rounded border border-border/50 flex items-center justify-center text-xs text-muted-foreground">
+                                    +
+                                    {(job.outputs || job.result?.images || [])
+                                      .length - 3}
+                                  </div>
+                                )}
+                            </div>
+                          )}
+                      </div>
+                    </div>
+                    <div className="flex items-center gap-4 flex-shrink-0">
+                      {job.progress !== undefined &&
+                        job.status === "processing" && (
+                          <div className="text-right">
+                            <div className="text-sm font-medium">
+                              {Math.max(0, Math.min(100, Math.round(job.progress * 100)))}%
+                            </div>
+                            <div className="w-24 h-1.5 bg-gray-200 rounded-full overflow-hidden">
+                              <div
+                                className="h-full bg-blue-500 transition-all duration-300"
+                                style={{ width: `${Math.max(0, Math.min(100, job.progress * 100))}%` }}
+                              />
+                            </div>
+                          </div>
+                        )}
+                      {(job.status === "queued" ||
+                        job.status === "processing") && (
+                          <Button
+                            variant="outline"
+                            size="sm"
+                            onClick={() => handleCancelJob(jobId)}
+                            disabled={actionLoading}
+                          >
+                            <XCircle className="h-4 w-4" />
+                          </Button>
+                        )}
+                    </div>
+                  </div>
+                )}
+              </CardContent>
+            </Card>
+          );
+        })}
+      </div>
+
+      {(!throttledJobs || throttledJobs.length === 0) && (
+        <Card>
+          <CardContent className="text-center py-12">
+            <Activity className="h-12 w-12 text-muted-foreground mx-auto mb-4" />
+            <h3 className="text-lg font-semibold mb-2">
+              {filterStatus === "all"
+                ? "No jobs in queue"
+                : `No ${filterStatus} jobs`}
+            </h3>
+            <p className="text-muted-foreground mb-4">
+              {filterStatus === "all"
+                ? "Start a generation to see jobs appear here."
+                : `Try changing the filter to see more jobs.`}
+            </p>
+            {filterStatus !== "all" && (
+              <Button variant="outline" onClick={() => setFilterStatus("all")}>
+                Show All Jobs
+              </Button>
+            )}
+          </CardContent>
+        </Card>
+      )}
+
+      {/* Image Modal */}
+      <ImageModal
+        isOpen={modalState.isOpen}
+        onClose={closeImageModal}
+        imageUrl={modalState.imageUrl}
+        title={modalState.title}
+        isVideo={modalState.isVideo}
+      />
+    </div>
+  );
+}