🚀 NEW: Professional 4K EXR Alpha Matte Extraction for VFX is now LIVE in VFX Studio!

PixelAPI Portrait Studio guarantees the output face matches your input. ArcFace similarity gate. No identity manipulation possible. This is a platform-wide commitment — not a setting.

Credits: 80 per portrait ($0.007 / ₹6.80)
Speed: ~30-45 seconds
Model: AI text-to-image Dev + identity-preserving AI v0.9.1
API: POST /v1/portrait/studio

Result

Upload a photo to get started

`; } async function handlePortraitSelect(input){ const file = input.files[0]; if(!file) return; if(file.size > 10 * 1024 * 1024) return toast('Max 10MB','error'); await _acceptPortraitFile(file); } async function handlePortraitDrop(e){ e.preventDefault(); e.target.closest('.upload-area').classList.remove('dragover'); const file = e.dataTransfer.files[0]; if(!file) return toast('Please drop an image','error'); if(file.size > 10 * 1024 * 1024) return toast('Max 10MB','error'); await _acceptPortraitFile(file); } async function _acceptPortraitFile(file){ // Show detecting state const fn = $('#portrait-filename'); fn.style.display = 'block'; fn.textContent = '🔍 Analysing photo… (detecting faces)'; let finalFile = file; try{ const fd = new FormData(); fd.append('image', file); const d = await apiUpload('/v1/image/detect-faces', fd); const count = d.count || 0; if(count === 0){ toast('No face detected. Try a clearer photo with a visible face.','error'); fn.textContent = '⚠ No face detected in ' + file.name; $('#portrait-submit').disabled = true; return; } // `selected` is the face object we chose — used for the auto-restore decision let selected = null; if(count === 1){ selected = d.faces[0]; finalFile = await _cropFileToBbox(file, selected.crop_x1, selected.crop_y1, selected.crop_x2, selected.crop_y2); fn.textContent = '✓ 1 face detected · ' + file.name; } else { const picked = await new Promise(resolve => _showFacePicker(file, d.faces, resolve)); if(!picked || !picked.file){ fn.textContent = '(cancelled)'; $('#portrait-submit').disabled = true; return; } finalFile = picked.file; selected = picked.face; fn.textContent = '✓ Face selected from group photo · ' + file.name; } // Smart auto-restore: if the face looks degraded, run AI face restoration first if(selected && selected.suggest_restore){ fn.textContent = '✨ Enhancing photo for best portrait… (may take ~10s)'; try{ const enhanced = await _autoRestoreFace(finalFile); if(enhanced){ finalFile = enhanced; const reasons = []; if(selected.is_grayscale) reasons.push('grayscale'); if(selected.blur_score < 100) reasons.push('low sharpness'); if(selected.face_pixel_size < 140) reasons.push('small face'); fn.textContent = '✨ Enhanced (' + reasons.join(', ') + ') · ' + file.name; } }catch(e){ console.warn('auto-restore failed, continuing with raw crop:', e); fn.textContent = '✓ Face ready · ' + file.name; } } }catch(e){ console.warn('face-detect failed, using file as-is', e); fn.textContent = '📎 ' + file.name + ' (' + (file.size/1024).toFixed(0) + ' KB)'; } state.portraitFile = finalFile; $('#portrait-submit').disabled = false; } async function _autoRestoreFace(file){ const fd = new FormData(); fd.append('image', file); const d = await apiUpload('/v1/image/restore-face', fd); const gid = d.generation_id || d.id; if(!gid) throw new Error('no generation_id from restore-face'); for(let i=0; i<22; i++){ await new Promise(x=>setTimeout(x, 2000)); const r = await api('GET', '/v1/image/' + gid); if(r.status === 'completed' && r.output_url){ const url = absUrl(r.output_url); const resp = await fetch(url); const blob = await resp.blob(); return new File([blob], 'restored.png', {type: blob.type || 'image/png'}); } if(r.status === 'failed' || r.status === 'blocked') throw new Error(r.error_message || 'restore failed'); } throw new Error('restore timed out'); } function _cropFileToBbox(file, x1, y1, x2, y2){ return new Promise((resolve, reject) => { const img = new Image(); img.onload = () => { const w = x2 - x1, h = y2 - y1; const canvas = document.createElement('canvas'); canvas.width = w; canvas.height = h; canvas.getContext('2d').drawImage(img, x1, y1, w, h, 0, 0, w, h); canvas.toBlob(blob => resolve(new File([blob], 'face_crop.jpg', {type:'image/jpeg'})), 'image/jpeg', 0.95); }; img.onerror = reject; img.src = URL.createObjectURL(file); }); } function _showFacePicker(file, faces, onSelect){ const html = '
' + '
' + '

' + faces.length + ' faces detected

' + '

Click the face you want to use for the portrait. We\'ll crop + pass it to the generator automatically.

' + '
' + faces.map((f,i) => '
').join('') + '
' + '
' + '
'; document.body.insertAdjacentHTML('beforeend', html); const modal = document.getElementById('face-picker-modal'); modal.querySelectorAll('.face-pick').forEach(el => { el.onmouseenter = () => { el.style.borderColor = 'var(--purple)'; el.style.transform = 'scale(1.03)'; }; el.onmouseleave = () => { el.style.borderColor = 'var(--border)'; el.style.transform = 'scale(1)'; }; el.onclick = async () => { const idx = parseInt(el.dataset.i); const f = faces[idx]; modal.remove(); toast('Cropping selected face…','info'); const cropped = await _cropFileToBbox(file, f.crop_x1, f.crop_y1, f.crop_x2, f.crop_y2); onSelect({file: cropped, face: f}); }; }); document.getElementById('face-cancel').onclick = () => { modal.remove(); onSelect({file: null, face: null}); }; } // Remove-BG face picker — offers "keep everyone" plus one-click extract per face. // Returns via onSelect: {keepAll:true} | {face:obj} | null(cancel) function _showRemoveBgFacePicker(faces, onSelect){ const html = '
' + '
' + '

' + faces.length + ' people detected

' + '

Pick ONE person to keep — we\'ll remove the others and the background. Or keep everyone.

' + '
' + '
👥
Keep everyone
in frame
' + faces.map((f,i) => '
').join('') + '
' + '
' + '
'; document.body.insertAdjacentHTML('beforeend', html); const modal = document.getElementById('rmbg-face-modal'); modal.querySelectorAll('.rmbg-pick').forEach(el => { el.onmouseenter = () => { el.style.borderColor = 'var(--purple)'; el.style.transform = 'scale(1.03)'; }; el.onmouseleave = () => { el.style.borderColor = 'var(--border)'; el.style.transform = 'scale(1)'; }; el.onclick = () => { if(el.dataset.keep === 'all'){ modal.remove(); return onSelect({keepAll: true}); } const idx = parseInt(el.dataset.i); modal.remove(); onSelect({face: faces[idx]}); }; }); document.getElementById('rmbg-face-cancel').onclick = () => { modal.remove(); onSelect(null); }; } async function submitPortrait(){ if(!state.portraitFile) return toast('Please upload a photo first','error'); const btn = $('#portrait-submit'); btn.disabled = true; btn.textContent = 'Processing...'; const res = $('#portrait-result'); res.innerHTML = '

Generating your portrait...

AI text-to-image + identity-preserving AI inference · ~30-45 seconds

'; try{ const form = new FormData(); form.append('image', state.portraitFile); form.append('style', $('#portrait-style').value); form.append('prompt', $('#portrait-prompt').value || ''); form.append('identity_strength', '1.8'); // Identity-locked, no user control form.append('gender', $('#portrait-gender').value); form.append('skin_tone', ($('#portrait-skintone')||{value:'auto'}).value); // PASSPORT_ATTIRE_V1: only meaningful for passport-visa, so only send it there. if ($('#portrait-style').value === 'passport-visa') { form.append('attire', ($('#portrait-attire')||{value:'existing'}).value); } // Pre-submit guard: warn user if cluster is overloaded (load_state-aware). try{ const adm = await checkAdmissionBeforeSubmit('/v1/portrait/studio'); if(!adm.ok){ toast(adm.message, 'error'); btn.disabled=false; btn.textContent='👔 Generate Portrait · 80 credits'; return; } if(adm.eta && adm.message){ toast('Submitting · ' + adm.message, ''); } }catch(_e){} const d = await apiUpload('/v1/portrait/studio', form); const gid = d.generation_id || d.id; state.credits -= (d.credits_used || 80); const dc = $('#dash-credits'); if(dc) dc.textContent = Math.max(0, state.credits); localStorage.setItem('credits', state.credits); pollPortraitResult(gid, btn); }catch(e){ // 503 from admission_controller — surface the friendly retry hint. if(await handleSubmitError(e, '/v1/portrait/studio', null)){ btn.disabled = false; btn.textContent = '👔 Generate Portrait · 80 credits'; return; } if(handlePaymentRequired(e, submitPortrait)){ btn.disabled = false; btn.textContent = '👔 Generate Portrait · 80 credits'; return; } const isCredits = e.message && (e.message.toLowerCase().includes('insufficient') || e.message.includes('402')); res.innerHTML = isCredits ? '

❌ Not enough credits

' : '

❌ ' + esc(e.message) + '

'; btn.disabled = false; btn.textContent = '👔 Generate Portrait · 80 credits'; } } async function portraitAiEdit(instruction, meta){ const cost = (meta && meta.cost) || 5; const have = (state && state.credits!=null) ? state.credits : '?'; // Server-side processing notice + explicit credit consent (Om: inform for every server op) if(!confirm('This AI edit runs on our GPUs and will use '+cost+' credits (you have '+have+').\n\nClient-side sliders above are free; only AI edits cost credits.\n\nContinue?')) return null; let d; try{ d = await api('POST','/v1/image/edit', {image: meta.imageUrl, prompt: instruction}); } catch(e){ if(String(e.message||e).indexOf('402')>=0 || /credit/i.test(e.message||'')) throw new Error('Not enough credits — top up on the Pricing page.'); throw e; } const eid = d.generation_id || d.id; if(!eid) throw new Error('Could not start the edit.'); toast('AI edit queued — this can take a few minutes on our GPUs…','success'); const res = await pollStatusWithETA(eid, '/v1/image/edit/'+eid, '/v1/image/edit', {intervalMs:3000, maxTicks:160}); if(res && res.status==='completed' && res.output_url){ if(typeof refreshBalance==='function') refreshBalance(); return res.output_url; } throw new Error((res && res.error_message) || 'The edit did not complete.'); } function pollPortraitResult(gid, btn){ // Use shared pollStatusWithETA helper so the user sees queue position + // estimated wait, and 503-mid-poll auto-retries cleanly. const resEl = document.getElementById('portrait-result'); const restoreBtn = ()=>{ btn.disabled = false; btn.textContent = '👔 Generate Portrait · 80 credits'; }; pollStatusWithETA(gid, '/v1/portrait/studio/' + gid, '/v1/portrait/studio', { intervalMs: 3000, maxTicks: 400, // 20 min — auto-heal (restore+colorize+retry) legitimately takes ~8-10 min onTick: function(status, etaText){ if(!status || status.status === 'queued' || status.status === 'pending' || status.status === 'processing'){ if(resEl){ // Auto-heal in progress: tell the user WHY it's longer (backend chose an // enhancement pipeline for this input) instead of a silent generic spinner. if(status && status.friendly_message && status.heal_stage){ resEl.innerHTML = '

✨ ' + esc(status.friendly_message) + '

' + '

⏳ ' + (etaText || 'Working…') + '

'; }else{ resEl.innerHTML = '

⏳ ' + (etaText || 'Generating your portrait…') + '

'; } } } }, }).then(function(d){ if(d && d.status === 'completed'){ resEl.innerHTML = '' + '

⬇ Download original ' + '

' + '
'; try{ if(window.HeadshotAdjust){ HeadshotAdjust.mountEditor(document.getElementById('portrait-adjust'), d.output_url, {filename:'headshot-adjusted.png', aiCredits:5, onAiEdit:portraitAiEdit}); } }catch(_e){} }else if(d && d.status === 'failed'){ resEl.innerHTML = '

❌ ' + esc(d.friendly_message||d.error_message||'Unknown error') + '

'; } restoreBtn(); }).catch(function(e){ if(resEl) resEl.innerHTML = '

⏱ ' + esc(e.message||'Timed out') + '

'; restoreBtn(); }); } function handleFileSelect(input){ // record aspect ratio + alpha presence for the fit-mode warning UI try { const f = input.files && input.files[0]; if (f && f.type && f.type.startsWith('image/')) { const _img = new Image(); _img.onload = () => { _toolUploadedAspect = _img.naturalWidth / _img.naturalHeight; // Detect alpha by drawing the corner pixels onto a canvas try { const c = document.createElement('canvas'); c.width = 4; c.height = 4; const ctx = c.getContext('2d'); ctx.clearRect(0, 0, 4, 4); ctx.drawImage(_img, 0, 0, 4, 4); const px = ctx.getImageData(0, 0, 4, 4).data; // alpha < 255 in ANY of 16 pixels → has_alpha let hasAlpha = false; for (let i = 3; i < px.length; i += 4) if (px[i] < 255) { hasAlpha = true; break; } _toolUploadedHasAlpha = hasAlpha; } catch (_e) { _toolUploadedHasAlpha = false; } // Re-check warning in case user already typed dims try { checkFitModeWarning(); } catch (_e) {} }; _img.src = URL.createObjectURL(f); } } catch (e) {} const file = input.files[0]; if(!file) return; state.uploadedFile = file; const preview = $('#upload-preview'); if(preview){ if(file.type && file.type.startsWith('image/')){ preview.src = URL.createObjectURL(file); preview.style.display = 'block'; }else{ preview.removeAttribute('src'); preview.style.display = 'none'; } } const fn = $('#upload-filename'); fn.style.display = 'block'; fn.textContent = `📎 ${file.name} (${(file.size/1024).toFixed(0)} KB)`; $('#tool-submit').disabled = false; } function handleDrop(e){ e.preventDefault(); e.target.closest('.upload-area').classList.remove('dragover'); const file = e.dataTransfer.files[0]; if(!file) return toast('Please drop a file','error'); state.uploadedFile = file; const preview = $('#upload-preview'); if(preview){ if(file.type && file.type.startsWith('image/')){ preview.src = URL.createObjectURL(file); preview.style.display = 'block'; }else{ preview.removeAttribute('src'); preview.style.display = 'none'; } } const fn = $('#upload-filename'); fn.style.display = 'block'; fn.textContent = `📎 ${file.name} (${(file.size/1024).toFixed(0)} KB)`; $('#tool-submit').disabled = false; } async function submitTool(toolKey){ const tool = TOOL_CONFIG[toolKey]; if(!state.uploadedFile) return toast('Please upload a file first','error'); const maxMB = tool.accept && tool.accept.includes('video') ? 500 : 20; if(state.uploadedFile.size > maxMB * 1024 * 1024) return toast('File too large (max ' + maxMB + 'MB). Please compress or trim your video.','error'); const btn = $('#tool-submit'); btn.disabled = true; btn.textContent = 'Processing...'; const res = $('#tool-result'); res.innerHTML = '

Uploading and processing...

'; try{ // Remove-BG: web-only face-detect + picker gate. Kicks in only when ≥2 // human faces are detected; API clients bypass this entirely. let _rmbgFaceBbox = null; if(toolKey === 'remove-bg'){ try{ const fd0 = new FormData(); fd0.append('image', state.uploadedFile); // inject extra fields (handles upscale custom-dims, etc.) try { const _ex = collectExtraFields(toolKey); for (const k in _ex) fd.append(k, _ex[k]); } catch(e){} const det = await apiUpload('/v1/image/detect-faces', fd0); if(det && det.count >= 2 && Array.isArray(det.faces)){ const pick = await new Promise(resolve => _showRemoveBgFacePicker(det.faces, resolve)); if(pick === null){ btn.disabled=false; btn.textContent = `${tool.icon} Process · ${tool.credits} credits`; res.innerHTML = '

Upload a file to get started

'; return; } if(pick && pick.face){ const f = pick.face; _rmbgFaceBbox = `${f.x},${f.y},${f.x + f.w},${f.y + f.h}`; } // else: keepAll — fall through without face_bbox } }catch(_e){ /* detect failed — fall through to normal flow */ } } const form = new FormData(); form.append(tool.fileKey || 'image', state.uploadedFile); if(_rmbgFaceBbox){ form.append('face_bbox', _rmbgFaceBbox); } if(tool.needsPrompt){ const prompt = $('#tool-prompt')?.value?.trim(); if(!prompt) { toast('Please enter a prompt','error'); btn.disabled=false; btn.textContent=`${tool.icon} Process · ${tool.credits} credits`; res.innerHTML='

Upload a file to get started

'; return; } form.append('prompt', prompt); } if(tool.needsMask){ const maskInput = $('#tool-mask-input'); if(maskInput && maskInput.files[0]) { form.append('mask', maskInput.files[0]); } else if(tool.maskRequired === false) { // Mask is optional — auto-mask will be generated from prompt } else { toast('A mask image is required. Paint white over the area to remove.', 'error'); btn.disabled=false; btn.textContent=`${tool.icon} Process · ${tool.credits} credits`; res.innerHTML='

Upload a file to get started

'; return; } } // Append any extra fields (strength, model, etc.) if(tool.extraFields){ for(const ef of tool.extraFields){ const el = $(`#tool-extra-${ef.name}`); const val = el ? el.value : ef.default; form.append(ef.name, val); } } // Pre-submit ETA check (load_state-aware). For known heavy endpoints, // surface estimated wait/queue position so the user knows what to expect. try{ const adm = await checkAdmissionBeforeSubmit(tool.endpoint); if(!adm.ok){ toast(adm.message, 'error'); btn.disabled=false; btn.textContent = `${tool.icon} Process · ${tool.credits} credits`; res.innerHTML = '

