Feinschliff Phase 2: Dashboard Redesign, RAM Check, Accordions
This commit is contained in:
@@ -104,6 +104,14 @@ function mount() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="cb-m-fit-container" style="margin-top:24px; padding:16px; background:var(--bg); border:1px solid var(--line); border-radius:8px; display:flex; justify-content:space-between; align-items:center;">
|
||||
<div>
|
||||
<div style="font-weight:600; font-size:14px; margin-bottom:4px;">Ressourcen-Check</div>
|
||||
<div class="meta" id="cb-m-fit-text">Berechne...</div>
|
||||
</div>
|
||||
<div id="cb-m-fit-badge"></div>
|
||||
</div>
|
||||
|
||||
<button class="primary" id="cb-m-download" style="width:100%; margin-top:24px; padding:12px">Herunterladen & Einpflegen</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -113,10 +121,47 @@ function mount() {
|
||||
$("#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("input", updateLiveFit);
|
||||
|
||||
renderCurated();
|
||||
}
|
||||
|
||||
function extractParamsB(name) {
|
||||
const moe = name.match(/(\d+)x(\d+(?:\.\d+)?)[bB]/i);
|
||||
if (moe) return parseInt(moe[1]) * parseFloat(moe[2]);
|
||||
const m = name.match(/(\d+(?:\.\d+)?)[bB](?![a-zA-Z])/i);
|
||||
if (m) return parseFloat(m[1]);
|
||||
return 7; // Fallback
|
||||
}
|
||||
|
||||
function extractQuant(filename) {
|
||||
const m = filename.match(/(Q\d_[A-Z0-9_]+|IQ\d_[A-Z0-9_]+|FP16|BF16)/i);
|
||||
return m ? m[1].toUpperCase() : "Q4_K_M";
|
||||
}
|
||||
|
||||
function updateLiveFit() {
|
||||
const repo = $("#cb-m-repo").textContent;
|
||||
const file = $("#cb-m-files").value;
|
||||
const ctx = parseInt($("#cb-m-ctx").value) || 8192;
|
||||
|
||||
if (!repo || !file) {
|
||||
$("#cb-m-fit-container").style.display = "none";
|
||||
return;
|
||||
}
|
||||
|
||||
const params_b = extractParamsB(repo);
|
||||
const quant = extractQuant(file);
|
||||
|
||||
const m = { params_b, quant, ctx };
|
||||
const fit = getFit(m, lastSys);
|
||||
|
||||
$("#cb-m-fit-container").style.display = "flex";
|
||||
$("#cb-m-fit-text").innerHTML = `Geschätzter Bedarf: <b>~${fit.req.toFixed(1)} GB RAM/VRAM</b> <br><small class="meta">${params_b}B Params · ${quant}</small>`;
|
||||
$("#cb-m-fit-badge").innerHTML = `<span class="badge ${fit.class}">${fit.text}</span>`;
|
||||
}
|
||||
|
||||
async function doSearch() {
|
||||
const q = $("#cb-search").value.trim();
|
||||
if (!q) return renderCurated();
|
||||
@@ -127,13 +172,13 @@ async function doSearch() {
|
||||
$("#cb-grid").innerHTML = `<div class="meta" style="grid-column:1/-1; text-align:center; padding:40px">Suche auf HuggingFace...</div>`;
|
||||
|
||||
try {
|
||||
const url = \`https://huggingface.co/api/models?search=\${encodeURIComponent(q)}&filter=gguf&sort=downloads&direction=-1&limit=12\`;
|
||||
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 = \`<div class="alert err" style="grid-column:1/-1">\${esc(e.message)}</div>\`;
|
||||
$("#cb-grid").innerHTML = `<div class="alert err" style="grid-column:1/-1">${esc(e.message)}</div>`;
|
||||
}
|
||||
btn.disabled = false; btn.textContent = "Suchen";
|
||||
}
|
||||
@@ -145,17 +190,17 @@ function renderResults(results) {
|
||||
return;
|
||||
}
|
||||
|
||||
grid.innerHTML = results.map((m, i) => \`
|
||||
<div class="card" style="display:flex; flex-direction:column; cursor:pointer" onclick="window.openModelModal(\${i})">
|
||||
<h3 style="margin:0; font-size:16px; word-break:break-all">\${esc(m.id.split('/').pop())}</h3>
|
||||
<div class="meta" style="font-size:12px; margin-top:4px">\${esc(m.author)}</div>
|
||||
grid.innerHTML = results.map((m, i) => `
|
||||
<div class="card" style="display:flex; flex-direction:column; cursor:pointer" onclick="window.openModelModal(${i})">
|
||||
<h3 style="margin:0; font-size:16px; word-break:break-all">${esc(m.id.split('/').pop())}</h3>
|
||||
<div class="meta" style="font-size:12px; margin-top:4px">${esc(m.author)}</div>
|
||||
<div style="flex:1; margin-top:16px;"></div>
|
||||
<div style="display:flex; justify-content:space-between; align-items:center; margin-top:16px">
|
||||
<span class="badge b-idle" style="font-size:11px">GGUF</span>
|
||||
<span class="meta" style="font-size:12px">⬇ \${(m.downloads||0).toLocaleString()}</span>
|
||||
<span class="meta" style="font-size:12px">⬇ ${(m.downloads||0).toLocaleString()}</span>
|
||||
</div>
|
||||
</div>
|
||||
\`).join("");
|
||||
`).join("");
|
||||
}
|
||||
|
||||
// Global hook for inline onclick
|
||||
@@ -172,7 +217,7 @@ window.openModelModal = async (index) => {
|
||||
$("#cb-m-loading").style.display = "block";
|
||||
|
||||
try {
|
||||
const url = \`https://huggingface.co/api/models/\${m.id}/tree/main\`;
|
||||
const url = `https://huggingface.co/api/models/${m.id}/tree/main`;
|
||||
const r = await fetch(url);
|
||||
const tree = await r.json();
|
||||
const files = tree.filter(f => f.path.endsWith('.gguf')).map(f => f.path);
|
||||
@@ -183,9 +228,11 @@ window.openModelModal = async (index) => {
|
||||
if (files.length === 0) {
|
||||
$("#cb-m-files").innerHTML = "<option value=''>Keine GGUF-Dateien im Hauptverzeichnis gefunden.</option>";
|
||||
$("#cb-m-download").disabled = true;
|
||||
$("#cb-m-fit-container").style.display = "none";
|
||||
} else {
|
||||
$("#cb-m-files").innerHTML = files.map(f => \`<option value="\${esc(f)}">\${esc(f)}</option>\`).join("");
|
||||
$("#cb-m-files").innerHTML = files.map(f => `<option value="${esc(f)}">${esc(f)}</option>`).join("");
|
||||
$("#cb-m-download").disabled = false;
|
||||
updateLiveFit();
|
||||
}
|
||||
} catch(e) {
|
||||
$("#cb-m-loading").textContent = "Fehler beim Laden der Dateien.";
|
||||
@@ -235,21 +282,21 @@ function renderCurated() {
|
||||
if (!grid) return;
|
||||
grid.innerHTML = CURATED_MODELS.map((m, i) => {
|
||||
const fit = getFit(m, lastSys);
|
||||
return \`
|
||||
<div class="card" style="display:flex; flex-direction:column; cursor:pointer" onclick="window.openCuratedModal(\${i})">
|
||||
return `
|
||||
<div class="card" style="display:flex; flex-direction:column; cursor:pointer" onclick="window.openCuratedModal(${i})">
|
||||
<div style="display:flex; justify-content:space-between; align-items:center;">
|
||||
<h3 style="margin:0; font-size:16px">\${esc(m.name)}</h3>
|
||||
<span class="badge \${fit.class}">\${fit.text}</span>
|
||||
<h3 style="margin:0; font-size:16px">${esc(m.name)}</h3>
|
||||
<span class="badge ${fit.class}">${fit.text}</span>
|
||||
</div>
|
||||
<div style="font-size:13px; color:var(--mut); margin-top:12px; flex:1; line-height:1.5;">
|
||||
\${m.desc}
|
||||
${m.desc}
|
||||
</div>
|
||||
<div style="display:flex; justify-content:space-between; margin-top:16px; font-size:12px" class="meta">
|
||||
<span>~\${fit.req.toFixed(1)} GB RAM</span>
|
||||
<span>\${m.quant}</span>
|
||||
<span>~${fit.req.toFixed(1)} GB RAM</span>
|
||||
<span>${m.quant}</span>
|
||||
</div>
|
||||
</div>
|
||||
\`;
|
||||
`;
|
||||
}).join("");
|
||||
}
|
||||
|
||||
@@ -259,12 +306,13 @@ window.openCuratedModal = (index) => {
|
||||
$("#cb-modal").style.display = "flex";
|
||||
$("#cb-m-title").textContent = m.name;
|
||||
$("#cb-m-repo").textContent = m.repo;
|
||||
$("#cb-m-files").innerHTML = \`<option value="\${esc(m.file)}">\${esc(m.file)}</option>\`;
|
||||
$("#cb-m-files").innerHTML = `<option value="${esc(m.file)}">${esc(m.file)}</option>`;
|
||||
$("#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;
|
||||
updateLiveFit();
|
||||
};
|
||||
|
||||
function onSystem(sys) {
|
||||
|
||||
+60
-39
@@ -7,37 +7,44 @@ function render() {
|
||||
if (!c) return;
|
||||
|
||||
c.innerHTML = `
|
||||
<div class="grid grid-2">
|
||||
<div class="card">
|
||||
<div class="card-h"><h3>Cline / Cursor (Juni 2026)</h3></div>
|
||||
<p>Nutze deine lokalen Modelle kostenlos in Cursor und Cline.</p>
|
||||
<label>Cursor: Settings -> Models -> Add Custom Model</label>
|
||||
<input class="tokin mono-sm" readonly value="coder">
|
||||
<label>Base URL (OpenAI API)</label>
|
||||
<input class="tokin mono-sm" readonly value="${currentUrl}/v1">
|
||||
<label>API Key</label>
|
||||
<input class="tokin mono-sm" readonly value="MissionControl-Token-oder-leer">
|
||||
<p style="margin-top:12px; font-size:12px; color:var(--mut);">
|
||||
In Cline: Wähle "OpenAI Compatible" als Provider.
|
||||
</p>
|
||||
</div>
|
||||
<div class="card-h"><h3>Guides & Integrationen</h3></div>
|
||||
<div class="card" style="padding:0; overflow:hidden;">
|
||||
|
||||
<details class="guide-acc">
|
||||
<summary>Cline / Cursor (Juni 2026)</summary>
|
||||
<div class="acc-body">
|
||||
<p>Nutze deine lokalen Modelle kostenlos in der Cursor IDE und Cline.</p>
|
||||
<label>Cursor: Settings -> Models -> Add Custom Model</label>
|
||||
<input class="tokin mono-sm" readonly value="coder">
|
||||
<label>Base URL (OpenAI API)</label>
|
||||
<input class="tokin mono-sm" readonly value="${currentUrl}/v1">
|
||||
<label>API Key</label>
|
||||
<input class="tokin mono-sm" readonly value="MissionControl-Token-oder-leer">
|
||||
<p style="margin-top:12px; font-size:12px; color:var(--mut);">
|
||||
In Cline: Wähle "OpenAI Compatible" als Provider.
|
||||
</p>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-h"><h3>OpenWebUI (Pipes & Connections)</h3></div>
|
||||
<p>Verbinde llama-swap nativ in OpenWebUI.</p>
|
||||
<label>Settings -> Admin Settings -> Connections</label>
|
||||
<input class="tokin mono-sm" readonly value="${currentUrl}/v1">
|
||||
<label>API Key</label>
|
||||
<input class="tokin mono-sm" readonly value="dummy-key">
|
||||
<p style="margin-top:12px; font-size:12px; color:var(--mut);">
|
||||
OpenWebUI erkennt nun vollautomatisch, wenn ein Modell ausgetauscht wird.
|
||||
</p>
|
||||
</div>
|
||||
<details class="guide-acc">
|
||||
<summary>OpenWebUI (Pipes & Connections)</summary>
|
||||
<div class="acc-body">
|
||||
<p>Verbinde llama-swap nativ in OpenWebUI.</p>
|
||||
<label>Settings -> Admin Settings -> Connections</label>
|
||||
<input class="tokin mono-sm" readonly value="${currentUrl}/v1">
|
||||
<label>API Key</label>
|
||||
<input class="tokin mono-sm" readonly value="dummy-key">
|
||||
<p style="margin-top:12px; font-size:12px; color:var(--mut);">
|
||||
OpenWebUI erkennt nun vollautomatisch, wenn ein Modell ausgetauscht wird.
|
||||
</p>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<div class="card" style="grid-column: span 2;">
|
||||
<div class="card-h"><h3>Python / LangChain (OpenAI SDK)</h3></div>
|
||||
<p>Nutze das offizielle <code>openai</code> Package, um mit llama-swap zu sprechen.</p>
|
||||
<pre style="background: var(--bg); padding: 12px; border-radius: 8px; border: 1px solid var(--line); overflow-x: auto; color: var(--tx); font-family: monospace; font-size: 13px;"><code>from openai import OpenAI
|
||||
<details class="guide-acc">
|
||||
<summary>Python / LangChain (OpenAI SDK)</summary>
|
||||
<div class="acc-body">
|
||||
<p>Nutze das offizielle <code>openai</code> Package, um mit llama-swap zu sprechen.</p>
|
||||
<pre style="background: var(--bg); padding: 12px; border-radius: 8px; border: 1px solid var(--line); overflow-x: auto; color: var(--tx); font-family: monospace; font-size: 13px;"><code>from openai import OpenAI
|
||||
|
||||
client = OpenAI(
|
||||
base_url="${currentUrl}/v1",
|
||||
@@ -50,17 +57,31 @@ response = client.chat.completions.create(
|
||||
)
|
||||
|
||||
print(response.choices[0].message.content)</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<details class="guide-acc">
|
||||
<summary>n8n (AI Agent Nodes)</summary>
|
||||
<div class="acc-body">
|
||||
<p>Nutze den "OpenAI Chat Model" Node in n8n Advanced AI.</p>
|
||||
<label>Credentials -> OpenAI API -> Custom URL</label>
|
||||
<input class="tokin mono-sm" readonly value="${currentUrl}/v1">
|
||||
<p style="margin-top:12px; font-size:12px; color:var(--mut);">
|
||||
Einfach den Node verbinden, "coder" als Modell-ID (Expression) eintippen und loslegen.
|
||||
</p>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<details class="guide-acc">
|
||||
<summary>Begrifflichkeiten (Glossar)</summary>
|
||||
<div class="acc-body">
|
||||
<p><b>LLM-Engine (llama-swap):</b> Der Server im Hintergrund, der die Sprachmodelle (LLMs) lädt und OpenAI-kompatible Schnittstellen bereitstellt.</p>
|
||||
<p><b>VRAM:</b> Der Grafikkarten-Speicher. KI-Modelle sind extrem groß und benötigen viel VRAM, um schnell zu laufen.</p>
|
||||
<p><b>Quantisierung (z.B. Q4_K_M):</b> Ein Verfahren, das die Genauigkeit der Modellgewichte leicht reduziert (von 16-Bit auf 4-Bit), damit sie in den VRAM passen, ohne signifikant dümmer zu werden.</p>
|
||||
<p><b>Context-Window:</b> Wie viel Text ("Tokens") sich das Modell gleichzeitig "merken" kann. Ein Buchstabe ist ~0.3 Tokens. Mehr Kontext braucht drastisch mehr VRAM.</p>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<div class="card" style="grid-column: span 2;">
|
||||
<div class="card-h"><h3>n8n (AI Agent Nodes)</h3></div>
|
||||
<p>Nutze den "OpenAI Chat Model" Node in n8n Advanced AI.</p>
|
||||
<label>Credentials -> OpenAI API -> Custom URL</label>
|
||||
<input class="tokin mono-sm" readonly value="${currentUrl}/v1">
|
||||
<p style="margin-top:12px; font-size:12px; color:var(--mut);">
|
||||
Einfach den Node verbinden, "coder" als Modell-ID (Expression) eintippen und loslegen.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
+66
-13
@@ -1,8 +1,10 @@
|
||||
// models.js — "Modelle"-Ansicht: Download + Einpflegen, Schnelltest-Chat, Modell-Tabelle.
|
||||
// models.js — "Modelle"-Ansicht: Schnelltest-Chat, Modell-Tabelle & Konfiguration.
|
||||
|
||||
import { api } from "../core/api.js";
|
||||
import { $, badge, esc, toast, icon } from "../core/ui.js";
|
||||
|
||||
let ALL_MODELS = [];
|
||||
|
||||
function refreshSoon() { document.dispatchEvent(new Event("mc:refresh")); }
|
||||
|
||||
function mount() {
|
||||
@@ -13,43 +15,63 @@ function mount() {
|
||||
<select id="chat-model"></select>
|
||||
<label>Nachricht</label>
|
||||
<textarea id="chat-msg" placeholder="Schreib was, um ein Modell zu wecken…"></textarea>
|
||||
<button class="primary" id="chat-btn">Senden</button>
|
||||
<button class="primary" id="chat-btn" title="Sendet eine Chat-Anfrage an das Modell (weckt es auf, falls es schläft)">Senden</button>
|
||||
<div id="chat-reply" class="reply" style="display:none"></div>`;
|
||||
|
||||
$("#m-table").innerHTML = `
|
||||
<div class="card-h"><h3>Modelle & Ports</h3><span class="meta" id="m-count"></span></div>
|
||||
<div class="hint" style="margin-bottom: 16px;">
|
||||
💡 <b>Modelle werden automatisch geladen</b>, sobald eine Chat-Anfrage (z.B. über OpenWebUI oder Cursor) an sie gestellt wird. Du musst sie hier nicht manuell starten.
|
||||
💡 <b>Modelle werden automatisch geladen</b>, sobald eine Chat-Anfrage an sie gestellt wird. Du musst sie nicht manuell starten.
|
||||
</div>
|
||||
<table>
|
||||
<thead><tr><th>Modell</th><th>Fähigkeiten</th><th>Details</th><th>Status</th><th>Port</th><th style="text-align:right">Aktion</th></tr></thead>
|
||||
<thead><tr><th>Modell</th><th>Fähigkeiten</th><th>Details</th><th>Status</th><th>Port</th><th style="text-align:right">Aktionen</th></tr></thead>
|
||||
<tbody id="models"></tbody>
|
||||
</table>
|
||||
<div id="models-empty" class="empty" style="display:none">Noch keine Modelle konfiguriert — zieh dir oben eins rein. 👇</div>`;
|
||||
<div id="models-empty" class="empty" style="display:none">Noch keine Modelle konfiguriert — zieh dir oben eins rein. 👇</div>
|
||||
|
||||
<!-- Config Modal -->
|
||||
<div id="cfg-modal" style="display:none; position:fixed; top:0; left:0; right:0; bottom:0; background:rgba(0,0,0,0.8); z-index:100; align-items:center; justify-content:center;">
|
||||
<div class="card" style="width:100%; max-width:400px; position:relative">
|
||||
<button id="cfg-close" class="ghost" style="position:absolute; top:12px; right:12px;">Schließen</button>
|
||||
<h2 style="margin-top:0">Modell konfigurieren</h2>
|
||||
<p class="meta" id="cfg-model-name"></p>
|
||||
|
||||
<div style="margin-top:24px">
|
||||
<label>Context-Size (Tokens)</label>
|
||||
<input id="cfg-ctx" type="number" class="tokin" value="8192">
|
||||
<div class="meta" style="font-size:12px; margin-top:4px;">Höhere Werte erlauben längere Dokumente, brauchen aber mehr VRAM.</div>
|
||||
</div>
|
||||
|
||||
<button class="primary" id="cfg-save" style="width:100%; margin-top:24px; padding:12px">Speichern</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
$("#chat-btn").addEventListener("click", sendChat);
|
||||
$("#cfg-close").addEventListener("click", () => $("#cfg-modal").style.display = "none");
|
||||
$("#cfg-save").addEventListener("click", saveConfig);
|
||||
}
|
||||
|
||||
function onStatus(s) {
|
||||
const models = s?.models || [];
|
||||
ALL_MODELS = s?.models || [];
|
||||
const tb = $("#models");
|
||||
if (!tb) return;
|
||||
tb.innerHTML = "";
|
||||
$("#models-empty").style.display = models.length ? "none" : "block";
|
||||
$("#m-count").textContent = models.length ? models.length + " konfiguriert" : "";
|
||||
$("#models-empty").style.display = ALL_MODELS.length ? "none" : "block";
|
||||
$("#m-count").textContent = ALL_MODELS.length ? ALL_MODELS.length + " konfiguriert" : "";
|
||||
|
||||
const sel = $("#chat-model");
|
||||
const cur = sel.value;
|
||||
sel.innerHTML = "";
|
||||
for (const m of models) {
|
||||
for (const m of ALL_MODELS) {
|
||||
const tr = document.createElement("tr");
|
||||
|
||||
let capsHtml = "–";
|
||||
if (m.meta && m.meta.caps) {
|
||||
capsHtml = m.meta.caps.map(c => {
|
||||
if (c === "Text") return `<span class="meta" title="Text" style="display:inline-block;width:18px">${icon("search")}</span>`;
|
||||
if (c === "Code") return `<strong style="color:var(--blue);display:inline-block;width:18px" title="Code">${icon("code")}</strong>`;
|
||||
if (c === "Bild") return `<strong style="color:var(--purple);display:inline-block;width:18px" title="Vision">${icon("eye")}</strong>`;
|
||||
if (c === "Text") return `<span class="meta" title="Text" style="display:inline-block;width:18px">${icon("compass")}</span>`;
|
||||
if (c === "Code") return `<strong style="color:var(--blue);display:inline-block;width:18px" title="Code">{ }</strong>`;
|
||||
if (c === "Bild") return `<strong style="color:var(--purple);display:inline-block;width:18px" title="Vision">👁</strong>`;
|
||||
return "";
|
||||
}).join(" ");
|
||||
}
|
||||
@@ -71,14 +93,45 @@ function onStatus(s) {
|
||||
<td>${detailsHtml}</td>
|
||||
<td>${badge(m.state)}${perfHtml}</td>
|
||||
<td class="port">${m.port ?? "auto"}</td>
|
||||
<td style="text-align:right"><button class="ghost" data-unload="${esc(m.name)}">Aus dem VRAM entfernen</button></td>`;
|
||||
<td style="text-align:right">
|
||||
<button class="ghost" data-cfg="${esc(m.name)}" title="Context-Size anpassen">Konfigurieren</button>
|
||||
<button class="ghost" data-unload="${esc(m.name)}" title="Beendet das Modell sofort und gibt den VRAM wieder frei">Aus VRAM entfernen</button>
|
||||
</td>`;
|
||||
tb.appendChild(tr);
|
||||
sel.insertAdjacentHTML("beforeend", `<option>${esc(m.name)}</option>`);
|
||||
}
|
||||
if (cur) sel.value = cur;
|
||||
|
||||
tb.querySelectorAll("[data-unload]").forEach(b =>
|
||||
b.addEventListener("click", () => unloadOne(b.getAttribute("data-unload")))
|
||||
);
|
||||
tb.querySelectorAll("[data-cfg]").forEach(b =>
|
||||
b.addEventListener("click", () => openConfig(b.getAttribute("data-cfg")))
|
||||
);
|
||||
}
|
||||
|
||||
function openConfig(alias) {
|
||||
const m = ALL_MODELS.find(x => x.name === alias);
|
||||
if (!m) return;
|
||||
$("#cfg-model-name").textContent = m.name;
|
||||
$("#cfg-ctx").value = m.meta?.ctx || 8192;
|
||||
$("#cfg-modal").style.display = "flex";
|
||||
}
|
||||
|
||||
async function saveConfig() {
|
||||
const alias = $("#cfg-model-name").textContent;
|
||||
const ctx = parseInt($("#cfg-ctx").value) || 8192;
|
||||
|
||||
$("#cfg-save").disabled = true;
|
||||
try {
|
||||
await api("/api/update_model", { method: "POST", body: JSON.stringify({ alias, ctx }) });
|
||||
toast("Gespeichert! Änderungen werden beim nächsten Modell-Start aktiv.");
|
||||
$("#cfg-modal").style.display = "none";
|
||||
refreshSoon();
|
||||
} catch (e) {
|
||||
toast(e.message, true);
|
||||
}
|
||||
$("#cfg-save").disabled = false;
|
||||
}
|
||||
|
||||
async function unloadOne(m) {
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
// overview.js — Dashboard-Kopf: Hero + kompakte Modell-Liste.
|
||||
// overview.js — Dashboard: Quick Actions, Modelle & Recent Jobs
|
||||
|
||||
import { $, esc } from "../core/ui.js";
|
||||
import { $, esc, icon } from "../core/ui.js";
|
||||
|
||||
let S = null; // letzter Status
|
||||
let J = []; // letzte Job-Liste
|
||||
let SYS = null;
|
||||
|
||||
const RUNNING = new Set(["running", "ready", "loading", "starting"]);
|
||||
|
||||
@@ -12,34 +13,48 @@ function counts() {
|
||||
return {
|
||||
total: models.length,
|
||||
running: models.filter(m => RUNNING.has(m.state)).length,
|
||||
jobsRun: J.filter(j => j.state === "running" || j.state === "queued").length,
|
||||
jobsErr: J.filter(j => j.state === "failed").length,
|
||||
};
|
||||
}
|
||||
|
||||
function mini(label, val, tone = "") {
|
||||
const v = tone ? `<b style="${tone === "bad" ? "color:var(--err)" : ""}">${val}</b>` : val;
|
||||
return `<div class="mini"><div class="l">${label}</div><div class="v">${v}</div></div>`;
|
||||
}
|
||||
|
||||
function renderHero() {
|
||||
const c = counts();
|
||||
$("#hero").innerHTML = `<div class="hero">
|
||||
<div>
|
||||
<div class="eyebrow">Übersicht</div>
|
||||
<div class="eyebrow">Dashboard</div>
|
||||
<h1>Mission Control</h1>
|
||||
<p>Steuerzentrale für deinen lokalen llama-swap-Stack — Modelle, Downloads,
|
||||
Wartung und Schnelltest an einem Ort.</p>
|
||||
</div>
|
||||
<div class="hero-stats">
|
||||
${mini("Modelle", c.total)}
|
||||
${mini("Aktiv", c.running, "on")}
|
||||
${mini("Jobs", c.jobsRun)}
|
||||
${mini("Fehler", c.jobsErr, c.jobsErr ? "bad" : "")}
|
||||
<p>Steuerzentrale für deinen lokalen llama-swap-Stack. Hier verwaltest du Modelle, Downloads und Server-Wartung.</p>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function renderQuickActions() {
|
||||
// 3 Kacheln (Cookbook, Server-Status, Aktivität/Guides)
|
||||
$("#ov-quick").innerHTML = `
|
||||
<div class="card" style="cursor:pointer; display:flex; flex-direction:column; gap:12px; transition:border-color 0.2s" onclick="document.querySelector('.nav-item[data-view=\\'cookbook\\']').click()" onmouseover="this.style.borderColor='var(--act)'" onmouseout="this.style.borderColor='var(--line)'">
|
||||
<div style="display:flex; justify-content:space-between; align-items:center;">
|
||||
<h3 style="margin:0; font-size:16px;">Modell finden</h3>
|
||||
<span style="color:var(--act)">${icon("book")}</span>
|
||||
</div>
|
||||
<p style="margin:0; font-size:13px; color:var(--mut);">Durchsuche HuggingFace nach neuen Modellen im Cookbook.</p>
|
||||
</div>
|
||||
|
||||
<div class="card" style="cursor:pointer; display:flex; flex-direction:column; gap:12px; transition:border-color 0.2s" onclick="document.querySelector('.nav-item[data-view=\\'activity\\']').click()" onmouseover="this.style.borderColor='var(--act)'" onmouseout="this.style.borderColor='var(--line)'">
|
||||
<div style="display:flex; justify-content:space-between; align-items:center;">
|
||||
<h3 style="margin:0; font-size:16px;">Live Metriken</h3>
|
||||
<span style="color:var(--act)">${icon("pulse")}</span>
|
||||
</div>
|
||||
<p style="margin:0; font-size:13px; color:var(--mut);">${SYS ? `System läuft (RAM: ${SYS.ram.percent.toFixed(0)}%, CPU: ${SYS.cpu.percent.toFixed(0)}%)` : 'Lade Metriken...'}</p>
|
||||
</div>
|
||||
|
||||
<div class="card" style="cursor:pointer; display:flex; flex-direction:column; gap:12px; transition:border-color 0.2s" onclick="document.querySelector('.nav-item[data-view=\\'server\\']').click()" onmouseover="this.style.borderColor='var(--act)'" onmouseout="this.style.borderColor='var(--line)'">
|
||||
<div style="display:flex; justify-content:space-between; align-items:center;">
|
||||
<h3 style="margin:0; font-size:16px;">Wartung</h3>
|
||||
<span style="color:var(--act)">${icon("server")}</span>
|
||||
</div>
|
||||
<p style="margin:0; font-size:13px; color:var(--mut);">Server neustarten, VRAM leeren oder Engine aktualisieren.</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function modelRow(m) {
|
||||
const on = RUNNING.has(m.state);
|
||||
const dot = m.state === "loading" || m.state === "starting" ? "load" : on ? "on" : "";
|
||||
@@ -53,15 +68,16 @@ function modelRow(m) {
|
||||
return "";
|
||||
}).join("");
|
||||
}
|
||||
|
||||
const filename = m.meta?.filename ? `<div class="li-sub" style="font-family:var(--mono); color:var(--mut);">${esc(m.meta.filename)}</div>` : '';
|
||||
|
||||
return `<div class="li">
|
||||
<span class="li-dot ${dot}"></span>
|
||||
<div class="li-main">
|
||||
<div class="li-id" style="font-weight:500">${esc(m.name)}${caps}</div>
|
||||
<div class="li-sub">TTL ${m.ttl ?? "—"}${typeof m.ttl === "number" ? "s" : ""}</div>
|
||||
${filename}
|
||||
</div>
|
||||
<div class="li-right">
|
||||
<div class="li-meta">${m.port ?? "auto"}</div>
|
||||
<div class="li-time">${state}</div>
|
||||
</div>
|
||||
</div>`;
|
||||
@@ -77,10 +93,43 @@ function renderModels() {
|
||||
<div class="e-s">Hol dir unter „Cookbook“ eins von HuggingFace.</div></div>`}`;
|
||||
}
|
||||
|
||||
function renderAll() { renderHero(); renderModels(); }
|
||||
function renderRecentJobs() {
|
||||
const latest = J.slice(0, 4);
|
||||
|
||||
const statusBadge = (s) => {
|
||||
if (s === "done") return '<span class="badge b-run" style="font-size:10px">fertig</span>';
|
||||
if (s === "failed") return '<span class="badge b-err" style="font-size:10px">fehler</span>';
|
||||
return '<span class="badge b-load" style="font-size:10px">lädt…</span>';
|
||||
};
|
||||
|
||||
$("#ov-recent-jobs").innerHTML = `
|
||||
<div class="card-h"><h3>Letzte Aktivitäten</h3><span class="meta" style="cursor:pointer" onclick="document.querySelector('.nav-item[data-view=\\'activity\\']').click()">Alle ansehen →</span></div>
|
||||
${latest.length
|
||||
? `<div class="list">
|
||||
${latest.map(j => `
|
||||
<div class="li" style="padding:10px 4px">
|
||||
<div class="li-main">
|
||||
<div class="li-id" style="font-size:12.5px">${esc(j.label)}</div>
|
||||
</div>
|
||||
<div class="li-right">${statusBadge(j.state)}</div>
|
||||
</div>
|
||||
`).join("")}
|
||||
</div>`
|
||||
: `<div class="empty-c"><div class="e-t">Keine Aktivitäten</div><div class="e-s">Alles läuft ruhig.</div></div>`
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
function renderAll() {
|
||||
renderHero();
|
||||
renderQuickActions();
|
||||
renderModels();
|
||||
renderRecentJobs();
|
||||
}
|
||||
|
||||
function mount() { renderAll(); }
|
||||
function onStatus(s) { S = s; renderAll(); }
|
||||
function onJobs(jobs) { J = jobs || []; renderHero(); }
|
||||
function onStatus(s) { S = s; renderModels(); }
|
||||
function onJobs(jobs) { J = jobs || []; renderRecentJobs(); }
|
||||
function onSystem(sys) { SYS = sys; renderQuickActions(); }
|
||||
|
||||
export default { id: "overview", mount, onStatus, onJobs };
|
||||
export default { id: "overview", mount, onStatus, onJobs, onSystem };
|
||||
|
||||
@@ -7,13 +7,11 @@ function refreshSoon() { document.dispatchEvent(new Event("mc:refresh")); }
|
||||
function mount() {
|
||||
$("#wartung").innerHTML = `
|
||||
<div class="card-h"><h3>Dienste & Applikation</h3></div>
|
||||
<div class="btn-row" style="margin-bottom:20px">
|
||||
<button id="w-restart-swap">llama-swap neustarten</button>
|
||||
<button id="w-restart-mc">Mission Control neustarten</button>
|
||||
</div>
|
||||
<div class="btn-row">
|
||||
<button id="w-update">Mission Control System-Update</button>
|
||||
<button id="w-unload" class="ghost">Aus dem VRAM entfernen</button>
|
||||
<button id="w-restart-swap" title="Startet die LLM-Engine neu. Dauer ca. 5 Sekunden.">llama-swap neustarten</button>
|
||||
<button id="w-restart-mc" title="Startet Mission Control (das Dashboard) neu.">Mission Control neustarten</button>
|
||||
<button id="w-update" title="Zieht den neuesten Code via Git und aktualisiert das Dashboard.">Mission Control System-Update</button>
|
||||
<button id="w-unload" class="ghost" title="Wirft alle derzeit in VRAM geladenen Modelle sofort raus.">Aus dem VRAM entfernen</button>
|
||||
</div>
|
||||
<div class="hint" style="margin-top:12px; margin-bottom:32px">
|
||||
Dienste starten via passwortlosem Sudo neu.
|
||||
@@ -21,8 +19,8 @@ function mount() {
|
||||
|
||||
<div class="card-h"><h3>Betriebssystem (Bosgame)</h3></div>
|
||||
<div class="btn-row">
|
||||
<button id="w-os-update">OS-Updates installieren (apt update)</button>
|
||||
<button id="w-reboot" class="danger">Server Reboot</button>
|
||||
<button id="w-os-update" title="Führt apt update & upgrade aus, um das Betriebssystem (Bosgame) zu aktualisieren.">OS-Updates installieren (apt update)</button>
|
||||
<button id="w-reboot" class="danger" title="Startet den gesamten Server physikalisch neu. Alles ist kurz offline.">Server Reboot</button>
|
||||
</div>
|
||||
<div class="hint" style="margin-top:12px; margin-bottom:32px">
|
||||
Für tiefe Eingriffe fragt das Dashboard einmalig das sudo-Passwort ab.
|
||||
|
||||
Reference in New Issue
Block a user