Files
html/wevia-claude-pattern.html

484 lines
21 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>
<!-- DOCTRINE-60-UX-ENRICH direct-inject-20260424-143216 -->
<style id="doctrine60-ux-direct">
/* DOCTRINE-60-UX-ENRICH injected-direct */
body::before {
content: '';
position: fixed;
top: 0; left: 0; width: 100vw; height: 100vh;
background: radial-gradient(circle at 50% 50%, rgba(100,180,255,0.08), transparent 60%);
pointer-events: none;
z-index: -1;
}
.card, .kpi, .panel, .btn {
transition: all 0.3s cubic-bezier(0.2,0,0.1,1);
}
.card:hover, .kpi:hover, .panel:hover {
box-shadow: 0 4px 20px rgba(100,180,255,0.2);
border-color: rgba(100,180,255,0.5);
}
@keyframes pulseD60 {
0%,100% { opacity: 1; transform: scale(1); }
50% { opacity: 0.7; transform: scale(1.05); }
}
.pulse, .live-indicator, .active, .online {
animation: pulseD60 3s ease-in-out infinite;
}
.modal, .chat, .speech, .overlay {
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
}
.enter-stagger {
animation: enterStagD60 0.5s cubic-bezier(0.2,0,0.1,1) forwards;
}
@keyframes enterStagD60 {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
</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>
<!-- DOCTRINE-60-UX-JS --><script id="doctrine60-ux-js-direct">
// DOCTRINE-60-UX-JS staggered entrance
(function(){
if (!('IntersectionObserver' in window)) return;
const obs = new IntersectionObserver((entries) => {
entries.forEach((e, i) => {
if (e.isIntersecting) {
setTimeout(() => e.target.classList.add('enter-stagger'), i * 80);
obs.unobserve(e.target);
}
});
});
document.querySelectorAll('.card, .kpi, .panel').forEach(el => obs.observe(el));
})();
</script>
</body>
</html>