Installation
npx shadcn@latest add @pixeldosa/generation-jobRequires `cn` at `@/lib/utils`. Pass `progress` only when you measure it; the component deliberately draws nothing otherwise.
Usage
"use client";
import { GenerationJob } from "@/components/ui/generation-job";
<GenerationJob
status={job.status}
title="4 product images · 1024 × 1024"
queue={{ position: job.queuePosition, typicalWait: "2 to 4 minutes" }}
stage={job.stage}
progress={job.measuredProgress}
startedAt={job.startedAt}
cost="40 credits"
refundNote="Anything that fails is refunded"
onCancel={job.cancel}
>
{job.outputs.map((output) => <img key={output.id} src={output.url} alt="" />)}
</GenerationJob>
Pass progress only when you measure it. Without it, no bar is drawn.
Why it exists
Reviews of image and video tools rank credits and unexplained waiting above quality. Queues of ten to thirty minutes are normal at peak, and people are given a spinner, no position, no estimate, and no idea what happens to their credits when a job fails.
Features
- The queue is a real state: position, plus what the wait usually is at this moment.
- No invented progress. A bar appears only when you pass a measured
progress. - Partial results show immediately. With
status="partial", two finished images of four are two finished images, not a spinner. - The refund rule is stated before a failure, and what happened to the money after one.
- Nobody has to watch. A "Tell me when it's done" checkbox, so long jobs don't hold someone at the screen.
- Stop stays visible for the whole run, and the elapsed counter ticks itself.
Variants
In the queue
A 6-second clip · 1080p
In the queue
Number 4 in the queue. Usually 8 to 12 minutes at this time.
120 credits. Not charged until it starts.
Failed, with the credits back
4 lifestyle images · 1536 × 1024
Failed
The model timed out after 4 minutes. Your 40 credits are back.
40 credits.
Props
| Prop | Type | Notes |
|---|---|---|
status | "queued" | "running" | "partial" | "done" | "failed" | "cancelled" | Required. |
title | string | Required. What's being made. |
queue | { position?, typicalWait? } | Say what you know. |
stage | string | "Rendering 3 of 4". Shimmers while running. |
progress | number | Measured, 0–1. Omit and no bar is drawn. |
startedAt | number | Date | Drives the elapsed counter. |
cost / refundNote | string | Formatted by you. |
error | { message, refunded? } | The reason, and what happened to the money. |
notify / onNotifyChange | boolean / (v) => void | "Tell me when it's done". |
onCancel / onRetry | () => void | Stop while active; Try again after a failure. |
children | ReactNode | Finished outputs, shown as they land. |
Accessibility
- A labelled
<section>with a real heading. - Status, queue position and failures are announced once through a polite live region — the ticking counter is not announced.
- The progress bar is decorative; the words carry the same information.
- The status dot's pulse stops under
prefers-reduced-motion.
When not to use
- Work that takes a second or two: use
Generation Placeholder. - An agent run with steps: use
Agent Planwith statuses. - A list of past jobs: this is one job. A queue of many is a different surface.
why it's built this way
Engineering Notes
Built against the complaints this category actually generates: reviews of image and video tools rank credits and unexplained waits above quality, and queue times of ten to thirty minutes at peak are normal. So the queue is a first-class state with a position and a typical wait rather than a spinner; `progress` is optional and a bar is drawn only when it is passed, because an invented bar on a job whose extent is unknown is the same lie as fake typing; `partial` exists as a status so a batch with two of four finished can show two, which the pipeline literature treats as the correct outcome rather than a pass or fail; and `error` carries a refund string beside the reason, because a failure that silently keeps someone's credits is the complaint underneath most one-star reviews. The elapsed counter ticks inside the component — the one place this system breaks its caller-owns-async rule — so a minutes-long job doesn't force a re-render a second on the consumer.
how it moves, and when it doesn't
Motion Notes
Motion means the machine is busy. The status dot pulses while queued, running or partial, the stage verb shimmers while work is ongoing, and every bit of it stops the moment the job reaches a terminal state. A measured bar animates its width over `--pd-duration-slow` with a linear ease, because it tracks real progress rather than decorating a wait. Under `prefers-reduced-motion` the pulse and shimmer are dropped and the state is carried by the words and the colour.