428 lines
19 KiB
HTML
428 lines
19 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="fr">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<title>WEVIA · Claude Pattern Dashboard</title>
|
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
<script src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
|
|
<script src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
|
|
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
|
|
<script src="https://cdn.tailwindcss.com"></script>
|
|
<style>
|
|
:root{--bg:#0a0e1a;--card:rgba(22,27,46,0.6);--border:rgba(255,255,255,0.08);--text:#e4e4f0;--muted:#8b93a7;--accent:#7c6bf0;--success:#10b981;--warning:#f59e0b;--danger:#ef4444;--blue:#3b82f6}
|
|
body{background:var(--bg);color:var(--text);font-family:ui-sans-serif,system-ui,-apple-system,"Segoe UI",sans-serif;margin:0;overflow-x:hidden}
|
|
body::before{content:"";position:fixed;inset:0;background:radial-gradient(circle at 20% 30%,rgba(124,107,240,0.15),transparent 50%),radial-gradient(circle at 80% 70%,rgba(16,185,129,0.08),transparent 50%);pointer-events:none;z-index:-1}
|
|
.card{background:var(--card);backdrop-filter:blur(20px);border:1px solid var(--border);border-radius:16px;transition:all 0.3s}
|
|
.card:hover{border-color:rgba(124,107,240,0.4)}
|
|
.btn{background:linear-gradient(135deg,var(--accent),#5a47d6);color:#fff;padding:10px 20px;border-radius:10px;font-weight:600;cursor:pointer;border:0;transition:all 0.2s}
|
|
.btn:hover{transform:translateY(-2px);box-shadow:0 10px 30px rgba(124,107,240,0.4)}
|
|
.btn:disabled{opacity:0.5;cursor:not-allowed;transform:none}
|
|
.input{background:rgba(255,255,255,0.04);border:1px solid var(--border);color:var(--text);padding:12px 16px;border-radius:10px;width:100%;font-size:14px}
|
|
.input:focus{outline:none;border-color:var(--accent);box-shadow:0 0 0 3px rgba(124,107,240,0.15)}
|
|
.pulse{animation:pulse 1.5s ease-in-out infinite}
|
|
@keyframes pulse{0%,100%{opacity:0.4}50%{opacity:1}}
|
|
.spin{animation:spin 1s linear infinite}
|
|
@keyframes spin{to{transform:rotate(360deg)}}
|
|
.fade-in{animation:fade .4s}
|
|
@keyframes fade{from{opacity:0;transform:translateY(6px)}to{opacity:1;transform:translateY(0)}}
|
|
.chip{background:rgba(255,255,255,0.05);border:1px solid var(--border);border-radius:20px;padding:4px 12px;font-size:12px;display:inline-flex;align-items:center;gap:6px}
|
|
.chip-success{border-color:var(--success);color:var(--success);background:rgba(16,185,129,0.08)}
|
|
.chip-running{border-color:var(--warning);color:var(--warning);background:rgba(245,158,11,0.08)}
|
|
.chip-pending{color:var(--muted)}
|
|
.monospace{font-family:"Monaco","Menlo",monospace;font-size:13px}
|
|
.scrollbar::-webkit-scrollbar{width:6px}
|
|
.scrollbar::-webkit-scrollbar-track{background:rgba(255,255,255,0.04)}
|
|
.scrollbar::-webkit-scrollbar-thumb{background:var(--border);border-radius:3px}
|
|
.gradient-text{background:linear-gradient(135deg,var(--accent),var(--success));-webkit-background-clip:text;background-clip:text;color:transparent}
|
|
.phase-dot{width:8px;height:8px;border-radius:50%;background:var(--border)}
|
|
.phase-dot.active{background:var(--warning);box-shadow:0 0 12px var(--warning)}
|
|
.phase-dot.done{background:var(--success)}
|
|
.progress-bar{height:4px;background:rgba(255,255,255,0.05);border-radius:2px;overflow:hidden}
|
|
.progress-fill{height:100%;background:linear-gradient(90deg,var(--accent),var(--success));transition:width .3s}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div id="root"></div>
|
|
<script type="text/babel" data-presets="react">
|
|
const { useState, useEffect, useRef, useCallback } = React;
|
|
|
|
function Header({ sessionId, phaseCount, totalMs, confidence }) {
|
|
return (
|
|
<header className="border-b border-white/5 backdrop-blur-xl bg-black/20 sticky top-0 z-50">
|
|
<div className="max-w-7xl mx-auto px-6 py-4 flex items-center justify-between">
|
|
<div className="flex items-center gap-3">
|
|
<div className="w-10 h-10 rounded-xl bg-gradient-to-br from-purple-500 to-blue-500 flex items-center justify-center font-bold">W</div>
|
|
<div>
|
|
<h1 className="text-lg font-bold gradient-text">WEVIA Claude Pattern</h1>
|
|
<p className="text-xs" style={{color:"var(--muted)"}}>Thinking → Plan → RAG → Execute → Test → Critique → Result</p>
|
|
</div>
|
|
</div>
|
|
<div className="flex items-center gap-4">
|
|
{sessionId && <span className="chip monospace">{sessionId.substring(0,16)}</span>}
|
|
{phaseCount > 0 && <span className="chip chip-success">{phaseCount}/7 phases</span>}
|
|
{totalMs > 0 && <span className="chip">{totalMs}ms</span>}
|
|
{confidence > 0 && <span className="chip chip-success">confiance {Math.round(confidence*100)}%</span>}
|
|
</div>
|
|
</div>
|
|
</header>
|
|
);
|
|
}
|
|
|
|
function PhaseTracker({ phases, activePhase }) {
|
|
const all = ["thinking","plan","rag","execute","test","result","critique"];
|
|
return (
|
|
<div className="card p-4 mb-4">
|
|
<div className="flex items-center justify-between gap-3">
|
|
{all.map((p,i) => (
|
|
<React.Fragment key={p}>
|
|
<div className="flex flex-col items-center gap-1.5">
|
|
<div className={`phase-dot ${phases[p]==="done"?"done":(phases[p]==="running"||activePhase===p?"active":"")}`}></div>
|
|
<span className={`text-xs capitalize ${phases[p]==="done"?"text-green-400":(activePhase===p?"text-yellow-400":"text-gray-500")}`}>{p}</span>
|
|
</div>
|
|
{i < all.length-1 && <div className="flex-1 h-px bg-white/5"></div>}
|
|
</React.Fragment>
|
|
))}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function ThinkingCard({ text, status, elapsed }) {
|
|
return (
|
|
<div className="card p-5 fade-in">
|
|
<div className="flex items-center justify-between mb-3">
|
|
<div className="flex items-center gap-2">
|
|
<span className="text-xl">🧠</span>
|
|
<h3 className="font-bold">Thinking</h3>
|
|
{status === "running" && <span className="chip chip-running pulse">en cours...</span>}
|
|
{status === "done" && <span className="chip chip-success">✓ {elapsed}ms</span>}
|
|
</div>
|
|
</div>
|
|
<p className="text-sm leading-relaxed" style={{color:"var(--muted)"}}>{text || "..."}<span className={status==="running"?"pulse":""}>{status==="running"?"▊":""}</span></p>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function PlanCard({ steps, elapsed, executions }) {
|
|
if (!steps || !steps.length) return null;
|
|
return (
|
|
<div className="card p-5 fade-in">
|
|
<div className="flex items-center justify-between mb-4">
|
|
<div className="flex items-center gap-2">
|
|
<span className="text-xl">📋</span>
|
|
<h3 className="font-bold">Plan d'exécution</h3>
|
|
<span className="chip">{steps.length} étapes</span>
|
|
</div>
|
|
{elapsed && <span className="chip chip-success">✓ {elapsed}ms</span>}
|
|
</div>
|
|
<div className="space-y-2">
|
|
{steps.map((s,i) => {
|
|
const exec = executions[s.n] || {};
|
|
const status = exec.status || "pending";
|
|
return (
|
|
<div key={i} className="flex items-start gap-3 p-3 rounded-lg" style={{background:"rgba(255,255,255,0.02)"}}>
|
|
<div className={`w-8 h-8 rounded-full flex items-center justify-center text-xs font-bold flex-shrink-0 ${status==="done"?"bg-green-500/20 text-green-400":(status==="running"?"bg-yellow-500/20 text-yellow-400 pulse":"bg-white/5 text-gray-500")}`}>
|
|
{status==="done"?"✓":(status==="running"?"⟳":s.n)}
|
|
</div>
|
|
<div className="flex-1">
|
|
<div className="font-semibold text-sm">{s.title}</div>
|
|
<div className="text-xs" style={{color:"var(--muted)"}}>{s.action}</div>
|
|
</div>
|
|
{exec.elapsed_ms && <span className="chip text-xs">{exec.elapsed_ms}ms</span>}
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function RagCard({ collections, hits, elapsed, status }) {
|
|
return (
|
|
<div className="card p-5 fade-in">
|
|
<div className="flex items-center justify-between mb-3">
|
|
<div className="flex items-center gap-2">
|
|
<span className="text-xl">📚</span>
|
|
<h3 className="font-bold">RAG · Qdrant</h3>
|
|
{status === "querying" && <span className="chip chip-running pulse">recherche...</span>}
|
|
{status === "done" && <span className="chip chip-success">✓ {hits.length} hits · {elapsed}ms</span>}
|
|
</div>
|
|
</div>
|
|
{collections > 0 && <p className="text-xs mb-3" style={{color:"var(--muted)"}}>{collections} collections disponibles · {hits.length} pertinentes</p>}
|
|
<div className="flex flex-wrap gap-2">
|
|
{hits.map((h,i) => (
|
|
<span key={i} className="chip chip-success monospace">
|
|
📦 {h.collection}
|
|
{h.keyword && <span className="opacity-60">← {h.keyword}</span>}
|
|
</span>
|
|
))}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function TestCard({ checks, elapsed }) {
|
|
if (!checks) return null;
|
|
return (
|
|
<div className="card p-5 fade-in">
|
|
<div className="flex items-center justify-between mb-3">
|
|
<div className="flex items-center gap-2">
|
|
<span className="text-xl">🧪</span>
|
|
<h3 className="font-bold">Tests de validation</h3>
|
|
{elapsed && <span className="chip chip-success">✓ {elapsed}ms</span>}
|
|
</div>
|
|
</div>
|
|
<div className="grid grid-cols-3 gap-2">
|
|
{Object.entries(checks).map(([k,v]) => (
|
|
<div key={k} className="p-3 rounded-lg text-center" style={{background:"rgba(255,255,255,0.02)"}}>
|
|
<div className={`text-2xl mb-1 ${v===true?"text-green-400":(v===false?"text-red-400":"text-gray-500 pulse")}`}>
|
|
{v===true?"✓":(v===false?"✗":"…")}
|
|
</div>
|
|
<div className="text-xs capitalize" style={{color:"var(--muted)"}}>{k.replace(/_/g," ")}</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function ResultCard({ text, words }) {
|
|
if (!text) return null;
|
|
return (
|
|
<div className="card p-5 fade-in" style={{borderColor:"rgba(16,185,129,0.3)"}}>
|
|
<div className="flex items-center justify-between mb-3">
|
|
<div className="flex items-center gap-2">
|
|
<span className="text-xl">✨</span>
|
|
<h3 className="font-bold gradient-text">Réponse finale</h3>
|
|
{words && <span className="chip">{words} mots</span>}
|
|
</div>
|
|
</div>
|
|
<div className="text-sm leading-relaxed whitespace-pre-wrap">{text}</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function CritiqueCard({ confidence, rag_hits, response_length, plan_coverage }) {
|
|
if (confidence === undefined) return null;
|
|
const confColor = confidence > 0.8 ? "text-green-400" : (confidence > 0.6 ? "text-yellow-400" : "text-red-400");
|
|
return (
|
|
<div className="card p-5 fade-in">
|
|
<div className="flex items-center gap-2 mb-3">
|
|
<span className="text-xl">🎯</span>
|
|
<h3 className="font-bold">Self-critique</h3>
|
|
</div>
|
|
<div className="grid grid-cols-4 gap-3">
|
|
<div>
|
|
<div className={`text-3xl font-bold ${confColor}`}>{Math.round(confidence*100)}%</div>
|
|
<div className="text-xs" style={{color:"var(--muted)"}}>Confiance</div>
|
|
</div>
|
|
<div>
|
|
<div className="text-3xl font-bold">{rag_hits || 0}</div>
|
|
<div className="text-xs" style={{color:"var(--muted)"}}>RAG hits</div>
|
|
</div>
|
|
<div>
|
|
<div className="text-3xl font-bold">{response_length || 0}</div>
|
|
<div className="text-xs" style={{color:"var(--muted)"}}>Caractères</div>
|
|
</div>
|
|
<div>
|
|
<div className="text-3xl font-bold">{plan_coverage || "0"}</div>
|
|
<div className="text-xs" style={{color:"var(--muted)"}}>Coverage</div>
|
|
</div>
|
|
</div>
|
|
<div className="progress-bar mt-4">
|
|
<div className="progress-fill" style={{width:`${Math.round(confidence*100)}%`}}></div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function ExampleButtons({ onPick }) {
|
|
const examples = [
|
|
{icon:"📊",q:"Comment optimiser la strategie pharma pour un lancement produit MENA ?"},
|
|
{icon:"🏗️",q:"Decrire le processus BPMN pour onboarding client B2B en lean six sigma"},
|
|
{icon:"⚡",q:"Quels agents WEVIA activer pour une campagne marketing multicanal ?"},
|
|
{icon:"🧬",q:"Analyse DMAIC du parcours HCP dans la distribution pharmaceutique"},
|
|
];
|
|
return (
|
|
<div className="flex flex-wrap gap-2">
|
|
{examples.map((e,i) => (
|
|
<button key={i} onClick={()=>onPick(e.q)} className="chip hover:bg-white/10 transition text-left cursor-pointer">
|
|
<span>{e.icon}</span>
|
|
<span className="truncate max-w-xs">{e.q}</span>
|
|
</button>
|
|
))}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function App() {
|
|
const [query, setQuery] = useState("");
|
|
const [running, setRunning] = useState(false);
|
|
const [sessionId, setSessionId] = useState(null);
|
|
|
|
const [thinking, setThinking] = useState({ text:"", status:"idle", elapsed:0 });
|
|
const [plan, setPlan] = useState({ steps:[], elapsed:0 });
|
|
const [executions, setExecutions] = useState({});
|
|
const [rag, setRag] = useState({ collections:0, hits:[], elapsed:0, status:"idle" });
|
|
const [test, setTest] = useState({ checks:null, elapsed:0 });
|
|
const [result, setResult] = useState({ text:"", words:0 });
|
|
const [critique, setCritique] = useState({});
|
|
const [phases, setPhases] = useState({});
|
|
const [activePhase, setActivePhase] = useState(null);
|
|
const [totalMs, setTotalMs] = useState(0);
|
|
|
|
const esRef = useRef(null);
|
|
const thinkingBuf = useRef("");
|
|
|
|
const reset = () => {
|
|
setThinking({ text:"", status:"idle", elapsed:0 });
|
|
setPlan({ steps:[], elapsed:0 });
|
|
setExecutions({});
|
|
setRag({ collections:0, hits:[], elapsed:0, status:"idle" });
|
|
setTest({ checks:null, elapsed:0 });
|
|
setResult({ text:"", words:0 });
|
|
setCritique({});
|
|
setPhases({});
|
|
setActivePhase(null);
|
|
setTotalMs(0);
|
|
thinkingBuf.current = "";
|
|
};
|
|
|
|
const run = () => {
|
|
if (!query.trim() || running) return;
|
|
reset();
|
|
setRunning(true);
|
|
const url = `/api/ambre-claude-pattern-sse.php?q=${encodeURIComponent(query)}&sid=react-${Date.now()}`;
|
|
const es = new EventSource(url);
|
|
esRef.current = es;
|
|
|
|
es.addEventListener("start", (e)=>{
|
|
const d = JSON.parse(e.data);
|
|
setSessionId(d.session);
|
|
});
|
|
es.addEventListener("thinking", (e)=>{
|
|
const d = JSON.parse(e.data);
|
|
if (d.status === "starting") { setActivePhase("thinking"); setPhases(p=>({...p,thinking:"running"})); }
|
|
else if (d.status === "done") { setThinking({text:d.full_text, status:"done", elapsed:d.elapsed_ms}); setPhases(p=>({...p,thinking:"done"})); }
|
|
});
|
|
es.addEventListener("thinking_chunk", (e)=>{
|
|
const d = JSON.parse(e.data);
|
|
thinkingBuf.current += (thinkingBuf.current ? " " : "") + d.text;
|
|
setThinking({ text:thinkingBuf.current, status:"running", elapsed:0 });
|
|
});
|
|
es.addEventListener("plan", (e)=>{
|
|
const d = JSON.parse(e.data);
|
|
setActivePhase("plan");
|
|
setPlan({ steps:d.steps, elapsed:d.elapsed_ms });
|
|
setPhases(p=>({...p,plan:"done"}));
|
|
});
|
|
es.addEventListener("rag", (e)=>{
|
|
const d = JSON.parse(e.data);
|
|
if (d.status === "querying") { setActivePhase("rag"); setPhases(p=>({...p,rag:"running"})); setRag(r=>({...r,status:"querying"})); }
|
|
else if (d.status === "done") { setRag({collections:d.total_collections, hits:d.hits, elapsed:d.elapsed_ms, status:"done"}); setPhases(p=>({...p,rag:"done"})); }
|
|
});
|
|
es.addEventListener("execute", (e)=>{
|
|
const d = JSON.parse(e.data);
|
|
setActivePhase("execute");
|
|
setExecutions(prev => ({...prev, [d.step_n]: d}));
|
|
if (d.status === "done") setPhases(p=>({...p,execute:"done"}));
|
|
});
|
|
es.addEventListener("test", (e)=>{
|
|
const d = JSON.parse(e.data);
|
|
setActivePhase("test");
|
|
if (d.status === "done") { setTest({checks:d.checks, elapsed:d.elapsed_ms}); setPhases(p=>({...p,test:"done"})); }
|
|
else if (d.status === "running") setPhases(p=>({...p,test:"running"}));
|
|
});
|
|
es.addEventListener("result_chunk", (e)=>{
|
|
const d = JSON.parse(e.data);
|
|
setActivePhase("result");
|
|
setResult({ text:d.text, words:d.words });
|
|
setPhases(p=>({...p,result:"running"}));
|
|
});
|
|
es.addEventListener("critique", (e)=>{
|
|
const d = JSON.parse(e.data);
|
|
setActivePhase("critique");
|
|
setCritique(d);
|
|
setPhases(p=>({...p,critique:"done", result:"done"}));
|
|
});
|
|
es.addEventListener("done", (e)=>{
|
|
const d = JSON.parse(e.data);
|
|
setTotalMs(d.total_ms);
|
|
setRunning(false);
|
|
setActivePhase(null);
|
|
es.close();
|
|
});
|
|
es.addEventListener("error", (e)=>{
|
|
console.error("SSE error", e);
|
|
setRunning(false);
|
|
es.close();
|
|
});
|
|
};
|
|
|
|
const phaseCount = Object.values(phases).filter(v => v === "done").length;
|
|
|
|
return (
|
|
<div className="min-h-screen">
|
|
<Header sessionId={sessionId} phaseCount={phaseCount} totalMs={totalMs} confidence={critique.confidence||0} />
|
|
|
|
<main className="max-w-6xl mx-auto px-6 py-6 space-y-4">
|
|
{/* Input */}
|
|
<div className="card p-5">
|
|
<div className="flex gap-3 mb-3">
|
|
<input
|
|
className="input"
|
|
value={query}
|
|
onChange={e=>setQuery(e.target.value)}
|
|
onKeyDown={e=>e.key==="Enter"&&!running&&run()}
|
|
placeholder="Posez une question complexe pour voir le pattern Claude complet..."
|
|
disabled={running}
|
|
/>
|
|
<button className="btn" onClick={run} disabled={running||!query.trim()}>
|
|
{running ? <span className="flex items-center gap-2"><span className="spin">⟳</span>En cours</span> : "🚀 Lancer"}
|
|
</button>
|
|
</div>
|
|
{!running && !sessionId && <ExampleButtons onPick={setQuery} />}
|
|
</div>
|
|
|
|
{/* Phase tracker */}
|
|
{(running || phaseCount > 0) && <PhaseTracker phases={phases} activePhase={activePhase} />}
|
|
|
|
{/* Thinking */}
|
|
{(thinking.status !== "idle") && <ThinkingCard {...thinking} />}
|
|
|
|
{/* Plan with live execution */}
|
|
<PlanCard steps={plan.steps} elapsed={plan.elapsed} executions={executions} />
|
|
|
|
{/* RAG */}
|
|
{rag.status !== "idle" && <RagCard {...rag} />}
|
|
|
|
{/* Test */}
|
|
<TestCard checks={test.checks} elapsed={test.elapsed} />
|
|
|
|
{/* Result */}
|
|
<ResultCard text={result.text} words={result.words} />
|
|
|
|
{/* Critique */}
|
|
<CritiqueCard {...critique} />
|
|
|
|
{!running && !sessionId && (
|
|
<div className="card p-8 text-center">
|
|
<div className="text-5xl mb-3">🧠</div>
|
|
<h2 className="text-xl font-bold mb-2 gradient-text">Pattern Claude Complet</h2>
|
|
<p className="text-sm mb-4" style={{color:"var(--muted)"}}>
|
|
Cette page visualise en direct le raisonnement interne de WEVIA : pensée, plan, consultation RAG Qdrant (17 collections),
|
|
exécution étape par étape, tests de validation, synthèse et auto-critique avec score de confiance.
|
|
</p>
|
|
<p className="text-xs" style={{color:"var(--muted)"}}>Connexion SSE · 7 phases streamées en temps réel · RAG + LLM souverain</p>
|
|
</div>
|
|
)}
|
|
</main>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
ReactDOM.createRoot(document.getElementById("root")).render(<App />);
|
|
</script>
|
|
</body>
|
|
</html>
|