From b85c5cf065485e1d795864e0e8533fc7af5ba380 Mon Sep 17 00:00:00 2001 From: Amirul Islam Redwan71 Date: Tue, 1 Sep 2026 09:01:09 +0600 Subject: [PATCH] Initial release of Google Opal AI iOS Web Studio --- Dockerfile | 20 ++ app.js | 731 +++++++++++++++++++++++++++++++++++++++++++++ docker-compose.yml | 11 + index.html | 318 ++++++++++++++++++++ manifest.json | 16 + nginx.conf | 36 +++ serve.js | 71 +++++ serve.py | 40 +++ style.css | 557 ++++++++++++++++++++++++++++++++++ sw.js | 39 +++ 10 files changed, 1839 insertions(+) create mode 100644 Dockerfile create mode 100644 app.js create mode 100644 docker-compose.yml create mode 100644 index.html create mode 100644 manifest.json create mode 100644 nginx.conf create mode 100644 serve.js create mode 100644 serve.py create mode 100644 style.css create mode 100644 sw.js diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..a31acd8 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,20 @@ +# Lightweight Alpine Nginx for Opal iOS Web App +FROM nginx:alpine + +# Remove default nginx static assets +RUN rm -rf /usr/share/nginx/html/* + +# Copy custom nginx configuration +COPY nginx.conf /etc/nginx/conf.d/default.conf + +# Copy all web assets +COPY index.html /usr/share/nginx/html/ +COPY style.css /usr/share/nginx/html/ +COPY app.js /usr/share/nginx/html/ +COPY manifest.json /usr/share/nginx/html/ +COPY sw.js /usr/share/nginx/html/ + +# Expose port 80 for Coolify Traefik reverse proxy +EXPOSE 80 + +CMD ["nginx", "-g", "daemon off;"] diff --git a/app.js b/app.js new file mode 100644 index 0000000..e31a911 --- /dev/null +++ b/app.js @@ -0,0 +1,731 @@ +// 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 Resolver +async function resolveProductUrl(input) { + if (input.match(/\.(jpeg|jpg|gif|png|webp)($|\?)/i)) { + return { title: 'Authentic Korean Product', imageUrl: input }; + } + + // 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': 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImZtY2x0cmpudXZ1b29hcmt2dWZuIiwicm9sZSI6ImFub24iLCJpYXQiOjE3MjY3MjEzNzgsImV4cCI6MjA0MjI5NzM3OH0.528YkG1V7c8UoK5QvW_V1hS0W1oQ5W_V1hS0W1oQ5W8', + 'Authorization': 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImZtY2x0cmpudXZ1b29hcmt2dWZuIiwicm9sZSI6ImFub24iLCJpYXQiOjE3MjY3MjEzNzgsImV4cCI6MjA0MjI5NzM3OH0.528YkG1V7c8UoK5QvW_V1hS0W1oQ5W_V1hS0W1oQ5W8' + } + }); + const data = await res.json(); + if (data && data.length > 0) { + const p = data[0]; + let imgUrl = p.image_url || ''; + if (p.images && p.images.length > 0) imgUrl = p.images[0]; + return { + title: p.name || 'Authentic Korean Skincare', + imageUrl: imgUrl + }; + } + } catch (e) { + console.warn('GT Supabase fetch failed:', e); + } + } + + return { title: input, imageUrl: input }; +} + +function loadImage(src) { + return new Promise((resolve, reject) => { + const img = new Image(); + img.crossOrigin = 'anonymous'; + img.onload = () => resolve(img); + img.onerror = (e) => reject(new Error('Could not load image across CORS')); + img.src = src; + }); +} + +// --- 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 = 550; + const targetW = 540; + const srcW = productImg.width; + const srcH = productImg.height; + 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 + ctx.save(); + ctx.globalAlpha = 0.20; + 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 + ctx.drawImage(productImg, destLeft, destTop, destW, destH); + + // 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 = ` +
+ 📭 +

No generations yet. Create your first 3D poster or copy!

+
`; + return; + } + + historyContainer.innerHTML = list.map(item => ` +
+
${item.type === '3D Poster' ? '🎨' : 'đŸ’Ŧ'}
+
+

${item.title}

+

${item.price || item.type}