' + esc(adm.message) + '

'; return; } if(adm.eta && adm.message){ toast('Submitting · ' + adm.message, ''); } }catch(_e){} const d = await apiUpload(tool.endpoint, form); const gid = d.generation_id || d.id; state.credits -= (d.credits_used || tool.credits); const dc=$('#dash-credits');if(dc)dc.textContent=Math.max(0,state.credits); localStorage.setItem('credits',state.credits); pollToolResult(gid, tool, btn); }catch(e){ // 503 admission_controller — friendly retry hint. Credits not charged. if(await handleSubmitError(e, tool.endpoint, null)){ btn.disabled = false; btn.textContent = `${tool.icon} Process · ${tool.credits} credits`; res.innerHTML = '

Server is busy — please retry shortly. No credits were charged.

'; return; } if(e.message && e.message.includes('Failed to fetch')){ const res = $('#tool-result'); res.innerHTML = '

❌ Upload Failed

The file may be too large. Maximum upload size is 500MB for videos. If upload fails, try a smaller file and 20MB for images. Try compressing or trimming your file.

'; btn.disabled = false; btn.textContent = tool.icon + ' Process · ' + tool.credits + ' credits'; return; } if(handlePaymentRequired(e, ()=>submitTool(toolKey))){ btn.disabled=false; btn.textContent = `${tool.icon} Process · ${tool.credits} credits`; return; } // Handle wrong_tool API error — guide user to correct tool if(e.data && e.data.detail && typeof e.data.detail === 'object' && e.data.detail.error === 'wrong_tool'){ const sug = e.data.detail.suggested_tool || 'remove-text'; res.innerHTML = `

