Spaces:
Sleeping
Sleeping
Download frontend/script.js from ShivaliGupta/ShopPilot_AI: direct link, hf CLI and curl.
- Browser
- Download file 9.82 kB
-
https://huggingface.co/spaces/ShivaliGupta/ShopPilot_AI/resolve/main/frontend/script.js
- Command line
-
hf download hf://spaces/ShivaliGupta/ShopPilot_AI/frontend/script.js
-
curl -L -o script.js https://huggingface.co/spaces/ShivaliGupta/ShopPilot_AI/resolve/main/frontend/script.js
9.82 kB
| const form = document.getElementById("search-form"); | |
| const queryInput = document.getElementById("query-input"); | |
| const budgetInput = document.getElementById("budget-input"); | |
| const searchBtn = document.getElementById("search-btn"); | |
| const pipelineSection = document.getElementById("pipeline"); | |
| // Matches the new pipeline-grid: each agent has a status <p> with these ids. | |
| const stageEls = [ | |
| document.getElementById("search-status"), | |
| document.getElementById("price-status"), | |
| document.getElementById("review-status"), | |
| document.getElementById("spec-status"), | |
| document.getElementById("rank-status"), | |
| ]; | |
| const stageDefaults = [ | |
| "Scanning listings...", | |
| "Computing value...", | |
| "Reading reviews...", | |
| "Extracting specifications...", | |
| "Preparing verdict...", | |
| ]; | |
| const stageDone = [ | |
| "Listings scanned ✓", | |
| "Value computed ✓", | |
| "Reviews synthesized ✓", | |
| "Specs extracted ✓", | |
| "Verdict filed ✓", | |
| ]; | |
| // The little status dot next to each agent name (span.dot inside .agent-top) | |
| const stageDots = document.querySelectorAll("#pipeline .agent .dot"); | |
| const resultsSection = document.getElementById("results"); | |
| const cardsWrap = document.getElementById("cards"); | |
| const banner = document.getElementById("banner"); | |
| const compareWrap = document.getElementById("compare-wrap"); | |
| const compareTable = document.getElementById("compare-table"); | |
| const toggleCompareBtn = document.getElementById("toggle-compare"); | |
| const emptyState = document.getElementById("empty-state"); | |
| const errorState = document.getElementById("error-state"); | |
| const errorText = document.getElementById("error-text"); | |
| // Optional elements -- safe to be absent, we just skip them if so. | |
| const resultsTitle = document.getElementById("results-title"); | |
| const dataSourceBadge = document.getElementById("data-source-badge"); | |
| let pipelineTimer = null; | |
| function setText(el, text) { | |
| if (el) el.textContent = text; | |
| } | |
| document.querySelectorAll(".chip").forEach((chip) => { | |
| chip.addEventListener("click", () => { | |
| queryInput.value = chip.dataset.q; | |
| form.requestSubmit(); | |
| }); | |
| }); | |
| if (toggleCompareBtn) { | |
| toggleCompareBtn.addEventListener("click", () => { | |
| compareWrap.classList.toggle("hidden"); | |
| setText( | |
| toggleCompareBtn, | |
| compareWrap.classList.contains("hidden") ? "Compare Products" : "Hide Comparison" | |
| ); | |
| }); | |
| } | |
| form.addEventListener("submit", async (e) => { | |
| e.preventDefault(); | |
| const query = queryInput.value.trim(); | |
| if (!query) return; | |
| const budget = budgetInput.value ? Number(budgetInput.value) : null; | |
| hide(emptyState); | |
| hide(errorState); | |
| hide(resultsSection); | |
| show(pipelineSection); | |
| animatePipeline(); | |
| searchBtn.disabled = true; | |
| const originalBtnHTML = searchBtn.innerHTML; | |
| searchBtn.innerHTML = '<i class="fa-solid fa-rocket"></i> Agents working…'; | |
| try { | |
| const res = await fetch("/api/recommend", { | |
| method: "POST", | |
| headers: { "Content-Type": "application/json" }, | |
| body: JSON.stringify({ query, budget_max: budget }), | |
| }); | |
| const data = await res.json(); | |
| if (!res.ok) { | |
| throw new Error(data.detail || "Something went wrong on the server."); | |
| } | |
| finishPipeline(); | |
| setTimeout(() => renderResults(query, data), 450); | |
| } catch (err) { | |
| finishPipeline(); | |
| hide(pipelineSection); | |
| showError(err.message); | |
| } finally { | |
| searchBtn.disabled = false; | |
| searchBtn.innerHTML = originalBtnHTML; | |
| } | |
| }); | |
| function animatePipeline() { | |
| clearInterval(pipelineTimer); | |
| stageEls.forEach((el, i) => setText(el, stageDefaults[i])); | |
| stageDots.forEach((dot) => { | |
| dot.style.background = ""; | |
| dot.style.boxShadow = ""; | |
| }); | |
| let i = 0; | |
| const activate = (idx) => { | |
| const dot = stageDots[idx]; | |
| if (dot) { | |
| dot.style.background = "#d98e2b"; | |
| dot.style.boxShadow = "0 0 0 4px rgba(217,142,43,0.18)"; | |
| } | |
| }; | |
| const complete = (idx) => { | |
| const dot = stageDots[idx]; | |
| if (dot) { | |
| dot.style.background = "#2f6b4f"; | |
| dot.style.boxShadow = "none"; | |
| } | |
| setText(stageEls[idx], stageDone[idx]); | |
| }; | |
| activate(0); | |
| pipelineTimer = setInterval(() => { | |
| complete(i); | |
| i++; | |
| if (i < stageEls.length) { | |
| activate(i); | |
| } else { | |
| clearInterval(pipelineTimer); | |
| } | |
| }, 900); | |
| } | |
| function finishPipeline() { | |
| clearInterval(pipelineTimer); | |
| stageEls.forEach((el, i) => setText(el, stageDone[i])); | |
| stageDots.forEach((dot) => { | |
| dot.style.background = "#2f6b4f"; | |
| dot.style.boxShadow = "none"; | |
| }); | |
| } | |
| function showError(message) { | |
| setText(errorText, message); | |
| show(errorState); | |
| } | |
| function renderResults(query, data) { | |
| hide(pipelineSection); | |
| show(resultsSection); | |
| if (resultsTitle) setText(resultsTitle, `Verdict for "${query}"`); | |
| if (dataSourceBadge) { | |
| if (data.used_live_data) { | |
| setText(dataSourceBadge, "LIVE DATA · SERPAPI"); | |
| dataSourceBadge.className = "badge live"; | |
| } else { | |
| setText(dataSourceBadge, "SAMPLE CATALOG"); | |
| dataSourceBadge.className = "badge sample"; | |
| } | |
| } | |
| if (data.errors && data.errors.length) { | |
| setText(banner, data.errors.join(" ")); | |
| show(banner); | |
| } else { | |
| hide(banner); | |
| } | |
| const results = data.results || []; | |
| if (results.length === 0) { | |
| cardsWrap.innerHTML = ""; | |
| showError("No products matched. Try a broader search term."); | |
| hide(resultsSection); | |
| return; | |
| } | |
| cardsWrap.innerHTML = results.map(cardHTML).join(""); | |
| compareTable.innerHTML = compareTableHTML(results); | |
| compareWrap.classList.add("hidden"); | |
| if (toggleCompareBtn) setText(toggleCompareBtn, "Compare Products"); | |
| } | |
| function fmtPrice(price, currency) { | |
| if (price === null || price === undefined) return "Price unavailable"; | |
| const symbol = currency === "INR" ? "₹" : currency ? currency + " " : ""; | |
| return `${symbol}${Number(price).toLocaleString("en-IN")}`; | |
| } | |
| function placeholderImage(title) { | |
| const short = (title || "Product").split(" ").slice(0, 3).join(" "); | |
| return `https://placehold.co/400x400/ece7db/14171f?font=roboto&text=${encodeURIComponent(short)}`; | |
| } | |
| function cardHTML(item) { | |
| const pros = item.review_analysis?.pros || []; | |
| const cons = item.review_analysis?.cons || []; | |
| const confidence = item.review_analysis?.confidence || ""; | |
| const features = item.spec_analysis?.key_features || []; | |
| const valueScore = item.price_analysis?.value_score; | |
| const valueVerdict = item.price_analysis?.verdict || ""; | |
| const realReviews = item.real_review_snippets || []; | |
| const imgSrc = item.thumbnail || placeholderImage(item.title); | |
| return ` | |
| <div class="card ${item.is_best_pick ? "best" : ""}"> | |
| ${item.is_best_pick ? '<div class="stamp">BEST PICK</div>' : ""} | |
| <div class="card-image-wrap"> | |
| <img class="card-image" src="${imgSrc}" alt="${escapeHTML(item.title)}" loading="lazy" | |
| onerror="this.onerror=null;this.src='${placeholderImage(item.title)}';" /> | |
| </div> | |
| <div class="card-rank">RANK #${item.rank ?? "-"}</div> | |
| <h3 class="card-title">${escapeHTML(item.title)}</h3> | |
| ${item.tag ? `<span class="card-tag">${escapeHTML(item.tag)}</span>` : ""} | |
| <div class="card-price-row"> | |
| <span class="card-price">${fmtPrice(item.price, item.currency)}</span> | |
| <span class="card-rating">${item.rating ? "★ " + item.rating : ""}${item.reviews ? ` · ${Number(item.reviews).toLocaleString()} reviews` : ""}</span> | |
| </div> | |
| ${item.reasoning ? `<p class="card-reasoning">${escapeHTML(item.reasoning)}</p>` : ""} | |
| ${valueScore !== undefined ? `<div class="card-value">Value score: <strong>${valueScore}/10</strong>${valueVerdict ? " — " + escapeHTML(valueVerdict) : ""}</div>` : ""} | |
| ${realReviews.length ? ` | |
| <div class="real-reviews"> | |
| <span class="real-reviews-label">Real customer snippets</span> | |
| ${realReviews.map(r => ` | |
| <div class="real-review"> | |
| <p>"${escapeHTML(r.snippet)}"</p> | |
| <span class="real-review-meta">${r.rating ? "★ " + r.rating + " · " : ""}${escapeHTML(r.source || "Google")}</span> | |
| </div> | |
| `).join("")} | |
| </div> | |
| ` : ""} | |
| ${(pros.length || cons.length) ? ` | |
| <div class="proscons"> | |
| ${pros.length ? `<div class="pros-col"><span>Pros</span><ul>${pros.map(p => `<li>${escapeHTML(p)}</li>`).join("")}</ul></div>` : ""} | |
| ${cons.length ? `<div class="cons-col"><span>Watch for</span><ul>${cons.map(c => `<li>${escapeHTML(c)}</li>`).join("")}</ul></div>` : ""} | |
| </div> | |
| ${confidence ? `<div class="confidence">review signal confidence: ${confidence}</div>` : ""} | |
| ` : ""} | |
| ${features.length ? `<div class="card-features">${features.map(f => `<span class="feature-pill">${escapeHTML(f)}</span>`).join("")}</div>` : ""} | |
| ${item.link ? `<a class="card-link" href="${item.link}" target="_blank" rel="noopener">View listing ↗</a>` : ""} | |
| </div> | |
| `; | |
| } | |
| function compareTableHTML(results) { | |
| const rows = results.map((item) => ` | |
| <tr class="${item.is_best_pick ? "best-row" : ""}"> | |
| <td>#${item.rank ?? "-"}</td> | |
| <td>${escapeHTML(item.title)}</td> | |
| <td>${fmtPrice(item.price, item.currency)}</td> | |
| <td>${item.rating ?? "—"}</td> | |
| <td>${item.reviews ? Number(item.reviews).toLocaleString() : "—"}</td> | |
| <td>${item.price_analysis?.value_score ?? "—"}</td> | |
| <td>${item.tag ?? ""}</td> | |
| </tr> | |
| `).join(""); | |
| return ` | |
| <thead> | |
| <tr> | |
| <th>Rank</th><th>Product</th><th>Price</th><th>Rating</th><th>Reviews</th><th>Value score</th><th>Tag</th> | |
| </tr> | |
| </thead> | |
| <tbody>${rows}</tbody> | |
| `; | |
| } | |
| function escapeHTML(str) { | |
| const div = document.createElement("div"); | |
| div.textContent = str; | |
| return div.innerHTML; | |
| } | |
| function show(el) { if (el) el.classList.remove("hidden"); } | |
| function hide(el) { if (el) el.classList.add("hidden"); } | |