186 lines
7.6 KiB
TypeScript

import { useEffect, useState, useCallback } from 'react';
import type { ProjectSummary, AgentStatus, AuditLog } from '../api/client';
import { api } from '../api/client';
import { useWebSocket } from '../api/useWebSocket';
const STATUS_COLORS: Record<string, string> = {
COMPLETED: 'badge-completed',
FAILED: 'badge-failed',
PAUSED: 'badge-paused',
};
function getStatusBadge(status: string) {
if (status.endsWith('_RUNNING')) return 'badge-running';
if (status.startsWith('AWAITING_')) return 'badge-awaiting';
return STATUS_COLORS[status] || 'badge-awaiting';
}
const STATUS_ICONS: Record<string, string> = {
COMPLETED: '✅', FAILED: '❌', PAUSED: '⏸️',
};
function getStatusIcon(s: string) {
if (s.endsWith('_RUNNING')) return '⚡';
if (s.startsWith('AWAITING_')) return '⏳';
return STATUS_ICONS[s] || '❓';
}
export default function DashboardPage() {
const [projects, setProjects] = useState<ProjectSummary[]>([]);
const [agents, setAgents] = useState<AgentStatus[]>([]);
const [logs, setLogs] = useState<AuditLog[]>([]);
const [loading, setLoading] = useState(true);
const fetchAll = useCallback(async () => {
try {
const [p, a, l] = await Promise.all([
api.listProjects(),
api.listAgents(),
api.listLogs({ limit: '10' }),
]);
setProjects(p); setAgents(a); setLogs(l);
} catch { /* ignore */ }
setLoading(false);
}, []);
useEffect(() => { fetchAll(); }, [fetchAll]);
const { connected } = useWebSocket(useCallback((msg) => {
if (msg.type === 'project_update' || msg.type === 'agent_status') fetchAll();
}, [fetchAll]));
const active = projects.filter(p => !['COMPLETED', 'FAILED'].includes(p.status));
const completed = projects.filter(p => p.status === 'COMPLETED');
const runningAgents = agents.filter(a => a.current_status === 'running');
if (loading) {
return (
<div className="flex items-center justify-center h-64">
<div className="text-fox-500 animate-spin-slow text-4xl">🦊</div>
</div>
);
}
return (
<div className="space-y-6 animate-fade-in">
{/* Connection indicator */}
<div className="flex items-center gap-2 text-xs text-gray-500">
<span className={`w-2 h-2 rounded-full ${connected ? 'bg-green-400' : 'bg-red-400'}`}></span>
{connected ? 'Connecté en temps réel' : 'Déconnecté — reconnexion...'}
</div>
{/* Stats Cards */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
<StatCard icon="📋" label="Projets actifs" value={active.length} accent="fox" />
<StatCard icon="✅" label="Projets terminés" value={completed.length} accent="green" />
<StatCard icon="🤖" label="Agents en cours" value={runningAgents.length} accent="blue" />
<StatCard icon="📊" label="Total projets" value={projects.length} accent="purple" />
</div>
{/* Active Projects */}
<div className="glass-card p-6">
<h2 className="text-lg font-bold text-white mb-4 flex items-center gap-2">
<span></span> Projets en cours
</h2>
{active.length === 0 ? (
<p className="text-gray-500 text-sm">Aucun projet actif</p>
) : (
<div className="space-y-3">
{active.map((p) => (
<div key={p.id} className="flex items-center justify-between p-3 rounded-xl bg-surface-800/50 hover:bg-surface-700/50 transition-all">
<div>
<span className="text-white font-semibold text-sm">{p.name}</span>
<div className="flex items-center gap-2 mt-1">
<span className={`badge ${getStatusBadge(p.status)}`}>
{getStatusIcon(p.status)} {p.status.replace(/_/g, ' ')}
</span>
<span className="text-xs text-gray-500">{p.workflow_type.replace(/_/g, ' ')}</span>
</div>
</div>
<div className="text-right">
<div className="text-sm text-gray-400">{p.tasks_done}/{p.task_count} tâches</div>
<div className="w-24 h-1.5 bg-surface-700 rounded-full mt-1 overflow-hidden">
<div
className="h-full bg-gradient-to-r from-fox-600 to-fox-400 rounded-full transition-all duration-500"
style={{ width: `${p.task_count > 0 ? (p.tasks_done / p.task_count) * 100 : 0}%` }}
></div>
</div>
</div>
</div>
))}
</div>
)}
</div>
{/* Agents + Recent Logs */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
{/* Agent Grid */}
<div className="glass-card p-6">
<h2 className="text-lg font-bold text-white mb-4 flex items-center gap-2">
<span>🤖</span> Agents
</h2>
<div className="grid grid-cols-2 gap-3">
{agents.map((a) => (
<div key={a.name} className="p-3 rounded-xl bg-surface-800/50 text-center">
<div className="text-2xl mb-1">
{a.current_status === 'running' ? '⚡' : a.current_status === 'failed' ? '❌' : '💤'}
</div>
<div className="text-xs text-white font-semibold">{a.display_name}</div>
<div className="text-[10px] text-gray-500 mt-0.5">{a.model}</div>
<span className={`badge mt-1 text-[10px] ${
a.current_status === 'running' ? 'badge-running' :
a.current_status === 'failed' ? 'badge-failed' : 'badge-paused'
}`}>
{a.current_status}
</span>
</div>
))}
</div>
</div>
{/* Recent Logs */}
<div className="glass-card p-6">
<h2 className="text-lg font-bold text-white mb-4 flex items-center gap-2">
<span>📜</span> Activité récente
</h2>
<div className="space-y-2 max-h-72 overflow-y-auto">
{logs.length === 0 ? (
<p className="text-gray-500 text-sm">Aucune activité</p>
) : logs.map((l) => (
<div key={l.id} className="flex gap-3 p-2 rounded-lg hover:bg-surface-800/50 text-xs">
<span className="text-fox-500 font-mono whitespace-nowrap">
{new Date(l.timestamp).toLocaleTimeString('fr-FR', { hour: '2-digit', minute: '2-digit' })}
</span>
<span className="text-blue-400 font-semibold whitespace-nowrap">{l.agent}</span>
<span className="text-gray-400 truncate">{l.message || l.action}</span>
</div>
))}
</div>
</div>
</div>
</div>
);
}
function StatCard({ icon, label, value, accent }: { icon: string; label: string; value: number; accent: string }) {
const bgMap: Record<string, string> = {
fox: 'from-fox-600/10 to-fox-700/5',
green: 'from-green-500/10 to-green-600/5',
blue: 'from-blue-500/10 to-blue-600/5',
purple: 'from-purple-500/10 to-purple-600/5',
};
const textMap: Record<string, string> = {
fox: 'text-fox-500', green: 'text-green-400', blue: 'text-blue-400', purple: 'text-purple-400',
};
return (
<div className={`glass-card p-5 bg-gradient-to-br ${bgMap[accent]}`}>
<div className="flex items-center justify-between">
<div>
<div className="text-xs text-gray-400 uppercase tracking-wider mb-1">{label}</div>
<div className={`text-3xl font-extrabold ${textMap[accent]}`}>{value}</div>
</div>
<div className="text-3xl opacity-60">{icon}</div>
</div>
</div>
);
}