# Widget spec — `picsart_asset_review`

Review board. **One question: is this asset good?** A grid of the assets you just
generated, one tile each, and two actions per tile — **Approve** or **Change**.
Approved assets are saved and never asked about again; changed ones come back on
a fresh board as a new version. The board is done when nothing is left to change.

Rebuilt again 2026-08-25 against an approved mock: the two actions stopped being
peers, the change panel grew multi-model retries, quick intents went, and the
footer gained one adaptive primary. Contract at v3.

Rebuilt 2026-08-21 from a board that had accumulated three jobs — casting
(`intent: "pick"`), batch approval (`approve_set`) and stress-test QC
(`verify`) — over one grid whose buttons never changed. An untouched tile meant
"no opinion", "approved" or "passed" depending on a flag the user could not see;
`reject` in pick mode and `select` in verify mode each did exactly what silence
already did. That is retired, not extended: see *What this board no longer does*.

## Tool input

```ts
{
  purpose: string,                 // 'the assets sc01–sc03 needs'
  assets: Array<{                  // 1..12 — ONE ENTRY PER ASSET
    url: string,                   // Picsart-hosted https only
    label?: string,                // 'The diner'  (falls back to 'Asset N')
    tag?: string,                  // '@diner' — film flow has one, video flow does not
    version?: string,              // 'v1', 'v2' …
    previousUrl?: string,          // the version this one replaces
    prompt?: string,
    model?: string,
    info?: string[],               // ≤4 lines: ['location', 'serves sc01–sc03']
    why?: string,                  // one sentence: what THIS asset has to carry
  }>,
  judge?: string,                  // 'the face and the light, not the clothes'
  notes?: string,                  // one line of guidance above the grid
  imageModels?: Array<{ id: string, why?: string }>,  // from a live catalog read
}
```

`why` is `judge` per asset — "every shot cuts back to this face, get it right
first" — on its own line under the info. `imageModels` never hardcodes an id;
read the catalog, and omit the field when you have not.

**One tile per asset.** These are different things — a character, a location, a
prop — not variants of one thing. Generate **one** version of each asset and let
the user iterate on it; do not fan out candidates and ask them to pick. Two tiles
sharing a `tag` is flagged in the UI, because "approve @cal" cannot name two
images.

**The one exception is a multi-model retry.** When a previous board's change
carried `models`, the next board legitimately shows several tiles of the SAME
asset, one per model, each with its `model` badge — the user asked to compare.
That is the only case where a shared tag is expected rather than an error.

**Batching.** One board per scene block, assets deduped — an asset appears in the
batch of its first scene and never again, so a lead is reviewed once rather than
once per scene. Split a block that exceeds 12 tiles.

**`previousUrl` earns its place on iteration boards.** Serial iteration is
cheaper than parallel candidates but it loses side-by-side comparison, so a v2
tile carries a thumbnail of the v1 it replaces. That is what keeps "go back to
the first one" available without reviving a pick mode.

## Layout

