From b84e8f3ac20bfe43383b165baac505114fc2bdaa Mon Sep 17 00:00:00 2001 From: Hitonabi Date: Sat, 20 Jun 2026 21:42:12 +0200 Subject: [PATCH] feat: cookbook (Feature 4) --- ROADMAP.md | 21 ++-- static/index.html | 11 +- static/js/main.js | 3 +- static/js/panels/cookbook.js | 225 +++++++++++++++++++++++++++++++++++ static/js/panels/models.js | 45 ------- 5 files changed, 247 insertions(+), 58 deletions(-) create mode 100644 static/js/panels/cookbook.js diff --git a/ROADMAP.md b/ROADMAP.md index de7f101..42db3c3 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -15,16 +15,19 @@ Modell-Listen, Aktivitäts-Stream) angelehnt an `docs/mission-control-overview.p **✅ Schritt 2 erledigt & live** — *Feature 3: Live-Auslastung*. `system.py` Router mit `psutil` und sysfs für CPU/RAM/Disk/GPU/Temp. **Reihenfolge (abgestimmt):** Design/Architektur zuerst (✅), dann Quick Wins, Security-Brocken zuletzt: -`1 (✅) Fundament → 2 (✅) Feature 3 Live-Auslastung → 3 (✅) Feature 6 Mehr LLM-Metriken → 4 (✅) Feature 1 Server-Management → [5] Feature 4 Cookbook ← NÄCHSTES → Feature 7 → Feature 2`. +`1 (✅) Fundament → 2 (✅) Feature 3 Live-Auslastung → 3 (✅) Feature 6 Mehr LLM-Metriken → 4 (✅) Feature 1 Server-Management → 5 (✅) Feature 4 Cookbook → [6] Feature 7 Integrations-Anleitungen ← NÄCHSTES → Feature 2`. **Arbeitsweise je Schritt:** neuer `routers/.py` + `js/panels/.js` + Nav-Eintrag, sauber degradierend. Bauen + Smoke-Test auf Windows, dann push→pull→rsync→restart auf den Bosgame (CLAUDE.md „Entwickeln & Deployen"). Ein Commit je Schritt. **Ich (KI) habe key-basierten SSH-Zugang zum Bosgame und kann selbst deployen+restarten.** -**→ Nächster Schritt konkret = Feature 4 (Cookbook + "Modell holen" verschmelzen).** -- Bisher: Textfelder für HuggingFace-Repo + Pfad unter "Modelle". Das ist super für Custom-Zeug. -- Neu: Ein Klick-Cookbook (Sidebar-Tab "Cookbook") mit kuratierter Liste (z.B. Qwen2.5-Coder 32B, Llama3 Vision, etc.). -- Klick auf Modellkarte im Cookbook triggert den Download via `/api/download`. +**→ Nächster Schritt konkret = Feature 7 (Integrations-Anleitungen).** +- Unter "Guides" (neuer Tab links) gibt es Copy-Paste Templates. +- Templates für die Integration von llama-swap/mission-control in: + - Cline / Cursor + - N8N / Zapier + - OpenWebUI + - LangChain / Python Code --- @@ -48,9 +51,11 @@ Aktuell gibt es nur "Container aktualisieren" + "Alles aus dem Speicher". Ziel: - [x] Quellen: **sysfs + psutil** — `amd-smi`/`rocm-smi` sind auf dem Bosgame NICHT installiert! GPU-Mem: `/sys/class/drm/card1/device/mem_info_*`; Temp: hwmon-`name` `amdgpu`/`k10temp`. -### 4. Cookbook + "Modell holen" verschmelzen -- [ ] Rezept-basiertes Einpflegen (Odysseus-Style): kuratierte Modell-Rezepte (Repo, Datei, ctx, Flags) per Klick installierbar -- [ ] "Modell holen" wird Teil des Cookbooks statt separates Formular +### 4. Cookbook + "Modell holen" verschmelzen (✅ Erledigt) +- Bisher: Textfelder für HuggingFace-Repo + Pfad unter "Modelle". Das ist super für Custom-Zeug. +- [x] Neu: Ein Klick-Cookbook (Sidebar-Tab "Cookbook") mit kuratierter Liste (z.B. Qwen2.5-Coder 32B, Llama3 Vision, etc.) inkl. Hardware-Aware "What Fits" Logik (wie bei Odysseus). +- [x] Klick auf Modellkarte im Cookbook triggert den Download via `/api/download`. +- [x] UI-Aufräumen: "Modell holen" Panel wandert ins Cookbook, unter "Modelle" bleibt nur die Tabelle & Chat. ### 5. Design 2.0 - [x] **Grundgerüst + Design-Sprache in Schritt 1 umgesetzt** (Sidebar-Nav, Topbar, Hero, getönte diff --git a/static/index.html b/static/index.html index 396fef4..1509b2b 100644 --- a/static/index.html +++ b/static/index.html @@ -18,7 +18,7 @@ - +
@@ -55,13 +55,16 @@ + +
diff --git a/static/js/main.js b/static/js/main.js index 978bdce..8486a0f 100644 --- a/static/js/main.js +++ b/static/js/main.js @@ -9,8 +9,9 @@ import overview from "./panels/overview.js"; import models from "./panels/models.js"; import maintenance from "./panels/maintenance.js"; import jobs from "./panels/jobs.js"; +import cookbook from "./panels/cookbook.js"; -const panels = [overview, models, maintenance, jobs]; +const panels = [overview, models, maintenance, jobs, cookbook]; let lastJobs = []; let lastSystem = null; diff --git a/static/js/panels/cookbook.js b/static/js/panels/cookbook.js new file mode 100644 index 0000000..d4844b9 --- /dev/null +++ b/static/js/panels/cookbook.js @@ -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 = ` +
+ +
+

