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:
@@ -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;">×</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
|
||||
|
||||
@@ -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()));
|
||||
|
||||
Reference in New Issue
Block a user