diff --git a/diagnostic/index.html b/diagnostic/index.html index c050e73..d117148 100644 --- a/diagnostic/index.html +++ b/diagnostic/index.html @@ -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 = ['']; + 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(``); } let [c, st] = sl.model ? (SLOT_STATE[sl.state] || ['#8888AA', escapeHtml(sl.state || '?')]) : ['#555570', 'leer']; @@ -9512,13 +9513,12 @@ ${sl.ramMB ? ' · ' + (sl.ramMB / 1024).toFixed(1).replace('.', ',') + ' GB im RAM' : ''}${sl.cpuMoe > 0 ? ' (' + sl.cpuMoe + ' Schichten)' : ''}` : ''} ${sl.onCpu ? '
⚠ Modell läuft auf der CPU, Karte leer — Treiber zu alt? (bootstrap.sh --upgrade-driver)
' : ''} -
- +
+ onchange="onLlmCardSelect(this)" style="display:none;">${opts.join('')} +
${ctxCell} ${jobTxt || st} @@ -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 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 = '' + + '
'; + 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) => `
` + + (o.value === sel.value ? '✓ ' : '') + escapeHtml(o.textContent) + '
').join('') + + (items.length > 1 || !inp.value ? '' : '
Kein Modell passt zur Suche.
'); + 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 ────────────────────────────────────────