feat: cookbook (Feature 4)
This commit is contained in:
@@ -0,0 +1,225 @@
|
||||
import { api } from "../core/api.js";
|
||||
import { $ } from "../core/ui.js";
|
||||
|
||||
const COOKBOOK_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 viel Speicher.",
|
||||
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"
|
||||
}
|
||||
];
|
||||
|
||||
// Odysseus Hardware Fit Logic
|
||||
function estimateMemoryGB(params_b, quant, ctx) {
|
||||
// Q4_K_M is roughly 0.6 bytes per param
|
||||
const bpp = 0.6;
|
||||
const weights = params_b * bpp;
|
||||
// Context roughly: 1GB per 8K for 7B
|
||||
const context = (ctx / 8192) * (params_b / 7) * 0.8;
|
||||
return weights + context;
|
||||
}
|
||||
|
||||
function getFit(m, sys) {
|
||||
const req = estimateMemoryGB(m.params_b, m.quant, m.ctx);
|
||||
// Falls keine sys-Daten da sind (Backend mock), nimm Standardwerte an
|
||||
const vram_bytes = sys?.gpu?.vram?.total || 0;
|
||||
const vram = vram_bytes / (1024 ** 3);
|
||||
const ram_bytes = sys?.ram?.total || 0;
|
||||
const ram_used = sys?.ram?.used || 0;
|
||||
const ram = (ram_bytes) / (1024 ** 3);
|
||||
const freeRam = (ram_bytes - ram_used) / (1024 ** 3);
|
||||
|
||||
// Wenn gar keine echten Metriken kommen (Windows Dummy Backend), immer "Fits" für Demo
|
||||
if (vram === 0 && ram === 0) return { level: "perfect", class: "green", text: "Fits (Mock)", req };
|
||||
|
||||
if (vram > 0 && req <= vram) return { level: "perfect", class: "green", text: "Fits VRAM", req };
|
||||
if (req <= (vram + freeRam)) return { level: "good", class: "yellow", text: "RAM Offload", req };
|
||||
return { level: "too_tight", class: "red", text: "OOM (Zu groß)", req };
|
||||
}
|
||||
|
||||
let lastSys = null;
|
||||
let dlPoll = null;
|
||||
|
||||
function mount() {
|
||||
render();
|
||||
}
|
||||
|
||||
function unmount() {
|
||||
if (dlPoll) clearInterval(dlPoll);
|
||||
}
|
||||
|
||||
function onSystem(sys) {
|
||||
lastSys = sys;
|
||||
renderGrid();
|
||||
}
|
||||
|
||||
export default { mount, unmount, onSystem };
|
||||
|
||||
function render() {
|
||||
const c = document.querySelector(".view[data-view='cookbook']");
|
||||
c.innerHTML = `
|
||||
<div class="grid grid-3" id="cb-grid"></div>
|
||||
|
||||
<div class="card" style="margin-top: 24px;">
|
||||
<div class="card-h"><h3>Custom Download</h3></div>
|
||||
<div style="display:flex; gap:12px; margin-bottom:12px;">
|
||||
<input id="cb-repo" class="tokin" placeholder="HuggingFace Repo (z.B. unsloth/Qwen2.5-Coder-7B-Instruct-GGUF)" style="flex:2">
|
||||
<input id="cb-file" class="tokin" placeholder="Dateipfad (z.B. Qwen2.5-Coder-7B-Instruct-Q4_K_M.gguf)" style="flex:1">
|
||||
<button class="btn" id="cb-btn-dl">Herunterladen</button>
|
||||
</div>
|
||||
<div id="cb-dl-prog" style="display:none; color:var(--text-dim); font-size:12px; margin-bottom:12px;"></div>
|
||||
<div id="cb-register-box" style="display:none; padding-top:12px; border-top:1px solid var(--border);">
|
||||
<div style="margin-bottom:8px;">Modell heruntergeladen! Jetzt einpflegen:</div>
|
||||
<div style="display:flex; gap:12px;">
|
||||
<input id="cb-reg-alias" class="tokin" placeholder="Alias (z.B. coder)" style="flex:1">
|
||||
<input id="cb-reg-path" class="tokin" placeholder="Modell-Pfad" style="flex:2" readonly>
|
||||
<input id="cb-reg-ctx" class="tokin" placeholder="Ctx (z.B. 8192)" type="number" style="width:100px;">
|
||||
<button class="btn" id="cb-btn-reg">Einpflegen</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.getElementById("cb-btn-dl").onclick = () => {
|
||||
startDownload(
|
||||
document.getElementById("cb-repo").value,
|
||||
document.getElementById("cb-file").value,
|
||||
"", 8192
|
||||
);
|
||||
};
|
||||
|
||||
document.getElementById("cb-btn-reg").onclick = async () => {
|
||||
const alias = document.getElementById("cb-reg-alias").value;
|
||||
const path = document.getElementById("cb-reg-path").value;
|
||||
const ctx = document.getElementById("cb-reg-ctx").value;
|
||||
if (!alias || !path) return;
|
||||
try {
|
||||
await api("/api/models/register", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ alias, model_path: path, ctx_len: parseInt(ctx)||4096 })
|
||||
});
|
||||
alert("Erfolgreich eingepflegt!");
|
||||
document.getElementById("cb-register-box").style.display = "none";
|
||||
} catch(e) {
|
||||
alert("Fehler: " + e);
|
||||
}
|
||||
};
|
||||
|
||||
renderGrid();
|
||||
}
|
||||
|
||||
function renderGrid() {
|
||||
const grid = document.getElementById("cb-grid");
|
||||
if (!grid) return;
|
||||
grid.innerHTML = COOKBOOK_MODELS.map(m => {
|
||||
const fit = getFit(m, lastSys);
|
||||
return `
|
||||
<div class="card" style="display:flex; flex-direction:column;">
|
||||
<div style="display:flex; justify-content:space-between; align-items:center;">
|
||||
<h3 style="margin:0">${m.name}</h3>
|
||||
<span style="font-size:11px; padding:2px 6px; border-radius:4px; background:${fit.class === 'green' ? 'rgba(0,255,0,0.1)' : fit.class === 'yellow' ? 'rgba(255,255,0,0.1)' : 'rgba(255,0,0,0.1)'}; color:${fit.class === 'green' ? '#4ade80' : fit.class === 'yellow' ? '#fde047' : '#f87171'};">
|
||||
${fit.text}
|
||||
</span>
|
||||
</div>
|
||||
<div style="font-size:12px; color:var(--text-dim); margin-top:8px; flex:1;">
|
||||
${m.desc}<br><br>
|
||||
<b>Größe:</b> ~${fit.req.toFixed(1)} GB (inkl. Context)<br>
|
||||
<b>Quant:</b> ${m.quant}
|
||||
</div>
|
||||
<button class="btn cb-btn-preset" data-id="${m.id}" style="margin-top:16px; width:100%;">Herunterladen</button>
|
||||
</div>
|
||||
`;
|
||||
}).join("");
|
||||
|
||||
document.querySelectorAll(".cb-btn-preset").forEach(b => {
|
||||
b.onclick = () => {
|
||||
const m = COOKBOOK_MODELS.find(x => x.id === b.dataset.id);
|
||||
if(m) {
|
||||
document.getElementById("cb-repo").value = m.repo;
|
||||
document.getElementById("cb-file").value = m.file;
|
||||
startDownload(m.repo, m.file, m.alias, m.ctx);
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
async function startDownload(repo, file, autoAlias, autoCtx) {
|
||||
if(!repo || !file) return;
|
||||
document.getElementById("cb-dl-prog").style.display = "block";
|
||||
document.getElementById("cb-dl-prog").innerText = "Starte Download...";
|
||||
document.getElementById("cb-register-box").style.display = "none";
|
||||
|
||||
try {
|
||||
const res = await api("/api/models/download", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ repo_id: repo, filename: file })
|
||||
});
|
||||
if (res.job_id) {
|
||||
if(dlPoll) clearInterval(dlPoll);
|
||||
dlPoll = setInterval(() => checkProg(res.job_id, autoAlias, autoCtx), 2000);
|
||||
}
|
||||
} catch(e) {
|
||||
document.getElementById("cb-dl-prog").innerText = "Fehler: " + e;
|
||||
}
|
||||
}
|
||||
|
||||
async function checkProg(jobId, autoAlias, autoCtx) {
|
||||
try {
|
||||
const res = await api("/api/jobs");
|
||||
const j = res.find(x => x.id === jobId);
|
||||
if (!j) {
|
||||
clearInterval(dlPoll);
|
||||
document.getElementById("cb-dl-prog").innerText = "Job verschwunden.";
|
||||
return;
|
||||
}
|
||||
document.getElementById("cb-dl-prog").innerText = j.status + (j.error ? (" - " + j.error) : "");
|
||||
if (j.status === "done") {
|
||||
clearInterval(dlPoll);
|
||||
document.getElementById("cb-register-box").style.display = "block";
|
||||
document.getElementById("cb-reg-path").value = j.result || "";
|
||||
if(autoAlias) document.getElementById("cb-reg-alias").value = autoAlias;
|
||||
if(autoCtx) document.getElementById("cb-reg-ctx").value = autoCtx;
|
||||
} else if (j.status === "failed") {
|
||||
clearInterval(dlPoll);
|
||||
}
|
||||
} catch(e) {}
|
||||
}
|
||||
Reference in New Issue
Block a user