Initial release of Google Opal AI iOS Web Studio

This commit is contained in:
Amirul Islam Redwan71
2026-09-01 09:01:09 +06:00
commit b85c5cf065
10 changed files with 1839 additions and 0 deletions
+20
View File
@@ -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;"]
+731
View File
@@ -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)));
}
+11
View File
@@ -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
View File
@@ -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>
+16
View File
@@ -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
View File
@@ -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";
}
}
+71
View File
@@ -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));
});
+40
View File
@@ -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.")
+557
View File
@@ -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);
}
+39
View File
@@ -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);
})
);
});