| <!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> |
|
|