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:
64
brain-ui/src/components/workspace/CosmosBackground.tsx
Normal file
64
brain-ui/src/components/workspace/CosmosBackground.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
39
brain-ui/src/components/workspace/GateOctahedron.tsx
Normal file
39
brain-ui/src/components/workspace/GateOctahedron.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
43
brain-ui/src/components/workspace/StepSphere.tsx
Normal file
43
brain-ui/src/components/workspace/StepSphere.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
91
brain-ui/src/components/workspace/WorkflowConstellation.tsx
Normal file
91
brain-ui/src/components/workspace/WorkflowConstellation.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
149
brain-ui/src/components/workspace/WorkspaceInfoPanel.tsx
Normal file
149
brain-ui/src/components/workspace/WorkspaceInfoPanel.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
59
brain-ui/src/components/workspace/WorkspaceMetrics.tsx
Normal file
59
brain-ui/src/components/workspace/WorkspaceMetrics.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
120
brain-ui/src/components/workspace/WorkspaceView.tsx
Normal file
120
brain-ui/src/components/workspace/WorkspaceView.tsx
Normal 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 />
|
||||
}
|
||||
Reference in New Issue
Block a user