${kind==='image'?'Der feste API-Name athena-image verwendet das hier freigegebene Bildprofil und dessen Auflösung. Höchstens ein Bildprofil ist am API-Endpunkt freigegeben. Ein neuer Haken ersetzt die bisherige Auswahl.':['audio','stt'].includes(kind)?'Eingerichtete Sprachprofile können am API-Endpunkt freigegeben werden.':kind==='chat'?'Mehrere Sprachmodellprofile können gleichzeitig am API-Endpunkt freigegeben sein.':'Für diesen Bereich ist noch keine ausführbare Laufzeit am Endpunkt angebunden.'} Freigegeben bedeutet nicht bereits in den GPU-Speicher geladen.
`);
root.querySelectorAll('[data-publish]').forEach(b=>b.onchange=async()=>{const checked=b.checked;root.querySelectorAll('[data-publish]').forEach(x=>x.disabled=true);try{await api('endpoint/profile',{id:b.dataset.publish,enabled:checked});await load();message('Endpunkt-Freigabe gespeichert. Geladen wird erst bei einer API-Anfrage.');}catch(error){await load();message(error.message);}});
root.querySelectorAll('[data-delete]').forEach(b=>b.onclick=async()=>{const p=rows.find(p=>p.id===b.dataset.delete);if(!confirm(`Profil „${p.name}“ löschen? Modelldateien und Komponenten bleiben in der Bibliothek.`))return;b.disabled=true;try{await api('profiles/delete',{id:p.id,revision:p.revision});panelSequence++;el('profile-editor').replaceChildren();await load();message('Profil gelöscht. Modelldateien und Komponenten bleiben erhalten.');}catch(error){message(error.message);b.disabled=false;}});
root.querySelectorAll('[data-components]').forEach(b=>b.onclick=()=>components(rows.find(p=>p.id===b.dataset.components)));
root.querySelectorAll('[data-edit]').forEach(b=>b.onclick=()=>editor(rows.find(p=>p.id===b.dataset.edit)));
root.querySelectorAll('[data-copy]').forEach(b=>b.onclick=()=>{const p=rows.find(p=>p.id===b.dataset.copy);editor({...p,id:null,revision:0,name:p.name.slice(0,55)+'-kopie'});});
}catch(error){message(error.message);}}
async function components(profile){
const sequence=++panelSequence;
el('profile-editor').innerHTML='
`:''}`;
const missing=data.requirements?.filter(r=>!r.available.length)||[];
if(el('components-download-all')){el('components-download-all').disabled=!missing.length;el('components-download-all').onclick=async()=>{
const b=el('components-download-all');b.disabled=true;let count=0;
try{for(const r of missing){const f=r.candidates.find(f=>!f.gated||f.auth_configured);if(!f)throw Error('Keine frei herunterladbare Datei für '+r.label);const catalog=await api('catalog');if((catalog.downloads||[]).some(j=>['queued','downloading'].includes(j.state)&&j.repo===f.repo&&j.file===f.name&&j.revision===f.revision))continue;await api('catalog/download',{repo:f.repo,filename:f.name,revision:f.revision,kind:'image'});count++;}if(el('components-message'))el('components-message').textContent='Dateien sind im Downloadplan. Nach Abschluss Bibliothek aktualisieren und Zuordnung speichern; passende Dateien werden vorausgewählt.';}
catch(error){if(el('components-message'))el('components-message').textContent=error.message;b.disabled=false;}
};}
el('components-close').onclick=()=>el('profile-editor').replaceChildren();
el('components-refresh')?.addEventListener('click',()=>components(profile));
el('components-form')?.addEventListener('submit',async event=>{event.preventDefault();const button=el('components-save');button.disabled=true;try{await api('profiles/components',{id:profile.id,revision:profile.revision,components:Object.fromEntries(new FormData(event.target))});if(!root.isConnected)return;el('profile-editor').replaceChildren();await load();message('Komponenten dem Profil zugeordnet. Kein Modell gestartet.');}catch(error){if(el('components-message'))el('components-message').textContent=error.message;}finally{button.disabled=false;}});
root.querySelectorAll('[data-component-download]').forEach(b=>b.onclick=async()=>{const [role,index]=b.dataset.componentDownload.split(':'),f=data.requirements.find(r=>r.role===role).candidates[Number(index)];b.disabled=true;try{await api('catalog/download',{repo:f.repo,filename:f.name,revision:f.revision,kind:'image'});if(el('components-message'))el('components-message').textContent='Download gestartet. Fortschritt und Abbruch findest du unter Downloads. Nach Abschluss hier die Bibliothek aktualisieren.';}catch(error){if(el('components-message'))el('components-message').textContent=error.message;b.disabled=false;}});
}catch(error){message(error.message);el('profile-editor').innerHTML='
Komponenten konnten nicht geladen werden. Öffne die Ansicht erneut.
';}
}
function gpuEditor(params){
const selected=params.gpu_devices||[],available=[...gpus];for(const uuid of selected)if(!available.some(g=>g.uuid===uuid))available.push({uuid,name:'Derzeit nicht verfügbar'});
const options=(value,empty)=>``+available.map(g=>``).join('');
return `
GPU-Auswahl und Verteilung
Manuelle Reserven gelten für die jeweils ausgewählte GPU. Ohne Reserve entfällt nur der zusätzliche GPU-Puffer; Modell, KV-Cache und die konservative Projektor-Prognose bleiben berücksichtigt. OOM bleibt möglich. Für einen Abbruch statt CPU-Auslagerung „Vollständig auf GPU“ wählen. Die Reihenfolge legt die logischen CUDA-Geräte des Profils fest. Für Medium: RTX 5080 zuerst, RTX 3060 danach, Layer-Split und 85,15. Leere Verteilung bedeutet automatisch. Dies ist keine Zusage, dass Modell und Kontext in den verfügbaren Speicher passen.
`;
}
function bindProjectorDiscovery(){
const form=el('profile-form'),box=el('projector-discovery'),select=form.elements.vision_projector;
let sequence=0;
const live=n=>box.isConnected&&n===sequence;
const mmproj=f=>/(?:^|[\/_-])mmproj(?:[\/_.-]|$)/i.test(f.name)&&f.name.toLowerCase().endsWith('.gguf');
async function refreshLibrary(){
const c=await api('catalog');if(!box.isConnected)return;
const selected=select.value;projectors=c.entries.filter(x=>x.role==='vision_projector'&&x.file.endsWith('.gguf'));
select.innerHTML=''+projectors.map(x=>``).join('');select.value=selected;
return c;
}
function showFiles(detail,target,n){
const files=detail.files.filter(mmproj);
target.innerHTML=`
`;target.append(row);
const button=row.querySelector('button'),status=row.querySelector('span');
function update(){const entry=projectors.find(x=>x.repo===detail.repo&&x.revision===detail.revision&&x.file===f.name);button.textContent=entry?'Im Profil auswählen':'Herunterladen';button.disabled=!!detail.gated&&!detail.auth_configured;return entry;}
update();button.onclick=async()=>{button.disabled=true;try{
await refreshLibrary();if(!live(n))return;const entry=update();
if(entry){select.value=entry.id;status.textContent=' Ausgewählt. Profil speichern, um die Zuordnung zu übernehmen.';return;}
await api('catalog/download',{repo:detail.repo,filename:f.name,revision:detail.revision,kind:'chat'});
status.textContent=' Download läuft. Fortschritt unter Downloads; danach Bibliothek aktualisieren und hier auswählen.';
}catch(error){status.textContent=error.message;}finally{if(live(n))update();}};
});
}
async function check(){
const n=++sequence,model=models.find(x=>x.id===form.elements.model_id.value);
box.innerHTML='
Eigenes Modell-Repository wird auf Projektoren geprüft …
${hasOwn?'Projektor aus dem Modell-Repository':'Externen Vision-Projektor hinzufügen'}
${hasOwn?'Das Modell-Repository bietet einen eigenen Projektor. Wähle eine passende Variante.':'Im Modell-Repository wurde keine separate mmproj-Datei gefunden. Suche im Repository des ursprünglichen Vision-Modells.'}
Nur für kompatible Vision-Architekturen. Ein Projektor macht aus einem reinen Textmodell kein Vision-Modell. Modellkarte und Modellversion prüfen.
`;
const status=box.querySelector('[data-status]'),target=box.querySelector('[data-files]');
box.querySelector('[data-refresh]').onclick=async()=>{try{await refreshLibrary();if(live(n)){status.textContent='Bibliothek aktualisiert. Projektor oben auswählen und Profil speichern.';}}catch(error){status.textContent=error.message;}};
if(hasOwn){showFiles(detail,target,n);return;}
const search=box.querySelector('[data-search]');search.innerHTML='';
let searchSequence=0;
search.querySelector('button').onclick=async()=>{
const current=++searchSequence;status.textContent='Repositories werden gesucht …';target.replaceChildren();
try{const result=await api('catalog/search?'+new URLSearchParams({q:search.querySelector('input').value.trim(),kind:'chat',purpose:'projector'}));if(!live(n)||current!==searchSequence)return;
const results=search.querySelector('[data-results]');results.replaceChildren();status.textContent=result.models.length?'Repository auswählen; angezeigt werden anschließend ausschließlich Projektordateien.':'Keine Treffer. Versuche den direkten Repository-Link.';
result.models.forEach(m=>{const b=document.createElement('button');b.type='button';b.className='secondary';b.textContent=m.repo;results.append(b);b.onclick=async()=>{const request=++searchSequence;status.textContent='Projektordateien werden geprüft …';try{const d=await api('catalog/files?'+new URLSearchParams({repo:m.repo}));if(!live(n)||request!==searchSequence)return;showFiles(d,target,n);status.textContent='Kompatibilität anhand der verlinkten Modellkarte prüfen.';}catch(error){if(live(n)&&request===searchSequence)status.textContent=error.message;}};});
}catch(error){if(live(n)&¤t===searchSequence)status.textContent=error.message;}
};
}catch(error){if(live(n)){box.innerHTML='
Eigener Projektor konnte nicht geprüft werden. Externe Suche bleibt bis zur Prüfung geschlossen.
CPU-Threads steuern CPU-Arbeit und Offloading, nicht die Anzahl der CUDA-Rechenkerne.
';advanced.append(threads);el('profile-form').insertBefore(advanced,el('profile-form').querySelector('.note'));}
if(kind==='chat'){bindProjectorDiscovery();const f=el('profile-form');const reserveFields=()=>{for(const n of ['reserve_first','reserve_second'])f.elements[n].disabled=f.elements.gpu_reserve_mode.value!=='manual';};f.elements.gpu_reserve_mode.onchange=reserveFields;reserveFields();}
el('profile-close').onclick=()=>el('profile-editor').replaceChildren();
el('profile-form').onsubmit=async event=>{event.preventDefault();const form=event.target,values=new FormData(form),button=form.querySelector('button:not([type=button])');button.disabled=true;
try{const parameters=Object.fromEntries(Object.keys(schema).map(k=>[k,Number(values.get(k))]));if(kind==='chat'){parameters.vision_projector=values.get('vision_projector')||null;parameters.vision_device=values.get('vision_device');parameters.mtp=values.has('mtp');parameters.gpu_offload=values.get('gpu_offload');if(values.get('gpu_second')&&!values.get('gpu_first'))throw Error('Bitte zuerst die erste GPU auswählen.');if((data.parameters.gpu_devices||[]).length>2)throw Error('Dieses Profil enthält mehr als zwei GPUs; der Editor unterstützt derzeit zwei.');parameters.gpu_devices=[values.get('gpu_first'),values.get('gpu_second')].filter(Boolean);parameters.gpu_reserve_mode=values.get('gpu_reserve_mode');parameters.gpu_reserve_mib=parameters.gpu_reserve_mode==='manual'?Object.fromEntries([['gpu_first','reserve_first'],['gpu_second','reserve_second']].filter(([g])=>values.get(g)).map(([g,r])=>[values.get(g),Number(values.get(r))])):{};parameters.split_mode=values.get('split_mode');const raw=String(values.get('tensor_split')).trim();parameters.tensor_split=raw?raw.split(',').map(x=>x.trim()?Number(x):NaN):[];}await api('profiles/save',{id:data.id,revision:data.revision,name:values.get('name'),kind,model_id:values.get('model_id'),parameters});if(!root.isConnected)return;el('profile-editor').replaceChildren();await load();message('Profil auf Athena gespeichert. Kein Modell gestartet.');}
catch(error){if(root.isConnected)el('profile-error').textContent=error.message;}finally{button.disabled=false;}
};
el('profile-editor').scrollIntoView({behavior:'smooth',block:'start'});
}
el('profile-new').disabled=true;el('profile-new').onclick=()=>editor();
load().then(()=>{if(modelId&&root.isConnected)editor(null,modelId);});
}
return {html,bind};
})();