Files
opal-ios-studio/app.js
T

785 lines
30 KiB
JavaScript

// Google Opal AI - iOS Studio & 3D Poster Lab Engine
document.addEventListener('DOMContentLoaded', () => {
initNavigation();
initSettings();
initPosterLab();
initInboxAI();
initHistory();
});
// --- 1. Tab Navigation ---
function initNavigation() {
const navItems = document.querySelectorAll('.nav-item');
const tabPages = document.querySelectorAll('.tab-page');
navItems.forEach(item => {
item.addEventListener('click', () => {
const targetTabId = item.getAttribute('data-tab');
navItems.forEach(n => n.classList.remove('active'));
tabPages.forEach(p => p.classList.remove('active'));
item.classList.add('active');
const targetPage = document.getElementById(targetTabId);
if (targetPage) targetPage.classList.add('active');
window.scrollTo({ top: 0, behavior: 'smooth' });
});
});
// Language Toggle
const langBtn = document.getElementById('langToggleBtn');
let currentLang = 'BN';
langBtn.addEventListener('click', () => {
currentLang = currentLang === 'BN' ? 'EN' : 'BN';
langBtn.textContent = currentLang === 'BN' ? '🌐 āĻŦāĻžāĻ‚' : '🌐 EN';
});
}
// Global App Switcher
window.openAppRunner = function(appType) {
if (appType === 'inbox_ai') {
document.querySelector('.nav-item[data-tab="tabInboxAI"]').click();
} else {
document.querySelector('.nav-item[data-tab="tabPosterLab"]').click();
}
};
window.copyText = function(elementId) {
const el = document.getElementById(elementId);
if (el && el.textContent) {
navigator.clipboard.writeText(el.textContent).then(() => {
alert('✅ Copied to Clipboard!');
});
}
};
// --- 2. Settings & Gemini/Gemma API ---
function initSettings() {
const apiKeyInput = document.getElementById('apiKeyInput');
const modelSelect = document.getElementById('modelSelect');
const n8nWebhookInput = document.getElementById('n8nWebhookInput');
const saveBtn = document.getElementById('saveApiKeyBtn');
const statusMsg = document.getElementById('settingsStatusMsg');
// Load saved settings
const savedKey = localStorage.getItem('opal_gemini_api_key') || '';
const savedModel = localStorage.getItem('opal_selected_model') || 'gemini-3.6-flash';
const savedN8n = localStorage.getItem('opal_n8n_webhook') || 'https://n8n.auraajenticai.cloud/webhook/gt-auto-post';
apiKeyInput.value = savedKey;
modelSelect.value = savedModel;
if (n8nWebhookInput) n8nWebhookInput.value = savedN8n;
saveBtn.addEventListener('click', async () => {
const key = apiKeyInput.value.trim();
const model = modelSelect.value;
const n8nUrl = n8nWebhookInput ? n8nWebhookInput.value.trim() : '';
if (n8nUrl) {
localStorage.setItem('opal_n8n_webhook', n8nUrl);
}
if (!key) {
alert('Settings saved!');
return;
}
statusMsg.classList.remove('hidden');
statusMsg.style.color = '#F472B6';
statusMsg.textContent = 'âŗ Testing AI connection with Google...';
try {
const res = await callGeminiApi(key, model, "Respond with: 'Opal AI Connected successfully!'");
if (res) {
localStorage.setItem('opal_gemini_api_key', key);
localStorage.setItem('opal_selected_model', model);
statusMsg.style.color = '#10B981';
statusMsg.textContent = `✅ Settings Saved & Verified: ${res.trim()}`;
}
} catch (e) {
statusMsg.style.color = '#EF4444';
statusMsg.textContent = `❌ Error: ${e.message}`;
}
});
}
async function callGeminiApi(apiKey, model, prompt) {
const url = `https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent?key=${apiKey}`;
const payload = {
contents: [{ parts: [{ text: prompt }] }],
generationConfig: { temperature: 0.7, maxOutputTokens: 2048 }
};
const response = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
if (!response.ok) {
const errData = await response.json();
throw new Error(errData?.error?.message || `HTTP ${response.status}`);
}
const data = await response.json();
return data?.candidates?.[0]?.content?.parts?.[0]?.text || '';
}
// --- 3. 3D Poster Lab Engine (True 3D Dual-Tier Gold Podium) ---
function initPosterLab() {
const productInput = document.getElementById('productInput');
const productPrice = document.getElementById('productPrice');
const photoFileInput = document.getElementById('photoFileInput');
const clearPhotoBtn = document.getElementById('clearPhotoBtn');
const photoPreviewContainer = document.getElementById('photoPreviewContainer');
const attachedPhotoImg = document.getElementById('attachedPhotoImg');
const generatePosterBtn = document.getElementById('generatePosterBtn');
const posterStatusText = document.getElementById('posterStatusText');
const posterResultCard = document.getElementById('posterResultCard');
const posterCanvas = document.getElementById('posterCanvas');
const downloadPosterBtn = document.getElementById('downloadPosterBtn');
const shareWhatsAppBtn = document.getElementById('shareWhatsAppBtn');
let attachedImageBitmap = null;
// Photo Input change
photoFileInput.addEventListener('change', (e) => {
const file = e.target.files[0];
if (file) {
const reader = new FileReader();
reader.onload = (event) => {
attachedPhotoImg.src = event.target.result;
photoPreviewContainer.classList.remove('hidden');
clearPhotoBtn.classList.remove('hidden');
const img = new Image();
img.onload = () => { attachedImageBitmap = img; };
img.src = event.target.result;
};
reader.readAsDataURL(file);
}
});
clearPhotoBtn.addEventListener('click', () => {
photoFileInput.value = '';
attachedImageBitmap = null;
photoPreviewContainer.classList.add('hidden');
clearPhotoBtn.classList.add('hidden');
});
// Generate 3D Poster
generatePosterBtn.addEventListener('click', async () => {
const inputVal = productInput.value.trim();
const priceVal = productPrice.value.trim();
posterStatusText.classList.remove('hidden');
posterStatusText.textContent = 'âŗ Resolving product image & rendering 3D gold stage...';
generatePosterBtn.disabled = true;
try {
let productImg = attachedImageBitmap;
let title = "Authentic Korean K-Beauty Care";
if (!productImg) {
// Universal Product / Image Link Resolver
const resolved = await resolveProductUrl(inputVal);
if (resolved && resolved.imageUrl) {
title = resolved.title || inputVal;
productImg = await loadImage(resolved.imageUrl);
} else {
// Fallback packshot image
const fallbackUrl = `https://image.pollinations.ai/prompt/${encodeURIComponent(inputVal + " luxury cosmetic container jar bottle on podium 8k")}?width=1024&height=1024&nologo=true`;
productImg = await loadImage(fallbackUrl);
title = inputVal;
}
}
if (productImg) {
// Render True 3D Poster on Canvas
await render3DCommercialPoster(posterCanvas, productImg, title, priceVal);
posterResultCard.classList.remove('hidden');
posterStatusText.classList.add('hidden');
posterResultCard.scrollIntoView({ behavior: 'smooth' });
// Save to History
saveGenerationToHistory({
type: '3D Poster',
title: title,
price: priceVal,
date: new Date().toLocaleTimeString()
});
} else {
posterStatusText.textContent = '❌ Failed to load product image. Please check link.';
}
} catch (err) {
posterStatusText.textContent = `❌ Error: ${err.message}`;
}
generatePosterBtn.disabled = false;
});
// Download Button (Save to iPhone Camera Roll)
downloadPosterBtn.addEventListener('click', () => {
const link = document.createElement('a');
link.download = `GT_Poster_${Date.now()}.jpg`;
link.href = posterCanvas.toDataURL('image/jpeg', 0.95);
link.click();
});
// WhatsApp Order Button
shareWhatsAppBtn.addEventListener('click', () => {
const text = encodeURIComponent(`✨ 100% Authentic Korean Skincare Order!\nProduct: ${productInput.value}\nPrice: ${productPrice.value}\nWebsite: https://glamourstouch.com`);
window.open(`https://wa.me/8801712426871?text=${text}`, '_blank');
});
// 1-Click Auto Post to Facebook Page (via n8n Webhook)
const autoPostN8nBtn = document.getElementById('autoPostN8nBtn');
const n8nPostStatusMsg = document.getElementById('n8nPostStatusMsg');
if (autoPostN8nBtn) {
autoPostN8nBtn.addEventListener('click', async () => {
const webhookUrl = localStorage.getItem('opal_n8n_webhook');
if (!webhookUrl) {
alert('âš ī¸ Please set your n8n Webhook URL in Settings first!');
document.querySelector('.nav-item[data-tab="tabSettings"]').click();
return;
}
n8nPostStatusMsg.classList.remove('hidden');
n8nPostStatusMsg.style.color = '#38BDF8';
n8nPostStatusMsg.textContent = 'âŗ Sending 3D poster & caption to n8n Webhook...';
autoPostN8nBtn.disabled = true;
try {
const posterDataUrl = posterCanvas.toDataURL('image/jpeg', 0.92);
const payload = {
title: productInput.value.trim() || 'Authentic Korean K-Beauty Skincare',
price: productPrice.value.trim() || 'ā§ŗ ā§§,ā§Ļā§Ŧā§Ļ',
caption: `✨ ${productInput.value.trim()} ✨\n\n💰 āĻ…āĻĢāĻžāϰ āĻŽā§‚āĻ˛ā§āϝ: ${productPrice.value.trim()}\nđŸ›Ąī¸ ā§§ā§Ļā§Ļ% āĻ…āϰāĻŋāϜāĻŋāύāĻžāϞ āϕ⧋āϰāĻŋ⧟āĻžāύ āĻ•āϏāĻŽā§‡āϟāĻŋāĻ•ā§āϏ āĻ—ā§āϝāĻžāϰāĻžāĻ¨ā§āϟāĻŋ!\n\nđŸ’Ŧ āϏāϰāĻžāϏāϰāĻŋ WhatsApp āĻ āĻ…āĻ°ā§āĻĄāĻžāϰ āĻ•āϰāϤ⧇ āχāύāĻŦāĻ•ā§āϏ āĻ•āϰ⧁āύ:\n👉 https://wa.me/8801712426871\n📞 āĻšāϟāϞāĻžāχāύ: 01712-426871\n🌐 āĻ“ā§Ÿā§‡āĻŦāϏāĻžāχāϟ: https://glamourstouch.com`,
image_url: posterDataUrl,
store: "Glamour's Touch",
website: "https://glamourstouch.com",
whatsapp: "01712-426871",
timestamp: new Date().toISOString()
};
const res = await fetch(webhookUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
if (res.ok) {
const resData = await res.json().catch(() => ({}));
n8nPostStatusMsg.style.color = '#10B981';
n8nPostStatusMsg.textContent = `✅ ${resData.message || '🎉 āĻĒā§‹āĻ¸ā§āϟāϟāĻŋ āϏāĻĢāϞāĻ­āĻžāĻŦ⧇ n8n-āĻāϰ āĻŽāĻžāĻ§ā§āϝāĻŽā§‡ Facebook Page-āĻ āĻĒāĻžāĻŦāϞāĻŋāĻļ āĻšā§Ÿā§‡āϛ⧇!'}`;
} else {
n8nPostStatusMsg.style.color = '#EF4444';
n8nPostStatusMsg.textContent = `❌ n8n Webhook returned HTTP ${res.status}`;
}
} catch (err) {
n8nPostStatusMsg.style.color = '#EF4444';
n8nPostStatusMsg.textContent = `❌ Connection Error: ${err.message}`;
}
autoPostN8nBtn.disabled = false;
});
}
}
// Universal Link & Product Name Resolver
async function resolveProductUrl(input) {
if (!input) return null;
if (input.match(/\.(jpeg|jpg|gif|png|webp)($|\?)/i)) {
return { title: 'Authentic Korean Product', imageUrl: input };
}
const key = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImZtY2x0cmpudXZ1b29hcmt2dWZuIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NzUxMzY3MDQsImV4cCI6MjA5MDcxMjcwNH0.PkSgBAZx41X4sZurfyOdxCVa01hkKTkyBhVkGzx_4y4';
// 1. Glamour's Touch Supabase UUID regex
const uuidMatch = input.match(/[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}/);
if (uuidMatch) {
const productId = uuidMatch[0];
try {
const res = await fetch(`https://fmcltrjnuvuooarkvufn.supabase.co/rest/v1/products?id=eq.${productId}&select=*`, {
headers: { 'apikey': key, 'Authorization': `Bearer ${key}` }
});
const data = await res.json();
if (data && data.length > 0) {
const p = data[0];
return {
title: p.name || 'Authentic Korean Skincare',
price: p.price ? 'ā§ŗ ' + p.price : '',
imageUrl: p.image || p.image_url || (p.images && p.images[0]) || ''
};
}
} catch (e) {
console.warn('GT Supabase fetch by ID failed:', e);
}
}
// 2. Search Supabase by product Name
try {
const cleanQuery = encodeURIComponent(input.replace(/https?:\/\/[^\s]+/g, '').trim().slice(0, 30));
if (cleanQuery) {
const res = await fetch(`https://fmcltrjnuvuooarkvufn.supabase.co/rest/v1/products?name=ilike.*${cleanQuery}*&select=*&limit=1`, {
headers: { 'apikey': key, 'Authorization': `Bearer ${key}` }
});
const data = await res.json();
if (data && data.length > 0) {
const p = data[0];
return {
title: p.name || input,
price: p.price ? 'ā§ŗ ' + p.price : '',
imageUrl: p.image || p.image_url || (p.images && p.images[0]) || ''
};
}
}
} catch (e) {
console.warn('GT Supabase search by name failed:', e);
}
// 3. Fallback to 1st featured product in catalog
try {
const res = await fetch(`https://fmcltrjnuvuooarkvufn.supabase.co/rest/v1/products?in_stock=eq.true&image=not.is.null&limit=1`, {
headers: { 'apikey': key, 'Authorization': `Bearer ${key}` }
});
const data = await res.json();
if (data && data.length > 0) {
const p = data[0];
return {
title: p.name || input,
price: p.price ? 'ā§ŗ ' + p.price : '',
imageUrl: p.image || ''
};
}
} catch (e) {
console.warn('Fallback failed:', e);
}
return { title: input, imageUrl: 'https://fmcltrjnuvuooarkvufn.supabase.co/storage/v1/object/public/products/product-images/koba-74d48daa-7695-41f9-8f10-9e5fb4f0e2d9.jpg' };
}
function loadImage(src) {
return new Promise((resolve, reject) => {
let loadUrl = src;
if (src.startsWith('http://') || src.startsWith('https://')) {
loadUrl = 'https://images.weserv.nl/?url=' + encodeURIComponent(src) + '&output=jpg&q=95';
}
const img = new Image();
img.crossOrigin = 'anonymous';
img.onload = () => resolve(img);
img.onerror = () => {
const fallbackImg = new Image();
fallbackImg.crossOrigin = 'anonymous';
fallbackImg.onload = () => resolve(fallbackImg);
fallbackImg.onerror = () => reject(new Error('Could not load image across CORS'));
fallbackImg.src = src;
};
img.src = loadUrl;
});
}
// --- 4. Masterpiece 3D Canvas Rendering Engine ---
async function render3DCommercialPoster(canvas, productImg, title, price) {
const ctx = canvas.getContext('2d');
const w = 1080;
const h = 1080;
// 1. Midnight Obsidian Background
const bgGrad = ctx.createRadialGradient(w / 2, h * 0.44, 50, w / 2, h * 0.44, w * 0.72);
bgGrad.addColorStop(0, '#18212D');
bgGrad.addColorStop(0.6, '#0C1117');
bgGrad.addColorStop(1, '#05080C');
ctx.fillStyle = bgGrad;
ctx.fillRect(0, 0, w, h);
// 2. White-to-Midnight Luxury Radial Spotlight (Smooth Studio Blend)
const stageCx = w / 2;
const stageCy = h * 0.44;
const stageRadius = w * 0.38;
const spotGrad = ctx.createRadialGradient(stageCx, stageCy, 0, stageCx, stageCy, stageRadius);
spotGrad.addColorStop(0, '#FFFFFF');
spotGrad.addColorStop(0.46, '#FFFFFF');
spotGrad.addColorStop(0.64, '#F0F5FA');
spotGrad.addColorStop(0.82, '#8E9FAC');
spotGrad.addColorStop(0.94, '#283545');
spotGrad.addColorStop(1, '#0C1117');
ctx.fillStyle = spotGrad;
ctx.beginPath();
ctx.arc(stageCx, stageCy, stageRadius, 0, Math.PI * 2);
ctx.fill();
// Luxury Gold Dashed Rings
ctx.strokeStyle = '#66E5B83A';
ctx.lineWidth = 2.5;
ctx.beginPath();
ctx.arc(stageCx, stageCy, stageRadius * 0.96, 0, Math.PI * 2);
ctx.stroke();
ctx.strokeStyle = '#99E5B83A';
ctx.lineWidth = 1.8;
ctx.setLineDash([12, 12]);
ctx.beginPath();
ctx.arc(stageCx, stageCy, stageRadius * 0.91, 0, Math.PI * 2);
ctx.stroke();
ctx.setLineDash([]);
// 3. True 3D Dual-Tier Cylindrical Podium
// --- TIER 1: Lower Base ---
const baseTierY = 745;
const baseTierRx = w * 0.38;
const baseTierRy = 34;
const baseTierHeight = 26;
// Tier 1 Ground Shadow
const baseShadow = ctx.createRadialGradient(stageCx, baseTierY + baseTierHeight + 16, 20, stageCx, baseTierY + baseTierHeight + 16, baseTierRx * 1.15);
baseShadow.addColorStop(0, 'rgba(0,0,0,0.7)');
baseShadow.addColorStop(1, 'transparent');
ctx.fillStyle = baseShadow;
drawOval(ctx, stageCx, baseTierY + baseTierHeight + baseTierRy * 0.6, baseTierRx * 1.15, baseTierRy * 1.6);
// Tier 1 Cylinder Body
const baseCylGrad = ctx.createLinearGradient(stageCx - baseTierRx, 0, stageCx + baseTierRx, 0);
baseCylGrad.addColorStop(0, '#090D12');
baseCylGrad.addColorStop(0.2, '#1C2633');
baseCylGrad.addColorStop(0.5, '#6C829E');
baseCylGrad.addColorStop(0.8, '#1C2633');
baseCylGrad.addColorStop(1, '#090D12');
ctx.fillStyle = baseCylGrad;
ctx.fillRect(stageCx - baseTierRx, baseTierY, baseTierRx * 2, baseTierHeight);
drawOval(ctx, stageCx, baseTierY + baseTierHeight, baseTierRx, baseTierRy);
// Tier 1 Top Cap
const baseTopGrad = ctx.createLinearGradient(stageCx - baseTierRx, baseTierY - baseTierRy, stageCx + baseTierRx, baseTierY + baseTierRy);
baseTopGrad.addColorStop(0, '#222D3A');
baseTopGrad.addColorStop(1, '#0B1017');
ctx.fillStyle = baseTopGrad;
drawOval(ctx, stageCx, baseTierY, baseTierRx, baseTierRy);
// --- TIER 2: Upper Product Pedestal ---
const topTierY = 708;
const topTierRx = w * 0.31;
const topTierRy = 28;
const topTierHeight = 34;
// Tier 2 Cylinder Body
const topCylGrad = ctx.createLinearGradient(stageCx - topTierRx, 0, stageCx + topTierRx, 0);
topCylGrad.addColorStop(0, '#070A0E');
topCylGrad.addColorStop(0.25, '#18212C');
topCylGrad.addColorStop(0.5, '#F5E6AB');
topCylGrad.addColorStop(0.75, '#18212C');
topCylGrad.addColorStop(1, '#070A0E');
ctx.fillStyle = topCylGrad;
ctx.fillRect(stageCx - topTierRx, topTierY, topTierRx * 2, topTierHeight);
drawOval(ctx, stageCx, topTierY + topTierHeight, topTierRx, topTierRy);
// Tier 2 Top Surface (Glossy White/Platinum Cap)
const topSurfaceGrad = ctx.createRadialGradient(stageCx, topTierY, 0, stageCx, topTierY, topTierRx);
topSurfaceGrad.addColorStop(0, '#FFFFFF');
topSurfaceGrad.addColorStop(0.45, '#EBF2F8');
topSurfaceGrad.addColorStop(0.8, '#899EAF');
topSurfaceGrad.addColorStop(1, '#1B2533');
ctx.fillStyle = topSurfaceGrad;
drawOval(ctx, stageCx, topTierY, topTierRx, topTierRy);
// Tier 2 Top Gold Beveled Rim
const goldRimGrad = ctx.createLinearGradient(stageCx - topTierRx, topTierY, stageCx + topTierRx, topTierY);
goldRimGrad.addColorStop(0, '#B38728');
goldRimGrad.addColorStop(0.5, '#FFFFFF');
goldRimGrad.addColorStop(1, '#AA771C');
ctx.strokeStyle = goldRimGrad;
ctx.lineWidth = 3.5;
strokeOval(ctx, stageCx, topTierY, topTierRx, topTierRy);
// 4. Hero Product Scaling & Placement
const targetH = 500;
const targetW = 520;
const srcW = productImg.width || 500;
const srcH = productImg.height || 500;
const scale = Math.min(targetW / srcW, targetH / srcH);
const destW = srcW * scale;
const destH = srcH * scale;
const destLeft = (w - destW) / 2;
const destTop = topTierY - destH + 18;
// 4.1 Glossy Mirror Product Reflection (Multiply blended)
ctx.save();
ctx.globalCompositeOperation = 'multiply';
ctx.globalAlpha = 0.25;
ctx.translate(0, (topTierY + 6) * 2);
ctx.scale(1, -1);
ctx.drawImage(productImg, destLeft, topTierY + 6, destW, destH * 0.35);
ctx.restore();
// Contact Drop Shadow
const contactShadow = ctx.createRadialGradient(stageCx, topTierY, 10, stageCx, topTierY, destW * 0.4);
contactShadow.addColorStop(0, 'rgba(0,0,0,0.8)');
contactShadow.addColorStop(1, 'transparent');
ctx.fillStyle = contactShadow;
drawOval(ctx, stageCx, topTierY + 2, destW * 0.38, 12);
// Draw Hero Product with Multiply Blend Mode (removes white background completely)
ctx.save();
ctx.globalCompositeOperation = 'multiply';
ctx.drawImage(productImg, destLeft, destTop, destW, destH);
ctx.restore();
// 5. Value Trust Badges
drawPillBadge(ctx, 40, 240, '🇰🇷 100% Original Korea', true);
drawPillBadge(ctx, 40, 315, 'đŸŒŋ Skin Friendly Formula', true);
drawPillBadge(ctx, w - 40, 240, '⚡ Fast Delivery BD', false);
drawPillBadge(ctx, w - 40, 315, 'đŸ›Ąī¸ Authentic Guarantee', false);
// 6. Top Header Ribbon
ctx.fillStyle = '#F2090D12';
ctx.fillRect(0, 0, w, 92);
ctx.strokeStyle = '#E5B83A';
ctx.lineWidth = 3;
ctx.beginPath();
ctx.moveTo(0, 92);
ctx.lineTo(w, 92);
ctx.stroke();
ctx.fillStyle = '#E5B83A';
ctx.font = 'bold 34px Plus Jakarta Sans, sans-serif';
ctx.textAlign = 'center';
ctx.fillText('✨ GLAMOUR\'S TOUCH ✨', w / 2, 52);
ctx.fillStyle = '#FFF4D0';
ctx.font = '17px Plus Jakarta Sans, sans-serif';
ctx.fillText('대한ë¯ŧęĩ­ 프ëĻŦë¯¸ė—„ ėŠ¤í‚¨ėŧ€ė–´ â€ĸ 100% AUTHENTIC KOREAN COSMETICS', w / 2, 80);
// 7. Product Title Card
const cleanTitle = title.length > 60 ? title.substring(0, 58) + '...' : title;
const titleCardTop = h - 235;
const titleCardBottom = h - 128;
ctx.fillStyle = '#EA0B0E14';
roundRect(ctx, 28, titleCardTop, w - 56, titleCardBottom - titleCardTop, 18);
ctx.fill();
ctx.strokeStyle = '#55E5B83A';
ctx.lineWidth = 2;
roundRect(ctx, 28, titleCardTop, w - 56, titleCardBottom - titleCardTop, 18);
ctx.stroke();
ctx.fillStyle = '#FFFFFF';
ctx.font = 'bold 27px Plus Jakarta Sans, Hind Siliguri, sans-serif';
ctx.textAlign = 'center';
ctx.fillText(cleanTitle, w / 2, titleCardTop + 42);
ctx.fillStyle = '#E5B83A';
ctx.font = 'bold 20px Hind Siliguri, sans-serif';
ctx.fillText('āĻ—ā§āϞāĻžāϏ āĻ¸ā§āĻ•āĻŋāύ āĻ“ āĻĻāĻžāĻ—āĻšā§€āύ āωāĻœā§āĻœā§āĻŦāϞāϤāĻžāϰ āϕ⧋āϰāĻŋ⧟āĻžāύ āϏāĻŋāĻ•ā§āϰ⧇āϟ â€ĸ āϏāϰāĻžāϏāϰāĻŋ āϕ⧋āϰāĻŋ⧟āĻž āĻĨ⧇āϕ⧇ āφāĻŽāĻĻāĻžāύāĻŋāĻ•ā§ƒāϤ', w / 2, titleCardTop + 78);
// 8. Bottom WhatsApp Order Ribbon (Green Banner)
const footerTop = h - 118;
const footerGrad = ctx.createLinearGradient(0, footerTop, w, h);
footerGrad.addColorStop(0, '#25D366');
footerGrad.addColorStop(1, '#1EB855');
ctx.fillStyle = footerGrad;
ctx.fillRect(0, footerTop, w, 118);
ctx.fillStyle = '#FFFFFF';
ctx.font = 'bold 32px Plus Jakarta Sans, sans-serif';
ctx.textAlign = 'left';
ctx.fillText('đŸ’Ŧ WhatsApp: 01712-426871', 35, footerTop + 48);
ctx.fillStyle = '#053314';
ctx.font = 'bold 21px Plus Jakarta Sans, sans-serif';
ctx.fillText('🌐 Website: glamourstouch.com â€ĸ 📞 Call: 01712-426871', 35, footerTop + 90);
// 8.1 Render Scannable QR Code on Bottom Right
await drawWhatsAppQrOnCanvas(ctx, w - 130, footerTop + 11, 96);
// 9. Outer Gold Frame Edge
ctx.strokeStyle = '#88E5B83A';
ctx.lineWidth = 6;
ctx.strokeRect(3, 3, w - 6, h - 6);
}
function drawOval(ctx, cx, cy, rx, ry) {
ctx.beginPath();
ctx.ellipse(cx, cy, rx, ry, 0, 0, Math.PI * 2);
ctx.fill();
}
function strokeOval(ctx, cx, cy, rx, ry) {
ctx.beginPath();
ctx.ellipse(cx, cy, rx, ry, 0, 0, Math.PI * 2);
ctx.stroke();
}
function roundRect(ctx, x, y, width, height, radius) {
ctx.beginPath();
ctx.moveTo(x + radius, y);
ctx.lineTo(x + width - radius, y);
ctx.quadraticCurveTo(x + width, y, x + width, y + radius);
ctx.lineTo(x + width, y + height - radius);
ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height);
ctx.lineTo(x + radius, y + height);
ctx.quadraticCurveTo(x, y + height, x, y + height - radius);
ctx.lineTo(x, y + radius);
ctx.quadraticCurveTo(x, y, x + radius, y);
ctx.closePath();
}
function drawPillBadge(ctx, x, y, text, isLeft) {
ctx.font = 'bold 18px Plus Jakarta Sans, Hind Siliguri, sans-serif';
const textWidth = ctx.measureText(text).width;
const pillW = textWidth + 28;
const pillH = 36;
const pillX = isLeft ? x : x - pillW;
const pillY = y - 22;
ctx.fillStyle = '#D90E141B';
roundRect(ctx, pillX, pillY, pillW, pillH, 18);
ctx.fill();
ctx.strokeStyle = '#55E5B83A';
ctx.lineWidth = 1.5;
roundRect(ctx, pillX, pillY, pillW, pillH, 18);
ctx.stroke();
ctx.fillStyle = '#FFF0C8';
ctx.textAlign = 'left';
ctx.fillText(text, pillX + 14, pillY + 24);
}
async function drawWhatsAppQrOnCanvas(ctx, x, y, size) {
// White background container
ctx.fillStyle = '#FFFFFF';
roundRect(ctx, x - 4, y - 4, size + 8, size + 8, 10);
ctx.fill();
// Create temporary QR element
const qrContainer = document.getElementById('qrcodeHidden');
qrContainer.innerHTML = '';
new QRCode(qrContainer, {
text: 'https://wa.me/8801712426871',
width: size,
height: size,
colorDark: '#0B2E13',
colorLight: '#FFFFFF',
correctLevel: QRCode.CorrectLevel.M
});
await new Promise(r => setTimeout(r, 80));
const qrImg = qrContainer.querySelector('img') || qrContainer.querySelector('canvas');
if (qrImg) {
ctx.drawImage(qrImg, x, y, size, size);
}
}
// --- 5. Customer Care & Inbox AI ---
function initInboxAI() {
const customerMsgInput = document.getElementById('inboxCustomerMsg');
const storeContextInput = document.getElementById('inboxStoreContext');
const generateBtn = document.getElementById('generateInboxReplyBtn');
const statusMsg = document.getElementById('inboxStatusMsg');
const repliesCard = document.getElementById('inboxRepliesCard');
const reply1 = document.getElementById('replyText1');
const reply2 = document.getElementById('replyText2');
const reply3 = document.getElementById('replyText3');
generateBtn.addEventListener('click', async () => {
const query = customerMsgInput.value.trim();
const storeInfo = storeContextInput.value.trim();
const apiKey = localStorage.getItem('opal_gemini_api_key') || '';
const model = localStorage.getItem('opal_selected_model') || 'gemini-3.6-flash';
if (!query) {
alert('Please enter a customer message');
return;
}
statusMsg.classList.remove('hidden');
statusMsg.textContent = 'âŗ Generating 3 sweet & polite sales replies with Gemma AI...';
generateBtn.disabled = true;
try {
if (apiKey) {
const prompt = `A customer sent this message in Glamour's Touch inbox:
'${query}'
Store Info: ${storeInfo}
Write 3 ready-to-copy customer responses in natural, sweet BENGALI (āĻŦāĻžāĻ‚āϞāĻž).
Format exactly with:
---REPLY1---
(Instant polite greeting and direct answer with price & benefits)
---REPLY2---
(Order confirmation closer asking for Name, Address, and Phone number)
---REPLY3---
(1-click WhatsApp order link: https://wa.me/8801712426871 and hotline 01712-426871)`;
const result = await callGeminiApi(apiKey, model, prompt);
const parts = result.split(/---REPLY[123]---/);
reply1.textContent = (parts[1] || 'āφāϏāϏāĻžāϞāĻžāĻŽā§ āφāϞāĻžāχāϕ⧁āĻŽ āĻĒā§āϰāĻŋ⧟ āĻ•āĻžāĻ¸ā§āϟāĻŽāĻžāϰ! āφāĻŽāĻžāĻĻ⧇āϰ āĻ—ā§āϞāĻžāϏ āĻ¸ā§āĻ•āĻŋāύ āϏāĻŋāϰāĻžāĻŽā§‡āϰ āĻĻāĻžāĻŽ āĻŽāĻžāĻ¤ā§āϰ ā§ŗā§§ā§Ŧā§Ļā§ĻāĨ¤ āĻāϟāĻŋ ā§§ā§Ļā§Ļ% āφāϏāϞ āϕ⧋āϰāĻŋ⧟āĻžāύ āĻĒā§āϰāĻĄāĻžāĻ•ā§āϟāĨ¤').trim();
reply2.textContent = (parts[2] || 'āĻ…āĻ°ā§āĻĄāĻžāϰāϟāĻŋ āĻ•āύāĻĢāĻžāĻ°ā§āĻŽ āĻ•āϰāϤ⧇ āφāĻĒāύāĻžāϰ āύāĻžāĻŽ, āϏāĻŽā§āĻĒā§‚āĻ°ā§āĻŖ āĻ āĻŋāĻ•āĻžāύāĻž āĻāĻŦāĻ‚ āĻŽā§‹āĻŦāĻžāχāϞ āύāĻŽā§āĻŦāϰāϟāĻŋ āϞāĻŋāϖ⧇ āĻĒāĻžāĻ āĻžāύāĨ¤ āφāĻŽāϰāĻž āĻĻā§āϰ⧁āϤ āĻ•ā§āϝāĻžāĻļ āĻ…āύ āĻĄā§‡āϞāĻŋāĻ­āĻžāϰāĻŋāϤ⧇ āĻĒāĻžāĻ āĻŋā§Ÿā§‡ āĻĻ⧇āĻŦā§‹!').trim();
reply3.textContent = (parts[3] || 'āϏāϰāĻžāϏāϰāĻŋ āĻšā§‹ā§ŸāĻžāϟāϏāĻ…ā§āϝāĻžāĻĒ⧇ āĻĻā§āϰ⧁āϤ āĻ…āĻ°ā§āĻĄāĻžāϰ āĻ•āϰāϤ⧇ āĻ•ā§āϞāĻŋāĻ• āĻ•āϰ⧁āύ: https://wa.me/8801712426871\nāĻšāϟāϞāĻžāχāύ: 01712-426871').trim();
} else {
// High quality offline fallback template
reply1.textContent = `āφāϏāϏāĻžāϞāĻžāĻŽā§ āφāϞāĻžāχāϕ⧁āĻŽ āĻĒā§āϰāĻŋ⧟ āφāĻĒ⧁/āĻ­āĻžāĻ‡ā§ŸāĻž! đŸĨ°\nāφāĻŽāĻžāĻĻ⧇āϰ āĻ•āĻžāϛ⧇ ā§§ā§Ļā§Ļ% āĻ…āϰāĻŋāϜāĻŋāύāĻžāϞ āϕ⧋āϰāĻŋ⧟āĻžāύ āĻ¸ā§āĻ•āĻŋāύāĻ•ā§‡ā§ŸāĻžāϰ āĻĒā§āϰ⧋āĻĄāĻžāĻ•ā§āϟ āĻ°ā§Ÿā§‡āϛ⧇āĨ¤\n\n💰 āĻ…āĻĢāĻžāϰ āĻŽā§‚āĻ˛ā§āϝ: ā§ŗ ā§§,ā§Ļā§Ŧā§Ļ (āϰ⧇āϗ⧁āϞāĻžāϰ ā§ŗ ā§§,ā§Ēā§­ā§Ļ)\n🚚 āĻšā§‹āĻŽ āĻĄā§‡āϞāĻŋāĻ­āĻžāϰāĻŋ: āĻĸāĻžāĻ•āĻžā§Ÿ ā§ŗā§­ā§Ļ, āĻĸāĻžāĻ•āĻžāϰ āĻŦāĻžāχāϰ⧇ ā§ŗā§§ā§Šā§Ļ\n\nāĻāϟāĻŋ āύāĻŋ⧟āĻŽāĻŋāϤ āĻŦā§āϝāĻŦāĻšāĻžāϰ⧇ āĻ¤ā§āĻŦāĻ• āĻšāĻŦ⧇ āĻ•āĻžāĻšā§‡āϰ āĻŽāϤ⧋ āϚāĻ•āϚāϕ⧇ āĻ“ āĻĻāĻžāĻ—āĻšā§€āύ! 🌸`;
reply2.textContent = `āĻ…āĻ°ā§āĻĄāĻžāϰāϟāĻŋ āĻ•āύāĻĢāĻžāĻ°ā§āĻŽ āĻ•āϰāϤ⧇ āĻ…āύ⧁āĻ—ā§āϰāĻš āĻ•āϰ⧇ āφāĻĒāύāĻžāϰ:\nā§§. āύāĻžāĻŽ\n⧍. āϏāĻŽā§āĻĒā§‚āĻ°ā§āĻŖ āĻ āĻŋāĻ•āĻžāύāĻž (āĻœā§‡āϞāĻž/āĻĨāĻžāύāĻž āϏāĻš)\nā§Š. āĻŽā§‹āĻŦāĻžāχāϞ āύāĻŽā§āĻŦāϰ\n\nāϞāĻŋāϖ⧇ āĻĒāĻžāĻ āĻŋā§Ÿā§‡ āĻĻāĻŋāύāĨ¤ āφāĻŽāϰāĻž āĻĻā§āϰ⧁āϤ āĻĒā§āϰ⧋āĻĄāĻžāĻ•ā§āϟ āĻĒāĻžāĻ āĻŋā§Ÿā§‡ āĻĻ⧇āĻŦā§‹! āϧāĻ¨ā§āϝāĻŦāĻžāĻĻāĨ¤ đŸ›ī¸`;
reply3.textContent = `āϏāϰāĻžāϏāϰāĻŋ āĻšā§‹ā§ŸāĻžāϟāϏāĻ…ā§āϝāĻžāĻĒ⧇ ā§§-āĻ•ā§āϞāĻŋāϕ⧇ āĻ…āĻ°ā§āĻĄāĻžāϰ āĻ•āϰāϤ⧇ āφāĻŽāĻžāĻĻ⧇āϰ āχāύāĻŦāĻ•ā§āϏ āĻ•āϰ⧁āύ:\nđŸ’Ŧ WhatsApp: https://wa.me/8801712426871\n📞 āĻšāϟāϞāĻžāχāύ: 01712-426871\n🌐 āĻ“ā§Ÿā§‡āĻŦāϏāĻžāχāϟ: https://glamourstouch.com`;
}
repliesCard.classList.remove('hidden');
statusMsg.classList.add('hidden');
repliesCard.scrollIntoView({ behavior: 'smooth' });
} catch (e) {
statusMsg.textContent = `❌ Error: ${e.message}`;
}
generateBtn.disabled = false;
});
}
// --- 6. History Management ---
function initHistory() {
const historyContainer = document.getElementById('historyListContainer');
const clearBtn = document.getElementById('clearHistoryBtn');
function renderHistory() {
const list = JSON.parse(localStorage.getItem('opal_history') || '[]');
if (list.length === 0) {
historyContainer.innerHTML = `
<div class="empty-state">
<span class="icon-empty">📭</span>
<p>No generations yet. Create your first 3D poster or copy!</p>
</div>`;
return;
}
historyContainer.innerHTML = list.map(item => `
<div class="app-card">
<div class="app-icon">${item.type === '3D Poster' ? '🎨' : 'đŸ’Ŧ'}</div>
<div class="app-info">
<h3>${item.title}</h3>
<p>${item.price || item.type}</p>
<span class="meta-tag">Saved at ${item.date}</span>
</div>
</div>
`).join('');
}
clearBtn.addEventListener('click', () => {
localStorage.removeItem('opal_history');
renderHistory();
});
renderHistory();
}
function saveGenerationToHistory(item) {
const list = JSON.parse(localStorage.getItem('opal_history') || '[]');
list.unshift(item);
localStorage.setItem('opal_history', JSON.stringify(list.slice(0, 30)));
}