feat(diagnostic): ℹ im Modell-Katalog — was kann das Modell, ist es zensiert?

Neben jedem Katalog-Eintrag ℹ → Fenster mit:
- Kurzbeschreibung auf Deutsch, beim ersten Oeffnen vom LOKALEN Modell aus
  der Modellkarte (Grundmodell + GGUF-Repo) erzeugt und im Katalog
  gespeichert (summary_de) — so hat jedes neue Modell eine, ohne Pflege.
  Hinweis "kann Fehler enthalten". Laeuft kein Modell: Meldung.
- Feste deutsche Texte je Modellfamilie (Qwen3-Coder, Foundation-Sec,
  VulnLLM, Dolphin, Hermes, Lexi, Gemma, Qwen, Llama, Mistral, DeepSeek,
  Phi), erkannt als ganzes Wort in Name/Grundmodell/Schlagworten.
- "unzensiert" / "mit Schutzfiltern" (uncensored/abliterated/heretic/
  dolphin/lexi … in Name oder Schlagworten).
- Fakten von HF: Grundmodell, Autor, Lizenz, Beliebtheit, Stand,
  Schlagworte; Auszug der Modellkarte (Link-Absaetze uebersprungen).

Server: /api/llm-model-info, /api/llm-model-summary, rvsRequest() als
Promise-Variante fuer eigene RVS-Anfragen.

