feat(diagnostic): Karten-Modellwahl als Liste mit Suchfeld oben drin
Statt Suchfeld neben einem normalen Dropdown (Tippen → Liste oeffnen → passt nicht → zurueck ins Feld) oeffnet ein Klick jetzt eine Liste mit dem Suchfeld oben. Tippen filtert sofort, Auswahl per Klick oder Pfeiltasten + Enter, Esc / Klick daneben / Scrollen schliesst. Das versteckte <select> haelt weiter Wert und onchange (onLlmCardSelect unveraendert). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+72
-15
@@ -9437,7 +9437,6 @@
|
|||||||
// Optionen = Modell-Katalog + was gerade geladen ist (falls nicht im
|
// Optionen = Modell-Katalog + was gerade geladen ist (falls nicht im
|
||||||
// Katalog). Zu gross fuer die Karte → markiert, aber waehlbar (Groesse ist
|
// Katalog). Zu gross fuer die Karte → markiert, aber waehlbar (Groesse ist
|
||||||
// nur eine Schaetzung, der Kontext braucht zusaetzlich Platz).
|
// nur eine Schaetzung, der Kontext braucht zusaetzlich Platz).
|
||||||
const _llmCardSearch = {}; // Suchtext je Karte (box|gpu), ueberlebt das Neuzeichnen
|
|
||||||
function renderLlmCardSettings() {
|
function renderLlmCardSettings() {
|
||||||
const box = document.getElementById('llm-card-settings');
|
const box = document.getElementById('llm-card-settings');
|
||||||
if (!box) return;
|
if (!box) return;
|
||||||
@@ -9460,6 +9459,7 @@
|
|||||||
const vramGB = sl.vramMB ? sl.vramMB / 1024 : 0;
|
const vramGB = sl.vramMB ? sl.vramMB / 1024 : 0;
|
||||||
const ids = new Set(_llmCatalog.map(m => m.id));
|
const ids = new Set(_llmCatalog.map(m => m.id));
|
||||||
const opts = ['<option value="">— leer —</option>'];
|
const opts = ['<option value="">— leer —</option>'];
|
||||||
|
let curLabel = '— leer —';
|
||||||
const list = _llmCatalog.filter(m => !m.unusable);
|
const list = _llmCatalog.filter(m => !m.unusable);
|
||||||
if (sl.model && !ids.has(sl.model)) list.unshift({ id: sl.model });
|
if (sl.model && !ids.has(sl.model)) list.unshift({ id: sl.model });
|
||||||
for (const m of list) {
|
for (const m of list) {
|
||||||
@@ -9469,6 +9469,7 @@
|
|||||||
const tooBig = vramGB && m.sizeGB && m.sizeGB > vramGB * 0.92;
|
const tooBig = vramGB && m.sizeGB && m.sizeGB > vramGB * 0.92;
|
||||||
const label = m.id + (m.sizeGB ? ` · ~${m.sizeGB} GB` : '') + (m.quant ? ` · ${m.quant}` : '')
|
const label = m.id + (m.sizeGB ? ` · ~${m.sizeGB} GB` : '') + (m.quant ? ` · ${m.quant}` : '')
|
||||||
+ (tooBig ? ' · passt nicht' : '');
|
+ (tooBig ? ' · passt nicht' : '');
|
||||||
|
if (m.id === sl.model) curLabel = label;
|
||||||
opts.push(`<option value="${escapeHtml(m.id)}"${m.id === sl.model ? ' selected' : ''}${tooBig ? ' style="color:#FF9F0A;"' : ''}>${escapeHtml(label)}</option>`);
|
opts.push(`<option value="${escapeHtml(m.id)}"${m.id === sl.model ? ' selected' : ''}${tooBig ? ' style="color:#FF9F0A;"' : ''}>${escapeHtml(label)}</option>`);
|
||||||
}
|
}
|
||||||
let [c, st] = sl.model ? (SLOT_STATE[sl.state] || ['#8888AA', escapeHtml(sl.state || '?')]) : ['#555570', 'leer'];
|
let [c, st] = sl.model ? (SLOT_STATE[sl.state] || ['#8888AA', escapeHtml(sl.state || '?')]) : ['#555570', 'leer'];
|
||||||
@@ -9512,13 +9513,12 @@
|
|||||||
<option value="-1"${sl.cpuMoe < 0 ? ' selected' : ''}>alle im Arbeitsspeicher</option>
|
<option value="-1"${sl.cpuMoe < 0 ? ' selected' : ''}>alle im Arbeitsspeicher</option>
|
||||||
</select>${sl.ramMB ? ' · ' + (sl.ramMB / 1024).toFixed(1).replace('.', ',') + ' GB im RAM' : ''}${sl.cpuMoe > 0 ? ' (' + sl.cpuMoe + ' Schichten)' : ''}</div>` : ''}
|
</select>${sl.ramMB ? ' · ' + (sl.ramMB / 1024).toFixed(1).replace('.', ',') + ' GB im RAM' : ''}${sl.cpuMoe > 0 ? ' (' + sl.cpuMoe + ' Schichten)' : ''}</div>` : ''}
|
||||||
${sl.onCpu ? '<div style="color:#FF9F0A;font-size:11px;" title="llama.cpp findet die Karte nicht und rechnet langsam auf der CPU. Ursache meist: Treiber zu alt fuer das CUDA im llama-swap-Image. Abhilfe: auf der Box sudo ./bootstrap.sh --upgrade-driver">⚠ Modell läuft auf der CPU, Karte leer — Treiber zu alt? (bootstrap.sh --upgrade-driver)</div>' : ''}</div>
|
${sl.onCpu ? '<div style="color:#FF9F0A;font-size:11px;" title="llama.cpp findet die Karte nicht und rechnet langsam auf der CPU. Ursache meist: Treiber zu alt fuer das CUDA im llama-swap-Image. Abhilfe: auf der Box sudo ./bootstrap.sh --upgrade-driver">⚠ Modell läuft auf der CPU, Karte leer — Treiber zu alt? (bootstrap.sh --upgrade-driver)</div>' : ''}</div>
|
||||||
<div style="display:flex;flex-direction:column;gap:4px;min-width:0;">
|
<div style="min-width:0;">
|
||||||
<input type="search" placeholder="🔍 Modell suchen …" data-key="${escapeHtml(b.instanceId + '|' + sl.gpu)}"
|
|
||||||
value="${escapeHtml(_llmCardSearch[b.instanceId + '|' + sl.gpu] || '')}" oninput="onLlmCardSearch(this)" ${job ? 'disabled' : ''}
|
|
||||||
style="background:#1E1E2E;border:1px solid #333;border-radius:4px;padding:4px 8px;color:#E0E0F0;font-size:11px;">
|
|
||||||
<select data-box="${escapeHtml(b.instanceId)}" data-gpu="${escapeHtml(sl.gpu)}" data-current="${escapeHtml(sl.model || '')}"
|
<select data-box="${escapeHtml(b.instanceId)}" data-gpu="${escapeHtml(sl.gpu)}" data-current="${escapeHtml(sl.model || '')}"
|
||||||
onchange="onLlmCardSelect(this)" ${job ? 'disabled' : ''}
|
onchange="onLlmCardSelect(this)" style="display:none;">${opts.join('')}</select>
|
||||||
style="background:#1E1E2E;border:1px solid #333;border-radius:4px;padding:5px 8px;color:#E0E0F0;font-size:12px;">${opts.join('')}</select>
|
<button type="button" onclick="openLlmPicker(this)" ${job ? 'disabled' : ''} title="Modell wählen — mit Suche"
|
||||||
|
style="width:100%;display:flex;justify-content:space-between;gap:8px;text-align:left;background:#1E1E2E;border:1px solid #333;border-radius:4px;padding:5px 8px;color:#E0E0F0;font-size:12px;cursor:pointer;">
|
||||||
|
<span style="overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">${escapeHtml(curLabel)}</span><span style="color:#8888AA;">▾</span></button>
|
||||||
</div>
|
</div>
|
||||||
${ctxCell}
|
${ctxCell}
|
||||||
<span style="color:${job ? '#FFD60A' : c};font-size:12px;">${jobTxt || st}</span>
|
<span style="color:${job ? '#FFD60A' : c};font-size:12px;">${jobTxt || st}</span>
|
||||||
@@ -9526,15 +9526,72 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
box.innerHTML = rows.join('');
|
box.innerHTML = rows.join('');
|
||||||
box.querySelectorAll('input[data-key]').forEach(onLlmCardSearch);
|
|
||||||
}
|
}
|
||||||
// Suchfeld ueber dem Karten-Dropdown: blendet nicht passende Modelle aus.
|
// Modell-Auswahl mit Suchfeld oben in der Liste (ein normales <select>
|
||||||
// "— leer —" und das geladene Modell bleiben immer sichtbar.
|
// kann das nicht). Das versteckte <select> haelt weiter Wert + onchange.
|
||||||
function onLlmCardSearch(inp) {
|
function openLlmPicker(btn) {
|
||||||
_llmCardSearch[inp.dataset.key] = inp.value;
|
closeLlmPicker();
|
||||||
const sel = inp.parentElement.querySelector('select');
|
const sel = btn.parentElement.querySelector('select');
|
||||||
if (!sel) return;
|
const r = btn.getBoundingClientRect();
|
||||||
for (const o of sel.options) o.hidden = !!o.value && o.value !== sel.dataset.current && !_llmMatches(o.textContent, inp.value);
|
const pop = document.createElement('div');
|
||||||
|
pop.id = 'llm-picker';
|
||||||
|
const below = window.innerHeight - r.bottom > 260;
|
||||||
|
pop.style.cssText = `position:fixed;left:${r.left}px;${below ? 'top:' + (r.bottom + 2) : 'bottom:' + (window.innerHeight - r.top + 2)}px;`
|
||||||
|
+ `width:${Math.max(r.width, 320)}px;z-index:9999;background:#12121E;border:1px solid #444466;border-radius:6px;`
|
||||||
|
+ 'box-shadow:0 6px 20px rgba(0,0,0,.5);padding:6px;';
|
||||||
|
pop.innerHTML = '<input type="search" placeholder="🔍 Modell suchen … (z.B. coder 30b)" style="width:100%;box-sizing:border-box;'
|
||||||
|
+ 'background:#1E1E2E;border:1px solid #333;border-radius:4px;padding:5px 8px;color:#E0E0F0;font-size:12px;">'
|
||||||
|
+ '<div style="max-height:300px;overflow-y:auto;margin-top:6px;"></div>';
|
||||||
|
btn.parentElement.appendChild(pop);
|
||||||
|
const inp = pop.querySelector('input'), list = pop.querySelector('div');
|
||||||
|
let items = [], active = 0;
|
||||||
|
const draw = () => {
|
||||||
|
items = [...sel.options].filter(o => !o.value || _llmMatches(o.textContent, inp.value));
|
||||||
|
active = Math.max(0, Math.min(active, items.length - 1));
|
||||||
|
list.innerHTML = items.map((o, i) => `<div data-i="${i}" style="padding:5px 8px;border-radius:4px;cursor:pointer;font-size:12px;`
|
||||||
|
+ `color:${o.style.color || '#E0E0F0'};${i === active ? 'background:#2A2A4A;' : ''}${o.value === sel.value ? 'font-weight:700;' : ''}">`
|
||||||
|
+ (o.value === sel.value ? '✓ ' : '') + escapeHtml(o.textContent) + '</div>').join('')
|
||||||
|
+ (items.length > 1 || !inp.value ? '' : '<div style="color:#6a6a88;padding:5px 8px;font-size:12px;">Kein Modell passt zur Suche.</div>');
|
||||||
|
const a = list.querySelector(`[data-i="${active}"]`);
|
||||||
|
if (a) a.scrollIntoView({ block: 'nearest' });
|
||||||
|
};
|
||||||
|
const pick = o => {
|
||||||
|
closeLlmPicker();
|
||||||
|
if (!o || o.value === sel.value) return;
|
||||||
|
sel.value = o.value;
|
||||||
|
onLlmCardSelect(sel);
|
||||||
|
};
|
||||||
|
list.addEventListener('mousedown', e => {
|
||||||
|
const d = e.target.closest('[data-i]');
|
||||||
|
if (d) { e.preventDefault(); pick(items[+d.dataset.i]); }
|
||||||
|
});
|
||||||
|
list.addEventListener('mousemove', e => {
|
||||||
|
const d = e.target.closest('[data-i]');
|
||||||
|
if (d && +d.dataset.i !== active) { active = +d.dataset.i; draw(); }
|
||||||
|
});
|
||||||
|
inp.addEventListener('input', () => { active = 0; draw(); });
|
||||||
|
inp.addEventListener('keydown', e => {
|
||||||
|
if (e.key === 'ArrowDown') { e.preventDefault(); active++; draw(); }
|
||||||
|
else if (e.key === 'ArrowUp') { e.preventDefault(); active--; draw(); }
|
||||||
|
else if (e.key === 'Enter') { e.preventDefault(); pick(items[active]); }
|
||||||
|
else if (e.key === 'Escape') { e.preventDefault(); closeLlmPicker(); }
|
||||||
|
});
|
||||||
|
active = Math.max(0, [...sel.options].findIndex(o => o.value === sel.value));
|
||||||
|
draw();
|
||||||
|
inp.focus();
|
||||||
|
setTimeout(() => document.addEventListener('mousedown', _llmPickerOutside), 0);
|
||||||
|
window.addEventListener('scroll', _llmPickerScroll, true);
|
||||||
|
}
|
||||||
|
function _llmPickerOutside(e) { const p = document.getElementById('llm-picker'); if (p && !p.contains(e.target)) closeLlmPicker(); }
|
||||||
|
function _llmPickerScroll(e) { const p = document.getElementById('llm-picker'); if (p && !p.contains(e.target)) closeLlmPicker(); }
|
||||||
|
function closeLlmPicker() {
|
||||||
|
document.removeEventListener('mousedown', _llmPickerOutside);
|
||||||
|
window.removeEventListener('scroll', _llmPickerScroll, true);
|
||||||
|
const p = document.getElementById('llm-picker');
|
||||||
|
if (!p) return;
|
||||||
|
const inp = p.querySelector('input');
|
||||||
|
if (inp) inp.blur(); // loest das aufgeschobene Neuzeichnen der Karten aus
|
||||||
|
p.remove();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Bestenlisten ────────────────────────────────────────
|
// ── Bestenlisten ────────────────────────────────────────
|
||||||
|
|||||||
Reference in New Issue
Block a user