// cookbook.js — Cookbook 2.0 (v4): Use-Case-Setups („Wofür?") + Profi-Suche. // Backend: /api/cookbook/{recipes,install-recipe,analyze,evaluate} (hw_math). import { api, getHfToken } from "../core/api.js"; import { $, esc, icon, toast, infoDot, confirmModal } from "../core/ui.js"; const CTX_HELP = "Kontext = das Kurzzeitgedächtnis des Modells. Größer = merkt sich mehr, braucht aber mehr Speicher und wird etwas langsamer."; const GGUF_HELP = "GGUF ist das lokal lauffähige Dateiformat. Unsere Engine lädt nur GGUF — Repos ohne GGUF-Datei kann sie nicht nutzen."; const MOE_HELP = "MoE (Mixture of Experts): viele Parameter fürs Wissen, aber pro Wort ist nur ein kleiner Teil aktiv → fast große-Modell-Qualität bei hohem Tempo. Ideal für deine APU."; // Was kann das Modell? (Fähigkeit aus dem Namen / der Backend-Rolle ableiten.) const ROLE_LABEL = { vision: "Bilder", coder: "Coden", reasoning: "Logik", agent: "Agenten & Tools", scout: "Allrounder", reviewer: "Review", manager: "Manager" }; const CAP_KW = [ { label: "Bilder", kw: ["-vl-", "-vl", "vision", "llava", "multimodal", "-mm-", "pixtral"] }, { label: "Coden", kw: ["coder", "-code-", "code-", "codestral", "starcoder"] }, { label: "Logik", kw: ["-r1", "deepseek-r1", "reasoning", "qwq", "magistral", "-think", "thinking", "-o1"] }, { label: "Agenten & Tools", kw: ["hermes", "-tool", "command-r", "watt", "-fc-", "function"] }, ]; function capLabel(name) { const l = (name || "").toLowerCase(); for (const c of CAP_KW) if (c.kw.some(k => l.includes(k))) return c.label; return "Allrounder"; } // MoE-Erkennung über strukturelle Marker im Namen (zuverlässig, keine Falschaussagen): // 8x7B-Stil, „…-A3B" (aktive Parameter), oder explizit „mixtral"/„moe". function isMoe(s = "") { const l = s.toLowerCase(); return /\d+x\d+(\.\d+)?b/.test(l) || /(?MoE${a ? ` · ~${a}B aktiv` : ""}`; } function capChip(label) { return label ? `${esc(label)}` : ""; } const FILTERS = [ { id: "", label: "Alle" }, { id: "coder", label: "Coder" }, { id: "scout", label: "Scout" }, { id: "vision", label: "Vision" }, { id: "manager", label: "Manager" }, { id: "reviewer", label: "Reviewer" }, ]; let lastSys = null; let currentResults = []; let currentAnalysis = null; let activeFilter = ""; let RECIPES = []; let RECOMMENDED = null; let cardFits = []; // Fit-Level je Such-Karte (fuer „Beste Wahl"-Markierung) const FIT_RANK = { perfect: 0, marginal: 1, too_tight: 2 }; const fitCls = lvl => lvl === "perfect" ? "ok" : lvl === "marginal" ? "warn" : "bad"; const fitWord = lvl => lvl === "perfect" ? "Passt locker" : lvl === "marginal" ? "Wird knapp" : "Zu groß"; function mount() { const c = $(".view[data-view='cookbook']"); c.innerHTML = `

Modell-Cookbook

Wofür willst du es nutzen? Wir empfehlen das passende Setup für deine Hardware.

Wofür möchtest du es nutzen?

deine Hardware
Lade Setups…

Aktuell beste Modelle für dein System

