Simplify control into LLM video and music modes
This commit is contained in:
@@ -264,3 +264,16 @@ aktuellen Browserseite. Beim erneuten Öffnen einer Bibliothek erscheinen deren
|
|||||||
zuletzt geladene Dateien sofort mit Aktualisierungshinweis. Aktionen bleiben bis
|
zuletzt geladene Dateien sofort mit Aktualisierungshinweis. Aktionen bleiben bis
|
||||||
zur frischen Serverantwort gesperrt; Löschschutz und Profilzuordnungen werden
|
zur frischen Serverantwort gesperrt; Löschschutz und Profilzuordnungen werden
|
||||||
weiterhin serverseitig geprüft. Ein Neuladen der Browserseite leert diesen Cache.
|
weiterhin serverseitig geprüft. Ein Neuladen der Browserseite leert diesen Cache.
|
||||||
|
|
||||||
|
### Vereinfachte Steuerung
|
||||||
|
|
||||||
|
Unter Steuerung zuerst LLM, Video oder Musik auswählen und anschließend starten.
|
||||||
|
Die Auswahl allein verändert den laufenden Betrieb nicht. Start wechselt bei
|
||||||
|
Bedarf die Betriebsart und öffnet den gemeinsamen API-Listener. Stoppen schließt
|
||||||
|
den Listener und beendet bei Video/Musik den exklusiven Betrieb durch Rückkehr
|
||||||
|
auf LLM; die bisherigen Schutzprüfungen und Wartebedingungen bleiben bestehen.
|
||||||
|
Während eines Wechsels erscheint ein unbestimmter Ladebalken mit der echten
|
||||||
|
Wechselphase, ohne erfundene Prozentangabe. Bereit bedeutet API-erreichbar; der
|
||||||
|
Hinweis zu Gewichten unterscheidet geladene Modelle vom Laden bei Anfrage.
|
||||||
|
API-Adressen und der Port werden für die ausgewählte Betriebsart angezeigt.
|
||||||
|
Technische Konfiguration und Profilfreigaben bleiben unter Einstellungen → Endpunkt.
|
||||||
|
|||||||
+43
-4
@@ -1,22 +1,61 @@
|
|||||||
window.EndpointUI=(()=>{
|
window.EndpointUI=(()=>{
|
||||||
const e=v=>String(v??'—').replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
|
const e=v=>String(v??'—').replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
|
||||||
const stateName={running:'Läuft',stopped:'Gestoppt',stopping:'Wird gestoppt · laufende Aufträge werden beendet',loading:'Modell lädt',ready:'Modell bereit',failed:'Fehlgeschlagen'};
|
const stateName={running:'Läuft',stopped:'Gestoppt',stopping:'Wird gestoppt · laufende Aufträge werden beendet',loading:'Modell lädt',ready:'Modell bereit',failed:'Fehlgeschlagen'};
|
||||||
let pending=false,sequence=0;
|
let pending=false,sequence=0,selectedMode=null,controlTimer=null;
|
||||||
async function api(path='',data){const r=await fetch('/api/v1/endpoint'+path,data!==undefined?{method:'POST',headers:{'Content-Type':'application/json','X-Athena-Deck':'1'},body:JSON.stringify(data)}:{});const v=await r.json();if(!r.ok)throw Error(v.error||'Endpunkt nicht erreichbar');return v;}
|
async function api(path='',data){const r=await fetch('/api/v1/endpoint'+path,data!==undefined?{method:'POST',headers:{'Content-Type':'application/json','X-Athena-Deck':'1'},body:JSON.stringify(data)}:{});const v=await r.json();if(!r.ok)throw Error(v.error||'Endpunkt nicht erreichbar');return v;}
|
||||||
function put(el,html){if(el._deckHtml!==html){el.innerHTML=html;el._deckHtml=html;}}
|
function put(el,html){if(el._deckHtml!==html){el.innerHTML=html;el._deckHtml=html;}}
|
||||||
function summary(s){return `<div class="grid"><section class="card wide"><div class="card-top"><h2>OpenAI-kompatibler Endpunkt</h2><span class="pill">${e(stateName[s.state])}</span></div><div class="metrics"><div><small>Erreichbarkeit</small><strong>${s.reachable?'Listener bereit':'Nicht erreichbar'}</strong></div><div><small>Lokaler API-Port</small><strong>${s.port}</strong></div><div><small>Laufende / wartende Aufträge</small><strong>${s.scheduler.active_requests} / ${s.scheduler.waiting_requests}</strong></div></div><p><code>${e(s.base_url)}</code> · <a class="link" href="#endpoint">Port und Profile einstellen →</a></p><p>Geladen: ${e(s.worker.profile_name||'Kein Sprachmodell')} · ${e(stateName[s.worker.state]||s.worker.state)}</p>${s.worker.error||s.error?`<p role="alert">${e(s.worker.error||s.error)}</p>`:''}<p>Aktivierte Profile werden angeboten. Erst eine Anfrage lädt das Modell. Wechsel warten auf laufende Antworten; Bildaufträge entladen vorher das eigene Sprachmodell.</p></section>${[['llm','Sprachmodelle'],['image','Bildgenerierung'],['tts','Text → Sprache (TTS)'],['stt','Sprache → Text (STT)'],['music','Musik']].map(([key,label])=>{const c=s.counts[key];return `<section class="card"><h3>${label}</h3><p>${c.supported?`${c.enabled} Profile aktiviert · ${c.available} ausführbar`:'Noch keine eigene Laufzeit eingerichtet'}</p><span class="pill">${c.loaded?'AKTIV IM SPEICHER':c.available?'BEI ANFRAGE LADEN':'NICHT AKTIV'}</span></section>`}).join('')}</div>`;}
|
function summary(s){return `<div class="grid"><section class="card wide"><div class="card-top"><h2>OpenAI-kompatibler Endpunkt</h2><span class="pill">${e(stateName[s.state])}</span></div><div class="metrics"><div><small>Erreichbarkeit</small><strong>${s.reachable?'Listener bereit':'Nicht erreichbar'}</strong></div><div><small>Lokaler API-Port</small><strong>${s.port}</strong></div><div><small>Laufende / wartende Aufträge</small><strong>${s.scheduler.active_requests} / ${s.scheduler.waiting_requests}</strong></div></div><p><code>${e(s.base_url)}</code> · <a class="link" href="#endpoint">Port und Profile einstellen →</a></p><p>Geladen: ${e(s.worker.profile_name||'Kein Sprachmodell')} · ${e(stateName[s.worker.state]||s.worker.state)}</p>${s.worker.error||s.error?`<p role="alert">${e(s.worker.error||s.error)}</p>`:''}<p>Aktivierte Profile werden angeboten. Erst eine Anfrage lädt das Modell. Wechsel warten auf laufende Antworten; Bildaufträge entladen vorher das eigene Sprachmodell.</p></section>${[['llm','Sprachmodelle'],['image','Bildgenerierung'],['tts','Text → Sprache (TTS)'],['stt','Sprache → Text (STT)'],['music','Musik']].map(([key,label])=>{const c=s.counts[key];return `<section class="card"><h3>${label}</h3><p>${c.supported?`${c.enabled} Profile aktiviert · ${c.available} ausführbar`:'Noch keine eigene Laufzeit eingerichtet'}</p><span class="pill">${c.loaded?'AKTIV IM SPEICHER':c.available?'BEI ANFRAGE LADEN':'NICHT AKTIV'}</span></section>`}).join('')}</div>`;}
|
||||||
|
const modeLabels={llm:'LLM',video:'Video',music:'Musik'};
|
||||||
|
function control(s,root){
|
||||||
|
const v=s.video||{},active=v.mode||'llm';
|
||||||
|
if(!selectedMode)selectedMode=active==='switching'?(v.target_mode||'llm'):active;
|
||||||
|
const selected=selectedMode,label=modeLabels[selected],switching=v.state==='switching',busy=pending||switching||s.state==='stopping';
|
||||||
|
const current=selected===active,available=selected==='video'?Boolean(v.service?.id):Boolean(s.counts[selected]?.available);
|
||||||
|
const ready=current&&s.reachable&&!switching&&(selected!=='video'||v.service?.ready);
|
||||||
|
const loading=current&&selected==='llm'&&s.worker.state==='loading';
|
||||||
|
const address=location.protocol+'//'+location.hostname+':'+s.port;
|
||||||
|
const routes=selected==='llm'?[['Chat',address+'/v1/chat/completions'],['Modelle',address+'/v1/models']]:selected==='music'?[['Musik erzeugen',address+'/v1/audio/music'],['Musikmodelle',address+'/v1/audio/music/models']]:[['Video-API',address+'/']];
|
||||||
|
const reason=available?'':selected==='video'?'Wähle zuerst ein eingerichtetes Videomodell.':'Gib zuerst ein ausführbares Profil am API-Endpunkt frei.';
|
||||||
|
const state=switching?'Wechsel läuft':loading?'Modell wird geladen':ready?'Bereit':current&&s.state==='stopping'?'Wird gestoppt':current&&s.reachable?'Startet':current?'Gestoppt':'Nicht aktiv';
|
||||||
|
put(root.querySelector('#control-modes'),`<div class="control-modes" role="tablist" aria-label="Betriebsart">${Object.entries(modeLabels).map(([id,name])=>`<button role="tab" aria-selected="${selected===id}" data-control-mode="${id}" class="${selected===id?'chosen':'secondary'}" ${busy?'disabled':''}>${name}</button>`).join('')}</div>`);
|
||||||
|
put(root.querySelector('#endpoint-summary'),`<section class="card control-card" role="tabpanel"><div class="control-heading"><h2>${label}</h2><span class="pill">${e(state)}</span></div><p>${active==='switching'?`Wechsel zu ${e(modeLabels[v.target_mode]||v.target_mode)}`:s.reachable?`Aktiver Betrieb: ${e(modeLabels[active]||active)}`:'Betrieb gestoppt'}</p><div class="card-actions"><button data-control-start ${busy||!available||ready?'disabled':''}>${label} starten</button><button data-control-stop class="secondary" ${busy||!current||(s.state==='stopped'&&active==='llm')?'disabled':''}>Stoppen</button></div>${switching||loading||pending||s.state==='stopping'?`<div class="video-activity" role="status" aria-live="polite"><strong>${e(switching?v.switch_phase:loading?s.worker.phase||'Modell wird geladen':s.state==='stopping'?'Laufende Aufträge werden beendet':'Start wird angefordert')}</strong><progress aria-label="Fortschritt"></progress></div>`:''}${reason?`<p class="warning">${e(reason)} <a class="link" href="#${selected==='video'?'video':selected==='music'?'music':'chat'}">Einrichten →</a></p>`:''}${(current&&(v.error||s.error||selected==='llm'&&s.worker.error))?`<p class="video-error" role="alert">${e(v.error||s.error||s.worker.error)}</p>`:''}<div class="control-api"><h3>API-Adresse</h3>${routes.map(([name,url])=>`<div><span>${name}</span><code>${e(url)}</code></div>`).join('')}<p class="small">${ready?'Erreichbar':'Erreichbar, sobald dieser Betrieb bereit ist'} · Port ${s.port}</p></div>${selected==='llm'?`<p class="small">${s.counts.llm.available} Chatprofile freigegeben · ${e(s.worker.profile_name||'Gewichte laden bei der ersten Anfrage')}</p>`:selected==='music'?`<p class="small">${e(v.music_profiles?.map(p=>p.name).join(', ')||'Kein Musikprofil freigegeben')} · Gewichte laden bei Anfrage</p>`:`<p class="small">${e(v.service?.name||'Kein Videomodell ausgewählt')} · ${v.runtime_kind==='original'?'LTX Original':'ComfyUI'} · Gewichte laden bei Generierung</p>${ready?`<a class="link" href="${e(v.runtime_kind==='original'?location.protocol+'//'+location.hostname+':8118/':address+'/')}" target="_blank" rel="noopener">${v.runtime_kind==='original'?'LTX DeskWEB':'ComfyUI'} öffnen →</a>`:''}`}<details class="control-details"><summary>Weitere Dienste & Einstellungen</summary><p>Im LLM-Betrieb stehen auch freigegebene Bild-, TTS- und STT-Profile bereit.</p><p><a class="link" href="#endpoint">Endpunkt und Port einstellen →</a></p><p>${s.scheduler.active_requests} laufende · ${s.scheduler.waiting_requests} wartende Aufträge</p></details></section>`);
|
||||||
|
root.querySelectorAll('[data-control-mode]').forEach(b=>b.onclick=()=>{selectedMode=b.dataset.controlMode;control(s,root);});
|
||||||
|
root.querySelector('[data-control-start]').onclick=()=>controlAction('start',selected);
|
||||||
|
root.querySelector('[data-control-stop]').onclick=()=>controlAction('stop',selected);
|
||||||
|
}
|
||||||
|
async function switchMode(mode){
|
||||||
|
const r=await fetch('/api/v1/video/mode',{method:'POST',headers:{'Content-Type':'application/json','X-Athena-Deck':'1'},body:JSON.stringify({mode})});
|
||||||
|
const result=await r.json();if(!r.ok)throw Error(result.error||'Wechsel fehlgeschlagen');return result;
|
||||||
|
}
|
||||||
|
async function controlAction(action,mode){
|
||||||
|
if(pending)return;pending=true;const root=document.querySelector('#endpoint-page');
|
||||||
|
root.querySelectorAll('button').forEach(b=>b.disabled=true);
|
||||||
|
root.querySelector('#endpoint-message').innerHTML=`<div class="video-activity" role="status"><strong>${action==='start'?e(modeLabels[mode])+' wird gestartet':'Betrieb wird gestoppt'}</strong><progress aria-label="Fortschritt"></progress></div>`;
|
||||||
|
try{
|
||||||
|
const s=await api();
|
||||||
|
if(action==='start'){
|
||||||
|
if((s.video?.mode||'llm')!==mode||(mode==='video'&&!s.video?.service?.ready))await switchMode(mode);
|
||||||
|
if(s.state==='stopped')await api('/start',{});
|
||||||
|
}else{
|
||||||
|
await api('/stop',{});
|
||||||
|
if(s.video&&s.video.mode!=='llm')await switchMode('llm');
|
||||||
|
}
|
||||||
|
if(root.isConnected)root.querySelector('#endpoint-message').textContent='';
|
||||||
|
}catch(error){if(root.isConnected)root.querySelector('#endpoint-message').textContent=error.message;}
|
||||||
|
finally{pending=false;if(root.isConnected){root.querySelector('#control-modes')._deckHtml=null;root.querySelector('#endpoint-summary')._deckHtml=null;render(false);}}
|
||||||
|
}
|
||||||
async function render(settings=false){
|
async function render(settings=false){
|
||||||
const view=document.querySelector('#view');let root=view.querySelector('#endpoint-page');
|
const view=document.querySelector('#view');let root=view.querySelector('#endpoint-page');
|
||||||
if(!root||root.dataset.settings!==String(settings)){
|
if(!root||root.dataset.settings!==String(settings)){
|
||||||
sequence++;view.innerHTML=`<section id="endpoint-page" data-settings="${settings}"><div class="kicker">${settings?'EINSTELLUNGEN / API':'ATHENA DECK / ROUTER'}</div><h1>${settings?'Endpunkt':'Steuerung'}</h1><p>Eigener Router für Deck-Profile. Bestehende Athena-Dienste bleiben unabhängig.</p><div class="card-actions"><button id="endpoint-start">Endpunkt starten</button><button class="secondary" id="endpoint-stop">Endpunkt stoppen</button></div><p id="endpoint-message" role="status"></p><div id="video-mode-panel"></div><div id="endpoint-summary"></div>${settings?'<section class="card"><h2>Lokaler API-Port</h2><form id="endpoint-port-form"><label>Port<input id="endpoint-port" name="port" type="number" required min="1024" max="65535"></label><button id="endpoint-port-save">Port speichern</button></form><p id="endpoint-port-help"></p><p>Der Zugriff verwendet den API-Token aus <a class="link" href="#access">Zugang & API-Token</a>. Verwaltungsoberfläche und API haben getrennte Ports.</p></section><section class="card"><h2>Am Endpunkt angebotene Profile</h2><p>Nur explizit aktivierte, ausführbare Profile erscheinen in <code>GET /v1/models</code>. Aktivieren lädt noch kein Modell.</p><div id="endpoint-profiles"></div></section><section class="card"><h2>API-Routen</h2><p><code>POST /v1/chat/completions</code> · Text und Streaming<br><code>POST /v1/images/generations</code> · Qwen-Image-Rezept, ein Bild als b64_json<br><code>POST /v1/audio/speech</code> und <code>POST /v1/audio/transcriptions</code> · noch nicht eingerichtet (501)</p><p>Alle Routen nutzen denselben Port und Bearer-Token. Bei Bildaufträgen bestimmt das Profil Auflösung, Schritte, Guidance und Seed. Noch keine Bildbearbeitung oder Vision-Eingaben.</p></section>':''}</section>`;
|
sequence++;clearTimeout(controlTimer);selectedMode=null;view.innerHTML=`<section id="endpoint-page" data-settings="${settings}"><div class="kicker">${settings?'EINSTELLUNGEN / API':'ATHENA DECK / ROUTER'}</div><h1>${settings?'Endpunkt':'Steuerung'}</h1>${settings?'<p>API-Port und freigegebene Profile verwalten.</p><div class="card-actions"><button id="endpoint-start">Endpunkt starten</button><button class="secondary" id="endpoint-stop">Endpunkt stoppen</button></div>':'<p>Betriebsart wählen und starten.</p><div id="control-modes"></div>'}<p id="endpoint-message" role="status"></p><div id="video-mode-panel"></div><div id="endpoint-summary"></div>${settings?'<section class="card"><h2>Lokaler API-Port</h2><form id="endpoint-port-form"><label>Port<input id="endpoint-port" name="port" type="number" required min="1024" max="65535"></label><button id="endpoint-port-save">Port speichern</button></form><p id="endpoint-port-help"></p><p>Der Zugriff verwendet den API-Token aus <a class="link" href="#access">Zugang & API-Token</a>. Verwaltungsoberfläche und API haben getrennte Ports.</p></section><section class="card"><h2>Am Endpunkt angebotene Profile</h2><p>Nur explizit aktivierte, ausführbare Profile erscheinen in <code>GET /v1/models</code>. Aktivieren lädt noch kein Modell.</p><div id="endpoint-profiles"></div></section><section class="card"><h2>API-Routen</h2><p><code>POST /v1/chat/completions</code> · Text und Streaming<br><code>POST /v1/images/generations</code> · Qwen-Image-Rezept, ein Bild als b64_json<br><code>POST /v1/audio/speech</code> und <code>POST /v1/audio/transcriptions</code> · noch nicht eingerichtet (501)</p><p>Alle Routen nutzen denselben Port und Bearer-Token. Bei Bildaufträgen bestimmt das Profil Auflösung, Schritte, Guidance und Seed. Noch keine Bildbearbeitung oder Vision-Eingaben.</p></section>':''}</section>`;
|
||||||
root=view.querySelector('#endpoint-page');
|
root=view.querySelector('#endpoint-page');
|
||||||
for(const action of ['start','stop'])root.querySelector('#endpoint-'+action).onclick=()=>mutate('/'+action,{});
|
if(settings)for(const action of ['start','stop'])root.querySelector('#endpoint-'+action).onclick=()=>mutate('/'+action,{});
|
||||||
root.querySelector('#endpoint-port-form')?.addEventListener('submit',event=>{event.preventDefault();mutate('/config',{port:Number(root.querySelector('#endpoint-port').value)});});
|
root.querySelector('#endpoint-port-form')?.addEventListener('submit',event=>{event.preventDefault();mutate('/config',{port:Number(root.querySelector('#endpoint-port').value)});});
|
||||||
}
|
}
|
||||||
if(pending)return;const id=sequence;
|
if(pending)return;const id=sequence;
|
||||||
try{const s=await api();if(!root.isConnected||id!==sequence)return;
|
try{const s=await api();if(!root.isConnected||id!==sequence)return;
|
||||||
|
if(!settings){control(s,root);clearTimeout(controlTimer);if(s.video?.state==='switching'||s.worker.state==='loading'||s.state==='stopping')controlTimer=setTimeout(()=>{if(root.isConnected)render(false);},1000);return;}
|
||||||
put(root.querySelector('#endpoint-summary'),summary(s));
|
put(root.querySelector('#endpoint-summary'),summary(s));
|
||||||
if(s.video)VideoUI.mode(root.querySelector('#video-mode-panel'),s.video,s.profiles,s.port);
|
|
||||||
root.querySelector('#endpoint-start').disabled=s.state!=='stopped';root.querySelector('#endpoint-stop').disabled=s.state!=='running';
|
root.querySelector('#endpoint-start').disabled=s.state!=='stopped';root.querySelector('#endpoint-stop').disabled=s.state!=='running';
|
||||||
if(settings){const port=root.querySelector('#endpoint-port');if(document.activeElement!==port)port.value=s.port;port.disabled=s.state!=='stopped';root.querySelector('#endpoint-port-save').disabled=s.state!=='stopped';
|
if(settings){const port=root.querySelector('#endpoint-port');if(document.activeElement!==port)port.value=s.port;port.disabled=s.state!=='stopped';root.querySelector('#endpoint-port-save').disabled=s.state!=='stopped';
|
||||||
root.querySelector('#endpoint-port-help').textContent=s.allowed_ports.length?'Docker-Testinstallation: veröffentlichte Loopback-Ports '+s.allowed_ports.join(', ')+'. Portwechsel nur bei gestopptem Endpunkt. Der SSH-Tunnel muss denselben Port weiterleiten.':'Nativer Dienst: freie Ports zwischen 1024 und 65535, nur bei gestopptem Endpunkt.';
|
root.querySelector('#endpoint-port-help').textContent=s.allowed_ports.length?'Docker-Testinstallation: veröffentlichte Loopback-Ports '+s.allowed_ports.join(', ')+'. Portwechsel nur bei gestopptem Endpunkt. Der SSH-Tunnel muss denselben Port weiterleiten.':'Nativer Dienst: freie Ports zwischen 1024 und 65535, nur bei gestopptem Endpunkt.';
|
||||||
|
|||||||
@@ -42,3 +42,6 @@ nav a.nav-sub{padding-left:28px;font-size:12px;color:var(--deck-tone-a4bda8, #a4
|
|||||||
/* Compact, expandable detail lists share one interaction across model categories. */
|
/* Compact, expandable detail lists share one interaction across model categories. */
|
||||||
.detail-list{display:grid;gap:8px;margin-top:20px}.detail-row{border:1px solid var(--deck-tone-303a37,#303a37);border-radius:9px;background:var(--deck-tone-181f21,#181f21);overflow:hidden}.detail-summary{display:grid;grid-template-columns:minmax(0,1.3fr) minmax(0,1.3fr) minmax(110px,.7fr) minmax(140px,1fr);gap:18px;align-items:center;padding:18px 18px 18px 38px;position:relative;list-style:none;line-height:1.5}.detail-summary::-webkit-details-marker{display:none}.detail-summary:before{content:'▸';position:absolute;left:16px;top:19px;color:var(--deck-tone-c5edaf,#c5edaf)}.detail-row[open]>.detail-summary:before{content:'▾'}.detail-summary:hover{background:var(--deck-tone-20292a,#20292a)}.detail-summary:focus-visible{outline:2px solid var(--deck-tone-c5edaf,#c5edaf);outline-offset:-3px}.detail-summary strong{font-size:14px;overflow-wrap:anywhere}.detail-summary span{font-size:12px;color:var(--deck-tone-97a79e,#97a79e);overflow-wrap:anywhere}.detail-summary small{display:block;margin-top:4px}.detail-body{padding:8px 22px 22px;border-top:1px solid var(--deck-tone-303a37,#303a37)}.detail-body>.card-actions{margin-top:16px}.detail-body .execution-setup{margin-top:12px;padding-top:12px}.detail-row .endpoint-choice{margin:0 8px 8px 0;font-size:12px}#studio[data-page=runtimes] .detail-summary{grid-template-columns:1fr 1fr 1fr}.download-row{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:8px 20px;padding:18px;margin-top:8px;border-radius:9px}.download-row .card-top,.download-row progress,.download-row .warning{grid-column:1/-1}.download-row h3,.download-row p{margin:0}.download-row h3{font-size:14px;overflow-wrap:anywhere}.download-row .hub-filename{grid-column:1/-1}.download-row button{margin:0;font-size:12px}.download-row .card-top{display:flex;justify-content:space-between}.download-row .card-top strong{font-size:14px}.nav-heading{margin:16px 12px 4px}.eyebrow{margin-top:24px}aside{padding-top:24px}nav a{padding:9px 12px}nav .nav-heading:first-child{margin-top:0}.studio-tabs{flex-wrap:wrap}
|
.detail-list{display:grid;gap:8px;margin-top:20px}.detail-row{border:1px solid var(--deck-tone-303a37,#303a37);border-radius:9px;background:var(--deck-tone-181f21,#181f21);overflow:hidden}.detail-summary{display:grid;grid-template-columns:minmax(0,1.3fr) minmax(0,1.3fr) minmax(110px,.7fr) minmax(140px,1fr);gap:18px;align-items:center;padding:18px 18px 18px 38px;position:relative;list-style:none;line-height:1.5}.detail-summary::-webkit-details-marker{display:none}.detail-summary:before{content:'▸';position:absolute;left:16px;top:19px;color:var(--deck-tone-c5edaf,#c5edaf)}.detail-row[open]>.detail-summary:before{content:'▾'}.detail-summary:hover{background:var(--deck-tone-20292a,#20292a)}.detail-summary:focus-visible{outline:2px solid var(--deck-tone-c5edaf,#c5edaf);outline-offset:-3px}.detail-summary strong{font-size:14px;overflow-wrap:anywhere}.detail-summary span{font-size:12px;color:var(--deck-tone-97a79e,#97a79e);overflow-wrap:anywhere}.detail-summary small{display:block;margin-top:4px}.detail-body{padding:8px 22px 22px;border-top:1px solid var(--deck-tone-303a37,#303a37)}.detail-body>.card-actions{margin-top:16px}.detail-body .execution-setup{margin-top:12px;padding-top:12px}.detail-row .endpoint-choice{margin:0 8px 8px 0;font-size:12px}#studio[data-page=runtimes] .detail-summary{grid-template-columns:1fr 1fr 1fr}.download-row{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:8px 20px;padding:18px;margin-top:8px;border-radius:9px}.download-row .card-top,.download-row progress,.download-row .warning{grid-column:1/-1}.download-row h3,.download-row p{margin:0}.download-row h3{font-size:14px;overflow-wrap:anywhere}.download-row .hub-filename{grid-column:1/-1}.download-row button{margin:0;font-size:12px}.download-row .card-top{display:flex;justify-content:space-between}.download-row .card-top strong{font-size:14px}.nav-heading{margin:16px 12px 4px}.eyebrow{margin-top:24px}aside{padding-top:24px}nav a{padding:9px 12px}nav .nav-heading:first-child{margin-top:0}.studio-tabs{flex-wrap:wrap}
|
||||||
@media(max-width:1000px){.detail-summary{grid-template-columns:minmax(0,1fr) minmax(0,1fr);gap:8px 18px}}@media(max-width:600px){.detail-summary,#studio[data-page=runtimes] .detail-summary{grid-template-columns:1fr;gap:5px}.detail-body{padding:8px 16px 16px}.download-row{grid-template-columns:1fr}.download-row>*{grid-column:1!important}nav{grid-template-columns:1fr 1fr}nav .nav-heading{grid-column:1/-1}.card-actions{gap:6px}}
|
@media(max-width:1000px){.detail-summary{grid-template-columns:minmax(0,1fr) minmax(0,1fr);gap:8px 18px}}@media(max-width:600px){.detail-summary,#studio[data-page=runtimes] .detail-summary{grid-template-columns:1fr;gap:5px}.detail-body{padding:8px 16px 16px}.download-row{grid-template-columns:1fr}.download-row>*{grid-column:1!important}nav{grid-template-columns:1fr 1fr}nav .nav-heading{grid-column:1/-1}.card-actions{gap:6px}}
|
||||||
|
|
||||||
|
/* Control is a focused mode chooser; detailed API configuration lives in settings. */
|
||||||
|
.control-modes{display:flex;gap:12px;margin:24px 0 20px}.control-modes button{min-width:110px;margin:0;font-size:15px}.control-card{max-width:900px}.control-heading{display:flex;align-items:center;justify-content:space-between;gap:16px}.control-heading h2{margin:0;font-size:25px}.control-card>.card-actions{margin-top:18px}.control-api{border-top:1px solid var(--deck-tone-303a37,#303a37);margin-top:24px;padding-top:8px}.control-api h3{font-size:14px}.control-api>div{display:grid;grid-template-columns:130px minmax(0,1fr);gap:14px;margin:12px 0;font-size:13px}.control-api span{color:var(--deck-tone-97a79e,#97a79e)}.control-api code{overflow-wrap:anywhere}.control-details{border-top:1px solid var(--deck-tone-303a37,#303a37);padding-top:16px;margin-top:20px}.control-card .small{font-size:12px}@media(max-width:600px){.control-modes{gap:8px}.control-modes button{min-width:0;flex:1;padding:12px}.control-api>div{grid-template-columns:1fr;gap:4px}}
|
||||||
|
|||||||
Reference in New Issue
Block a user