// 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 `
`; } 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.
` : ""}
`; } 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 ? `
Auswahl aufheben (alle)
` : ""}
`; } window.cnClearModels = () => { selectedModelIds = []; renderGuide(); }; function toolBtn(id, label, desc) { const active = selectedTool === id; return ``; } 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.
${esc(block)}

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.
${esc(block)}

Danach in OpenCode /modelsllama-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.
${esc(snippet)}
`; } 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 };