Improve model discovery, variant selection and download visibility
This commit is contained in:
@@ -75,3 +75,29 @@ Build-Einstellungen und Update-Ansicht geöffnet, deaktivierte Aktionsknöpfe so
|
||||
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.
|
||||
|
||||
## Modellgalerie und Download-Ablauf (2026-09-28)
|
||||
|
||||
Die Live-Galerie verwendet eine eigene Deck-Oberfläche, inspiriert von LocalAIs
|
||||
[Modellgalerie](https://github.com/mudler/LocalAI/blob/master/core/http/react-ui/src/pages/Models.jsx)
|
||||
und [Download-Anzeige](https://github.com/mudler/LocalAI/blob/master/core/http/react-ui/src/components/OperationCard.jsx).
|
||||
Es wurde kein LocalAI-Frontendcode übernommen.
|
||||
|
||||
- Beim Öffnen erscheinen beliebte Hugging-Face-Repositories des Bereichs.
|
||||
- Suche liefert bis zu 20 Treffer; Sortierung nach Downloads oder Namen.
|
||||
- Ergebnis auswählen: Modelldetails, Lizenzlink und feste Quellrevision.
|
||||
- Dateien nach Name/Quantisierungsbezeichnung und Format filtern; genau eine Datei auswählen.
|
||||
- Download explizit starten. Zugangsbeschränkte Repositories, bereits vorhandene
|
||||
Dateien, laufende Downloads und unzureichender Speicher sperren den Button.
|
||||
- Fortschritt, Bytes, Abbruch und Fehler werden aus der vorhandenen API angezeigt.
|
||||
- Bibliothek zeigt abgeschlossene Dateien einschließlich Version und Integritätsprüfung.
|
||||
|
||||
Grenzen: weiterhin Einzeldateien, keine vollständige Pipeline-Installation,
|
||||
keine Warteschlange, kein Resume, keine automatischen Modellstarts. Audio-Suche
|
||||
ist derzeit auf Text-to-Speech begrenzt. Hardware-/Kontext-Eignung wird in dieser
|
||||
Galerie noch nicht berechnet; Dateigröße wird ausdrücklich nicht als VRAM-Bedarf
|
||||
angezeigt. Der separate llama.cpp-Einpassungstest bleibt unter Einstellungen.
|
||||
|
||||
Validierung: 46 bestehende Python-Tests bestanden; JavaScript-Syntax geprüft;
|
||||
Browserprüfung mit echten Hugging-Face-Such- und Dateidaten, Dateifilter,
|
||||
Auswahl und Speicherreserve-Sperre. Keine Modellgewichte für den UI-Test geladen.
|
||||
|
||||
+83
-10
@@ -1,15 +1,88 @@
|
||||
const CatalogUI=(()=>{
|
||||
const e=v=>String(v??'').replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
|
||||
const size=n=>(n/1024**3).toFixed(2)+' GiB';
|
||||
async function api(path,data){const r=await fetch('/api/v1/catalog'+path,data?{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 html(library){return `<section id="live-catalog" class="card"><span class="pill">LIVE · HUGGING FACE</span><h2>${library?'Dateien auf Athena':'Modelle suchen'}</h2><p>Öffentliche Repositories. Downloads laden kein Modell in RAM oder VRAM. Für Bild, Audio und Video können mehrere Dateien und ein eigener Worker nötig sein.</p>${library?'':'<form id="hub-search"><label>Suchbegriff<input name="q" maxlength="120" placeholder="Qwen, FLUX, Whisper …"></label><button>Suchen</button></form>'}<button class="secondary" id="hub-refresh">Downloadstatus aktualisieren</button><p id="hub-message" role="status"></p><div id="hub-job"></div><div id="hub-results"></div><div id="hub-files"></div></section>`;}
|
||||
/* 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 `<section id="live-catalog"><div class="section-heading"><div><span class="kicker">HUGGING FACE · LIVE-KATALOG</span><h2>${library ? 'Deine Dateien auf Athena' : 'Das nächste Modell entdecken'}</h2><p>${library ? 'Heruntergeladene Dateien mit festgehaltener Version und Integritätsprüfung.' : 'Modell suchen, Variante vergleichen und gezielt auf Athena herunterladen.'}</p></div><button class="secondary" id="hub-refresh">Aktualisieren</button></div>
|
||||
<div id="hub-capacity" class="small muted"></div><div id="hub-job"></div><p id="hub-message" role="status" aria-live="polite"></p>
|
||||
${library ? '<div id="hub-results" class="model-grid"></div>' : `<form id="hub-search" class="hub-search"><label>Modell oder Anbieter<input name="q" maxlength="120" placeholder="z. B. Qwen oder FLUX" autocomplete="off"></label><button>Suchen</button></form><div class="hub-layout"><div><div class="hub-list-heading"><span id="hub-count" class="small muted">Beliebte Modelle</span><label class="sr-only" for="hub-sort">Ergebnisse sortieren</label><select id="hub-sort"><option value="downloads">Meiste Downloads</option><option value="name">Name A–Z</option></select></div><div id="hub-results" class="hub-results"></div></div><section id="hub-files" class="card hub-detail" aria-label="Modelldetails"><span class="kicker">01 ENTDECKEN → 02 AUSWÄHLEN → 03 LADEN</span><h2>Welches Modell interessiert dich?</h2><p>Wähle ein Ergebnis, um Dateien, Varianten und Lizenz zu prüfen.</p><p class="note">Downloads belegen Festplattenspeicher. Sie starten kein Modell. Für Bild, Audio und Video sind häufig mehrere Dateien und ein passender Worker erforderlich.</p></section></div>`}</section>`;
|
||||
}
|
||||
function bind(kind, library) {
|
||||
const root = document.querySelector('#live-catalog'); if (!root) return;
|
||||
const message=t=>{if(root.isConnected)root.querySelector('#hub-message').textContent=t;};
|
||||
async function refresh(){try{const v=await api('');if(!root.isConnected)return;const j=v.job;root.querySelector('#hub-job').innerHTML=`<p>Freier Speicher: ${size(v.free_bytes)} · Reserve: 10 GiB</p>`+(j?`<p>${e(j.repo)} / ${e(j.file)} · ${e(j.state)} · ${size(j.bytes)} / ${size(j.total)}</p>${j.error?`<p>${e(j.error)}</p>`:''}${j.state==='downloading'?'<button id="hub-cancel" class="secondary">Download abbrechen</button>':''}`:'');root.querySelector('#hub-cancel')?.addEventListener('click',async()=>{await api('/cancel',{});message('Abbruch angefordert; laufende Netzwerkabfrage kann noch bis zu 20 Sekunden dauern.');});if(library)root.querySelector('#hub-results').innerHTML=v.entries.filter(x=>x.kind===kind).map(x=>`<article class="card"><h3>${e(x.repo)}</h3><p>${e(x.file)} · ${size(x.size)}</p><p>Heruntergeladen · Laufzeit noch nicht angebunden</p><small>Revision ${e(x.revision)} · ${x.upstream_hash_verified?'SHA-256 mit Quelle geprüft':'SHA-256 lokal erfasst; Größe mit Quelle geprüft'}</small></article>`).join('')||'<p>Noch keine Dateien für diesen Bereich heruntergeladen.</p>';if(j?.state==='downloading')setTimeout(()=>{if(root.isConnected)refresh();},2000);}catch(err){message(err.message);}}
|
||||
root.querySelector('#hub-refresh').onclick=refresh;
|
||||
root.querySelector('#hub-search')?.addEventListener('submit',async event=>{event.preventDefault();message('Suche …');try{const v=await api('/search?'+new URLSearchParams({q:new FormData(event.target).get('q'),kind}));if(!root.isConnected)return;root.querySelector('#hub-results').innerHTML=v.models.map(m=>`<article class="card"><h3>${e(m.repo)}</h3><p>${m.gated?'Zugangsbeschränkt':'Öffentlich'} · ${e(m.downloads)} Downloads</p><button data-repo="${e(m.repo)}">Dateien auswählen</button></article>`).join('');message(v.models.length+' Ergebnisse');root.querySelectorAll('[data-repo]').forEach(b=>b.onclick=()=>files(b.dataset.repo));}catch(err){message(err.message);}});
|
||||
async function files(repo){message('Dateien werden gelesen …');try{const v=await api('/files?'+new URLSearchParams({repo}));if(!root.isConnected)return;root.querySelector('#hub-files').innerHTML=`<h3>${e(repo)}</h3><p>Lizenz: ${e(v.license||'Nicht angegeben')} · <a target="_blank" rel="noopener noreferrer" href="${e(v.url)}">Modellkarte und Bedingungen lesen</a></p><p>VRAM- und Kontext-Eignung noch nicht berechnet. Dateigröße allein belegt keine Lauffähigkeit.</p>`+v.files.map((f,i)=>`<article class="card"><p>${e(f.name)} · ${size(f.size)}</p><button data-file="${i}" ${v.gated?'disabled':''}>Datei auf Athena herunterladen</button></article>`).join('');message(v.files.length+' unterstützte Dateien (GGUF, Safetensors, JSON).');root.querySelectorAll('[data-file]').forEach(b=>b.onclick=async()=>{b.disabled=true;try{await api('/download',{repo,filename:v.files[Number(b.dataset.file)].name,revision:v.revision,kind});message('Download gestartet. Kein Modell wird ausgeführt.');refresh();}catch(err){message(err.message);}finally{b.disabled=false;}});}catch(err){message(err.message);}}
|
||||
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 => `<button type="button" class="hub-model ${selected === m.repo ? 'selected' : ''}" data-repo="${escape(m.repo)}" aria-pressed="${selected === m.repo}"><span class="hub-author">${escape(m.repo.split('/')[0])}</span><strong>${escape(m.repo.split('/').slice(1).join('/'))}</strong><span class="hub-model-meta">↓ ${number(m.downloads)} Downloads · ${m.gated ? 'Zugang beschränkt' : 'Öffentlich'}</span>${state.entries.some(x => x.repo === m.repo) ? '<span class="tag">Dateien in Bibliothek</span>' : ''}</button>`).join('') || '<div class="empty"><h3>Keine Treffer</h3><p>Versuche einen anderen Namen. Die Suche ist auf den aktuellen Bereich begrenzt.</p></div>';
|
||||
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 => `<article class="card"><span class="pill">DATEI HERUNTERGELADEN</span><h3>${escape(x.repo)}</h3><p class="hub-filename">${escape(x.file)}</p><strong>${size(x.size)}</strong><p>Laufzeit noch nicht angebunden</p><details><summary>Version und Integrität</summary><p class="hub-filename">Revision ${escape(x.revision)}</p><p>${x.upstream_hash_verified ? 'SHA-256 mit Quelle geprüft' : 'SHA-256 lokal erfasst; Größe mit Quelle geprüft'}</p></details></article>`).join('') || '<div class="empty wide"><h3>Deine Bibliothek ist noch leer</h3><p>Unter „Entdecken“ kannst du Dateien für diesen Bereich auswählen und herunterladen.</p></div>';
|
||||
}
|
||||
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 = `<section class="card hub-operation"><div class="card-top"><span class="kicker">${escape(labels[j.state] || j.state)}</span><strong>${percent.toFixed(1)} %</strong></div><h3>${escape(j.repo)}</h3><p class="hub-filename">${escape(j.file)}</p><progress max="100" value="${percent}" aria-label="Download-Fortschritt"></progress><p>${size(j.bytes)} von ${size(j.total)}${j.state === 'complete' ? ' · geprüft und in der Bibliothek gespeichert' : ''}</p>${j.error ? `<p class="warning">${escape(j.error)}</p>` : ''}${j.state === 'downloading' ? '<button class="secondary" id="hub-cancel">Download abbrechen</button>' : ''}</section>`;
|
||||
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 => `<label class="hub-variant"><input type="radio" name="hub-file" value="${f.index}" ${current === String(f.index) ? 'checked' : ''}><span><strong>${escape(f.name)}</strong><small>${f.name.toLowerCase().endsWith('.json') ? 'Konfiguration' : 'Modelldatei'}${installed(f) ? ' · In Bibliothek' : ''}${f.sha256 ? ' · SHA-256 verfügbar' : ''}</small></span><b>${size(f.size)}</b></label>`).join('') || '<p>Keine passenden Dateien. Unterstützt werden GGUF, Safetensors und JSON.</p>';
|
||||
el('hub-variants').onchange = updateSelection; updateSelection();
|
||||
}
|
||||
async function loadFiles(repo) {
|
||||
const sequence = ++detailSequence; selected = repo; detail = null; renderModels();
|
||||
el('hub-files').innerHTML = '<p role="status">Modelldetails werden geladen …</p>';
|
||||
try {
|
||||
const result = await api('/files?'+new URLSearchParams({repo})); if (!root.isConnected || sequence !== detailSequence) return; detail = result;
|
||||
el('hub-files').innerHTML = `<span class="kicker">MODELLDETAILS</span><h2 class="hub-filename">${escape(repo)}</h2><div class="hub-badges"><span class="pill">${detail.gated ? 'Zugang beschränkt' : 'Öffentlich'}</span><span class="pill">${detail.files.length} unterstützte Dateien</span></div><p>Lizenz: ${escape(detail.license || 'Nicht angegeben')} · <a class="link" href="${escape(detail.url)}" target="_blank" rel="noopener noreferrer">Modellkarte lesen ↗</a></p><div class="hub-fit"><strong>Hardware-Eignung noch nicht ermittelt</strong><p>Dateigröße ist kein VRAM-Bedarf. Kontext, Quantisierung und Laufzeit müssen gemeinsam geprüft werden.</p></div>${detail.gated ? '<p class="warning">Dieses Repository benötigt eine Freigabe. Downloads mit Hugging-Face-Zugangsdaten werden noch nicht unterstützt.</p>' : ''}<h3>Variante oder Datei auswählen</h3><div class="hub-file-controls"><label>Dateiname filtern<input id="hub-file-filter" placeholder="z. B. Q4_K_M"></label><label>Format<select id="hub-format"><option value="">Alle Formate</option><option value=".gguf">GGUF</option><option value=".safetensors">Safetensors</option><option value=".json">JSON / Konfiguration</option></select></label></div><div id="hub-variants" class="hub-variants"></div><div class="hub-review"><p id="hub-selection" class="hub-filename"></p><button id="hub-download" disabled>Ausgewählte Datei herunterladen</button><p class="small">Einzeldatei-Download auf Athena. Geteilte Modelle und Bild-/Audio-/Video-Pipelines benötigen weitere Dateien; dies installiert noch keinen vollständigen Worker.</p><details><summary>Feste Quellversion</summary><p class="hub-filename">${escape(detail.revision)}</p></details></div>`;
|
||||
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 = '<p>Details konnten nicht geladen werden. Wähle das Modell erneut, um es noch einmal zu versuchen.</p>'; 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};
|
||||
|
||||
@@ -6,3 +6,9 @@ label{display:block;margin:18px 0 8px;font-size:13px;color:#c4d1c6}input,select{
|
||||
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}
|
||||
|
||||
/* Live model catalogue: browse, inspect a file, download, then library. */
|
||||
.sr-only{position:absolute;width:1px;height:1px;overflow:hidden;clip-path:inset(50%)}
|
||||
.hub-search{display:flex;align-items:end;gap:12px;margin:24px 0}.hub-search label{flex:1;margin:0}.hub-search input{max-width:none;margin-bottom:0}.hub-search button{margin:0}
|
||||
.hub-layout{display:grid;grid-template-columns:minmax(240px,.8fr) minmax(0,1.4fr);gap:24px;align-items:start}.hub-list-heading{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-bottom:12px}.hub-list-heading select{width:auto;max-width:160px;font-size:12px;margin:0;padding:8px}.hub-results{display:grid;gap:8px;max-height:850px;overflow-y:auto;padding:3px}.hub-model{display:grid;gap:9px;width:100%;margin:0;text-align:left;padding:18px;background:#181f21;color:#e4eee5;border:1px solid #303a37}.hub-model:hover{background:#222d27}.hub-model.selected{border-color:#bddda9;background:#26342b}.hub-model strong{font-size:14px;overflow-wrap:anywhere}.hub-author,.hub-model-meta{font-size:11px;color:#96a89c}.hub-detail{min-width:0}.hub-detail h2{font-size:22px}.hub-badges{display:flex;gap:8px;flex-wrap:wrap}.hub-fit{padding:16px;background:#202a25;border-radius:9px;font-size:13px;margin:22px 0}.hub-fit p{font-size:12px;margin-bottom:0}.hub-file-controls{display:grid;grid-template-columns:1.3fr 1fr;gap:12px}.hub-file-controls label{margin-top:0}.hub-file-controls input,.hub-file-controls select{font-size:12px;margin-bottom:0}.hub-variants{max-height:380px;overflow:auto;border-top:1px solid #303a37}.hub-variant{display:flex;align-items:center;gap:12px;padding:16px 8px;margin:0;border-bottom:1px solid #303a37;cursor:pointer}.hub-variant:has(input:checked){background:#28362b}.hub-variant input{width:16px;min-width:16px;margin:0;accent-color:#c5edaf}.hub-variant span{flex:1;min-width:0}.hub-variant strong{display:block;font-weight:500;font-size:12px;overflow-wrap:anywhere}.hub-variant small{display:block;margin-top:7px;font-size:10px;color:#95a69b}.hub-variant b{font-size:11px;white-space:nowrap}.hub-review{margin-top:22px}.hub-review button{width:100%;font-size:13px}.hub-filename{overflow-wrap:anywhere}.hub-operation{margin-top:20px;border-color:#647e58}.hub-operation h3{font-size:16px}.hub-operation p{margin:10px 0;font-size:12px}.hub-operation progress{height:9px}.hub-operation .card-top strong{font-size:20px}
|
||||
@media(max-width:1100px){.hub-layout{grid-template-columns:1fr}.hub-results{max-height:360px}.hub-file-controls{grid-template-columns:1fr 1fr}}@media(max-width:600px){.hub-search{align-items:stretch;flex-direction:column}.hub-file-controls{grid-template-columns:1fr}.hub-variant{gap:8px}}
|
||||
|
||||
Reference in New Issue
Block a user