feat: cookbook (Feature 4)
This commit is contained in:
+13
-8
@@ -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/<x>.py` + `js/panels/<x>.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
|
||||
|
||||
+7
-4
@@ -18,7 +18,7 @@
|
||||
<a class="nav-item" data-view="models" title="Modelle" data-ic="cpu"></a>
|
||||
<span class="nav-item disabled" title="Aktivität — siehe Übersicht" data-ic="pulse"></span>
|
||||
<span class="nav-item disabled" title="Server (bald)" data-ic="server"></span>
|
||||
<span class="nav-item disabled" title="Cookbook (bald)" data-ic="book"></span>
|
||||
<a class="nav-item" data-view="cookbook" title="Cookbook" data-ic="book"></a>
|
||||
<span class="nav-item disabled" title="Guides (bald)" data-ic="help"></span>
|
||||
</nav>
|
||||
<div class="side-foot">
|
||||
@@ -55,13 +55,16 @@
|
||||
</section>
|
||||
|
||||
<section class="view" data-view="models" hidden>
|
||||
<div class="grid grid-2">
|
||||
<div class="card" id="m-download"></div>
|
||||
<div class="card" id="m-chat"></div>
|
||||
<div class="grid grid-2" id="m-top-grid">
|
||||
<div class="card" id="m-chat" style="grid-column: span 2;"></div>
|
||||
</div>
|
||||
<div class="card" id="m-table"></div>
|
||||
</section>
|
||||
|
||||
<section class="view" data-view="cookbook" hidden>
|
||||
<!-- Wird von cookbook.js gerendert -->
|
||||
</section>
|
||||
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+2
-1
@@ -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;
|
||||
|
||||
@@ -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) {}
|
||||
}
|
||||
@@ -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 = `
|
||||
<div class="card-h"><h3>Modell holen</h3></div>
|
||||
<label>HuggingFace-Repo</label>
|
||||
<input id="dl-repo" placeholder="unsloth/Qwen3-Coder-30B-A3B-Instruct-GGUF">
|
||||
<label>Datei (GGUF)</label>
|
||||
<input id="dl-file" placeholder="Q4_K_M/Qwen3-Coder-30B-A3B-Instruct-Q4_K_M.gguf">
|
||||
<button class="primary" id="dl-btn">Modell herunterladen</button>
|
||||
<div id="register-box" style="display:none;margin-top:16px;border-top:1px solid var(--line);padding-top:14px">
|
||||
<div class="card-h"><h3>Einpflegen</h3></div>
|
||||
<div class="row">
|
||||
<div><label>Alias</label><input id="rg-alias"></div>
|
||||
<div><label>Kontext</label><input id="rg-ctx" value="8192"></div>
|
||||
</div>
|
||||
<input id="rg-path" class="mono-sm" readonly>
|
||||
<button class="primary" id="rg-btn">In Config eintragen</button>
|
||||
</div>`;
|
||||
|
||||
$("#m-chat").innerHTML = `
|
||||
<div class="card-h"><h3>Schnelltest</h3></div>
|
||||
@@ -41,8 +24,6 @@ function mount() {
|
||||
</table>
|
||||
<div id="models-empty" class="empty" style="display:none">Noch keine Modelle konfiguriert — zieh dir oben eins rein. 👇</div>`;
|
||||
|
||||
$("#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" });
|
||||
|
||||
Reference in New Issue
Block a user