From 7b61f18e0015fbb2884edbec66b313579a9e9215 Mon Sep 17 00:00:00 2001 From: Tetardtek Date: Fri, 20 Mar 2026 21:47:49 +0100 Subject: [PATCH] =?UTF-8?q?feat:=20TierDashboard=20=E2=80=94=20pages=20tie?= =?UTF-8?q?r=20generees=20dynamiquement=20depuis=20brain-compose.yml?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- brain-ui/src/components/DocsView.tsx | 47 ++-- brain-ui/src/components/TierDashboard.tsx | 254 ++++++++++++++++++++++ 2 files changed, 286 insertions(+), 15 deletions(-) create mode 100644 brain-ui/src/components/TierDashboard.tsx diff --git a/brain-ui/src/components/DocsView.tsx b/brain-ui/src/components/DocsView.tsx index 975d1b9..4f1c60c 100644 --- a/brain-ui/src/components/DocsView.tsx +++ b/brain-ui/src/components/DocsView.tsx @@ -1,5 +1,6 @@ import { useState, useEffect, ReactNode } from 'react' import ReactMarkdown, { Components } from 'react-markdown' +import { TierComparatif, TierSingle } from './TierDashboard' interface DocFile { name: string @@ -200,21 +201,37 @@ export default function DocsView() { {/* Content */}
- {loading && ( -
- Chargement... -
- )} - {error && ( -
- {error} -
- )} - {!loading && !error && ( -
- {content} -
- )} + {(() => { + // Mode live + page tier โ†’ composant React dynamique + if (liveMode && activeDoc === 'vue-tiers') { + return
+ } + if (liveMode && activeDoc.startsWith('vue-')) { + const tierName = activeDoc.replace('vue-', '') + return
+ } + + // Mode standard โ€” markdown + return ( + <> + {loading && ( +
+ Chargement... +
+ )} + {error && ( +
+ {error} +
+ )} + {!loading && !error && ( +
+ {content} +
+ )} + + ) + })()}
) diff --git a/brain-ui/src/components/TierDashboard.tsx b/brain-ui/src/components/TierDashboard.tsx new file mode 100644 index 0000000..f70ade9 --- /dev/null +++ b/brain-ui/src/components/TierDashboard.tsx @@ -0,0 +1,254 @@ +import { useState, useEffect } from 'react' + +const API_BASE = import.meta.env.VITE_BRAIN_API ?? '' + +interface TierData { + description: string + coach_level: string + distillation: boolean + agents_new: string[] + agents_total: string[] + agents_count: number + sessions_new: string[] + sessions_total: string[] + sessions_count: number +} + +interface TiersResponse { + version: string + tiers: Record + tier_chain: string[] +} + +const TIER_COLORS: Record = { + free: { emoji: '๐ŸŸข', border: '#22c55e', bg: 'rgba(34,197,94,0.06)', text: '#4ade80' }, + featured: { emoji: '๐Ÿ”ต', border: '#3b82f6', bg: 'rgba(59,130,246,0.06)', text: '#60a5fa' }, + pro: { emoji: '๐ŸŸ ', border: '#f59e0b', bg: 'rgba(245,158,11,0.06)', text: '#fbbf24' }, + full: { emoji: '๐ŸŸฃ', border: '#a855f7', bg: 'rgba(168,85,247,0.06)', text: '#c084fc' }, +} + +const TIER_TITLES: Record = { + free: 'Tu forkes, ca marche', + featured: 'Le brain te connait', + pro: "L'atelier complet", + full: 'Ton brain, tes regles', +} + +const COACH_LABELS: Record = { + boot: 'Observation โ€” intervient sur risque critique uniquement', + full: 'Mentorat complet โ€” bilans, objectifs, progression', + L2: 'Mentorat long terme โ€” anticipe, challenge, milestones', +} + +// Comparatif โ€” vue multi-tiers +export function TierComparatif() { + const [data, setData] = useState(null) + const [error, setError] = useState(null) + + useEffect(() => { + fetch(`${API_BASE}/brain-compose/tiers`) + .then(r => r.json()) + .then(setData) + .catch(e => setError(e.message)) + }, []) + + if (error) return
Erreur: {error}
+ if (!data) return
Chargement...
+ + return ( +
+

Comparatif tiers

+

+ Donnees live depuis brain-compose.yml v{data.version} +

+ + {data.tier_chain.map(tierName => { + const tier = data.tiers[tierName] + if (!tier) return null + const colors = TIER_COLORS[tierName] + return ( +
+

+ + {colors.emoji} {tierName} โ€” {TIER_TITLES[tierName]} + +

+

+ {tier.agents_count} agents. {tier.sessions_count} sessions. + {tier.distillation && Distillation RAG active.} +

+

{tier.description}

+
+ ) + })} + +

Detail par tier

+ + {data.tier_chain.map(tierName => { + const tier = data.tiers[tierName] + if (!tier) return null + const colors = TIER_COLORS[tierName] + return ( +
+

{colors.emoji} {tierName}

+ +

Sessions ({tier.sessions_count}) :

+

{tier.sessions_total.join(' ยท ')}

+ +

Agents ({tier.agents_count}) :

+

+ {tier.agents_total.join(' ยท ')} +

+ +

+ Coach : {COACH_LABELS[tier.coach_level] || tier.coach_level} +

+
+ ) + })} +
+ ) +} + +// Vue single tier +export function TierSingle({ tierName }: { tierName: string }) { + const [data, setData] = useState(null) + const [error, setError] = useState(null) + + useEffect(() => { + fetch(`${API_BASE}/brain-compose/tiers`) + .then(r => r.json()) + .then(setData) + .catch(e => setError(e.message)) + }, []) + + if (error) return
Erreur: {error}
+ if (!data) return
Chargement...
+ + const tier = data.tiers[tierName] + if (!tier) return
Tier "{tierName}" introuvable
+ + const colors = TIER_COLORS[tierName] + const chain = data.tier_chain + const tierIndex = chain.indexOf(tierName) + + // Trouver les agents/sessions "nouveaux" par rapport au tier precedent + const prevTier = tierIndex > 0 ? data.tiers[chain[tierIndex - 1]] : null + const prevAgents = new Set(prevTier?.agents_total || []) + const newAgents = tier.agents_total.filter(a => !prevAgents.has(a)) + const prevSessions = new Set(prevTier?.sessions_total || []) + const newSessions = tier.sessions_total.filter(s => !prevSessions.has(s)) + + // Tier suivant pour le "ce que tu n'as pas encore" + const nextTierName = tierIndex < chain.length - 1 ? chain[tierIndex + 1] : null + const nextTier = nextTierName ? data.tiers[nextTierName] : null + + return ( +
+

