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/connect.js
T
Hitonabi ee4e3c1dd4 feat(connect): Wizard-Style Verbinden-Tab v5
- 4-Schritt-Wizard: Verbindungstest → Auto-Swap-Erklaerung → Modell-Auswahl → Tool-Picker → Config-Snippet
- Merge-sichere Snippets (nur den einzufuegenden Block, nicht die ganze Datei) fuer Zed, OpenCode, Cline, OpenWebUI
- OpenCode: Warnung "llama-swap-Eintrag vorher loeschen" behebt Duplikat-Problem
- Auto-Swap erklaert (3 Kacheln): kein manuelles Starten, automatisches Wechseln, 2-Modelle-Tipp
- Modell-Chips: interaktive Auswahl welche Modelle im Snippet erscheinen
- Tool-Picker: Radio-Button-Stil, aktive Wahl hervorgehoben

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-22 21:36:21 +02:00

309 lines
14 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.
// connect.js — „Verbinden" (v5): Wizard-Style, Merge-sichere Configs, Auto-Swap erklärt.
import { api, getToken } from "../core/api.js";
import { $, esc, icon, toast } from "../core/ui.js";
let S = null;
let lastSig = "";
let selectedTool = "zed";
let selectedModelIds = [];
window.mcCopyCode = btn => {
const code = btn.parentElement.querySelector("code,pre");
navigator.clipboard?.writeText(code?.textContent || "");
const o = btn.textContent; btn.textContent = "Kopiert!"; setTimeout(() => (btn.textContent = o), 1400);
};
window.mcCopyVal = btn => {
const i = btn.previousElementSibling;
navigator.clipboard?.writeText(i.value);
const o = btn.textContent; btn.textContent = "Kopiert!"; setTimeout(() => (btn.textContent = o), 1400);
};
window.cnGo = v => document.querySelector(`.nav-item[data-view="${v}"]`)?.click();
window.cnSelectTool = t => { selectedTool = t; renderGuide(); };
window.cnToggleModel = id => {
if (selectedModelIds.includes(id)) selectedModelIds = selectedModelIds.filter(x => x !== id);
else selectedModelIds.push(id);
renderGuide();
};
function baseUrl() { return `http://${location.hostname}:8080/v1`; }
function isLanIp(h) { return /^\d{1,3}(\.\d{1,3}){3}$/.test(h); }
function behindProxy() { return location.protocol === "https:" || !isLanIp(location.hostname); }
function models() { return (S?.models || []).filter(m => !m.incomplete); }
function activeIds() {
return selectedModelIds.length ? selectedModelIds : models().map(m => m.name);
}
function field(label, value) {
return `<label>${label}</label>
<div class="flex gap-2" style="align-items:stretch;margin-bottom:12px">
<input class="mono-sm" readonly value="${esc(value)}" style="flex:1;margin:0">
<button class="ghost" onclick="window.mcCopyVal(this)">Kopieren</button>
</div>`;
}
const okChip = txt =>
`<span class="chip" style="background:rgba(63,185,80,.12);border-color:rgba(63,185,80,.25);color:#7ee29a">${icon("check")}${esc(txt)}</span>`;
// ---- Merge-sichere Config-Snippets ----
function zedMergeBlock(url, ids) {
const ms = ids.length
? models().filter(m => ids.includes(m.name))
: (models().length ? models() : [{ name: "coder", meta: {} }]);
const entries = ms.map(m => {
const img = (m.meta?.caps || []).some(c => /Bild|Vision/i.test(c));
const caps = img ? `, "capabilities": { "tools": true, "images": true }` : ``;
return ` { "name": "${m.name}", "display_name": "${m.name}", "max_tokens": 32768${caps} }`;
}).join(",\n");
return `"language_models": {
"openai_compatible": {
"bosgame": {
"api_url": "${url}",
"available_models": [
${entries}
]
}
}
}`;
}
function openCodeMergeBlock(url, ids) {
const list = ids.length ? ids : (models().length ? models().map(m => m.name) : ["coder"]);
const modelMap = list.map(id => ` "${id}": { "name": "${id}" }`).join(",\n");
return `"provider": {
"llama-swap": {
"npm": "@ai-sdk/openai-compatible",
"name": "Bosgame (llama-swap)",
"options": {
"baseURL": "${url}",
"apiKey": "local"
},
"models": {
${modelMap}
}
}
}`;
}
// ---- Render-Abschnitte ----
function renderConnectionCard(url) {
return `<div class="card" id="cn-conn-card">
<div class="card-h"><h3>1 · Verbindung prüfen</h3></div>
<div class="card-sub">Stelle sicher, dass die LLM-Engine erreichbar ist, bevor du Tools konfigurierst.</div>
${field("Engine-Adresse (für alle Tools)", url)}
${behindProxy() ? `<div class="alert warn" style="margin:0 0 12px"><span class="a-dot"></span><span>
Du öffnest Mission Control über einen <b>Proxy/Namen</b> (HTTPS o.ä.). Die Engine läuft auf <b>Port 8080 ohne HTTPS</b> direkt am Bosgame.
Trag in deinen Tools die <b>LAN-IP</b> ein: <code>http://192.168.178.151:8080/v1</code> — nicht den Proxy-Namen.</span></div>` : ""}
<div class="btn-row" style="display:flex;align-items:center;gap:10px">
<button class="primary" id="cn-test">Verbindung testen</button>
<span id="cn-test-result" class="mono-sm"></span>
</div>
</div>`;
}
function renderAutoSwapCard() {
return `<div class="card">
<div class="card-h"><h3>Wie funktioniert das Auto-Swap?</h3></div>
<div style="display:flex;flex-direction:column;gap:10px">
<div class="tile" style="display:flex;gap:12px;align-items:flex-start">
<span style="font-size:20px;line-height:1;flex:0 0 auto">⚡</span>
<div>
<div style="font-weight:500;font-size:13.5px">Kein manuelles Starten nötig</div>
<div class="card-sub" style="margin:4px 0 0">Wenn du in Zed oder OpenCode <b>„coder"</b> auswählst und eine Anfrage sendest,
lädt llama-swap das Modell automatisch — in ca. 25 Sekunden. Du siehst es hier im Dashboard unter „Aktives Modell".</div>
</div>
</div>
<div class="tile" style="display:flex;gap:12px;align-items:flex-start">
<span style="font-size:20px;line-height:1;flex:0 0 auto">🔄</span>
<div>
<div style="font-weight:500;font-size:13.5px">Modelle wechseln sich automatisch ab</div>
<div class="card-sub" style="margin:4px 0 0">Rufst du ein anderes Modell auf, entlädt llama-swap das aktuelle (nach TTL-Ablauf)
und lädt das neue. Es läuft immer nur <b>eines gleichzeitig</b> — llama-swap verwaltet das komplett selbst.</div>
</div>
</div>
<div class="tile" style="display:flex;gap:12px;align-items:flex-start">
<span style="font-size:20px;line-height:1;flex:0 0 auto">🧠</span>
<div>
<div style="font-weight:500;font-size:13.5px">Zwei Modelle gleichzeitig aktiv halten</div>
<div class="card-sub" style="margin:4px 0 0">Du hast 124 GB RAM — genug für z.B. Scout 8B (~5 GB) <b>dauerhaft geladen</b> + ein
größeres Modell das swappt. Dafür beim Scout im <b>Modelle-Tab → Konfigurieren</b> die TTL auf <code>99999</code> setzen
(= nie automatisch entladen). Das kleine Modell beantwortet schnelle Fragen sofort, ohne jede Wartezeit.</div>
</div>
</div>
</div>
</div>`;
}
function renderModelSelector() {
const ms = models();
if (!ms.length) return "";
const chips = ms.map(m =>
`<span class="chip" style="cursor:pointer;${selectedModelIds.includes(m.name) ? "background:rgba(45,212,191,.2);border-color:var(--accent);color:var(--accent)" : ""}"
onclick="window.cnToggleModel('${esc(m.name)}')">${esc(m.name)}</span>`
).join(" ");
const note = selectedModelIds.length
? `${selectedModelIds.length} von ${ms.length} ausgewählt`
: "alle werden eingeschlossen";
return `<div class="card">
<div class="card-h"><h3>2 · Modelle auswählen</h3><span class="meta">${note}</span></div>
<div class="card-sub">Wähle, welche Modelle im Config-Snippet erscheinen sollen. Standardmäßig alle.</div>
<div class="flex gap-2" style="flex-wrap:wrap">${chips}</div>
${selectedModelIds.length ? `<div style="margin-top:10px"><a href="#" onclick="event.preventDefault();window.cnClearModels()" style="color:var(--mut);font-size:12.5px">Auswahl aufheben (alle)</a></div>` : ""}
</div>`;
}
window.cnClearModels = () => { selectedModelIds = []; renderGuide(); };
function toolBtn(id, label, desc) {
const active = selectedTool === id;
return `<button class="card-btn${active ? " cb-best" : ""}" style="text-align:left;padding:12px 16px"
onclick="window.cnSelectTool('${id}')">
<div style="font-weight:500;font-size:13.5px">${esc(label)}</div>
<div class="card-sub" style="margin:3px 0 0">${esc(desc)}</div>
</button>`;
}
function renderToolPicker() {
return `<div class="card">
<div class="card-h"><h3>3 · Tool wählen</h3></div>
<div class="card-sub">Für welches Werkzeug brauchst du die Konfiguration?</div>
<div class="grid grid-4" style="gap:8px">
${toolBtn("zed", "Zed", "Schneller nativer Editor — Empfohlen")}
${toolBtn("opencode", "OpenCode", "Open-Source Coding-Agent (Terminal/Desktop)")}
${toolBtn("cline", "Cline / Cursor", "VS Code Extension, Bring-Your-Own-Key")}
${toolBtn("openwebui", "OpenWebUI", "Browser-Chat-Interface")}
</div>
</div>`;
}
function renderConfigCard(url) {
const ids = activeIds();
const firstModel = ids[0] || "coder";
let content = "";
if (selectedTool === "zed") {
const block = zedMergeBlock(url, ids);
content = `
<p>Öffne die Zed-Settings mit <code>Cmd/Strg + ,</code> und <b>füge diesen Block</b> in deine bestehende
<code>settings.json</code> ein. Falls bereits ein <code>"bosgame"</code>-Eintrag existiert: vorher löschen.</p>
<div class="alert warn" style="margin-bottom:12px;padding:10px 14px">
<span class="a-dot"></span><span>Nur diesen Block einfügen, <b>nicht die ganze Datei ersetzen</b> — sonst gehen andere Einstellungen verloren.</span>
</div>
<div style="position:relative;margin-top:4px">
<button class="ghost" style="position:absolute;top:8px;right:8px;z-index:1" onclick="window.mcCopyCode(this)">Kopieren</button>
<div class="log" style="max-height:none"><code>${esc(block)}</code></div>
</div>
<p style="margin-top:12px"><b>API-Key:</b> Öffne den Agent-Panel → Anbieter <b>bosgame</b> → trag irgendeinen Wert ein (z.B. <code>local</code>).
Alternativ <code>BOSGAME_API_KEY=local</code> als Umgebungsvariable.</p>
<div class="hint">Danach im Agent-Panel das Modell <b>bosgame / ${esc(firstModel)}</b> auswählen. Das Auto-Swap lädt es beim ersten Request.</div>`;
} else if (selectedTool === "opencode") {
const block = openCodeMergeBlock(url, ids);
content = `
<p>Datei anlegen oder öffnen:</p>
<p class="mono-sm" style="line-height:1.6">Windows: <code>%USERPROFILE%\\.config\\opencode\\opencode.json</code><br>
Linux/Mac: <code>~/.config/opencode/opencode.json</code><br>
Oder im Projektordner: <code>opencode.json</code></p>
<div class="alert warn" style="margin:8px 0 12px;padding:10px 14px">
<span class="a-dot"></span><span>Nur den <code>"provider"</code>-Block einfügen oder mergen.
Falls bereits ein <code>"llama-swap"</code>-Eintrag vorhanden ist: <b>vorher löschen</b>, sonst erscheinen Modelle doppelt.</span>
</div>
<div style="position:relative">
<button class="ghost" style="position:absolute;top:8px;right:8px;z-index:1" onclick="window.mcCopyCode(this)">Kopieren</button>
<div class="log" style="max-height:none"><code>${esc(block)}</code></div>
</div>
<p style="margin-top:12px">Danach in OpenCode <code>/models</code> → <b>llama-swap/${esc(firstModel)}</b> wählen.</p>
<div class="hint">Statt alles zu ersetzen: nur den <code>provider</code>-Block in deine bestehende Config einfügen — so bleiben andere Anbieter erhalten.</div>`;
} else if (selectedTool === "cline") {
content = `
<p>In Cline/Cursor den Provider <b>„OpenAI Compatible"</b> wählen und folgende Felder eintragen:</p>
${field("Basis-URL (OpenAI Compatible)", url)}
${field("Modell-ID", firstModel)}
<div class="hint">API-Key: ein beliebiger Wert (z.B. <code>local</code>) — llama-swap prüft ihn nicht.</div>`;
} else if (selectedTool === "openwebui") {
content = `
<p>Settings → Admin Panel → Connections → neue OpenAI-Verbindung:</p>
${field("Basis-URL", url)}
${field("API-Key", "dummy-key")}
<div class="hint">Nach dem Speichern erscheinen die Modelle automatisch in der Modell-Auswahl.</div>`;
}
return `<div class="card">
<div class="card-h"><h3>4 · Config-Snippet</h3></div>
${content}
</div>`;
}
function renderVisionCard(url) {
const visionModel = models().find(m => m.meta?.caps?.includes("Bild"))?.name;
if (!visionModel) return "";
const snippet = `curl ${url}/chat/completions \\
-H "Content-Type: application/json" \\
-d '{
"model": "${visionModel}",
"messages": [{"role":"user","content":[
{"type":"text","text":"Was ist auf dem Bild falsch?"},
{"type":"image_url","image_url":{"url":"data:image/png;base64,<DEIN_BILD>"}}
]}]
}'`;
return `<div class="card">
<div class="card-h"><h3>Bilder einbinden (Vision)</h3>${okChip(`${visionModel}" bereit`)}</div>
<div class="card-sub">Mit Vision-Modellen kannst du Screenshots oder Fehlerbilder direkt mitschicken.</div>
<div style="position:relative;margin-top:8px">
<button class="ghost" style="position:absolute;top:8px;right:8px;z-index:1" onclick="window.mcCopyCode(this)">Kopieren</button>
<div class="log" style="max-height:none"><code>${esc(snippet)}</code></div>
</div>
</div>`;
}
function render() {
const c = $(".view[data-view='connect']"); if (!c) return;
const url = baseUrl();
c.innerHTML = `
<div class="pagehead"><div>
<h1>Verbinden</h1>
<div class="sub">Schritt für Schritt: KI-Modelle in deine Tools einbinden — getestet, erklärt, mit Copy-Paste-Config.</div>
</div></div>
${renderConnectionCard(url)}
${renderAutoSwapCard()}
${renderModelSelector()}
${renderToolPicker()}
${renderConfigCard(url)}
${renderVisionCard(url)}`;
$("#cn-test")?.addEventListener("click", testConnection);
}
function renderGuide() {
const url = baseUrl();
const sel = $(".view[data-view='connect'] .card:nth-of-type(3)");
if (!sel) { render(); return; }
// Nur die unteren 3 Karten neu rendern (Modell-Auswahl, Tool-Picker, Config)
const c = $(".view[data-view='connect']");
const cards = c?.querySelectorAll(".card");
if (!cards || cards.length < 3) { render(); return; }
// Einfachster Weg: komplett neu rendern (Scroll-Position bleibt wegen Hash-Routing ok)
render();
}
async function testConnection() {
const res = $("#cn-test-result"); res.innerHTML = `<span class="text-mut">Teste…</span>`;
try {
const r = await api("/api/integration/test");
res.innerHTML = r.ok
? okChip(`Verbunden — ${r.models.length} Modell(e): ${r.models.join(", ")}`)
: `<span style="color:var(--err)">✗ Keine Verbindung: ${esc(r.error)}</span>`;
} catch (e) { res.innerHTML = `<span style="color:var(--err)">✗ ${esc(e.message)}</span>`; }
}
function mount() { render(); }
function onStatus(s) {
S = s;
const sig = (s?.models || []).map(m => m.name).join(",") + "|" + !!s;
if (sig !== lastSig) { lastSig = sig; render(); }
}
export default { id: "connect", mount, onStatus };