Files
mission-control/static/js/panels/models.js
T

161 lines
6.7 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: 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() {
$("#m-chat").innerHTML = `
<div class="card-h"><h3>Schnelltest</h3></div>
<label>Modell</label>
<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" 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 &amp; Ports</h3><span class="meta" id="m-count"></span></div>
<div class="hint mb-4">
💡 <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">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>
<!-- 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 class="mt-6">
<label>Context-Size (Tokens)</label>
<input id="cfg-ctx" type="number" class="tokin" value="8192">
<div class="meta text-xs mt-2">Höhere Werte erlauben längere Dokumente, brauchen aber mehr VRAM.</div>
</div>
<button class="primary mt-6" id="cfg-save" style="width:100%; padding:var(--sp-3)">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) {
ALL_MODELS = s?.models || [];
const tb = $("#models");
if (!tb) return;
tb.innerHTML = "";
$("#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 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("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(" ");
}
let detailsHtml = "";
if (m.meta) {
const q = m.meta.quant || "?";
const c = m.meta.ctx ? (m.meta.ctx / 1024).toFixed(0) + "K" : "?";
const s = m.meta.size_bytes ? (m.meta.size_bytes / 1024 / 1024 / 1024).toFixed(1) + " GB" : "?";
detailsHtml = `<span class="meta" style="font-size:0.9em">${q} · ${c} · ${s}</span>`;
}
const perfHtml = m.state === "running" ? `<br><small class="meta" style="font-size:0.75em">n/a t/s</small>` : "";
const filenameHtml = m.meta?.filename ? `<br><small class="meta" style="font-size:0.8em">${esc(m.meta.filename)}</small>` : "";
tr.innerHTML = `<td class="mid" style="font-weight:500">${esc(m.name)}${filenameHtml}</td>
<td>${capsHtml}</td>
<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-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) {
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 Swap 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 };