⚠️

Wrong Tool Selected

${esc(e.data.detail.message)}

`; btn.disabled = false; btn.textContent = `${tool.icon} Process · ${tool.credits} credits`; return; } const isCredits = e.message.toLowerCase().includes('insufficient') || e.message.includes('402'); res.innerHTML = isCredits ? `

❌ Not enough credits

This operation requires ${tool.credits} credits. You have ${Math.max(0,state.credits)} remaining.

` : `

❌ ${esc(e.message)}

`; btn.disabled = false; btn.textContent = `${tool.icon} Process · ${tool.credits} credits`; } } function pollToolResult(gid, tool, btn){ let elapsed = 0; const iv = setInterval(async()=>{ elapsed += 2; if(elapsed > 900){ clearInterval(iv); const res=$("#tool-result"); res.innerHTML=`

⏱ Request timed out after 15 minutes. Please try again.

`; btn.disabled=false; btn.textContent=tool.icon+" Process · "+tool.credits+" credits"; return; } const pct = Math.min(95, Math.round((elapsed/tool.est)*100)); const pb = $('#tool-progress'); if(pb) pb.style.width = pct+'%'; try{ const pollEndpoint = tool.pollEndpoint ? `${tool.pollEndpoint}${gid}` : (tool.endpoint.includes('video') ? `/v1/video/${gid}` : `/v1/image/${gid}`); const d = await api('GET', pollEndpoint); if(d.status === 'completed'){ clearInterval(iv); const res = $('#tool-result'); if(tool.is3D && d.output_url){ res.innerHTML = '
' + '' + '
' + '⬇ Download GLB' + '' + '
'; } else if(tool.showBeforeAfter && d.output_url && state.uploadedFile){ // Side-by-side before/after (opt-in per tool via showBeforeAfter flag) const beforeUrl = URL.createObjectURL(state.uploadedFile); res.innerHTML = '
' + '
' + '
Before
' + '' + '
' + '
' + '
After
' + '' + '
' + '
' + '

\u2b07 Download ' + '

'; } else { res.innerHTML = getMediaTag(d.output_url) + '

\u2b07 Download ' + '

'; } btn.disabled = false; btn.textContent = tool.icon + ' Process \u00b7 ' + tool.credits + ' credits'; }else if(d.status==='failed'||d.status==='blocked'){ clearInterval(iv); const res=$('#tool-result'); res.innerHTML=`

❌ ${d.status}: ${esc(d.error_message||'Processing failed')}

`; btn.disabled=false; btn.textContent=`${tool.icon} Process · ${tool.credits} credits`; } }catch(e){ clearInterval(iv); toast(e.message,'error'); btn.disabled=false; btn.textContent=`${tool.icon} Process · ${tool.credits} credits`; } }, 5000); } /* ── Auto Color Variants (color-variant grid for replace-bg) ── */ window._selectedVariantUrl = null; function selectVariant(el, url){ document.querySelectorAll('.variant-card').forEach(c=>c.style.borderColor='var(--border)'); el.style.borderColor='var(--purple)'; el.style.borderWidth='3px'; window._selectedVariantUrl = url; const dl = document.getElementById('variant-dl'); if(dl) dl.href = url; } async function generateColorVariants(outputUrl){ var presets = ['golden-hour', 'cinematic-moody', 'teal-orange']; var labels = ['\ud83c\udf05 Warm Golden', '\ud83c\udfac Cinematic', '\ud83c\udfa8 Teal-Orange']; var grid = document.getElementById('variant-grid'); if(!grid) return; var cards = grid.querySelectorAll('.variant-card'); var blob; try { var resp = await fetch(outputUrl); blob = await resp.blob(); } catch(e){ console.error('Failed to fetch output for variants', e); return; } for(var i=0; i
' + label + '
'; card.style.cursor='pointer'; (function(c, u){ c.onclick = function(){ selectVariant(c, u); }; })(card, rurl); } else { card.innerHTML = '
Failed
'; } } catch(e){ card.innerHTML = '
' + (e.message||'Error') + '
'; } } } /* ── AI Image Generator ── */ function renderSmartGenerate(){ const m=$('#main-content'); m.innerHTML=`

🧠 AI Image Generator

AI-powered content generation. Automatically detects if you want an infographic, chart, diagram, logo, or image.

Smart Pipeline: Infographics, charts & diagrams = instant CPU render (3-5 credits). Logos & images = enhanced prompt + image-generation engine (2 credits).

Recent Generations

`; loadRecent(); setTimeout(checkSmartGenerateReconnect, 500); } async function smartGenerate(){ const prompt=$('#smart-prompt')?.value.trim(); if(!prompt)return toast('Enter a prompt','error'); const width=parseInt($('#smart-w').value)||1024, height=parseInt($('#smart-h').value)||1024; const btn=$('#smart-btn');btn.disabled=true;btn.textContent='Generating...'; const res=$('#smart-result'); res.innerHTML='

Analyzing intent...

'; try{ const d=await api('POST','/v1/image/smart-generate',{prompt,width,height}); const intent=d.intent||'smart-standard'; const credits=d.credits_used||2; // Update credits state.credits-=credits; const dc=$('#dash-credits');if(dc)dc.textContent=Math.max(0,state.credits); localStorage.setItem('credits',state.credits); if(d.status==='completed'&&d.output_url){ // Instant render (infographic/chart/diagram) const _u=absUrl(d.output_url); res.innerHTML=`Generated

⬇ Download Image

✅ ${esc(intent)} · ${credits} credits · Instant CPU render

`; btn.disabled=false;btn.textContent='Generate 🧠'; loadRecent(); }else if(d.status==='queued'&&d.generation_id){ // Save job for reconnection after page reload localStorage.setItem('pixelapi_smart_job', JSON.stringify({gid: d.generation_id, prompt, ts: Date.now()})); // Queued to image engine res.innerHTML=`

Queued to AI workers... ~10-15s

🎨 ${esc(intent)} · ${credits} credits · Enhanced prompt queued

`; pollSmartGeneration(d.generation_id,15); }else{ throw new Error('Unexpected response status'); } }catch(e){ if(handlePaymentRequired(e, ()=>smartGenerate())){ btn.disabled=false;btn.textContent='Generate 🧠'; return; } const isCredits = e.message.toLowerCase().includes('insufficient') || e.message.includes('402'); if(isCredits){ res.innerHTML=`

❌ Not enough credits

${esc(e.message)}

`; } else { res.innerHTML='';toast(e.message,'error'); } btn.disabled=false;btn.textContent='Generate 🧠'; } } function pollSmartGeneration(gid,est){ let elapsed=0; const iv=setInterval(async()=>{ if(elapsed>300){clearInterval(iv);toast("Request timed out. Check Gallery for results.","error");const b=$("#smart-btn");if(b){b.disabled=false;b.textContent="Generate 🧠"};return} elapsed+=2; const pct=Math.min(95,Math.round((elapsed/est)*100)); const pb=$('#smart-progress');if(pb)pb.style.width=pct+'%'; try{ const d=await api('GET',`/v1/image/${gid}`); if(d.status==='completed'){ clearInterval(iv); localStorage.removeItem('pixelapi_smart_job'); // Clear saved job const res=$('#smart-result'); const _u2=absUrl(d.output_url); res.innerHTML=`Generated

