Add visual model catalog, editable profile drafts and llama.cpp build settings
This commit is contained in:
@@ -0,0 +1,58 @@
|
||||
/* Visual prototype only. Drafts stay in this browser; no runtime/download API calls. */
|
||||
const Studio=(()=>{
|
||||
const key='athena-deck-studio-v1';
|
||||
const seed={profiles:[{id:'alltag',name:'qwen-alltag',model:'qwen-iq4',context:76800,slots:1,gpu:'both',split:'85,15',cache:'q8_0',batch:2048,ubatch:128,threads:8,unified:true,flash:true,vision:false},{id:'parallel',name:'qwen-parallel',model:'qwen-iq4',context:160000,slots:2,gpu:'both',split:'85,15',cache:'q8_0',batch:2048,ubatch:128,threads:8,unified:true,flash:true,vision:false},{id:'lang',name:'qwen-langkontext',model:'qwen-iq4',context:262144,slots:1,gpu:'both',split:'80,20',cache:'q8_0',batch:2048,ubatch:128,threads:8,unified:true,flash:true,vision:false}],runtime:{backend:'CUDA',revision:'',architectures:'auto',custom:'',jobs:4,toolkit:'auto',reserve:2048}};
|
||||
let state=structuredClone(seed);try{const saved=JSON.parse(localStorage.getItem(key));if(saved?.profiles&&Array.isArray(saved.profiles)&&saved.runtime)state=saved;}catch{}
|
||||
let section='discover',category='',editing=null;
|
||||
const models=[{id:'qwen-iq4',kind:'chat',name:'Qwen 27B',variant:'IQ4_XS · GGUF',source:'Hugging Face',description:'Sprachmodell · Beispiel für mehrere Profile mit einer Modelldatei.'},{id:'qwen-q4',kind:'chat',name:'Qwen 27B',variant:'Q4_K_M · GGUF',source:'Hugging Face',description:'Alternative Quantisierung · eigener Bibliothekseintrag.'},{id:'image-example',kind:'image',name:'Bildmodell',variant:'Modellpaket · Beispiel',source:'Modellkatalog',description:'Textencoder, Diffusionsmodell und VAE als zusammengehöriges Paket.'},{id:'tts-example',kind:'audio',name:'Sprachausgabe',variant:'TTS · Beispiel',source:'Modellkatalog',description:'Sprachmodell und Stimmen mit passender Audio-Laufzeit.'},{id:'asr-example',kind:'audio',name:'Spracherkennung',variant:'ASR · Beispiel',source:'Modellkatalog',description:'Audio in Text umwandeln · CPU-/GPU-Eignung später prüfen.'},{id:'video-example',kind:'video',name:'Videomodell',variant:'Modellpaket · Beispiel',source:'Modellkatalog',description:'Auflösung, Bildanzahl und Zusatzkomponenten gemeinsam planen.'}];
|
||||
const labels={chat:'Sprachmodelle / Chat',image:'Bildgenerierung',audio:'Audio',video:'Video'};
|
||||
const e=v=>String(v??'').replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
|
||||
const count=n=>Number(n).toLocaleString('de-DE');
|
||||
const selected=(a,b)=>a===b?'selected':'';
|
||||
const check=v=>v?'checked':'';
|
||||
function save(){try{localStorage.setItem(key,JSON.stringify(state));return true;}catch{notice('Speichern im Browser nicht möglich. Entwurf bleibt nur für diese Ansicht erhalten.');return false;}}
|
||||
function notice(message){const node=document.querySelector('#studio-notice');if(node)node.textContent=message;}
|
||||
const banner=()=>'<div class="prototype-banner"><span class="pill">GUI-VORSCHAU</span><span>Beispiele und lokale Entwürfe. Keine Downloads, Builds oder Modellstarts.</span></div>';
|
||||
function header(title,description){return `<div class="kicker">ATHENA / MODELLVERWALTUNG</div><h1>${title}</h1><p>${description}</p>${banner()}`;}
|
||||
function render(page,force=false){
|
||||
if(!force&&document.querySelector('#studio')?.dataset.page===page)return;
|
||||
if(category!==page){category=page;section='discover';editing=null;}
|
||||
const view=document.querySelector('#view');
|
||||
view.innerHTML=`<div id="studio" data-page="${e(page)}">${page==='runtime'?runtime():workspace(page)}<p id="studio-notice" role="status" aria-live="polite"></p></div>`;
|
||||
bind(page);
|
||||
}
|
||||
function workspace(page){
|
||||
return header(labels[page], 'Modelle entdecken, als Bibliothek organisieren und über Profile bereitstellen.')+`<div class="studio-tabs" role="tablist" aria-label="Modellverwaltung">${[['discover','Entdecken'],['library','Bibliothek'],['profiles','Profile'],['running','Laufend']].map(([id,label])=>`<button role="tab" aria-selected="${section===id}" class="${section===id?'chosen':'secondary'}" data-section="${id}">${label}${id==='profiles'&&page==='chat'?` <span>${state.profiles.length}</span>`:''}</button>`).join('')}</div><div role="tabpanel">${section==='discover'?discover(page):section==='library'?library(page):section==='profiles'?profiles(page):running(page)}</div>`;
|
||||
}
|
||||
function modelCard(m,library=false){return `<article class="card model-card"><div class="card-top"><span class="model-icon">${m.kind==='chat'?'Aa':m.kind==='image'?'▧':m.kind==='audio'?'≈':'▷'}</span><span class="pill">${library?'Bibliotheksbeispiel':'Katalogbeispiel'}</span></div><h2>${e(m.name)}</h2><div class="tag">${e(m.variant)}</div><p>${e(m.description)}</p><dl class="model-facts"><div><dt>Quelle</dt><dd>${e(m.source)} · nicht verbunden</dd></div><div><dt>Downloadgröße</dt><dd>Noch nicht ermittelt</dd></div><div><dt>Eignung für Athena</dt><dd><span class="unknown-dot"></span> Noch nicht geprüft</dd></div></dl><div class="card-actions"><button class="secondary" data-detail="${m.id}">Details & Eignung</button>${library&&m.kind==='chat'?`<button data-create="${m.id}">Profil entwerfen</button>`:'<button disabled>Herunterladen</button>'}</div></article>`;}
|
||||
function discover(page){return `<div class="studio-toolbar"><div><label for="model-search">Modell suchen</label><input id="model-search" type="search" placeholder="Beispiele nach Name oder Variante filtern …"></div><div><label for="source-filter">Quelle</label><select id="source-filter"><option value="all">Alle Quellen</option><option>Hugging Face</option><option>Modellkatalog</option></select></div></div><p class="small muted">Die Suche filtert nur diese Beispieldaten. Eine Live-Verbindung zu Hugging Face folgt später.</p><div class="model-grid" id="model-results">${models.filter(m=>m.kind===page).map(m=>modelCard(m)).join('')}</div><p id="no-models" hidden>Keine passenden Beispiele gefunden.</p><section id="model-detail"></section>`;}
|
||||
function library(page){return `<div class="section-heading"><div><h2>Deine Modellbibliothek</h2><p>Eine Modelldatei kann von mehreren Profilen verwendet werden.</p></div><span class="pill">0 verifizierte Installationen</span></div><div class="model-grid">${models.filter(m=>m.kind===page).slice(0,1).map(m=>modelCard(m,true)).join('')}</div><p class="note">Die Einträge zeigen die geplante Darstellung. Es wurden weder lokale Modelldateien importiert noch Downloads ausgeführt.</p><section id="model-detail"></section>`;}
|
||||
function profiles(page){
|
||||
if(page!=='chat')return `<section class="card"><span class="pill">VORBEREITET</span><h2>${page==='image'?'Auflösung und Generierung':page==='audio'?'Sprache, Stimmen und Verarbeitung':'Auflösung und Videolänge'}</h2><div class="form-grid">${page==='image'?'<label>Breite / Höhe<input disabled value="1024 × 1024 · Beispiel"></label><label>Schritte<input disabled value="30 · Beispiel"></label>':page==='audio'?'<label>Aufgabe<select disabled><option>Sprachausgabe / Spracherkennung</option></select></label><label>Sprache<select disabled><option>Automatisch</option></select></label>':'<label>Auflösung<input disabled value="1280 × 720 · Beispiel"></label><label>Bildanzahl<input disabled value="Noch nicht festgelegt"></label>'}</div><p>Eigene Worker-Adapter und Eignungsregeln folgen. LLM-Kontextparameter werden hier nicht übernommen.</p><button disabled>Profil speichern</button></section>`;
|
||||
return `<div class="section-heading"><div><h2>Ein Modell. Mehrere Möglichkeiten.</h2><p>API-Namen und Startparameter als lokale Profilentwürfe.</p></div><button id="new-profile">+ Neues Profil</button></div><div class="profile-list">${state.profiles.map(p=>`<article class="card profile-row"><div><span class="label">PROFILENTWURF</span><h2>${e(p.name)}</h2><p>${e(models.find(m=>m.id===p.model)?.variant||'Modell nicht zugeordnet')}</p></div><div><span class="label">Kontextbudget</span><strong>${count(p.context)}</strong></div><div><span class="label">Slots</span><strong>${e(p.slots)}</strong></div><div><span class="label">Zustand</span><strong class="muted">Nicht bereitgestellt</strong></div><div class="profile-actions"><button class="secondary" data-edit="${e(p.id)}">Bearbeiten</button><button class="secondary" data-duplicate="${e(p.id)}">Duplizieren</button></div></article>`).join('')||'<section class="empty">Noch keine Profilentwürfe. Lege dein erstes Profil an.</section>'}</div><section class="card endpoint-preview"><span class="label">GEPLANTE API · NICHT AKTIV</span><h2>Ein Endpunkt für deine Profile</h2><code>GET /v1/models</code><pre>${e(JSON.stringify({data:state.profiles.map(p=>({id:p.name,object:'model'}))},null,2))}</pre><p>Später wählt der Client den Profilnamen im Feld <code>model</code>. Deck verwaltet den passenden llama-server-Prozess.</p></section><div id="profile-editor"></div>`;
|
||||
}
|
||||
function running(page){return `<section class="card runtime-empty"><span class="model-icon">○</span><h2>Keine von Deck verwaltete Modelllaufzeit</h2><p>${page==='chat'?'llama.cpp ist in dieser Vorschau nicht angebunden. Gespeicherte Profile starten keinen Prozess.':'Für diesen Bereich ist noch kein Worker angebunden.'}</p><button disabled>Profil starten</button><a class="link" href="#runtime">Laufzeit-Einstellungen ansehen →</a></section><section class="card"><h2>So soll der Profilwechsel ablaufen</h2><ol class="flow"><li><b>Anfrage zuordnen</b><span>Profil anhand des API-Namens auswählen.</span></li><li><b>Arbeit abschließen</b><span>Laufende Anfragen beenden lassen; neue einreihen.</span></li><li><b>Prozess wechseln</b><span>llama-server beenden und mit dem Zielprofil starten.</span></li><li><b>Bereitschaft prüfen</b><span>Erst danach wartende Anfragen weiterleiten.</span></li></ol><p class="note">Mehrere angebotene Profile sind nicht automatisch mehrere geladene Modelle. Produktive Athena-Prozesse werden in dieser Vorschau nicht gesteuert.</p></section>`;}
|
||||
function detail(id){const m=models.find(m=>m.id===id);document.querySelector('#model-detail').innerHTML=`<article class="card detail-card"><div class="card-top"><h2>${e(m.name)} · Eignungsprüfung</h2><span class="pill">NOCH NICHT BERECHNET</span></div><div class="fit-grid"><div><b>Unterstützt?</b><p>Format und Laufzeit-Kompatibilität noch nicht geprüft.</p></div><div><b>Passt grundsätzlich?</b><p>Gewichte, Zusatzkomponenten und Arbeitsspeicherbedarf noch nicht ermittelt.</p></div><div><b>Kann jetzt starten?</b><p>Keine Ressourcenreservierung oder Prozesssteuerung angebunden.</p></div></div><p class="note">${m.kind==='chat'?'Kontextlänge, KV-Cache, Slots und GPU-Aufteilung beeinflussen den Speicherbedarf. Die beiden GPUs werden nicht pauschal als ein gemeinsamer VRAM-Pool gerechnet.':'Die spätere Einschätzung berücksichtigt die zur Aufgabe passenden Parameter und den jeweiligen Worker.'} Es gibt in dieser Vorschau keine erfundenen Speicherwerte.</p></article>`;document.querySelector('#model-detail').scrollIntoView({behavior:'smooth',block:'nearest'});}
|
||||
function field(label,name,value,type='number',attrs=''){return `<label>${label}<input name="${name}" type="${type}" value="${e(value)}" ${attrs}></label>`;}
|
||||
function editor(id,model){editing=id||null;const p=id?state.profiles.find(p=>p.id===id):{...seed.profiles[0],name:'qwen-neues-profil',model:model||'qwen-iq4'};if(!p)return;
|
||||
document.querySelector('#profile-editor').innerHTML=`<section class="card editor"><div class="card-top"><h2>${id?'Profil bearbeiten':'Neues Profil'}</h2><span class="pill">NUR ENTWURF</span></div><form id="profile-form"><div class="form-grid">${field('API-Profilname','name',p.name,'text','required pattern="[a-zA-Z0-9][a-zA-Z0-9_-]{1,63}" maxlength="64"')}<label>Modelldatei / Quantisierung<select name="model">${models.filter(m=>m.kind==='chat').map(m=>`<option value="${m.id}" ${selected(m.id,p.model)}>${e(m.name)} · ${e(m.variant)} (Beispiel)</option>`).join('')}</select></label>${field('Gesamtes Kontextbudget (Token)','context',p.context,'number','required min="512" max="2097152" step="1"')}${field('Parallele Slots','slots',p.slots,'number','required min="1" max="16" step="1"')}<label>GPU-Zuordnung<select name="gpu"><option value="both" ${selected(p.gpu,'both')}>RTX 5080 + RTX 3060</option><option value="5080" ${selected(p.gpu,'5080')}>Nur RTX 5080</option><option value="3060" ${selected(p.gpu,'3060')}>Nur RTX 3060</option><option value="cpu" ${selected(p.gpu,'cpu')}>CPU</option></select></label><label>KV-Cache-Format<select name="cache">${['f16','q8_0','q4_0'].map(v=>`<option ${selected(v,p.cache)}>${v}</option>`).join('')}</select></label></div><label class="checkbox"><input type="checkbox" name="unified" ${check(p.unified)}> Gemeinsamen KV-Pool vorsehen</label><p class="small">Das Kontextbudget gilt für das Profil, nicht automatisch je Slot. Die genaue Slot-Zuteilung und Unterstützung werden später mit der gewählten llama.cpp-Version geprüft.</p><details><summary>Erweiterte Startoptionen</summary><div class="form-grid">${field('Gewichtung 5080 : 3060','split',p.split,'text','required pattern="[0-9]+(\\.[0-9]+)?,[0-9]+(\\.[0-9]+)?"')}${field('Batch-Größe','batch',p.batch,'number','required min="1" max="8192"')}${field('Microbatch-Größe','ubatch',p.ubatch,'number','required min="1" max="8192"')}${field('CPU-Threads','threads',p.threads,'number','required min="1" max="256"')}</div><label class="checkbox"><input type="checkbox" name="flash" ${check(p.flash)}> Flash Attention vorsehen</label><label class="checkbox"><input type="checkbox" name="vision" ${check(p.vision)}> Vision-Projektor vorsehen (noch nicht zugeordnet)</label></details><div class="card-actions"><button>Entwurf speichern</button><button type="button" class="secondary" id="cancel-edit">Schließen</button>${id?'<button type="button" class="secondary danger" id="delete-profile">Entwurf entfernen</button>':''}</div><p id="profile-error" role="alert"></p></form></section>`;
|
||||
document.querySelector('#profile-form').onsubmit=event=>{event.preventDefault();const data=new FormData(event.target),value=Object.fromEntries(data);for(const n of ['context','slots','batch','ubatch','threads'])value[n]=Number(value[n]);for(const n of ['unified','flash','vision'])value[n]=data.has(n);const out=document.querySelector('#profile-error');if(state.profiles.some(q=>q.id!==editing&&q.name===value.name)){out.textContent='Dieser API-Profilname ist bereits vergeben.';return;}if(value.ubatch>value.batch){out.textContent='Microbatch darf nicht größer als Batch sein.';return;}if(value.gpu==='both'&&!value.split.split(',').some(v=>Number(v)>0)){out.textContent='Mindestens eine GPU-Gewichtung muss größer als null sein.';return;}value.id=editing||crypto.randomUUID();const idx=state.profiles.findIndex(q=>q.id===editing);if(idx<0)state.profiles.push(value);else state.profiles[idx]=value;const stored=save();render(category,true);notice(stored?'Profilentwurf in diesem Browser gespeichert. Kein Modell gestartet.':'Entwurf nur im Speicher gehalten; Browser-Speicher nicht verfügbar.');};
|
||||
document.querySelector('#cancel-edit').onclick=()=>document.querySelector('#profile-editor').replaceChildren();
|
||||
if(id)document.querySelector('#delete-profile').onclick=()=>{state.profiles=state.profiles.filter(q=>q.id!==id);save();render(category,true);notice('Lokalen Profilentwurf entfernt. Modelldateien wurden nicht verändert.');};
|
||||
document.querySelector('#profile-editor').scrollIntoView({behavior:'smooth',block:'start'});
|
||||
}
|
||||
function runtime(){const r=state.runtime;return header('llama.cpp-Laufzeit','Installation, GPU-Unterstützung und Versionen an einem Ort vorbereiten.')+`<div class="runtime-stats"><section class="card"><span class="label">INSTALLIERTE VERSION</span><h2>Nicht angebunden</h2><p>Kein Build erkannt oder ausgeführt.</p></section><section class="card"><span class="label">UPDATE-STATUS</span><h2>Noch nicht geprüft</h2><p>Keine Live-Abfrage von Releases.</p></section><section class="card"><span class="label">ZIELSYSTEM</span><h2>Athena</h2><p>Geplant: RTX 5080 + RTX 3060</p></section></div><section class="card"><div class="card-top"><h2>Installation & Build</h2><span class="pill">LOKALER ENTWURF</span></div><form id="build-form"><div class="form-grid"><label>Rechen-Backend<select name="backend">${['CUDA','CPU','Vulkan'].map(v=>`<option ${selected(v,r.backend)}>${v}</option>`).join('')}</select></label>${field('Gewünschtes Release / Commit','revision',r.revision,'text','placeholder="Noch keine Version ausgewählt" maxlength="80"')}<label>GPU-Architekturen<select name="architectures"><option value="auto" ${selected('auto',r.architectures)}>Auf dem Zielserver automatisch erkennen</option><option value="custom" ${selected('custom',r.architectures)}>Explizite Build-Ziele vorgeben</option></select></label>${field('Explizite Architekturziele (optional)','custom',r.custom,'text','placeholder="Später anhand beider GPUs prüfen" maxlength="80"')}<label>CUDA-Toolkit<select name="toolkit"><option value="auto">Verfügbarkeit und Kompatibilität prüfen</option></select></label>${field('Parallele Build-Jobs','jobs',r.jobs,'number','required min="1" max="64"')}${field('Geplante VRAM-Reserve je GPU (MiB)','reserve',r.reserve,'number','required min="256" max="16384" step="256"')}</div><p class="note">Bei verschiedenen GPUs muss der CUDA-Build die Architektur beider Karten unterstützen. Die konkreten Build-Ziele und die passende Toolkit-Version werden später auf Athena geprüft. Diese Vorschau installiert keine Treiber oder Host-Pakete.</p><button>Build-Entwurf speichern</button><button disabled>llama.cpp herunterladen & bauen</button></form></section><section class="card"><div class="card-top"><h2>Updates & Änderungen</h2><button id="release-preview" class="secondary">Update-Ansicht öffnen</button></div><p>Vorgesehen: installierte Version mit verfügbaren Releases vergleichen, Fixes ansehen und einen neuen Build getrennt vorbereiten.</p><div id="release-panel" hidden><div class="release-comparison"><div><span class="label">AKTUELL</span><h3>Nicht ermittelt</h3></div><span>→</span><div><span class="label">VERFÜGBAR</span><h3>Nicht abgefragt</h3></div></div><h3>Release-Notes & Fixes</h3><p>Hier erscheinen später die tatsächlichen Änderungen mit Quelle und Veröffentlichungsdatum. Keine Beispieldaten werden als neue Version oder behobener Fehler ausgegeben.</p><a class="link" href="https://github.com/ggml-org/llama.cpp/releases" target="_blank" rel="noopener noreferrer">Offizielle llama.cpp-Releases öffnen ↗</a><div class="card-actions"><button disabled>Nach Updates suchen</button><button disabled>Neue Version bauen</button></div></div></section><section class="card"><h2>Geplanter Build-Ablauf</h2><ol class="flow"><li><b>Voraussetzungen prüfen</b><span>Compiler, CUDA, beide GPUs und freier Speicher.</span></li><li><b>Version beziehen</b><span>Ausgewähltes Release oder festen Commit herunterladen.</span></li><li><b>Separat bauen & prüfen</b><span>Neue Binärdatei prüfen; laufende Version beibehalten.</span></li><li><b>Gezielt aktivieren</b><span>Nach laufenden Anfragen wechseln; vorigen Build behalten.</span></li></ol><button disabled>Vorherigen Build wiederherstellen</button><p class="note">Kein Build aktiv · keine Warteschlange · keine Wiederherstellungsversion vorhanden.</p></section>`;}
|
||||
function bind(page){
|
||||
document.querySelectorAll('[data-section]').forEach(b=>b.onclick=()=>{section=b.dataset.section;render(page,true);});
|
||||
document.querySelectorAll('[data-detail]').forEach(b=>b.onclick=()=>detail(b.dataset.detail));
|
||||
document.querySelectorAll('[data-create]').forEach(b=>b.onclick=()=>{const model=b.dataset.create;section='profiles';render(page,true);editor(null,model);});
|
||||
document.querySelectorAll('[data-edit]').forEach(b=>b.onclick=()=>editor(b.dataset.edit));
|
||||
document.querySelectorAll('[data-duplicate]').forEach(b=>b.onclick=()=>{const old=state.profiles.find(p=>p.id===b.dataset.duplicate);let n=2,name=old.name.slice(0,55)+'-kopie';while(state.profiles.some(p=>p.name===name))name=old.name.slice(0,50)+'-kopie-'+n++;state.profiles.push({...old,id:crypto.randomUUID(),name});save();render(page,true);notice('Profil lokal dupliziert. Kein zusätzlicher Prozess gestartet.');});
|
||||
document.querySelector('#new-profile')?.addEventListener('click',()=>editor());
|
||||
const filter=()=>{const q=document.querySelector('#model-search').value.toLowerCase(),source=document.querySelector('#source-filter').value;const filtered=models.filter(m=>m.kind===page&&(source==='all'||source===m.source)&&(m.name+' '+m.variant).toLowerCase().includes(q));document.querySelector('#model-results').innerHTML=filtered.map(m=>modelCard(m)).join('');document.querySelector('#no-models').hidden=!!filtered.length;document.querySelectorAll('[data-detail]').forEach(b=>b.onclick=()=>detail(b.dataset.detail));};
|
||||
document.querySelector('#model-search')?.addEventListener('input',filter);document.querySelector('#source-filter')?.addEventListener('change',filter);
|
||||
document.querySelector('#release-preview')?.addEventListener('click',()=>{const p=document.querySelector('#release-panel');p.hidden=!p.hidden;});
|
||||
document.querySelector('#build-form')?.addEventListener('submit',event=>{event.preventDefault();const value=Object.fromEntries(new FormData(event.target));value.jobs=Number(value.jobs);value.reserve=Number(value.reserve);if(value.architectures==='custom'&&!value.custom.trim()){notice('Bitte explizite Architekturziele eintragen oder automatische Erkennung wählen.');return;}state.runtime=value;notice(save()?'Build-Entwurf im Browser gespeichert. Kein Download oder Build ausgeführt.':'Entwurf nur für diese Sitzung gespeichert.');});
|
||||
}
|
||||
return {render};
|
||||
})();
|
||||
Reference in New Issue
Block a user