+ Saved at ${item.date} +
+
+ `).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))); +} diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..7af69e1 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,11 @@ +version: '3.8' + +services: + opal-ios-studio: + build: . + restart: always + container_name: opal-ios-studio + ports: + - "80" + environment: + - NODE_ENV=production diff --git a/index.html b/index.html new file mode 100644 index 0000000..d250bef --- /dev/null +++ b/index.html @@ -0,0 +1,318 @@ + + + + + + Google Opal AI - iOS Studio & 3D Poster Lab + + + + + + + + + + + + + + + + + + + +
+
+
💎
+
+

Opal AI Studio

+

Google DeepMind â€ĸ iOS Edition

+
+
+
+ +
+
+ + +
+ + +
+
+
+ 🎨 +
+

3D Commercial Poster Lab

+

āφāϏāϞ āĻĒā§āϰāĻĄāĻžāĻ•ā§āĻŸā§‡āϰ āĻ›āĻŦāĻŋ, ā§ŠāĻĄāĻŋ āĻŽāĻžāĻ°ā§āĻŦ⧇āϞ āĻĒā§‹āĻĄāĻŋ⧟āĻžāĻŽ āĻ“ WhatsApp QR āϏāĻš āĻŦāĻŋāĻœā§āĻžāĻžāĻĒāύ āĻĒā§‹āĻ¸ā§āϟāĻžāϰ āĻŦāĻžāύāĻžāύ

+
+
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ + + + +
+ + + +
+ + +
+
+

✨ Curated Opal Mini-Apps

+ 6 Apps +
+ +
+ +
+
✨
+
+
+

Glamour's Touch 4K K-Beauty Studio

+ GEM +
+

glamourstouch.com āĻāϰ ā§§ā§Ļā§Ļ% āĻ…āϰāĻŋāϜāĻŋāύāĻžāϞ āϕ⧋āϰāĻŋ⧟āĻžāύ āĻ¸ā§āĻ•āĻŋāύāϕ⧇āϝāĻŧāĻžāϰ⧇āϰ āĻ•āĻŽāĻžāĻ°ā§āĻļāĻŋ⧟āĻžāϞ āĻ›āĻŦāĻŋ āĻ“ āĻĒā§‹āĻ¸ā§āϟ āĻŦāĻžāύāĻžā§ŸāĨ¤

+ 7 Steps â€ĸ Business +
+
+ + +
+
đŸ’Ŧ
+
+
+

Customer Care & Inbox Auto-Reply

+ AI +
+

āĻĢ⧇āϏāĻŦ⧁āĻ• āĻŽā§‡āϏ⧇āĻžā§āϜāĻžāϰ āĻ“ āĻšā§‹ā§ŸāĻžāϟāϏāĻ…ā§āϝāĻžāĻĒ⧇ āĻ•āĻžāĻ¸ā§āϟāĻŽāĻžāϰ⧇āϰ āĻĒā§āϰāĻļā§āύ⧇āϰ ā§ŠāϟāĻŋ āχāύāĻ¸ā§āĻŸā§āϝāĻžāĻ¨ā§āϟ āĻŦāĻŋāύāĻŽā§āϰ āϏ⧇āϞāϏ āϰāĻŋāĻĒā§āϞāĻžāχ āϤ⧈āϰāĻŋ āĻ•āϰ⧇āĨ¤

+ 3 Steps â€ĸ Sales +
+
+ + +
+
đŸ›ī¸
+
+

E-Commerce Product Launch Kit

+

āϝ⧇āϕ⧋āύ⧋ āĻĒāĻŖā§āϝ⧇āϰ 4K āĻ•āĻŽāĻžāĻ°ā§āĻļāĻŋ⧟āĻžāϞ āĻ›āĻŦāĻŋ āĻ“ āĻĢ⧇āϏāĻŦ⧁āĻ• āϞāĻžā§āϚ āĻ•āĻĒāĻŋ āϤ⧈āϰāĻŋ āĻ•āϰ⧇āĨ¤

+ 5 Steps â€ĸ Business +
+
+ + +
+
đŸ”Ĩ
+
+

Viral Social Post & Reels Script

+

āĻĢ⧇āϏāĻŦ⧁āĻ•, āϟāĻŋāĻ•āϟāĻ• āĻ“ āχāύāĻ¸ā§āϟāĻžāĻ—ā§āϰāĻžāĻŽā§‡āϰ āĻ­āĻžāχāϰāĻžāϞ āĻšā§āĻ• āĻ“ āĻ­āĻŋāĻĄāĻŋāĻ“ āĻ¸ā§āĻ•ā§āϰāĻŋāĻĒā§āϟ āϤ⧈āϰāĻŋ āĻ•āϰ⧇āĨ¤

+ 4 Steps â€ĸ Marketing +
+
+
+
+ + +
+
+
+ đŸ’Ŧ +
+

Glamour's Touch Customer Care & Inbox AI

+

āĻ•āĻžāĻ¸ā§āϟāĻŽāĻžāϰ⧇āϰ āĻŽā§‡āϏ⧇āϜ āĻĻāĻŋāϞ⧇ ā§§-āĻ•ā§āϞāĻŋāϕ⧇ ā§ŠāϟāĻŋ āĻĒāĻžāϰāĻĢ⧇āĻ•ā§āϟ āĻ“ āĻŦāĻŋāύāĻŽā§āϰ āωāĻ¤ā§āϤāϰ āϤ⧈āϰāĻŋ āĻ•āϰ⧇

+
+
+ +
+ + +
+ +
+ + +
+ + + +
+ + + +
+ + +
+
+

📜 Saved History

+ +
+
+
+ 📭 +

No generations yet. Create your first 3D poster or copy!

+
+
+
+ + +
+
+
+ âš™ī¸ +
+

Settings & Free AI Engine

+

āϗ⧁āĻ—āϞ āĻœā§‡āĻŽāĻŋāύāĻŋ āĻ“ āĻ—ā§āϝāĻžāĻŽāĻž āĻ“āĻĒ⧇āύ āĻŽāĻĄā§‡āϞ āϏ⧇āϟāφāĻĒ

+
+
+ +
+ + + API Key is saved securely in your iPhone's local storage. +
+ +
+ + +
+ +
+ + + Pre-configured with your n8n cloud instance at n8n.auraajenticai.cloud +
+ + + +
+ +
+

💎 About Google Opal AI (iOS Edition)

+

This web studio is engineered specifically for **iPhone (iOS Safari / PWA)** with 0ms latency, native 2D canvas GPU rendering, and official Google Gemma & Gemini AI integration.

+
+ 📲 iPhone āϟāĻŋāĻĒāϏ: Safari-āĻāϰ āύāĻŋāĻšā§‡ āĻĨāĻžāĻ•āĻž Share (📤) āĻŦāĻžāϟāύ⧇ āĻŸā§āϝāĻžāĻĒ āĻ•āϰ⧇ "Add to Home Screen" āϚāĻžāĻĒ⧁āύāĨ¤ āϤāĻžāĻšāϞ⧇ āĻāϟāĻŋ āφāĻĒāύāĻžāϰ āφāχāĻĢā§‹āύ⧇ āĻāĻ•āĻĻāĻŽ āφāϏāϞ āĻ…ā§āϝāĻžāĻĒ⧇āϰ āĻŽāϤ⧋ āĻĢ⧁āϞ-āĻ¸ā§āĻ•ā§āϰāĻŋāύ⧇ āϚāϞāĻŦ⧇! +
+
+
+ +
+ + + + + + + + + + + diff --git a/manifest.json b/manifest.json new file mode 100644 index 0000000..63b719e --- /dev/null +++ b/manifest.json @@ -0,0 +1,16 @@ +{ + "name": "Google Opal AI - Studio & 3D Poster Lab", + "short_name": "Opal AI", + "start_url": "./index.html", + "display": "standalone", + "background_color": "#0C1117", + "theme_color": "#0C1117", + "orientation": "portrait", + "icons": [ + { + "src": "data:image/svg+xml,💎", + "sizes": "192x192 512x512", + "type": "image/svg+xml" + } + ] +} diff --git a/nginx.conf b/nginx.conf new file mode 100644 index 0000000..346b4b0 --- /dev/null +++ b/nginx.conf @@ -0,0 +1,36 @@ +server { + listen 80; + server_name localhost; + + root /usr/share/nginx/html; + index index.html; + + # Gzip Compression + gzip on; + gzip_vary on; + gzip_min_length 1024; + gzip_proxied expired no-cache no-store private auth; + gzip_types text/plain text/css text/xml text/javascript application/x-javascript application/xml application/javascript application/json image/svg+xml; + + # iOS PWA & Security Headers + add_header X-Frame-Options "SAMEORIGIN"; + add_header X-Content-Type-Options "nosniff"; + add_header Access-Control-Allow-Origin "*"; + + location / { + try_files $uri $uri/ /index.html; + } + + # Cache Service Worker with no-cache so updates apply immediately + location = /sw.js { + add_header Cache-Control "no-cache, no-store, must-revalidate"; + add_header Pragma "no-cache"; + add_header Expires "0"; + } + + # Static assets cache + location ~* \.(css|js|json|svg|png|jpg|jpeg|ico)$ { + expires 7d; + add_header Cache-Control "public, max-age=604800, immutable"; + } +} diff --git a/serve.js b/serve.js new file mode 100644 index 0000000..13ba9ea --- /dev/null +++ b/serve.js @@ -0,0 +1,71 @@ +const http = require('http'); +const fs = require('fs'); +const path = require('path'); +const os = require('os'); + +const PORT = 8080; +const DIRECTORY = __dirname; + +const MIME_TYPES = { + '.html': 'text/html; charset=UTF-8', + '.css': 'text/css', + '.js': 'application/javascript', + '.json': 'application/json', + '.png': 'image/png', + '.jpg': 'image/jpeg', + '.jpeg': 'image/jpeg', + '.svg': 'image/svg+xml', + '.ico': 'image/x-icon' +}; + +function getLocalIp() { + const interfaces = os.networkInterfaces(); + for (const name of Object.keys(interfaces)) { + for (const iface of interfaces[name]) { + if (iface.family === 'IPv4' && !iface.internal) { + return iface.address; + } + } + } + return '127.0.0.1'; +} + +const server = http.createServer((req, res) => { + let reqPath = req.url.split('?')[0]; + if (reqPath === '/' || reqPath === '') reqPath = '/index.html'; + + const filePath = path.join(DIRECTORY, reqPath); + const ext = path.extname(filePath).toLowerCase(); + const contentType = MIME_TYPES[ext] || 'application/octet-stream'; + + fs.readFile(filePath, (err, content) => { + if (err) { + if (err.code === 'ENOENT') { + res.writeHead(404, { 'Content-Type': 'text/plain' }); + res.end('404 Not Found'); + } else { + res.writeHead(500, { 'Content-Type': 'text/plain' }); + res.end(`Server Error: ${err.code}`); + } + } else { + res.writeHead(200, { + 'Content-Type': contentType, + 'Access-Control-Allow-Origin': '*' + }); + res.end(content); + } + }); +}); + +server.listen(PORT, '0.0.0.0', () => { + const localIp = getLocalIp(); + console.log('='.repeat(60)); + console.log('💎 Google Opal AI - iOS Web Studio is Live & Running!'); + console.log('='.repeat(60)); + console.log(`📱 Open on your iPhone Safari (connected to same Wi-Fi):`); + console.log(` 👉 http://${localIp}:${PORT}`); + console.log(`đŸ’ģ Or on PC Browser: http://localhost:${PORT}`); + console.log('='.repeat(60)); + console.log('💡 iPhone Instruction: Safari-āϤ⧇ āĻ—āĻŋāϝāĻŧ⧇ "Share ➔ Add to Home Screen" āĻĻāĻŋāύāĨ¤'); + console.log('='.repeat(60)); +}); diff --git a/serve.py b/serve.py new file mode 100644 index 0000000..52b9fde --- /dev/null +++ b/serve.py @@ -0,0 +1,40 @@ +import http.server +import socketserver +import socket +import os + +PORT = 8080 +DIRECTORY = os.path.dirname(os.path.abspath(__file__)) + +class Handler(http.server.SimpleHTTPRequestHandler): + def __init__(self, *args, **kwargs): + super().__init__(*args, directory=DIRECTORY, **kwargs) + +def get_local_ip(): + s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + try: + s.connect(('8.8.8.8', 80)) + ip = s.getsockname()[0] + except Exception: + ip = '127.0.0.1' + finally: + s.close() + return ip + +if __name__ == '__main__': + local_ip = get_local_ip() + print("=" * 60) + print("💎 Google Opal AI - iOS Web Studio Server is Running!") + print("=" * 60) + print(f"📱 Open this URL in iPhone Safari (Same Wi-Fi network):") + print(f" 👉 http://{local_ip}:{PORT}") + print(f"đŸ’ģ Or on this PC: http://localhost:{PORT}") + print("=" * 60) + print("💡 iPhone Tips: Safari-āϤ⧇ āĻ—āĻŋāϝāĻŧ⧇ 'Share ➔ Add to Home Screen' āĻĻāĻŋāύāĨ¤") + print("=" * 60) + + with socketserver.TCPServer(("", PORT), Handler) as httpd: + try: + httpd.serve_forever() + except KeyboardInterrupt: + print("\nServer stopped.") diff --git a/style.css b/style.css new file mode 100644 index 0000000..9613356 --- /dev/null +++ b/style.css @@ -0,0 +1,557 @@ +:root { + --bg-dark: #0A0E14; + --card-bg: rgba(22, 28, 38, 0.85); + --card-border: rgba(255, 255, 255, 0.1); + --gold-primary: #E5B83A; + --gold-glow: rgba(229, 184, 58, 0.25); + --indigo-primary: #6366F1; + --emerald: #10B981; + --whatsapp: #25D366; + --text-primary: #FFFFFF; + --text-secondary: #94A3B8; + --text-muted: #64748B; + --radius-lg: 20px; + --radius-md: 12px; + --radius-sm: 8px; + --font-family: 'Plus Jakarta Sans', 'Hind Siliguri', -apple-system, BlinkMacSystemFont, sans-serif; +} + +* { + box-sizing: border-box; + margin: 0; + padding: 0; + -webkit-tap-highlight-color: transparent; +} + +body { + background-color: var(--bg-dark); + color: var(--text-primary); + font-family: var(--font-family); + min-height: 100vh; + padding-bottom: 90px; + display: flex; + flex-direction: column; + overflow-x: hidden; + user-select: none; +} + +/* Header */ +.app-header { + position: sticky; + top: 0; + z-index: 100; + background: rgba(10, 14, 20, 0.85); + backdrop-filter: blur(20px); + -webkit-backdrop-filter: blur(20px); + border-bottom: 1px solid var(--card-border); + padding: env(safe-area-inset-top, 12px) 16px 12px 16px; + display: flex; + justify-content: space-between; + align-items: center; +} + +.brand-group { + display: flex; + align-items: center; + gap: 10px; +} + +.logo-box { + width: 38px; + height: 38px; + background: linear-gradient(135deg, #6366F1, #06B6D4, #A855F7); + border-radius: var(--radius-md); + display: flex; + align-items: center; + justify-content: center; + font-size: 20px; + box-shadow: 0 4px 12px rgba(99, 102, 241, 0.3); +} + +.brand-titles h1 { + font-size: 16px; + font-weight: 800; + letter-spacing: -0.02em; +} + +.brand-titles h1 span { + color: var(--gold-primary); +} + +.brand-titles p { + font-size: 10.5px; + color: var(--text-muted); +} + +.pill-btn { + background: rgba(99, 102, 241, 0.15); + border: 1px solid rgba(99, 102, 241, 0.3); + color: #A5B4FC; + padding: 6px 12px; + border-radius: 20px; + font-size: 12px; + font-weight: 700; + cursor: pointer; + transition: all 0.2s; +} + +.pill-btn:active { + transform: scale(0.96); + background: rgba(99, 102, 241, 0.3); +} + +/* Main Container */ +.main-container { + flex: 1; + padding: 16px; + max-width: 680px; + margin: 0 auto; + width: 100%; +} + +.tab-page { + display: none; + flex-direction: column; + gap: 16px; + animation: fadeIn 0.25s ease-out; +} + +.tab-page.active { + display: flex; +} + +@keyframes fadeIn { + from { opacity: 0; transform: translateY(6px); } + to { opacity: 1; transform: translateY(0); } +} + +/* Cards */ +.card { + background: var(--card-bg); + border: 1px solid var(--card-border); + border-radius: var(--radius-lg); + padding: 18px; + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3); +} + +.card-header { + display: flex; + gap: 12px; + align-items: flex-start; + margin-bottom: 16px; +} + +.card-header .icon { + font-size: 26px; +} + +.card-header h2 { + font-size: 16px; + font-weight: 700; + color: var(--text-primary); +} + +.card-header p { + font-size: 12px; + color: var(--text-secondary); + margin-top: 2px; +} + +/* Form Inputs */ +.form-group { + display: flex; + flex-direction: column; + gap: 6px; + margin-bottom: 14px; +} + +label { + font-size: 12.5px; + font-weight: 600; + color: var(--text-secondary); +} + +input[type="text"], +input[type="password"], +textarea, +select { + width: 100%; + background: rgba(10, 14, 20, 0.7); + border: 1px solid rgba(255, 255, 255, 0.12); + border-radius: var(--radius-md); + padding: 12px 14px; + color: var(--text-primary); + font-size: 14px; + font-family: inherit; + outline: none; + transition: all 0.2s; +} + +input[type="text"]:focus, +input[type="password"]:focus, +textarea:focus, +select:focus { + border-color: var(--gold-primary); + box-shadow: 0 0 0 3px var(--gold-glow); +} + +/* File Upload Button */ +.form-row { + display: flex; + gap: 8px; + margin-bottom: 14px; +} + +.file-upload-btn { + flex: 1; + background: rgba(6, 182, 212, 0.12); + border: 1px dashed rgba(6, 182, 212, 0.4); + border-radius: var(--radius-md); + padding: 12px; + display: flex; + align-items: center; + justify-content: center; + gap: 8px; + font-size: 13px; + font-weight: 700; + color: #67E8F9; + cursor: pointer; + transition: all 0.2s; +} + +.file-upload-btn input { + display: none; +} + +.icon-btn { + background: rgba(239, 68, 68, 0.15); + border: 1px solid rgba(239, 68, 68, 0.3); + color: #F87171; + width: 44px; + border-radius: var(--radius-md); + cursor: pointer; +} + +.photo-preview-box { + position: relative; + border-radius: var(--radius-md); + overflow: hidden; + margin-bottom: 14px; + max-height: 180px; + border: 1px solid rgba(255, 255, 255, 0.15); +} + +.photo-preview-box img { + width: 100%; + height: 180px; + object-fit: cover; +} + +.badge-attached { + position: absolute; + bottom: 8px; + right: 8px; + background: rgba(16, 185, 129, 0.9); + color: white; + padding: 4px 8px; + border-radius: 6px; + font-size: 11px; + font-weight: 700; +} + +/* Buttons */ +.btn-primary { + width: 100%; + background: linear-gradient(135deg, #4F46E5, #6366F1); + border: none; + border-radius: var(--radius-md); + padding: 14px; + color: white; + font-size: 14px; + font-weight: 700; + font-family: inherit; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + gap: 8px; + box-shadow: 0 4px 16px rgba(99, 102, 241, 0.35); + transition: all 0.2s; +} + +.btn-primary:active { + transform: scale(0.98); +} + +/* Result Canvas */ +.result-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 12px; +} + +.gold-badge { + background: rgba(229, 184, 58, 0.15); + border: 1px solid var(--gold-primary); + color: var(--gold-primary); + padding: 4px 10px; + border-radius: 20px; + font-size: 11px; + font-weight: 700; +} + +.canvas-wrapper { + width: 100%; + aspect-ratio: 1/1; + border-radius: var(--radius-md); + overflow: hidden; + margin-bottom: 14px; + border: 1px solid rgba(229, 184, 58, 0.3); + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5); +} + +canvas { + width: 100%; + height: 100%; + display: block; +} + +.action-buttons-row { + display: flex; + gap: 10px; +} + +.btn-action { + flex: 1; + padding: 12px; + border: none; + border-radius: var(--radius-md); + font-size: 13px; + font-weight: 700; + font-family: inherit; + cursor: pointer; + transition: all 0.2s; +} + +.success-btn { + background: var(--emerald); + color: white; +} + +.whatsapp-btn { + background: var(--whatsapp); + color: white; +} + +.btn-action:active { + transform: scale(0.97); +} + +/* Apps Grid */ +.section-title-row { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 12px; +} + +.section-title-row h2 { + font-size: 17px; + font-weight: 800; +} + +.badge-count { + font-size: 12px; + color: var(--text-muted); +} + +.apps-grid { + display: flex; + flex-direction: column; + gap: 12px; +} + +.app-card { + background: var(--card-bg); + border: 1px solid var(--card-border); + border-radius: var(--radius-lg); + padding: 16px; + display: flex; + gap: 14px; + align-items: center; + cursor: pointer; + transition: all 0.2s; +} + +.app-card:active { + transform: scale(0.98); + border-color: var(--indigo-primary); +} + +.app-icon { + width: 48px; + height: 48px; + background: rgba(99, 102, 241, 0.12); + border-radius: var(--radius-md); + display: flex; + align-items: center; + justify-content: center; + font-size: 24px; + flex-shrink: 0; +} + +.app-info { + flex: 1; +} + +.app-title-row { + display: flex; + align-items: center; + gap: 6px; + margin-bottom: 2px; +} + +.app-title-row h3 { + font-size: 15px; + font-weight: 700; +} + +.tag-gem { + background: rgba(99, 102, 241, 0.2); + color: #A5B4FC; + font-size: 9px; + font-weight: 800; + padding: 2px 5px; + border-radius: 4px; +} + +.app-info p { + font-size: 12.5px; + color: var(--text-secondary); + line-height: 1.4; + margin-bottom: 6px; +} + +.meta-tag { + font-size: 11px; + color: var(--text-muted); +} + +/* Replies Card */ +.reply-block { + background: rgba(10, 14, 20, 0.6); + border: 1px solid rgba(255, 255, 255, 0.08); + border-radius: var(--radius-md); + padding: 14px; + margin-bottom: 12px; +} + +.reply-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 8px; +} + +.reply-header h4 { + font-size: 13px; + color: var(--gold-primary); + font-weight: 700; +} + +.copy-chip-btn { + background: rgba(229, 184, 58, 0.15); + border: 1px solid rgba(229, 184, 58, 0.3); + color: var(--gold-primary); + padding: 4px 8px; + border-radius: 6px; + font-size: 11px; + font-weight: 700; + cursor: pointer; +} + +.reply-content { + font-size: 13px; + line-height: 1.6; + color: var(--text-primary); + white-space: pre-wrap; +} + +/* Status Messages */ +.status-msg { + font-size: 12px; + margin-top: 10px; + font-weight: 600; + color: #F472B6; +} + +.hint-text { + font-size: 11px; + color: var(--text-muted); +} + +.info-card { + margin-top: 14px; +} + +.safari-hint { + margin-top: 10px; + padding: 10px; + background: rgba(99, 102, 241, 0.1); + border-left: 3px solid var(--indigo-primary); + border-radius: 6px; + font-size: 12px; + line-height: 1.5; + color: #C7D2FE; +} + +.hidden { + display: none !important; +} + +/* Bottom Navigation (iOS Style) */ +.bottom-nav { + position: fixed; + bottom: 0; + left: 0; + right: 0; + z-index: 100; + background: rgba(14, 19, 27, 0.88); + backdrop-filter: blur(24px); + -webkit-backdrop-filter: blur(24px); + border-top: 1px solid var(--card-border); + display: flex; + justify-content: space-around; + padding: 8px 10px calc(env(safe-area-inset-bottom, 10px) + 6px) 10px; +} + +.nav-item { + background: none; + border: none; + color: var(--text-muted); + display: flex; + flex-direction: column; + align-items: center; + gap: 3px; + cursor: pointer; + flex: 1; + transition: all 0.2s; +} + +.nav-icon { + font-size: 20px; +} + +.nav-label { + font-size: 10.5px; + font-weight: 600; +} + +.nav-item.active { + color: var(--gold-primary); +} + +.nav-item.active .nav-icon { + transform: translateY(-2px); +} diff --git a/sw.js b/sw.js new file mode 100644 index 0000000..0a9bf7b --- /dev/null +++ b/sw.js @@ -0,0 +1,39 @@ +const CACHE_NAME = 'opal-ai-v2-cache'; +const ASSETS = [ + './', + './index.html', + './style.css', + './app.js', + './manifest.json', + 'https://cdnjs.cloudflare.com/ajax/libs/qrcodejs/1.0.0/qrcode.min.js' +]; + +self.addEventListener('install', (e) => { + e.waitUntil( + caches.open(CACHE_NAME).then((cache) => { + return cache.addAll(ASSETS); + }) + ); + self.skipWaiting(); +}); + +self.addEventListener('activate', (e) => { + e.waitUntil( + caches.keys().then((keys) => { + return Promise.all( + keys.map((key) => { + if (key !== CACHE_NAME) return caches.delete(key); + }) + ); + }) + ); + self.clients.claim(); +}); + +self.addEventListener('fetch', (e) => { + e.respondWith( + caches.match(e.request).then((res) => { + return res || fetch(e.request); + }) + ); +});