feat: synchroniser la configuration IA active du dashboard
All checks were successful
Validation Dashboard / Tests backend et build frontend (push) Successful in 43s
All checks were successful
Validation Dashboard / Tests backend et build frontend (push) Successful in 43s
This commit is contained in:
@@ -222,7 +222,7 @@ export default function AppCard({ app, commits, onRefresh }) {
|
||||
{/* Moteurs IA déclarés dans app.json */}
|
||||
<div>
|
||||
<span className="text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Moteur IA
|
||||
{app.ai?.runtimeStatus === 'active' ? 'Moteur IA actif' : 'Moteur IA'}
|
||||
</span>
|
||||
<div className="mt-1.5 space-y-2">
|
||||
{aiEngines.length > 0 ? aiEngines.map((engine, index) => (
|
||||
@@ -242,15 +242,22 @@ export default function AppCard({ app, commits, onRefresh }) {
|
||||
<p className="text-xs text-primary-400 break-words">
|
||||
{engine.model}{engine.role ? ` — ${engine.role}` : ''}
|
||||
</p>
|
||||
<p className="mt-0.5 text-[11px] leading-snug text-gray-500">
|
||||
<p
|
||||
className={`mt-0.5 text-[11px] leading-snug ${
|
||||
engine.source === 'referential' ? 'text-emerald-300' : 'text-orange-300'
|
||||
}`}
|
||||
>
|
||||
{engine.source === 'referential'
|
||||
? 'Utilisation du référentiel IA'
|
||||
: "Moteur IA configuré dans l'applicatif"}
|
||||
</p>
|
||||
</div>
|
||||
)) : (
|
||||
<p className="text-sm text-gray-600">Non déclaré</p>
|
||||
<p className="text-sm text-gray-600">
|
||||
{app.ai?.activeConfigurationPath ? 'Configuration active indisponible' : 'Non déclaré'}
|
||||
</p>
|
||||
)}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -19,6 +19,16 @@ import {
|
||||
Clock,
|
||||
Zap,
|
||||
Package,
|
||||
Bot,
|
||||
BrainCircuit,
|
||||
Workflow,
|
||||
ShieldCheck,
|
||||
Route,
|
||||
ArrowDown,
|
||||
CircleAlert,
|
||||
LockKeyhole,
|
||||
ServerCog,
|
||||
Sparkles,
|
||||
} from 'lucide-react';
|
||||
|
||||
/* ─────────────────────────────────────────────
|
||||
@@ -95,6 +105,22 @@ const RECETTE = {
|
||||
url: 'contacts.recette.santinova-soft.org',
|
||||
href: 'https://contacts.recette.santinova-soft.org',
|
||||
},
|
||||
{
|
||||
icon: BrainCircuit,
|
||||
color: 'purple',
|
||||
name: 'Référentiel IA Santinova',
|
||||
url: 'referentiel-ia.recette.santinova-soft.org',
|
||||
desc: 'Décisions, contrats sécurisés et budgets IA',
|
||||
href: 'https://referentiel-ia.recette.santinova-soft.org',
|
||||
},
|
||||
{
|
||||
icon: ServerCog,
|
||||
color: 'cyan',
|
||||
name: 'Moteur IA local',
|
||||
url: 'llm-local.recette.santinova-soft.org',
|
||||
desc: 'Ollama privé · modèle local disponible',
|
||||
href: 'https://llm-local.recette.santinova-soft.org',
|
||||
},
|
||||
{
|
||||
icon: '🚗',
|
||||
color: 'yellow',
|
||||
@@ -248,6 +274,74 @@ function SectionTitle({ children, color = 'amber' }) {
|
||||
return <h3 className={`text-xs font-bold uppercase tracking-widest ${cls} mb-3`}>{children}</h3>;
|
||||
}
|
||||
|
||||
function WorkflowNode({ icon: Icon, eyebrow, title, description, tone = 'blue' }) {
|
||||
const tones = {
|
||||
blue: 'border-blue-500/30 bg-blue-500/10 text-blue-300',
|
||||
purple: 'border-purple-500/30 bg-purple-500/10 text-purple-300',
|
||||
teal: 'border-teal-500/30 bg-teal-500/10 text-teal-300',
|
||||
emerald: 'border-emerald-500/30 bg-emerald-500/10 text-emerald-300',
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`rounded-2xl border p-4 ${tones[tone]}`}>
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="rounded-xl bg-dark-950/35 p-2.5"><Icon className="h-5 w-5" aria-hidden="true" /></div>
|
||||
<div className="min-w-0">
|
||||
<p className="text-[10px] font-bold uppercase tracking-[0.16em] opacity-70">{eyebrow}</p>
|
||||
<h4 className="mt-1 text-sm font-bold text-white">{title}</h4>
|
||||
<p className="mt-1 text-xs leading-5 text-gray-300">{description}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function AiDecisionWorkflow() {
|
||||
const modes = [
|
||||
{ icon: Sparkles, title: 'Recommandation', description: 'Le Référentiel retient le moteur adapté au cas d’usage et aux règles de coût.', tone: 'blue' },
|
||||
{ icon: Route, title: 'Choix manuel', description: 'L’administrateur sélectionne un moteur externe parmi ceux autorisés.', tone: 'purple' },
|
||||
{ icon: ServerCog, title: 'Moteur IA local', description: 'L’application utilise le modèle Ollama privé disponible.', tone: 'teal' },
|
||||
];
|
||||
|
||||
return (
|
||||
<section className="rounded-2xl border border-cyan-500/25 bg-gradient-to-br from-cyan-500/[0.07] via-dark-900 to-purple-500/[0.06] p-6" aria-labelledby="ia-workflow-title">
|
||||
<div className="flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between">
|
||||
<div className="flex gap-3">
|
||||
<div className="flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-xl bg-cyan-500/15 text-cyan-300"><Workflow className="h-5 w-5" aria-hidden="true" /></div>
|
||||
<div>
|
||||
<p className="text-xs font-bold uppercase tracking-[0.16em] text-cyan-400">Parcours applicatif</p>
|
||||
<h2 id="ia-workflow-title" className="mt-1 text-xl font-bold text-white">Quel moteur IA utilise chaque application ?</h2>
|
||||
<p className="mt-2 max-w-3xl text-sm leading-6 text-gray-300">Le Référentiel IA décide et protège le parcours ; le Moteur IA local exécute les demandes privées sur Ollama lorsque ce mode est retenu.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 rounded-xl border border-emerald-500/25 bg-emerald-500/10 px-3 py-2 text-xs text-emerald-300"><ShieldCheck className="h-4 w-4" aria-hidden="true" />Aucune clé fournisseur ni URL Ollama privée</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-7 flex flex-col items-stretch gap-3 lg:flex-row lg:items-center lg:justify-center">
|
||||
<div className="w-full max-w-sm lg:w-72"><WorkflowNode icon={Bot} eyebrow="1 · demande" title="Application cliente" description="Envoie sa demande à la passerelle du Référentiel IA." tone="blue" /></div>
|
||||
<ArrowRight className="mx-auto h-5 w-5 rotate-90 text-cyan-400 lg:rotate-0" aria-hidden="true" />
|
||||
<div className="w-full max-w-sm lg:w-80"><WorkflowNode icon={BrainCircuit} eyebrow="2 · décision" title="Référentiel IA Santinova" description="Applique le choix, les autorisations, les budgets et la traçabilité." tone="purple" /></div>
|
||||
</div>
|
||||
|
||||
<div className="mx-auto flex h-8 w-px items-center justify-center bg-gradient-to-b from-purple-400 to-cyan-400" aria-hidden="true"><ArrowDown className="mt-6 h-4 w-4 text-cyan-300" /></div>
|
||||
<div className="grid gap-3 md:grid-cols-3" role="list" aria-label="Modes de décision du moteur IA">
|
||||
{modes.map((mode) => <div key={mode.title} role="listitem"><WorkflowNode icon={mode.icon} eyebrow="3 · mode de décision" title={mode.title} description={mode.description} tone={mode.tone} /></div>)}
|
||||
</div>
|
||||
|
||||
<div className="mt-4 grid gap-4 lg:grid-cols-[1fr_auto_1fr] lg:items-stretch">
|
||||
<WorkflowNode icon={ServerCog} eyebrow="4a · exécution privée" title="Moteur IA local" description="Le service exécute localement sur Ollama. Les requêtes et jetons locaux disposent de plafonds dédiés." tone="teal" />
|
||||
<div className="flex items-center justify-center" aria-hidden="true"><ArrowRight className="h-5 w-5 rotate-90 text-gray-500 lg:rotate-0" /></div>
|
||||
<WorkflowNode icon={Globe} eyebrow="4b · exécution externe" title="Moteur externe autorisé" description="La passerelle appelle exclusivement le fournisseur prévu par la décision ou le choix manuel." tone="emerald" />
|
||||
</div>
|
||||
|
||||
<div className="mt-4 grid gap-3 lg:grid-cols-[1fr_1.35fr]">
|
||||
<div className="flex gap-3 rounded-xl border border-amber-500/25 bg-amber-500/[0.08] p-4"><CircleAlert className="h-5 w-5 flex-shrink-0 text-amber-400" aria-hidden="true" /><div><p className="text-sm font-semibold text-amber-200">Basculement externe autorisé</p><p className="mt-1 text-xs leading-5 text-gray-300">Uniquement si le moteur local est indisponible, si l’option est activée et vers le moteur externe autorisé choisi par l’administrateur.</p></div></div>
|
||||
<div className="flex gap-3 rounded-xl border border-dark-700 bg-dark-950/40 p-4"><LockKeyhole className="h-5 w-5 flex-shrink-0 text-cyan-300" aria-hidden="true" /><div><p className="text-sm font-semibold text-white">Plafonds séparés et traces de décision</p><p className="mt-1 text-xs leading-5 text-gray-400">Les consommations locales et externes restent séparées. Une limite locale atteinte bloque l’appel local sans déclencher de repli.</p></div></div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
/* ─────────────────────────────────────────────
|
||||
Page principale
|
||||
───────────────────────────────────────────── */
|
||||
@@ -267,7 +361,7 @@ export default function DocumentationPage() {
|
||||
</div>
|
||||
<div className="ml-auto flex items-center gap-2 px-3 py-1.5 rounded-full bg-emerald-500/10 border border-emerald-500/20">
|
||||
<Clock className="w-3.5 h-3.5 text-emerald-400" />
|
||||
<span className="text-xs text-emerald-400">Mis à jour le 04 Mai 2026</span>
|
||||
<span className="text-xs text-emerald-400">Mis à jour le 05 Septembre 2026</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -300,6 +394,8 @@ export default function DocumentationPage() {
|
||||
)}
|
||||
</div>
|
||||
|
||||
<AiDecisionWorkflow />
|
||||
|
||||
{/* ── Grille Recette / CI-CD / Production ── */}
|
||||
<div className="grid grid-cols-1 xl:grid-cols-[1fr_auto_1fr] gap-6">
|
||||
|
||||
|
||||
28
src/frontend/test/documentation-ai.test.mjs
Normal file
28
src/frontend/test/documentation-ai.test.mjs
Normal file
@@ -0,0 +1,28 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { readFile } from 'node:fs/promises';
|
||||
|
||||
const sourcePath = new URL('../src/pages/DocumentationPage.jsx', import.meta.url);
|
||||
|
||||
test('la documentation présente les deux applications IA et les trois modes de décision', async () => {
|
||||
const source = await readFile(sourcePath, 'utf8');
|
||||
|
||||
assert.match(source, /Référentiel IA Santinova/);
|
||||
assert.match(source, /Moteur IA local/);
|
||||
assert.match(source, /Recommandation/);
|
||||
assert.match(source, /Choix manuel/);
|
||||
assert.match(source, /Moteur IA local/);
|
||||
assert.match(source, /Basculement externe autorisé/);
|
||||
});
|
||||
|
||||
test('le workflow rappelle la séparation des budgets et des accès internes', async () => {
|
||||
const source = await readFile(sourcePath, 'utf8');
|
||||
|
||||
assert.match(source, /Plafonds séparés/);
|
||||
assert.match(source, /Aucune clé fournisseur ni URL Ollama privée/);
|
||||
assert.match(source, /aria-labelledby="ia-workflow-title"/);
|
||||
assert.match(source, /aria-label="Modes de décision du moteur IA"/);
|
||||
assert.match(source, /referentiel-ia\.recette\.santinova-soft\.org/);
|
||||
assert.match(source, /llm-local\.recette\.santinova-soft\.org/);
|
||||
assert.doesNotMatch(source, /http:\/\/ollama:11434/);
|
||||
});
|
||||
Reference in New Issue
Block a user