Getestet: Info-Teil gegen HuggingFace (4 Modelle). Die Zusammenfassung
durch das lokale Modell ungetestet (Karte gerade im Gegenlauf belegt).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-31 19:24:33 +01:00
co-authored by Claude Opus 5.5
parent 848eee5d1c
commit faa4868ee2
2 changed files with 212 additions and 1 deletions
+67 -1
View File
@@ -9822,7 +9822,9 @@
: '<option value="">(keine Box online)</option>';
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 + 'white-space:nowrap;"><b>' + escapeHtml(m.id) + '</b>'
+ (m.hfRepo ? ' <button class="btn secondary" onclick="openModelInfo(' + i + ')" title="Was kann das Modell? Zensiert oder nicht?" style="padding:0 6px;font-size:11px;margin-left:4px;">ℹ</button>' : '')
+ '</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>' : '')
+ (m.unusable ? ' <span style="color:#FF6B6B;" title="Im Repo liegt keine ladbare GGUF-Modelldatei mehr">· nicht ladbar</span>' : '')
@@ -10241,6 +10243,70 @@
: '');
} catch (e) { out.innerHTML = '<span style="color:#FF6B6B;">✗ ' + escapeHtml(e.message) + '</span>'; }
}
// ℹ: Kurzbeschreibung, zensiert/unzensiert, Fakten von HuggingFace
async function openModelInfo(i) {
const m = _llmCatalog[i];
if (!m || !m.hfRepo) return;
const box = _modelInfoModal('<div style="color:#8888AA;">Frage HuggingFace nach ' + escapeHtml(m.hfRepo) + ' …</div>');
let j;
try { j = await (await fetch('/api/llm-model-info?repo=' + encodeURIComponent(m.hfRepo))).json(); }
catch (e) { j = { ok: false, error: e.message }; }
if (!document.body.contains(box)) return;
if (!j.ok) { _modelInfoModal('<div style="color:#FF6B6B;">✗ ' + escapeHtml(j.error || 'Fehler') + '</div>'); return; }
const fam = (j.families || []).map(f => `<div style="margin-bottom:8px;"><b>${escapeHtml(f.name)}</b><div style="color:#C8C8DA;margin-top:2px;">${escapeHtml(f.text)}</div></div>`).join('')
|| '<div style="color:#8888AA;">Für diese Modellfamilie habe ich keine Kurzbeschreibung — siehe Modellkarte unten.</div>';
const cens = j.uncensored
? `<span style="background:#3A2410;color:#FFB020;border:1px solid #FFB020;border-radius:10px;padding:1px 8px;">unzensiert</span> <span style="color:#8888AA;font-size:11px;">${escapeHtml(j.uncensoredWhy)} — lehnt kaum etwas ab; du bist selbst verantwortlich, was du damit machst.</span>`
: `<span style="background:#103A20;color:#34C759;border:1px solid #34C759;border-radius:10px;padding:1px 8px;">mit Schutzfiltern</span> <span style="color:#8888AA;font-size:11px;">Standard des Herstellers — lehnt heikle Anfragen ab (keine Hinweise auf „uncensored“ o.ä.).</span>`;
_loadModelSummary(m.hfRepo);
const row = (k, v) => v ? `<tr><td style="color:#8888AA;padding:2px 10px 2px 0;white-space:nowrap;">${k}</td><td>${v}</td></tr>` : '';
const date = x => x ? new Date(x).toLocaleDateString('de-DE') : '';
_modelInfoModal(`
<h3 style="margin:0 0 4px;">ℹ ${escapeHtml(m.id)}</h3>
<a href="https://huggingface.co/${escapeHtml(m.hfRepo)}" target="_blank" rel="noopener noreferrer" style="color:#4DB8FF;font-size:12px;">${escapeHtml(m.hfRepo)}</a>
<div style="margin:12px 0;">${cens}</div>
<h4 style="margin:10px 0 6px;">Was kann es?</h4>
<div id="model-info-summary" style="margin-bottom:10px;color:#8888AA;">Kurzbeschreibung wird vom lokalen Modell aus der Modellkarte erstellt …</div>
${fam}
<table style="font-size:12px;margin-top:8px;">
${row('Größe', m.sizeGB ? '~' + m.sizeGB + ' GB · ' + escapeHtml(m.quant || '') : '')}
${row('Grundmodell', j.base ? escapeHtml(j.base) : '')}
${row('Von', escapeHtml(j.author))}
${row('Lizenz', escapeHtml(j.license))}
${row('Beliebtheit', '↓ ' + Number(j.downloads).toLocaleString('de') + ' · ♥ ' + j.likes)}
${row('Stand', date(j.updated))}
${row('Schlagworte', (j.tags || []).map(t => '<span style="color:#9a9ab5;">' + escapeHtml(t) + '</span>').join(', '))}
</table>
${j.intro ? `<details style="margin-top:10px;"><summary style="cursor:pointer;color:#8888AA;">Aus der Modellkarte (${escapeHtml(j.introFrom)}, englisch)</summary><div style="white-space:pre-wrap;color:#C8C8DA;font-size:12px;margin-top:6px;">${escapeHtml(j.intro)}</div></details>` : ''}`);
}
// Deutsche Kurzbeschreibung: einmal vom lokalen Modell erzeugt, danach aus dem Katalog
async function _loadModelSummary(repo) {
let j;
try { j = await (await fetch('/api/llm-model-summary?repo=' + encodeURIComponent(repo))).json(); }
catch (e) { j = { ok: false, error: e.message }; }
const el = document.getElementById('model-info-summary');
if (!el) return;
if (j.ok) {
el.style.color = '#E0E0F0';
el.innerHTML = escapeHtml(j.summary) + `<div style="color:#555570;font-size:11px;margin-top:4px;">Zusammengefasst aus der Modellkarte von ${escapeHtml(j.by || 'dem lokalen Modell')} — kann Fehler enthalten.</div>`;
} else {
el.innerHTML = '<span style="color:#8888AA;">Keine Kurzbeschreibung: ' + escapeHtml(j.error || 'unbekannt') + ' (läuft ein lokales Modell?)</span>';
}
}
function _modelInfoModal(html) {
let m = document.getElementById('model-info-modal');
if (!m) {
m = document.createElement('div');
m.id = 'model-info-modal';
m.style.cssText = 'position:fixed;inset:0;z-index:9000;background:rgba(0,0,0,.6);display:flex;align-items:center;justify-content:center;padding:16px;';
m.addEventListener('click', e => { if (e.target === m) m.remove(); });
document.addEventListener('keydown', function esc(e) { if (e.key === 'Escape') { m.remove(); document.removeEventListener('keydown', esc); } });
document.body.appendChild(m);
}
m.innerHTML = `<div style="position:relative;background:#12121E;border:1px solid #444466;border-radius:8px;padding:16px 18px;max-width:640px;width:100%;max-height:86vh;overflow-y:auto;color:#E0E0F0;font-size:13px;line-height:1.5;">
<button onclick="document.getElementById('model-info-modal').remove()" style="position:absolute;top:8px;right:10px;background:none;border:none;color:#8888AA;font-size:22px;cursor:pointer;">&times;</button>${html}</div>`;
return m;
}
async function addTrendingToCatalog(btn, repo) {
btn.disabled = true; btn.textContent = '…';
try {