Image Generation Studio
A one-screen generation studio — prompt composer and model picker beside a feed of queued, staged, blocked and ready image results with variants.
image-studio.tsx210 lines3 componentsReact + Tailwind, no dependencies
npx shadcn@latest add https://scrimui.dev/r/image-studio.jsonAgent promptClaude Code · Cursor · any agent
Add the Image Generation Studio pattern from the Scrim UI registry to this project — a complete screen, not a single component.
Image Generation Studio — A one-screen generation studio — prompt composer and model picker beside a feed of queued, staged, blocked and ready image results with variants.
## 1. Install
```bash
npx shadcn@latest add https://scrimui.dev/r/image-studio.json
```
This writes the screen to `components/blocks/image-studio.tsx` and pulls in the components it is built from, each landing at `components/ui/`. Everything is plain React + Tailwind with no runtime dependencies. The imports in the block already point at those paths, so it compiles as installed.
## 2. What it is made of
- `generated-media` — Generated Media Result
- `prompt-input` — Prompt Input
- `model-selector` — Model Selector
Each is a separate file you can edit or replace on its own; the block is the arrangement, not a monolith.
## 3. Rules this layout depends on
Keep these when adapting the screen — they are the reasons it works, and they are easy to break while restyling:
- Put the prompt in the feed the moment it's submitted — a queued card with a position beats a spinner over nothing.
- Stage generation in words and keep the card's shape fixed from queued to ready.
- Return variants per prompt and make switching between them cheap and reversible.
- Split blocked from failed: policy refusals ask for a rephrase, worker failures offer a retry that works.
- Keep every result's prompt one click from the composer — re-use is the core loop of a studio.
## 4. Do not do these
- A generation that vanishes into a global loading state, leaving the user nothing to read or cancel.
- Treating a safety block as an error with Retry — the retry will just be blocked again.
- New results replacing old ones instead of accumulating — a studio is a feed, not a lightbox.
- Params that live only in a tooltip; if the user can't compare two results' settings, variants are lottery tickets.
The demo content in the file — messages, file names, model names — is placeholder. Replace it with this project's real data and wire the handlers to real state rather than shipping the stubs.
Reference: https://scrimui.dev/patterns/image-studioInstalls the screen and every component it is built from, and carries the layout rules from this page so an agent does not restyle them away.
Live Preview
“A mountain ridge at dusk, soft gradient sky, minimal illustration”
Built from these components
The result card for AI-generated images, audio and video — queue and stage states, variants, download and regenerate, and a safety block that isn't an error.
The message input at the heart of an AI chat app — file attachments, model picker, tool toggles, voice, and a send button that turns into stop.
A standalone model picker — compare models in a dropdown with badges for speed, context length and what each supports.
Pattern code
This file composes the components above. Copy each component from its page, then this pattern file wires them together.
"use client";
import * as React from "react";
import { PromptInput } from "../../prompt-input/prompt-input";
import { ModelSelector } from "../../model-selector/model-selector";
import { GeneratedMediaResult, type MediaStatus } from "../../generated-media/generated-media";
/** CSS-painted stand-in for a generated image — patterns ship no assets. */
function ImageMock({ hue = 210 }: { hue?: number }) {
return (
<div
role="img"
aria-label="Generated illustration: a mountain ridge at dusk"
className="flex h-full min-h-[220px] w-full items-end p-4"
style={{
background: `linear-gradient(180deg, hsl(${hue} 60% 75%) 0%, hsl(${hue} 55% 45%) 60%, hsl(${hue} 50% 25%) 100%)`,
}}
>
<svg viewBox="0 0 400 80" className="w-full" aria-hidden="true">
<path d="M0 80 L90 20 L160 60 L240 10 L320 55 L400 30 L400 80 Z" fill="hsl(0 0% 100% / 0.25)" />
<path d="M0 80 L120 45 L220 70 L340 40 L400 60 L400 80 Z" fill="hsl(0 0% 0% / 0.2)" />
</svg>
</div>
);
}
/**
* A one-screen image generation studio.
*
* The flow this pattern exists to show:
*
* 1. **The queue is visible.** A submitted prompt enters the feed as a
* queued card immediately — position stated, no spinner-and-nothing.
* 2. **Generation is staged, then it settles.** Queued → generating (stage
* in words) → ready; the card never changes shape underneath the reader.
* 3. **Results come back as variants.** One prompt, three takes — picking
* one is cheap and reversible.
* 4. **Blocked and failed are different days.** The second generation in
* this script hits the content policy (rephrase, no retry); the third
* fails on the worker (retry, which then succeeds).
* 5. **The prompt is the re-use path.** Clicking a result's prompt loads it
* back into the composer.
*/
type Result = {
id: number;
kind: "image";
status: MediaStatus;
prompt: string;
params: string[];
stage?: string;
queuePosition?: number;
variants: { id: string }[];
currentVariantId: string;
hue: number;
errorMessage?: string;
blockedReason?: string;
};
const STAGES = ["Reading the prompt…", "Composing the scene…", "Diffusing latents…", "Upsampling…"];
const MODELS = [
{ id: "fable-image", name: "Fable Image 2", hint: "Quality" },
{ id: "sketch", name: "Sketch Turbo", hint: "Fast drafts" },
];
const INITIAL_RESULTS: Result[] = [
{
id: 1,
kind: "image",
status: "ready",
prompt: "A mountain ridge at dusk, soft gradient sky, minimal illustration",
params: ["1024×1024", "seed 4815", "illustration"],
variants: [{ id: "v1" }, { id: "v2" }, { id: "v3" }],
currentVariantId: "v1",
hue: 210,
},
];
export function ImageStudioPattern() {
const [results, setResults] = React.useState<Result[]>(INITIAL_RESULTS);
const [model, setModel] = React.useState("fable-image");
const [draftPrompt, setDraftPrompt] = React.useState("");
const idRef = React.useRef(2);
const timers = React.useRef<number[]>([]);
function patch(id: number, p: Partial<Result>) {
setResults((rs) => rs.map((r) => (r.id === id ? { ...r, ...p } : r)));
}
function schedule(id: number, fn: () => void, ms: number) {
timers.current.push(window.setTimeout(fn, ms));
}
/** The scripted outcomes: 1st new generation succeeds, 2nd is policy-
blocked, 3rd fails (its Retry succeeds), 4th+ succeed again. */
function runGeneration(prompt: string) {
const id = idRef.current++;
const attempt = results.length; // initial card counts as a past success
const outcome = attempt % 3 === 1 ? "blocked" : attempt % 3 === 2 ? "failed" : "ready";
const hue = [160, 330, 45, 260][id % 4];
const base: Result = {
id,
kind: "image",
status: "queued",
prompt,
params: ["1024×1024", `seed ${1000 + id * 37}`, model === "sketch" ? "draft" : "illustration"],
queuePosition: 2,
variants: [{ id: "v1" }, { id: "v2" }, { id: "v3" }],
currentVariantId: "v1",
hue,
};
setResults((rs) => [base, ...rs]);
schedule(id, () => patch(id, { status: "generating", stage: STAGES[0], queuePosition: undefined }), 1200);
STAGES.forEach((s, i) => schedule(id, () => patch(id, { stage: s }), 1200 + 700 * i));
const settleAt = 1200 + 700 * STAGES.length;
if (outcome === "ready") {
schedule(id, () => patch(id, { status: "ready", stage: undefined }), settleAt);
} else if (outcome === "blocked") {
schedule(
id,
() =>
patch(id, {
status: "blocked",
stage: undefined,
blockedReason: "The prompt names a real public figure. Describe a fictional character or scene instead.",
}),
settleAt,
);
} else {
schedule(
id,
() => patch(id, { status: "failed", stage: undefined, errorMessage: "The worker ran out of memory mid-generation." }),
settleAt,
);
}
}
function retry(r: Result) {
patch(r.id, { status: "generating", stage: STAGES[1], errorMessage: undefined });
schedule(r.id, () => patch(r.id, { status: "ready", stage: undefined }), 1600);
}
function regenerate(r: Result) {
const order = ["v1", "v2", "v3"];
const next = order[(order.indexOf(r.currentVariantId) + 1) % order.length];
patch(r.id, { status: "generating", stage: STAGES[2], currentVariantId: next });
schedule(r.id, () => patch(r.id, { status: "ready", stage: undefined }), 1400);
}
return (
<div className="flex h-[640px] overflow-hidden rounded-2xl border border-zinc-200 bg-white dark:border-zinc-800 dark:bg-zinc-900">
{/* Composer rail */}
<aside className="hidden w-72 shrink-0 flex-col gap-3 border-r border-zinc-200 p-3 dark:border-zinc-800 md:flex">
<p className="text-[13px] font-semibold text-zinc-900 dark:text-zinc-100">Image Studio</p>
<ModelSelector options={MODELS} value={model} onSelect={setModel} />
{draftPrompt && (
<p className="rounded-lg bg-zinc-50 px-2.5 py-2 text-[11px] leading-4 text-zinc-500 dark:bg-zinc-800/60 dark:text-zinc-400">
Reusing: “{draftPrompt}”
</p>
)}
<div className="mt-auto">
<PromptInput
placeholder="Describe the image…"
onSubmit={(v) => {
runGeneration(v);
setDraftPrompt("");
}}
/>
</div>
</aside>
{/* Results feed */}
<div className="flex min-w-0 flex-1 flex-col">
<div className="border-b border-zinc-200 px-4 py-3 dark:border-zinc-800 md:hidden">
<PromptInput
placeholder="Describe the image…"
onSubmit={(v) => runGeneration(v)}
/>
</div>
<div className="flex-1 space-y-4 overflow-y-auto px-4 py-4">
{results.map((r) => (
<GeneratedMediaResult
key={r.id}
kind={r.kind}
status={r.status}
prompt={r.prompt}
params={r.params}
stage={r.stage}
queuePosition={r.queuePosition}
variants={r.variants}
currentVariantId={r.currentVariantId}
onVariantChange={(vid) => patch(r.id, { currentVariantId: vid })}
errorMessage={r.errorMessage}
blockedReason={r.blockedReason}
onDownload={() => {}}
onRegenerate={() => regenerate(r)}
onRetry={() => retry(r)}
onCancel={() => patch(r.id, { status: "cancelled", stage: undefined })}
>
<ImageMock hue={r.hue + (r.currentVariantId === "v2" ? 30 : r.currentVariantId === "v3" ? -30 : 0)} />
</GeneratedMediaResult>
))}
</div>
</div>
</div>
);
}
When to use it
- Put the prompt in the feed the moment it's submitted — a queued card with a position beats a spinner over nothing.
- Stage generation in words and keep the card's shape fixed from queued to ready.
- Return variants per prompt and make switching between them cheap and reversible.
- Split blocked from failed: policy refusals ask for a rephrase, worker failures offer a retry that works.
- Keep every result's prompt one click from the composer — re-use is the core loop of a studio.
What breaks in production
- A generation that vanishes into a global loading state, leaving the user nothing to read or cancel.
- Treating a safety block as an error with Retry — the retry will just be blocked again.
- New results replacing old ones instead of accumulating — a studio is a feed, not a lightbox.
- Params that live only in a tooltip; if the user can't compare two results' settings, variants are lottery tickets.
More Patterns
The canonical chat interface — sidebar, streaming messages, prompt input with model selector, and sources.
AI Research AssistantA research flow that shows search tool calls, reasoning, sources and a cited final answer.
AI Coding AgentA coding run with agent status, tool calls, diffs and a human-in-the-loop approval gate.
AI Voice AssistantA voice-first conversation — live waveform states, a recording input, a spoken transcript and a typed fallback.
Model & Memory PreferencesA preferences screen that picks the model, reasoning level and tools, and manages persistent memory.
Artifact WorkspaceChat on the left, generated output on the right — artifacts open from the answer, stream, version, and fail without breaking the conversation.
Document Q&A WorkspaceAsk your own documents — upload and parse, cited answers with inspectable passages, an honest not-found state, and a visible context budget.
Structured Extraction & ReviewUpload a document, watch fields fill in, then review the flagged ones — per-field confidence, corrections that keep the original, export earned.
Multi-agent Ops ConsoleWatch a fleet of agents at once — parallel statuses, an inspectable handoff, a waiting approval, a failed child run, and per-run plus fleet cost.
Customer Support CopilotGrounded reply drafts with citations, honest low-confidence answers, inline corrections, an approval gate on refunds, and a rating row on every draft.
Generative UI DashboardThe model assembles a dashboard from a controlled widget registry — streamed props, an unsupported-request fallback, and widget clicks that re-enter the chat.