vibes.diy/docs Home Discord
creator docs · reference · generated

ImgGen Component

This is the same document the Vibes code generator reads when it builds apps — rendered from prompts/pkg/llms/image-gen.md on every deploy, so it can't drift from what the platform actually does. Read it from your terminal any time with npx vibes-diy skills.

Generate and edit images from a text prompt. Each generated image lands as a file ref on the doc — display reads the platform-minted URL via _files.

When to use it: reach for <ImgGen> when a picture adds real value — items whose identity includes an image (cards, creatures, products, covers), user-personalized art (avatars, portraits from a photo), or apps whose whole point is imagery. Most text/data apps (lists, trackers, notes, forms) need no images — leave the import unused rather than decorating with gratuitous pictures.

Basic Usage

Start with a minimal image generation component:

App.jsx

jsximport React from "react";
import { useFireproof } from "use-fireproof";
import { ImgGen } from "use-vibes";

export default function App() {
  return (
    <div>
      <h2>Image Generator</h2>
      <ImgGen prompt="A sunset over mountains" />
    </div>
  );
}

<ImgGen> stores generated versions in its component-owned media database, associated with the host database name and _id. Read a saved image through useImgGen({ _id, database }) from use-vibes: its document composes current media versions and legacy host versions. From that composed document, select document.versions?.[document.currentVersion ?? 0], then document._files?.[version.id]. The returned file ref supplies both .url for display and .file() for a transformation input, so a ref you have in hand is ready to pass straight to images. Ordinary host records hold the app's own data; the image hook supplies the associated generated output.

For example, refining a saved pottery illustration reads its actual output and saves the result on another persisted host record. Supply vessel and glazedVessel from this database; create a fresh saved result record for each deliberate new attempt:

jsximport { ImgGen, useImgGen } from "use-vibes";

function GlazingPreview({ database, vessel, glazedVessel }) {
  const { document: original, pending, error } = useImgGen({ _id: vessel._id, database });
  const version = original?.versions?.[original.currentVersion ?? 0];
  const source = version?.id ? original?._files?.[version.id] : undefined;
  const sourceReady = source !== undefined;
  return <section>
    <ImgGen _id={vessel._id} database={database} showControls={false} />
    {sourceReady ? <ImgGen _id={glazedVessel._id} database={database}
      prompt="Add a blue glaze while preserving this vessel's shape and composition"
      images={[source]} /> : <p>{error ? error.message : pending ? "Loading saved source…" : "Source image unavailable"}</p>}
  </section>;
}

This is the same _files-shape contract documented in fireproof.md's "Working with Files" section — read it first if you have not seen the platform's file/URL story.

Editing an Uploaded Image

Pass a File object via images to run img2img. Adding a file picker that feeds into ImgGen:

App.jsx

