PixelDosa UI

Variation Grid

The results of one prompt — keep the good ones and regenerate only the rest.

A dark bottle on a stone surface

4 results

  • 1
    Version 1
  • 2
    Version 2
  • 3
    Version 3
  • 4
    Version 4

40 credits · what you kept stays.

Installation

bash
npx shadcn@latest add @pixeldosa/variation-grid

Requires `cn` at `@/lib/utils`. Render each `preview` yourself: an `img`, a poster frame, a card of text. Set `aspectRatio` to your output's shape so the grid never jumps.

Usage

"use client";

import { VariationGrid } from "@/components/ui/variation-grid";

<VariationGrid
  title="A dark bottle on a stone surface"
  items={results.map((result) => ({
    id: result.id,
    label: result.label,
    status: result.status,
    preview: <img src={result.url} alt="" className="size-full object-cover" />,
  }))}
  keptIds={kept}
  onKeptChange={setKept}
  onRegenerateRest={regenerate}
  regenerateCost="30 credits"
  onUse={insert}
/>

Why it exists

Research on how people use generation tools calls it apple picking: they keep the two results they like and run the rest again. Almost no tool supports it, so people download their favourites, re-prompt, and lose track of which was which — paying for the good ones twice.

Features

  • Keeping is a state. Kept results are marked and survive the next regeneration.
  • "Regenerate the other 3" replaces only what isn't kept, and says so.
  • The price is on the button, before the click. Unpriced iteration is the loudest complaint in this category.
  • Failures are per cell. One timed-out result shows its own reason and its own Retry; the others stay usable.
  • The grid never jumps. Set aspectRatio to your output's shape.
  • Only unfinished cells move. Kept ones hold still.

Props

PropTypeNotes
items{ id, preview?, label?, status?, note? }[]Required. You render the previews.
keptIds / onKeptChangestring[] / (ids) => voidRequired. Caller-owned, so keeping survives a regeneration.
onRegenerateRest(idsToReplace) => voidReceives exactly the ids being replaced.
regenerateCoststringShown beside the button.
onUse / useLabel(id) => void / stringTakes one result forward.
onMore(id) => voidMore like this one.
onRetry(id) => voidRetries a single failed cell.
aspectRatiostringDefaults to "1 / 1".
titlestringDefaults to "Results".
headingLevel2 | 3 | 4 | 5 | 6Defaults to 3.

Accessibility

  • A labelled <section>; the count says how many are kept and how many can be replaced.
  • Keep is a toggle button with aria-pressed and a label naming the result.
  • A failed cell states its reason in text, not only in colour.
  • Running cells say "Generating…"; their pulse stops under prefers-reduced-motion.

When not to use

  • A single result: use Generation Placeholder while it renders, then your own frame.
  • A gallery of past work: this belongs to one prompt.
  • Text alternatives in a chat: use Response Versions.

why it's built this way

Engineering Notes

The kept set is caller-owned and passed in, because it has to survive the regeneration that replaces the other cells — holding it internally would lose the user's choices exactly when the component re-renders with new results. `onRegenerateRest` hands back the ids it is replacing rather than a bare signal, so the caller can keep positions stable and never has to diff two arrays to work out what the user meant. A cell that is still running is excluded from the replaceable set, which stops a second click from cancelling work already in flight. Per-cell status rather than a batch status comes from how these pipelines actually fail: partial success is the common case, and a grid that collapses to a single error state throws away results the user already paid for. The regeneration cost is a caller-formatted string on the button because unpriced iteration is the single most common complaint in this category.

how it moves, and when it doesn't

Motion Notes

Only unfinished cells move: a running cell pulses its dot while the rest hold still, so a grid mid-regeneration reads as 'these two are working, the others are yours'. Kept cells change by border weight rather than by animation, since keeping is the user's turn and a celebratory motion there would be noise. Under `prefers-reduced-motion` the pulse stops and the running state is carried by its label.