⬇ Download Image

⏰ Images stored for 24 hours. Please download to keep a copy.

`; const btn=$('#smart-btn');if(btn){btn.disabled=false;btn.textContent='Generate 🧠'} loadRecent(); }else if(d.status==='failed'||d.status==='blocked'){ clearInterval(iv); localStorage.removeItem('pixelapi_smart_job'); // Clear saved job const res=$('#smart-result');res.innerHTML=`

❌ Generation failed: ${esc(d.error_message||'Unknown error')}

`; const btn=$('#smart-btn');if(btn){btn.disabled=false;btn.textContent='Generate 🧠'} } }catch(e){clearInterval(iv);toast(e.message,'error');const btn=$('#smart-btn');if(btn){btn.disabled=false;btn.textContent='Generate 🧠'}} },2000); } function checkSmartGenerateReconnect(){ // Reconnect to any in-progress smart generate job after page reload const saved = localStorage.getItem('pixelapi_smart_job'); if(!saved) return; try{ const {gid, prompt, ts} = JSON.parse(saved); const age = (Date.now() - ts) / 1000; if(age > 1800){ localStorage.removeItem('pixelapi_smart_job'); return; } // 30 min max // Check if we're on the smart-generate page const smartPrompt = $('#smart-prompt'); if(!smartPrompt) return; // Not on smart generate page // Show status $('#smart-result').innerHTML = '

Reconnecting to job...

'; if(prompt) smartPrompt.value = prompt; $('#smart-btn').disabled = true; $('#smart-btn').textContent = 'Generating...'; toast('🔄 Reconnected to your smart generation job — still processing...','info'); pollSmartGeneration(gid, 15); }catch(e){ localStorage.removeItem('pixelapi_smart_job'); } } /* ── Generate (text-to-image) ── */ /* ── Thumbnail Generator ── */ async function renderTextTo3D(){ const m=$('#main-content'); m.innerHTML=`

🧊 AI 3D Generator

Turn a text prompt into a textured 3D model (GLB) in ~75 seconds. 15 credits ($0.015) per model.

Text-to-3D

Examples

🏺
Blue Vase
ceramic, wooden table
🦆
Rubber Duck
bath, toy, soft light
🗿
Buddha Statue
gold, marble, dramatic
Format: GLB (binary glTF) · Compatibility: Blender, Unity, Unreal Engine, Three.js
Pipeline: fast image generation (10-step image) + AI 3D reconstruction (mesh extraction) · Speed: ~75s
Pricing: 15 credits = $0.015 · lower-priced than Meshy.ai or Sketchfab AI
`; } function fillT3DPrompt(text){ $('#t3d-prompt').value=text; } async function generateTextTo3D(){ const prompt=($('#t3d-prompt')?.value||'').trim(); if(!prompt){toast('Enter a prompt','error');return} const resolution=$('#t3d-res')?.value||'128'; const btn=$('#t3d-btn'); btn.disabled=true;btn.textContent='⏳ Generating...'; const res=$('#t3d-result'); res.innerHTML='

Building yAI 3D generation (~75 seconds)...

'; try{ const formData=new FormData(); formData.append('prompt',prompt); formData.append('resolution',resolution); const d=await api('POST','/v1/3d/text-generate',Object.fromEntries(formData),true); const gid=d.generation_id||d.id||d.job_id; const est=d.estimated_seconds||75; res.innerHTML='

Generating 3D model (~'+est+'s)...

'; state.credits-=(d.credits_used||15); const dc=$('#dash-credits');if(dc)dc.textContent=Math.max(0,state.credits); localStorage.setItem('credits',state.credits); pollText3D(gid,est); }catch(e){ if(handlePaymentRequired(e,()=>generateTextTo3D())){btn.disabled=false;btn.textContent='🧊 Generate · 15 credits';return} const isCredits=e.message.toLowerCase().includes('insufficient')||e.message.includes('402'); if(isCredits){ res.innerHTML='

❌ Not enough credits

'; }else{res.innerHTML='';toast(e.message,'error');} btn.disabled=false;btn.textContent='🧊 Generate · 15 credits'; } } function pollText3D(gid,est){ let elapsed=0; const iv=setInterval(async()=>{ if(elapsed>300){clearInterval(iv);toast("Timed out. Check Gallery.","error");const b=$("#t3d-btn");if(b){b.disabled=false;b.textContent='🧊 Generate · 15 credits'};return} elapsed+=2; const pct=Math.min(95,Math.round((elapsed/est)*100)); const pb=$('#t3d-progress');if(pb)pb.style.width=pct+'%'; try{ const d=await api('GET','/v1/3d/text-status/'+gid); if(d.status==='completed'){ clearInterval(iv); const res=$('#t3d-result'); res.innerHTML='
' + '

Generated image

' + '
⬇ Download GLB
'; const btn=$('#t3d-btn');if(btn){btn.disabled=false;btn.textContent='🧊 Generate · 15 credits'} }else if(d.status==='failed'||d.status==='blocked'){ clearInterval(iv); const res=$('#t3d-result');res.innerHTML='

❌ Failed: '+esc(d.error_message||'Unknown')+'

'; const btn=$('#t3d-btn');if(btn){btn.disabled=false;btn.textContent='🧊 Generate · 15 credits'} } }catch(e){} },2000); } /* ── Image Generation ── */ function renderGenerate(){ const m=$('#main-content'); m.innerHTML=`

🖼️ Generate Image

Text to Image

Recent Generations

`; loadRecent(); } async function generate(){ const prompt=$('#gen-prompt')?.value.trim();if(!prompt)return toast('Enter a prompt','error'); const model=$('#gen-model').value, width=parseInt($('#gen-w').value)||1024, height=parseInt($('#gen-h').value)||1024; const btn=$('#gen-btn');btn.disabled=true;btn.textContent='Generating...'; const res=$('#gen-result'); res.innerHTML='

Starting generation...

'; try{ const d=await api('POST','/v1/image/generate',{prompt,model,width,height}); const gid=d.generation_id||d.id; const est=d.estimated_seconds||10; res.innerHTML=`

Generating... ~${est}s

`; state.credits-=(d.credits_used||3); const dc=$('#dash-credits');if(dc)dc.textContent=Math.max(0,state.credits); localStorage.setItem('credits',state.credits); pollGeneration(gid,est); }catch(e){ if(handlePaymentRequired(e, ()=>generate())){ btn.disabled=false;btn.textContent='Generate ✨'; return; } const isCredits = e.message.toLowerCase().includes('insufficient') || e.message.includes('402'); if(isCredits){ res.innerHTML=`

❌ Not enough credits

${esc(e.message)}

`; } else { res.innerHTML='';toast(e.message,'error'); } btn.disabled=false;btn.textContent='Generate ✨'; } } function pollGeneration(gid,est){ let elapsed=0; const iv=setInterval(async()=>{ if(elapsed>300){clearInterval(iv);toast("Request timed out after 5 minutes. Check Gallery for results.","error");const b=$("#gen-btn");if(b){b.disabled=false;b.textContent="Generate ✨"};return} elapsed+=2; const pct=Math.min(95,Math.round((elapsed/est)*100)); const pb=$('#gen-progress');if(pb)pb.style.width=pct+'%'; try{ const d=await api('GET',`/v1/image/${gid}`); if(d.status==='completed'){ clearInterval(iv); const res=$('#gen-result'); res.innerHTML=`Generated

⬇ Download Image

⏰ Images are stored for 24 hours. Please download to keep a copy.

`; const btn=$('#gen-btn');if(btn){btn.disabled=false;btn.textContent='Generate ✨'} loadRecent(); }else if(d.status==='failed'||d.status==='blocked'){ clearInterval(iv); const res=$('#gen-result');res.innerHTML=`

❌ Generation failed: ${esc(d.error_message||'Unknown error')}

`; const btn=$('#gen-btn');if(btn){btn.disabled=false;btn.textContent='Generate ✨'} } }catch(e){clearInterval(iv);toast(e.message,'error');const btn=$('#gen-btn');if(btn){btn.disabled=false;btn.textContent='Generate ✨'}} },2000); } /* ── Gallery ── */ let gallerySelectMode=false, gallerySelected=new Set(); async function renderGallery(){ const m=$('#main-content'); gallerySelected=new Set(); m.innerHTML='

📸 Gallery

'; try{ const d=await api('GET','/v1/account/usage'); const items=(d.usage||d.generations||d||[]).filter(it=>it.status==='completed'&&it.output_url); state.generations=d.usage||d.generations||d||[]; if(!items.length){m.querySelector('.loading-center').innerHTML='

No images yet. Try a tool from the Dashboard!

';return} const bar=`
${gallerySelectMode?``:''}
`; m.querySelector('.loading-center').outerHTML=bar+`
${items.map((it,i)=>{ const idx=state.generations.indexOf(it); const gid=esc(it.id||it.generation_id||''); const click=gallerySelectMode?`galleryToggleItem('${gid}',this)`:`showGenDetail(${idx})`; return`
${gallerySelectMode?`
`:``} ${getMediaTag(it.output_url, true)}
${esc(it.prompt||it.operation||'')}
${esc(it.operation||it.model||'')}${fmtDate(it.created_at||it.date)}
`; }).join('')}
`; }catch(e){m.querySelector('.loading-center').innerHTML=`

