feat: brain-engine + brain-ui + docs — template full stack standalone

- brain-engine: server, embed, search, RAG, MCP, start.sh (standalone)
- brain-ui: source React complète, build.sh, DocsView avec tier colors
- docs: 14 pages guides humains (getting-started, architecture, sessions, workflows, agents, vues tier)
- brain-compose.yml v0.9.0: tier featured ajouté, sessions/agents par tier, coach_level, API key schema
- DISTRIBUTION_CHECKLIST v1.2: brain-engine + brain-ui + docs dans la checklist
This commit is contained in:
2026-03-20 20:25:40 +01:00
parent c249d417f5
commit 8244a07881
93 changed files with 12088 additions and 34 deletions

View File

@@ -0,0 +1,64 @@
import { useMemo } from 'react'
import * as THREE from 'three'
import type { CosmosPoint, ZoneKey } from '../../types'
const ZONE_COLORS: Record<ZoneKey, string> = {
public: '#6366f1',
work: '#22c55e',
kernel: '#f59e0b',
unknown: '#6b7280',
}
interface Props {
points: CosmosPoint[]
}
export function CosmosBackground({ points }: Props) {
const { positions, colors } = useMemo(() => {
const positions = new Float32Array(points.length * 3)
const colors = new Float32Array(points.length * 3)
const color = new THREE.Color()
points.forEach((p, i) => {
positions[i * 3] = p.x * 3
positions[i * 3 + 1] = p.y * 3
positions[i * 3 + 2] = p.z * 3
color.set(ZONE_COLORS[p.zone] ?? ZONE_COLORS.unknown)
colors[i * 3] = color.r
colors[i * 3 + 1] = color.g
colors[i * 3 + 2] = color.b
})
return { positions, colors }
}, [points])
if (points.length === 0) return null
return (
<points>
<bufferGeometry>
<bufferAttribute
attach="attributes-position"
array={positions}
count={points.length}
itemSize={3}
/>
<bufferAttribute
attach="attributes-color"
array={colors}
count={points.length}
itemSize={3}
/>
</bufferGeometry>
<pointsMaterial
size={0.04}
vertexColors
transparent
opacity={0.2}
sizeAttenuation
depthWrite={false}
/>
</points>
)
}

View File

@@ -0,0 +1,39 @@
import { useRef, useState } from 'react'
import { useFrame } from '@react-three/fiber'
import * as THREE from 'three'
import type { WorkspaceStep } from '../../types'
interface Props {
step: WorkspaceStep
onClick: () => void
}
export function GateOctahedron({ step, onClick }: Props) {
const meshRef = useRef<THREE.Mesh>(null)
const [hovered, setHovered] = useState(false)
useFrame((_, delta) => {
if (meshRef.current) {
meshRef.current.rotation.y += delta * 0.8
meshRef.current.rotation.x += delta * 0.3
}
})
return (
<mesh
ref={meshRef}
position={[step.x, step.y, step.z]}
onClick={(e) => { e.stopPropagation(); onClick() }}
onPointerOver={() => setHovered(true)}
onPointerOut={() => setHovered(false)}
>
<octahedronGeometry args={[hovered ? 0.45 : 0.35]} />
<meshStandardMaterial
color="#f59e0b"
emissive="#f59e0b"
emissiveIntensity={hovered ? 0.6 : 0.3}
wireframe={!hovered}
/>
</mesh>
)
}

View File

