303 lines
17 KiB
HTML
303 lines
17 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">>_</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">>_</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,'&').replace(/</g,'<').replace(/>/g,'>')}
|
|
|
|
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>
|
|
</body>
|
|
</html>
|