Initial release of Google Opal AI iOS Web Studio
This commit is contained in:
+20
@@ -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;"]
|
||||||
@@ -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 = `
|
||||||
|
<div class="empty-state">
|
||||||
|
<span class="icon-empty">📭</span>
|
||||||
|
<p>No generations yet. Create your first 3D poster or copy!</p>
|
||||||
|
</div>`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
historyContainer.innerHTML = list.map(item => `
|
||||||
|
<div class="app-card">
|
||||||
|
<div class="app-icon">${item.type === '3D Poster' ? '🎨' : '💬'}</div>
|
||||||
|
<div class="app-info">
|
||||||
|
<h3>${item.title}</h3>
|
||||||
|
<p>${item.price || item.type}</p>
|
||||||
|
<span class="meta-tag">Saved at ${item.date}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
clearBtn.addEventListener('click', () => {
|
||||||
|
localStorage.removeItem('opal_history');
|
||||||
|
renderHistory();
|
||||||
|
});
|
||||||
|
|
||||||
|
renderHistory();
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveGenerationToHistory(item) {
|
||||||
|
const list = JSON.parse(localStorage.getItem('opal_history') || '[]');
|
||||||
|
list.unshift(item);
|
||||||
|
localStorage.setItem('opal_history', JSON.stringify(list.slice(0, 30)));
|
||||||
|
}
|
||||||
@@ -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
|
||||||
+318
@@ -0,0 +1,318 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="bn">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
|
||||||
|
<title>Google Opal AI - iOS Studio & 3D Poster Lab</title>
|
||||||
|
|
||||||
|
<!-- iOS PWA Meta Tags -->
|
||||||
|
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||||
|
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
||||||
|
<meta name="apple-mobile-web-app-title" content="Opal AI">
|
||||||
|
<link rel="manifest" href="manifest.json">
|
||||||
|
<link rel="apple-touch-icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><rect width='100' height='100' rx='20' fill='%236366F1'/><text x='50' y='65' font-size='50' text-anchor='middle' fill='white'>💎</text></svg>">
|
||||||
|
|
||||||
|
<!-- Google Fonts & QR Code Library -->
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;600;700;800&family=Hind+Siliguri:wght@400;600;700&display=swap" rel="stylesheet">
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/qrcodejs/1.0.0/qrcode.min.js"></script>
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="style.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<!-- Top App Header -->
|
||||||
|
<header class="app-header">
|
||||||
|
<div class="brand-group">
|
||||||
|
<div class="logo-box">💎</div>
|
||||||
|
<div class="brand-titles">
|
||||||
|
<h1>Opal AI <span>Studio</span></h1>
|
||||||
|
<p>Google DeepMind • iOS Edition</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="header-actions">
|
||||||
|
<button id="langToggleBtn" class="pill-btn">🌐 বাং</button>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<!-- Main Scrollable App Container -->
|
||||||
|
<main class="main-container">
|
||||||
|
|
||||||
|
<!-- TAB 1: 3D Poster Lab (Default / Hero Tool) -->
|
||||||
|
<section id="tabPosterLab" class="tab-page active">
|
||||||
|
<div class="card glass-card">
|
||||||
|
<div class="card-header">
|
||||||
|
<span class="icon">🎨</span>
|
||||||
|
<div>
|
||||||
|
<h2>3D Commercial Poster Lab</h2>
|
||||||
|
<p>আসল প্রডাক্টের ছবি, ৩ডি মার্বেল পোডিয়াম ও WhatsApp QR সহ বিজ্ঞাপন পোস্টার বানান</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="productInput">প্রডাক্ট লিংক বা নাম (Product Link or Name):</label>
|
||||||
|
<input type="text" id="productInput" placeholder="e.g. https://www.glamourstouch.com/product/..." value="https://www.glamourstouch.com/product/8a8ca4d8-1e7f-497c-904c-a18352cafdde">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="productPrice">মূল্য ও অফার (Price & Discount):</label>
|
||||||
|
<input type="text" id="productPrice" placeholder="e.g. ৳ ১,০৬০ (৳ ১,৪৭০) 28% OFF" value="৳ ১,০৬০ (৳ ১,৪৭০) 28% OFF">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-row">
|
||||||
|
<label class="file-upload-btn" for="photoFileInput">
|
||||||
|
📸 <span>iPhone গ্যালারি থেকে ছবি সিলেক্ট করুন</span>
|
||||||
|
<input type="file" id="photoFileInput" accept="image/*">
|
||||||
|
</label>
|
||||||
|
<button id="clearPhotoBtn" class="icon-btn danger-btn hidden" title="Clear Photo">✖</button>
|
||||||
|
</div>
|
||||||
|
<div id="photoPreviewContainer" class="photo-preview-box hidden">
|
||||||
|
<img id="attachedPhotoImg" src="" alt="Attached Photo">
|
||||||
|
<span class="badge-attached">✅ Photo Attached</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button id="generatePosterBtn" class="btn-primary">
|
||||||
|
<span class="btn-icon">🚀</span> Generate 3D Facebook Ad Poster (1080x1080)
|
||||||
|
</button>
|
||||||
|
<p id="posterStatusText" class="status-msg hidden"></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Poster Canvas & Export Section -->
|
||||||
|
<div id="posterResultCard" class="card glass-card result-card hidden">
|
||||||
|
<div class="result-header">
|
||||||
|
<h3>✨ 100% Commercial Ad Poster (1080x1080)</h3>
|
||||||
|
<span class="gold-badge">3D Marble & Gold</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="canvas-wrapper">
|
||||||
|
<canvas id="posterCanvas" width="1080" height="1080"></canvas>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="action-buttons-row">
|
||||||
|
<button id="downloadPosterBtn" class="btn-action success-btn">
|
||||||
|
💾 Save to Photos
|
||||||
|
</button>
|
||||||
|
<button id="shareWhatsAppBtn" class="btn-action whatsapp-btn">
|
||||||
|
💬 WhatsApp Order
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<button id="autoPostN8nBtn" class="btn-primary" style="margin-top: 10px; background: linear-gradient(135deg, #1877F2, #0A58CA);">
|
||||||
|
<span class="btn-icon">🚀</span> 1-Click Auto Post to Facebook Page (via n8n)
|
||||||
|
</button>
|
||||||
|
<p id="n8nPostStatusMsg" class="status-msg hidden"></p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- TAB 2: Apps & Workflow Grid -->
|
||||||
|
<section id="tabApps" class="tab-page">
|
||||||
|
<div class="section-title-row">
|
||||||
|
<h2>✨ Curated Opal Mini-Apps</h2>
|
||||||
|
<span class="badge-count">6 Apps</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="apps-grid">
|
||||||
|
<!-- App 1: Glamour's Touch Studio -->
|
||||||
|
<div class="app-card" onclick="openAppRunner('gt_studio')">
|
||||||
|
<div class="app-icon">✨</div>
|
||||||
|
<div class="app-info">
|
||||||
|
<div class="app-title-row">
|
||||||
|
<h3>Glamour's Touch 4K K-Beauty Studio</h3>
|
||||||
|
<span class="tag-gem">GEM</span>
|
||||||
|
</div>
|
||||||
|
<p>glamourstouch.com এর ১০০% অরিজিনাল কোরিয়ান স্কিনকেয়ারের কমার্শিয়াল ছবি ও পোস্ট বানায়।</p>
|
||||||
|
<span class="meta-tag">7 Steps • Business</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- App 2: Customer Care & Inbox AI -->
|
||||||
|
<div class="app-card" onclick="openAppRunner('inbox_ai')">
|
||||||
|
<div class="app-icon">💬</div>
|
||||||
|
<div class="app-info">
|
||||||
|
<div class="app-title-row">
|
||||||
|
<h3>Customer Care & Inbox Auto-Reply</h3>
|
||||||
|
<span class="tag-gem">AI</span>
|
||||||
|
</div>
|
||||||
|
<p>ফেসবুক মেসেঞ্জার ও হোয়াটসঅ্যাপে কাস্টমারের প্রশ্নের ৩টি ইনস্ট্যান্ট বিনম্র সেলস রিপ্লাই তৈরি করে।</p>
|
||||||
|
<span class="meta-tag">3 Steps • Sales</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- App 3: E-Commerce Product Studio -->
|
||||||
|
<div class="app-card" onclick="openAppRunner('ecommerce')">
|
||||||
|
<div class="app-icon">🛍️</div>
|
||||||
|
<div class="app-info">
|
||||||
|
<h3>E-Commerce Product Launch Kit</h3>
|
||||||
|
<p>যেকোনো পণ্যের 4K কমার্শিয়াল ছবি ও ফেসবুক লঞ্চ কপি তৈরি করে।</p>
|
||||||
|
<span class="meta-tag">5 Steps • Business</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- App 4: Viral Social Post Maker -->
|
||||||
|
<div class="app-card" onclick="openAppRunner('viral_post')">
|
||||||
|
<div class="app-icon">🔥</div>
|
||||||
|
<div class="app-info">
|
||||||
|
<h3>Viral Social Post & Reels Script</h3>
|
||||||
|
<p>ফেসবুক, টিকটক ও ইনস্টাগ্রামের ভাইরাল হুক ও ভিডিও স্ক্রিপ্ট তৈরি করে।</p>
|
||||||
|
<span class="meta-tag">4 Steps • Marketing</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- TAB 3: Customer Care & Inbox AI Dedicated Studio -->
|
||||||
|
<section id="tabInboxAI" class="tab-page">
|
||||||
|
<div class="card glass-card">
|
||||||
|
<div class="card-header">
|
||||||
|
<span class="icon">💬</span>
|
||||||
|
<div>
|
||||||
|
<h2>Glamour's Touch Customer Care & Inbox AI</h2>
|
||||||
|
<p>কাস্টমারের মেসেজ দিলে ১-ক্লিকে ৩টি পারফেক্ট ও বিনম্র উত্তর তৈরি করে</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="inboxCustomerMsg">কাস্টমার ইনবক্সে কী লিখেছে? (Customer Query):</label>
|
||||||
|
<textarea id="inboxCustomerMsg" rows="3" placeholder="e.g. আপনাদের কাছে কি গ্লাস স্কিন সিরাম আছে? দাম কত আর হোম ডেলিভারি হবে?">আপনাদের কাছে কি কোরিয়ান গ্লাস স্কিন সিরাম আছে? দাম কত আর হোম ডেলিভারি হবে?</textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="inboxStoreContext">দোকানের পলিসি ও হটলাইন (Store Info):</label>
|
||||||
|
<input type="text" id="inboxStoreContext" value="Store: Glamour's Touch (glamourstouch.com), Hotline: 01712-426871, 100% Authentic Korean Skincare, Delivery: Dhaka ৳70, Outside Dhaka ৳130, Free delivery on ৳2000+">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button id="generateInboxReplyBtn" class="btn-primary">
|
||||||
|
<span class="btn-icon">⚡</span> Generate 3 Ready-to-Send Replies with Gemma AI
|
||||||
|
</button>
|
||||||
|
<p id="inboxStatusMsg" class="status-msg hidden"></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Generated Replies Container -->
|
||||||
|
<div id="inboxRepliesCard" class="card glass-card hidden">
|
||||||
|
<div class="reply-block">
|
||||||
|
<div class="reply-header">
|
||||||
|
<h4>⚡ ১. দ্রুত ও মিষ্টি উত্তর (Instant Polite Reply)</h4>
|
||||||
|
<button class="copy-chip-btn" onclick="copyText('replyText1')">📋 Copy</button>
|
||||||
|
</div>
|
||||||
|
<p id="replyText1" class="reply-content"></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="reply-block">
|
||||||
|
<div class="reply-header">
|
||||||
|
<h4>🛍️ ২. সেলস অর্ডার কনফার্মেশন (Order Booking Closer)</h4>
|
||||||
|
<button class="copy-chip-btn" onclick="copyText('replyText2')">📋 Copy</button>
|
||||||
|
</div>
|
||||||
|
<p id="replyText2" class="reply-content"></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="reply-block">
|
||||||
|
<div class="reply-header">
|
||||||
|
<h4>💬 ৩. ১-ক্লিক WhatsApp লিঙ্ক (Direct Order Link)</h4>
|
||||||
|
<button class="copy-chip-btn" onclick="copyText('replyText3')">📋 Copy</button>
|
||||||
|
</div>
|
||||||
|
<p id="replyText3" class="reply-content"></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- TAB 4: History -->
|
||||||
|
<section id="tabHistory" class="tab-page">
|
||||||
|
<div class="section-title-row">
|
||||||
|
<h2>📜 Saved History</h2>
|
||||||
|
<button id="clearHistoryBtn" class="pill-btn danger-text">Clear All</button>
|
||||||
|
</div>
|
||||||
|
<div id="historyListContainer" class="history-list">
|
||||||
|
<div class="empty-state">
|
||||||
|
<span class="icon-empty">📭</span>
|
||||||
|
<p>No generations yet. Create your first 3D poster or copy!</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- TAB 5: Settings & Free Gemma/Gemini Model Selection -->
|
||||||
|
<section id="tabSettings" class="tab-page">
|
||||||
|
<div class="card glass-card">
|
||||||
|
<div class="card-header">
|
||||||
|
<span class="icon">⚙️</span>
|
||||||
|
<div>
|
||||||
|
<h2>Settings & Free AI Engine</h2>
|
||||||
|
<p>গুগল জেমিনি ও গ্যামা ওপেন মডেল সেটআপ</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="apiKeyInput">Google Gemini API Key (100% Free):</label>
|
||||||
|
<input type="password" id="apiKeyInput" placeholder="Paste your AI Studio API key (AIzaSy...)">
|
||||||
|
<small class="hint-text">API Key is saved securely in your iPhone's local storage.</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="modelSelect">Default AI Model (এআই মডেল):</label>
|
||||||
|
<select id="modelSelect">
|
||||||
|
<option value="gemini-3.6-flash">Google Gemini 3.6 Flash (Recommended, Fast & Multimodal)</option>
|
||||||
|
<option value="gemma-4-31b-it">Google Gemma 4 (গ্যামা 31B ওপেন-ওয়েট AI)</option>
|
||||||
|
<option value="gemma-2-27b-it">Google Gemma 2 (গ্যামা 27B লাইভ ইঞ্জিন)</option>
|
||||||
|
<option value="gemini-3.5-flash">Google Gemini 3.5 Flash (Ultra Stable)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="n8nWebhookInput">n8n Auto-Post Webhook URL (Aura Ajentic AI):</label>
|
||||||
|
<input type="text" id="n8nWebhookInput" placeholder="https://n8n.auraajenticai.cloud/webhook/gt-auto-post" value="https://n8n.auraajenticai.cloud/webhook/gt-auto-post">
|
||||||
|
<small class="hint-text">Pre-configured with your n8n cloud instance at n8n.auraajenticai.cloud</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button id="saveApiKeyBtn" class="btn-primary">
|
||||||
|
💾 Save Settings & Test Connection
|
||||||
|
</button>
|
||||||
|
<p id="settingsStatusMsg" class="status-msg hidden"></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card glass-card info-card">
|
||||||
|
<h3>💎 About Google Opal AI (iOS Edition)</h3>
|
||||||
|
<p>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.</p>
|
||||||
|
<div class="safari-hint">
|
||||||
|
<strong>📲 iPhone টিপস:</strong> Safari-এর নিচে থাকা <strong>Share (📤)</strong> বাটনে ট্যাপ করে <strong>"Add to Home Screen"</strong> চাপুন। তাহলে এটি আপনার আইফোনে একদম আসল অ্যাপের মতো ফুল-স্ক্রিনে চলবে!
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<!-- Bottom Navigation Bar (iOS 18 Style) -->
|
||||||
|
<nav class="bottom-nav">
|
||||||
|
<button class="nav-item active" data-tab="tabPosterLab">
|
||||||
|
<span class="nav-icon">🎨</span>
|
||||||
|
<span class="nav-label">পোস্টার ল্যাব</span>
|
||||||
|
</button>
|
||||||
|
<button class="nav-item" data-tab="tabApps">
|
||||||
|
<span class="nav-icon">🏠</span>
|
||||||
|
<span class="nav-label">অ্যাপস</span>
|
||||||
|
</button>
|
||||||
|
<button class="nav-item" data-tab="tabInboxAI">
|
||||||
|
<span class="nav-icon">💬</span>
|
||||||
|
<span class="nav-label">ইনবক্স এআই</span>
|
||||||
|
</button>
|
||||||
|
<button class="nav-item" data-tab="tabHistory">
|
||||||
|
<span class="nav-icon">📜</span>
|
||||||
|
<span class="nav-label">হিস্ট্রি</span>
|
||||||
|
</button>
|
||||||
|
<button class="nav-item" data-tab="tabSettings">
|
||||||
|
<span class="nav-icon">⚙️</span>
|
||||||
|
<span class="nav-label">সেটিংস/গ্যামা</span>
|
||||||
|
</button>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<!-- Hidden container for generating QR code SVG/Canvas -->
|
||||||
|
<div id="qrcodeHidden" style="display:none;"></div>
|
||||||
|
|
||||||
|
<script src="app.js"></script>
|
||||||
|
<script>
|
||||||
|
if ('serviceWorker' in navigator) {
|
||||||
|
window.addEventListener('load', () => {
|
||||||
|
navigator.serviceWorker.register('sw.js').catch(err => console.log('SW reg error:', err));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -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,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><rect width='100' height='100' rx='20' fill='%236366F1'/><text x='50' y='65' font-size='50' text-anchor='middle' fill='white'>💎</text></svg>",
|
||||||
|
"sizes": "192x192 512x512",
|
||||||
|
"type": "image/svg+xml"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
+36
@@ -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";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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));
|
||||||
|
});
|
||||||
@@ -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.")
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user