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, '&quot;'); } 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