import React, { useState, useRef, useEffect } from 'react'; import { motion, AnimatePresence } from 'motion/react'; import { FileText, Loader2, ShieldCheck, Sparkles, Workflow } from 'lucide-react'; import { onAuthStateChanged, signInWithPopup, signOut, User } from 'firebase/auth'; import { Header } from './components/Header'; import { InputArea } from './components/InputArea'; import { MessageBubble } from './components/MessageBubble'; import { LoginScreen } from './components/LoginScreen'; import { auth, googleProvider } from './lib/firebase'; import { Message, Attachment, AudioRecording } from './types'; export default function App() { const welcomeText = `### Portal Brief CDC Este portal funciona por pasos y no como un chat libre. **Orden del proceso:** 1. **Nota de voz** _(obligatorio)_ 2. **Imágenes de referencia** _(obligatorio)_ 3. **Documentos relacionados** _(opcional)_ 4. **Campaña o elementos gráficos existentes** _(opcional)_ 5. **Enlaces de referencia online** _(opcional)_ 6. **Revisión y envío** **Qué hace el sistema con cada entrada:** - **Nota de voz:** se analiza - **Imágenes:** se analizan - **Documentos y Campaña:** se guardan en la carpeta del proyecto - **Enlaces online:** se listan dentro del brief Cuando completes los pasos obligatorios, podrás enviar la solicitud en el paso final.`; const [messages, setMessages] = useState([ { id: 'welcome', sender: 'system', text: welcomeText, timestamp: new Date(), status: 'sent' } ]); const getInitialWebhookUrl = () => { if (typeof window === 'undefined') { return import.meta.env.VITE_WEBHOOK_URL || ''; } return ( window.localStorage.getItem('cdc-brief-webhook-url') || import.meta.env.VITE_WEBHOOK_URL || '' ); }; const [webhookUrl] = useState(getInitialWebhookUrl); const [isProcessing, setIsProcessing] = useState(false); const [isConnected, setIsConnected] = useState(true); const [user, setUser] = useState(null); const [authLoading, setAuthLoading] = useState(true); const [loginLoading, setLoginLoading] = useState(false); const [authError, setAuthError] = useState(''); const messagesEndRef = useRef(null); const scrollToBottom = () => { messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); }; useEffect(() => { scrollToBottom(); }, [messages, isProcessing]); useEffect(() => { if (typeof window !== 'undefined' && webhookUrl) { window.localStorage.setItem('cdc-brief-webhook-url', webhookUrl); } }, [webhookUrl]); useEffect(() => { const unsubscribe = onAuthStateChanged(auth, (currentUser) => { setUser(currentUser); setAuthLoading(false); }); return () => unsubscribe(); }, []); const handleGoogleLogin = async () => { try { setLoginLoading(true); setAuthError(''); const result = await signInWithPopup(auth, googleProvider); const email = result.user.email || ''; const allowedDomain = '@gomezleemarketing.com'; if (!email.toLowerCase().endsWith(allowedDomain.toLowerCase())) { await signOut(auth); setAuthError('Solo se permite acceso con cuentas corporativas de la empresa.'); return; } setUser(result.user); } catch (error) { console.error('Google login error:', error); setAuthError('No se pudo iniciar sesión con Google.'); } finally { setLoginLoading(false); } }; const handleLogout = async () => { await signOut(auth); setUser(null); }; const handleSendMessage = async ( text: string, attachments: Attachment[], audio?: AudioRecording ) => { const userMessageId = Math.random().toString(36).substring(7); const newMessage: Message = { id: userMessageId, sender: 'user', text, attachments, audio, timestamp: new Date(), status: 'sending' }; setMessages((prev) => [...prev, newMessage]); setIsProcessing(true); try { const formData = new FormData(); formData.append('text', text); formData.append('timestamp', new Date().toISOString()); attachments.forEach((att, index) => { const key = att.category === 'campaign' ? `campaign_file_${index}` : `file_${index}`; formData.append(key, att.file); }); if (audio) { formData.append('audio', audio.blob, 'voice_note.webm'); } const response = await fetch(webhookUrl, { method: 'POST', body: formData, }); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } let responseData: unknown; const contentType = response.headers.get('content-type'); if (contentType && contentType.includes('application/json')) { responseData = await response.json(); } else { responseData = await response.text(); } setMessages((prev) => prev.map((msg) => msg.id === userMessageId ? { ...msg, status: 'sent' } : msg ) ); const systemMessage: Message = { id: Math.random().toString(36).substring(7), sender: 'system', text: typeof responseData === 'object' ? JSON.stringify(responseData, null, 2) : (responseData as string) || 'Solicitud recibida correctamente.', timestamp: new Date(), status: 'sent' }; setMessages((prev) => [...prev, systemMessage]); setIsConnected(true); } catch (error) { console.error('Webhook error:', error); setIsConnected(false); setMessages((prev) => prev.map((msg) => msg.id === userMessageId ? { ...msg, status: 'error' } : msg ) ); const errorMessage: Message = { id: Math.random().toString(36).substring(7), sender: 'system', text: `**Error de conexión:** No se pudo conectar con el webhook configurado. Verifica que la URL productiva esté correcta en el entorno y que el endpoint esté activo y acepte solicitudes POST con FormData.`, timestamp: new Date(), status: 'error' }; setMessages((prev) => [...prev, errorMessage]); } finally { setIsProcessing(false); } }; if (authLoading) { return (
Cargando acceso...
); } if (!user) { return ( ); } return (
Portal inteligente de briefs

Carga el brief en un flujo claro, visual y mucho más profesional.

La interfaz guía al usuario paso por paso, reduce errores de carga y organiza audio, imágenes, archivos y referencias para construir un brief listo para revisión interna.

{[ { icon: ShieldCheck, label: 'Validaciones obligatorias' }, { icon: Workflow, label: 'Proceso guiado por pasos' }, { icon: FileText, label: 'Entrega estructurada al flujo' }, ].map(({ icon: Icon, label }) => (
{label}
))}

Resumen del sistema

{[ 'La nota de voz, las imágenes y los PDFs se analizan automáticamente.', 'Excel, PowerPoint, Word y otros adjuntos se guardan como soporte en la carpeta del proyecto.', 'Los enlaces online se registran para alimentar el brief final.', 'El usuario no necesita usar el portal como chat libre: todo queda guiado.', ].map((item) => (

{item}

))}

Carga guiada

Completa el brief y envía la solicitud

Respuesta del portal

Resultado de la solicitud

{messages.map((message) => ( ))} {isProcessing && ( Procesando la solicitud... )}
); }