// AGENTFORGE-IA — Pruebas (chat operativo por agente) const Playground = ({ data, activeAgentId, setRoute }) => { const [threadId, setThreadId] = React.useState('t1'); const [selectedAgentId, setSelectedAgentId] = React.useState(activeAgentId || ''); const [input, setInput] = React.useState(''); const [sending, setSending] = React.useState(false); const [threadMessages, setThreadMessages] = React.useState({}); const [threadTrace, setThreadTrace] = React.useState({}); const [threadStats, setThreadStats] = React.useState({}); const notify = (msg) => window.afNotify && window.afNotify(msg); const activeAgent = React.useMemo(() => { const source = Array.isArray(data.agents) ? data.agents : []; if (!selectedAgentId) return null; return source.find((a) => String(a.id) === String(selectedAgentId)) || null; }, [data.agents, selectedAgentId]); const activeCat = React.useMemo(() => { if (!activeAgent) return null; return (data.categories || []).find((c) => c.id === activeAgent.cat) || null; }, [data.categories, activeAgent]); const [threads, setThreads] = React.useState([{ id: 't1', name: 'Conversación actual', when: 'ahora', snip: 'Hilo activo' }]); const bootMessage = React.useMemo(() => { if (!activeAgent) { return 'Selecciona un agente para iniciar la prueba. Puedes elegir uno del Marketplace o de Mis agentes.'; } return `Prueba lista para ${activeAgent.name}. Escribe tu caso y pulsa Enter para ejecutar.`; }, [activeAgent]); const messages = threadMessages[threadId] || [{ id: `boot_${threadId}`, who: 'agent', kind: 'text', text: bootMessage }]; const trace = threadTrace[threadId] || []; const stats = threadStats[threadId] || { runId: '-', latencyMs: 0, tokens: 0 }; /** * Actualiza mensajes del hilo activo. * @param {Array<{id:string,who:string,kind:string,text:string}> | ((prev: Array<{id:string,who:string,kind:string,text:string}>) => Array<{id:string,who:string,kind:string,text:string}>)} updater * @returns {void} */ const updateCurrentThreadMessages = (updater) => { setThreadMessages((prev) => { const current = prev[threadId] || [{ id: `boot_${threadId}`, who: 'agent', kind: 'text', text: bootMessage }]; const next = typeof updater === 'function' ? updater(current) : updater; return { ...prev, [threadId]: next }; }); }; React.useEffect(() => { if (!activeAgentId) return; setSelectedAgentId(String(activeAgentId)); }, [activeAgentId]); React.useEffect(() => { setThreads([{ id: 't1', name: 'Conversación actual', when: 'ahora', snip: 'Hilo activo' }]); setThreadId('t1'); setThreadMessages({ t1: [{ id: crypto.randomUUID(), who: 'agent', kind: 'text', text: bootMessage }], }); setThreadTrace({ t1: [] }); setThreadStats({ t1: { runId: '-', latencyMs: 0, tokens: 0 } }); }, [activeAgent?.id, bootMessage]); /** * Ejecuta una iteración de prueba backend del agente seleccionado. * @param {string} prompt * @returns {Promise} */ const runAgent = async (prompt) => { const cleanPrompt = String(prompt || '').trim(); if (!cleanPrompt || sending || !activeAgent) return; setSending(true); const userMessage = { id: crypto.randomUUID(), who: 'user', kind: 'text', text: cleanPrompt }; updateCurrentThreadMessages((prev) => [...prev, userMessage]); const baseMessages = messages .filter((m) => m && m.kind === 'text' && typeof m.text === 'string' && m.text.trim()) .filter((m) => !String(m.text).startsWith('Prueba lista para ')); const historyPayload = baseMessages .map((m) => ({ role: m.who === 'user' ? 'user' : 'assistant', content: String(m.text), })) .slice(-24); historyPayload.push({ role: 'user', content: cleanPrompt }); try { const token = sessionStorage.getItem('af_token') || ''; if (!token) { throw new Error('Sesión no iniciada'); } const res = await fetch('/api/v1/integrations/external-agents/test-run', { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + token, }, body: JSON.stringify({ agent_id: activeAgent.id, agent_name: activeAgent.name, prompt: cleanPrompt, thread_id: threadId, history: historyPayload, }), }); if (!res.ok) { let reason = `HTTP ${res.status}`; try { const payload = await res.json(); reason = payload?.detail || reason; } catch (_) { const text = await res.text(); if (text) reason = text; } throw new Error(reason); } const payload = await res.json(); setThreadTrace((prev) => ({ ...prev, [threadId]: (Array.isArray(payload.trace) ? payload.trace : []) })); const latencyMs = Number(payload.latency_ms || 0); const tokensUsed = Number(payload.tokens || 0); setThreadStats((prev) => ({ ...prev, [threadId]: { runId: payload.run_id || '-', latencyMs, tokens: tokensUsed, }, })); updateCurrentThreadMessages((prev) => [ ...prev, { id: crypto.randomUUID(), who: 'agent', kind: 'text', text: String(payload.assistant_message || 'Sin respuesta del agente.'), }, ]); // Log run fire-and-forget fetch('/api/v1/runs/log', { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + token }, body: JSON.stringify({ agent_name: activeAgent.name, status: 'ok', trigger_type: 'playground', duration_ms: latencyMs || null, tokens_used: tokensUsed, cost_eur: parseFloat((tokensUsed * 0.000003).toFixed(4)), }), }).catch(() => {}); } catch (err) { const msg = err instanceof Error ? err.message : 'Error desconocido'; updateCurrentThreadMessages((prev) => [ ...prev, { id: crypto.randomUUID(), who: 'agent', kind: 'text', text: `Error ejecutando la prueba del agente: ${msg}`, }, ]); // Log failed run fire-and-forget fetch('/api/v1/runs/log', { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + (sessionStorage.getItem('af_token') || '') }, body: JSON.stringify({ agent_name: activeAgent?.name || 'Desconocido', status: 'error', trigger_type: 'playground', error_message: msg.slice(0, 500), }), }).catch(() => {}); notify('Error en prueba del agente'); } finally { setSending(false); } }; /** * Maneja el envío por teclado desde el textarea. * @param {React.KeyboardEvent} e */ const onInputKeyDown = (e) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); const payload = input.trim(); if (!payload) return; setInput(''); runAgent(payload); } }; /** * Crea un nuevo hilo de conversación. * @returns {void} */ const createThread = () => { const next = `t${Date.now()}`; setThreads((prev) => [ { id: next, name: `Conversación ${prev.length + 1}`, when: 'ahora', snip: 'Nuevo hilo' }, ...prev, ]); setThreadId(next); setThreadMessages((prev) => ({ ...prev, [next]: [ { id: crypto.randomUUID(), who: 'agent', kind: 'text', text: `Nuevo hilo creado para ${activeAgent?.name || 'el agente seleccionado'}.`, }, ], })); setThreadTrace((prev) => ({ ...prev, [next]: [] })); setThreadStats((prev) => ({ ...prev, [next]: { runId: '-', latencyMs: 0, tokens: 0 } })); }; /** * Copia la traza actual al portapapeles. * @returns {Promise} */ const copyTrace = async () => { const payload = { runId: stats.runId, latencyMs: stats.latencyMs, tokens: stats.tokens, trace, }; try { await navigator.clipboard.writeText(JSON.stringify(payload, null, 2)); notify('Traza copiada'); } catch (_) { notify('No se pudo copiar la traza'); } }; return (
Conversaciones
{threads.map((t) => (
setThreadId(t.id)}>
{t.name}
{t.snip}
))}
{activeAgent?.name || 'Agente'}
{activeCat?.name || 'general'} · pruebas operativas
{messages.map((m) => { if (m.who === 'user') return
{m.text}
; return (

{m.text}

); })} {sending && (

 Procesando caso…

)}