feat(diagnostic): Suche im Modell-Katalog und in den Karten-Dropdowns
- Modell-Katalog: Suchfeld (erscheint, sobald die Liste geladen ist), Teilsuche ueber Name, Beschreibung, Repo und Quantisierung; mehrere Woerter muessen alle vorkommen. - LLM-Karten: Suchfeld ueber jedem Modell-Dropdown blendet nicht passende Modelle aus; "— leer —" und das geladene Modell bleiben sichtbar. - Fix: Ein offenes Karten-Dropdown klappte nach ein paar Sekunden zu, weil jedes worker_update die Liste neu zeichnete. Solange ein Dropdown oder Suchfeld darin den Fokus hat, wird das Neuzeichnen aufgeschoben und beim Verlassen nachgeholt (gilt auch fuer die Ziel-Karte im Katalog). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+60
-5
@@ -956,6 +956,8 @@
|
||||
den Fortschritt. Danach ist es im Modell-Dropdown oben waehlbar.
|
||||
<span style="color:#FFD60A;">Achte auf den VRAM der Box (Spalte VRAM).</span>
|
||||
</p>
|
||||
<input id="llm-catalog-search" type="search" placeholder="🔍 Modell suchen … (z.B. coder, qwen3, 30b)" oninput="filterLlmCatalog()"
|
||||
style="display:none;width:100%;max-width:360px;box-sizing:border-box;margin:0 0 8px;background:#1E1E2E;border:1px solid #333;border-radius:4px;padding:6px 8px;color:#E0E0F0;font-size:12px;">
|
||||
<div id="llm-catalog-list" style="font-size:12px;color:#8888AA;overflow-x:auto;">(lade Katalog…)</div>
|
||||
<div id="llm-catalog-status" style="font-size:11px;color:#6a6a88;margin-top:6px;min-height:14px;"></div>
|
||||
</div>
|
||||
@@ -9254,10 +9256,41 @@
|
||||
// online LLM-Boxen (fuer die Ziel-Auswahl + Verfuegbarkeit)
|
||||
function onlineLlmBoxes() { return workers.filter(w => w.service === 'llm' && w.online); }
|
||||
function boxesServing(id) { return onlineLlmBoxes().filter(w => (w.models || []).includes(id) || w.model === id); }
|
||||
// Neu zeichnen schliesst ein offenes Dropdown und loescht Eingaben. Solange
|
||||
// darin etwas den Fokus hat, warten — beim Verlassen wird nachgeholt.
|
||||
function _llmRenderDeferred(box, fn) {
|
||||
const a = document.activeElement;
|
||||
if (!a || !box.contains(a) || !['SELECT', 'INPUT'].includes(a.tagName)) return false;
|
||||
if (!box._llmDeferred) {
|
||||
box._llmDeferred = true;
|
||||
box.addEventListener('focusout', () => setTimeout(() => {
|
||||
box._llmDeferred = false;
|
||||
if (_llmRenderDeferred(box, fn)) return; // Fokus nur innerhalb gewechselt → weiter warten
|
||||
fn();
|
||||
}, 0), { once: true });
|
||||
}
|
||||
return true;
|
||||
}
|
||||
// Teilsuche: jedes Wort muss irgendwo in Name/Beschreibung/Repo vorkommen.
|
||||
function _llmMatches(text, query) {
|
||||
const t = String(text || '').toLowerCase();
|
||||
return String(query || '').toLowerCase().split(/\s+/).filter(Boolean).every(w => t.includes(w));
|
||||
}
|
||||
function filterLlmCatalog() {
|
||||
const q = (document.getElementById('llm-catalog-search') || {}).value || '';
|
||||
const rows = document.querySelectorAll('#llm-catalog-list tbody tr[data-search]');
|
||||
let shown = 0;
|
||||
rows.forEach(tr => { const ok = _llmMatches(tr.dataset.search, q); tr.style.display = ok ? '' : 'none'; if (ok) shown++; });
|
||||
const none = document.getElementById('llm-catalog-none');
|
||||
if (none) none.style.display = rows.length && !shown ? '' : 'none';
|
||||
}
|
||||
function renderLlmCatalog() {
|
||||
const box = document.getElementById('llm-catalog-list');
|
||||
if (!box) return;
|
||||
renderLlmCardSettings();
|
||||
if (_llmRenderDeferred(box, renderLlmCatalog)) return;
|
||||
const search = document.getElementById('llm-catalog-search');
|
||||
if (search) search.style.display = _llmCatalog.length ? '' : 'none';
|
||||
if (!_llmCatalog.length) { box.innerHTML = '<span style="color:#6a6a88;">Katalog leer.</span>'; return; }
|
||||
const boxes = onlineLlmBoxes();
|
||||
const th = 'style="padding:6px 8px;text-align:left;position:sticky;top:0;background:#12122A;"';
|
||||
@@ -9277,7 +9310,8 @@
|
||||
const opts = targets.length
|
||||
? targets.map(t => `<option value="${escapeHtml(t.value)}"${t.value === pre ? ' selected' : ''}>${escapeHtml(t.label)}</option>`).join('')
|
||||
: '<option value="">(keine Box online)</option>';
|
||||
return '<tr style="border-bottom:1px solid #1E1E2E;">' +
|
||||
const searchText = [m.id, m.description, m.hfRepo, m.quant].filter(Boolean).join(' ');
|
||||
return '<tr data-search="' + escapeHtml(searchText) + '" style="border-bottom:1px solid #1E1E2E;">' +
|
||||
'<td ' + td + 'white-space:nowrap;"><b>' + escapeHtml(m.id) + '</b></td>' +
|
||||
'<td ' + td + 'color:#9a9ab5;min-width:220px;">' + escapeHtml(m.description || m.hfRepo || '')
|
||||
+ (m.quant ? ' <span style="color:#555570;" title="Quantisierung, die geladen wird' + (m.quants ? ' — vorhanden: ' + escapeHtml(m.quants.join(', ')) : '') + '">· ' + escapeHtml(m.quant) + '</span>' : '')
|
||||
@@ -9293,7 +9327,9 @@
|
||||
'<thead><tr style="color:#8888AA;border-bottom:1px solid #2A2A3E;">' +
|
||||
'<th ' + th + '>Modell</th><th ' + th + '>Kann gut / Beschreibung</th><th ' + th + '>VRAM</th>' +
|
||||
'<th ' + th + '>Status</th><th ' + th + '>Ziel-Karte</th><th ' + th + '></th>' +
|
||||
'</tr></thead><tbody>' + rows + '</tbody></table>';
|
||||
'</tr></thead><tbody>' + rows + '</tbody></table>' +
|
||||
'<div id="llm-catalog-none" style="display:none;color:#6a6a88;padding:10px 8px;">Kein Modell passt zur Suche.</div>';
|
||||
filterLlmCatalog();
|
||||
}
|
||||
// ── Download-Fenster ──────────────────────────────────────────────
|
||||
// Zustand kommt vom Diagnostic-Server (llm_provision_progress / llm_jobs);
|
||||
@@ -9401,6 +9437,7 @@
|
||||
// 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;
|
||||
@@ -9408,6 +9445,7 @@
|
||||
window._llmCatalogRequested = true;
|
||||
loadLlmCatalog(); // ruft renderLlmCatalog → renderLlmCardSettings
|
||||
}
|
||||
if (_llmRenderDeferred(box, renderLlmCardSettings)) return;
|
||||
const boxes = onlineLlmBoxes().filter(b => Array.isArray(b.slots));
|
||||
if (!boxes.length) {
|
||||
const old = onlineLlmBoxes().length;
|
||||
@@ -9474,15 +9512,29 @@
|
||||
<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>
|
||||
<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>
|
||||
<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;">
|
||||
<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>
|
||||
</div>
|
||||
${ctxCell}
|
||||
<span style="color:${job ? '#FFD60A' : c};font-size:12px;">${jobTxt || st}</span>
|
||||
</div>`);
|
||||
}
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
// ── Bestenlisten ────────────────────────────────────────
|
||||
@@ -9616,6 +9668,7 @@
|
||||
// die Karte auf den alten Stand zurueck.
|
||||
function onLlmCardCtx(sel) {
|
||||
const target = sel.dataset.box, gpu = sel.dataset.gpu, model = sel.dataset.model;
|
||||
sel.blur(); // Fokus weg → aufgeschobenes Neuzeichnen holt den Stand nach
|
||||
const cur = Number(sel.dataset.current || 0), next = Number(sel.value);
|
||||
if (next === cur) return;
|
||||
const m = _llmCatalog.find(x => x.id === model);
|
||||
@@ -9632,6 +9685,7 @@
|
||||
|
||||
function onLlmCardSelect(sel) {
|
||||
const target = sel.dataset.box, gpu = sel.dataset.gpu, current = sel.dataset.current, next = sel.value;
|
||||
sel.blur(); // Fokus weg → aufgeschobenes Neuzeichnen holt den Stand nach
|
||||
const node = _boxName(target);
|
||||
if (next === current) return;
|
||||
if (!next) {
|
||||
@@ -9656,6 +9710,7 @@
|
||||
// MoE: Experten zwischen Karte und Arbeitsspeicher — gleiches Modell neu laden
|
||||
function onLlmCardMoe(sel) {
|
||||
const target = sel.dataset.box, gpu = sel.dataset.gpu, model = sel.dataset.model;
|
||||
sel.blur(); // Fokus weg → aufgeschobenes Neuzeichnen holt den Stand nach
|
||||
const cur = Number(sel.dataset.current || 0), next = Number(sel.value);
|
||||
const m = _llmCatalog.find(x => x.id === model);
|
||||
if (!m || !m.hfRepo) { alert(`${model} steht nicht im Modell-Katalog.`); sel.value = String(cur); return; }
|
||||
|
||||
Reference in New Issue
Block a user