Sign in to view your recent images.

`} } function toggleGallerySelect(){gallerySelectMode=!gallerySelectMode;renderGallery()} function galleryToggleItem(gid,el){ if(gallerySelected.has(gid)){gallerySelected.delete(gid);el.querySelector('.gal-check').textContent='';el.style.outline='';} else{gallerySelected.add(gid);el.querySelector('.gal-check').textContent='✓';el.style.outline='2px solid var(--accent, #7c6cf0)';} const b=$('#bulk-del-btn'); if(b){b.disabled=gallerySelected.size===0;b.textContent=`🗑 Delete selected (${gallerySelected.size})`;} } async function galleryDeleteOne(gid,btn){ if(!confirm('Delete this image permanently? This cannot be undone.'))return; try{ await api('DELETE','/v1/account/generations/'+gid); const card=btn.closest('.recent-card'); if(card)card.remove(); toast('Image deleted','success'); }catch(e){toast('Delete failed: '+(e.message||e),'error')} } async function galleryDeleteSelected(){ if(!gallerySelected.size)return; if(!confirm(`Delete ${gallerySelected.size} image(s) permanently? This cannot be undone.`))return; try{ const r=await api('POST','/v1/account/generations/delete-bulk',{ids:[...gallerySelected]}); toast(`Deleted ${r.deleted} of ${r.requested}`,'success'); gallerySelectMode=false; renderGallery(); }catch(e){toast('Bulk delete failed: '+(e.message||e),'error')} } /* ── API Keys ── */ function renderApiKeys(){ const masked=state.api_key?state.api_key.slice(0,8)+'•'.repeat(24)+state.api_key.slice(-4):''; $('#main-content').innerHTML=`

🔑 API Keys

Your API Key

${esc(masked)}

Rate Limits — ${esc(state.plan)} Plan

LimitValue
Requests/minute30
Concurrent jobs5
Max upload size10MB

Code Examples

Remove Background

curl -X POST https://api.pixelapi.dev/v1/image/remove-background \\ -H "Authorization: Bearer ${esc(state.api_key||'YOUR_KEY')}" \\ -F "image=@product.jpg"

Python SDK

from pixelapi import PixelAPI client = PixelAPI("${esc(state.api_key||'YOUR_KEY')}") # Remove background result = client.remove_background("product.jpg") print(result.url) # Replace background result = client.replace_background( "product.jpg", prompt="marble table, studio lighting" ) print(result.url)

Generate Image

curl -X POST https://api.pixelapi.dev/v1/image/generate \\ -H "Authorization: Bearer ${esc(state.api_key||'YOUR_KEY')}" \\ -H "Content-Type: application/json" \\ -d '{"prompt":"product photo of sneakers, studio lighting"}'
`; } let keyRevealed=false; function revealKey(){ keyRevealed=!keyRevealed; $('#api-key-display').textContent=keyRevealed?state.api_key:(state.api_key.slice(0,8)+'•'.repeat(24)+state.api_key.slice(-4)); $('#key-toggle').textContent=keyRevealed?'🙈 Hide':'👁 Show'; } function copyKey(){navigator.clipboard.writeText(state.api_key).then(()=>toast('API key copied!','success')).catch(()=>toast('Failed to copy','error'))} /* ── Usage ── */ async function renderUsage(){ const m=$('#main-content'); m.innerHTML='

📊 Usage & Analytics

'; try{ const [d, wh] = await Promise.all([ api('GET','/v1/account/analytics?days=30'), api('GET','/v1/account/webhook').catch(()=>null), ]); const s=d.summary||{}; const pct=Math.round((state.credits/(state.credits+s.credits_spent+1))*100); const barData=d.daily||[]; const maxJobs=Math.max(1,...barData.map(x=>x.total)); m.innerHTML=`

📊 Usage & Analytics

💎
${state.credits.toLocaleString()}
Credits Remaining
🔥
${(s.credits_spent||0).toLocaleString()}
Credits Used (30d)
🖼️
${(s.total_jobs||0).toLocaleString()}
Total Jobs (30d)
${s.success_rate||0}%
Success Rate
${Math.round((s.avg_processing_ms||0)/1000)}s
Avg Processing

Credit Balance

${state.credits.toLocaleString()} remaining${(s.credits_spent||0).toLocaleString()} used this month
${barData.length ? `

Daily Activity (30 days)

${barData.map(r=>{ const h=Math.round((r.total/maxJobs)*72); const color=r.failed>0?'var(--yellow)':'var(--purple)'; return `
`; }).join('')}
${barData[0]?.date||''}${barData[barData.length-1]?.date||''}
` : ''} ${d.top_models?.length ? `

Top Models Used

${d.top_models.map(r=>``).join('')}
ModelUsesSuccessCredits Spent
${esc(r.model)} ${r.uses} ${Math.round(r.succeeded/Math.max(r.uses,1)*100)}% ${r.credits_spent}
` : ''}

🔔 Webhook

Configure →
${wh?.webhook_url ? `

✅ Active: ${esc(wh.webhook_url)}

Signature secret: ${wh.has_secret ? '✅ set' : '⚠️ not set'}

` : `

No webhook configured. Set one to receive push notifications when jobs complete.

` }
`; }catch(e){m.querySelector('.loading-center').innerHTML=`

Sign in to view your recent images.usage data.

