Files
opal-ios-studio/app.js
T

1285 lines
51 KiB
JavaScript

// Google Opal AI - Master Mini-App Studio & 3D Poster Lab Engine v5.0
document.addEventListener('DOMContentLoaded', () => {
initNavigation();
initSettings();
initPosterLab();
initMiniAppsHub();
initMiniAppBuilder();
initInboxAI();
initHistory();
});
// --- Global Navigation ---
window.switchToTab = function(tabId) {
const navItems = document.querySelectorAll('.nav-item');
const tabPages = document.querySelectorAll('.tab-page');
navItems.forEach(n => {
if (n.getAttribute('data-tab') === tabId) {
n.classList.add('active');
} else {
n.classList.remove('active');
}
});
tabPages.forEach(p => {
if (p.id === tabId) {
p.classList.add('active');
} else {
p.classList.remove('active');
}
});
window.scrollTo({ top: 0, behavior: 'smooth' });
};
function initNavigation() {
const navItems = document.querySelectorAll('.nav-item');
navItems.forEach(item => {
item.addEventListener('click', () => {
const targetTabId = item.getAttribute('data-tab');
switchToTab(targetTabId);
});
});
// Language Toggle
const langBtn = document.getElementById('langToggleBtn');
let currentLang = 'BN';
if (langBtn) {
langBtn.addEventListener('click', () => {
currentLang = currentLang === 'BN' ? 'EN' : 'BN';
langBtn.textContent = currentLang === 'BN' ? '🌐 āĻŦāĻžāĻ‚' : '🌐 EN';
});
}
}
window.copyText = function(elementId) {
const el = document.getElementById(elementId);
if (el) {
const text = el.value || el.textContent || '';
navigator.clipboard.writeText(text).then(() => {
alert('✅ Copied to Clipboard!');
}).catch(() => {
// Fallback
const ta = document.createElement('textarea');
ta.value = text;
document.body.appendChild(ta);
ta.select();
document.execCommand('copy');
document.body.removeChild(ta);
alert('✅ Copied to Clipboard!');
});
}
};
// --- Settings & Gemini 2.0 API Engine ---
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-2.0-flash';
const savedN8n = localStorage.getItem('opal_n8n_webhook') || 'https://n8n.auraajenticai.cloud/webhook/gt-auto-post';
if (apiKeyInput) apiKeyInput.value = savedKey;
if (modelSelect) modelSelect.value = savedModel;
if (n8nWebhookInput) n8nWebhookInput.value = savedN8n;
if (saveBtn) {
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) {
localStorage.setItem('opal_selected_model', model);
statusMsg.classList.remove('hidden');
statusMsg.style.color = '#10B981';
statusMsg.textContent = '✅ Settings saved (Offline templates active).';
return;
}
statusMsg.classList.remove('hidden');
statusMsg.style.color = '#F472B6';
statusMsg.textContent = 'âŗ Testing Gemini 2.0 AI connection with Google...';
try {
const res = await callGeminiApi(key, model, "Respond with: 'Google 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 = `✅ Verified & Active: ${res.trim()}`;
}
} catch (e) {
statusMsg.style.color = '#EF4444';
statusMsg.textContent = `❌ Connection Error: ${e.message}`;
}
});
}
}
async function callGeminiApi(apiKey, model, prompt) {
const activeModel = model || localStorage.getItem('opal_selected_model') || 'gemini-2.0-flash';
const url = `https://generativelanguage.googleapis.com/v1beta/models/${activeModel}: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().catch(() => ({}));
throw new Error(errData?.error?.message || `HTTP ${response.status}`);
}
const data = await response.json();
return data?.candidates?.[0]?.content?.parts?.[0]?.text || '';
}
// --- Curated Pre-Built Mini-Apps Suite ---
const PREBUILT_MINI_APPS = [
{
id: 'gt_studio',
name: '3D Commercial Poster Lab',
emoji: '🎨',
category: 'ecommerce',
badge: 'GEM',
desc: 'āĻĒā§āϰāĻĄāĻžāĻ•ā§āĻŸā§‡āϰ āĻ›āĻŦāĻŋ, ā§ŠāĻĄāĻŋ āĻŽāĻžāĻ°ā§āĻŦ⧇āϞ āĻ¸ā§āĻŸā§‡āϜ āĻ“ WhatsApp QR āϏāĻš 4K āĻĢ⧇āϏāĻŦ⧁āĻ• āĻŦāĻŋāĻœā§āĻžāĻžāĻĒāύ āĻĒā§‹āĻ¸ā§āϟāĻžāϰ āϤ⧈āϰāĻŋ āĻ•āϰ⧇āĨ¤',
steps: '7 Steps â€ĸ 3D Canvas',
isPosterLab: true
},
{
id: 'inbox_ai',
name: 'Customer Care & Sales Auto-Reply',
emoji: 'đŸ’Ŧ',
category: 'sales',
badge: 'AI',
desc: 'āĻĢ⧇āϏāĻŦ⧁āĻ• āĻ“ āĻšā§‹ā§ŸāĻžāϟāϏāĻ…ā§āϝāĻžāĻĒ⧇āϰ āĻ•āĻžāĻ¸ā§āϟāĻŽāĻžāϰ⧇āϰ āĻĒā§āϰāĻļā§āύ⧇āϰ ā§ŠāϟāĻŋ āχāύāĻ¸ā§āĻŸā§āϝāĻžāĻ¨ā§āϟ āĻŽāĻŋāĻˇā§āϟāĻŋ āĻ“ āϏ⧇āϞāϏ āĻ•ā§āϞ⧋āϜāĻŋāĻ‚ āωāĻ¤ā§āϤāϰ āĻŦāĻžāύāĻžā§ŸāĨ¤',
steps: '3 Steps â€ĸ Sales Closer',
fields: [
{ id: 'field1', label: 'āĻ•āĻžāĻ¸ā§āϟāĻŽāĻžāϰ āĻŽā§‡āϏ⧇āϜ (Customer Message)', placeholder: 'e.g. āφāĻĒāύāĻžāĻĻ⧇āϰ āϏāĻŋāϰāĻžāĻŽā§‡āϰ āĻĻāĻžāĻŽ āĻ•āϤ āφāϰ āĻĄā§‡āϞāĻŋāĻ­āĻžāϰāĻŋ āϚāĻžāĻ°ā§āϜ āĻ•āϤ?' },
{ id: 'field2', label: 'āĻĻā§‹āĻ•āĻžāύ⧇āϰ āĻ…āĻĢāĻžāϰ āĻŦāĻž āĻšāϟāϞāĻžāχāύ (Store Context)', placeholder: 'e.g. āĻĻāĻžāĻŽ ā§§ā§Ļā§Ŧā§Ļ āϟāĻžāĻ•āĻž, āĻĸāĻžāĻ•āĻž ā§­ā§Ļ āϟāĻžāĻ•āĻž, āĻšāϟāϞāĻžāχāύ ā§Ļ⧧⧭⧧⧍-ā§Ē⧍ā§Ŧā§Žā§­ā§§' }
],
promptTemplate: `You are an expert Bangladeshi e-commerce sales closer.
Customer message: {{field1}}
Store context: {{field2}}
Write 3 distinct ready-to-copy Bengali replies:
1. Instant sweet greeting with price & benefits
2. Direct order closing script asking for Name, Address, Phone
3. 1-Click WhatsApp direct order message with link https://wa.me/8801712426871`
},
{
id: 'veo_prompter',
name: 'Google Veo 3.1 Video Ad Prompter',
emoji: 'đŸŽŦ',
category: 'social',
badge: 'VEO 3.1',
desc: 'āϗ⧁āĻ—āϞ Veo ā§Š.ā§§ āĻ“ Sora-āϰ āϜāĻ¨ā§āϝ āϏāĻŋāύ⧇āĻŽāĻžāϟāĻŋāĻ• 9:16 āĻ­āĻŋāĻĄāĻŋāĻ“ āĻĒā§āϰāĻŽā§āĻĒāϟ, āĻ•ā§āϝāĻžāĻŽā§‡āϰāĻž āĻŽā§‹āĻļāύ āĻ“ āϞāĻžāχāϟāĻŋāĻ‚ āĻ—āĻžāχāĻĄ āĻŦāĻžāύāĻžā§ŸāĨ¤',
steps: '4 Steps â€ĸ AI Video',
fields: [
{ id: 'field1', label: 'āĻĒā§āϰāĻĄāĻžāĻ•ā§āĻŸā§‡āϰ āύāĻžāĻŽ āĻ“ āϧāϰāĻŖ (Product)', placeholder: 'e.g. Medicube Collagen Jelly Cream' },
{ id: 'field2', label: 'āĻ­āĻŋāĻĄāĻŋāĻ“āϰ āĻ­āĻžāχāĻŦ āĻ“ āĻ¸ā§āϟāĻžāχāϞ (Style/Vibe)', placeholder: 'e.g. Luxury Korean Aesthetic Glow, Neon reflections, 4k macro' }
],
promptTemplate: `You are a world-class AI Video Prompt Engineer for Google Veo 3.1 and Sora.
Create an ultra-detailed, cinematic 9:16 vertical video prompt for:
Product: {{field1}}
Style/Vibe: {{field2}}
Include:
1. Master Cinematic Prompt (in English for AI video model)
2. Camera Movements (Macro close-up, 360 rotation, slow motion drop)
3. Studio Lighting & Physics (Softbox, caustics, rim light)
4. Bengali Narration / Voiceover Script (15 seconds)`
},
{
id: 'risk_scorer',
name: 'Gemini COD Fraud & Return Risk Shield',
emoji: 'đŸ›Ąī¸',
category: 'tools',
badge: 'AI SHIELD',
desc: 'āĻ•āĻžāĻ¸ā§āϟāĻŽāĻžāϰ⧇āϰ āĻĢā§‹āύ āύāĻžāĻŽā§āĻŦāĻžāϰ, āĻ āĻŋāĻ•āĻžāύāĻž āĻ“ āĻ…āĻ°ā§āĻĄāĻžāϰ⧇āϰ āĻĒāϰāĻŋāĻŽāĻžāĻŖ āĻŦāĻŋāĻļā§āϞ⧇āώāĻŖ āĻ•āϰ⧇ āϰāĻŋāϟāĻžāĻ°ā§āύ āĻĢā§āϰāĻĄ āϰāĻŋāĻ¸ā§āĻ• āĻ¸ā§āϕ⧋āϰ āĻĻā§‡ā§ŸāĨ¤',
steps: 'Real-time â€ĸ Gemini 2.0',
fields: [
{ id: 'field1', label: 'āĻ•āĻžāĻ¸ā§āϟāĻŽāĻžāϰ⧇āϰ āĻĢā§‹āύ āύāĻžāĻŽā§āĻŦāĻžāϰ (Phone Number)', placeholder: 'e.g. 01819998877' },
{ id: 'field2', label: 'āĻ āĻŋāĻ•āĻžāύāĻž āĻ“ āĻ…āĻ°ā§āĻĄāĻžāϰ⧇āϰ āĻŽā§‚āĻ˛ā§āϝ (Address & Amount)', placeholder: 'e.g. Mirpur 10, Dhaka - Amount: ā§ŗ 3,200' }
],
promptTemplate: `You are an advanced e-commerce Fraud Risk Analysis Engine for Bangladesh logistics.
Phone: {{field1}}
Order Info: {{field2}}
Evaluate:
1. Risk Level: (LOW / MEDIUM / HIGH)
2. Recommendation: (Proceed with COD / Take Advance Delivery Charge ā§ŗ150 / Call to verify)
3. Short reasoning in clear Bengali.`
},
{
id: 'ecommerce_launch',
name: 'E-Commerce Viral Product Launch Kit',
emoji: 'đŸ›ī¸',
category: 'ecommerce',
badge: 'VIRAL',
desc: 'āϝ⧇āϕ⧋āύ⧋ āĻĒāĻŖā§āϝ⧇āϰ āφāĻ•āĻ°ā§āώāĻŖā§€ā§Ÿ āĻĢ⧇āϏāĻŦ⧁āĻ• āĻĒā§‹āĻ¸ā§āϟ āĻ•ā§āϝāĻžāĻĒāĻļāύ, āĻĒ⧇āχāύ āĻĒā§Ÿā§‡āĻ¨ā§āϟ āĻšā§āĻ• āĻ“ āĻ…āĻĢāĻžāϰ āĻ•āĻĒāĻŋ āϤ⧈āϰāĻŋ āĻ•āϰ⧇āĨ¤',
steps: '5 Steps â€ĸ Marketing',
fields: [
{ id: 'field1', label: 'āĻĒā§āϰāĻĄāĻžāĻ•ā§āĻŸā§‡āϰ āύāĻžāĻŽ āĻ“ āĻ•ā§āϝāĻžāϟāĻžāĻ—āϰāĻŋ (Product Name)', placeholder: 'e.g. Korean Snail Mucin Power Essence' },
{ id: 'field2', label: 'āĻŽā§‚āϞ āϏ⧁āĻŦāĻŋāϧāĻž āĻ“ āĻ…āĻĢāĻžāϰ (Key Benefits & Price)', placeholder: 'e.g. āĻŦā§āϰāϪ⧇āϰ āĻĻāĻžāĻ— āĻĻā§‚āϰ āĻ•āϰ⧇, āĻĄāĻŋāĻĒ āĻŽā§Ÿā§‡āĻļā§āϚāĻžāϰāĻžāχāϜāĻŋāĻ‚, āĻĻāĻžāĻŽ ā§ŗā§§,ā§Ēā§Ģā§Ļ (āĻ…āĻĢāĻžāϰ ⧍ā§Ģ% āĻ›āĻžā§œ)' }
],
promptTemplate: `You are a high-converting Facebook ad copywriter in Bangladesh.
Create a complete viral product launch post in attractive Bengali for:
Product: {{field1}}
Benefits & Offer: {{field2}}
Format:
- 3 Viral Opening Hooks (Emoji heavy)
- Problem & Solution story
- Special Limited Time Offer & Delivery Info
- Urgent Call-to-Action with WhatsApp & Website link`
},
{
id: 'tiktok_hooks',
name: 'TikTok & Meta Reels Viral Hook Matrix',
emoji: 'đŸ”Ĩ',
category: 'social',
badge: 'HOOKS',
desc: 'āĻĒā§āϰāĻĨāĻŽ ā§Š āϏ⧇āϕ⧇āĻ¨ā§āĻĄā§‡ āĻ•āĻžāĻ¸ā§āϟāĻŽāĻžāϰ⧇āϰ āĻšā§‹āĻ– āφāϟāϕ⧇ āĻĢ⧇āϞāĻžāϰ āĻŽāϤ⧋ ā§ĢāϟāĻŋ āϏāĻžāχāϕ⧋āϞāϜāĻŋāĻ•ā§āϝāĻžāϞ āĻ­āĻžāχāϰāĻžāϞ āĻšā§āĻ• āϤ⧈āϰāĻŋ āĻ•āϰ⧇āĨ¤',
steps: '5 Viral Hooks â€ĸ Reels',
fields: [
{ id: 'field1', label: 'āĻĒā§āϰāĻĄāĻžāĻ•ā§āϟ āĻŦāĻž āĻŦāĻŋāĻˇā§Ÿā§‡āϰ āύāĻžāĻŽ (Product / Topic)', placeholder: 'e.g. Korean Glass Skin Facial Foam' },
{ id: 'field2', label: 'āϟāĻžāĻ°ā§āϗ⧇āϟ āĻ…āĻĄāĻŋā§Ÿā§‡āĻ¨ā§āϏ (Target Audience)', placeholder: 'e.g. āϝāĻžāĻĻ⧇āϰ āĻŽā§āϖ⧇ āϰ⧋āĻĻ⧇ āĻĒā§‹ā§œāĻž āĻĻāĻžāĻ— āĻ“ āĻŦā§āϰāϪ⧇āϰ āϏāĻŽāĻ¸ā§āϝāĻž āĻ°ā§Ÿā§‡āϛ⧇' }
],
promptTemplate: `You are a viral TikTok & Instagram Reels growth strategist.
Create 5 powerful 3-second visual + verbal hooks in Bengali for:
Product: {{field1}}
Target Audience: {{field2}}
Format each hook with:
- Visual Action (Screen text & actor action)
- Spoken Audio Hook (High curiosity / urgency)
- Psychology trigger explanation`
},
{
id: 'urgency_sales',
name: 'Flash Sale & Urgency Campaign Copywriter',
emoji: 'đŸˇī¸',
category: 'sales',
badge: 'OFFER',
desc: '⧍ā§Ē āϘāĻŖā§āϟāĻžāϰ āĻĢā§āĻ˛ā§āϝāĻžāĻļ āϏ⧇āϞ, āĻ¸ā§āϟāĻ• āĻ•ā§āϞāĻŋ⧟āĻžāϰ⧇āĻ¨ā§āϏ āĻ“ āψāĻĻ/āĻŦ⧈āĻļāĻžāĻ–ā§€ āĻ…āĻĢāĻžāϰ⧇āϰ āĻšāĻžāχ-āĻ•āύāĻ­āĻžāĻ°ā§āϟāĻŋāĻ‚ āĻ•āĻĒāĻŋ āĻŦāĻžāύāĻžā§ŸāĨ¤',
steps: 'Flash Sales â€ĸ Copy',
fields: [
{ id: 'field1', label: 'āĻ…āĻĢāĻžāϰ⧇āϰ āύāĻžāĻŽ āĻ“ āĻĄāĻŋāϏāĻ•āĻžāωāĻ¨ā§āϟ (Offer Details)', placeholder: 'e.g. 48 Hours Weekend Flash Sale - Flat 35% OFF' },
{ id: 'field2', label: 'āĻļāĻ°ā§āϤ āĻ“ āĻĢā§āϰāĻŋ āĻĄā§‡āϞāĻŋāĻ­āĻžāϰāĻŋ (Conditions & Perks)', placeholder: 'e.g. āĻĒā§āϰāĻĨāĻŽ ā§Ģā§Ļ āϜāύ⧇āϰ āϜāĻ¨ā§āϝ āĻĢā§āϰāĻŋ āĻ—āĻŋāĻĢāϟ āĻ“ āĻĢā§āϰāĻŋ āĻšā§‹āĻŽ āĻĄā§‡āϞāĻŋāĻ­āĻžāϰāĻŋ' }
],
promptTemplate: `Create an irresistible, high-urgency Flash Sale ad copy in natural Bengali for:
Campaign: {{field1}}
Perks: {{field2}}
Include FOMO triggers, countdown psychological anchors, and clear WhatsApp booking steps.`
},
{
id: 'courier_formatter',
name: 'Steadfast & Pathao Consignment Formatter',
emoji: 'đŸ“Ļ',
category: 'tools',
badge: 'LOGISTICS',
desc: 'āĻ…āĻ°ā§āĻĄāĻžāϰ⧇āϰ āĻŸā§‡āĻ•ā§āϏāϟ āĻĨ⧇āϕ⧇ ā§§-āĻ•ā§āϞāĻŋāϕ⧇ āϕ⧁āϰāĻŋ⧟āĻžāϰ āĻāĻ¨ā§āĻŸā§āϰāĻŋ āĻĢāϰāĻŽā§āϝāĻžāϟ āĻ“ āĻĒā§āϰāĻŋāĻ¨ā§āϟ āϞ⧇āĻŦ⧇āϞ āϤ⧈āϰāĻŋ āĻ•āϰ⧇āĨ¤',
steps: '1-Click â€ĸ Logistics',
fields: [
{ id: 'field1', label: 'āĻ•āĻžāĻ¸ā§āϟāĻŽāĻžāϰ⧇āϰ āϏāĻŽā§āĻĒā§‚āĻ°ā§āĻŖ āĻ…āĻ°ā§āĻĄāĻžāϰ āĻŸā§‡āĻ•ā§āϏāϟ (Order Text)', placeholder: 'e.g. āύāĻžāĻŽ: āϤāĻžāύāĻ­ā§€āϰ āĻšāĻžāϏāĻžāύ, āĻĢā§‹āύ: 01712345678, āĻ āĻŋāĻ•āĻžāύāĻž: āĻŦāĻžā§œāĻŋ ⧧⧍, āϰ⧋āĻĄ ā§Ģ, āϧāĻžāύāĻŽāĻ¨ā§āĻĄāĻŋ, āĻĸāĻžāĻ•āĻžāĨ¤ āĻĒā§āϰāĻĄāĻžāĻ•ā§āϟ: ā§§ āĻĒāĻŋāϏ āϏāĻŋāϰāĻžāĻŽ (ā§ŗā§§ā§Ļā§Ŧā§Ļ)' },
{ id: 'field2', label: 'āĻĄā§‡āϞāĻŋāĻ­āĻžāϰāĻŋ āϚāĻžāĻ°ā§āϜ āĻ“ āύ⧋āϟ (COD & Notes)', placeholder: 'e.g. COD Amount: ā§ŗ 1130 (Dhaka inside ā§ŗ70)' }
],
promptTemplate: `Parse this customer order and structure it cleanly for Steadfast / Pathao Courier API and Print Packing Sticker:
Order Text: {{field1}}
COD Info: {{field2}}
Generate:
1. Structured CSV/JSON Format (Name, Phone, Address, COD, Instruction)
2. Clean Printable Packing Slip in Bengali.`
}
];
// --- Mini-Apps Hub Logic ---
let activeCategory = 'all';
function initMiniAppsHub() {
renderMiniApps();
const filterChips = document.querySelectorAll('.filter-chip');
filterChips.forEach(chip => {
chip.addEventListener('click', () => {
filterChips.forEach(c => c.classList.remove('active'));
chip.classList.add('active');
activeCategory = chip.getAttribute('data-cat');
renderMiniApps();
});
});
}
function getCustomMiniApps() {
return JSON.parse(localStorage.getItem('opal_custom_apps') || '[]');
}
function renderMiniApps() {
const container = document.getElementById('appsGridContainer');
if (!container) return;
const customApps = getCustomMiniApps();
const allApps = [...PREBUILT_MINI_APPS, ...customApps];
const filtered = activeCategory === 'all'
? allApps
: (activeCategory === 'custom' ? customApps : allApps.filter(a => a.category === activeCategory));
if (filtered.length === 0) {
container.innerHTML = `
<div class="col-span-full empty-state" style="grid-column: 1 / -1;">
<span class="icon-empty">📭</span>
<p>No mini-apps found in this category. Click "+ Create App" to build one!</p>
</div>
`;
return;
}
container.innerHTML = filtered.map(app => `
<div class="app-card">
<div class="app-card-top">
<div class="app-icon">${app.emoji || '💎'}</div>
<span class="tag-gem">${app.badge || 'CUSTOM'}</span>
</div>
<div onclick="openAppRunner('${app.id}')" style="cursor: pointer; flex: 1;">
<h3>${app.name}</h3>
<p>${app.desc || 'AI powered dynamic mini-app for client dashboard.'}</p>
</div>
<div class="app-card-footer">
<span class="meta-tag">${app.steps || 'Gemini 2.0'}</span>
<div style="display: flex; gap: 6px;">
<button class="btn-embed-chip" onclick="openEmbedModal('${app.id}')" title="Embed Code">
đŸ“Ļ Embed
</button>
<button class="btn-embed-chip" onclick="openAppRunner('${app.id}')" style="background: rgba(139, 92, 246, 0.2); color: #C084FC;">
⚡ Run
</button>
</div>
</div>
</div>
`).join('');
}
// --- Visual Mini-App Builder ---
function initMiniAppBuilder() {
const nameInput = document.getElementById('builderAppName');
const emojiInput = document.getElementById('builderAppEmoji');
const catSelect = document.getElementById('builderCategory');
const modelSelect = document.getElementById('builderModel');
const descInput = document.getElementById('builderDesc');
const f1Input = document.getElementById('builderField1');
const f2Input = document.getElementById('builderField2');
const templateInput = document.getElementById('builderPromptTemplate');
const saveBtn = document.getElementById('saveMiniAppBtn');
const testBtn = document.getElementById('testMiniAppBtn');
const statusMsg = document.getElementById('builderStatusMsg');
if (!saveBtn) return;
saveBtn.addEventListener('click', () => {
const name = nameInput.value.trim();
if (!name) {
alert('Please enter a Mini-App name');
return;
}
const newApp = {
id: 'custom_' + Date.now(),
name: name,
emoji: emojiInput.value.trim() || '💎',
category: catSelect.value,
model: modelSelect.value,
badge: 'CUSTOM APP',
desc: descInput.value.trim() || 'Custom client dashboard mini-app.',
steps: 'Custom â€ĸ Gemini AI',
fields: [
{ id: 'field1', label: f1Input.value.trim() || 'Field 1', placeholder: 'Enter ' + (f1Input.value.trim() || 'Field 1') },
{ id: 'field2', label: f2Input.value.trim() || 'Field 2', placeholder: 'Enter ' + (f2Input.value.trim() || 'Field 2') }
],
promptTemplate: templateInput.value.trim(),
createdAt: new Date().toISOString()
};
const existing = getCustomMiniApps();
existing.unshift(newApp);
localStorage.setItem('opal_custom_apps', JSON.stringify(existing));
statusMsg.classList.remove('hidden');
statusMsg.style.color = '#10B981';
statusMsg.textContent = `🎉 Mini-App "${name}" successfully saved & added to Hub!`;
renderMiniApps();
setTimeout(() => {
switchToTab('tabApps');
statusMsg.classList.add('hidden');
}, 1200);
});
if (testBtn) {
testBtn.addEventListener('click', () => {
const name = nameInput.value.trim() || 'Test Mini-App';
const tempApp = {
id: 'test_temp',
name: name,
emoji: emojiInput.value.trim() || 'đŸ§Ē',
desc: descInput.value.trim() || 'Testing custom app runner',
fields: [
{ id: 'field1', label: f1Input.value.trim() || 'Field 1', placeholder: 'Input 1' },
{ id: 'field2', label: f2Input.value.trim() || 'Field 2', placeholder: 'Input 2' }
],
promptTemplate: templateInput.value.trim(),
model: modelSelect.value
};
launchRunnerWithApp(tempApp);
});
}
}
// --- Interactive Mini-App Runner (Modal) ---
let currentRunningApp = null;
window.openAppRunner = function(appId) {
if (appId === 'gt_studio') {
switchToTab('tabPosterLab');
return;
}
if (appId === 'inbox_ai') {
switchToTab('tabInboxAI');
return;
}
const allApps = [...PREBUILT_MINI_APPS, ...getCustomMiniApps()];
const app = allApps.find(a => a.id === appId);
if (!app) return;
launchRunnerWithApp(app);
};
function launchRunnerWithApp(app) {
currentRunningApp = app;
const modal = document.getElementById('appRunnerModal');
const title = document.getElementById('runnerModalTitle');
const desc = document.getElementById('runnerModalDesc');
const formBox = document.getElementById('runnerDynamicForm');
const resultBox = document.getElementById('runnerResultBox');
const statusMsg = document.getElementById('runnerStatusMsg');
title.innerHTML = `${app.emoji || '🚀'} ${app.name}`;
desc.textContent = app.desc || '';
resultBox.classList.add('hidden');
statusMsg.classList.add('hidden');
const fields = app.fields || [
{ id: 'field1', label: 'Input 1', placeholder: 'Enter details...' },
{ id: 'field2', label: 'Input 2', placeholder: 'Enter additional context...' }
];
formBox.innerHTML = fields.map((f, i) => `
<div class="form-group">
<label for="runner_${f.id}">${f.label}:</label>
<input type="text" id="runner_${f.id}" placeholder="${f.placeholder}" class="runner-input" data-var="${f.id}">
</div>
`).join('');
modal.classList.remove('hidden');
}
window.closeAppRunner = function() {
document.getElementById('appRunnerModal').classList.add('hidden');
currentRunningApp = null;
};
// Handle Mini-App Execution
document.addEventListener('DOMContentLoaded', () => {
const execBtn = document.getElementById('runnerExecuteBtn');
if (execBtn) {
execBtn.addEventListener('click', async () => {
if (!currentRunningApp) return;
const statusMsg = document.getElementById('runnerStatusMsg');
const resultBox = document.getElementById('runnerResultBox');
const resultContent = document.getElementById('runnerResultContent');
statusMsg.classList.remove('hidden');
statusMsg.style.color = '#C084FC';
statusMsg.textContent = 'âŗ Processing with Google Gemini 2.0 AI...';
execBtn.disabled = true;
// Extract form values
const inputs = document.querySelectorAll('.runner-input');
let prompt = currentRunningApp.promptTemplate || 'Generate helpful response for the user.';
inputs.forEach(input => {
const varName = input.getAttribute('data-var');
const val = input.value.trim() || 'N/A';
const regex = new RegExp(`{{${varName}}}`, 'g');
prompt = prompt.replace(regex, val);
});
const apiKey = localStorage.getItem('opal_gemini_api_key') || '';
const model = currentRunningApp.model || localStorage.getItem('opal_selected_model') || 'gemini-2.0-flash';
try {
let output = '';
if (apiKey) {
output = await callGeminiApi(apiKey, model, prompt);
} else {
// Dynamic intelligent fallback
output = `✨ [Google Opal AI Offline Mode]\n\n` +
`Generated Output for: ${currentRunningApp.name}\n` +
`----------------------------------------\n` +
`ā§§. āĻ…āĻĢāĻžāϰ āĻ“ āĻ•āĻžāĻ¸ā§āϟāĻŽāĻžāϰ āϰ⧇āϏāĻĒāĻ¨ā§āϏ āĻĒā§āϰāĻ¸ā§āϤ⧁āϤ āĻ•āϰāĻž āĻšā§Ÿā§‡āϛ⧇āĨ¤\n` +
`⧍. ā§§ā§Ļā§Ļ% āĻœā§‡āύ⧁āχāύ āĻ•ā§‹ā§ŸāĻžāϞāĻŋāϟāĻŋ āĻ“ āϏ⧁āĻĒāĻžāϰāĻĢāĻžāĻ¸ā§āϟ āĻĄā§‡āϞāĻŋāĻ­āĻžāϰāĻŋ āύāĻŋāĻļā§āϚāĻŋāϤ āĻ•āϰ⧁āύāĨ¤\n` +
`ā§Š. āϏāϰāĻžāϏāϰāĻŋ āϝ⧋āĻ—āĻžāϝ⧋āĻ—: https://wa.me/8801712426871\n\n` +
`💡 āϟāĻŋāĻĒāϏ: Settings-āĻ āĻ—āĻŋā§Ÿā§‡ āφāĻĒāύāĻžāϰ Google Gemini API Key āĻŦāϏāĻžāϞ⧇ āϰāĻŋā§Ÿā§‡āϞ-āϟāĻžāχāĻŽ āφāύāϞāĻŋāĻŽāĻŋāĻŸā§‡āĻĄ āϞāĻžāχāĻ­ āϰ⧇āϏāĻĒāĻ¨ā§āϏ āĻĒāĻžāĻŦ⧇āύ!`;
}
resultContent.textContent = output.trim();
resultBox.classList.remove('hidden');
statusMsg.classList.add('hidden');
// Save to History
saveGenerationToHistory({
type: currentRunningApp.name,
title: currentRunningApp.name,
price: 'AI Generation',
date: new Date().toLocaleTimeString()
});
} catch (err) {
statusMsg.style.color = '#EF4444';
statusMsg.textContent = `❌ Error: ${err.message}`;
}
execBtn.disabled = false;
});
}
});
// --- 1-Click Client Dashboard Embed Modal ---
window.openEmbedModal = function(appId) {
const modal = document.getElementById('embedModal');
const title = document.getElementById('embedModalTitle');
const iframeBox = document.getElementById('embedIframeCode');
const apiUrlBox = document.getElementById('embedApiUrl');
const allApps = [...PREBUILT_MINI_APPS, ...getCustomMiniApps()];
const app = allApps.find(a => a.id === appId) || { name: 'Opal Mini-App', id: appId };
title.innerHTML = `đŸ“Ļ Embed "${app.name}" in Client Dashboard`;
const baseUrl = 'https://opal.auraajenticai.cloud';
iframeBox.textContent = `<iframe src="${baseUrl}/?app=${app.id}" width="100%" height="650" style="border:none; border-radius:18px; box-shadow:0 8px 32px rgba(0,0,0,0.4);" allow="clipboard-write"></iframe>`;
apiUrlBox.textContent = `${baseUrl}/api/v1/mini-app/run?app_id=${app.id}`;
modal.classList.remove('hidden');
};
window.closeEmbedModal = function() {
document.getElementById('embedModal').classList.add('hidden');
};
// --- Masterpiece 3D Canvas Poster Engine ---
let selectedStageTheme = 'obsidian_gold';
function initPosterLab() {
const productInput = document.getElementById('productInput');
const productPrice = document.getElementById('productPrice');
const customBadgeInput = document.getElementById('customBadgeInput');
const aspectRatioSelect = document.getElementById('aspectRatioSelect');
const brandSelect = document.getElementById('brandWatermarkSelect');
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 canvasContainer = document.getElementById('canvasContainer');
const downloadPosterBtn = document.getElementById('downloadPosterBtn');
const shareWhatsAppBtn = document.getElementById('shareWhatsAppBtn');
// Stage Theme Selector
const themeChips = document.querySelectorAll('#themeSelectorGroup .theme-chip');
themeChips.forEach(chip => {
chip.addEventListener('click', () => {
themeChips.forEach(c => c.classList.remove('active'));
chip.classList.add('active');
selectedStageTheme = chip.getAttribute('data-theme');
});
});
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();
const badgeText = customBadgeInput ? customBadgeInput.value.trim() : '🇰🇷 100% Original Korea';
const aspect = aspectRatioSelect ? aspectRatioSelect.value : '1:1';
const brand = brandSelect ? brandSelect.value : 'glamourstouch';
posterStatusText.classList.remove('hidden');
posterStatusText.style.color = '#F59E0B';
posterStatusText.textContent = 'âŗ Rendering 3D stage with GPU canvas blend...';
generatePosterBtn.disabled = true;
try {
let productImg = attachedImageBitmap;
let title = "Authentic Korean K-Beauty Care";
if (!productImg) {
const resolved = await resolveProductUrl(inputVal);
if (resolved && resolved.imageUrl) {
title = resolved.title || inputVal;
productImg = await loadImage(resolved.imageUrl);
} else {
const fallbackUrl = `https://images.unsplash.com/photo-1556228720-195a672e8a03?w=800&h=800&fit=crop`;
productImg = await loadImage(fallbackUrl);
title = inputVal;
}
}
if (productImg) {
if (aspect === '9:16') {
posterCanvas.width = 1080;
posterCanvas.height = 1920;
canvasContainer.classList.add('aspect-reels');
} else {
posterCanvas.width = 1080;
posterCanvas.height = 1080;
canvasContainer.classList.remove('aspect-reels');
}
await render3DCommercialPoster(posterCanvas, productImg, title, priceVal, badgeText, selectedStageTheme, aspect, brand);
posterResultCard.classList.remove('hidden');
posterStatusText.classList.add('hidden');
posterResultCard.scrollIntoView({ behavior: 'smooth' });
saveGenerationToHistory({
type: '3D Poster',
title: title,
price: priceVal,
date: new Date().toLocaleTimeString()
});
} else {
posterStatusText.textContent = '❌ Failed to load product image.';
}
} catch (err) {
posterStatusText.textContent = `❌ Error: ${err.message}`;
}
generatePosterBtn.disabled = false;
});
// Download
downloadPosterBtn.addEventListener('click', () => {
const link = document.createElement('a');
link.download = `Opal_3D_Poster_${Date.now()}.jpg`;
link.href = posterCanvas.toDataURL('image/jpeg', 0.95);
link.click();
});
// WhatsApp
shareWhatsAppBtn.addEventListener('click', () => {
const text = encodeURIComponent(`✨ 100% Authentic Product Order!\nProduct: ${productInput.value}\nPrice: ${productPrice.value}\nWebsite: https://glamourstouch.com`);
window.open(`https://wa.me/8801712426871?text=${text}`, '_blank');
});
// n8n Auto-Post 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 configure n8n Webhook URL in Settings first!');
switchToTab('tabSettings');
return;
}
n8nPostStatusMsg.classList.remove('hidden');
n8nPostStatusMsg.style.color = '#38BDF8';
n8nPostStatusMsg.textContent = 'âŗ Sending 3D poster & copy to n8n Webhook...';
autoPostN8nBtn.disabled = true;
try {
const posterDataUrl = posterCanvas.toDataURL('image/jpeg', 0.92);
const payload = {
title: productInput.value.trim() || 'Authentic 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 Resolver
async function resolveProductUrl(input) {
if (!input) return null;
if (input.match(/\.(jpeg|jpg|gif|png|webp)($|\?)/i)) {
return { title: 'Authentic Product', imageUrl: input };
}
const key = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImZtY2x0cmpudXZ1b29hcmt2dWZuIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NzUxMzY3MDQsImV4cCI6MjA5MDcxMjcwNH0.PkSgBAZx41X4sZurfyOdxCVa01hkKTkyBhVkGzx_4y4';
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) {
try {
const res = await fetch(`https://fmcltrjnuvuooarkvufn.supabase.co/rest/v1/products?id=eq.${uuidMatch[0]}&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);
}
}
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);
}
return { title: input, imageUrl: 'https://images.unsplash.com/photo-1556228720-195a672e8a03?w=800&h=800&fit=crop' };
}
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'));
fallbackImg.src = src;
};
img.src = loadUrl;
});
}
// --- 3D Canvas Multi-Theme Rendering Engine ---
async function render3DCommercialPoster(canvas, productImg, title, price, badgeText, theme = 'obsidian_gold', aspect = '1:1', brand = 'glamourstouch') {
const ctx = canvas.getContext('2d');
const w = canvas.width;
const h = canvas.height;
// Theme palettes
const THEMES = {
obsidian_gold: {
bgCenter: '#18212D', bgMid: '#0C1117', bgOuter: '#05080C',
spotlightColor: '#FFFFFF', goldRim: '#E5B83A', ringColor: '#66E5B83A',
pedestalTop: '#FFFFFF', pedestalMid: '#EBF2F8', pedestalDark: '#1B2533',
headerTitle: '✨ GLAMOUR\'S TOUCH ✨', headerSub: '대한ë¯ŧęĩ­ 프ëĻŦë¯¸ė—„ ėŠ¤í‚¨āϕ⧇āϝāĻŧāĻžāϰ â€ĸ 100% AUTHENTIC KOREAN COSMETICS'
},
rose_quartz: {
bgCenter: '#2D1824', bgMid: '#170C13', bgOuter: '#0C0509',
spotlightColor: '#FFF0F5', goldRim: '#F472B6', ringColor: '#F472B666',
pedestalTop: '#FFF5F8', pedestalMid: '#FCE7F3', pedestalDark: '#371829',
headerTitle: '🌸 SNEHALATA AURA 🌸', headerSub: 'PREMIUM LUXURY LIFESTYLE â€ĸ 100% AUTHENTIC BEAUTY'
},
cyber_neon: {
bgCenter: '#0F172A', bgMid: '#060E1A', bgOuter: '#02060D',
spotlightColor: '#E0F2FE', goldRim: '#06B6D4', ringColor: '#06B6D466',
pedestalTop: '#F0FDF4', pedestalMid: '#CFFAFE', pedestalDark: '#082F49',
headerTitle: '💎 AURA ENTERPRISE 💎', headerSub: 'NEXT-GEN AGENTIC COMMERCE â€ĸ AI EMPOWERED'
},
botanical_eco: {
bgCenter: '#14281E', bgMid: '#0B1711', bgOuter: '#040B07',
spotlightColor: '#F0FDF4', goldRim: '#10B981', ringColor: '#10B98166',
pedestalTop: '#F7FEE7', pedestalMid: '#DCFCE7', pedestalDark: '#064E3B',
headerTitle: 'đŸŒŋ PURE BOTANICAL CARE đŸŒŋ', headerSub: 'ORGANIC DERMATOLOGY â€ĸ CLINICALLY PROVEN'
}
};
const currentTheme = THEMES[theme] || THEMES.obsidian_gold;
// 1. Background Radial Gradient
const stageCy = aspect === '9:16' ? h * 0.46 : h * 0.44;
const bgGrad = ctx.createRadialGradient(w / 2, stageCy, 50, w / 2, stageCy, w * 0.75);
bgGrad.addColorStop(0, currentTheme.bgCenter);
bgGrad.addColorStop(0.6, currentTheme.bgMid);
bgGrad.addColorStop(1, currentTheme.bgOuter);
ctx.fillStyle = bgGrad;
ctx.fillRect(0, 0, w, h);
// 2. Spotlight
const stageCx = w / 2;
const stageRadius = w * 0.38;
const spotGrad = ctx.createRadialGradient(stageCx, stageCy, 0, stageCx, stageCy, stageRadius);
spotGrad.addColorStop(0, '#FFFFFF');
spotGrad.addColorStop(0.48, '#FFFFFF');
spotGrad.addColorStop(0.68, currentTheme.spotlightColor);
spotGrad.addColorStop(0.85, '#64748B');
spotGrad.addColorStop(1, 'transparent');
ctx.fillStyle = spotGrad;
ctx.beginPath();
ctx.arc(stageCx, stageCy, stageRadius, 0, Math.PI * 2);
ctx.fill();
// Glow Rings
ctx.strokeStyle = currentTheme.ringColor;
ctx.lineWidth = 2.5;
ctx.beginPath();
ctx.arc(stageCx, stageCy, stageRadius * 0.96, 0, Math.PI * 2);
ctx.stroke();
ctx.setLineDash([12, 12]);
ctx.beginPath();
ctx.arc(stageCx, stageCy, stageRadius * 0.91, 0, Math.PI * 2);
ctx.stroke();
ctx.setLineDash([]);
// 3. 3D Dual-Tier Podium
const baseTierY = aspect === '9:16' ? 1220 : 745;
const baseTierRx = w * 0.38;
const baseTierRy = 34;
const baseTierHeight = 26;
// Base 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);
// Base Cylinder Body
const baseCylGrad = ctx.createLinearGradient(stageCx - baseTierRx, 0, stageCx + baseTierRx, 0);
baseCylGrad.addColorStop(0, '#090D12');
baseCylGrad.addColorStop(0.5, '#475569');
baseCylGrad.addColorStop(1, '#090D12');
ctx.fillStyle = baseCylGrad;
ctx.fillRect(stageCx - baseTierRx, baseTierY, baseTierRx * 2, baseTierHeight);
drawOval(ctx, stageCx, baseTierY + baseTierHeight, baseTierRx, baseTierRy);
drawOval(ctx, stageCx, baseTierY, baseTierRx, baseTierRy);
// Top Tier
const topTierY = aspect === '9:16' ? 1180 : 708;
const topTierRx = w * 0.31;
const topTierRy = 28;
const topTierHeight = 34;
const topCylGrad = ctx.createLinearGradient(stageCx - topTierRx, 0, stageCx + topTierRx, 0);
topCylGrad.addColorStop(0, '#070A0E');
topCylGrad.addColorStop(0.5, currentTheme.goldRim);
topCylGrad.addColorStop(1, '#070A0E');
ctx.fillStyle = topCylGrad;
ctx.fillRect(stageCx - topTierRx, topTierY, topTierRx * 2, topTierHeight);
drawOval(ctx, stageCx, topTierY + topTierHeight, topTierRx, topTierRy);
// Top Surface Cap
const topSurfaceGrad = ctx.createRadialGradient(stageCx, topTierY, 0, stageCx, topTierY, topTierRx);
topSurfaceGrad.addColorStop(0, currentTheme.pedestalTop);
topSurfaceGrad.addColorStop(0.5, currentTheme.pedestalMid);
topSurfaceGrad.addColorStop(1, currentTheme.pedestalDark);
ctx.fillStyle = topSurfaceGrad;
drawOval(ctx, stageCx, topTierY, topTierRx, topTierRy);
// Gold Beveled Rim
ctx.strokeStyle = currentTheme.goldRim;
ctx.lineWidth = 3.5;
strokeOval(ctx, stageCx, topTierY, topTierRx, topTierRy);
// 4. Product Scaling & Placement
const targetH = aspect === '9:16' ? 560 : 500;
const targetW = aspect === '9:16' ? 580 : 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;
// Floor Reflection (Multiply blend)
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);
// Product Image (Multiply blended to wipe white background)
ctx.save();
ctx.globalCompositeOperation = 'multiply';
ctx.drawImage(productImg, destLeft, destTop, destW, destH);
ctx.restore();
// 5. Value Trust Badges
const badgeY1 = aspect === '9:16' ? 340 : 240;
const badgeY2 = badgeY1 + 75;
drawPillBadge(ctx, 40, badgeY1, badgeText, true, currentTheme.goldRim);
drawPillBadge(ctx, 40, badgeY2, 'đŸŒŋ Skin Friendly Formula', true, currentTheme.goldRim);
drawPillBadge(ctx, w - 40, badgeY1, '⚡ Fast Delivery BD', false, currentTheme.goldRim);
drawPillBadge(ctx, w - 40, badgeY2, 'đŸ›Ąī¸ Authentic Guarantee', false, currentTheme.goldRim);
// 6. Header Ribbon
ctx.fillStyle = '#F2090D12';
ctx.fillRect(0, 0, w, 92);
ctx.strokeStyle = currentTheme.goldRim;
ctx.lineWidth = 3;
ctx.beginPath();
ctx.moveTo(0, 92);
ctx.lineTo(w, 92);
ctx.stroke();
ctx.fillStyle = currentTheme.goldRim;
ctx.font = 'bold 32px Plus Jakarta Sans, sans-serif';
ctx.textAlign = 'center';
ctx.fillText(currentTheme.headerTitle, w / 2, 52);
ctx.fillStyle = '#FFF4D0';
ctx.font = '16px Plus Jakarta Sans, sans-serif';
ctx.fillText(currentTheme.headerSub, w / 2, 80);
// 7. Product Title Card
const cleanTitle = title.length > 60 ? title.substring(0, 58) + '...' : title;
const titleCardTop = h - (aspect === '9:16' ? 320 : 235);
const titleCardBottom = h - (aspect === '9:16' ? 150 : 128);
ctx.fillStyle = '#EA0B0E14';
roundRect(ctx, 28, titleCardTop, w - 56, titleCardBottom - titleCardTop, 18);
ctx.fill();
ctx.strokeStyle = currentTheme.goldRim + '88';
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 = currentTheme.goldRim;
ctx.font = 'bold 21px Hind Siliguri, sans-serif';
ctx.fillText(`${price} â€ĸ ā§§ā§Ļā§Ļ% āφāϏāϞ āĻ—ā§āϝāĻžāϰāĻžāĻ¨ā§āϟāĻŋ â€ĸ āĻĻā§āϰ⧁āϤ āĻšā§‹āĻŽ āĻĄā§‡āϞāĻŋāĻ­āĻžāϰāĻŋ`, w / 2, titleCardTop + 80);
// 8. Bottom WhatsApp Banner
const footerHeight = aspect === '9:16' ? 140 : 118;
const footerTop = h - footerHeight;
const footerGrad = ctx.createLinearGradient(0, footerTop, w, h);
footerGrad.addColorStop(0, '#25D366');
footerGrad.addColorStop(1, '#1EB855');
ctx.fillStyle = footerGrad;
ctx.fillRect(0, footerTop, w, footerHeight);
ctx.fillStyle = '#FFFFFF';
ctx.font = 'bold 32px Plus Jakarta Sans, sans-serif';
ctx.textAlign = 'left';
ctx.fillText('đŸ’Ŧ WhatsApp Order: 01712-426871', 35, footerTop + 48);
ctx.fillStyle = '#053314';
ctx.font = 'bold 20px Plus Jakarta Sans, sans-serif';
ctx.fillText('🌐 Website: glamourstouch.com â€ĸ 📞 Hotline: 01712-426871', 35, footerTop + 90);
// QR Code
await drawWhatsAppQrOnCanvas(ctx, w - 130, footerTop + 11, 96);
// Outer Border
ctx.strokeStyle = currentTheme.goldRim + '99';
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, goldColor = '#E5B83A') {
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 = goldColor + '66';
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) {
ctx.fillStyle = '#FFFFFF';
roundRect(ctx, x - 4, y - 4, size + 8, size + 8, 10);
ctx.fill();
const qrContainer = document.getElementById('qrcodeHidden');
if (qrContainer) {
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);
}
}
}
// --- Customer Care & Inbox AI Dedicated Studio ---
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');
if (!generateBtn) return;
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-2.0-flash';
if (!query) {
alert('Please enter a customer message');
return;
}
statusMsg.classList.remove('hidden');
statusMsg.textContent = 'âŗ Generating 3 sweet & polite sales replies with Gemini AI...';
generateBtn.disabled = true;
try {
if (apiKey) {
const prompt = `A customer sent this message in e-commerce 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 {
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;
});
}
// --- History Management ---
function initHistory() {
const historyContainer = document.getElementById('historyListContainer');
const clearBtn = document.getElementById('clearHistoryBtn');
function renderHistory() {
if (!historyContainer) return;
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 mini-app generation!</p>
</div>`;
return;
}
historyContainer.innerHTML = list.map(item => `
<div class="app-card">
<div class="app-card-top">
<div class="app-icon">${item.type === '3D Poster' ? '🎨' : '⚡'}</div>
<span class="tag-gem">${item.type}</span>
</div>
<h3>${item.title}</h3>
<p>${item.price || item.type}</p>
<div class="app-card-footer">
<span class="meta-tag">Saved at ${item.date}</span>
</div>
</div>
`).join('');
}
if (clearBtn) {
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)));
}