Mise Intelligent Chef

Claude-powered swaps · recipe import · prep mode · guided cook

A full-stack cooking app built around Claude. Import any recipe URL, get AI-driven ingredient swaps tuned to real dietary shifts, work through a prep checklist, then cook one clear step at a time.

The problem

Mise editorial home — import form and featured recipe hero
Editorial mode — import a URL or browse what's already in your kitchen.

Recipe websites are built for traffic, not cooking. You get five paragraphs of backstory, an ingredients list that doesn't match the steps, and instructions that assume you've done this before.

The gap between "found a recipe" and "actually cooked it" is mostly friction—cognitive load from switching context, tracking where you are, and figuring out what to do before the heat goes on. And if a recipe calls for heavy cream you don't have, or dairy you can't eat, you're back to Google.

Mise treats those as solvable design problems—and uses Claude to solve the ones that need more than layout.

Two modes, one session

The core architecture decision was a UX decision first: browsing a recipe and actively cooking it are completely different jobs, in completely different physical contexts, with completely different tolerance for cognitive load. They needed different surfaces.

Editorial mode

Browse your imported recipes, discover new ones, manage your kitchen. Calm, readable, meant for planning—not for a kitchen with flour on your hands.

Cook mode

A focused, touch-friendly surface that shows one step at a time with only the ingredients relevant to that step alongside. No scrolling. No distractions. Timer synced to the session so you can step away and come back.

The session model means switching mid-recipe is safe— cook_sessions tracks your current step and timer state in Supabase, so a refresh or a phone lock doesn't lose your place.

Mise recipe detail page showing ingredients, prep tasks, and cook button
Recipe detail — ingredients, prep checklist, and a single action to start cooking.

What I built

  • AI ingredient swaps (Claude) — A slide-up sheet powered by Claude Haiku generates 2–3 substitutions per ingredient, focused on real dietary shifts: dairy-free, lower calorie, lower carb, higher protein. The prompt is scope-controlled: vegetables, aromatics, herbs, and spices correctly return nothing—swaps are only offered when they actually matter (fats, dairy, proteins, grains, sweeteners). Combinations are encouraged where they work best (e.g. cream cheese + whole milk as a one-for-one heavy cream swap). Quality is enforced at the prompt level: cheap neutral oils are hard-banned from suggestions. Claude receives the full ingredient list as context so its reasoning is grounded in the actual dish, not just the ingredient name in isolation. Saved swaps write to recipe_modifications and carry over between sessions.
  • Batch dietary goal shifts (Claude) — A second, distinct Claude integration separate from per-ingredient swaps. The user selects one or more dietary goals (higher protein, lower carb, dairy-free, etc.) and Claude reads the full ingredient list at once, applies the most practical combination of swaps for the actual dish, and writes all results in a single pass. The prompt instructs Claude to resolve goal conflicts in a single pass—if two goals disagree on the same ingredient, it picks the swap that serves the most goals simultaneously rather than surfacing a mechanical priority-order dialog. Batch-shifted lines are tagged in recipe_modifications so they can be cleared independently of manual swaps. A static swap catalog serves as a zero-latency fallback if the API is unavailable.
  • Three-tier recipe import pipeline — Spoonacular handles enriched imports when a key is configured. When it fails or isn't available, a custom JSON-LD adapter fetches the URL directly, parses <script type="application/ld+json"> blocks, and extracts the @type: "Recipe" node— handling nested @graph, HowToSection with itemListElement, and plain string instruction arrays. A demo mock adapter provides a working end-to-end fallback with no external dependencies. Most major recipe sites import cleanly at one of the three tiers.
  • Server-side image proxy — Recipe images are routed through /api/image-proxy, which fetches with a browser User-Agent and sets Referer to the source hostname—bypassing hotlink protection silently. Private IP ranges are blocked. Images are validated by content-type, capped at 8 MB, and served with Cache-Control: private to prevent CDN-level caching.
  • Prep editor — A checklist of tasks that need to happen before the heat goes on: sourcing, mise en place, equipment. Editable and persisted per-recipe so it reflects your actual habits, not a generic list.
  • Cook mode — Step navigator with per-step ingredient narrowing: getIngredientsForStep() matches ingredient names against step text so you see only what's relevant now. Multi-sentence steps are split into a numbered list. Timer persists to cook_sessions.timer_state as a fixed endsAt timestamp—a background tab or refresh doesn't drift the clock.
  • My Kitchen — Import recipes, toggle favorites, and search your collection without leaving the page. A "continue cooking" banner surfaces when an active session exists—no badge, just context when you need it. The list paginates client-side from a larger server-fetched set so the initial render stays fast.
  • Auth & security — Supabase SSR with cookie-aware server and client boundaries. Middleware enforces session requirements on Editorial and Cook routes; RLS on all tables means no row is accessible outside its owner. The Claude-powered swap endpoint requires a valid session and applies a sliding-window rate limit per user to prevent API cost abuse.

