Eco-Try-webapp / templates /tryon.html
EcoTry's picture
Upload 10 files
026c3c0 verified
{# File: templates/_ai_side_nav.html #}
<style>
.ai-nav-card {
border: 1px solid rgba(255,255,255,0.10);
border-radius: 18px;
background: rgba(255,255,255,0.04);
padding: 14px;
margin-top: 14px;
}
.ai-nav-title {
font-weight: 900;
letter-spacing: 0.5px;
font-size: 13px;
opacity: 0.9;
margin-bottom: 10px;
text-transform: uppercase;
}
.ai-nav-btn {
width: 100%;
text-align: left;
border: 1px solid rgba(255,255,255,0.10);
background: rgba(0,0,0,0.18);
color: inherit;
border-radius: 14px;
padding: 10px 12px;
cursor: pointer;
font-weight: 800;
display: flex;
align-items: center;
justify-content: space-between;
margin-top: 8px;
}
.ai-nav-btn.active {
border-color: rgba(123,220,59,0.35);
background: rgba(123,220,59,0.08);
}
.ai-nav-panel {
margin-top: 10px;
display: none;
gap: 10px;
flex-direction: column;
}
.ai-nav-panel.active { display: flex; }
.ai-muted { opacity: .75; font-size: 12px; }
.ai-error { color: #ff6b6b; font-size: 12px; margin-top: 8px; }
.ai-ok { color: rgba(123,220,59,0.95); font-size: 12px; margin-top: 8px; }
.ai-swatch { width: 14px; height: 14px; border-radius: 999px; border: 1px solid rgba(255,255,255,0.20); display:inline-block; }
.ai-chip {
border: 1px solid rgba(255,255,255,0.12);
background: rgba(0,0,0,0.18);
color: inherit;
border-radius: 999px;
padding: 7px 10px;
cursor: pointer;
font-weight: 800;
font-size: 12px;
}
.ai-chip.active {
border-color: rgba(123,220,59,0.35);
background: rgba(123,220,59,0.08);
}
.ai-mini-card {
border: 1px solid rgba(255,255,255,0.10);
border-radius: 16px;
padding: 10px;
background: rgba(0,0,0,0.12);
}
.ai-loading {
opacity: .85;
border: 1px dashed rgba(255,255,255,0.18);
background: rgba(0,0,0,0.10);
}
.ai-row {
display:flex;
gap:10px;
align-items:center;
}
</style>
<form method="POST" enctype="multipart/form-data">
<h3>{{ product.product_name }}</h3>
<!-- Cloth Image (dataset se) -->
<img src="{{ product.image_src }}" width="200">
<br><br>
<!-- Person Image Upload -->
<input type="file" name="person_image" id="person_image" required>
<br><br>
<button type="submit">Try On</button>
</form>
<!-- Result Image -->
{% if result_image_url %}
<h3>Result:</h3>
<img src="{{ result_image_url }}" width="300">
{% endif %}
{# Unique wrapper so this partial can be included on multiple pages safely #}
<div class="ai-nav-card" data-ai-stylist>
<div class="ai-nav-title">AI Stylist</div>
<button class="ai-nav-btn active" type="button" data-ai-tab="palette">
<span>🎨 Color Palette</span><span></span>
</button>
<button class="ai-nav-btn" type="button" data-ai-tab="style">
<span>🧥 Style Recs</span><span></span>
</button>
<div class="ai-error" data-ai-error></div>
<div class="ai-ok" data-ai-ok></div>
<div class="ai-nav-panel active" data-ai-panel="palette">
<div class="ai-mini-card">
<div style="font-weight:900; font-size:13px;">Skin tone</div>
<div class="ai-muted">
On Try-On page, it uses the same uploaded photo automatically (<code>#person_image</code>).
Otherwise upload here.
</div>
<input type="file" accept="image/*" style="margin-top:10px;" data-ai-selfie />
<div class="ai-muted" style="margin-top:10px;" data-ai-skin></div>
<div class="ai-muted" style="margin-top:6px;" data-ai-hint></div>
</div>
<div style="display:flex; flex-direction:column; gap:10px;" data-ai-palettes></div>
</div>
<div class="ai-nav-panel" data-ai-panel="style">
<div class="ai-mini-card">
<div style="font-weight:900; font-size:13px;">Vibe</div>
<div style="display:flex; gap:8px; flex-wrap:wrap; margin-top:10px;">
<button class="ai-chip active" data-ai-vibe="minimal" type="button">Minimal</button>
<button class="ai-chip" data-ai-vibe="streetwear" type="button">Streetwear</button>
<button class="ai-chip" data-ai-vibe="formal" type="button">Formal</button>
<button class="ai-chip" data-ai-vibe="smart-casual" type="button">Smart</button>
</div>
<button class="ai-nav-btn" type="button" style="margin-top:10px;" data-ai-style-run>
<span>Refresh</span><span></span>
</button>
<div class="ai-muted">Upload a photo above for better results.</div>
</div>
<div style="display:flex; flex-direction:column; gap:10px;" data-ai-style></div>
</div>
</div>
<script>
(() => {
const root = document.currentScript?.previousElementSibling;
if (!root || !root.matches("[data-ai-stylist]")) return;
const tabButtons = root.querySelectorAll("[data-ai-tab]");
const panels = root.querySelectorAll("[data-ai-panel]");
const selfieInput = root.querySelector("[data-ai-selfie]");
const errBox = root.querySelector("[data-ai-error]");
const okBox = root.querySelector("[data-ai-ok]");
const skinBox = root.querySelector("[data-ai-skin]");
const hintBox = root.querySelector("[data-ai-hint]");
const palettesBox = root.querySelector("[data-ai-palettes]");
const styleBox = root.querySelector("[data-ai-style]");
const runStyleBtn = root.querySelector("[data-ai-style-run]");
let vibe = "minimal";
let busy = false;
function setError(msg) {
if (errBox) errBox.textContent = msg || "";
if (okBox) okBox.textContent = "";
}
function setOk(msg) {
if (okBox) okBox.textContent = msg || "";
if (errBox) errBox.textContent = "";
}
function setTab(which) {
tabButtons.forEach((b) => b.classList.toggle("active", b.getAttribute("data-ai-tab") === which));
panels.forEach((p) => p.classList.toggle("active", p.getAttribute("data-ai-panel") === which));
}
tabButtons.forEach((b) => b.addEventListener("click", () => setTab(b.getAttribute("data-ai-tab"))));
root.querySelectorAll("[data-ai-vibe]").forEach((btn) => {
btn.addEventListener("click", () => {
root.querySelectorAll("[data-ai-vibe]").forEach((b) => b.classList.remove("active"));
btn.classList.add("active");
vibe = btn.getAttribute("data-ai-vibe") || "minimal";
setOk(`Vibe set to: ${vibe}`);
});
});
function getTryonFileIfExists() {
const tryonInput = document.getElementById("person_image");
if (tryonInput && tryonInput.files && tryonInput.files[0]) return tryonInput.files[0];
return null;
}
function getSelfieFile() {
const f1 = getTryonFileIfExists();
if (f1) return f1;
if (selfieInput && selfieInput.files && selfieInput.files[0]) return selfieInput.files[0];
return null;
}
function setLoading(targetBox, on) {
if (!targetBox) return;
targetBox.classList.toggle("ai-loading", !!on);
}
function renderEmptyHint() {
if (!hintBox) return;
const hasTryon = !!document.getElementById("person_image");
const hasFile = !!getSelfieFile();
if (hasFile) {
hintBox.textContent = "";
return;
}
hintBox.textContent = hasTryon
? "Choose a photo in the Try-On upload to auto-detect."
: "Upload a selfie here to get palette + recommendations.";
}
async function fetchPalette() {
if (busy) return;
busy = true;
setError("");
setOk("");
if (skinBox) skinBox.textContent = "";
if (palettesBox) palettesBox.innerHTML = "";
renderEmptyHint();
const f = getSelfieFile();
if (!f) { busy = false; return; }
setLoading(palettesBox, true);
setOk("Analyzing skin tone…");
try {
const fd = new FormData();
fd.append("selfie", f);
const res = await fetch("/api/ai/palette", { method: "POST", body: fd });
const data = await res.json();
if (!data.ok) { setError(data.error || "Palette failed."); return; }
const rgb = data.skin.rgb;
if (skinBox) {
skinBox.innerHTML =
`Detected skin: <span class="ai-swatch" style="background: rgb(${rgb[0]},${rgb[1]},${rgb[2]});"></span>
<span class="ai-muted"> rgb(${rgb.join(",")})</span>`;
}
(data.palettes || []).forEach((p) => {
const card = document.createElement("div");
card.className = "ai-mini-card";
card.innerHTML = `
<div style="font-weight:900; font-size:13px;">${p.name}</div>
<div class="ai-muted">${p.note}</div>
<div style="display:flex; gap:6px; flex-wrap:wrap; margin-top:10px;">
${(p.swatches || []).map(hex => `<span class="ai-swatch" title="${hex}" style="background:${hex};"></span>`).join("")}
</div>
`;
palettesBox.appendChild(card);
});
setOk("Palette ready.");
} catch (e) {
setError("Palette request failed.");
} finally {
setLoading(palettesBox, false);
busy = false;
}
}
async function fetchStyle() {
if (busy) return;
busy = true;
setError("");
setOk("");
if (styleBox) styleBox.innerHTML = "";
renderEmptyHint();
setLoading(styleBox, true);
setOk("Finding best matches…");
try {
const fd = new FormData();
fd.append("vibe", vibe);
const f = getSelfieFile();
if (f) fd.append("selfie", f);
const res = await fetch("/api/ai/style", { method: "POST", body: fd });
const data = await res.json();
if (!data.ok) { setError(data.error || "Style failed."); return; }
(data.recs || []).forEach((r) => {
const p = r.product;
const div = document.createElement("div");
div.className = "ai-mini-card";
div.innerHTML = `
<div class="ai-row">
<img src="${p.image_src}" style="width:52px; height:52px; border-radius:14px; object-fit:cover; border:1px solid rgba(255,255,255,.12);" />
<div style="min-width:0; flex:1;">
<div style="font-weight:900; font-size:13px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis;">${p.product_name}</div>
<div class="ai-muted">Score: ${Number(r.score || 0).toFixed(3)} · ${p.category}</div>
<a href="/tryon/${p.product_id}" class="ai-muted" style="display:inline-block; margin-top:4px;">Try →</a>
</div>
</div>
`;
styleBox.appendChild(div);
});
setOk("Recommendations ready.");
} catch (e) {
setError("Style request failed.");
} finally {
setLoading(styleBox, false);
busy = false;
}
}
if (selfieInput) {
selfieInput.addEventListener("change", async () => {
await fetchPalette();
await fetchStyle();
});
}
const tryonInput = document.getElementById("person_image");
if (tryonInput) {
tryonInput.addEventListener("change", async () => {
await fetchPalette();
await fetchStyle();
});
}
if (runStyleBtn) runStyleBtn.addEventListener("click", fetchStyle);
renderEmptyHint();
setTimeout(() => { fetchPalette(); fetchStyle(); }, 300);
})();
</script>