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 = [];
function mount() {
const c = $(".view[data-view='cookbook']");
c.innerHTML = `
Kuratierte Empfehlungen
Modell
repo/name
Lade Dateien...
Ressourcen-Check
Berechne...
`;
$("#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
let currentAnalysis = null;
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) {}
}
async function doSearch() {
const q = $("#cb-search").value.trim();
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)}
GGUF
⬇ ${(m.downloads||0).toLocaleString()}
`).join("");
}
// 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;
if (!$("#cb-search").value.trim()) renderCurated();
}
function unmount() {}
export default { id: "cookbook", mount, unmount, onSystem };