`} } async function showWebhookConfig(){ const wh=await api('GET','/v1/account/webhook').catch(()=>null); const url=wh?.webhook_url||''; const inp=prompt('Enter HTTPS webhook URL (blank to remove):',url); if(inp===null) return; try{ const r=await api('POST','/v1/account/webhook',{webhook_url:inp||null,regenerate_secret:!wh?.has_secret}); if(r.webhook_secret) alert('Webhook secret (save this — shown once):\n\n'+r.webhook_secret); else alert(r.message||'Webhook updated'); renderView('usage'); }catch(e){alert('Error: '+(e.message||JSON.stringify(e)))} } /* ── Pricing / Upgrade ── */ let _detectedCountry=null; function getUserCurrency(){ try{ // If Cloudflare detected a country, use ONLY that (IP-based = most reliable) if(_detectedCountry){ if(_detectedCountry==='IN') return 'INR'; if(_detectedCountry==='GB') return 'GBP'; if(['DE','FR','ES','IT','NL','PT','AT','BE','IE','FI','GR','LU','MT','SK','SI','EE','LV','LT','CY','HR'].includes(_detectedCountry)) return 'EUR'; return 'USD'; } // Fallback ONLY if Cloudflare trace failed/timed out: timezone + language const tz=Intl.DateTimeFormat().resolvedOptions().timeZone||''; const lang=navigator.language||''; if(tz.startsWith('Asia/Kolkata')||tz.startsWith('Asia/Calcutta')||lang.startsWith('hi')||lang==='en-IN') return 'INR'; if(tz.startsWith('Europe/')||lang.startsWith('de')||lang.startsWith('fr')||lang.startsWith('es')||lang.startsWith('it')||lang.startsWith('nl')||lang.startsWith('pt')) return 'EUR'; if(tz.startsWith('Europe/London')||lang==='en-GB') return 'GBP'; }catch(e){} return 'USD'; } // Detect country from Cloudflare header on page load (with timeout) async function detectCountry(){ try{ const controller=new AbortController(); const timer=setTimeout(()=>controller.abort(),2000); const r=await fetch('/cdn-cgi/trace',{signal:controller.signal}); clearTimeout(timer); const t=await r.text(); const m=t.match(/loc=(\w+)/); if(m) _detectedCountry=m[1]; }catch(e){/* timeout or network error — fall back to timezone detection */} } // Scale USD is intentionally $349: same 300K-credit external-wrapper tier as ₹29,000 INR; do not map it back to the old underpriced plan. function fmtPrice(usd){ const c=getUserCurrency(); if(c==='INR'){const v=Math.round(usd*85);return usd===0?'₹0':`₹${v.toLocaleString('en-IN')}`} if(c==='EUR'){const v=Math.round(usd*0.92);return usd===0?'€0':`€${v}`} if(c==='GBP'){const v=Math.round(usd*0.79);return usd===0?'£0':`£${v}`} return `$${usd}`; } function renderPricing(){ const cur=getUserCurrency(); const isIndia=cur==='INR'; const plans = isIndia ? [ {key:'free',name:'Free',price:'₹0',period:'/mo',credits:'24h trial · up to 5,000',features:['All tools available','5 requests/min','Community support','24h image storage'],current:state.plan==='free',color:'var(--muted)'}, {key:'starter',name:'Starter',price:'₹500',period:'/mo',credits:'10,000 credits/month',features:['All tools available','20 requests/min','Email support','Priority queue','~10,000 images or 3min video/mo','UPI / Cards / Netbanking'],current:state.plan==='starter',color:'var(--purple)'}, {key:'pro',name:'Pro',price:'₹1,500',period:'/mo',credits:'60,000 credits/month',features:['All tools available','60 requests/min','Priority support','Webhooks','~60,000 images or 20min video/mo','Early access to new tools'],current:state.plan==='pro',color:'var(--green)',popular:true}, {key:'scale',name:'Scale',price:'₹29,000',period:'/mo',credits:'300,000 credits/month',features:['⚡ Try-on fast lane — ~10–15s, no queue','250 credits/image for fast-lane try-on','All tools available','200 requests/min','Dedicated support','Custom models'],current:state.plan==='scale',color:'var(--cyan)'}, ] : [ {key:'free',name:'Free',price:fmtPrice(0),period:'/mo',credits:'24h trial · up to 5,000',features:['All tools available','5 requests/min','Community support','24h image storage'],current:state.plan==='free',color:'var(--muted)'}, {key:'starter',name:'Starter',price:fmtPrice(10),period:'/mo',credits:'10,000 credits/month',features:['All tools available','20 requests/min','Email support','Priority queue','~10,000 images or 3min video/mo'],current:state.plan==='starter',color:'var(--purple)'}, {key:'pro',name:'Pro',price:fmtPrice(50),period:'/mo',credits:'60,000 credits/month',features:['All tools available','60 requests/min','Priority support','Webhooks','~60,000 images or 20min video/mo','Early access to new tools'],current:state.plan==='pro',color:'var(--green)',popular:true}, {key:'scale',name:'Scale',price:fmtPrice(349),period:'/mo',credits:'300,000 credits/month',features:['⚡ Try-on fast lane — ~10–15s, no queue','250 credits/image for fast-lane try-on','All tools available','200 requests/min','Dedicated support','Custom models'],current:state.plan==='scale',color:'var(--cyan)'}, ]; const topups = [ {key:'pack_200',price:'₹200',credits:'2,000',bonus:''}, {key:'pack_500',price:'₹500',credits:'5,000',bonus:'Try first',highlight:true}, {key:'pack_1000',price:'₹1,000',credits:'12,000',bonus:'20% bonus'}, {key:'pack_5000',price:'₹5,000',credits:'75,000',bonus:'50% bonus'}, ]; const topupsUSD = [ {key:'usd_1k',price:'$1',credits:'1,000',bonus:'Try first',highlight:true}, {key:'usd_5k',price:'$5',credits:'5,000',bonus:''}, {key:'usd_10k',price:'$10',credits:'10,000',bonus:''}, {key:'usd_50k',price:'$50',credits:'50,000',bonus:''}, ]; const showCancel = state.plan && state.plan !== 'free'; const payBtn = isIndia ? 'Pay with Razorpay' : 'Pay with Card'; $('#main-content').innerHTML=`

💎 Upgrade Your Plan

Your current plan: ${esc(state.plan)} · Credits remaining: ${state.credits}

