// models.js — "Modelle"-Ansicht (v3): Schnelltest-Chat, Modell-Tabelle, Kontext-Konfiguration. import { api } from "../core/api.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."; 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."; // Kanonische Rollen für die Schnellwahl (frei tippbar bleibt möglich). const ROLES = [ { id: "coder", label: "Coder", desc: "Programmieren & Code" }, { id: "vision", label: "Vision", desc: "Bilder verstehen" }, { id: "scout", label: "Scout", desc: "Schneller Allrounder" }, { id: "reviewer", label: "Reviewer", desc: "Code/Texte prüfen" }, { id: "manager", label: "Manager", desc: "Planen & koordinieren" }, ]; let ALL = []; function refreshSoon() { document.dispatchEvent(new Event("mc:refresh")); } function roleTag(role) { return role ? `${esc(role)}` : ""; } // Zeigt die API-Namen (echter Name + Rollen-Alias) mit Kopier-Knopf — das, was man im Tool angibt. function apiIdLine(m) { const ids = (m.api_ids && m.api_ids.length ? m.api_ids : [m.name]); return `
API-Name: ${ids.map(id => `${esc(id)}`).join('·')}
`; } function capTags(caps) { if (!caps?.length) return "–"; return caps.map(c => c === "Code" ? `Code` : c === "Bild" ? `Bild` : `Text`).join(" "); } function details(meta) { if (!meta) return "–"; const q = meta.quant || "?"; const c = meta.ctx ? Math.round(meta.ctx / 1024) + "K" : "?"; const s = meta.size_bytes ? (meta.size_bytes / 1024 ** 3).toFixed(1) + " GB" : "?"; return `${esc(q)} · ${c} · ${s}`; } function mount() { $("#m-head").innerHTML = `

Modelle

Deine konfigurierten Modelle — testen, Kontext anpassen oder aus dem Speicher werfen.
`; $("#m-chat").innerHTML = `

Schnelltest

Schreib eine Nachricht — das gewählte Modell wird automatisch geweckt.
`; $("#m-table").innerHTML = `

Modelle & Ports

Modelle laden automatisch, sobald eine Anfrage kommt — du musst nichts manuell starten.
ModellKannDetailsStatusPortAktionen
`; $("#chat-btn").addEventListener("click", sendChat); $("#cfg-close").addEventListener("click", () => $("#cfg-modal").style.display = "none"); $("#cfg-modal").addEventListener("click", e => { if (e.target.id === "cfg-modal") $("#cfg-modal").style.display = "none"; }); $("#cfg-save").addEventListener("click", saveConfig); $("#role-close").addEventListener("click", () => $("#role-modal").style.display = "none"); $("#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("")); } let roleTarget = null; function openRole(name) { roleTarget = name; const m = ALL.find(x => x.name === name); $("#role-model-name").textContent = name; $("#role-custom").value = ""; $("#role-chips").innerHTML = ROLES.map(r => ``).join(""); $("#role-chips").querySelectorAll("[data-rolepick]").forEach(b => b.addEventListener("click", () => saveRole(b.getAttribute("data-rolepick")))); $("#role-modal").style.display = "flex"; } async function saveRole(role) { if (!roleTarget) return; try { await api("/api/set_role", { method: "POST", body: JSON.stringify({ alias: roleTarget, role }) }); toast(role ? `Rolle gesetzt: ${role}` : "Rolle entfernt."); $("#role-modal").style.display = "none"; refreshSoon(); } catch (e) { toast(e.message, true); } } function onStatus(s) { ALL = s?.models || []; const tb = $("#models"); if (!tb) return; $("#models-empty").style.display = ALL.length ? "none" : "flex"; $("#m-count").textContent = ALL.length ? ALL.length + " konfiguriert" : ""; 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 ` ${esc(m.name)}
kein Modell hinterlegt
leere Rolle leer `; } const fn = m.meta?.filename ? `
${esc(m.meta.filename)}
` : ""; return `
${esc(m.name)}${roleTag(m.role)}
${fn}${apiIdLine(m)} ${capTags(m.meta?.caps)} ${details(m.meta)} ${badge(m.state)} ${m.port ?? "auto"} `; }).join(""); for (const m of ALL) if (!m.incomplete) sel.insertAdjacentHTML("beforeend", ``); 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(); })); } async function deleteModel(m) { const ok = await confirmModal({ title: `„${m}" wirklich löschen?`, body: "Das Modell wird aus der Konfiguration ausgetragen und seine Dateien werden von der Festplatte gelöscht, um Speicher freizugeben. Das lässt sich nicht rückgängig machen — später brauchst du dafür einen neuen Download.", confirmLabel: "Endgültig löschen", danger: true }); if (!ok) return; try { const r = await api("/api/delete_model", { method: "POST", body: JSON.stringify({ alias: m }) }); toast(r.note || ("Gelöscht: " + m)); setTimeout(refreshSoon, 600); } catch (e) { toast(e.message, true); } } function openConfig(alias) { const m = ALL.find(x => x.name === alias); if (!m) return; $("#cfg-model-name").textContent = m.name; $("#cfg-ctx").value = m.meta?.ctx || 8192; const opt = m.meta?.optimal_ctx, rec = $("#cfg-rec"); if (opt) { 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.
` + `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"; } $("#cfg-modal").style.display = "flex"; } async function saveConfig() { const alias = $("#cfg-model-name").textContent, 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 — aktiv beim nächsten Modell-Start."); $("#cfg-modal").style.display = "none"; refreshSoon(); } catch (e) { toast(e.message, true); } $("#cfg-save").disabled = false; } async function unloadOne(m) { if (!await confirmModal({ title: `„${m}" entladen?`, body: "Das Modell wird aus dem Grafikspeicher geworfen und lädt beim nächsten Aufruf automatisch neu.", confirmLabel: "Entladen" })) return; try { await api("/api/unload?model=" + encodeURIComponent(m), { method: "POST" }); toast("Entladen: " + m); setTimeout(refreshSoon, 600); } catch (e) { toast(e.message, true); } } async function sendChat() { const model = $("#chat-model").value, message = $("#chat-msg").value.trim(); if (!model) return toast("Kein Modell vorhanden.", true); if (!message) return; const btn = $("#chat-btn"); btn.disabled = true; btn.textContent = "…"; const box = $("#chat-reply"); box.style.display = "block"; box.textContent = "(wecke Modell, kann beim Laden kurz dauern…)"; try { const r = await api("/api/chat", { method: "POST", body: JSON.stringify({ model, message }) }); box.textContent = r.reply; } catch (e) { box.textContent = "Fehler: " + e.message; } btn.disabled = false; btn.textContent = "Senden"; refreshSoon(); } export default { id: "models", mount, onStatus };