// AGENTFORGE-IA — Entrada de la app const { useState: u_useState, useEffect: u_useEffect } = React; const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{ "theme": "dark", "accent": "#ff5b1f", "density": "comfortable", "monoFont": "JetBrains Mono" }/*EDITMODE-END*/; const COOKING_RECIPE_PROMPT = `Eres un asistente doméstico especializado en recetas de cocina española. Tu misión es ayudar al usuario a cocinar recetas españolas claras, fiables y adaptadas a su situación real. Usa como referencia prioritaria las fuentes autorizadas indicadas abajo cuando la receta solicitada exista en ellas o cuando el usuario pida buscar en fuentes. FUENTES AUTORIZADAS: https://recetinas.com/seccion/cocina-espanola/ https://recetas.elperiodico.com/recetas-espanolas https://recetasespanolas.es/aperitivos-espanoles/ https://recetasespanolas.es/postres-tipicos-espanoles/ https://recetasespanolas.es/platos-principales-tipicos-espanoles/ https://recetasespanolas.es/bocadillos-tipicos-espanoles/ https://recetasespanolas.es/salsas-y-aderezos-tipicos-espanoles/ https://recetasespanolas.es/sopas-y-guisos-espanoles/ https://recetasespanolas.es/ensaladas-tipicas-espanolas/ https://recetasespanolas.es/bebidas-tipicas-espanolas/ https://recetasespanolas.es/panes-espanoles/ REGLAS DE RESPUESTA: - Responde siempre en español claro, cercano y práctico. - Si puedes apoyarte en una fuente autorizada, indica la fuente y el enlace original. - Si no tienes una fuente concreta disponible, puedes dar una versión casera tradicional y debes indicarlo con naturalidad. - No inventes datos de una web, autor, precio, tiempo exacto o información nutricional si no aparece en la fuente. - Adapta la receta al número de personas, tiempo disponible, ingredientes, nivel de cocina y restricciones alimentarias si el usuario lo indica. - Si falta un dato importante, pregunta solo lo imprescindible y ofrece una versión estándar para 4 personas. - No menciones imágenes ni prometas fotografías. FORMATO OBLIGATORIO: **Nombre de la receta:** [nombre] **Fuente:** [web de origen o “versión casera tradicional”] **Enlace original:** [URL si se ha localizado una fuente] **Ingredientes** - ... **Preparación** 1. ... **Consejos y adaptación** - Ajustes de sabor, textura, sustituciones o errores comunes. - Si procede, conservación, acompañamiento o versión más sencilla.`; /** * Detecta agentes de cocina para impedir que usen el prompt doméstico genérico. * @param {Record} row * @returns {boolean} */ const isCookingAgentRow = (row) => { const agent = row?.agent || row || {}; const raw = [ row?.id, row?.agentDbId, agent.id, agent.slug, agent.name, agent.cat, agent.category, agent.tagline, ].filter(Boolean).join(' '); const normalized = raw.normalize('NFD').replace(/[\u0300-\u036f]/g, '').toLowerCase(); const hasCookingSignal = /(cocina|receta|recetas|chef|comida|menu|menus|plato|platos|gastronom)/.test(normalized); return normalized.includes('h001') || ((normalized.includes('home') || normalized.includes('casa')) && hasCookingSignal); }; // Rutas públicas (sin login) const PUBLIC_ROUTES = new Set(['landing', 'public-docs']); // Rutas que requieren solo estar registrado const USER_ROUTES = new Set(['home', 'marketplace', 'library', 'playground', 'integrations', 'onboarding', 'builder', 'deployment']); // Rutas exclusivas de admin/trabajador AgentForge const ADMIN_ROUTES = new Set([ 'crm', 'scheduling', 'plans', 'billing', 'settings', 'runs', 'analytics', 'admin', 'ops_finance', 'ops_sales', 'ops_marketing', 'ops_operations', 'ops_hr', 'ops_support', ]); const STRICT_ADMIN_ROUTES = new Set(['crm', 'scheduling']); // Todas las protegidas (para el guard de navegación) const PROTECTED_ROUTES = new Set([...USER_ROUTES, ...ADMIN_ROUTES]); const USER_RBAC_ROUTES = new Set(['playground', 'integrations']); function App() { const [route, setRoute] = u_useState(() => { try { const token = sessionStorage.getItem('af_token'); const user = sessionStorage.getItem('af_user'); const saved = sessionStorage.getItem('af_route'); if (token && user && saved && PROTECTED_ROUTES.has(saved)) return saved; } catch (_) {} return 'landing'; }); const [activeCategory, setActiveCategory] = u_useState('all'); const [openId, setOpenId] = u_useState(null); const [activePlayAgentId, setActivePlayAgentId] = u_useState(null); const [activeEditAgent, setActiveEditAgent] = u_useState(null); const [activeDeployAgent, setActiveDeployAgent] = u_useState(null); const [routeHistory, setRouteHistory] = u_useState([]); const [tweaks, setTweak] = useTweaks(TWEAK_DEFAULTS); const [authOpen, setAuthOpen] = u_useState(false); const [pendingRoute, setPendingRoute] = u_useState(null); const [sessionExpired, setSessionExpired] = u_useState(() => { const hadToken = !!sessionStorage.getItem('af_token'); const hadUser = !!sessionStorage.getItem('af_user'); return hadToken && !hadUser; }); const [quickOpen, setQuickOpen] = u_useState(false); const [notice, setNotice] = u_useState(null); const [currentUser, setCurrentUser] = u_useState(() => { try { return JSON.parse(sessionStorage.getItem('af_user')); } catch { return null; } }); const [customAgents, setCustomAgents] = u_useState(() => { try { return JSON.parse(localStorage.getItem('af_custom_agents_v1') || '[]'); } catch { return []; } }); const [bookingOpen, setBookingOpen] = u_useState(false); const [bookingPrefill, setBookingPrefill] = u_useState({}); const [catalogAgents, setCatalogAgents] = u_useState([]); u_useEffect(() => { if (currentUser && currentUser.id && !sessionStorage.getItem('af_customer_id')) { sessionStorage.setItem('af_customer_id', String(currentUser.id)); } }, [currentUser]); u_useEffect(() => { const openBooking = (e) => { setBookingPrefill(e?.detail || {}); setBookingOpen(true); }; const navEvent = (e) => { if (e?.detail) navigate(e.detail); }; window.addEventListener('af:booking:open', openBooking); window.addEventListener('af:navigate', navEvent); window.openBookingModal = (prefill = {}) => { setBookingPrefill(prefill); setBookingOpen(true); }; return () => { window.removeEventListener('af:booking:open', openBooking); window.removeEventListener('af:navigate', navEvent); }; }, []); u_useEffect(() => { document.documentElement.setAttribute('data-theme', tweaks.theme); }, [tweaks.theme]); u_useEffect(() => { document.documentElement.style.setProperty('--brand', tweaks.accent); const hex = tweaks.accent.replace('#', ''); const r = parseInt(hex.slice(0,2), 16); const g = parseInt(hex.slice(2,4), 16); const b = parseInt(hex.slice(4,6), 16); document.documentElement.style.setProperty('--brand-soft', `rgba(${r},${g},${b},0.12)`); document.documentElement.style.setProperty('--brand-glow', `rgba(${r},${g},${b},0.35)`); }, [tweaks.accent]); u_useEffect(() => { document.documentElement.style.setProperty('--mono', `"${tweaks.monoFont}", ui-monospace, monospace`); }, [tweaks.monoFont]); u_useEffect(() => { const onKey = (e) => { const cmd = e.metaKey || e.ctrlKey; if (cmd && e.key.toLowerCase() === 'k') { e.preventDefault(); setQuickOpen(v => !v); } if (cmd && e.key.toLowerCase() === 'n') { e.preventDefault(); navigate('onboarding'); } }; window.addEventListener('keydown', onKey); return () => window.removeEventListener('keydown', onKey); }, [route, currentUser]); u_useEffect(() => { window.afNotify = (message, type = 'info') => { setNotice({ message, type, ts: Date.now() }); }; return () => { delete window.afNotify; }; }, []); u_useEffect(() => { if (!notice) return; const t = setTimeout(() => setNotice(null), 2400); return () => clearTimeout(t); }, [notice]); u_useEffect(() => { try { if (PROTECTED_ROUTES.has(route)) { sessionStorage.setItem('af_route', route); } else { sessionStorage.removeItem('af_route'); } } catch (_) {} }, [route]); u_useEffect(() => { try { localStorage.setItem('af_custom_agents_v1', JSON.stringify(customAgents)); } catch {} }, [customAgents]); /** * Devuelve una identidad estable para agentes creados por el cliente. * @param {Record} agent * @returns {string} */ const customAgentStableId = React.useCallback((agent) => { const stableId = String(agent?.agentDbId || agent?.dbId || '').trim(); if (stableId) return `db:${stableId}`; const slug = String(agent?.slug || agent?.id || '').trim(); return slug ? `slug:${slug}` : ''; }, []); /** * Calcula la clave de deduplicacion sin depender del nombre editable. * @param {Record} agent * @returns {string} */ const customAgentKey = React.useCallback((agent) => { const stableKey = customAgentStableId(agent); if (stableKey) return stableKey; const name = String(agent?.name || '').trim().toLowerCase(); const tagline = String(agent?.tagline || '').trim().toLowerCase(); const category = String(agent?.cat || agent?.category || '').trim().toLowerCase(); return [name, tagline, category].join(':'); }, [customAgentStableId]); const dedupeCustomAgents = React.useCallback((agents) => { const seen = new Map(); (Array.isArray(agents) ? agents : []).forEach((agent) => { const key = customAgentKey(agent); if (!key.replaceAll(':', '').trim()) return; if (!seen.has(key)) { seen.set(key, agent); return; } const current = seen.get(key); const currentTs = Number(current?.updatedAt || current?.createdAt || 0); const nextTs = Number(agent?.updatedAt || agent?.createdAt || 0); if (nextTs > currentTs) seen.set(key, agent); }); return Array.from(seen.values()); }, [customAgentKey]); u_useEffect(() => { setCustomAgents((prev) => { const next = dedupeCustomAgents(prev); return next.length === prev.length ? prev : next; }); }, [dedupeCustomAgents]); u_useEffect(() => { const applyAgentRename = (event) => { const detail = event?.detail || {}; const agentSlug = String(detail.agentSlug || ''); const agentId = String(detail.agentId || ''); const agentName = String(detail.agentName || '').trim(); const agentTagline = String(detail.agentTagline || '').trim(); if (!agentName || (!agentSlug && !agentId)) return; setCustomAgents((prev) => dedupeCustomAgents(prev.map((agent) => { const sameAgent = String(agent.id || '') === agentSlug || String(agent.slug || '') === agentSlug || String(agent.agentDbId || '') === agentId || String(agent.dbId || '') === agentId; if (!sameAgent) return agent; return { ...agent, name: agentName, tagline: agentTagline || agent.tagline || '', updatedAt: Date.now(), }; }))); setCatalogAgents((prev) => prev.map((agent) => { const sameAgent = String(agent.id || '') === agentSlug || String(agent.slug || '') === agentSlug || String(agent.agentDbId || '') === agentId || String(agent.dbId || '') === agentId; if (!sameAgent) return agent; return { ...agent, name: agentName, tagline: agentTagline || agent.tagline || '', }; })); try { const latest = JSON.parse(localStorage.getItem('af_last_created_agent') || 'null'); if ( latest && ( String(latest.id || '') === agentSlug || String(latest.slug || '') === agentSlug || String(latest.agentDbId || '') === agentId || String(latest.dbId || '') === agentId ) ) { localStorage.setItem('af_last_created_agent', JSON.stringify({ ...latest, name: agentName, tagline: agentTagline || latest.tagline || '', })); } } catch (_) {} }; window.addEventListener('af:agent:renamed', applyAgentRename); return () => window.removeEventListener('af:agent:renamed', applyAgentRename); }, [dedupeCustomAgents]); const loadCatalogAgents = React.useCallback(() => { let mounted = true; (async () => { try { const token = sessionStorage.getItem('af_token') || ''; const catalogRes = await fetch('/api/v1/agents'); const rows = catalogRes.ok ? await catalogRes.json() : []; let purchased = []; if (token) { const purchasedRes = await fetch('/api/v1/purchases/my-agents', { headers: { Authorization: 'Bearer ' + token }, }); purchased = purchasedRes.ok ? await purchasedRes.json() : []; } const ownedById = new Map( (Array.isArray(purchased) ? purchased : []).map((p) => [String(p.agent_id), p]) ); if (!mounted) return; const mapped = (Array.isArray(rows) ? rows : []).map((a) => ({ id: a.slug || String(a.id || ''), name: ownedById.get(String(a.id))?.agent_name || a.name || 'Agente', cat: a.category || 'ops', tagline: ownedById.get(String(a.id))?.agent_tagline || a.tagline || '', baseName: a.name || '', author: a.author || 'Forge Labs', verified: !!a.verified, runs: Number(a.runs || 0), rating: Number(a.rating || 0), reviews: Number(a.reviews || 0), price: a.price_display || 'Gratis', tools: Array.isArray(a.tools) ? a.tools : [], tags: Array.isArray(a.tags) ? a.tags : [], color: a.color || '#ff5b1f', agentDbId: a.id || null, })); setCatalogAgents(mapped); } catch (_) { if (mounted) setCatalogAgents([]); } })(); return () => { mounted = false; }; }, []); u_useEffect(() => { const cleanup = loadCatalogAgents(); return cleanup; }, [loadCatalogAgents]); u_useEffect(() => { const onRefresh = () => { loadCatalogAgents(); }; window.addEventListener('af:catalog:refresh', onRefresh); return () => window.removeEventListener('af:catalog:refresh', onRefresh); }, [loadCatalogAgents]); // Completa OAuth de Google Calendar aunque Google redirija a "/" (landing). u_useEffect(() => { const params = new URLSearchParams(window.location.search); const oauthError = params.get('error'); if (oauthError) { const clean = window.location.origin + window.location.pathname; window.history.replaceState({}, document.title, clean); window.afNotify && window.afNotify('Google Workspace rechazó la conexión. Usa una cuenta agentforge-ia.pro.', 'error'); if (currentUser) setRoute('scheduling'); return; } const code = params.get('code'); const state = params.get('state'); if (!code || !state) return; const token = sessionStorage.getItem('af_token') || ''; if (!token) return; (async () => { try { const res = await fetch( `/api/v1/scheduling/google/exchange?code=${encodeURIComponent(code)}&state=${encodeURIComponent(state)}`, { method: 'POST', headers: { Authorization: 'Bearer ' + token }, } ); if (!res.ok) return; window.afNotify && window.afNotify('Google Workspace conectado'); const clean = window.location.origin + window.location.pathname; window.history.replaceState({}, document.title, clean); if (currentUser) setRoute('scheduling'); } catch (_) { window.afNotify && window.afNotify('No se pudo completar Google Workspace. Reintenta con una cuenta autorizada.', 'error'); const clean = window.location.origin + window.location.pathname; window.history.replaceState({}, document.title, clean); if (currentUser) setRoute('scheduling'); } })(); }, [currentUser]); u_useEffect(() => { const params = new URLSearchParams(window.location.search); if (!params.get('purchase_success') && !params.get('purchase_cancelled')) return; const clean = window.location.origin + window.location.pathname; window.history.replaceState({}, document.title, clean); if (params.get('purchase_cancelled')) { window.afNotify && window.afNotify('Compra cancelada'); return; } window.afNotify && window.afNotify('¡Pago completado! Tu agente ya está activo.'); if (currentUser) navigate('library'); }, []); /** * Evalúa permisos RBAC para secciones de usuario. * Para "Pruebas" e "Integraciones" exigimos sección explícita. * @param {string} r * @param {any} user * @returns {boolean} */ const canAccessUserSection = (r, user) => { if (!USER_RBAC_ROUTES.has(r)) return true; if (!user) return false; if (user.is_admin) return true; return !!(user.sections && user.sections[r]); }; // Comprueba si un usuario puede acceder a una sección admin (is_admin O tiene sections[r]) const canAccess = (r, user) => { if (!canAccessUserSection(r, user)) return false; if (!ADMIN_ROUTES.has(r)) return true; if (!user) return false; if (STRICT_ADMIN_ROUTES.has(r)) return !!user.is_admin; if (user.is_admin) return true; return !!(user.sections && user.sections[r]); }; u_useEffect(() => { if (!PROTECTED_ROUTES.has(route)) return; if (canAccess(route, currentUser)) return; setRoute(currentUser ? 'marketplace' : 'landing'); }, [route, currentUser]); const goBack = () => { setRouteHistory(h => { if (h.length === 0) return h; const prev = [...h]; const target = prev.pop(); setRoute(target); return prev; }); }; // Guard: rutas protegidas abren modal de login; rutas admin comprueban roles const navigate = (r) => { if (!currentUser && !PUBLIC_ROUTES.has(r)) { if (sessionStorage.getItem('af_token') && !sessionStorage.getItem('af_user')) setSessionExpired(true); setPendingRoute(r); setAuthOpen(true); return; } if (!canAccess(r, currentUser)) { return; // sin permiso — silencioso } setRouteHistory(h => [...h, route]); setRoute(r); }; const handleLoginSuccess = (u) => { setSessionExpired(false); if (u && u.id) sessionStorage.setItem('af_customer_id', String(u.id)); setCurrentUser(u); setAuthOpen(false); if (pendingRoute) { const dest = canAccess(pendingRoute, u) ? pendingRoute : 'marketplace'; setRoute(dest); setPendingRoute(null); return; } if (PUBLIC_ROUTES.has(route)) setRoute('home'); }; const handleLogout = () => { setSessionExpired(false); sessionStorage.removeItem('af_token'); sessionStorage.removeItem('af_user'); sessionStorage.removeItem('af_customer_id'); setCurrentUser(null); setRoute('landing'); }; const baseData = window.AF_DATA; const sourceAgents = catalogAgents.length > 0 ? catalogAgents : (Array.isArray(baseData.agents) ? baseData.agents : []); const unique = new Map(); [...customAgents, ...sourceAgents].forEach((a) => { const isCustomAgent = a?._custom || String(a?.author || '').toLowerCase() === 'cliente' || /personalizado|privado/i.test(String(a?.price || '')); const key = isCustomAgent ? `custom:${customAgentStableId(a) || [ String(a.name || '').trim().toLowerCase(), String(a.tagline || '').trim().toLowerCase(), String(a.cat || a.category || '').trim().toLowerCase(), ].join(':')}` : String(a.id || a.slug || '').trim(); if (!key) return; if (!unique.has(key)) unique.set(key, a); }); const data = { ...baseData, agents: Array.from(unique.values()) }; const createAgent = (payload) => { const slugBase = (payload.name || 'nuevo-agente').toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, ''); const id = `${slugBase || 'agente'}-${Date.now().toString().slice(-6)}`; const cat = payload.cat || 'ops'; const catObj = (baseData.categories || []).find((c) => c.id === cat); const agent = { id, name: payload.name || 'Nuevo agente', cat, tagline: payload.tagline || 'Agente creado desde Editor', author: 'Mi espacio', verified: false, runs: 0, rating: 0, reviews: 0, price: 'Privado', tools: payload.tools || [], tags: ['custom'], color: (catObj && catObj.accent) || '#ff5b1f', _custom: true, contextFiles: Array.isArray(payload.contextFiles) ? payload.contextFiles : [], contextUrls: Array.isArray(payload.contextUrls) ? payload.contextUrls : [], contextNotes: payload.contextNotes || '', deployment: payload.deployment || 'borrador', lastRun: 'nunca', createdAt: Date.now(), updatedAt: Date.now(), }; setCustomAgents((prev) => [agent, ...prev]); try { localStorage.setItem('af_last_created_agent', JSON.stringify(agent)); } catch {} window.afNotify && window.afNotify(`Agente creado: ${agent.name}`); return agent; }; /** * Elimina un agente local/custom del estado frontend. * @param {string} agentId * @returns {void} */ const removeCustomAgent = (agentId) => { if (!agentId) return; setCustomAgents((prev) => prev.filter((a) => a.id !== agentId)); }; const openAgent = (id) => setOpenId(id); const handleEditAgent = async (row) => { let systemPrompt = ''; let flowNodes = []; let flowEdges = []; if (row.agentDbId) { try { const token = sessionStorage.getItem('af_token') || ''; const res = await fetch(`/api/v1/agents/${row.agentDbId}/detail`, { headers: { Authorization: 'Bearer ' + token }, }); if (res.ok) { const detail = await res.json(); systemPrompt = detail.system_prompt || ''; flowNodes = Array.isArray(detail.flow_nodes) ? detail.flow_nodes : []; flowEdges = Array.isArray(detail.flow_edges) ? detail.flow_edges : []; } } catch (_) {} } if (isCookingAgentRow(row)) { systemPrompt = COOKING_RECIPE_PROMPT; } try { localStorage.setItem('af_builder_seed_v1', JSON.stringify({ agentName: row.agent?.name || 'Agente', agentSlug: row.id, agentId: row.agentDbId || '', agentTagline: row.agent?.tagline || '', agentColor: row.agent?.color || '#ff5b1f', systemPrompt, flowNodes, flowEdges, blank: false, isEditMode: true, fromLibrary: true, })); } catch (_) {} setActiveEditAgent(row); navigate('builder'); }; const handleDeployAgent = (row) => { try { sessionStorage.setItem('af_deploy_agent', JSON.stringify({ agentId: row.agentDbId || '', agentSlug: row.id, agentName: row.agent?.name || 'Agente', })); } catch (_) {} setActiveDeployAgent(row); navigate('deployment'); }; const openAgentObj = openId ? data.agents.find(a => a.id === openId) : null; const openCat = openAgentObj ? data.categories.find(c => c.id === openAgentObj.cat) : null; const _parsePriceCents = (priceStr) => { if (!priceStr || /gratis/i.test(priceStr)) return 0; const m = String(priceStr).match(/(\d+)/); return m ? parseInt(m[1], 10) * 100 : 0; }; /** * Resuelve el UUID real de backend a partir del slug frontend. * Si el agente no existe en la BD, lo crea usando los datos del catálogo. */ const resolveBackendAgentId = async (agent) => { const slug = agent.id; const token = sessionStorage.getItem('af_token') || ''; const headers = { 'Content-Type': 'application/json', Authorization: 'Bearer ' + token }; // Primero intentar encontrarlo por slug en el listado público try { const listRes = await fetch('/api/v1/agents'); if (listRes.ok) { const agents = await listRes.json(); const match = Array.isArray(agents) ? agents.find((a) => a.slug === slug) : null; if (match) return match.id; } } catch (_) {} // Fallback: find-or-create desde datos del catálogo frontend const catalogRes = await fetch('/api/v1/agents/from-catalog', { method: 'POST', headers, body: JSON.stringify({ slug, name: agent.name || slug, tagline: agent.tagline || '', author: agent.author || 'Forge Labs', price_display: agent.price || 'Gratis', price_cents: _parsePriceCents(agent.price), category: agent.cat || '', color: agent.color || '#ff5b1f', tools: Array.isArray(agent.tools) ? agent.tools : [], tags: Array.isArray(agent.tags) ? agent.tags : [], runs: agent.runs || 0, rating: agent.rating || 5.0, reviews: agent.reviews || 0, }), }); if (!catalogRes.ok) return null; const catalogData = await catalogRes.json(); return catalogData.id || null; }; const _callBillingEndpoint = async (agent, endpoint) => { const backendAgentId = await resolveBackendAgentId(agent); if (!backendAgentId) { window.afNotify && window.afNotify('No se pudo registrar el agente. Inténtalo de nuevo.'); return; } const token = sessionStorage.getItem('af_token') || ''; const res = await fetch(endpoint, { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + token }, body: JSON.stringify({ agent_id: backendAgentId }), }); if (res.status === 409) { window.afNotify && window.afNotify('Ese agente ya está activo en tu cuenta'); setOpenId(null); navigate('library'); return; } if (!res.ok) { window.afNotify && window.afNotify('No se pudo procesar la compra'); return; } const data = await res.json(); setOpenId(null); if (data.is_free) { window.afNotify && window.afNotify(`${agent.name} activado correctamente`); navigate('library'); } else { window.location.href = data.checkout_url; } }; const startCheckout = async (agent) => { if (!currentUser) { setPendingRoute('library'); setAuthOpen(true); return; } try { await _callBillingEndpoint(agent, '/api/v1/billing/checkout-session'); } catch (_) { window.afNotify && window.afNotify('Error de red durante la compra'); } }; const simulateCheckout = async (agent) => { if (!currentUser) { setPendingRoute('library'); setAuthOpen(true); return; } try { await _callBillingEndpoint(agent, '/api/v1/billing/simulate-payment'); } catch (_) { window.afNotify && window.afNotify('Error de red durante la simulación'); } }; const screen = (() => { switch (route) { case 'home': return ; case 'marketplace': return ; case 'library': return { setActivePlayAgentId(agentId); navigate('playground'); }} onEditAgent={handleEditAgent} onDeployAgent={handleDeployAgent} />; case 'crm': return ; case 'scheduling': return ; case 'deployment': return ; case 'builder': return ; case 'playground': return ; case 'onboarding': return ; case 'integrations': return ; case 'plans': return ; case 'billing': return ; case 'settings': return ; case 'runs': return ; case 'analytics': return ; case 'admin': return (currentUser && (currentUser.is_admin || (currentUser.sections && Object.keys(currentUser.sections).length > 0))) ? : ; case 'ops_finance': return ; case 'ops_sales': return ; case 'ops_marketing': return ; case 'ops_operations': return ; case 'ops_hr': return ; case 'ops_support': return ; case 'landing': return ( { setPendingRoute('home'); setAuthOpen(true); }} onDocs={() => navigate('public-docs')} /> ); case 'public-docs': return navigate('landing')} />; default: return ; } })(); const toggleTheme = () => setTweak('theme', tweaks.theme === 'dark' ? 'light' : 'dark'); const isLanding = route === 'landing'; const isPublicDocs = route === 'public-docs'; const isPublicView = isLanding || isPublicDocs; const quickActions = [ ['home', 'Inicio'], ['marketplace', 'Marketplace'], ['library', 'Mis agentes'], ['playground', 'Playground'], ['builder', 'Builder'], ['deployment', 'Deployment'], ['settings', 'Ajustes'], ]; return (
{!isPublicView && ( <> setQuickOpen(true)} currentUser={currentUser} onLoginClick={() => setAuthOpen(true)} onLogout={handleLogout} goBack={goBack} canGoBack={routeHistory.length > 0} sessionExpired={sessionExpired} /> )}
{screen}
{openAgentObj && setOpenId(null)} onInstall={startCheckout} onSimulate={simulateCheckout} onTry={() => { setActivePlayAgentId(openAgentObj.id); setOpenId(null); navigate('playground'); }} />} { setAuthOpen(false); setPendingRoute(null); }} onSuccess={handleLoginSuccess} /> setTweak('theme', v)} options={[{ value:'dark', label:'Oscuro' },{ value:'light', label:'Claro' }]} /> setTweak('accent', v)} /> setTweak('monoFont', v)} options={[{value:'JetBrains Mono',label:'JetBrains Mono'},{value:'IBM Plex Mono',label:'IBM Plex Mono'},{value:'Geist Mono',label:'Geist Mono'},{value:'Fira Code',label:'Fira Code'}]} /> {!isPublicView && quickOpen && (
{ if (e.target === e.currentTarget) setQuickOpen(false); }}>
Comandos rápidos
{quickActions.map(([id, label]) => ( ))}
)} {notice && (
{notice.message}
)} {bookingOpen && setBookingOpen(false)} />}
); } function LandingHero({ onLogin, onDocs }) { const [lang, setLang] = u_useState('es'); u_useEffect(() => { try { localStorage.setItem('af_ui_lang', lang); window.afUiLang = lang; window.dispatchEvent(new CustomEvent('af:lang', { detail: lang })); } catch (_) {} }, [lang]); const t = lang === 'es' ? { badge: 'NUEVA GENERACIÓN DE AGENTES IA', titleA: 'Automatiza operaciones con', titleB: 'agentes que sí trabajan', subtitle: 'Diseña, despliega y opera agentes reales para soporte, ventas y backoffice en una sola plataforma.', cta1: 'Entrar al panel', cta2: 'Iniciar sesión', contact: 'Contacto', k1: 'Flujos activos', k2: 'Ejecuciones hoy', k3: 'Ahorro estimado', nav1: 'Producto', nav4: 'Docs', } : { badge: 'NEXT-GEN AI AGENT PLATFORM', titleA: 'Automate operations with', titleB: 'agents that actually work', subtitle: 'Design, deploy and operate real agents for support, sales and back-office in one platform.', cta1: 'Enter dashboard', cta2: 'Sign in', contact: 'Contact', k1: 'Active flows', k2: 'Executions today', k3: 'Estimated savings', nav1: 'Product', nav4: 'Docs', }; return (