ShopPilot_AI / frontend /script.js
ShivaliGupta's picture
Fix script.js
f766171
Raw History Blame Contribute Delete
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"); }