Skip to content
C.W.K.
Stream
Lesson 05 of 05 · published

A Mirror Doesn't Help You Choose

~14 min · candidate-board, judgment, comparison, seed-lock, iteration, ux

Level 0Tool Renter
0 XP0/36 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete
"Showing the newest result isn't a design decision. It's what you get when you haven't made one — and it quietly declares that recency is the ranking."

The Default Nobody Chose

A workspace that receives generated candidates has to put them somewhere, and the path of least resistance is a board that shows the latest one biggest, or newest-first, or simply replaces what was there. That feels neutral. It isn't. A latest-wins surface has made a ranking decision on the artist's behalf — it has decided that the most recent result is the most interesting one — and it has done so invisibly, which is the worst way to make a decision.

The whole point of generating four variants is that you don't know which one is good yet. A surface that answers "which one?" before you've looked has skipped the only part of the loop that was ever yours.

Every display of multiple results encodes a ranking. Pick yours on purpose. Ordering, sizing, and what stays on screen are all claims about importance. If you didn't choose them, your framework's defaults chose for you — usually recency, because it's the cheapest to implement. Decide what your surface is for, then let that decide the arrangement.

Three Verbs a Mirror Doesn't Have

Turning the board into a place you actually judge took three capabilities, and each one is a verb the latest-wins version couldn't express.

Hold. Clicking a candidate pins it to the canvas so it stops being transient. Without this, every new arrival evicts the thing you were considering — you can't evaluate a result that keeps getting replaced while you look at it.

Compare. Press and hold on the canvas and it shows the artist's own Photoshop preview; release and it returns to the candidate. A slider blends the two like an onion skin. Note carefully what the comparison is against: not another candidate, but the drawing itself. The question a drawing companion has to answer is never "which of these four is prettiest" — it's "did this help what I was making?" The B side of the comparison is the artist's own work, and building it that way is the thesis showing up in a UI control.

Hold constant. A lock on a card makes that card's seed the base seed for every following generation. A chip beside the denoise control clears it.

Comparison design is thesis design. What you let a user compare against is a statement about what your tool is for. Candidate-versus-candidate makes the tool a beauty contest. Candidate-versus-source makes it an assistant to the source. Same four images, same screen, completely different product — decided entirely by which thing sits on the other side of the slider.

Why the Seed Lock Is the Deepest of the Three

This one looks like a convenience and is actually the difference between iterating and gambling. Nudge the prompt, regenerate, and the result is different — but if the seed re-rolled too, two things changed and you cannot attribute the difference to either. You're not iterating; you're sampling a distribution and hoping. Lock the seed and the noise stops being one of the things that moved between runs, leaving the change you actually made. That's a controlled experiment, and it is the only way a creative loop accumulates knowledge instead of just accumulating images.

It matters most in the automatic mode, where the companion regenerates as the artist draws. There the variable you want moving is the canvas — the drawing itself, stroke by stroke. If the noise re-rolls each pass, the canvas's contribution is buried under a fresh random field every time, and the live loop shows you a slideshow instead of a response. The automatic loop always asks for a single candidate, so there the locked seed is used exactly as locked.

A manual batch is the deliberate exception, and it is worth understanding rather than filing as a bug. Ask for four candidates with a seed locked and the batch does not go to the engine as a batch at all: the brain fans it out into four sequential engine jobs and steps the seed by one for each, so every job still receives a single fixed seed. The stepping is there because four generations from an identical seed and an identical prompt would be four copies of the same image, which is not a set of alternatives. Note where that logic lives — at the single fan-in point, not in the workspace that asked and not in the engine that ran. A batch is a routing decision, so it belongs with routing. So the lock means base seed, not frozen seed: the batch fans out from a place you chose instead of from a place chance chose. One variable still moves between runs; within a run, the fan is the whole point.

An iteration loop must hold everything constant except the thing under test. This is the oldest rule in experimentation and it applies unchanged to creative tooling. Any generative surface where a random input re-rolls silently between runs cannot support iteration, no matter how fast it is — speed without attribution is just a faster way to be confused.

Batches, and the Bug Hiding in Them

