File size: 3,070 Bytes
a1ddd34
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
import type { JobItem } from "../types";

interface JobCardProps {
  job: JobItem;
}

function getProgress(job: JobItem): number | null {
  if (typeof job.progress_percent === "number") return job.progress_percent;
  if (job.step_max && job.step_max > 0) {
    return Math.round(((job.step_value || 0) / job.step_max) * 100);
  }
  return null;
}

function statusLabel(status: string) {
  if (status === "pending") return "Queued";
  if (status === "running") return "Generating";
  if (status === "failed") return "Failed";
  return status;
}

export function JobCard({ job }: JobCardProps) {
  const progress = getProgress(job);
  const isRunning = job.status === "running";
  const isFailed = job.status === "failed";

  return (
    <div className={`
      rounded-xl overflow-hidden border aspect-video relative p-4 flex flex-col justify-between
      transition-all duration-300
      ${isFailed
        ? "border-red-800/40 bg-red-950/10"
        : "border-amber-800/30 bg-gray-950/80 hover:border-amber-700/60 hover:shadow-lg hover:shadow-amber-900/10"
      }
    `}>
      {/* Ambient gradient */}
      <div className={`absolute inset-0 bg-gradient-to-br from-amber-500/5 via-transparent to-rose-600/5 transition-opacity ${isFailed ? "opacity-20" : "opacity-100"}`} />

      {/* Status header */}
      <div className={`relative flex items-center justify-between gap-2 text-xs font-medium uppercase tracking-wide ${isFailed ? "text-red-400" : "text-amber-400"}`}>
        <span className="flex items-center gap-2">
          {isRunning && <span className="inline-block w-2 h-2 rounded-full bg-amber-400 animate-pulse" />}
          {statusLabel(job.status)}
          {job.queue_position ? ` · Queue ${job.queue_position}` : ""}
        </span>
        {progress !== null && !isFailed && <span className="tabular-nums">{progress}%</span>}
      </div>

      {/* Content */}
      <div className="relative space-y-2.5">
        <p className="text-sm font-medium line-clamp-2 text-gray-200 leading-relaxed">
          {job.prompt || "Generation job"}
        </p>

        <div className="space-y-1.5">
          <div className={`h-1.5 rounded-full overflow-hidden ${isFailed ? "bg-red-950" : "bg-gray-800"}`}>
            <div
              className={`h-full rounded-full transition-all duration-700 ${isFailed ? "bg-red-500/60" : "bg-gradient-to-r from-amber-400 to-amber-300"}`}
              style={{ width: `${isFailed ? 100 : Math.max(3, progress ?? 3)}%` }}
            />
          </div>

          <p className="text-[11px] text-gray-500 truncate">
            {job.current_node
              ? `Node: ${job.current_node}`
              : isRunning
                ? "Starting..."
                : job.status === "pending"
                  ? "Waiting for GPU..."
                  : ""}
            {job.step_max ? ` · step ${job.step_value || 0}/${job.step_max}` : ""}
          </p>
        </div>
      </div>

      {/* ID */}
      <p className="relative text-[10px] text-gray-600 font-mono truncate">{job.prompt_id}</p>
    </div>
  );
}