|
import React, { useState, useCallback, useEffect } from 'react'; |
|
import type { Session } from '@supabase/gotrue-js'; |
|
import { Header } from '@/components/Header'; |
|
import { PromptForm } from '@/components/PromptForm'; |
|
import { SqlViewer } from '@/components/SqlViewer'; |
|
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<Session | null>(null); |
|
const [cooldownUntil, setCooldownUntil] = useState<Date | null>(null); |
|
|
|
|
|
const [generatedImagesB64, setGeneratedImagesB64] = useState<string[] | null>(null); |
|
const [isLoading, setIsLoading] = useState<boolean>(false); |
|
const [error, setError] = useState<string | null>(null); |
|
|
|
|
|
const [textOverlay, setTextOverlay] = useState<string>(''); |
|
const [compositionId, setCompositionId] = useState<string>('random'); |
|
const [textPosition, setTextPosition] = useState<GenerateOptions['textPosition']>('center'); |
|
const [subtitleOutline, setSubtitleOutline] = useState<GenerateOptions['subtitleOutline']>('auto'); |
|
const [artStylesForFont, setArtStylesForFont] = useState<string[]>([]); |
|
|
|
|
|
const [currentBasePrompt, setCurrentBasePrompt] = useState<string>(''); |
|
const [currentTheme, setCurrentTheme] = useState<string>(''); |
|
const [brandData, setBrandData] = useState<GenerateOptions['brandData']>({ name: '', slogan: '', weight: 25 }); |
|
const [priceData, setPriceData] = useState<PriceData>({ text: '', modelText: '', style: 'circle', position: 'none', color: 'red'}); |
|
const [featureDetails, setFeatureDetails] = useState<FeatureDetails[] | null>(null); |
|
|
|
|
|
|
|
const [adCopy, setAdCopy] = useState<AdCopy | null>(null); |
|
const [isAdCopyLoading, setIsAdCopyLoading] = useState<boolean>(false); |
|
const [adCopyError, setAdCopyError] = useState<string | null>(null); |
|
|
|
|
|
const [isGuideOpen, setIsGuideOpen] = useState(false); |
|
|
|
const isAuthEnabled = !!supabase; |
|
|
|
const triggerCooldown = useCallback((durationMs: number = 60000) => { |
|
setCooldownUntil(new Date(Date.now() + durationMs)); |
|
}, []); |
|
|
|
useEffect(() => { |
|
if (supabase) { |
|
|
|
const { data: authListener } = supabase.auth.onAuthStateChange((_event, session) => { |
|
setSession(session); |
|
}); |
|
|
|
return () => { |
|
|
|
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; |
|
} |
|
|
|
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); |
|
setAdCopyError(null); |
|
setFeatureDetails(null); |
|
|
|
|
|
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'; |
|
} |
|
} |
|
|
|
|
|
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) { |
|
|
|
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 { |
|
|
|
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(); |
|
} |
|
|
|
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(); |
|
} |
|
|
|
setAdCopyError((e as Error).message); |
|
console.error("Failed to generate ad copy:", e); |
|
} finally { |
|
setIsAdCopyLoading(false); |
|
} |
|
}, [currentBasePrompt, textOverlay, currentTheme, brandData, triggerCooldown]); |
|
|
|
return ( |
|
<div className="min-h-screen bg-gray-100 text-gray-800 font-sans flex flex-col"> |
|
<Header |
|
session={session} |
|
onLogin={handleLogin} |
|
onLogout={handleLogout} |
|
isAuthEnabled={isAuthEnabled} |
|
onOpenGuide={() => setIsGuideOpen(true)} |
|
/> |
|
<div className="flex-grow container mx-auto p-4 sm:p-6 md:p-8"> |
|
<main className="flex flex-col lg:flex-row gap-8 h-full"> |
|
<div className="lg:w-2/5 flex flex-col"> |
|
<PromptForm |
|
onGenerate={handleGenerate} |
|
isLoading={isLoading} |
|
cooldownUntil={cooldownUntil} |
|
onCooldown={triggerCooldown} |
|
/> |
|
</div> |
|
<div className="lg:w-3/5 flex flex-col"> |
|
<SqlViewer |
|
imagesB64={generatedImagesB64} |
|
textOverlay={textOverlay} |
|
compositionId={compositionId} |
|
textPosition={textPosition} |
|
subtitleOutline={subtitleOutline} |
|
artStyles={artStylesForFont} |
|
isLoading={isLoading} |
|
error={error} |
|
adCopy={adCopy} |
|
isAdCopyLoading={isAdCopyLoading} |
|
adCopyError={adCopyError} |
|
onGenerateAds={handleGenerateAds} |
|
brandData={brandData} |
|
priceData={priceData} |
|
featureDetails={featureDetails} |
|
// Setters for editing |
|
setTextOverlay={setTextOverlay} |
|
setCompositionId={setCompositionId} |
|
setTextPosition={setTextPosition} |
|
setSubtitleOutline={setSubtitleOutline} |
|
setPriceData={setPriceData} |
|
setFeatureDetails={setFeatureDetails} |
|
/> |
|
</div> |
|
</main> |
|
</div> |
|
<footer className="text-center p-4 text-gray-500 border-t border-gray-200"> |
|
<p>Powered by Gemini, React, and Supabase</p> |
|
</footer> |
|
<GuideModal isOpen={isGuideOpen} onClose={() => setIsGuideOpen(false)} /> |
|
</div> |
|
); |
|
}; |
|
|
|
export default App; |