// AGENTFORGE-IA — pantalla Implantación const Deployment = ({ preselectedAgent }) => { const [filter, setFilter] = React.useState('todas'); const [selected, setSelected] = React.useState(null); const [showAssistant, setShowAssistant] = React.useState(false); const [showCompare, setShowCompare] = React.useState(false); const [showSales, setShowSales] = React.useState(false); const [showPdf, setShowPdf] = React.useState(false); const [assistantAnswers, setAssistantAnswers] = React.useState({ sector: 'general', data: 'standard', team: 'small', budget: 'starter', }); const [requests, setRequests] = React.useState([]); const [sendingRequest, setSendingRequest] = React.useState(false); const [deployments, setDeployments] = React.useState([]); const [showDeploymentsList, setShowDeploymentsList] = React.useState(false); const [showNewDeploy, setShowNewDeploy] = React.useState(false); const emptyDeployForm = { agent_id: '', channel: 'web', whatsapp_access_token: '', whatsapp_phone_number_id: '', whatsapp_verify_token: '', telegram_bot_token: '', allowed_origins: '*', system_prompt_override: '', }; const [deployForm, setDeployForm] = React.useState(emptyDeployForm); const [deployFormLoading, setDeployFormLoading] = React.useState(false); const [agents, setAgents] = React.useState([]); const [agentSearch, setAgentSearch] = React.useState(''); const [pendingDeployAgent, setPendingDeployAgent] = React.useState(null); const [lastCreatedDeployment, setLastCreatedDeployment] = React.useState(null); const [deployFeedback, setDeployFeedback] = React.useState(null); const [copiedToken, setCopiedToken] = React.useState(null); const notify = (msg) => window.afNotify && window.afNotify(msg); const contextPromptFromForm = (ctx) => { if (!ctx) return ''; const lines = []; if (ctx.empresa) lines.push(`Empresa: ${ctx.empresa}`); if (ctx.descripcion) lines.push(`Descripción del negocio: ${ctx.descripcion}`); if (ctx.productos) lines.push(`Productos/Servicios: ${ctx.productos}`); if (ctx.audiencia) lines.push(`Audiencia objetivo: ${ctx.audiencia}`); if (ctx.tono) lines.push(`Tono recomendado: ${ctx.tono}`); if (ctx.faqs) lines.push(`FAQs: ${ctx.faqs}`); if (ctx.instrucciones) lines.push(`Instrucciones extra: ${ctx.instrucciones}`); if (lines.length === 0) return ''; return `Contexto del cliente para este agente:\n${lines.join('\n')}\n\nUsa este contexto para responder con precisión y en el idioma del usuario.`; }; const loadAgentContextBySlug = (slug) => { if (!slug) return null; try { const raw = localStorage.getItem('af_agent_ctx_' + slug); if (!raw) return null; return JSON.parse(raw); } catch (_) { return null; } }; const getToken = () => sessionStorage.getItem('af_token') || ''; /** * Carga solicitudes de implantación del usuario autenticado. * @returns {Promise} */ const loadRequests = React.useCallback(async () => { const token = getToken(); if (!token) return; try { const res = await fetch('/api/v1/deployment/requests/mine', { headers: { Authorization: 'Bearer ' + token }, }); if (!res.ok) { notify('No se pudieron cargar solicitudes de implantación'); return; } const data = await res.json(); setRequests(Array.isArray(data) ? data.slice().reverse() : []); } catch (_) { notify('Error de red cargando solicitudes de implantación'); } }, []); const loadDeployments = React.useCallback(async () => { const token = getToken(); if (!token) return; try { const res = await fetch('/api/v1/deployment/deployments', { headers: { Authorization: 'Bearer ' + token }, }); if (!res.ok) { notify('No se pudieron cargar despliegues'); return; } const data = await res.json(); setDeployments(Array.isArray(data) ? data : []); } catch (_) { notify('Error de red cargando despliegues'); } }, []); const loadAgents = React.useCallback(async () => { try { const token = getToken(); const [catalogRes, purchasedRes] = await Promise.all([ fetch('/api/v1/agents'), token ? fetch('/api/v1/purchases/my-agents', { headers: { Authorization: 'Bearer ' + token } }) : Promise.resolve(null), ]); if (!catalogRes.ok) { notify('No se pudo cargar el catálogo de agentes'); } if (purchasedRes && !purchasedRes.ok) { notify('No se pudieron cargar tus agentes comprados'); } const catalogData = catalogRes.ok ? await catalogRes.json() : []; const catalogAgents = Array.isArray(catalogData) ? catalogData : (Array.isArray(catalogData.agents) ? catalogData.agents : []); const purchasedData = purchasedRes && purchasedRes.ok ? await purchasedRes.json() : []; const purchasedAgents = (Array.isArray(purchasedData) ? purchasedData : []).map((item) => ({ id: item.agent_id, slug: item.agent_slug, name: item.agent_name, tagline: `Agente instalado · ${item.plan || 'plan activo'}`, source: 'installed', })); const byId = new Map(); [...purchasedAgents, ...catalogAgents].forEach((agent) => { if (agent?.id && !byId.has(agent.id)) byId.set(agent.id, agent); }); setAgents(Array.from(byId.values())); } catch (_) { notify('Error de red cargando agentes'); } }, []); /** * Normaliza texto para que la búsqueda no falle por acentos o mayúsculas. * @param {unknown} value Texto de entrada. * @returns {string} Texto normalizado para comparar. */ const normalizeSearchText = (value) => String(value || '') .normalize('NFD') .replace(/[\u0300-\u036f]/g, '') .toLowerCase() .trim(); /** * Convierte un agente recibido desde navegación en una opción seleccionable. * @param {Record|null} target Agente recibido desde Mis agentes. * @returns {Record|null} Opción compatible con el selector. */ const optionFromTargetAgent = (target) => { if (!target) return null; const id = target.agentId || target.agentDbId || target.agent_id || target.agent?.agentDbId || ''; const slug = target.agentSlug || target.slug || target.id || target.agent?.id || ''; const name = target.agentName || target.name || target.agent?.name || ''; if (!id || !name) return null; return { id, slug, name, tagline: target.tagline || target.agent?.tagline || 'Agente seleccionado para despliegue', source: 'selected', }; }; /** * Convierte despliegues existentes en opciones de agente para reutilizarlos. * @param {Array>} items Despliegues del usuario. * @returns {Array>} Agentes deduplicados desde despliegues. */ const optionsFromDeployments = (items) => { const byId = new Map(); (Array.isArray(items) ? items : []).forEach((deployment) => { const id = deployment.agent_id || deployment.agentId || deployment.agent?.id || ''; const name = deployment.agent_name || deployment.agentName || deployment.agent?.name || ''; if (!id || !name || byId.has(String(id))) return; byId.set(String(id), { id, slug: deployment.agent_slug || deployment.agentSlug || deployment.agent?.slug || '', name, tagline: `Ya desplegado · ${deployment.channel || 'canal activo'}`, source: 'deployed', }); }); return Array.from(byId.values()); }; React.useEffect(() => { loadRequests(); loadDeployments(); loadAgents(); }, [loadRequests, loadDeployments, loadAgents]); React.useEffect(() => { try { const raw = sessionStorage.getItem('af_deploy_agent'); if (!raw) return; sessionStorage.removeItem('af_deploy_agent'); setPendingDeployAgent(JSON.parse(raw)); } catch (_) {} }, []); const deploymentAgents = React.useMemo(() => optionsFromDeployments(deployments), [deployments]); const selectableAgents = React.useMemo(() => { const byId = new Map(); const add = (agent) => { if (!agent?.id) return; const key = String(agent.id); const current = byId.get(key); byId.set(key, { ...current, ...agent, name: agent.name || current?.name || 'Agente', slug: agent.slug || current?.slug || '', }); }; agents.forEach(add); deploymentAgents.forEach(add); add(optionFromTargetAgent(preselectedAgent)); add(optionFromTargetAgent(pendingDeployAgent)); return Array.from(byId.values()); }, [agents, deploymentAgents, preselectedAgent, pendingDeployAgent]); /** * Resuelve el agente elegido desde Mis agentes o desde el selector del modal. * @param {Record|null} target Agente origen. * @returns {Record|null} Agente resuelto con UUID real. */ const resolveDeployTarget = React.useCallback((target) => { if (!target) return null; const targetOption = optionFromTargetAgent(target); const agentSlug = targetOption?.slug || target.agentSlug || target.id || target.agent?.id || ''; const agentDbId = targetOption?.id || target.agentId || target.agentDbId || ''; const agentName = targetOption?.name || target.agentName || target.agent?.name || target.name || 'seleccionado'; const normalizedName = normalizeSearchText(agentName); const match = selectableAgents.find((a) => String(a.id || '') === String(agentDbId || '') || String(a.slug || '') === String(agentSlug || '') || normalizeSearchText(a.name) === normalizedName ); const resolvedId = agentDbId || match?.id || ''; if (!resolvedId) return null; return { id: resolvedId, slug: agentSlug || match?.slug || '', name: agentName || match?.name || 'seleccionado', }; }, [selectableAgents]); /** * Preselecciona en el formulario el agente que llegó desde Mis agentes. * @param {Record|null} target * @param {boolean} openDrawer * @returns {boolean} */ const prefillDeployAgent = React.useCallback((target, openDrawer = true) => { const resolved = resolveDeployTarget(target); if (!resolved) return false; const ctx = loadAgentContextBySlug(resolved.slug); const promptFromCtx = contextPromptFromForm(ctx); setDeployForm((f) => ({ ...f, agent_id: resolved.id, system_prompt_override: promptFromCtx || f.system_prompt_override, })); if (openDrawer) { setShowNewDeploy(true); } return true; }, [resolveDeployTarget]); // Pre-seleccionar agente si venimos desde "Mis Agentes → Desplegar". React.useEffect(() => { const target = preselectedAgent || pendingDeployAgent; if (!target) return; const ok = prefillDeployAgent(target, true); if (ok) { const name = target.agentName || target.agent?.name || target.name || 'seleccionado'; notify(`Agente "${name}" pre-cargado para despliegue`); } }, [preselectedAgent, pendingDeployAgent, selectableAgents, prefillDeployAgent]); /** * Registra una solicitud comercial de implantación. * @param {{source:string, mode_id?:string, mode_name?:string, note?:string}} payload * @returns {Promise} */ const submitDeploymentRequest = async (payload) => { const token = getToken(); if (!token) { notify('Necesitas iniciar sesión'); return; } setSendingRequest(true); try { const res = await fetch('/api/v1/deployment/requests', { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + token, }, body: JSON.stringify(payload), }); if (!res.ok) { notify('No se pudo registrar la solicitud'); return; } const created = await res.json(); setRequests((prev) => [created, ...prev]); notify('Solicitud comercial enviada'); } catch (_) { notify('Error de red al enviar la solicitud'); } finally { setSendingRequest(false); } }; const modes = [ { id: 'saas-multi', name: 'SaaS multi-tenant', tagline: 'Cloud compartido — empezar en minutos', icon: 'cloud', color: '#10b981', tier: 'starter', where: 'Nuestra nube (UE/EEUU)', tenancy: 'Compartida (aislada lógicamente)', time: 'Minutos', mgmt: 'Nosotros', price: 'Suscripción + consumo', desc: 'El cliente accede a AgentForge-IA en nuestra infraestructura cloud, compartiendo recursos con otros tenants. Datos siempre aislados lógicamente, cifrados en reposo y tránsito.', ideal: ['PYMEs y startups', 'Equipos que quieren empezar ya', 'Casos de uso estándar sin requisitos sectoriales fuertes'], includes: ['Self-serve desde la web', 'Actualizaciones automáticas', 'Región configurable (UE/EEUU)', 'Marketplace completo de agentes'], excludes: ['Aislamiento físico de cómputo', 'BYOK', 'Air-gap'], sla: '99,9 %', regions: ['UE-Madrid','UE-Frankfurt','EEUU-Este','EEUU-Oeste'], services: [ { name: 'Hosting cloud (Azure/AWS)', cost: 'incluido' }, { name: 'Backups diarios', cost: 'incluido' }, { name: 'SSL/TLS y CDN', cost: 'incluido' }, { name: 'Actualizaciones automáticas', cost: 'incluido' }, { name: 'Onboarding remoto', cost: '490 €' }, ], }, { id: 'saas-dedicated', name: 'SaaS dedicado', tagline: 'Single-tenant en nuestra nube', icon: 'shield', color: '#3b82f6', tier: 'business', where: 'Nuestra nube (región fijada)', tenancy: 'Dedicada (single-tenant)', time: '1–2 días', mgmt: 'Nosotros (con ventanas)', price: 'Suscripción anual + cuota fija', desc: 'Instancia aislada del cliente — su propia base de datos, cómputo y dominio. Nosotros operamos pero el cliente decide cuándo aplicar actualizaciones.', ideal: ['Empresas medianas con cumplimiento RGPD reforzado', 'ENS medio', 'Sector financiero o salud sin restricciones de "no SaaS"'], includes: ['Dominio propio (empresa.agentforge.ai)','Copias de seguridad dedicadas','BYOK opcional','Ventanas de mantenimiento pactadas','Auditoría SOC 2 / ISO 27001'], excludes: ['Despliegue en nube del cliente','Operación air-gapped'], sla: '99,95 %', regions: ['UE-Madrid','UE-Frankfurt','UE-Irlanda'], services: [ { name: 'Infraestructura dedicada (single-tenant)', cost: 'desde 180 €/mes' }, { name: 'Dominio propio + SSL', cost: 'incluido' }, { name: 'Copias de seguridad dedicadas', cost: 'incluido' }, { name: 'Configuración inicial', cost: '890 €' }, { name: 'Monitorización 24/7', cost: '120 €/mes' }, { name: 'Soporte técnico prioritario', cost: '150 €/mes' }, ], }, { id: 'byoc', name: 'Private Cloud / BYOC', tagline: 'Despliegue en la nube del cliente', icon: 'lock', color: '#8b5cf6', tier: 'enterprise', where: 'Cuenta cloud del cliente', tenancy: 'Dedicada en su tenant', time: '1–3 semanas', mgmt: 'Compartido', price: 'Licencia anual + servicios profesionales', desc: 'Desplegamos AgentForge-IA en la cuenta cloud del cliente (AWS, Azure o GCP). Nosotros gestionamos el software vía un agente de despliegue; los datos y el cómputo nunca salen de su tenant.', ideal: ['Banca y seguros','Salud y farmacia','Sector público','Grandes corporaciones con políticas de "no SaaS"'], includes: ['Integración con KMS, IAM y VPC del cliente','Logs y métricas en su SIEM','Datos nunca salen de su tenant','Actualizaciones validadas por el cliente'], excludes: ['Operación 100 % offline (ver On-prem)'], sla: '99,95 %', regions: ['AWS · Azure · GCP — cualquier región del cliente'], services: [ { name: 'Despliegue en tenant del cliente', cost: 'incluido en licencia' }, { name: 'Configuración inicial (IaC + CI/CD)', cost: '1.490 €' }, { name: 'Integración KMS/IAM/VPC', cost: '890 €' }, { name: 'Monitorización 24/7', cost: '120 €/mes' }, { name: 'Soporte técnico prioritario', cost: '150 €/mes' }, ], }, { id: 'on-prem', name: 'On-premise', tagline: 'Auto-hospedado en infraestructura propia', icon: 'server', color: '#f59e0b', tier: 'enterprise', where: 'Data center del cliente', tenancy: 'Total — sin dependencia externa', time: '3–8 semanas', mgmt: 'Cliente (con soporte)', price: 'Licencia anual o perpetua + soporte enterprise', desc: 'El cliente ejecuta AgentForge-IA en sus propios servidores. Entregamos contenedores firmados (Docker / Helm para Kubernetes). Compatible con operación air-gapped usando LLMs locales.', ideal: ['Defensa e infraestructuras críticas','Administración pública con datos clasificados','Sectores regulados sin acceso a cloud público'], includes: ['Contenedores firmados','Helm chart para Kubernetes','Soporte para LLMs locales (Llama, Mistral, modelos propios)','Modo air-gapped','Parches mensuales con changelog'], excludes: ['Marketplace de agentes en tiempo real (sólo paquetes offline)'], sla: 'Definido por el cliente', regions: ['Cualquiera — incluso sin internet'], services: [ { name: 'Despliegue en infraestructura cliente', cost: 'consultar' }, { name: 'Integración SSO/LDAP', cost: '1.200 €' }, { name: 'Formación equipo técnico', cost: '800 €/día' }, { name: 'SLA dedicado + soporte 24/7', cost: 'desde 400 €/mes' }, { name: 'Parches y actualizaciones anuales', cost: 'incluido en licencia' }, ], }, { id: 'edge', name: 'Edge / híbrido', tagline: 'Datos en local, marketplace en cloud', icon: 'wifi', color: '#06b6d4', tier: 'business', where: 'On-prem o BYOC + servicios cloud', tenancy: 'Híbrida', time: '2–4 semanas', mgmt: 'Compartido', price: 'Licencia + suscripción a servicios cloud', desc: 'El orquestador y los datos sensibles viven on-prem o en BYOC; ciertos componentes (modelos grandes, marketplace) se consumen como servicio. Pensado para sitios distribuidos con baja latencia.', ideal: ['Retail con muchas tiendas','Industria y fábricas','Logística y centros distribuidos','Operación parcialmente offline'], includes: ['Sincronización con cloud central','Caché local de modelos','Failover offline','Despliegue por sucursal'], excludes: ['Air-gapped completo'], sla: '99,9 %', regions: ['Combinación cliente + nuestra nube'], services: [ { name: 'Nodo edge por sucursal', cost: 'desde 80 €/mes' }, { name: 'Configuración inicial (nodo + cloud)', cost: '1.190 €' }, { name: 'Sincronización y caché de modelos', cost: 'incluido' }, { name: 'Monitorización 24/7', cost: '120 €/mes' }, { name: 'Soporte técnico prioritario', cost: '150 €/mes' }, ], }, { id: 'oem', name: 'White-label / OEM', tagline: 'Integra AgentForge-IA en tu producto', icon: 'package', color: '#ec4899', tier: 'partner', where: 'Cualquier infraestructura', tenancy: 'Según el partner', time: '4–8 semanas', mgmt: 'Partner', price: 'Revenue share o licencia por volumen', desc: 'Un partner integra AgentForge-IA en su propio producto bajo su marca, vía API y SDK. Branding, dominios y facturación al cliente final son del partner.', ideal: ['Consultoras e integradores','ISVs verticales (salud, legal, edu…)','Plataformas que quieren ofrecer agentes a sus clientes'], includes: ['API y SDKs (JS, Python, Go)','Branding personalizable (colores, logos, dominios)','Multi-tenant gestionado por el partner','Revenue share configurable'], excludes: ['UI estándar de AgentForge — el partner construye la suya'], sla: 'Según contrato del partner', regions: ['Sin restricción'], services: [ { name: 'Integración API + SDK', cost: 'consultar' }, { name: 'Branding y white-label setup', cost: '1.500 €' }, { name: 'Formación equipo técnico partner', cost: '800 €/día' }, { name: 'SLA dedicado + soporte 24/7', cost: 'desde 400 €/mes' }, { name: 'Gestor de cuenta partner', cost: 'incluido en contrato' }, ], }, ]; const tiers = { starter: { lbl: 'Starter', c: '#10b981' }, business: { lbl: 'Business', c: '#3b82f6' }, enterprise: { lbl: 'Enterprise', c: '#8b5cf6' }, partner: { lbl: 'Partner', c: '#ec4899' } }; const filtered = filter === 'todas' ? modes : modes.filter(m => m.tier === filter); const services = [ { name: 'Onboarding asistido', desc: 'Descubrimiento, conexión de fuentes y primeros 3–5 agentes en producción', dur: '1–4 semanas', icon: 'rocket' }, { name: 'Migración', desc: 'Importación de datos, agentes y conversaciones desde otras plataformas', dur: '1–3 semanas', icon: 'upload' }, { name: 'Formación', desc: 'Sesiones para administradores, builders y usuarios finales', dur: '8–24 horas', icon: 'book' }, { name: 'Soporte', desc: 'Standard (8×5), Business (SLA 4h) o Enterprise (24×7, TAM dedicado)', dur: 'Continuo', icon: 'headset' }, { name: 'Servicios profesionales', desc: 'Agentes a medida, integraciones bespoke, auditorías de seguridad', dur: 'Bajo demanda', icon: 'wrench' }, ]; const recommendMode = () => { const { sector, data, team, budget } = assistantAnswers; if (data === 'airgap') return modes.find((m) => m.id === 'on-prem') || modes[3]; if (data === 'customer-cloud') return modes.find((m) => m.id === 'byoc') || modes[2]; if (sector === 'partner') return modes.find((m) => m.id === 'oem') || modes[5]; if (team === 'distributed') return modes.find((m) => m.id === 'edge') || modes[4]; if (budget === 'starter') return modes.find((m) => m.id === 'saas-multi') || modes[0]; return modes.find((m) => m.id === 'saas-dedicated') || modes[1]; }; const createDeployment = async () => { const token = getToken(); setDeployFeedback(null); if (!token) { setDeployFeedback({ type: 'error', message: 'Necesitas iniciar sesión para crear el despliegue.' }); notify('Necesitas iniciar sesión'); return; } if (!deployForm.agent_id) { setDeployFeedback({ type: 'error', message: 'Selecciona un agente antes de crear el despliegue.' }); notify('Selecciona un agente'); return; } if (deployForm.channel === 'web' && !deployForm.allowed_origins.trim()) { setDeployFeedback({ type: 'error', message: 'Indica al menos un dominio permitido o * para pruebas.' }); notify('Indica al menos un dominio permitido o * para pruebas'); return; } if ( deployForm.channel === 'whatsapp' && ( !deployForm.whatsapp_access_token.trim() || !deployForm.whatsapp_phone_number_id.trim() || !deployForm.whatsapp_verify_token.trim() ) ) { setDeployFeedback({ type: 'error', message: 'WhatsApp Meta requiere Access Token, Phone Number ID y Verify Token.' }); notify('Meta requiere Access Token, Phone Number ID y Verify Token'); return; } if (deployForm.channel === 'telegram' && !deployForm.telegram_bot_token.trim()) { setDeployFeedback({ type: 'error', message: 'El token del bot de Telegram es obligatorio para Telegram.' }); notify('El token del bot de Telegram es obligatorio para Telegram'); return; } setDeployFormLoading(true); try { const body = { agent_id: deployForm.agent_id, channel: deployForm.channel }; if (deployForm.allowed_origins) body.allowed_origins = deployForm.allowed_origins; if (deployForm.whatsapp_access_token) body.whatsapp_access_token = deployForm.whatsapp_access_token; if (deployForm.whatsapp_phone_number_id) body.whatsapp_phone_number_id = deployForm.whatsapp_phone_number_id; if (deployForm.whatsapp_verify_token) body.whatsapp_verify_token = deployForm.whatsapp_verify_token; if (deployForm.telegram_bot_token) body.telegram_bot_token = deployForm.telegram_bot_token; if (deployForm.system_prompt_override) body.system_prompt_override = deployForm.system_prompt_override; const res = await fetch('/api/v1/deployment/deploy', { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + token }, body: JSON.stringify(body), }); if (!res.ok) { let reason = `HTTP ${res.status}`; try { const payload = await res.json(); reason = payload?.detail || payload?.message || reason; } catch (_) {} setDeployFeedback({ type: 'error', message: `No se pudo crear el despliegue: ${reason}` }); notify('Error al crear el despliegue'); return; } const created = await res.json(); setDeployments((prev) => [created, ...prev]); notify(created.channel_configured ? 'Despliegue creado y configurado' : 'Despliegue creado: revisa configuración pendiente'); setLastCreatedDeployment(created); setDeployFeedback({ type: 'success', message: `Despliegue creado para ${created.agent_name} en ${created.channel}.`, deployment: created, }); } catch (_) { setDeployFeedback({ type: 'error', message: 'Error de red al crear el despliegue. Revisa conexión o sesión.' }); notify('Error de red'); } finally { setDeployFormLoading(false); } }; const deleteDeployment = async (id) => { const token = getToken(); if (!token) return; try { const res = await fetch('/api/v1/deployment/deployments/' + id, { method: 'DELETE', headers: { Authorization: 'Bearer ' + token }, }); if (!res.ok) { let reason = `HTTP ${res.status}`; try { const payload = await res.json(); reason = payload?.detail || payload?.message || reason; } catch (_) {} notify(`No se pudo eliminar el despliegue: ${reason}`); return; } setDeployments((prev) => prev.filter((d) => d.id !== id)); notify('Despliegue eliminado'); } catch (_) { notify('Error al eliminar'); } }; const copyToClipboard = (text, key) => { navigator.clipboard.writeText(text).then(() => { setCopiedToken(key); setTimeout(() => setCopiedToken(null), 2000); }); }; const openNewDeployForChannel = (channel) => { const target = preselectedAgent || pendingDeployAgent; let nextAgentId = deployForm.agent_id; let nextPrompt = ''; if (target) { const resolved = resolveDeployTarget(target); if (resolved) { const ctx = loadAgentContextBySlug(resolved.slug); nextAgentId = resolved.id; nextPrompt = contextPromptFromForm(ctx); } } setAgentSearch(''); setDeployFeedback(null); setLastCreatedDeployment(null); setDeployForm({ ...emptyDeployForm, agent_id: nextAgentId || '', channel, system_prompt_override: nextPrompt, }); setShowNewDeploy(true); }; const closeNewDeploy = () => { setShowNewDeploy(false); setDeployFeedback(null); setLastCreatedDeployment(null); setAgentSearch(''); setDeployForm(emptyDeployForm); }; const selectedAgentOption = React.useMemo( () => selectableAgents.find((a) => String(a.id) === String(deployForm.agent_id)) || null, [selectableAgents, deployForm.agent_id], ); const filteredAgents = React.useMemo(() => { const q = normalizeSearchText(agentSearch); const matches = !q ? selectableAgents : selectableAgents.filter((a) => normalizeSearchText([ a.name, a.slug, a.tagline, a.source === 'installed' ? 'instalado' : '', a.source === 'deployed' ? 'desplegado' : '', a.source === 'selected' ? 'seleccionado' : '', ].filter(Boolean).join(' ')).includes(q)); if (!selectedAgentOption || matches.some((a) => String(a.id) === String(selectedAgentOption.id))) { return matches; } return [selectedAgentOption, ...matches]; }, [selectableAgents, agentSearch, selectedAgentOption]); const activeChannels = [ { id: 'web', name: 'Web del cliente', desc: 'Crea un snippet de widget para insertar el agente en la web del cliente.', color: '#38bdf8', icon: 'globe', }, { id: 'whatsapp', name: 'WhatsApp Meta Oficial', desc: 'Crea webhook oficial de WhatsApp Cloud API y conecta el número del cliente en Meta.', color: '#4ade80', icon: 'send', }, { id: 'telegram', name: 'Telegram', desc: 'Crea webhook y registra automáticamente el bot mediante Telegram Bot API.', color: '#c084fc', icon: 'chat', }, ]; const channelHelp = { web: { title: 'Web del cliente', text: 'Crea un token público y un snippet JavaScript. El widget vive en AgentForge y se inserta en la web del cliente.', result: 'Resultado: snippet + URL técnica de chat.', }, whatsapp: { title: 'WhatsApp Cloud API (Meta oficial)', text: 'Conecta Access Token, Phone Number ID y Verify Token de Meta para operar con el canal oficial del cliente.', result: 'Resultado: webhook oficial de Meta + envío/recepción por Graph API.', }, telegram: { title: 'Telegram', text: 'Crea un webhook público y lo registra en Telegram Bot API con el token del bot.', result: 'Resultado: webhook Telegram + bot conectado al agente.', }, }; const channelActionLabel = { web: 'Crear despliegue Web', whatsapp: 'Conectar Meta oficial', telegram: 'Conectar Telegram', }; const addOnOptions = [ { id: 'web-build', name: 'Web creada por AgentForge', desc: 'Diseño y entrega de web comercial con integración de agentes desde el día 1.' }, { id: 'onboarding', name: 'Onboarding asistido', desc: 'Acompañamiento para configurar fuentes, pruebas y primeras respuestas reales.' }, { id: 'training', name: 'Formación de equipo', desc: 'Sesión guiada para administradores, builders y usuarios finales.' }, ]; /** * Descarga una ficha técnica textual de la modalidad seleccionada. * @returns {void} */ const downloadTechSheet = () => { const mode = modes.find((m) => m.id === selected) || null; if (!mode) { notify('Selecciona una modalidad para descargar su ficha'); return; } const lines = [ `AgentForge · Ficha técnica`, `Modalidad: ${mode.name}`, `Tagline: ${mode.tagline}`, ``, `Descripción:`, mode.desc, ``, `Datos clave:`, `- Dónde viven los datos: ${mode.where}`, `- Tenancy: ${mode.tenancy}`, `- Puesta en marcha: ${mode.time}`, `- Operación: ${mode.mgmt}`, `- SLA: ${mode.sla}`, `- Modelo de precio: ${mode.price}`, ``, `Incluye:`, ...mode.includes.map((v) => `- ${v}`), ``, `No incluye:`, ...mode.excludes.map((v) => `- ${v}`), ``, `Regiones: ${mode.regions.join(', ')}`, ]; const blob = new Blob([lines.join('\n')], { type: 'text/plain;charset=utf-8;' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `ficha-${mode.id}.txt`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); notify(`Descarga iniciada: ${mode.name}`); }; return (

