4 Commits

14 changed files with 823 additions and 406 deletions
+6
View File
@@ -51,7 +51,13 @@ if FRONTEND_DIST.exists():
@app.get("/{full_path:path}") @app.get("/{full_path:path}")
def spa(full_path: str): def spa(full_path: str):
# Falls die Datei direkt in FRONTEND_DIST liegt (z.B. manifest.webmanifest, favicon.ico), liefere sie aus
target = FRONTEND_DIST / full_path
if target.is_file():
return FileResponse(target)
index = FRONTEND_DIST / "index.html" index = FRONTEND_DIST / "index.html"
if index.exists(): if index.exists():
return FileResponse(index) return FileResponse(index)
return {"detail": "frontend not built"} return {"detail": "frontend not built"}
+4 -1
View File
@@ -24,13 +24,16 @@ def agent_status() -> dict:
config_path = home / "config.yaml" config_path = home / "config.yaml"
if config_path.exists(): if config_path.exists():
try: try:
from ruamel.yaml import YAML
r_yaml = YAML()
with config_path.open("r", encoding="utf-8") as f: with config_path.open("r", encoding="utf-8") as f:
cfg = yaml.load(f) or {} cfg = r_yaml.load(f) or {}
if isinstance(cfg, dict): if isinstance(cfg, dict):
brain_model = cfg.get("model", {}).get("model", "auto") brain_model = cfg.get("model", {}).get("model", "auto")
except Exception: except Exception:
pass pass
return { return {
"gateway_url": HERMES_API_URL, "gateway_url": HERMES_API_URL,
"webui_url": HERMES_WEBUI_URL, "webui_url": HERMES_WEBUI_URL,
+10 -3
View File
@@ -12,7 +12,7 @@ import re
import httpx import httpx
from ruamel.yaml.scalarstring import LiteralScalarString from ruamel.yaml.scalarstring import LiteralScalarString
from config import CMD_TEMPLATE, CONFIG_PATH, DEFAULT_TTL, LLAMA_SWAP_URL, yaml from config import CMD_TEMPLATE, CONFIG_PATH, DEFAULT_TTL, LLAMA_SWAP_URL
# Kanonische Rollen (vereinheitlicht ggü. v1: kein manager/reviewer mehr). # Kanonische Rollen (vereinheitlicht ggü. v1: kein manager/reviewer mehr).
ROLE_IDS = {"vision", "coder", "reasoning", "agent", "scout"} ROLE_IDS = {"vision", "coder", "reasoning", "agent", "scout"}
@@ -26,8 +26,11 @@ _QUANT_RE = re.compile(r"(Q\d_[A-Z0-9_]+|IQ\d_[A-Z0-9_]+|fp16|bf16)\.gguf", re.I
def read_config() -> dict: def read_config() -> dict:
if not CONFIG_PATH.exists(): if not CONFIG_PATH.exists():
return {"models": {}} return {"models": {}}
from ruamel.yaml import YAML
r_yaml = YAML()
r_yaml.preserve_quotes = True
with CONFIG_PATH.open("r", encoding="utf-8") as f: with CONFIG_PATH.open("r", encoding="utf-8") as f:
data = yaml.load(f) or {} data = r_yaml.load(f) or {}
if not data.get("models"): if not data.get("models"):
data["models"] = {} data["models"] = {}
return data return data
@@ -143,8 +146,11 @@ def write_config(cfg: dict) -> None:
try: try:
CONFIG_PATH.parent.mkdir(parents=True, exist_ok=True) CONFIG_PATH.parent.mkdir(parents=True, exist_ok=True)
tmp = CONFIG_PATH.with_name(CONFIG_PATH.name + ".tmp") tmp = CONFIG_PATH.with_name(CONFIG_PATH.name + ".tmp")
from ruamel.yaml import YAML
r_yaml = YAML()
r_yaml.preserve_quotes = True
with tmp.open("w", encoding="utf-8") as f: with tmp.open("w", encoding="utf-8") as f:
yaml.dump(cfg, f) r_yaml.dump(cfg, f)
os.replace(tmp, CONFIG_PATH) os.replace(tmp, CONFIG_PATH)
except PermissionError as exc: except PermissionError as exc:
raise PermissionError( raise PermissionError(
@@ -153,6 +159,7 @@ def write_config(cfg: dict) -> None:
) from exc ) from exc
def register_model(model_path: str, role: str | None = None, ctx: int = 8192, def register_model(model_path: str, role: str | None = None, ctx: int = 8192,
ttl: int | None = None, mmproj_path: str | None = None, ttl: int | None = None, mmproj_path: str | None = None,
jinja: bool = False) -> str: jinja: bool = False) -> str:
+2
View File
@@ -20,7 +20,9 @@ Environment=MC_CONFIG_PATH=/etc/llama-swap/config.yaml
Environment=MC_MODELS_DIR=/srv/models Environment=MC_MODELS_DIR=/srv/models
# Geteiltes Gedächtnis = die bestehende v1-DB (Kontinuität bis/über Cutover). # Geteiltes Gedächtnis = die bestehende v1-DB (Kontinuität bis/über Cutover).
Environment=MC_MEMORY_DB=/srv/models/mission-control-memory.db Environment=MC_MEMORY_DB=/srv/models/mission-control-memory.db
Environment=MC_ENGINE_UPDATE_CMD=/usr/local/bin/update-llamacpp
Restart=on-failure Restart=on-failure
RestartSec=3 RestartSec=3
[Install] [Install]
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
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -6,8 +6,8 @@
<meta name="theme-color" content="#0d1117" /> <meta name="theme-color" content="#0d1117" />
<link rel="manifest" href="/manifest.webmanifest" /> <link rel="manifest" href="/manifest.webmanifest" />
<title>Mission Control 2.0</title> <title>Mission Control 2.0</title>
<script type="module" crossorigin src="/assets/index-DpycPcE0.js"></script> <script type="module" crossorigin src="/assets/index-DQSWM8o1.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-GX6Y3U0B.css"> <link rel="stylesheet" crossorigin href="/assets/index-1l8zG4Un.css">
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>
+10 -3
View File
@@ -4,11 +4,11 @@ import { NAV, type ViewId } from "@/nav"
import { CommandPalette } from "@/components/CommandPalette" import { CommandPalette } from "@/components/CommandPalette"
import { DashboardView } from "@/views/DashboardView" import { DashboardView } from "@/views/DashboardView"
import { ModelsView } from "@/views/ModelsView" import { ModelsView } from "@/views/ModelsView"
import { RoutingView } from "@/views/RoutingView"
import { SystemView } from "@/views/SystemView" import { SystemView } from "@/views/SystemView"
import { ConnectView } from "@/views/ConnectView" import { ConnectView } from "@/views/ConnectView"
import { MemoryView } from "@/views/MemoryView" import { MemoryView } from "@/views/MemoryView"
import { AgentView } from "@/views/AgentView" import { AgentView } from "@/views/AgentView"
import { GuideView } from "@/views/GuideView"
import { Placeholder } from "@/views/Placeholder" import { Placeholder } from "@/views/Placeholder"
import { SystemDrawer } from "@/components/SystemDrawer" import { SystemDrawer } from "@/components/SystemDrawer"
import { api, type Health, type UpdatesResp } from "@/lib/api" import { api, type Health, type UpdatesResp } from "@/lib/api"
@@ -41,6 +41,12 @@ export default function App() {
localStorage.setItem("mc_theme", dark ? "dark" : "light") localStorage.setItem("mc_theme", dark ? "dark" : "light")
}, [dark]) }, [dark])
useEffect(() => {
const handleOpen = () => setDrawerOpen(true)
window.addEventListener("open-system-drawer", handleOpen)
return () => window.removeEventListener("open-system-drawer", handleOpen)
}, [])
const active = NAV.find((n) => n.id === view)! const active = NAV.find((n) => n.id === view)!
const totalUpdates = updates ? (updates.os + updates.engine + updates.models) : 0 const totalUpdates = updates ? (updates.os + updates.engine + updates.models) : 0
@@ -182,17 +188,18 @@ export default function App() {
<main className="flex-1 overflow-y-auto p-6 scrollbar-thin"> <main className="flex-1 overflow-y-auto p-6 scrollbar-thin">
{view === "dashboard" && <DashboardView />} {view === "dashboard" && <DashboardView />}
{view === "models" && <ModelsView />} {view === "models" && <ModelsView />}
{view === "routing" && <RoutingView />}
{view === "system" && <SystemView />} {view === "system" && <SystemView />}
{view === "connect" && <ConnectView />} {view === "connect" && <ConnectView />}
{view === "memory" && <MemoryView />} {view === "memory" && <MemoryView />}
{view === "agent" && <AgentView />} {view === "agent" && <AgentView />}
{!["dashboard", "models", "routing", "system", "connect", "memory", "agent"].includes(view) && ( {view === "guide" && <GuideView />}
{!["dashboard", "models", "system", "connect", "memory", "agent", "guide"].includes(view) && (
<Placeholder title={active.label} hint={active.hint} /> <Placeholder title={active.label} hint={active.hint} />
)} )}
</main> </main>
</div> </div>
</div> </div>
) )
} }
+6 -4
View File
@@ -1,15 +1,15 @@
import { import {
LayoutDashboard, LayoutDashboard,
Boxes, Boxes,
Route,
Cpu, Cpu,
Brain, Brain,
Plug, Plug,
Bot, Bot,
HelpCircle,
type LucideIcon, type LucideIcon,
} from "lucide-react" } from "lucide-react"
export type ViewId = "dashboard" | "models" | "routing" | "system" | "memory" | "connect" | "agent" export type ViewId = "dashboard" | "models" | "system" | "memory" | "connect" | "agent" | "guide"
export interface NavItem { export interface NavItem {
id: ViewId id: ViewId
@@ -21,10 +21,12 @@ export interface NavItem {
// Eine Quelle der Wahrheit für Sidebar UND Command-Palette. // Eine Quelle der Wahrheit für Sidebar UND Command-Palette.
export const NAV: NavItem[] = [ export const NAV: NavItem[] = [
{ id: "dashboard", label: "Zentrale", hint: "System- & Stack-Status", icon: LayoutDashboard }, { id: "dashboard", label: "Zentrale", hint: "System- & Stack-Status", icon: LayoutDashboard },
{ id: "models", label: "Modelle & Routing", hint: "Modelle kuratieren, Gruppen & Auto-Routing", icon: Boxes }, { id: "models", label: "Modell-Zentrale", hint: "Verwalten, laden & Gateway-Routing", icon: Boxes },
{ id: "routing", label: "Routing", hint: "Gateway-Regeln: schnell ↔ schwer", icon: Route },
{ id: "system", label: "Diagnose", hint: "Metriken, Dienste, Logs", icon: Cpu }, { id: "system", label: "Diagnose", hint: "Metriken, Dienste, Logs", icon: Cpu },
{ id: "memory", label: "Gedächtnis", hint: "Geteiltes Memory verwalten", icon: Brain }, { id: "memory", label: "Gedächtnis", hint: "Geteiltes Memory verwalten", icon: Brain },
{ id: "connect", label: "Verbinden", hint: "IDE-/Agent-Configs erzeugen", icon: Plug }, { id: "connect", label: "Verbinden", hint: "IDE-/Agent-Configs erzeugen", icon: Plug },
{ id: "agent", label: "Hermes", hint: "Agent-Status & WebUI öffnen", icon: Bot }, { id: "agent", label: "Hermes", hint: "Agent-Status & WebUI öffnen", icon: Bot },
{ id: "guide", label: "Anleitung", hint: "Einrichten & Vibe-Coding", icon: HelpCircle },
] ]
+125 -52
View File
@@ -1,6 +1,6 @@
import { useEffect, useState } from "react" import { useEffect, useState } from "react"
import { Bot, Cpu, ExternalLink, Brain, Layers, Plus } from "lucide-react" import { Bot, Cpu, ExternalLink, Brain, Layers, Plus, ShieldAlert } from "lucide-react"
import { api, type SystemStatus, type AgentStatus, type ModelInfo, type Memory } from "@/lib/api" import { api, type SystemStatus, type AgentStatus, type ModelInfo, type Memory, type UpdatesResp } from "@/lib/api"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
function gb(b: number) { function gb(b: number) {
@@ -12,7 +12,6 @@ function RadialGauge({ value, label, detail }: { value: number; label: string; d
const circ = 2 * Math.PI * radius const circ = 2 * Math.PI * radius
const offset = circ - (Math.min(value, 100) / 100) * circ const offset = circ - (Math.min(value, 100) / 100) * circ
// Verfärbung bei hoher Last
const strokeColor = value > 90 const strokeColor = value > 90
? "stroke-red-500" ? "stroke-red-500"
: value > 75 : value > 75
@@ -40,6 +39,7 @@ export function DashboardView() {
const [models, setModels] = useState<ModelInfo[]>([]) const [models, setModels] = useState<ModelInfo[]>([])
const [running, setRunning] = useState<string[]>([]) const [running, setRunning] = useState<string[]>([])
const [memories, setMemories] = useState<Memory[]>([]) const [memories, setMemories] = useState<Memory[]>([])
const [updates, setUpdates] = useState<UpdatesResp | null>(null)
// Quick Memory Form State // Quick Memory Form State
const [memContent, setMemContent] = useState("") const [memContent, setMemContent] = useState("")
@@ -51,11 +51,12 @@ export function DashboardView() {
api<AgentStatus>("/api/agent/status").then(setAgent).catch(() => {}) api<AgentStatus>("/api/agent/status").then(setAgent).catch(() => {})
api<{ models: ModelInfo[]; running?: string[] }>("/api/models") api<{ models: ModelInfo[]; running?: string[] }>("/api/models")
.then((d) => { .then((d) => {
setModels(d.models) setModels(d.models || [])
setRunning(d.running || []) setRunning(d.running || [])
}) })
.catch(() => {}) .catch(() => {})
api<Memory[]>("/api/memory?category=").then((d) => setMemories(d.slice(0, 3))).catch(() => {}) api<Memory[]>("/api/memory?category=").then((d) => setMemories(d.slice(0, 3))).catch(() => {})
api<UpdatesResp>("/api/maintenance/updates").then(setUpdates).catch(() => {})
} }
useEffect(() => { useEffect(() => {
@@ -73,7 +74,6 @@ export function DashboardView() {
body: JSON.stringify({ content: memContent, category: memCat, source: "dashboard" }), body: JSON.stringify({ content: memContent, category: memCat, source: "dashboard" }),
}) })
setMemContent("") setMemContent("")
// Liste sofort aktualisieren
api<Memory[]>("/api/memory?category=").then((d) => setMemories(d.slice(0, 3))).catch(() => {}) api<Memory[]>("/api/memory?category=").then((d) => setMemories(d.slice(0, 3))).catch(() => {})
} catch (e) { } catch (e) {
console.error(e) console.error(e)
@@ -86,6 +86,7 @@ export function DashboardView() {
return ( return (
<div className="space-y-6"> <div className="space-y-6">
{/* Title */}
<div> <div>
<h1 className="text-2xl font-space font-bold tracking-tight bg-gradient-to-r from-foreground via-foreground to-primary bg-clip-text text-transparent"> <h1 className="text-2xl font-space font-bold tracking-tight bg-gradient-to-r from-foreground via-foreground to-primary bg-clip-text text-transparent">
Zentrale Zentrale
@@ -93,9 +94,10 @@ export function DashboardView() {
<p className="text-sm text-muted-foreground">Aktueller Status von System, Modellen und Agent.</p> <p className="text-sm text-muted-foreground">Aktueller Status von System, Modellen und Agent.</p>
</div> </div>
<div className="grid gap-6 sm:grid-cols-2"> {/* Top Grid: System stats & Updates (3 Columns) */}
{/* Card 1: System Status */} <div className="grid gap-6 md:grid-cols-3">
<div className="flex flex-col justify-between rounded-2xl border border-border/60 bg-card/45 backdrop-blur-md p-5 shadow-lg shadow-black/15"> {/* Card 1: System Status (2 columns wide) */}
<div className="md:col-span-2 flex flex-col justify-between rounded-2xl border border-border/60 bg-card/45 backdrop-blur-md p-5 shadow-lg shadow-black/15">
<div> <div>
<div className="flex items-center gap-2 mb-4"> <div className="flex items-center gap-2 mb-4">
<Cpu className="h-4.5 w-4.5 text-primary" /> <Cpu className="h-4.5 w-4.5 text-primary" />
@@ -105,11 +107,11 @@ export function DashboardView() {
<div className="grid grid-cols-2 gap-3 sm:grid-cols-4"> <div className="grid grid-cols-2 gap-3 sm:grid-cols-4">
<RadialGauge value={sys.cpu.percent} label="CPU" detail={`${sys.cpu.cores} Cores`} /> <RadialGauge value={sys.cpu.percent} label="CPU" detail={`${sys.cpu.cores} Cores`} />
<RadialGauge value={sys.ram.percent} label="RAM" detail={`${gb(sys.ram.used)} / ${gb(sys.ram.total)} GB`} /> <RadialGauge value={sys.ram.percent} label="RAM" detail={`${gb(sys.ram.used)} / ${gb(sys.ram.total)} GB`} />
{sys.gpu && sys.gpu.busy_percent != null && ( {sys.gpu && sys.gpu.busy_percent != null && sys.gpu.gtt_used != null && sys.gpu.gtt_total != null && (
<RadialGauge <RadialGauge
value={sys.gpu.busy_percent} value={sys.gpu.busy_percent}
label="GPU" label="GPU"
detail={sys.gpu.gtt_used != null && sys.gpu.gtt_total != null ? `${gb(sys.gpu.gtt_used)} / ${gb(sys.gpu.gtt_total)} GB` : undefined} detail={`${gb(sys.gpu.gtt_used)} / ${gb(sys.gpu.gtt_total)} GB`}
/> />
)} )}
{sys.disk && ( {sys.disk && (
@@ -128,7 +130,80 @@ export function DashboardView() {
)} )}
</div> </div>
{/* Card 2: Hermes Agent Status */} {/* Card 2: Updates & Wartung (1 column wide) */}
<div className="rounded-2xl border border-border/60 bg-card/45 backdrop-blur-md p-5 shadow-lg shadow-black/15 flex flex-col justify-between">
<div>
<div className="flex items-center gap-2 mb-4">
<ShieldAlert className="h-4.5 w-4.5 text-primary" />
<h2 className="text-sm font-semibold tracking-wide uppercase text-foreground">Updates &amp; Pflege</h2>
</div>
{updates ? (
<div className="space-y-2.5">
<div className="space-y-1.5">
<div className={cn(
"flex items-center justify-between px-3 py-1.5 rounded-lg border text-xs",
updates.os > 0
? "border-amber-500/30 bg-amber-500/5 text-amber-400 font-semibold"
: "border-border/30 bg-background/25 text-muted-foreground"
)}>
<span>OS-Pakete</span>
<span className="font-mono">{updates.os > 0 ? `${updates.os} verfügbar` : "aktuell"}</span>
</div>
<div className={cn(
"flex items-center justify-between px-3 py-1.5 rounded-lg border text-xs",
updates.engine > 0
? "border-amber-500/30 bg-amber-500/5 text-amber-400 font-semibold"
: "border-border/30 bg-background/25 text-muted-foreground"
)}>
<span>Engine (llama.cpp)</span>
<span className="font-mono">{updates.engine > 0 ? "Update verfügbar" : "aktuell"}</span>
</div>
<div className={cn(
"flex items-center justify-between px-3 py-1.5 rounded-lg border text-xs",
updates.models > 0
? "border-primary/30 bg-primary/5 text-primary font-semibold animate-pulse"
: "border-border/30 bg-background/25 text-muted-foreground"
)}>
<span>Modell-Upgrades</span>
<span className="font-mono">{updates.models > 0 ? `${updates.models} verfügbar` : "aktuell"}</span>
</div>
</div>
{updates.model_list.length > 0 && (
<div className="mt-2 space-y-1">
<div className="text-[9px] uppercase font-bold text-muted-foreground/60 tracking-wider">Upgrades:</div>
<div className="max-h-20 overflow-y-auto space-y-1 pr-1 scrollbar-thin">
{updates.model_list.map((m) => (
<div key={m.repo} className="text-[9px] bg-background/20 border border-border/20 rounded p-1.5 font-mono text-muted-foreground truncate" title={`${m.role}: ${m.repo}`}>
<span className="text-primary font-semibold uppercase">{m.role}</span>: {m.repo.split("/").pop()}
</div>
))}
</div>
</div>
)}
</div>
) : (
<div className="h-24 flex items-center justify-center text-xs text-muted-foreground">Lade Updates...</div>
)}
</div>
<div className="mt-4 border-t border-border/30 pt-3 shrink-0">
<button
onClick={() => window.dispatchEvent(new CustomEvent("open-system-drawer"))}
className="w-full h-8 flex items-center justify-center gap-1.5 rounded-lg bg-primary text-primary-foreground text-xs font-semibold hover:opacity-90 transition-all cursor-pointer shadow-md shadow-primary/10"
>
System-Zentrale öffnen
</button>
</div>
</div>
</div>
{/* Bottom Grid: Agent, Roles & Memory (3 Columns) */}
<div className="grid gap-6 md:grid-cols-3">
{/* Card 3: Hermes Agent Status */}
<div className="rounded-2xl border border-border/60 bg-card/45 backdrop-blur-md p-5 shadow-lg shadow-black/15 flex flex-col justify-between"> <div className="rounded-2xl border border-border/60 bg-card/45 backdrop-blur-md p-5 shadow-lg shadow-black/15 flex flex-col justify-between">
<div> <div>
<div className="flex items-center justify-between mb-4"> <div className="flex items-center justify-between mb-4">
@@ -188,10 +263,8 @@ export function DashboardView() {
Gedächtnis & Stack-Tools via MCP gekoppelt. Gedächtnis & Stack-Tools via MCP gekoppelt.
</div> </div>
</div> </div>
</div>
<div className="grid gap-6 sm:grid-cols-2"> {/* Card 4: Active Model Roles */}
{/* Card 3: Active Model Roles */}
<div className="rounded-2xl border border-border/60 bg-card/45 backdrop-blur-md p-5 shadow-lg shadow-black/15 flex flex-col justify-between"> <div className="rounded-2xl border border-border/60 bg-card/45 backdrop-blur-md p-5 shadow-lg shadow-black/15 flex flex-col justify-between">
<div> <div>
<div className="flex items-center gap-2 mb-4"> <div className="flex items-center gap-2 mb-4">
@@ -199,7 +272,7 @@ export function DashboardView() {
<h2 className="text-sm font-semibold tracking-wide uppercase text-foreground">Aktive Rollen</h2> <h2 className="text-sm font-semibold tracking-wide uppercase text-foreground">Aktive Rollen</h2>
</div> </div>
<div className="space-y-2"> <div className="space-y-2 max-h-48 overflow-y-auto pr-1 scrollbar-thin">
{activeModels.length === 0 ? ( {activeModels.length === 0 ? (
<div className="text-xs text-muted-foreground py-6 text-center">Keine Modelle als Rollen zugewiesen.</div> <div className="text-xs text-muted-foreground py-6 text-center">Keine Modelle als Rollen zugewiesen.</div>
) : ( ) : (
@@ -209,15 +282,15 @@ export function DashboardView() {
<div <div
key={m.name} key={m.name}
className={cn( className={cn(
"flex items-center justify-between p-3 rounded-xl border transition-all duration-300", "flex items-center justify-between p-2.5 rounded-xl border transition-all duration-300",
isRunning isRunning
? "border-primary/50 bg-primary/5 shadow-md shadow-primary/5" ? "border-primary/50 bg-primary/5 shadow-md shadow-primary/5"
: "border-border/40 bg-background/20" : "border-border/40 bg-background/20"
)} )}
> >
<div> <div className="min-w-0 flex-1 mr-2">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span className={cn("text-xs font-semibold uppercase px-1.5 py-0.5 rounded", <span className={cn("text-[9px] font-semibold uppercase px-1.5 py-0.5 rounded font-mono",
m.role === "fast" ? "bg-cyan-500/15 text-cyan-400" : m.role === "fast" ? "bg-cyan-500/15 text-cyan-400" :
m.role === "heavy" ? "bg-amber-500/15 text-amber-400" : m.role === "heavy" ? "bg-amber-500/15 text-amber-400" :
m.role === "coder" ? "bg-violet-500/15 text-violet-400" : m.role === "coder" ? "bg-violet-500/15 text-violet-400" :
@@ -232,12 +305,12 @@ export function DashboardView() {
</span> </span>
)} )}
</div> </div>
<div className="text-xs font-medium mt-1.5 truncate max-w-[200px] sm:max-w-[280px]" title={m.name}> <div className="text-[10px] font-medium mt-1 truncate" title={m.name}>
{m.name} {m.name}
</div> </div>
</div> </div>
<div className="text-[10px] font-mono text-muted-foreground"> <div className="text-[9px] font-mono text-muted-foreground shrink-0">
{isRunning ? "Warm / Aktiv" : "Bereit"} {isRunning ? "Warm" : "Bereit"}
</div> </div>
</div> </div>
) )
@@ -250,29 +323,27 @@ export function DashboardView() {
</div> </div>
</div> </div>
{/* Card 4: Quick Memory Input */} {/* Card 5: Quick Memory Input */}
<div className="rounded-2xl border border-border/60 bg-card/45 backdrop-blur-md p-5 shadow-lg shadow-black/15 flex flex-col justify-between"> <div className="rounded-2xl border border-border/60 bg-card/45 backdrop-blur-md p-5 shadow-lg shadow-black/15 flex flex-col justify-between">
<div> <div>
<div className="flex items-center gap-2 mb-4"> <div className="flex items-center gap-2 mb-4">
<Brain className="h-4.5 w-4.5 text-primary" /> <Brain className="h-4.5 w-4.5 text-primary" />
<h2 className="text-sm font-semibold tracking-wide uppercase text-foreground">Gedächtnis-Schnellform</h2> <h2 className="text-sm font-semibold tracking-wide uppercase text-foreground">Gedächtnis</h2>
</div> </div>
<div className="space-y-3"> <div className="space-y-3">
<div className="flex items-start gap-2"> <textarea
<textarea value={memContent}
value={memContent} onChange={(e) => setMemContent(e.target.value)}
onChange={(e) => setMemContent(e.target.value)} placeholder="Fakt / Regel im Pool speichern..."
placeholder="Fakt / Regel auf der Box speichern..." rows={2}
rows={2} className="w-full resize-none rounded-xl border border-border/50 bg-background/30 p-2 text-[10px] outline-none focus:ring-1 focus:ring-primary transition-all leading-normal"
className="flex-1 resize-none rounded-xl border border-border/50 bg-background/30 px-3 py-2 text-xs outline-none focus:ring-1.5 focus:ring-primary transition-all" />
/> <div className="flex items-center gap-2 justify-between">
</div>
<div className="flex items-center gap-2 justify-end">
<select <select
value={memCat} value={memCat}
onChange={(e) => setMemCat(e.target.value)} onChange={(e) => setMemCat(e.target.value)}
className="rounded-lg border border-border/50 bg-background/50 px-2 py-1 text-xs outline-none" className="h-7 rounded-lg border border-border/50 bg-background/50 px-2 text-[10px] outline-none cursor-pointer"
> >
<option value="stable">🔵 Fakt</option> <option value="stable">🔵 Fakt</option>
<option value="instruction">📋 Regel</option> <option value="instruction">📋 Regel</option>
@@ -282,33 +353,35 @@ export function DashboardView() {
<button <button
onClick={saveQuickMemory} onClick={saveQuickMemory}
disabled={!memContent.trim() || savingMem} disabled={!memContent.trim() || savingMem}
className="flex items-center gap-1 rounded-lg bg-primary px-3 py-1 text-xs font-semibold text-primary-foreground hover:opacity-90 disabled:opacity-50 transition-all cursor-pointer" className="flex h-7 items-center gap-1 rounded-lg bg-primary px-3 text-[10px] font-semibold text-primary-foreground hover:opacity-90 disabled:opacity-50 transition-all cursor-pointer"
> >
<Plus className="h-3.5 w-3.5" /> Speichern <Plus className="h-3.5 w-3.5" /> Speichern
</button> </button>
</div> </div>
</div> </div>
<div className="mt-4 space-y-2"> <div className="mt-3.5 space-y-1.5">
<div className="text-[10px] text-muted-foreground uppercase font-semibold tracking-wider">Zuletzt gespeichert:</div> <div className="text-[9px] text-muted-foreground uppercase font-bold tracking-wider">Zuletzt gespeichert:</div>
{memories.length === 0 ? ( <div className="max-h-20 overflow-y-auto space-y-1 pr-1 scrollbar-thin">
<div className="text-xs text-muted-foreground/75 py-2">Keine Einträge vorhanden.</div> {memories.length === 0 ? (
) : ( <div className="text-[10px] text-muted-foreground/75 py-1">Keine Einträge vorhanden.</div>
memories.map((m) => ( ) : (
<div key={m.id} className="text-xs bg-background/10 border border-border/30 rounded-lg p-2 flex items-start gap-2"> memories.map((m) => (
<span className="shrink-0 text-[10px] font-mono text-muted-foreground/80 px-1 py-0.5 rounded bg-muted/20"> <div key={m.id} className="text-[10px] bg-background/10 border border-border/30 rounded p-1.5 flex items-start gap-1.5">
{m.category} <span className="shrink-0 text-[8px] font-mono text-muted-foreground/80 px-1 py-0.5 rounded bg-muted/20">
</span> {m.category}
<span className="truncate flex-1 text-muted-foreground hover:text-foreground transition-colors" title={m.content}> </span>
{m.content} <span className="truncate flex-1 text-muted-foreground hover:text-foreground transition-colors" title={m.content}>
</span> {m.content}
</div> </span>
)) </div>
)} ))
)}
</div>
</div> </div>
</div> </div>
<div className="mt-4 text-[10px] text-muted-foreground/80 border-t border-border/30 pt-3"> <div className="mt-4 text-[10px] text-muted-foreground/80 border-t border-border/30 pt-3">
Steht allen Clients (IDEs, Hermes) per MCP zur Verfügung. Steht allen Clients per MCP zur Verfügung.
</div> </div>
</div> </div>
</div> </div>
+305
View File
@@ -0,0 +1,305 @@
import { useState, useEffect } from "react"
import { BookOpen, Layers, Brain, Terminal, Code, RefreshCw, Cpu, Star } from "lucide-react"
import { api, type Health } from "@/lib/api"
import { cn } from "@/lib/utils"
export function GuideView() {
const [activeTab, setActiveTab] = useState<"roocode" | "cursor" | "opencode">("roocode")
const [health, setHealth] = useState<Health | null>(null)
const [testing, setTesting] = useState(false)
const [testResult, setTestResult] = useState<string | null>(null)
function checkConnection() {
setTesting(true)
api<Health>("/api/health")
.then((h) => {
setHealth(h)
setTestResult(h.engine_reachable ? "success" : "partial")
})
.catch(() => {
setHealth(null)
setTestResult("fail")
})
.finally(() => setTesting(false))
}
useEffect(() => {
checkConnection()
}, [])
return (
<div className="space-y-6">
{/* Title */}
<div>
<h1 className="text-2xl font-space font-bold tracking-tight bg-gradient-to-r from-foreground via-foreground to-primary bg-clip-text text-transparent">
Stack-Anleitung &amp; Vibe-Coding-Guide
</h1>
<p className="text-sm text-muted-foreground">
Einsteigerfreundliche Erklärungen zu deinem Stack und Schritt-für-Schritt-Anleitungen zur Anbindung deiner Editoren.
</p>
</div>
{/* Connection HUD Panel */}
<div className="rounded-2xl border border-border/60 bg-card/45 backdrop-blur-md p-5 shadow-lg shadow-black/10 flex flex-col sm:flex-row justify-between sm:items-center gap-4">
<div className="flex items-center gap-3">
<span className={cn(
"h-3 w-3 rounded-full ring-2 ring-black/40",
testResult === "success" && "bg-emerald-500 animate-pulse",
testResult === "partial" && "bg-amber-500",
testResult === "fail" && "bg-red-500",
!testResult && "bg-muted"
)} />
<div>
<div className="text-xs font-bold uppercase tracking-wider text-foreground">Lokaler Verbindungs-Check</div>
<div className="text-[10px] text-muted-foreground mt-0.5 font-mono">
{testResult === "success" && `Erfolgreich! Dein PC hat Zugriff auf das Box-Gateway (v${health?.version || ""}).`}
{testResult === "partial" && "Gateway erreichbar, aber die llama-cpp-Engine ist offline."}
{testResult === "fail" && "Verbindung fehlgeschlagen. Ist die Box im selben LAN-Netzwerk?"}
{!testResult && "Verbindung wird geprüft..."}
</div>
</div>
</div>
<button
onClick={checkConnection}
disabled={testing}
className="h-8 px-3 flex items-center gap-1.5 text-xs font-semibold rounded-lg border border-border/60 bg-background/20 hover:border-primary/50 transition-colors disabled:opacity-50 cursor-pointer self-start sm:self-auto shrink-0"
>
<RefreshCw className={cn("h-3.5 w-3.5", testing && "animate-spin")} />
<span>Testen</span>
</button>
</div>
{/* Conceptual Explanation Grid */}
<div className="space-y-3">
<div className="flex items-center gap-2 px-1">
<BookOpen className="h-4.5 w-4.5 text-primary" />
<h2 className="text-xs font-bold uppercase tracking-wider text-foreground">Wie funktioniert mein Stack?</h2>
</div>
<div className="grid gap-4 sm:grid-cols-3">
{/* Card 1: Dashboard */}
<div className="p-4 rounded-xl border border-border/60 bg-card/20 space-y-2">
<div className="flex items-center gap-1.5">
<Cpu className="h-4 w-4 text-cyan-400" />
<h3 className="text-xs font-bold text-foreground">1. Die Zentrale</h3>
</div>
<p className="text-[11px] text-muted-foreground leading-relaxed">
Dein Dashboard. Hier siehst du die CPU-/RAM- und GPU-Last der Box und siehst sofort, ob Updates anstehen.
</p>
</div>
{/* Card 2: Llama Swap */}
<div className="p-4 rounded-xl border border-border/60 bg-card/20 space-y-2">
<div className="flex items-center gap-1.5">
<Layers className="h-4 w-4 text-violet-400" />
<h3 className="text-xs font-bold text-foreground">2. Modell-Zentrale</h3>
</div>
<p className="text-[11px] text-muted-foreground leading-relaxed">
Deine GGUF-Datenbank. Gesteuert von <strong>llama-swap</strong>. Lädt dein schnelles Alltags-Hirn (`fast`) oder dein schweres Logik-Hirn (`heavy`) vollautomatisch im VRAM.
</p>
</div>
{/* Card 3: Memory */}
<div className="p-4 rounded-xl border border-border/60 bg-card/20 space-y-2">
<div className="flex items-center gap-1.5">
<Brain className="h-4 w-4 text-indigo-400" />
<h3 className="text-xs font-bold text-foreground">3. Das Gedächtnis</h3>
</div>
<p className="text-[11px] text-muted-foreground leading-relaxed">
Dein geteiltes Langzeitgedächtnis (Memory-Pool). Hier merkt sich dein Agent Regeln, Projekt-Details und Vorlieben.
</p>
</div>
</div>
</div>
{/* Editor Integration Guides */}
<div className="space-y-4">
<div className="flex items-center gap-2 px-1">
<Code className="h-4.5 w-4.5 text-primary" />
<h2 className="text-xs font-bold uppercase tracking-wider text-foreground">Vibe Coding auf dem PC einrichten</h2>
</div>
{/* Editor Selector Tabs */}
<div className="flex gap-1.5 bg-card/20 p-1 border border-border/40 rounded-xl max-w-fit">
<button
onClick={() => setActiveTab("roocode")}
className={cn(
"rounded-lg px-4 py-1.5 text-xs font-semibold tracking-wide uppercase transition-all cursor-pointer flex items-center gap-1.5",
activeTab === "roocode"
? "bg-primary text-primary-foreground shadow-md shadow-primary/10"
: "text-muted-foreground hover:text-foreground"
)}
>
<Star className="h-3.5 w-3.5 fill-amber-400/20" />
Roo Code (VS Code)
</button>
<button
onClick={() => setActiveTab("cursor")}
className={cn(
"rounded-lg px-4 py-1.5 text-xs font-semibold tracking-wide uppercase transition-all cursor-pointer",
activeTab === "cursor"
? "bg-primary text-primary-foreground shadow-md shadow-primary/10"
: "text-muted-foreground hover:text-foreground"
)}
>
Cursor IDE
</button>
<button
onClick={() => setActiveTab("opencode")}
className={cn(
"rounded-lg px-4 py-1.5 text-xs font-semibold tracking-wide uppercase transition-all cursor-pointer",
activeTab === "opencode"
? "bg-primary text-primary-foreground shadow-md shadow-primary/10"
: "text-muted-foreground hover:text-foreground"
)}
>
OpenCode Desktop
</button>
</div>
{/* Guide Content */}
<div className="rounded-2xl border border-border/60 bg-card/45 backdrop-blur-md p-6 space-y-4 shadow-lg shadow-black/10">
{activeTab === "roocode" && (
<div className="space-y-4 text-xs leading-relaxed text-muted-foreground">
<div className="space-y-1">
<h3 className="text-sm font-bold text-foreground">Roo Code Kopplung (Empfohlene Open-Source-Erweiterung)</h3>
<p>Roo Code ist die beliebteste und flexibelste Vibe-Coding-Erweiterung für VS Code im Jahr 2026. Sie ermöglicht vollen Zugriff auf das Terminal und das MCP-Gedächtnis.</p>
</div>
<div className="space-y-3.5 border-t border-border/20 pt-4">
<div className="space-y-1">
<div className="font-semibold text-foreground flex items-center gap-1.5">
<span className="h-4.5 w-4.5 rounded-full bg-primary/20 text-primary flex items-center justify-center font-mono text-[10px]">1</span>
Roo Code installieren
</div>
<p className="pl-6">Suche in VS Code nach der Erweiterung <strong>Roo Code</strong> und installiere sie.</p>
</div>
<div className="space-y-1">
<div className="font-semibold text-foreground flex items-center gap-1.5">
<span className="h-4.5 w-4.5 rounded-full bg-primary/20 text-primary flex items-center justify-center font-mono text-[10px]">2</span>
API-Anbindung konfigurieren
</div>
<p className="pl-6">Klicke auf das Roo-Code-Symbol in der Sidebar, öffne die Einstellungen (Zahnrad) und stelle folgendes ein:</p>
<div className="pl-6 pt-1">
<div className="p-3 bg-background/25 rounded-xl border border-border/30 font-mono text-[10px] space-y-1 text-foreground">
<div><span className="text-muted-foreground/60">API Provider:</span> OpenAI Compatible</div>
<div><span className="text-muted-foreground/60">Base URL:</span> http://192.168.178.151:9001/v1</div>
<div><span className="text-muted-foreground/60">API Key:</span> <span className="italic text-muted-foreground/50">beliebig (z.B. "local")</span></div>
<div><span className="text-muted-foreground/60">Model ID:</span> auto</div>
</div>
</div>
</div>
<div className="space-y-1">
<div className="font-semibold text-foreground flex items-center gap-1.5">
<span className="h-4.5 w-4.5 rounded-full bg-primary/20 text-primary flex items-center justify-center font-mono text-[10px]">3</span>
MCP Gedächtnis verknüpfen (Optional, aber empfohlen)
</div>
<p className="pl-6">Damit Roo Code auf deinen <strong>Gedächtnis-Pool</strong> zugreifen kann, kopiere die MCP-Konfiguration aus dem Reiter <strong>Verbinden</strong> und füge sie in deine lokale Roo-Code-Konfigurationsdatei ein.</p>
</div>
</div>
</div>
)}
{activeTab === "cursor" && (
<div className="space-y-4 text-xs leading-relaxed text-muted-foreground">
<div className="space-y-1">
<h3 className="text-sm font-bold text-foreground">Cursor IDE Kopplung (Proprietäre All-in-One IDE)</h3>
<p>Cursor ist ein polierter, extrem schneller VS-Code-Fork mit hervorragenden, tief integrierten Autovervollständigungen (Tab-Completions).</p>
</div>
<div className="space-y-3.5 border-t border-border/20 pt-4">
<div className="space-y-1">
<div className="font-semibold text-foreground flex items-center gap-1.5">
<span className="h-4.5 w-4.5 rounded-full bg-primary/20 text-primary flex items-center justify-center font-mono text-[10px]">1</span>
Einstellungen öffnen
</div>
<p className="pl-6">Öffne Cursor, klicke oben rechts auf das Zahnrad (Settings) und navigiere zu <strong>Models</strong>.</p>
</div>
<div className="space-y-1">
<div className="font-semibold text-foreground flex items-center gap-1.5">
<span className="h-4.5 w-4.5 rounded-full bg-primary/20 text-primary flex items-center justify-center font-mono text-[10px]">2</span>
OpenAI API überschreiben
</div>
<p className="pl-6">Deaktiviere die Standard-Cloudmodelle, klappe den Bereich <strong>OpenAI API</strong> auf und konfiguriere:</p>
<div className="pl-6 pt-1">
<div className="p-3 bg-background/25 rounded-xl border border-border/30 font-mono text-[10px] space-y-1 text-foreground">
<div><span className="text-muted-foreground/60">Override Base URL:</span> http://192.168.178.151:9001/v1</div>
<div><span className="text-muted-foreground/60">API Key:</span> <span className="italic text-muted-foreground/50">beliebig (z.B. "local")</span></div>
</div>
</div>
</div>
<div className="space-y-1">
<div className="font-semibold text-foreground flex items-center gap-1.5">
<span className="h-4.5 w-4.5 rounded-full bg-primary/20 text-primary flex items-center justify-center font-mono text-[10px]">3</span>
Modell hinzufügen
</div>
<p className="pl-6">Trage in der Modell-Liste ein neues Modell mit dem Namen <strong>auto</strong> ein und wähle es als aktives Modell aus. Cursor leitet ab jetzt alle deine Anfragen an die Box weiter.</p>
</div>
</div>
</div>
)}
{activeTab === "opencode" && (
<div className="space-y-4 text-xs leading-relaxed text-muted-foreground">
<div className="space-y-1">
<h3 className="text-sm font-bold text-foreground">OpenCode Desktop Kopplung (Open-Source Vibe-Coding-App)</h3>
<p>OpenCode ist eine standalone Desktop-App für ein ablenkungsfreies Coden über natürliche Sprache. Es trennt den KI-Prozess von deinem Editor.</p>
</div>
<div className="space-y-3.5 border-t border-border/20 pt-4">
<div className="space-y-1">
<div className="font-semibold text-foreground flex items-center gap-1.5">
<span className="h-4.5 w-4.5 rounded-full bg-primary/20 text-primary flex items-center justify-center font-mono text-[10px]">1</span>
OpenCode Desktop herunterladen
</div>
<p className="pl-6">Lade die Desktop-Anwendung von der offiziellen Website (opencode.ai) herunter und starte sie.</p>
</div>
<div className="space-y-1">
<div className="font-semibold text-foreground flex items-center gap-1.5">
<span className="h-4.5 w-4.5 rounded-full bg-primary/20 text-primary flex items-center justify-center font-mono text-[10px]">2</span>
Endpunkt auf Box-Gateway setzen
</div>
<p className="pl-6">Gehe in den Bereich Einstellungen Provider, deaktiviere die Cloud-Voreinstellungen und trage deinen lokalen Server ein:</p>
<div className="pl-6 pt-1">
<div className="p-3 bg-background/25 rounded-xl border border-border/30 font-mono text-[10px] space-y-1 text-foreground">
<div><span className="text-muted-foreground/60">Base URL:</span> http://192.168.178.151:9001/v1</div>
<div><span className="text-muted-foreground/60">Model:</span> auto</div>
</div>
</div>
</div>
<div className="space-y-1">
<div className="font-semibold text-foreground flex items-center gap-1.5">
<span className="h-4.5 w-4.5 rounded-full bg-primary/20 text-primary flex items-center justify-center font-mono text-[10px]">3</span>
Erster Vibe-Coding Test
</div>
<p className="pl-6">Starte eine neue Session und teste die Verbindung mit einem einfachen Prompt, z. B. *"Erstelle ein einfaches Skript, das die Fibonaccizahlen berechnet"*. Das Box-Gateway tauscht das Modell im Hintergrund vollautomatisch aus.</p>
</div>
</div>
</div>
)}
</div>
</div>
{/* Diagnostic Tips Card */}
<div className="rounded-2xl border border-border/60 bg-card/45 backdrop-blur-md p-5 space-y-3 shadow-lg shadow-black/10">
<div className="flex items-center gap-2">
<Terminal className="h-4.5 w-4.5 text-primary" />
<h3 className="text-xs font-bold uppercase tracking-wider text-foreground">Was tun, wenn das Coden hakt?</h3>
</div>
<ul className="text-[11px] text-muted-foreground space-y-2 list-disc pl-4 leading-relaxed">
<li><strong>Keine Verbindung?</strong> Überprüfe im Live-Verbindungsprüfer oben, ob die Box-IP erreichbar ist. Stelle sicher, dass dein lokaler PC im selben WLAN/LAN-Netzwerk wie die Box eingeloggt ist.</li>
<li><strong>Modell antwortet nicht?</strong> Schaue unter <strong>Diagnose</strong>, ob der Dienst `llama-swap` aktiv (grün) ist. Wenn nicht, klicke daneben auf <strong>Restart</strong>.</li>
<li><strong>Hermes Agent reagiert merkwürdig?</strong> Starte in der Hermes WebUI einfach eine frische Session (neuen Chat). Ältere Chats sammeln Kontext-Müll an.</li>
</ul>
</div>
</div>
)
}
+7 -5
View File
@@ -2,6 +2,7 @@ import { useEffect, useState } from "react"
import { Download, Search, Trash2, Edit3, Star, Layers, Activity, HardDrive } from "lucide-react" import { Download, Search, Trash2, Edit3, Star, Layers, Activity, HardDrive } from "lucide-react"
import { api, type DiscoverResp, type Fit, type Job, type ModelInfo } from "@/lib/api" import { api, type DiscoverResp, type Fit, type Job, type ModelInfo } from "@/lib/api"
import { CapsChips } from "@/components/CapsChips" import { CapsChips } from "@/components/CapsChips"
import { RoutingView } from "./RoutingView"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
function fmtBytes(b?: number) { function fmtBytes(b?: number) {
@@ -492,7 +493,7 @@ function Discover() {
} }
export function ModelsView() { export function ModelsView() {
const [tab, setTab] = useState<"installed" | "discover">("installed") const [tab, setTab] = useState<"installed" | "discover" | "routing">("installed")
return ( return (
<div className="space-y-6"> <div className="space-y-6">
<div className="flex flex-col sm:flex-row justify-between sm:items-center gap-4"> <div className="flex flex-col sm:flex-row justify-between sm:items-center gap-4">
@@ -501,12 +502,12 @@ export function ModelsView() {
Modell-Zentrale Modell-Zentrale
</h1> </h1>
<p className="text-sm text-muted-foreground"> <p className="text-sm text-muted-foreground">
Verwalte installierte GGUFs, weise Systemrollen zu und lade neue Modelle von HuggingFace. Verwalte installierte GGUFs, weise Systemrollen zu, lade neue Modelle oder konfiguriere das Gateway-Routing.
</p> </p>
</div> </div>
<div className="flex rounded-xl border border-border/60 bg-card/45 backdrop-blur-md p-1 self-start"> <div className="flex rounded-xl border border-border/60 bg-card/45 backdrop-blur-md p-1 self-start">
{(["installed", "discover"] as const).map((t) => ( {(["installed", "discover", "routing"] as const).map((t) => (
<button <button
key={t} key={t}
onClick={() => setTab(t)} onClick={() => setTab(t)}
@@ -517,7 +518,7 @@ export function ModelsView() {
: "text-muted-foreground hover:text-foreground", : "text-muted-foreground hover:text-foreground",
)} )}
> >
{t === "installed" ? "Installiert" : "Suchen & Entdecken"} {t === "installed" ? "Bibliothek" : t === "discover" ? "Modelle finden" : "Gateway-Routing"}
</button> </button>
))} ))}
</div> </div>
@@ -526,8 +527,9 @@ export function ModelsView() {
<JobsBar /> <JobsBar />
<div className="transition-all duration-300"> <div className="transition-all duration-300">
{tab === "installed" ? <Installed /> : <Discover />} {tab === "installed" ? <Installed /> : tab === "discover" ? <Discover /> : <RoutingView />}
</div> </div>
</div> </div>
) )
} }