import React, { useState, useCallback, useEffect } from 'react'; import type { Session } from '@supabase/gotrue-js'; import { Header } from '@/components/Header'; import { PromptForm } from '@/components/PromptForm'; // Reimagined as CreationPanel import { SqlViewer } from '@/components/SqlViewer'; // Reimagined as PreviewCanvas import { GuideModal } from '@/components/GuideModal'; import { generateImage, generateAdCopy, generateFeatureDescriptions } from '@/services/geminiService'; import { supabase } from '@/services/supabaseClient'; import type { GenerateOptions, AdCopy, BrandConcept, PriceData, FeatureDetails } from '@/types'; import { RateLimitError } from '@/lib/errors'; import { compositionPresets } from '@/lib/compositions'; const App: React.FC = () => { const [session, setSession] = useState(null); const [cooldownUntil, setCooldownUntil] = useState(null); // Generation state const [generatedImagesB64, setGeneratedImagesB64] = useState(null); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); // Post content state (for display and marketing tools) const [textOverlay, setTextOverlay] = useState(''); const [compositionId, setCompositionId] = useState('random'); const [textPosition, setTextPosition] = useState('center'); const [subtitleOutline, setSubtitleOutline] = useState('auto'); const [artStylesForFont, setArtStylesForFont] = useState([]); // Context for marketing tools const [currentBasePrompt, setCurrentBasePrompt] = useState(''); const [currentTheme, setCurrentTheme] = useState(''); const [brandData, setBrandData] = useState({ name: '', slogan: '', weight: 25 }); const [priceData, setPriceData] = useState({ text: '', modelText: '', style: 'circle', position: 'none', color: 'red'}); const [featureDetails, setFeatureDetails] = useState(null); // Marketing suite state const [adCopy, setAdCopy] = useState(null); const [isAdCopyLoading, setIsAdCopyLoading] = useState(false); const [adCopyError, setAdCopyError] = useState(null); // Modal state const [isGuideOpen, setIsGuideOpen] = useState(false); const isAuthEnabled = !!supabase; const triggerCooldown = useCallback((durationMs: number = 60000) => { setCooldownUntil(new Date(Date.now() + durationMs)); }, []); useEffect(() => { if (supabase) { // Supabase v2: onAuthStateChange handles the initial session check and any subsequent changes. const { data: authListener } = supabase.auth.onAuthStateChange((_event, session) => { setSession(session); }); return () => { // Cleanup subscription on component unmount authListener.subscription?.unsubscribe(); }; } }, []); const handleLogin = async () => { if (!supabase) { setError("A funcionalidade de login está desabilitada pois a configuração do serviço está ausente."); console.warn("Login attempt failed: Supabase client is not initialized."); return; } // Supabase v2: use signInWithOAuth const { error } = await supabase.auth.signInWithOAuth({ provider: 'google' }); if (error) { console.error('Error logging in with Google:', error); setError('Falha ao fazer login com o Google.'); } }; const handleLogout = async () => { if (supabase) { await supabase.auth.signOut(); } }; const handleGenerate = useCallback(async (options: GenerateOptions) => { setIsLoading(true); setError(null); setGeneratedImagesB64(null); setTextOverlay(''); setAdCopy(null); // Reset ads on new generation setAdCopyError(null); // Also reset ad error setFeatureDetails(null); // Reset feature details // BUG FIX: Resolve 'random' composition ID to a specific one to prevent layout shifts on re-render. let finalCompositionId = options.compositionId; if (options.compositionId === 'random') { const availablePresets = compositionPresets.filter(p => p.id !== 'random'); if (availablePresets.length > 0) { const randomPreset = availablePresets[Math.floor(Math.random() * availablePresets.length)]; finalCompositionId = randomPreset.id; } else { finalCompositionId = 'impacto-light'; // Fallback if all presets are somehow filtered out } } // Persist UI state from options setCompositionId(finalCompositionId); setTextPosition(options.textPosition); setSubtitleOutline(options.subtitleOutline); setArtStylesForFont(options.artStyles); setCurrentBasePrompt(options.basePrompt); setCurrentTheme(options.theme); setBrandData(options.brandData); setPriceData(options.priceData); try { if (!process.env.API_KEY) { throw new Error("A variável de ambiente API_KEY não foi definida."); } if (options.scenario === 'isometric_details' && options.concept) { // Special dual-call flow for detailed isometric view const concept = options.concept; setTextOverlay(options.textOverlay); const [imageResults, descriptionResults] = await Promise.all([ generateImage(options.imagePrompt, options.negativeImagePrompt, 1), generateFeatureDescriptions(options.basePrompt, concept) ]); setGeneratedImagesB64(imageResults); setFeatureDetails(descriptionResults); } else { // Standard single-call flow const safeNumberOfImages = Math.max(1, Math.min(options.numberOfImages, 4)); const imageResults = await generateImage(options.imagePrompt, options.negativeImagePrompt, safeNumberOfImages); setGeneratedImagesB64(imageResults); setTextOverlay(options.textOverlay); } } catch (e) { if (e instanceof RateLimitError) { triggerCooldown(); } // The service now provides a complete, user-friendly message. setError((e as Error).message); console.error(e); } finally { setIsLoading(false); } }, [triggerCooldown]); const handleGenerateAds = useCallback(async () => { if (!currentBasePrompt || !textOverlay) return; setIsAdCopyLoading(true); setAdCopy(null); setAdCopyError(null); try { const result = await generateAdCopy(currentBasePrompt, textOverlay, currentTheme, brandData); setAdCopy(result); } catch (e) { if (e instanceof RateLimitError) { triggerCooldown(); } // The service now provides a complete, user-friendly message. setAdCopyError((e as Error).message); console.error("Failed to generate ad copy:", e); } finally { setIsAdCopyLoading(false); } }, [currentBasePrompt, textOverlay, currentTheme, brandData, triggerCooldown]); return (
setIsGuideOpen(true)} />

Powered by Gemini, React, and Supabase

setIsGuideOpen(false)} />
); }; export default App;