Implantación

contratación modalidades de despliegue
{/* Mis Despliegues */}
Canales operativos de despliegue
Estos son los canales que crean un despliegue real para cualquier agente seleccionado. Las modalidades de abajo son modelos de implantación comercial y arquitectura.
{lastCreatedDeployment && (
Despliegue creado correctamente
{lastCreatedDeployment.agent_name} · {lastCreatedDeployment.channel} · estado {lastCreatedDeployment.status}
{lastCreatedDeployment.webhook_url &&
Webhook: {lastCreatedDeployment.webhook_url}
} {lastCreatedDeployment.chat_url &&
Chat URL: {lastCreatedDeployment.chat_url}
} {lastCreatedDeployment.embed_snippet &&
Snippet: {lastCreatedDeployment.embed_snippet}
}
)}
{activeChannels.map((channel) => (
{channel.name} Operativo
{channel.desc}
))}
Mis Despliegues {deployments.length > 0 && {deployments.length}}
{!showDeploymentsList ? (
Lista de despliegues oculta para priorizar las modalidades de implantación. Puedes gestionarlos desde Mis agentes o abrir la lista aquí cuando lo necesites.
) : deployments.length === 0 ? (
No tienes despliegues activos. Crea uno para incrustar un agente en tu web, WhatsApp o Telegram.
) : (
{deployments.map((d) => (
{d.agent_name} {d.channel} #{d.id.slice(0,8)}
{d.channel_configured ? 'Configuración OK' : 'Config pendiente'} {d.status === 'active' ? 'Activo' : d.status} {d.channel === 'web' ? 'Web del cliente' : d.channel === 'whatsapp' ? 'WhatsApp Meta' : 'Telegram'}
{d.channel_config_note && (
{d.channel_config_note}
)}
CHECKLIST DESPLIEGUE
{(() => { const checks = [ { ok: !!d.channel_configured, label: 'Configuración del canal' }, { ok: d.channel === 'web' ? !!d.embed_snippet : !!d.webhook_url, label: d.channel === 'web' ? 'Snippet generado' : 'Webhook generado' }, { ok: d.status === 'active', label: 'Despliegue activo' }, ]; return (
{checks.map((c, idx) => (
{c.label}
))}
); })()}
{d.embed_snippet && (
{d.embed_snippet}
)} {d.webhook_url && (
{d.webhook_url}
)}
))}
)}
Modalidades de implantación comercial
Estas opciones definen dónde vive la plataforma, quién opera la infraestructura y qué servicios acompañan el proyecto.
Filtrar por nivel {['todas', 'starter', 'business', 'enterprise', 'partner'].map(t => ( ))} {filtered.length} modalidades
{filtered.map(m => (
setSelected(selected === m.id ? null : m.id)}>
{m.name}
{m.tagline}
{tiers[m.tier].lbl}
Solicitud comercial

{m.desc}

Dónde viven los datos{m.where}
Tenancy{m.tenancy}
Tiempo de puesta en marcha{m.time}
Quién opera{m.mgmt}
SLA{m.sla}
Modelo de precio{m.price}
{m.services && m.services.length > 0 && (
Servicios incluidos
{m.services.map((s, idx) => (
{s.name} {s.cost}
))}
)} {selected === m.id && (
Ideal para
    {m.ideal.map((x,i) =>
  • {x}
  • )}
Incluye
    {m.includes.map((x,i) =>
  • {x}
  • )}
No incluye
    {m.excludes.map((x,i) =>
  • {x}
  • )}
Regiones
{m.regions.map(r => {r})}
)}
))}
Servicios que acompañan toda implantación
{services.map(s => (
{s.name}
{s.desc}
{s.dur}
))}
Opciones adicionales de implantación
{addOnOptions.map((opt) => (
{opt.name}
{opt.desc}
))}
Solicitudes comerciales
{requests.length === 0 && ( )} {requests.map((r) => ( ))}
ID Origen Modalidad Nota Fecha
Todavía no hay solicitudes.
{String(r.id).slice(0, 8)} {r.source || 'deployment_page'} {r.mode_name || r.mode_id || 'General'} {r.note || '—'} {new Date(r.created_at).toLocaleString('es-ES')}
{showAssistant && (
setShowAssistant(false)}>
e.stopPropagation()}>

