Add visual model catalog, editable profile drafts and llama.cpp build settings

This commit is contained in:
Mikei386
2026-09-28 15:24:55 +02:00
parent 9f3a13e45f
commit e1a54e3485
8 changed files with 141 additions and 8 deletions
+10 -4
View File
@@ -1,9 +1,15 @@
# Athena Deck · Prototyp 0.3 # Athena Deck · Prototyp 0.4
Eigenständige neue Oberfläche, Python-Standardbibliothek und HTML/CSS/JavaScript. Eigenständige neue Oberfläche, Python-Standardbibliothek und HTML/CSS/JavaScript.
Keine Installation von Python-Paketen oder Frontend-Builds nötig. Python >= 3.10. Keine Installation von Python-Paketen oder Frontend-Builds nötig. Python >= 3.10.
## Neu: Zugang und API-Token ## Neu: Modellverwaltung und llama.cpp-Einstellungen
Katalog, Bibliothek, Profilentwürfe und eine Laufzeit-/Update-Ansicht sind als
bedienbarer GUI-Prototyp vorhanden. Entwürfe bleiben im Browser; Modellstarts,
Downloads und Builds sind deaktiviert. Details: [GUI-Prototyp](STUDIO.md).
## Zugang und API-Token
Beim ersten Öffnen werden Oberflächenkennwort und separater API-Token eingerichtet. Beim ersten Öffnen werden Oberflächenkennwort und separater API-Token eingerichtet.
Beide sind unter **Zugang & API** änderbar. Die lokale Vorschau ist jetzt ebenfalls Beide sind unter **Zugang & API** änderbar. Die lokale Vorschau ist jetzt ebenfalls
@@ -83,8 +89,8 @@ Keine Historie und kein Hintergrund-Collector bei geschlossener Hardware-Seite.
- `index.html`, `app.js`, `style.css`: neue responsive Oberfläche. - `index.html`, `app.js`, `style.css`: neue responsive Oberfläche.
- `test_server.py`: Prozess-Lebenszyklus, Kontrollgrenzen, Hardware-Ausfall. - `test_server.py`: Prozess-Lebenszyklus, Kontrollgrenzen, Hardware-Ausfall.
Sprachmodelle/Chat, Bildgenerierung, Audio und Weitere Dienste sind ausdrücklich Sprachmodelle/Chat, Bildgenerierung, Audio und Video besitzen jetzt eine
Platzhalter. Keine Downloads, Installation, Presets, Chats oder Modellwechsel. GUI-Vorschau; Weitere Dienste bleibt Platzhalter. Keine Downloads, Installation, Presets, Chats oder Modellwechsel.
Spätere Modellprofile und native llama.cpp-Worker sollten eigene Service-Adapter Spätere Modellprofile und native llama.cpp-Worker sollten eigene Service-Adapter
mit derselben Status-/Start-/Stopp-Trennung erhalten. Noch kein Worker-Registry, mit derselben Status-/Start-/Stopp-Trennung erhalten. Noch kein Worker-Registry,
Scheduler oder produktionsreifer Worker-Supervisor. Die optionale Server-Instanz Scheduler oder produktionsreifer Worker-Supervisor. Die optionale Server-Instanz
+64
View File
@@ -0,0 +1,64 @@
# Modellverwaltung und llama.cpp · GUI-Prototyp 0.4
Diese Erweiterung ist ausdrücklich rein optisch und lokal bedienbar. Keine
Modelldateien, Laufzeiten, Downloads, Builds oder produktiven Dienste werden verändert.
## Ansichten
- Sprachmodelle/Chat, Bild, Audio und Video: Entdecken, Bibliothek, Profile, Laufend.
- Katalog: Suche/Quellenfilter über gekennzeichnete Beispiele, Detailansicht mit
getrennter Prüfung von Unterstützung, grundsätzlicher Eignung und aktueller
Startmöglichkeit. Keine Online-Suche und keine berechneten Speicherwerte.
- Bibliothek: Beispiel einer einmaligen Modelldatei mit mehreren Profilen;
ausdrücklich keine bestätigte Installation.
- LLM-Profile: drei vorbelegte Beispiele, Anlegen, Bearbeiten, Duplizieren und
Entfernen lokaler Entwürfe. Name, Modellvariante, Kontextbudget, Slots,
GPU-Zuordnung, KV-Cache, gemeinsamer Pool, GPU-Gewichtung, Batch/Microbatch,
CPU-Threads, Flash Attention und vorgesehener Vision-Projektor.
- Einzigartige API-Profilnamen, gültige Zahlenbereiche, Microbatch <= Batch und
nicht ausschließlich nullwertige GPU-Gewichtung werden im Formular geprüft.
- API-Vorschau zeigt die vorgesehenen Profilnamen. `/v1/models` wird dadurch
**nicht** als funktionsfähiger Endpunkt implementiert.
- Laufend: leerer Zustand sowie geplanter Ablauf des Profilwechsels, ohne
simulierte laufende Modelle oder Warteschlangen.
- Bild/Audio/Video-Profile: vorbereitete, deaktivierte aufgabenspezifische Felder.
## Einstellungen → llama.cpp & Updates
Build-Entwurf mit Backend (CUDA/CPU/Vulkan), gewünschtem Release oder Commit,
automatischer bzw. expliziter GPU-Architekturauswahl, Toolkit-Prüfung, parallelen
Build-Jobs und vorgesehener VRAM-Reserve. Für Athena sind RTX 5080 und RTX 3060 als
bekanntes Zielsystem benannt; eine Live-Build-Kompatibilitätsprüfung erfolgt nicht.
Die Update-Ansicht lässt sich aufklappen. Sie zeigt Platz für aktuellen Build,
verfügbares Release, echte Release-Notes mit Quelle und Datum sowie Neubau und
spätere Wiederherstellung. Alle zustandsändernden Laufzeit-Aktionen und die
Live-Update-Suche sind deaktiviert. Keine erfundenen Versionsnummern oder Fixes.
Ein Link führt zu den offiziellen llama.cpp-Releases.
## Speicherung und spätere Anbindung
Entwürfe liegen ausschließlich im localStorage dieses Browsers und Origins unter
`athena-deck-studio-v1`. Sie bleiben nach Neuladen erhalten, enthalten keine
Zugangsdaten und werden nicht zum Server übertragen. Andere Browser oder die
Server-Instanz teilen diesen Stand nicht. Bei gesperrtem Browser-Speicher weist die
Oberfläche darauf hin, dass der Entwurf nur im aktuellen Speicher bleibt.
Die neuen Ansichten in `studio.js` rufen keine API auf. Vorhandene Anmeldung,
Hardwareanzeige, Demo-Service und Netzwerkverwaltung behalten ihre bisherigen
Funktionen. `server.py` liefert zusätzlich nur die neue JavaScript-Datei aus.
Vor echter Nutzung erforderlich: Modellkatalog-Adapter, Metadaten-/GGUF-Auswertung,
Downloadverwaltung, persistente Profil-API, GPU-/KV-Planung, Build-Manager und
Prozess-Supervisor mit Warteschlange. Gespeicherte Entwürfe dürfen nicht ungeprüft
als Shell-Befehle interpretiert werden. Die genaue Kontext-/Slot-Semantik muss an
die eingesetzte llama.cpp-Version gekoppelt und überprüft werden.
## Prüfung
JavaScript-Syntax geprüft. In einer isolierten statischen Browservorschau:
Profil bearbeitet (Kontext geändert), gespeichert und nach Reload wiedergefunden;
Build-Einstellungen und Update-Ansicht geöffnet, deaktivierte Aktionsknöpfe sowie
Layout kontrolliert. Die Testvorschau verwendet einen eigenen Origin, sodass keine
Entwürfe der eigentlichen Anwendung überschrieben wurden. Keine SSH-/Modell- oder
Build-Aktion für diese GUI-Erweiterung ausgeführt.
+1
View File
@@ -8,6 +8,7 @@ const metric=(title,value)=>`<div><small>${title}</small><strong>${value}</stron
const bar=v=>v==null?'':`<progress max="100" value="${Number(v)}" aria-label="Auslastung"></progress>`; const bar=v=>v==null?'':`<progress max="100" value="${Number(v)}" aria-label="Auslastung"></progress>`;
const placeholders={chat:['Sprachmodelle / Chat','Modellprofile, Laufzeit und Chat','Hier werden später Modellprofile und native llama.cpp-Prozesse verwaltet.'],image:['Bildgenerierung','Worker und Aufträge','Hier entsteht später die Übersicht für Bild-Worker und deren Zustand.'],audio:['Audio','Spracheingabe und Sprachausgabe','Hier werden später getrennte Worker für Erkennung und Ausgabe eingebunden.'],services:['Weitere Dienste','Erweiterbare Dienste','Hier finden später zusätzliche Worker und ihre Schnittstellen Platz.']}; const placeholders={chat:['Sprachmodelle / Chat','Modellprofile, Laufzeit und Chat','Hier werden später Modellprofile und native llama.cpp-Prozesse verwaltet.'],image:['Bildgenerierung','Worker und Aufträge','Hier entsteht später die Übersicht für Bild-Worker und deren Zustand.'],audio:['Audio','Spracheingabe und Sprachausgabe','Hier werden später getrennte Worker für Erkennung und Ausgabe eingebunden.'],services:['Weitere Dienste','Erweiterbare Dienste','Hier finden später zusätzliche Worker und ihre Schnittstellen Platz.']};
async function refresh(){if(refreshing)return;refreshing=true;const page=location.hash.slice(1)||'home';document.querySelectorAll('nav a').forEach(a=>a.classList.toggle('active',a.hash==='#'+page));try{error.textContent='';let html; async function refresh(){if(refreshing)return;refreshing=true;const page=location.hash.slice(1)||'home';document.querySelectorAll('nav a').forEach(a=>a.classList.toggle('active',a.hash==='#'+page));try{error.textContent='';let html;
if(['chat','image','audio','video','runtime'].includes(page)){Studio.render(page);return;}
if(page==='access'){await accessPage();return;} if(page==='access'){await accessPage();return;}
if(page==='network'){await networkPage();return;} if(page==='network'){await networkPage();return;}
if(page==='home'){const s=await api('status'),d=s.demo;html=heading('DEIN KONTROLLZENTRUM','Alles beginnt mit einer klaren Basis.','Eine kleine Oberfläche für Status, Hardware und den ersten steuerbaren Dienst.')+`<div class="grid"><section class="card"><div class="label">Anwendung</div><div class="value"><i></i> Bereit</div><p>Athena Deck läuft auf ${esc(s.location)}.</p><div class="note">Laufzeit ${fmt(s.uptime_seconds,' s')} · API v1</div></section><section class="card"><div class="label">Betriebsmodus</div><div class="value">Isoliert</div><p>Ein eigenständiger Prototyp mit lesendem Hardware-Zugriff auf Athena.</p><div class="note">Keine Modell- oder Router-Steuerung angebunden</div></section><section class="card wide"><div class="card-top"><h2>Demo-Dienst</h2><span class="pill">LOKAL / CPU</span></div><p>Ein minimaler HTTP-Prozess zum Testen von Start, Stopp und Erreichbarkeit.</p><div class="metrics">${metric('Prozess',d.state==='running'?'Läuft':'Gestoppt')}${metric('Health-Prüfung',d.reachable?'Erreichbar':'Nicht erreichbar')}${metric('Lokaler Port',d.port??'—')}</div><button id="start" ${busy||d.state==='running'?'disabled':''}>Dienst starten</button><button id="stop" class="secondary" ${busy||d.state!=='running'?'disabled':''}>Dienst stoppen</button><div class="note">Lädt keine Modelle · endet beim Beenden von Athena Deck</div></section></div>`; if(page==='home'){const s=await api('status'),d=s.demo;html=heading('DEIN KONTROLLZENTRUM','Alles beginnt mit einer klaren Basis.','Eine kleine Oberfläche für Status, Hardware und den ersten steuerbaren Dienst.')+`<div class="grid"><section class="card"><div class="label">Anwendung</div><div class="value"><i></i> Bereit</div><p>Athena Deck läuft auf ${esc(s.location)}.</p><div class="note">Laufzeit ${fmt(s.uptime_seconds,' s')} · API v1</div></section><section class="card"><div class="label">Betriebsmodus</div><div class="value">Isoliert</div><p>Ein eigenständiger Prototyp mit lesendem Hardware-Zugriff auf Athena.</p><div class="note">Keine Modell- oder Router-Steuerung angebunden</div></section><section class="card wide"><div class="card-top"><h2>Demo-Dienst</h2><span class="pill">LOKAL / CPU</span></div><p>Ein minimaler HTTP-Prozess zum Testen von Start, Stopp und Erreichbarkeit.</p><div class="metrics">${metric('Prozess',d.state==='running'?'Läuft':'Gestoppt')}${metric('Health-Prüfung',d.reachable?'Erreichbar':'Nicht erreichbar')}${metric('Lokaler Port',d.port??'—')}</div><button id="start" ${busy||d.state==='running'?'disabled':''}>Dienst starten</button><button id="stop" class="secondary" ${busy||d.state!=='running'?'disabled':''}>Dienst stoppen</button><div class="note">Lädt keine Modelle · endet beim Beenden von Athena Deck</div></section></div>`;
+1 -1
View File
@@ -1 +1 @@
<!doctype html><html lang="de"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Athena Deck</title><link rel="stylesheet" href="/style.css"><script src="/network-ui.js" defer></script><script src="/access-ui.js" defer></script><script src="/app.js" defer></script></head><body><aside><a class="brand" href="#home"><span>Α</span> ATHENA <b>DECK</b></a><p class="eyebrow">WORKSPACE / PROTOTYP 01</p><nav aria-label="Hauptnavigation"><a href="#home">Übersicht <span>01</span></a><a href="#chat">Sprachmodelle / Chat <span>02</span></a><a href="#image">Bildgenerierung <span>03</span></a><a href="#audio">Audio <span>04</span></a><a href="#services">Weitere Dienste <span>05</span></a><a href="#hardware">Hardware <span>06</span></a><a href="#network">Netzwerk <span>07</span></a><a href="#access">Zugang &amp; API <span>08</span></a></nav><footer><i></i> Isolierte Entwicklungsumgebung<p>Steuerung auf dem Mac<br>Hardware von Athena · nur lesend</p></footer></aside><main><header><span>ATHENA CONTROL SURFACE</span><span class="pill">v0.3 · Prototyp</span></header><div id="view"></div><p id="error" role="alert"></p></main></body></html> <!doctype html><html lang="de"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Athena Deck</title><link rel="stylesheet" href="/style.css"><script src="/network-ui.js" defer></script><script src="/access-ui.js" defer></script><script src="/studio.js" defer></script><script src="/app.js" defer></script></head><body><aside><a class="brand" href="#home"><span>Α</span> ATHENA <b>DECK</b></a><p class="eyebrow">WORKSPACE / PROTOTYP 01</p><nav aria-label="Hauptnavigation"><a href="#home">Übersicht <span>01</span></a><a href="#chat">Sprachmodelle / Chat <span>02</span></a><a href="#image">Bildgenerierung <span>03</span></a><a href="#audio">Audio <span>04</span></a><a href="#video">Video <span>05</span></a><a href="#services">Weitere Dienste <span>06</span></a><a href="#hardware">Hardware <span>07</span></a><p class="nav-heading">EINSTELLUNGEN</p><a href="#runtime">llama.cpp &amp; Updates <span>↗</span></a><a href="#network">Netzwerk <span>↗</span></a><a href="#access">Zugang &amp; API <span>↗</span></a></nav><footer><i></i> Isolierte Entwicklungsumgebung<p>Steuerung auf dem Mac<br>Hardware von Athena · nur lesend</p></footer></aside><main><header><span>ATHENA CONTROL SURFACE</span><span class="pill">v0.4 · GUI-Prototyp</span></header><div id="view"></div><p id="error" role="alert"></p></main></body></html>
+1 -1
View File
@@ -12,7 +12,7 @@ import sys
NAME = 'athena-deck-network' NAME = 'athena-deck-network'
BASE = Path('/opt/athena-deck') BASE = Path('/opt/athena-deck')
FILES = {'auth.py', 'access-ui.js', 'server.py', 'demo.py', 'collect_hardware.py', 'index.html', 'app.js', 'style.css', 'network-ui.js', 'login.html', 'login.js', 'network/__init__.py', 'network/config.py', 'network/policy.py', 'network/rpc.py', 'network/agent.py', 'network/client.py', 'network/Dockerfile', '.dockerignore'} FILES = {'studio.js', 'auth.py', 'access-ui.js', 'server.py', 'demo.py', 'collect_hardware.py', 'index.html', 'app.js', 'style.css', 'network-ui.js', 'login.html', 'login.js', 'network/__init__.py', 'network/config.py', 'network/policy.py', 'network/rpc.py', 'network/agent.py', 'network/client.py', 'network/Dockerfile', '.dockerignore'}
def run(*args, **kwargs): def run(*args, **kwargs):
return subprocess.run(args, capture_output=True, timeout=600, **kwargs) return subprocess.run(args, capture_output=True, timeout=600, **kwargs)
+2 -2
View File
@@ -252,12 +252,12 @@ class Handler(BaseHTTPRequestHandler):
return self.respond({'error':'Anmeldung erforderlich.'},401) return self.respond({'error':'Anmeldung erforderlich.'},401)
if not self.authenticated() and self.path == '/': if not self.authenticated() and self.path == '/':
return self.respond((ROOT/'login.html').read_bytes(), mime='text/html; charset=utf-8') return self.respond((ROOT/'login.html').read_bytes(), mime='text/html; charset=utf-8')
routes = {'/': ('index.html', 'text/html; charset=utf-8'), '/app.js': ('app.js', 'text/javascript'), '/style.css': ('style.css', 'text/css'), '/network-ui.js': ('network-ui.js', 'text/javascript'), '/access-ui.js': ('access-ui.js', 'text/javascript')} routes = {'/': ('index.html', 'text/html; charset=utf-8'), '/app.js': ('app.js', 'text/javascript'), '/style.css': ('style.css', 'text/css'), '/network-ui.js': ('network-ui.js', 'text/javascript'), '/access-ui.js': ('access-ui.js', 'text/javascript'), '/studio.js': ('studio.js', 'text/javascript')}
if self.path in routes: if self.path in routes:
name, mime = routes[self.path] name, mime = routes[self.path]
return self.respond((ROOT/name).read_bytes(), mime=mime) return self.respond((ROOT/name).read_bytes(), mime=mime)
if self.path == '/api/v1/status': if self.path == '/api/v1/status':
return self.respond(dict(name='Athena Deck', version='0.3.0', state='ready', uptime_seconds=round(time.time()-self.server.started), mode='isolated', location='Athena' if os.environ.get('DECK_AUTH_RPC') else 'Mac', demo=self.server.demo.status())) return self.respond(dict(name='Athena Deck', version='0.4.0', state='ready', uptime_seconds=round(time.time()-self.server.started), mode='isolated', location='Athena' if os.environ.get('DECK_AUTH_RPC') else 'Mac', demo=self.server.demo.status()))
if self.path == '/api/v1/hardware': if self.path == '/api/v1/hardware':
return self.respond(self.server.hardware.snapshot()) return self.respond(self.server.hardware.snapshot())
if self.path == '/api/v1/network': if self.path == '/api/v1/network':
+58
View File
@@ -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=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[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 &amp; 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 &amp; 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 &amp; bauen</button></form></section><section class="card"><div class="card-top"><h2>Updates &amp; Ä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 &amp; 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 &amp; 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};
})();
+4
View File
@@ -2,3 +2,7 @@
label{display:block;margin:18px 0 8px;font-size:13px;color:#c4d1c6}input,select{display:block;width:100%;max-width:560px;padding:12px;margin-bottom:16px;background:#101718;color:#e4eee5;border:1px solid #465348;border-radius:7px;font:inherit}input[type=file]{font-size:13px}button{margin-bottom:8px}.network-message{white-space:pre-wrap;color:#d1eabf}.warning{color:#f2c891}.login{margin:10vh auto;width:100%;max-width:640px}.network-status{display:flex;gap:30px;flex-wrap:wrap}.network-status strong{display:block;margin:8px 0}.link{color:#c5edaf}.network-card a{color:#c5edaf}.network-card button{font-size:13px}.network-card .note{overflow-wrap:anywhere} label{display:block;margin:18px 0 8px;font-size:13px;color:#c4d1c6}input,select{display:block;width:100%;max-width:560px;padding:12px;margin-bottom:16px;background:#101718;color:#e4eee5;border:1px solid #465348;border-radius:7px;font:inherit}input[type=file]{font-size:13px}button{margin-bottom:8px}.network-message{white-space:pre-wrap;color:#d1eabf}.warning{color:#f2c891}.login{margin:10vh auto;width:100%;max-width:640px}.network-status{display:flex;gap:30px;flex-wrap:wrap}.network-status strong{display:block;margin:8px 0}.link{color:#c5edaf}.network-card a{color:#c5edaf}.network-card button{font-size:13px}.network-card .note{overflow-wrap:anywhere}
#access-page .card{margin-top:20px}#access-message{color:#d1eabf;white-space:pre-wrap} #access-page .card{margin-top:20px}#access-message{color:#d1eabf;white-space:pre-wrap}
/* Model studio: all sample and draft states are explicit. */
aside{overflow-y:auto}nav{gap:3px}nav a{padding:11px 12px}.prototype-banner{display:flex;align-items:center;gap:14px;background:#202b23;border:1px solid #40503f;border-radius:9px;padding:15px 18px;margin:24px 0;color:#bdd0ba;font-size:12px}.studio-tabs{display:flex;gap:7px;border-bottom:1px solid #344038;padding-bottom:10px;margin:28px 0}.studio-tabs button{font-size:13px}.studio-tabs button span{margin-left:8px;opacity:.6}.model-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:20px}.model-card h2{margin-bottom:8px}.model-icon{display:inline-flex;align-items:center;justify-content:center;width:42px;height:42px;background:#29372b;color:#d0e5c4;border-radius:10px;font-size:23px}.model-facts{font-size:12px;line-height:1.6}.model-facts>div{display:flex;justify-content:space-between;gap:15px;padding:10px 0;border-bottom:1px solid #2a3430}.model-facts dt{color:#819386}.model-facts dd{margin:0;text-align:right;color:#c9d3ca}.unknown-dot{display:inline-block;width:6px;height:6px;border-radius:50%;background:#a2aea5;margin-right:5px}.card-actions{display:flex;gap:8px;flex-wrap:wrap;margin-top:22px}.card-actions button{margin-right:0;font-size:12px}.studio-toolbar{display:grid;grid-template-columns:2fr 1fr;gap:18px}.studio-toolbar label{margin-top:0}.studio-toolbar input,.studio-toolbar select{max-width:none;margin-bottom:0}.small{font-size:12px}.section-heading{display:flex;justify-content:space-between;gap:20px;align-items:center;margin-bottom:20px}.section-heading h2{margin-bottom:3px}.section-heading p{margin-top:5px}.section-heading button{white-space:nowrap}.profile-list{display:grid;gap:12px}.profile-row{display:grid;grid-template-columns:1.5fr 1fr .5fr 1fr;gap:18px;align-items:center;padding:22px}.profile-row h2{font-size:18px;margin:9px 0 4px;overflow-wrap:anywhere}.profile-row p{font-size:12px;margin:0}.profile-row strong{display:block;margin-top:10px;font-size:14px;font-weight:500}.profile-actions{grid-column:1/-1;display:flex;border-top:1px solid #303a37;padding-top:14px}.profile-actions button{font-size:12px;margin-bottom:0}.endpoint-preview,.detail-card,.editor{margin-top:24px}pre{background:#101718;padding:20px;border-radius:8px;overflow:auto;font-size:12px;line-height:1.6;color:#bed4b8;max-height:280px}.form-grid{display:grid;grid-template-columns:1fr 1fr;gap:0 24px}.form-grid input,.form-grid select{max-width:none}.checkbox{display:flex;align-items:center;gap:10px}.checkbox input{width:16px;height:16px;margin:0;accent-color:#bfdda9}.editor details{border-top:1px solid #303a37;margin-top:22px;padding-top:18px}summary{cursor:pointer;color:#ccdbc7;font-size:14px}.danger{color:#e9aaa0!important}#studio-notice{color:#c7e4b6}#profile-error{color:#ffc1a7}.runtime-empty{text-align:center;padding:40px;margin-bottom:20px}.runtime-empty a{display:block;margin-top:16px;font-size:13px}.flow{list-style:none;counter-reset:steps;padding:0;display:grid;grid-template-columns:repeat(2,1fr);gap:20px;margin:28px 0}.flow li{counter-increment:steps;padding:0 0 0 35px;position:relative}.flow li:before{content:counter(steps,decimal-leading-zero);position:absolute;left:0;color:#b6d2a8;font-size:12px;top:2px}.flow b{font-size:13px;font-weight:500}.flow span{display:block;color:#82968a;font-size:12px;line-height:1.7;margin-top:7px}.runtime-stats{display:grid;grid-template-columns:repeat(3,1fr);gap:15px;margin-bottom:22px}.runtime-stats .card{padding:22px}.runtime-stats h2{font-size:18px}.runtime-stats p{font-size:12px}#studio[data-page=runtime]>.card{margin-top:20px}.release-comparison{display:flex;gap:35px;align-items:center;margin:25px 0}.fit-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:22px}.fit-grid b{font-size:13px}.fit-grid p{font-size:12px}@media(max-width:1100px){.model-grid{grid-template-columns:1fr}.runtime-stats{grid-template-columns:1fr}.profile-row{grid-template-columns:1fr 1fr}.fit-grid{grid-template-columns:1fr}}@media(max-width:600px){.form-grid,.studio-toolbar,.flow{grid-template-columns:1fr}.prototype-banner,.section-heading{align-items:flex-start;flex-direction:column}.studio-tabs{flex-wrap:wrap}.studio-tabs button{padding:10px 12px}.profile-row{gap:18px}.card{padding:20px}.model-facts>div{flex-direction:column;gap:3px}.model-facts dd{text-align:left}}
.nav-heading{font-size:10px;letter-spacing:1.5px;color:#75867b;margin:15px 12px 5px}