fix(phase-a): Kontext-Cap, Download-State, Recipe-Edit, OS-Badge, Swap-Flash
A1: 32768-Kontext-Cap entfernt (install-recipe, install-model, register) →
max_ctx_for() liefert nun bis zu 128k auf Strix Halo; behebt "context
size exceeded" bei externen Tools.
A2: Download-State jetzt im Status-Endpoint sichtbar: Modelle zeigen
"↓ Download X%" statt "bereit" während Job läuft (Backend + Frontend).
A3: PUT /api/cookbook/user-recipe/{id} + Edit-Button (✎) für eigene Setups.
Download-Modal setzt Kontext-Input automatisch auf optimal.
A4: /api/updates liefert apt_cache_age_h; Badge zeigt Tooltip + ⚠ wenn >24h.
A5: Swap-Flash: Topbar-Text pulst kurz teal wenn Modell den State wechselt.
A6: LLM-Engine-Update fragt jetzt per confirmModal nach (Konsistenz).
A7: Event-Delegation statt per-render addEventListener in models.js.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+45
-23
@@ -3,6 +3,8 @@
|
||||
import { api } from "../core/api.js";
|
||||
import { $, badge, esc, toast, confirmModal, infoDot } from "../core/ui.js";
|
||||
|
||||
let JOBS_CACHE = [];
|
||||
|
||||
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.";
|
||||
const ROLE_HELP = "Die Rolle ist ein zusätzlicher, sprechender Name (z.B. coder). Du kannst in deinen Tools entweder den echten Modellnamen ODER die Rolle angeben — beides führt zum selben Modell.";
|
||||
|
||||
@@ -18,6 +20,11 @@ const ROLES = [
|
||||
let ALL = [];
|
||||
function refreshSoon() { document.dispatchEvent(new Event("mc:refresh")); }
|
||||
|
||||
// Badge mit Download-Fortschritt aus Backend-State (download_progress vom status endpoint)
|
||||
function stateBadge(m) {
|
||||
return badge(m.state, m.download_progress);
|
||||
}
|
||||
|
||||
function roleTag(role) {
|
||||
return role ? `<span class="tag" style="background:rgba(45,212,191,.14);color:var(--accent);border-color:rgba(45,212,191,.3)">${esc(role)}</span>` : "";
|
||||
}
|
||||
@@ -26,7 +33,7 @@ function apiIdLine(m) {
|
||||
const ids = (m.api_ids && m.api_ids.length ? m.api_ids : [m.name]);
|
||||
return `<div class="li-sub mono-sm" style="margin-top:3px;display:flex;align-items:center;gap:6px;flex-wrap:wrap">
|
||||
<span class="text-mut">API-Name:</span>
|
||||
${ids.map(id => `<code style="cursor:pointer" data-copy="${esc(id)}" title="Klicken zum Kopieren">${esc(id)}</code>`).join('<span class="text-mut">·</span>')}
|
||||
${ids.map(id => `<code style="cursor:pointer" data-action="copy" data-name="${esc(id)}" title="Klicken zum Kopieren">${esc(id)}</code>`).join('<span class="text-mut">·</span>')}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
@@ -113,6 +120,23 @@ function mount() {
|
||||
$("#role-modal").addEventListener("click", e => { if (e.target.id === "role-modal") $("#role-modal").style.display = "none"; });
|
||||
$("#role-save").addEventListener("click", () => saveRole($("#role-custom").value.trim().toLowerCase()));
|
||||
$("#role-clear").addEventListener("click", () => saveRole(""));
|
||||
|
||||
// Event-Delegation: ein einziger Listener für alle Tabellen-Aktionen
|
||||
$("#m-table").addEventListener("click", e => {
|
||||
const btn = e.target.closest("[data-action]");
|
||||
if (!btn) return;
|
||||
const name = btn.getAttribute("data-name"), action = btn.getAttribute("data-action");
|
||||
if (!name) return;
|
||||
if (action === "cfg") openConfig(name);
|
||||
else if (action === "role") openRole(name);
|
||||
else if (action === "unload") unloadOne(name);
|
||||
else if (action === "del") deleteModel(name);
|
||||
else if (action === "copy") { navigator.clipboard?.writeText(name); toast("Kopiert: " + name); }
|
||||
else if (action === "assign") {
|
||||
toast("Lade im Cookbook ein Modell und setze den Alias auf: " + name);
|
||||
document.querySelector(".nav-item[data-view='cookbook']")?.click();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
let roleTarget = null;
|
||||
@@ -137,8 +161,7 @@ async function saveRole(role) {
|
||||
} catch (e) { toast(e.message, true); }
|
||||
}
|
||||
|
||||
function onStatus(s) {
|
||||
ALL = s?.models || [];
|
||||
function renderTable() {
|
||||
const tb = $("#models"); if (!tb) return;
|
||||
$("#models-empty").style.display = ALL.length ? "none" : "flex";
|
||||
$("#m-count").textContent = ALL.length ? ALL.length + " konfiguriert" : "";
|
||||
@@ -146,7 +169,6 @@ function onStatus(s) {
|
||||
const sel = $("#chat-model"), cur = sel.value; sel.innerHTML = "";
|
||||
tb.innerHTML = ALL.map(m => {
|
||||
if (m.incomplete) {
|
||||
// Leere Rolle (Platzhalter ohne Modell-Datei) — ehrlich kennzeichnen statt „bereit".
|
||||
return `<tr style="opacity:.9">
|
||||
<td class="mid" style="font-weight:500">${esc(m.name)}<div class="li-sub mono-sm" style="color:var(--warn)">kein Modell hinterlegt</div></td>
|
||||
<td>—</td>
|
||||
@@ -154,8 +176,8 @@ function onStatus(s) {
|
||||
<td><span class="badge">leer</span></td>
|
||||
<td class="port">—</td>
|
||||
<td style="text-align:right;white-space:nowrap">
|
||||
<button class="ghost" data-assign="${esc(m.name)}">Modell zuweisen</button>
|
||||
<button class="ghost del" data-del="${esc(m.name)}">Löschen</button>
|
||||
<button class="ghost" data-action="assign" data-name="${esc(m.name)}">Modell zuweisen</button>
|
||||
<button class="ghost del" data-action="del" data-name="${esc(m.name)}">Löschen</button>
|
||||
</td></tr>`;
|
||||
}
|
||||
const fn = m.meta?.filename ? `<div class="li-sub mono-sm">${esc(m.meta.filename)}</div>` : "";
|
||||
@@ -165,29 +187,29 @@ function onStatus(s) {
|
||||
${fn}${apiIdLine(m)}</td>
|
||||
<td>${capTags(m.meta?.caps)}</td>
|
||||
<td>${details(m.meta)}</td>
|
||||
<td>${badge(m.state)}</td>
|
||||
<td>${stateBadge(m)}</td>
|
||||
<td class="port">${m.port ?? "auto"}</td>
|
||||
<td style="text-align:right;white-space:nowrap">
|
||||
<button class="ghost" data-role="${esc(m.name)}">Rolle</button>
|
||||
<button class="ghost" data-cfg="${esc(m.name)}">Konfigurieren</button>
|
||||
<button class="ghost" data-unload="${esc(m.name)}">Entladen</button>
|
||||
<button class="ghost del" data-del="${esc(m.name)}">Löschen</button>
|
||||
<button class="ghost" data-action="role" data-name="${esc(m.name)}">Rolle</button>
|
||||
<button class="ghost" data-action="cfg" data-name="${esc(m.name)}">Konfigurieren</button>
|
||||
<button class="ghost" data-action="unload" data-name="${esc(m.name)}">Entladen</button>
|
||||
<button class="ghost del" data-action="del" data-name="${esc(m.name)}">Löschen</button>
|
||||
</td></tr>`;
|
||||
}).join("");
|
||||
for (const m of ALL) if (!m.incomplete) 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"))));
|
||||
tb.querySelectorAll("[data-del]").forEach(b => b.addEventListener("click", () => deleteModel(b.getAttribute("data-del"))));
|
||||
tb.querySelectorAll("[data-role]").forEach(b => b.addEventListener("click", () => openRole(b.getAttribute("data-role"))));
|
||||
tb.querySelectorAll("[data-copy]").forEach(c => c.addEventListener("click", () => {
|
||||
navigator.clipboard?.writeText(c.getAttribute("data-copy")); toast("Kopiert: " + c.getAttribute("data-copy"));
|
||||
}));
|
||||
tb.querySelectorAll("[data-assign]").forEach(b => b.addEventListener("click", () => {
|
||||
toast("Lade im Cookbook ein Modell und setze den Alias auf: " + b.getAttribute("data-assign"));
|
||||
document.querySelector(".nav-item[data-view='cookbook']")?.click();
|
||||
}));
|
||||
function onStatus(s) {
|
||||
ALL = s?.models || [];
|
||||
renderTable();
|
||||
}
|
||||
|
||||
function onJobs(jobs) {
|
||||
JOBS_CACHE = jobs || [];
|
||||
// Nur neu rendern wenn sich der Download-Zustand geändert hat (minimiert DOM-Flicker)
|
||||
const hasDownload = JOBS_CACHE.some(j => (j.state === "running" || j.state === "queued") && /^download\b/i.test(j.label));
|
||||
if (hasDownload) renderTable();
|
||||
}
|
||||
|
||||
async function deleteModel(m) {
|
||||
@@ -243,4 +265,4 @@ async function sendChat() {
|
||||
btn.disabled = false; btn.textContent = "Senden"; refreshSoon();
|
||||
}
|
||||
|
||||
export default { id: "models", mount, onStatus };
|
||||
export default { id: "models", mount, onStatus, onJobs };
|
||||
|
||||
Reference in New Issue
Block a user