// AGENTFORGE-IA — Biblioteca const Library = ({ data, openAgent, setRoute, onDeleteCustomAgent, onRunAgent, onEditAgent, onDeployAgent }) => { const me = (() => { try { return JSON.parse(sessionStorage.getItem('af_user')); } catch { return null; } })(); const cats = data.categories; const [rows, setRows] = React.useState([]); const [loadingRows, setLoadingRows] = React.useState(true); const [rowsError, setRowsError] = React.useState(''); const [busyCancelId, setBusyCancelId] = React.useState(''); const [personalizeOpen, setPersonalizeOpen] = React.useState(false); const [personalizeRow, setPersonalizeRow] = React.useState(null); const [personalizeForm, setPersonalizeForm] = React.useState({ empresa: '', descripcion: '', productos: '', audiencia: '', tono: 'profesional', faqs: '', instrucciones: '', }); const [personalizeSaving, setPersonalizeSaving] = React.useState(false); const [renameOpen, setRenameOpen] = React.useState(false); const [renameRow, setRenameRow] = React.useState(null); const [renameForm, setRenameForm] = React.useState({ name: '', tagline: '' }); const [renameSaving, setRenameSaving] = React.useState(false); const [openRowMenu, setOpenRowMenu] = React.useState(''); const [deploymentFilter, setDeploymentFilter] = React.useState('all'); const [menuPos, setMenuPos] = React.useState({ x: 0, y: 0 }); const menuRef = React.useRef(null); /** * Devuelve una identidad estable para comparar filas sin usar el nombre. * @param {Record} row * @returns {string} */ const rowStableKey = React.useCallback((row) => { const dbId = String(row?.agentDbId || row?.agent?.agentDbId || '').trim(); if (dbId) return `db:${dbId}`; const slug = String(row?.id || row?.agent?.id || row?.agent?.slug || '').trim(); return slug ? `slug:${slug}` : ''; }, []); /** * Actualiza caches locales para que el nombre sea coherente al volver de ruta. * @param {string} agentId * @param {string} agentSlug * @param {string} name * @param {string} tagline * @returns {void} */ const persistLocalRename = React.useCallback((agentId, agentSlug, name, tagline) => { const matchesAgent = (agent) => ( String(agent?.agentDbId || '') === String(agentId || '') || String(agent?.id || '') === String(agentSlug || '') || String(agent?.slug || '') === String(agentSlug || '') ); try { const raw = localStorage.getItem('af_custom_agents_v1') || '[]'; const list = JSON.parse(raw); if (Array.isArray(list)) { localStorage.setItem('af_custom_agents_v1', JSON.stringify(list.map((agent) => ( matchesAgent(agent) ? { ...agent, name, tagline: tagline || agent.tagline || '', updatedAt: Date.now() } : agent )))); } } catch (_) {} try { const latest = JSON.parse(localStorage.getItem('af_last_created_agent') || 'null'); if (latest && matchesAgent(latest)) { localStorage.setItem('af_last_created_agent', JSON.stringify({ ...latest, name, tagline: tagline || latest.tagline || '', updatedAt: Date.now(), })); } } catch (_) {} }, []); /** * Carga agentes instalados del usuario autenticado. * @returns {Promise} */ const loadPurchasedAgents = React.useCallback(async () => { const token = sessionStorage.getItem('af_token') || ''; if (!token) { setRows([]); setLoadingRows(false); return; } setLoadingRows(true); setRowsError(''); try { const res = await fetch('/api/v1/purchases/my-agents', { headers: { Authorization: 'Bearer ' + token }, }); if (!res.ok) { let reason = `HTTP ${res.status}`; try { const payload = await res.json(); reason = payload?.detail || reason; } catch (_) {} setRowsError(`No se pudieron cargar tus agentes instalados (${reason}).`); setRows([]); return; } const purchased = await res.json(); const normalized = (Array.isArray(purchased) ? purchased : []).map((p, idx) => { const agent = data.agents.find((a) => a.id === p.agent_slug); const cat = agent ? cats.find((c) => c.id === agent.cat) : null; const visibleAgent = { ...(agent || {}), id: p.agent_slug || String(p.agent_id), name: p.agent_name || agent?.name || 'Agente', tagline: p.agent_tagline || agent?.tagline || '', baseName: p.base_agent_name || agent?.name || '', color: agent?.color || '#ff5b1f', verified: !!agent?.verified, }; return { id: p.agent_slug || p.agent_id, agentDbId: p.agent_id || '', deployment: p.status === 'active' ? 'producción' : p.status || 'activo', region: 'eu-west', runs7d: 0, success: 1.0, p95: 0, owner: (me?.full_name || me?.email || 'U').slice(0, 2).toUpperCase(), lastRun: '—', cost7d: 0, agent: visibleAgent, cat, _idx: idx, }; }); setRows(normalized); setRowsError(''); } catch (_) { setRowsError('Error de red al cargar la biblioteca.'); setRows([]); } finally { setLoadingRows(false); } }, [cats, data.agents, me?.email, me?.full_name]); const loadPersonalization = React.useCallback((agentId) => { try { const raw = localStorage.getItem('af_agent_ctx_' + agentId); if (!raw) return null; return JSON.parse(raw); } catch (_) { return null; } }, []); const savePersonalization = React.useCallback(async (agentId, form) => { try { localStorage.setItem('af_agent_ctx_' + agentId, JSON.stringify(form)); window.afNotify && window.afNotify('Contexto del agente guardado'); } catch (_) { window.afNotify && window.afNotify('No se pudo guardar el contexto'); } }, []); const openRenameAgent = React.useCallback((row) => { setRenameRow(row); setRenameForm({ name: row?.agent?.name || '', tagline: row?.agent?.tagline || '', }); setRenameOpen(true); }, []); const saveRenameAgent = React.useCallback(async () => { const token = sessionStorage.getItem('af_token') || ''; const name = renameForm.name.trim(); const agentId = renameRow?.agentDbId || renameRow?.agent?.agentDbId || ''; const agentSlug = renameRow?.id || renameRow?.agent?.id || ''; const tagline = renameForm.tagline.trim(); if (!name) { window.afNotify && window.afNotify('El nombre del agente es obligatorio'); return; } setRenameSaving(true); try { let updated = null; if (agentId && token) { let res = await fetch('/api/v1/purchases/' + agentId, { method: 'PATCH', headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + token }, body: JSON.stringify({ display_name: name, display_tagline: tagline || null, }), }); if (!res.ok && (res.status === 403 || res.status === 404)) { res = await fetch('/api/v1/agents/' + agentId, { method: 'PATCH', headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + token }, body: JSON.stringify({ name, tagline: tagline || null, }), }); } if (!res.ok) { window.afNotify && window.afNotify('No se pudo renombrar el agente'); return; } updated = await res.json().catch(() => null); } const finalName = updated?.agent_name || updated?.name || name; const finalTagline = updated?.agent_tagline || updated?.tagline || tagline; persistLocalRename(agentId, agentSlug, finalName, finalTagline); try { window.dispatchEvent(new CustomEvent('af:agent:renamed', { detail: { agentId: String(agentId || ''), agentSlug: String(agentSlug || ''), agentName: finalName, agentTagline: finalTagline, }, })); } catch (_) {} try { window.dispatchEvent(new CustomEvent('af:catalog:refresh')); } catch (_) {} window.afNotify && window.afNotify('Nombre del agente actualizado'); setRenameOpen(false); setRenameRow(null); await loadPurchasedAgents(); } catch (_) { window.afNotify && window.afNotify('Error de red al renombrar'); } finally { setRenameSaving(false); } }, [loadPurchasedAgents, persistLocalRename, renameForm.name, renameForm.tagline, renameRow]); /** * Elimina un agente local del espacio del usuario. * @param {string} localAgentId * @returns {void} */ const removeLocalAgent = React.useCallback((localAgentId) => { if (!localAgentId) return; const ok = window.confirm('¿Eliminar este agente local de tu espacio?'); if (!ok) return; if (typeof onDeleteCustomAgent === 'function') { onDeleteCustomAgent(localAgentId); } const last = localStorage.getItem('af_last_created_agent'); if (last) { try { const parsed = JSON.parse(last); if (parsed?.id === localAgentId) { localStorage.removeItem('af_last_created_agent'); } } catch (_) {} } window.afNotify && window.afNotify('Agente local eliminado'); }, [onDeleteCustomAgent]); /** * Elimina un agente instalado del espacio del usuario. * @param {string} agentDbId UUID del agente en base de datos. * @returns {Promise} */ const cancelPurchasedAgent = React.useCallback(async (agentDbId) => { if (!agentDbId) return; const ok = window.confirm('¿Eliminar este agente de tu espacio? Se desactivarán también sus despliegues. Podrás volver a comprarlo cuando quieras.'); if (!ok) return; setBusyCancelId(agentDbId); try { const token = sessionStorage.getItem('af_token') || ''; const res = await fetch('/api/v1/purchases/' + agentDbId, { method: 'DELETE', headers: { Authorization: 'Bearer ' + token }, }); if (!res.ok) { let reason = `HTTP ${res.status}`; try { const payload = await res.json(); reason = payload?.detail || reason; } catch (_) {} window.afNotify && window.afNotify(`No se pudo eliminar el agente (${reason})`); return; } window.afNotify && window.afNotify('Agente eliminado correctamente'); await loadPurchasedAgents(); } catch (_) { window.afNotify && window.afNotify('Error de red al eliminar el agente'); } finally { setBusyCancelId(''); } }, [loadPurchasedAgents]); React.useEffect(() => { loadPurchasedAgents(); }, [loadPurchasedAgents]); React.useEffect(() => { const refresh = () => loadPurchasedAgents(); window.addEventListener('af:library:refresh', refresh); return () => window.removeEventListener('af:library:refresh', refresh); }, [loadPurchasedAgents]); React.useEffect(() => { const applyRename = (event) => { const detail = event?.detail || {}; const agentId = String(detail.agentId || ''); const agentSlug = String(detail.agentSlug || ''); const agentName = String(detail.agentName || '').trim(); const agentTagline = String(detail.agentTagline || '').trim(); if (!agentName || (!agentId && !agentSlug)) return; setRows((prev) => prev.map((row) => { const sameAgent = String(row.agentDbId || '') === agentId || String(row.id || '') === agentSlug || String(row.agent?.id || '') === agentSlug; if (!sameAgent) return row; return { ...row, agent: { ...(row.agent || {}), name: agentName, tagline: agentTagline || row.agent?.tagline || '', }, }; })); }; window.addEventListener('af:agent:renamed', applyRename); return () => window.removeEventListener('af:agent:renamed', applyRename); }, []); React.useEffect(() => { if (!openRowMenu) return; const onDocClick = (e) => { if (menuRef.current && !menuRef.current.contains(e.target)) { setOpenRowMenu(''); } }; document.addEventListener('mousedown', onDocClick); return () => document.removeEventListener('mousedown', onDocClick); }, [openRowMenu]); React.useLayoutEffect(() => { if (!openRowMenu || !menuRef.current) return; const rect = menuRef.current.getBoundingClientRect(); const margin = 10; let nextX = menuPos.x; let nextY = menuPos.y; if (rect.right > window.innerWidth - margin) { nextX -= (rect.right - (window.innerWidth - margin)); } if (rect.left < margin) { nextX += (margin - rect.left); } if (rect.bottom > window.innerHeight - margin) { nextY -= (rect.bottom - (window.innerHeight - margin)); } if (rect.top < margin) { nextY += (margin - rect.top); } if (nextX !== menuPos.x || nextY !== menuPos.y) { setMenuPos({ x: nextX, y: nextY }); } }, [openRowMenu, menuPos.x, menuPos.y]); // "Mis agentes" debe representar solo instalaciones activas del backend. // Los borradores/locales viven en Editor/Marketplace para evitar reapariciones fantasma. const allRows = Array.from( rows.reduce((acc, row) => { const key = rowStableKey(row); if (!key) return acc; const current = acc.get(key); if (!current || (!current.agentDbId && row.agentDbId)) { acc.set(key, row); } return acc; }, new Map()).values() ); const visibleRows = allRows.filter((r) => { if (deploymentFilter === 'all') return true; if (deploymentFilter === 'live') return r.deployment === 'producción' || r.deployment === 'active'; if (deploymentFilter === 'draft') return r.deployment === 'borrador' || r.deployment === 'draft'; if (deploymentFilter === 'staging') return r.deployment === 'staging'; return true; }); const totalRuns = visibleRows.reduce((s, r) => s + (r.runs7d || 0), 0); const totalCost = visibleRows.reduce((s, r) => s + (r.cost7d || 0), 0); const liveCount = visibleRows.filter(r => r.deployment === 'producción' || r.deployment === 'active').length; const avgSuccess = visibleRows.length > 0 ? visibleRows.reduce((s, r) => s + (r.success || 0), 0) / visibleRows.length : 0; const fmt = n => n.toLocaleString('es-ES'); /** * Abre el playground con el agente seleccionado. * @param {string} agentId * @returns {void} */ const openPlaygroundFor = (agentId) => { if (typeof onRunAgent === 'function') { onRunAgent(agentId); return; } setRoute('playground'); }; /** * Exporta la tabla actual de agentes a CSV. * @returns {void} */ const exportLibraryCsv = () => { const header = ['agente', 'estado', 'region', 'ejec_7d', 'exito', 'latencia_p95', 'propietario', 'ultima_ejec']; const rowsCsv = allRows.map((r) => [ r.agent?.name || r.id, r.deployment, r.region, String(r.runs7d || 0), r.runs7d > 0 ? `${(r.success * 100).toFixed(1)}%` : 'N/D', r.p95 ? `${r.p95} ms` : 'N/D', r.owner, r.lastRun || 'N/D', ]); const csv = [header, ...rowsCsv] .map((line) => line.map((v) => `"${String(v).replaceAll('"', '""')}"`).join(',')) .join('\n'); const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = 'agentforge-mis-agentes.csv'; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); window.afNotify && window.afNotify('Exportación de Mis agentes completada'); }; const Spark = ({ color, seed = 1 }) => { const pts = Array.from({ length: 24 }, (_, i) => { const v = 0.4 + 0.6 * Math.abs(Math.sin(i * 0.6 + seed) * Math.cos(i * 0.3 + seed * 1.7)); return [i * (100/23), 28 - v * 22]; }); const d = 'M ' + pts.map(p => p.join(',')).join(' L '); return ( ); }; return (

