feat(diagnostic): Bestenlisten — Ranglisten + HF-Trends direkt in den Katalog

Modell-Katalog → "🏆 Bestenlisten": Fenster mit laufend gepflegten
Ranglisten nach Kategorie (Allgemein, Programmieren, Bilder erzeugen,
Lokal/Community — Arena, Artificial Analysis, LiveBench, Aider, SWE-bench,
Vellum, r/LocalLLaMA, HF), darunter "Gerade im Trend auf HuggingFace" pro
Kategorie (Allgemein / Programmieren / Bilder verstehen): nur Repos mit
ladbarem GGUF, echte Groesse + Quantisierung, "passt / zu gross" gegen die
groesste LLM-Karte, standardmaessig nur bekannte Anbieter (Trend != gut).
"In Katalog" uebernimmt das Repo; geladen wird wie gewohnt im Katalog.

Server: GET /api/llm-discover (HF trendingScore, 30 min Cache),
POST /api/llm-catalog/add.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-25 14:56:37 +01:00
co-authored by Claude Opus 5.5
parent 12f3cd3e55
commit c1b06aa26a
2 changed files with 186 additions and 0 deletions
+102
View File
@@ -197,6 +197,26 @@
</div>
</div>
<!-- Bestenlisten: Ranglisten-Links + HF-Trends (direkt in den Katalog) -->
<div id="boards-modal" style="display:none;position:fixed;inset:0;z-index:1002;background:rgba(0,0,0,0.8);align-items:center;justify-content:center;" onclick="if(event.target===this)this.style.display='none'">
<div style="background:#0D0D1A;border:1px solid #2A2A3E;border-radius:10px;padding:16px 20px;max-width:1000px;width:94%;max-height:92vh;overflow:auto;color:#C8C8D8;font-size:13px;">
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:6px;">
<h3 style="margin:0;color:#fff;">🏆 Bestenlisten</h3>
<button onclick="document.getElementById('boards-modal').style.display='none'" style="background:none;border:none;color:#8888AA;font-size:22px;cursor:pointer;">&times;</button>
</div>
<div style="font-size:12px;color:#8888AA;margin-bottom:10px;">Laufend gepflegte Ranglisten (öffnen in neuem Tab). Achte auf <b>offene Modelle</b> („open weights") — nur die laufen lokal. Eine einzige Seite für alle Kategorien gibt es nicht.</div>
<div id="boards-links" style="display:grid;grid-template-columns:repeat(auto-fill,minmax(290px,1fr));gap:10px;"></div>
<div style="display:flex;align-items:center;gap:10px;flex-wrap:wrap;margin:18px 0 6px;">
<h3 style="margin:0;color:#fff;font-size:15px;">🔥 Gerade im Trend auf HuggingFace — direkt ladbar</h3>
<span id="boards-tabs" style="display:flex;gap:6px;"></span>
<label style="font-size:12px;color:#8888AA;display:flex;gap:4px;align-items:center;"><input type="checkbox" id="boards-trusted" checked onchange="loadBoardsTrending()"> nur bekannte Anbieter</label>
</div>
<div style="font-size:11px;color:#8888AA;margin-bottom:6px;">„Im Trend" heißt viel Aufmerksamkeit, nicht automatisch gut — im Zweifel oben in den Ranglisten gegenprüfen. Größe = Datei in der angegebenen Quantisierung; die Karte braucht zusätzlich ~1,5 GB plus Kontext.</div>
<div id="boards-trending" style="font-size:12px;">…</div>
</div>
</div>
<!-- Erklaerung Kontextfenster (i-Button bei den LLM-Karten) -->
<div id="ctx-info-modal" style="display:none;position:fixed;inset:0;z-index:1002;background:rgba(0,0,0,0.8);align-items:center;justify-content:center;" onclick="if(event.target===this)this.style.display='none'">
<div style="background:#0D0D1A;border:1px solid #2A2A3E;border-radius:10px;padding:18px 20px;max-width:620px;width:94%;max-height:90vh;overflow:auto;color:#C8C8D8;font-size:13px;line-height:1.55;">
@@ -902,6 +922,7 @@
<h2 style="margin:0;">Modell-Katalog</h2>
<button class="btn secondary" onclick="refreshLlmCatalog()" style="padding:4px 10px;font-size:11px;">Von HuggingFace aktualisieren</button>
<button class="btn secondary" id="llm-dl-btn" onclick="openLlmDownloads()" title="Fortschritt der Modell-Downloads auf die LLM-Boxen" style="padding:4px 10px;font-size:11px;">⬇ Downloads <span id="llm-dl-badge" style="display:none;margin-left:4px;padding:0 6px;border-radius:8px;background:#FFD60A;color:#000;font-weight:700;"></span></button>
<button class="btn secondary" onclick="openModelBoards()" title="Ranglisten der besten Modelle + was gerade auf HuggingFace im Trend ist (direkt ladbar)" style="padding:4px 10px;font-size:11px;">🏆 Bestenlisten</button>
</div>
<div class="card" style="max-width:1100px;">
<p style="color:#8888AA;font-size:12px;margin:0 0 8px;">
@@ -9425,6 +9446,87 @@
box.innerHTML = rows.join('');
}
// ── Bestenlisten ────────────────────────────────────────
const MODEL_BOARDS = [
{ cat: '💬 Allgemein / Chat', items: [
['Arena (LMArena) — Text', 'https://arena.ai/leaderboard', 'Menschen vergleichen Antworten blind. Filter „Open" für lokale Modelle.'],
['Artificial Analysis — offene Modelle', 'https://artificialanalysis.ai/models/open-source', 'Intelligenz-Index, Tempo, Größe der offenen Modelle.'],
['Vellum — Open LLM Leaderboard', 'https://www.vellum.ai/open-llm-leaderboard', 'Nur offene Modelle, nach Benchmarks.'] ] },
{ cat: '💻 Programmieren', items: [
['LiveBench', 'https://livebench.ai/', 'Laufend neue Aufgaben (u.a. Coding, agentisches Coding) — schwer zu „lernen".'],
['Aider — Polyglot-Bestenliste', 'https://aider.chat/docs/leaderboards/', 'Code in vielen Sprachen bearbeiten — nah an echter Arbeit.'],
['SWE-bench', 'https://www.swebench.com/', 'Echte GitHub-Issues lösen (agentisch).'],
['Arena — Code / WebDev', 'https://arena.ai/leaderboard/webdev', 'Blindvergleich beim Bauen von Web-Apps.'] ] },
{ cat: '🖼️ Bilder erzeugen', note: 'Laufen bei ARIA über FLUX, nicht in einer LLM-Karte.', items: [
['Arena — Text-to-Image', 'https://arena.ai/leaderboard/text-to-image', 'Blindvergleich von Bildgeneratoren.'],
['Artificial Analysis — Bild-Arena', 'https://artificialanalysis.ai/text-to-image/arena', 'Rangliste inkl. offener Modelle.'] ] },
{ cat: '🏠 Lokal / Community', items: [
['r/LocalLLaMA — Top der Woche', 'https://www.reddit.com/r/LocalLLaMA/top/?t=week', 'Erfahrungen mit lokalen Modellen, neue Releases zuerst.'],
['HuggingFace — GGUF im Trend', 'https://huggingface.co/models?library=gguf&sort=trending', 'Alle ladbaren GGUF-Modelle, nach Trend.'] ] },
];
let _boardsCat = 'chat';
function openModelBoards() {
document.getElementById('boards-modal').style.display = 'flex';
document.getElementById('boards-links').innerHTML = MODEL_BOARDS.map(g =>
'<div style="border:1px solid #1E1E2E;border-radius:8px;padding:10px 12px;background:#12122A;">' +
'<div style="color:#E0E0F0;font-weight:700;margin-bottom:4px;">' + escapeHtml(g.cat) + '</div>' +
(g.note ? '<div style="color:#FFB020;font-size:11px;margin-bottom:4px;">' + escapeHtml(g.note) + '</div>' : '') +
g.items.map(([name, url, desc]) => '<div style="margin:5px 0;"><a href="' + escapeHtml(url) + '" target="_blank" rel="noopener noreferrer" style="color:#4DB8FF;">' +
escapeHtml(name) + ' ↗</a><div style="color:#8888AA;font-size:11px;">' + escapeHtml(desc) + '</div></div>').join('') +
'</div>').join('');
renderBoardsTabs();
loadBoardsTrending();
}
function renderBoardsTabs() {
const tabs = { chat: 'Allgemein', code: 'Programmieren', vision: 'Bilder verstehen' };
document.getElementById('boards-tabs').innerHTML = Object.entries(tabs).map(([k, v]) =>
'<button class="btn ' + (k === _boardsCat ? '' : 'secondary') + '" onclick="_boardsCat=\'' + k + '\';renderBoardsTabs();loadBoardsTrending()" style="padding:3px 10px;font-size:11px;">' + v + '</button>').join('');
}
async function loadBoardsTrending() {
const box = document.getElementById('boards-trending');
box.innerHTML = '<span style="color:#8888AA;">frage HuggingFace …</span>';
const trusted = document.getElementById('boards-trusted').checked ? 1 : 0;
try {
const j = await (await fetch('/api/llm-discover?cat=' + _boardsCat + '&trusted=' + trusted)).json();
if (!j.ok) { box.innerHTML = '<span style="color:#FF6B6B;">✗ ' + escapeHtml(j.error || 'Fehler') + '</span>'; return; }
renderBoardsTrending(j.items || []);
} catch (e) { box.innerHTML = '<span style="color:#FF6B6B;">✗ ' + escapeHtml(e.message) + '</span>'; }
}
function renderBoardsTrending(items) {
const box = document.getElementById('boards-trending');
if (!items.length) { box.innerHTML = '<span style="color:#8888AA;">Gerade nichts Ladbares im Trend' + (document.getElementById('boards-trusted').checked ? ' (Haken „nur bekannte Anbieter" raus = mehr)' : '') + '.</span>'; return; }
// Groesste LLM-Karte im Cluster: passt das Modell (Datei + ~1,5 GB) drauf?
const maxCardMB = Math.max(0, ...workers.filter(w => w.service === 'llm' && w.online)
.flatMap(w => (w.slots || []).map(sl => sl.vramMB || 0)));
const td = 'padding:6px 8px;border-top:1px solid #1E1E2E;vertical-align:top;';
box.innerHTML = '<div style="overflow-x:auto;"><table style="width:100%;border-collapse:collapse;">' +
'<tr style="color:#8888AA;text-align:left;"><th style="padding:4px 8px;">Modell</th><th style="padding:4px 8px;">Größe</th><th style="padding:4px 8px;">passt?</th><th style="padding:4px 8px;">Beliebtheit</th><th></th></tr>' +
items.map(i => {
const needMB = i.sizeGB * 1024 + 1536;
const fit = !maxCardMB ? '<span style="color:#8888AA;">?</span>'
: needMB <= maxCardMB ? '<span style="color:#3FFF9F;">✓ passt</span>'
: '<span style="color:#FF6B6B;" title="Braucht ca. ' + (needMB / 1024).toFixed(1) + ' GB, größte LLM-Karte hat ' + (maxCardMB / 1024).toFixed(0) + ' GB">✗ zu groß</span>';
const act = i.inCatalog ? '<span style="color:#3FFF9F;">im Katalog ✓</span>'
: '<button class="btn secondary" onclick="addTrendingToCatalog(this, \'' + escapeHtml(i.repo) + '\')" style="padding:3px 10px;font-size:11px;">In Katalog</button>';
return '<tr><td style="' + td + '"><a href="https://huggingface.co/' + escapeHtml(i.repo) + '" target="_blank" rel="noopener noreferrer" style="color:#E0E0F0;">' + escapeHtml(i.repo) + '</a></td>' +
'<td style="' + td + 'white-space:nowrap;">' + i.sizeGB + ' GB <span style="color:#555570;">· ' + escapeHtml(i.quant) + '</span></td>' +
'<td style="' + td + 'white-space:nowrap;">' + fit + '</td>' +
'<td style="' + td + 'color:#8888AA;white-space:nowrap;">↓ ' + Number(i.downloads).toLocaleString('de') + ' · ♥ ' + i.likes + '</td>' +
'<td style="' + td + 'text-align:right;white-space:nowrap;">' + act + '</td></tr>';
}).join('') + '</table></div>';
}
async function addTrendingToCatalog(btn, repo) {
btn.disabled = true; btn.textContent = '…';
try {
const j = await (await fetch('/api/llm-catalog/add', { method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ hfRepo: repo }) })).json();
if (!j.ok) { btn.disabled = false; btn.textContent = 'In Katalog'; alert('Nicht übernommen: ' + (j.error || 'Fehler')); return; }
_llmCatalog = j.models || _llmCatalog;
renderLlmCatalog();
btn.outerHTML = '<span style="color:#3FFF9F;" title="Im Modell-Katalog Ziel-Karte wählen und „Laden"">im Katalog ✓ — dort „Laden"</span>';
} catch (e) { btn.disabled = false; btn.textContent = 'In Katalog'; alert(e.message); }
}
function openCtxInfo() { document.getElementById('ctx-info-modal').style.display = 'flex'; }
// Kontextfenster einer Karte aendern = dasselbe Modell mit anderem -c neu
+84
View File
@@ -554,6 +554,61 @@ async function refreshLlmCatalogFromHF() {
return { models: merged, added };
}
// ── Gerade im Trend: ladbare GGUF-Modelle von HuggingFace ───────────
// Fuer das Fenster "Bestenlisten" im Modell-Katalog. "Trend" ist nicht
// "das Beste" — viele Community-Mischungen. Darum standardmaessig nur
// bekannte Anbieter. Ergebnis 30 min gecacht (HF nicht dauernd fragen).
const TRUSTED_GGUF_AUTHORS = new Set(["unsloth", "bartowski", "lmstudio-community", "ggml-org", "qwen",
"google", "mistralai", "microsoft", "meta-llama", "deepseek-ai", "ibm-granite", "nvidia", "openai",
"allenai", "huggingfacetb", "thudm", "zai-org", "moonshotai", "minimaxai", "tencent", "baidu"]);
const DISCOVER_CATS = {
chat: { label: "Allgemein / Chat", q: "pipeline_tag=text-generation" },
code: { label: "Programmieren", q: "search=coder" },
vision: { label: "Bilder verstehen", q: "pipeline_tag=image-text-to-text" },
};
const _discoverCache = new Map(); // key → {ts, items}
async function discoverGguf(cat, trusted) {
const c = DISCOVER_CATS[cat] || DISCOVER_CATS.chat;
const key = `${cat}|${trusted ? 1 : 0}`;
const hit = _discoverCache.get(key);
if (hit && Date.now() - hit.ts < 30 * 60 * 1000) return hit.items;
const r = await fetch(`https://huggingface.co/api/models?library=gguf&sort=trendingScore&direction=-1&limit=100&${c.q}`,
{ headers: { "User-Agent": "aria-diagnostic" } });
if (!r.ok) throw new Error(`HF API ${r.status}`);
let list = (await r.json()).filter(m => m && m.id);
if (trusted) list = list.filter(m => TRUSTED_GGUF_AUTHORS.has(String(m.id).split("/")[0].toLowerCase()));
list = list.slice(0, 18);
const items = [];
for (let i = 0; i < list.length; i += 6) {
await Promise.all(list.slice(i, i + 6).map(async m => {
const q = await hfRepoQuant(m.id, "Q4_K_M");
if (!q) return; // kein ladbares GGUF im Repo
items.push({ repo: m.id, author: String(m.id).split("/")[0], downloads: m.downloads || 0,
likes: m.likes || 0, trending: m.trendingScore || 0,
lastModified: m.lastModified || m.createdAt || "", quant: q.quant, sizeGB: q.sizeGB });
}));
}
items.sort((a, b) => b.trending - a.trending);
_discoverCache.set(key, { ts: Date.now(), items });
return items;
}
// Ein HF-Repo in den Katalog uebernehmen (danach dort wie gewohnt "Laden")
async function addHfRepoToCatalog(repo) {
if (!/^[\w.-]+\/[\w.-]+$/.test(String(repo || ""))) throw new Error("ungueltiges Repo");
const q = await hfRepoQuant(repo, "Q4_K_M");
if (!q) throw new Error("kein ladbares GGUF-Modell in diesem Repo");
const cat = loadLlmCatalog();
let id = slugModelId(repo);
const same = cat.find(m => m.hfRepo === repo);
if (same) return { models: cat, id: same.id, existed: true };
while (cat.some(m => m.id === id)) id += "-2";
cat.push({ id, hfRepo: repo, quant: q.quant, ctx: 8192, sizeGB: q.sizeGB, quants: q.quants,
description: "Aus den HuggingFace-Trends übernommen", source: "hf" });
saveLlmCatalog(cat);
return { models: cat, id, existed: false };
}
// ── File-Project-Manifest ───────────────────────────────────────────
// Jeder Eintrag map[absoluter_pfad] = project_id (leer = Hauptchat).
// Wird vom files-list-Endpoint + files-set-project gepflegt.
@@ -2333,6 +2388,35 @@ const server = http.createServer((req, res) => {
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify({ ok: false, error: err.message, models: loadLlmCatalog() }));
});
} else if (req.url.startsWith("/api/llm-discover") && req.method === "GET") {
const u = new URL(req.url, "http://x");
discoverGguf(u.searchParams.get("cat") || "chat", u.searchParams.get("trusted") !== "0")
.then(items => {
const inCat = new Set(loadLlmCatalog().map(m => m.hfRepo));
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify({ ok: true, items: items.map(i => ({ ...i, inCatalog: inCat.has(i.repo) })) }));
})
.catch(err => {
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify({ ok: false, error: err.message, items: [] }));
});
} else if (req.url === "/api/llm-catalog/add" && req.method === "POST") {
let body = "";
req.on("data", c => { body += c; if (body.length > 10000) req.destroy(); });
req.on("end", () => {
let repo = "";
try { repo = JSON.parse(body || "{}").hfRepo; } catch {}
addHfRepoToCatalog(repo)
.then(r => {
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify({ ok: true, ...r }));
if (!r.existed) log("info", "llm", `Katalog: ${repo} aus den HF-Trends übernommen (${r.id})`);
})
.catch(err => {
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify({ ok: false, error: err.message }));
});
});
} else if (req.url === "/api/local-llm-config" && req.method === "GET") {
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify(readLocalLlmConfig()));