|
|
<!DOCTYPE html> |
|
|
<html lang="pt-BR" class="dark"> |
|
|
<head> |
|
|
<meta charset="UTF-8"> |
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|
|
<title>PromptCraft Studio v2.0</title> |
|
|
<script src="https://cdn.tailwindcss.com"></script> |
|
|
<script src="https://cdn.jsdelivr.net/npm/feather-icons/dist/feather.min.js"></script> |
|
|
<style> |
|
|
body { |
|
|
font-family: 'Inter', system-ui, sans-serif; |
|
|
background-color: #050505; |
|
|
color: #ffffff; |
|
|
} |
|
|
.step { |
|
|
display: none; |
|
|
opacity: 0; |
|
|
transform: translateY(10px); |
|
|
transition: opacity 0.4s ease, transform 0.4s ease; |
|
|
} |
|
|
.step.active { |
|
|
display: block; |
|
|
opacity: 1; |
|
|
transform: translateY(0); |
|
|
} |
|
|
.drop-zone { |
|
|
transition: all 0.2s ease; |
|
|
border-color: #333; |
|
|
} |
|
|
.drop-zone.drag-active { |
|
|
border-color: #fff; |
|
|
background-color: rgba(255, 255, 255, 0.05); |
|
|
transform: scale(1.01); |
|
|
} |
|
|
.glass-panel { |
|
|
background: rgba(20, 20, 20, 0.6); |
|
|
backdrop-filter: blur(12px); |
|
|
border: 1px solid rgba(255, 255, 255, 0.08); |
|
|
} |
|
|
.btn-primary { |
|
|
background: white; |
|
|
color: black; |
|
|
transition: all 0.2s; |
|
|
} |
|
|
.btn-primary:hover { |
|
|
transform: translateY(-1px); |
|
|
box-shadow: 0 4px 12px rgba(255, 255, 255, 0.15); |
|
|
} |
|
|
.loader { |
|
|
border: 3px solid rgba(255, 255, 255, 0.1); |
|
|
} |
|
|
@keyframes spin { |
|
|
0% { transform: rotate(0deg); } |
|
|
100% { transform: rotate(360deg); } |
|
|
} |
|
|
::-webkit-scrollbar { |
|
|
width: 8px; |
|
|
height: 8px; |
|
|
} |
|
|
::-webkit-scrollbar-track { |
|
|
background: #111; |
|
|
} |
|
|
::-webkit-scrollbar-thumb { |
|
|
background: #333; |
|
|
border-radius: 4px; |
|
|
} |
|
|
::-webkit-scrollbar-thumb:hover { |
|
|
background: #444; |
|
|
} |
|
|
.concept-card { |
|
|
transition: all 0.3s ease; |
|
|
} |
|
|
.concept-card.selected { |
|
|
border-color: white; |
|
|
background-color: rgba(255, 255, 255, 0.05); |
|
|
} |
|
|
.fade-in { |
|
|
animation: fadeIn 0.5s ease-in; |
|
|
} |
|
|
@keyframes fadeIn { |
|
|
from { opacity: 0; } |
|
|
to { opacity: 1; } |
|
|
} |
|
|
.color-chip { |
|
|
display: inline-flex; |
|
|
align-items: center; |
|
|
gap: 0.5rem; |
|
|
padding: 0.5rem 1rem; |
|
|
background-color: rgba(55, 65, 81, 0.5); |
|
|
border-radius: 9999px; |
|
|
border: 1px solid rgba(55, 65, 81, 0.7); |
|
|
} |
|
|
.toast { |
|
|
position: fixed; |
|
|
bottom: 1rem; |
|
|
right: 1rem; |
|
|
background-color: rgba(31, 41, 55, 0.9); |
|
|
color: white; |
|
|
padding: 0.75rem 1rem; |
|
|
border-radius: 0.5rem; |
|
|
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1); |
|
|
animation: pulse 2s infinite; |
|
|
} |
|
|
.toast.success { |
|
|
background-color: rgba(34, 197, 94, 0.9); |
|
|
} |
|
|
.toast.error { |
|
|
background-color: rgba(239, 68, 68, 0.9); |
|
|
} |
|
|
</style> |
|
|
</head> |
|
|
<body class="min-h-screen flex flex-col antialiased"> |
|
|
|
|
|
<header class="border-b border-gray-800 glass-panel sticky top-0 z-50"> |
|
|
<div class="container mx-auto px-6 py-4 flex justify-between items-center"> |
|
|
<div class="flex items-center gap-3"> |
|
|
<div class="w-8 h-8 bg-white rounded flex items-center justify-center text-black font-bold text-lg">P</div> |
|
|
<span class="font-bold text-xl tracking-tight">PromptCraft Studio</span> |
|
|
</div> |
|
|
<div class="text-xs text-gray-500 font-mono">v2.0 Optimized</div> |
|
|
</div> |
|
|
</header> |
|
|
|
|
|
|
|
|
<main class="flex-grow container mx-auto px-4 py-8"> |
|
|
<div class="max-w-5xl mx-auto"> |
|
|
|
|
|
<section class="text-center mb-10"> |
|
|
<h1 class="text-4xl md:text-5xl font-extrabold mb-4 bg-clip-text text-transparent bg-gradient-to-r from-white to-gray-400">Product Photography AI</h1> |
|
|
<p class="text-gray-400 text-lg max-w-2xl mx-auto"> |
|
|
Transforme produtos simples em campanhas editoriais de alta performance com Inteligência Artificial. |
|
|
</p> |
|
|
</section> |
|
|
|
|
|
|
|
|
<div class="glass-panel rounded-2xl p-6 md:p-10 shadow-2xl relative overflow-hidden"> |
|
|
|
|
|
<div class="absolute top-0 left-0 h-1 bg-gradient-to-r from-blue-500 to-purple-500 transition-all duration-500" id="progress-bar" style="width: 25%"></div> |
|
|
|
|
|
|
|
|
<div id="toast-container" class="fixed top-24 right-6 flex flex-col gap-2 z-50 pointer-events-none"></div> |
|
|
|
|
|
|
|
|
<section id="step-product" class="step active" aria-label="Passo 1: Produto e Imagens"> |
|
|
<h2 class="text-2xl font-bold mb-6 text-center">O que vamos fotografar hoje?</h2> |
|
|
|
|
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8"> |
|
|
<div> |
|
|
<label class="block text-sm font-medium text-gray-300 mb-2">Imagens de Referência</label> |
|
|
<div id="drop-area" |
|
|
class="drop-zone border-2 border-dashed border-gray-700 rounded-xl p-8 text-center cursor-pointer hover:border-gray-500 transition-all h-64 flex flex-col items-center justify-center relative bg-gray-900/50"> |
|
|
<i data-feather="upload-cloud" class="w-10 h-10 mb-3 text-gray-400"></i> |
|
|
<p class="text-lg font-medium text-white">Arraste fotos aqui</p> |
|
|
<p class="text-sm text-gray-500 mt-1">ou clique para selecionar</p> |
|
|
<input type="file" id="image-upload" accept="image/*" multiple class="hidden"> |
|
|
<div id="uploading-overlay" class="hidden absolute inset-0 bg-black/80 flex flex-col items-center justify-center rounded-xl z-10"> |
|
|
<div class="loader border-blue-500 mb-2"></div> |
|
|
<span class="text-xs text-gray-300">Enviando para Imgur...</span> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
|
|
|
<div id="image-preview-container" class="hidden mt-4 grid grid-cols-3 gap-2"></div> |
|
|
</div> |
|
|
|
|
|
<div class="flex flex-col gap-6"> |
|
|
<div> |
|
|
<label for="product-input" class="block text-sm font-medium text-gray-300 mb-2">Nome do Produto</label> |
|
|
<input type="text" id="product-input" |
|
|
class="w-full bg-gray-800 border border-gray-700 rounded-xl px-4 py-3 focus:ring-2 focus:ring-white focus:border-transparent outline-none transition-all placeholder-gray-600" |
|
|
placeholder="Ex: Camiseta Oversized Algodão Egípcio 100%"> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<div class="mt-8 flex justify-end"> |
|
|
<button id="btn-next-1" onclick="nextStep('colors')" |
|
|
class="btn-primary px-8 py-3 rounded-xl font-bold flex items-center gap-2"> |
|
|
Continuar <i data-feather="arrow-right" class="w-4 h-4"></i> |
|
|
</button> |
|
|
</div> |
|
|
</section> |
|
|
|
|
|
|
|
|
<section id="step-colors" class="step hidden" aria-label="Passo 2: Cores e Detalhes"> |
|
|
<h2 class="text-2xl font-bold mb-6 text-center">Paleta de Cores</h2> |
|
|
|
|
|
<div class="bg-gray-800 rounded-xl p-6 mb-6 border border-gray-700"> |
|
|
<h3 class="text-sm font-medium text-gray-400 mb-3 uppercase tracking-wider">Cores Detectadas</h3> |
|
|
<div id="detected-colors" class="flex flex-wrap gap-3 min-h-[40px]"> |
|
|
<span class="text-sm text-gray-400 italic">Nenhuma cor detectada</span> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<div class="mb-8"> |
|
|
<label for="colors-input" class="block text-sm font-medium text-gray-300 mb-2">Cores Adicionais</label> |
|
|
<input type="text" id="colors-input" |
|
|
class="w-full bg-gray-800 border border-gray-700 rounded-xl px-4 py-3 focus:ring-2 focus:ring-white focus:border-transparent outline-none placeholder-gray-600" |
|
|
placeholder="Ex: Off-White, Bege, Navy (separadas por vírgula)"> |
|
|
</div> |
|
|
|
|
|
<div class="flex justify-between items-center"> |
|
|
<button class="text-gray-400 hover:text-white px-4 py-2 font-medium transition-colors" onclick="prevStep('product')"> |
|
|
Voltar |
|
|
</button> |
|
|
<button id="btn-next-2" onclick="nextStep('concept')" |
|
|
class="btn-primary px-8 py-3 rounded-xl font-bold flex items-center gap-2"> |
|
|
Definir Conceito <i data-feather="image" class="w-4 h-4"></i> |
|
|
</button> |
|
|
</div> |
|
|
</section> |
|
|
|
|
|
|
|
|
<section id="step-concept" class="step hidden" aria-label="Passo 3: Conceito Visual"> |
|
|
<h2 class="text-2xl font-bold mb-8 text-center">Escolha o Estilo</h2> |
|
|
|
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-8" role="radiogroup"> |
|
|
<button class="concept-card w-full text-left p-6 rounded-xl border border-gray-700 hover:border-white hover:bg-white/5 transition-all group" |
|
|
data-concept="FOTO PRINCIPAL" onclick="selectConcept('FOTO PRINCIPAL')"> |
|
|
<i data-feather="aperture" class="w-8 h-8 mb-4 text-gray-400 group-hover:text-white"></i> |
|
|
<h3 class="font-bold text-lg mb-1">Foto Principal (Catálogo)</h3> |
|
|
<p class="text-sm text-gray-500">Fundo infinito, iluminação de estúdio perfeita, foco 100% no produto.</p> |
|
|
</button> |
|
|
|
|
|
<button class="concept-card w-full text-left p-6 rounded-xl border border-gray-700 hover:border-white hover:bg-white/5 transition-all group" |
|
|
data-concept="FOTO DESCRIÇÃO" onclick="selectConcept('FOTO DESCRIÇÃO')"> |
|
|
<i data-feather="zoom-in" class="w-8 h-8 mb-4 text-gray-400 group-hover:text-white"></i> |
|
|
<h3 class="font-bold text-lg mb-1">Close-up Textura</h3> |
|
|
<p class="text-sm text-gray-500">Detalhe macro do tecido e acabamento. Sensação tátil.</p> |
|
|
</button> |
|
|
|
|
|
<button class="concept-card w-full text-left p-6 rounded-xl border border-gray-700 hover:border-white hover:bg-white/5 transition-all group" |
|
|
data-concept="FOTO AVALIAÇÃO" onclick="selectConcept('FOTO AVALIAÇÃO')"> |
|
|
<i data-feather="smartphone" class="w-8 h-8 mb-4 text-gray-400 group-hover:text-white"></i> |
|
|
<h3 class="font-bold text-lg mb-1">Estilo "Review Real"</h3> |
|
|
<p class="text-sm text-gray-500">Foto orgânica, estilo UGC (User Generated Content), luz natural.</p> |
|
|
</button> |
|
|
|
|
|
<button class="concept-card w-full text-left p-6 rounded-xl border border-gray-700 hover:border-white hover:bg-white/5 transition-all group" |
|
|
data-concept="FOTO DESCRIÇÃO 2" onclick="selectConcept('FOTO DESCRIÇÃO 2')"> |
|
|
<i data-feather="user" class="w-8 h-8 mb-4 text-gray-400 group-hover:text-white"></i> |
|
|
<h3 class="font-bold text-lg mb-1">Lifestyle / Modelo</h3> |
|
|
<p class="text-sm text-gray-500">Produto sendo usado em contexto urbano ou estúdio.</p> |
|
|
</button> |
|
|
</div> |
|
|
|
|
|
<div class="flex justify-between items-center"> |
|
|
<button class="text-gray-400 hover:text-white px-4 py-2 font-medium transition-colors" onclick="prevStep('colors')">Voltar</button> |
|
|
</div> |
|
|
</section> |
|
|
|
|
|
|
|
|
<section id="step-result" class="step hidden" aria-label="Passo 4: Geração"> |
|
|
<h2 class="text-2xl font-bold mb-6 text-center">Seu Prompt Otimizado</h2> |
|
|
|
|
|
|
|
|
<div class="mb-4"> |
|
|
<div class="flex justify-between items-center mb-2"> |
|
|
<label class="text-xs font-bold text-gray-500 uppercase tracking-widest">Google Gemini API Key</label> |
|
|
</div> |
|
|
<input type="password" id="api-key-input" |
|
|
value="AIzaSyDGL3xn15bIkEHkI-24OMO9Ub2G_pDnQ-M" |
|
|
class="w-full bg-black/30 border border-gray-800 rounded px-3 py-2 text-xs text-gray-400 focus:text-white transition-colors"> |
|
|
</div> |
|
|
|
|
|
<div class="bg-gray-900 p-6 rounded-lg mb-6 relative group"> |
|
|
<div class="absolute top-4 right-4 opacity-0 group-hover:opacity-100 transition-opacity"> |
|
|
<button id="btn-copy-prompt" class="bg-white text-black px-3 py-1 text-xs font-bold rounded shadow hover:bg-gray-200 transition-colors">Copiar</button> |
|
|
</div> |
|
|
<pre id="prompt-output" class="p-6 text-xs md:text-sm font-mono text-gray-300 whitespace-pre-wrap max-h-64 overflow-y-auto custom-scrollbar"></pre> |
|
|
</div> |
|
|
|
|
|
<div class="mt-8 flex justify-center"> |
|
|
<button class="text-gray-500 hover:text-white text-sm" onclick="resetForm()">Começar Novo Projeto</button> |
|
|
</div> |
|
|
</section> |
|
|
</div> |
|
|
</div> |
|
|
</main> |
|
|
|
|
|
|
|
|
<footer class="border-t border-gray-900 mt-auto py-8"> |
|
|
<div class="container mx-auto px-4 text-center text-gray-600 text-sm"> |
|
|
PromptCraft Studio v2.0 • Powered by Google Gemini & Imagen |
|
|
</div> |
|
|
</footer> |
|
|
|
|
|
|
|
|
<div id="image-modal" class="fixed inset-0 bg-black bg-opacity-90 flex items-center justify-center z-50 hidden"> |
|
|
<div class="relative max-w-4xl max-h-screen"> |
|
|
<button id="close-modal" class="absolute -top-10 right-0 text-white text-3xl cursor-pointer hover:text-gray-300">×</button> |
|
|
<img id="modal-image" src="" alt="Full size preview" class="max-w-full max-h-screen object-contain"> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
|
|
|
<script> |
|
|
// Sistema Imgur GARANTIDO - Upload Direto + Detecção Melhorada |
|
|
// Gemini Flash Integration - Geração Interna de Imagens |
|
|
|
|
|
// Store user inputs |
|
|
let userData = { |
|
|
product: '', |
|
|
colors: '', |
|
|
concept: '', |
|
|
images: [], // Armazena objetos {file, dataUrl, uploadedUrl} |
|
|
additionalColors: [], |
|
|
hostedImageUrls: [], |
|
|
detectedColors: [] |
|
|
}; |
|
|
|
|
|
// Store image URLs for sharing |
|
|
let imageUrls = []; |
|
|
|
|
|
// Variáveis de controle |
|
|
let useImgurDirect = true; // SEMPRE usar Imgur diretamente |
|
|
let uploadMethod = 'Imgur Direto'; // Uploader Imgur Direto (GARANTIDO) |
|
|
|
|
|
class ImgurDirectUploader { |
|
|
constructor() { |
|
|
// Client ID público do Imgur (funciona sem configuração) |
|
|
this.clientId = '546c25a59c58ad7'; |
|
|
} |
|
|
|
|
|
async uploadImage(file, onProgress) { |
|
|
return new Promise((resolve, reject) => { |
|
|
const formData = new FormData(); |
|
|
formData.append('image', file); |
|
|
formData.append('type', 'file'); |
|
|
formData.append('name', file.name); |
|
|
|
|
|
const xhr = new XMLHttpRequest(); |
|
|
|
|
|
xhr.upload.addEventListener('progress', (e) => { |
|
|
if (e.lengthComputable) { |
|
|
const percentComplete = (e.loaded / e.total) * 100; |
|
|
onProgress && onProgress(Math.round(percentComplete)); |
|
|
}}); |
|
|
|
|
|
xhr.addEventListener('load', () => { |
|
|
if (xhr.status === 200) { |
|
|
try { |
|
|
const response = JSON.parse(xhr.responseText); |
|
|
if (response.success && response.data) { |
|
|
resolve({ |
|
|
id: response.data.id, |
|
|
url: response.data.link, |
|
|
deletehash: response.data.deletehash, |
|
|
size: response.data.size, |
|
|
name: file.name, |
|
|
originalSize: file.size, |
|
|
type: response.data.type || 'image/jpeg', |
|
|
width: response.data.width, |
|
|
height: response.data.height |
|
|
}); |
|
|
} else { |
|
|
reject(new Error('Resposta Imgur inválida')); |
|
|
} |
|
|
} catch (e) { |
|
|
reject(new Error('Erro ao processar resposta Imgur')); |
|
|
} |
|
|
} else { |
|
|
reject(new Error(`HTTP ${xhr.status}: ${xhr.statusText}`)); |
|
|
}}); |
|
|
|
|
|
xhr.addEventListener('error', () => { |
|
|
reject(new Error('Erro de rede Imgur')); |
|
|
}}); |
|
|
|
|
|
xhr.addEventListener('timeout', () => { |
|
|
reject(new Error('Timeout Imgur')); |
|
|
}}); |
|
|
|
|
|
xhr.open('POST', 'https://api.imgur.com/3/image'); |
|
|
xhr.timeout = 30000; |
|
|
xhr.setRequestHeader('Authorization', `Client-ID ${this.clientId}`)); |
|
|
xhr.send(formData); |
|
|
}}); |
|
|
} |
|
|
|
|
|
async uploadMultiple(files, onProgress) { |
|
|
const results = []; |
|
|
const totalFiles = files.length; |
|
|
|
|
|
for (let i = 0; i < files.length; i++) { |
|
|
try { |
|
|
const file = files[i]; |
|
|
const result = await this.uploadImage(file, (progress) => { |
|
|
const overallProgress = ((i + progress/100) / totalFiles) * 100; |
|
|
onProgress && onProgress(Math.round(overallProgress), i + 1, totalFiles)); |
|
|
}}); |
|
|
results.push({ ...result, success: true }); |
|
|
} catch (error) { |
|
|
console.warn(`Erro upload Imgur ${files[i].name}:`, error); |
|
|
results.push({ success: false, error: error.message, name: files[i].name }); |
|
|
} |
|
|
} |
|
|
|
|
|
return results; |
|
|
} |
|
|
} |
|
|
|
|
|
// Inicialização |
|
|
let imgurUploader = null; |
|
|
|
|
|
// Inicializar uploader Imgur |
|
|
function initializeImgurUploader() { |
|
|
imgurUploader = new ImgurDirectUploader(); |
|
|
console.log('✅ Uploader Imgur inicializado - Upload garantido!'); |
|
|
} |
|
|
|
|
|
// Upload para Imgur (GARANTIDO) |
|
|
async function uploadToImgur(imageFiles) { |
|
|
if (!imgurUploader) { |
|
|
initializeImgurUploader(); |
|
|
} |
|
|
|
|
|
uploadMethod = 'Imgur Direto'; |
|
|
showUploadingState(true, uploadMethod); |
|
|
|
|
|
try { |
|
|
const results = await imgurUploader.uploadMultiple( |
|
|
imageFiles.map(img => img.file), |
|
|
(progress, current, total) => updateUploadProgress(progress, current, total) |
|
|
); |
|
|
const successful = results.filter(r => r.success); |
|
|
if (successful.length > 0) { |
|
|
userData.hostedImageUrls = successful.map(r => r.url); |
|
|
console.log(`✅ Upload Imgur: ${successful.length}/${results.length} imagens'); |
|
|
|
|
|
// Atualizar status das imagens |
|
|
results.forEach((result, index) => { |
|
|
updateImageStatus(index + 1, result.success ? '✓ Imgur OK' : '❌ Falhou')); |
|
|
}}}); |
|
|
return results; |
|
|
} else { |
|
|
throw new Error('Nenhuma imagem foi enviada com sucesso')); |
|
|
} |
|
|
} catch (error) { |
|
|
console.error('Erro upload Imgur:', error); |
|
|
throw error; |
|
|
} |
|
|
} |
|
|
|
|
|
// Fallback Local (só se Imgur falhar completamente) |
|
|
async function uploadToLocalFallback(imageFiles) { |
|
|
uploadMethod = 'Local (Fallback)'; |
|
|
showUploadingState(true, uploadMethod); |
|
|
|
|
|
const results = []; |
|
|
for (let i = 0; i < imageFiles.length; i++) { |
|
|
const imgData = imageFiles[i]; |
|
|
updateImageStatus(i + 1, '⚙️ Processando...')); |
|
|
try { |
|
|
await new Promise(resolve => setTimeout(resolve, 300)); |
|
|
userData.hostedImageUrls.push(imgData.dataUrl); |
|
|
results.push({ success: true, url: imgData.dataUrl }); |
|
|
updateImageStatus(i + 1, '✓ Local OK')); |
|
|
} catch (error) { |
|
|
results.push({ success: false, error: error.message }); |
|
|
updateImageStatus(i + 1, '❌ Erro')); |
|
|
} |
|
|
const progress = Math.round(((i + 1) / imageFiles.length) * 100; |
|
|
updateUploadProgress(progress, i + 1, imageFiles.length)); |
|
|
}} |
|
|
|
|
|
return results; |
|
|
} |
|
|
|
|
|
// Navegação entre steps |
|
|
async function nextStep(nextStepId) { |
|
|
const currentStep = document.querySelector('.step.active')); |
|
|
const nextStepElement = document.getElementById(`step-${nextStepId}`)); |
|
|
if (!currentStep || !nextStepElement) { |
|
|
console.error('Step não encontrado')); |
|
|
return; |
|
|
} |
|
|
|
|
|
if (currentStep.id === 'step-product') { |
|
|
userData.product = document.getElementById('product-input').value.trim()); |
|
|
if (!userData.product) { |
|
|
alert('Por favor, descreva o produto.')); |
|
|
return; |
|
|
} |
|
|
|
|
|
// Inicializar uploader Imgur |
|
|
initializeImgurUploader(); |
|
|
|
|
|
// Processar imagens |
|
|
if (userData.images.length > 0) { |
|
|
try { |
|
|
// Tentar upload Imgur PRIMEIRO |
|
|
try { |
|
|
await uploadToImgur(userData.images); |
|
|
} catch (imgurError) { |
|
|
console.warn('⚠️ Imgur falhou, tentando fallback local:', imgurError); |
|
|
await uploadToLocalFallback(userData.images); |
|
|
} |
|
|
|
|
|
// Detectar cores |
|
|
const detectedColors = await detectColorsFromImages(userData.hostedImageUrls); |
|
|
displayDetectedColors(detectedColors); |
|
|
} catch (error) { |
|
|
console.error('Erro crítico no processamento:', error); |
|
|
alert('Erro ao processar imagens. Tente novamente.')); |
|
|
return; |
|
|
} finally { |
|
|
showUploadingState(false)); |
|
|
} |
|
|
} |
|
|
|
|
|
// Navegar |
|
|
currentStep.classList.remove('active')); |
|
|
currentStep.classList.add('hidden')); |
|
|
nextStepElement.classList.remove('hidden')); |
|
|
nextStepElement.classList.add('active')); |
|
|
} else if (currentStep.id === 'step-colors') { |
|
|
const additionalColorsInput = document.getElementById('colors-input').value.trim()); |
|
|
if (additionalColorsInput) { |
|
|
userData.additionalColors = additionalColorsInput.split(',').map(color => color.trim())); |
|
|
} else { |
|
|
userData.additionalColors = []; |
|
|
} |
|
|
|
|
|
const allColors = [...new Set([...(userData.detectedColors || []), ...userData.additionalColors])]; |
|
|
userData.colors = allColors.join(', ')); |
|
|
if (!userData.colors && userData.detectedColors.length === 0) { |
|
|
alert('Por favor, informe as cores disponíveis.')); |
|
|
return; |
|
|
} |
|
|
|
|
|
currentStep.classList.remove('active')); |
|
|
currentStep.classList.add('hidden')); |
|
|
nextStepElement.classList.remove('hidden')); |
|
|
nextStepElement.classList.add('active')); |
|
|
} else if (currentStep.id === 'step-concept') { |
|
|
if (!userData.concept) { |
|
|
alert('Por favor, selecione um conceito.')); |
|
|
return; |
|
|
} |
|
|
|
|
|
await generatePrompt()); |
|
|
currentStep.classList.remove('active')); |
|
|
currentStep.classList.add('hidden')); |
|
|
nextStepElement.classList.remove('hidden')); |
|
|
nextStepElement.classList.add('active')); |
|
|
} |
|
|
} |
|
|
|
|
|
function prevStep(prevStepId) { |
|
|
const currentStep = document.querySelector('.step.active')); |
|
|
const prevStepElement = document.getElementById(`step-${prevStepId}`)); |
|
|
if (!currentStep || !prevStepElement) { |
|
|
console.error('Step não encontrado')); |
|
|
return; |
|
|
} |
|
|
|
|
|
currentStep.classList.remove('active')); |
|
|
currentStep.classList.add('hidden')); |
|
|
prevStepElement.classList.remove('hidden')); |
|
|
prevStepElement.classList.add('active')); |
|
|
} |
|
|
|
|
|
// Estado de upload |
|
|
function showUploadingState(show, mode = 'Imgur Direto') { |
|
|
let loadingDiv = document.getElementById('uploading-overlay'); |
|
|
if (show) { |
|
|
loadingDiv = document.createElement('div'); |
|
|
loadingDiv.id = 'uploading-overlay'; |
|
|
loadingDiv.className = 'fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50'; |
|
|
loadingDiv.innerHTML = ` |
|
|
<div class="bg-gray-900 rounded-lg p-6 flex flex-col items-center gap-4 max-w-md w-full mx-4"> |
|
|
<div class="animate-spin rounded-full h-8 w-8 border-b-2 ${mode.includes('Imgur') ? 'border-blue-500' : 'border-green-500'}"></div> |
|
|
<span class="text-white text-center font-medium"> |
|
|
${mode.includes('Imgur') ? 'Enviando para Imgur...' : 'Processando localmente...'} |
|
|
</span> |
|
|
<div class="w-full"> |
|
|
<div class="flex justify-between text-xs text-gray-400 mb-1"> |
|
|
<span id="upload-status">Preparando...</span> |
|
|
<span id="upload-progress">0%</span> |
|
|
</div> |
|
|
<div class="w-full bg-gray-800 rounded-full h-2"> |
|
|
<div id="upload-bar" class="${mode.includes('Imgur') ? 'bg-blue-500' : 'bg-green-500'} h-2 rounded-full transition-all duration-300" style="width: 0%"></div> |
|
|
</div> |
|
|
</div> |
|
|
<div class="text-xs text-gray-500 text-center"> |
|
|
${mode.includes('Imgur') ? 'Upload rápido e persistente' : 'Processamento no navegador'} |
|
|
</div> |
|
|
</div> |
|
|
`; |
|
|
document.body.appendChild(loadingDiv); |
|
|
} else if (loadingDiv) { |
|
|
loadingDiv.remove()); |
|
|
} |
|
|
} |
|
|
|
|
|
function updateUploadProgress(percent, current, total) { |
|
|
const statusEl = document.getElementById('upload-status')); |
|
|
const progressEl = document.getElementById('upload-progress')); |
|
|
const barEl = document.getElementById('upload-bar')); |
|
|
if (statusEl && progressEl && barEl) { |
|
|
statusEl.textContent = `Enviando ${current || '?'} de ${total || '?'}'; |
|
|
progressEl.textContent = `${percent}%`; |
|
|
barEl.style.width = `${percent}%`; |
|
|
} |
|
|
} |
|
|
|
|
|
function updateImageStatus(imageIndex, status) { |
|
|
const previews = document.querySelectorAll('#image-preview-container > div'); |
|
|
if (previews[imageIndex - 1]) { |
|
|
const statusEl = previews[imageIndex - 1].querySelector('.upload-status')); |
|
|
if (statusEl) { |
|
|
statusEl.textContent = status; |
|
|
const parentEl = statusEl.parentElement; |
|
|
parentEl.classList.remove('bg-green-600', 'bg-blue-600', 'bg-red-600', 'bg-yellow-600', 'bg-gray-600')); |
|
|
if (status.includes('✓ Imgur')) { |
|
|
parentEl.classList.add('bg-blue-600')); |
|
|
} else if (status.includes('✓ Local')) { |
|
|
parentEl.classList.add('bg-green-600')); |
|
|
} else if (status.includes('Enviando') || status.includes('Processando')) { |
|
|
parentEl.classList.add('bg-yellow-600')); |
|
|
} else if (status.includes('Erro')) { |
|
|
parentEl.classList.add('bg-red-600')); |
|
|
} else { |
|
|
parentEl.classList.add('bg-gray-600')); |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
// Detecção de cores |
|
|
async function detectColorsFromImages(imageSources) { |
|
|
const detectedColors = []; |
|
|
for (const source of imageSources) { |
|
|
try { |
|
|
const colors = await extractColorsFromImageSource(source)); |
|
|
detectedColors.push(...colors)); |
|
|
} catch (error) { |
|
|
console.error('Erro ao processar imagem:', error); |
|
|
} |
|
|
} |
|
|
|
|
|
const uniqueColors = [...new Set(detectedColors)].slice(0, 10); |
|
|
return uniqueColors; |
|
|
} |
|
|
|
|
|
function extractColorsFromImageSource(imageSource) { |
|
|
return new Promise((resolve) => { |
|
|
const img = new Image(); |
|
|
if (imageSource.startsWith('http')) { |
|
|
img.crossOrigin = 'anonymous'; |
|
|
} |
|
|
img.onload = function() { |
|
|
const canvas = document.createElement('canvas'); |
|
|
const ctx = canvas.getContext('2d')); |
|
|
const maxSize = 100; |
|
|
let { width, height } = img; |
|
|
if (width > height) { |
|
|
if (width > maxSize) { |
|
|
height = (height * maxSize) / width; |
|
|
width = maxSize; |
|
|
} |
|
|
} else { |
|
|
if (height > maxSize) { |
|
|
width = (width * maxSize) / height; |
|
|
height = maxSize; |
|
|
} |
|
|
} |
|
|
canvas.width = width; |
|
|
canvas.height = height; |
|
|
ctx.drawImage(img, 0, 0, width, height); |
|
|
const imageData = ctx.getImageData(0, 0, width, height); |
|
|
const data = imageData.data; |
|
|
const colors = []; |
|
|
const sampleRate = 5; |
|
|
for (let i = 0; i < data.length; i += 4 * sampleRate) { |
|
|
const r = data[i]; |
|
|
const g = data[i + 1]; |
|
|
const b = data[i + 2]; |
|
|
const a = data[i + 3]; |
|
|
if (a < 128) continue; |
|
|
const hex = rgbToHex(r, g, b); |
|
|
colors.push(hex); |
|
|
} |
|
|
const dominantColors = getDominantColors(colors, 3); |
|
|
resolve(dominantColors); |
|
|
}; |
|
|
img.onerror = () => { |
|
|
console.warn('Não foi possível carregar imagem para análise de cores')); |
|
|
resolve([])); |
|
|
}}; |
|
|
img.src = imageSource; |
|
|
}}); |
|
|
} |
|
|
|
|
|
// Funções de cor |
|
|
function rgbToHex(r, g, b) { |
|
|
return "#" + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1).toUpperCase()); |
|
|
} |
|
|
|
|
|
function getDominantColors(colors, count) { |
|
|
const colorCount = {}; |
|
|
colors.forEach(color => { |
|
|
colorCount[color] = (colorCount[color] || 0) + 1; |
|
|
}}); |
|
|
const sortedColors = Object.entries(colorCount) |
|
|
.sort(([,a], [,b]) => b - a) |
|
|
.map(([color]) => color); |
|
|
const filteredColors = filterSimilarColors(sortedColors); |
|
|
return filteredColors.slice(0, count); |
|
|
} |
|
|
|
|
|
function filterSimilarColors(colors) { |
|
|
const filtered = []; |
|
|
const threshold = 30; |
|
|
for (const color of colors) { |
|
|
let isSimilar = false; |
|
|
for (const filteredColor of filtered) { |
|
|
if (colorDifference(color, filteredColor) < threshold) { |
|
|
isSimilar = true; |
|
|
break; |
|
|
} |
|
|
} |
|
|
if (!isSimilar) { |
|
|
filtered.push(color)); |
|
|
} |
|
|
} |
|
|
return filtered; |
|
|
} |
|
|
|
|
|
function colorDifference(color1, color2) { |
|
|
const rgb1 = hexToRgb(color1)); |
|
|
const rgb2 = hexToRgb(color2)); |
|
|
return Math.sqrt( |
|
|
Math.pow(rgb1.r - rgb2.r, 2) + |
|
|
Math.pow(rgb1.g - rgb2.g, 2) + |
|
|
Math.pow(rgb1.b - rgb2.b, 2) |
|
|
); |
|
|
} |
|
|
|
|
|
function hexToRgb(hex) { |
|
|
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)); |
|
|
return result ? { |
|
|
r: parseInt(result[1], 16), |
|
|
g: parseInt(result[2], 16), |
|
|
b: parseInt(result[3], 16) |
|
|
} : {r: 0, g: 0, b: 0}; |
|
|
} |
|
|
|
|
|
function displayDetectedColors(colors) { |
|
|
const container = document.getElementById('detected-colors')); |
|
|
userData.detectedColors = colors; |
|
|
if (colors.length === 0) { |
|
|
container.innerHTML = '<span class="text-sm text-gray-400">Nenhuma cor detectada</span>'; |
|
|
return; |
|
|
} |
|
|
container.innerHTML = ''; |
|
|
colors.forEach((color, index) => { |
|
|
const colorElement = document.createElement('div')); |
|
|
colorElement.className = 'flex items-center gap-2 px-3 py-2 bg-gray-800 rounded-lg relative'; |
|
|
colorElement.innerHTML = ` |
|
|
<div class="w-4 h-4 rounded-full" style="background-color: ${color}"></div> |
|
|
<span class="text-sm">${color}</span> |
|
|
<button class="remove-color ml-2 text-red-500 hover:text-red-300" data-index="${index}"> |
|
|
<i data-feather="x" class="w-4 h-4"></i> |
|
|
</button> |
|
|
`; |
|
|
container.appendChild(colorElement)); |
|
|
}}); |
|
|
container.querySelectorAll('.remove-color').forEach(button => { |
|
|
button.addEventListener('click', function() { |
|
|
const index = parseInt(this.getAttribute('data-index'))); |
|
|
removeDetectedColor(index)); |
|
|
}}); |
|
|
feather.replace()); |
|
|
} |
|
|
|
|
|
function removeDetectedColor(index) { |
|
|
if (userData.detectedColors && index >= 0 && index < userData.detectedColors.length) { |
|
|
userData.detectedColors.splice(index, 1); |
|
|
displayDetectedColors(userData.detectedColors)); |
|
|
const allColors = [...new Set([...userData.detectedColors, ...userData.additionalColors])]; |
|
|
userData.colors = allColors.join(', ')); |
|
|
} |
|
|
} |
|
|
|
|
|
// Seleção de conceito |
|
|
async function selectConcept(concept) { |
|
|
console.log('selectConcept() chamado com:', concept); |
|
|
userData.concept = concept; |
|
|
await nextStep('result')); |
|
|
} |
|
|
|
|
|
// Gerar prompt |
|
|
async function generatePrompt() { |
|
|
console.log('generatePrompt() iniciado'); |
|
|
showLoadingState(true)); |
|
|
try { |
|
|
updatePromptDisplay(userData.concept)); |
|
|
createConceptSelector()); |
|
|
console.log('Prompt gerado com sucesso')); |
|
|
} catch (error) { |
|
|
console.error('Erro ao gerar prompt:', error); |
|
|
alert('⚠️ Erro ao gerar prompt. Tente novamente.')); |
|
|
} finally { |
|
|
showLoadingState(false)); |
|
|
} |
|
|
} |
|
|
|
|
|
// Estado de loading |
|
|
function showLoadingState(show) { |
|
|
let loadingDiv = document.getElementById('loading-overlay')); |
|
|
if (show) { |
|
|
loadingDiv = document.createElement('div')); |
|
|
loadingDiv.id = 'loading-overlay'; |
|
|
loadingDiv.className = 'fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50'; |
|
|
loadingDiv.innerHTML = ` |
|
|
<div class="bg-gray-900 rounded-lg p-6 flex flex-col items-center gap-4"> |
|
|
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-white"></div> |
|
|
<span class="text-white">Processando...</span> |
|
|
</div> |
|
|
`; |
|
|
document.body.appendChild(loadingDiv)); |
|
|
} else if (loadingDiv) { |
|
|
loadingDiv.remove()); |
|
|
} |
|
|
} |
|
|
|
|
|
// Validador Técnico Imgur - Garantia de Integridade Visual |
|
|
class ImgurTechnicalValidator { |
|
|
constructor() { |
|
|
this.validationResults = []; |
|
|
} |
|
|
|
|
|
async validateImageUrls(urls) { |
|
|
this.validationResults = []; |
|
|
const results = []; |
|
|
for (let i = 0; i < urls.length; i++) { |
|
|
const url = urls[i]; |
|
|
const result = await this.validateSingleImage(url, i + 1)); |
|
|
results.push(result); |
|
|
this.validationResults.push(result)); |
|
|
} |
|
|
return results; |
|
|
} |
|
|
|
|
|
async validateSingleImage(url, index) { |
|
|
const isImgur = url.includes('imgur.com')); |
|
|
try { |
|
|
if (!isImgur) { |
|
|
return { |
|
|
index, |
|
|
url, |
|
|
status: 'WARNING', |
|
|
message: 'URL não é do Imgur - integridade não garantida', |
|
|
format: this.getFormatFromUrl(url), |
|
|
size: 'N/A', |
|
|
availability: 'Unknown' |
|
|
}; |
|
|
} |
|
|
|
|
|
// Validação técnica Imgur |
|
|
const img = new Image(); |
|
|
img.crossOrigin = 'anonymous'; |
|
|
return new Promise((resolve) => { |
|
|
const timeout = setTimeout(() => { |
|
|
resolve({ |
|
|
index, |
|
|
url, |
|
|
status: 'ERROR', |
|
|
message: 'Timeout na validação', |
|
|
format: 'Unknown', |
|
|
size: 'N/A', |
|
|
availability: 'Failed' |
|
|
}}, 10000)); |
|
|
|
|
|
img.onload = () => { |
|
|
clearTimeout(timeout); |
|
|
resolve({ |
|
|
index, |
|
|
url, |
|
|
status: 'VALIDATED', |
|
|
message: 'URL Imgur validada com sucesso', |
|
|
format: img.naturalWidth > 0 ? `${img.naturalWidth}x${img.naturalHeight}` : 'Unknown', |
|
|
size: `${(img.naturalWidth * img.naturalHeight * 3 / 1024 / 1024).toFixed(2)}MB estimado', |
|
|
availability: 'Online' |
|
|
}}; |
|
|
}; |
|
|
img.onerror = () => { |
|
|
clearTimeout(timeout); |
|
|
resolve({ |
|
|
index, |
|
|
url, |
|
|
status: 'ERROR', |
|
|
message: 'Imagem não disponível', |
|
|
format: 'Unknown', |
|
|
size: 'N/A', |
|
|
availability: 'Offline' |
|
|
}}; |
|
|
}; |
|
|
img.src = url; |
|
|
}}); |
|
|
} catch (error) { |
|
|
return { |
|
|
index, |
|
|
url, |
|
|
status: 'ERROR', |
|
|
message: error.message, |
|
|
format: 'Unknown', |
|
|
size: 'N/A', |
|
|
availability: 'Failed' |
|
|
}; |
|
|
} |
|
|
} |
|
|
|
|
|
getFormatFromUrl(url) { |
|
|
if (url.startsWith('data:')) { |
|
|
return 'Base64'; |
|
|
} |
|
|
const extension = url.split('.').pop().split('?')[0]; |
|
|
return extension ? extension.toUpperCase() : 'Unknown'; |
|
|
} |
|
|
|
|
|
generateValidationReport() { |
|
|
const valid = this.validationResults.filter(r => r.status === 'VALIDATED').length; |
|
|
const total = this.validationResults.length; |
|
|
const hasImgur = this.validationResults.some(r => r.url.includes('imgur.com'))); |
|
|
return { |
|
|
total, |
|
|
valid, |
|
|
invalid: total - valid, |
|
|
hasImgur, |
|
|
integrity: hasImgur && valid === total ? 'GARANTIDA' : 'PARCIAL', |
|
|
report: this.validationResults |
|
|
}; |
|
|
} |
|
|
} |
|
|
|
|
|
// Master V3.0 Prompt Structure - FINAL REFORCED |
|
|
function generateMasterPromptV3(concept) { |
|
|
console.log('🚀 GERANDO MASTER PROMPT v3.0 - FINAL REFOrCED'); |
|
|
|
|
|
// Validação técnica das imagens |
|
|
const validator = new ImgurTechnicalValidator()); |
|
|
let validationReport = { |
|
|
total: 0, |
|
|
valid: 0, |
|
|
invalid: 0, |
|
|
hasImgur: false, |
|
|
integrity: 'NENHUMA', |
|
|
report: [] |
|
|
}; |
|
|
if (userData.hostedImageUrls && userData.hostedImageUrls.length > 0) { |
|
|
validationReport = validator.generateValidationReport()); |
|
|
} |
|
|
|
|
|
const masterPromptConfig = { |
|
|
"prompt_version": "FINAL_REFORCED_v3.0", |
|
|
"ai_role": "GEMINI - MASTER E-COMMERCE PHOTOGRAPHIC RENDERING AI WITH ULTRA-REALISM CAPABILITIES", |
|
|
"objective": "GENERATE ULTRA-REALISTIC CINEMATIC IMAGE FOR PREMIUM E-COMMERCE", |
|
|
"product_main": { |
|
|
"name": userData.product, |
|
|
"material": "High Quality Material (Texture focus)", |
|
|
"quantity": "As specified in product name", |
|
|
"colors": userData.colors, |
|
|
"bonuses": "Include if mentioned in product name", |
|
|
"reference_image": "Use input images as strict reference" |
|
|
}, |
|
|
"imgur_validation": { |
|
|
"total_images": validationReport.total, |
|
|
"validated_images": validationReport.valid, |
|
|
"integrity_status": validationReport.integrity, |
|
|
"has_imgur_urls": validationReport.hasImgur, |
|
|
"validation_details": validationReport.report.map(r => ({ |
|
|
index: r.index, |
|
|
url: r.url, |
|
|
status: r.status, |
|
|
format: r.format, |
|
|
availability: r.availability |
|
|
})) |
|
|
}, |
|
|
"imgur_urls": userData.hostedImageUrls, |
|
|
"photo_blocks": { |
|
|
"FOTO PRINCIPAL": { |
|
|
"concept_name": "FOTO_PRINCIPAL", |
|
|
"prompt_structure": { |
|
|
"image_specifications": { |
|
|
"format": "1:1 (square)", |
|
|
"perspective": "completely realistic - front and aligned", |
|
|
"prohibited_elements": ["promotional texts", "arrows", "seals", "watermarks", "graphic elements"] |
|
|
}, |
|
|
"scene_composition": { |
|
|
"background": "#F0F0F0 (very light gray)", |
|
|
"base": "minimalist white marble pedestal with subtle veins", |
|
|
"product_arrangement": { |
|
|
"shirts": "OPEN AND ALIGNED HORIZONTALLY - NOT STACKED", |
|
|
"spacing": "uniform spacing between each item", |
|
|
"presentation": "each item fully opened/visible to show entire surface and texture" |
|
|
}, |
|
|
"layout": "aesthetic and balanced arrangement - HORIZONTAL AND ORGANIZED", |
|
|
"visibility_rule": "all items fully visible, no overlaps hiding details" |
|
|
}, |
|
|
"lighting_style": { |
|
|
"type": "professional studio lighting - MAXIMUM HIGHLIGHT", |
|
|
"equipment": "softboxes + strategic spotlights", |
|
|
"shadows": "soft, diffused and realistic - no distractions", |
|
|
"contrast": "HIGH contrast for maximum outline and volume emphasis" |
|
|
}, |
|
|
"visual_quality": { |
|
|
"style": "hyper-realistic cinematic - MAXIMUM PROFESSIONALISM", |
|
|
"reference_quality": "premium fashion magazine cover quality", |
|
|
"depth_of_field": { |
|
|
"main_focus": "ALL ITEMS SHARP FOCUS", |
|
|
"accessories": "slight blur, but recognizable and sharp" |
|
|
} |
|
|
}, |
|
|
"strict_prohibitions": [ |
|
|
"DO NOT add promotional texts", |
|
|
"DO NOT distort perspective", |
|
|
"DO NOT create items not present", |
|
|
"DO NOT hide important parts", |
|
|
"DO NOT use |