video understanding
Some checks failed
Build and Deploy / build (push) Failing after 2m17s
Build and Deploy / docker-build (push) Has been skipped
Deploy to Server / deploy (push) Successful in 43s

This commit is contained in:
2026-04-29 11:50:44 -05:00
parent 5bf0622c38
commit 193a825899
14 changed files with 1435 additions and 15 deletions

48
dist/assets/index-CrgoFxjd.js vendored Normal file
View File

@@ -0,0 +1,48 @@
(function(){const t=document.createElement("link").relList;if(t&&t.supports&&t.supports("modulepreload"))return;for(const i of document.querySelectorAll('link[rel="modulepreload"]'))s(i);new MutationObserver(i=>{for(const o of i)if(o.type==="childList")for(const l of o.addedNodes)l.tagName==="LINK"&&l.rel==="modulepreload"&&s(l)}).observe(document,{childList:!0,subtree:!0});function n(i){const o={};return i.integrity&&(o.integrity=i.integrity),i.referrerPolicy&&(o.referrerPolicy=i.referrerPolicy),i.crossOrigin==="use-credentials"?o.credentials="include":i.crossOrigin==="anonymous"?o.credentials="omit":o.credentials="same-origin",o}function s(i){if(i.ep)return;i.ep=!0;const o=n(i);fetch(i.href,o)}})();let g="",y="";const B=document.getElementById("login-btn"),$=document.getElementById("login-overlay"),S=document.getElementById("app-content");B.addEventListener("click",async()=>{const e=document.getElementById("login-username").value,t=document.getElementById("login-password").value,n=document.getElementById("login-btn");n.disabled=!0,n.innerText="Verifying...";try{(await fetch("/api/login",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({username:e,password:t})})).ok?(g=e,y=t,$.style.display="none",S.style.display="block"):document.getElementById("login-error").style.display="block"}catch{document.getElementById("login-error").innerText="Network error. Please try again.",document.getElementById("login-error").style.display="block"}n.disabled=!1,n.innerText="Sign in"});const r=[],v=document.getElementById("ing-wrapper"),c=document.getElementById("ing-input"),P=document.getElementById("gen-btn");P.addEventListener("click",C);v.addEventListener("click",()=>c.focus());c.addEventListener("keydown",e=>{(e.key==="Enter"||e.key===",")&&c.value.trim()?(e.preventDefault(),b(c.value.trim().replace(/,$/,"")),c.value=""):e.key==="Backspace"&&!c.value&&r.length&&O(r.length-1)});function b(e){if(!e||r.includes(e.toLowerCase()))return;r.push(e.toLowerCase());const t=document.createElement("div");t.className="tag",t.innerHTML=e+'<span class="tag-x">×</span>',t.querySelector(".tag-x").addEventListener("click",n=>{n.stopPropagation(),M(e.toLowerCase())}),v.insertBefore(t,c)}function M(e){const t=r.indexOf(e);t>-1&&(r.splice(t,1),I())}function O(e){r.splice(e,1),I()}function I(){v.querySelectorAll(".tag").forEach(t=>t.remove());const e=[...r];r.length=0,e.forEach(t=>b(t))}document.querySelectorAll(".check-pill").forEach(e=>{e.addEventListener("click",()=>{const t=e.querySelector("input");t.checked=!t.checked,e.classList.toggle("checked",t.checked)})});async function C(){const e=document.getElementById("goal").value.trim();if(!e&&r.length===0){E("Please add some ingredients or describe what you want to make.");return}const t=[...document.querySelectorAll(".check-pill input:checked")].map(a=>a.value),n=document.getElementById("cuisine").value,s=document.getElementById("time").value,i=document.getElementById("skill").value,o=document.getElementById("servings").value,l=document.getElementById("notes").value.trim(),d=document.getElementById("gen-btn");d.disabled=!0,d.innerHTML='<span class="spinner"></span>Generating…',document.getElementById("error-area").innerHTML="",document.getElementById("recipe-area").innerHTML='<div class="status">Crafting your recipe with care…</div>';const L=[r.length?`Ingredients available: ${r.join(", ")}`:"",e?`Food goal: ${e}`:"",t.length?`Dietary restrictions: ${t.join(", ")}`:"",n?`Preferred cuisine: ${n}`:"",s?`Time constraint: ${s}`:"",i?`Skill level: ${i}`:"",`Servings: ${o}`,l?`Additional preferences: ${l}`:""].filter(Boolean).join(`
`),T=`You are an expert chef and culinary writer. Create a complete, precise recipe based on the user's inputs.
CRITICAL: Instructions must be specific — never vague. Include exact temperatures, times, visual cues, sounds, smells, and textures. 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."
Respond with ONLY a valid JSON object. Use this exact structure:
{"title":"string","description":"string","servings":2,"prepTime":"string","cookTime":"string","ingredients":[{"amount":"string","unit":"string","name":"string"}],"steps":[{"title":"string","instruction":"string"}],"notes":"string or null"}
Rules: use provided ingredients as the base, add pantry staples as needed, 510 steps, each instruction 13 richly detailed sentences, respect all dietary restrictions strictly.`;try{const a=await fetch("/api/generate",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({username:g,password:y,systemPrompt:T,userPrompt:L})}),w=await a.json();if(!a.ok)throw new Error(w.error||`Server error ${a.status}`);const k=w.candidates[0].content.parts[0].text,x=JSON.parse(k);f(x)}catch(a){document.getElementById("recipe-area").innerHTML="",E("Error: "+(a.message||"Unknown error. Please try again."))}d.disabled=!1,d.innerHTML="Generate my recipe"}function E(e){document.getElementById("error-area").innerHTML=`<div class="error-msg">${e}</div>`}let u=null,m=null;const h=document.getElementById("saved-menu"),p=document.getElementById("saved-list");document.getElementById("open-menu-btn").addEventListener("click",async()=>{h.classList.add("open"),await R()});document.getElementById("close-menu-btn").addEventListener("click",()=>{h.classList.remove("open")});async function R(){p.innerHTML='<p style="font-size: 13px; color: var(--color-text-secondary); text-align: center;">Loading...</p>';try{const e=await fetch("/api/recipes",{headers:{"x-username":g,"x-password":y}});if(!e.ok)throw new Error("Failed to fetch");const t=await e.json();if(t.length===0){p.innerHTML='<p style="font-size: 13px; color: var(--color-text-secondary); text-align: center;">No saved recipes yet.</p>';return}p.innerHTML=t.reverse().map(n=>`
<div class="saved-recipe-item" onclick="loadSavedRecipe('${n.id}')">
<h3>${n.title}</h3>
<p>${n.description}</p>
</div>
`).join("")}catch{p.innerHTML='<p style="font-size: 13px; color: var(--color-text-danger); text-align: center;">Error loading recipes.</p>'}}window.loadSavedRecipe=async function(e){h.classList.remove("open");try{const t=await fetch(`/api/recipes/${e}`);if(!t.ok)throw new Error("Failed to fetch recipe");const n=await t.json();n.id=e,f(n),document.getElementById("recipe-area").scrollIntoView({behavior:"smooth"})}catch(t){console.error(t)}};window.saveCurrentRecipe=async function(){if(!u)return;const e=document.getElementById("save-recipe-btn"),t=e.innerText;e.innerText="Saving...",e.disabled=!0;try{const n=await fetch("/api/recipes",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({username:g,password:y,recipe:u})});if(n.ok){const s=await n.json();m=s.id,u.id=s.id,e.innerText="Saved!"}else throw new Error("Save failed")}catch{e.innerText="Error Saving",setTimeout(()=>{e.innerText=t,e.disabled=!1},2e3)}};window.shareRecipeLink=function(){if(!m){alert("Please save the recipe first to share it!");return}const e=`${window.location.origin}${window.location.pathname}#shared=${m}`;navigator.clipboard.writeText(e).then(()=>{const t=document.getElementById("share-recipe-btn"),n=t.innerText;t.innerText="Link Copied!",setTimeout(()=>t.innerText=n,2e3)})};function f(e,t=!1){u=e,m=e.id||null;const n=(e.ingredients||[]).map(i=>`<li><span class="ing-amount">${i.amount}${i.unit?" "+i.unit:""}</span><span class="ing-name">${i.name}</span></li>`).join(""),s=(e.steps||[]).map((i,o)=>`<li class="step-item">
<span class="step-num">${o+1}</span>
<div>
<div class="step-title">${i.title}</div>
<div class="step-instr">${i.instruction}</div>
</div>
</li>`).join("");document.getElementById("recipe-area").innerHTML=`
<div class="recipe-out">
<div class="recipe-title">${e.title}</div>
<div class="recipe-desc">${e.description}</div>
<div class="meta-row">
<div class="meta-chip"><strong>${e.servings}</strong> servings</div>
<div class="meta-chip">Prep <strong>${e.prepTime}</strong></div>
<div class="meta-chip">Cook <strong>${e.cookTime}</strong></div>
</div>
<div class="section-label">Ingredients</div>
<ul class="ingredients-list">${n}</ul>
<div class="section-label">Method</div>
<ol class="steps-list">${s}</ol>
${e.notes?`<div class="notes-box">${e.notes}</div>`:""}
<div class="recipe-actions">
${t?`
<button class="action-btn" style="background: var(--color-text-primary); color: white;" onclick="window.location.href=window.location.pathname">Return to Login</button>
`:`
<button class="action-btn" id="save-recipe-btn" onclick="saveCurrentRecipe()">Save to Menu</button>
<button class="action-btn" id="share-recipe-btn" onclick="shareRecipeLink()">Copy Share Link</button>
`}
</div>
</div>`}window.addEventListener("DOMContentLoaded",async()=>{if(window.location.hash.startsWith("#shared="))try{const e=window.location.hash.slice(8);document.getElementById("login-overlay").innerText="Loading Shared Recipe...",document.getElementById("login-overlay").style.color="#1a1a1a";const t=await fetch(`/api/recipes/${e}`);if(!t.ok)throw new Error("Shared recipe not found");const n=await t.json();n.id=e,document.getElementById("login-overlay").style.display="none",document.querySelectorAll("#open-menu-btn, .header, .form-grid, #gen-btn, #error-area").forEach(s=>{s&&(s.style.display="none")}),document.getElementById("app-content").style.display="block",f(n,!0)}catch(e){console.error("Failed to load shared recipe",e),document.getElementById("login-overlay").innerHTML=`
<div class="login-card" style="text-align:center;">
<h2 style="font-family: 'Lora', serif; font-size: 20px;">Recipe Not Found</h2>
<p style="font-size: 14px; margin-top: 10px;">The shared recipe link appears to be invalid or has been deleted.</p>
<button class="generate-btn" style="margin-top: 16px;" onclick="window.location.href=window.location.pathname">Return to Login</button>
</div>
`}});

