import { useState, useEffect, useCallback } from 'react' const API = import.meta.env.VITE_BRAIN_API ?? '' interface SearchResult { score: number title: string filepath: string excerpt: string } interface HealthData { status: string indexed: number uptime: number } interface ClaimData { sess_id: string type: string scope: string status: string opened_at: string closed_at: string | null } interface AgentData { id: string label: string tier: string status: string scope: string } interface DocData { name: string label: string group: string } function formatUptime(seconds: number): string { if (seconds < 60) return `${seconds}s` if (seconds < 3600) return `${Math.floor(seconds / 60)}min` if (seconds < 86400) return `${Math.floor(seconds / 3600)}h ${Math.floor((seconds % 3600) / 60)}min` return `${Math.floor(seconds / 86400)}j ${Math.floor((seconds % 86400) / 3600)}h` } function formatTimeAgo(dateStr: string): string { const diff = Date.now() - new Date(dateStr).getTime() const mins = Math.floor(diff / 60000) if (mins < 1) return "à l'instant" if (mins < 60) return `il y a ${mins}min` const hours = Math.floor(mins / 60) if (hours < 24) return `il y a ${hours}h` const days = Math.floor(hours / 24) return `il y a ${days}j` } function StatCard({ label, value, sub, color }: { label: string; value: string | number; sub?: string; color?: string }) { return (
{label}
{value}
{sub && (
{sub}
)}
) } function SessionRow({ claim }: { claim: ClaimData }) { const isOpen = claim.status === 'open' return (
{claim.sess_id}
{claim.type} — {claim.scope}
{formatTimeAgo(claim.opened_at)}
) } function FileViewer({ path, onClose }: { path: string; onClose: () => void }) { const [content, setContent] = useState(null) const [error, setError] = useState(null) useEffect(() => { fetch(`${API}/brain/${path}`) .then(r => { if (!r.ok) throw new Error(`${r.status}`); return r.json() }) .then(d => setContent(d.content)) .catch(e => setError(`Impossible de charger ${path}: ${e.message}`)) }, [path]) return (
e.stopPropagation()} >
{path}
{error &&
{error}
} {!content && !error &&
Chargement...
} {content && (
              {content}
            
)}
) } function SearchBar() { const [query, setQuery] = useState('') const [results, setResults] = useState([]) const [searching, setSearching] = useState(false) const [searched, setSearched] = useState(false) const [viewingFile, setViewingFile] = useState(null) const search = useCallback(async (q: string) => { if (q.trim().length < 2) { setResults([]); setSearched(false); return } setSearching(true) try { const res = await fetch(`${API}/search?q=${encodeURIComponent(q)}&top=6`) if (!res.ok) throw new Error() const data = await res.json() setResults(data.results ?? []) setSearched(true) } catch { setResults([]) } finally { setSearching(false) } }, []) useEffect(() => { const timer = setTimeout(() => { if (query.trim().length >= 2) search(query) }, 400) return () => clearTimeout(timer) }, [query, search]) return (
🔍 setQuery(e.target.value)} onKeyDown={(e) => { if (e.key === 'Enter') search(query) }} style={{ flex: 1, background: 'transparent', border: 'none', outline: 'none', color: '#e5e7eb', fontSize: 14, fontFamily: 'inherit', }} /> {searching && ...}
{searched && results.length > 0 && (
{results.map((r, i) => (
setViewingFile(r.filepath)} onMouseEnter={e => (e.currentTarget.style.background = '#1a1a1a')} onMouseLeave={e => (e.currentTarget.style.background = 'transparent')} >
{r.filepath} {(r.score * 100).toFixed(0)}%
{r.excerpt.slice(0, 200)}{r.excerpt.length > 200 ? '...' : ''}
))}
)} {searched && results.length === 0 && !searching && (
Aucun résultat pour "{query}"
)} {viewingFile && setViewingFile(null)} />}
) } export default function Dashboard() { const [health, setHealth] = useState(null) const [claims, setClaims] = useState([]) const [agents, setAgents] = useState([]) const [docs, setDocs] = useState([]) const [error, setError] = useState(null) useEffect(() => { Promise.allSettled([ fetch(`${API}/health`).then(r => r.json()), fetch(`${API}/bsi/claims`).then(r => r.ok ? r.json() : []), fetch(`${API}/agents`).then(r => r.ok ? r.json() : []), fetch(`${API}/docs`).then(r => r.ok ? r.json() : { docs: [] }), ]).then(([h, c, a, d]) => { if (h.status === 'fulfilled') setHealth(h.value) if (c.status === 'fulfilled') setClaims(Array.isArray(c.value) ? c.value : []) if (a.status === 'fulfilled') setAgents(Array.isArray(a.value) ? a.value : []) if (d.status === 'fulfilled') setDocs(d.value?.docs ?? []) }).catch(() => setError('Impossible de charger les données')) }, []) const openClaims = claims.filter(c => c.status === 'open') const recentClaims = claims.slice(0, 8) const agentsByTier = agents.reduce>((acc, a) => { acc[a.tier] = (acc[a.tier] || 0) + 1 return acc }, {}) return (
{/* Header */}

Dashboard

{health ? `brain-engine up — ${formatUptime(health.uptime)}` : 'connexion...'}

{error && (
{error}
)} {/* Search */} {/* Stats row */}
`${n} ${t}`).join(' · ') || undefined} color="#22c55e" /> 0 ? 'actives' : 'aucune active'} color={openClaims.length > 0 ? '#22c55e' : '#6b7280'} />
{/* Two columns */}
{/* Recent sessions */}
Sessions récentes
{recentClaims.length === 0 ? (
Aucune session enregistrée
) : ( recentClaims.map(c => ) )}
{/* Quick links */}
{/* Agents by scope */}
Agents par scope
{Object.entries( agents.reduce>((acc, a) => { acc[a.scope || 'unknown'] = (acc[a.scope || 'unknown'] || 0) + 1 return acc }, {}) ).map(([scope, count]) => (
{count}
{scope}
))}
{/* Docs groups */}
Documentation Ouvrir ↗
{Object.entries( docs.reduce>((acc, d) => { acc[d.group || 'Autres'] = (acc[d.group || 'Autres'] || 0) + 1 return acc }, {}) ).map(([group, count]) => (
{count}
{group}
))}
) }