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
|
||||
// Katalog). Zu gross fuer die Karte → markiert, aber waehlbar (Groesse ist
|
||||
// nur eine Schaetzung, der Kontext braucht zusaetzlich Platz).
|
||||
const _llmCardSearch = {}; // Suchtext je Karte (box|gpu), ueberlebt das Neuzeichnen
|
||||
function renderLlmCardSettings() {
|
||||
const box = document.getElementById('llm-card-settings');
|
||||
if (!box) return;
|
||||
@@ -9460,6 +9459,7 @@
|
||||
const vramGB = sl.vramMB ? sl.vramMB / 1024 : 0;
|
||||
const ids = new Set(_llmCatalog.map(m => m.id));
|
||||
const opts = ['<option value="">— leer —</option>'];
|
||||
let curLabel = '— leer —';
|
||||
const list = _llmCatalog.filter(m => !m.unusable);
|
||||
if (sl.model && !ids.has(sl.model)) list.unshift({ id: sl.model });
|
||||
for (const m of list) {
|
||||
@@ -9469,6 +9469,7 @@
|
||||
const tooBig = vramGB && m.sizeGB && m.sizeGB > vramGB * 0.92;
|
||||
const label = m.id + (m.sizeGB ? ` · ~${m.sizeGB} GB` : '') + (m.quant ? ` · ${m.quant}` : '')
|
||||
+ (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>`);
|
||||
}
|
||||
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>
|
||||
</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>
|
||||
<div style="display:flex;flex-direction:column;gap:4px;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;">
|
||||
<div style="min-width:0;">
|
||||
<select data-box="${escapeHtml(b.instanceId)}" data-gpu="${escapeHtml(sl.gpu)}" data-current="${escapeHtml(sl.model || '')}"
|
||||
onchange="onLlmCardSelect(this)" ${job ? 'disabled' : ''}
|
||||
style="background:#1E1E2E;border:1px solid #333;border-radius:4px;padding:5px 8px;color:#E0E0F0;font-size:12px;">${opts.join('')}</select>
|
||||
onchange="onLlmCardSelect(this)" style="display:none;">${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>
|
||||
${ctxCell}
|
||||
<span style="color:${job ? '#FFD60A' : c};font-size:12px;">${jobTxt || st}</span>
|
||||
@@ -9526,15 +9526,72 @@
|
||||
}
|
||||
}
|
||||
box.innerHTML = rows.join('');
|
||||
box.querySelectorAll('input[data-key]').forEach(onLlmCardSearch);
|
||||
}
|
||||
// Suchfeld ueber dem Karten-Dropdown: blendet nicht passende Modelle aus.
|
||||
// "— leer —" und das geladene Modell bleiben immer sichtbar.
|
||||
function onLlmCardSearch(inp) {
|
||||
_llmCardSearch[inp.dataset.key] = inp.value;
|
||||
const sel = inp.parentElement.querySelector('select');
|
||||
if (!sel) return;
|
||||
for (const o of sel.options) o.hidden = !!o.value && o.value !== sel.dataset.current && !_llmMatches(o.textContent, inp.value);
|
||||
// Modell-Auswahl mit Suchfeld oben in der Liste (ein normales <select>
|
||||
// kann das nicht). Das versteckte <select> haelt weiter Wert + onchange.
|
||||
function openLlmPicker(btn) {
|
||||
closeLlmPicker();
|
||||
const sel = btn.parentElement.querySelector('select');
|
||||
const r = btn.getBoundingClientRect();
|
||||
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 ────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user