// cookbook.js — "App-Store" für Modelle (v3): Hardware-Fit als Ampel, Klartext-Urteile. // Backend: /api/cookbook/{analyze,evaluate} (hw_math), /api/download, /api/register. import { api } from "../core/api.js"; import { $, esc, icon, toast } from "../core/ui.js"; const CURATED = [ { name: "Qwen 2.5 Coder 7B", repo: "unsloth/Qwen2.5-Coder-7B-Instruct-GGUF", file: "Qwen2.5-Coder-7B-Instruct-Q4_K_M.gguf", desc: "Schnell und locker auf deiner Kiste — ideal zum Programmieren.", params_b: 7, quant: "Q4_K_M", ctx: 32768, alias: "coder-fast" }, { name: "Qwen 2.5 Coder 32B", repo: "unsloth/Qwen2.5-Coder-32B-Instruct-GGUF", file: "Qwen2.5-Coder-32B-Instruct-Q4_K_M.gguf", desc: "Mehr Können, etwas langsamer — der starke Allround-Coder.", params_b: 32, quant: "Q4_K_M", ctx: 32768, alias: "coder" }, { name: "Llama 3.2 Vision 11B", repo: "unsloth/Llama-3.2-11B-Vision-Instruct-GGUF", file: "Llama-3.2-11B-Vision-Instruct-Q4_K_M.gguf", desc: "Versteht Bilder und Text — gut für multimodale Aufgaben.", params_b: 11, quant: "Q4_K_M", ctx: 8192, alias: "vision" }, { name: "Qwen 2.5 7B", repo: "unsloth/Qwen2.5-7B-Instruct-GGUF", file: "Qwen2.5-7B-Instruct-Q4_K_M.gguf", desc: "Hervorragender Allrounder — schnell und vielseitig.", params_b: 7, quant: "Q4_K_M", ctx: 8192, alias: "scout" }, ]; 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 = ""; const fitCls = lvl => lvl === "perfect" ? "ok" : lvl === "marginal" ? "warn" : "bad"; function mount() { const c = $(".view[data-view='cookbook']"); c.innerHTML = `

Modell-Cookbook

Finde ein KI-Modell, das auf deinen Mini-PC passt — den Hardware-Check rechnen wir für dich aus.

Empfohlen für deine Hardware

deine Hardware
passt locker läuft, aber knapp zu groß für deinen Speicher
`; renderFilters(); $("#cb-btn-search").addEventListener("click", doSearch); $("#cb-search").addEventListener("keydown", e => { if (e.key === "Enter") 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); renderHwChip(); renderCurated(); } 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`; } } // ---- Karten ---- function metricLine(fit) { return `~${fit.req_gb.toFixed(1)} GB · ~${Math.round(fit.tps)} Tok/s`; } function curatedCard(m, i, fit) { return `

${esc(m.name)}

${esc(fit.text)}
${esc(m.desc)}
${metricLine(fit)}${esc(m.quant)}
`; } async function renderCurated() { $("#cb-section-title").textContent = "Empfohlen für deine Hardware"; const grid = $("#cb-grid"); if (!grid) return; grid.innerHTML = `
Berechne Hardware-Fit…
`; try { let html = ""; for (let i = 0; i < CURATED.length; i++) { const m = CURATED[i]; const fit = await api("/api/cookbook/evaluate", { method: "POST", body: JSON.stringify({ params_b: m.params_b, quant: m.quant, ctx: m.ctx }) }); html += curatedCard(m, i, fit); } grid.innerHTML = html; grid.querySelectorAll("[data-cur]").forEach(el => el.addEventListener("click", () => openCurated(+el.getAttribute("data-cur")))); } catch (e) { grid.innerHTML = `
Konnte Empfehlungen nicht laden: ${esc(e.message)}
`; } } async function doSearch() { let q = $("#cb-search").value.trim(); if (activeFilter) q = q ? q + " " + activeFilter : activeFilter; if (!q) return renderCurated(); const btn = $("#cb-btn-search"); btn.disabled = true; btn.textContent = "Lade…"; $("#cb-section-title").textContent = "Suchergebnisse: " + q; const grid = $("#cb-grid"); grid.innerHTML = `
Suche auf HuggingFace…
`; try { const url = `https://huggingface.co/api/models?search=${encodeURIComponent(q)}&filter=gguf&sort=downloads&direction=-1&limit=12`; const r = await fetch(url); currentResults = await r.json(); renderResults(currentResults); } catch (e) { grid.innerHTML = `
${esc(e.message)}
`; } btn.disabled = false; btn.textContent = "Suchen"; } function renderResults(results) { const grid = $("#cb-grid"); 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…
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 bad"; b.textContent = "keine GGUFs"; mt.textContent = "—"; 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"); } catch { const b = $("#cb-b-" + i); if (b) { b.className = "fit-badge bad"; b.textContent = "Fehler"; } } } // ---- Modal ---- 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 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 = m.id.split("/").pop().toLowerCase().replace(/[^a-z0-9]/g, "-"); $("#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 openCurated(i) { const m = CURATED[i]; if (!m) return; openModalBase(m.name, m.repo); $("#cb-m-files").style.display = "block"; $("#cb-m-loading").style.display = "none"; $("#cb-m-files").innerHTML = ``; $("#cb-m-alias").value = m.alias; $("#cb-m-ctx").value = m.ctx; $("#cb-m-download").disabled = false; try { const fit = await api("/api/cookbook/evaluate", { method: "POST", body: JSON.stringify({ params_b: m.params_b, quant: m.quant, ctx: m.ctx }) }); currentAnalysis = { repo: m.repo, params_b: m.params_b, files: [{ filename: m.file, quant: m.quant, fit }] }; showFit(); } catch {} } async function doDownload() { const repo = $("#cb-m-repo").textContent, file = $("#cb-m-files").value; const alias = $("#cb-m-alias").value.trim(), ctx = parseInt($("#cb-m-ctx").value) || 8192; if (!repo || !file || !alias) return toast("Bitte alle Felder ausfüllen.", 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 }) }); await api("/api/register", { method: "POST", body: JSON.stringify({ alias, 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 };