Spaces:
Sleeping
Sleeping
| import React, { useState, useEffect, useRef } from 'react'; | |
| import { Users, ShieldCheck, Bot, History, ClipboardList, Lock } from 'lucide-react'; | |
| import { cn } from '../lib/utils'; | |
| import { LOG_ENTRIES } from '../constants'; | |
| import { api, hasAdminKey } from '../api'; | |
| import type { LogEntry, Proposal } from '../types'; | |
| interface AgentStatus { | |
| name: string; | |
| version: string; | |
| status: string; | |
| progress: number; | |
| skill_count: number; | |
| ts: string; | |
| alert: boolean; | |
| model?: string; | |
| } | |
| export const BotConfig: React.FC = () => { | |
| const [proactiveAlerts, setProactiveAlerts] = useState(true); | |
| const [metaNotifications, setMetaNotifications] = useState(true); | |
| const [agents, setAgents] = useState<AgentStatus[]>([]); | |
| const [logs, setLogs] = useState<LogEntry[]>(LOG_ENTRIES); | |
| const [proposals, setProposals] = useState<Proposal[]>([]); | |
| const [hallazgos, setHallazgos] = useState<Proposal[]>([]); | |
| const teamTokenRef = useRef<HTMLInputElement>(null); | |
| const teamChatIdRef = useRef<HTMLInputElement>(null); | |
| const adminChatIdRef = useRef<HTMLInputElement>(null); | |
| useEffect(() => { | |
| api.getAgentsStatus().then(setAgents).catch(() => {}); | |
| api.getLogs().then((data: LogEntry[]) => { if (data?.length) setLogs(data); }).catch(() => {}); | |
| api.getProposals().then((data: { proposals: Proposal[]; hallazgos: Proposal[] }) => { | |
| if (data?.proposals?.length) setProposals(data.proposals); | |
| if (data?.hallazgos?.length) setHallazgos(data.hallazgos); | |
| }).catch(() => {}); | |
| }, []); | |
| const adminUnlocked = hasAdminKey(); | |
| const handleSaveBotConfig = async () => { | |
| // En modo demo (sin key) no se guarda: evita el prompt de admin al pasar por los inputs. | |
| if (!adminUnlocked) return; | |
| try { | |
| await api.saveTelegramConfig({ | |
| team_token: teamTokenRef.current?.value || '', | |
| team_chat_id: teamChatIdRef.current?.value || '', | |
| admin_chat_id: adminChatIdRef.current?.value || '', | |
| proactive_alerts: proactiveAlerts, | |
| meta_notifications: metaNotifications, | |
| xyz_threshold: 'alta', | |
| }); | |
| } catch {} | |
| }; | |
| const displayAgents = agents.length > 0 ? agents : [ | |
| { name: 'AgenteOrquestador', version: 'v2.4.12', status: 'Stable', ts: '', progress: 80, skill_count: 3, alert: false, model: 'meta-llama/Llama-3.2-1B-Instruct' }, | |
| { name: 'AgenteML', version: 'v4.0.0', status: 'Adjusting', ts: '', progress: 55, skill_count: 5, alert: true, model: 'Qwen/Qwen2.5-7B-Instruct' }, | |
| { name: 'AgenteMeta', version: 'v1.2.8', status: 'Active', ts: '', progress: 92, skill_count: 7, alert: false, model: 'meta-llama/Llama-3.1-8B-Instruct' }, | |
| { name: 'AgenteNotificador', version: 'v2.1.0', status: 'Stable', ts: '', progress: 72, skill_count: 2, alert: false, model: '' }, | |
| ]; | |
| return ( | |
| <div className="p-8 space-y-8"> | |
| {/* Agent Stats */} | |
| <section className="grid grid-cols-4 gap-4"> | |
| {displayAgents.map((agent, idx) => { | |
| const dark = idx === 2; // AgenteMeta siempre con fondo oscuro | |
| return ( | |
| <div | |
| key={agent.name} | |
| className={cn( | |
| "border-[0.5px] border-border-technical p-4 flex flex-col space-y-3", | |
| dark ? "bg-primary-industrial text-white" : "bg-surface-container" | |
| )} | |
| > | |
| <div className="flex justify-between items-start"> | |
| <div className="flex flex-col"> | |
| <span className="font-mono text-[10px] font-bold uppercase">{agent.name}</span> | |
| <span className="font-mono text-[9px] opacity-50">{agent.version}</span> | |
| {agent.model ? ( | |
| <span className="font-mono text-[8px] opacity-40 mt-0.5"> | |
| {agent.model.split('/').pop()?.replace('-Instruct', '') ?? agent.model} | |
| </span> | |
| ) : ( | |
| <span className="font-mono text-[8px] opacity-30 mt-0.5">sin LLM</span> | |
| )} | |
| </div> | |
| <span className={cn( | |
| "px-1.5 py-0.5 font-mono text-[9px] font-bold uppercase border-[0.5px]", | |
| dark ? "bg-tertiary-industrial/40 text-white border-white/30" : | |
| agent.alert ? "bg-secondary-industrial/10 text-secondary-industrial border-secondary-industrial/30" : | |
| "bg-tertiary-industrial/10 text-tertiary-light border-tertiary-light/30" | |
| )}> | |
| {agent.status} | |
| </span> | |
| </div> | |
| <div className="flex items-center justify-between font-mono text-[9px] opacity-70"> | |
| <span>TS: {agent.ts || '—'}</span> | |
| </div> | |
| <div className={cn("relative h-1 w-full mt-2", dark ? "bg-white/10" : "bg-surface-base")}> | |
| <div | |
| className={cn("absolute left-0 top-0 h-full", dark ? "bg-white" : "bg-primary-industrial")} | |
| style={{ width: `${agent.progress}%` }} | |
| /> | |
| <div className={cn("absolute left-[50%] top-1/2 -translate-y-1/2 w-[1px] h-3", dark ? "bg-white" : "bg-primary-industrial")} /> | |
| <div className={cn("absolute left-[80%] top-1/2 -translate-y-1/2 w-[1px] h-3", dark ? "bg-white" : "bg-primary-industrial")} /> | |
| </div> | |
| </div> | |
| ); | |
| })} | |
| </section> | |
| {/* Logs and Proposals */} | |
| <div className="grid grid-cols-2 gap-8 items-start"> | |
| {/* Cambios Aplicados */} | |
| <section className="bg-surface-container technical-border flex flex-col h-[500px]"> | |
| <div className="p-4 border-b-[0.5px] border-border-technical flex items-center justify-between"> | |
| <h2 className="font-mono text-xs font-bold uppercase tracking-widest flex items-center"> | |
| <History size={18} className="mr-2 text-primary-industrial" /> | |
| Cambios aplicados | |
| </h2> | |
| <span className="font-mono text-[10px] opacity-50 uppercase">Historial Reactivo</span> | |
| </div> | |
| <div className="flex-grow overflow-y-auto p-4 space-y-4"> | |
| {logs.map((log) => ( | |
| <div key={log.id} className="border-l-2 border-primary-industrial pl-4 py-2 space-y-1"> | |
| <p className="text-sm font-medium">{log.title}</p> | |
| <div className="flex items-center space-x-4 font-mono text-[10px] opacity-60"> | |
| <span>Hash: {log.hash}</span> | |
| <span>{log.timestamp}</span> | |
| </div> | |
| </div> | |
| ))} | |
| </div> | |
| <div className="p-4 border-t-[0.5px] border-border-technical text-center"> | |
| <button className="font-mono text-[10px] font-bold uppercase text-primary-industrial hover:underline"> | |
| Ver log completo | |
| </button> | |
| </div> | |
| </section> | |
| {/* Propuestas y Hallazgos */} | |
| <section className="bg-surface-container technical-border flex flex-col h-[500px]"> | |
| <div className="p-4 border-b-[0.5px] border-border-technical flex items-center justify-between"> | |
| <h2 className="font-mono text-xs font-bold uppercase tracking-widest flex items-center"> | |
| <ClipboardList size={18} className="mr-2 text-primary-industrial" /> | |
| Propuestas y Hallazgos | |
| </h2> | |
| <span className="font-mono text-[10px] text-primary-industrial font-bold uppercase">Solo bot admin</span> | |
| </div> | |
| <div className="flex-grow overflow-y-auto p-4 space-y-6"> | |
| {/* Propuestas proactivas */} | |
| {proposals.length > 0 && ( | |
| <div className="space-y-4"> | |
| <span className="font-mono text-[9px] font-bold uppercase text-slate-400 tracking-widest">Propuestas</span> | |
| {proposals.map((p) => ( | |
| <div key={p.id} className="border-[0.5px] border-border-technical bg-white p-4 space-y-3"> | |
| <div> | |
| <span className="font-mono text-[9px] font-bold uppercase text-slate-400">Propuesta #{p.seq}</span> | |
| <p className="text-xs font-bold mt-0.5">{p.title}</p> | |
| <p className="text-[11px] text-slate-600 mt-1">{p.description}</p> | |
| </div> | |
| <div className="bg-surface-base p-3"> | |
| <span className="font-mono text-[9px] uppercase font-bold opacity-50 block mb-1">Justificación técnica</span> | |
| <p className="text-[11px] leading-relaxed italic">{p.justification}</p> | |
| </div> | |
| <div className="pt-2 border-t border-border-technical flex items-center justify-between"> | |
| <span className={cn( | |
| "text-[9px] font-bold font-mono uppercase px-2 py-0.5 border", | |
| p.status === "aprobado" ? "text-green-700 border-green-300 bg-green-50" : | |
| p.status === "rechazado" ? "text-secondary-industrial border-red-200 bg-red-50" : | |
| "text-yellow-700 border-yellow-300 bg-yellow-50" | |
| )}> | |
| {p.status === "aprobado" ? "Aprobado" : p.status === "rechazado" ? "Rechazado" : "Pendiente"} | |
| </span> | |
| {(!p.status || p.status === "pendiente") && ( | |
| <p className="text-[10px] font-mono text-slate-400"> | |
| Bot: <span className="text-primary-industrial font-bold">/aprobar {p.id}</span> | |
| · <span className="text-secondary-industrial font-bold">/rechazar {p.id}</span> | |
| </p> | |
| )} | |
| </div> | |
| </div> | |
| ))} | |
| </div> | |
| )} | |
| {/* Hallazgos del sistema */} | |
| {hallazgos.length > 0 && ( | |
| <div className="space-y-4"> | |
| <span className="font-mono text-[9px] font-bold uppercase text-slate-400 tracking-widest">Hallazgos del sistema</span> | |
| {hallazgos.map((h) => ( | |
| <div key={h.id} className="border-[0.5px] border-blue-100 bg-blue-50/40 p-4 space-y-2"> | |
| <div> | |
| <span className="font-mono text-[9px] font-bold uppercase text-slate-400">Hallazgo #{h.seq}</span> | |
| <p className="text-xs font-bold mt-0.5 text-primary-industrial">{h.title}</p> | |
| <p className="text-[11px] text-slate-600 mt-1">{h.description}</p> | |
| </div> | |
| </div> | |
| ))} | |
| </div> | |
| )} | |
| {proposals.length === 0 && hallazgos.length === 0 && ( | |
| <p className="text-[11px] text-slate-400 italic text-center pt-8">Sin propuestas ni hallazgos registrados.</p> | |
| )} | |
| </div> | |
| </section> | |
| </div> | |
| {/* Bot Configuration */} | |
| <section className="bg-surface-container technical-border"> | |
| <div className="p-4 border-b-[0.5px] border-border-technical"> | |
| <h2 className="font-mono text-xs font-bold uppercase tracking-widest flex items-center text-primary-industrial"> | |
| <Bot size={18} className="mr-2" /> | |
| Configuración del Bot | |
| </h2> | |
| </div> | |
| {!adminUnlocked && ( | |
| <div className="px-4 py-2 border-b-[0.5px] border-border-technical bg-surface-base flex items-center gap-2"> | |
| <Lock size={12} className="text-yellow-600" /> | |
| <span className="text-[10px] font-mono uppercase text-slate-500"> | |
| Solo lectura en modo demo — la configuración requiere API key de administrador | |
| </span> | |
| </div> | |
| )} | |
| <div className="grid grid-cols-2 divide-x-[0.5px] divide-border-technical"> | |
| {/* Canal Equipo */} | |
| <div className="p-6 space-y-6"> | |
| <div className="flex items-center space-x-2 mb-4"> | |
| <Users size={18} className="text-primary-industrial" /> | |
| <span className="text-xs font-bold uppercase tracking-tight">Canal Equipo</span> | |
| </div> | |
| <div className="grid grid-cols-2 gap-4"> | |
| <div className="flex flex-col space-y-1"> | |
| <label className="font-mono text-[9px] uppercase font-bold opacity-50">Token</label> | |
| <input | |
| ref={teamTokenRef} | |
| className="border-[0.5px] border-border-technical bg-white px-3 py-2 text-xs font-mono focus:ring-0 focus:border-primary-industrial outline-none disabled:opacity-50" | |
| type="password" | |
| placeholder="••••••••••••" | |
| disabled={!adminUnlocked} | |
| onBlur={handleSaveBotConfig} | |
| /> | |
| </div> | |
| <div className="flex flex-col space-y-1"> | |
| <label className="font-mono text-[9px] uppercase font-bold opacity-50">Chat ID</label> | |
| <input | |
| ref={teamChatIdRef} | |
| className="border-[0.5px] border-border-technical bg-white px-3 py-2 text-xs font-mono focus:ring-0 focus:border-primary-industrial outline-none disabled:opacity-50" | |
| type="text" | |
| placeholder="-100XXXXXXXXX" | |
| disabled={!adminUnlocked} | |
| onBlur={handleSaveBotConfig} | |
| /> | |
| </div> | |
| </div> | |
| <div className="flex items-center justify-between"> | |
| <span className="text-xs">Alertas Proactivas</span> | |
| <button | |
| onClick={() => setProactiveAlerts(!proactiveAlerts)} | |
| className={cn( | |
| "w-8 h-4 relative transition-colors duration-200", | |
| proactiveAlerts ? "bg-tertiary-industrial" : "bg-slate-300" | |
| )} | |
| > | |
| <div className={cn( | |
| "absolute top-0.5 w-3 h-3 bg-white transition-all duration-200", | |
| proactiveAlerts ? "right-0.5" : "left-0.5" | |
| )} /> | |
| </button> | |
| </div> | |
| <div className="flex flex-col space-y-1"> | |
| <label className="font-mono text-[9px] uppercase font-bold opacity-50">Umbral XYZ Selector</label> | |
| <select className="border-[0.5px] border-border-technical bg-white px-3 py-2 text-xs font-mono focus:ring-0 focus:border-primary-industrial outline-none appearance-none cursor-pointer"> | |
| <option>Alta Varianza (>15%)</option> | |
| <option>Media Varianza (>8%)</option> | |
| <option>Solo Críticos</option> | |
| </select> | |
| </div> | |
| </div> | |
| {/* Canal Admin */} | |
| <div className="p-6 space-y-6"> | |
| <div className="flex items-center space-x-2 mb-4"> | |
| <ShieldCheck size={18} className="text-primary-industrial" /> | |
| <span className="text-xs font-bold uppercase tracking-tight">Canal Admin</span> | |
| </div> | |
| <div className="flex flex-col space-y-1"> | |
| <label className="font-mono text-[9px] uppercase font-bold opacity-50">Chat ID Admin</label> | |
| <input | |
| ref={adminChatIdRef} | |
| className="border-[0.5px] border-border-technical bg-white px-3 py-2 text-xs font-mono focus:ring-0 focus:border-primary-industrial outline-none disabled:opacity-50" | |
| type="text" | |
| placeholder="XXXXXXXXX" | |
| disabled={!adminUnlocked} | |
| onBlur={handleSaveBotConfig} | |
| /> | |
| </div> | |
| <div className="flex items-center justify-between"> | |
| <span className="text-xs">Notificaciones AgenteMeta</span> | |
| <button | |
| onClick={() => setMetaNotifications(!metaNotifications)} | |
| className={cn( | |
| "w-8 h-4 relative transition-colors duration-200", | |
| metaNotifications ? "bg-tertiary-industrial" : "bg-slate-300" | |
| )} | |
| > | |
| <div className={cn( | |
| "absolute top-0.5 w-3 h-3 bg-white transition-all duration-200", | |
| metaNotifications ? "right-0.5" : "left-0.5" | |
| )} /> | |
| </button> | |
| </div> | |
| <div className="pt-4 border-t-[0.5px] border-border-technical border-dashed"> | |
| <div className="flex items-center space-x-2 text-[10px] font-mono"> | |
| <span className="w-2 h-2 bg-tertiary-light rounded-full animate-pulse"></span> | |
| <span className="uppercase font-bold text-tertiary-light">Conectado</span> | |
| <span className="opacity-50">· última actividad hace 2h</span> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </section> | |
| </div> | |
| ); | |
| }; | |