# Presets — Full Reference

A **preset** is a reusable prompt template with typed input variables (`requirements`). It binds a prompt to a model + recommended settings, and lets users (or AI agents) generate images by filling in the variables.

> Source of truth for the rules below: `apps/worker-queue/src/queue/ai-preset.processor.ts` (`SYSTEM_PROMPT`). If you edit one of these, sync the other.

## How `prompt` ↔ `requirements` ↔ `variables` Connect

```
preset.prompt        ──── contains {{key}} placeholders
preset.requirements  ──── declares each {{key}} (its type, label, options, …)
generate.variables   ──── caller fills in { key: value } per requirement
```

Three invariants the API enforces or strongly expects:

1. Every `requirement.key` referenced by `{{key}}` must exist in `requirements`.
2. Every `requirement` should be referenced in `prompt` (either as `{{key}}` or `{{#if key}}…{{/if}}`).
3. `key` MUST match `^[a-z][a-z0-9_]*$` (snake_case).

## Requirement Schema

```ts
type RequirementType = 'text' | 'number' | 'file' | 'dropdown' | 'table' | 'color';

interface Requirement {
  key: string;            // snake_case, English; referenced as {{key}} in prompt
  label: string;          // display name (Thai or English)
  note?: string;          // hint shown beside the input
  placeholder?: string;
  required: boolean;      // default false
  type: RequirementType;
  options?: DropdownOption[];   // required when type='dropdown'
  defaultValue?: string;
  multiline?: boolean;          // for type='text'
  multiple?: boolean;           // for type='file' or type='dropdown'
  min?: number;                 // for type='number'
  max?: number;
}

interface DropdownOption {
  id?: string;            // server fills in if omitted
  label: string;          // display label (Thai/English OK)
  value: string;          // value substituted into the prompt (English recommended)
  thumbnailUrl?: string;
  promptImageUrl?: string;
}
```

### Type Notes

- **`text`** — free text. Use `multiline: true` for textarea.
- **`number`** — numeric, validated by `min` / `max`. Sent as a string in `variables`.
- **`file`** — image reference. Used inline as `{{key}}` in the prompt; image data is uploaded with the generate call (see `images.md`). Optional file inputs SHOULD use `{{#if key}}[reference image: {{key}}]{{/if}}`.
- **`dropdown`** — pick from `options[]`. The `value` (not the `label`) is what gets substituted. With `multiple: true`, several values are joined by `, `.
- **`table`** — JSON array of rows, formatted to a markdown table at generation time.
- **`color`** — hex string (e.g. `#FF5733`).

## Conditional Blocks

The system supports Handlebars-style conditionals so optional variables don't leave dangling commas / orphan phrases:

```
{{#if key}}content when key has value{{/if}}
{{#if key}}content when filled{{else}}content when empty{{/if}}
```

Rules:

- `required: true` → use `{{key}}` directly (no conditional needed).
- `required: false` → wrap in `{{#if key}}…{{/if}}` whenever the surrounding text only makes sense if the user filled it in.
- Optional `type: file` → use `{{#if key}}[reference image: {{key}}]{{/if}}`.

## Endpoint: `POST /v0/images/presets`

Create a new preset bound to the API key's team (FULL access).

### Request body

```json
{
  "name": "Product Photography",
  "note": "ภาพโฆษณาสินค้าสไตล์ studio",
  "prompt": "ภาพถ่ายโฆษณาสินค้า {{product_name}} สำหรับกลุ่มเป้าหมาย {{target_audience}} สไตล์ {{art_style}} ฉากหลัง {{background}} แสง {{lighting}}{{#if extra_note}}, {{extra_note}}{{/if}} คุณภาพสูง รายละเอียดชัดเจน",
  "requirements": [
    { "key": "product_name", "label": "ชื่อสินค้า", "type": "text", "required": true, "placeholder": "เช่น รองเท้าวิ่ง Nike Air Max" },
    { "key": "target_audience", "label": "กลุ่มเป้าหมาย", "type": "dropdown", "required": false, "defaultValue": "adults aged 25-45",
      "options": [
        { "label": "วัยรุ่น (Teenagers)", "value": "teenagers aged 15-25" },
        { "label": "ผู้ใหญ่ (Adults)",     "value": "adults aged 25-45" },
        { "label": "ผู้สูงอายุ (Seniors)", "value": "seniors aged 50+" }
      ] },
    { "key": "art_style",  "label": "สไตล์ภาพ",  "type": "text",     "defaultValue": "minimalist", "placeholder": "เช่น minimalist, cinematic" },
    { "key": "background", "label": "ฉากหลัง",    "type": "text",     "defaultValue": "clean white studio" },
    { "key": "lighting",   "label": "แสง",        "type": "dropdown", "options": [
        { "label": "Natural", "value": "soft natural daylight" },
        { "label": "Studio",  "value": "professional studio lighting" }
      ] },
    { "key": "extra_note", "label": "หมายเหตุเพิ่มเติม (ถ้ามี)", "type": "text", "required": false }
  ],
  "modelVersion": "<see GET /v0/models for available ids on this deployment>",
  "lockModel": true,
  "lockAspectRatio": false,
  "defaultAspectRatio": "1:1",
  "tags": ["product", "studio", "advertising"]
}
```

