ChatRAG / index.html
Binayak Panigrahi
Add application file
f0f26c7
<!DOCTYPE html>
<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>