- TypeScript 89.3%
- JavaScript 8.5%
- CSS 1.9%
- Shell 0.2%
| public | ||
| scripts | ||
| src | ||
| .env.local.example | ||
| .gitignore | ||
| eslint.config.js | ||
| HANDOFF.md | ||
| index.html | ||
| package-lock.json | ||
| package.json | ||
| postcss.config.js | ||
| README.md | ||
| tailwind.config.js | ||
| tsconfig.app.json | ||
| tsconfig.json | ||
| tsconfig.node.json | ||
| vite.config.ts | ||
Ember
A personal vegetarian dinner app with a transparent taste-learning loop. Built for one user — you.
What's in this build (tier 0+)
The learning loop:
- Home screen — top match, three different directions, "Because you cooked X," "Crispy & substantial," Learning Corner.
- Recipe Detail — Why-this panel showing live signal weights, source attribution, ingredients, method, texture tip, failure warning. Plus/minus stepper next to "Serves" scales all ingredient quantities live (with smart rounding — eggs stay whole, grams round to 5/10/25).
- Taste Profile — editable weights, pattern insights, novelty slider, reset.
- Post-cook feedback modal — 3-button verdict + reason chips → updates weights and shows the receipt.
The Tuesday-at-6pm tool:
- Cook from Ingredients (rule-based, no API) — pick ingredients (autocomplete from your library + pantry), optional filters (time / vibe / texture), star one as must-use. Returns 5 format-diverse matches from your library ranked by ingredient overlap × taste score. Each shows match %, missing ingredients, source attribution.
The library:
- 20 seeded recipes with diverse authors and visual styles — inspired by Ottolenghi, Hetty McKinnon, Felicity Cloake, J. Kenji López-Alt, Andrea Nguyen, Smitten Kitchen, Eric Kim, Pailin Chongchitnant, Sohla El-Waylly, Anna Jones, Madhur Jaffrey.
- Recipes tab — search, filter by cuisine, browse All / Saved / Cooked / Imported.
- Source attribution on every recipe — type (house / imported / inspired), author, publication, optional URL, optional inspiration note.
The growth path:
- URL import — paste a URL from any site that publishes schema.org/Recipe data (NYT Cooking, Bon Appétit, Smitten Kitchen, Serious Eats, Guardian, BBC Good Food, Half Baked Harvest, etc.). Goes through a CORS proxy, auto-tags with sensible defaults, you adjust before saving.
- Paste-HTML fallback — when the URL fetch is blocked (CORS / paywall / wifi), paste the page HTML directly.
Persistence:
localStoragekeyember.userState.v1holds your weights, interactions, saved/cooked sets, novelty ratio, and imported recipes.- No backend, no accounts, no third-party analytics.
What's not in this build (still deferred)
- Weekly Planner · Shopping List · Cook Mode · Technique Lab.
- "Goes well with" pairings on Recipe Detail (next obvious add).
- LLM-assisted parsing for messy sites without schema.org/Recipe.
- RSS-feed-based discovery. Requires a server — that's tier 2.
- Vercel deploy (5-minute job when you want it).
- AI recipe generation. Deliberately skipped — rule-based matching against your real library is honest and doesn't need an API key.
Run it
npm install
npm run dev -- --host 0.0.0.0
The dev server prints two URLs:
- Local — open on this machine: http://localhost:5173
- Network — open on your phone (same wifi): e.g.
http://192.168.x.x:5173
Open on your phone (the real test)
- Make sure your phone is on the same wifi as this Mac.
- Open Safari, type the Network URL.
- Tap the share icon → Add to Home Screen. Installs like a real app.
If the network URL doesn't work: AP isolation is enabled on the wifi. Switch it off in router settings, or run npx ngrok http 5173 for a temporary public URL.
The recipe sourcing strategy (the long view)
| Stage | What it is | Where you are now |
|---|---|---|
| Manual seeds | Hand-written house recipes with attribution to the chefs they emulate. | ✓ Built (20 recipes) |
| URL import | Paste any URL with schema.org Recipe data → app parses, you adjust tags, save. | ✓ Built |
| Paste HTML fallback | For sites blocked by CORS — paste page source manually. | ✓ Built |
| LLM-assisted parsing | For messy pages with no schema, send to Claude API for parsing. | Tier 1 |
| RSS discovery feed | Subscribe to NYT Cooking, Guardian, Smitten Kitchen, Ottolenghi, etc. Daily cron fetches new posts, Claude pre-tags, you adopt the keepers. | Tier 2 (needs server) |
| Self-hosted on your server | Move from localStorage to your own DB, enable RSS cron jobs, optional sync between devices. | Tier 2+ |
Mass-scraping entire sites is deliberately not on this list — both legally murky and a maintenance nightmare. RSS + structured-data extraction is the clean path.
How the learning loop works
Every interaction adjusts weights immediately:
| Action | Effect |
|---|---|
| Open a recipe | Logged. No weight change. (Implicit signal — future use.) |
| ✕ Skip (Recipe Detail) | −0.08 across tags. 365-day cooldown on this recipe. |
| ☆ Save | Marks saved (implicit positive). |
| 🍳 Cook this | Marks as cooked, opens the post-cook modal. |
| Post-cook: Make again | +0.12 across tags. Strongest positive signal. |
| Post-cook: It was fine | +0.02. Mild positive. |
| Post-cook: Not worth it | −0.15. Strong negative. |
| Reason chips (positive) | +0.04 with category-specific bumps (crispy → texture weight). |
| Reason chips (negative) | −0.05 with anti-flag-specific bumps (mushy → mushy anti-tag). |
The Why-this panel and Taste Profile read directly from these weights. No hidden model — what you see is what's scoring.
Source attribution model
Every recipe carries:
source: {
type: 'house' | 'imported' | 'inspired',
url?: string, // present for imported
author?: string, // chef / writer
publication?: string, // NYT Cooking, Smitten Kitchen, etc.
note?: string, // free-text inspiration note
}
house— composed by Ember, with a note pointing to the chef/style that inspired the technique.imported— parsed from a URL via the import flow, full attribution preserved, original URL stored for verification.inspired— composed but explicitly modelled on a specific named recipe.
Every Recipe Detail shows a source panel at the top. Every mini card on Home and Recipes shows a small attribution line.
Add or change recipes manually
Edit src/data/recipes.ts. The Recipe shape:
{
id, title, subtitle,
source: { type, author, publication, note, url? },
cuisine, format, textures[], carbBase, vibes[], antiTags[],
keyIngredients[], timeMinutes, effort, serves,
ingredients: [{ qty, name }],
steps: [string],
textureTip, // the one technique move that matters
failureWarning, // what usually goes wrong
imageClass, // CSS gradient class in src/index.css
recoveryAdaptation? // optional cycling-mode swap
}
Adding more recipes shows up on Home immediately — the scoring engine doesn't need a rebuild.
Reset your taste profile
Taste Profile screen → "Reset everything" at the bottom. Wipes localStorage and reloads seed weights.
Stack
- Vite + React 18 + TypeScript
- Tailwind CSS v3 (warm palette: ivory, ember, olive, paprika, brick — no purple AI gradients)
- localStorage for persistence
- Recipe parsing: schema.org/Recipe JSON-LD via DOM regex + CORS proxy (corsproxy.io with AllOrigins fallback)
Next session candidates
- LLM-assisted parsing when schema.org is missing or broken — send page text to Claude API, get a structured Recipe back. Needs an API key.
- Cook from Ingredients — five-direction generator from your pantry.
- Weekly Planner + Shopping List.
- Move to a real server — your own host, Postgres, RSS cron jobs for nightly discovery. The big one.
- Deploy to Vercel (interim) so you can use it on the road.