Feat: Verbinden-Tab Rework — zwei Leitungen (Modell + Gedaechtnis) sichtbar getrennt

- Hero-Diagramm: lokaler Agent -> Leitung 1 (Gateway/Modell) + Leitung 2 (MCP/Gedaechtnis)
- Live-Status pro Leitung: neuer Endpoint GET /api/connect/health (llama-swap + mem0-Sidecar)
- Zwei nummerierte Setup-Spalten statt flacher Tab-Leiste; Memory-MCP aus tools{} geloest
- Claude-Code-Snippet: echte env-Anleitung (ANTHROPIC_BASE_URL/AUTH_TOKEN/MODEL + Shim-Hinweis),
  da Gateway kein /v1/messages bietet (live verifiziert: :9001 405, :8080 404)
- MCP-Scriptpfad jetzt klar nur Leitung 2 zugeordnet

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Hitonabi
2026-06-27 22:30:32 +02:00
parent d3157d2535
commit f82729f88e
10 changed files with 428 additions and 207 deletions
+7 -1
View File
@@ -2,7 +2,7 @@
from fastapi import APIRouter from fastapi import APIRouter
from services.connect import DEFAULT_HOST, build_snippets from services.connect import DEFAULT_HOST, build_snippets, check_health
router = APIRouter(prefix="/api") router = APIRouter(prefix="/api")
@@ -13,3 +13,9 @@ def connect(host: str = DEFAULT_HOST, mcp_path: str | None = None) -> dict:
if mcp_path: if mcp_path:
kwargs["mcp_script_path"] = mcp_path kwargs["mcp_script_path"] = mcp_path
return build_snippets(host=host, **kwargs) return build_snippets(host=host, **kwargs)
@router.get("/connect/health")
def connect_health() -> dict:
"""Live-Status der zwei Leitungen (Gateway + Gedächtnis) für den Verbinden-Tab."""
return check_health()
+51 -11
View File
@@ -9,7 +9,9 @@ die häufigste Fehlerquelle. Der Aufrufer übergibt den Host explizit.
import json import json
from config import PORT import httpx
from config import LLAMA_SWAP_URL, MEM0_SERVICE_URL, PORT
DEFAULT_HOST = "192.168.178.151" DEFAULT_HOST = "192.168.178.151"
@@ -79,12 +81,20 @@ def build_snippets(host: str = DEFAULT_HOST,
] ]
}, indent=2) }, indent=2)
# Claude Code spricht Anthropic-Format; der eingebaute Gateway ist OpenAI-kompatibel. # Claude Code spricht das Anthropic-Format; der Gateway ist OpenAI-kompatibel und
# bietet KEIN /v1/messages (verifiziert). Daher braucht es einen kleinen Übersetzer
# (Anthropic ⇄ OpenAI) als Aufsatz. Die env-Vars sind Claude Codes echte Schnittstelle.
claude_code = ( claude_code = (
f"# Der eingebaute Gateway ist OpenAI-kompatibel ({gw}, model: auto).\n" f"# Claude Code spricht das Anthropic-Format — der Gateway ist OpenAI-kompatibel ({gw})\n"
f"# Claude Code nutzt das Anthropic-Format — dafür braucht es einen Anthropic-Shim\n" f"# und hat kein /v1/messages. Dazwischen muss ein Übersetzer (Anthropic ⇄ OpenAI) laufen:\n"
f"# (z.B. LiteLLM /v1/messages) als Aufsatz. Für lokale Modelle direkt: Cline / OpenCode /\n" f"# • claude-code-router (leichtgewichtig, npm)\n"
f"# Continue / Zed nutzen (oben), die sprechen OpenAI-kompatibel mit diesem Gateway." f"# • oder LiteLLM mit /v1/messages-Bridge\n"
f"# Den Übersetzer auf den Gateway zeigen lassen: baseURL={gw}, model=auto, apiKey=local.\n"
f"# Dann Claude Code auf den lokalen Übersetzer richten (Beispiel-Port 3456):\n"
f"\n"
f'export ANTHROPIC_BASE_URL="http://localhost:3456"\n'
f'export ANTHROPIC_AUTH_TOKEN="local"\n'
f'export ANTHROPIC_MODEL="auto"'
) )
memory_mcp = json.dumps({ memory_mcp = json.dumps({
@@ -100,10 +110,12 @@ def build_snippets(host: str = DEFAULT_HOST,
return { return {
"host": host, "host": host,
"gateway_url": gw, "gateway_url": gw,
"mc_url": mc_url,
# Leitung 1 — das MODELL. Alle Snippets zeigen auf den OpenAI-kompatiblen Gateway.
"tools": { "tools": {
"cline": {"label": "Roo Code / Cline (VS Code)", "lang": "json", "snippet": cline, "cline": {"label": "Roo Code / Cline", "lang": "json", "snippet": cline,
"note": "OpenAI-Provider → Gateway. Modell 'auto' (schnell, eskaliert bei Bedarf)."}, "note": "OpenAI-Provider → Gateway. Modell 'auto' (schnell, eskaliert bei Bedarf)."},
"cursor": {"label": "Cursor IDE", "lang": "json", "snippet": cursor, "cursor": {"label": "Cursor", "lang": "json", "snippet": cursor,
"note": "Einstellungen ➔ Models ➔ OpenAI API key + Base URL."}, "note": "Einstellungen ➔ Models ➔ OpenAI API key + Base URL."},
"opencode": {"label": "OpenCode", "lang": "jsonc", "snippet": opencode, "opencode": {"label": "OpenCode", "lang": "jsonc", "snippet": opencode,
"note": "Datei opencode.jsonc, Key 'provider'."}, "note": "Datei opencode.jsonc, Key 'provider'."},
@@ -112,8 +124,36 @@ def build_snippets(host: str = DEFAULT_HOST,
"continue": {"label": "Continue", "lang": "json", "snippet": cont, "continue": {"label": "Continue", "lang": "json", "snippet": cont,
"note": "~/.continue/config.json (oder config.yaml mit identischen Keys)."}, "note": "~/.continue/config.json (oder config.yaml mit identischen Keys)."},
"claude_code": {"label": "Claude Code", "lang": "bash", "snippet": claude_code, "claude_code": {"label": "Claude Code", "lang": "bash", "snippet": claude_code,
"note": "Anthropic-Format über LiteLLM /v1/messages."}, "note": "Braucht einen Anthropic⇄OpenAI-Übersetzer vor dem Gateway."},
"memory_mcp": {"label": "Shared Memory (MCP)", "lang": "json", "snippet": memory_mcp,
"note": "Für jedes MCP-fähige Tool. mcp_memory.py muss lokal liegen."},
}, },
# Leitung 2 — das GEDÄCHTNIS. Separater MCP-Server, gilt zusätzlich zu jedem Tool oben.
"memory": {"label": "Shared Memory (MCP)", "lang": "json", "snippet": memory_mcp,
"note": "Eigene Leitung: MCP-Block für jedes MCP-fähige Tool. mcp_memory.py muss lokal liegen."},
} }
def check_health() -> dict:
"""Live-Erreichbarkeit der beiden Leitungen, aus Sicht der Box:
Leitung 1 = Gateway/Engine (llama-swap), Leitung 2 = Gedächtnis-Sidecar (Mem0)."""
gateway = {"ok": False, "detail": "nicht erreichbar"}
try:
with httpx.Client(timeout=3.0) as c:
r = c.get(f"{LLAMA_SWAP_URL}/v1/models")
if r.status_code == 200:
n = len(r.json().get("data", []))
gateway = {"ok": True, "detail": f"{n} Modelle verfügbar" if n else "bereit"}
else:
gateway = {"ok": False, "detail": f"HTTP {r.status_code}"}
except Exception: # noqa: BLE001
pass
memory = {"ok": False, "detail": "nicht erreichbar"}
try:
with httpx.Client(timeout=3.0) as c:
r = c.get(f"{MEM0_SERVICE_URL}/health")
memory = ({"ok": True, "detail": "bereit"} if r.status_code == 200
else {"ok": False, "detail": f"HTTP {r.status_code}"})
except Exception: # noqa: BLE001
pass
return {"gateway": gateway, "memory": memory}
@@ -1,4 +1,4 @@
import{r as K,a as Hv,g as Hr,R as Um,c as q2,j as Ne,l as Z2,S as K2,b as Vx,T as $2}from"./index-CazXw6yq.js";/** import{r as K,a as Hv,g as Hr,R as Um,c as q2,j as Ne,l as Z2,S as K2,b as Vx,T as $2}from"./index-COVfHYKu.js";/**
* @license * @license
* Copyright 2010-2023 Three.js Authors * Copyright 2010-2023 Three.js Authors
* SPDX-License-Identifier: MIT * SPDX-License-Identifier: MIT
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -7,8 +7,8 @@
<link rel="manifest" href="/manifest.webmanifest" /> <link rel="manifest" href="/manifest.webmanifest" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" /> <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<title>Mission Control 2.0</title> <title>Mission Control 2.0</title>
<script type="module" crossorigin src="/assets/index-CazXw6yq.js"></script> <script type="module" crossorigin src="/assets/index-COVfHYKu.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-SdhW3gX3.css"> <link rel="stylesheet" crossorigin href="/assets/index-BSw7DIjB.css">
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>
+12 -1
View File
@@ -257,7 +257,18 @@ export interface ConnectTool {
export interface ConnectResp { export interface ConnectResp {
host: string host: string
gateway_url: string gateway_url: string
tools: Record<string, ConnectTool> mc_url: string
tools: Record<string, ConnectTool> // Leitung 1 — Modell (IDEs/Agenten → Gateway)
memory: ConnectTool // Leitung 2 — Gedächtnis (separater MCP-Server)
}
export interface ConnectLine {
ok: boolean
detail: string
}
export interface ConnectHealth {
gateway: ConnectLine
memory: ConnectLine
} }
export interface Memory { export interface Memory {
+10
View File
@@ -7,6 +7,7 @@ import {
api, api,
type AgentStatus, type AgentStatus,
type ConnectResp, type ConnectResp,
type ConnectHealth,
type DiscoverResp, type DiscoverResp,
type DraftsResp, type DraftsResp,
type HermesBrainResp, type HermesBrainResp,
@@ -37,6 +38,7 @@ export const qk = {
discover: ["discover"] as const, discover: ["discover"] as const,
drafts: (target?: string) => ["drafts", target ?? ""] as const, drafts: (target?: string) => ["drafts", target ?? ""] as const,
connect: (params?: string) => ["connect", params ?? ""] as const, connect: (params?: string) => ["connect", params ?? ""] as const,
connectHealth: ["connect-health"] as const,
memory: (q?: string, category?: string) => ["memory", q ?? "", category ?? ""] as const, memory: (q?: string, category?: string) => ["memory", q ?? "", category ?? ""] as const,
memoryGraph: ["memory-graph"] as const, memoryGraph: ["memory-graph"] as const,
} }
@@ -101,6 +103,14 @@ export const useConnect = (params?: string) =>
queryFn: () => api<ConnectResp>(params ? `/api/connect?${params}` : "/api/connect"), queryFn: () => api<ConnectResp>(params ? `/api/connect?${params}` : "/api/connect"),
}) })
// Live-Status der zwei Leitungen (Gateway + Gedächtnis) — alle 15s aktualisiert.
export const useConnectHealth = () =>
useQuery({
queryKey: qk.connectHealth,
queryFn: () => api<ConnectHealth>("/api/connect/health"),
refetchInterval: 15000,
})
export const useMemory = (opts?: { q?: string; category?: string; limit?: number }) => export const useMemory = (opts?: { q?: string; category?: string; limit?: number }) =>
useQuery({ useQuery({
queryKey: qk.memory(opts?.q, opts?.category), queryKey: qk.memory(opts?.q, opts?.category),
+189 -55
View File
@@ -1,17 +1,90 @@
import { useState } from "react" import { useState } from "react"
import { Check, Copy, Terminal, Info, Globe, FolderOpen } from "lucide-react" import {
import { useConnect } from "@/lib/queries" Check, Copy, Cpu, Brain, Globe, FolderOpen, Laptop, ArrowRight,
Info, CircleCheck, CircleX, Loader2,
} from "lucide-react"
import { useConnect, useConnectHealth } from "@/lib/queries"
import type { ConnectTool, ConnectLine } from "@/lib/api"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
// Dateiziel je Tool — nur Hinweis im Editor-Kopf, keine Logik.
const FILE_NAMES: Record<string, string> = {
cline: "cline_settings.json",
cursor: "Settings → Models",
opencode: "opencode.jsonc",
zed: "settings.json",
continue: "~/.continue/config.json",
claude_code: "~/.zshrc / env",
}
function StatusBadge({ line, loading }: { line?: ConnectLine; loading: boolean }) {
if (loading || !line) {
return (
<span className="inline-flex items-center gap-1 text-[10px] font-semibold text-muted-foreground">
<Loader2 className="h-3 w-3 animate-spin" /> prüfe
</span>
)
}
return line.ok ? (
<span className="inline-flex items-center gap-1 text-[10px] font-semibold text-emerald-400">
<CircleCheck className="h-3 w-3" /> {line.detail}
</span>
) : (
<span className="inline-flex items-center gap-1 text-[10px] font-semibold text-red-400">
<CircleX className="h-3 w-3" /> {line.detail}
</span>
)
}
// Schwarzes „Editor-Fenster" mit Ampel-Dots + Kopieren-Knopf.
function CodeWindow({
tool, fileName, accent, copied, onCopy,
}: {
tool: ConnectTool
fileName: string
accent: "teal" | "violet"
copied: boolean
onCopy: () => void
}) {
return (
<div className="flex flex-col border border-border/60 bg-black/60 rounded-2xl overflow-hidden shadow-2xl">
<div className="flex h-11 items-center justify-between px-4 border-b border-border/40 bg-black/40 shrink-0">
<div className="flex items-center gap-1.5">
<span className="h-3 w-3 rounded-full bg-red-500/80" />
<span className="h-3 w-3 rounded-full bg-amber-500/80" />
<span className="h-3 w-3 rounded-full bg-emerald-500/80" />
</div>
<div className="flex items-center gap-1.5 text-[10px] font-mono text-muted-foreground/75 bg-background/30 px-3 py-1 rounded-md border border-border/20">
<span>{fileName}</span>
</div>
<button
onClick={onCopy}
className="flex h-7 px-2.5 items-center gap-1.5 rounded-lg border border-border/40 bg-background/30 text-[10px] font-semibold text-muted-foreground hover:text-foreground hover:bg-background/60 transition-all cursor-pointer"
>
{copied ? <Check className="h-3.5 w-3.5 text-emerald-400" /> : <Copy className="h-3.5 w-3.5" />}
<span>{copied ? "Kopiert" : "Kopieren"}</span>
</button>
</div>
<pre className={cn(
"p-5 overflow-x-auto text-xs font-mono whitespace-pre leading-relaxed scrollbar-thin select-text bg-black/10",
accent === "teal" ? "text-cyan-200/90" : "text-violet-200/90",
)}>
<code>{tool.snippet}</code>
</pre>
</div>
)
}
export function ConnectView() { export function ConnectView() {
const [host, setHost] = useState(localStorage.getItem("mc_host") || "192.168.178.151") const [host, setHost] = useState(localStorage.getItem("mc_host") || "192.168.178.151")
const [mcpPath, setMcpPath] = useState(localStorage.getItem("mc_mcp_path") || "") const [mcpPath, setMcpPath] = useState(localStorage.getItem("mc_mcp_path") || "")
const [active, setActive] = useState("cline") const [active, setActive] = useState("cline")
const [copied, setCopied] = useState(false) const [copied, setCopied] = useState<"model" | "memory" | null>(null)
const params = new URLSearchParams({ host }) const params = new URLSearchParams({ host })
if (mcpPath) params.set("mcp_path", mcpPath) if (mcpPath) params.set("mcp_path", mcpPath)
const { data, error: dataErr } = useConnect(params.toString()) const { data, error: dataErr } = useConnect(params.toString())
const { data: health, isLoading: healthLoading } = useConnectHealth()
const error = dataErr ? String(dataErr) : "" const error = dataErr ? String(dataErr) : ""
function saveHost(v: string) { function saveHost(v: string) {
@@ -26,11 +99,11 @@ export function ConnectView() {
const tool = data?.tools[active] const tool = data?.tools[active]
async function copy() { async function copy(which: "model" | "memory", snippet?: string) {
if (!tool) return if (!snippet) return
await navigator.clipboard.writeText(tool.snippet) await navigator.clipboard.writeText(snippet)
setCopied(true) setCopied(which)
setTimeout(() => setCopied(false), 1500) setTimeout(() => setCopied(null), 1500)
} }
return ( return (
@@ -41,11 +114,62 @@ export function ConnectView() {
Verbindung &amp; Integration Verbindung &amp; Integration
</h1> </h1>
<p className="text-sm text-muted-foreground"> <p className="text-sm text-muted-foreground">
Kopiere vorgefertigte Konfigurationsdateien für deinen lokalen PC (IDEs, Cline, Roo Code, Cursor), um direkt auf das geteilte Gedächtnis und den Auto-Swap-Gateway der Box zuzugreifen. Binde deinen lokalen Agenten an die Box an über <span className="text-foreground">zwei getrennte Leitungen</span>:
das Modell (Gateway) und das geteilte Gedächtnis (MCP).
</p> </p>
</div> </div>
{/* Connection Variables Panel */} {/* Hero: die zwei Leitungen */}
<div className="p-5 rounded-2xl border border-border/60 bg-card/45 backdrop-blur-md shadow-lg shadow-black/10">
<div className="grid gap-4 md:grid-cols-[170px_1fr] items-center">
{/* Lokaler Agent */}
<div className="rounded-2xl border border-border/60 bg-background/40 p-4 text-center">
<Laptop className="h-7 w-7 mx-auto text-muted-foreground" />
<div className="mt-2 text-sm font-semibold text-foreground">Dein lokaler Agent</div>
<div className="text-[11px] text-muted-foreground">Cline · Cursor · Zed </div>
</div>
{/* Zwei Leitungen */}
<div className="flex flex-col gap-2.5">
{/* Leitung 1 — Modell */}
<div className="flex items-center gap-3">
<ArrowRight className="h-4 w-4 text-muted-foreground shrink-0" />
<div className="flex-1 rounded-xl border border-primary/25 bg-primary/5 px-3.5 py-2.5">
<div className="flex items-center justify-between gap-2">
<div className="flex items-center gap-2 text-xs font-semibold text-primary">
<Cpu className="h-3.5 w-3.5" /> Leitung 1 Modell
</div>
<StatusBadge line={health?.gateway} loading={healthLoading} />
</div>
<div className="mt-1 text-[11px] font-mono text-primary/80">
Gateway · :9001/v1 · model auto
</div>
</div>
</div>
{/* Leitung 2 — Gedächtnis */}
<div className="flex items-center gap-3">
<ArrowRight className="h-4 w-4 text-muted-foreground shrink-0" />
<div className="flex-1 rounded-xl border border-violet-500/25 bg-violet-500/5 px-3.5 py-2.5">
<div className="flex items-center justify-between gap-2">
<div className="flex items-center gap-2 text-xs font-semibold text-violet-300">
<Brain className="h-3.5 w-3.5" /> Leitung 2 Gedächtnis
</div>
<StatusBadge line={health?.memory} loading={healthLoading} />
</div>
<div className="mt-1 text-[11px] font-mono text-violet-300/80">
MCP · mcp_memory.py · separat
</div>
</div>
</div>
</div>
</div>
<p className="mt-3.5 text-[11px] text-muted-foreground leading-relaxed">
Der Gateway liefert <span className="text-foreground">nur das LLM</span>. Das geteilte Gedächtnis läuft über einen
<span className="text-foreground"> eigenen MCP-Server</span> beide werden unabhängig eingerichtet.
</p>
</div>
{/* Gemeinsame Variablen */}
<div className="grid gap-4 md:grid-cols-2 p-5 rounded-2xl border border-border/60 bg-card/45 backdrop-blur-md shadow-lg shadow-black/10"> <div className="grid gap-4 md:grid-cols-2 p-5 rounded-2xl border border-border/60 bg-card/45 backdrop-blur-md shadow-lg shadow-black/10">
<div className="space-y-1.5"> <div className="space-y-1.5">
<label className="text-[10px] font-bold uppercase tracking-wider text-muted-foreground flex items-center gap-1.5"> <label className="text-[10px] font-bold uppercase tracking-wider text-muted-foreground flex items-center gap-1.5">
@@ -61,13 +185,14 @@ export function ConnectView() {
<div className="space-y-1.5"> <div className="space-y-1.5">
<label className="text-[10px] font-bold uppercase tracking-wider text-muted-foreground flex items-center gap-1.5"> <label className="text-[10px] font-bold uppercase tracking-wider text-muted-foreground flex items-center gap-1.5">
<FolderOpen className="h-3.5 w-3.5 text-primary" /> Lokaler MCP-Scriptpfad <FolderOpen className="h-3.5 w-3.5 text-violet-400" /> Lokaler MCP-Scriptpfad
<span className="text-violet-400/70 normal-case font-semibold tracking-normal">(nur Leitung 2)</span>
</label> </label>
<input <input
value={mcpPath} value={mcpPath}
onChange={(e) => saveMcpPath(e.target.value)} onChange={(e) => saveMcpPath(e.target.value)}
placeholder="z.B. F:\Coding Stuff\mission-control-2\mcp\mcp_memory.py" placeholder="z.B. F:\Coding Stuff\mission-control-2\mcp\mcp_memory.py"
className="w-full h-9 rounded-lg border border-border/60 bg-background/40 px-3 font-mono text-xs outline-none focus:ring-1 focus:ring-primary/50 text-foreground" className="w-full h-9 rounded-lg border border-border/60 bg-background/40 px-3 font-mono text-xs outline-none focus:ring-1 focus:ring-violet-500/50 text-foreground"
/> />
</div> </div>
</div> </div>
@@ -79,18 +204,28 @@ export function ConnectView() {
)} )}
{data && ( {data && (
<div className="space-y-4"> <div className="grid gap-5 lg:grid-cols-2">
{/* Tool Tab Bar */} {/* Leitung 1 — Modell anbinden */}
<div className="flex flex-wrap gap-1 bg-card/20 p-1 border border-border/40 rounded-xl max-w-fit"> <div className="space-y-3 rounded-2xl border border-border/60 border-t-2 border-t-primary/70 bg-card/30 p-4">
<div>
<div className="flex items-center gap-2 text-sm font-space font-bold text-foreground">
<span className="flex h-5 w-5 items-center justify-center rounded-md bg-primary/15 text-primary text-[11px]">1</span>
Modell anbinden
</div>
<p className="text-[11px] text-muted-foreground mt-0.5">Wähle dein Tool das Snippet zeigt auf den Gateway.</p>
</div>
{/* Tool-Picker */}
<div className="flex flex-wrap gap-1.5">
{Object.entries(data.tools).map(([key, t]) => ( {Object.entries(data.tools).map(([key, t]) => (
<button <button
key={key} key={key}
onClick={() => setActive(key)} onClick={() => setActive(key)}
className={cn( className={cn(
"rounded-lg px-4 py-1.5 text-xs font-semibold tracking-wide uppercase transition-all cursor-pointer", "rounded-lg px-3 py-1.5 text-[11px] font-semibold tracking-wide transition-all cursor-pointer",
active === key active === key
? "bg-primary text-primary-foreground shadow-md shadow-primary/10" ? "bg-primary text-primary-foreground shadow-md shadow-primary/10"
: "text-muted-foreground hover:text-foreground", : "border border-border/50 text-muted-foreground hover:text-foreground",
)} )}
> >
{t.label} {t.label}
@@ -99,50 +234,49 @@ export function ConnectView() {
</div> </div>
{tool && ( {tool && (
<div className="space-y-3"> <>
{/* Note / Info */}
{tool.note && ( {tool.note && (
<div className="flex items-start gap-2.5 p-3 rounded-xl bg-primary/5 border border-primary/20 text-xs text-muted-foreground leading-relaxed"> <div className="flex items-start gap-2.5 p-3 rounded-xl bg-primary/5 border border-primary/20 text-[11px] text-muted-foreground leading-relaxed">
<Info className="h-4.5 w-4.5 text-primary shrink-0 mt-0.5" /> <Info className="h-4 w-4 text-primary shrink-0 mt-0.5" />
<span>{tool.note}</span> <span>{tool.note}</span>
</div> </div>
)} )}
<CodeWindow
{/* Editor Mockup Window */} tool={tool}
<div className="flex flex-col border border-border/60 bg-black/60 rounded-2xl overflow-hidden shadow-2xl"> fileName={FILE_NAMES[active] || "config.json"}
{/* Editor Header Bar */} accent="teal"
<div className="flex h-11 items-center justify-between px-4 border-b border-border/40 bg-black/40 shrink-0"> copied={copied === "model"}
{/* Left: Window Control Dots */} onCopy={() => copy("model", tool.snippet)}
<div className="flex items-center gap-1.5"> />
<span className="h-3 w-3 rounded-full bg-red-500/80 shadow-md shadow-red-500/10" /> </>
<span className="h-3 w-3 rounded-full bg-amber-500/80 shadow-md shadow-amber-500/10" />
<span className="h-3 w-3 rounded-full bg-emerald-500/80 shadow-md shadow-emerald-500/10" />
</div>
{/* Center: File Title */}
<div className="flex items-center gap-1.5 text-[10px] font-mono text-muted-foreground/75 bg-background/30 px-3 py-1 rounded-md border border-border/20">
<Terminal className="h-3.5 w-3.5 text-primary" />
<span>{active === "cline" || active === "cursor" ? "config.json" : "settings.json"}</span>
</div>
{/* Right: Copy Action */}
<button
onClick={copy}
className="flex h-7 px-2.5 items-center gap-1.5 rounded-lg border border-border/40 bg-background/30 text-[10px] font-semibold text-muted-foreground hover:text-foreground hover:bg-background/60 transition-all cursor-pointer"
>
{copied ? <Check className="h-3.5 w-3.5 text-emerald-400" /> : <Copy className="h-3.5 w-3.5" />}
<span>{copied ? "Kopiert" : "Kopieren"}</span>
</button>
</div>
{/* Editor Code Area */}
<pre className="p-5 overflow-x-auto text-xs font-mono text-cyan-200/90 whitespace-pre leading-relaxed scrollbar-thin select-text bg-black/10">
<code>{tool.snippet}</code>
</pre>
</div>
</div>
)} )}
</div> </div>
{/* Leitung 2 — Gedächtnis anbinden */}
<div className="space-y-3 rounded-2xl border border-border/60 border-t-2 border-t-violet-500/70 bg-card/30 p-4">
<div>
<div className="flex items-center gap-2 text-sm font-space font-bold text-foreground">
<span className="flex h-5 w-5 items-center justify-center rounded-md bg-violet-500/15 text-violet-300 text-[11px]">2</span>
Gedächtnis anbinden
<span className="text-[10px] font-medium text-muted-foreground normal-case">optional</span>
</div>
<p className="text-[11px] text-muted-foreground mt-0.5">Ein MCP-Block gilt zusätzlich für <em>jedes</em> Tool aus Schritt 1.</p>
</div>
<div className="flex items-start gap-2.5 p-3 rounded-xl bg-violet-500/5 border border-violet-500/20 text-[11px] text-muted-foreground leading-relaxed">
<Info className="h-4 w-4 text-violet-400 shrink-0 mt-0.5" />
<span>{data.memory.note}</span>
</div>
<CodeWindow
tool={data.memory}
fileName="mcp.json"
accent="violet"
copied={copied === "memory"}
onCopy={() => copy("memory", data.memory.snippet)}
/>
</div>
</div>
)} )}
</div> </div>
) )