|
|
|
|
|
const API_BASE = ""; |
|
|
|
|
|
|
|
|
const form = document.getElementById("researchForm"); |
|
|
const categoryInput = document.getElementById("productCategory"); |
|
|
const descriptionInput = document.getElementById("productDescription"); |
|
|
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"); |
|
|
|
|
|
|
|
|
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 = []; |
|
|
|
|
|
|
|
|
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>'; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
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(); |
|
|
}); |
|
|
|
|
|
|
|
|
toggleBtns.forEach((btn) => { |
|
|
btn.addEventListener("click", () => { |
|
|
toggleBtns.forEach((b) => b.classList.remove("active")); |
|
|
btn.classList.add("active"); |
|
|
selectedMethod = btn.dataset.method; |
|
|
}); |
|
|
}); |
|
|
|
|
|
|
|
|
form.addEventListener("submit", async (e) => { |
|
|
e.preventDefault(); |
|
|
hideError(); |
|
|
hideResults(); |
|
|
|
|
|
const payload = { |
|
|
target_audience: selectedAudiences, |
|
|
product_category: categoryInput.value.trim(), |
|
|
product_description: descriptionInput.value.trim(), |
|
|
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) throw new Error("Server error"); |
|
|
|
|
|
const data = await res.json(); |
|
|
renderResults(data.results, selectedMethod); |
|
|
} catch (err) { |
|
|
showError(err.message || "Something went wrong."); |
|
|
} finally { |
|
|
setLoading(false); |
|
|
} |
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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(); |
|
|
} |
|
|
|
|
|
|
|
|
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; |
|
|
|
|
|
|
|
|
const siblings = card.parentElement.querySelectorAll(".trigger-card"); |
|
|
siblings.forEach((s) => { |
|
|
if (s !== card) s.classList.remove("open"); |
|
|
}); |
|
|
|
|
|
card.classList.toggle("open"); |
|
|
}); |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
function setLoading(isLoading) { |
|
|
submitBtn.disabled = isLoading; |
|
|
btnText.classList.toggle("hidden", isLoading); |
|
|
btnLoader.classList.toggle("hidden", !isLoading); |
|
|
} |
|
|
|
|
|
function showError(msg) { |
|
|
errorBanner.textContent = msg; |
|
|
errorBanner.classList.remove("hidden"); |
|
|
} |
|
|
|
|
|
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, """).replace(/'/g, "'"); |
|
|
} |
|
|
|
|
|
loadAudiences(); |
|
|
|