// 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}
Kopieren
`;
}
const okChip = txt =>
`${icon("check")}${esc(txt)} `;
// ---- 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 `
1 · Verbindung prüfen
Stelle sicher, dass die LLM-Engine erreichbar ist, bevor du Tools konfigurierst.
${field("Engine-Adresse (für alle Tools)", url)}
${behindProxy() ? `
Du öffnest Mission Control über einen Proxy/Namen (HTTPS o.ä.). Die Engine läuft auf Port 8080 ohne HTTPS direkt am Bosgame.
Trag in deinen Tools die LAN-IP ein: http://192.168.178.151:8080/v1 — nicht den Proxy-Namen.
` : ""}
Verbindung testen
`;
}
function renderAutoSwapCard() {
return `
Wie funktioniert das Auto-Swap?
⚡
Kein manuelles Starten nötig
Wenn du in Zed oder OpenCode „coder" auswählst und eine Anfrage sendest,
lädt llama-swap das Modell automatisch — in ca. 2–5 Sekunden. Du siehst es hier im Dashboard unter „Aktives Modell".
🔄
Modelle wechseln sich automatisch ab
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 eines gleichzeitig — llama-swap verwaltet das komplett selbst.
🧠
Zwei Modelle gleichzeitig aktiv halten
Du hast 124 GB RAM — genug für z.B. Scout 8B (~5 GB) dauerhaft geladen + ein
größeres Modell das swappt. Dafür beim Scout im Modelle-Tab → Konfigurieren die TTL auf 99999 setzen
(= nie automatisch entladen). Das kleine Modell beantwortet schnelle Fragen sofort, ohne jede Wartezeit.
`;
}
function renderModelSelector() {
const ms = models();
if (!ms.length) return "";
const chips = ms.map(m =>
`${esc(m.name)} `
).join(" ");
const note = selectedModelIds.length
? `${selectedModelIds.length} von ${ms.length} ausgewählt`
: "alle werden eingeschlossen";
return `
2 · Modelle auswählen ${note}
Wähle, welche Modelle im Config-Snippet erscheinen sollen. Standardmäßig alle.
${chips}
${selectedModelIds.length ? `
` : ""}
`;
}
window.cnClearModels = () => { selectedModelIds = []; renderGuide(); };
function toolBtn(id, label, desc) {
const active = selectedTool === id;
return `
${esc(label)}
${esc(desc)}
`;
}
function renderToolPicker() {
return `
3 · Tool wählen
Für welches Werkzeug brauchst du die Konfiguration?
${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")}
`;
}
function renderConfigCard(url) {
const ids = activeIds();
const firstModel = ids[0] || "coder";
let content = "";
if (selectedTool === "zed") {
const block = zedMergeBlock(url, ids);
content = `
Öffne die Zed-Settings mit Cmd/Strg + , und füge diesen Block in deine bestehende
settings.json ein. Falls bereits ein "bosgame"-Eintrag existiert: vorher löschen.
Nur diesen Block einfügen, nicht die ganze Datei ersetzen — sonst gehen andere Einstellungen verloren.
API-Key: Öffne den Agent-Panel → Anbieter bosgame → trag irgendeinen Wert ein (z.B. local).
Alternativ BOSGAME_API_KEY=local als Umgebungsvariable.
Danach im Agent-Panel das Modell bosgame / ${esc(firstModel)} auswählen. Das Auto-Swap lädt es beim ersten Request.
`;
} else if (selectedTool === "opencode") {
const block = openCodeMergeBlock(url, ids);
content = `
Datei anlegen oder öffnen:
Windows: %USERPROFILE%\\.config\\opencode\\opencode.json
Linux/Mac: ~/.config/opencode/opencode.json
Oder im Projektordner: opencode.json
Nur den "provider"-Block einfügen oder mergen.
Falls bereits ein "llama-swap"-Eintrag vorhanden ist: vorher löschen , sonst erscheinen Modelle doppelt.
Danach in OpenCode /models → llama-swap/${esc(firstModel)} wählen.
Statt alles zu ersetzen: nur den provider-Block in deine bestehende Config einfügen — so bleiben andere Anbieter erhalten.
`;
} else if (selectedTool === "cline") {
content = `
In Cline/Cursor den Provider „OpenAI Compatible" wählen und folgende Felder eintragen:
${field("Basis-URL (OpenAI Compatible)", url)}
${field("Modell-ID", firstModel)}
API-Key: ein beliebiger Wert (z.B. local) — llama-swap prüft ihn nicht.
`;
} else if (selectedTool === "openwebui") {
content = `
Settings → Admin Panel → Connections → neue OpenAI-Verbindung:
${field("Basis-URL", url)}
${field("API-Key", "dummy-key")}
Nach dem Speichern erscheinen die Modelle automatisch in der Modell-Auswahl.
`;
}
return `
4 · Config-Snippet
${content}
`;
}
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,"}}
]}]
}'`;
return `
Bilder einbinden (Vision) ${okChip(`„${visionModel}" bereit`)}
Mit Vision-Modellen kannst du Screenshots oder Fehlerbilder direkt mitschicken.
`;
}
function render() {
const c = $(".view[data-view='connect']"); if (!c) return;
const url = baseUrl();
c.innerHTML = `
Verbinden
Schritt für Schritt: KI-Modelle in deine Tools einbinden — getestet, erklärt, mit Copy-Paste-Config.
${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 = `Teste… `;
try {
const r = await api("/api/integration/test");
res.innerHTML = r.ok
? okChip(`Verbunden — ${r.models.length} Modell(e): ${r.models.join(", ")}`)
: `✗ Keine Verbindung: ${esc(r.error)} `;
} catch (e) { res.innerHTML = `✗ ${esc(e.message)} `; }
}
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 };