From de4cd85798665b9080ce41266d3ad3a2ae03d0de Mon Sep 17 00:00:00 2001 From: Tetardtek Date: Fri, 20 Mar 2026 21:36:52 +0100 Subject: [PATCH] =?UTF-8?q?feat:=20docs=20live=20=E2=80=94=20brain-engine?= =?UTF-8?q?=20sert=20docs/=20en=20API,=20DocsView=20fetch=20dynamique=20av?= =?UTF-8?q?ec=20fallback=20statique?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- brain-engine/server.py | 76 +++++++++++++++++- brain-ui/src/components/DocsView.tsx | 116 +++++++++++++++++++++------ 2 files changed, 165 insertions(+), 27 deletions(-) diff --git a/brain-engine/server.py b/brain-engine/server.py index cfade94..ca3c6f1 100644 --- a/brain-engine/server.py +++ b/brain-engine/server.py @@ -211,7 +211,7 @@ _ws_clients: list[WebSocket] = [] # Racine du brain (un niveau au-dessus de brain-engine/) BRAIN_ROOT = Path(__file__).parent.parent -app = FastAPI(title='Brain-as-a-Service', version='BE-4', docs_url='/docs') +app = FastAPI(title='Brain-as-a-Service', version='BE-4', docs_url='/api-docs') # ── Montage brain-ui static (si build disponible) ──────────────────────────── @@ -275,6 +275,80 @@ def health(): return JSONResponse(status_code=503, content={'status': 'error', 'detail': str(e), 'uptime': uptime}) +# ── Docs live — sert docs/*.md depuis le filesystem ──────────────────────────── + +@app.get('/docs') +def docs_list(): + """Liste les fichiers docs/*.md avec métadonnées (frontmatter group/label).""" + docs_dir = BRAIN_ROOT / 'docs' + if not docs_dir.is_dir(): + return {'docs': []} + + results = [] + for f in sorted(docs_dir.glob('*.md')): + if f.name == 'README.md': + continue + # Extraire le group depuis le contenu (heuristique basée sur le nom) + name = f.stem + group = _guess_doc_group(name) + label = _guess_doc_label(name) + results.append({ + 'name': name, + 'label': label, + 'group': group, + 'path': f'/docs/{f.name}', + 'size': f.stat().st_size, + 'modified': datetime.fromtimestamp(f.stat().st_mtime, tz=timezone.utc).isoformat(), + }) + return {'docs': results} + + +@app.get('/docs/{filename}') +def docs_read(filename: str): + """Retourne le contenu brut d'un fichier docs/*.md.""" + # Sécurité : pas de path traversal + if '/' in filename or '..' in filename: + raise HTTPException(status_code=400, detail='Nom de fichier invalide') + target = BRAIN_ROOT / 'docs' / filename + if not target.exists() or not target.suffix == '.md': + raise HTTPException(status_code=404, detail=f'{filename} introuvable') + content = target.read_text(encoding='utf-8') + # Strip frontmatter + content = re.sub(r'^---[\s\S]*?---\n*', '', content) + return JSONResponse(content={'name': target.stem, 'content': content}) + + +def _guess_doc_group(name: str) -> str: + """Heuristique pour grouper les docs par famille.""" + if name.startswith('agents'): + return 'Agents' + if name.startswith('vue-'): + return 'Vues' + return 'Guides' + + +def _guess_doc_label(name: str) -> str: + """Heuristique pour le label sidebar.""" + labels = { + 'getting-started': 'Demarrer', + 'architecture': 'Architecture', + 'sessions': 'Sessions', + 'workflows': 'Workflows', + 'satellites': 'Satellites', + 'brain-engine-guide': 'Brain-engine', + 'agents': "Vue d'ensemble", + 'agents-code': 'Code & Qualite', + 'agents-infra': 'Infra & Deploy', + 'agents-brain': 'Brain & Systeme', + 'vue-tiers': 'Comparatif', + 'vue-free': '🟢 free', + 'vue-featured': '🔵 featured', + 'vue-pro': '🟠 pro', + 'vue-full': '🟣 full', + } + return labels.get(name, name.replace('-', ' ').title()) + + @app.get('/search') def search( q: str = Query(..., description='Requête en langage naturel'), diff --git a/brain-ui/src/components/DocsView.tsx b/brain-ui/src/components/DocsView.tsx index e4b14e0..975d1b9 100644 --- a/brain-ui/src/components/DocsView.tsx +++ b/brain-ui/src/components/DocsView.tsx @@ -5,17 +5,20 @@ interface DocFile { name: string label: string path: string - group?: string + group: string } -const DOCS: DocFile[] = [ +const API_BASE = import.meta.env.VITE_BRAIN_API ?? '' + +// Fallback statique — utilisé si brain-engine n'est pas dispo +const STATIC_DOCS: DocFile[] = [ { name: 'getting-started', label: 'Demarrer', path: import.meta.env.BASE_URL + 'docs/getting-started.md', group: 'Guides' }, { name: 'architecture', label: 'Architecture', path: import.meta.env.BASE_URL + 'docs/architecture.md', group: 'Guides' }, { name: 'sessions', label: 'Sessions', path: import.meta.env.BASE_URL + 'docs/sessions.md', group: 'Guides' }, + { name: 'workflows', label: 'Workflows', path: import.meta.env.BASE_URL + 'docs/workflows.md', group: 'Guides' }, { name: 'satellites', label: 'Satellites', path: import.meta.env.BASE_URL + 'docs/satellites.md', group: 'Guides' }, { name: 'brain-engine-guide', label: 'Brain-engine', path: import.meta.env.BASE_URL + 'docs/brain-engine-guide.md', group: 'Guides' }, - { name: 'workflows', label: 'Workflows', path: import.meta.env.BASE_URL + 'docs/workflows.md', group: 'Guides' }, - { name: 'agents', label: 'Vue d\'ensemble', path: import.meta.env.BASE_URL + 'docs/agents.md', group: 'Agents' }, + { name: 'agents', label: "Vue d'ensemble", path: import.meta.env.BASE_URL + 'docs/agents.md', group: 'Agents' }, { name: 'agents-code', label: 'Code & Qualite', path: import.meta.env.BASE_URL + 'docs/agents-code.md', group: 'Agents' }, { name: 'agents-infra', label: 'Infra & Deploy', path: import.meta.env.BASE_URL + 'docs/agents-infra.md', group: 'Agents' }, { name: 'agents-brain', label: 'Brain & Systeme', path: import.meta.env.BASE_URL + 'docs/agents-brain.md', group: 'Agents' }, @@ -26,6 +29,9 @@ const DOCS: DocFile[] = [ { name: 'vue-full', label: '🟣 full', path: import.meta.env.BASE_URL + 'docs/vue-full.md', group: 'Vues' }, ] +// Order for consistent sidebar display +const GROUP_ORDER = ['Guides', 'Agents', 'Vues'] + // Detect tier markers in blockquote content and apply CSS class const TIER_MARKERS: Record = { '\u{1F7E2}': 'tier-free', // 🟢 @@ -58,42 +64,91 @@ const mdComponents: Components = { } export default function DocsView() { + const [docs, setDocs] = useState(STATIC_DOCS) const [activeDoc, setActiveDoc] = useState('getting-started') const [content, setContent] = useState('') const [loading, setLoading] = useState(true) const [error, setError] = useState(null) + const [liveMode, setLiveMode] = useState(false) + // Fetch docs list from brain-engine API — fallback to static useEffect(() => { - const doc = DOCS.find((d) => d.name === activeDoc) - if (!doc) return + fetch(`${API_BASE}/docs`) + .then((res) => { + if (!res.ok) throw new Error('API indisponible') + return res.json() + }) + .then((data) => { + if (data.docs && data.docs.length > 0) { + setDocs(data.docs) + setLiveMode(true) + } + }) + .catch(() => { + // Silencieux — on reste sur les docs statiques + }) + }, []) + // Fetch active doc content + useEffect(() => { setLoading(true) setError(null) - fetch(doc.path) - .then((res) => { - if (!res.ok) throw new Error(`${res.status}`) - return res.text() - }) - .then((text) => { - const stripped = text.replace(/^---[\s\S]*?---\n*/, '') - setContent(stripped) - setLoading(false) - }) - .catch((err) => { - setError(`Impossible de charger ${doc.path}: ${err.message}`) - setLoading(false) - }) - }, [activeDoc]) + if (liveMode) { + // Mode live — fetch depuis l'API brain-engine + fetch(`${API_BASE}/docs/${activeDoc}.md`) + .then((res) => { + if (!res.ok) throw new Error(`${res.status}`) + return res.json() + }) + .then((data) => { + setContent(data.content) + setLoading(false) + }) + .catch((err) => { + setError(`Impossible de charger ${activeDoc}: ${err.message}`) + setLoading(false) + }) + } else { + // Mode statique — fetch depuis les fichiers dist/ + const doc = docs.find((d) => d.name === activeDoc) + if (!doc) { setLoading(false); return } - // Group docs by group - const groups = DOCS.reduce>((acc, doc) => { + fetch(doc.path) + .then((res) => { + if (!res.ok) throw new Error(`${res.status}`) + return res.text() + }) + .then((text) => { + const stripped = text.replace(/^---[\s\S]*?---\n*/, '') + setContent(stripped) + setLoading(false) + }) + .catch((err) => { + setError(`Impossible de charger ${doc.path}: ${err.message}`) + setLoading(false) + }) + } + }, [activeDoc, liveMode, docs]) + + // Group docs + const groups = docs.reduce>((acc, doc) => { const g = doc.group || 'Autres' if (!acc[g]) acc[g] = [] acc[g].push(doc) return acc }, {}) + // Sort groups by defined order + const sortedGroups = GROUP_ORDER + .filter((g) => groups[g]) + .map((g) => [g, groups[g]] as [string, DocFile[]]) + + // Add any groups not in ORDER + Object.entries(groups).forEach(([g, d]) => { + if (!GROUP_ORDER.includes(g)) sortedGroups.push([g, d]) + }) + return (
{/* Sidebar docs */} @@ -101,13 +156,22 @@ export default function DocsView() { className="flex flex-col flex-shrink-0 border-r overflow-y-auto" style={{ width: 200, borderColor: '#2a2a2a', background: '#141414' }} > -
+
Documentation + {liveMode && ( + + live + + )}