@@ -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 =>
+ '
' +
+ '
' + escapeHtml(g.cat) + '
' +
+ (g.note ? '
' + escapeHtml(g.note) + '
' : '') +
+ g.items.map(([name, url, desc]) => '
').join('') +
+ '
').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]) =>
+ '
' + v + ' ').join('');
+ }
+ async function loadBoardsTrending() {
+ const box = document.getElementById('boards-trending');
+ box.innerHTML = '
frage HuggingFace … ';
+ 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 = '
✗ ' + escapeHtml(j.error || 'Fehler') + ' '; return; }
+ renderBoardsTrending(j.items || []);
+ } catch (e) { box.innerHTML = '
✗ ' + escapeHtml(e.message) + ' '; }
+ }
+ function renderBoardsTrending(items) {
+ const box = document.getElementById('boards-trending');
+ if (!items.length) { box.innerHTML = '
Gerade nichts Ladbares im Trend' + (document.getElementById('boards-trusted').checked ? ' (Haken „nur bekannte Anbieter" raus = mehr)' : '') + '. '; 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 = '
' +
+ 'Modell Größe passt? Beliebtheit ' +
+ items.map(i => {
+ const needMB = i.sizeGB * 1024 + 1536;
+ const fit = !maxCardMB ? '? '
+ : needMB <= maxCardMB ? '✓ passt '
+ : '✗ zu groß ';
+ const act = i.inCatalog ? 'im Katalog ✓ '
+ : 'In Katalog ';
+ return '' + escapeHtml(i.repo) + ' ' +
+ '' + i.sizeGB + ' GB · ' + escapeHtml(i.quant) + ' ' +
+ '' + fit + ' ' +
+ '↓ ' + Number(i.downloads).toLocaleString('de') + ' · ♥ ' + i.likes + ' ' +
+ '' + act + ' ';
+ }).join('') + '
';
+ }
+ 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 = '
im Katalog ✓ — dort „Laden" ';
+ } 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
diff --git a/diagnostic/server.js b/diagnostic/server.js
index 889fc30..81bd39f 100644
--- a/diagnostic/server.js
+++ b/diagnostic/server.js
@@ -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()));