Automatisch aus vertrauenswürdigen Quellen (HuggingFace) — laufend aktuell, gefiltert auf das, was auf deine Hardware passt.
Lade aktuelle Empfehlungen…
Profi-Modus: HuggingFace direkt durchsuchen
passt locker läuft, aber knapp zu groß für deinen Speicher
Es werden nur Modelle im GGUF-Format ${infoDot(GGUF_HELP)} gezeigt — nur die laufen lokal.
`; renderFilters(); $("#cb-btn-search").addEventListener("click", doSearch); $("#cb-search").addEventListener("keydown", e => { if (e.key === "Enter") doSearch(); }); $("#cb-sort").addEventListener("change", () => { if ($("#cb-search").value.trim() || activeFilter) doSearch(); }); $("#cb-modal-close").addEventListener("click", () => $("#cb-modal").style.display = "none"); $("#cb-modal").addEventListener("click", e => { if (e.target.id === "cb-modal") $("#cb-modal").style.display = "none"; }); $("#cb-m-download").addEventListener("click", doDownload); $("#cb-m-files").addEventListener("change", updateLiveFit); $("#cb-m-ctx").addEventListener("change", reanalyzeCtx); window.cbSetCtx = v => { $("#cb-m-ctx").value = v; reanalyzeCtx(); }; $("#cb-r-close").addEventListener("click", () => $("#cb-recipe-modal").style.display = "none"); $("#cb-recipe-modal").addEventListener("click", e => { if (e.target.id === "cb-recipe-modal") $("#cb-recipe-modal").style.display = "none"; }); $("#cb-new-open").addEventListener("click", openNewRecipe); $("#cb-new-close").addEventListener("click", () => $("#cb-new-modal").style.display = "none"); $("#cb-new-modal").addEventListener("click", e => { if (e.target.id === "cb-new-modal") $("#cb-new-modal").style.display = "none"; }); $("#cb-new-add").addEventListener("click", () => $("#cb-new-models").insertAdjacentHTML("beforeend", newModelRow())); $("#cb-new-models").addEventListener("click", e => { const d = e.target.closest(".cb-nm-del"); if (d) d.closest(".cb-nm-row").remove(); }); $("#cb-new-save").addEventListener("click", saveNewRecipe); $("#cb-disc-refresh").addEventListener("click", () => loadDiscover(true)); $("#cb-discover").addEventListener("click", e => { const b = e.target.closest("[data-inst]"); if (!b) return; installDiscovered(b.getAttribute("data-inst"), b.getAttribute("data-role"), b.getAttribute("data-pb"), b); }); renderHwChip(); loadRecipes(); loadDiscover(); } // ---- Automatische Modell-Entdeckung („aktuell beste Modelle") ---- function fmtAgo(ts) { const s = Date.now() / 1000 - ts; if (s < 90) return "gerade eben"; if (s < 3600) return `vor ${Math.round(s / 60)} min`; if (s < 86400) return `vor ${Math.round(s / 3600)} h`; return `vor ${Math.round(s / 86400)} Tg.`; } async function loadDiscover(force = false) { const box = $("#cb-discover"), when = $("#cb-disc-when"), btn = $("#cb-disc-refresh"); if (force) { btn.disabled = true; btn.textContent = "Suche…"; box.innerHTML = `
Frage Quellen ab…
`; } try { const d = await api("/api/cookbook/discover" + (force ? "?force=true" : "")); if (when) when.textContent = d.updated ? `aktualisiert ${fmtAgo(d.updated)}` : ""; renderDiscover(d); } catch (e) { box.innerHTML = `
Empfehlungen nicht ladbar: ${esc(e.message)}
`; } btn.disabled = false; btn.textContent = "Aktualisieren"; } function renderDiscover(d) { const box = $("#cb-discover"); const cats = d.categories || []; if (!cats.length) { box.innerHTML = `
Keine passenden Modelle gefunden.
`; return; } box.innerHTML = cats.map(c => `
${icon(c.icon)}

${esc(c.title)}

${c.models.map(m => discCard(m, c.recommended)).join("")}
`).join(""); } function discCard(m, recommended) { const reqs = (m.requirements || []).map(r => `
⚠ ${esc(r)}
`).join(""); const best = recommended && m.repo === recommended; const tag = best ? `
★ Beste Wahl für dein System
` : ""; const chips = `
${capChip(ROLE_LABEL[m.role] || capLabel(m.repo))}${moeChip(m.repo)}
`; const action = m.installed ? `✓ installiert` : ``; return `
${tag}

${esc(m.name)}