Asistente de implantación

{(() => { const rec = recommendMode(); return ( <>
Recomendación
{rec.name}
{rec.tagline}
); })()}
)} {showCompare && (
setShowCompare(false)}>
e.stopPropagation()}>

Comparador de modalidades

{modes.map((m) => ( ))}
ModalidadTiempoSLAOperaciónPrecio
{m.name}{m.time}{m.sla}{m.mgmt}{m.price}
)} {showSales && (
setShowSales(false)}>
e.stopPropagation()}>

Solicitud comercial creada

Hemos registrado tu solicitud de implantación{selected ? ` para ${modes.find((m) => m.id === selected)?.name || 'la modalidad seleccionada'}` : ''}. El equipo contactará contigo para definir alcance, cronograma y presupuesto.

)} {showPdf && (
setShowPdf(false)}>
e.stopPropagation()}>

Ficha técnica

{selected ? `Ficha técnica de ${modes.find((m) => m.id === selected)?.name || 'modalidad seleccionada'} lista para descarga.` : 'Selecciona una modalidad para ver su ficha técnica detallada.'}

)} {showNewDeploy && (
e.stopPropagation()}>

Nuevo despliegue

{deployFeedback && (
{deployFeedback.type === 'success' ? 'Despliegue creado correctamente' : 'No se pudo crear el despliegue'}
{deployFeedback.message}
{deployFeedback.deployment?.webhook_url && (
Webhook: {deployFeedback.deployment.webhook_url}
)} {deployFeedback.deployment?.channel === 'whatsapp' && deployFeedback.deployment?.webhook_url && (
Siguiente paso en Meta: pega este webhook como Callback URL en tu App de Meta, usa el mismo Verify Token indicado en el formulario y suscribe el evento messages.
)} {deployFeedback.deployment?.embed_snippet && (
Snippet: {deployFeedback.deployment.embed_snippet}
)}
)}
MODOS ACTIVOS DE DESPLIEGUE
Elige dónde vivirá la conversación del cliente. El agente se guarda en AgentForge.
Web · WhatsApp Meta · Telegram
{channelHelp[deployForm.channel]?.title}
{channelHelp[deployForm.channel]?.text}
{channelHelp[deployForm.channel]?.result}
{deployForm.channel === 'whatsapp' && (
Datos necesarios para WhatsApp Cloud API (Meta)
Usa credenciales oficiales de Meta Business: Access Token, Phone Number ID y Verify Token. Al crear el despliegue, AgentForge genera el webhook que debes registrar en Meta para recibir mensajes entrantes. No sirve un WhatsApp personal: necesitas WhatsApp Business Platform / Cloud API con número aprobado o conectado en Meta.
Tras crear el despliegue, copia el webhook generado en la tarjeta y configúralo en Meta App Dashboard > WhatsApp > Configuration.
)} {deployForm.channel === 'telegram' && ( )} {deployForm.channel === 'web' && ( )}