### Body fields

| Field | Type | Required | Description |
|---|---|---|---|
| `name` | string | yes | Display name |
| `note` | string | no | Note shown to humans below the title |
| `prompt` | string | yes | Template with `{{key}}` / `{{#if key}}…{{/if}}` |
| `requirements` | `Requirement[]` | no (default `[]`) | Variable declarations |
| `model` | `'IMAGEN' \| 'GEMINI' \| 'STABLE_DIFFUSION'` | no | Model family |
| `modelVersion` | string | no | Specific model id — call `GET /v0/models` first to discover the exact ids available on this deployment |
| `lockModel` | boolean | no (default `true`) | Prevent users from switching model |
| `lockAspectRatio` | boolean | no (default `false`) | Prevent users from switching aspect |
| `defaultAspectRatio` | string | no | e.g. `"1:1"`, `"16:9"`, `"9:16"` |
| `isCommunity` | boolean | no (default `false`) | Publish to Community library |
| `tags` | string[] | no | Tag names (lowercased server-side) |

### Response — `201 Created`

```json
{
  "data": {
    "id": "uuid",
    "name": "Product Photography",
    "note": "...",
    "modelVersion": "<see GET /v0/models for available ids on this deployment>",
    "defaultAspectRatio": "1:1",
    "lockModel": true,
    "lockAspectRatio": false,
    "thumbnailUrl": null,
    "requirements": [...],
    "isCommunity": false,
    "categories": [],
    "accessLevel": "FULL",
    "originTeam": null,
    "createdAt": "2026-04-26T..."
  }
}
```

> **Note:** the response intentionally **omits the prompt template** — it's never echoed back to API clients (to avoid leaking templates across teams when the same key reads multiple presets).

## Endpoint: `PATCH /v0/images/presets/:presetId`

Same body shape as `POST` but every field is optional. Only fields present are updated.

**403 Forbidden** if the preset is `COMMUNITY`-imported (a ชุมชนพรีเช็ต added to the team, not authored by the team). Use the team's own preset, or fork the ชุมชนพรีเช็ต via the web UI.

A version snapshot is automatically created on every successful update — see `PresetVersion` history in the web UI.

## Endpoint: `DELETE /v0/images/presets/:presetId`

Removes the preset from the team. If no other team has access, the preset row is deleted entirely; otherwise just the `TeamPreset` link is removed.

### Response — `200 OK`

```json
{ "data": { "success": true, "presetId": "...", "removed": true } }
```

## Endpoint: `GET /v0/images/presets`

| Query | Default | Description |
|---|---|---|
| `teamOnly` | `false` | When `true`, returns only presets owned/imported by the team (excludes globals) |

Returns the same slim shape as the `POST`/`PATCH` response.

## Endpoint: `GET /v0/images/presets/:presetId`

Returns the slim shape for a single preset. **404** if the preset isn't accessible by the team.

## Conventions / Best Practices

1. Use `snake_case` English `key`s. Never `productName`, `Product_Name`, or Thai keys.
2. Write the `prompt` in either Thai or English — but dropdown `value`s should be English (image models understand them better). `label`s can stay in Thai for the UI.
3. Aim for 3–6 requirements. Fewer = inflexible; more = annoying.
4. Set `defaultValue` on optional requirements to keep prompts coherent if users skip them.
5. Use `{{#if}}` for optional sections — avoids `, ,` and trailing-comma artifacts.
6. Set `lockModel: true` if the prompt is engineered for a specific model (e.g., Gemini-vs-Imagen tone).
7. Pair `lockAspectRatio: true` with `defaultAspectRatio` so users can't pick incompatible sizes.
8. 2–5 lowercase `tags` for community discovery.

## Using a Preset in Generate

After creating a preset, call `/v0/images/generations` with `presetId` and `variables`:

```bash
curl -X POST "$BASE/v0/images/generations" \
  -H "Authorization: Bearer $KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "presetId": "<id>",
    "prompt": "",
    "variables": {
      "product_name": "Nike Air Zoom Pegasus",
      "target_audience": "adults aged 25-45",
      "art_style": "cinematic",
      "background": "outdoor running track at sunrise",
      "lighting": "soft natural daylight",
      "extra_note": "with motion blur effect"
    },
    "aspectRatio": "1:1",
    "sampleCount": 1
  }'
```

For `type: file` requirements, use multipart form with the field name = the requirement's `key` (or `<key>_0`, `<key>_1`, … for `multiple: true`). See `images.md` for full multipart examples.
