← Files PicsartARCHIVED FILE
docs/widget-specs/asset-review.md
10.7 KB · Oct 10, 2026 · 18:02 UTC
# 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`.
SHA-256: 38afd9ca08e3cce7d04076a00d93f2929c7c58c0e944a707bb12518fc58cb596