diff --git a/src/App.jsx b/src/App.jsx new file mode 100644 index 0000000..628f59e --- /dev/null +++ b/src/App.jsx @@ -0,0 +1,810 @@ +import React, { useState, useEffect, useRef } from 'react'; +import { initializeApp } from 'firebase/app'; +import { getAuth, signInAnonymously, signInWithCustomToken, onAuthStateChanged } from 'firebase/auth'; +import { getFirestore, collection, addDoc, onSnapshot, query, serverTimestamp } from 'firebase/firestore'; +import { Bot, Sparkles, Clapperboard, Lightbulb, Library, Hash, Video, Music, Loader, Link as LinkIcon, FlaskConical, Send, Globe, Palette, Copy, Check, CalendarDays, Languages } from 'lucide-react'; + +// --- FIREBASE CONFIGURATION --- +const firebaseConfig = typeof __firebase_config !== 'undefined' ? JSON.parse(__firebase_config) : {}; +const appId = typeof __app_id !== 'undefined' ? __app_id : 'default-app-id'; + +// --- CONSTANTS --- +const CATEGORIES = ["Todas las Categorías", "Innovación & Futuro", "Negocios & Finanzas", "Crecimiento Personal", "Conocimiento & Educación", "Cultura & Estilo de Vida", "Noticias de Impacto"]; +const CONTENT_TYPES = ["Todos", "Historia", "Estudio", "Noticia"]; +const COUNTRIES = ["Mundial", "Perú", "México", "Colombia", "Argentina", "España", "EE.UU."]; +const LANGUAGES = ["Español", "English", "Português"]; +const API_KEYS = [ + "AIzaSyCTMdKxAcH8VvlzFsVIlyM5xwXNMDs4UQ0", // Primary API + "AIzaSyA2hRXelMJxOxjy8PADq4Y0DPDYSyVmC2g" // Backup API +]; + +const THEME_COLORS = { + dark: { + '--bg-primary': '#111827', '--bg-secondary': '#1f2937', '--bg-tertiary': '#374151', + '--text-primary': '#f9fafb', '--text-secondary': '#d1d5db', '--accent': '#8b5cf6', + }, + light: { + '--bg-primary': '#f9fafb', '--bg-secondary': '#ffffff', '--bg-tertiary': '#e5e7eb', + '--text-primary': '#111827', '--text-secondary': '#4b5563', '--accent': '#7c3aed', + } +}; + +// --- IMPROVED FALLBACK IDEAS (TIMELESS EXAMPLES) --- +const fallbackIdeas = [ + { id: 'fb1', type: "Historia", category: "Crecimiento Personal", title: "El 'Efecto Dunning-Kruger': por qué los incompetentes no saben que lo son", description: "Analiza el sesgo cognitivo por el cual las personas con pocas habilidades sufren un sentimiento de superioridad ilusorio.", source: { name: "Journal of Personality and Social Psychology", url: "https://psycnet.apa.org/record/1999-15054-002" }, date: "1 de Diciembre, 1999" }, + { id: 'fb2', type: "Historia", category: "Negocios & Finanzas", title: "La crisis de los tulipanes: la primera burbuja especulativa de la historia", description: "La fascinante historia de cómo en el siglo XVII el precio de los bulbos de tulipán en Holanda alcanzó niveles absurdos antes de colapsar.", source: { name: "Wikipedia", url: "https://es.wikipedia.org/wiki/Crisis_de_los_tulipanes" }, date: "Febrero, 1637" }, +]; + + +// --- UI COMPONENTS --- + +const NavItem = ({ icon, text, isActive, onClick }) => ( + +); + +const LoadingSpinner = ({ size = 'h-8 w-8' }) => ( +
+
+
+); + +const IdeaCard = ({ idea, onGenerate }) => ( +
+
+
+ {idea.category} + {idea.type} +
+

{idea.title}

+ {idea.date && ( +
+ + {idea.date} +
+ )} +

{idea.description}

+
+
+ {idea.source?.name && ( +
+ + {idea.source.url ? ( + + {idea.source.name} + + ) : ( + {idea.source.name} + )} +
+ )} +
+
+ +
+
+); + +const ScriptRenderer = ({ text }) => { + if (!text) return null; + return text.split('\n').map((line, index) => { + const formattedLine = line.replace(/^(GANCHO:|DESARROLLO:|CLÍMAX:|CTA:)/, '$1'); + return

; + }); +}; + +const ToolContentRenderer = ({ content, type }) => { + if (!content || content.trim() === '') { + return

No hay sugerencias disponibles.

; + } + + if (type === 'hashtags') { + const tags = content.split(' ').filter(tag => tag.startsWith('#')); + if(tags.length === 0) return

No se encontraron hashtags.

; + + return ( +
+ {tags.map((tag, index) => ( + + {tag} + + ))} +
+ ); + } + + const lines = content.split('\n').filter(line => line.trim() !== ''); + return ( + + ); +}; + + +const ScriptModal = ({ scriptData, onClose, onSave, onGenerateTool, loadingTools, copyToClipboard }) => { + if (!scriptData) return null; + const { title, script, source, hashtags, broll, audio, isFromLibrary } = scriptData; + + return ( +
+
+
+
+
+

{title}

+

Guion Generado con IA

+ {source?.name && ( +
+ + {source.url ? ( + + {source.name} + + ) : ( + {source.name} + )} +
+ )} +
+ +
+
+
+
+

Guion 📜

+ +
+
+ +
+
+

Herramientas Avanzadas ✨

+
+ } title="Hashtags" content={hashtags} onGenerate={() => onGenerateTool('hashtags')} loading={loadingTools.hashtags} copyToClipboard={copyToClipboard} /> + } title="Ideas B-Roll" content={broll} onGenerate={() => onGenerateTool('broll')} loading={loadingTools.broll} copyToClipboard={copyToClipboard} /> + } title="Audio Tendencia" content={audio} onGenerate={() => onGenerateTool('audio')} loading={loadingTools.audio} copyToClipboard={copyToClipboard} /> +
+
+
+ {!isFromLibrary && ( +
+ +
+ )} +
+
+ ); +}; + +const ToolCard = ({ icon, title, content, onGenerate, loading, type, copyToClipboard }) => { + const [copied, setCopied] = useState(false); + + const handleCopy = () => { + if (!content) return; + copyToClipboard(content); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + }; + + return ( +
+
+
+ {icon} +

{title}

+
+ {content && ( + + )} +
+ {loading ? : (content ? : )} +
+)}; + + +// --- MAIN VIEWS --- + +const LanguageSelector = ({ language, setLanguage, disabled }) => ( +
+ + +
+); + + +const CountrySelector = ({ country, setCountry, disabled }) => ( +
+ + +
+); + +const EngineView = ({ ideas, onGenerate, onLoadMore, isLoading, isAddingMore, filters, setFilters, country, setCountry, language, setLanguage }) => ( +
+

Guion de Noticias

+

Filtra y descubre ideas frescas y verificadas para tu próximo video viral.

+
+
+ +
+
+ +
+
+ +
+
+ +
+
+ {isLoading ? : ( + <> +
+ {ideas.map((idea) => )} +
+ {ideas.length === 0 && ( +
+

No hay ideas para esta selección

+

Prueba con otros filtros o genera nuevas ideas.

+
+ )} + + )} +
+ +
+
+); + +const SparkView = ({ onSpark, isSparking, sparkIdeas, onGenerate, country, setCountry, language, setLanguage }) => { + const [selectedCategory, setSelectedCategory] = useState(CATEGORIES[1]); + return ( +
+

La Chispa de Inspiración

+

¿Necesitas una idea desde cero? La IA busca ángulos únicos para ti.

+
+ + +
+ + +
+ +
+
+ {isSparking &&

Buscando las mejores ideas para ti...

} + {sparkIdeas.length > 0 &&
{sparkIdeas.map((idea) => )}
} +
+
+ ); +}; + +const IdeaLabView = ({ onGenerate, isGenerating, labIdeas, generateScript, country, setCountry, language, setLanguage }) => { + const [prompt, setPrompt] = useState(''); + const handleGenerateClick = () => { if (prompt.trim()) { onGenerate(prompt); } }; + + return ( +
+

Laboratorio de Ideas

+

Escribe un tema y deja que la IA cree ideas de guion únicas para ti.

+
+
+ + +
+
+ + +
+
+
+ {isGenerating && } + {labIdeas.length > 0 && +
+ {labIdeas.map((idea) => )} +
+ } +
+
+ ); +}; + + +const LibraryView = ({ scripts, onSelect }) => ( +
+

Mi Biblioteca

+

Tus ideas y guiones guardados.

+
+ {scripts.length === 0 ? (

Tu biblioteca está vacía

Genera un guion y guárdalo para que aparezca aquí.

) : (scripts.map(script => (
onSelect(script)}>

{script.title}

{script.source?.name &&
{script.source.url ? {script.source.name} : {script.source.name}}
}

{script.script}

Guardado: {script.createdAt?.toDate ? script.createdAt.toDate().toLocaleString() : 'Hace un momento'}
)))} +
+
+); + +const PersonalizationView = ({ theme, setTheme, generateTheme, isGeneratingTheme }) => { + const [prompt, setPrompt] = useState(''); + + return ( +
+

Personalización

+

Cambia la apariencia de la aplicación a tu gusto.

+ +
+ {/* Theme Mode */} +
+

Modo de Apariencia

+
+ + +
+
+ + {/* AI Personalization */} +
+

Estudio de Diseño con IA

+

Describe un tema visual y la IA creará una paleta de colores única para ti.

+
+ + +
+
+
+
+ ); +}; + +// --- MAIN APP COMPONENT --- +export default function App() { + const [activeView, setActiveView] = useState('engine'); + const [generatedScriptData, setGeneratedScriptData] = useState(null); + const [isGeneratingScript, setIsGeneratingScript] = useState(false); + const [loadingTools, setLoadingTools] = useState({ hashtags: false, broll: false, audio: false }); + const [libraryScripts, setLibraryScripts] = useState([]); + const [message, setMessage] = useState(''); + const [db, setDb] = useState(null); + const [auth, setAuth] = useState(null); + const [userId, setUserId] = useState(null); + const [cachedIdeas, setCachedIdeas] = useState({}); + const [engineIdeas, setEngineIdeas] = useState([]); + const [isLoading, setIsLoading] = useState(false); + const [isAddingMore, setIsAddingMore] = useState(false); + const [filters, setFilters] = useState({ category: CATEGORIES[0], type: 'Todos' }); + const [country, setCountry] = useState('Mundial'); + const [language, setLanguage] = useState('Español'); + const [sparkIdeas, setSparkIdeas] = useState([]); + const [isSparking, setIsSparking] = useState(false); + const [labIdeas, setLabIdeas] = useState([]); + const [isGeneratingLabIdeas, setIsGeneratingLabIdeas] = useState(false); + const [theme, setTheme] = useState('dark'); + const [isGeneratingTheme, setIsGeneratingTheme] = useState(false); + const requestRef = useRef(0); + + const applyTheme = (themeName, colors = null) => { + const root = document.documentElement; + const themeColors = colors || THEME_COLORS[themeName]; + if (themeColors) { + Object.entries(themeColors).forEach(([key, value]) => { + root.style.setProperty(key, value); + }); + } + }; + + const handleSetTheme = (newTheme) => { + setTheme(newTheme); + applyTheme(newTheme); + }; + + useEffect(() => { + applyTheme(theme); + }, []); + + + useEffect(() => { + if (firebaseConfig.apiKey) { + try { + const app = initializeApp(firebaseConfig); + setDb(getFirestore(app)); + setAuth(getAuth(app)); + } catch (error) { console.error("Error initializing Firebase:", error); } + } + }, []); + + useEffect(() => { + if (!auth) return; + onAuthStateChanged(auth, async (user) => { + if (user) { setUserId(user.uid); } + else { + try { + const token = typeof __initial_auth_token !== 'undefined' ? __initial_auth_token : null; + if (token) await signInWithCustomToken(auth, token); + else await signInAnonymously(auth); + } catch (error) { console.error("Authentication error:", error); } + } + }); + }, [auth]); + + useEffect(() => { + if (!db || !userId) return; + // Note: Removed orderBy('createdAt', 'desc') to prevent potential Firestore index errors. + // Sorting will be done client-side after fetching. + const q = query(collection(db, `artifacts/${appId}/users/${userId}/scripts`)); + const unsubscribe = onSnapshot(q, (snapshot) => { + const scriptsData = []; + snapshot.forEach((doc) => { + scriptsData.push({ id: doc.id, ...doc.data() }); + }); + // Sort by creation date, newest first. Handle null timestamps. + scriptsData.sort((a, b) => (b.createdAt?.toMillis() || 0) - (a.createdAt?.toMillis() || 0)); + setLibraryScripts(scriptsData); + }, (error) => { console.error("Error loading scripts:", error); }); + return () => unsubscribe(); + }, [db, userId]); + + const callGeminiAPI = async (prompt, useJsonSchema = false) => { + for (const apiKey of API_KEYS) { + try { + const url = `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key=${apiKey}`; + const payload = { + contents: [{ role: "user", parts: [{ text: prompt }] }], + generationConfig: { temperature: 0.7, topP: 1, maxOutputTokens: 8192 }, + }; + + if (useJsonSchema) { + payload.generationConfig.responseMimeType = "application/json"; + payload.generationConfig.responseSchema = useJsonSchema; + } + + const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); + if (!response.ok) { + throw new Error(`API Key ${apiKey.substring(0, 8)}... failed with status: ${response.status}`); + } + const result = await response.json(); + if (result.candidates?.[0]?.content?.parts?.[0]?.text) { + return result.candidates[0].content.parts[0].text; + } + } catch (error) { + console.warn(error.message); + } + } + + showMessage("There was a problem contacting the AI with all available keys."); + return null; + }; + + const getIdeasPrompt = () => { + const countryContext = country === 'Mundial' ? 'de cualquier parte del mundo' : `específicamente de ${country}`; + const categoryPrompt = filters.category === 'Todas las Categorías' ? 'cualquier categoría de actualidad' : `la categoría "${filters.category}"`; + const typePrompt = filters.type === 'Todos' ? 'Noticia, Estudio, o Historia' : `el tipo "${filters.type}"`; + + return `Actúa como un estratega de contenido viral y periodista de investigación. Tu misión es encontrar 4 ideas de contenido excepcionales y verificables en idioma ${language}. + + REQUISITOS OBLIGATORIOS: + 1. **Relevancia y Actualidad:** Las ideas deben basarse en noticias, estudios o eventos REALES y RECIENTES (últimos 30-60 días). Busca temas con contexto, no solo titulares. + 2. **Verificabilidad:** La URL de la fuente debe ser un enlace DIRECTO y FUNCIONAL al artículo o estudio original. No uses URLs de portadas de periódicos o redes sociales. La fuente debe ser creíble (medios de comunicación, revistas científicas, informes oficiales). + 3. **Contexto en la Descripción:** La descripción debe explicar POR QUÉ la idea es interesante. ¿Cuál es el impacto? ¿Qué la hace sorprendente o relevante para la audiencia? No te limites a repetir el titular. + 4. **Filtros:** Enfócate estrictamente en noticias ${countryContext}, de ${categoryPrompt} y de tipo ${typePrompt}. + 5. **Formato:** No incluyas corchetes "[]" ni markdown en los títulos. La fecha debe ser plausible y reciente. + + Devuelve exclusivamente un array JSON válido.`; + }; + + const loadIdeas = async (isAddingMoreRequest = false) => { + const cacheKey = `${country}-${language}-${filters.category}-${filters.type}`; + + if (!isAddingMoreRequest && cachedIdeas[cacheKey]) { + setEngineIdeas(cachedIdeas[cacheKey]); + return; + } + + if(isAddingMoreRequest) { + setIsAddingMore(true); + } else { + setIsLoading(true); + setEngineIdeas([]); // Clear previous ideas for a new search + } + + const requestId = ++requestRef.current; + + const ideasSchema = { + type: "ARRAY", + items: { + type: "OBJECT", + properties: { + category: { type: "STRING" }, type: { type: "STRING" }, + title: { type: "STRING" }, description: { type: "STRING" }, + source: { type: "OBJECT", properties: { name: { type: "STRING" }, url: { type: "STRING" } } }, + date: { type: "STRING" } + }, + required: ["category", "type", "title", "description", "source", "date"] + } + }; + + const resultText = await callGeminiAPI(getIdeasPrompt(), ideasSchema); + + if (requestRef.current !== requestId) { + if(isAddingMoreRequest) setIsAddingMore(false); else setIsLoading(false); + return; + } + + let newIdeas = []; + if (resultText) { + try { + newIdeas = JSON.parse(resultText).map(idea => ({...idea, id: crypto.randomUUID()})); + } catch (e) { showMessage("The AI returned an unexpected format."); } + } + + const ideasToSet = newIdeas.length > 0 ? newIdeas : (isAddingMoreRequest ? [] : fallbackIdeas); + + if (isAddingMoreRequest) { + const currentContent = cachedIdeas[cacheKey] || engineIdeas; + const combined = [...currentContent, ...ideasToSet]; + setEngineIdeas(combined); + setCachedIdeas(prev => ({ ...prev, [cacheKey]: combined })); + + } else { + setEngineIdeas(ideasToSet); + setCachedIdeas(prev => ({ ...prev, [cacheKey]: ideasToSet })); + } + + if (isAddingMoreRequest) setIsAddingMore(false); + else setIsLoading(false); + }; + + useEffect(() => { + loadIdeas(); + }, [filters, country, language]); + + const showMessage = (msg, duration = 4000) => { + setMessage(msg); + setTimeout(() => setMessage(''), duration); + }; + + const copyToClipboard = (text) => { + if (!text) return; + const textArea = document.createElement("textarea"); + textArea.value = text; + textArea.style.position = "fixed"; + textArea.style.left = "-9999px"; + document.body.appendChild(textArea); + textArea.focus(); + textArea.select(); + try { + document.execCommand('copy'); + showMessage("¡Copiado al portapapeles!"); + } catch (err) { + console.error('Error copying: ', err); + showMessage("Error copying text.", "error"); + } + document.body.removeChild(textArea); + }; + + const handleGenerateScript = async (idea) => { + setIsGeneratingScript(true); + setGeneratedScriptData(null); + const prompt = `Crea un guion para un video corto (30-60 segundos) para TikTok/Reels en idioma ${language} basado en la siguiente idea real: + **Título:** "${idea.title}" + **Descripción:** "${idea.description}" + **Fuente:** "${idea.source.name}" + + **Instrucciones CRÍTICAS E INNEGOCIABLES:** + 1. **MÁXIMA ESPECIFICIDAD y CONTEXTO:** El guion debe ser extremadamente concreto y explicar el "porqué" de la noticia. Si la idea trata sobre un descubrimiento, explica qué significa. Si es sobre un evento, detalla las consecuencias. **NO SEAS GENÉRICO.** Debes demostrar que entiendes el contexto de la fuente. + 2. **ESTRUCTURA NARRATIVA:** El guion debe contar una mini-historia. + - **GANCHO:** Una frase impactante que capture la atención en 2 segundos. + - **DESARROLLO:** Explica el núcleo de la noticia con datos y contexto. + - **CLÍMAX:** Presenta el dato más sorprendente o la conclusión clave. + - **CTA (Llamada a la acción):** Una pregunta para el público o una invitación a comentar. + 3. **FORMATO LIMPIO Y ESTRICTO:** La respuesta debe ser **únicamente** el guion, sin introducciones, explicaciones o notas. No incluyas indicaciones visuales como "(Mostrar...)" o "VOZ/TEXTO:". + + **Formato de Salida Obligatorio:** + GANCHO: [Texto del gancho en ${language}] + DESARROLLO: [Texto del desarrollo en ${language}] + CLÍMAX: [Texto del clímax en ${language}] + CTA: [Texto del CTA en ${language}]`; + const scriptText = await callGeminiAPI(prompt); + if (scriptText) setGeneratedScriptData({ title: idea.title, script: scriptText, source: idea.source, isFromLibrary: false }); + setIsGeneratingScript(false); + }; + + const handleGenerateFromLab = async (promptText) => { + setIsGeneratingLabIdeas(true); + setLabIdeas([]); + const countryContext = country === 'Mundial' ? 'mundial' : `de ${country}`; + const prompt = `Actúa como un generador de ideas de contenido viral. El usuario ha proporcionado el tema: "${promptText}". Tu misión es generar 4 ideas de contenido con un enfoque ${countryContext} y en idioma ${language}. + + **Instrucción Crítica:** Tu principal objetivo es generar ideas que **expliquen, definan y den ejemplos reales y específicos sobre el tema del usuario**. No te desvíes a temas generales. Si el tema es "El futuro de la energía geotérmica en los Andes", las ideas deben ser sobre proyectos, tecnologías o estudios específicos en esa región, no sobre energía renovable en general. + + Para cada idea, proporciona: categoría, tipo, título (sin "[]"), una descripción que aporte contexto y una fuente real con nombre y URL verificable. + + Devuelve exclusivamente un array JSON.`; + const ideasSchema = { + type: "ARRAY", + items: { + type: "OBJECT", + properties: { + category: { type: "STRING" }, type: { type: "STRING" }, + title: { type: "STRING" }, description: { type: "STRING" }, + source: { type: "OBJECT", properties: { name: { type: "STRING" }, url: { type: "STRING" } } }, + date: { type: "STRING" } + }, + required: ["category", "type", "title", "description", "source", "date"] + } + }; + const resultText = await callGeminiAPI(prompt, ideasSchema); + if(resultText) { + try { setLabIdeas(JSON.parse(resultText).map(idea => ({...idea, id: crypto.randomUUID()}))); } catch (e) { showMessage("The AI returned an unexpected format.");} + } + setIsGeneratingLabIdeas(false); + }; + + const handleGenerateAdvancedTool = async (toolType) => { + if (!generatedScriptData?.script) return; + setLoadingTools(prev => ({...prev, [toolType]: true})); + let prompt = ''; + switch (toolType) { + case 'hashtags': + prompt = `Basado en el siguiente guion en ${language}, sugiere una estrategia de 10 hashtags para máxima viralidad en TikTok/Instagram. Incluye una mezcla de: 3 hashtags generales y populares, 4 hashtags de nicho específicos del tema, y 3 hashtags de tendencia actual (si aplica). Devuelve solo los hashtags separados por espacios, empezando con #.\n\nGuion: "${generatedScriptData.script}"`; + break; + case 'broll': + prompt = `Para el siguiente guion en ${language}, sugiere 5 ideas de clips de B-Roll (tomas de apoyo visual) que sean dinámicas y cinemáticas. Describe la acción y el tipo de plano. Devuelve una lista numerada limpia. Ejemplo:\n1. Plano aéreo de la ciudad al amanecer.\n2. Primer plano de los ojos de una persona reaccionando.\n\nGuion: "${generatedScriptData.script}"`; + break; + case 'audio': + prompt = `Para este guion en ${language}, recomienda 3 tipos de audios/canciones en tendencia que encajen con el tono del video. Sé específico, si es una canción, menciona el artista y el título. Si es un estilo, descríbelo. Devuelve una lista numerada limpia.\n\nGuion: "${generatedScriptData.script}"`; + break; + default: setLoadingTools(prev => ({...prev, [toolType]: false})); return; + } + const resultText = await callGeminiAPI(prompt); + if (resultText) setGeneratedScriptData(prev => ({ ...prev, [toolType]: resultText })); + setLoadingTools(prev => ({...prev, [toolType]: false})); + }; + + const handleGenerateTheme = async (prompt) => { + setIsGeneratingTheme(true); + const themePrompt = `Basado en la descripción visual "${prompt}", genera una paleta de colores para un sitio web. Proporciona los siguientes colores en formato hexadecimal: un color de fondo primario (oscuro o claro), un fondo secundario ligeramente diferente, un fondo terciario para bordes, un color de texto primario con excelente contraste, un texto secundario más tenue y un color de acento vibrante y llamativo. Devuelve solo un objeto JSON con las claves: "--bg-primary", "--bg-secondary", "--bg-tertiary", "--text-primary", "--text-secondary", "--accent".`; + const themeSchema = { + type: "OBJECT", + properties: { + "--bg-primary": { type: "STRING" }, "--bg-secondary": { type: "STRING" }, + "--bg-tertiary": { type: "STRING" }, "--text-primary": { type: "STRING" }, + "--text-secondary": { type: "STRING" }, "--accent": { type: "STRING" }, + }, + required: ["--bg-primary", "--bg-secondary", "--bg-tertiary", "--text-primary", "--text-secondary", "--accent"] + }; + + const resultText = await callGeminiAPI(themePrompt, themeSchema); + if(resultText) { + try { + const colors = JSON.parse(resultText); + applyTheme(null, colors); + // Determine if the new theme is light or dark for state consistency + const primaryBg = colors['--bg-primary'].replace('#',''); + const r = parseInt(primaryBg.substring(0,2), 16); + const g = parseInt(primaryBg.substring(2,4), 16); + const b = parseInt(primaryBg.substring(4,6), 16); + const brightness = (r * 299 + g * 587 + b * 114) / 1000; + setTheme(brightness < 128 ? 'dark' : 'light'); + } catch(e) { + showMessage("Could not generate theme. Try a different prompt."); + } + } + setIsGeneratingTheme(false); + }; + + const handleSaveScript = async () => { + if (!db || !userId || !generatedScriptData) return showMessage("Error saving."); + try { + await addDoc(collection(db, `artifacts/${appId}/users/${userId}/scripts`), { ...generatedScriptData, createdAt: serverTimestamp() }); + showMessage("Script saved to your library!"); + setGeneratedScriptData(null); + } catch (error) { showMessage("Could not save script."); } + }; + + const handleSparkIdeas = async (category) => { + setIsSparking(true); + setSparkIdeas([]); + const countryContext = country === 'Mundial' ? 'global' : `de ${country}`; + const prompt = `Actúa como un experto en encontrar ángulos virales. Dame 5 ideas fascinantes y contraintuitivas para videos cortos sobre "${category}" con un enfoque ${countryContext} y en idioma ${language}. Busca conexiones inesperadas o perspectivas que desafíen lo convencional. Para cada una, da título (sin "[]"), descripción contextualizada, y una fuente real con nombre y URL. Formato JSON.`; + const ideasSchema = { + type: "ARRAY", + items: { + type: "OBJECT", + properties: { + category: { type: "STRING" }, type: { type: "STRING" }, + title: { type: "STRING" }, description: { type: "STRING" }, + source: { type: "OBJECT", properties: { name: { type: "STRING" }, url: { type: "STRING" } } }, + date: { type: "STRING" } + }, + required: ["category", "type", "title", "description", "source", "date"] + } + }; + const resultText = await callGeminiAPI(prompt, ideasSchema); + if (resultText) { + try { setSparkIdeas(JSON.parse(resultText).map(idea => ({...idea, id: crypto.randomUUID()}))); } catch(e) { showMessage("The AI returned an unexpected format."); } + } + setIsSparking(false); + }; + + const handleSelectScriptFromLibrary = (script) => { + setGeneratedScriptData({ ...script, isFromLibrary: true }); + }; + + const renderView = () => { + switch (activeView) { + case 'engine': return loadIdeas(true)} isLoading={isLoading} isAddingMore={isAddingMore} filters={filters} setFilters={setFilters} country={country} setCountry={setCountry} language={language} setLanguage={setLanguage} />; + case 'spark': return ; + case 'lab': return ; + case 'personalization': return ; + case 'library': return ; + default: return
View not found
; + } + }; + + return ( + <> + +
+ {message &&
{message}
} + +
{renderView()}
+ {isGeneratingScript && (

Generando tu guion con IA... 🚀

)} + {generatedScriptData && { setGeneratedScriptData(null); }} onSave={handleSaveScript} onGenerateTool={handleGenerateAdvancedTool} loadingTools={loadingTools} copyToClipboard={copyToClipboard} />} +
+ + ); +}