@@ -0,0 +1,43 @@
import { useRef, useState } from 'react'
import { useFrame } from '@react-three/fiber'
import * as THREE from 'three'
import type { WorkspaceStep } from '../../types'
interface Props {
step: WorkspaceStep
color: string
onClick: () => void
}
export function StepSphere({ step, color, onClick }: Props) {
const meshRef = useRef<THREE.Mesh>(null)
const [hovered, setHovered] = useState(false)
useFrame(() => {
if (!meshRef.current) return
if (step.status === 'in-progress') {
meshRef.current.scale.setScalar(1 + Math.sin(Date.now() * 0.003) * 0.08)
}
})
const size = step.status === 'done' ? 0.18 : 0.25
return (
<mesh
ref={meshRef}
position={[step.x, step.y, step.z]}
onClick={(e) => { e.stopPropagation(); onClick() }}
onPointerOver={() => setHovered(true)}
onPointerOut={() => setHovered(false)}
>
<sphereGeometry args={[hovered ? size * 1.3 : size, 16, 16]} />
<meshStandardMaterial
color={color}
emissive={color}
emissiveIntensity={step.status === 'in-progress' ? 0.4 : hovered ? 0.3 : 0.1}
transparent
opacity={step.status === 'done' ? 0.5 : 1}
/>
</mesh>
)
}

View File

@@ -0,0 +1,91 @@
import { Text } from '@react-three/drei'
import * as THREE from 'three'
import { StepSphere } from './StepSphere'
import { GateOctahedron } from './GateOctahedron'
import type { WorkspaceWorkflow, WorkspaceStep } from '../../types'
interface Props {
workflow: WorkspaceWorkflow
onStepClick: (step: WorkspaceStep) => void
}
const STATUS_COLORS: Record<string, string> = {
done: '#22c55e',
'in-progress': '#6366f1',
pending: '#4b5563',
gate: '#f59e0b',
fail: '#ef4444',
blocked: '#6b7280',
}
function ConnectionLine({
from,
to,
color,
animated,
}: {
from: [number, number, number]
to: [number, number, number]
color: string
animated: boolean
}) {
const points = [new THREE.Vector3(...from), new THREE.Vector3(...to)]
const geometry = new THREE.BufferGeometry().setFromPoints(points)
const line = new THREE.Line(
geometry,
new THREE.LineBasicMaterial({ color, opacity: animated ? 1 : 0.4, transparent: true })
)
return <primitive object={line} />
}
export function WorkflowConstellation({ workflow, onStepClick }: Props) {
const firstStep = workflow.steps[0]
return (
<group>
{firstStep && (
<Text
position={[firstStep.x, firstStep.y + 1.2, firstStep.z]}
fontSize={0.25}
color={workflow.color}
anchorX="center"
anchorY="bottom"
font={undefined}
>
{workflow.name}
</Text>
)}
{workflow.steps.slice(0, -1).map((step, i) => {
const next = workflow.steps[i + 1]
return (
<ConnectionLine
key={`edge-${step.id}-${next.id}`}
from={[step.x, step.y, step.z]}
to={[next.x, next.y, next.z]}
color={STATUS_COLORS[step.status] ?? '#4b5563'}
animated={step.status === 'in-progress'}
/>
)
})}
{workflow.steps.map((step) =>
step.isGate ? (
<GateOctahedron
key={step.id}
step={step}
onClick={() => onStepClick(step)}
/>
) : (
<StepSphere
key={step.id}
step={step}
color={STATUS_COLORS[step.status] ?? '#4b5563'}
onClick={() => onStepClick(step)}
/>
)
)}
</group>
)
}

View File