Mis agentes

espacio {me?.name || 'Mi espacio'} / producción
Despliegues activos
{liveCount} / {visibleRows.length}
estado actual
Ejecuciones · últimos 7 d
{(totalRuns/1000).toFixed(1).replace('.',',')} k
agregado visible
Tasa de éxito media
{allRows.length ? (avgSuccess*100).toFixed(1).replace('.',',') + ' %' : 'N/D'}
promedio actual
Gasto · últimos 7 d
{totalCost.toFixed(2).replace('.',',')} €
coste acumulado
Agentes desplegados· {visibleRows.length} visibles
{rowsError && rows.length === 0 && (
{rowsError}
)}
{loadingRows && ( )} {!loadingRows && visibleRows.length === 0 && ( )} {visibleRows.map(r => ( openPlaygroundFor(r.id)}> ))}
Agente Estado Región Ejec. · 7 d Éxito Latencia p95 Propietario Última ejec. Acciones
Cargando agentes instalados...
No hay agentes para el filtro seleccionado.
{r.agent.name} {r.agent.verified && }
{r.id} · {r.cat?.name || 'Sin categoría'}
{r.deployment} {r.region} {fmt(r.runs7d)}
0.97 ? 'var(--ok)' : r.success > 0.93 ? 'var(--warn)' : 'var(--bad)' }} />
{r.runs7d > 0 ? (r.success*100).toFixed(1).replace('.',',') + ' %' : 'N/D'}
{r.p95 ? r.p95 + ' ms' : '—'}
{r.owner}
{r.lastRun}
Actividad reciente
{visibleRows.length > 0 ? 'La actividad reciente se alimentará con ejecuciones reales de tus agentes.' : 'Sin agentes activos, no hay actividad reciente todavía.'}
{renameOpen && (
{ if (e.target === e.currentTarget) setRenameOpen(false); }} >

Renombrar agente

Este nombre se usará en Mis agentes, Editor y despliegues.