/* Original Deck UI, inspired by LocalAI's explore/detail/download workflow. */ window.CatalogUI = (() => { const escape = value => String(value ?? '').replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c])); const size = n => { if (!Number.isFinite(n)) return 'Unbekannt'; if(n < 1024) return n+' B'; const unit = n >= 1024**3 ? 'GiB' : n >= 1024**2 ? 'MiB' : 'KiB'; return (n / (unit === 'GiB' ? 1024**3 : unit === 'MiB' ? 1024**2 : 1024)).toFixed(1)+' '+unit; }; const number = n => Number.isFinite(n) ? new Intl.NumberFormat('de-DE', {notation:'compact'}).format(n) : '–'; async function api(path, data) { const response = await fetch('/api/v1/catalog'+path, data ? {method:'POST',headers:{'Content-Type':'application/json','X-Athena-Deck':'1'},body:JSON.stringify(data)} : {}); const result = await response.json(); if (!response.ok) throw Error(result.error || 'Anfrage fehlgeschlagen'); return result; } function html(library) { return `
HUGGING FACE · LIVE-KATALOG

${library ? 'Deine Dateien auf Athena' : 'Das nächste Modell entdecken'}

${library ? 'Heruntergeladene Dateien mit festgehaltener Version und Integritätsprüfung.' : 'Modell suchen, Variante vergleichen und gezielt auf Athena herunterladen.'}

${library ? '
' : `
Beliebte Modelle
01 ENTDECKEN → 02 AUSWÄHLEN → 03 LADEN

Welches Modell interessiert dich?

Wähle ein Ergebnis, um Dateien, Varianten und Lizenz zu prüfen.

Downloads belegen Festplattenspeicher. Sie starten kein Modell. Für Bild, Audio und Video sind häufig mehrere Dateien und ein passender Worker erforderlich.

`}
`; } function bind(kind, library) { const root = document.querySelector('#live-catalog'); if (!root) return; const el = id => root.querySelector('#'+id); let state = {entries:[],job:null,free_bytes:null}, models = [], selected = null, detail = null, timer, refreshing = false, searchSequence = 0, detailSequence = 0, starting = false, libraryRendered = false; const message = text => { if (root.isConnected) el('hub-message').textContent = text; }; const installed = file => detail && state.entries.some(x => x.repo === detail.repo && x.revision === detail.revision && x.file === file.name); function renderModels() { const sorted = [...models].sort(el('hub-sort').value === 'name' ? (a,b) => a.repo.localeCompare(b.repo) : (a,b) => (b.downloads||0)-(a.downloads||0)); el('hub-count').textContent = `${models.length} Ergebnisse · höchstens 20 pro Suche`; el('hub-results').innerHTML = sorted.map(m => ``).join('') || '

Keine Treffer

Versuche einen anderen Namen. Die Suche ist auf den aktuellen Bereich begrenzt.

'; root.querySelectorAll('[data-repo]').forEach(b => b.onclick = () => loadFiles(b.dataset.repo)); } function renderLibrary() { el('hub-results').innerHTML = state.entries.filter(x => x.kind === kind).map(x => `
DATEI HERUNTERGELADEN

${escape(x.repo)}

${escape(x.file)}

${size(x.size)}

Laufzeit noch nicht angebunden

Version und Integrität

Revision ${escape(x.revision)}

${x.upstream_hash_verified ? 'SHA-256 mit Quelle geprüft' : 'SHA-256 lokal erfasst; Größe mit Quelle geprüft'}

`).join('') || '

Deine Bibliothek ist noch leer

Unter „Entdecken“ kannst du Dateien für diesen Bereich auswählen und herunterladen.

'; } function renderJob() { el('hub-capacity').textContent = `Athena · ${size(state.free_bytes)} freier Speicher · 10 GiB bleiben als Reserve frei`; const j = state.job; if (!j) { el('hub-job').innerHTML = ''; return; } const labels = {downloading:'Wird heruntergeladen',complete:'Download abgeschlossen',cancelled:'Download abgebrochen',failed:'Download fehlgeschlagen'}; const percent = j.total > 0 ? Math.min(100,Math.max(0,j.bytes/j.total*100)) : 0; el('hub-job').innerHTML = `
${escape(labels[j.state] || j.state)}${percent.toFixed(1)} %

${escape(j.repo)}

${escape(j.file)}

${size(j.bytes)} von ${size(j.total)}${j.state === 'complete' ? ' · geprüft und in der Bibliothek gespeichert' : ''}

${j.error ? `

${escape(j.error)}

` : ''}${j.state === 'downloading' ? '' : ''}
`; el('hub-cancel')?.addEventListener('click', async event => { event.target.disabled = true; try { await api('/cancel',{}); message('Abbruch angefordert. Eine laufende Netzwerkabfrage kann noch bis zu 20 Sekunden dauern.'); } catch(error) { message(error.message); event.target.disabled = false; } }); } async function refresh() { if (refreshing || !root.isConnected) return; refreshing = true; clearTimeout(timer); try { const next = await api(''); if (!root.isConnected) return; const entriesChanged = JSON.stringify(next.entries) !== JSON.stringify(state.entries); const statusChanged = JSON.stringify(next) !== JSON.stringify(state); state = next; if (statusChanged) renderJob(); if (library) { if (entriesChanged || !libraryRendered) { renderLibrary(); libraryRendered = true; } } else { if (entriesChanged) renderModels(); updateSelection(); } } catch(error) { message(error.message); } finally { refreshing = false; if (root.isConnected) timer = setTimeout(refresh, 2500); } } async function search(q) { const sequence = ++searchSequence; message('Modelle werden gesucht …'); try { const result = await api('/search?'+new URLSearchParams({q,kind})); if (!root.isConnected || sequence !== searchSequence) return; models = result.models; renderModels(); message(kind === 'audio' ? 'Audio-Suche: derzeit Text-to-Speech-Modelle.' : 'Ergebnisse nach Beliebtheit. Wähle ein Modell für die Dateiauswahl.'); } catch(error) { if (sequence === searchSequence) message(error.message); } } function updateSelection() { if (!detail || !el('hub-download')) return; const chosen = el('hub-variants').querySelector('input:checked'); const file = chosen ? detail.files[Number(chosen.value)] : null; const busy = starting || state.job?.state === 'downloading'; const exists = file && installed(file); const fitsDisk = file && Number.isFinite(state.free_bytes) && file.size <= state.free_bytes - 10*1024**3; el('hub-download').disabled = !file || !!detail.gated || busy || exists || !fitsDisk; el('hub-download').textContent = starting ? 'Download wird vorbereitet …' : exists ? 'Bereits in Bibliothek' : busy ? 'Ein Download läuft bereits' : 'Ausgewählte Datei herunterladen'; el('hub-selection').textContent = file ? `${file.name} · ${size(file.size)}${!fitsDisk ? ' · Speicherplatz noch nicht bestätigt oder Reserve unterschritten' : ''}` : 'Wähle eine Datei aus der Liste.'; } function renderFiles() { const query = el('hub-file-filter').value.toLowerCase(), format = el('hub-format').value; const current = el('hub-variants').querySelector('input:checked')?.value; const files = detail.files.map((f,index) => ({...f,index})).filter(f => f.name.toLowerCase().includes(query) && (!format || f.name.toLowerCase().endsWith(format))).sort((a,b) => Number(a.name.toLowerCase().endsWith('.json'))-Number(b.name.toLowerCase().endsWith('.json')) || a.size-b.size); el('hub-variants').innerHTML = files.map(f => ``).join('') || '

Keine passenden Dateien. Unterstützt werden GGUF, Safetensors und JSON.

'; el('hub-variants').onchange = updateSelection; updateSelection(); } async function loadFiles(repo) { const sequence = ++detailSequence; selected = repo; detail = null; renderModels(); el('hub-files').innerHTML = '

Modelldetails werden geladen …

'; try { const result = await api('/files?'+new URLSearchParams({repo})); if (!root.isConnected || sequence !== detailSequence) return; detail = result; el('hub-files').innerHTML = `MODELLDETAILS

${escape(repo)}

${detail.gated ? 'Zugang beschränkt' : 'Öffentlich'}${detail.files.length} unterstützte Dateien

Lizenz: ${escape(detail.license || 'Nicht angegeben')} · Modellkarte lesen ↗

Hardware-Eignung noch nicht ermittelt

Dateigröße ist kein VRAM-Bedarf. Kontext, Quantisierung und Laufzeit müssen gemeinsam geprüft werden.

${detail.gated ? '

Dieses Repository benötigt eine Freigabe. Downloads mit Hugging-Face-Zugangsdaten werden noch nicht unterstützt.

' : ''}

Variante oder Datei auswählen

Einzeldatei-Download auf Athena. Geteilte Modelle und Bild-/Audio-/Video-Pipelines benötigen weitere Dateien; dies installiert noch keinen vollständigen Worker.

Feste Quellversion

${escape(detail.revision)}

`; el('hub-file-filter').oninput = renderFiles; el('hub-format').onchange = renderFiles; renderFiles(); el('hub-download').onclick = async () => { const chosen = el('hub-variants').querySelector('input:checked'); if (!chosen || starting) return; const request = {repo:detail.repo,revision:detail.revision,filename:detail.files[Number(chosen.value)].name,kind}; starting = true; updateSelection(); try { await api('/download', request); message('Download gestartet. Den Fortschritt siehst du oberhalb der Galerie.'); await refresh(); } catch(error) { message(error.message); } finally { starting = false; if (root.isConnected) updateSelection(); } }; } catch(error) { if (sequence === detailSequence) { el('hub-files').innerHTML = '

Details konnten nicht geladen werden. Wähle das Modell erneut, um es noch einmal zu versuchen.

'; message(error.message); } } } el('hub-refresh').onclick = refresh; if (!library) { el('hub-sort').onchange = renderModels; el('hub-search').onsubmit = event => { event.preventDefault(); search(new FormData(event.target).get('q').trim()); }; search(''); } refresh(); } return {html,bind}; })();