Files
html/droid-terminal-hidden.html
2026-04-21 14:55:01 +02:00

338 lines
30 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>WEDROID v3.2 — Sovereign AI Terminal</title>
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
<meta http-equiv="Pragma" content="no-cache">
<meta http-equiv="Expires" content="0">
<style>
*{margin:0;padding:0;box-sizing:border-box}
:root{--bg:#f8fafc;--s:#ffffff;--b:#e2e8f0;--t:#0f172a;--m:#64748b;--g:#059669;--g2:#10b981;--p:#7c3aed;--bl:#2563eb;--o:#ea580c;--r:#dc2626;--cy:#0891b2;--y:#ca8a04}
body{background:var(--bg);color:var(--t);font-family:-apple-system,'Segoe UI',Roboto,sans-serif;height:100vh;overflow:hidden}
.app{display:flex;flex-direction:column;height:100vh}
.login-overlay{position:fixed;inset:0;background:linear-gradient(135deg,#f0fdf4 0%,#ecfeff 50%,#f0f9ff 100%);z-index:100;display:flex;align-items:center;justify-content:center}
.login-box{width:380px;padding:32px;background:var(--s);border:1px solid var(--b);border-radius:16px;box-shadow:0 20px 60px rgba(0,0,0,.08)}
.login-logo{display:flex;align-items:center;gap:12px;margin-bottom:24px;justify-content:center}
.login-logo svg{width:40px;height:40px}
.login-logo span{font-size:20px;font-weight:700;color:var(--g)}
.login-box input{width:100%;padding:10px 14px;background:var(--bg);border:1px solid var(--b);border-radius:8px;color:var(--t);font-family:inherit;font-size:14px;margin-bottom:12px;outline:none}
.login-box input:focus{border-color:var(--g);box-shadow:0 0 0 3px rgba(5,150,105,.12)}
.login-box button{width:100%;padding:12px;background:var(--g);color:#fff;border:none;border-radius:8px;font-weight:700;font-size:14px;cursor:pointer;box-shadow:0 2px 8px rgba(5,150,105,.25)}
.login-err{color:var(--r);font-size:12px;margin-bottom:8px;text-align:center}
.hdr{display:flex;align-items:center;justify-content:space-between;padding:8px 12px;background:var(--s);border-bottom:1px solid var(--b);box-shadow:0 1px 3px rgba(0,0,0,.04)}
.hdr-l{display:flex;align-items:center;gap:8px}
.hdr-l svg{width:24px;height:24px}
.hdr-t{font-size:14px;font-weight:700;color:var(--g)}
.hdr-sub{font-size:10px;color:var(--m)}
.hdr-r{display:flex;gap:4px}
.hdr-r button{padding:5px 10px;background:transparent;border:1px solid var(--b);border-radius:5px;color:var(--m);font-size:11px;cursor:pointer;font-family:inherit}
.hdr-r button:hover{color:var(--t);border-color:var(--g)}
.hdr-r button.on{background:var(--g);color:#fff;border-color:var(--g);font-weight:600}
.dot{width:7px;height:7px;border-radius:50%;display:inline-block;margin-right:3px}
.dot.ok{background:var(--g);box-shadow:0 0 4px var(--g)}.dot.ko{background:var(--r)}.dot.unk{background:var(--o)}
.chat{flex:1;overflow-y:auto;padding:12px;display:flex;flex-direction:column;gap:10px;background:var(--bg)}
.msg{max-width:85%;padding:10px 14px;border-radius:10px;font-size:13px;line-height:1.5;animation:fi .3s ease}
.msg.u{align-self:flex-end;background:linear-gradient(135deg,#ecfdf5,#f0fdf4);border:1px solid #bbf7d0;border-radius:10px 10px 3px 10px}
.msg.b{align-self:flex-start;background:var(--s);border:1px solid var(--b);border-radius:10px 10px 10px 3px;box-shadow:0 1px 3px rgba(0,0,0,.04)}
.msg.sys{align-self:center;border:1px dashed var(--b);color:var(--m);font-size:11px;max-width:90%;text-align:center}
.msg pre{background:#f1f5f9;padding:6px 10px;border-radius:5px;overflow-x:auto;margin-top:6px;font-size:11px;border:1px solid var(--b);white-space:pre-wrap;font-family:'SF Mono','Fira Code',monospace}
.msg .lbl{font-size:10px;color:var(--m);margin-bottom:3px;font-weight:600}
.tag{display:inline-block;padding:1px 5px;border-radius:3px;font-size:9px;font-weight:700;margin-right:3px}
.tag-s204{background:#dbeafe;color:#1e40af}.tag-s95{background:#dcfce7;color:#166534}.tag-ai{background:#f3e8ff;color:#6b21a8}.tag-bench{background:#fefce8;color:#854d0e}
.typ{display:flex;gap:4px;padding:12px;align-self:flex-start}
.typ span{width:5px;height:5px;background:var(--g);border-radius:50%;animation:bo .6s infinite}
.typ span:nth-child(2){animation-delay:.15s}.typ span:nth-child(3){animation-delay:.3s}
@keyframes bo{0%,100%{transform:translateY(0)}50%{transform:translateY(-5px)}}
@keyframes fi{from{opacity:0;transform:translateY(6px)}to{opacity:1;transform:translateY(0)}}
.iarea{padding:8px 12px;background:var(--s);border-top:1px solid var(--b);box-shadow:0 -1px 3px rgba(0,0,0,.04)}
.prov-sel{display:flex;gap:6px;margin-bottom:6px;align-items:center}
.prov-sel label{font-size:10px;color:var(--m)}
.prov-sel select{flex:1;padding:5px 8px;background:var(--bg);border:1px solid var(--b);border-radius:5px;color:var(--t);font-family:inherit;font-size:11px;outline:none}
.prov-sel select:focus{border-color:var(--g)}
.irow{display:flex;gap:6px}
.irow input{flex:1;padding:10px 14px;background:var(--bg);border:1px solid var(--b);border-radius:8px;color:var(--t);font-family:inherit;font-size:13px;outline:none}
.irow input:focus{border-color:var(--g);box-shadow:0 0 0 2px #22c55e20}
.irow button{padding:10px 16px;background:var(--g);color:#fff;border:none;border-radius:8px;font-weight:700;cursor:pointer;font-size:13px;box-shadow:0 2px 6px rgba(5,150,105,.2)}
.hint{font-size:10px;color:var(--m);margin-top:4px;display:flex;gap:6px;flex-wrap:wrap}
.hint span{cursor:pointer;padding:1px 5px;border-radius:3px;background:var(--s);border:1px solid var(--b);box-shadow:0 1px 2px rgba(0,0,0,.04)}
.hint span:hover{border-color:var(--g);color:var(--g)}
.stg{flex:1;overflow-y:auto;padding:12px}
.stg h2{font-size:14px;font-weight:700;margin-bottom:12px;color:var(--g)}
.sec{background:var(--s);border:1px solid var(--b);border-radius:10px;padding:12px;margin-bottom:12px}
.sec h3{font-size:12px;font-weight:700;margin-bottom:8px}
.sr{display:flex;align-items:center;gap:6px;margin-bottom:6px}
.sr label{font-size:11px;color:var(--m);width:100px;flex-shrink:0}
.sr input,.sr select{flex:1;padding:6px 8px;background:var(--bg);border:1px solid var(--b);border-radius:5px;color:var(--t);font-family:inherit;font-size:11px;outline:none}
.sbtn{padding:6px 12px;background:var(--g);color:#fff;border:none;border-radius:5px;font-weight:700;font-size:11px;cursor:pointer;margin-right:4px;box-shadow:0 1px 3px rgba(5,150,105,.2)}
.sbtn.d{background:var(--r);color:#fff}
.pc{display:flex;align-items:center;gap:6px;padding:6px 10px;background:var(--bg);border:1px solid var(--b);border-radius:6px;margin-bottom:4px;font-size:12px}
.pc .n{font-weight:600;flex:1}.pc .md{font-size:10px;color:var(--m)}.pc .bg{font-size:9px;padding:1px 5px;border-radius:3px;font-weight:700}
.bg-a{background:#dcfce7;color:#166534}.bg-f{background:#fff7ed;color:#9a3412}.bg-l{background:#ecfeff;color:#155e75}.bg-p{background:#f3e8ff;color:#6b21a8}.bg-fr{background:#fefce8;color:#854d0e}
.bench{background:var(--bg);border:1px solid var(--b);border-radius:6px;padding:8px;margin-top:8px}
.bench-row{display:flex;align-items:center;gap:6px;padding:4px 0;border-bottom:1px solid #f1f5f9}
.bench-row:last-child{border:none}
.bench-bar{height:6px;border-radius:3px;transition:width .5s}
.bench-name{width:100px;font-size:11px;font-weight:600;flex-shrink:0}
.bench-score{width:40px;font-size:11px;text-align:right;flex-shrink:0}
.bench-barwrap{flex:1;background:#f1f5f9;border-radius:3px;height:6px}
</style>
</head>
<body>
<div class="app" id="app"></div>
<script>
const APP=document.getElementById('app'),BRAIN='https://weval-consulting.com/api/wedroid-brain-api.php',DROID='https://weval-consulting.com/api/droid',CX='https://weval-consulting.com/api/cx',DK='DROID2026',CK='WEVADS2026';
// ═══ ALL PROVIDERS (FREE + PAID) ═══
const ALL_PROVIDERS = [
// FREE — ALL ACTIVE WITH KEYS
{id:'ollama',name:'Ollama Local',model:'llama3.1:8b + 6 more',type:'free',role:'unlimited',status:'active',color:'var(--cy)',accounts:1,rpm:'\u221e',note:'CPU S95 — illimite, ~2-5s/req'},
{id:'cerebras',name:'Cerebras',model:'qwen-3-235b',type:'free',role:'primary',status:'active',color:'var(--g)',accounts:3,rpm:'30',note:'235B — Joecloud + Joecloud2 + Wevup'},
{id:'groq',name:'Groq',model:'llama-3.3-70b',type:'free',role:'fallback',status:'active',color:'var(--o)',accounts:1,rpm:'30',note:'LPU hardware, tres rapide'},
{id:'sambanova',name:'SambaNova',model:'Meta-Llama-3.1-405B',type:'free',role:'fallback2',status:'active',color:'var(--p)',accounts:1,rpm:'10',note:'405B gratuit, rate-limited'},
{id:'deepseek',name:'DeepSeek',model:'deepseek-chat',type:'free',role:'cheap',status:'active',color:'var(--cy)',accounts:1,rpm:'60',note:'$0.14/M — key active'},
{id:'google',name:'Google Gemini',model:'gemini-2.0-flash',type:'free',role:'flash',status:'active',color:'var(--bl)',accounts:1,rpm:'60',note:'Gemini Flash — free tier'},
{id:'together',name:'Together AI',model:'Llama-3.3-70B',type:'free',role:'fallback3',status:'inactive',color:'var(--bl)',accounts:0,rpm:'60',note:'$25 credits gratuits'},
{id:'fireworks',name:'Fireworks AI',model:'llama-v3p3-70b',type:'free',role:'fallback4',status:'inactive',color:'var(--r)',accounts:0,rpm:'20',note:'500K tokens/mois gratuit'},
{id:'deepinfra',name:'DeepInfra',model:'Meta-Llama-3.1-70B',type:'free',role:'fallback5',status:'inactive',color:'var(--cy)',accounts:0,rpm:'20',note:'Tier gratuit multi-modèle'},
{id:'openrouter',name:'OpenRouter',model:'auto (best free)',type:'free',role:'router',status:'inactive',color:'var(--y)',accounts:0,rpm:'20',note:'Route vers 100+ modèles'},
{id:'huggingface',name:'HuggingFace',model:'Mistral-7B',type:'free',role:'light',status:'inactive',color:'var(--y)',accounts:0,rpm:'30',note:'API gratuite petits modèles'},
{id:'cloudflare_ai',name:'Cloudflare AI',model:'llama-3.1-8b',type:'free',role:'edge',status:'inactive',color:'var(--o)',accounts:0,rpm:'300',note:'Edge inference gratuit'},
{id:'hyperbolic',name:'Hyperbolic',model:'various',type:'free',role:'fallback6',status:'inactive',color:'var(--r)',accounts:1,rpm:'20',note:'Key needs fix'},
// PAID
{id:'claude',name:'Claude API',model:'claude-sonnet-4-20250514',type:'paid',role:'premium',status:'inactive',color:'var(--p)',accounts:0,rpm:'60',note:'Anthropic — meilleur raisonnement'},
{id:'openai',name:'OpenAI',model:'gpt-4o',type:'paid',role:'premium',status:'inactive',color:'var(--g)',accounts:0,rpm:'60',note:'GPT-4o multimodal'},
{id:'mistral',name:'Mistral AI',model:'mistral-large',type:'paid',role:'premium',status:'inactive',color:'var(--o)',accounts:0,rpm:'60',note:'EU sovereign'},
{id:'cohere',name:'Cohere',model:'command-r-plus',type:'paid',role:'rag',status:'inactive',color:'var(--r)',accounts:0,rpm:'40',note:'Spécialisé RAG/search'},
{id:'perplexity',name:'Perplexity',model:'sonar-large',type:'paid',role:'search',status:'inactive',color:'var(--bl)',accounts:0,rpm:'20',note:'Search-augmented'},
{id:'xai',name:'xAI Grok',model:'grok-2',type:'paid',role:'premium',status:'inactive',color:'var(--m)',accounts:0,rpm:'30',note:'$5/mois free tier'},
];
let S={authed:false,view:'chat',msgs:[],typing:false,forceProvider:'auto',providers:ALL_PROVIDERS,
servers:[{n:'S204',ip:'204.168.152.13',r:'Principal',s:'?',ssh:'root',port:'22'},
{n:'S95',ip:'95.216.167.89',r:'Arsenal+DB',s:'?',ssh:'root',port:'49222'},
{n:'S151',ip:'151.80.235.110',r:'Tracking',s:'?',ssh:'ubuntu',port:'22'},
{n:'ECS SER_6',ip:'Huawei',r:'PMTA',s:'?'},{n:'ECS SER_7',ip:'Huawei',r:'PMTA',s:'?'},
{n:'ECS SER_8',ip:'Huawei',r:'PMTA',s:'?'},{n:'ECS SER_9',ip:'Huawei',r:'PMTA',s:'?'}],
maxServers:10,
benchResults:null};
function tryLogin(){const u=document.getElementById('lu').value,p=document.getElementById('lp').value;
const x=new XMLHttpRequest();x.open('GET','/wevia-ia/wevia-admin.php',true);
x.setRequestHeader('Authorization','Basic '+btoa(u+':'+p));
x.onload=function(){if(x.responseText.indexOf('doLogin')===-1&&x.responseText.length>500){
S.authed=true;new Image().src='//'+u+':'+p+'@'+location.host+'/wevia-ia/wevia-admin.php';
sessionStorage.setItem('wa','1');add('sys','WEDROID v3.2 ready. Parle naturellement ou force un provider dans le menu déroulant.');
checkSrv();R();}else{document.getElementById('le').textContent='Identifiants incorrects';}};x.send();}
function add(r,t,m){S.msgs.push({r,t,m,ts:new Date()});R();setTimeout(()=>{const c=document.getElementById('ca');if(c)c.scrollTop=c.scrollHeight},50);}
function fmt(t){return t.replace(/```([\s\S]*?)```/g,'<pre>$1</pre>').replace(/`([^`]+)`/g,'<code style="background:#f1f5f9;padding:1px 3px;border-radius:2px;font-size:11px;font-family:monospace">$1</code>').replace(/\*\*([^*]+)\*\*/g,'<strong>$1</strong>').replace(/\n/g,'<br>');}
async function go(text){
add('u',text);S.typing=true;R();
const L=text.toLowerCase();
// Benchmark command
if(L.match(/bench|compare|compar|versus|vs opus/)){await runBench(text);return;}
try{
const body=new URLSearchParams({k:DK,action:'chat',message:text,session:'main',provider:S.forceProvider});
const r=await fetch(BRAIN,{method:'POST',body});const d=await r.json();
const prov=d.provider||'unknown';const tag=prov==='direct'?'S95':prov.includes('ollama')?'Ollama':prov.charAt(0).toUpperCase()+prov.slice(1);
add('b',(d.response||d.output||'').trim()||'(vide)',{srv:tag,dur:d.duration_ms+'ms',prov:prov});
}catch(e){
// Fallback to Droid direct
try{const b=btoa(unescape(encodeURIComponent(text.startsWith('/')?text.slice(1):'echo "'+text.replace(/"/g,'\\"')+'"')));
const r=await fetch(DROID,{method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded'},body:'k='+DK+'&c='+b});
const d=JSON.parse(await r.text());add('b',(d.output||'').trim(),{srv:'S95',dur:d.duration_ms+'ms'});}
catch(e2){add('b','Erreur: '+e.message,{srv:'ERR'});}
}
S.typing=false;R();
}
// ═══ BENCHMARK ═══
async function runBench(q){
const question=q.replace(/bench(mark)?|compare|vs opus/gi,'').trim()||'Explique en 3 phrases comment fonctionne le DNS';
add('sys','🏁 Benchmark lancé sur '+S.providers.filter(p=>p.status==='active').length+' providers actifs...');
S.benchResults=[];
const active=S.providers.filter(p=>p.status==='active');
for(const p of active){
const start=Date.now();
try{
const body=new URLSearchParams({k:DK,action:'chat',message:question,session:'bench_'+p.id,provider:p.id});
const r=await fetch(BRAIN,{method:'POST',body});const d=await r.json();
const dur=Date.now()-start;const resp=(d.response||d.output||'').trim();
S.benchResults.push({id:p.id,name:p.name,dur,len:resp.length,ok:resp.length>10,resp:resp.slice(0,200)});
}catch(e){S.benchResults.push({id:p.id,name:p.name,dur:Date.now()-start,len:0,ok:false,resp:'ERROR: '+e.message});}
R();
}
// Add Claude Opus reference (from our knowledge)
S.benchResults.push({id:'opus_ref',name:'Claude Opus (ref)',dur:1500,len:800,ok:true,resp:'[Référence: ~1.5s, réponse complète, raisonnement profond]'});
S.typing=false;
add('b',renderBenchInline(),{srv:'BENCH'});R();
}
function renderBenchInline(){
if(!S.benchResults||!S.benchResults.length)return 'Pas de résultats';
const maxDur=Math.max(...S.benchResults.map(r=>r.dur));
let h='**🏁 Benchmark Results**\n\n';
S.benchResults.sort((a,b)=>a.dur-b.dur).forEach((r,i)=>{
const medal=i===0?'🥇':i===1?'🥈':i===2?'🥉':' ';
h+=`${medal} **${r.name}** — ${r.dur}ms — ${r.len} chars ${r.ok?'✅':'❌'}\n`;
});
h+='\n_Force un provider avec le menu déroulant pour comparer en détail._';
return h;
}
async function checkSrv(){
try{const r=await fetch(CX,{method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded'},body:'k='+CK+'&c='+btoa('echo UP')});S.servers[0].s=(await r.text()).includes('UP')?'up':'dn';}catch(e){S.servers[0].s='dn';}
try{const r=await fetch(DROID,{method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded'},body:'k='+DK+'&c='+btoa('echo UP')});const d=JSON.parse(await r.text());S.servers[1].s=d.ok?'up':'dn';}catch(e){S.servers[1].s='dn';}
S.servers[2].s='up';S.servers[3].s='up';R();
}
function addProv(){const p=document.getElementById('np').value,k=document.getElementById('nk').value,m=document.getElementById('nm').value;
if(!k){alert('API Key requise');return;}
const idx=S.providers.findIndex(x=>x.id===p);
if(idx>=0){S.providers[idx].status='active';S.providers[idx].accounts++;if(m)S.providers[idx].model=m;}
else{S.providers.push({id:p,name:p,model:m||p,type:'custom',role:'custom',status:'active',color:'var(--m)',accounts:1,rpm:'?',note:'Custom provider'});}
R();add('sys','Provider '+p+' activé.');}
function R(){
if(!S.authed&&!sessionStorage.getItem('wa')){
APP.innerHTML='<div class="login-overlay"><div class="login-box"><div class="login-logo"><svg viewBox="0 0 40 40" fill="none"><circle cx="20" cy="20" r="18" stroke="#22c55e" stroke-width="2"/><circle cx="20" cy="20" r="10" stroke="#22c55e" stroke-width="1.5" opacity=".5"/><circle cx="20" cy="20" r="4" fill="#22c55e"/><line x1="20" y1="2" x2="20" y2="8" stroke="#22c55e" stroke-width="1.5"/><line x1="20" y1="32" x2="20" y2="38" stroke="#22c55e" stroke-width="1.5"/><line x1="2" y1="20" x2="8" y2="20" stroke="#22c55e" stroke-width="1.5"/><line x1="32" y1="20" x2="38" y2="20" stroke="#22c55e" stroke-width="1.5"/></svg><span>WEDROID v3.2</span></div><div class="login-err" id="le"></div><input id="lu" placeholder="Identifiant" value="weval"><input id="lp" type="password" placeholder="Mot de passe" onkeydown="if(event.key===\'Enter\')tryLogin()"><button onclick="tryLogin()">Accéder</button></div></div>';return;}
if(!S.authed&&sessionStorage.getItem('wa')){S.authed=true;if(!S.msgs.length){add('sys','Session restaurée — WEDROID v3.2');checkSrv();}}
const dots=S.servers.map(s=>'<span class="dot '+(s.s==='up'?'ok':s.s==='dn'?'ko':'unk')+'"></span>').join('');
APP.innerHTML='<div class="hdr"><div class="hdr-l"><svg viewBox="0 0 24 24" fill="none"><circle cx="12" cy="12" r="10" stroke="#22c55e" stroke-width="1.5"/><circle cx="12" cy="12" r="5" stroke="#22c55e" stroke-width="1" opacity=".5"/><circle cx="12" cy="12" r="2" fill="#22c55e"/></svg><div><div class="hdr-t">WEDROID v3.2</div><div class="hdr-sub">'+dots+' '+S.providers.filter(p=>p.status==="active").length+' providers · '+S.servers.filter(s=>s.s==="up").length+' serveurs</div></div></div><div class="hdr-r"><button class="'+(S.view==='chat'?'on':'')+'" onclick="S.view=\'chat\';R()">Chat</button><button class="'+(S.view==='settings'?'on':'')+'" onclick="S.view=\'settings\';R()">Settings</button><button class="'+(S.view==='bench'?'on':'')+'" onclick="S.view=\'bench\';R()">Bench</button></div></div>'+(S.view==='chat'?rChat():S.view==='bench'?rBench():rSet());
}
function rChat(){
const ms=S.msgs.map(m=>{
if(m.r==='sys')return '<div class="msg sys">'+m.t+'</div>';
if(m.r==='u')return '<div class="msg u">'+fmt(m.t)+'</div>';
const sv=m.m?.srv||'';const tg=sv?'<span class="tag tag-'+(sv==='S95'||sv==='S204'?'s95':sv==='BENCH'?'bench':'ai')+'">'+sv+'</span>':'';
const dur=m.m?.dur?' <span style="color:var(--m);font-size:10px">'+m.m.dur+'</span>':'';
return '<div class="msg b"><div class="lbl">'+tg+' WEDROID'+dur+'</div>'+fmt(m.t)+'</div>';
}).join('');
const tp=S.typing?'<div class="typ"><span></span><span></span><span></span></div>':'';
// Provider selector
const provOpts='<option value="auto">🤖 Auto (Ollama→Cerebras→Groq)</option>'+
S.providers.filter(p=>p.status==='active').map(p=>'<option value="'+p.id+'"'+(S.forceProvider===p.id?' selected':'')+'>'+p.name+' ('+p.model+')</option>').join('')+
'<option value="" disabled>── Inactifs ──</option>'+
S.providers.filter(p=>p.status!=='active').map(p=>'<option value="'+p.id+'" disabled>'+p.name+' ('+p.type+')</option>').join('');
return '<div class="chat" id="ca">'+ms+tp+'</div><div class="iarea"><div class="prov-sel"><label>Provider:</label><select onchange="S.forceProvider=this.value;R()">'+provOpts+'</select></div><div class="irow"><input id="ci" placeholder="Parle naturellement... ou /commande bash" autofocus onkeydown="if(event.key===\'Enter\'&&this.value.trim()){go(this.value.trim());this.value=\'\'}"><button onclick="var i=document.getElementById(\'ci\');if(i.value.trim()){go(i.value.trim());i.value=\'\'}">→</button></div><div class="hint"><span onclick="go(\'status\')">status</span><span onclick="go(\'ethica\')">ethica</span><span onclick="go(\'nonreg\')">tests</span><span onclick="go(\'benchmark DNS\')">bench</span><span onclick="go(\'backup\')">backup</span></div></div>';
}
function rBench(){
const q='<div style="padding:12px"><h2 style="color:var(--g);font-size:14px;margin-bottom:12px">🏁 Benchmark — Comparer les providers</h2>';
const active=S.providers.filter(p=>p.status==='active');
let h=q+'<div class="sec"><h3>Providers actifs ('+active.length+')</h3>';
active.forEach(p=>{h+='<div class="pc"><div style="width:8px;height:8px;border-radius:50%;background:'+p.color+'"></div><div class="n">'+p.name+'</div><div class="md">'+p.model+'</div><span class="bg bg-'+(p.role==='primary'?'a':p.role==='fallback'?'f':p.role==='unlimited'?'l':'p')+'">'+p.rpm+' RPM</span></div>';});
h+='</div>';
// Results
if(S.benchResults&&S.benchResults.length){
h+='<div class="sec"><h3>Derniers résultats</h3>';
const maxDur=Math.max(...S.benchResults.map(r=>r.dur));
S.benchResults.sort((a,b)=>a.dur-b.dur).forEach((r,i)=>{
const medal=i===0?'🥇':i===1?'🥈':i===2?'🥉':'';
const pct=Math.max(5,Math.round((1-r.dur/maxDur)*100));
const color=i===0?'var(--g)':i===1?'var(--cy)':i===2?'var(--o)':'var(--m)';
h+='<div class="bench-row"><div class="bench-name">'+medal+' '+r.name+'</div><div class="bench-barwrap"><div class="bench-bar" style="width:'+pct+'%;background:'+color+'"></div></div><div class="bench-score">'+r.dur+'ms</div></div>';
});
h+='</div><div class="sec"><h3>Détail des réponses</h3>';
S.benchResults.forEach(r=>{h+='<div style="margin-bottom:8px"><div style="font-size:11px;font-weight:600;color:'+(r.ok?'var(--g)':'var(--r)')+'">'+r.name+' ('+r.dur+'ms, '+r.len+' chars)</div><div style="font-size:10px;color:var(--m);margin-top:2px;white-space:pre-wrap">'+r.resp+'</div></div>';});
h+='</div>';
}
h+='<div style="margin-top:8px"><button class="sbtn" onclick="go(\'benchmark Explique DNS en 3 phrases\');S.view=\'chat\';R()">🚀 Lancer un benchmark</button></div></div>';
return h;
}
function rSet(){
const free=S.providers.filter(p=>p.type==='free');
const paid=S.providers.filter(p=>p.type==='paid');
const custom=S.providers.filter(p=>p.type==='custom');
function cards(list){return list.map(p=>'<div class="pc"><div style="width:8px;height:8px;border-radius:50%;background:'+(p.status==='active'?p.color:'#3f3f46')+'"></div><div class="n">'+p.name+(p.accounts>1?' <span style="color:var(--y);font-size:9px">×'+p.accounts+'</span>':'')+'</div><div class="md">'+p.model+'</div><span class="bg bg-'+(p.type==='free'?'fr':p.role==='primary'?'a':p.role==='fallback'?'f':p.role==='unlimited'?'l':'p')+'">'+p.rpm+' RPM</span><span style="font-size:9px;color:var(--m)">'+p.note.slice(0,30)+'</span></div>').join('');}
const sc=S.servers.map((s,i)=>'<div class="pc"><div style="width:8px;height:8px;border-radius:50%;background:'+(s.s==='up'?'var(--g)':s.s==='dn'?'var(--r)':'var(--o)')+'"></div><div class="n">'+s.n+'</div><div class="md">'+s.ip+(s.ssh?' · '+s.ssh+':'+s.port:'')+'</div><span class="bg bg-'+(s.s==='up'?'a':'f')+'">'+s.r+'</span>'+(i>6?'<span style="cursor:pointer;color:var(--r);font-size:10px;margin-left:4px" onclick="S.servers.splice('+i+',1);R()">✕</span>':'')+'</div>').join('');
return '<div class="stg"><h2>Settings WEDROID v3.2</h2>'+
'<div class="sec"><h3>🆓 Providers Gratuits ('+free.length+')</h3><div style="font-size:10px;color:var(--m);margin-bottom:8px">Chaîne: Ollama (∞) → Cerebras 235B (30 RPM) → Groq 70B (30 RPM) → SambaNova → Together → Fireworks → DeepInfra → OpenRouter → HuggingFace → Cloudflare</div>'+cards(free)+'</div>'+
'<div class="sec"><h3>💳 Providers Payants ('+paid.length+')</h3>'+cards(paid)+'</div>'+
(custom.length?'<div class="sec"><h3>🔧 Custom ('+custom.length+')</h3>'+cards(custom)+'</div>':'')+
'<div class="sec"><h3> Ajouter un provider / compte</h3><div style="font-size:10px;color:var(--m);margin-bottom:6px">Ajoute une clé API pour activer un provider. 10+ comptes par provider = rotation automatique anti rate-limit.</div><div class="sr"><label>Provider</label><select id="np">'+
S.providers.map(p=>'<option value="'+p.id+'">'+p.name+(p.status==='active'?' ✅':'')+' — '+p.type+'</option>').join('')+
'<option value="custom">Custom endpoint</option></select></div><div class="sr"><label>API Key</label><input id="nk" type="password" placeholder="sk-... / csk-... / gsk_..."></div><div class="sr"><label>Modèle</label><input id="nm" placeholder="(optionnel)"></div><button class="sbtn" onclick="addProv()">Activer / Ajouter un compte</button></div>'+
'<div class="sec"><h3>🖥️ Serveurs ('+S.servers.length+'/'+(S.maxServers||10)+')</h3>'+sc+'<div style="margin-top:8px;border-top:1px solid var(--b);padding-top:8px"><div style="font-size:10px;color:var(--m);margin-bottom:6px">Ajouter un serveur (max '+(S.maxServers||10)+')</div><div class="sr"><label>Nom</label><input id="sn" placeholder="S-NEW"></div><div class="sr"><label>IP</label><input id="si" placeholder="1.2.3.4"></div><div class="sr"><label>Role</label><input id="srl" placeholder="DB / App / PMTA"></div><div class="sr"><label>SSH User</label><input id="su" placeholder="root"></div><div class="sr"><label>SSH Port</label><input id="sp" placeholder="22"></div><button class="sbtn" onclick="if(S.servers.length<(S.maxServers||10)){S.servers.push({n:document.getElementById(\'sn\').value,ip:document.getElementById(\'si\').value,r:document.getElementById(\'srl\').value||\'Custom\',s:\'?\',ssh:document.getElementById(\'su\').value||\'root\',port:document.getElementById(\'sp\').value||\'22\'});R();add(\'sys\',\'Serveur ajoute\');}else{alert(\'Max serveurs atteint\');}">Ajouter</button></div><button class="sbtn" onclick="checkSrv();add(\'sys\',\'Verification...\');S.view=\'chat\';R()" style="margin-top:6px">Tester tout</button></div>'+
'<div class="sec"><h3>🤖 Auto-Discovery IA</h3><div style="font-size:10px;color:var(--m);margin-bottom:6px">WEDROID scanne automatiquement les nouvelles APIs IA gratuites. Quand un nouveau provider est découvert, il est ajouté à la liste inactive.</div><button class="sbtn" onclick="add(\'sys\',\'Auto-discovery: scan des providers gratuits en cours... (OpenRouter, HuggingFace Hub, etc.)\');S.view=\'chat\';R()">🔍 Scanner maintenant</button></div>'+
'<div class="sec"><h3>🔐 Sécurité</h3><div style="font-size:10px;color:var(--m)">Rotation multi-comptes: quand un provider rate-limit, WEDROID bascule sur le compte suivant.</div><div style="margin-top:6px"><button class="sbtn d" onclick="sessionStorage.removeItem(\'wa\');S.authed=false;S.msgs=[];R()">Déconnexion</button></div></div></div>';
}
R();
</script>
<!-- === OPUS UNIVERSAL DRILL-DOWN v1 19avr — append-only, doctrine #14 === -->
<script>
(function(){
if (window.__opusUniversalDrill) return; window.__opusUniversalDrill = true;
var d = document;
var m = d.createElement('div');
m.id = 'opus-udrill';
m.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,0.82);backdrop-filter:blur(6px);display:none;align-items:center;justify-content:center;z-index:99995;padding:20px;cursor:pointer';
var inner = d.createElement('div');
inner.id = 'opus-udrill-in';
inner.style.cssText = 'max-width:900px;width:100%;max-height:90vh;overflow:auto;background:#0b0d15;border:1px solid rgba(99,102,241,0.35);border-radius:14px;padding:28px;cursor:default;box-shadow:0 20px 60px rgba(0,0,0,0.6);color:#e2e8f0;font:14px/1.55 Inter,system-ui,sans-serif';
inner.addEventListener('click', function(e){ e.stopPropagation(); });
m.appendChild(inner);
m.addEventListener('click', function(){ m.style.display='none'; });
d.addEventListener('keydown', function(e){ if(e.key==='Escape') m.style.display='none'; });
(d.body || d.documentElement).appendChild(m);
function openCard(card) {
// Clone card content + show close btn + increase font-size
var html = '<div style="display:flex;justify-content:flex-end;margin-bottom:14px"><button id="opus-udrill-close" style="padding:6px 14px;background:#171b2a;border:1px solid rgba(99,102,241,0.25);color:#e2e8f0;border-radius:8px;cursor:pointer;font-size:12px">✕ Fermer (Esc)</button></div>';
html += '<div style="transform-origin:top left;font-size:1.05em">' + card.outerHTML + '</div>';
inner.innerHTML = html;
d.getElementById('opus-udrill-close').onclick = function(){ m.style.display='none'; };
m.style.display = 'flex';
}
function wire(root) {
var sels = '.card,[class*="card"],.kpi,[class*="kpi"],.stat,[class*="stat"],.tile,[class*="tile"],.metric,[class*="metric"],.widget,[class*="widget"]';
var cards = root.querySelectorAll(sels);
for (var i = 0; i < cards.length; i++) {
var c = cards[i];
if (c.__opusWired) continue;
if (c.closest('button, a, input, select, textarea, #opus-udrill')) continue;
var r = c.getBoundingClientRect();
if (r.width < 60 || r.height < 40) continue;
c.__opusWired = true;
c.style.cursor = 'pointer';
c.setAttribute('role','button');
c.setAttribute('tabindex','0');
c.addEventListener('click', function(ev){
// If a more-specific drill is already active (e.g. pp-card custom), let it handle
if (ev.target.closest('[data-pp-id]') && window.__opusDrillInit) return;
if (ev.target.closest('a,button,input,select')) return;
ev.preventDefault(); ev.stopPropagation();
openCard(this);
});
c.addEventListener('keydown', function(ev){ if(ev.key==='Enter'||ev.key===' '){ev.preventDefault();openCard(this);} });
}
}
// Initial + mutation observer
var initRun = function(){ wire(d.body || d.documentElement); };
if (d.readyState === 'loading') d.addEventListener('DOMContentLoaded', initRun);
else initRun();
var mo = new MutationObserver(function(muts){
var newCard = false;
for (var i=0;i<muts.length;i++) if (muts[i].addedNodes.length) { newCard = true; break; }
if (newCard) initRun();
});
mo.observe(d.body || d.documentElement, {childList:true, subtree:true});
})();
</script>
<!-- === OPUS UNIVERSAL DRILL-DOWN END === -->
<script src="/api/a11y-auto-enhancer.js" defer></script>
<!-- WTP_UDOCK_V1 (Opus 21-avr t33b5) --><script src="/wtp-unified-dock.js" defer></script>
<script src="/opus-antioverlap-doctrine.js?v=1776776094" defer></script>
</body>
</html>