- **Header**: *"Approve the cast, or send any back"* with `notes` beneath it
  (default: *"Approve what works. Anything you send back comes straight to a new
  board."*). The counter and the approve-all button live in the progress bar and
  footer respectively, not here.
- **`Judge by:` line** under the header when `judge` is passed. Without it users
  judge everything and stall on nothing.
- **Grid**, always **two up** (one when there is a single asset), staggered in,
  in a centred column so a tile stays big enough to judge a face on. Per tile:
  the square image, `version` + `model` badges together **top-left** (on a retry
  board those two badges are the only thing telling sibling tiles apart), the
  `previousUrl` "was" thumbnail, then tag · `info` on one line and `why` under
  it.
- **Two actions per tile, and they are not peers.** `Approve` is a pill;
  `Change this` is a text button with a pencil. One is the answer most tiles
  get, the other opens work. Approving turns the pill green, badges the image
  `✓ Approved` and rings the card; `Change this` becomes `✕ Never mind`.
- **The change panel** — note (**required**) plus `Try it on`: the models this
  asset may be re-tried on, **up to three**, each returning one new version. A
  line under it says what the picks buy in the count chosen ("you get 3 versions
  of this asset on the next board, one from each model"). Chips come from
  `imageModels`; absent, no model choice is offered and the same model tries
  again. Beyond three, a further chip is inert rather than disabled — the cap is
  stated in the label above.
- **The note takes focus on its own only where a keyboard is already on screen.**
  On a `(pointer: coarse)` device the panel opens without the focus, because
  raising the soft keyboard covers the field that just appeared under the button
  the user pressed — and a widget iframe is sized to its content and clips
  overflow rather than scrolling, so nothing on this side can put it back in
  view. The keyboard comes up on the user's own tap instead, and a field they
  tapped is a field they could see. With a mouse it still focuses, where that
  only saves a click.
- **Progress bar** under the header with `N of M decided`, turning green when
  nothing is left.
- **Footer**: optional overall comment, a hint naming what is pending, `Start
  over` once anything is touched, and ONE primary that says what pressing it
  does — `Approve all N remaining` while tiles are undecided (the fastest true
  answer for a clean batch, never a disabled Send the user has to earn), then
  `Send N back` if anything is going back, or `Approved` if nothing is.
- Quick intents (`Cut out` / `Enhance`) were **removed** in the 2026-08-25
  rebuild: they were edits smuggled into a verdict surface, and the mock has no
  place for them.
- **Nothing is decided by silence.** An untouched tile is *pending*, and the
  board disables its own send until every tile has a verdict and every change has
  its note.

## Feedback → `asset_review_feedback` **v3**

Full contract in
`../../skills/film/references/REVIEW-FEEDBACK.md`.

```json
{
  "type": "asset_review_feedback", "version": 3,
  "purpose": "the assets sc01 needs", "total": 3,
  "approved": [ { "id": "a-1", "tag": "@cal", "label": "Cal", "url": "…",
                  "version": "v1" } ],
  "changes":  [ { "id": "a-2", "tag": "@diner", "label": "The diner", "url": "…",
                  "version": "v1", "note": "colder light, and lose the neon",
                  "models": ["flux-2-pro", "nano-banana-2"] } ],
  "generalComment": null
}
```

Changes from v2: `quickIntent` is gone, and a change may carry **`models`** —
≤3, omitted when empty (empty would mean the same thing as absent, so the reader
never has to tell them apart).

**`models` is the one sanctioned fan-out on this board.** Normally a tag names
exactly one image, which is why two tiles sharing a tag is flagged. A multi-model
retry deliberately returns several versions of the SAME asset on the next board —
sibling tiles sharing its tag, each badged with the model that made it — and
approving one discards the rest. **Scope the duplicate-tag warning accordingly:
it is an error on a first-round board and expected on a retry board.** The
feedback message says this explicitly whenever `models` is present, and says
nothing about it otherwise.

- **`approved.length + changes.length === total`, always.** One checkable line
  replaces every "an untouched tile means…" rule the earlier versions needed.
- **There is no `verdict`.** The two arrays are the verdict: `changes` empty means
  the batch is done. v1's field existed only to reconcile a pick against a
  pending redo, which can no longer happen.
- Approved → save it. Film:
  `picsart_save_asset` with `projectFolderUid`, v1 — `status: "review"` for a
  passport-tier asset, which files it under the project's `Assets/` and leaves
  the library for its later lock; `status: "locked"` plus
  `stressTest: { waived: … }` for a done-at-v1 asset, whose approve click *is*
  its lock (see `film-assets`, step 4). Approving never writes the library for
  an asset that still owes a passport. Video: record the URL in the plan's
  `assets`.
- Changed → regenerate **that one asset** with its `note` folded in surgically,
  then open a **fresh board** with only those, `version` bumped and
  `previousUrl` set. Approved assets are never re-boarded.
- Publishes in-progress state silently while the user decides, so a typed "the
  diner is fine" can be resolved. In-progress state is never a decision.

## What this board no longer does

| Retired | Why | Where it went |
|---|---|---|
| `intent: "pick"` + 2–4 candidates | a competition framing over per-card edit affordances; the two never composed into an approval | one version per asset, iterated |
| `intent: "approve_set"` | it *was* the right model — but as a mode, with approval by silence | it is the board now, with approval made explicit |
| `intent: "verify"` | a battery judges frames of an already-approved asset; not the same question | the asset sheet’s consistency check, which already displays the score |
| `allowMultiple` | only existed to decide whether picking demoted a sibling | nothing demotes anything |
| `Reject` / `Fails` / `Needs work` | `reject` was a documented no-op — *"Leave `reject` alone"* | `Change` with a note ("none of this works — try X") |
| `verdict`, `selected`, `candidates[].action` | reconciliation machinery for a mode that is gone | `approved[]` / `changes[]` |

## Build notes

Source: `pa-ai-models-mcp-server/server/widgets/src/asset-review/` —
`AssetReview.tsx` (board, send gate, Approve-all), `AssetCard.tsx` (the two
buttons; replaced `CandidateCard.tsx`), `feedback.ts` (v2 payload + the
`pending`/`noteless` guards), `types.ts`. Tool at
`server/src/mcp/tools/asset-review.tool.ts`, output schema
`AssetReviewOutputSchema` in `server/src/mcp/shared/output-schemas.ts`, widget URI
`ui://widget/picsart-asset-review-v2.html` in `shared/openai-app.ts`.

Tests: `server/widgets/src/asset-review/feedback.test.ts` (node:test — asserts
the `approved + changes === total` invariant) and
`server/src/mcp/tools/asset-review-tool-tests.ts` (jest).

The resource `readFileSync`s `widgets/dist/asset-review.html` **at module load**,
so a widget change needs `npm run build:widgets` *and* a server restart before it
is served. Changing the tool description requires
`npm run mcp:descriptions:update` and `npm run mcp:contract:update`.
