diff --git a/app.py b/app.py index 2b6a142..c245b3e 100644 --- a/app.py +++ b/app.py @@ -20,7 +20,7 @@ from fastapi import FastAPI, HTTPException from fastapi.responses import FileResponse, JSONResponse from fastapi.staticfiles import StaticFiles -from routers import jobs, maintenance, models, system, cookbook +from routers import jobs, maintenance, models, system, cookbook, integration app = FastAPI(title="Mission Control") @@ -41,6 +41,7 @@ app.include_router(jobs.router) app.include_router(maintenance.router) app.include_router(system.router) app.include_router(cookbook.router) +app.include_router(integration.router) _STATIC = Path(__file__).parent / "static" diff --git a/routers/integration.py b/routers/integration.py new file mode 100644 index 0000000..18d3517 --- /dev/null +++ b/routers/integration.py @@ -0,0 +1,24 @@ +""" +Integrations-Router: testet die OpenAI-kompatible Verbindung zur LLM-Engine. +Damit der „Verbinden"-Assistent dem Nutzer sagen kann: funktioniert ✓ + welche Modelle. +""" + +from fastapi import APIRouter, Depends + +from auth import auth +from config import LLAMA_SWAP_URL +from llamaswap import _swap_get + +router = APIRouter(prefix="/api/integration", dependencies=[Depends(auth)]) + + +@router.get("/test") +def test_connection(): + """Ruft die OpenAI-kompatible Modell-Liste der Engine ab (das, was externe Tools nutzen).""" + try: + data = _swap_get("/v1/models") + items = data.get("data", data) if isinstance(data, dict) else data + models = [m.get("id") for m in (items or []) if isinstance(m, dict) and m.get("id")] + return {"ok": True, "models": models, "url": LLAMA_SWAP_URL} + except Exception as exc: # noqa: BLE001 + return {"ok": False, "error": str(exc), "url": LLAMA_SWAP_URL} diff --git a/static/index.html b/static/index.html index c5b2a2c..ace5eeb 100644 --- a/static/index.html +++ b/static/index.html @@ -19,6 +19,7 @@ Aktivität Server Cookbook + Verbinden Guides
@@ -72,6 +73,10 @@ + + diff --git a/static/js/main.js b/static/js/main.js index dc1ee5b..98fd516 100644 --- a/static/js/main.js +++ b/static/js/main.js @@ -10,9 +10,10 @@ import models from "./panels/models.js"; import server from "./panels/server.js"; import jobs from "./panels/jobs.js"; import cookbook from "./panels/cookbook.js"; +import connect from "./panels/connect.js"; import guides from "./panels/guides.js"; -const panels = [overview, models, server, jobs, cookbook, guides]; +const panels = [overview, models, server, jobs, cookbook, connect, guides]; let lastJobs = []; let lastSystem = null; diff --git a/static/js/panels/connect.js b/static/js/panels/connect.js new file mode 100644 index 0000000..c6c06b3 --- /dev/null +++ b/static/js/panels/connect.js @@ -0,0 +1,134 @@ +// connect.js — „Verbinden" (v4): IDEs/Agenten anbinden, Verbindung testen, Bild/Vision-Flow. + +import { api } from "../core/api.js"; +import { $, esc, icon } from "../core/ui.js"; + +let S = null; +let lastSig = ""; + +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); +}; +window.cnGo = v => document.querySelector(`.nav-item[data-view="${v}"]`)?.click(); + +function baseUrl() { return `${location.protocol}//${location.hostname}:8080/v1`; } +function models() { return S?.models || []; } + +function field(label, value) { + return ` +
+ + +
`; +} +const okChip = txt => `${icon("check")}${esc(txt)}`; + +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 = ` +
+

Verbinden

+
Binde deine lokalen Modelle in IDEs, Agenten & Tools ein — getestet, mit Copy-Paste-Config.
+ +
+

Verbindung zur Engine

+
Alle Tools nutzen diese OpenAI-kompatible Adresse. Das Auto-Swapping lädt das passende Modell je Anfrage automatisch.
+ ${field("Basis-URL (OpenAI-kompatibel)", url)} +
+
+
+ +
+

Bilder einbinden (Vision)

+
Für Fehleranalyse per Screenshot brauchst du ein Vision-Modell.
+
+
+ +
+
+ OpenCode (Windows) +
+

Lege in OpenCode einen OpenAI-kompatiblen Provider an:

+ ${field("Basis-URL", url)} + ${field("Modell-ID", firstModel)} + ${field("API-Key", "(dein Token oder leer lassen)")} +
+
+
+ Cline / Cursor +
+

Provider „OpenAI Compatible" wählen:

+ ${field("Basis-URL", url)} + ${field("Modell-ID", firstModel)} +
+
+
+ OpenWebUI +
+

Settings → Admin → Connections → neue OpenAI-Verbindung:

+ ${field("Basis-URL", url)} + ${field("API-Key", "dummy-key")} +
+
+
`; + + $("#cn-test").addEventListener("click", testConnection); + renderVision(url, visionModel); +} + +function renderVision(url, visionModel) { + const el = $("#cn-vision"); if (!el) return; + if (visionModel) { + 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,"}} + ]}] +}'`; + el.innerHTML = `${okChip(`Vision-Modell „${visionModel}" bereit`)} +
+ +
${esc(snippet)}
+
`; + } else { + el.innerHTML = `
+
Noch kein Vision-Modell installiert
+
Im Cookbook unter „Bilder verstehen" mit einem Klick einrichten.
+ +
`; + } +} + +async function testConnection() { + const res = $("#cn-test-result"); res.innerHTML = `
Teste Verbindung…
`; + try { + const r = await api("/api/integration/test"); + res.innerHTML = r.ok + ? okChip(`Funktioniert — ${r.models.length} Modell(e): ${r.models.join(", ")}`) + : `
Keine Verbindung zur Engine: ${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 };