sushilideaclan01's picture
Add product scraping functionality and AI concept filling
76c3397
// ===== Configuration =====
const API_BASE = "";
// ===== DOM Elements =====
const form = document.getElementById("researchForm");
const categoryInput = document.getElementById("productCategory");
const descriptionInput = document.getElementById("productDescription");
const productUrlInput = document.getElementById("productUrl");
const conceptsCountInput = document.getElementById("conceptsCount");
const scrapeBtn = document.getElementById("scrapeBtn");
const scrapeBtnText = scrapeBtn.querySelector(".scrape-btn-text");
const scrapeBtnLoader = scrapeBtn.querySelector(".scrape-btn-loader");
const submitBtn = document.getElementById("submitBtn");
const btnText = submitBtn.querySelector(".btn-text");
const btnLoader = submitBtn.querySelector(".btn-loader");
const errorBanner = document.getElementById("errorBanner");
const resultsDiv = document.getElementById("results");
const toggleBtns = document.querySelectorAll(".toggle-btn");
// Multi-select elements
const multiselect = document.getElementById("audienceMultiselect");
const selectedContainer = document.getElementById("selectedAudiences");
const dropdown = document.getElementById("audienceDropdown");
const searchInput = document.getElementById("audienceSearch");
const optionsContainer = document.getElementById("audienceOptions");
let selectedMethod = "gpt";
let selectedAudiences = [];
let allAudiences = [];
// ===== Load Target Audiences =====
async function loadAudiences() {
try {
const res = await fetch(`${API_BASE}/api/target-audiences`);
if (!res.ok) throw new Error("Failed to load audiences");
const data = await res.json();
allAudiences = data.audiences;
renderOptions();
renderSelected();
} catch (err) {
console.error("Could not load audiences:", err);
selectedContainer.innerHTML =
'<span class="multiselect-placeholder">⚠ Could not load — is the backend running?</span>';
}
}
// ===== Multi-select Logic (UNCHANGED) =====
function renderOptions(filter = "") {
const filterLower = filter.toLowerCase();
const filtered = allAudiences.filter((a) =>
a.toLowerCase().includes(filterLower)
);
optionsContainer.innerHTML = filtered
.map((a) => {
const isSelected = selectedAudiences.includes(a);
return `
<div class="multiselect-option ${isSelected ? "selected" : ""}" data-value="${escapeAttr(a)}">
<span class="check">${isSelected ? "✓" : ""}</span>
<span>${escapeHtml(a)}</span>
</div>
`;
})
.join("");
optionsContainer.querySelectorAll(".multiselect-option").forEach((opt) => {
opt.addEventListener("click", () => {
const val = opt.dataset.value;
if (selectedAudiences.includes(val)) {
selectedAudiences = selectedAudiences.filter((a) => a !== val);
} else {
selectedAudiences.push(val);
}
renderOptions(searchInput.value);
renderSelected();
});
});
}
function renderSelected() {
if (selectedAudiences.length === 0) {
selectedContainer.innerHTML =
'<span class="multiselect-placeholder">Select target audiences…</span>';
return;
}
selectedContainer.innerHTML = selectedAudiences
.map(
(a) => `
<span class="multiselect-tag">
${escapeHtml(a)}
<span class="multiselect-tag-remove" data-value="${escapeAttr(a)}">×</span>
</span>
`
)
.join("");
selectedContainer.querySelectorAll(".multiselect-tag-remove").forEach((btn) => {
btn.addEventListener("click", (e) => {
e.stopPropagation();
const val = btn.dataset.value;
selectedAudiences = selectedAudiences.filter((a) => a !== val);
renderOptions(searchInput.value);
renderSelected();
});
});
}
selectedContainer.addEventListener("click", () => {
dropdown.classList.toggle("hidden");
});
document.addEventListener("click", (e) => {
if (!multiselect.contains(e.target)) {
dropdown.classList.add("hidden");
}
});
searchInput.addEventListener("input", () => {
renderOptions(searchInput.value);
});
dropdown.addEventListener("click", (e) => {
e.stopPropagation();
});
// ===== Method Toggle =====
toggleBtns.forEach((btn) => {
btn.addEventListener("click", () => {
toggleBtns.forEach((b) => b.classList.remove("active"));
btn.classList.add("active");
selectedMethod = btn.dataset.method;
});
});
// ===== Scrape Product =====
scrapeBtn.addEventListener("click", async () => {
const url = productUrlInput.value.trim();
if (!url) {
showError("Please enter a product URL.");
return;
}
// Basic URL validation
try {
new URL(url);
} catch (e) {
showError("Please enter a valid URL.");
return;
}
hideError();
setScrapeLoading(true);
try {
const res = await fetch(`${API_BASE}/api/scrape-product`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ url }),
});
if (!res.ok) {
const errorData = await res.json().catch(() => ({}));
throw new Error(errorData.detail || "Failed to scrape product data.");
}
const data = await res.json();
// Auto-fill form fields: category, description, and target audience
if (data.category) {
categoryInput.value = data.category;
}
if (data.description) {
descriptionInput.value = data.description;
}
// Scrape & Fill also fills target audience from AI suggestions
if (data.target_audience && data.target_audience.length > 0) {
selectedAudiences = [...data.target_audience];
renderOptions(searchInput.value);
renderSelected();
}
const audienceNote = (data.target_audience && data.target_audience.length > 0)
? ` Target audience filled (${data.target_audience.length} selected).`
: "";
showError(`✓ Product data scraped successfully!${data.product_name ? ` Found: ${data.product_name}.` : ""}${audienceNote}`, "success");
// Clear URL input after successful scrape
productUrlInput.value = "";
} catch (err) {
showError(err.message || "Something went wrong while scraping the product.");
} finally {
setScrapeLoading(false);
}
});
function setScrapeLoading(isLoading) {
scrapeBtn.disabled = isLoading;
scrapeBtnText.classList.toggle("hidden", isLoading);
scrapeBtnLoader.classList.toggle("hidden", !isLoading);
}
// ===== Form Submit =====
form.addEventListener("submit", async (e) => {
e.preventDefault();
hideError();
hideResults();
const count = Math.min(15, Math.max(1, parseInt(conceptsCountInput.value, 10) || 5));
const payload = {
target_audience: selectedAudiences,
product_category: categoryInput.value.trim(),
product_description: descriptionInput.value.trim(),
count,
method: selectedMethod,
};
if (
payload.target_audience.length === 0 ||
!payload.product_category ||
!payload.product_description
) {
showError(
"Please fill in all fields and select at least one target audience."
);
return;
}
setLoading(true);
try {
const res = await fetch(`${API_BASE}/api/research`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
});
if (!res.ok) {
const errData = await res.json().catch(() => ({}));
const msg = Array.isArray(errData.detail) ? errData.detail.map((e) => e.msg || e).join("; ") : (errData.detail || "Server error");
throw new Error(msg);
}
const data = await res.json();
renderResults(data.results, selectedMethod);
} catch (err) {
showError(err.message || "Something went wrong.");
} finally {
setLoading(false);
}
});
// =====================================================
// ✅ NEW RESULTS (VERTICAL NESTED COLLAPSIBLE)
// =====================================================
function renderResults(audienceResults, method) {
const badge =
method === "gpt"
? `<span class="results-badge gpt">GPT</span>`
: `<span class="results-badge claude">Claude</span>`;
let html = `
<div class="results-header">
<h2 class="results-title">Results</h2>
${badge}
</div>
<div class="audience-stack">
`;
audienceResults.forEach((group, groupIndex) => {
html += `
<div class="audience-card ${groupIndex === 0 ? "open" : ""}">
<div class="audience-card-header">
<div class="audience-header-left">
<span>👥</span>
<h3>${escapeHtml(group.target_audience)}</h3>
</div>
<span class="audience-chevron">▾</span>
</div>
<div class="audience-card-body">
`;
group.output.forEach((item, idx) => {
html += `
<div class="trigger-card ${idx === 0 && groupIndex === 0 ? "open" : ""}">
<div class="trigger-header">
<div>
<span class="trigger-label">Trigger ${idx + 1}</span>
<h4 class="trigger-name">${escapeHtml(item.phsychologyTriggers)}</h4>
</div>
<span class="trigger-chevron">▾</span>
</div>
<div class="trigger-body">
<div class="trigger-section">
<p class="section-title">Ad Angles</p>
<ul class="section-list">
${item.angles.map((a) => `<li>${escapeHtml(a)}</li>`).join("")}
</ul>
</div>
<div class="trigger-section">
<p class="section-title">Ad Concepts</p>
<ul class="section-list">
${item.concepts.map((c) => `<li>${escapeHtml(c)}</li>`).join("")}
</ul>
</div>
</div>
</div>
`;
});
html += `
</div>
</div>
`;
});
html += `</div>`;
resultsDiv.innerHTML = html;
resultsDiv.classList.remove("hidden");
attachAccordionEvents();
}
// ===== Accordion Logic =====
function attachAccordionEvents() {
document.querySelectorAll(".audience-card-header").forEach((header) => {
header.addEventListener("click", () => {
const card = header.parentElement;
card.classList.toggle("open");
});
});
document.querySelectorAll(".trigger-header").forEach((header) => {
header.addEventListener("click", () => {
const card = header.parentElement;
// Close siblings
const siblings = card.parentElement.querySelectorAll(".trigger-card");
siblings.forEach((s) => {
if (s !== card) s.classList.remove("open");
});
card.classList.toggle("open");
});
});
}
// ===== Helpers =====
function setLoading(isLoading) {
submitBtn.disabled = isLoading;
btnText.classList.toggle("hidden", isLoading);
btnLoader.classList.toggle("hidden", !isLoading);
}
function showError(msg, type = "error") {
errorBanner.textContent = msg;
errorBanner.classList.remove("hidden");
// Update styling based on type
if (type === "success") {
errorBanner.style.background = "rgba(16, 163, 127, 0.1)";
errorBanner.style.borderColor = "rgba(16, 163, 127, 0.3)";
errorBanner.style.color = "#10a37f";
} else {
errorBanner.style.background = "rgba(232, 84, 84, 0.1)";
errorBanner.style.borderColor = "rgba(232, 84, 84, 0.3)";
errorBanner.style.color = "var(--danger)";
}
}
function hideError() {
errorBanner.classList.add("hidden");
}
function hideResults() {
resultsDiv.classList.add("hidden");
}
function escapeHtml(str) {
const div = document.createElement("div");
div.textContent = str;
return div.innerHTML;
}
function escapeAttr(str) {
return str.replace(/"/g, "&quot;").replace(/'/g, "&#39;");
}
loadAudiences();