DDMRP-MONITOR / src /components /BotConfig.tsx
dyandun's picture
Opening this demo for the public
dbe0232
Raw
History Blame Contribute Delete
16.9 kB
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:&nbsp;<span className="text-primary-industrial font-bold">/aprobar {p.id}</span>
&nbsp;·&nbsp;<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 (&gt;15%)</option>
<option>Media Varianza (&gt;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>
);
};