/* ── TheNicheQuiz.com – Frontend Logic ─────────────────────────────────── */
let adCount = 1;
let previewAdIndex = 0;
let lastCSVData = null;
let lastExportCSVData = null;
// Quiz state
let quizState = {
step: 1,
industry: '',
subNiche: '',
microNiche: ''
};
// Parallel campaigns state
let parallelCampaigns = [];
let galleryMode = false;
// ── Save Campaign Set ───────────────────────────────────────────────────────
async function saveCampaignSet() {
if (parallelCampaigns.length === 0) {
showToast('No campaigns to save', 'error');
return;
}
const name = quizState.microNiche || quizState.subNiche || quizState.industry || 'Untitled Campaign';
try {
const res = await fetch('/api/save-campaign', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
name: name + ' — ' + new Date().toLocaleDateString(),
industry: quizState.industry,
sub_niche: quizState.subNiche,
micro_niche: quizState.microNiche,
campaign_data: { campaigns: parallelCampaigns }
})
});
const data = await res.json();
if (data.error) throw new Error(data.error);
showToast('Campaigns saved! View them in My Campaigns.', 'success');
} catch (e) {
showToast('Failed to save: ' + e.message, 'error');
}
}
// ── Particle Background ─────────────────────────────────────────────────────
(function initParticles() {
const canvas = document.getElementById('particleCanvas');
if (!canvas) return;
const ctx = canvas.getContext('2d');
let particles = [];
const PARTICLE_COUNT = 60;
function resize() {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
}
resize();
window.addEventListener('resize', resize);
class Particle {
constructor() { this.reset(); }
reset() {
this.x = Math.random() * canvas.width;
this.y = Math.random() * canvas.height;
this.vx = (Math.random() - 0.5) * 0.3;
this.vy = (Math.random() - 0.5) * 0.3;
this.radius = Math.random() * 1.5 + 0.5;
this.alpha = Math.random() * 0.5 + 0.1;
const colors = ['245, 158, 11', '59, 130, 246', '168, 85, 247'];
this.color = colors[Math.floor(Math.random() * colors.length)];
}
update() {
this.x += this.vx;
this.y += this.vy;
if (this.x < 0 || this.x > canvas.width) this.vx *= -1;
if (this.y < 0 || this.y > canvas.height) this.vy *= -1;
}
draw() {
ctx.beginPath();
ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2);
ctx.fillStyle = `rgba(${this.color}, ${this.alpha})`;
ctx.fill();
}
}
for (let i = 0; i < PARTICLE_COUNT; i++) particles.push(new Particle());
function drawConnections() {
for (let i = 0; i < particles.length; i++) {
for (let j = i + 1; j < particles.length; j++) {
const dx = particles[i].x - particles[j].x;
const dy = particles[i].y - particles[j].y;
const dist = Math.sqrt(dx * dx + dy * dy);
if (dist < 150) {
ctx.beginPath();
ctx.moveTo(particles[i].x, particles[i].y);
ctx.lineTo(particles[j].x, particles[j].y);
ctx.strokeStyle = `rgba(255, 255, 255, ${0.03 * (1 - dist / 150)})`;
ctx.lineWidth = 0.5;
ctx.stroke();
}
}
}
}
function animate() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
particles.forEach(p => { p.update(); p.draw(); });
drawConnections();
requestAnimationFrame(animate);
}
animate();
})();
// ═══════════════════════════════════════════════════════════════════════════
// NICHE QUIZ
// ═══════════════════════════════════════════════════════════════════════════
function updateStepIndicator() {
const fill = document.getElementById('stepLineFill');
for (let i = 1; i <= 3; i++) {
const dot = document.getElementById(`stepDot${i}`);
dot.classList.remove('active', 'completed');
if (i < quizState.step) dot.classList.add('completed');
else if (i === quizState.step) dot.classList.add('active');
}
fill.style.width = ((quizState.step - 1) / 2 * 100) + '%';
}
function goToStep(step) {
quizState.step = step;
document.getElementById('quizStep1').classList.add('hidden');
document.getElementById('quizStep2').classList.add('hidden');
document.getElementById('quizStep3').classList.add('hidden');
document.getElementById('quizComplete').classList.add('hidden');
document.getElementById(`quizStep${step}`).classList.remove('hidden');
updateStepIndicator();
}
function selectIndustry(btn) {
document.querySelectorAll('.industry-card').forEach(c => c.classList.remove('selected'));
btn.classList.add('selected');
document.getElementById('customIndustryRow').classList.add('hidden');
const industry = btn.dataset.industry;
quizState.industry = industry;
setTimeout(() => {
goToStep(2);
loadSubNiches(industry);
}, 300);
}
function selectIndustryOther(btn) {
document.querySelectorAll('.industry-card').forEach(c => c.classList.remove('selected'));
btn.classList.add('selected');
document.getElementById('customIndustryRow').classList.remove('hidden');
document.getElementById('customIndustryInput').focus();
}
function confirmCustomIndustry() {
const val = document.getElementById('customIndustryInput').value.trim();
if (!val) { showToast('Please enter your industry', 'error'); return; }
quizState.industry = val;
goToStep(2);
loadSubNiches(val);
}
async function loadSubNiches(industry) {
const grid = document.getElementById('subNicheGrid');
const loading = document.getElementById('nicheLoading2');
document.getElementById('selectedIndustryLabel').textContent = industry;
grid.innerHTML = '';
loading.classList.remove('hidden');
setStatus('Finding sub-niches...', 'loading');
try {
const res = await fetch('/api/suggest-niches', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ industry })
});
const data = await res.json();
loading.classList.add('hidden');
setStatus('Ready', 'ready');
if (data.error) { showToast(data.error, 'error'); return; }
data.niches.forEach(niche => {
const card = document.createElement('button');
card.className = 'niche-card';
card.textContent = niche;
card.onclick = () => selectSubNiche(niche, card);
grid.appendChild(card);
});
} catch (e) {
loading.classList.add('hidden');
setStatus('Ready', 'ready');
showToast('Failed to load sub-niches', 'error');
}
}
function selectSubNiche(niche, card) {
document.querySelectorAll('#subNicheGrid .niche-card').forEach(c => c.classList.remove('selected'));
if (card) card.classList.add('selected');
quizState.subNiche = niche;
setTimeout(() => {
goToStep(3);
loadMicroNiches(quizState.industry, niche);
}, 300);
}
function confirmCustomSubNiche() {
const val = document.getElementById('customSubNicheInput').value.trim();
if (!val) { showToast('Please enter your sub-niche', 'error'); return; }
selectSubNiche(val, null);
}
async function loadMicroNiches(industry, subNiche) {
const grid = document.getElementById('microNicheGrid');
const loading = document.getElementById('nicheLoading3');
document.getElementById('breadcrumb1').textContent = industry;
document.getElementById('breadcrumb2').textContent = subNiche;
grid.innerHTML = '';
loading.classList.remove('hidden');
setStatus('Finding micro-niches...', 'loading');
try {
const res = await fetch('/api/suggest-niches', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ industry, sub_niche: subNiche })
});
const data = await res.json();
loading.classList.add('hidden');
setStatus('Ready', 'ready');
if (data.error) { showToast(data.error, 'error'); return; }
data.niches.forEach(niche => {
const card = document.createElement('button');
card.className = 'niche-card';
card.textContent = niche;
card.onclick = () => selectMicroNiche(niche, card);
grid.appendChild(card);
});
} catch (e) {
loading.classList.add('hidden');
setStatus('Ready', 'ready');
showToast('Failed to load micro-niches', 'error');
}
}
function selectMicroNiche(niche, card) {
document.querySelectorAll('#microNicheGrid .niche-card').forEach(c => c.classList.remove('selected'));
if (card) card.classList.add('selected');
quizState.microNiche = niche;
setTimeout(() => showQuizComplete(), 300);
}
function confirmCustomMicroNiche() {
const val = document.getElementById('customMicroNicheInput').value.trim();
if (!val) { showToast('Please enter your micro-niche', 'error'); return; }
quizState.microNiche = val;
showQuizComplete();
}
function showQuizComplete() {
quizState.step = 4;
document.getElementById('quizStep1').classList.add('hidden');
document.getElementById('quizStep2').classList.add('hidden');
document.getElementById('quizStep3').classList.add('hidden');
document.getElementById('quizComplete').classList.remove('hidden');
document.getElementById('finalIndustry').textContent = quizState.industry;
document.getElementById('finalSubNiche').textContent = quizState.subNiche;
document.getElementById('finalMicroNiche').textContent = quizState.microNiche;
// Update step indicator to all completed
for (let i = 1; i <= 3; i++) {
document.getElementById(`stepDot${i}`).classList.remove('active');
document.getElementById(`stepDot${i}`).classList.add('completed');
}
document.getElementById('stepLineFill').style.width = '100%';
showToast('Niche locked in!', 'success');
}
function skipToManual() {
document.getElementById('quizSection').classList.add('hidden');
document.getElementById('manualBuilder').classList.remove('hidden');
// Pre-fill with quiz data if available
const nameInput = document.querySelector('#campaignSection [data-field="campaign_name"]');
const adsetInput = document.querySelector('#adsetSection [data-field="adset_name"]');
const bodyInput = document.querySelector('.ad-card [data-field="body"]');
const titleInput = document.querySelector('.ad-card [data-field="title"]');
if (quizState.microNiche) {
if (nameInput) nameInput.value = quizState.microNiche + ' Campaign';
if (adsetInput) adsetInput.value = quizState.microNiche + ' - Core Audience';
if (titleInput) titleInput.value = quizState.microNiche;
updatePreview();
}
}
// ═══════════════════════════════════════════════════════════════════════════
// PARALLEL CAMPAIGNS
// ═══════════════════════════════════════════════════════════════════════════
async function generateParallelCampaigns() {
document.getElementById('quizSection').classList.add('hidden');
document.getElementById('campaignsSection').classList.remove('hidden');
document.getElementById('generationLoading').classList.remove('hidden');
document.getElementById('campaignCardsGrid').innerHTML = '';
document.getElementById('campaignsNicheLabel').textContent = quizState.microNiche;
setStatus('Generating campaigns...', 'loading');
try {
const res = await fetch('/api/generate-parallel-campaigns', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
industry: quizState.industry,
sub_niche: quizState.subNiche,
micro_niche: quizState.microNiche
})
});
const data = await res.json();
document.getElementById('generationLoading').classList.add('hidden');
if (data.error) {
showToast('Campaign generation failed: ' + data.error, 'error');
setStatus('Ready', 'ready');
return;
}
parallelCampaigns = data.campaigns;
renderCampaignCards();
setStatus('Ready', 'ready');
showToast(`Generated ${parallelCampaigns.length} campaigns!`, 'success');
} catch (e) {
document.getElementById('generationLoading').classList.add('hidden');
setStatus('Ready', 'ready');
showToast('Failed to generate campaigns', 'error');
}
}
const CTA_MAP = {
'LEARN_MORE': 'Learn More', 'SHOP_NOW': 'Shop Now', 'SIGN_UP': 'Sign Up',
'DOWNLOAD': 'Download', 'GET_QUOTE': 'Get Quote', 'CONTACT_US': 'Contact Us',
'BOOK_NOW': 'Book Now', 'APPLY_NOW': 'Apply Now', 'NO_BUTTON': ''
};
function renderCampaignCards() {
const grid = document.getElementById('campaignCardsGrid');
grid.innerHTML = '';
parallelCampaigns.forEach((c, i) => {
const card = document.createElement('div');
card.className = `campaign-card ${c.selected ? '' : 'deselected'}`;
card.id = `campaignCard${i}`;
const targeting = c.targeting || {};
const adCopy = c.ad_copy || {};
const hasImage = !!c.image_url;
card.innerHTML = `
${escapeHtml(c.micro_niche || '')}
${hasImage
? `

`
: `
`
}
${escapeHtml(adCopy.body || '')}
🎯 Targeting: Ages ${targeting.age_min || '?'}-${targeting.age_max || '?'} · ${targeting.gender || 'All'}
Interests: ${escapeHtml(targeting.interests || 'N/A')}
Behaviors: ${escapeHtml(targeting.behaviors || 'N/A')}
📄 ${escapeHtml(c.landing_page_angle || '')}
`;
grid.appendChild(card);
});
}
function toggleCampaign(index) {
parallelCampaigns[index].selected = !parallelCampaigns[index].selected;
const card = document.getElementById(`campaignCard${index}`);
const toggle = card.querySelector('.campaign-toggle');
if (parallelCampaigns[index].selected) {
card.classList.remove('deselected');
toggle.classList.add('active');
} else {
card.classList.add('deselected');
toggle.classList.remove('active');
}
// Also update gallery view if visible
const galleryCard = document.getElementById(`galleryCard${index}`);
if (galleryCard) {
if (parallelCampaigns[index].selected) {
galleryCard.classList.remove('deselected');
} else {
galleryCard.classList.add('deselected');
}
}
}
async function generateCampaignImage(index) {
const campaign = parallelCampaigns[index];
const imageArea = document.getElementById(`campaignImage${index}`);
const card = document.getElementById(`campaignCard${index}`);
const btn = card.querySelector('.btn-generate');
if (!campaign.image_prompt) {
showToast('No image prompt for this campaign', 'error');
return;
}
btn.disabled = true;
btn.classList.add('generating');
imageArea.innerHTML = ``;
setStatus(`Generating image ${index + 1}...`, 'loading');
try {
const res = await fetch('/api/generate-image', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ prompt: campaign.image_prompt })
});
const data = await res.json();
if (data.error) throw new Error(data.error);
campaign.image_url = data.url;
imageArea.innerHTML = `
`;
// Update gallery view too
const galleryImg = document.querySelector(`#galleryCard${index} .gallery-fb-image`);
if (galleryImg) {
galleryImg.innerHTML = `
`;
}
setStatus('Ready', 'ready');
showToast(`Image generated for campaign #${index + 1}`, 'success');
} catch (e) {
imageArea.innerHTML = `Failed
`;
setStatus('Ready', 'ready');
showToast(`Image failed: ${e.message}`, 'error');
}
btn.disabled = false;
btn.classList.remove('generating');
}
async function generateAllImages() {
const selected = parallelCampaigns.map((c, i) => ({ campaign: c, index: i })).filter(x => x.campaign.selected);
if (selected.length === 0) {
showToast('No campaigns selected', 'error');
return;
}
const progressBar = document.getElementById('imageProgress');
const fill = document.getElementById('progressBarFill');
const text = document.getElementById('progressText');
const btn = document.getElementById('generateAllBtn');
progressBar.classList.remove('hidden');
btn.disabled = true;
let completed = 0;
text.textContent = `Generating images: 0/${selected.length}`;
fill.style.width = '0%';
for (const item of selected) {
if (item.campaign.image_url) {
completed++;
text.textContent = `Generating images: ${completed}/${selected.length}`;
fill.style.width = (completed / selected.length * 100) + '%';
continue;
}
await generateCampaignImage(item.index);
completed++;
text.textContent = `Generating images: ${completed}/${selected.length}`;
fill.style.width = (completed / selected.length * 100) + '%';
}
btn.disabled = false;
text.textContent = `All ${completed} images generated!`;
showToast('All images generated!', 'success');
setTimeout(() => progressBar.classList.add('hidden'), 3000);
}
// ═══════════════════════════════════════════════════════════════════════════
// GALLERY VIEW
// ═══════════════════════════════════════════════════════════════════════════
function toggleGalleryView() {
galleryMode = !galleryMode;
const grid = document.getElementById('campaignCardsGrid');
const gallery = document.getElementById('galleryView');
const label = document.getElementById('galleryToggleLabel');
if (galleryMode) {
grid.classList.add('hidden');
gallery.classList.remove('hidden');
label.textContent = 'Card View';
renderGallery();
} else {
grid.classList.remove('hidden');
gallery.classList.add('hidden');
label.textContent = 'Gallery View';
}
}
function renderGallery() {
const gallery = document.getElementById('galleryView');
gallery.innerHTML = '';
parallelCampaigns.forEach((c, i) => {
const adCopy = c.ad_copy || {};
const ctaText = CTA_MAP[c.cta] || 'Learn More';
const card = document.createElement('div');
card.className = `gallery-ad-card ${c.selected ? '' : 'deselected'}`;
card.id = `galleryCard${i}`;
card.innerHTML = `
${escapeHtml(adCopy.body || '')}
${c.image_url
? `

`
: `
No image
`
}
${escapeHtml(adCopy.headline || '')}
${escapeHtml(adCopy.description || '')}
${escapeHtml(c.micro_niche || '')}
`;
gallery.appendChild(card);
});
}
function updateCampaignField(index, field, value) {
if (!parallelCampaigns[index].ad_copy) parallelCampaigns[index].ad_copy = {};
parallelCampaigns[index].ad_copy[field] = value;
}
// ═══════════════════════════════════════════════════════════════════════════
// CSV EXPORT (for parallel campaigns)
// ═══════════════════════════════════════════════════════════════════════════
async function exportSelectedCampaigns() {
// Check if we have parallel campaigns
if (parallelCampaigns.length > 0) {
exportParallelCampaignsCSV();
return;
}
// Fallback: original manual flow
generateCSV();
}
async function exportParallelCampaignsCSV() {
const selected = parallelCampaigns.filter(c => c.selected);
if (selected.length === 0) {
showToast('No campaigns selected for export', 'error');
return;
}
// Build CSV rows
const ads = selected.map(c => {
const targeting = c.targeting || {};
const adCopy = c.ad_copy || {};
return {
campaign_name: c.campaign_name || '',
campaign_objective: 'OUTCOME_LEADS',
buying_type: 'AUCTION',
campaign_status: 'PAUSED',
campaign_budget_optimization: 'FALSE',
adset_name: c.adset_name || '',
adset_status: 'PAUSED',
targeting_age_min: String(targeting.age_min || 18),
targeting_age_max: String(targeting.age_max || 65),
targeting_genders: targeting.gender || 'All',
targeting_interests: targeting.interests || '',
targeting_behaviors: targeting.behaviors || '',
targeting_geo_locations: 'US',
ad_name: (c.campaign_name || 'Ad') + ' - Creative 1',
ad_status: 'PAUSED',
body: adCopy.body || '',
title: adCopy.headline || '',
caption: adCopy.description || '',
call_to_action: c.cta || 'LEARN_MORE',
image_file: c.image_url || ''
};
});
setStatus('Generating CSV...', 'loading');
try {
const res = await fetch('/api/generate-csv', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ ads })
});
const data = await res.json();
if (data.error) throw new Error(data.error);
lastExportCSVData = data;
// Show CSV preview section
const section = document.getElementById('csvPreviewSection');
section.classList.remove('hidden');
document.getElementById('csvExportRowCount').textContent = `${data.rows.length} campaign(s) · ${data.columns.length} columns`;
// Build table
const activeCols = data.columns.filter(col =>
data.rows.some(row => row[col] && row[col].trim())
);
let html = '';
activeCols.forEach(col => { html += `| ${escapeHtml(col)} | `; });
html += '
';
data.rows.forEach(row => {
html += '';
activeCols.forEach(col => {
html += `| ${escapeHtml(row[col] || '—')} | `;
});
html += '
';
});
html += '
';
document.getElementById('csvExportTableWrap').innerHTML = html;
section.scrollIntoView({ behavior: 'smooth' });
setStatus('Ready', 'ready');
showToast(`CSV generated — ${data.rows.length} row(s)`, 'success');
} catch (e) {
setStatus('Ready', 'ready');
showToast('CSV generation failed: ' + e.message, 'error');
}
}
function downloadExportCSV() {
if (!lastExportCSVData) return;
const blob = new Blob([lastExportCSVData.csv_content], { type: 'text/csv' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = lastExportCSVData.filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
showToast(`Downloaded ${lastExportCSVData.filename}`, 'success');
}
// ═══════════════════════════════════════════════════════════════════════════
// ORIGINAL MANUAL BUILDER FUNCTIONS
// ═══════════════════════════════════════════════════════════════════════════
function toggleSection(id) {
const body = document.getElementById(`body-${id}`);
const chevron = document.getElementById(`chevron-${id}`);
if (body) body.classList.toggle('collapsed');
if (chevron) chevron.classList.toggle('rotated');
}
function toggleSubSection(id) {
const body = document.getElementById(`body-${id}`);
const chevron = document.getElementById(`chevron-${id}`);
if (body) body.classList.toggle('collapsed');
if (chevron) chevron.classList.toggle('rotated');
}
function toggleCBO(value) {
const group = document.getElementById('cboBudgetGroup');
if (group) group.style.display = value === 'TRUE' ? '' : 'none';
}
function switchTab(tab) {
document.querySelectorAll('.preview-tab').forEach(t => t.classList.remove('active'));
document.querySelector(`[data-tab="${tab}"]`).classList.add('active');
const previewPanel = document.getElementById('previewPanel');
const csvPanel = document.getElementById('csvPanel');
if (tab === 'preview') {
previewPanel.classList.remove('hidden');
csvPanel.classList.add('hidden');
} else {
previewPanel.classList.add('hidden');
csvPanel.classList.remove('hidden');
}
}
function updateCharCount(textarea) {
const counter = textarea.parentElement.querySelector('.char-count');
if (counter) {
const len = textarea.value.length;
const max = textarea.maxLength || 2200;
counter.textContent = `${len.toLocaleString()} / ${max.toLocaleString()}`;
counter.style.color = len > max * 0.9 ? '#ef4444' : '';
}
}
function updatePreview() {
const adCard = document.querySelectorAll('.ad-card')[previewAdIndex];
if (!adCard) return;
const body = getFieldValue(adCard, 'body') || 'Your ad text will appear here.';
document.getElementById('previewBody').textContent = body;
const title = getFieldValue(adCard, 'title') || 'Your Headline Here';
document.getElementById('previewHeadline').textContent = title;
const caption = getFieldValue(adCard, 'caption') || 'Your description text';
document.getElementById('previewDescription').textContent = caption;
const link = getFieldValue(adCard, 'link') || getFieldValue(adCard, 'display_link') || '';
let displayDomain = 'example.com';
if (link) { try { displayDomain = new URL(link).hostname; } catch { displayDomain = link.replace(/https?:\/\//, '').split('/')[0] || 'example.com'; } }
document.getElementById('previewDisplayLink').textContent = getFieldValue(adCard, 'display_link') || displayDomain;
const cta = getFieldValue(adCard, 'call_to_action') || 'LEARN_MORE';
const ctaBtn = document.getElementById('previewCTA');
const ctaText = CTA_MAP[cta] || 'Learn More';
ctaBtn.textContent = ctaText;
ctaBtn.style.display = ctaText ? '' : 'none';
const campaignName = document.querySelector('[data-field="campaign_name"]');
if (campaignName && campaignName.value) {
document.getElementById('previewPageName').textContent = campaignName.value;
}
}
function getFieldValue(container, fieldName) {
const el = container.querySelector(`[data-field="${fieldName}"]`);
return el ? el.value : '';
}
function addAd() {
const container = document.getElementById('adsContainer');
const index = adCount++;
const num = document.querySelectorAll('.ad-card').length + 1;
const section = document.createElement('section');
section.className = 'form-card ad-card';
section.dataset.adIndex = index;
section.innerHTML = `
`;
container.appendChild(section);
section.scrollIntoView({ behavior: 'smooth', block: 'center' });
showToast('Ad creative added', 'success');
}
function removeAd(btn) {
const card = btn.closest('.ad-card');
if (document.querySelectorAll('.ad-card').length <= 1) {
showToast('You need at least one ad', 'error');
return;
}
card.remove();
document.querySelectorAll('.ad-card .ad-number').forEach((num, i) => num.textContent = `#${i + 1}`);
previewAdIndex = Math.min(previewAdIndex, document.querySelectorAll('.ad-card').length - 1);
updatePreview();
showToast('Ad removed', 'info');
}
// ── AI Image Generation (Manual) ────────────────────────────────────────────
async function generateImage(btn) {
const adCard = btn.closest('.ad-card') || btn.closest('.form-card');
const promptInput = adCard.querySelector('[data-ai-prompt]');
const container = adCard.querySelector('[data-generated-images]');
const prompt = promptInput.value.trim();
if (!prompt) { showToast('Please enter an image prompt', 'error'); promptInput.focus(); return; }
btn.disabled = true;
btn.classList.add('generating');
const origText = btn.innerHTML;
btn.innerHTML = ` Generating...`;
const placeholder = document.createElement('div');
placeholder.className = 'generating-placeholder';
placeholder.innerHTML = '';
container.appendChild(placeholder);
setStatus('Generating image...', 'loading');
try {
const res = await fetch('/api/generate-image', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ prompt })
});
const data = await res.json();
if (data.error) throw new Error(data.error);
placeholder.remove();
const thumb = document.createElement('div');
thumb.className = 'generated-image-thumb';
thumb.innerHTML = `
`;
thumb.onclick = () => selectImage(thumb, data.url, adCard);
container.appendChild(thumb);
if (container.querySelectorAll('.generated-image-thumb').length === 1) {
selectImage(thumb, data.url, adCard);
}
setStatus('Ready', 'ready');
showToast('Image generated successfully', 'success');
} catch (err) {
placeholder.remove();
setStatus('Ready', 'ready');
showToast(`Image generation failed: ${err.message}`, 'error');
}
btn.disabled = false;
btn.classList.remove('generating');
btn.innerHTML = origText;
}
function selectImage(thumb, url, adCard) {
thumb.parentElement.querySelectorAll('.generated-image-thumb').forEach(t => t.classList.remove('selected'));
thumb.classList.add('selected');
document.getElementById('previewImage').innerHTML = `
`;
const imageFileInput = adCard.querySelector('[data-field="image_file"]');
if (imageFileInput) imageFileInput.value = url;
}
// ── CSV Generation (Manual) ─────────────────────────────────────────────────
async function generateCSV() {
const ads = collectFormData();
if (ads.length === 0) { showToast('No ad data to export', 'error'); return; }
for (let i = 0; i < ads.length; i++) {
if (!ads[i].campaign_name) { showToast('Campaign Name is required', 'error'); return; }
if (!ads[i].adset_name) { showToast('Ad Set Name is required', 'error'); return; }
if (!ads[i].ad_name) { showToast(`Ad #${i + 1}: Ad Name is required`, 'error'); return; }
}
setStatus('Generating CSV...', 'loading');
try {
const res = await fetch('/api/generate-csv', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ ads })
});
const data = await res.json();
if (data.error) throw new Error(data.error);
lastCSVData = data;
renderCSVPreview(data);
switchTab('csv');
setStatus('Ready', 'ready');
showToast(`CSV generated — ${data.rows.length} row(s)`, 'success');
} catch (err) {
setStatus('Ready', 'ready');
showToast(`CSV generation failed: ${err.message}`, 'error');
}
}
function collectFormData() {
const ads = [];
const adCards = document.querySelectorAll('.ad-card');
const sharedData = {};
const campaignSection = document.getElementById('campaignSection');
const adsetSection = document.getElementById('adsetSection');
if (campaignSection) campaignSection.querySelectorAll('[data-field]').forEach(el => { sharedData[el.dataset.field] = el.value; });
if (adsetSection) adsetSection.querySelectorAll('[data-field]').forEach(el => { sharedData[el.dataset.field] = el.value; });
adCards.forEach(card => {
const adData = { ...sharedData };
card.querySelectorAll('[data-field]').forEach(el => { adData[el.dataset.field] = el.value; });
ads.push(adData);
});
return ads;
}
function renderCSVPreview(data) {
document.getElementById('csvEmpty').classList.add('hidden');
document.getElementById('csvContent').classList.remove('hidden');
document.getElementById('csvRowCount').textContent = `${data.rows.length} row(s) · ${data.columns.length} columns`;
const activeCols = data.columns.filter(col => data.rows.some(row => row[col] && row[col].trim()));
let html = '';
activeCols.forEach(col => { html += `| ${escapeHtml(col)} | `; });
html += '
';
data.rows.forEach(row => {
html += '';
activeCols.forEach(col => { html += `| ${escapeHtml(row[col] || '—')} | `; });
html += '
';
});
html += '
';
document.getElementById('csvTableWrap').innerHTML = html;
}
function downloadCSV() {
if (!lastCSVData) return;
const blob = new Blob([lastCSVData.csv_content], { type: 'text/csv' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url; a.download = lastCSVData.filename;
document.body.appendChild(a); a.click(); document.body.removeChild(a);
URL.revokeObjectURL(url);
showToast(`Downloaded ${lastCSVData.filename}`, 'success');
}
// ── Status & Toast ──────────────────────────────────────────────────────────
function setStatus(text, type) {
const badge = document.getElementById('statusBadge');
if (!badge) return;
badge.textContent = text;
badge.className = 'status-badge';
if (type === 'loading') badge.classList.add('loading');
if (type === 'error') badge.classList.add('error');
}
function showToast(message, type = 'info') {
const container = document.getElementById('toastContainer');
const toast = document.createElement('div');
toast.className = `toast ${type}`;
const icons = { success: '✓', error: '✕', info: 'ℹ' };
toast.innerHTML = `${icons[type] || 'ℹ'}${escapeHtml(message)}`;
container.appendChild(toast);
setTimeout(() => {
toast.style.animation = 'slideOut 0.3s ease forwards';
setTimeout(() => toast.remove(), 300);
}, 4000);
}
function escapeHtml(str) {
if (!str) return '';
const div = document.createElement('div');
div.textContent = str;
return div.innerHTML;
}
// ── CSS Animation Helper ────────────────────────────────────────────────────
const style = document.createElement('style');
style.textContent = `@keyframes spin { to { transform: rotate(360deg); } } .animate-spin { animation: spin 1s linear infinite; }`;
document.head.appendChild(style);
// ── Init ────────────────────────────────────────────────────────────────────
document.addEventListener('DOMContentLoaded', () => {
updateStepIndicator();
});