${!isIndia&&cur!=='USD'?`

💱 Prices shown in ${cur} are approximate. Billing is in USD; choose card via Razorpay or PayPal.

`:''} ${isIndia?'

🇮🇳 Indian pricing — pay via UPI, cards, or netbanking through Razorpay

':'

🌍 International checkout — choose credit/debit card via Razorpay or PayPal.

'}
${plans.map(p=>`
${p.popular?'
BEST VALUE
':''} ${p.current?'
CURRENT
':''}

${p.name}

${p.price}${p.period}

${p.credits}

    ${p.features.map(f=>`
  • ✓ ${f}
  • `).join('')}
${p.current ?'' :p.key==='free' ?'' :isIndia ?`` :`
` }
`).join('')}
${true?`

💰 Pay-as-you-Go Credit Packs

Buy credits on demand — perfect for businesses with variable usage.

${!isIndia?'

Important: Credit packs add paid credits only — they do not activate a plan. Without an active subscription (minimum Starter at $10/mo) your credits stay locked: after the 24h trial you cannot use the API until you subscribe. If you are on the Free plan, subscribe first, then top up. To activate a plan choose Starter $10, Pro $50, or Scale $349.

':'

Important: Credit packs add paid credits only — they do not activate a plan or unlock API access. You must have an active subscription (minimum Starter at ₹500/mo) to use credits via API. If you are on the Free plan, subscribe first, then top up. Payments are non-refundable.

'}
${(isIndia?topups:topupsUSD).map(t=>`
${t.highlight?'
⭐ MOST POPULAR
':''}
${t.price}

${t.credits} credits

${t.bonus?`

🎁 ${t.bonus}

`:''}
`).join('')}
`:''}

Credit Costs per Operation

OperationCreditsCost ${isIndia?'(Starter ₹500)':'(Starter)'}
🎬 AI Video 480p (per sec)17${isIndia?'₹1.43':'$0.017'}
🎬 AI Video 720p (per sec)25${isIndia?'₹2.10':'$0.025'}
✂️ Background Removal1${isIndia?'₹0.08':'$0.001'}
🎨 Background Replace3${isIndia?'₹0.25':'$0.003'}
🖼️ Image Generation1${isIndia?'₹0.08':'$0.001'}
🔍 4x Upscaling2${isIndia?'₹0.17':'$0.002'}
👤 Face Restoration5${isIndia?'₹0.42':'$0.00105'}
🧹 Object Removal5${isIndia?'₹0.42':'$0.00105'}
🎵 AI Music5${isIndia?'₹0.42':'$0.00105'}
${showCancel?`

Want to cancel your subscription?

You keep your remaining credits after cancellation.

`:''}

Need a custom plan?

For high-volume usage, custom models, or enterprise features — contact us.

Contact support@pixelapi.dev
`; const up=new URLSearchParams(window.location.search); if(up.get('payment')==='success'){ // Google Ads purchase conversion (PayPal/PayU redirect flow fired nothing before 2026-07-27) try{ if(window.gtag){ gtag('event','conversion',{ send_to:'AW-17925044948/NgEACP626PEbENT1qeNC', transaction_id:(up.get('product')||up.get('plan')||'plan')+'_'+Date.now() }); } }catch(e){} const paidProduct=up.get('product')||up.get('plan')||''; const planMap={starter:'starter',pro:'pro',scale:'scale',usd_10k:'starter',usd_50k:'pro'}; const effectivePlan=planMap[paidProduct]||''; const topupOnly=/^usd_/.test(paidProduct)&&!effectivePlan; if(topupOnly){ showPaymentMsg('🎉 Payment successful! Your credits have been added or will appear shortly. This was a credit top-up only — your plan remains unchanged, and Starter/Pro/Scale validity is not activated. Starter requires the listed $10 plan amount.','var(--green)'); history.replaceState(null,'','/app/'); setTimeout(()=>{loadAuth();refreshBalance();location.hash='dashboard';},1200); }else{ const planName=effectivePlan||paidProduct||'plan'; showPaymentMsg('🎉 Payment successful! Your '+planName+' plan is now active.','var(--green)'); history.replaceState(null,'','/app/'); setTimeout(()=>showBillingModal(planName, ()=>{ location.hash='dashboard'; refreshBalance(); }), 600); } } if(up.get('payment')==='cancelled'){ showPaymentMsg('Payment was cancelled. No charges were made.','var(--muted)'); history.replaceState(null,'','/app/#pricing'); } } function showPaymentMsg(text,color){const m=$('#payment-msg');if(m){m.style.display='block';m.style.background=color+'15';m.style.border='1px solid '+color;m.style.color=color;m.textContent=text}} async function subscribePlan(planKey){ const btn=$('#btnpp-'+planKey)||$('#btn-'+planKey); if(btn){btn.disabled=true;btn.textContent='Redirecting to PayPal...'} try{ const d=await api('POST','/v1/payments/create-subscription',{plan:planKey}); if(d.approval_url){ toast('Redirecting to PayPal...','success'); window.location.href=d.approval_url; } else { throw new Error('No approval URL returned'); } }catch(e){ showPaymentMsg('Error: '+e.message,'#ef4444'); if(btn){btn.disabled=false;btn.textContent='Pay with PayPal'} } } async function cancelSubscription(){ if(!confirm('Are you sure you want to cancel your subscription? You will keep your current plan until the end of your billing period.'))return; try{ const d=await api('POST','/v1/payments/cancel-subscription'); showPaymentMsg(d.message||'Subscription cancelled. You keep access until the end of your billing period.','var(--purple)'); setTimeout(()=>renderPricing(),1500); }catch(e){showPaymentMsg('Error: '+e.message,'#ef4444')} } /* ── Razorpay (Indian payments) ── */ function topupNeedsSubWarning(productKey){ // Top-up packs add credits but do NOT activate a plan; post-trial free accounts // cannot spend credits until subscribed (server gate trial_ended_subscribe). const isTopup=/^(pack_|usd_(1k|5k)$)/.test(productKey); const hasActivePlan=!!(state.plan&&state.plan!=='free'); if(!isTopup||hasActivePlan) return true; return confirm('⚠️ Important — please read before paying\n\nYou do NOT have an active subscription. This purchase is a CREDIT TOP-UP only:\n\n• It will NOT activate a plan.\n• Your credits will stay LOCKED until you subscribe to a plan (Starter — $10\/mo or ₹500\/mo).\n• credits carry forward only when recharged before validity expires and unlock the moment you subscribe.\n\nRecommended: choose the Starter plan instead of this top-up.\n\nPress OK to pay anyway (credits stay locked until you subscribe), or Cancel to go back.'); } async function razorpayPay(productKey, btnLabel){ if(!topupNeedsSubWarning(productKey)) return; const btn=document.getElementById('btn-'+productKey); if(btn){btn.disabled=true;btn.textContent='Processing...';} try{ let d; try{ d=await api('POST','/v1/razorpay/create-order',{product:productKey}); }catch(err){ const em=String(err&&err.message||''); if(em.indexOf('TOPUP_NO_SUBSCRIPTION')<0) throw err; const ok=await topupConsentFlow('You do not have an active subscription.'); if(!ok){ if(btn){btn.disabled=false;btn.textContent=btnLabel||'Pay with Razorpay';} return; } d=await api('POST','/v1/razorpay/create-order',{product:productKey,topup_ack:true}); } const options={ key:d.razorpay_key_id, amount:d.amount, currency:d.currency, name:d.name, description:d.description, order_id:d.order_id, prefill:{email:d.prefill_email||state.email}, theme:{color:'#635bff'}, handler:async function(response){ try{ const v=await api('POST','/v1/razorpay/verify-payment',{ razorpay_order_id:response.razorpay_order_id, razorpay_payment_id:response.razorpay_payment_id, razorpay_signature:response.razorpay_signature, product:productKey, }); // Google Ads + GA4 purchase conversion try{ if(window.gtag){ const inrAmount = parseFloat(d.amount||0)/100; gtag('event','purchase',{ transaction_id: response.razorpay_payment_id, value: inrAmount, currency: 'INR', items: [{item_id: productKey, item_name: v.plan||productKey, price: inrAmount, quantity: 1}] }); gtag('event','conversion',{ send_to:'AW-17925044948/NgEACP626PEbENT1qeNC', value: inrAmount, currency: 'INR', transaction_id: response.razorpay_payment_id }); }}catch(e){} const msg=v.plan ?`🎉 ${v.plan.charAt(0).toUpperCase()+v.plan.slice(1)} plan activated! ${v.credits_added.toLocaleString()} credits added.` :`🎉 ${v.credits_added.toLocaleString()} credits added to your account!`; showPaymentMsg(msg,'var(--green)'); setTimeout(()=>{loadAuth();renderPricing()},2000); }catch(e){showPaymentMsg('Verification failed: '+e.message+'. Contact support@pixelapi.dev','#ef4444')} }, modal:{ondismiss:function(){if(btn){btn.disabled=false;btn.textContent=btnLabel||'Pay with Razorpay';}}}, }; const rzp=new Razorpay(options); rzp.open(); }catch(e){ showPaymentMsg('Error: '+e.message,'#ef4444'); if(btn){btn.disabled=false;btn.textContent=btnLabel||'Pay with Razorpay';} } } /* Ask for a mobile number once — Razorpay requires it on international payment links (2026-08-07). Stored server-side so we never ask twice. */ function askPhoneForCheckout(){ return new Promise(res=>{ window.__phoneGate=res; openModal('
' +'

One quick detail

' +'

Our card processor needs a mobile number to complete an international payment. We only use it for this transaction and your receipt.

' +'' +'
' +'
' +'' +'' +'
'); setTimeout(()=>{ const i=document.getElementById('checkoutPhone'); if(i){i.focus(); i.addEventListener('keydown',e=>{if(e.key==='Enter')submitCheckoutPhone();});} const cb=document.querySelector('#modal .modal-close'); if(cb)cb.onclick=()=>{closeModal();window.__phoneGate(null);}; },0); }); } function submitCheckoutPhone(){ const i=document.getElementById('checkoutPhone'); const e=document.getElementById('checkoutPhoneErr'); const v=(i&&i.value||'').trim(); const digits=v.replace(/[^0-9]/g,''); if(digits.length<8||digits.length>15||new Set(digits).size<=2){ if(e)e.textContent='Please enter a valid mobile number with country code.'; return; } closeModal(); window.__phoneGate(v); } /* Top-up consent (Om 2026-08-07): credits bought without an active plan are LOCKED until the user subscribes, so we must get an explicit 'I agree' before taking money. Checkbox — not just a button — and the confirm stays disabled until it is ticked. The server enforces the same rule (TOPUP_NO_SUBSCRIPTION), this is the humane front end for it. */ function askTopupConsent(msg){ return new Promise(res=>{ window.__topupGate=res; openModal('
' +'

Before you pay — please read

' +'

'+(msg||'')+'

' +'

' +'These credits will stay locked until you subscribe to a paid plan, and payments are non-refundable.

' +'' +'
' +'' +'' +'' +'
'); setTimeout(()=>{const cb=document.querySelector('#modal .modal-close'); if(cb)cb.onclick=()=>{closeModal();window.__topupGate(false);};},0); }); } async function topupConsentFlow(msg){ const r=await askTopupConsent(msg); if(r==='subscribe'){ navigate('upgrade'); return false; } return r===true; } /* ── Razorpay (International — USD credit/debit card) ── */ async function razorpayPayUSD(productKey, btnLabel){ if(!topupNeedsSubWarning(productKey)) return; // International cards: use hosted Razorpay Payment Link (avoids inline // Checkout Click-to-Pay/SRC "DPA entity data not found" failures). const btn=document.getElementById('btn-'+productKey); if(btn){btn.disabled=true;btn.textContent='Processing...';} try{ // 2026-08-07: Razorpay now requires a mobile number on international payment // links. Ask once (we store it), then retry — never dead-end the customer. let d; try{ d=await api('POST','/v1/razorpay/create-payment-link-usd',{product:productKey}); }catch(err){ const _em=String(err&&err.message||''); if(_em.indexOf('TOPUP_NO_SUBSCRIPTION')>=0){ const ok=await topupConsentFlow('You do not have an active subscription.'); if(!ok){ if(btn){btn.disabled=false;btn.textContent=btnLabel||'Pay with Card';} return; } try{ d=await api('POST','/v1/razorpay/create-payment-link-usd',{product:productKey,topup_ack:true}); }catch(err2){ if(!String(err2&&err2.message||'').includes('PHONE_REQUIRED')) throw err2; const phone=await askPhoneForCheckout(); if(!phone){ if(btn){btn.disabled=false;btn.textContent=btnLabel||'Pay with Card';} return; } d=await api('POST','/v1/razorpay/create-payment-link-usd',{product:productKey,contact:phone,topup_ack:true}); } } else if(!_em.includes('PHONE_REQUIRED')) throw err; else { const phone=await askPhoneForCheckout(); if(!phone){ if(btn){btn.disabled=false;btn.textContent=btnLabel||'Pay with Card';} return; } try{ d=await api('POST','/v1/razorpay/create-payment-link-usd',{product:productKey,contact:phone}); }catch(err3){ if(String(err3&&err3.message||'').indexOf('TOPUP_NO_SUBSCRIPTION')<0) throw err3; const ok=await topupConsentFlow('You do not have an active subscription.'); if(!ok){ if(btn){btn.disabled=false;btn.textContent=btnLabel||'Pay with Card';} return; } d=await api('POST','/v1/razorpay/create-payment-link-usd',{product:productKey,contact:phone,topup_ack:true}); } } } if(!d||!d.short_url){throw new Error('Could not create payment link');} // GA4 begin_checkout (best-effort) before redirect try{ if(window.gtag){ const usdAmount = parseFloat(d.amount_display||0); gtag('event','begin_checkout',{ value: usdAmount, currency: 'USD', items: [{item_id: productKey, item_name: productKey, price: usdAmount, quantity: 1}] }); }}catch(e){} // Hand off to the hosted Razorpay page. On success Razorpay redirects back // to /app/?payment=success&plan=; crediting is done server-side by // the payment.captured webhook (link notes carry user_id/product). showPaymentMsg('Redirecting to secure Razorpay checkout…','var(--green)'); window.location.assign(d.short_url); }catch(e){ showPaymentMsg('Error: '+e.message,'#ef4444'); if(btn){btn.disabled=false;btn.textContent=btnLabel||'Pay with Card';} } } /* ── Settings ── */ async function renderSettings(){ let subInfo = { subscription_status: 'none', plan_expires_at: null }; try { subInfo = await api('GET', '/v1/payments/subscription-status'); } catch(e) {} let billingInfo = {}; try { billingInfo = await api('GET', '/v1/account/billing'); } catch(e) {} const isActive = subInfo.subscription_status === 'active'; const isCancelled = subInfo.subscription_status === 'cancelled'; const expiresAt = subInfo.plan_expires_at ? new Date(subInfo.plan_expires_at).toLocaleDateString('en-US', {month:'long',day:'numeric',year:'numeric'}) : null; let subSection = ''; if (isActive) { subSection = `

Subscription

Status: Active

Next billing: ${expiresAt || 'N/A'}

Subscription ID: ${esc(subInfo.paypal_subscription_id||'')}

You'll keep your plan until the end of the billing period.

`; } else if (isCancelled) { subSection = `

Subscription

Status: Cancelled

Access until: ${expiresAt || 'end of billing period'}

`; } const settingsCur=getUserCurrency(); const settingsIndia=settingsCur==='INR'; const settingsPrices=settingsIndia ? {starter:'₹500',pro:'₹1,500',scale:'₹29,000'} : {starter:'$10',pro:'$50',scale:'$349'}; const settingsCurrencyNote=settingsIndia ? '

🇮🇳 Razorpay INR pricing. Scale is ₹29,000/mo. Credit packs are separate top-ups.

' : ''; $('#main-content').innerHTML=`

⚙️ Settings

Account

Email: ${esc(state.email)}

Plan: ${esc(state.plan)}

Credits: ${Math.floor(state.credits)}

${subSection}

Upgrade Plan

${settingsCurrencyNote}

Starter

${settingsPrices.starter}/mo
10,000 credits

Pro

${settingsPrices.pro}/mo
60,000 credits

Scale

${settingsPrices.scale}/mo
300,000 credits

🧾 Billing Address

Required for GST/tax-compliant invoices on all your payments.

`; } /* ── Billing Modal (post-payment) ── */ function showBillingModal(planName, onComplete){ // Don't show if already shown this session if(sessionStorage.getItem('billing_modal_shown')) { if(onComplete) onComplete(); return; } sessionStorage.setItem('billing_modal_shown','1'); const el = document.createElement('div'); el.className = 'billing-modal-overlay'; el.id = 'bm-overlay'; el.innerHTML = `

🧾 Almost done!

Your ${planName||'plan'} is active ✅
Fill your billing address below — we'll generate your GST invoice instantly.

You can update this anytime in ⚙️ Settings → Billing Address.

`; document.body.appendChild(el); window._bmCallback = onComplete; // Pre-fill name if available setTimeout(()=>{ const n=document.getElementById('bm-name'); if(n&&state.name) n.value=state.name; },100); } async function submitBillingModal(){ const btn = document.getElementById('bm-save-btn'); if(btn){ btn.disabled=true; btn.textContent='Saving...'; } const fields = {}; const map = {billing_name:'bm-name',billing_address:'bm-addr',billing_city:'bm-city', billing_pincode:'bm-pin',billing_state:'bm-state',billing_country:'bm-country',billing_gstin:'bm-gstin'}; for(const [k,id] of Object.entries(map)){ const v=(document.getElementById(id)||{}).value?.trim(); if(v) fields[k]=v; } try{ await api('PUT','/v1/account/billing',fields); // Trigger deferred invoice generation with the now-known address try{ await api('POST','/v1/payments/generate-invoice'); }catch(_){} closeBillingModal(); toast('Billing info saved! Invoice is being generated.','success'); }catch(e){ if(btn){ btn.disabled=false; btn.textContent='Save & Generate Invoice'; } toast('Error: '+e.message,'error'); } } function skipBillingModal(){ // Generate invoice anyway — address can be added later try{ api('POST','/v1/payments/generate-invoice'); }catch(_){} closeBillingModal(); } function closeBillingModal(){ const el=document.getElementById('bm-overlay'); if(el) el.remove(); if(window._bmCallback){ window._bmCallback(); window._bmCallback=null; } } /* ── Billing ── */ async function saveBillingAddress(){ const data={}; const map={billing_name:'b-name',billing_address:'b-address',billing_city:'b-city', billing_pincode:'b-pincode',billing_state:'b-state',billing_country:'b-country',billing_gstin:'b-gstin'}; for(const[k,id] of Object.entries(map)){const v=document.getElementById(id)?.value?.trim();if(v)data[k]=v;} try{ await api('PUT','/v1/account/billing',data); const msg=document.getElementById('billing-msg'); if(msg){msg.textContent='✅ Billing address saved!';msg.style.display='block';setTimeout(()=>msg.style.display='none',3000);} }catch(e){alert('Failed to save: '+e.message);} } /* ── Helpers ── */ function esc(s){if(!s)return'';const d=document.createElement('div');d.textContent=s;return d.innerHTML} function fmtDate(d){if(!d)return'—';try{return new Date(d).toLocaleDateString('en-US',{month:'short',day:'numeric',hour:'2-digit',minute:'2-digit'})}catch{return d}} /* ── Init ── */ window.addEventListener('hashchange',route); // Localize landing page prices function localizeLandingPrices(){ try{ const c=getUserCurrency(); if(c==='USD')return; const inrPrices={0:'₹0',10:'₹500',50:'₹1,500',349:'₹29,000'}; document.querySelectorAll('#landing-pricing-grid .price[data-usd]').forEach(el=>{ const usd=parseInt(el.dataset.usd); el.innerHTML=(c==='INR'&&inrPrices[usd]?inrPrices[usd]:fmtPrice(usd))+'/mo'; }); const note=document.getElementById('landing-currency-note'); if(note){ note.style.display='block'; note.textContent=c==='INR' ?'🇮🇳 Indian pricing — pay via UPI, cards, or netbanking through Razorpay' :'💱 Prices shown in '+c+' are approximate. Billing is in USD; choose card via Razorpay or PayPal.'; } }catch(e){} } window.addEventListener('load',async()=>{ // Backup: fire signup conversion if prior hop stashed the flag (auth-callback / race) try{ if(localStorage.getItem('pix_pending_signup_conv')==='1' && window.gtag){ localStorage.removeItem('pix_pending_signup_conv'); gtag('event','sign_up',{method:'oauth'}); gtag('event','conversion',{send_to:'AW-17925044948/oTsCCKmZ8accENT1qeNC',value:1.0,currency:'INR'}); } }catch(e){} try{await detectCountry();}catch(e){} try{localizeLandingPrices();}catch(e){} // Handle OAuth callback via query params const p=new URLSearchParams(window.location.search); if(p.get('token')){ saveAuth({token:p.get('token'),api_key:p.get('api_key'),email:p.get('email'),credits:p.get('credits')}); // Fire GEOmind attribution conversion on new signups if(p.get('is_new')==='1'||p.get('is_new')==='true'){ try{localStorage.setItem('pix_pending_signup_conv','1');}catch(e){} try{ if(window._gmTrackConversion) window._gmTrackConversion(0); }catch(e){} // Google Ads + GA4 signup conversion try{ if(window.gtag){ gtag('event','sign_up',{method:'oauth'}); gtag('event','conversion',{ send_to:'AW-17925044948/oTsCCKmZ8accENT1qeNC', value: 1.0, currency: 'INR' }); }}catch(e){} } history.replaceState(null,'','/app/#dashboard'); } loadAuth(); // Handle ?plan= parameter from landing page if(p.get('plan') && !p.get('payment') && !p.get('token')){ if(isLoggedIn()){ location.hash='pricing'; } else { // Not logged in — save plan, scroll to auth after render localStorage.setItem('pendingPlan', p.get('plan')); setTimeout(()=>{ scrollToAuth(); const ab=document.getElementById('auth-box'); if(ab){ const msg=document.createElement('div'); msg.style.cssText='background:#7c75ff20;border:1px solid #7c75ff;color:#7c75ff;padding:12px;border-radius:8px;margin-bottom:16px;text-align:center'; msg.textContent='Sign in with Google to subscribe to the '+p.get('plan').charAt(0).toUpperCase()+p.get('plan').slice(1)+' plan'; ab.insertBefore(msg,ab.firstChild); } },100); } } // After login, check for pending plan if(isLoggedIn() && localStorage.getItem('pendingPlan')){ localStorage.removeItem('pendingPlan'); location.hash='pricing'; } // Handle ?payment= parameter — user returning from PayPal if(p.get('payment') && isLoggedIn()) location.hash='pricing'; route(); });