{colors.emoji} {tierName} โ€” Ce que tu as

+

+ Donnees live depuis brain-compose.yml v{data.version} +

+ +
+

+ + {tier.agents_count} agents. {tier.sessions_count} sessions. + + {tier.distillation && Distillation RAG active.} +

+

{tier.description}

+
+ +
+ +

Sessions {tierIndex > 0 && newSessions.length > 0 ? `(+${newSessions.length} nouvelles)` : ''}

+ {tierIndex > 0 && newSessions.length > 0 && ( + <> +

Ajoutees a ce tier :

+
    {newSessions.map(s =>
  • {s}
  • )}
+ + )} +

Toutes les sessions disponibles :

+

{tier.sessions_total.join(' ยท ')}

+ +
+ +

Agents {tierIndex > 0 && newAgents.length > 0 ? `(+${newAgents.length} nouveaux)` : ''}

+ {tierIndex > 0 && newAgents.length > 0 && ( + <> +

Ajoutes a ce tier :

+

+ {newAgents.join(' ยท ')} +

+ + )} +

Tous les agents disponibles ({tier.agents_count}) :

+

+ {tier.agents_total.join(' ยท ')} +

+ +
+ +

Coach

+
+

{tier.coach_level} โ€” {COACH_LABELS[tier.coach_level] || tier.coach_level}

+
+ + {nextTier && nextTierName && ( + <> +
+

Ce que tu n'as pas encore

+
+

+ + {TIER_COLORS[nextTierName].emoji} {nextTierName} + + {' '}te donne : +{nextTier.agents_count - tier.agents_count} agents, +{nextTier.sessions_count - tier.sessions_count} sessions. +

+

{nextTier.description}

+
+ + )} + + {tierName === 'full' && ( + <> +
+

Tu as tout

+

C'est ton brain. Tu peux modifier n'importe quel agent, forger les tiens, restructurer le kernel. Le seul gate c'est toi.

+ + )} +
+ ) +}