window.ChatTestUI=(()=>{
const e=v=>String(v??'').replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
const gib=v=>v==null?'unbekannt':(v/1024).toFixed(2)+' GiB';
async function api(path,data){const r=await fetch('/api/v1/'+path,data===undefined?{}:{method:'POST',headers:{'Content-Type':'application/json','X-Athena-Deck':'1'},body:JSON.stringify(data)});const v=await r.json();if(!r.ok)throw Error(v.error||'Anfrage fehlgeschlagen');return v;}
function put(el,html){if(el._chatHtml!==html){el.innerHTML=html;el._chatHtml=html;}}
function html(){return ` Ein einfacher Textchat mit deinem gespeicherten Profil. Nutzt denselben Worker wie API-Anfragen; eine öffentliche Profilfreigabe ist nicht erforderlich. Entladen ist nur ohne laufende Anfragen möglich. Abbrechen beendet nur diesen Test; andere Antworten werden nicht gestoppt. Prognose mit Reserve, keine Garantie für jede Anfrage. GPU-Layer können automatisch auf die CPU ausgelagert werden. Bei Fehlern helfen häufig kleinerer Kontext oder Microbatch. Hardware-Messwerte →Sprachmodell testen
Laufzeit & Diagnose
Speicherprüfung
| Phase | Token | Dauer | Token/s |
|---|---|---|---|
| Eingabe / Prefill (berechnet) | ${num(t.prompt_n)} | ${sec(t.prompt_ms)} | ${num(t.prompt_per_second,2)} |
| Ausgabe / Generierung | ${num(t.predicted_n)} | ${sec(t.predicted_ms)} | ${num(t.predicted_per_second,2)} |
Eingabe gesamt: ${num(u.prompt_tokens)} Token · Ausgabe gesamt: ${num(u.completion_tokens)} Token
Messwerte von llama.cpp nach Abschluss der Antwort. Prefill zählt neu berechnete Eingabe-Token; wiederverwendeter Kontext kann die Arbeit reduzieren. Ausgabe umfasst auch Reasoning. Modellladen und Wartezeit sind nicht Teil der Token/s. — = noch nicht verfügbar, etwa während der Antwort oder nach Abbruch.
`; } function bind(){ const root=document.querySelector('#chat-test'),el=id=>root.querySelector('#'+id);let profiles=[],messages=[],turns=[],ownJob=null,finished=null,status=null,pending=false; const notice=t=>{if(root.isConnected)el('chat-message').textContent=t;}; function transcript(){put(el('chat-transcript'),turns.map(t=>`${e(t.content)}
${t.reasoning?`${e(t.reasoning)}${e(j?.phase||'Noch kein Chat-Test gestartet.')}
Worker: ${e(w.phase||w.state)} · ${e(w.profile_name||'kein Modell geladen')}
${s.scheduler.active_requests} laufende / ${s.scheduler.waiting_requests} wartende Anfragen
${e(j.error)}
`:''}${w.error?`${e(w.error)}
`:''}${metrics(j)}${j?`Dauer: ${Math.max(0,Math.round((j.finished_at||Date.now()/1000)-j.started_at))} s
`:''}`); put(el('chat-memory'),(plan?`Letzte Prognose für ${e(plan.profile_name||w.profile_name||'das Modell')}
${plan.fits?'Prognose passt einschließlich Reserve':'Prognose passt nicht in den verfügbaren Speicher'} · ${plan.context.toLocaleString('de-DE')} Token / ${plan.slots} Slots · GPU-Layerlimit ${plan.gpu_layers===999?'alle':plan.gpu_layers}
${plan.gpus.map(g=>`${e(g.name)}: geschätzt ${gib(g.required_mib)} + ${gib(g.reserve_mib)} Reserve / ${gib(g.free_mib)} beim Prüfen frei
`).join('')}RAM-Budget: ${gib(plan.host_required_mib)} benötigt / ${gib(plan.host_available_mib)} verfügbar
`:'Noch keine Speicherprognose für einen Modellstart vorhanden.
')+`${e(g.name)}: ${gib(g.used_mib)} / ${gib(g.total_mib)} belegt · ${g.percent??'—'} % Auslastung
`).join('')||'GPU-Messwerte nicht verfügbar.
'}`); if(j&&j.id===ownJob){let response=turns[turns.length-1];if(response?.role!=='assistant'){response={role:'assistant',content:'',reasoning:''};turns.push(response);}response.content=j.answer;response.reasoning=j.reasoning;transcript(); if(j.state!=='running'&&finished!==j.id){finished=j.id;if(j.state==='complete'&&j.answer)messages.push({role:'assistant',content:j.answer});else{messages=[];notice('Der nächste Prompt beginnt einen neuen Kontext; der letzte Test wurde nicht vollständig beantwortet.');}if(j.finish_reason==='length')notice('Antwort am Tokenlimit beendet. Bei Bedarf das Antwortlimit erhöhen.');} }selection(); }catch(error){notice(error.message);}finally{if(root.isConnected)setTimeout(refresh,1500);}} api('profiles').then(s=>{if(!root.isConnected)return;profiles=s.profiles.filter(p=>p.kind==='chat');el('chat-profile').innerHTML=profiles.map(p=>``).join('')||'';selection();}).catch(err=>notice(err.message)); const clear=()=>{messages=[];turns=[];ownJob=null;finished=null;transcript();notice('Neuer Chat.');};el('chat-clear').onclick=clear;el('chat-profile').onchange=()=>{clear();selection();}; el('chat-test-form').onsubmit=async event=>{event.preventDefault();pending=true;selection();const text=el('chat-prompt').value.trim();try{const next=[...messages,{role:'user',content:text}];const job=await api('chat-tests/start',{profile_id:el('chat-profile').value,messages:next,max_tokens:Number(el('chat-limit').value)});messages=next;turns.push({role:'user',content:text});ownJob=job.id;finished=null;status={...status,job};el('chat-prompt').value='';transcript();notice('Test gestartet. Speicherprüfung, Modellstart und Antwort erscheinen unter Diagnose.');}catch(error){notice(error.message);}finally{pending=false;selection();}}; el('chat-cancel').onclick=async()=>{try{await api('chat-tests/cancel',{});notice('Abbruch angefordert.');}catch(error){notice(error.message);}}; el('chat-unload').onclick=async()=>{try{await api('chat-tests/unload',{});notice('Eigener Modellprozess entladen.');}catch(error){notice(error.message);}}; refresh(); } return {html,bind}; })();