feat(rollen): echte Modellnamen als API-ID + Rollen als Tags/Alias

Modelle erscheinen jetzt mit ihrem ECHTEN Namen (= API-Name, den man in Zed/
OpenCode angibt) statt unter "coder"/"vision". Die Rolle wird ein zusaetzlicher,
sprechender Name (llama-swap-`aliases`) -> in der API funktionieren BEIDE.

- llamaswap.py: model_id_from_path (Repo-Ordner ohne -GGUF) + set_role_alias
  (Rolle als eindeutiger Alias, beim Setzen bei anderen Modellen entfernt) +
  ROLE_IDS (vision/coder/scout/reviewer/manager).
- models.py: register schreibt Key=Modellname, Rolle als Alias; /status liefert
  role + aliases + api_ids; neuer POST /set_role. Rueckwaertskompatibel: Legacy-
  Eintraege (coder/vision ohne Alias) behalten ihren Key als Rolle.
- cookbook.py: install-recipe/-model nutzen dasselbe Schema (Name + Rollen-Alias).
- models.js: Zeile zeigt echten Namen + Rollen-Tag + API-Name (klick=kopieren);
  neuer "Rolle"-Dialog mit 5 Schnellwahl-Rollen + freier Eingabe.
- cookbook.js: Profi-Download-Feld ist jetzt optionale Rolle (Name kommt vom
  Modell), Pflichtfeld-Pruefung entsprechend gelockert.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Hitonabi
2026-06-22 16:58:07 +02:00
parent 0c16fb28c2
commit 4077d06b2d
5 changed files with 169 additions and 17 deletions
+72 -1
View File
@@ -4,10 +4,32 @@ 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 ? `<span class="tag" style="background:rgba(45,212,191,.14);color:var(--accent);border-color:rgba(45,212,191,.3)">${esc(role)}</span>` : "";
}
// 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 `<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>')}
</div>`;
}
function capTags(caps) {
if (!caps?.length) return "";
return caps.map(c =>
@@ -65,12 +87,54 @@ function mount() {
<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>
<div id="role-modal" class="modal-overlay" style="display:none">
<div class="modal-card">
<button id="role-close" class="ghost" style="position:absolute;top:14px;right:14px">Schließen</button>
<h3>Rolle festlegen ${infoDot(ROLE_HELP)}</h3>
<p class="mono-sm" id="role-model-name" style="margin:-4px 0 16px"></p>
<div class="hint" style="margin-bottom:10px">Wähle eine Rolle als zweiten, sprechenden Namen. In deinen Tools funktionieren danach <b>beide</b>: der echte Modellname und die Rolle.</div>
<div id="role-chips" class="flex gap-2" style="flex-wrap:wrap;margin-bottom:12px"></div>
<label>Eigene Rolle (optional)</label>
<input id="role-custom" placeholder="z.B. uebersetzer">
<div class="btn-row" style="margin-top:14px;display:flex;gap:8px">
<button class="ghost" id="role-clear" style="flex:1">Rolle entfernen</button>
<button class="primary" id="role-save" style="flex:2">Speichern</button>
</div>
</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);
$("#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 =>
`<button class="${m?.role === r.id ? "primary" : "ghost"}" data-rolepick="${r.id}" title="${esc(r.desc)}" style="border-radius:999px">${esc(r.label)}</button>`).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) {
@@ -96,12 +160,15 @@ function onStatus(s) {
}
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 class="mid">
<div style="font-weight:500;display:flex;align-items:center;gap:7px;flex-wrap:wrap">${esc(m.name)}${roleTag(m.role)}</div>
${fn}${apiIdLine(m)}</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-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>
@@ -113,6 +180,10 @@ function onStatus(s) {
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();