import { api } from "../core/api.js"; import { $, esc, icon, toast } from "../core/ui.js"; import { track } from "./jobs.js"; const CURATED_MODELS = [ { id: "qwen-coder-32b", 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: "Top-Tier lokales Coder-Modell. Braucht ca. 24GB VRAM.", params_b: 32, quant: "Q4_K_M", ctx: 32768, alias: "coder" }, { id: "qwen-coder-7b", 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: "Schneller Coder. Perfekte Balance aus Speed und Qualität.", params_b: 7, quant: "Q4_K_M", ctx: 32768, alias: "coder-fast" }, { id: "llama3-vision-11b", 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: "Modell für Bilderkennung und multimodale Tasks.", params_b: 11, quant: "Q4_K_M", ctx: 8192, alias: "vision" }, { id: "qwen-general-7b", name: "Qwen 2.5 7B", repo: "unsloth/Qwen2.5-7B-Instruct-GGUF", file: "Qwen2.5-7B-Instruct-Q4_K_M.gguf", desc: "Hervorragendes Generalist/Scout Modell.", params_b: 7, quant: "Q4_K_M", ctx: 8192, alias: "scout" } ]; // Lokale Mathe entfernt. Wir nutzen jetzt das Backend. let lastSys = null; let currentResults = []; let currentAnalysis = null; let activeFilter = ""; const FILTERS = [ { id: "", label: "Alle" }, { id: "coder", label: "Coding" }, { id: "vision", label: "Vision / Multimodal" }, { id: "roleplay", label: "Roleplay" }, { id: "german", label: "Deutsch" } ]; function mount() { const c = $(".view[data-view='cookbook']"); const filtersHtml = FILTERS.map(f => `` ).join(""); c.innerHTML = `

App Store für Modelle

Entdecke Modelle live auf HuggingFace, Hardware-Fit inklusive.

${filtersHtml}

Kuratierte Empfehlungen

`; $("#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-m-download").addEventListener("click", doDownload); $("#cb-m-files").addEventListener("change", updateLiveFit); $("#cb-m-ctx").addEventListener("change", reanalyzeCtx); renderCurated(); } // Aktuelle Analyse-Daten vom Backend function updateLiveFit() { const file = $("#cb-m-files").value; if (!currentAnalysis || !file) { $("#cb-m-fit-container").style.display = "none"; return; } const fData = currentAnalysis.files.find(f => f.filename === file); if (!fData) return; const fit = fData.fit; const cls = fit.level === "perfect" ? "b-run" : (fit.level === "marginal" ? "b-load" : "b-err"); $("#cb-m-fit-container").style.display = "flex"; $("#cb-m-fit-text").innerHTML = `Geschätzter Bedarf: ~${fit.req_gb.toFixed(1)} GB RAM/VRAM
${currentAnalysis.params_b}B Params · ${fData.quant} · ~${Math.round(fit.tps)} t/s`; $("#cb-m-fit-badge").innerHTML = `${fit.text}`; // Wenn "too_tight", machen wir den Download-Button gelb zur Warnung, erlauben ihn aber const btn = $("#cb-m-download"); if (fit.level === "too_tight") { btn.className = "primary warn"; btn.innerHTML = "Trotzdem herunterladen (OOM Risiko!)"; } else { btn.className = "primary"; btn.innerHTML = "Herunterladen & Einpflegen"; } } async function reanalyzeCtx() { if (!currentAnalysis) return; const ctx = parseInt($("#cb-m-ctx").value) || 8192; const repo = currentAnalysis.repo; const file = $("#cb-m-files").value; $("#cb-m-fit-text").innerHTML = "Berechne neues Context-Limit..."; try { const res = await api("/api/cookbook/analyze", { method: "POST", body: JSON.stringify({ repo_id: repo, ctx }) }); currentAnalysis = res; // Auswahl beibehalten $("#cb-m-files").value = file; updateLiveFit(); } catch(e) {} } window.setFilter = (filterId) => { activeFilter = filterId; const buttons = document.querySelectorAll('.filter-btn'); buttons.forEach(b => { b.style.background = 'var(--bg)'; b.style.color = ''; b.style.border = '1px solid var(--line)'; }); const idx = FILTERS.findIndex(f => f.id === filterId); if (idx !== -1 && buttons[idx]) { buttons[idx].style.background = 'var(--hi)'; buttons[idx].style.color = 'var(--bg)'; buttons[idx].style.border = 'none'; } doSearch(); }; 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 für: " + esc(q); $("#cb-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); const data = await r.json(); currentResults = data; renderResults(data); } catch (e) { $("#cb-grid").innerHTML = `
${esc(e.message)}
`; } btn.disabled = false; btn.textContent = "Suchen"; } function renderResults(results) { const grid = $("#cb-grid"); if (!results || results.length === 0) { grid.innerHTML = `
Keine GGUF-Modelle gefunden.
`; return; } grid.innerHTML = results.map((m, i) => `

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

