Files
html/openclaw.html
2026-04-19 21:20:03 +02:00

368 lines
20 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>OpenClaw — WEVAL AI Gateway</title>
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&family=Outfit:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<style>
*{margin:0;padding:0;box-sizing:border-box}
:root{--bg:#0a0a0f;--bg2:#12121a;--bg3:#1a1a28;--bg4:#222235;--tx:#e8e6f0;--tx2:#9896a8;--tx3:#5c5a6e;--ac:#00e5ff;--ac2:#7c4dff;--ac3:#00e676;--ac4:#ff6e40;--bd:#2a2a3e;--red:#ff5252}
html,body{height:100%;background:var(--bg);color:var(--tx);font-family:'Outfit',sans-serif;overflow:hidden}
.app{display:grid;grid-template-columns:260px 1fr;height:100vh}
.sidebar{background:var(--bg2);border-right:1px solid var(--bd);display:flex;flex-direction:column;overflow-y:auto}
.sidebar::-webkit-scrollbar{width:4px}.sidebar::-webkit-scrollbar-thumb{background:var(--bd);border-radius:2px}
.sidebar-header{padding:16px 14px 10px;border-bottom:1px solid var(--bd)}
.logo{display:flex;align-items:center;gap:8px}
.logo-icon{width:32px;height:32px;background:linear-gradient(135deg,var(--ac),var(--ac2));border-radius:8px;display:flex;align-items:center;justify-content:center;font-weight:700;font-size:12px;color:#000;font-family:'JetBrains Mono',monospace}
.logo-text{font-size:16px;font-weight:600;letter-spacing:-.5px}
.logo-sub{font-size:9px;color:var(--tx3);text-transform:uppercase;letter-spacing:1.5px;margin-top:1px}
.logo-count{margin-left:auto;font-size:10px;color:var(--ac);font-family:'JetBrains Mono',monospace;background:rgba(0,229,255,.1);padding:2px 6px;border-radius:4px}
.section{padding:10px 14px 4px}
.section-label{font-size:9px;text-transform:uppercase;letter-spacing:1.5px;color:var(--tx3);margin-bottom:6px;font-weight:500;display:flex;align-items:center;gap:6px}
.tier-dot{width:6px;height:6px;border-radius:50%}
.tier-dot.free{background:var(--ac3)}.tier-dot.paid{background:var(--ac4)}.tier-dot.sovereign{background:var(--ac)}
.provider-btn{background:var(--bg3);border:1px solid transparent;border-radius:6px;padding:7px 10px;cursor:pointer;text-align:left;transition:all .12s;color:var(--tx2);font-family:'Outfit',sans-serif;font-size:12px;display:flex;align-items:center;gap:6px;width:100%;margin-bottom:3px}
.provider-btn:hover{background:var(--bg4);color:var(--tx)}
.provider-btn.active{background:rgba(0,229,255,.08);border-color:rgba(0,229,255,.3);color:var(--ac)}
.provider-btn .name{flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.speed{font-size:8px;padding:1px 4px;border-radius:3px;font-weight:600;text-transform:uppercase;letter-spacing:.5px}
.speed.ultra{background:rgba(0,229,255,.15);color:var(--ac)}.speed.fast{background:rgba(0,230,118,.12);color:var(--ac3)}.speed.slow{background:rgba(255,110,64,.1);color:var(--ac4)}
.mcnt{font-size:9px;color:var(--tx3);font-family:'JetBrains Mono',monospace}
select{width:100%;background:var(--bg3);border:1px solid var(--bd);border-radius:6px;padding:6px 8px;color:var(--tx);font-family:'JetBrains Mono',monospace;font-size:11px;outline:none;cursor:pointer;-webkit-appearance:none}
select:focus{border-color:var(--ac)}
.ctrl-section{padding:8px 14px}
.ctrl-label{font-size:9px;text-transform:uppercase;letter-spacing:1px;color:var(--tx3);margin-bottom:4px;font-weight:500}
.temp-row{display:flex;align-items:center;gap:6px}
.temp-row input[type=range]{flex:1;accent-color:var(--ac);height:3px;-webkit-appearance:none;background:var(--bd);border-radius:2px;outline:none}
.temp-row input[type=range]::-webkit-slider-thumb{-webkit-appearance:none;width:12px;height:12px;border-radius:50%;background:var(--ac);cursor:pointer}
.temp-val{font-family:'JetBrains Mono',monospace;font-size:11px;color:var(--ac);min-width:24px}
textarea.sys{width:100%;background:var(--bg3);border:1px solid var(--bd);border-radius:6px;padding:6px 8px;color:var(--tx);font-family:'JetBrains Mono',monospace;font-size:10px;resize:vertical;min-height:50px;outline:none;line-height:1.4}
textarea.sys:focus{border-color:var(--ac)}
.stats{padding:10px 14px;border-top:1px solid var(--bd);margin-top:auto}
.stat-row{display:flex;justify-content:space-between;font-size:10px;color:var(--tx3);margin-bottom:3px}
.stat-val{color:var(--tx2);font-family:'JetBrains Mono',monospace}
.main{display:flex;flex-direction:column;height:100vh;background:var(--bg)}
.chat-header{padding:10px 20px;border-bottom:1px solid var(--bd);display:flex;align-items:center;gap:10px;background:var(--bg2)}
.active-badge{display:flex;align-items:center;gap:5px;font-size:12px;color:var(--tx2)}
.active-badge .dot{width:5px;height:5px;border-radius:50%;background:var(--ac3);animation:pulse 2s infinite}
.model-name{font-family:'JetBrains Mono',monospace;font-size:12px;color:var(--ac);margin-left:auto;max-width:400px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.clear-btn{background:none;border:1px solid var(--bd);border-radius:5px;padding:4px 10px;color:var(--tx3);font-size:10px;cursor:pointer;font-family:'Outfit',sans-serif;transition:all .12s}
.clear-btn:hover{border-color:var(--red);color:var(--red)}
@keyframes pulse{0%,100%{opacity:1}50%{opacity:.4}}
.messages{flex:1;overflow-y:auto;padding:16px 20px;display:flex;flex-direction:column;gap:14px}
.messages::-webkit-scrollbar{width:5px}.messages::-webkit-scrollbar-thumb{background:var(--bd);border-radius:3px}
.msg{display:flex;gap:10px;max-width:85%;animation:msgIn .2s ease-out}
.msg.user{align-self:flex-end;flex-direction:row-reverse}
.msg.assistant{align-self:flex-start}
@keyframes msgIn{from{opacity:0;transform:translateY(6px)}to{opacity:1;transform:translateY(0)}}
.msg-avatar{width:26px;height:26px;border-radius:7px;display:flex;align-items:center;justify-content:center;font-size:11px;font-weight:600;flex-shrink:0;font-family:'JetBrains Mono',monospace}
.msg.user .msg-avatar{background:var(--ac2);color:#fff}
.msg.assistant .msg-avatar{background:var(--ac);color:#000}
.msg-bubble{padding:10px 14px;border-radius:12px;font-size:13px;line-height:1.6;white-space:pre-wrap;word-break:break-word}
.msg.user .msg-bubble{background:var(--bg4);border-bottom-right-radius:4px}
.msg.assistant .msg-bubble{background:var(--bg2);border:1px solid var(--bd);border-bottom-left-radius:4px}
.msg-bubble code{background:var(--bg);padding:1px 4px;border-radius:3px;font-family:'JetBrains Mono',monospace;font-size:11px}
.msg-bubble pre{background:var(--bg);padding:10px;border-radius:6px;overflow-x:auto;margin:6px 0;font-family:'JetBrains Mono',monospace;font-size:11px;line-height:1.4}
.msg-meta{font-size:9px;color:var(--tx3);margin-top:3px;font-family:'JetBrains Mono',monospace}
.typing{display:flex;gap:3px;padding:6px 0}
.typing span{width:5px;height:5px;border-radius:50%;background:var(--ac);animation:blink 1.4s infinite}
.typing span:nth-child(2){animation-delay:.2s}
.typing span:nth-child(3){animation-delay:.4s}
@keyframes blink{0%,100%{opacity:.2}50%{opacity:1}}
.empty{flex:1;display:flex;align-items:center;justify-content:center;flex-direction:column;gap:12px;color:var(--tx3)}
.empty-icon{font-size:40px;opacity:.12;font-family:'JetBrains Mono',monospace}
.empty-text{font-size:13px;text-align:center;line-height:1.5}
.empty-stats{font-family:'JetBrains Mono',monospace;font-size:11px;color:var(--ac);opacity:.5}
.input-area{padding:12px 20px;border-top:1px solid var(--bd);background:var(--bg2)}
.input-row{display:flex;gap:8px;align-items:flex-end}
.input-box{flex:1;position:relative}
.input-box textarea{width:100%;background:var(--bg3);border:1px solid var(--bd);border-radius:10px;padding:10px 44px 10px 14px;color:var(--tx);font-family:'Outfit',sans-serif;font-size:13px;resize:none;outline:none;min-height:44px;max-height:140px;line-height:1.4;transition:border .12s}
.input-box textarea:focus{border-color:var(--ac)}
.input-box textarea::placeholder{color:var(--tx3)}
.send-btn{position:absolute;right:6px;bottom:6px;width:32px;height:32px;border-radius:7px;background:var(--ac);border:none;cursor:pointer;display:flex;align-items:center;justify-content:center;transition:all .12s}
.send-btn:hover{background:#33ecff;transform:scale(1.05)}
.send-btn:disabled{background:var(--bg4);cursor:not-allowed;transform:none}
.send-btn svg{width:14px;height:14px}
.input-hint{font-size:9px;color:var(--tx3);margin-top:4px;text-align:center}
@media(max-width:768px){.app{grid-template-columns:1fr}.sidebar{display:none}}
</style>
</head>
<body>
<div class="app">
<div class="sidebar">
<div class="sidebar-header">
<div class="logo">
<div class="logo-icon">OC</div>
<div><div class="logo-text">OpenClaw</div><div class="logo-sub">WEVAL AI Gateway</div></div>
<div class="logo-count" id="totalModels"></div>
</div>
</div>
<div id="providerList"></div>
<div class="ctrl-section">
<div class="ctrl-label">Model</div>
<select id="modelSelect"></select>
</div>
<div class="ctrl-section">
<div class="ctrl-label">Temperature</div>
<div class="temp-row">
<input type="range" min="0" max="20" value="7" id="tempSlider" step="1" oninput="document.getElementById('tempVal').textContent=(this.value/10).toFixed(1)">
<span class="temp-val" id="tempVal">0.7</span>
</div>
</div>
<div class="ctrl-section">
<div class="ctrl-label">System prompt</div>
<textarea class="sys" id="sysPrompt" placeholder="Tu es un assistant IA expert..."></textarea>
</div>
<div class="stats">
<div class="stat-row"><span>Messages</span><span class="stat-val" id="statMsgs">0</span></div>
<div class="stat-row"><span>Tokens (est.)</span><span class="stat-val" id="statTokens">0</span></div>
<div class="stat-row"><span>Latency</span><span class="stat-val" id="statLatency"></span></div>
</div>
</div>
<div class="main">
<div class="chat-header">
<div class="active-badge"><span class="dot"></span> Connected</div>
<span class="model-name" id="headerModel"></span>
<button class="clear-btn" onclick="clearChat()">Clear</button>
</div>
<div class="messages" id="messages">
<div class="empty">
<div class="empty-icon">&gt;_</div>
<div class="empty-text">10 providers. 40 models. Zero cost.<br>Cloud + Sovereign AI — your choice.</div>
<div class="empty-stats" id="emptyStats"></div>
</div>
</div>
<div class="input-area">
<div class="input-row">
<div class="input-box">
<textarea id="userInput" rows="1" placeholder="Send a message..." onkeydown="if(event.key==='Enter'&&!event.shiftKey){event.preventDefault();sendMsg()}"></textarea>
<button class="send-btn" id="sendBtn" onclick="sendMsg()">
<svg viewBox="0 0 24 24" fill="none" stroke="#000" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><line x1="22" y1="2" x2="11" y2="13"/><polygon points="22 2 15 22 11 13 2 9 22 2"/></svg>
</button>
</div>
</div>
<div class="input-hint">Enter to send · Shift+Enter newline · Streaming</div>
</div>
</div>
</div>
<script>
const API='/api/openclaw-proxy.php';
let providers=[],activeProvider='groq',history=[],isStreaming=false;
const tierLabels={free:'Cloud free',paid:'Cloud paid',sovereign:'Sovereign'};
const tierOrder=['free','paid','sovereign'];
async function loadProviders(){
try{
const r=await fetch(API+'?action=providers');
const d=await r.json();
providers=d.providers;
document.getElementById('totalModels').textContent=d.total_models+' models';
document.getElementById('emptyStats').textContent=providers.length+' providers · '+d.total_models+' models';
renderProviders();
selectProvider('groq');
}catch(e){console.error(e)}
}
function renderProviders(){
const list=document.getElementById('providerList');
let html='';
for(const tier of tierOrder){
const group=providers.filter(p=>p.tier===tier);
if(!group.length) continue;
html+=`<div class="section"><div class="section-label"><span class="tier-dot ${tier}"></span>${tierLabels[tier]}</div>`;
for(const p of group){
const active=p.id===activeProvider?'active':'';
const nokey=p.has_key?'':`<span style="font-size:8px;color:var(--red)">NO KEY</span>`;
html+=`<button class="provider-btn ${active}" onclick="selectProvider('${p.id}')">
<span class="name">${p.name}</span>
${nokey}
<span class="speed ${p.speed}">${p.speed}</span>
<span class="mcnt">${p.models.length}</span>
</button>`;
}
html+=`</div>`;
}
list.innerHTML=html;
}
function selectProvider(id){
activeProvider=id;
renderProviders();
const p=providers.find(x=>x.id===id);
if(!p) return;
const sel=document.getElementById('modelSelect');
sel.innerHTML=p.models.map(m=>`<option value="${m.id}">${m.name}</option>`).join('');
document.getElementById('headerModel').textContent=p.name+' / '+p.models[0].name;
sel.onchange=()=>{
const m=p.models.find(x=>x.id===sel.value);
document.getElementById('headerModel').textContent=p.name+' / '+(m?m.name:sel.value);
};
}
function clearChat(){
history=[];
document.getElementById('messages').innerHTML=`<div class="empty"><div class="empty-icon">&gt;_</div><div class="empty-text">10 providers. 40 models. Zero cost.<br>Cloud + Sovereign AI — your choice.</div></div>`;
updateStats();
}
function addMsg(role,content,meta){
const el=document.getElementById('messages');
const empty=el.querySelector('.empty');
if(empty) empty.remove();
const div=document.createElement('div');
div.className='msg '+role;
const avatar=role==='user'?'Y':'AI';
div.innerHTML=`<div class="msg-avatar">${avatar}</div><div><div class="msg-bubble">${escapeHtml(content)}</div>${meta?`<div class="msg-meta">${meta}</div>`:''}</div>`;
el.appendChild(div);
el.scrollTop=el.scrollHeight;
return div;
}
function escapeHtml(t){return t.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;')}
function updateStats(){
document.getElementById('statMsgs').textContent=history.length;
document.getElementById('statTokens').textContent=history.reduce((a,m)=>a+Math.ceil(m.content.length/4),0).toLocaleString();
}
async function sendMsg(){
if(isStreaming) return;
const input=document.getElementById('userInput');
const text=input.value.trim();
if(!text) return;
input.value='';input.style.height='auto';
history.push({role:'user',content:text});
addMsg('user',text);
updateStats();
const sel=document.getElementById('modelSelect');
const model=sel.value;
const temp=parseFloat(document.getElementById('tempSlider').value)/10;
const sys=document.getElementById('sysPrompt').value.trim();
const p=providers.find(x=>x.id===activeProvider);
const modelName=p?p.models.find(m=>m.id===model)?.name||model:model;
const msgDiv=addMsg('assistant','');
const bubble=msgDiv.querySelector('.msg-bubble');
bubble.innerHTML='<div class="typing"><span></span><span></span><span></span></div>';
document.getElementById('sendBtn').disabled=true;
isStreaming=true;
const t0=performance.now();
let fullText='';
try{
const resp=await fetch(API,{method:'POST',headers:{'Content-Type':'application/json'},
body:JSON.stringify({provider:activeProvider,model,messages:history.filter(m=>m.role!=='system'),stream:true,temperature:temp,system:sys})});
const reader=resp.body.getReader();
const decoder=new TextDecoder();
let buf='';
while(true){
const{done,value}=await reader.read();
if(done) break;
buf+=decoder.decode(value,{stream:true});
const lines=buf.split('\n');
buf=lines.pop();
for(const line of lines){
if(!line.startsWith('data: ')) continue;
const data=line.slice(6).trim();
if(data==='[DONE]') continue;
try{
const j=JSON.parse(data);
const delta=j.choices?.[0]?.delta?.content||'';
if(delta){fullText+=delta;bubble.textContent=fullText;}
}catch(e){}
}
}
}catch(e){fullText='Error: '+e.message;bubble.textContent=fullText;bubble.style.color='var(--red)'}
if(!fullText||fullText.startsWith('Error')||fullText.length<2){const fallbacks=['mistral','cerebras','sambanova','nvidia'];const fb=fallbacks.find(f=>f!==activeProvider);if(fb){bubble.textContent='Retrying with '+fb+'...';try{const r2=await fetch(API,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({provider:fb,model:'',messages:history.filter(m=>m.role!=='system'),stream:false,temperature:temp,system:sys})});const d=await r2.json();fullText=d.choices?.[0]?.message?.content||d.error?.message||'No response';bubble.textContent=fullText;const meta2=document.querySelector('.msg-meta:last-of-type');if(meta2)meta2.textContent=fb+' (fallback)'}catch(e2){bubble.textContent='All providers failed: '+e2.message;bubble.style.color='var(--red)'}}}
const latency=Math.round(performance.now()-t0);
const metaDiv=document.createElement('div');
metaDiv.className='msg-meta';
metaDiv.textContent=(p?p.name:activeProvider)+' / '+modelName+' · '+latency+'ms';
bubble.parentElement.appendChild(metaDiv);
if(fullText&&!fullText.startsWith('Error')){history.push({role:'assistant',content:fullText})}
isStreaming=false;
document.getElementById('sendBtn').disabled=false;
document.getElementById('statLatency').textContent=latency+'ms';
updateStats();
document.getElementById('messages').scrollTop=document.getElementById('messages').scrollHeight;
}
document.getElementById('userInput').addEventListener('input',function(){this.style.height='auto';this.style.height=Math.min(this.scrollHeight,140)+'px'});
loadProviders();
</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 === -->
</body>
</html>