v5 Phase 1: Erklaeren (Swapping/RAM, Kontext, Guides-Neubau)

- Swapping-Klartext + Spitzenbedarf (peak_ram) pro Modell: Modelle-Konfig + Cookbook-Setup-Modal
  zeigen 'nur ein Modell gleichzeitig, groesstes zaehlt'. models.py: peak_ram_gb/_optimal in Meta.
- Kontextfenster: infoDot(ⓘ)-Helfer in ui.js + Tooltips an ctx-Feldern.
- Guides-Tab komplett neu = echter Anfaenger-Guide (LLM/GGUF/Quant/Kontext/Swapping/MCP/Skills/Agenten).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Hitonabi
2026-06-21 13:58:57 +02:00
parent dbb9e8d72e
commit 8e7a5425d4
6 changed files with 86 additions and 109 deletions
+5 -3
View File
@@ -16,7 +16,7 @@ from auth import auth
from config import CMD_TEMPLATE, CONFIG_PATH, DEFAULT_TTL, LLAMA_SWAP_URL, MODELS_DIR, TOKEN
from jobengine import JOBS, start_job
from llamaswap import _swap_get, read_config, write_config
from hw_math import extract_params_b, max_ctx_for
from hw_math import extract_params_b, max_ctx_for, estimate_memory_gb
import re
import os
import psutil
@@ -101,8 +101,10 @@ def status():
"quant": quant,
"caps": caps,
"filename": filename,
"params_b": extract_params_b(filename or name),
"optimal_ctx": (max_ctx_for(extract_params_b(filename or name), quant or "Q4_K_M", ram_gb) if ram_gb else None),
"params_b": (_pb := extract_params_b(filename or name)),
"optimal_ctx": (_oc := (max_ctx_for(_pb, quant or "Q4_K_M", ram_gb) if ram_gb else None)),
"peak_ram_gb": round(estimate_memory_gb(_pb, quant or "Q4_K_M", ctx), 1),
"peak_ram_optimal_gb": (round(estimate_memory_gb(_pb, quant or "Q4_K_M", _oc), 1) if _oc else None),
}
}
swap_ok = True
+3
View File
@@ -145,6 +145,9 @@ input:focus,textarea:focus,select:focus{outline:none;border-color:var(--accent)}
textarea{resize:vertical;min-height:64px;font-family:var(--sans)}
.row{display:flex;gap:10px}.row>div{flex:1}
.hint{font-size:12px;color:var(--mut);margin:-4px 0 12px}
.info-dot{display:inline-flex;vertical-align:-3px;color:var(--dim);cursor:help;margin-left:5px}
.info-dot svg{width:14px;height:14px}
.info-dot:hover{color:var(--accent)}
.mono-sm{font-family:var(--mono);font-size:11.5px;color:var(--mut)}
/* ---- Buttons ---- */
+5
View File
@@ -74,6 +74,11 @@ export const ICON = {
export function icon(name) { return ICON[name] || ""; }
// kleines Info-ⓘ mit Klartext-Tooltip (native title) — für Erklärungen direkt am Feld.
export function infoDot(text) {
return `<span class="info-dot" tabindex="0" title="${esc(text)}" aria-label="${esc(text)}">${ICON.info}</span>`;
}
// ---------------------------------------------------------------------------
// Format-Helfer (Klartext-Zahlen)
// ---------------------------------------------------------------------------
+9 -3
View File
@@ -2,7 +2,9 @@
// Backend: /api/cookbook/{recipes,install-recipe,analyze,evaluate} (hw_math).
import { api } from "../core/api.js";
import { $, esc, icon, toast } from "../core/ui.js";
import { $, esc, icon, toast, infoDot } from "../core/ui.js";
const CTX_HELP = "Kontext = das Kurzzeitgedächtnis des Modells. Größer = merkt sich mehr, braucht aber mehr Speicher und wird etwas langsamer.";
const FILTERS = [
{ id: "", label: "Alle" }, { id: "coder", label: "Coder" }, { id: "scout", label: "Scout" },
@@ -63,7 +65,8 @@ function mount() {
<h3 id="cb-r-title">Setup</h3>
<p class="text-mut text-sm" id="cb-r-desc" style="margin:-4px 0 16px"></p>
<div id="cb-r-models"></div>
<button class="primary" id="cb-r-install" style="width:100%;margin-top:16px">Komplettes Setup installieren</button>
<div class="hint" id="cb-r-note" style="margin-top:12px"></div>
<button class="primary" id="cb-r-install" style="width:100%;margin-top:8px">Komplettes Setup installieren</button>
<div class="hint" style="margin-top:8px">Lädt alle Modelle herunter und pflegt sie mit optimalem Kontext ein. Fortschritt in der Aktivität.</div>
</div>
</div>
@@ -78,7 +81,7 @@ function mount() {
<div class="hint" id="cb-m-loading" style="display:none">Lade Dateien von HuggingFace…</div>
<div class="row" style="margin-top:4px">
<div><label>Alias (Rolle)</label><input id="cb-m-alias" placeholder="z.B. coder"></div>
<div><label>Kontext-Größe</label><input id="cb-m-ctx" type="number" value="8192"></div>
<div><label>Kontext-Größe ${infoDot(CTX_HELP)}</label><input id="cb-m-ctx" type="number" value="8192"></div>
</div>
<div class="hint" id="cb-m-ctx-rec" style="margin:-6px 0 14px"></div>
<div id="cb-m-fit" class="tile" style="display:flex;justify-content:space-between;align-items:center;margin:8px 0 18px">
@@ -147,6 +150,9 @@ function openRecipe(id) {
</div>
<span class="fit-badge ${fitCls(m.fit.level)}">${esc(m.fit.text)}</span>
</div>`).join("")}</div>`;
const maxRam = Math.max(...r.models.map(m => m.fit.req_gb));
$("#cb-r-note").innerHTML = `Größter Spitzenbedarf: <b>~${maxRam.toFixed(1)} GB</b>. ` +
`Es läuft immer nur <b>ein</b> Modell gleichzeitig — das größte bestimmt, ob das Setup passt (kein Summieren).`;
const btn = $("#cb-r-install");
btn.className = r.fit_level === "too_tight" ? "primary warn" : "primary";
btn.textContent = r.fit_level === "too_tight" ? "Trotzdem installieren (zu groß)" : "Komplettes Setup installieren";
+54 -96
View File
@@ -1,106 +1,64 @@
// guides.js — Integrations-Anleitungen (v3): Copy-Paste-Configs für externe Tools.
// guides.js — Anfänger-Guide (v5): erklärt die Local-LLM-Welt idiotensicher.
// Reine Erklär-Seite. Tool-Einrichtung/Configs liegen im Tab „Verbinden".
import { $, esc } from "../core/ui.js";
import { $ } from "../core/ui.js";
// In die Zwischenablage kopieren (kopiert das vorhergehende Eingabefeld / Codeblock).
window.mcCopy = (btn, sel) => {
const src = sel ? btn.parentElement.querySelector(sel) : btn.previousElementSibling;
const text = src.value !== undefined ? src.value : src.textContent;
navigator.clipboard?.writeText(text);
const old = btn.textContent; btn.textContent = "Kopiert!";
setTimeout(() => (btn.textContent = old), 1200);
};
// Basis-URL aus dem Browser ableiten (zeigt die echte Bosgame-Adresse statt localhost).
function apiBase() {
const host = location.hostname || "localhost";
return `${location.protocol}//${host}:8080/v1`;
}
// Beschriftetes Feld mit Kopier-Button.
function field(label, value) {
return `<label>${label}</label>
<div class="flex gap-2" style="align-items:stretch;margin-bottom:12px">
<input class="mono-sm" readonly value="${esc(value)}" style="flex:1;margin:0">
<button class="ghost" onclick="window.mcCopy(this)">Kopieren</button>
</div>`;
}
function render() {
const c = $(".view[data-view='guides']");
if (!c) return;
const url = apiBase();
const py = `from openai import OpenAI
client = OpenAI(
base_url="${url}",
api_key="dein_token" # optional
)
resp = client.chat.completions.create(
model="coder", # Alias aus dem Cookbook
messages=[{"role": "user", "content": "Hallo Modell!"}]
)
print(resp.choices[0].message.content)`;
const SECTIONS = [
["Erste Schritte — in 3 Schritten loslegen", `
<ol style="padding-left:18px;line-height:1.9">
<li><b>Modell holen:</b> Im <b>Cookbook</b> auf „Wofür möchtest du es nutzen?" deinen Fall wählen
(z.B. „Coden") → „Komplettes Setup installieren". Der Download läuft im Tab <b>Aktivität</b>.</li>
<li><b>Verbinden:</b> Im Tab <b>Verbinden</b> die Basis-URL kopieren und in deinem Tool (z.B. OpenCode)
als „OpenAI-kompatibel" eintragen. Mit „Verbindung testen" prüfen.</li>
<li><b>Loslegen:</b> Im Tool das Modell (z.B. <code>coder</code>) auswählen und arbeiten. Die Engine
lädt das passende Modell automatisch.</li>
</ol>`],
["Was ist ein LLM bzw. ein Modell?", `
<p>Ein <b>LLM</b> (Large Language Model) ist ein KI-Sprachmodell — die „Intelligenz", die Text versteht
und schreibt (wie ChatGPT, nur <b>lokal auf deinem Mini-PC</b>, ohne Cloud). Ein <b>Modell</b> ist eine
konkrete Datei davon. Verschiedene Modelle sind unterschiedlich gut/schnell und auf Dinge spezialisiert
(Coden, Bilder, Chat).</p>`],
["Was ist GGUF?", `
<p><b>GGUF</b> ist das Datei<b>format</b>, in dem lokale Modelle gespeichert werden — gemacht, damit sie
effizient auf normaler Hardware laufen. Unsere Engine (llama-swap/llama.cpp) lädt <b>nur GGUF</b>.
Steht im Cookbook „keine GGUF", hat dieses HuggingFace-Repo schlicht keine passende Datei — dann ein
anderes Repo nehmen (oft gibt's „…-GGUF"-Varianten).</p>`],
["Quantisierung (z.B. Q4_K_M)?", `
<p>Modelle sind riesig. <b>Quantisierung</b> verkleinert sie, indem die Zahlen gröber gespeichert werden
(16-Bit → 4-Bit) — sie passen dann in den Speicher und laufen schneller, bei kaum spürbarem
Qualitätsverlust. <b>Q4_K_M</b> ist der beliebte Sweetspot aus Größe und Qualität. Höhere Zahl (Q6, Q8)
= etwas besser, aber größer/langsamer.</p>`],
["Kontextfenster — das Gedächtnis", `
<p>Das <b>Kontextfenster</b> ist das Kurzzeitgedächtnis: wie viel Text (Tokens) sich das Modell
<i>gleichzeitig</i> merken kann. <b>Größer</b> = es überblickt längere Dateien/Gespräche, braucht aber
<b>mehr Speicher</b> und wird etwas langsamer. Fürs Coden (viele Dateien) hilft groß; für kurzen Chat
reicht klein. Mission Control rechnet dir das <b>Optimum für deine Hardware</b> aus.</p>`],
["Swapping — warum nur ein Modell läuft", `
<p>llama-swap lädt <b>immer nur ein Modell gleichzeitig</b> und tauscht es bei Bedarf automatisch aus.
Vorteil: Du kannst viele Modelle einrichten, ohne dass sie sich den Speicher teilen müssen.
Heißt auch: Der Speicherbedarf deines Setups = das <b>größte</b> Einzelmodell, <b>nicht</b> die Summe.</p>`],
["MCP, Skills & Agenten", `
<p><b>Agent:</b> Ein Programm, das das Modell <i>selbstständig</i> Aufgaben erledigen lässt — Dateien
lesen/schreiben, Befehle ausführen, mehrere Schritte planen (z.B. OpenCode, Cline).</p>
<p><b>MCP (Model Context Protocol):</b> Ein Standard, über den ein Agent dem Modell <b>zusätzliche
Werkzeuge</b> gibt — z.B. „durchsuche das Web", „lies meine Datenbank", „steuere Tool X". So wird aus
einem reinen Textmodell ein nützlicher Helfer mit Zugriff auf echte Tools.</p>
<p><b>Skills:</b> Vorgefertigte Fähigkeiten/Anleitungen, die ein Agent für wiederkehrende Aufgaben nutzt.</p>`],
];
function mount() {
const c = $(".view[data-view='guides']"); if (!c) return;
c.innerHTML = `
<div class="pagehead"><div>
<h1>Guides &amp; Integrationen</h1>
<div class="sub">Binde deine lokalen Modelle in andere Tools ein — fertige Configs zum Kopieren.</div></div></div>
<h1>Guide für Einsteiger</h1>
<div class="sub">Die Local-LLM-Welt in Klartext — ohne Vorwissen verständlich.</div></div></div>
<div class="card" style="padding:0;overflow:hidden">
<details class="guide-acc">
<summary>Cline / Cursor</summary>
<div class="acc-body">
<p>Nutze deine lokalen Modelle kostenlos in Cursor oder Cline (Provider: „OpenAI Compatible").</p>
${field("Modell-ID", "coder")}
${field("Basis-URL (OpenAI-kompatibel)", url)}
${field("API-Key", "(dein Token oder leer lassen)")}
</div>
</details>
<details class="guide-acc">
<summary>OpenWebUI</summary>
<div class="acc-body">
<p>Settings → Admin → Connections → neue OpenAI-Verbindung:</p>
${field("Basis-URL", url)}
${field("API-Key", "dummy-key")}
<p class="hint" style="margin-top:10px">OpenWebUI erkennt automatisch, wenn ein Modell getauscht wird.</p>
</div>
</details>
<details class="guide-acc">
<summary>Python / LangChain (openai-SDK)</summary>
<div class="acc-body">
<p>Mit dem offiziellen <code>openai</code>-Paket:</p>
<div style="position:relative">
<button class="ghost" style="position:absolute;top:8px;right:8px;z-index:1" onclick="window.mcCopy(this,'code')">Kopieren</button>
<div class="log" style="max-height:none"><code>${esc(py)}</code></div>
</div>
</div>
</details>
<details class="guide-acc">
<summary>n8n (AI-Agent-Nodes)</summary>
<div class="acc-body">
<p>„OpenAI Chat Model"-Node → Credentials → Custom URL:</p>
${field("Basis-URL", url)}
<p class="hint" style="margin-top:10px">Modell-ID „coder" eintragen und loslegen.</p>
</div>
</details>
<details class="guide-acc">
<summary>Begriffe einfach erklärt (Glossar)</summary>
<div class="acc-body">
<p><b>LLM-Engine (llama-swap):</b> Der Dienst im Hintergrund, der die Sprachmodelle lädt und eine OpenAI-kompatible Schnittstelle bereitstellt.</p>
<p><b>VRAM / Grafikspeicher:</b> Der schnelle Speicher, in dem ein Modell laufen muss. Große Modelle brauchen viel davon.</p>
<p><b>Quantisierung (z.B. Q4_K_M):</b> Verkleinert ein Modell (16-Bit → 4-Bit), damit es in den Speicher passt — bei kaum Qualitätsverlust.</p>
<p><b>Kontext-Größe:</b> Wie viel Text sich das Modell gleichzeitig „merken" kann. Mehr Kontext = deutlich mehr Speicherbedarf.</p>
</div>
</details>
${SECTIONS.map(([title, body], i) => `
<details class="guide-acc"${i === 0 ? " open" : ""}>
<summary>${title}</summary>
<div class="acc-body">${body}</div>
</details>`).join("")}
</div>`;
}
function mount() { render(); }
export default { id: "guides", mount };
+10 -7
View File
@@ -1,7 +1,9 @@
// models.js — "Modelle"-Ansicht (v3): Schnelltest-Chat, Modell-Tabelle, Kontext-Konfiguration.
import { api } from "../core/api.js";
import { $, badge, esc, toast, confirmModal } from "../core/ui.js";
import { $, badge, esc, toast, confirmModal, infoDot } from "../core/ui.js";
const CTX_HELP = "Kontext = das Kurzzeitgedächtnis des Modells. Größer = merkt sich mehr (längere Dateien/Chats), braucht aber mehr Speicher und wird etwas langsamer.";
let ALL = [];
function refreshSoon() { document.dispatchEvent(new Event("mc:refresh")); }
@@ -53,14 +55,14 @@ function mount() {
<button id="cfg-close" class="ghost" style="position:absolute;top:14px;right:14px">Schließen</button>
<h3>Modell konfigurieren</h3>
<p class="mono-sm" id="cfg-model-name" style="margin:-4px 0 16px"></p>
<label>Kontext-Größe (Tokens)</label>
<label>Kontext-Größe (Tokens) ${infoDot(CTX_HELP)}</label>
<input id="cfg-ctx" type="number" value="8192">
<div class="hint">Höhere Werte erlauben längere Texte, brauchen aber mehr Grafikspeicher.</div>
<div class="tile" id="cfg-rec" style="display:none;margin:0 0 14px;justify-content:space-between;align-items:center" >
<div class="tile" id="cfg-rec" style="display:none;margin:8px 0 10px;justify-content:space-between;align-items:center" >
<div style="flex:1"><div style="font-size:13px">Empfohlen für deine Hardware</div>
<div class="hint" id="cfg-rec-note" style="margin:4px 0 0"></div></div>
<button class="ghost" id="cfg-rec-apply">Optimal übernehmen</button>
</div>
<div class="hint" style="margin-bottom:14px">Es läuft immer nur <b>ein</b> Modell gleichzeitig — ein größerer Kontext hier beeinflusst deine anderen Modelle nicht.</div>
<button class="primary" id="cfg-save" style="width:100%;margin-top:6px">Speichern</button>
</div>
</div>`;
@@ -104,9 +106,10 @@ function openConfig(alias) {
$("#cfg-ctx").value = m.meta?.ctx || 8192;
const opt = m.meta?.optimal_ctx, rec = $("#cfg-rec");
if (opt) {
const cur = m.meta?.ctx || 0;
$("#cfg-rec-note").textContent =
`Bis ~${Math.round(opt / 1024)}k passt komfortabel. Aktuell: ~${Math.round(cur / 1024)}k.`;
const cur = m.meta?.ctx || 0, peak = m.meta?.peak_ram_gb, peakOpt = m.meta?.peak_ram_optimal_gb;
$("#cfg-rec-note").innerHTML =
`Optimal ~${Math.round(opt / 1024)}k → ~${peakOpt} GB Spitzenbedarf.<br>` +
`Aktuell ~${Math.round(cur / 1024)}k → ~${peak} GB.`;
$("#cfg-rec-apply").onclick = () => { $("#cfg-ctx").value = opt; };
rec.style.display = "flex";
} else { rec.style.display = "none"; }