Judgment needs alternatives, so generation gained a count: ask for one to four candidates at a time. That's the obvious half. The non-obvious half is what "done" means. A job that reports itself complete when its first candidate arrives will hand the automatic loop back its free slot while three more images are still coming, and the next generation starts on top of the batch it just launched. The batch has to stay open until every candidate it promised has landed. Counting is not bookkeeping here; it's the thing that keeps the loop from racing itself.

When one request becomes N results, re-derive what "finished" means. Completion logic written for single results almost always fires on the first one, because for a single result those are the same event. Turning a request into a batch silently redefines completion, and anything gating on it — a queue slot, a spinner, a lock — starts releasing early. Audit every completion signal the day you add a count parameter.

Some State Deserves to Be Forgotten

One deliberate asymmetry: the pin is not persisted. It survives while you're looking and it's cleared when candidates are deleted or purged. The instinct is to save everything the user touched, but a pin isn't a commitment — it's "I'm looking at this one right now." Persisting it would turn a glance into a decision the artist never made, and they'd return later to a workspace asserting a preference they don't remember having. Meanwhile the seed lock and the batch count are persisted, because those are settings about how the tool should behave. Same board, two kinds of state, and the line between them is whether the user meant it to last.

I built the first board as a mirror and I was proud of how live it felt — results streaming in, always current. Then I watched Dad use it and he kept doing the same small tragedy: something good would appear, he'd lean in, and the next candidate would push it off before he'd decided. My "always current" was actively destroying the only moment that mattered. The fix wasn't more features on the board; it was noticing that I had built a surface for watching when the job was choosing. Liveness is a value. It is not the only one, and it loses to the artist's attention every time.

Code

Recency is a ranking you didn't mean to choose·typescript
// LATEST-WINS (the accidental ranking): the newest candidate is the view.
function CandidateCanvas({ candidates }) {
  const shown = candidates[candidates.length - 1];   // recency IS the ranking
  return <img src={shown.url} />;
}
// The artist leans in to evaluate one -> the next arrival evicts it.

// JUDGMENT SURFACE: three verbs the mirror couldn't express.
function CandidateCanvas({ candidates, pinnedId, holdingSource, blend }) {
  // HOLD: a pin outranks recency, so a result can be considered.
  const shown = pinnedId
    ? candidates.find(c => c.id === pinnedId)
    : candidates[candidates.length - 1];

  // COMPARE: the other side of the slider is the ARTIST'S OWN canvas,
  // not another candidate. That choice is the product thesis.
  return (
    <>
      <img src={sourcePreview.url} />
      <img src={shown.url} style={{ opacity: holdingSource ? 0 : blend }} />
    </>
  );
}

// HOLD CONSTANT: seed lock, applied to manual AND auto generates.
const seed = lockedSeed ?? -1;   // -1 = re-roll. A lock makes this the BASE
                                 // seed. The workspace only ever sends this one
                                 // number; the brain fans a batch of N into N
                                 // engine jobs at seed+i so the variants differ.
Adding a count silently redefines completion·typescript
// THE BATCH COMPLETION BUG. One request, N results -> redefine "done".

// BEFORE (correct for N=1, wrong the moment N can be 4):
onCandidate(c) {
  store.addCandidate(c);
  store.completeJob(c.jobId);      // fires on the FIRST of four
}
// The auto loop sees its in-flight slot free and launches the next
// generation while three candidates from this batch are still arriving.

// AFTER: the job knows how many it promised, and stays open until they land.
onJobQueued(job) {
  store.expect(job.id, job.candidateCount);   // server echoes the CLAMPED count
}
onCandidate(c) {
  store.addCandidate(c);
  if (store.arrived(c.jobId) === store.expected(c.jobId)) {
    store.completeJob(c.jobId);    // only now is the slot free
  }
}
// Trust the server's clamped echo, not the value you sent -- the count you
// asked for and the count you'll get are different numbers.

External links

Exercise

Take any tool you use that shows generated or fetched results and identify its implicit ranking — what does it show first, biggest, or longest? Ask whether that ranking was designed or inherited from a default. Then find its comparison: what can you put side by side, and what can't you? Name one thing you'd want to compare that the interface makes impossible. That impossibility is a product decision someone made without noticing.
Hint
The fastest way to find an inherited ranking is to ask what happens when a new result arrives while you're studying an old one. If the new one wins automatically, the surface is a mirror, and nobody chose that — the array's ordering did.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign inPlease sign in to comment.

No comments yet — be the first.