@@ -0,0 +1,149 @@
import { useState } from 'react'
import type { WorkspaceStep, WorkspaceWorkflow } from '../../types'
const API_BASE = import.meta.env.VITE_BRAIN_API ?? ''
const STATUS_COLORS: Record<string, string> = {
done: '#22c55e',
'in-progress': '#6366f1',
pending: '#4b5563',
gate: '#f59e0b',
fail: '#ef4444',
blocked: '#6b7280',
}
interface Props {
selection: { step: WorkspaceStep; wf: WorkspaceWorkflow } | null
onClose: () => void
}
export function WorkspaceInfoPanel({ selection, onClose }: Props) {
const [busy, setBusy] = useState(false)
if (!selection) return null
const { step, wf } = selection
const gateAction = async (action: 'approve' | 'abort') => {
setBusy(true)
try {
await fetch(
`${API_BASE}/gate/${encodeURIComponent(wf.id)}/${encodeURIComponent(step.id)}/approve`,
{
method: 'POST',
credentials: 'include',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ action }),
}
)
onClose()
} finally {
setBusy(false)
}
}
return (
<div
style={{
position: 'absolute',
top: 0,
right: 0,
bottom: 0,
width: 320,
background: '#0d0d0d',
borderLeft: '1px solid #2a2a2a',
display: 'flex',
flexDirection: 'column',
zIndex: 10,
}}
>
<div
style={{
display: 'flex',
alignItems: 'center',
padding: '12px 16px',
borderBottom: '1px solid #2a2a2a',
gap: 8,
}}
>
<span
style={{ color: '#6b7280', fontFamily: 'monospace', fontSize: 11, flex: 1 }}
>
{wf.name}
</span>
<button
onClick={onClose}
style={{
background: 'transparent',
border: 'none',
color: '#6b7280',
cursor: 'pointer',
fontSize: 16,
}}
>
</button>
</div>
<div
style={{
padding: '16px',
flex: 1,
display: 'flex',
flexDirection: 'column',
gap: 12,
}}
>
<div style={{ color: '#e5e7eb', fontWeight: 600, fontSize: 16 }}>{step.label}</div>
<span
style={{
display: 'inline-block',
padding: '2px 8px',
borderRadius: 4,
fontSize: 11,
background: `${STATUS_COLORS[step.status] ?? '#4b5563'}22`,
color: STATUS_COLORS[step.status] ?? '#4b5563',
}}
>
{step.status}
</span>
{step.isGate && (
<div style={{ display: 'flex', gap: 8, marginTop: 8 }}>
<button
disabled={busy}
onClick={() => gateAction('approve')}
style={{
background: '#16a34a',
color: '#fff',
border: 'none',
borderRadius: 6,
padding: '6px 16px',
fontWeight: 600,
cursor: busy ? 'not-allowed' : 'pointer',
opacity: busy ? 0.6 : 1,
}}
>
Approuver
</button>
<button
disabled={busy}
onClick={() => gateAction('abort')}
style={{
background: '#dc2626',
color: '#fff',
border: 'none',
borderRadius: 6,
padding: '6px 16px',
fontWeight: 600,
cursor: busy ? 'not-allowed' : 'pointer',
opacity: busy ? 0.6 : 1,
}}
>
Rejeter
</button>
</div>
)}
</div>
</div>
)
}

View File

@@ -0,0 +1,59 @@
import type { WorkspaceWorkflow } from '../../types'
interface Props {
workflows: WorkspaceWorkflow[]
}
export function WorkspaceMetrics({ workflows }: Props) {
const total = workflows.reduce((n, wf) => n + wf.steps.length, 0)
const active = workflows.reduce(
(n, wf) => n + wf.steps.filter((s) => s.status === 'in-progress').length,
0
)
const gates = workflows.reduce(
(n, wf) => n + wf.steps.filter((s) => s.isGate && s.status === 'gate').length,
0
)
return (
<div
style={{
position: 'absolute',
bottom: 0,
left: 0,
right: 0,
height: 40,
background: '#0d0d0d',
borderTop: '1px solid #2a2a2a',
display: 'flex',
alignItems: 'center',
padding: '0 16px',
gap: 16,
fontFamily: 'monospace',
fontSize: 11,
color: '#6b7280',
zIndex: 5,
}}
>
<span>
Workflows : <span style={{ color: '#e5e7eb' }}>{workflows.length}</span>
</span>
<span style={{ color: '#2a2a2a' }}>|</span>
<span>
Steps : <span style={{ color: '#e5e7eb' }}>{total}</span>
</span>
<span style={{ color: '#2a2a2a' }}>|</span>
<span>
Actifs : <span style={{ color: '#6366f1' }}>{active}</span>
</span>
{gates > 0 && (
<>
<span style={{ color: '#2a2a2a' }}>|</span>
<span>
Gates en attente : <span style={{ color: '#f59e0b' }}>{gates}</span>
</span>
</>
)}
</div>
)
}

