Spaces:
Sleeping
Sleeping
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8" /> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0" /> | |
| <title>BiteWise — Recipe Adapter</title> | |
| <link rel="preconnect" href="https://fonts.googleapis.com"> | |
| <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> | |
| <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=Playfair+Display:wght@600;700&display=swap" rel="stylesheet"> | |
| <style> | |
| :root{ | |
| --bg:#fafbfc; | |
| --card:#ffffff; | |
| --text:#0f1a2e; | |
| --text-soft:#3a4a63; | |
| --muted:#6b7689; | |
| --muted2:#9aa3b5; | |
| --line:#eaedf3; | |
| --line-soft:#f2f4f8; | |
| --shadow-lg: 0 24px 60px -20px rgba(15,26,46,.18); | |
| --shadow-md: 0 10px 30px -10px rgba(15,26,46,.10); | |
| --shadow-sm: 0 2px 8px rgba(15,26,46,.04); | |
| --green:#3fb487; | |
| --green-dark:#1f8d63; | |
| --green-darker:#176b4a; | |
| --mint:#e8f8f1; | |
| --mint-soft:#f3fbf7; | |
| --amber:#fff4de; | |
| --amber-text:#a86c0b; | |
| --blue:#eef3ff; | |
| --blue-text:#4057c7; | |
| --rose:#fcedef; | |
| --rose-text:#bd4b60; | |
| --radius-lg: 22px; | |
| --radius-md: 16px; | |
| --radius-sm: 12px; | |
| } | |
| * { box-sizing: border-box; } | |
| html, body { margin:0; padding:0; } | |
| body{ | |
| font-family: Inter, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; | |
| color: var(--text); | |
| background: | |
| radial-gradient(1200px 600px at 10% -10%, rgba(63,180,135,.10), transparent 60%), | |
| radial-gradient(900px 500px at 100% 0%, rgba(64,87,199,.07), transparent 55%), | |
| var(--bg); | |
| min-height: 100vh; | |
| -webkit-font-smoothing: antialiased; | |
| } | |
| .shell{ | |
| max-width: 1180px; | |
| margin: 0 auto; | |
| padding: 28px 24px 64px; | |
| } | |
| /* ---------- Top bar ---------- */ | |
| .topbar{ | |
| display:flex; | |
| justify-content:space-between; | |
| align-items:center; | |
| gap:16px; | |
| margin-bottom: 36px; | |
| } | |
| .brand{ | |
| display:flex; | |
| align-items:center; | |
| gap:14px; | |
| } | |
| .brand-mark{ | |
| width: 44px; | |
| height: 44px; | |
| border-radius: 14px; | |
| background: linear-gradient(135deg, #ffffff 0%, #f1f9f5 100%); | |
| border: 1px solid rgba(63,180,135,.22); | |
| display:grid; | |
| place-items:center; | |
| box-shadow: var(--shadow-sm); | |
| font-size: 20px; | |
| } | |
| .brand-name h1{ | |
| margin:0; | |
| font-family: "Playfair Display", serif; | |
| font-size: 26px; | |
| letter-spacing: -0.03em; | |
| font-weight: 700; | |
| line-height: 1; | |
| } | |
| .brand-name span{ | |
| display:block; | |
| margin-top: 5px; | |
| color: var(--muted); | |
| font-size: 13px; | |
| } | |
| .top-pill{ | |
| display:inline-flex; | |
| align-items:center; | |
| gap:8px; | |
| padding: 9px 14px; | |
| border-radius: 999px; | |
| border: 1px solid var(--line); | |
| background: rgba(255,255,255,.7); | |
| color: var(--muted); | |
| font-size: 12.5px; | |
| font-weight: 500; | |
| } | |
| .top-pill::before{ | |
| content:""; | |
| width:7px; height:7px; | |
| border-radius:50%; | |
| background: var(--green); | |
| box-shadow: 0 0 0 4px rgba(63,180,135,.15); | |
| } | |
| /* ---------- Hero ---------- */ | |
| .hero{ | |
| display:grid; | |
| grid-template-columns: 1fr; | |
| gap: 14px; | |
| margin-bottom: 28px; | |
| text-align: center; | |
| } | |
| .eyebrow{ | |
| display:inline-flex; | |
| align-items:center; | |
| gap:8px; | |
| font-size: 11.5px; | |
| font-weight: 700; | |
| letter-spacing: .12em; | |
| text-transform: uppercase; | |
| color: var(--green-darker); | |
| background: var(--mint); | |
| border: 1px solid rgba(63,180,135,.18); | |
| padding: 7px 12px; | |
| border-radius: 999px; | |
| margin: 0 auto; | |
| } | |
| .hero h2{ | |
| margin: 0; | |
| font-size: clamp(36px, 5vw, 60px); | |
| line-height: 1.02; | |
| letter-spacing: -0.045em; | |
| font-weight: 700; | |
| } | |
| .hero h2 em{ | |
| font-family: "Playfair Display", serif; | |
| font-style: italic; | |
| color: var(--green-darker); | |
| font-weight: 600; | |
| } | |
| .hero p{ | |
| margin: 4px auto 0; | |
| max-width: 58ch; | |
| font-size: 16px; | |
| line-height: 1.65; | |
| color: var(--muted); | |
| } | |
| /* ---------- Card ---------- */ | |
| .card{ | |
| background: var(--card); | |
| border: 1px solid var(--line); | |
| border-radius: var(--radius-lg); | |
| box-shadow: var(--shadow-md); | |
| } | |
| /* ---------- Composer ---------- */ | |
| .composer{ | |
| padding: 26px; | |
| margin-bottom: 22px; | |
| } | |
| .composer-head{ | |
| display:flex; | |
| justify-content:space-between; | |
| align-items:center; | |
| gap:14px; | |
| margin-bottom: 18px; | |
| flex-wrap:wrap; | |
| } | |
| .composer-head h3{ | |
| margin:0; | |
| font-size: 17px; | |
| letter-spacing: -0.02em; | |
| font-weight: 700; | |
| } | |
| .composer-head p{ | |
| margin:4px 0 0; | |
| color: var(--muted); | |
| font-size: 13.5px; | |
| } | |
| .segmented{ | |
| display:inline-flex; | |
| gap: 4px; | |
| padding: 4px; | |
| border-radius: 999px; | |
| background: var(--line-soft); | |
| border: 1px solid var(--line); | |
| } | |
| .diet-btn{ | |
| border: 0; | |
| background: transparent; | |
| color: var(--muted); | |
| padding: 9px 16px; | |
| border-radius: 999px; | |
| font-size: 13px; | |
| font-weight: 600; | |
| cursor: pointer; | |
| transition: all .18s ease; | |
| } | |
| .diet-btn:hover{ color: var(--text); } | |
| .diet-btn.active-vegan{ background: #fff; color: var(--green-darker); box-shadow: 0 1px 3px rgba(15,26,46,.08); } | |
| .diet-btn.active-keto{ background: #fff; color: var(--amber-text); box-shadow: 0 1px 3px rgba(15,26,46,.08); } | |
| .diet-btn.active-both{ background: #fff; color: var(--blue-text); box-shadow: 0 1px 3px rgba(15,26,46,.08); } | |
| .textarea-wrap{ | |
| position: relative; | |
| } | |
| textarea{ | |
| width: 100%; | |
| min-height: 180px; | |
| resize: vertical; | |
| border-radius: var(--radius-md); | |
| border: 1px solid var(--line); | |
| background: #fcfcfd; | |
| color: var(--text); | |
| padding: 16px 18px; | |
| font: inherit; | |
| font-size: 15px; | |
| line-height: 1.65; | |
| outline:none; | |
| transition: border-color .15s ease, box-shadow .15s ease, background .15s ease; | |
| } | |
| textarea:focus{ | |
| border-color: rgba(63,180,135,.45); | |
| box-shadow: 0 0 0 4px rgba(63,180,135,.10); | |
| background: #fff; | |
| } | |
| textarea::placeholder{ color:#a3adbe; } | |
| /* Quick try chips */ | |
| .tries-row{ | |
| display:flex; | |
| align-items:center; | |
| gap:10px; | |
| margin-top: 14px; | |
| flex-wrap:wrap; | |
| } | |
| .tries-label{ | |
| font-size: 12.5px; | |
| font-weight: 600; | |
| color: var(--muted); | |
| } | |
| .chip{ | |
| appearance:none; | |
| border: 1px solid var(--line); | |
| background: #fff; | |
| color: var(--text-soft); | |
| border-radius: 999px; | |
| padding: 8px 13px; | |
| font-size: 12.5px; | |
| font-weight: 600; | |
| cursor:pointer; | |
| transition: all .15s ease; | |
| display:inline-flex; | |
| align-items:center; | |
| gap:6px; | |
| } | |
| .chip:hover{ | |
| border-color: rgba(63,180,135,.4); | |
| background: var(--mint-soft); | |
| color: var(--green-darker); | |
| transform: translateY(-1px); | |
| } | |
| .chip-icon{ font-size: 13px; } | |
| .input-actions{ | |
| margin-top: 18px; | |
| display:flex; | |
| flex-wrap:wrap; | |
| gap: 10px; | |
| align-items:center; | |
| justify-content:space-between; | |
| } | |
| .helper{ | |
| display:flex; | |
| flex-wrap:wrap; | |
| gap:6px; | |
| color: var(--muted); | |
| font-size: 12.5px; | |
| align-items:center; | |
| } | |
| .helper .step{ | |
| display:inline-flex; | |
| align-items:center; | |
| gap:6px; | |
| } | |
| .step-num{ | |
| width:18px; height:18px; | |
| border-radius:50%; | |
| background: var(--mint); | |
| color: var(--green-darker); | |
| font-weight:700; | |
| font-size: 11px; | |
| display:grid; | |
| place-items:center; | |
| } | |
| .helper .sep{ color: var(--muted2); } | |
| .action-row{ | |
| display:flex; | |
| gap: 10px; | |
| align-items:center; | |
| } | |
| .primary{ | |
| border: none; | |
| border-radius: var(--radius-md); | |
| padding: 13px 22px; | |
| font-weight: 700; | |
| font-size: 14px; | |
| cursor:pointer; | |
| color:#fff; | |
| background: linear-gradient(135deg, var(--green) 0%, var(--green-dark) 100%); | |
| box-shadow: 0 8px 20px -4px rgba(63,180,135,.45); | |
| transition: all .18s ease; | |
| display:inline-flex; | |
| align-items:center; | |
| gap:8px; | |
| } | |
| .primary:hover{ | |
| transform: translateY(-1px); | |
| box-shadow: 0 12px 28px -4px rgba(63,180,135,.55); | |
| } | |
| .primary:active{ transform: translateY(0); } | |
| .primary:disabled{ cursor: not-allowed; } | |
| .ghost{ | |
| border: 1px solid var(--line); | |
| background:#fff; | |
| color: var(--text-soft); | |
| border-radius: var(--radius-md); | |
| padding: 13px 18px; | |
| font-weight: 600; | |
| font-size: 14px; | |
| cursor:pointer; | |
| transition: all .15s ease; | |
| } | |
| .ghost:hover{ | |
| background: var(--line-soft); | |
| border-color: #d8dde6; | |
| } | |
| .loading{ | |
| display:none; | |
| margin-top: 14px; | |
| align-items:center; | |
| gap:10px; | |
| font-size: 13px; | |
| color: var(--muted); | |
| } | |
| .spinner{ | |
| width: 16px; | |
| height: 16px; | |
| border-radius: 999px; | |
| border: 2px solid rgba(63,180,135,.18); | |
| border-top-color: var(--green); | |
| animation: spin .8s linear infinite; | |
| } | |
| @keyframes spin{ to { transform: rotate(360deg); } } | |
| .error{ | |
| display:none; | |
| margin-top: 14px; | |
| border: 1px solid rgba(189,75,96,.22); | |
| background: var(--rose); | |
| color: var(--rose-text); | |
| border-radius: var(--radius-md); | |
| padding: 12px 14px; | |
| font-size: 13px; | |
| font-weight: 500; | |
| } | |
| /* ---------- Feature row (replaces hero mini-cards) ---------- */ | |
| .feature-row{ | |
| display:grid; | |
| grid-template-columns: repeat(3, 1fr); | |
| gap: 14px; | |
| margin-bottom: 28px; | |
| } | |
| .feature{ | |
| background: #fff; | |
| border: 1px solid var(--line); | |
| border-radius: var(--radius-md); | |
| padding: 18px; | |
| transition: transform .2s ease, box-shadow .2s ease; | |
| } | |
| .feature:hover{ | |
| transform: translateY(-2px); | |
| box-shadow: var(--shadow-md); | |
| } | |
| .feature-icon{ | |
| width: 36px; | |
| height: 36px; | |
| border-radius: 10px; | |
| display:grid; | |
| place-items:center; | |
| font-size: 16px; | |
| margin-bottom: 12px; | |
| } | |
| .feature-icon.g{ background: var(--mint); } | |
| .feature-icon.b{ background: var(--blue); } | |
| .feature-icon.a{ background: var(--amber); } | |
| .feature h4{ | |
| margin: 0 0 4px; | |
| font-size: 14px; | |
| font-weight: 700; | |
| letter-spacing: -0.01em; | |
| } | |
| .feature p{ | |
| margin: 0; | |
| color: var(--muted); | |
| font-size: 13px; | |
| line-height: 1.55; | |
| } | |
| /* ---------- Results ---------- */ | |
| .results-wrap{ | |
| display:none; | |
| padding: 26px; | |
| } | |
| .stats{ | |
| display:grid; | |
| grid-template-columns: repeat(3, 1fr); | |
| gap: 12px; | |
| margin-bottom: 20px; | |
| } | |
| .stat{ | |
| border: 1px solid var(--line); | |
| background: linear-gradient(180deg, #fff 0%, #fafbfd 100%); | |
| border-radius: var(--radius-md); | |
| padding: 16px 18px; | |
| } | |
| .stat .num{ | |
| font-size: 28px; | |
| font-weight: 800; | |
| letter-spacing: -0.04em; | |
| line-height: 1; | |
| } | |
| .stat .lbl{ | |
| color: var(--muted); | |
| font-size: 11.5px; | |
| text-transform: uppercase; | |
| letter-spacing: .08em; | |
| font-weight: 600; | |
| margin-top: 6px; | |
| } | |
| .results-head{ | |
| display:flex; | |
| justify-content:space-between; | |
| align-items:flex-start; | |
| gap:12px; | |
| margin-bottom: 16px; | |
| flex-wrap:wrap; | |
| } | |
| .results-head h3{ | |
| margin:0; | |
| font-size: 20px; | |
| letter-spacing: -0.03em; | |
| font-weight: 700; | |
| } | |
| .results-head p{ | |
| margin:6px 0 0; | |
| color: var(--muted); | |
| font-size: 13.5px; | |
| } | |
| .badge{ | |
| display:inline-flex; | |
| align-items:center; | |
| gap:8px; | |
| font-size: 12px; | |
| font-weight: 700; | |
| padding: 8px 12px; | |
| border-radius: 999px; | |
| } | |
| .badge-vegan{ background: var(--mint); color: var(--green-darker); } | |
| .badge-keto{ background: var(--amber); color: var(--amber-text); } | |
| .badge-both{ background: var(--blue); color: var(--blue-text); } | |
| .list{ | |
| display:grid; | |
| gap: 10px; | |
| } | |
| .item{ | |
| overflow:hidden; | |
| border-radius: var(--radius-md); | |
| border: 1px solid var(--line); | |
| background: #fff; | |
| transition: border-color .15s ease, box-shadow .15s ease; | |
| } | |
| .item:hover{ | |
| border-color: #dde2eb; | |
| box-shadow: var(--shadow-sm); | |
| } | |
| .item-top{ | |
| display:flex; | |
| justify-content:space-between; | |
| align-items:center; | |
| gap:12px; | |
| padding: 14px 16px; | |
| cursor:pointer; | |
| user-select: none; | |
| } | |
| .item-left{ | |
| display:flex; | |
| align-items:center; | |
| gap:12px; | |
| min-width: 0; | |
| } | |
| .dot{ | |
| width: 9px; | |
| height: 9px; | |
| border-radius:999px; | |
| flex:0 0 auto; | |
| position: relative; | |
| } | |
| .dot::after{ | |
| content:""; | |
| position: absolute; | |
| inset: -4px; | |
| border-radius: 50%; | |
| opacity: .25; | |
| } | |
| .dot-ok{ background: var(--green); } | |
| .dot-ok::after{ background: var(--green); } | |
| .dot-swap{ background: #f0a84c; } | |
| .dot-swap::after{ background: #f0a84c; } | |
| .dot-warn{ background: #f36f7f; } | |
| .dot-warn::after{ background: #f36f7f; } | |
| .item-name{ | |
| font-weight: 600; | |
| font-size: 14.5px; | |
| white-space:nowrap; | |
| overflow:hidden; | |
| text-overflow:ellipsis; | |
| } | |
| .item-right{ | |
| display:flex; | |
| align-items:center; | |
| gap:10px; | |
| flex-shrink: 0; | |
| } | |
| .item-status{ | |
| color: var(--muted); | |
| font-size: 12px; | |
| font-weight: 500; | |
| } | |
| .chevron{ | |
| width: 16px; | |
| height: 16px; | |
| color: var(--muted2); | |
| transition: transform .2s ease; | |
| } | |
| .item-top.open .chevron{ transform: rotate(180deg); } | |
| .item-body{ | |
| display:none; | |
| border-top: 1px solid var(--line); | |
| background: #fcfcfd; | |
| padding: 18px; | |
| } | |
| .item-body.open{ display:block; } | |
| .label{ | |
| font-size: 10.5px; | |
| text-transform: uppercase; | |
| letter-spacing: .1em; | |
| color: var(--muted2); | |
| margin-bottom: 6px; | |
| font-weight: 700; | |
| } | |
| .value{ | |
| font-size: 15px; | |
| font-weight: 600; | |
| color: var(--text); | |
| line-height: 1.55; | |
| margin-bottom: 14px; | |
| } | |
| .value.soft{ | |
| font-weight: 500; | |
| color: var(--text-soft); | |
| font-size: 14px; | |
| } | |
| .meta{ | |
| font-size: 11.5px; | |
| color: var(--muted); | |
| display:flex; | |
| flex-wrap:wrap; | |
| gap: 6px; | |
| } | |
| .meta span{ | |
| background: #fff; | |
| border: 1px solid var(--line); | |
| border-radius: 999px; | |
| padding: 5px 10px; | |
| font-weight: 500; | |
| } | |
| @media (max-width: 880px){ | |
| .feature-row, .stats{ grid-template-columns: 1fr; } | |
| .topbar{ flex-direction: column; align-items:flex-start; } | |
| .composer-head{ flex-direction: column; align-items:flex-start; } | |
| } | |
| @media (max-width: 560px){ | |
| .shell{ padding: 18px 14px 40px; } | |
| .composer, .results-wrap{ padding: 20px; } | |
| .input-actions{ flex-direction: column; align-items:stretch; } | |
| .action-row{ width:100%; } | |
| .action-row .primary, .action-row .ghost{ | |
| flex: 1; | |
| justify-content:center; | |
| } | |
| .tries-row{ flex-direction: column; align-items: flex-start; } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="shell"> | |
| <div class="topbar"> | |
| <div class="brand"> | |
| <div class="brand-mark">🍽️</div> | |
| <div class="brand-name"> | |
| <h1>BiteWise</h1> | |
| <span>Recipe adaptation that feels simple.</span> | |
| </div> | |
| </div> | |
| <div class="top-pill">Built for vegan and keto cooking</div> | |
| </div> | |
| <section class="hero"> | |
| <span class="eyebrow">Diet-friendly cooking, without the guesswork</span> | |
| <h2>Turn any recipe into a <em>clear</em> adaptation.</h2> | |
| <p>Paste a recipe, choose a diet, and get ingredient-by-ingredient substitutions with short, practical instructions.</p> | |
| </section> | |
| <section class="feature-row"> | |
| <div class="feature"> | |
| <div class="feature-icon g">🔄</div> | |
| <h4>Ingredient-level swaps</h4> | |
| <p>Each item gets its own card with the substitute and a short usage note.</p> | |
| </div> | |
| <div class="feature"> | |
| <div class="feature-icon b">📖</div> | |
| <h4>Context aware</h4> | |
| <p>Results adapt to baking or cooking based on what you paste in.</p> | |
| </div> | |
| <div class="feature"> | |
| <div class="feature-icon a">✓</div> | |
| <h4>Clear, reviewable output</h4> | |
| <p>Open any card to inspect the substitute, instructions, and source.</p> | |
| </div> | |
| </section> | |
| <section class="card composer"> | |
| <div class="composer-head"> | |
| <div> | |
| <h3>Adapt a recipe</h3> | |
| <p>Paste anything from a recipe post or notes app, then pick a diet. Separate ingredients with commas. This is an assistive recipe tool, not nutritional or allergen medical advice. Always verify substitutions based on your dietary needs.</p> | |
| </div> | |
| <div class="segmented" role="tablist" aria-label="Diet mode"> | |
| <button class="diet-btn active-vegan" data-diet="vegan" onclick="setDiet(this)">Vegan</button> | |
| <button class="diet-btn" data-diet="keto" onclick="setDiet(this)">Keto</button> | |
| <button class="diet-btn" data-diet="both" onclick="setDiet(this)">Both</button> | |
| </div> | |
| </div> | |
| <div class="textarea-wrap"> | |
| <textarea id="recipeInput" placeholder="Paste your recipe here. For example: 2 cups flour, 2 eggs, 1/2 cup butter, 1 cup milk, vanilla extract..."></textarea> | |
| </div> | |
| <div class="tries-row"> | |
| <span class="tries-label">Try a sample:</span> | |
| <button class="chip" onclick="fillExample('alfredo')"><span class="chip-icon">🍝</span>Fettuccine Alfredo</button> | |
| <button class="chip" onclick="fillExample('pancakes')"><span class="chip-icon">🥞</span>Pancakes</button> | |
| <button class="chip" onclick="fillExample('butter chicken')"><span class="chip-icon">🍛</span>Butter Chicken</button> | |
| </div> | |
| <div class="input-actions"> | |
| <div class="helper" aria-live="polite"> | |
| <span class="step"><span class="step-num">1</span>Paste recipe</span> | |
| <span class="sep">·</span> | |
| <span class="step"><span class="step-num">2</span>Choose diet</span> | |
| <span class="sep">·</span> | |
| <span class="step"><span class="step-num">3</span>Review swaps</span> | |
| </div> | |
| <div class="action-row"> | |
| <button class="ghost" onclick="clearInput()">Clear</button> | |
| <button class="primary" id="submitBtn" onclick="run()"> | |
| <span>Adapt recipe</span> | |
| <span aria-hidden="true">→</span> | |
| </button> | |
| </div> | |
| </div> | |
| <div class="loading" id="loading"> | |
| <div class="spinner"></div> | |
| <div>Adapting recipe…</div> | |
| </div> | |
| <div class="error" id="errBox"></div> | |
| </section> | |
| <section class="card results-wrap" id="resultsWrap"> | |
| <div class="stats"> | |
| <div class="stat"> | |
| <div class="num" id="statTotal">—</div> | |
| <div class="lbl">Ingredients found</div> | |
| </div> | |
| <div class="stat"> | |
| <div class="num" id="statSwap">—</div> | |
| <div class="lbl">Substitutions suggested</div> | |
| </div> | |
| <div class="stat"> | |
| <div class="num" id="statContext">—</div> | |
| <div class="lbl">Recipe type</div> | |
| </div> | |
| </div> | |
| <div class="results-head"> | |
| <div> | |
| <h3 id="resultTitle">Adapted recipe</h3> | |
| <p id="resultSubtitle">Review each ingredient and expand any card for details.</p> | |
| </div> | |
| <div class="badge" id="resultBadge"></div> | |
| </div> | |
| <div class="list" id="ingList"></div> | |
| </section> | |
| </div> | |
| <script> | |
| let currentDiet = "vegan"; | |
| function setDiet(btn) { | |
| currentDiet = btn.dataset.diet; | |
| document.querySelectorAll(".diet-btn").forEach(b => b.className = "diet-btn"); | |
| btn.className = "diet-btn active-" + currentDiet; | |
| } | |
| function fillExample(name) { | |
| const examples = { | |
| "alfredo": "24 ounces fettuccine, 1 cup butter, 1 pint heavy cream, 1 cup grated Pecorino romano, 1 cup grated Parmesan", | |
| "pancakes": "1 cup all-purpose flour, 1 tbsp white sugar, 1 tsp baking powder, 1 cup milk, 2 tbsp melted butter", | |
| "butter chicken": "450g chicken breast, 2 tbsp butter, 1 cup tomato puree, 1/2 cup heavy cream, 1/2 cup yogurt" | |
| }; | |
| const ta = document.getElementById("recipeInput"); | |
| ta.value = examples[name] || ""; | |
| ta.focus(); | |
| } | |
| function clearInput() { | |
| document.getElementById("recipeInput").value = ""; | |
| document.getElementById("recipeInput").focus(); | |
| } | |
| function toggleCard(id) { | |
| const el = document.getElementById("body-" + id); | |
| const top = document.getElementById("top-" + id); | |
| if (el) el.classList.toggle("open"); | |
| if (top) top.classList.toggle("open"); | |
| } | |
| function normalizeResponse(data) { | |
| const ingredientItems = Array.isArray(data.ingredients) ? data.ingredients : []; | |
| return { | |
| diet: data.diet || currentDiet, | |
| recipe_type: data.recipe_type || data.recipeType || "cooked", | |
| ingredients_found: data.ingredients_found ?? ingredientItems.length, | |
| substitution_count: data.substitution_count ?? ingredientItems.filter(i => !i.compliant).length, | |
| ingredients: ingredientItems.map((item) => ({ | |
| original: item.original ?? item.ingredient ?? item.normalized ?? "unknown ingredient", | |
| normalized: item.normalized ?? item.original ?? item.ingredient ?? "", | |
| compliant: Boolean(item.compliant), | |
| substitute: item.substitute ?? item.vegan_sub ?? item.keto_sub ?? item.sub ?? item.original ?? "", | |
| instructions: item.instructions ?? item.instruction ?? item.notes ?? "No details provided.", | |
| source: item.source ?? item.via ?? item.matched_via ?? "lookup", | |
| matched_ingredient: item.matched_ingredient ?? item.matchedIngredient ?? null, | |
| confidence: item.confidence ?? null, | |
| notes: item.notes ?? "" | |
| })) | |
| }; | |
| } | |
| function badgeForDiet(diet) { | |
| if (diet === "keto") return ["Keto", "badge-keto"]; | |
| if (diet === "both") return ["Vegan + Keto", "badge-both"]; | |
| return ["Vegan", "badge-vegan"]; | |
| } | |
| function recipeTypeLabel(recipeType) { | |
| const t = String(recipeType || "").toLowerCase(); | |
| if (t.includes("bak")) return "Baking"; | |
| return "Cooking"; | |
| } | |
| function setLoading(on) { | |
| document.getElementById("loading").style.display = on ? "flex" : "none"; | |
| const btn = document.getElementById("submitBtn"); | |
| btn.disabled = on; | |
| btn.style.opacity = on ? "0.7" : "1"; | |
| } | |
| function run() { | |
| const recipe = document.getElementById("recipeInput").value.trim(); | |
| const err = document.getElementById("errBox"); | |
| err.style.display = "none"; | |
| if (!recipe || recipe.length < 8) { | |
| err.textContent = "Paste a recipe with a few ingredients first."; | |
| err.style.display = "block"; | |
| return; | |
| } | |
| setLoading(true); | |
| fetch("/api/adapt", { | |
| method: "POST", | |
| headers: { "Content-Type": "application/json" }, | |
| body: JSON.stringify({ | |
| recipe_text: recipe, | |
| diet: currentDiet | |
| }) | |
| }) | |
| .then(async (res) => { | |
| if (!res.ok) { | |
| const text = await res.text(); | |
| throw new Error(text || "Server error"); | |
| } | |
| return res.json(); | |
| }) | |
| .then((data) => { | |
| const out = normalizeResponse(data); | |
| renderResults(out); | |
| }) | |
| .catch((e) => { | |
| err.textContent = "The backend could not be reached. Check the Space logs and try again."; | |
| err.style.display = "block"; | |
| console.error(e); | |
| }) | |
| .finally(() => { | |
| setLoading(false); | |
| }); | |
| } | |
| function renderResults(data) { | |
| document.getElementById("resultsWrap").style.display = "block"; | |
| document.getElementById("statTotal").textContent = data.ingredients_found; | |
| document.getElementById("statSwap").textContent = data.substitution_count; | |
| document.getElementById("statContext").textContent = recipeTypeLabel(data.recipe_type); | |
| const [badgeText, badgeClass] = badgeForDiet(data.diet); | |
| const badge = document.getElementById("resultBadge"); | |
| badge.textContent = badgeText; | |
| badge.className = "badge " + badgeClass; | |
| document.getElementById("resultTitle").textContent = recipeTypeLabel(data.recipe_type) + " recipe ready"; | |
| document.getElementById("resultSubtitle").textContent = "Open any card to inspect the substitution, notes, and source."; | |
| const list = document.getElementById("ingList"); | |
| list.innerHTML = ""; | |
| data.ingredients.forEach((r, i) => { | |
| const dotClass = r.compliant | |
| ? "dot dot-ok" | |
| : (r.confidence && r.confidence < 0.6 ? "dot dot-warn" : "dot dot-swap"); | |
| const statusText = r.compliant ? "compatible" : "swap suggested"; | |
| const chevron = `<svg class="chevron" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"></polyline></svg>`; | |
| const item = document.createElement("div"); | |
| item.className = "item"; | |
| item.innerHTML = ` | |
| <div class="item-top" id="top-${i}" onclick="toggleCard(${i})"> | |
| <div class="item-left"> | |
| <span class="${dotClass}"></span> | |
| <div class="item-name">${escapeHtml(r.original)}</div> | |
| </div> | |
| <div class="item-right"> | |
| <div class="item-status">${statusText}</div> | |
| ${chevron} | |
| </div> | |
| </div> | |
| <div class="item-body" id="body-${i}"> | |
| <div class="label">${r.compliant ? "Result" : "Use instead"}</div> | |
| <div class="value">${escapeHtml(r.substitute || r.original)}</div> | |
| <div class="label">How to use</div> | |
| <div class="value soft">${escapeHtml(r.instructions || "No details provided.")}</div> | |
| <div class="meta"> | |
| <span>Source: ${escapeHtml(r.source || "lookup")}</span> | |
| ${r.matched_ingredient ? `<span>Matched: ${escapeHtml(r.matched_ingredient)}</span>` : ""} | |
| ${r.confidence !== null && r.confidence !== undefined ? `<span>Confidence: ${escapeHtml(String(r.confidence))}</span>` : ""} | |
| </div> | |
| </div> | |
| `; | |
| list.appendChild(item); | |
| }); | |
| document.getElementById("resultsWrap").scrollIntoView({ behavior: "smooth", block: "start" }); | |
| } | |
| function escapeHtml(str) { | |
| return String(str) | |
| .replaceAll("&", "&") | |
| .replaceAll("<", "<") | |
| .replaceAll(">", ">") | |
| .replaceAll('"', """) | |
| .replaceAll("'", "'"); | |
| } | |
| </script> | |
| </body> | |
| </html> |