webui / preset_creator.html
Nerdur's picture
Upload 3 files
720200d verified
Raw
History Blame Contribute Delete
14.6 kB
<!DOCTYPE html>
<html lang="bs">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>WebUI Preset Creator</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@tabler/icons-webfont@3.19.0/dist/tabler-icons.min.css">
<style>
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;background:#0f0f0f;color:#e0e0e0;padding:1rem;max-width:480px;margin:0 auto}
h1{font-size:16px;font-weight:500;margin-bottom:1rem;color:#fff}
.sec{font-size:11px;font-weight:500;color:#888;text-transform:uppercase;letter-spacing:.06em;margin:1.2rem 0 .4rem}
.card{background:#1a1a1a;border:0.5px solid #333;border-radius:12px;padding:1rem 1.25rem;margin-bottom:10px}
.row{display:flex;align-items:flex-start;gap:8px;margin-bottom:8px}
.row label{font-size:13px;color:#888;width:100px;flex-shrink:0;padding-top:8px}
.row input,.row select,.row textarea{flex:1;font-size:13px;background:#111;border:0.5px solid #444;border-radius:8px;padding:7px 10px;color:#e0e0e0;outline:none}
.row input:focus,.row textarea:focus{border-color:#555}
.grid2{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:6px;margin-bottom:8px}
.prov-btn{background:#222;border:0.5px solid #444;border-radius:8px;padding:10px 6px;text-align:center;cursor:pointer;font-size:12px;color:#ccc;transition:all .15s}
.prov-btn:hover{background:#2a2a2a;border-color:#555}
.prov-btn.active{border:1.5px solid #3b82f6;background:#1e3a5f;color:#93c5fd}
.prov-btn i{font-size:18px;display:block;margin-bottom:3px}
.model-list{max-height:200px;overflow-y:auto;border:0.5px solid #333;border-radius:8px;margin-top:6px;display:none}
.model-item{padding:8px 12px;font-size:13px;cursor:pointer;border-bottom:0.5px solid #2a2a2a;color:#ccc}
.model-item:last-child{border-bottom:none}
.model-item:hover{background:#222}
.model-item.selected{background:#1e3a5f;color:#93c5fd}
.sel-badge{font-size:12px;font-family:monospace;color:#666;margin-top:4px;min-height:16px;word-break:break-all}
.check-grid{display:flex;flex-direction:column;gap:4px}
.check-item{display:flex;align-items:center;gap:8px;padding:7px 10px;background:#222;border-radius:8px;border:0.5px solid #333;cursor:pointer}
.check-item input{width:16px;height:16px;cursor:pointer;flex-shrink:0;accent-color:#3b82f6}
.check-item .lbl{font-size:13px;flex:1;color:#ccc;cursor:pointer}
.check-item .tag{font-size:10px;padding:2px 6px;border-radius:4px;background:#1e3a5f;color:#93c5fd}
.check-item .tag.tool{background:#14532d;color:#86efac}
.empty{font-size:13px;color:#666;padding:8px}
button{background:#222;border:0.5px solid #444;border-radius:8px;padding:8px 16px;color:#ccc;font-size:13px;cursor:pointer;width:auto;transition:all .15s}
button:hover{background:#2a2a2a;border-color:#555}
button.primary{background:#1e3a5f;border-color:#3b82f6;color:#93c5fd}
button.danger{color:#f87171;border-color:#7f1d1d}
.status{font-size:13px;padding:8px 12px;border-radius:8px;margin-top:8px;display:none}
.status.ok{display:block;background:#14532d;color:#86efac}
.status.err{display:block;background:#450a0a;color:#f87171}
.status.info{display:block;background:#1e3a5f;color:#93c5fd}
.prog{font-size:12px;color:#666;margin-top:4px;min-height:14px}
.spin{display:inline-block;animation:spin 1s linear infinite}
@keyframes spin{to{transform:rotate(360deg)}}
.refresh-row{display:flex;align-items:center;justify-content:space-between;margin-bottom:8px}
.refresh-row span{font-size:13px;color:#888}
.refresh-row button{width:auto;font-size:12px;padding:3px 10px}
</style>
</head>
<body>
<h1><i class="ti ti-sparkles"></i> WebUI Preset Creator</h1>
<div class="sec">Prijava na WebUI</div>
<div class="card">
<div class="row"><label>URL</label><input type="text" id="url" value="https://nerdur-webui.hf.space"></div>
<div class="row"><label>Email</label><input type="email" id="email" placeholder="admin@email.com"></div>
<div class="row"><label>Lozinka</label><input type="password" id="pass" placeholder="••••••••"></div>
<button class="primary" onclick="doLogin()" style="width:100%"><i class="ti ti-login"></i> Prijavi se</button>
<div class="status" id="ls"></div>
</div>
<div id="main" style="display:none">
<div class="sec">Odaberi provajdera i model</div>
<div class="card">
<div class="grid2" id="pgrid">
<div class="prov-btn" onclick="loadModels(this,'groq')"><i class="ti ti-bolt"></i>Groq</div>
<div class="prov-btn" onclick="loadModels(this,'gemini')"><i class="ti ti-sparkles"></i>Gemini</div>
<div class="prov-btn" onclick="loadModels(this,'mistral')"><i class="ti ti-circle"></i>Mistral</div>
<div class="prov-btn" onclick="loadModels(this,'sambanova')"><i class="ti ti-bolt"></i>SambaNova</div>
<div class="prov-btn" onclick="loadModels(this,'nvidia')"><i class="ti ti-cpu"></i>NVIDIA</div>
<div class="prov-btn" onclick="loadModels(this,'hf')"><i class="ti ti-mood-wink"></i>HuggingFace</div>
<div class="prov-btn" onclick="loadModels(this,'pollinations')"><i class="ti ti-flower"></i>Pollinations</div>
<div class="prov-btn" onclick="loadModels(this,'cerebras')"><i class="ti ti-brain"></i>Cerebras</div>
</div>
<div id="model-loading" style="display:none;font-size:13px;color:#888;padding:6px 0">
<i class="ti ti-refresh spin"></i> Učitavam modele...
</div>
<div id="model-list" class="model-list"></div>
<div class="sel-badge" id="sel-model">← Odaberi provajdera pa model</div>
</div>
<div class="sec">Detalji preseta</div>
<div class="card">
<div class="row"><label>Preset ID</label><input type="text" id="pid" placeholder="npr. moj_groq_llama"></div>
<div class="row"><label>Naziv</label><input type="text" id="pname" placeholder="npr. ⚡ Groq Llama"></div>
<div class="row">
<label>Temperatura</label>
<input type="number" id="temp" value="0.2" min="0" max="2" step="0.1" style="width:70px;flex:none">
<span style="font-size:13px;color:#888;padding-top:8px;margin-left:10px">Max tokena</span>
<input type="number" id="maxtok" value="8192" step="256" style="width:85px;flex:none;margin-left:6px">
</div>
<div class="row"><label>System Prompt</label><textarea id="sysp" rows="3" style="flex:1;font-size:13px;resize:vertical" placeholder="Ostavi prazno za default..."></textarea></div>
</div>
<div class="sec">Funkcije i toolovi</div>
<div class="card">
<div class="refresh-row">
<span>Odaberi šta prikaći na preset</span>
<button onclick="refreshFunctions()"><i class="ti ti-refresh"></i> Osvježi</button>
</div>
<div id="fn-loading" style="font-size:13px;color:#888;display:none"><i class="ti ti-refresh spin"></i> Učitavam...</div>
<div id="fn-list" class="check-grid"><div class="empty">Prijavi se da učitam funkcije i toolove.</div></div>
</div>
<button class="primary" onclick="createPreset()" style="width:100%;margin-bottom:6px">
<i class="ti ti-sparkles"></i> Kreiraj / Ažuriraj Preset
</button>
<div class="status" id="ps"></div>
<div class="prog" id="prog"></div>
<div class="sec">Obriši preset</div>
<div class="card">
<div class="row"><label>Preset ID</label><input type="text" id="did" placeholder="npr. nerdur_claude"></div>
<button class="danger" onclick="deletePreset()" style="width:100%">
<i class="ti ti-trash"></i> Obriši
</button>
</div>
</div>
<script>
let TOKEN='', BASE='';
function setS(id,msg,t){const e=document.getElementById(id);e.className='status '+t;e.textContent=msg}
async function doLogin(){
BASE=document.getElementById('url').value.trim().replace(/\/$/,'');
const email=document.getElementById('email').value.trim();
const pass=document.getElementById('pass').value;
setS('ls','Prijavljivanje...','info');
try{
const r=await fetch(BASE+'/api/v1/auths/signin',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({email,password:pass})});
const d=await r.json();
if(d.token){
TOKEN=d.token;
setS('ls','✅ Ulogovan kao '+(d.name||email),'ok');
document.getElementById('main').style.display='block';
refreshFunctions();
} else {
setS('ls','❌ '+(d.detail||'Pogrešni podaci'),'err');
}
}catch(e){setS('ls','❌ '+e.message,'err')}
}
const PROVIDER_KEYWORDS={
groq:['groq','llama','gemma','whisper','moonshotai'],
gemini:['gemini','google','models/'],
mistral:['mistral','codestral','mixtral'],
sambanova:['sambanova','meta-llama'],
nvidia:['nvidia','integrate.api.nvidia','mistral-ai/','deepseek-ai/','qwen/','zhipuai/','stepfun','baidu','canopy','bytedance'],
hf:['huggingface','hf-inference','qwen2','falcon','bloom','phi'],
pollinations:['pollinations','openai','gpt','dall'],
cerebras:['cerebras','llama-4','scout']
};
async function loadModels(btn,provider){
document.querySelectorAll('.prov-btn').forEach(b=>b.classList.remove('active'));
btn.classList.add('active');
const ml=document.getElementById('model-list');
const loading=document.getElementById('model-loading');
ml.style.display='none'; ml.innerHTML='';
loading.style.display='block';
document.getElementById('sel-model').textContent='Učitavam modele...';
try{
const r=await fetch(BASE+'/api/models',{headers:{'Authorization':'Bearer '+TOKEN}});
const d=await r.json();
const keys=PROVIDER_KEYWORDS[provider]||[];
const models=(d.data||[]).filter(m=>{
const id=(m.id||'').toLowerCase();
const owned=(m.owned_by||'').toLowerCase();
const name=(m.name||'').toLowerCase();
return keys.some(k=>id.includes(k)||owned.includes(k)||name.includes(k));
});
loading.style.display='none';
if(models.length===0){
ml.innerHTML='<div class="empty">Nema modela za ovog provajdera.</div>';
ml.style.display='block'; return;
}
models.forEach(m=>{
const div=document.createElement('div');
div.className='model-item';
div.textContent=m.name||m.id;
div.title=m.id;
div.onclick=()=>{
document.querySelectorAll('.model-item').forEach(x=>x.classList.remove('selected'));
div.classList.add('selected');
document.getElementById('sel-model').textContent=m.id;
if(!document.getElementById('pid').value) document.getElementById('pid').value='nerdur_'+provider;
if(!document.getElementById('pname').value) document.getElementById('pname').value=m.name||m.id;
};
ml.appendChild(div);
});
ml.style.display='block';
}catch(e){
loading.style.display='none';
ml.innerHTML='<div class="empty">Greška: '+e.message+'</div>';
ml.style.display='block';
}
}
async function refreshFunctions(){
if(!TOKEN) return;
const h={'Authorization':'Bearer '+TOKEN};
const fl=document.getElementById('fn-list');
const fload=document.getElementById('fn-loading');
fl.innerHTML=''; fload.style.display='block';
try{
const [fr,tr]=await Promise.all([
fetch(BASE+'/api/v1/functions/',{headers:h}).then(r=>r.json()),
fetch(BASE+'/api/v1/tools/',{headers:h}).then(r=>r.json())
]);
fload.style.display='none';
const items=[];
(Array.isArray(fr)?fr:[]).forEach(f=>items.push({id:f.id,name:f.name,type:'filter'}));
(Array.isArray(tr)?tr:[]).forEach(t=>items.push({id:t.id,name:t.name,type:'tool'}));
if(items.length===0){fl.innerHTML='<div class="empty">Nema instaliranih funkcija/toolova.</div>';return;}
items.forEach(item=>{
const div=document.createElement('div');
div.className='check-item';
div.innerHTML=`<input type="checkbox" id="fn_${item.id}" value="${item.id}" data-type="${item.type}">
<label class="lbl" for="fn_${item.id}">${item.name}</label>
<span class="tag ${item.type==='tool'?'tool':''}">${item.type}</span>`;
fl.appendChild(div);
});
}catch(e){
fload.style.display='none';
fl.innerHTML='<div class="empty">Greška: '+e.message+'</div>';
}
}
async function createPreset(){
const modelId=document.getElementById('sel-model').textContent.trim();
const presetId=document.getElementById('pid').value.trim();
const name=document.getElementById('pname').value.trim();
if(!TOKEN){setS('ps','❌ Prvo se prijavi!','err');return}
if(!modelId||modelId.startsWith('←')){setS('ps','❌ Odaberi model!','err');return}
if(!presetId||!name){setS('ps','❌ Popuni Preset ID i Naziv!','err');return}
const filterIds=[],toolIds=[];
document.querySelectorAll('#fn-list input[type=checkbox]:checked').forEach(cb=>{
if(cb.dataset.type==='tool') toolIds.push(cb.value);
else filterIds.push(cb.value);
});
const prog=document.getElementById('prog');
prog.textContent='Provjera...';
setS('ps','Kreiram...','info');
const payload={
id:presetId,name,
base_model_id:modelId,
meta:{description:name+' — preset',profile_image_url:'/static/favicon.png',tags:[{name:'custom'}],capabilities:{vision:false,citations:true,usage:true}},
params:{system:document.getElementById('sysp').value.trim()||'',temperature:parseFloat(document.getElementById('temp').value),max_tokens:parseInt(document.getElementById('maxtok').value)},
filter_ids:filterIds,
tool_ids:toolIds
};
const h={'Content-Type':'application/json','Authorization':'Bearer '+TOKEN};
try{
const chk=await fetch(BASE+'/api/v1/models/model?id='+encodeURIComponent(presetId),{headers:h});
const ep=chk.status===200?'/api/v1/models/model/update?id='+encodeURIComponent(presetId):'/api/v1/models/create';
prog.textContent=chk.status===200?'Ažuriram...':'Kreiram novi...';
const r=await fetch(BASE+ep,{method:'POST',headers:h,body:JSON.stringify(payload)});
const d=await r.json();
if(r.ok){
setS('ps','✅ "'+name+'" '+(chk.status===200?'ažuriran':'kreiran')+'!','ok');
prog.textContent='Filtri: '+filterIds.length+' | Toolovi: '+toolIds.length+' | Model: '+modelId;
} else {
setS('ps','❌ '+(d.detail||JSON.stringify(d).slice(0,120)),'err');
prog.textContent='';
}
}catch(e){setS('ps','❌ '+e.message,'err');prog.textContent=''}
}
async function deletePreset(){
if(!TOKEN){alert('Prvo se prijavi!');return}
const id=document.getElementById('did').value.trim();
if(!id){alert('Unesi Preset ID');return}
try{
const r=await fetch(BASE+'/api/v1/models/model/delete?id='+encodeURIComponent(id),{method:'DELETE',headers:{'Authorization':'Bearer '+TOKEN}});
if(r.ok) alert('✅ Preset "'+id+'" obrisan!');
else alert('❌ Greška ili preset ne postoji.');
}catch(e){alert('❌ '+e.message)}
}
</script>
</body>
</html>