Spaces:
Sleeping
Sleeping
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8" /> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0" /> | |
| <title>ShopBot — Product Search</title> | |
| <link rel="preconnect" href="https://fonts.googleapis.com" /> | |
| <link href="https://fonts.googleapis.com/css2?family=DM+Serif+Display:ital@0;1&family=DM+Sans:ital,opsz,wght@0,9..40,300;0,9..40,400;0,9..40,500;1,9..40,300&display=swap" rel="stylesheet" /> | |
| <style> | |
| *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } | |
| :root { | |
| --sage: #3d6b4f; | |
| --sage-light:#e8f0eb; | |
| --sage-mid: #a8c4b0; | |
| --cream: #faf8f4; | |
| --ink: #1a1a18; | |
| --ink-muted: #6b6b66; | |
| --ink-faint: #a8a8a4; | |
| --surface: #ffffff; | |
| --border: rgba(0,0,0,0.08); | |
| --accent: #c17d3c; | |
| --accent-bg: #fdf3e7; | |
| --radius-sm: 8px; | |
| --radius-md: 14px; | |
| --radius-lg: 22px; | |
| } | |
| body { | |
| font-family: 'DM Sans', sans-serif; | |
| background: var(--cream); | |
| color: var(--ink); | |
| min-height: 100vh; | |
| display: flex; | |
| flex-direction: column; | |
| } | |
| /* ── HEADER ── */ | |
| header { | |
| padding: 1.25rem 2rem; | |
| background: var(--surface); | |
| border-bottom: 1px solid var(--border); | |
| display: flex; | |
| align-items: center; | |
| gap: 14px; | |
| position: sticky; | |
| top: 0; | |
| z-index: 100; | |
| } | |
| .logo-mark { | |
| width: 38px; height: 38px; | |
| background: var(--sage); | |
| border-radius: 10px; | |
| display: flex; align-items: center; justify-content: center; | |
| flex-shrink: 0; | |
| } | |
| .logo-mark svg { width: 20px; height: 20px; fill: none; stroke: #fff; stroke-width: 2; stroke-linecap: round; } | |
| .header-text h1 { font-family: 'DM Serif Display', serif; font-size: 1.15rem; font-weight: 400; color: var(--ink); letter-spacing: -0.01em; } | |
| .header-text p { font-size: 0.75rem; color: var(--ink-muted); margin-top: 1px; } | |
| .status-dot { margin-left: auto; display: flex; align-items: center; gap: 6px; font-size: 0.72rem; color: var(--ink-muted); } | |
| .dot { width: 7px; height: 7px; border-radius: 50%; background: #4caf7d; animation: pulse 2s infinite; } | |
| @keyframes pulse { 0%,100%{opacity:1} 50%{opacity:.4} } | |
| /* ── MAIN LAYOUT ── */ | |
| main { | |
| flex: 1; | |
| display: flex; | |
| flex-direction: column; | |
| max-width: 860px; | |
| width: 100%; | |
| margin: 0 auto; | |
| padding: 0 1rem; | |
| } | |
| /* ── CHAT AREA ── */ | |
| #chat { | |
| flex: 1; | |
| overflow-y: auto; | |
| padding: 2rem 0 1rem; | |
| display: flex; | |
| flex-direction: column; | |
| gap: 1.5rem; | |
| min-height: 0; | |
| } | |
| /* ── WELCOME ── */ | |
| .welcome { | |
| text-align: center; | |
| padding: 3rem 1rem 2rem; | |
| } | |
| .welcome-icon { | |
| width: 64px; height: 64px; | |
| background: var(--sage-light); | |
| border-radius: 18px; | |
| display: flex; align-items: center; justify-content: center; | |
| margin: 0 auto 1.25rem; | |
| } | |
| .welcome-icon svg { width: 32px; height: 32px; stroke: var(--sage); fill: none; stroke-width: 1.8; stroke-linecap: round; } | |
| .welcome h2 { font-family: 'DM Serif Display', serif; font-size: 1.7rem; font-weight: 400; color: var(--ink); margin-bottom: 0.5rem; } | |
| .welcome p { font-size: 0.88rem; color: var(--ink-muted); max-width: 360px; margin: 0 auto 1.75rem; line-height: 1.6; } | |
| .chips { display: flex; flex-wrap: wrap; justify-content: center; gap: 8px; } | |
| .chip { | |
| padding: 7px 14px; | |
| border-radius: 999px; | |
| border: 1px solid var(--border); | |
| background: var(--surface); | |
| font-size: 0.78rem; | |
| color: var(--ink-muted); | |
| cursor: pointer; | |
| transition: all .15s; | |
| } | |
| .chip:hover { border-color: var(--sage-mid); color: var(--sage); background: var(--sage-light); } | |
| /* ── MESSAGE BUBBLES ── */ | |
| .msg { display: flex; gap: 10px; } | |
| .msg.user { flex-direction: row-reverse; } | |
| .msg.bot { flex-direction: row; } | |
| .avatar { | |
| width: 30px; height: 30px; border-radius: 50%; | |
| flex-shrink: 0; display: flex; align-items: center; justify-content: center; | |
| font-size: 0.7rem; font-weight: 500; margin-top: 2px; | |
| } | |
| .avatar.bot { background: var(--sage); color: #fff; } | |
| .avatar.user { background: var(--ink); color: #fff; font-size: 0.65rem; } | |
| .bubble { | |
| max-width: 75%; | |
| padding: 0.85rem 1.1rem; | |
| border-radius: var(--radius-lg); | |
| font-size: 0.875rem; | |
| line-height: 1.65; | |
| } | |
| .msg.user .bubble { | |
| background: var(--ink); | |
| color: #fff; | |
| border-bottom-right-radius: var(--radius-sm); | |
| } | |
| .msg.bot .bubble { | |
| background: var(--surface); | |
| border: 1px solid var(--border); | |
| border-bottom-left-radius: var(--radius-sm); | |
| color: var(--ink); | |
| } | |
| /* ── PRODUCT GRID ── */ | |
| .products-grid { | |
| display: grid; | |
| grid-template-columns: repeat(auto-fill, minmax(190px, 1fr)); | |
| gap: 10px; | |
| margin-top: 12px; | |
| } | |
| .product-card { | |
| background: var(--cream); | |
| border: 1px solid var(--border); | |
| border-radius: var(--radius-md); | |
| padding: 12px; | |
| cursor: pointer; | |
| transition: all .2s; | |
| position: relative; | |
| } | |
| .product-card:hover { border-color: var(--sage-mid); transform: translateY(-1px); } | |
| .product-card .badge { | |
| position: absolute; top: 10px; right: 10px; | |
| background: var(--accent-bg); | |
| color: var(--accent); | |
| font-size: 0.65rem; | |
| font-weight: 500; | |
| padding: 2px 7px; | |
| border-radius: 999px; | |
| } | |
| .product-img { | |
| width: 100%; height: 100px; | |
| background: var(--sage-light); | |
| border-radius: var(--radius-sm); | |
| display: flex; align-items: center; justify-content: center; | |
| margin-bottom: 10px; | |
| overflow: hidden; | |
| } | |
| .product-img svg { width: 36px; height: 36px; stroke: var(--sage); fill: none; stroke-width: 1.4; } | |
| .product-img img { width: 100%; height: 100%; object-fit: cover; } | |
| .product-name { font-size: 0.8rem; font-weight: 500; color: var(--ink); margin-bottom: 4px; line-height: 1.3; } | |
| .product-meta { font-size: 0.7rem; color: var(--ink-muted); display: flex; gap: 6px; flex-wrap: wrap; margin-bottom: 6px; } | |
| .product-price { font-size: 0.95rem; font-weight: 500; color: var(--sage); } | |
| .product-rating { font-size: 0.7rem; color: var(--accent); } | |
| /* ── SQL DEBUG ── */ | |
| .sql-block { | |
| margin-top: 10px; | |
| background: #f4f4f0; | |
| border-radius: var(--radius-sm); | |
| padding: 8px 12px; | |
| font-family: 'Courier New', monospace; | |
| font-size: 0.7rem; | |
| color: var(--ink-muted); | |
| white-space: pre-wrap; | |
| word-break: break-all; | |
| border-left: 3px solid var(--sage-mid); | |
| cursor: pointer; | |
| } | |
| .sql-label { font-size: 0.65rem; color: var(--ink-faint); margin-bottom: 4px; text-transform: uppercase; letter-spacing: 0.05em; } | |
| .sql-toggle { font-size: 0.72rem; color: var(--ink-muted); cursor: pointer; display: inline-flex; align-items: center; gap: 4px; margin-top: 8px; } | |
| .sql-toggle:hover { color: var(--sage); } | |
| /* ── TYPING INDICATOR ── */ | |
| .typing { display: flex; gap: 5px; align-items: center; padding: 12px 16px; } | |
| .typing span { | |
| width: 6px; height: 6px; border-radius: 50%; | |
| background: var(--sage-mid); display: inline-block; | |
| animation: bounce 1.2s infinite; | |
| } | |
| .typing span:nth-child(2) { animation-delay: .2s; } | |
| .typing span:nth-child(3) { animation-delay: .4s; } | |
| @keyframes bounce { 0%,100%{transform:translateY(0)} 50%{transform:translateY(-5px)} } | |
| /* ── INPUT BAR ── */ | |
| .input-bar { | |
| padding: 1rem 0 1.5rem; | |
| position: sticky; | |
| bottom: 0; | |
| background: linear-gradient(to top, var(--cream) 80%, transparent); | |
| } | |
| .input-wrap { | |
| display: flex; | |
| align-items: flex-end; | |
| gap: 10px; | |
| background: var(--surface); | |
| border: 1px solid var(--border); | |
| border-radius: var(--radius-lg); | |
| padding: 10px 10px 10px 16px; | |
| box-shadow: 0 2px 16px rgba(0,0,0,0.06); | |
| } | |
| #user-input { | |
| flex: 1; | |
| border: none; | |
| outline: none; | |
| resize: none; | |
| font-family: 'DM Sans', sans-serif; | |
| font-size: 0.875rem; | |
| color: var(--ink); | |
| background: transparent; | |
| line-height: 1.5; | |
| max-height: 120px; | |
| min-height: 22px; | |
| } | |
| #user-input::placeholder { color: var(--ink-faint); } | |
| #send-btn { | |
| width: 36px; height: 36px; | |
| border-radius: 50%; | |
| border: none; | |
| background: var(--sage); | |
| color: #fff; | |
| cursor: pointer; | |
| display: flex; align-items: center; justify-content: center; | |
| transition: all .15s; | |
| flex-shrink: 0; | |
| } | |
| #send-btn:hover { background: #2e5039; transform: scale(1.05); } | |
| #send-btn:disabled { background: var(--ink-faint); cursor: not-allowed; transform: none; } | |
| #send-btn svg { width: 16px; height: 16px; stroke: #fff; fill: none; stroke-width: 2.2; stroke-linecap: round; } | |
| .hint { text-align: center; font-size: 0.68rem; color: var(--ink-faint); margin-top: 8px; } | |
| /* ── CATEGORY ICONS ── */ | |
| .cat-icon { width: 36px; height: 36px; stroke: var(--sage); fill: none; stroke-width: 1.4; } | |
| </style> | |
| </head> | |
| <body> | |
| <header> | |
| <div class="logo-mark"> | |
| <svg viewBox="0 0 24 24"><path d="M6 2l.01 6L10 12l-3.99 4.01L6 22h12v-6l-4-4 4-3.99V2H6z"/></svg> | |
| </div> | |
| <div class="header-text"> | |
| <h1>ShopBot</h1> | |
| <p>AI-powered product search</p> | |
| </div> | |
| <div class="status-dot"><span class="dot"></span> Live</div> | |
| </header> | |
| <main> | |
| <div id="chat"> | |
| <div class="welcome" id="welcome"> | |
| <div class="welcome-icon"> | |
| <svg viewBox="0 0 24 24"><circle cx="11" cy="11" r="8"/><path d="M21 21l-4.35-4.35"/></svg> | |
| </div> | |
| <h2>What are you looking for?</h2> | |
| <p>Describe what you need in plain language — size, color, price, category — and I'll find it.</p> | |
| <div class="chips"> | |
| <span class="chip" onclick="setInput('Blue dress under ₹500')">Blue dress under ₹500</span> | |
| <span class="chip" onclick="setInput('XXL white shirt for men')">XXL white shirt for men</span> | |
| <span class="chip" onclick="setInput('Nike running shoes size 9')">Nike shoes size 9</span> | |
| <span class="chip" onclick="setInput('Women ethnic wear below 700')">Women ethnic below ₹700</span> | |
| <span class="chip" onclick="setInput('Best rated jeans')">Best rated jeans</span> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="input-bar"> | |
| <div class="input-wrap"> | |
| <textarea id="user-input" rows="1" placeholder="Try: 'I need a red XXL shirt within ₹500'…" maxlength="400"></textarea> | |
| <button id="send-btn" onclick="sendMessage()" title="Search"> | |
| <svg viewBox="0 0 24 24"><line x1="22" y1="2" x2="11" y2="13"/><polygon points="22 2 15 22 11 13 2 9 22 2"/></svg> | |
| </button> | |
| </div> | |
| <p class="hint">Press Enter to send · Shift+Enter for new line</p> | |
| </div> | |
| </main> | |
| <script> | |
| const API_BASE = ""; // same origin; change to "http://localhost:8000" for dev | |
| let history = []; | |
| let isLoading = false; | |
| const chatEl = document.getElementById("chat"); | |
| const inputEl = document.getElementById("user-input"); | |
| const sendBtn = document.getElementById("send-btn"); | |
| const welcomeEl= document.getElementById("welcome"); | |
| function setInput(text) { | |
| inputEl.value = text; | |
| inputEl.focus(); | |
| autoResize(); | |
| } | |
| inputEl.addEventListener("keydown", e => { | |
| if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); sendMessage(); } | |
| }); | |
| inputEl.addEventListener("input", autoResize); | |
| function autoResize() { | |
| inputEl.style.height = "auto"; | |
| inputEl.style.height = Math.min(inputEl.scrollHeight, 120) + "px"; | |
| } | |
| function sendMessage() { | |
| const text = inputEl.value.trim(); | |
| if (!text || isLoading) return; | |
| if (welcomeEl) { welcomeEl.style.display = "none"; } | |
| appendBubble("user", text); | |
| inputEl.value = ""; | |
| autoResize(); | |
| const typingId = appendTyping(); | |
| setLoading(true); | |
| fetch(`${API_BASE}/search`, { | |
| method: "POST", | |
| headers: { "Content-Type": "application/json" }, | |
| body: JSON.stringify({ message: text, conversation_history: history }), | |
| }) | |
| .then(r => r.json()) | |
| .then(data => { | |
| removeTyping(typingId); | |
| setLoading(false); | |
| if (data.detail) { | |
| appendBubble("bot", `⚠️ ${data.detail}`); | |
| return; | |
| } | |
| history.push({ role: "user", content: text }); | |
| history.push({ role: "assistant", content: data.reply }); | |
| if (history.length > 20) history = history.slice(-20); | |
| appendBotResponse(data); | |
| }) | |
| .catch(err => { | |
| removeTyping(typingId); | |
| setLoading(false); | |
| appendBubble("bot", "Connection error. Is the server running? (" + err.message + ")"); | |
| }); | |
| } | |
| function setLoading(v) { | |
| isLoading = v; | |
| sendBtn.disabled = v; | |
| } | |
| function appendBubble(role, text) { | |
| const wrap = document.createElement("div"); | |
| wrap.className = "msg " + role; | |
| const av = document.createElement("div"); | |
| av.className = "avatar " + role; | |
| av.textContent = role === "bot" ? "S" : "You"; | |
| const bub = document.createElement("div"); | |
| bub.className = "bubble"; | |
| bub.textContent = text; | |
| wrap.appendChild(av); | |
| wrap.appendChild(bub); | |
| chatEl.appendChild(wrap); | |
| scrollToBottom(); | |
| return wrap; | |
| } | |
| function appendBotResponse(data) { | |
| const wrap = document.createElement("div"); | |
| wrap.className = "msg bot"; | |
| const av = document.createElement("div"); | |
| av.className = "avatar bot"; | |
| av.textContent = "S"; | |
| const bub = document.createElement("div"); | |
| bub.className = "bubble"; | |
| const replyText = document.createElement("div"); | |
| replyText.style.marginBottom = data.products && data.products.length ? "12px" : "0"; | |
| replyText.textContent = data.reply; | |
| bub.appendChild(replyText); | |
| // Product grid | |
| if (data.products && data.products.length > 0) { | |
| const grid = document.createElement("div"); | |
| grid.className = "products-grid"; | |
| data.products.forEach(p => grid.appendChild(makeCard(p))); | |
| bub.appendChild(grid); | |
| } | |
| // SQL debug toggle | |
| if (data.generated_sql) { | |
| const toggle = document.createElement("span"); | |
| toggle.className = "sql-toggle"; | |
| toggle.innerHTML = `<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/></svg> View generated SQL`; | |
| const sqlBlock = document.createElement("div"); | |
| sqlBlock.className = "sql-block"; | |
| sqlBlock.style.display = "none"; | |
| const label = document.createElement("div"); | |
| label.className = "sql-label"; | |
| label.textContent = "Generated SQL · " + (data.row_count || 0) + " results"; | |
| const code = document.createElement("code"); | |
| code.textContent = data.generated_sql; | |
| sqlBlock.appendChild(label); | |
| sqlBlock.appendChild(code); | |
| let open = false; | |
| toggle.onclick = () => { | |
| open = !open; | |
| sqlBlock.style.display = open ? "block" : "none"; | |
| toggle.innerHTML = (open | |
| ? `<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="18 15 12 9 6 15"/></svg> Hide SQL` | |
| : `<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/></svg> View generated SQL`); | |
| }; | |
| bub.appendChild(toggle); | |
| bub.appendChild(sqlBlock); | |
| } | |
| wrap.appendChild(av); | |
| wrap.appendChild(bub); | |
| chatEl.appendChild(wrap); | |
| scrollToBottom(); | |
| } | |
| function makeCard(p) { | |
| const card = document.createElement("div"); | |
| card.className = "product-card"; | |
| card.title = p.description || p.name; | |
| const discountPct = parseInt(p.discount_pct) || 0; | |
| if (discountPct > 0) { | |
| const badge = document.createElement("div"); | |
| badge.className = "badge"; | |
| badge.textContent = `-${discountPct}%`; | |
| card.appendChild(badge); | |
| } | |
| const img = document.createElement("div"); | |
| img.className = "product-img"; | |
| if (p.image_url) { | |
| const i = document.createElement("img"); | |
| i.src = 'http://localhost/api_ct/productimage/'+p.image_url; | |
| i.alt = p.name; | |
| i.onerror = () => { img.innerHTML = categoryIcon(p.category); }; | |
| img.appendChild(i); | |
| } else { | |
| img.innerHTML = categoryIcon(p.category); | |
| } | |
| card.appendChild(img); | |
| const name = document.createElement("div"); | |
| name.className = "product-name"; | |
| name.textContent = p.name; | |
| card.appendChild(name); | |
| const meta = document.createElement("div"); | |
| meta.className = "product-meta"; | |
| if (p.color) meta.innerHTML += `<span>${p.color}</span>`; | |
| if (p.size) meta.innerHTML += `<span>· ${p.size}</span>`; | |
| if (p.brand) meta.innerHTML += `<span>· ${p.brand}</span>`; | |
| card.appendChild(meta); | |
| const bottom = document.createElement("div"); | |
| bottom.style.display = "flex"; bottom.style.justifyContent = "space-between"; bottom.style.alignItems = "center"; | |
| const price = document.createElement("div"); | |
| price.className = "product-price"; | |
| const finalPrice = discountPct > 0 ? (parseFloat(p.price) * (1 - discountPct/100)).toFixed(0) : parseFloat(p.price).toFixed(0); | |
| price.textContent = `₹${finalPrice}`; | |
| if (discountPct > 0) { | |
| const original = document.createElement("span"); | |
| original.style.cssText = "font-size:0.65rem;color:#a8a8a4;text-decoration:line-through;margin-left:4px;"; | |
| original.textContent = `₹${parseFloat(p.price).toFixed(0)}`; | |
| price.appendChild(original); | |
| } | |
| const rating = document.createElement("div"); | |
| rating.className = "product-rating"; | |
| if (p.rating) rating.textContent = `★ ${parseFloat(p.rating).toFixed(1)}`; | |
| bottom.appendChild(price); | |
| bottom.appendChild(rating); | |
| card.appendChild(bottom); | |
| return card; | |
| } | |
| function categoryIcon(cat) { | |
| const c = (cat || "").toLowerCase(); | |
| if (c.includes("dress") || c.includes("kurti")) | |
| return `<svg class="cat-icon" viewBox="0 0 24 24"><path d="M12 2l-4 4H5l2 4v12h10V10l2-4h-3L12 2z"/></svg>`; | |
| if (c.includes("shirt") || c.includes("t-shirt") || c.includes("tee")) | |
| return `<svg class="cat-icon" viewBox="0 0 24 24"><path d="M2 7l4-4 4 4v13H2V7zm20 0l-4-4-4 4v13h8V7z"/><line x1="6" y1="7" x2="18" y2="7"/></svg>`; | |
| if (c.includes("jean") || c.includes("pant")) | |
| return `<svg class="cat-icon" viewBox="0 0 24 24"><path d="M4 2h16v4l-2 16h-4l-2-8-2 8H6L4 6V2z"/></svg>`; | |
| if (c.includes("shoe") || c.includes("sandal") || c.includes("sneaker")) | |
| return `<svg class="cat-icon" viewBox="0 0 24 24"><path d="M3 16l2-8h6l2 4h7a1 1 0 010 2l-1 2H3z"/><line x1="7" y1="8" x2="7" y2="16"/></svg>`; | |
| return `<svg class="cat-icon" viewBox="0 0 24 24"><rect x="3" y="3" width="18" height="18" rx="2"/><path d="M9 3v18M15 3v18"/></svg>`; | |
| } | |
| function appendTyping() { | |
| const id = "typing-" + Date.now(); | |
| const wrap = document.createElement("div"); | |
| wrap.className = "msg bot"; wrap.id = id; | |
| const av = document.createElement("div"); | |
| av.className = "avatar bot"; av.textContent = "S"; | |
| const bub = document.createElement("div"); | |
| bub.className = "bubble"; | |
| bub.innerHTML = `<div class="typing"><span></span><span></span><span></span></div>`; | |
| wrap.appendChild(av); wrap.appendChild(bub); | |
| chatEl.appendChild(wrap); | |
| scrollToBottom(); | |
| return id; | |
| } | |
| function removeTyping(id) { | |
| const el = document.getElementById(id); | |
| if (el) el.remove(); | |
| } | |
| function scrollToBottom() { | |
| chatEl.scrollTop = chatEl.scrollHeight; | |
| } | |
| </script> | |
| </body> | |
| </html> |