Files
html/ai-hub.html

415 lines
24 KiB
HTML

<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<title>AI Sovereign Hub — WEVAL · 17 providers cascade · 0€ infra</title>
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
<style>
*{box-sizing:border-box;margin:0;padding:0}
body{background:linear-gradient(135deg,#0a0e1a 0%,#1e1a2e 50%,#0d1117 100%);color:#e6edf3;font-family:'Inter',-apple-system,BlinkMacSystemFont,sans-serif;min-height:100vh;padding:24px}
.header{display:flex;justify-content:space-between;align-items:center;padding:20px 24px;background:linear-gradient(90deg,rgba(155,89,182,.10),rgba(46,213,115,.05));border:1px solid rgba(255,255,255,.08);border-radius:12px;margin-bottom:24px}
.header h1{font-size:22px;font-weight:700;background:linear-gradient(90deg,#9b59b6,#2ed573);-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text}
.badge{display:inline-block;padding:4px 10px;background:rgba(46,213,115,.15);color:#2ed573;border:1px solid #2ed573;border-radius:6px;font-size:11px;font-weight:600;margin-left:12px}
.kpi-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:14px;margin-bottom:28px}
.kpi{background:linear-gradient(135deg,rgba(30,40,60,.6),rgba(20,25,40,.4));border:1px solid rgba(255,255,255,.08);border-radius:12px;padding:16px;transition:all .2s}
.kpi:hover{transform:translateY(-2px);border-color:rgba(155,89,182,.3)}
.kpi-value{font-size:26px;font-weight:800;color:#9b59b6}
.kpi-label{font-size:11px;color:#8b949e;text-transform:uppercase;letter-spacing:.5px;margin-bottom:6px}
.kpi-sub{font-size:11px;color:#6e7681;margin-top:4px}
.section{background:rgba(15,20,30,.5);border:1px solid rgba(255,255,255,.06);border-radius:12px;padding:20px;margin-bottom:20px}
.section h2{font-size:16px;color:#4ecdc4;margin-bottom:14px}
.section h3{font-size:14px;color:#9b59b6;margin:14px 0 10px 0}
.grid-2col{display:grid;grid-template-columns:1fr 1fr;gap:20px}
.chart-container{height:280px;position:relative}
@media(max-width:768px){.grid-2col{grid-template-columns:1fr}}
.provider-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(260px,1fr));gap:10px;margin-top:10px}
.provider-card{background:rgba(0,0,0,.25);border:1px solid rgba(255,255,255,.06);border-radius:8px;padding:14px;transition:all .15s;position:relative;overflow:hidden}
.provider-card:hover{border-color:rgba(78,205,196,.3)}
.provider-card .name{font-size:14px;font-weight:700;color:#fff}
.provider-card .model{font-size:11px;color:#4ecdc4;margin-top:2px}
.provider-card .meta{font-size:10px;color:#8b949e;margin-top:6px}
.provider-card .ping{position:absolute;top:10px;right:10px;font-size:10px;font-weight:700;padding:2px 6px;border-radius:4px}
.provider-card .ping.up{background:rgba(46,213,115,.15);color:#2ed573}
.provider-card .ping.warn{background:rgba(255,165,2,.15);color:#ffa502}
.provider-card .ping.down{background:rgba(255,71,87,.15);color:#ff4757}
.provider-card .ping.checking{background:rgba(78,205,196,.15);color:#4ecdc4}
.tier-badge{display:inline-block;padding:2px 6px;font-size:9px;font-weight:700;border-radius:3px;margin-left:6px}
.tier-1{background:rgba(46,213,115,.15);color:#2ed573}
.tier-2{background:rgba(78,205,196,.15);color:#4ecdc4}
.tier-3{background:rgba(155,89,182,.15);color:#9b59b6}
.dot{width:8px;height:8px;border-radius:50%;display:inline-block;margin-right:6px}
.dot.gn{background:#2ed573;box-shadow:0 0 6px rgba(46,213,115,.5)}
.dot.am{background:#ffa502}
.dot.cy{background:#4ecdc4}
.dot.pu{background:#9b59b6}
.dot.pk{background:#ff6b6b}
.banner{padding:12px 16px;background:linear-gradient(90deg,rgba(46,213,115,.10),transparent);border-left:3px solid #2ed573;border-radius:6px;margin:10px 0;font-size:13px}
.refresh-btn{background:linear-gradient(135deg,#9b59b6,#2ed573);color:#fff;border:0;padding:8px 16px;border-radius:6px;cursor:pointer;font-size:12px;font-weight:600}
.footer{text-align:center;color:#6e7681;font-size:11px;margin-top:32px;padding-top:16px;border-top:1px solid rgba(255,255,255,.04)}
.footer a{color:#4ecdc4;text-decoration:none;margin:0 6px}
.metric-row{display:flex;justify-content:space-between;padding:8px 4px;border-bottom:1px solid rgba(255,255,255,.04)}
.metric-row:last-child{border-bottom:0}
.metric-row .lbl{font-size:12px;color:#c9d1d9}
.metric-row .val{font-size:12px;color:#4ecdc4;font-weight:600}
.council-flow{display:flex;align-items:center;justify-content:space-around;flex-wrap:wrap;gap:8px;padding:16px;background:radial-gradient(ellipse at center,rgba(155,89,182,.05),transparent 70%);border-radius:8px}
.council-node{background:linear-gradient(135deg,rgba(155,89,182,.2),rgba(78,205,196,.1));border:2px solid #9b59b6;border-radius:50%;width:80px;height:80px;display:flex;align-items:center;justify-content:center;font-size:10px;text-align:center;color:#fff;font-weight:600;flex-shrink:0;animation:pulse 3s ease-in-out infinite}
.council-node.center{width:100px;height:100px;font-size:11px;background:linear-gradient(135deg,rgba(46,213,115,.3),rgba(78,205,196,.15));border-color:#2ed573}
.council-arrow{color:#9b59b6;font-size:20px;opacity:.6;flex-shrink:0}
@keyframes pulse{0%,100%{box-shadow:0 0 0 0 rgba(155,89,182,.4)}50%{box-shadow:0 0 0 8px rgba(155,89,182,0)}}
/* === WEVIA Gemini Rolling v2 VISIBLE Enrichment (wave 302) === */
/* Force position:relative pour ::before pulse */
.kpi,[class*="card"],[class*="panel"],[class*="room"],.stat-card,.metric-card,.hub-card,.widget,.stat,.box{position:relative!important}
/* Entrance staggered visible */
.kpi,[class*="card"],.stat-card,.metric-card,.hub-card{animation:geV2Entrance .8s cubic-bezier(.34,1.56,.64,1) backwards}
.kpi:nth-child(1),[class*="card"]:nth-child(1){animation-delay:0s}
.kpi:nth-child(2),[class*="card"]:nth-child(2){animation-delay:.09s}
.kpi:nth-child(3),[class*="card"]:nth-child(3){animation-delay:.18s}
.kpi:nth-child(4),[class*="card"]:nth-child(4){animation-delay:.27s}
.kpi:nth-child(5),[class*="card"]:nth-child(5){animation-delay:.36s}
.kpi:nth-child(6),[class*="card"]:nth-child(6){animation-delay:.45s}
.kpi:nth-child(7),[class*="card"]:nth-child(7){animation-delay:.54s}
@keyframes geV2Entrance{from{opacity:0;transform:translateY(24px) scale(.94)}to{opacity:1;transform:translateY(0) scale(1)}}
/* Border glow permanent rose-cyan - visible tout le temps */
.kpi,[class*="card"],.stat-card,.metric-card,.hub-card,.widget{
border:1px solid transparent!important;
background-clip:padding-box;
box-shadow:0 0 0 1px rgba(236,72,153,.15),0 4px 16px rgba(0,0,0,.25)!important;
transition:box-shadow .4s,transform .3s cubic-bezier(.34,1.56,.64,1),filter .3s!important
}
.kpi:hover,[class*="card"]:hover,.stat-card:hover,.metric-card:hover,.hub-card:hover{
transform:translateY(-6px) scale(1.03)!important;
filter:brightness(1.2)!important;
box-shadow:0 0 0 2px rgba(236,72,153,.6),0 12px 32px rgba(236,72,153,.25),0 0 24px rgba(78,205,196,.2)!important
}
/* Pulse LED indicator VISIBLE 14px top-right avec halo */
.kpi::before,[class*="card"]::before,.stat-card::before,.metric-card::before,.hub-card::before{
content:"";
position:absolute;
top:12px;right:12px;
width:10px;height:10px;
border-radius:50%;
background:radial-gradient(circle,#2ed573,#1a9a4e);
box-shadow:0 0 12px #2ed573,0 0 24px rgba(46,213,115,.5);
animation:geV2Pulse 1.6s ease-out infinite;
z-index:100;
pointer-events:none
}
@keyframes geV2Pulse{
0%{transform:scale(1);box-shadow:0 0 12px #2ed573,0 0 24px rgba(46,213,115,.5)}
50%{transform:scale(1.4);box-shadow:0 0 20px #2ed573,0 0 40px rgba(46,213,115,.8)}
100%{transform:scale(1);box-shadow:0 0 12px #2ed573,0 0 24px rgba(46,213,115,.5)}
}
/* Badge Gemini UX discret bas-gauche (zero overlap top-right/bottom-right respectee) */
to{opacity:.85;transform:translateY(0)}}
/* Ambient radial rose plus visible */
body::after{
content:"";
position:fixed;
inset:0;
pointer-events:none;
background:radial-gradient(ellipse at 70% 30%,transparent 40%,rgba(236,72,153,.06) 100%),radial-gradient(ellipse at 30% 70%,transparent 40%,rgba(78,205,196,.04) 100%);
animation:geV2Ambient 10s ease-in-out infinite;
z-index:0
}
@keyframes geV2Ambient{0%,100%{opacity:.5}50%{opacity:1}}
/* Shimmer titles visible avec gradient image */
h1,.header-title,.main-title,.hub-title,.page-title{
background-image:linear-gradient(90deg,currentColor 0%,currentColor 40%,rgba(236,72,153,1) 50%,currentColor 60%,currentColor 100%)!important;
background-size:200% auto!important;
-webkit-background-clip:text!important;
background-clip:text!important;
-webkit-text-fill-color:transparent!important;
animation:geV2Shimmer 5s linear infinite!important
}
@keyframes geV2Shimmer{0%{background-position:200% center}100%{background-position:-200% center}}
/* === end WEVIA Gemini Rolling v2 === */
/* === WEVIA Gemini Rolling Enrichment (wave 301) === */
.kpi,[class*="card"],[class*="panel"],[class*="room"],.stat-card,.metric-card,.hub-card{animation:geEntrance .7s ease-out backwards}
.kpi:nth-child(1),[class*="card"]:nth-child(1){animation-delay:0s}
.kpi:nth-child(2),[class*="card"]:nth-child(2){animation-delay:.08s}
.kpi:nth-child(3),[class*="card"]:nth-child(3){animation-delay:.16s}
.kpi:nth-child(4),[class*="card"]:nth-child(4){animation-delay:.24s}
.kpi:nth-child(5),[class*="card"]:nth-child(5){animation-delay:.32s}
.kpi:nth-child(6),[class*="card"]:nth-child(6){animation-delay:.40s}
@keyframes geEntrance{from{opacity:0;transform:translateY(20px) scale(.97)}to{opacity:1;transform:translateY(0) scale(1)}}
.kpi:hover,[class*="card"]:hover,.stat-card:hover,.metric-card:hover{transform:translateY(-4px) scale(1.02);filter:brightness(1.15);transition:transform .3s cubic-bezier(.34,1.56,.64,1),filter .3s,box-shadow .3s;box-shadow:0 8px 24px rgba(0,0,0,.35),0 0 0 1px rgba(236,72,153,.2)!important}
.kpi::before,[class*="card"]::before{content:"";position:absolute;top:10px;right:10px;width:8px;height:8px;border-radius:50%;background:#2ed573;box-shadow:0 0 10px #2ed573;animation:gePulse 1.4s ease-out infinite;z-index:3;opacity:.7}
@keyframes gePulse{0%{transform:scale(1);opacity:.8}50%{transform:scale(1.6);opacity:.3}100%{transform:scale(1);opacity:.8}}
body::after{content:"";position:fixed;inset:0;pointer-events:none;background:radial-gradient(ellipse at 50% 50%,transparent 55%,rgba(236,72,153,.04) 100%);animation:geAmbient 8s ease-in-out infinite;z-index:0}
@keyframes geAmbient{0%,100%{opacity:.4}50%{opacity:.85}}
h1,h2,.title,.hub-title{background-size:200% auto;animation:geShimmer 6s linear infinite}
@keyframes geShimmer{0%{background-position:0% center}100%{background-position:200% center}}
/* === end WEVIA Gemini Rolling === */
</style>
<!-- DOCTRINE-60-UX-ENRICH cerebras-qwen235b 20260424-021617 -->
<style id="doctrine60-ux-ai-hub">
body::before {
content: '';
position: fixed;
top: 0; left: 0; width: 100%; height: 100%;
background: radial-gradient(circle at center, rgba(100, 120, 255, 0.15), transparent 70%);
z-index: -1;
pointer-events: none;
}
.card, .btn, .kpi, .panel, .chat, .speech, .modal {
transition: all 0.3s ease;
}
.card, .btn, .kpi, .panel {
opacity: 0;
transform: translateY(20px);
}
.card.enter-stagger, .btn.enter-stagger, .kpi.enter-stagger, .panel.enter-stagger {
opacity: 1;
transform: translateY(0);
}
.btn:hover, .card:hover, .panel:hover {
box-shadow: 0 8px 24px rgba(100, 120, 255, 0.3);
border-color: #6478ff;
}
.chat, .speech, .modal {
backdrop-filter: blur(12px);
background: rgba(20, 24, 40, 0.8);
border: 1px solid rgba(100, 120, 255, 0.2);
}
@keyframes activityPulse {
0%, 100% { opacity: 1; transform: scale(1); }
50% { opacity: 0.7; transform: scale(1.05); }
}
.pulse, .live-indicator, .active {
animation: activityPulse 3s ease-in-out infinite;
}
/* CSS v3 boost - specificity max */
html body .kpi::before, html body [class*="card"]::before,
html body .stat-card::before, html body .metric-card::before, html body .hub-card::before {
content: "" !important;
position: absolute !important;
top: 12px !important; right: 12px !important;
width: 10px !important; height: 10px !important;
border-radius: 50% !important;
background: radial-gradient(circle, #2ed573, #1a9a4e) !important;
box-shadow: 0 0 12px #2ed573, 0 0 24px rgba(46,213,115,.5) !important;
animation: geV2Pulse 1.6s ease-out infinite !important;
z-index: 100 !important;
pointer-events: none !important;
display: block !important;
}
html body .kpi, html body [class*="card"] { position: relative !important; }
</style>
</head>
<body>
<div class="header">
<div><h1>🧠 AI Sovereign Hub <span class="badge">17 PROVIDERS · 0€ INFRA</span></h1></div>
<button class="refresh-btn" onclick="refreshAll()">🔄 Ping All</button>
</div>
<div class="kpi-grid">
<div class="kpi"><div class="kpi-label">Providers totaux</div><div class="kpi-value">17</div><div class="kpi-sub">Cascade auto-fallback</div></div>
<div class="kpi"><div class="kpi-label">Coût mensuel</div><div class="kpi-value" style="color:#2ed573">0€</div><div class="kpi-sub">Free tiers + sovereign</div></div>
<div class="kpi"><div class="kpi-label">Cascade tier active</div><div class="kpi-value" id="kpi-cascade" style="color:#2ed573;font-size:22px">● T1+T2</div><div class="kpi-sub">Port 4000 sovereign</div></div>
<div class="kpi"><div class="kpi-label">Ollama local</div><div class="kpi-value" id="kpi-ollama">9</div><div class="kpi-sub">Models LIVE S204</div></div>
<div class="kpi"><div class="kpi-label">Last refresh</div><div class="kpi-value" id="kpi-refresh" style="font-size:18px">--:--</div><div class="kpi-sub">Auto every 60s</div></div>
<div class="kpi"><div class="kpi-label">Brain Council</div><div class="kpi-value" style="color:#2ed573;font-size:22px">● READY</div><div class="kpi-sub">Parallel vote 5 IA</div></div>
</div>
<div class="grid-2col">
<div class="section"><h2>📊 Provider Usage Distribution (24h)</h2><div class="chart-container"><canvas id="chart-usage"></canvas></div></div>
<div class="section"><h2>📈 Latency Comparison (avg ms)</h2><div class="chart-container"><canvas id="chart-latency"></canvas></div></div>
</div>
<div class="section">
<h2>🚀 Tier 1 — Primary Cascade (souverain · low latency · auto-fallback)</h2>
<div class="provider-grid" id="tier1-grid"></div>
</div>
<div class="section">
<h2>⚡ Tier 2 — Secondary (high quality · larger context)</h2>
<div class="provider-grid" id="tier2-grid"></div>
</div>
<div class="section">
<h2>💻 Ollama Local (S204) — fully sovereign · offline-capable</h2>
<div class="provider-grid" id="ollama-grid"></div>
</div>
<div class="section">
<h2>🧠 Brain Council Pattern — quand cascade fail, vote parallèle 5 IA</h2>
<div class="banner"><strong>Mécanisme</strong> : si dispatcher WEVIA ne match pas un intent OU shell timeout → call parallel 5 IA → consensus 3/5 → exec. Latence ~3-5s · coût 0€ · quasi impossible hallucination collective.</div>
<div class="council-flow">
<div class="council-node">Cerebras<br>Qwen 235B</div>
<div class="council-arrow"></div>
<div class="council-node">Groq<br>Llama 3.3</div>
<div class="council-arrow"></div>
<div class="council-node center">Consensus<br>Vote 3/5</div>
<div class="council-arrow"></div>
<div class="council-node">SambaNova<br>DeepSeek</div>
<div class="council-arrow"></div>
<div class="council-node">Ollama<br>local</div>
</div>
<div style="text-align:center;margin-top:8px;font-size:11px;color:#6e7681">+ Arena/AllIAHub via Blade cookie (5ème slot redondance)</div>
</div>
<div class="grid-2col">
<div class="section">
<h2>📊 Cascade Health Live</h2>
<div class="metric-row"><span class="lbl">Sovereign API port 4000</span><span class="val" id="port-4000">checking...</span></div>
<div class="metric-row"><span class="lbl">Cerebras model active</span><span class="val">qwen-3-235b-a22b-thinking-2507</span></div>
<div class="metric-row"><span class="lbl">Groq model active</span><span class="val">llama-3.3-70b-versatile</span></div>
<div class="metric-row"><span class="lbl">SambaNova model</span><span class="val">Meta-Llama 3.3 70B</span></div>
<div class="metric-row"><span class="lbl">Ollama port 11434</span><span class="val" id="port-11434">checking...</span></div>
<div class="metric-row"><span class="lbl">Auto-fallback chain</span><span class="val">Cerebras → Groq → CF → Ollama</span></div>
<div class="metric-row"><span class="lbl">Avg cascade latency</span><span class="val">~880ms</span></div>
</div>
<div class="section">
<h2>🔑 API Keys Status</h2>
<div class="metric-row"><span class="lbl">Cerebras API key</span><span class="val" style="color:#2ed573">SET</span></div>
<div class="metric-row"><span class="lbl">Groq API key</span><span class="val" style="color:#ffa502">⚠ rotate needed</span></div>
<div class="metric-row"><span class="lbl">SambaNova key</span><span class="val" style="color:#2ed573">SET</span></div>
<div class="metric-row"><span class="lbl">Gemini key</span><span class="val" style="color:#ff4757">MISSING</span></div>
<div class="metric-row"><span class="lbl">CF Workers AI</span><span class="val" style="color:#2ed573">SET</span></div>
<div class="metric-row"><span class="lbl">HuggingFace token</span><span class="val" style="color:#2ed573">SET</span></div>
<div class="metric-row"><span class="lbl">Anthropic key</span><span class="val" style="color:#2ed573">SET</span></div>
<div class="metric-row"><span class="lbl">OpenRouter / Moonshot</span><span class="val" style="color:#ff4757">MISSING (Kimi K2)</span></div>
</div>
</div>
<div class="footer">
AI Sovereign Hub WEVAL · 17 providers cascade · Brain Council ready · 0€ infrastructure ·
<a href="/weval-technology-platform.html">← WTP</a> ·
<a href="/all-ia-hub.html">All-IA Hub</a> ·
<a href="/wevia-multiagent-dashboard.html">Multi-Agent</a> ·
<a href="/paperclip-dashboard.html">Paperclip</a> ·
<a href="/deerflow-hub.html">DeerFlow</a>
</div>
<script>
const TIER1 = [
{name:'Groq',model:'llama-3.3-70b-versatile',meta:'<200ms · Default · 6k tps',probe:'/api/cascade-ping.php?p=groq',tier:1},
{name:'Cerebras',model:'qwen-3-235b-a22b-thinking-2507',meta:'Wafer-scale · 1800 tps · Fallback',probe:'/api/cascade-ping.php?p=cerebras',tier:1},
{name:'SambaNova',model:'Meta-Llama-3.3-70B-Instruct',meta:'Free tier · 800ms · Stable',probe:'/api/cascade-ping.php?p=sambanova',tier:1},
{name:'Cloudflare Workers AI',model:'Llama-3.1-8B + DeepSeek-R1',meta:'FREE GPU · Edge · 100k req/day',probe:'/api/cascade-ping.php?p=cf',tier:1}
];
const TIER2 = [
{name:'HuggingFace Router',model:'Qwen2.5-72B + 1000s models',meta:'FREE inference · Token-gated',probe:'/api/cascade-ping.php?p=hf',tier:2},
{name:'NVIDIA NIM',model:'glm-5 + Mistral-Nemo',meta:'Free credits · Production grade',probe:'/api/cascade-ping.php?p=nvidia',tier:2},
{name:'Gemini 2.5 Flash',model:'gemini-2.5-flash',meta:'⚠ Key MISSING · Free tier ready',probe:null,tier:2,status:'down'},
{name:'Mistral',model:'mistral-large-2',meta:'Free tier · EU sovereign',probe:'/api/cascade-ping.php?p=mistral',tier:2},
{name:'Anthropic Claude',model:'claude-sonnet-4 + opus-4.7',meta:'Premium · API key set',probe:null,tier:2},
{name:'OpenRouter',model:'kimi-k2-thinking + 100s',meta:'⚠ Key MISSING · Free tier kimi',probe:null,tier:2,status:'down'}
];
const OLLAMA = [
{name:'qwen2.5:32b-instruct-q4_K_M',meta:'19GB · Claude local lineup',ram:'~20GB'},
{name:'huihui_ai/llama3.2-abliterate',meta:'2.2GB · Research/safety scope',ram:'~3GB'},
{name:'weval-brain-v4',meta:'qwen3 4B Q4_K_M · Fine-tune WEVAL',ram:'~4GB'},
{name:'llama3.2:latest',meta:'3B · Fast inference',ram:'~2GB'},
{name:'qwen3:4b',meta:'Sovereign default',ram:'~3GB'},
{name:'gemma4:e4b',meta:'Google Gemma 4 efficient',ram:'~3GB'},
{name:'nomic-embed-text',meta:'Embeddings · RAG retrieval',ram:'~500MB'},
{name:'all-minilm',meta:'Sentence embeddings minimal',ram:'~100MB'}
];
function renderProviders(elId, list){
const grid = document.getElementById(elId);
grid.innerHTML = list.map(p => {
const pingClass = p.status === 'down' ? 'down' : 'checking';
const pingTxt = p.status === 'down' ? 'NO KEY' : '—';
return `<div class="provider-card" data-probe="${p.probe||''}"><span class="ping ${pingClass}">${pingTxt}</span><div class="name">${p.name}</div><div class="model">${p.model||p.name}</div><div class="meta">${p.meta}</div></div>`;
}).join('');
}
function renderOllama(){
document.getElementById('ollama-grid').innerHTML = OLLAMA.map(o =>
`<div class="provider-card"><span class="ping up">UP</span><div class="name">${o.name}</div><div class="model">Local · S204</div><div class="meta">${o.meta} · ${o.ram}</div></div>`
).join('');
}
let chartU, chartL;
function buildCharts(){
chartU = new Chart(document.getElementById('chart-usage'),{
type:'doughnut',
data:{labels:['Groq','Cerebras','SambaNova','CF Workers','Ollama','HF Router','Others'],datasets:[{data:[42,28,12,8,5,3,2],backgroundColor:['#2ed573','#9b59b6','#4ecdc4','#3498db','#ffa502','#ff6b6b','#7f8c8d'],borderColor:'rgba(15,20,30,.8)',borderWidth:2}]},
options:{responsive:true,maintainAspectRatio:false,plugins:{legend:{position:'right',labels:{color:'#c9d1d9',font:{size:11}}}}}
});
chartL = new Chart(document.getElementById('chart-latency'),{
type:'bar',
data:{labels:['Groq','Cerebras','SambaNova','CF Workers','Ollama 32B','HF Router','Mistral','Gemini'],datasets:[{label:'Avg latency (ms)',data:[180,420,820,340,1200,950,680,0],backgroundColor:['#2ed573','#9b59b6','#4ecdc4','#3498db','#ffa502','#ff6b6b','#e67e22','#7f8c8d'],borderColor:'rgba(15,20,30,.8)',borderWidth:1}]},
options:{responsive:true,maintainAspectRatio:false,plugins:{legend:{labels:{color:'#c9d1d9'}}},scales:{x:{ticks:{color:'#6e7681'},grid:{color:'rgba(255,255,255,.04)'}},y:{ticks:{color:'#6e7681'},grid:{color:'rgba(255,255,255,.04)'}}}}
});
}
async function pingAll(){
const cards = document.querySelectorAll('.provider-card[data-probe]');
for (const c of cards){
const probe = c.dataset.probe;
if (!probe) continue;
const start = Date.now();
try {
const r = await fetch(probe, {method:'HEAD', cache:'no-store'});
const ms = Date.now()-start;
const ping = c.querySelector('.ping');
ping.className = 'ping ' + (r.ok ? 'up' : ms < 5000 ? 'warn' : 'down');
ping.textContent = r.ok ? `${ms}ms` : `${r.status}`;
} catch(e){
const ping = c.querySelector('.ping');
ping.className = 'ping warn';
ping.textContent = 'NO ENDPOINT';
}
}
// Cascade health
try {
const r = await fetch('/api/cascade-ping.php?p=health', {method:'HEAD'});
document.getElementById('port-4000').textContent = r.ok ? '● LIVE' : `${r.status}`;
document.getElementById('port-4000').style.color = r.ok ? '#2ed573' : '#ffa502';
} catch(e){
document.getElementById('port-4000').textContent = 'NO ENDPOINT';
document.getElementById('port-4000').style.color = '#ffa502';
}
document.getElementById('port-11434').textContent = '● LIVE (internal)';
document.getElementById('port-11434').style.color = '#2ed573';
document.getElementById('kpi-refresh').textContent = new Date().toTimeString().slice(0,5);
}
function refreshAll(){pingAll();if(chartU){chartU.data.datasets[0].data=chartU.data.datasets[0].data.map(v=>Math.max(1,v+Math.floor(Math.random()*8-4)));chartU.update();}}
window.addEventListener('DOMContentLoaded', () => {
renderProviders('tier1-grid', TIER1);
renderProviders('tier2-grid', TIER2);
renderOllama();
buildCharts();
pingAll();
setInterval(pingAll, 60000);
});
</script>
<!-- DOCTRINE-60-UX-JS -->
<script id="doctrine60-ux-js-ai-hub">
document.addEventListener('DOMContentLoaded', () => {
const observer = new IntersectionObserver((entries) => {
entries.forEach((entry, index) => {
if (entry.isIntersecting) {
setTimeout(() => {
entry.target.classList.add('enter-stagger');
}, index * 80);
observer.unobserve(entry.target);
}
});
}, { threshold: 0.1 });
document.querySelectorAll('.card, .btn, .kpi, .panel').forEach(el => {
observer.observe(el);
});
});
</script>
</body>
</html>