sync: kernel v0.8.0 → template
This commit is contained in:
14
brain-ui/.env.example
Normal file
14
brain-ui/.env.example
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
# brain-ui — variables d'environnement
|
||||||
|
# Copier vers .env.local et adapter
|
||||||
|
|
||||||
|
# Mode mock — true = pas de VPS nécessaire (laptop dev)
|
||||||
|
VITE_USE_MOCK=true
|
||||||
|
|
||||||
|
# URL de base de l'API brain-engine
|
||||||
|
# Vide = relatif à l'hôte Apache (/api proxy)
|
||||||
|
# http://localhost:7700 = brain-engine local
|
||||||
|
VITE_BRAIN_API=
|
||||||
|
|
||||||
|
# Tier actif — owner (toutes features) | pro | free
|
||||||
|
# Géré par brain-engine, pas directement ici
|
||||||
|
# BRAIN_TIER est une variable d'environnement du process brain-engine (MYSECRETS ou export)
|
||||||
5
brain-ui/.gitignore
vendored
Normal file
5
brain-ui/.gitignore
vendored
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
node_modules/
|
||||||
|
dist/
|
||||||
|
.env.production
|
||||||
|
.env.local
|
||||||
|
.env.production
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
# brain-ui/build.sh — Build le dashboard pour servir via brain-engine
|
|
||||||
# Usage : bash brain-ui/build.sh
|
|
||||||
# Prérequis : Node.js 18+, npm
|
|
||||||
|
|
||||||
set -e
|
|
||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
||||||
|
|
||||||
echo "=== brain-ui — build ==="
|
|
||||||
|
|
||||||
# 1. Vérifier Node
|
|
||||||
if ! command -v node &>/dev/null; then
|
|
||||||
echo "❌ Node.js requis (18+). Installe-le : https://nodejs.org/"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# 2. Install deps
|
|
||||||
cd "$SCRIPT_DIR"
|
|
||||||
if [ ! -d "node_modules" ]; then
|
|
||||||
echo "→ Installation des dépendances..."
|
|
||||||
npm install
|
|
||||||
fi
|
|
||||||
|
|
||||||
# 3. Build (skip type check — erreurs TS pré-existantes non bloquantes)
|
|
||||||
echo "→ Build en cours..."
|
|
||||||
npx vite build
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "✅ brain-ui build dans dist/"
|
|
||||||
echo " Servi automatiquement par brain-engine sur /ui/"
|
|
||||||
echo " Lance : bash brain-engine/start.sh"
|
|
||||||
echo " Puis ouvre : http://localhost:7700/ui/"
|
|
||||||
5353
brain-ui/package-lock.json
generated
Normal file
5353
brain-ui/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1 +0,0 @@
|
|||||||
../../../docs/README.md
|
|
||||||
@@ -1 +1 @@
|
|||||||
../../../docs/agents-brain.md
|
/home/tetardtek/Dev/Brain/docs/agents-brain.md
|
||||||
@@ -1 +1 @@
|
|||||||
../../../docs/agents-code.md
|
/home/tetardtek/Dev/Brain/docs/agents-code.md
|
||||||
@@ -1 +1 @@
|
|||||||
../../../docs/agents-infra.md
|
/home/tetardtek/Dev/Brain/docs/agents-infra.md
|
||||||
@@ -1 +1 @@
|
|||||||
../../../docs/agents.md
|
/home/tetardtek/Dev/Brain/docs/agents.md
|
||||||
@@ -1 +1 @@
|
|||||||
../../../docs/architecture.md
|
/home/tetardtek/Dev/Brain/docs/architecture.md
|
||||||
@@ -1 +1 @@
|
|||||||
../../../docs/getting-started.md
|
/home/tetardtek/Dev/Brain/docs/getting-started.md
|
||||||
@@ -1 +1 @@
|
|||||||
../../../docs/sessions.md
|
/home/tetardtek/Dev/Brain/docs/sessions.md
|
||||||
@@ -1 +1 @@
|
|||||||
../../../docs/vue-featured.md
|
/home/tetardtek/Dev/Brain/docs/vue-featured.md
|
||||||
@@ -1 +1 @@
|
|||||||
../../../docs/vue-free.md
|
/home/tetardtek/Dev/Brain/docs/vue-free.md
|
||||||
@@ -1 +1 @@
|
|||||||
../../../docs/vue-full.md
|
/home/tetardtek/Dev/Brain/docs/vue-full.md
|
||||||
@@ -1 +1 @@
|
|||||||
../../../docs/vue-pro.md
|
/home/tetardtek/Dev/Brain/docs/vue-pro.md
|
||||||
@@ -1 +1 @@
|
|||||||
../../../docs/vue-tiers.md
|
/home/tetardtek/Dev/Brain/docs/vue-tiers.md
|
||||||
@@ -1 +1 @@
|
|||||||
../../../docs/workflows.md
|
/home/tetardtek/Dev/Brain/docs/workflows.md
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useState, useEffect, Suspense, lazy } from 'react'
|
import { useState, useEffect, Suspense, lazy } from 'react'
|
||||||
|
import Dashboard from './components/Dashboard'
|
||||||
import WorkflowBoard from './components/WorkflowBoard'
|
import WorkflowBoard from './components/WorkflowBoard'
|
||||||
import SecretsZone, { MOCK_SECTIONS } from './components/SecretsZone'
|
import SecretsZone, { MOCK_SECTIONS } from './components/SecretsZone'
|
||||||
import WorkflowBuilder from './components/WorkflowBuilder'
|
|
||||||
import GatesDrawer from './components/GatesDrawer'
|
import GatesDrawer from './components/GatesDrawer'
|
||||||
import GateDrawer from './components/GateDrawer'
|
import GateDrawer from './components/GateDrawer'
|
||||||
import LogDrawer from './components/LogDrawer'
|
import LogDrawer from './components/LogDrawer'
|
||||||
@@ -16,14 +16,15 @@ import { useTier } from './hooks/useTier'
|
|||||||
|
|
||||||
const CosmosView = lazy(() => import('./components/cosmos/CosmosView'))
|
const CosmosView = lazy(() => import('./components/cosmos/CosmosView'))
|
||||||
const WorkspaceView = lazy(() => import('./components/workspace/WorkspaceView'))
|
const WorkspaceView = lazy(() => import('./components/workspace/WorkspaceView'))
|
||||||
type ActiveView = 'workflows' | 'builder' | 'secrets' | 'infra' | 'cosmos' | 'workspace'
|
const DocsView = lazy(() => import('./components/DocsView'))
|
||||||
|
|
||||||
|
type ActiveView = 'dashboard' | 'cosmos' | 'workflows' | 'secrets' | 'infra' | 'workspace'
|
||||||
|
|
||||||
interface NavItem {
|
interface NavItem {
|
||||||
id: ActiveView
|
id: ActiveView
|
||||||
icon: string
|
icon: string
|
||||||
label: string
|
label: string
|
||||||
separator?: boolean
|
separator?: boolean
|
||||||
disabled?: boolean
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface PendingGate {
|
interface PendingGate {
|
||||||
@@ -33,11 +34,11 @@ interface PendingGate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const NAV_ITEMS: NavItem[] = [
|
const NAV_ITEMS: NavItem[] = [
|
||||||
{ id: 'workflows', icon: '🔀', label: 'Workflows', disabled: true },
|
{ id: 'dashboard', icon: '⬡', label: 'Dashboard' },
|
||||||
{ id: 'builder', icon: '⚡', label: 'Nouveau', disabled: true },
|
{ id: 'cosmos', icon: '🌌', label: 'Cosmos' },
|
||||||
{ id: 'secrets', icon: '🔑', label: 'Secrets', disabled: true },
|
{ id: 'workflows', icon: '🔀', label: 'Workflows', separator: true },
|
||||||
{ id: 'infra', icon: '🖥️', label: 'Infra', disabled: true },
|
{ id: 'infra', icon: '🖥️', label: 'Infra' },
|
||||||
{ id: 'cosmos', icon: '🌌', label: 'Cosmos', separator: true },
|
{ id: 'secrets', icon: '🔑', label: 'Secrets' },
|
||||||
]
|
]
|
||||||
|
|
||||||
function AppInner() {
|
function AppInner() {
|
||||||
@@ -46,10 +47,9 @@ function AppInner() {
|
|||||||
// Detect URL path for direct routing (/ui/docs → docs view)
|
// Detect URL path for direct routing (/ui/docs → docs view)
|
||||||
const initialView = (): ActiveView => {
|
const initialView = (): ActiveView => {
|
||||||
const path = window.location.pathname
|
const path = window.location.pathname
|
||||||
// /docs → redirige vers docs.html (page standalone)
|
|
||||||
if (path.includes('/cosmos')) return 'cosmos'
|
if (path.includes('/cosmos')) return 'cosmos'
|
||||||
if (path.includes('/workspace')) return 'workspace'
|
if (path.includes('/workspace')) return 'workspace'
|
||||||
return 'workflows'
|
return 'dashboard'
|
||||||
}
|
}
|
||||||
const [activeView, setActiveView] = useState<ActiveView>(initialView)
|
const [activeView, setActiveView] = useState<ActiveView>(initialView)
|
||||||
const [pendingGate, setPendingGate] = useState<PendingGate | null>(null)
|
const [pendingGate, setPendingGate] = useState<PendingGate | null>(null)
|
||||||
@@ -146,19 +146,10 @@ function AppInner() {
|
|||||||
<div className="mx-3 my-1" style={{ borderTop: '1px solid #2a2a2a' }} />
|
<div className="mx-3 my-1" style={{ borderTop: '1px solid #2a2a2a' }} />
|
||||||
)}
|
)}
|
||||||
<button
|
<button
|
||||||
onClick={() => !item.disabled && handleViewChange(item.id)}
|
onClick={() => handleViewChange(item.id)}
|
||||||
className="flex items-center gap-3 px-3 py-2 rounded text-sm font-medium text-left transition-colors w-full"
|
className="flex items-center gap-3 px-3 py-2 rounded text-sm font-medium text-left transition-colors w-full"
|
||||||
title={item.disabled ? 'Disponible avec brain boot' : undefined}
|
|
||||||
style={
|
style={
|
||||||
item.disabled
|
isActive
|
||||||
? {
|
|
||||||
color: '#374151',
|
|
||||||
borderLeft: '2px solid transparent',
|
|
||||||
paddingLeft: 10,
|
|
||||||
cursor: 'default',
|
|
||||||
opacity: 0.5,
|
|
||||||
}
|
|
||||||
: isActive
|
|
||||||
? {
|
? {
|
||||||
background: 'rgba(99,102,241,0.2)',
|
background: 'rgba(99,102,241,0.2)',
|
||||||
color: '#6366f1',
|
color: '#6366f1',
|
||||||
@@ -174,9 +165,6 @@ function AppInner() {
|
|||||||
>
|
>
|
||||||
<span className="text-base leading-none">{item.icon}</span>
|
<span className="text-base leading-none">{item.icon}</span>
|
||||||
<span>{item.label}</span>
|
<span>{item.label}</span>
|
||||||
{item.disabled && (
|
|
||||||
<span style={{ marginLeft: 'auto', fontSize: 8, color: '#374151', fontFamily: 'monospace' }}>soon</span>
|
|
||||||
)}
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
@@ -244,6 +232,9 @@ function AppInner() {
|
|||||||
|
|
||||||
{/* Main content */}
|
{/* Main content */}
|
||||||
<main className="flex-1 overflow-hidden flex flex-col">
|
<main className="flex-1 overflow-hidden flex flex-col">
|
||||||
|
{activeView === 'dashboard' && (
|
||||||
|
<Dashboard />
|
||||||
|
)}
|
||||||
{activeView === 'workflows' && (
|
{activeView === 'workflows' && (
|
||||||
<WorkflowBoard
|
<WorkflowBoard
|
||||||
workflows={workflows}
|
workflows={workflows}
|
||||||
@@ -251,9 +242,6 @@ function AppInner() {
|
|||||||
onWorkflowClick={(wfId) => setLogsProject(wfId)}
|
onWorkflowClick={(wfId) => setLogsProject(wfId)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{activeView === 'builder' && (
|
|
||||||
<WorkflowBuilder />
|
|
||||||
)}
|
|
||||||
{activeView === 'secrets' && (
|
{activeView === 'secrets' && (
|
||||||
<TierGate feature="secrets" hasFeature={hasFeature}>
|
<TierGate feature="secrets" hasFeature={hasFeature}>
|
||||||
<SecretsZone sections={MOCK_SECTIONS} onSecretSave={handleSecretSave} />
|
<SecretsZone sections={MOCK_SECTIONS} onSecretSave={handleSecretSave} />
|
||||||
|
|||||||
404
brain-ui/src/components/Dashboard.tsx
Normal file
404
brain-ui/src/components/Dashboard.tsx
Normal file
@@ -0,0 +1,404 @@
|
|||||||
|
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 (
|
||||||
|
<div style={{
|
||||||
|
background: '#141414', border: '1px solid #2a2a2a', borderRadius: 8,
|
||||||
|
padding: '16px 20px', flex: '1 1 0', minWidth: 140,
|
||||||
|
}}>
|
||||||
|
<div style={{ fontSize: 11, color: '#6b7280', fontFamily: 'monospace', textTransform: 'uppercase', letterSpacing: '0.05em' }}>
|
||||||
|
{label}
|
||||||
|
</div>
|
||||||
|
<div style={{ fontSize: 28, fontWeight: 700, color: color ?? '#e5e7eb', marginTop: 4 }}>
|
||||||
|
{value}
|
||||||
|
</div>
|
||||||
|
{sub && (
|
||||||
|
<div style={{ fontSize: 11, color: '#4b5563', marginTop: 2, fontFamily: 'monospace' }}>
|
||||||
|
{sub}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function SessionRow({ claim }: { claim: ClaimData }) {
|
||||||
|
const isOpen = claim.status === 'open'
|
||||||
|
return (
|
||||||
|
<div style={{
|
||||||
|
display: 'flex', alignItems: 'center', gap: 12,
|
||||||
|
padding: '10px 16px', borderBottom: '1px solid #1e1e1e',
|
||||||
|
}}>
|
||||||
|
<span style={{
|
||||||
|
width: 8, height: 8, borderRadius: '50%', flexShrink: 0,
|
||||||
|
background: isOpen ? '#22c55e' : '#4b5563',
|
||||||
|
}} />
|
||||||
|
<div style={{ flex: 1, minWidth: 0 }}>
|
||||||
|
<div style={{ fontSize: 13, color: '#e5e7eb', fontFamily: 'monospace' }}>
|
||||||
|
{claim.sess_id}
|
||||||
|
</div>
|
||||||
|
<div style={{ fontSize: 11, color: '#6b7280', marginTop: 2 }}>
|
||||||
|
{claim.type} — {claim.scope}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style={{ fontSize: 11, color: '#4b5563', fontFamily: 'monospace', flexShrink: 0 }}>
|
||||||
|
{formatTimeAgo(claim.opened_at)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function FileViewer({ path, onClose }: { path: string; onClose: () => void }) {
|
||||||
|
const [content, setContent] = useState<string | null>(null)
|
||||||
|
const [error, setError] = useState<string | null>(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 (
|
||||||
|
<div style={{
|
||||||
|
position: 'fixed', inset: 0, zIndex: 50,
|
||||||
|
background: 'rgba(0,0,0,0.7)', display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||||
|
}} onClick={onClose}>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
background: '#141414', border: '1px solid #2a2a2a', borderRadius: 12,
|
||||||
|
width: '70%', maxWidth: 800, maxHeight: '80vh',
|
||||||
|
display: 'flex', flexDirection: 'column', overflow: 'hidden',
|
||||||
|
}}
|
||||||
|
onClick={e => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
<div style={{
|
||||||
|
padding: '12px 20px', borderBottom: '1px solid #2a2a2a',
|
||||||
|
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
|
||||||
|
}}>
|
||||||
|
<span style={{ fontSize: 13, fontFamily: 'monospace', color: '#818cf8' }}>{path}</span>
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
style={{ background: 'none', border: 'none', color: '#6b7280', cursor: 'pointer', fontSize: 18 }}
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div style={{ padding: '16px 20px', overflowY: 'auto', flex: 1 }}>
|
||||||
|
{error && <div style={{ color: '#ef4444', fontSize: 13 }}>{error}</div>}
|
||||||
|
{!content && !error && <div style={{ color: '#4b5563', fontSize: 13, fontFamily: 'monospace' }}>Chargement...</div>}
|
||||||
|
{content && (
|
||||||
|
<pre style={{
|
||||||
|
fontSize: 13, lineHeight: 1.6, color: '#d1d5db',
|
||||||
|
fontFamily: "'JetBrains Mono', 'Fira Code', monospace",
|
||||||
|
whiteSpace: 'pre-wrap', wordBreak: 'break-word', margin: 0,
|
||||||
|
}}>
|
||||||
|
{content}
|
||||||
|
</pre>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function SearchBar() {
|
||||||
|
const [query, setQuery] = useState('')
|
||||||
|
const [results, setResults] = useState<SearchResult[]>([])
|
||||||
|
const [searching, setSearching] = useState(false)
|
||||||
|
const [searched, setSearched] = useState(false)
|
||||||
|
const [viewingFile, setViewingFile] = useState<string | null>(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 (
|
||||||
|
<div style={{ marginBottom: 24 }}>
|
||||||
|
<div style={{
|
||||||
|
display: 'flex', alignItems: 'center', gap: 8,
|
||||||
|
background: '#141414', border: '1px solid #2a2a2a', borderRadius: 8,
|
||||||
|
padding: '8px 16px',
|
||||||
|
}}>
|
||||||
|
<span style={{ color: '#4b5563', fontSize: 16 }}>🔍</span>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Rechercher dans le brain..."
|
||||||
|
value={query}
|
||||||
|
onChange={(e) => 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 && <span style={{ color: '#4b5563', fontSize: 12, fontFamily: 'monospace' }}>...</span>}
|
||||||
|
</div>
|
||||||
|
{searched && results.length > 0 && (
|
||||||
|
<div style={{
|
||||||
|
marginTop: 8, background: '#141414', border: '1px solid #2a2a2a',
|
||||||
|
borderRadius: 8, overflow: 'hidden',
|
||||||
|
}}>
|
||||||
|
{results.map((r, i) => (
|
||||||
|
<div key={i} style={{
|
||||||
|
padding: '12px 16px', borderBottom: i < results.length - 1 ? '1px solid #1e1e1e' : 'none',
|
||||||
|
cursor: 'pointer', transition: 'background 0.15s',
|
||||||
|
}}
|
||||||
|
onClick={() => setViewingFile(r.filepath)}
|
||||||
|
onMouseEnter={e => (e.currentTarget.style.background = '#1a1a1a')}
|
||||||
|
onMouseLeave={e => (e.currentTarget.style.background = 'transparent')}
|
||||||
|
>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 4 }}>
|
||||||
|
<span style={{ fontSize: 12, fontFamily: 'monospace', color: '#818cf8' }}>
|
||||||
|
{r.filepath}
|
||||||
|
</span>
|
||||||
|
<span style={{
|
||||||
|
fontSize: 10, fontFamily: 'monospace', color: '#4b5563',
|
||||||
|
marginLeft: 'auto',
|
||||||
|
}}>
|
||||||
|
{(r.score * 100).toFixed(0)}%
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div style={{ fontSize: 13, color: '#9ca3af', lineHeight: 1.5 }}>
|
||||||
|
{r.excerpt.slice(0, 200)}{r.excerpt.length > 200 ? '...' : ''}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{searched && results.length === 0 && !searching && (
|
||||||
|
<div style={{ marginTop: 8, fontSize: 13, color: '#4b5563', fontFamily: 'monospace', padding: '8px 16px' }}>
|
||||||
|
Aucun résultat pour "{query}"
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{viewingFile && <FileViewer path={viewingFile} onClose={() => setViewingFile(null)} />}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Dashboard() {
|
||||||
|
const [health, setHealth] = useState<HealthData | null>(null)
|
||||||
|
const [claims, setClaims] = useState<ClaimData[]>([])
|
||||||
|
const [agents, setAgents] = useState<AgentData[]>([])
|
||||||
|
const [docs, setDocs] = useState<DocData[]>([])
|
||||||
|
const [error, setError] = useState<string | null>(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<Record<string, number>>((acc, a) => {
|
||||||
|
acc[a.tier] = (acc[a.tier] || 0) + 1
|
||||||
|
return acc
|
||||||
|
}, {})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ padding: '2rem 3rem', overflowY: 'auto', height: '100%' }}>
|
||||||
|
{/* Header */}
|
||||||
|
<div style={{ marginBottom: 24 }}>
|
||||||
|
<h1 style={{ fontSize: 20, fontWeight: 700, color: '#fff', margin: 0 }}>
|
||||||
|
Dashboard
|
||||||
|
</h1>
|
||||||
|
<p style={{ fontSize: 12, color: '#4b5563', fontFamily: 'monospace', marginTop: 4 }}>
|
||||||
|
{health ? `brain-engine up — ${formatUptime(health.uptime)}` : 'connexion...'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<div style={{ color: '#ef4444', fontSize: 13, marginBottom: 16 }}>{error}</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Search */}
|
||||||
|
<SearchBar />
|
||||||
|
|
||||||
|
{/* Stats row */}
|
||||||
|
<div style={{ display: 'flex', gap: 12, marginBottom: 24, flexWrap: 'wrap' }}>
|
||||||
|
<StatCard
|
||||||
|
label="Embeddings"
|
||||||
|
value={health?.indexed?.toLocaleString() ?? '—'}
|
||||||
|
sub="chunks indexés"
|
||||||
|
color="#818cf8"
|
||||||
|
/>
|
||||||
|
<StatCard
|
||||||
|
label="Agents"
|
||||||
|
value={agents.length || '—'}
|
||||||
|
sub={Object.entries(agentsByTier).map(([t, n]) => `${n} ${t}`).join(' · ') || undefined}
|
||||||
|
color="#22c55e"
|
||||||
|
/>
|
||||||
|
<StatCard
|
||||||
|
label="Docs"
|
||||||
|
value={docs.length || '—'}
|
||||||
|
sub="pages live"
|
||||||
|
color="#f59e0b"
|
||||||
|
/>
|
||||||
|
<StatCard
|
||||||
|
label="Sessions"
|
||||||
|
value={openClaims.length}
|
||||||
|
sub={openClaims.length > 0 ? 'actives' : 'aucune active'}
|
||||||
|
color={openClaims.length > 0 ? '#22c55e' : '#6b7280'}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Two columns */}
|
||||||
|
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 16 }}>
|
||||||
|
{/* Recent sessions */}
|
||||||
|
<div style={{ background: '#141414', border: '1px solid #2a2a2a', borderRadius: 8, overflow: 'hidden' }}>
|
||||||
|
<div style={{
|
||||||
|
padding: '12px 16px', borderBottom: '1px solid #2a2a2a',
|
||||||
|
fontSize: 12, fontFamily: 'monospace', color: '#6b7280',
|
||||||
|
textTransform: 'uppercase', letterSpacing: '0.05em',
|
||||||
|
}}>
|
||||||
|
Sessions récentes
|
||||||
|
</div>
|
||||||
|
{recentClaims.length === 0 ? (
|
||||||
|
<div style={{ padding: 16, fontSize: 13, color: '#4b5563' }}>
|
||||||
|
Aucune session enregistrée
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
recentClaims.map(c => <SessionRow key={c.sess_id} claim={c} />)
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Quick links */}
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
|
||||||
|
{/* Agents by scope */}
|
||||||
|
<div style={{ background: '#141414', border: '1px solid #2a2a2a', borderRadius: 8, overflow: 'hidden' }}>
|
||||||
|
<div style={{
|
||||||
|
padding: '12px 16px', borderBottom: '1px solid #2a2a2a',
|
||||||
|
fontSize: 12, fontFamily: 'monospace', color: '#6b7280',
|
||||||
|
textTransform: 'uppercase', letterSpacing: '0.05em',
|
||||||
|
}}>
|
||||||
|
Agents par scope
|
||||||
|
</div>
|
||||||
|
<div style={{ padding: 16, display: 'flex', gap: 16, flexWrap: 'wrap' }}>
|
||||||
|
{Object.entries(
|
||||||
|
agents.reduce<Record<string, number>>((acc, a) => {
|
||||||
|
acc[a.scope || 'unknown'] = (acc[a.scope || 'unknown'] || 0) + 1
|
||||||
|
return acc
|
||||||
|
}, {})
|
||||||
|
).map(([scope, count]) => (
|
||||||
|
<div key={scope} style={{ textAlign: 'center' }}>
|
||||||
|
<div style={{ fontSize: 20, fontWeight: 700, color: '#e5e7eb' }}>{count}</div>
|
||||||
|
<div style={{ fontSize: 10, color: '#6b7280', fontFamily: 'monospace' }}>{scope}</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Docs groups */}
|
||||||
|
<div style={{ background: '#141414', border: '1px solid #2a2a2a', borderRadius: 8, overflow: 'hidden' }}>
|
||||||
|
<div style={{
|
||||||
|
padding: '12px 16px', borderBottom: '1px solid #2a2a2a',
|
||||||
|
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
|
||||||
|
}}>
|
||||||
|
<span style={{ fontSize: 12, fontFamily: 'monospace', color: '#6b7280', textTransform: 'uppercase', letterSpacing: '0.05em' }}>
|
||||||
|
Documentation
|
||||||
|
</span>
|
||||||
|
<a href="/docs" target="_blank" rel="noopener noreferrer"
|
||||||
|
style={{ fontSize: 11, color: '#818cf8', textDecoration: 'none', fontFamily: 'monospace' }}>
|
||||||
|
Ouvrir ↗
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div style={{ padding: 16, display: 'flex', gap: 16, flexWrap: 'wrap' }}>
|
||||||
|
{Object.entries(
|
||||||
|
docs.reduce<Record<string, number>>((acc, d) => {
|
||||||
|
acc[d.group || 'Autres'] = (acc[d.group || 'Autres'] || 0) + 1
|
||||||
|
return acc
|
||||||
|
}, {})
|
||||||
|
).map(([group, count]) => (
|
||||||
|
<div key={group} style={{ textAlign: 'center' }}>
|
||||||
|
<div style={{ fontSize: 20, fontWeight: 700, color: '#e5e7eb' }}>{count}</div>
|
||||||
|
<div style={{ fontSize: 10, color: '#6b7280', fontFamily: 'monospace' }}>{group}</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -212,7 +212,14 @@ export default function DocsView() {
|
|||||||
return <article className="docs-markdown"><TierSingle tierName={tierName} /></article>
|
return <article className="docs-markdown"><TierSingle tierName={tierName} /></article>
|
||||||
}
|
}
|
||||||
if (liveMode && activeDoc === 'agents') {
|
if (liveMode && activeDoc === 'agents') {
|
||||||
return <article className="docs-markdown"><AgentCatalog /></article>
|
return (
|
||||||
|
<article className="docs-markdown">
|
||||||
|
{!loading && content && (
|
||||||
|
<ReactMarkdown components={mdComponents}>{content}</ReactMarkdown>
|
||||||
|
)}
|
||||||
|
<AgentCatalog />
|
||||||
|
</article>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mode standard — markdown
|
// Mode standard — markdown
|
||||||
|
|||||||
@@ -116,6 +116,18 @@ if [ -d "$BRAIN_ROOT/contexts" ]; then
|
|||||||
echo " ✅ $ctx_count contexts"
|
echo " ✅ $ctx_count contexts"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# --- Brain-UI (source + dist) ---
|
||||||
|
echo ""
|
||||||
|
echo "── brain-ui/ ──────────────────────────────────"
|
||||||
|
if [ -d "$BRAIN_ROOT/brain-ui" ]; then
|
||||||
|
if [ -z "$DRY" ]; then
|
||||||
|
rsync -a --delete \
|
||||||
|
--exclude='node_modules/' \
|
||||||
|
"$BRAIN_ROOT/brain-ui/" "$TEMPLATE_DIR/brain-ui/"
|
||||||
|
fi
|
||||||
|
echo " ✅ brain-ui (src + dist)"
|
||||||
|
fi
|
||||||
|
|
||||||
# --- Gitkeep ---
|
# --- Gitkeep ---
|
||||||
[ -z "$DRY" ] && mkdir -p "$TEMPLATE_DIR/locks" && \
|
[ -z "$DRY" ] && mkdir -p "$TEMPLATE_DIR/locks" && \
|
||||||
touch "$TEMPLATE_DIR/locks/.gitkeep"
|
touch "$TEMPLATE_DIR/locks/.gitkeep"
|
||||||
|
|||||||
Reference in New Issue
Block a user