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>
This commit is contained in:
+232
-157
@@ -1,39 +1,77 @@
|
||||
// connect.js — „Verbinden" (v4): IDEs/Agenten anbinden, Verbindung testen, Bild/Vision-Flow.
|
||||
// connect.js — „Verbinden" (v5): Wizard-Style, Merge-sichere Configs, Auto-Swap erklärt.
|
||||
|
||||
import { api } from "../core/api.js";
|
||||
import { $, esc, icon } from "../core/ui.js";
|
||||
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), 1200);
|
||||
};
|
||||
window.mcCopyCode = btn => {
|
||||
const code = btn.parentElement.querySelector("code");
|
||||
navigator.clipboard?.writeText(code.textContent);
|
||||
const o = btn.textContent; btn.textContent = "Kopiert!"; setTimeout(() => (btn.textContent = o), 1200);
|
||||
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();
|
||||
};
|
||||
|
||||
// Die Engine-API spricht IMMER reines HTTP auf Port 8080 direkt am Bosgame.
|
||||
// Bewusst nicht location.protocol uebernehmen: wer MC hinter einem Reverse-Proxy
|
||||
// (NPM) ueber https://<name> aufruft, bekaeme sonst eine kaputte https-:8080-URL.
|
||||
function baseUrl() { return `http://${location.hostname}:8080/v1`; }
|
||||
function isLanIp(h) { return /^\d{1,3}(\.\d{1,3}){3}$/.test(h); }
|
||||
// true, wenn MC offenbar ueber einen Proxy/Namen aufgerufen wird (nicht direkt per IP).
|
||||
function behindProxy() { return location.protocol === "https:" || !isLanIp(location.hostname); }
|
||||
function models() { return S?.models || []; }
|
||||
function models() { return (S?.models || []).filter(m => !m.incomplete); }
|
||||
function activeIds() {
|
||||
return selectedModelIds.length ? selectedModelIds : models().map(m => m.name);
|
||||
}
|
||||
|
||||
// Fertige opencode.json fuer einen OpenAI-kompatiblen Provider auf llama-swap.
|
||||
function openCodeConfig(url, ids) {
|
||||
const list = ids.length ? ids : ["coder"];
|
||||
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 `{
|
||||
"$schema": "https://opencode.ai/config.json",
|
||||
"provider": {
|
||||
return `"provider": {
|
||||
"llama-swap": {
|
||||
"npm": "@ai-sdk/openai-compatible",
|
||||
"name": "Bosgame (llama-swap)",
|
||||
@@ -45,151 +83,160 @@ function openCodeConfig(url, ids) {
|
||||
${modelMap}
|
||||
}
|
||||
}
|
||||
}
|
||||
}`;
|
||||
}
|
||||
|
||||
// Fertige Zed-settings.json fuer einen OpenAI-kompatiblen Provider auf llama-swap.
|
||||
// Vision-Modelle bekommen capabilities.images=true, sonst sieht Zed sie als reine Text-Modelle.
|
||||
function zedConfig(url) {
|
||||
const ms = models();
|
||||
const list = ms.length ? ms : [{ name: "coder" }];
|
||||
const entries = list.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}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}`;
|
||||
}
|
||||
// ---- Render-Abschnitte ----
|
||||
|
||||
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>`;
|
||||
|
||||
function toolRow(name, desc, url, badge) {
|
||||
return `<div class="li" style="align-items:flex-start">
|
||||
<div class="li-main">
|
||||
<div class="flex items-center gap-2"><span class="li-id" style="font-family:var(--sans);font-weight:500">${esc(name)}</span>${badge ? `<span class="fit-badge ok">${esc(badge)}</span>` : ""}</div>
|
||||
<div class="li-sub">${esc(desc)}</div></div>
|
||||
<a href="${esc(url)}" target="_blank" rel="noopener"><button class="ghost">Öffnen</button></a>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function render() {
|
||||
const c = $(".view[data-view='connect']"); if (!c) return;
|
||||
const url = baseUrl();
|
||||
const ids = models().map(m => m.name);
|
||||
const firstModel = ids[0] || "coder";
|
||||
const visionModel = models().find(m => m.meta?.caps?.includes("Bild"))?.name;
|
||||
|
||||
c.innerHTML = `
|
||||
<div class="pagehead"><div>
|
||||
<h1>Verbinden</h1>
|
||||
<div class="sub">Binde deine lokalen Modelle in IDEs, Agenten & Tools ein — getestet, mit Copy-Paste-Config.</div></div></div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-h"><h3>Verbindung zur Engine</h3></div>
|
||||
<div class="card-sub">Alle Tools nutzen diese OpenAI-kompatible Adresse. Das Auto-Swapping lädt das passende Modell je Anfrage automatisch.</div>
|
||||
${field("Basis-URL (OpenAI-kompatibel)", url)}
|
||||
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 rufst Mission Control offenbar über einen <b>Proxy/Namen</b> auf (NPM o.ä.). Die <b>Engine-API</b> läuft aber
|
||||
direkt am Bosgame auf <b>Port 8080 ohne HTTPS</b> — ein Proxy davor stört nur. Trag in deinen Tools die
|
||||
<b>rohe LAN-Adresse</b> ein: <code>http://<Bosgame-IP>:8080/v1</code> (z.B. <code>http://192.168.178.151:8080/v1</code>),
|
||||
<b>nicht</b> den HTTPS-Namen.</span></div>` : ""}
|
||||
<div class="btn-row"><button class="primary" id="cn-test">Verbindung testen</button></div>
|
||||
<div id="cn-test-result" style="margin-top:12px"></div>
|
||||
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>`;
|
||||
}
|
||||
|
||||
<div class="card">
|
||||
<div class="card-h"><h3>Bilder einbinden (Vision)</h3></div>
|
||||
<div class="card-sub">Für Fehleranalyse per Screenshot brauchst du ein Vision-Modell.</div>
|
||||
<div id="cn-vision"></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. 2–5 Sekunden. Du siehst es hier im Dashboard unter „Aktives Modell".</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-h"><h3>Empfohlene Tools (Juni 2026)</h3></div>
|
||||
<div class="card-sub">Diese verbinden sich nahtlos mit deinem Stack. Einrichtungs-Configs findest du darunter.</div>
|
||||
${toolRow("Zed", "Schneller nativer Editor mit eingebautem KI-Agenten — verbindet sich direkt mit deinem Stack.", "https://zed.dev", "Empfohlen")}
|
||||
${toolRow("OpenCode", "Open-Source-Coding-Agent (MIT) — Terminal, Desktop & IDE. Gute Alternative.", "https://opencode.ai")}
|
||||
${toolRow("Cline", "Bestes Erlebnis in VS Code, Bring-Your-Own-Key, Mensch-im-Loop.", "https://cline.bot")}
|
||||
${toolRow("Continue.dev", "Feinkörnige Kontrolle + bestes MCP-Ökosystem für Werkzeuge.", "https://continue.dev")}
|
||||
<div class="hint" style="margin-top:12px">Diese Tools sind <b>Agenten</b>: Sie lassen dein Modell selbst Dateien ändern und Befehle ausführen.
|
||||
Noch unklar, was <b>Agent</b>, <b>Skill</b> oder <b>MCP</b> heißt?
|
||||
<a href="#" onclick="event.preventDefault();window.cnGo('guides')">Im Guide von Grund auf erklärt →</a></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>`;
|
||||
}
|
||||
|
||||
<div class="card" style="padding:0;overflow:hidden">
|
||||
<details class="guide-acc" open>
|
||||
<summary>Zed — settings.json (empfohlen)</summary>
|
||||
<div class="acc-body">
|
||||
<p>Zed konfigurierst du über seine <b>settings.json</b> (in Zed: <code>Cmd/Strg + ,</code>). Füge diesen Block ein —
|
||||
er meldet deine Modelle als eigenen Anbieter „bosgame" an:</p>
|
||||
<div style="position:relative;margin-top:12px">
|
||||
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(zedConfig(url))}</code></div>
|
||||
<div class="log" style="max-height:none"><code>${esc(block)}</code></div>
|
||||
</div>
|
||||
<p style="margin-top:12px"><b>API-Key:</b> Zed speichert Keys <u>nicht</u> in der settings.json. Öffne den
|
||||
Agent-Panel → Anbieter <b>bosgame</b> → trag irgendeinen Wert ein (z.B. <code>local</code>) — llama-swap prüft ihn nicht.
|
||||
Alternativ als Umgebungsvariable <code>BOSGAME_API_KEY=local</code>.</p>
|
||||
<div class="hint" style="margin-top:8px">Danach im Agent-Panel oben das Modell <b>bosgame / ${esc(firstModel)}</b> wählen.
|
||||
Das Auto-Swapping lädt das passende Modell selbst. ${behindProxy() ? "<b>Achtung:</b> in <code>api_url</code> die LAN-IP des Bosgame nutzen (siehe Warnung oben), nicht den Proxy-Namen." : ""}</div>
|
||||
</div>
|
||||
</details>
|
||||
<details class="guide-acc">
|
||||
<summary>OpenCode — Config-Datei (JSON)</summary>
|
||||
<div class="acc-body">
|
||||
<p>OpenCode richtest du nicht über Felder ein, sondern über eine <b>Config-Datei</b>. Lege sie hier an:</p>
|
||||
<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>
|
||||
(oder im Projektordner als <code>opencode.json</code>)</p>
|
||||
<div style="position:relative;margin-top:12px">
|
||||
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(openCodeConfig(url, ids))}</code></div>
|
||||
<div class="log" style="max-height:none"><code>${esc(block)}</code></div>
|
||||
</div>
|
||||
<p style="margin-top:12px">Danach in OpenCode <code>/models</code> tippen und <b>llama-swap/${esc(firstModel)}</b> wählen. Das Auto-Swapping lädt das passende Modell von selbst.</p>
|
||||
<div class="hint" style="margin-top:8px"><b>Fehler „Connect to API could not be established"?</b> Dann zeigt die <code>baseURL</code> ins Leere. Sie muss auf die <b>LAN-IP des Bosgame</b> zeigen (oben: <code>${esc(url)}</code>) — niemals <code>localhost</code> oder <code>127.0.0.1</code>, denn das wäre dein eigener PC, nicht der Server.</div>
|
||||
</div>
|
||||
</details>
|
||||
<details class="guide-acc">
|
||||
<summary>Cline / Cursor</summary>
|
||||
<div class="acc-body">
|
||||
<p>Provider „OpenAI Compatible" wählen:</p>
|
||||
${field("Basis-URL", url)}
|
||||
<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>
|
||||
</details>
|
||||
<details class="guide-acc">
|
||||
<summary>OpenWebUI</summary>
|
||||
<div class="acc-body">
|
||||
<p>Settings → Admin → Connections → neue OpenAI-Verbindung:</p>
|
||||
<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>
|
||||
</details>
|
||||
</div>`;
|
||||
|
||||
$("#cn-test").addEventListener("click", testConnection);
|
||||
renderVision(url, visionModel);
|
||||
<div class="hint">Nach dem Speichern erscheinen die Modelle automatisch in der Modell-Auswahl.</div>`;
|
||||
}
|
||||
|
||||
function renderVision(url, visionModel) {
|
||||
const el = $("#cn-vision"); if (!el) return;
|
||||
if (visionModel) {
|
||||
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 '{
|
||||
@@ -199,28 +246,56 @@ function renderVision(url, visionModel) {
|
||||
{"type":"image_url","image_url":{"url":"data:image/png;base64,<DEIN_BILD>"}}
|
||||
]}]
|
||||
}'`;
|
||||
el.innerHTML = `${okChip(`Vision-Modell „${visionModel}" bereit`)}
|
||||
<div style="position:relative;margin-top:12px">
|
||||
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>`;
|
||||
} else {
|
||||
el.innerHTML = `<div class="empty-c" style="padding:24px">
|
||||
<div class="e-t">Noch kein Vision-Modell installiert</div>
|
||||
<div class="e-s">Im Cookbook unter „Bilder verstehen" mit einem Klick einrichten.</div>
|
||||
<button class="primary" style="margin-top:14px" onclick="window.cnGo('cookbook')">Zum Cookbook</button>
|
||||
</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 = `<div class="hint">Teste Verbindung…</div>`;
|
||||
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(`Funktioniert — ${r.models.length} Modell(e): ${r.models.join(", ")}`)
|
||||
: `<div class="alert warn" style="margin:0"><span class="a-dot"></span><span>Keine Verbindung zur Engine: ${esc(r.error)}</span></div>`;
|
||||
} catch (e) { res.innerHTML = `<div class="alert" style="margin:0"><span class="a-dot"></span><span>${esc(e.message)}</span></div>`; }
|
||||
? 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(); }
|
||||
|
||||
Reference in New Issue
Block a user