Cook mode in detail

The step view has one job: put the right information in front of you, at the right time, with as little noise as possible.

  • Large type for the instruction—readable at arm's length from a phone propped up in the kitchen.
  • Ingredient list narrows to just what's relevant to the current step, with a fallback to everything when matching confidence is low.
  • Swap button on each ingredient opens the Claude-powered sheet without losing your step position. The sheet knows what recipe you're in and surfaces dietary-shift suggestions—not generic alternatives.
  • Back / Next at thumb reach; the final step marks the session complete and prompts for a rating.
Mise home page — continue where you left off banner for an active recipe
Persistent session — pick up exactly where you stopped, down to the step.
Mise recipe detail with lower-calorie swaps applied — SWAP tags on changed ingredients
Shift the recipe — pick one or more dietary goals and Claude applies the most practical swaps, grounded in the actual dish.
Mise shift-the-recipe panel — dietary goal selector with six options
Lower calorie applied — SWAP tags appear inline on each changed ingredient, with a note explaining the tradeoff.
Mise manual swap sheet — two substitution options shown for a single ingredient
Manual swap — tap any ingredient to see Claude's substitution options with honest tradeoff notes.

Prep & kitchen

Prep is a separate route from the recipe detail—a deliberate checkpoint before committing to a cook session. The checklist is editable and persists so it reflects each person's actual mise en place habits.

My Kitchen deliberately avoids dashboard energy. A short list with titles, ratings, and a delete button. Favorites are separated so they're easy to find again. The continue-cooking banner appears at the top when there's an active session—no notification badge, just context when you need it.

Mise prep page — checklist of tasks before cooking starts
Prep view — everything that needs doing before the heat goes on.
Mise My Kitchen page with recently imported and favorited recipes
My Kitchen — a quiet shelf for what you're cooking, not a crowded dashboard.
Mise recently imported recipe grid — six recipes with photos and star ratings
Recently imported — your shelf of saved recipes, each ready to open or cook.

Design decisions

Two modes because cooking and browsing are different mental states

Editorial and Cook aren't tabs on the same screen—they're genuinely different surfaces with different interaction models. Editorial is calm and browsable, designed for when you're sitting down and planning. Cook is focused, large-type, and touch-optimized, designed for a kitchen counter with flour on your hands. Collapsing them into one view would have made each worse. The mode switch is a commitment—it changes what the app is for. That separation turned out to be the right call; each surface stayed clean in ways a unified view would have compromised.

A swap button that appears means something

The AI swap feature was scoped deliberately: no suggestions for aromatics, herbs, spices, or vegetables—categories where swaps either don't matter or produce generic results. That was a design decision enforced at the prompt level, not a technical gap. If a swap button appears, the suggestion will be meaningful. If it doesn't, there's nothing worth saying. Comprehensiveness would have trained users to ignore the button. Restraint makes it useful. The same logic applied to cheap neutral oils—they're hard-banned from suggestions by name, because a "swap" that makes the dish worse isn't a feature.

The AI doesn't explain itself—it shows up in the UI

Both Claude integrations return structured JSON that maps directly onto existing interface elements. The per-ingredient swap slides up in a sheet. The batch shift rewrites ingredient lines in place. There's no chat, no explanation of how it worked, no AI branding on the interaction. The intelligence shows up as UI state. That keeps the experience coherent and avoids the pattern where AI features feel bolted on rather than designed in—the swap is just part of the recipe, not a separate AI product inside the product.

Prep as a checkpoint, not a feature

The prep checklist is a deliberate pause before committing to cook mode—not a to-do list for its own sake, but a mental state change. Working through mise en place tasks shifts you from planning mode to ready-to-cook mode. Keeping it as a separate route rather than a modal or tab reinforces that it's a transition: you check out of one context and check into another. The boundary is the point.