View File

@@ -0,0 +1,120 @@
import { useState, Suspense } from 'react'
function checkWebGL(): boolean {
try {
const canvas = document.createElement('canvas')
return !!(canvas.getContext('webgl') || canvas.getContext('experimental-webgl'))
} catch { return false }
}
function NoWebGL() {
return (
<div className="flex flex-col items-center justify-center h-full" style={{ background: '#080808' }}>
<div className="text-3xl mb-3">🖥</div>
<div style={{ color: '#ef4444' }} className="text-sm font-mono mb-1">WebGL non disponible</div>
<div style={{ color: '#4b5563' }} className="text-xs text-center max-w-xs">
Active l'accélération matérielle dans Chrome : Paramètres → Système → Utiliser l'accélération matérielle
</div>
</div>
)
}
import { Canvas } from '@react-three/fiber'
import { OrbitControls } from '@react-three/drei'
import { useWorkspaceData } from '../../hooks/useWorkspaceData'
import { useCosmosData } from '../../hooks/useCosmosData'
import { WorkflowConstellation } from './WorkflowConstellation'
import { WorkspaceInfoPanel } from './WorkspaceInfoPanel'
import { WorkspaceMetrics } from './WorkspaceMetrics'
import { CosmosBackground } from './CosmosBackground'
import type { WorkspaceStep, WorkspaceWorkflow } from '../../types'
function WorkspaceInner() {
const { workflows } = useWorkspaceData()
const { points } = useCosmosData()
const [selectedStep, setSelectedStep] = useState<{
step: WorkspaceStep
wf: WorkspaceWorkflow
} | null>(null)
const [showCosmos, setShowCosmos] = useState(true)
if (workflows.length === 0) {
return (
<div
className="flex flex-col items-center justify-center h-full"
style={{ background: '#080808' }}
>
<div className="text-4xl mb-3">🌌</div>
<div style={{ color: '#4b5563' }} className="text-sm font-mono">
Aucun workflow actif
</div>
<div style={{ color: '#374151' }} className="text-xs mt-1">
Créer un workflow via K Nouveau workflow
</div>
</div>
)
}
return (
<div style={{ width: '100%', height: '100%', background: '#080808', position: 'relative' }}>
<Canvas
camera={{ position: [0, 2, 12], fov: 60 }}
gl={{ antialias: true }}
style={{ width: '100%', height: '100%' }}
>
<ambientLight intensity={0.2} />
<pointLight position={[10, 10, 10]} intensity={0.5} />
<Suspense fallback={null}>
{showCosmos && <CosmosBackground points={points} />}
{workflows.map((wf) => (
<WorkflowConstellation
key={wf.id}
workflow={wf}
onStepClick={(step) => setSelectedStep({ step, wf })}
/>
))}
</Suspense>
<OrbitControls
enableDamping
dampingFactor={0.05}
rotateSpeed={0.4}
minDistance={3}
maxDistance={30}
/>
</Canvas>
<WorkspaceInfoPanel
selection={selectedStep}
onClose={() => setSelectedStep(null)}
/>
<WorkspaceMetrics workflows={workflows} />
<button
onClick={() => setShowCosmos((v) => !v)}
style={{
position: 'absolute',
top: '0.5rem',
right: '0.5rem',
background: '#1a1a1a',
border: '1px solid #2a2a2a',
color: showCosmos ? '#6366f1' : '#6b7280',
fontFamily: 'monospace',
fontSize: '0.75rem',
padding: '0.25rem 0.5rem',
borderRadius: '0.25rem',
cursor: 'pointer',
zIndex: 10,
}}
>
🌌 Cosmos
</button>
</div>
)
}
export default function WorkspaceView() {
if (!checkWebGL()) return <NoWebGL />
return <WorkspaceInner />
}