Custom Download

+
+ + + +
+ + +
+ `; + + 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 ` +
+
+

${m.name}

+ + ${fit.text} + +
+
+ ${m.desc}

+ Größe: ~${fit.req.toFixed(1)} GB (inkl. Context)
+ Quant: ${m.quant} +
+ +
+ `; + }).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) {} +} diff --git a/static/js/panels/models.js b/static/js/panels/models.js index 7cb276e..81e2516 100644 --- a/static/js/panels/models.js +++ b/static/js/panels/models.js @@ -2,27 +2,10 @@ import { api } from "../core/api.js"; import { $, badge, esc, toast } from "../core/ui.js"; -import { track } from "./jobs.js"; function refreshSoon() { document.dispatchEvent(new Event("mc:refresh")); } function mount() { - $("#m-download").innerHTML = ` -

Modell holen

- - - - - - `; $("#m-chat").innerHTML = `

Schnelltest

@@ -41,8 +24,6 @@ function mount() { `; - $("#dl-btn").addEventListener("click", pull); - $("#rg-btn").addEventListener("click", register); $("#chat-btn").addEventListener("click", sendChat); } @@ -95,32 +76,6 @@ function onStatus(s) { ); } -async function pull() { - const repo = $("#dl-repo").value.trim(), file = $("#dl-file").value.trim(); - if (!repo || !file) return toast("Repo und Datei angeben.", true); - try { - const r = await api("/api/download", { method: "POST", body: JSON.stringify({ repo, file }) }); - toast("Download gestartet."); - const stem = file.split("/").pop().replace(/\.gguf$/i, ""); - $("#rg-alias").value = stem; - $("#rg-path").value = r.expected_path; - $("#register-box").style.display = "block"; - track(r.job_id); - } catch (e) { toast(e.message, true); } -} - -async function register() { - const alias = $("#rg-alias").value.trim(); - const model_path = $("#rg-path").value; - const ctx = parseInt($("#rg-ctx").value) || 8192; - if (!alias) return toast("Alias angeben.", true); - try { - await api("/api/register", { method: "POST", body: JSON.stringify({ alias, model_path, ctx }) }); - toast("Eingepflegt — llama-swap lädt neu."); - refreshSoon(); - } catch (e) { toast(e.message, true); } -} - async function unloadOne(m) { try { await api("/api/unload?model=" + encodeURIComponent(m), { method: "POST" });