The timer stores a timestamp, not a countdown

This is the kind of design decision that's invisible until it fails. A countdown timer drifts across tab switches and phone locks—it's ticking against the JavaScript event loop, not wall time. A fixed endsAt timestamp doesn't drift. Remaining seconds are always calculated fresh from the current time on each tick. The user experience difference is small when everything works; it's the whole experience when someone steps away to stir something and comes back.

Stack

  • Claude Haiku (Anthropic) — Used in two distinct integrations via @anthropic-ai/sdk: per-ingredient swap suggestions (Route Handler, authenticated + rate-limited) and batch dietary goal shifts (Server Action, reads the full ingredient list in a single pass). Each integration has its own prompt-engineering constraints: scope guards, quality rules, and structured JSON output requirements.
  • Next.js 15 (App Router) — Server Components for data fetching, Server Actions for mutations, Route Handlers for the AI swap and image proxy endpoints. Write paths go through typed actions in lib/actions/.
  • Supabase SSR — Per-request server clients for auth-aware queries; browser client for cook mode's step and timer writes. RLS on all tables—no row accessible outside its owner.
  • Recipe import pipeline — Spoonacular for enriched parsing when configured; a custom JSON-LD structured-data adapter as a free fallback that handles virtually any major recipe site; a demo mock adapter for zero-dependency demos.
  • shadcn/ui — Dialog, Sheet, Checkbox, and scroll areas; component-level overrides kept minimal.

What building it surfaced

  • Prompt scope matters more than prompt cleverness. The Claude swap prompt went through several iterations. The breakthrough wasn't better positive instructions—it was tighter exclusions. Explicitly telling Claude what not to suggest (cheap neutral oils, aromatic vegetables, spices) produced dramatically more useful output than rewording what it should suggest. Cheap oils were hard-banned by name. The scope rule—return [] for anything that isn't a meaningful dietary swap candidate—kept the UI honest: a swap button that appears means something.
  • Context makes AI suggestions credible. Sending only the ingredient name produced generic results. Sending the full ingredient list alongside it gave Claude enough recipe context to reason about what a substitution actually does to the dish—not just whether it's chemically similar.
  • CDN cache collisions are silent and maddening. The image proxy shipped with Cache-Control: public. In production on Netlify, every imported recipe started displaying the same image—the first one ever fetched. The CDN was caching the proxy response by path alone, ignoring the ?url= query parameter entirely. The fix was Cache-Control: private—browser-only caching, no edge layer. Unsplash URLs were also moved to bypass the proxy entirely since their CDN is already public. The symptom (all images identical) was obvious; the root cause (edge cache keying behavior) took careful reading of Netlify's caching docs to confirm.
  • A timestamp beats a counter. The timer stores a fixed endsAt timestamp rather than a countdown. A counter drifts across tab switches and phone locks. A timestamp doesn't—remaining seconds are always calculated fresh on each tick.
  • Honest beats clever in ingredient matching. Step-ingredient matching surfaced an inherent tension: strict matching misses paraphrased ingredients; loose matching shows too much. The solution was to show everything when confidence is low and label it clearly—not hide the fallback.
  • The import pipeline was designed for failure. Spoonacular occasionally extracts garbage from paywalled or redirect pages. JSON-LD structured data is surprisingly reliable as a free fallback—most major recipe sites include it for SEO. Designing each adapter against the same interface kept the fallback logic clean and testable.
  • Server Actions kept mutations clean. No route handlers, no client-side fetches for writes, cache invalidation via revalidatePath() co-located with the action. The write pattern stayed consistent across every mutation in the app.

Outcome

  • Mise is where architecture, AI integration, and UX deliberateness all had to work together. Claude isn't a single bolt-on feature—it runs in two separate places, each with a different job: swapping individual ingredients on demand, and shifting an entire recipe toward a dietary goal in one pass. Each integration is scoped, prompt-engineered, and quality-controlled at the model level rather than post-processed in code.
  • The import pipeline handles real-world failure gracefully at every tier. The session model means you can set down your phone mid-recipe and come back without losing your place. The image proxy was simple until it wasn't—and debugging the CDN cache collision required understanding how Netlify's edge layer keys its cache, not just reading error logs.
  • The separation between Editorial and Cook turned out to be the right call: each surface has one job, which kept both clean in ways that a unified view would have compromised.
  • Code lives here: github.com/meganleclair/mise-intelligent-chef.