This repository has been archived on 2026-07-22. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
mission-control/static/js/panels/models.js
T
Hitonabi 45ab32955a feat: leere Rollen ehrlich anzeigen + Vision-cmd automatisch (mmproj/jinja)
- status(): Flag "incomplete" fuer Rollen ohne Modell (-m), z.B. provision.sh-
  Platzhalter. Wurden bisher irrefuehrend als "bereit / ? . 8K . ?" gezeigt.
- Modelle-Tab: leere Rollen klar als "kein Modell hinterlegt / leer" + Aktion
  "Modell zuweisen" (-> Cookbook); nicht im Schnelltest-Dropdown waehlbar
- register(): erkennt einen mmproj-Projektor im selben Ordner und ergaenzt
  --mmproj + --jinja automatisch (Vision braucht das in llama.cpp)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 18:38:16 +02:00

176 lines
9.2 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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.";
let ALL = [];
function refreshSoon() { document.dispatchEvent(new Event("mc:refresh")); }
function capTags(caps) {
if (!caps?.length) return "";
return caps.map(c =>
c === "Code" ? `<span class="tag code">Code</span>`
: c === "Bild" ? `<span class="tag img">Bild</span>`
: `<span class="tag text">Text</span>`).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 `<span class="mono-sm">${esc(q)} · ${c} · ${s}</span>`;
}
function mount() {
$("#m-head").innerHTML = `<div class="pagehead"><div>
<h1>Modelle</h1>
<div class="sub">Deine konfigurierten Modelle — testen, Kontext anpassen oder aus dem Speicher werfen.</div></div></div>`;
$("#m-chat").innerHTML = `
<div class="card-h"><h3>Schnelltest</h3></div>
<div class="card-sub">Schreib eine Nachricht — das gewählte Modell wird automatisch geweckt.</div>
<label>Modell</label>
<select id="chat-model"></select>
<label>Nachricht</label>
<textarea id="chat-msg" placeholder="z.B. „Erklär mir kurz, was du kannst."></textarea>
<button class="primary" id="chat-btn">Senden</button>
<div id="chat-reply" class="reply" style="display:none"></div>`;
$("#m-table").innerHTML = `
<div class="card-h"><h3>Modelle &amp; Ports</h3><span class="meta" id="m-count"></span></div>
<div class="card-sub">Modelle laden automatisch, sobald eine Anfrage kommt — du musst nichts manuell starten.</div>
<table>
<thead><tr><th>Modell</th><th>Kann</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-c" style="display:none">
<div class="e-t">Noch keine Modelle konfiguriert</div>
<div class="e-s">Hol dir unter „Cookbook" ein passendes Modell.</div>
</div>
<div id="cfg-modal" class="modal-overlay" style="display:none">
<div class="modal-card">
<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) ${infoDot(CTX_HELP)}</label>
<input id="cfg-ctx" type="number" value="8192">
<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>`;
$("#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);
}
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 `<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>
<td><span class="mono-sm text-mut">leere Rolle</span></td>
<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>
</td></tr>`;
}
const fn = m.meta?.filename ? `<div class="li-sub mono-sm">${esc(m.meta.filename)}</div>` : "";
return `<tr>
<td class="mid" style="font-weight:500">${esc(m.name)}${fn}</td>
<td>${capTags(m.meta?.caps)}</td>
<td>${details(m.meta)}</td>
<td>${badge(m.state)}</td>
<td class="port">${m.port ?? "auto"}</td>
<td style="text-align:right;white-space:nowrap">
<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>
</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-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. <b>Das lässt sich nicht rückgängig machen</b> — 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.<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"; }
$("#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 };