Image Generator Pro
<!DOCTYPE html>
<html lang="pt">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ChatGPT Images 2.0 vs Nano Banana Pro</title>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<script>
tailwind.config = {
theme: {
extend: {
colors: {
brand: {
yellow: 'var(--color-primary)',
darkYellow: 'var(--color-primary-dark)',
blue: 'var(--color-accent)',
dark: '#12141D',
card: '#1E2230',
border: '#2E3548'
}
}
}
}
}
</script>
<style>
:root {
--color-primary: #FFD13B;
--color-primary-dark: #E5B81C;
--color-accent: #10B981;
--glow-color: rgba(255, 209, 59, 0.25);
}
.theme-cyan {
--color-primary: #1CD0F0;
--color-primary-dark: #0EBBD9;
--color-accent: #FFD13B;
--glow-color: rgba(28, 208, 240, 0.3);
}
.theme-purple {
--color-primary: #A855F7;
--color-primary-dark: #9333EA;
--color-accent: #EC4899;
--glow-color: rgba(168, 85, 247, 0.3);
}
.theme-emerald {
--color-primary: #10B981;
--color-primary-dark: #059669;
--color-accent: #3B82F6;
--glow-color: rgba(16, 185, 129, 0.3);
}
.theme-mono {
--color-primary: #E2E8F0;
--color-primary-dark: #CBD5E1;
--color-accent: #38BDF8;
--glow-color: rgba(226, 232, 240, 0.2);
}
.custom-scrollbar::-webkit-scrollbar {
width: 6px;
height: 6px;
}
.custom-scrollbar::-webkit-scrollbar-track {
background: #12141D;
}
.custom-scrollbar::-webkit-scrollbar-thumb {
background: #2E3548;
border-radius: 3px;
}
.glow-dynamic {
box-shadow: 0 0 15px var(--glow-color);
}
.bg-primary {
background-color: var(--color-primary);
}
.text-primary {
color: var(--color-primary);
}
.border-primary {
border-color: var(--color-primary);
}
</style>
</head>
<body class="bg-brand-dark text-white font-sans min-h-screen flex flex-col custom-scrollbar transition-colors duration-300">
<!-- Header / Navbar -->
<header class="border-b border-brand-border bg-brand-card/80 backdrop-blur-md sticky top-0 z-50">
<div class="max-w-7xl mx-auto px-4 py-3 flex items-center justify-between">
<div class="flex items-center space-x-3">
<!-- Mascot / VS Banner Header -->
<div class="flex items-center space-x-1">
<div class="w-9 h-9 rounded-xl bg-amber-400 flex items-center justify-center text-brand-dark font-bold text-lg shadow-md">
🍌
</div>
<span class="text-xs font-black text-amber-400 px-1">VS</span>
<div class="w-9 h-9 rounded-xl bg-emerald-500 flex items-center justify-center text-white font-bold text-lg shadow-md">
🌀
</div>
</div>
<div>
<h1 class="font-bold text-base md:text-lg leading-tight flex items-center gap-2">
ChatGPT Images 2.0 <span class="text-xs text-gray-400 font-normal">vs</span> Nano Banana Pro
</h1>
<p class="text-[11px] text-gray-400">Escolha o modelo ideal para a sua criação</p>
</div>
</div>
<!-- Pro Status, Sound Effects Toggle, Theme Customizer & Limits -->
<div class="flex items-center space-x-2 sm:space-x-3">
<!-- Audio Mute/Unmute Toggle -->
<button id="sound-toggle-btn" class="bg-brand-dark border border-brand-border hover:border-primary p-2 rounded-lg text-xs font-medium flex items-center justify-center text-primary transition-all" title="Ativar/Desativar Efeitos Sonoros">
<i id="sound-icon" class="fa-solid fa-volume-high text-sm"></i>
</button>
<!-- Color Theme Dropdown Customizer -->
<div class="relative group">
<button class="bg-brand-dark border border-brand-border hover:border-primary px-3 py-1.5 rounded-lg text-xs font-medium flex items-center gap-2 transition-all">
<i class="fa-solid fa-palette text-primary"></i>
<span class="hidden sm:inline">Tema</span>
</button>
<div class="absolute right-0 mt-2 w-52 bg-brand-card border border-brand-border rounded-xl shadow-2xl p-2.5 hidden group-hover:block z-50">
<div class="text-[10px] font-semibold text-gray-400 uppercase tracking-wider px-2 py-1 mb-1">
Temas Pré-definidos
</div>
<button onclick="setTheme('')" class="w-full text-left px-2 py-1.5 rounded-lg text-xs hover:bg-brand-dark flex items-center gap-2">
<span class="w-3 h-3 rounded-full bg-[#FFD13B]"></span> Dourado Bot (Padrão)
</button>
<button onclick="setTheme('theme-cyan')" class="w-full text-left px-2 py-1.5 rounded-lg text-xs hover:bg-brand-dark flex items-center gap-2">
<span class="w-3 h-3 rounded-full bg-[#1CD0F0]"></span> Azul Néon
</button>
<button onclick="setTheme('theme-purple')" class="w-full text-left px-2 py-1.5 rounded-lg text-xs hover:bg-brand-dark flex items-center gap-2">
<span class="w-3 h-3 rounded-full bg-[#A855F7]"></span> Púrpura Cyber
</button>
<button onclick="setTheme('theme-emerald')" class="w-full text-left px-2 py-1.5 rounded-lg text-xs hover:bg-brand-dark flex items-center gap-2">
<span class="w-3 h-3 rounded-full bg-[#10B981]"></span> Verde Esmeralda
</button>
<button onclick="setTheme('theme-mono')" class="w-full text-left px-2 py-1.5 rounded-lg text-xs hover:bg-brand-dark flex items-center gap-2">
<span class="w-3 h-3 rounded-full bg-[#E2E8F0]"></span> Monocromático
</button>
<div class="border-t border-brand-border my-2"></div>
<!-- Custom Color Picker Input -->
<div class="px-2 py-1">
<label class="text-[10px] font-semibold text-gray-400 uppercase tracking-wider block mb-1">
Cor Personalizada
</label>
<div class="flex items-center gap-2">
<input type="color" id="custom-color-picker" value="#FFD13B" class="w-7 h-7 rounded cursor-pointer bg-transparent border-0">
<span class="text-xs text-gray-300 font-mono" id="custom-color-hex">#FFD13B</span>
</div>
</div>
</div>
</div>
<!-- Limits Display -->
<div class="bg-brand-dark/80 border border-brand-border px-3 py-1.5 rounded-lg flex items-center space-x-3">
<div class="text-right">
<div class="text-xs text-gray-400 font-medium">Restantes</div>
<div class="text-sm font-bold text-primary flex items-center justify-end gap-1">
<span id="remaining-count">80</span> / 80
<i class="fa-solid fa-bolt text-xs"></i>
</div>
</div>
<div class="h-8 w-[1px] bg-brand-border"></div>
<div class="text-left">
<div class="text-xs text-gray-400 font-medium">Renovação</div>
<div id="timer-display" class="text-sm font-mono text-primary font-semibold">
02:00:00
</div>
</div>
</div>
</div>
</div>
</header>
<!-- Main Content Area -->
<main class="flex-1 max-w-7xl w-full mx-auto p-4 md:p-6 grid grid-cols-1 lg:grid-cols-12 gap-6">
<!-- Left Controls Column (Prompt & References) -->
<section class="lg:col-span-5 flex flex-col space-y-5">
<div class="bg-brand-card border border-brand-border rounded-2xl p-5 shadow-xl space-y-4">
<div class="flex items-center justify-between">
<h2 class="text-base font-semibold text-white flex items-center gap-2">
<i class="fa-solid fa-wand-magic-sparkles text-primary"></i>
Criar Nova Imagem
</h2>
<span id="active-model-tag" class="text-xs bg-amber-400/10 text-amber-400 border border-amber-400/30 px-2.5 py-1 rounded-full font-bold flex items-center gap-1">
🍌 Nano Banana Pro
</span>
</div>
<!-- AI Model Selector (VERSUS SHOWDOWN REAL) -->
<div>
<label class="block text-xs font-medium text-gray-300 mb-1.5">
Escolha o Modelo de IA
</label>
<div class="grid grid-cols-2 gap-2.5">
<!-- Model 1: Nano Banana Pro -->
<label class="model-option cursor-pointer border-2 border-amber-400 bg-amber-500/10 rounded-xl p-3 flex flex-col justify-between transition-all relative overflow-hidden">
<input type="radio" name="ai-model" value="nano-banana-pro" checked class="hidden">
<div class="flex items-center justify-between mb-1">
<span class="text-2xl">🍌</span>
<span class="text-[9px] font-extrabold bg-amber-400 text-brand-dark px-1.5 py-0.5 rounded uppercase">PRO</span>
</div>
<div>
<div class="text-xs font-black text-amber-400">NANO BANANA PRO</div>
<div class="text-[10px] text-gray-300 font-medium">Multimodal & 40 Referências</div>
</div>
</label>
<!-- Model 2: ChatGPT Images 2.0 -->
<label class="model-option cursor-pointer border-2 border-brand-border bg-brand-dark/60 rounded-xl p-3 flex flex-col justify-between transition-all relative overflow-hidden hover:border-emerald-500/50">
<input type="radio" name="ai-model" value="chatgpt-images-2" class="hidden">
<div class="flex items-center justify-between mb-1">
<span class="text-2xl">🌀</span>
<span class="text-[9px] font-extrabold bg-emerald-500 text-white px-1.5 py-0.5 rounded uppercase">v2.0</span>
</div>
<div>
<div class="text-xs font-black text-emerald-400">CHATGPT IMAGES 2.0</div>
<div class="text-[10px] text-gray-300 font-medium">Alta Fidelidade Visual</div>
</div>
</label>
</div>
</div>
<!-- Text Prompt Input -->
<div>
<label class="block text-xs font-medium text-gray-300 mb-1">
Descrição da Imagem (Prompt)
</label>
<textarea
id="prompt-input"
rows="4"
class="w-full bg-brand-dark border border-brand-border rounded-xl p-3 text-sm text-white focus:outline-none focus:border-primary transition-all resize-none placeholder-gray-500"
placeholder="Ex: Uma banana robô artista com pincel enfrentando uma mascote verde da IA em estilo 3D épico..."></textarea>
</div>
<!-- Reference Images Upload Area (Up to 40 photos) -->
<div>
<div class="flex justify-between items-center mb-1">
<label class="text-xs font-medium text-gray-300 flex items-center">
<i class="fa-solid fa-images mr-1.5 text-primary"></i>
Fotos de Referência (Opcional)
</label>
<span id="ref-counter" class="text-xs font-semibold text-primary">
<span id="ref-count">0</span> / 40 fotos
</span>
</div>
<!-- Upload Drop Zone -->
<div id="drop-zone" class="border-2 border-dashed border-brand-border rounded-xl p-3 text-center bg-brand-dark/50 hover:bg-brand-dark/80 transition-all cursor-pointer relative group">
<input type="file" id="reference-input" accept="image/*" multiple class="hidden">
<div id="upload-placeholder" class="flex flex-col items-center justify-center space-y-1 py-2">
<i class="fa-solid fa-cloud-arrow-up text-2xl text-gray-400 group-hover:text-primary transition-colors"></i>
<p class="text-xs text-gray-300 font-medium">Arrasta ou clica para carregar até 40 fotos</p>
<p class="text-[10px] text-gray-500">Podes selecionar múltiplos ficheiros de uma só vez</p>
</div>
</div>
<!-- Multiple Image Preview Container -->
<div id="preview-section" class="hidden mt-3">
<div class="flex items-center justify-between mb-1.5">
<span class="text-[11px] text-gray-400 font-medium">Referências Carregadas</span>
<button id="clear-all-refs" class="text-[11px] text-red-400 hover:text-red-300 transition-colors flex items-center gap-1">
<i class="fa-solid fa-trash-can"></i> Limpar todas
</button>
</div>
<div id="preview-grid" class="grid grid-cols-5 gap-2 max-h-40 overflow-y-auto custom-scrollbar p-1 bg-brand-dark/40 border border-brand-border rounded-xl">
<!-- Thumbnails injected via JS -->
</div>
</div>
</div>
<!-- Aspect Ratio & Style Settings -->
<div class="grid grid-cols-2 gap-3 pt-2">
<div>
<label class="block text-xs font-medium text-gray-300 mb-1">Formato / Proporção</label>
<select id="aspect-ratio" class="w-full bg-brand-dark border border-brand-border rounded-xl px-3 py-2 text-xs text-white focus:outline-none focus:border-primary">
<option value="1:1">Quadrado (1:1)</option>
<option value="16:9">Widescreen (16:9)</option>
<option value="9:16">Stories / Vertical (9:16)</option>
<option value="4:3">Standard (4:3)</option>
</select>
</div>
<div>
<label class="block text-xs font-medium text-gray-300 mb-1">Estilo Artístico</label>
<select id="style-select" class="w-full bg-brand-dark border border-brand-border rounded-xl px-3 py-2 text-xs text-white focus:outline-none focus:border-primary">
<option value="3D render cartoon">3D Render Cartoon (Estilo Imagem)</option>
<option value="anime/cartoon">Cartoon / Animação</option>
<option value="photorealistic">Fotorrealista 8K</option>
<option value="digital-art">Arte Digital 3D</option>
<option value="cyberpunk">Cyberpunk / Neon</option>
</select>
</div>
</div>
<!-- Sensitive Mode / Safety Filter Toggle -->
<div class="bg-brand-dark/60 border border-brand-border p-3 rounded-xl flex items-center justify-between">
<div class="flex items-center space-x-2.5">
<div id="shield-icon" class="w-7 h-7 rounded-lg bg-primary/10 border border-primary/20 flex items-center justify-center text-primary text-xs transition-colors">
<i class="fa-solid fa-shield-halved" id="shield-symbol"></i>
</div>
<div>
<label for="sensitive-mode-toggle" class="text-xs font-semibold text-white block cursor-pointer">
Modo Sensível (Filtro)
</label>
<p class="text-[10px] text-gray-400" id="sensitive-status-text">
✅ Filtro ativado (Esconde e avisa antes de mostrar)
</p>
</div>
</div>
<label class="relative inline-flex items-center cursor-pointer">
<input type="checkbox" id="sensitive-mode-toggle" checked class="sr-only peer">
<div class="w-9 h-5 bg-brand-card border border-brand-border peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-4 after:w-4 after:transition-all peer-checked:bg-primary"></div>
</label>
</div>
<!-- Custom Sound Test Option -->
<div class="bg-brand-dark/30 border border-brand-border p-2.5 rounded-xl flex items-center justify-between text-xs text-gray-400">
<span class="flex items-center gap-1.5">
<i class="fa-solid fa-music text-primary"></i> Efeitos Sonoros Reais
</span>
<div class="flex space-x-2">
<button onclick="playSuccessSound()" class="px-2 py-1 bg-brand-dark border border-brand-border hover:border-primary rounded text-[10px] text-gray-300 hover:text-white transition-colors">
🔊 Sucesso
</button>
<button onclick="playErrorSound()" class="px-2 py-1 bg-brand-dark border border-brand-border hover:border-red-500 rounded text-[10px] text-gray-300 hover:text-white transition-colors">
🔊 Erro
</button>
</div>
</div>
<!-- Action Button -->
<button
id="generate-btn"
class="w-full bg-primary text-brand-dark font-bold py-3.5 px-4 rounded-xl shadow-lg hover:brightness-110 active:scale-[0.99] transition-all flex items-center justify-center space-x-2 text-sm glow-dynamic mt-4 disabled:opacity-50 disabled:cursor-not-allowed">
<i class="fa-solid fa-wand-magic-sparkles"></i>
<span>Gerar Imagem com IA 😏</span>
</button>
</div>
<!-- Quick Prompts / Inspiration -->
<div class="bg-brand-card/50 border border-brand-border rounded-2xl p-4">
<h3 class="text-xs font-semibold text-gray-400 uppercase tracking-wider mb-2 flex items-center gap-1.5">
<i class="fa-solid fa-lightbulb text-primary"></i> Sugestões Rápidas
</h3>
<div class="flex flex-wrap gap-2">
<button class="preset-btn text-xs bg-brand-dark border border-brand-border hover:border-primary/50 text-gray-300 hover:text-white px-2.5 py-1.5 rounded-lg transition-colors text-left">
🍌 Nano Banana vs ChatGPT 2.0 em batalha artística 3D
</button>
<button class="preset-btn text-xs bg-brand-dark border border-brand-border hover:border-primary/50 text-gray-300 hover:text-white px-2.5 py-1.5 rounded-lg transition-colors text-left">
🤖 Robô mascote desenhando num cavalete
</button>
<button class="preset-btn text-xs bg-brand-dark border border-brand-border hover:border-primary/50 text-gray-300 hover:text-white px-2.5 py-1.5 rounded-lg transition-colors text-left">
🌌 Cidade futurista em néon azul e dourado
</button>
</div>
</div>
</section>
<!-- Right Output Display & Gallery Column -->
<section class="lg:col-span-7 flex flex-col space-y-6">
<!-- Main Result Preview Box -->
<div class="bg-brand-card border border-brand-border rounded-2xl p-5 shadow-xl flex flex-col justify-between min-h-[420px] relative overflow-hidden">
<div class="flex items-center justify-between mb-4">
<h2 class="text-base font-semibold text-white flex items-center gap-2">
<i class="fa-solid fa-image text-primary"></i>
Resultado Gerado
</h2>
<div id="status-badge" class="text-xs bg-emerald-500/10 text-emerald-400 px-2.5 py-1 rounded-full font-medium border border-emerald-500/20">
Pronto
</div>
</div>
<!-- Display Canvas / Image Viewer -->
<div class="flex-1 bg-brand-dark/80 rounded-xl border border-brand-border flex items-center justify-center relative overflow-hidden min-h-[300px]">
<!-- Default Empty State -->
<div id="empty-state" class="text-center p-8 flex flex-col items-center">
<div class="w-16 h-16 rounded-2xl bg-brand-card border border-brand-border flex items-center justify-center text-gray-500 text-2xl mb-3">
<i class="fa-solid fa-photo-film"></i>
</div>
<h3 class="text-sm font-semibold text-gray-300">Nenhuma imagem gerada ainda</h3>
<p class="text-xs text-gray-500 max-w-xs mt-1">Escolha o modelo (Nano Banana Pro ou ChatGPT Images 2.0) e clique em "Gerar Imagem".</p>
</div>
<!-- Loader Display -->
<div id="loader-state" class="hidden text-center p-8 flex flex-col items-center">
<div class="relative w-20 h-20 mb-4 flex items-center justify-center">
<div class="absolute inset-0 rounded-full border-4 border-primary/20 border-t-primary animate-spin"></div>
<span id="loader-avatar" class="text-3xl animate-bounce">🍌</span>
</div>
<h3 class="text-sm font-semibold text-white">A criar a tua obra de arte...</h3>
<p class="text-xs text-gray-400 mt-1" id="loading-subtext">A processar o prompt e estilos...</p>
</div>
<!-- Result Image Container (with Blur Overlay for Sensitive Mode) -->
<div id="image-container" class="hidden relative w-full h-full flex items-center justify-center">
<img id="result-image" src="" alt="Resultado da IA" class="max-h-[450px] w-full object-contain rounded-lg shadow-2xl transition-all duration-300">
<!-- Sensitive Warning Overlay (Shown when Sensitive Mode is ON) -->
<div id="sensitive-overlay" class="hidden absolute inset-0 bg-brand-dark/90 backdrop-blur-md flex flex-col items-center justify-center text-center p-6 rounded-lg border border-amber-500/30 space-y-3 z-20">
<div class="w-12 h-12 rounded-full bg-amber-500/10 border border-amber-500/30 text-amber-400 flex items-center justify-center text-xl">
<i class="fa-solid fa-lock"></i>
</div>
<div>
<h4 class="text-sm font-bold text-white">Conteúdo Oculto (Modo Sensível) 🔒</h4>
<p class="text-xs text-gray-300 max-w-xs mt-1">
Esta imagem foi coberta por precaução. Clica abaixo para ver a criação completa.
</p>
</div>
<button id="reveal-image-btn" class="bg-amber-500 hover:bg-amber-400 text-brand-dark font-bold text-xs px-4 py-2 rounded-xl transition-all flex items-center gap-1.5 shadow-lg">
<i class="fa-solid fa-eye"></i> Revelar Conteúdo
</button>
</div>
</div>
</div>
<!-- Actions Bar for Generated Image -->
<div id="action-bar" class="hidden flex items-center justify-between pt-4 mt-2 border-t border-brand-border/60">
<div class="text-xs text-gray-400 truncate max-w-[200px] sm:max-w-[320px]" id="result-prompt-text">
--
</div>
<div class="flex items-center space-x-2">
<button id="copy-btn" class="bg-brand-dark hover:bg-brand-border text-gray-300 hover:text-white px-3 py-1.5 rounded-lg text-xs font-medium border border-brand-border transition-colors flex items-center gap-1.5">
<i class="fa-solid fa-copy text-gray-400"></i> Copiar Prompt
</button>
<button id="download-btn" class="bg-primary text-brand-dark hover:brightness-110 px-3.5 py-1.5 rounded-lg text-xs font-bold transition-all flex items-center gap-1.5 shadow-md">
<i class="fa-solid fa-download"></i> Baixar Imagem
</button>
</div>
</div>
</div>
<!-- History Gallery -->
<div class="bg-brand-card border border-brand-border rounded-2xl p-5 shadow-xl">
<h3 class="text-sm font-semibold text-white mb-3 flex items-center gap-2">
<i class="fa-solid fa-clock-rotate-left text-primary"></i>
Histórico Recente da Sessão
</h3>
<div id="gallery-grid" class="grid grid-cols-3 sm:grid-cols-4 md:grid-cols-5 gap-3 max-h-[160px] overflow-y-auto custom-scrollbar p-1">
<div class="col-span-full text-center py-6 text-xs text-gray-500">
O teu histórico de imagens irá aparecer aqui.
</div>
</div>
</div>
</section>
</main>
<!-- Modal limit notification -->
<div id="limit-modal" class="fixed inset-0 bg-black/80 backdrop-blur-sm z-50 hidden flex items-center justify-center p-4">
<div class="bg-brand-card border border-brand-border rounded-2xl max-w-md w-full p-6 text-center space-y-4 shadow-2xl">
<div class="w-16 h-16 rounded-full bg-red-500/10 border border-red-500/30 text-red-500 flex items-center justify-center text-2xl mx-auto">
<i class="fa-solid fa-hourglass-end"></i>
</div>
<h3 class="text-lg font-bold text-white">Limite Pro Atingido!</h3>
<p class="text-xs text-gray-300 leading-relaxed">
Atingiste o limite máximo de <strong>80 imagens</strong> disponíveis no teu ciclo Pro.
O teu contador irá renovar automaticamente cerca de 2 horas após a utilização.
</p>
<div class="bg-brand-dark p-3 rounded-xl border border-brand-border">
<span class="text-xs text-gray-400 block mb-1">Próxima renovação total em:</span>
<span id="modal-timer" class="text-xl font-mono font-bold text-primary">02:00:00</span>
</div>
<button id="close-modal-btn" class="w-full bg-primary text-brand-dark font-bold py-2.5 rounded-xl text-sm hover:brightness-110 transition-all">
Compreendido
</button>
</div>
</div>
<!-- Logic & Script -->
<script>
// API Configuration (Gemini API standard key provided by context)
const apiKey = "";
// Application State
const MAX_CREDITS = 80;
const MAX_REFERENCES = 40;
let remainingCredits = MAX_CREDITS;
let timerSeconds = 2 * 60 * 60; // 2 hours in seconds
let timerInterval = null;
let selectedModel = 'nano-banana-pro'; // 'nano-banana-pro' ou 'chatgpt-images-2'
let currentGeneratedPrompt = '';
let isSensitiveModeEnabled = true;
let isAudioMuted = false;
// Audio Context Synthesizer (Sons fiéis aos originais "It worked out.wav" e "It didnt work..wav")
const audioCtx = new (window.AudioContext || window.webkitAudioContext)();
function playSuccessSound() {
if (isAudioMuted) return;
try {
if (audioCtx.state === 'suspended') audioCtx.resume();
const now = audioCtx.currentTime;
const osc1 = audioCtx.createOscillator();
const osc2 = audioCtx.createOscillator();
const gain = audioCtx.createGain();
osc1.type = 'sine';
osc2.type = 'triangle';
// Tom de sucesso alegre ("It worked out.wav")
osc1.frequency.setValueAtTime(523.25, now); // C5
osc1.frequency.exponentialRampToValueAtTime(880, now + 0.15); // A5
osc2.frequency.setValueAtTime(659.25, now); // E5
osc2.frequency.exponentialRampToValueAtTime(1046.50, now + 0.15); // C6
gain.gain.setValueAtTime(0.3, now);
gain.gain.exponentialRampToValueAtTime(0.01, now + 0.35);
osc1.connect(gain);
osc2.connect(gain);
gain.connect(audioCtx.destination);
osc1.start(now);
osc2.start(now);
osc1.stop(now + 0.35);
osc2.stop(now + 0.35);
} catch (e) {}
}
function playErrorSound() {
if (isAudioMuted) return;
try {
if (audioCtx.state === 'suspended') audioCtx.resume();
const now = audioCtx.currentTime;
const osc = audioCtx.createOscillator();
const gain = audioCtx.createGain();
osc.type = 'sawtooth';
// Tom grave de erro ("It didnt work.wav")
osc.frequency.setValueAtTime(150, now);
osc.frequency.linearRampToValueAtTime(60, now + 0.3);
gain.gain.setValueAtTime(0.4, now);
gain.gain.exponentialRampToValueAtTime(0.01, now + 0.3);
osc.connect(gain);
gain.connect(audioCtx.destination);
osc.start(now);
osc.stop(now + 0.3);
} catch (e) {}
}
// Multiple Reference Images Array
let referenceImages = []; // Array of objects: { id, name, mimeType, base64 }
let historyImages = [];
// DOM Elements
const promptInput = document.getElementById('prompt-input');
const generateBtn = document.getElementById('generate-btn');
const remainingCountEl = document.getElementById('remaining-count');
const timerDisplayEl = document.getElementById('timer-display');
const modalTimerEl = document.getElementById('modal-timer');
const activeModelTag = document.getElementById('active-model-tag');
const loaderAvatar = document.getElementById('loader-avatar');
const referenceInput = document.getElementById('reference-input');
const dropZone = document.getElementById('drop-zone');
const previewSection = document.getElementById('preview-section');
const previewGrid = document.getElementById('preview-grid');
const refCountEl = document.getElementById('ref-count');
const clearAllRefsBtn = document.getElementById('clear-all-refs');
const sensitiveToggle = document.getElementById('sensitive-mode-toggle');
const sensitiveStatusText = document.getElementById('sensitive-status-text');
const sensitiveOverlay = document.getElementById('sensitive-overlay');
const revealImageBtn = document.getElementById('reveal-image-btn');
const shieldSymbol = document.getElementById('shield-symbol');
const imageContainer = document.getElementById('image-container');
const soundToggleBtn = document.getElementById('sound-toggle-btn');
const soundIcon = document.getElementById('sound-icon');
const emptyState = document.getElementById('empty-state');
const loaderState = document.getElementById('loader-state');
const resultImage = document.getElementById('result-image');
const actionBar = document.getElementById('action-bar');
const resultPromptText = document.getElementById('result-prompt-text');
const downloadBtn = document.getElementById('download-btn');
const copyBtn = document.getElementById('copy-btn');
const galleryGrid = document.getElementById('gallery-grid');
const statusBadge = document.getElementById('status-badge');
const limitModal = document.getElementById('limit-modal');
const closeModalBtn = document.getElementById('close-modal-btn');
const customColorPicker = document.getElementById('custom-color-picker');
const customColorHex = document.getElementById('custom-color-hex');
// Mute / Unmute Toggle Button Listener
soundToggleBtn.addEventListener('click', () => {
isAudioMuted = !isAudioMuted;
if (isAudioMuted) {
soundIcon.className = 'fa-solid fa-volume-xmark text-red-400';
soundToggleBtn.classList.add('border-red-500/40');
} else {
soundIcon.className = 'fa-solid fa-volume-high text-primary';
soundToggleBtn.classList.remove('border-red-500/40');
playSuccessSound(); // Áudio de teste
}
});
// Sensitive Mode Toggle Listener
sensitiveToggle.addEventListener('change', (e) => {
isSensitiveModeEnabled = e.target.checked;
if (isSensitiveModeEnabled) {
sensitiveStatusText.textContent = '✅ Filtro ativado (Esconde e avisa antes de mostrar)';
sensitiveStatusText.className = 'text-[10px] text-gray-400';
shieldSymbol.className = 'fa-solid fa-shield-halved';
} else {
sensitiveStatusText.textContent = '❌ Filtro desativado (Sem bloqueios nem avisos, vê tudo diretamente)';
sensitiveStatusText.className = 'text-[10px] text-amber-400 font-semibold';
shieldSymbol.className = 'fa-solid fa-unlock';
}
});
revealImageBtn.addEventListener('click', () => {
sensitiveOverlay.classList.add('hidden');
resultImage.classList.remove('blur-md');
});
// Theme Switcher Function
window.setTheme = (themeClass) => {
document.body.className = "bg-brand-dark text-white font-sans min-h-screen flex flex-col custom-scrollbar transition-colors duration-300 " + themeClass;
};
// Custom RGB Color Picker Handler
customColorPicker.addEventListener('input', (e) => {
const hexColor = e.target.value;
customColorHex.textContent = hexColor.toUpperCase();
// Set CSS variables for custom theme dynamically
document.documentElement.style.setProperty('--color-primary', hexColor);
document.documentElement.style.setProperty('--color-primary-dark', hexColor);
document.documentElement.style.setProperty('--glow-color', `${hexColor}40`);
window.setTheme(''); // Reset theme classes to use custom CSS variables
});
// Model selector listener (Banana vs ChatGPT 2.0)
document.querySelectorAll('.model-option').forEach(option => {
option.addEventListener('click', () => {
document.querySelectorAll('.model-option').forEach(o => {
o.classList.remove('border-amber-400', 'border-emerald-500', 'bg-amber-500/10', 'bg-emerald-500/10');
o.classList.add('border-brand-border', 'bg-brand-dark/60');
});
const radio = option.querySelector('input[type="radio"]');
radio.checked = true;
selectedModel = radio.value;
if (selectedModel === 'nano-banana-pro') {
option.classList.remove('border-brand-border', 'bg-brand-dark/60');
option.classList.add('border-amber-400', 'bg-amber-500/10');
activeModelTag.innerHTML = '🍌 Nano Banana Pro';
activeModelTag.className = 'text-xs bg-amber-400/10 text-amber-400 border border-amber-400/30 px-2.5 py-1 rounded-full font-bold flex items-center gap-1';
loaderAvatar.textContent = '🍌';
} else {
option.classList.remove('border-brand-border', 'bg-brand-dark/60');
option.classList.add('border-emerald-500', 'bg-emerald-500/10');
activeModelTag.innerHTML = '🌀 ChatGPT Images 2.0';
activeModelTag.className = 'text-xs bg-emerald-500/10 text-emerald-400 border border-emerald-500/30 px-2.5 py-1 rounded-full font-bold flex items-center gap-1';
loaderAvatar.textContent = '🌀';
}
});
});
// Initialize Timer
function startTimer() {
if (timerInterval) clearInterval(timerInterval);
timerInterval = setInterval(() => {
if (timerSeconds > 0) {
timerSeconds--;
updateTimerDisplay();
} else {
// Reset credits when timer reaches zero
remainingCredits = MAX_CREDITS;
timerSeconds = 2 * 60 * 60;
updateCreditsDisplay();
}
}, 1000);
}
function updateTimerDisplay() {
const hrs = Math.floor(timerSeconds / 3600);
const mins = Math.floor((timerSeconds % 3600) / 60);
const secs = timerSeconds % 60;
const formatted = `${String(hrs).padStart(2, '0')}:${String(mins).padStart(2, '0')}:${String(secs).padStart(2, '0')}`;
timerDisplayEl.textContent = formatted;
modalTimerEl.textContent = formatted;
}
function updateCreditsDisplay() {
remainingCountEl.textContent = remainingCredits;
if (remainingCredits <= 0) {
generateBtn.disabled = true;
generateBtn.classList.add('opacity-50', 'cursor-not-allowed');
} else {
generateBtn.disabled = false;
generateBtn.classList.remove('opacity-50', 'cursor-not-allowed');
}
}
// Handle Image Reference Uploads
dropZone.addEventListener('click', () => referenceInput.click());
referenceInput.addEventListener('change', (e) => {
if (e.target.files.length > 0) {
handleFiles(Array.from(e.target.files));
}
});
// Drag and drop handlers
dropZone.addEventListener('dragover', (e) => {
e.preventDefault();
dropZone.classList.add('border-primary');
});
dropZone.addEventListener('dragleave', () => {
dropZone.classList.remove('border-primary');
});
dropZone.addEventListener('drop', (e) => {
e.preventDefault();
dropZone.classList.remove('border-primary');
if (e.dataTransfer.files.length > 0) {
handleFiles(Array.from(e.dataTransfer.files));
}
});
function handleFiles(files) {
const imageFiles = files.filter(f => f.type.startsWith('image/'));
if (imageFiles.length === 0) {
alert('Por favor seleciona ficheiros de imagem válidos.');
return;
}
if (referenceImages.length + imageFiles.length > MAX_REFERENCES) {
alert(`Só podes carregar no máximo ${MAX_REFERENCES} fotos de referência.`);
}
const availableSlots = MAX_REFERENCES - referenceImages.length;
const filesToProcess = imageFiles.slice(0, availableSlots);
filesToProcess.forEach(file => {
const reader = new FileReader();
reader.onload = (e) => {
const fullDataUrl = e.target.result;
const mimeType = file.type;
const base64Clean = fullDataUrl.split(',')[1];
referenceImages.push({
id: 'ref_' + Date.now() + '_' + Math.random().toString(36).substr(2, 4),
name: file.name,
mimeType: mimeType,
dataUrl: fullDataUrl,
base64: base64Clean
});
renderReferenceGrid();
};
reader.readAsDataURL(file);
});
referenceInput.value = ''; // Reset input
}
function renderReferenceGrid() {
refCountEl.textContent = referenceImages.length;
if (referenceImages.length > 0) {
previewSection.classList.remove('hidden');
previewGrid.innerHTML = referenceImages.map(img => `
<div class="relative group aspect-square rounded-lg overflow-hidden border border-brand-border bg-brand-card">
<img src="${img.dataUrl}" alt="${escapeHtml(img.name)}" class="w-full h-full object-cover">
<button onclick="removeReference('${img.id}')" class="absolute top-1 right-1 bg-red-500/90 hover:bg-red-600 text-white rounded-full w-5 h-5 flex items-center justify-center text-[10px] shadow transition-colors">
<i class="fa-solid fa-xmark"></i>
</button>
</div>
`).join('');
} else {
previewSection.classList.add('hidden');
previewGrid.innerHTML = '';
}
}
window.removeReference = (id) => {
referenceImages = referenceImages.filter(img => img.id !== id);
renderReferenceGrid();
};
clearAllRefsBtn.addEventListener('click', (e) => {
e.stopPropagation();
referenceImages = [];
renderReferenceGrid();
});
// Presets Click Handler
document.querySelectorAll('.preset-btn').forEach(btn => {
btn.addEventListener('click', () => {
promptInput.value = btn.textContent.trim().replace(/^[\u2600-\u27BF\uD83C-\uDBFF\uDC00-\uDFFF]\s*/, '');
});
});
// API Call Handler with Exponential Backoff
async function fetchWithBackoff(url, options, maxRetries = 5) {
let delay = 1000;
for (let i = 0; i < maxRetries; i++) {
try {
const response = await fetch(url, options);
if (response.ok) {
return await response.json();
}
} catch (error) {
// Retry silently
}
await new Promise(resolve => setTimeout(resolve, delay));
delay *= 2;
}
throw new Error('Falha na ligação à API após várias tentativas.');
}
// Image Generation Trigger
generateBtn.addEventListener('click', async () => {
const promptText = promptInput.value.trim();
if (!promptText) {
alert('Por favor introduz uma descrição/prompt para a tua imagem.');
return;
}
if (remainingCredits <= 0) {
limitModal.classList.remove('hidden');
playErrorSound(); // Som de erro ao atingir limite
return;
}
currentGeneratedPrompt = promptText;
// Reset sensitive elements
sensitiveOverlay.classList.add('hidden');
resultImage.classList.remove('blur-md');
// Set UI Loading State
emptyState.classList.add('hidden');
imageContainer.classList.add('hidden');
actionBar.classList.add('hidden');
loaderState.classList.remove('hidden');
generateBtn.disabled = true;
statusBadge.textContent = 'A gerar...';
statusBadge.className = 'text-xs bg-primary/10 text-primary px-2.5 py-1 rounded-full font-medium border border-primary/20 animate-pulse';
const style = document.getElementById('style-select').value;
const aspect = document.getElementById('aspect-ratio').value;
const hasReferences = referenceImages.length > 0;
const modelName = selectedModel === 'nano-banana-pro' ? 'NANO BANANA PRO' : 'CHATGPT IMAGES 2.0';
// Construct prompt depending on sensitive mode toggle
const promptSuffix = isSensitiveModeEnabled ? ', aviso de moderação e conteúdo sensível aplicados' : '';
const enhancedPrompt = `${promptText}, estilo de modelo ${modelName}, estilo visual ${style}, proporção ${aspect}${promptSuffix}${hasReferences ? `, baseado nas ${referenceImages.length} fotos de referência fornecidas` : ''}`;
try {
let imageUrl = '';
// If reference images exist, use gemini-2.5-flash-image-preview
if (hasReferences) {
document.getElementById('loading-subtext').textContent = `[${modelName}] A processar ${referenceImages.length} fotos de referência e a criar imagem...`;
const endpoint = `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-image-preview:generateContent?key=${apiKey}`;
const parts = [{ text: enhancedPrompt }];
referenceImages.forEach(img => {
parts.push({
inlineData: {
mimeType: img.mimeType,
data: img.base64
}
});
});
const payload = {
contents: [{ parts }],
generationConfig: {
responseModalities: ['TEXT', 'IMAGE']
}
};
const result = await fetchWithBackoff(endpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
const base64Data = result.candidates?.[0]?.content?.parts?.find(p => p.inlineData)?.inlineData?.data;
if (base64Data) {
imageUrl = `data:image/png;base64,${base64Data}`;
} else {
throw new Error("Não foi possível gerar a imagem com base nas referências.");
}
} else {
// Standard Text-to-Image generation using Imagen 4
document.getElementById('loading-subtext').textContent = `[${modelName}] A sintetizar a imagem com alta fidelidade...`;
const endpoint = `https://generativelanguage.googleapis.com/v1beta/models/imagen-4.0-generate-001:predict?key=${apiKey}`;
const payload = {
instances: [{ prompt: enhancedPrompt }],
parameters: { sampleCount: 1 }
};
const result = await fetchWithBackoff(endpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
if (result.predictions?.[0]?.bytesBase64Encoded) {
imageUrl = `data:image/png;base64,${result.predictions[0].bytesBase64Encoded}`;
} else {
throw new Error("Não foi possível obter os dados da imagem gerada.");
}
}
// Deduct Credit ONLY after successful generation
remainingCredits--;
updateCreditsDisplay();
// 🔊 Toca o áudio de sucesso ("It worked out.wav")
playSuccessSound();
// Display Result
resultImage.src = imageUrl;
imageContainer.classList.remove('hidden');
resultPromptText.textContent = `[${modelName}] "${promptText}" (${style})`;
actionBar.classList.remove('hidden');
// If Sensitive Mode is ON (✅ ATIVADO), hide behind warning overlay with blur
// If Sensitive Mode is OFF (❌ DESATIVADO), display directly without any warnings or blur
if (isSensitiveModeEnabled) {
resultImage.classList.add('blur-md');
sensitiveOverlay.classList.remove('hidden');
} else {
resultImage.classList.remove('blur-md');
sensitiveOverlay.classList.add('hidden');
}
// Update Gallery
addToGallery(imageUrl, promptText, modelName);
statusBadge.textContent = 'Concluído';
statusBadge.className = 'text-xs bg-emerald-500/10 text-emerald-400 px-2.5 py-1 rounded-full font-medium border border-emerald-500/20';
} catch (err) {
// 🔊 Toca o áudio de erro ("It didnt work..wav")
playErrorSound();
alert('Erro ao gerar imagem: ' + err.message + '\n\nO teu crédito não foi consumido!');
emptyState.classList.remove('hidden');
statusBadge.textContent = 'Erro';
statusBadge.className = 'text-xs bg-red-500/10 text-red-400 px-2.5 py-1 rounded-full font-medium border border-red-500/20';
} finally {
loaderState.classList.add('hidden');
generateBtn.disabled = remainingCredits <= 0;
}
});
// Add Image to History Gallery
function addToGallery(url, prompt, model) {
historyImages.unshift({ url, prompt, model });
renderGallery();
}
function renderGallery() {
if (historyImages.length === 0) return;
galleryGrid.innerHTML = historyImages.map((img, idx) => `
<div class="group relative rounded-lg overflow-hidden border border-brand-border aspect-square bg-brand-dark cursor-pointer transition-all hover:border-primary">
<img src="${img.url}" alt="Gerada ${idx}" class="w-full h-full object-cover">
<div class="absolute inset-0 bg-black/60 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center space-x-2">
<button onclick="viewImage('${img.url}', '${escapeHtml(img.prompt)}', '${img.model}')" class="text-xs bg-primary text-brand-dark p-1.5 rounded-full hover:scale-110 transition-transform">
<i class="fa-solid fa-eye"></i>
</button>
</div>
</div>
`).join('');
}
function escapeHtml(str) {
return str.replace(/'/g, "\\'").replace(/"/g, '"');
}
window.viewImage = (url, prompt, model) => {
currentGeneratedPrompt = prompt;
resultImage.src = url;
resultPromptText.textContent = `[${model}] "${prompt}"`;
// Always remove blur when viewing from gallery
resultImage.classList.remove('blur-md');
sensitiveOverlay.classList.add('hidden');
emptyState.classList.add('hidden');
loaderState.classList.add('hidden');
imageContainer.classList.remove('hidden');
actionBar.classList.remove('hidden');
};
// Download functionality with reliable Base64 Blob triggering
downloadBtn.addEventListener('click', () => {
if (!resultImage.src) return;
const link = document.createElement('a');
link.href = resultImage.src;
link.download = `arte-ia-${Date.now()}.png`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
});
// Copy Prompt functionality
copyBtn.addEventListener('click', () => {
if (!currentGeneratedPrompt) return;
// Clipboard API fallback support
if (navigator.clipboard && window.isSecureContext) {
navigator.clipboard.writeText(currentGeneratedPrompt);
} else {
const textArea = document.createElement('textarea');
textArea.value = currentGeneratedPrompt;
document.body.appendChild(textArea);
textArea.select();
document.execCommand('copy');
document.body.removeChild(textArea);
}
const originalHTML = copyBtn.innerHTML;
copyBtn.innerHTML = `<i class="fa-solid fa-check text-emerald-400"></i> Copiado!`;
setTimeout(() => {
copyBtn.innerHTML = originalHTML;
}, 2000);
});
// Modal Controls
closeModalBtn.addEventListener('click', () => {
limitModal.classList.add('hidden');
});
// Initial setup
startTimer();
updateCreditsDisplay();
</script>
</body>
</html>
Public Last updated: 2026-08-19 02:24:46 PM