jsx<<<<<<< SEARCH
export default function App() {
  return (
    <div>
      <h2>Image Generator</h2>
=======
export default function App() {
  const [file, setFile] = React.useState(null);

  return (
    <div>
      <h2>Image Generator</h2>
      <input type="file" accept="image/*" onChange={(e) => setFile(e.target.files[0])} />
      {file && <ImgGen prompt="Make it look like a watercolor painting" images={[file]} />}
>>>>>>> REPLACE

The input image is automatically resized (max 1024px) and compressed as JPEG before sending.

images accepts either a raw File (fresh from an <input type="file">) or a stored Fireproof file ref — anything shaped like { file: () => Promise<File> } passes straight through. So if the app has already saved the upload onto a doc, feed it back directly:

jsx// doc was saved earlier with: database.put({ ..., _files: { photo: file } })
<ImgGen prompt="Turn this pet into a cute cartoon character" images={[doc._files.photo]} _id={doc._id} database={database} />

Do not invent wrappers like images={x?.file ? something : undefined} — pass the _files entry itself. If the stored ref might be absent, gate the whole <ImgGen> mount on it instead: {doc._files?.photo && <ImgGen images={[doc._files.photo]} ... />}.

Displaying an img2img result later (gallery, detail view, reload): render by _id and do NOT re-pass images — <ImgGen _id={doc._id} database={database} showControls={false} /> shows the stored version. ImgGen records which input produced each version, so re-passing that SAME input (the doc's own stored source, e.g. images={[doc._files.photo]} on every render) — or _id alone — just displays the saved result and will not force a fresh, billed generation. Passing a genuinely DIFFERENT input to the same _id DOES generate a new version — that's a requested img2img edit, not a reload: a new upload, a different stored ref, or a PRIOR OUTPUT fed back in to refine it. To force a new version from the CURRENT input, use the regenerate control (or set generationId). Generate once with images, display forever with _id alone.

Do not set model for img2img: when an input image is present the platform automatically selects its image-edit default, which is tuned to produce edits faithful to the source. An explicit model override bypasses that routing — only use one when the app has a specific, stated reason.

Restyling a Captured Photo

A photo booth, a style camera or an avatar-from-a-selfie app saves the camera frame and style prompt on the shot's own record, then uses that record's _id for its styled result. Save a fresh shot record with its source frame and style prompt for each deliberate new attempt. The styled picture's frame is simply <ImgGen>, shown at full size: ImgGen paints the saved result when there is one, and otherwise its own progress bar, its own message when a picture needs another try, and — in the Vibes iPhone app — the Create with Image Playground button that a picture started by the camera (rather than a tap) waits on. The person sees each of those right in the frame. Your own words about the shot sit beside or below the frame.

Mount that same <ImgGen> wherever an unstyled shot is shown in full — the fresh-capture card and the detail view — so a shot saved in one visit styles the next time it opens. Gallery tiles read the saved result through useImgGen and show the original until it arrives.

Show the person's own photo beside every styled result, so they can see their picture went in. A small thumbnail of the source next to (or tucked into a corner of) the result turns "is this even my photo?" into a before-and-after they enjoy. A stored ref displays with its url: <img src={shot._files.original.url} />.

jsximport { ImgGen, useImgGen } from "use-vibes";

function StyledShot({ shot, database }) {
  const { document: styled } = useImgGen({ _id: shot._id, database });
  const version = styled?.versions?.[styled.currentVersion ?? 0];
  const ready = Boolean(version?.id && styled?._files?.[version.id]);
  const original = shot._files?.original;
  return (
    <figure>
      <div className="relative aspect-square w-full overflow-hidden rounded-md">
        {original && (
          <ImgGen _id={shot._id} database={database} prompt={shot.prompt}
            images={[original]} alt={`${shot.style} version`} className="h-full w-full object-contain" />
        )}
        {original?.url && (
          <img src={original.url} alt="Your original photo"
            className="absolute bottom-2 left-2 h-16 w-16 rounded border-2 border-border object-cover shadow" />
        )}
      </div>
      <figcaption>{ready ? shot.style : `Styling as ${shot.style}. Your original is saved.`}</figcaption>
    </figure>
  );
}

When one photo becomes several looks at once (four costumes, a style sampler), upload it once: save the photo on its own record, then save one look record per style holding that photo record's _id, and pass the photo record's _files.original to every look's <ImgGen>. Every look then shares one upload and one source thumbnail, and each look still keeps its own _id for its result.

Loading a Specific Doc

Load a previously generated image with its saved host record's _id: <ImgGen _id={doc._id} database={database} />. A literal such as _id="my-image-id" refers to a host record already saved under that exact _id in this database. If the host has a prompt but no stored image yet, the component generates one.

Illustrations That Are Part of an Item's Identity

When an item's picture is intrinsic to what it is — a card in a deck, a creature in a bestiary, a product in a catalog, a character in a cast — render <ImgGen> by default for every such item that lacks an image, so the illustration appears as soon as the item does. Because ImgGen auto-generates when the target doc has a prompt but no _files yet, mounting it directly on the item is all it takes:

jsx<ImgGen
  _id={card._id}
  database={database}
  prompt={`Illustration of ${card.title}. ${card.description ?? ""}`}
/>

Let the first picture arrive with the item itself, so every card opens already illustrated and the app reads as a picture app from the first glance. For these items, buttons and showControls are for regenerating an image that already exists. Pictures that are extras rather than identity — an avatar upload, a one-off hero graphic, and the text-first snippets below — come from an explicit action the person chooses.

Pictures on Request for Text-First Snippets

When each item's substance is its words — a generated joke, a short verse, a pun, a one-liner — give every snippet its own picture button and save the picture on that snippet's record. Tapping the button shows a "Making a picture…" state until the image opens in its place. ImgGen keeps the picture under the snippet's own _id in database, so it is there after a reload, viewing it again costs nothing, and a snippet with a saved picture shows it straight away. Keep the snippet's words in a field like text; a prompt field on the record is ImgGen's cue to draw right away.

jsximport { ImgGen, useImgGen } from "use-vibes";
import { Button, RowCard, Text } from "@vibes.diy/pouf";

function PunCard({ pun, database }) {
  const [wanted, setWanted] = React.useState(false);
  const { document: saved } = useImgGen({ _id: pun._id, database });
  const hasPicture = Object.keys(saved?._files ?? {}).length > 0;
  return (
    <RowCard>
      <Text>{pun.text}</Text>
      {wanted || hasPicture ? (
        <figure>
          <ImgGen _id={pun._id} database={database} prompt={`A cheerful cartoon of this pun: ${pun.text}`} />
          {hasPicture ? null : <figcaption>Making a picture…</figcaption>}
        </figure>
      ) : (
        <Button type="button" size="sm" variant="quiet" onClick={() => setWanted(true)}>
          Picture this
        </Button>
      )}
    </RowCard>
  );
}

Keep gallery entries as app-owned records with the saved image's host _id. Query those records with useLiveQuery, then let each <ImgGen> read its associated media. This also keeps the app's title, caption and other editable fields separate from image versions:

jsxfunction Gallery() {
  const { database, useLiveQuery } = useFireproof("artwork");
  const { docs } = useLiveQuery("type", { key: "artwork" });
  return <div>{docs.map(doc => <figure key={doc._id}>
    <ImgGen _id={doc._id} database={database} showControls={false} />
    <figcaption>{doc.caption}</figcaption>
  </figure>)}</div>;
}

Caching and Versions

  • The _id is always the host record's own _id. Store the character, card or audition record first, then pass its _id to every portrait of that record, including list cards and detail sheets. Its stored versions are shared across mounts and reloads.
  • Each image has a regenerate button that appends a new version (writes a new _files.v<N> entry).
  • Prev / next controls navigate between stored versions.
  • In the Vibes iPhone app, on iPhones with Apple Intelligence, pictures come from Apple's Image Playground, free. A picture started by a tap — ↻, or a button that shows the <ImgGen> — opens Image Playground with its prompt (and the images input picture, for an edit), and the image the person confirms is saved as a version like any other. A picture that starts on its own shows a Create with Image Playground button that does the same. Your code stays the same, and everywhere else pictures are made as usual. Passing model keeps that picture on the platform's cloud generation.
  • Set showControls={false} to hide regenerate and version navigation.

Private drafts and public images

Generated images and prompts live in media:<database>, and by default each image follows the record it was generated for: whoever can read that record can read its images, and the person who generated an image always can. A private record's images stay with its readers and each image's creator, with no extra rule. For images that should be more private than their record — drafts only their creator sees on a shared record — add an explicit export { privateMedia as "media:yourDatabase" } in access.js, with creator-owned version/pointer rules and private read grants. An app-wide default export does not override media. See the access skill's "Private generated images and published snapshots" recipe.

Publish a separate record containing only the chosen image File and the intended public facts. Public viewers render its _files URL with <img>; keep generation and revision controls on the creator's draft. Share the same useImgGen hook result between preview, download and publication so they select the same version.

Access Functions and pictures

Your access.js governs your app's own records — the story, the hat, the roast — and ImgGen keeps its pictures beside them in media:<database>, under the platform's media rules: every generation, a regenerate or an img2img edit included, is a new version record authored by whoever asked for it, and each person keeps their own pointer record for the picture they are viewing. So a type-keyed access function with one branch per record type already serves an image app, author checks and all; give each record the author and channel its own sharing calls for. When pictures should be more open or more closed than their record, write the access skill's media export, which checks that doc.authorHandle is the caller and admits both version and pointer records.

Choosing a Model

Every <ImgGen> draws with the app's default image model. listModels({ kind: "image" }) (from call-ai, beside callAI) returns the image models this app can use, the app's own choice first, and each entry's id goes straight into the model prop: <ImgGen prompt="An astronaut riding a horse" model={m.id} />. The entry with isDefault: true is the one ImgGen uses when model is left out; with images present the default is the platform's image-edit model instead, so img2img leaves model out (see above). When the app's maker saved their own provider key, it covers the image model they chose in settings; other models run on the person's credits. The callAI skill's "Choosing a model" section lists every field.

To compare image models, call listModels({ kind: "image" }) and render one <ImgGen> per entry with model={m.id}. Every picture shares one prompt, each card is labelled with m.name, and each picture uses its stored audition record's _id so a reload shows the saved pictures. Each click saves one new record per model, with its prompt and model choice:

App.jsx

jsximport React, { useEffect, useState } from "react";
import { useFireproof } from "use-fireproof";
import { ImgGen, useVibe } from "use-vibes";
import { listModels } from "call-ai";
import { Button, Card, Field, Grid, Input, Skeleton, Stack, Text } from "@vibes.diy/pouf";

function AuditionComposer({ database, models }) {
  const { can, me, ready } = useVibe("auditions");
  const [draft, setDraft] = useState("a penguin riding a bicycle");
  const [message, setMessage] = useState("");
  const candidates = models.map((m) => ({
    type: "audition", authorHandle: me?.userHandle, prompt: draft,
    modelId: m.id, modelName: m.name, isDefault: m.isDefault
  }));
  const verdicts = candidates.map((candidate) => can.create(candidate));
  if (!ready || verdicts.some((verdict) => verdict.pending)) return <Skeleton variant="row" />;
  if (verdicts.some((verdict) => !verdict.ok)) return <Text muted>Join this app to save an image audition.</Text>;
  async function drawAudition() {
    await Promise.all(candidates.map((candidate) => database.put(candidate)))
      .catch((error) => setMessage(error.message));
  }
  return <Stack>
    <Field label="Picture">{(id) => <Input id={id} value={draft} onChange={setDraft} />}</Field>
    <Button onClick={drawAudition} disabled={draft.trim().length === 0 || models.length === 0}>Draw it with every model</Button>
    <Text>{message}</Text>
  </Stack>;
}

export default function App() {
  const { database, useLiveQuery } = useFireproof("auditions");
  const { docs } = useLiveQuery("type", { key: "audition" });
  const [models, setModels] = useState([]);
  const [message, setMessage] = useState("");
  useEffect(() => {
    listModels({ kind: "image" })
      .then(setModels)
      .catch((error) => setMessage(error.message));
  }, []);
  return (
    <Stack>
      <AuditionComposer database={database} models={models} />
      <Text>{message}</Text>
      <Grid cols={3}>
        {docs.map((audition) => (
          <Card key={audition._id}>
            <ImgGen _id={audition._id} database={database} prompt={audition.prompt} model={audition.modelId} alt={`${audition.prompt}, drawn by ${audition.modelName}`} />
            <Text>{audition.isDefault ? `${audition.modelName} · this app's default` : audition.modelName}</Text>
          </Card>
        ))}
      </Grid>
    </Stack>
  );
}

Props

  • prompt: text prompt (required unless _id is provided)
  • images: array of File objects or stored _files refs for img2img (uses first image; only pass while generating — display stored results via _id)
  • _id: the host record's _id; reads its stored image or saves a new image for that record
  • database: Fireproof db name or instance (default "ImgGen")
  • className, alt, style: standard image styling
  • showControls: toggle regenerate + version nav (default true)
  • model: an id from listModels({ kind: "image" }) to draw with instead of the app's default