|
|
import React, { useState, useMemo, useEffect } from 'react'; |
|
|
import { SparklesIcon, LoaderIcon, PlusIcon, XIcon, MegaphoneIcon, ChevronLeftIcon, ChevronRightIcon, AlertTriangleIcon, ProductIcon, UsersIcon, FamilyIcon, LayersIcon, DetailedViewIcon, PosterIcon, BlueprintIcon } from '@/components/icons'; |
|
|
import { artStyles, professionalThemes } from '@/lib/styles'; |
|
|
import { generateProductConcepts, analyzeAdTrends, generateSlogan, generateDesignConcepts } from '@/services/geminiService'; |
|
|
import type { BrandConcept, MixedStyle, RegionalityData, TextPosition, AdTrendAnalysis, SubtitleOutlineStyle, BrandData, PriceData, PriceTagStyleId, PriceTagPosition, PriceTagColor, GenerateOptions } from '@/types'; |
|
|
import { RateLimitError } from '@/lib/errors'; |
|
|
|
|
|
interface CreationPanelProps { |
|
|
onGenerate: (options: GenerateOptions) => void; |
|
|
isLoading: boolean; |
|
|
cooldownUntil: Date | null; |
|
|
onCooldown: () => void; |
|
|
} |
|
|
|
|
|
const rebalancePercentages = (styles: Omit<MixedStyle, 'percentage'>[]): MixedStyle[] => { |
|
|
const count = styles.length; |
|
|
if (count === 0) return []; |
|
|
|
|
|
const basePercentage = Math.floor(100 / count); |
|
|
let remainder = 100 % count; |
|
|
|
|
|
return styles.map((style, i) => { |
|
|
const percentage = basePercentage + (remainder > 0 ? 1 : 0); |
|
|
if(remainder > 0) remainder--; |
|
|
return { ...style, percentage }; |
|
|
}); |
|
|
}; |
|
|
|
|
|
export const PromptForm = ({ onGenerate, isLoading, cooldownUntil, onCooldown }: CreationPanelProps): React.JSX.Element => { |
|
|
const [basePrompt, setBasePrompt] = useState<string>(''); |
|
|
const [textOverlay, setTextOverlay] = useState<string>(''); |
|
|
const [mixedStyles, setMixedStyles] = useState<MixedStyle[]>([]); |
|
|
const [styleToAdd, setStyleToAdd] = useState<string>(artStyles[0]); |
|
|
|
|
|
const [regionality, setRegionality] = useState<RegionalityData>({ |
|
|
country: '', |
|
|
city: '', |
|
|
neighborhood: '', |
|
|
weight: 25, |
|
|
}); |
|
|
|
|
|
const [brandName, setBrandName] = useState(''); |
|
|
const [brandSlogan, setBrandSlogan] = useState(''); |
|
|
const [brandWeight, setBrandWeight] = useState(25); |
|
|
const [isSloganLoading, setIsSloganLoading] = useState(false); |
|
|
const [sloganError, setSloganError] = useState<string | null>(null); |
|
|
|
|
|
|
|
|
const [priceText, setPriceText] = useState(''); |
|
|
const [priceModelText, setPriceModelText] = useState(''); |
|
|
const [priceStyle, setPriceStyle] = useState<PriceTagStyleId>('circle'); |
|
|
const [pricePosition, setPricePosition] = useState<PriceTagPosition>('none'); |
|
|
const [priceColor, setPriceColor] = useState<PriceTagColor>('red'); |
|
|
|
|
|
const [selectedTheme, setSelectedTheme] = useState<string>(professionalThemes[0]); |
|
|
|
|
|
|
|
|
const [adTrendAnalysis, setAdTrendAnalysis] = useState<AdTrendAnalysis | null>(null); |
|
|
const [isAdTrendLoading, setIsAdTrendLoading] = useState(false); |
|
|
const [adTrendError, setAdTrendError] = useState<string | null>(null); |
|
|
|
|
|
|
|
|
const [brandConcepts, setBrandConcepts] = useState<BrandConcept[] | null>(null); |
|
|
const [isConceptLoading, setIsConceptLoading] = useState<boolean>(false); |
|
|
const [conceptError, setConceptError] = useState<string | null>(null); |
|
|
const [carouselOptionsVisible, setCarouselOptionsVisible] = useState<{ [key: number]: boolean }>({}); |
|
|
|
|
|
|
|
|
const [countdown, setCountdown] = useState(0); |
|
|
|
|
|
const isInteractionDisabled = isLoading || countdown > 0 || isAdTrendLoading || isSloganLoading || isConceptLoading; |
|
|
|
|
|
const isProductConceptTheme = useMemo(() => ['Nova Marca de:', 'Nova Loja de:'].some(keyword => selectedTheme.startsWith(keyword)), [selectedTheme]); |
|
|
const isDesignConceptTheme = useMemo(() => selectedTheme.startsWith('Design de Interiores'), [selectedTheme]); |
|
|
const isConceptGeneratorVisible = isProductConceptTheme || isDesignConceptTheme; |
|
|
|
|
|
|
|
|
useEffect(() => { |
|
|
if (!cooldownUntil) { |
|
|
setCountdown(0); |
|
|
return; |
|
|
} |
|
|
|
|
|
const intervalId = setInterval(() => { |
|
|
const now = Date.now(); |
|
|
const remaining = Math.ceil((cooldownUntil.getTime() - now) / 1000); |
|
|
if (remaining > 0) { |
|
|
setCountdown(remaining); |
|
|
} else { |
|
|
setCountdown(0); |
|
|
clearInterval(intervalId); |
|
|
} |
|
|
}, 1000); |
|
|
|
|
|
|
|
|
const now = Date.now(); |
|
|
const remaining = Math.ceil((cooldownUntil.getTime() - now) / 1000); |
|
|
setCountdown(remaining > 0 ? remaining : 0); |
|
|
|
|
|
return () => clearInterval(intervalId); |
|
|
}, [cooldownUntil]); |
|
|
|
|
|
const handleAnalyzeTrends = async () => { |
|
|
if (!selectedTheme.trim() || isInteractionDisabled) return; |
|
|
|
|
|
setIsAdTrendLoading(true); |
|
|
setAdTrendError(null); |
|
|
setAdTrendAnalysis(null); |
|
|
try { |
|
|
const currentBrandData: BrandData = { name: brandName, slogan: brandSlogan, weight: brandWeight }; |
|
|
const result = await analyzeAdTrends(selectedTheme, regionality, currentBrandData); |
|
|
setAdTrendAnalysis(result); |
|
|
} catch (e) { |
|
|
if (e instanceof RateLimitError) { |
|
|
onCooldown(); |
|
|
} |
|
|
setAdTrendError((e as Error).message); |
|
|
} finally { |
|
|
setIsAdTrendLoading(false); |
|
|
} |
|
|
}; |
|
|
|
|
|
const handleGenerateSlogan = async () => { |
|
|
if (!brandName.trim() || !selectedTheme.trim() || isInteractionDisabled) { |
|
|
setSloganError("Preencha o nome da marca e selecione um tema profissional."); |
|
|
return; |
|
|
} |
|
|
setIsSloganLoading(true); |
|
|
setSloganError(null); |
|
|
try { |
|
|
const result = await generateSlogan(brandName, selectedTheme); |
|
|
setBrandSlogan(result.slogan); |
|
|
} catch (e) { |
|
|
if (e instanceof RateLimitError) { |
|
|
onCooldown(); |
|
|
} |
|
|
setSloganError((e as Error).message); |
|
|
} finally { |
|
|
setIsSloganLoading(false); |
|
|
} |
|
|
}; |
|
|
|
|
|
const availableStyles = useMemo(() => { |
|
|
const selectedNames = new Set(mixedStyles.map(s => s.name)); |
|
|
return artStyles.filter(s => !selectedNames.has(s)); |
|
|
}, [mixedStyles]); |
|
|
|
|
|
const handleAddStyle = () => { |
|
|
if (styleToAdd && mixedStyles.length < 3 && !mixedStyles.some(s => s.name === styleToAdd)) { |
|
|
const newStyles = [...mixedStyles, { name: styleToAdd, percentage: 0 }]; |
|
|
setMixedStyles(rebalancePercentages(newStyles)); |
|
|
if(availableStyles.length > 1) { |
|
|
const nextStyle = availableStyles.find(s => s !== styleToAdd) || ''; |
|
|
setStyleToAdd(nextStyle); |
|
|
} else { |
|
|
setStyleToAdd(''); |
|
|
} |
|
|
} |
|
|
}; |
|
|
|
|
|
const handleRemoveStyle = (indexToRemove: number) => { |
|
|
const newStyles = mixedStyles.filter((_, i) => i !== indexToRemove); |
|
|
setMixedStyles(rebalancePercentages(newStyles)); |
|
|
}; |
|
|
|
|
|
const handleSliderChange = (indexToUpdate: number, newPercentageValue: number) => { |
|
|
let styles = [...mixedStyles]; |
|
|
if (styles.length <= 1) { |
|
|
setMixedStyles([{...styles[0], percentage: 100}]); |
|
|
return; |
|
|
} |
|
|
|
|
|
const oldValue = styles[indexToUpdate].percentage; |
|
|
|
|
|
styles[indexToUpdate].percentage = newPercentageValue; |
|
|
|
|
|
let otherTotal = 100 - oldValue; |
|
|
let newOtherTotal = 100 - newPercentageValue; |
|
|
|
|
|
if (otherTotal > 0) { |
|
|
for(let i = 0; i < styles.length; i++) { |
|
|
if (i !== indexToUpdate) { |
|
|
styles[i].percentage = styles[i].percentage * (newOtherTotal / otherTotal); |
|
|
} |
|
|
} |
|
|
} else { |
|
|
const share = newOtherTotal / (styles.length - 1); |
|
|
for(let i = 0; i < styles.length; i++) { |
|
|
if (i !== indexToUpdate) { |
|
|
styles[i].percentage = share; |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
let finalStyles = styles.map(s => ({...s, percentage: Math.round(s.percentage)})); |
|
|
let roundedTotal = finalStyles.reduce((sum, s) => sum + s.percentage, 0); |
|
|
|
|
|
let diffToDistribute = 100 - roundedTotal; |
|
|
if(diffToDistribute !== 0 && finalStyles.length > 0) { |
|
|
|
|
|
let targetIndex = -1; |
|
|
let maxPercent = -1; |
|
|
for (let i = 0; i < finalStyles.length; i++) { |
|
|
if (i !== indexToUpdate && finalStyles[i].percentage > maxPercent) { |
|
|
maxPercent = finalStyles[i].percentage; |
|
|
targetIndex = i; |
|
|
} |
|
|
} |
|
|
if(targetIndex !== -1) finalStyles[targetIndex].percentage += diffToDistribute; |
|
|
else finalStyles[0].percentage += diffToDistribute; |
|
|
} |
|
|
|
|
|
setMixedStyles(finalStyles); |
|
|
}; |
|
|
|
|
|
const handleRegionalityChange = (e: React.ChangeEvent<HTMLInputElement>) => { |
|
|
const { name, value } = e.target; |
|
|
setRegionality(prev => ({ |
|
|
...prev, |
|
|
[name]: name === 'weight' ? parseInt(value, 10) : value, |
|
|
})); |
|
|
}; |
|
|
|
|
|
const handleUseAdConcept = (headline: string, primaryText: string) => { |
|
|
const firstLineOfPrimary = primaryText.split('\n')[0] || ''; |
|
|
setTextOverlay(`${headline}\n${firstLineOfPrimary}`); |
|
|
} |
|
|
|
|
|
const handleGenerateConcepts = async () => { |
|
|
if (!basePrompt.trim() || isInteractionDisabled) return; |
|
|
setIsConceptLoading(true); |
|
|
setConceptError(null); |
|
|
setBrandConcepts(null); |
|
|
try { |
|
|
let concepts; |
|
|
if (isProductConceptTheme) { |
|
|
const productType = selectedTheme.split(': ').pop()?.split('(')[0].trim() || 'produto'; |
|
|
concepts = await generateProductConcepts(basePrompt, productType); |
|
|
} else if (isDesignConceptTheme) { |
|
|
const designType = selectedTheme.split(': ').pop() || ''; |
|
|
concepts = await generateDesignConcepts(basePrompt, designType); |
|
|
} else { |
|
|
return; |
|
|
} |
|
|
setBrandConcepts(concepts); |
|
|
setCarouselOptionsVisible({}); |
|
|
} catch (e) { |
|
|
if (e instanceof RateLimitError) { |
|
|
onCooldown(); |
|
|
} |
|
|
setConceptError((e as Error).message); |
|
|
console.error(e); |
|
|
} finally { |
|
|
setIsConceptLoading(false); |
|
|
} |
|
|
}; |
|
|
|
|
|
const handleToggleCarouselOptions = (conceptIndex: number) => { |
|
|
setCarouselOptionsVisible(prev => ({...prev, [conceptIndex]: !prev[conceptIndex]})); |
|
|
}; |
|
|
|
|
|
const handleGenerateFromConcept = (concept: BrandConcept, scenario: 'product' | 'couple' | 'family' | 'isometric_details' | 'poster' | 'executive_project') => { |
|
|
const styleKeywords = mixedStyles.map(s => `${s.name} (${s.percentage}%)`); |
|
|
let finalPrompt = ''; |
|
|
let textForOverlay = `${concept.name}\n${concept.philosophy}`; |
|
|
let negativePrompt = ''; |
|
|
|
|
|
if (isProductConceptTheme) { |
|
|
const productType = selectedTheme.split(': ').pop()?.split('(')[0].trim() || 'produto'; |
|
|
const baseConceptPrompt = `**Conceito do Produto (${productType}):**\n- **Nome:** ${concept.name}\n- **Filosofia:** "${concept.philosophy}"\n- **Design:** ${concept.visualStyle}\n- **Estilos Adicionais:** ${styleKeywords.join(', ')}.`; |
|
|
negativePrompt = "logotipos, logos, marcas comerciais, texto, palavras, imitação, plágio"; |
|
|
|
|
|
switch(scenario) { |
|
|
case 'product': |
|
|
finalPrompt = `Fotografia de produto de alta qualidade para e-commerce. ${baseConceptPrompt}\n**Diretivas Visuais:** Foco absoluto no produto (${productType}) isolado, em um fundo neutro de estúdio (branco ou cinza claro). Iluminação profissional que realça texturas e a forma do produto. Ângulo de 3/4. Imagem limpa e premium para catálogo.`; |
|
|
textForOverlay = `${concept.name}`; |
|
|
break; |
|
|
case 'couple': |
|
|
finalPrompt = `Fotografia de lifestyle com um casal estiloso. ${baseConceptPrompt}\n**Diretivas Visuais:** Um casal jovem interagindo autenticamente em um ambiente urbano. **O produto (${productType}) DEVE estar em evidência**, sendo usado ou interagindo com ele de forma natural. A composição guia o olhar para o produto. Estética natural, momento espontâneo.`; |
|
|
break; |
|
|
case 'family': |
|
|
finalPrompt = `Fotografia de lifestyle com uma família moderna. ${baseConceptPrompt}\n**Diretivas Visuais:** Uma família feliz em um momento de lazer (parque, passeio). **O produto (${productType}) DEVE estar em evidência**, sendo usado por um ou mais membros, mostrando conforto e estilo para o dia a dia. Estética vibrante, clara e cheia de vida. Posicionar o produto como a escolha da família.`; |
|
|
textForOverlay = `${concept.name}\nPara toda a família`; |
|
|
break; |
|
|
case 'isometric_details': |
|
|
finalPrompt = `Criação de arte técnica e de marketing de alta qualidade. ${baseConceptPrompt}\n**Diretivas Visuais:** Gere UMA ÚNICA imagem no formato de um diagrama isométrico que mostra o produto (${productType}) de forma detalhada. A imagem NÃO deve ter texto. Em vez de texto, use 4 SETAS ou LINHAS DE CHAMADA (callouts) que apontam de características importantes do produto para os 4 cantos da imagem (canto superior esquerdo, superior direito, inferior esquerdo, inferior direito), deixando essas áreas livres para anotações posteriores. A estética deve ser limpa, técnica, mas estilizada para se alinhar ao conceito da marca. Fundo branco ou de cor neutra.`; |
|
|
textForOverlay = concept.name; |
|
|
negativePrompt = "pessoas, paisagens, cenários complexos, desordem, texto, palavras, logos, marcas comerciais, plágio"; |
|
|
break; |
|
|
case 'poster': |
|
|
finalPrompt = `Criação de um cartaz de marketing ou mood board de alta qualidade. ${baseConceptPrompt}\n**Diretivas Visuais:** Gere UMA ÚNICA imagem que seja um pôster ou cartaz. O layout deve ser uma composição limpa e moderna com MÚLTIPLAS imagens de lifestyle menores no estilo "photo dump" ou "trend", mostrando o produto (${productType}) em diferentes contextos autênticos. A estética deve ser de revista de design, com espaço negativo para texto.`; |
|
|
textForOverlay = `${concept.name}`; |
|
|
negativePrompt = "imagem única, uma foto só, desordem, texto, palavras, logos"; |
|
|
break; |
|
|
case 'executive_project': |
|
|
finalPrompt = `Criação de uma folha de projeto técnico (blueprint) de alta qualidade. ${baseConceptPrompt}\n**Diretivas Visuais:** Gere UMA ÚNICA imagem que funciona como uma folha de desenho técnico. O layout deve conter as seguintes vistas do produto (${productType}): uma VISTA ISOMÉTRICA grande e proeminente, e quatro vistas ortográficas menores e alinhadas: VISTA DE TOPO, VISTA FRONTAL, VISTA LATERAL e VISTA DE COSTAS. A estética deve ser limpa, minimalista e profissional, como um desenho de engenharia ou patente, com linhas finas e precisas sobre um fundo branco. A imagem deve ser totalmente livre de textos, números ou dimensões.`; |
|
|
textForOverlay = concept.name; |
|
|
negativePrompt = "texto, palavras, números, dimensões, logos, marcas, pessoas, paisagens, cenários complexos, desordem, cores vibrantes, sombras, fotorrealismo"; |
|
|
break; |
|
|
} |
|
|
} else { |
|
|
const designType = selectedTheme.split(': ').pop() || ''; |
|
|
const baseConceptPrompt = `**Conceito de Design para ${designType}:**\n- **Nome:** ${concept.name}\n- **Filosofia:** "${concept.philosophy}"\n- **Estilo Visual e Materiais:** ${concept.visualStyle}\n- **Estilos Adicionais:** ${styleKeywords.join(', ')}.`; |
|
|
negativePrompt = "desordem, bagunça, má qualidade de renderização, deformado, irrealista, feio, desfocado"; |
|
|
|
|
|
switch(scenario) { |
|
|
case 'product': |
|
|
finalPrompt = `Renderização 3D fotorrealista e cinematográfica de um(a) ${designType} com base no conceito a seguir. ${baseConceptPrompt}\n**Diretivas Visuais:** Foco absoluto no móvel/ambiente. Apresentar em um cenário de estúdio minimalista ou com um fundo sutil que complemente o design. Iluminação profissional que destaca materiais, texturas e formas. Qualidade de imagem de revista de arquitetura de luxo.`; |
|
|
textForOverlay = `${concept.name}`; |
|
|
break; |
|
|
case 'couple': |
|
|
finalPrompt = `Fotografia de lifestyle fotorrealista. ${baseConceptPrompt}\n**Diretivas Visuais:** Um casal interagindo de forma autêntica e elegante no ambiente projetado (${designType}). Ex: cozinhando juntos, relaxando na sala. A arquitetura e o design do mobiliário são o pano de fundo aspiracional. A atmosfera é de conforto, sofisticação e felicidade. Luz natural invasora.`; |
|
|
break; |
|
|
case 'family': |
|
|
finalPrompt = `Fotografia de lifestyle fotorrealista e calorosa. ${baseConceptPrompt}\n**Diretivas Visuais:** Uma família interagindo em um momento feliz e descontraído no ambiente projetado (${designType}). Ex: pais e filhos lendo na sala, preparando uma refeição na cozinha. A cena deve transmitir a funcionalidade e a beleza do espaço no dia a dia. O design serve como um lar acolhedor.`; |
|
|
textForOverlay = `${concept.name}\nPara toda a família`; |
|
|
break; |
|
|
case 'isometric_details': |
|
|
finalPrompt = `Renderização 3D fotorrealista técnica de um(a) ${designType}. ${baseConceptPrompt}\n**Diretivas Visuais:** Gere UMA ÚNICA imagem no formato de uma planta isométrica ou vista de corte (cutaway view) do ambiente (${designType}). A imagem NÃO deve ter texto. Em vez de texto, use 4 SETAS ou LINHAS DE CHAMADA (callouts) para destacar 4 áreas ou detalhes de design chave (ex: um móvel, fluxo de layout, um material) para os 4 cantos da imagem. A estética deve ser limpa e informativa, como de uma revista de arquitetura.`; |
|
|
textForOverlay = concept.name; |
|
|
negativePrompt = "desordem, má qualidade de renderização, deformado, irrealista, feio, desfocado, texto, palavras"; |
|
|
break; |
|
|
case 'poster': |
|
|
finalPrompt = `Criação de um cartaz de marketing ou mood board de design de interiores. ${baseConceptPrompt}\n**Diretivas Visuais:** Gere UMA ÚNICA imagem que seja um pôster ou um mood board. O layout deve conter MÚLTIPLAS imagens menores mostrando diferentes ângulos, detalhes e texturas do ambiente (${designType}). A composição deve ser limpa, profissional, como em uma revista de arquitetura, com espaço negativo para texto.`; |
|
|
textForOverlay = `${concept.name}`; |
|
|
negativePrompt = "uma foto só, desordem, texto, palavras"; |
|
|
break; |
|
|
case 'executive_project': |
|
|
finalPrompt = `Criação de uma folha de projeto de arquitetura de alta qualidade. ${baseConceptPrompt}\n**Diretivas Visuais:** Gere UMA ÚNICA imagem que funciona como uma prancha de apresentação de arquitetura. O layout deve conter as seguintes vistas do ambiente (${designType}): uma VISTA ISOMÉTRICA grande e renderizada de forma limpa, e quatro vistas técnicas menores e alinhadas: PLANTA BAIXA, ELEVAÇÃO FRONTAL, ELEVAÇÃO LATERAL e uma VISTA DE CORTE (cross-section) revelando o interior. A estética deve ser de uma revista de arquitetura de ponta: minimalista, profissional, com linhas finas e precisas sobre um fundo branco. A imagem deve ser totalmente livre de textos, cotas ou anotações.`; |
|
|
textForOverlay = concept.name; |
|
|
negativePrompt = "texto, palavras, cotas, números, anotações, logos, pessoas, desordem, bagunça, cores excessivas, renderização de má qualidade"; |
|
|
break; |
|
|
} |
|
|
} |
|
|
|
|
|
const options: GenerateOptions = { |
|
|
basePrompt, |
|
|
imagePrompt: finalPrompt, |
|
|
textOverlay: textForOverlay, |
|
|
compositionId: 'impacto-light', |
|
|
textPosition: scenario === 'isometric_details' ? 'top-right' : 'center', |
|
|
subtitleOutline: 'auto', |
|
|
artStyles: styleKeywords, |
|
|
theme: selectedTheme, |
|
|
brandData: { name: brandName, slogan: brandSlogan, weight: brandWeight }, |
|
|
priceData: { text: priceText, modelText: priceModelText, style: priceStyle, position: pricePosition, color: priceColor }, |
|
|
negativeImagePrompt: negativePrompt, |
|
|
numberOfImages: 1, |
|
|
scenario, |
|
|
concept |
|
|
}; |
|
|
onGenerate(options); |
|
|
}; |
|
|
|
|
|
const handleGenerateCarousel = (concept: BrandConcept, type: 'cta' | 'educational' | 'trend') => { |
|
|
const styleKeywords = mixedStyles.map(s => `${s.name} (${s.percentage}%)`); |
|
|
let finalPrompt = ''; |
|
|
const numberOfImagesToGenerate = 4; |
|
|
let negativePrompt = ''; |
|
|
let scenario: GenerateOptions['scenario']; |
|
|
|
|
|
if (isProductConceptTheme) { |
|
|
const productType = selectedTheme.split(': ').pop()?.split('(')[0].trim() || 'produto'; |
|
|
const baseConceptPrompt = `**Conceito Base do Produto (${productType}):**\n- **Nome:** ${concept.name}\n- **Filosofia:** "${concept.philosophy}"\n- **Design:** ${concept.visualStyle}\n- **Estilos Adicionais:** ${styleKeywords.join(', ')}.`; |
|
|
negativePrompt = "logotipos, logos, marcas, texto, palavras, imitação, plágio"; |
|
|
|
|
|
switch (type) { |
|
|
case 'cta': |
|
|
scenario = 'carousel_cta'; |
|
|
finalPrompt = `Gere um carrossel de ${numberOfImagesToGenerate} imagens de anúncio de alta conversão. ${baseConceptPrompt}\n**Diretriz:** Cada imagem deve ser uma variação de um 'hero shot' do produto (${productType}), com foco em criar desejo imediato. Imagem 1: Produto em close-up extremo, mostrando um detalhe de material premium. Imagem 2: Produto em um ângulo de 3/4 dinâmico com iluminação de estúdio dramática. Imagem 3: O produto flutuando em um fundo de cor vibrante. Imagem 4: O produto em movimento ou em uso, com um leve desfoque para indicar ação. O produto é o herói absoluto em todas as ${numberOfImagesToGenerate} imagens.`; |
|
|
break; |
|
|
case 'educational': |
|
|
scenario = 'carousel_educational'; |
|
|
finalPrompt = `Gere um carrossel de ${numberOfImagesToGenerate} imagens educativas. ${baseConceptPrompt}\n**Diretriz:** Cada imagem deve destacar um detalhe técnico ou de design diferente do produto (${productType}). Use um estilo limpo, quase diagramático. Imagem 1: Close na textura de um material inovador. Imagem 2: Close em uma característica tecnológica. Imagem 3: Close em um detalhe de design único. Imagem 4: Uma visão explodida dos componentes chave, mostrando como eles se encaixam.`; |
|
|
break; |
|
|
case 'trend': |
|
|
scenario = 'carousel_trend'; |
|
|
finalPrompt = `Gere um carrossel de ${numberOfImagesToGenerate} imagens de lifestyle no estilo 'photo dump' para redes sociais. ${baseConceptPrompt}\n**Diretriz:** Capture o produto (${productType}) em contextos autênticos e da moda, com estética de filme granulado. Imagem 1: 'Fit check' ou 'setup check' em um espelho, com o look/ambiente completo em foco. Imagem 2: Close no produto em uso, em um local interessante. Imagem 3: 'Unboxing' estético em uma superfície bem composta. Imagem 4: O produto como parte de um 'flat lay' com objetos que complementam seu universo.`; |
|
|
break; |
|
|
} |
|
|
} else { |
|
|
const designType = selectedTheme.split(': ').pop() || ''; |
|
|
const baseConceptPrompt = `**Conceito de Design para ${designType}:**\n- **Nome:** ${concept.name}\n- **Filosofia:** "${concept.philosophy}"\n- **Estilo Visual e Materiais:** ${concept.visualStyle}\n- **Estilos Adicionais:** ${styleKeywords.join(', ')}.`; |
|
|
negativePrompt = "desordem, bagunça, má qualidade de renderização, deformado, irrealista, feio, desfocado, texto, palavras"; |
|
|
|
|
|
switch (type) { |
|
|
case 'cta': |
|
|
scenario = 'carousel_cta'; |
|
|
finalPrompt = `Gere um carrossel fotorrealista de ${numberOfImagesToGenerate} imagens de anúncio para um(a) ${designType}. ${baseConceptPrompt}\n**Diretriz:** Foco em desejo e luxo. Imagem 1: Ângulo amplo mostrando o ambiente completo. Imagem 2: Close-up em um detalhe de material nobre (ex: veio de mármore, textura da madeira). Imagem 3: Close-up em uma solução de design inteligente (ex: gaveta oculta, sistema de iluminação). Imagem 4: O ambiente visto de uma perspectiva humana, como se o espectador estivesse prestes a entrar.`; |
|
|
break; |
|
|
case 'educational': |
|
|
scenario = 'carousel_educational'; |
|
|
finalPrompt = `Gere um carrossel fotorrealista de ${numberOfImagesToGenerate} imagens educativas sobre um(a) ${designType}. ${baseConceptPrompt}\n**Diretriz:** Foco em funcionalidade e inovação. Imagem 1: Visão geral do design. Imagem 2: Detalhe mostrando a durabilidade ou facilidade de limpeza de um material. Imagem 3: Detalhe mostrando a capacidade de armazenamento ou organização. Imagem 4: Detalhe mostrando a ergonomia ou o conforto do design em uso.`; |
|
|
break; |
|
|
case 'trend': |
|
|
scenario = 'carousel_trend'; |
|
|
finalPrompt = `Gere um carrossel de ${numberOfImagesToGenerate} imagens de lifestyle para redes sociais, com estética 'clean' e orgânica, para um(a) ${designType}. ${baseConceptPrompt}\n**Diretriz:** Capture momentos autênticos no espaço. Imagem 1: Mãos preparando um café na bancada da cozinha. Imagem 2: Um livro e uma manta sobre uma poltrona na sala. Imagem 3: Um canto do ambiente com uma planta e luz natural. Imagem 4: Detalhe da organização de um armário ou closet. O ambiente é o protagonista silencioso.`; |
|
|
break; |
|
|
} |
|
|
} |
|
|
|
|
|
const options: GenerateOptions = { |
|
|
basePrompt, |
|
|
imagePrompt: finalPrompt, |
|
|
textOverlay: "", |
|
|
compositionId: 'impacto-light', |
|
|
textPosition: 'center', |
|
|
subtitleOutline: 'auto', |
|
|
artStyles: styleKeywords, |
|
|
theme: selectedTheme, |
|
|
brandData: { name: concept.name, slogan: concept.philosophy, weight: 100 }, |
|
|
priceData: { text: '', modelText: '', style: 'circle', position: 'none', color: 'red' }, |
|
|
negativeImagePrompt: negativePrompt, |
|
|
numberOfImages: numberOfImagesToGenerate, |
|
|
scenario, |
|
|
concept, |
|
|
}; |
|
|
onGenerate(options); |
|
|
}; |
|
|
|
|
|
const handleSubmit = (e: React.FormEvent) => { |
|
|
e.preventDefault(); |
|
|
if (isInteractionDisabled || !basePrompt.trim()) return; |
|
|
|
|
|
const artStyleKeywords = mixedStyles.map(style => `${style.name} (${style.percentage}%)`); |
|
|
|
|
|
let imagePrompt = `${basePrompt}, tema: ${selectedTheme}.`; |
|
|
if (artStyleKeywords.length > 0) { |
|
|
imagePrompt += ` Estilos visuais: ${artStyleKeywords.join(', ')}.`; |
|
|
} |
|
|
|
|
|
const regionalityKeywords = [regionality.country, regionality.city, regionality.neighborhood].filter(Boolean).join(', '); |
|
|
if (regionalityKeywords && regionality.weight > 10) { |
|
|
imagePrompt += ` Influência regional de ${regionalityKeywords} (${regionality.weight}%).`; |
|
|
} |
|
|
if (brandName && brandWeight > 10) { |
|
|
imagePrompt += ` Associado à marca ${brandName} (${brandWeight}%).`; |
|
|
} |
|
|
|
|
|
const options: GenerateOptions = { |
|
|
basePrompt, |
|
|
imagePrompt, |
|
|
textOverlay, |
|
|
compositionId: 'random', |
|
|
textPosition: 'center', |
|
|
subtitleOutline: 'auto', |
|
|
artStyles: artStyleKeywords, |
|
|
theme: selectedTheme, |
|
|
brandData: { name: brandName, slogan: brandSlogan, weight: brandWeight }, |
|
|
priceData: { text: priceText, modelText: priceModelText, style: priceStyle, position: pricePosition, color: priceColor }, |
|
|
numberOfImages: 1 |
|
|
}; |
|
|
|
|
|
onGenerate(options); |
|
|
}; |
|
|
|
|
|
return ( |
|
|
<form onSubmit={handleSubmit} className="bg-white p-4 sm:p-6 rounded-lg shadow-md border border-gray-200 flex flex-col h-full overflow-y-auto"> |
|
|
<div className="space-y-4 flex-grow"> |
|
|
<h2 className="text-xl font-bold text-gray-800 tracking-tight">Painel de Criação</h2> |
|
|
|
|
|
{/* --- SEÇÃO 1: IDEIA CENTRAL --- */} |
|
|
<div className="space-y-3"> |
|
|
<label htmlFor="basePrompt" className="block text-sm font-bold text-gray-700">1. Qual é a sua ideia?</label> |
|
|
<textarea |
|
|
id="basePrompt" |
|
|
value={basePrompt} |
|
|
onChange={(e) => setBasePrompt(e.target.value)} |
|
|
placeholder="Ex: um tênis futurista para corrida noturna, uma cozinha com ilha central em estilo industrial..." |
|
|
className="w-full h-20 p-2 bg-gray-50 rounded-md border border-gray-300 focus:ring-2 focus:ring-purple-500 focus:outline-none transition-shadow duration-200" |
|
|
required |
|
|
/> |
|
|
</div> |
|
|
|
|
|
{/* --- SEÇÃO 2: TEMA PROFISSIONAL --- */} |
|
|
<div className="space-y-3"> |
|
|
<label htmlFor="selectedTheme" className="block text-sm font-bold text-gray-700">2. Qual é o seu nicho profissional?</label> |
|
|
<select |
|
|
id="selectedTheme" |
|
|
value={selectedTheme} |
|
|
onChange={(e) => setSelectedTheme(e.target.value)} |
|
|
className="w-full p-2 bg-gray-50 rounded-md border border-gray-300 focus:ring-2 focus:ring-purple-500 focus:outline-none" |
|
|
> |
|
|
{professionalThemes.map(theme => <option key={theme} value={theme}>{theme}</option>)} |
|
|
</select> |
|
|
</div> |
|
|
|
|
|
{/* --- SEÇÃO 3: ESTILOS VISUAIS --- */} |
|
|
<div className="space-y-3"> |
|
|
<label className="block text-sm font-bold text-gray-700">3. Misture até 3 estilos visuais</label> |
|
|
{mixedStyles.map((style, i) => ( |
|
|
<div key={i} className="flex items-center gap-2"> |
|
|
<span className="text-sm font-medium text-gray-600 w-1/3 truncate" title={style.name}>{style.name}</span> |
|
|
<input |
|
|
type="range" |
|
|
min="0" |
|
|
max="100" |
|
|
value={style.percentage} |
|
|
onChange={(e) => handleSliderChange(i, parseInt(e.target.value, 10))} |
|
|
className="w-2/3 h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer accent-purple-600" |
|
|
disabled={isInteractionDisabled} |
|
|
/> |
|
|
<span className="text-sm font-semibold w-12 text-right">{style.percentage}%</span> |
|
|
<button type="button" onClick={() => handleRemoveStyle(i)} className="p-1 text-gray-400 hover:text-red-500" disabled={isInteractionDisabled}> |
|
|
<XIcon className="w-4 h-4"/> |
|
|
</button> |
|
|
</div> |
|
|
))} |
|
|
{mixedStyles.length < 3 && ( |
|
|
<div className="flex items-center gap-2"> |
|
|
<select value={styleToAdd} onChange={(e) => setStyleToAdd(e.target.value)} className="w-full p-2 bg-gray-50 rounded-md border border-gray-300 text-sm" disabled={!availableStyles.length || isInteractionDisabled}> |
|
|
{availableStyles.map(s => <option key={s} value={s}>{s}</option>)} |
|
|
</select> |
|
|
<button type="button" onClick={handleAddStyle} className="flex-shrink-0 p-2 bg-purple-100 text-purple-600 rounded-md hover:bg-purple-200 disabled:opacity-50" disabled={!styleToAdd || isInteractionDisabled}> |
|
|
<PlusIcon className="w-5 h-5"/> |
|
|
</button> |
|
|
</div> |
|
|
)} |
|
|
</div> |
|
|
|
|
|
{/* --- SEÇÃO 4: TEXTO DA ARTE --- */} |
|
|
<div className="space-y-3"> |
|
|
<label htmlFor="textOverlay" className="block text-sm font-bold text-gray-700">4. Texto para a Imagem (opcional)</label> |
|
|
<textarea |
|
|
id="textOverlay" |
|
|
value={textOverlay} |
|
|
onChange={(e) => setTextOverlay(e.target.value)} |
|
|
placeholder="Título (primeira linha) Subtítulo (opcional)" |
|
|
className="w-full h-20 p-2 bg-gray-50 rounded-md border border-gray-300 focus:ring-2 focus:ring-purple-500 focus:outline-none transition-shadow duration-200" |
|
|
maxLength={280} |
|
|
/> |
|
|
</div> |
|
|
|
|
|
{/* --- FERRAMENTAS DE IA --- */} |
|
|
{isConceptGeneratorVisible && ( |
|
|
<div className="p-3 bg-purple-50 border border-purple-200 rounded-lg space-y-3"> |
|
|
<h3 className="font-bold text-purple-800">{isProductConceptTheme ? 'Protótipo de Nova Marca' : 'Laboratório de Conceitos de Design'}</h3> |
|
|
<p className="text-sm text-purple-700">Use a sua ideia do passo 1 para gerar conceitos completos e depois gerar a imagem.</p> |
|
|
<button type="button" onClick={handleGenerateConcepts} disabled={isInteractionDisabled || !basePrompt.trim()} className="w-full flex items-center justify-center gap-2 px-4 py-2 bg-purple-600 text-white font-semibold rounded-lg hover:bg-purple-700 transition-colors disabled:bg-purple-300"> |
|
|
{isConceptLoading ? <LoaderIcon className="animate-spin w-5 h-5"/> : <SparklesIcon className="w-5 h-5"/>} |
|
|
<span>{isProductConceptTheme ? 'Gerar Conceitos de Marca' : 'Gerar Conceitos de Design'}</span> |
|
|
</button> |
|
|
{conceptError && <p className="text-xs text-red-600 text-center">{conceptError}</p>} |
|
|
{brandConcepts && ( |
|
|
<div className="space-y-4 pt-2"> |
|
|
{brandConcepts.map((concept, i) => ( |
|
|
<div key={i} className="p-3 bg-white rounded-md border border-purple-200 space-y-3"> |
|
|
<div> |
|
|
<h4 className="font-bold text-gray-800">{concept.name}</h4> |
|
|
<p className="text-xs text-gray-500 italic">"{concept.philosophy}"</p> |
|
|
<p className="text-sm text-gray-700 mt-1">{concept.visualStyle}</p> |
|
|
</div> |
|
|
|
|
|
<div className="grid grid-cols-3 gap-2 text-xs"> |
|
|
<button type="button" onClick={() => handleGenerateFromConcept(concept, 'product')} disabled={isInteractionDisabled} className="flex items-center justify-center gap-1.5 px-2 py-2 bg-green-500 text-white font-semibold rounded-md hover:bg-green-600 transition-colors disabled:bg-green-300"> |
|
|
<ProductIcon className="w-4 h-4" /><span>Produto</span> |
|
|
</button> |
|
|
<button type="button" onClick={() => handleGenerateFromConcept(concept, 'couple')} disabled={isInteractionDisabled} className="flex items-center justify-center gap-1.5 px-2 py-2 bg-blue-500 text-white font-semibold rounded-md hover:bg-blue-600 transition-colors disabled:bg-blue-300"> |
|
|
<UsersIcon className="w-4 h-4" /><span>Casal</span> |
|
|
</button> |
|
|
<button type="button" onClick={() => handleGenerateFromConcept(concept, 'family')} disabled={isInteractionDisabled} className="flex items-center justify-center gap-1.5 px-2 py-2 bg-orange-500 text-white font-semibold rounded-md hover:bg-orange-600 transition-colors disabled:bg-orange-300"> |
|
|
<FamilyIcon className="w-4 h-4" /><span>Família</span> |
|
|
</button> |
|
|
<button type="button" onClick={() => handleGenerateFromConcept(concept, 'isometric_details')} disabled={isInteractionDisabled} className="flex items-center justify-center gap-1.5 px-2 py-2 bg-teal-500 text-white font-semibold rounded-md hover:bg-teal-600 transition-colors disabled:bg-teal-300"> |
|
|
<DetailedViewIcon className="w-4 h-4" /><span>Detalhes</span> |
|
|
</button> |
|
|
<button type="button" onClick={() => handleGenerateFromConcept(concept, 'executive_project')} disabled={isInteractionDisabled} className="flex items-center justify-center gap-1.5 px-2 py-2 bg-gray-700 text-white font-semibold rounded-md hover:bg-gray-800 transition-colors disabled:bg-gray-400"> |
|
|
<BlueprintIcon className="w-4 h-4" /><span>Executivo</span> |
|
|
</button> |
|
|
<button type="button" onClick={() => handleGenerateFromConcept(concept, 'poster')} disabled={isInteractionDisabled} className="flex items-center justify-center gap-1.5 px-2 py-2 bg-indigo-500 text-white font-semibold rounded-md hover:bg-indigo-600 transition-colors disabled:bg-indigo-300"> |
|
|
<PosterIcon className="w-4 h-4" /><span>Cartaz</span> |
|
|
</button> |
|
|
<button type="button" onClick={() => handleToggleCarouselOptions(i)} disabled={isInteractionDisabled} className={`col-span-3 flex items-center justify-center gap-1.5 px-2 py-2 font-semibold rounded-md border-2 transition-colors disabled:opacity-50 ${carouselOptionsVisible[i] ? 'bg-purple-500 text-white border-purple-500' : 'bg-transparent text-gray-600 border-gray-400 hover:bg-gray-100'}`}> |
|
|
<LayersIcon className="w-4 h-4" /><span>Gerar Carrossel</span> |
|
|
</button> |
|
|
</div> |
|
|
|
|
|
{carouselOptionsVisible[i] && ( |
|
|
<div className="pt-2"> |
|
|
<div className="p-3 bg-purple-100/50 rounded-lg border border-purple-200"> |
|
|
<h5 className="text-xs font-bold text-center text-purple-800 mb-2">Gerar Carrossel de 4 Imagens</h5> |
|
|
<div className="grid grid-cols-3 gap-2 text-xs"> |
|
|
<button onClick={() => handleGenerateCarousel(concept, 'cta')} disabled={isInteractionDisabled} className="px-2 py-1.5 bg-purple-600 text-white font-semibold rounded hover:bg-purple-700 disabled:bg-purple-300">CTA</button> |
|
|
<button onClick={() => handleGenerateCarousel(concept, 'educational')} disabled={isInteractionDisabled} className="px-2 py-1.5 bg-purple-600 text-white font-semibold rounded hover:bg-purple-700 disabled:bg-purple-300">Educativo</button> |
|
|
<button onClick={() => handleGenerateCarousel(concept, 'trend')} disabled={isInteractionDisabled} className="px-2 py-1.5 bg-purple-600 text-white font-semibold rounded hover:bg-purple-700 disabled:bg-purple-300">Trend</button> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
)} |
|
|
</div> |
|
|
))} |
|
|
</div> |
|
|
)} |
|
|
</div> |
|
|
)} |
|
|
</div> |
|
|
|
|
|
<div className="flex-shrink-0 pt-4 border-t border-gray-200"> |
|
|
<button type="submit" disabled={isInteractionDisabled || !basePrompt.trim()} className="w-full flex items-center justify-center gap-2 px-4 py-3 bg-purple-600 text-white text-lg font-bold rounded-lg hover:bg-purple-700 focus:outline-none focus:ring-4 focus:ring-purple-300 transition-all duration-200 disabled:bg-purple-300 disabled:cursor-not-allowed"> |
|
|
{isLoading ? ( |
|
|
<LoaderIcon className="w-6 h-6 animate-spin" /> |
|
|
) : ( |
|
|
<SparklesIcon className="w-6 h-6" /> |
|
|
)} |
|
|
<span>{countdown > 0 ? `Aguarde (${countdown}s)` : 'Gerar Arte Principal'}</span> |
|
|
</button> |
|
|
</div> |
|
|
</form> |
|
|
); |
|
|
}; |