282
dist/index.html vendored Normal file
View File

@@ -0,0 +1,282 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Recipe Generator</title>
<style>
@import url('https://fonts.googleapis.com/css2?family=Lora:ital,wght@0,400;0,600;1,400&family=DM+Sans:wght@300;400;500&display=swap');
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--color-text-primary: #1a1a1a;
--color-text-secondary: #6b6b6b;
--color-background-primary: #ffffff;
--color-background-secondary: #f5f5f4;
--color-background-danger: #fef2f2;
--color-text-danger: #b91c1c;
--color-border-primary: #a3a3a3;
--color-border-secondary: #d4d4d4;
--color-border-tertiary: #e5e5e5;
--border-radius-md: 8px;
--border-radius-lg: 12px;
}
body {
font-family: 'DM Sans', sans-serif;
background: var(--color-background-primary);
color: var(--color-text-primary);
padding: 2rem;
display: flex;
justify-content: center;
}
.app { display: none; width: 100%; max-width: 680px; padding: 2rem 0; }
#login-overlay { position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; background: var(--color-background-secondary); display: flex; align-items: center; justify-content: center; z-index: 1000; }
.login-card { background: var(--color-background-primary); padding: 2rem; border: 0.5px solid var(--color-border-secondary); border-radius: var(--border-radius-lg); width: 100%; max-width: 320px; box-shadow: 0 4px 12px rgba(0,0,0,0.05); }
.login-card h2 { font-family: 'Lora', serif; font-size: 24px; margin-bottom: 1.25rem; text-align: center; }
.header { margin-bottom: 2rem; border-bottom: 0.5px solid var(--color-border-tertiary); padding-bottom: 1.25rem; }
.header h1 { font-family: 'Lora', serif; font-size: 26px; font-weight: 600; letter-spacing: -0.5px; margin-bottom: 4px; }
.header p { font-size: 14px; color: var(--color-text-secondary); font-weight: 300; }
.form-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; margin-bottom: 14px; }
.form-field { display: flex; flex-direction: column; gap: 6px; }
.form-field.full { grid-column: 1 / -1; }
label { font-size: 12px; font-weight: 500; color: var(--color-text-secondary); letter-spacing: 0.04em; text-transform: uppercase; }
input, textarea, select {
font-family: 'DM Sans', sans-serif; font-size: 14px; padding: 9px 12px;
border: 0.5px solid var(--color-border-secondary); border-radius: var(--border-radius-md);
background: var(--color-background-primary); color: var(--color-text-primary);
width: 100%; transition: border-color 0.15s; outline: none;
}
input:focus, textarea:focus, select:focus { border-color: var(--color-border-primary); }
textarea { resize: vertical; min-height: 72px; line-height: 1.5; }
.tags-input-wrapper {
border: 0.5px solid var(--color-border-secondary); border-radius: var(--border-radius-md);
background: var(--color-background-primary); padding: 6px 8px;
display: flex; flex-wrap: wrap; gap: 6px; align-items: center;
cursor: text; min-height: 40px; transition: border-color 0.15s;
}
.tags-input-wrapper:focus-within { border-color: var(--color-border-primary); }
.tag { background: var(--color-background-secondary); border: 0.5px solid var(--color-border-tertiary); border-radius: 4px; padding: 2px 8px; font-size: 13px; display: flex; align-items: center; gap: 5px; white-space: nowrap; }
.tag-x { cursor: pointer; color: var(--color-text-secondary); font-size: 15px; line-height: 1; }
.tag-x:hover { color: var(--color-text-primary); }
.tags-input { border: none !important; padding: 2px 4px !important; flex: 1; min-width: 100px; font-size: 14px; background: transparent !important; outline: none !important; }
.checkboxes { display: flex; flex-wrap: wrap; gap: 8px; }
.check-pill { display: flex; align-items: center; gap: 6px; padding: 5px 12px; border: 0.5px solid var(--color-border-secondary); border-radius: 20px; font-size: 13px; cursor: pointer; transition: all 0.12s; background: var(--color-background-primary); user-select: none; }
.check-pill input { display: none; }
.check-pill.checked { background: var(--color-background-secondary); border-color: var(--color-border-primary); font-weight: 500; }
.dot { width: 7px; height: 7px; border-radius: 50%; background: var(--color-border-secondary); flex-shrink: 0; }
.check-pill.checked .dot { background: var(--color-text-primary); }
.generate-btn { width: 100%; padding: 11px; background: var(--color-text-primary); color: var(--color-background-primary); border: none; border-radius: var(--border-radius-md); font-family: 'DM Sans', sans-serif; font-size: 15px; font-weight: 500; cursor: pointer; margin-top: 8px; transition: opacity 0.15s; }
.generate-btn:hover { opacity: 0.85; }
.generate-btn:disabled { opacity: 0.45; cursor: not-allowed; }
.status { text-align: center; padding: 2rem 0; color: var(--color-text-secondary); font-size: 14px; font-style: italic; }
.recipe-out { margin-top: 2.5rem; border-top: 0.5px solid var(--color-border-tertiary); padding-top: 2rem; animation: fadein 0.4s ease; }
@keyframes fadein { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: translateY(0); } }
.recipe-actions { display: flex; gap: 10px; margin-top: 1.5rem; }
.action-btn { flex: 1; padding: 10px; background: var(--color-background-primary); border: 0.5px solid var(--color-border-primary); border-radius: var(--border-radius-md); font-family: 'DM Sans', sans-serif; font-size: 14px; font-weight: 500; cursor: pointer; transition: all 0.15s; }
.action-btn:hover { background: var(--color-background-secondary); }
.saved-menu-btn { position: fixed; top: 20px; right: 20px; padding: 8px 16px; background: var(--color-text-primary); color: white; border: none; border-radius: 20px; font-size: 13px; font-weight: 500; cursor: pointer; z-index: 100; box-shadow: 0 2px 8px rgba(0,0,0,0.15); }
#saved-menu { position: fixed; top: 0; right: -350px; width: 320px; height: 100vh; background: var(--color-background-primary); box-shadow: -4px 0 15px rgba(0,0,0,0.05); transition: right 0.3s ease; z-index: 200; padding: 2rem 1.5rem; overflow-y: auto; border-left: 1px solid var(--color-border-tertiary); }
#saved-menu.open { right: 0; }
.saved-menu-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 1.5rem; }
.saved-menu-header h2 { font-family: 'Lora', serif; font-size: 20px; }
.close-menu { cursor: pointer; font-size: 24px; color: var(--color-text-secondary); line-height: 1; background: none; border: none; }
.saved-recipe-item { padding: 12px; border: 0.5px solid var(--color-border-secondary); border-radius: var(--border-radius-md); margin-bottom: 10px; cursor: pointer; transition: border-color 0.15s; }
.saved-recipe-item:hover { border-color: var(--color-text-primary); }
.saved-recipe-item h3 { font-size: 15px; margin-bottom: 4px; }
.saved-recipe-item p { font-size: 12px; color: var(--color-text-secondary); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.recipe-title { font-family: 'Lora', serif; font-size: 28px; font-weight: 600; letter-spacing: -0.5px; margin-bottom: 6px; }
.recipe-desc { font-size: 14px; color: var(--color-text-secondary); font-style: italic; margin-bottom: 1.25rem; line-height: 1.6; }
.meta-row { display: flex; gap: 12px; flex-wrap: wrap; margin-bottom: 1.75rem; }
.meta-chip { background: var(--color-background-secondary); border-radius: var(--border-radius-md); padding: 6px 14px; font-size: 13px; color: var(--color-text-secondary); }
.meta-chip strong { color: var(--color-text-primary); font-weight: 500; }
.section-label { font-size: 11px; font-weight: 500; letter-spacing: 0.08em; text-transform: uppercase; color: var(--color-text-secondary); margin-bottom: 10px; margin-top: 1.5rem; }
.ingredients-list { list-style: none; border: 0.5px solid var(--color-border-tertiary); border-radius: var(--border-radius-lg); overflow: hidden; }
.ingredients-list li { padding: 9px 16px; font-size: 14px; border-bottom: 0.5px solid var(--color-border-tertiary); display: flex; gap: 10px; align-items: baseline; }
.ingredients-list li:last-child { border-bottom: none; }
.ing-amount { font-weight: 500; min-width: 60px; }
.ing-name { color: var(--color-text-secondary); }
.steps-list { list-style: none; }
.step-item { display: flex; gap: 16px; margin-bottom: 1.25rem; }
.step-num { font-family: 'Lora', serif; font-size: 18px; font-weight: 600; color: var(--color-text-secondary); min-width: 28px; line-height: 1.5; }
.step-title { font-weight: 500; font-size: 14px; margin-bottom: 4px; }
.step-instr { font-size: 14px; line-height: 1.7; color: var(--color-text-secondary); }
.notes-box { background: var(--color-background-secondary); border-radius: 0 var(--border-radius-md) var(--border-radius-md) 0; padding: 12px 16px; font-size: 14px; line-height: 1.6; color: var(--color-text-secondary); margin-top: 1.5rem; border-left: 2px solid var(--color-border-primary); }
.error-msg { background: var(--color-background-danger); color: var(--color-text-danger); border-radius: var(--border-radius-md); padding: 12px 16px; font-size: 13px; margin-top: 1rem; line-height: 1.5; }
.spinner { display: inline-block; width: 16px; height: 16px; border: 2px solid rgba(255,255,255,0.35); border-top-color: #fff; border-radius: 50%; animation: spin 0.7s linear infinite; vertical-align: middle; margin-right: 6px; }
@keyframes spin { to { transform: rotate(360deg); } }
</style>
<script type="module" crossorigin src="/assets/index-CrgoFxjd.js"></script>
</head>
<body>
<div id="login-overlay">
<div class="login-card">
<h2 style="font-family: 'Lora', serif; font-size: 26px; font-weight: 600; letter-spacing: -0.5px; margin-bottom: 20px;">Welcome</h2>
<div class="form-field" style="margin-bottom: 12px;">
<label>Username</label>
<input type="text" id="login-username" />
</div>
<div class="form-field" style="margin-bottom: 12px;">
<label>Password</label>
<input type="password" id="login-password" />
</div>
<button class="generate-btn" id="login-btn" style="margin-top: 12px;">Sign in</button>
<div id="login-error" style="color: var(--color-text-danger); font-size: 13px; margin-top: 10px; text-align: center; display: none;">Invalid credentials</div>
</div>
</div>
<div class="app" id="app-content">
<button class="saved-menu-btn" id="open-menu-btn">View Saved Recipes</button>
<div id="saved-menu">
<div class="saved-menu-header">
<h2>Saved Recipes</h2>
<button class="close-menu" id="close-menu-btn">×</button>
</div>
<div id="saved-list">
<!-- Saved items go here -->
</div>
</div>
<div class="header">
<h1>Recipe Generator</h1>
<p>Tell the AI what you have and what you want — it handles the rest.</p>
</div>
<div class="form-grid">
<div class="form-field full">
<label>Ingredients on hand</label>
<div class="tags-input-wrapper" id="ing-wrapper">
<input class="tags-input" id="ing-input" placeholder="Type an ingredient, press Enter..." />
</div>
</div>
<div class="form-field full">
<label>What do you want to make?</label>
<input type="text" id="goal" placeholder="e.g. a hearty weeknight pasta, a light summer salad..." />
</div>
<div class="form-field full">
<label>Dietary restrictions</label>
<div class="checkboxes">
<label class="check-pill"><input type="checkbox" value="vegan"><span class="dot"></span>Vegan</label>
<label class="check-pill"><input type="checkbox" value="vegetarian"><span class="dot"></span>Vegetarian</label>
<label class="check-pill"><input type="checkbox" value="gluten-free"><span class="dot"></span>Gluten-free</label>
<label class="check-pill"><input type="checkbox" value="dairy-free"><span class="dot"></span>Dairy-free</label>
<label class="check-pill"><input type="checkbox" value="nut-free"><span class="dot"></span>Nut-free</label>
<label class="check-pill"><input type="checkbox" value="low-carb"><span class="dot"></span>Low-carb</label>
</div>
</div>
<div class="form-field">
<label>Cuisine style</label>
<select id="cuisine">
<option value="">Any cuisine</option>
<option>Italian</option><option>Mexican</option><option>Asian fusion</option>
<option>Mediterranean</option><option>French</option><option>Indian</option>
<option>American</option><option>Middle Eastern</option><option>Japanese</option><option>Thai</option>
</select>
</div>
<div class="form-field">
<label>Time available</label>
<select id="time">
<option value="">No limit</option>
<option value="under 20 minutes">Under 20 minutes</option>
<option value="under 30 minutes">Under 30 minutes</option>
<option value="under 45 minutes">Under 45 minutes</option>
<option value="under 1 hour">Under 1 hour</option>
<option value="12 hours">12 hours</option>
</select>
</div>
<div class="form-field">
<label>Skill level</label>
<select id="skill">
<option value="">Any level</option>
<option value="beginner-friendly">Beginner-friendly</option>
<option value="intermediate">Intermediate</option>
<option value="advanced">Advanced chef techniques</option>
</select>
</div>
<div class="form-field">
<label>Servings</label>
<select id="servings">
<option value="1">1</option><option value="2" selected>2</option>
<option value="4">4</option><option value="6">6</option><option value="8">8</option>
</select>
</div>
<div class="form-field full">
<label>Any other notes or preferences</label>
<textarea id="notes" placeholder="e.g. my kids hate mushrooms, I want it spicy, make it impressive enough for guests..."></textarea>
</div>
</div>
<button class="generate-btn" id="gen-btn">Generate my recipe</button>
<div id="error-area"></div>
<div id="recipe-area"></div>
</div>
</body>
</html>