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:
+67
-1
@@ -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;">×</button>${html}</div>`;
|
||||
return m;
|
||||
}
|
||||
async function addTrendingToCatalog(btn, repo) {
|
||||
btn.disabled = true; btn.textContent = '…';
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user