${esc(m.author)} · ${m.params_b}B
${esc(m.fit.text)}
${chips}
${metricLine(m.fit)} · ⬇ ${(m.downloads || 0).toLocaleString()}
${reqs}
${action}
`; } async function installDiscovered(repo, role, params_b, btn) { btn.disabled = true; btn.textContent = "Starte…"; try { await api("/api/cookbook/install-model", { method: "POST", body: JSON.stringify({ repo, role, params_b: parseFloat(params_b) || 7, quant: "Q4_K_M", hf_token: getHfToken() }) }); toast("Download gestartet — siehe Aktivität."); document.querySelector(".nav-item[data-view='activity']")?.click(); } catch (e) { toast("Fehler: " + e.message, true); btn.disabled = false; btn.textContent = "Installieren"; } } // ---- Use-Case-Setups ---- async function loadRecipes() { try { const d = await api("/api/cookbook/recipes"); RECIPES = d.recipes || []; RECOMMENDED = d.recommended_id || null; renderRecipes(); } catch (e) { $("#cb-recipes").innerHTML = `
Setups nicht ladbar: ${esc(e.message)}
`; } } function renderRecipes() { $("#cb-recipes").innerHTML = RECIPES.map(r => { const best = r.id === RECOMMENDED; const tag = best ? `
★ Beste Wahl für dein System
` : r.user ? `
Dein Setup
` : ""; const del = r.user ? ` ×` : ""; return ``; }).join(""); $("#cb-recipes").querySelectorAll("[data-recipe]").forEach(b => b.addEventListener("click", () => openRecipe(b.getAttribute("data-recipe")))); $("#cb-recipes").querySelectorAll("[data-delr]").forEach(x => x.addEventListener("click", e => { e.stopPropagation(); deleteRecipe(x.getAttribute("data-delr")); })); $("#cb-recipes").querySelectorAll("[data-editr]").forEach(x => x.addEventListener("click", e => { e.stopPropagation(); openEditRecipe(x.getAttribute("data-editr")); })); } // ---- Eigenes Setup erstellen/löschen ---- function newModelRow() { return `
`; } let editRecipeId = null; function openNewRecipe() { editRecipeId = null; $("#cb-new-title").value = ""; $("#cb-new-desc").value = ""; $("#cb-new-models").innerHTML = newModelRow(); $("#cb-new-modal-title").textContent = "Eigenes Setup erstellen"; $("#cb-new-save").textContent = "Setup speichern"; $("#cb-new-modal").style.display = "flex"; } function openEditRecipe(id) { const r = RECIPES.find(x => x.id === id); if (!r) return; editRecipeId = id; $("#cb-new-title").value = r.title; $("#cb-new-desc").value = r.desc || ""; $("#cb-new-models").innerHTML = r.models.map(m => { const row = document.createElement("div"); row.innerHTML = newModelRow(); const el = row.firstElementChild; el.querySelector(".cb-nm-repo").value = m.repo; el.querySelector(".cb-nm-role").value = m.role; const quant = el.querySelector(".cb-nm-quant"); if (quant) [...quant.options].forEach(o => { if (o.value === m.quant) o.selected = true; }); return el.outerHTML; }).join(""); $("#cb-new-modal-title").textContent = "Setup bearbeiten"; $("#cb-new-save").textContent = "Änderungen speichern"; $("#cb-new-modal").style.display = "flex"; } async function saveNewRecipe() { const title = $("#cb-new-title").value.trim(); if (!title) return toast("Bitte einen Titel angeben.", true); const models = [...$("#cb-new-models").querySelectorAll(".cb-nm-row")].map(row => ({ repo: row.querySelector(".cb-nm-repo").value.trim(), role: row.querySelector(".cb-nm-role").value.trim() || "modell", quant: row.querySelector(".cb-nm-quant").value, })).filter(m => m.repo); if (!models.length) return toast("Mindestens ein Modell (Repo) angeben.", true); const btn = $("#cb-new-save"); btn.disabled = true; btn.textContent = "Speichere…"; try { const body = JSON.stringify({ title, desc: $("#cb-new-desc").value.trim(), models }); if (editRecipeId) { await api("/api/cookbook/user-recipe/" + encodeURIComponent(editRecipeId), { method: "PUT", body }); toast("Setup aktualisiert."); } else { await api("/api/cookbook/user-recipe", { method: "POST", body }); toast("Setup gespeichert."); } editRecipeId = null; $("#cb-new-modal").style.display = "none"; loadRecipes(); } catch (e) { toast("Fehler: " + e.message, true); } btn.disabled = false; btn.textContent = editRecipeId ? "Änderungen speichern" : "Setup speichern"; } async function deleteRecipe(id) { const r = RECIPES.find(x => x.id === id); if (!await confirmModal({ title: `„${r ? r.title : id}" löschen?`, body: "Dein eigenes Setup wird aus dem Cookbook entfernt. Bereits installierte Modelle bleiben unangetastet.", confirmLabel: "Löschen", danger: true })) return; try { await api("/api/cookbook/user-recipe/" + encodeURIComponent(id), { method: "DELETE" }); toast("Setup gelöscht."); loadRecipes(); } catch (e) { toast(e.message, true); } } function openRecipe(id) { const r = RECIPES.find(x => x.id === id); if (!r) return; $("#cb-r-title").textContent = r.title; $("#cb-r-desc").textContent = r.desc; $("#cb-r-models").innerHTML = `
${r.models.map(m => `
${esc(m.name)}${esc(m.role)}${moeChip(m.repo)}
${esc(m.why)}
~${m.fit.req_gb.toFixed(1)} GB · ~${Math.round(m.fit.tps)} Tok/s · optimal ~${Math.round(m.optimal_ctx / 1024)}k Kontext
${esc(m.fit.text)}
`).join("")}
`; const maxRam = Math.max(...r.models.map(m => m.fit.req_gb)); $("#cb-r-note").innerHTML = `Größter Spitzenbedarf: ~${maxRam.toFixed(1)} GB. ` + `Es läuft immer nur ein Modell gleichzeitig — das größte bestimmt, ob das Setup passt (kein Summieren).`; const btn = $("#cb-r-install"); btn.className = r.fit_level === "too_tight" ? "primary warn" : "primary"; btn.textContent = r.fit_level === "too_tight" ? "Trotzdem installieren (zu groß)" : "Komplettes Setup installieren"; btn.onclick = () => installRecipe(r.id); $("#cb-recipe-modal").style.display = "flex"; } async function installRecipe(id) { const btn = $("#cb-r-install"); btn.disabled = true; btn.textContent = "Starte…"; try { const r = await api("/api/cookbook/install-recipe", { method: "POST", body: JSON.stringify({ recipe_id: id, hf_token: getHfToken() }) }); toast(`Setup wird installiert (${r.count} Modelle) — siehe Aktivität.`); $("#cb-recipe-modal").style.display = "none"; document.querySelector(".nav-item[data-view='activity']")?.click(); } catch (e) { toast("Fehler: " + e.message, true); } btn.disabled = false; btn.textContent = "Komplettes Setup installieren"; } // ---- Hardware-Chip / Filter ---- function renderFilters() { $("#cb-filters").innerHTML = FILTERS.map(f => ``).join(""); $("#cb-filters").querySelectorAll("[data-f]").forEach(b => b.addEventListener("click", () => { activeFilter = b.getAttribute("data-f"); renderFilters(); doSearch(); })); } function renderHwChip() { const el = $("#cb-hw"); if (!el) return; if (lastSys?.ram?.total) el.innerHTML = `${icon("cpu")}${Math.round(lastSys.ram.total / 1024 ** 3)} GB Speicher`; } // ---- Profi-Suche (HuggingFace) ---- function metricLine(fit) { return `~${fit.req_gb.toFixed(1)} GB · ~${Math.round(fit.tps)} Tok/s`; } // Erkennt eine direkte Modell-Angabe: ganze HuggingFace-URL ODER „owner/repo". // So kann man Links wie https://huggingface.co/unsloth/MiniMax-M3-GGUF einfach // reinkopieren und landet exakt auf diesem Modell (statt einer unscharfen Suche). function parseHfRepo(s) { s = s.trim(); const u = s.match(/huggingface\.co\/([^\s/]+\/[^\s/?#]+)/i); if (u) return u[1]; if (/^[\w.-]+\/[\w.-]+$/.test(s)) return s; return null; } async function doSearch() { const raw = $("#cb-search").value.trim(); const repo = parseHfRepo(raw); let q = raw; if (activeFilter && !repo) q = q ? q + " " + activeFilter : activeFilter; if (!q && !repo) { $("#cb-grid").innerHTML = ""; return; } const btn = $("#cb-btn-search"); btn.disabled = true; btn.textContent = "Lade…"; $("#cb-grid").innerHTML = `
Suche auf HuggingFace…
`; try { if (repo) { // Direkter Treffer per Link/Repo — keine Stichwortsuche, sondern genau dieses Modell. currentResults = [{ id: repo, author: repo.split("/")[0], downloads: 0 }]; renderResults(currentResults); } else { const sort = $("#cb-sort")?.value || "downloads"; // limit=40: auch große/seltenere Modelle (z.B. Llama-4-Scout 109B) erscheinen, // statt von den Top-12-Trends verdrängt zu werden. Fit-Ampel sagt ehrlich, was passt. const url = `https://huggingface.co/api/models?search=${encodeURIComponent(q)}&filter=gguf&sort=${encodeURIComponent(sort)}&direction=-1&limit=40`; const r = await fetch(url); currentResults = await r.json(); renderResults(currentResults); } } catch (e) { $("#cb-grid").innerHTML = `
${esc(e.message)}
`; } btn.disabled = false; btn.textContent = "Suchen"; } function renderResults(results) { const grid = $("#cb-grid"); cardFits = []; if (!results?.length) { grid.innerHTML = `
Keine GGUF-Modelle gefunden.
`; return; } grid.innerHTML = results.map((m, i) => `

