Files
SousChefAI/recipe-generator-engineering-notes.md
2026-04-28 19:06:37 -05:00

7.9 KiB
Raw Blame History

Recipe Generator — Engineering Notes

Handoff doc covering technical decisions, design choices, and bug fixes made during initial development. Last updated: April 28, 2026.


What It Is

A single-page HTML app that takes user-supplied ingredients, a food goal, dietary restrictions, and preferences, then calls the Anthropic API directly from the browser to generate a structured, chef-quality recipe with precise step-by-step instructions.

No backend. No build step. One .html file.


Tech Stack

Layer Choice Reason
Runtime Vanilla HTML/CSS/JS No framework needed; single file, zero dependencies
Fonts Google Fonts (Lora + DM Sans) Lora for editorial/recipe headings; DM Sans for UI chrome
AI Anthropic API (claude-sonnet-4-20250514) Direct browser fetch with anthropic-dangerous-direct-browser-access header
Output format JSON (structured) Predictable, renderable, extensible

Inputs Collected

Field Type Notes
Ingredients on hand Tag input (multi-value) Comma or Enter to add; Backspace to remove last
Food goal Free text e.g. "a hearty weeknight pasta"
Dietary restrictions Multi-select pill toggles Vegan, Vegetarian, Gluten-free, Dairy-free, Nut-free, Low-carb
Cuisine style Dropdown 10 options + "Any cuisine"
Time available Dropdown Under 20 min → 12 hrs
Skill level Dropdown Beginner / Intermediate / Advanced
Servings Dropdown 1, 2, 4, 6, 8
Additional notes Textarea Free-form catch-all

API Design

Endpoint

POST https://api.anthropic.com/v1/messages

Required Headers

{
  'content-type': 'application/json',
  'anthropic-version': '2023-06-01',
  'anthropic-dangerous-direct-browser-access': 'true'
}

⚠️ Do not include x-api-key when running inside the Claude.ai artifact environment — the platform injects the key automatically. Passing an empty string causes an auth failure (see Bug Fixes below).

Model

claude-sonnet-4-20250514 — good balance of quality and speed for recipe generation. Max tokens set to 1800.

Prompt Architecture

Two-part: system prompt + user message.

System prompt defines:

  • Role: expert chef and culinary writer
  • Explicit anti-vagueness instruction with a concrete example of bad vs. good instruction style
  • Strict JSON-only output requirement (no markdown fences, no preamble)
  • The exact JSON schema expected
  • Hard rules: respect dietary restrictions, 510 steps, precise sensory details

User message is assembled dynamically from form state, filtering out any empty fields:

Ingredients available: pasta, garlic, tomato, tofu
Food goal: quick but protein packed pasta
Dietary restrictions: vegetarian, dairy-free
Time constraint: under 20 minutes
Servings: 6

Response Schema

{
  "title": "string",
  "description": "string",
  "servings": 2,
  "prepTime": "string",
  "cookTime": "string",
  "ingredients": [
    { "amount": "string", "unit": "string", "name": "string" }
  ],
  "steps": [
    { "title": "string", "instruction": "string" }
  ],
  "notes": "string | null"
}
  • servings is typed as a number (not string) — explicitly called out in system prompt to avoid JSON parse issues
  • unit can be omitted for whole/countable items (the renderer handles undefined gracefully)
  • notes is nullable

Prompt Engineering Decisions

Anti-vagueness instruction

The single most impactful prompt decision. Rather than just saying "be detailed," the system prompt gives a concrete counter-example:

Instead of "cook the onions", write "cook the onions in the butter over medium heat, stirring occasionally, for 810 minutes until they turn translucent and just begin to turn golden at the edges."

This anchors the model's output quality at the instruction level, not just the ingredient level.

JSON-only output enforcement

The system prompt instructs the model to return nothing before or after the JSON. To be defensive against any stray text, the parser uses:

const jsonStart = raw.indexOf('{');
const jsonEnd = raw.lastIndexOf('}');
const recipe = JSON.parse(raw.slice(jsonStart, jsonEnd + 1));

This is more robust than JSON.parse(raw) directly, which would fail on any leading/trailing characters.


Design Decisions

Tag input for ingredients

Standard text input UX isn't great for multi-item lists. Tags let users see all their ingredients at a glance and remove individual ones without retyping. Implemented in vanilla JS — no library.

  • Enter or comma commits a tag
  • Backspace on empty input removes the last tag
  • Duplicate ingredients are silently ignored (case-insensitive)

Pill toggles for dietary restrictions

Checkboxes are functional but visually coarse for this context. Pills feel more natural for preference selection and render well on mobile. The native <input type="checkbox"> is hidden; state is managed via .checked property and a .checked CSS class on the parent.

Typography split

  • Lora (serif): recipe title, step numbers — editorial, food-magazine feel
  • DM Sans (sans-serif): all UI chrome, form labels, body text — clean and modern

Recipe output layout

  • Meta chips (servings / prep / cook time) give a quick overview before diving in
  • Ingredients in a bordered list table — scannable, easy to follow while cooking
  • Steps as numbered items with a bold title + detail body — mirrors professional cookbook formatting
  • Chef's notes in a left-bordered aside box — visually distinct from the main method

Bug Fixes

Bug 1 — "Load failed" / auth error on API call

Root cause: The initial implementation passed 'x-api-key': '' as a header. In the Claude.ai artifact environment, the API key is injected by the platform — but only when the header is absent. Passing an explicit (even empty) x-api-key header overrides the platform injection and results in an authentication failure before the request reaches the model.

Fix: Remove the x-api-key header entirely from the fetch call.

// ❌ Before
headers: {
  'Content-Type': 'application/json',
  'x-api-key': '',   // <-- causes auth failure
  'anthropic-version': '2023-06-01',
  'anthropic-dangerous-direct-browser-access': 'true'
}

// ✅ After
headers: {
  'content-type': 'application/json',
  'anthropic-version': '2023-06-01',
  'anthropic-dangerous-direct-browser-access': 'true'
}

Bug 2 — Silent generic error message

Root cause: The original catch block showed a hardcoded "Something went wrong" message regardless of what the API returned, making it impossible to diagnose failures.

Fix: Parse the API error response body and surface the actual error.message field:

const data = await resp.json();
if (!resp.ok) {
  throw new Error(data.error?.message || `API error ${resp.status}`);
}

Bug 3 — Missing required browser-access header

Root cause: anthropic-dangerous-direct-browser-access: true was absent in the initial version, which causes the API to reject browser-origin requests as a CORS/security policy enforcement.

Fix: Added the header explicitly to all fetch calls.


Possible Next Steps

  • Streaming — use the Anthropic streaming API so the recipe appears token by token rather than all at once after a delay
  • Persistent recipe book — use the Claude.ai artifact storage API to save generated recipes across sessions
  • Serving scaler — the structured JSON makes it trivial to multiply ingredient amounts; add a stepper UI to the output
  • Nutrition estimates — add a nutritionFacts field to the JSON schema and ask the model to estimate macros
  • Shopping list view — cross-reference the generated ingredients against what the user said they have, and produce a "what to buy" list
  • Image generation — call an image model with the recipe title and description to generate a hero photo
  • Export to PDF — render the recipe output as a printable/downloadable PDF