BiteWiseFinal / index.html
anaygupta's picture
Update index.html
8753029 verified
<!DOCTYPE html>
<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("&", "&amp;")
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;")
.replaceAll('"', "&quot;")
.replaceAll("'", "&#39;");
}
</script>
</body>
</html>