${esc(m.author)}
Analysiere...
Berechne Hardware-Fit... ⬇ ${(m.downloads||0).toLocaleString()} GGUF
`).join(""); // Lade Metriken im Hintergrund results.forEach((m, i) => fetchAnalysisForCard(i, m.id)); } async function fetchAnalysisForCard(index, repo_id) { try { const res = await api("/api/cookbook/analyze", { method: "POST", body: JSON.stringify({ repo_id, ctx: 8192 }) }); const badgeEl = document.getElementById(`cb-s-badge-${index}`); const metricsEl = document.getElementById(`cb-s-metrics-${index}`); const quantEl = document.getElementById(`cb-s-quant-${index}`); if (!badgeEl || !metricsEl || !quantEl) return; // card might be gone if (!res.files || res.files.length === 0) { badgeEl.className = "badge b-err"; badgeEl.textContent = "Keine GGUFs"; metricsEl.textContent = "Keine Dateien gefunden"; return; } let best = res.files.find(f => f.quant && f.quant.includes("Q4_K_M")); if (!best) best = res.files[0]; const fit = best.fit; const cls = fit.level === "perfect" ? "b-run" : (fit.level === "marginal" ? "b-load" : "b-err"); badgeEl.className = `badge ${cls}`; badgeEl.textContent = fit.text; metricsEl.innerHTML = `~${fit.req_gb.toFixed(1)} GB RAM/VRAM · ~${Math.round(fit.tps)} t/s`; quantEl.textContent = best.quant || "GGUF"; } catch(e) { const badgeEl = document.getElementById(`cb-s-badge-${index}`); const metricsEl = document.getElementById(`cb-s-metrics-${index}`); if (badgeEl) { badgeEl.className = "badge b-err"; badgeEl.textContent = "Fehler"; } if (metricsEl) { metricsEl.textContent = "Metriken konnten nicht geladen werden"; } } } // Global hook for inline onclick window.openModelModal = async (index) => { const m = currentResults[index]; if (!m) return; $("#cb-modal").style.display = "flex"; $("#cb-m-title").textContent = m.id.split('/').pop(); $("#cb-m-repo").textContent = m.id; $("#cb-m-files").innerHTML = ""; $("#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; const res = await api("/api/cookbook/analyze", { method: "POST", body: JSON.stringify({ repo_id: m.id, ctx }) }); currentAnalysis = res; $("#cb-m-loading").style.display = "none"; $("#cb-m-files").style.display = "block"; if (!res.files || res.files.length === 0) { $("#cb-m-files").innerHTML = ""; $("#cb-m-fit-container").style.display = "none"; } else { // Optische Indikatoren im Dropdown $("#cb-m-files").innerHTML = res.files.map(f => { let mark = ""; if (f.fit.level === "perfect") mark = "🟢"; else if (f.fit.level === "marginal") mark = "🟡"; else mark = "🔴"; return ``; }).join(""); $("#cb-m-download").disabled = false; updateLiveFit(); } } catch(e) { $("#cb-m-loading").textContent = "Fehler: " + e.message; } }; async function doDownload() { const repo = $("#cb-m-repo").textContent; const file = $("#cb-m-files").value; const alias = $("#cb-m-alias").value.trim(); const ctx = parseInt($("#cb-m-ctx").value) || 8192; if (!repo || !file || !alias) return toast("Bitte alle Felder ausfüllen.", true); $("#cb-m-download").disabled = true; $("#cb-m-download").textContent = "Starte..."; try { const res = await api("/api/download", { method: "POST", body: JSON.stringify({ repo, file }) }); // Register directly, jobengine will download it, MC will pick it up when done await api("/api/register", { method: "POST", body: JSON.stringify({ alias, model_path: res.expected_path, ctx }) }); toast("Download gestartet! Siehe Aktivitäten."); $("#cb-modal").style.display = "none"; track(res.job_id); // open in jobs // Switch to activity tab document.querySelector(".nav-item[data-view='activity']").click(); } catch (e) { toast("Fehler: " + e.message, true); } $("#cb-m-download").disabled = false; $("#cb-m-download").textContent = "Herunterladen & Einpflegen"; } async function renderCurated() { $("#cb-section-title").textContent = "Kuratierte Empfehlungen"; const grid = $("#cb-grid"); if (!grid) return; grid.innerHTML = "
Berechne Hardware-Fit für Empfehlungen...
"; try { let html = ""; for (let i = 0; i < CURATED_MODELS.length; i++) { const m = CURATED_MODELS[i]; const fit = await api("/api/cookbook/evaluate", { method: "POST", body: JSON.stringify({ params_b: m.params_b, quant: m.quant, ctx: m.ctx }) }); const cls = fit.level === "perfect" ? "b-run" : (fit.level === "marginal" ? "b-load" : "b-err"); html += `

${esc(m.name)}

${fit.text}
${m.desc}
~${fit.req_gb.toFixed(1)} GB RAM/VRAM · ~${Math.round(fit.tps)} t/s ${m.quant}
`; } grid.innerHTML = html; } catch (e) { grid.innerHTML = `
Fehler beim Laden der Empfehlungen: ${e.message}
`; } } window.openCuratedModal = async (index) => { const m = CURATED_MODELS[index]; if (!m) return; $("#cb-modal").style.display = "flex"; $("#cb-m-title").textContent = m.name; $("#cb-m-repo").textContent = m.repo; $("#cb-m-files").innerHTML = ``; $("#cb-m-files").style.display = "block"; $("#cb-m-loading").style.display = "none"; $("#cb-m-alias").value = m.alias; $("#cb-m-ctx").value = m.ctx; $("#cb-m-download").disabled = false; // Wir nutzen die neue API Struktur auch für das simulierte Modal 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: fit }] }; updateLiveFit(); } catch(e) {} }; function onSystem(sys) { lastSys = sys; } function unmount() {} export default { id: "cookbook", mount, unmount, onSystem };