${esc(m.id.split("/").pop())}

${esc(m.author || "")}
prüfe…
${capChip(capLabel(m.id))}${moeChip(m.id)}
Hardware-Fit… ⬇ ${(m.downloads || 0).toLocaleString()}
`).join(""); grid.querySelectorAll("[data-res]").forEach(el => el.addEventListener("click", () => openResult(+el.getAttribute("data-res")))); results.forEach((m, i) => fetchFitForCard(i, m.id)); } async function fetchFitForCard(i, repo_id) { try { const res = await api("/api/cookbook/analyze", { method: "POST", body: JSON.stringify({ repo_id, ctx: 8192 }) }); const b = $("#cb-b-" + i), mt = $("#cb-m-" + i); if (!b || !mt) return; if (!res.files?.length) { b.className = "fit-badge"; b.style.cssText = "background:rgba(139,151,165,.14);color:var(--mut)"; b.title = GGUF_HELP; b.textContent = "kein GGUF"; mt.textContent = "—"; cardFits[i] = null; markBestResult(); return; } let best = res.files.find(f => f.quant?.includes("Q4_K_M")) || res.files[0]; b.className = "fit-badge " + fitCls(best.fit.level); b.textContent = best.fit.text; mt.textContent = metricLine(best.fit) + " · " + (best.quant || "GGUF"); cardFits[i] = best.fit.level; markBestResult(); } catch { const b = $("#cb-b-" + i); if (b) { b.className = "fit-badge bad"; b.textContent = "Fehler"; } cardFits[i] = null; markBestResult(); } } // Markiert die beste System-Wahl in den Suchergebnissen: das meistgeladene Modell // (Liste ist nach Downloads sortiert), das noch sauber auf die Hardware passt. function markBestResult() { const grid = $("#cb-grid"); if (!grid) return; let bestI = -1, bestRank = 99; cardFits.forEach((lvl, i) => { if (lvl == null) return; const rank = FIT_RANK[lvl] ?? 99; if (rank < bestRank) { bestRank = rank; bestI = i; } }); grid.querySelectorAll("[data-res]").forEach(el => { el.classList.remove("res-best"); el.querySelector(".cb-best-tag")?.remove(); }); if (bestI < 0 || bestRank > 1) return; // nur hervorheben, wenn etwas Lauffähiges (perfect/marginal) dabei ist const el = grid.querySelector(`[data-res="${bestI}"]`); if (el) { el.classList.add("res-best"); el.insertAdjacentHTML("afterbegin", `
★ Beste Wahl für dein System
`); } } function showFit() { const file = $("#cb-m-files").value; const f = currentAnalysis?.files.find(x => x.filename === file); if (!f) { $("#cb-m-fit").style.display = "none"; return; } $("#cb-m-fit").style.display = "flex"; $("#cb-m-fit-text").innerHTML = `Bedarf: ~${f.fit.req_gb.toFixed(1)} GB · ${currentAnalysis.params_b}B · ${esc(f.quant)} · ~${Math.round(f.fit.tps)} Tok/s`; $("#cb-m-fit-badge").innerHTML = `${esc(f.fit.text)}`; const opt = f.optimal_ctx, recEl = $("#cb-m-ctx-rec"); if (opt) { // Kontext-Input auto auf optimal setzen, wenn noch auf Default (8192) const ctxInput = $("#cb-m-ctx"); if (ctxInput && (ctxInput.value === "8192" || ctxInput.value === "")) ctxInput.value = opt; } if (recEl) recEl.innerHTML = opt ? `Empfohlener Kontext für deine Hardware: ~${Math.round(opt / 1024)}kübernehmen` : ""; const btn = $("#cb-m-download"); if (f.fit.level === "too_tight") { btn.className = "primary warn"; btn.textContent = "Trotzdem holen (zu groß)"; } else { btn.className = "primary"; btn.textContent = "Herunterladen & Einpflegen"; } } function updateLiveFit() { showFit(); } async function reanalyzeCtx() { if (!currentAnalysis) return; const ctx = parseInt($("#cb-m-ctx").value) || 8192; const file = $("#cb-m-files").value; try { currentAnalysis = await api("/api/cookbook/analyze", { method: "POST", body: JSON.stringify({ repo_id: currentAnalysis.repo, ctx }) }); $("#cb-m-files").value = file; showFit(); } catch {} } function openModalBase(title, repo) { $("#cb-modal").style.display = "flex"; $("#cb-m-title").textContent = title; $("#cb-m-repo").textContent = repo; } async function openResult(i) { const m = currentResults[i]; if (!m) return; openModalBase(m.id.split("/").pop(), m.id); $("#cb-m-alias").value = ""; $("#cb-m-files").style.display = "none"; $("#cb-m-loading").style.display = "block"; $("#cb-m-download").disabled = true; try { const ctx = parseInt($("#cb-m-ctx").value) || 8192; currentAnalysis = await api("/api/cookbook/analyze", { method: "POST", body: JSON.stringify({ repo_id: m.id, ctx }) }); $("#cb-m-loading").style.display = "none"; $("#cb-m-files").style.display = "block"; if (!currentAnalysis.files?.length) { $("#cb-m-files").innerHTML = ""; $("#cb-m-fit").style.display = "none"; return; } $("#cb-m-files").innerHTML = currentAnalysis.files.map(f => { const mark = f.fit.level === "perfect" ? "●" : f.fit.level === "marginal" ? "◐" : "○"; return ``; }).join(""); $("#cb-m-download").disabled = false; showFit(); } catch (e) { $("#cb-m-loading").textContent = "Fehler: " + e.message; } } async function doDownload() { const repo = $("#cb-m-repo").textContent, file = $("#cb-m-files").value; const role = $("#cb-m-alias").value.trim().toLowerCase(), ctx = parseInt($("#cb-m-ctx").value) || 8192; if (!repo || !file) return toast("Bitte eine GGUF-Datei wählen.", true); const btn = $("#cb-m-download"); btn.disabled = true; btn.textContent = "Starte…"; try { const res = await api("/api/download", { method: "POST", body: JSON.stringify({ repo, file, hf_token: getHfToken() }) }); await api("/api/register", { method: "POST", body: JSON.stringify({ role, model_path: res.expected_path, ctx }) }); toast("Download gestartet — siehe Aktivität."); $("#cb-modal").style.display = "none"; document.querySelector(".nav-item[data-view='activity']")?.click(); } catch (e) { toast("Fehler: " + e.message, true); } btn.disabled = false; btn.textContent = "Herunterladen & Einpflegen"; } function onSystem(sys) { lastSys = sys; renderHwChip(); } export default { id: "cookbook", mount, onSystem };