Compare commits
2 Commits
563e7837b9
...
066feee3ea
| Author | SHA1 | Date | |
|---|---|---|---|
| 066feee3ea | |||
| 0c7b0b19af |
@@ -1,12 +1,23 @@
|
||||
"""Agent-Endpoint: Hermes-Status + WebUI-Link (MC verlinkt nur, betreibt nicht)."""
|
||||
|
||||
from fastapi import APIRouter
|
||||
from pydantic import BaseModel
|
||||
|
||||
from services.agent import agent_status
|
||||
from services.agent import agent_status, update_brain_model
|
||||
|
||||
router = APIRouter(prefix="/api")
|
||||
|
||||
|
||||
class BrainReq(BaseModel):
|
||||
model: str
|
||||
|
||||
|
||||
@router.get("/agent/status")
|
||||
def status() -> dict:
|
||||
return agent_status()
|
||||
|
||||
|
||||
@router.post("/agent/brain")
|
||||
def set_brain_model(body: BrainReq) -> dict:
|
||||
ok = update_brain_model(body.model)
|
||||
return {"ok": ok}
|
||||
|
||||
@@ -21,6 +21,13 @@ class RestartReq(BaseModel):
|
||||
def updates() -> dict:
|
||||
return maintenance.updates()
|
||||
|
||||
@router.post("/maintenance/check-updates")
|
||||
def check_updates(body: SudoReq) -> dict:
|
||||
res = maintenance.check_updates_job(body.sudo_password)
|
||||
if isinstance(res, dict) and not res.get("ok", True):
|
||||
return res
|
||||
return res
|
||||
|
||||
|
||||
@router.post("/maintenance/os-update")
|
||||
def os_update(body: SudoReq) -> dict:
|
||||
|
||||
@@ -46,3 +46,47 @@ def agent_status() -> dict:
|
||||
"has_skills": (home / "skills").exists(),
|
||||
"has_memories": (home / "memories").exists(),
|
||||
}
|
||||
|
||||
|
||||
def update_brain_model(new_model: str) -> bool:
|
||||
from config import HERMES_HOME
|
||||
home = HERMES_HOME
|
||||
config_path = home / "config.yaml"
|
||||
|
||||
# Ensure home directory exists
|
||||
home.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
cfg = {}
|
||||
if config_path.exists():
|
||||
try:
|
||||
from ruamel.yaml import YAML
|
||||
r_yaml = YAML()
|
||||
with config_path.open("r", encoding="utf-8") as f:
|
||||
cfg = r_yaml.load(f) or {}
|
||||
except Exception:
|
||||
cfg = {}
|
||||
|
||||
if not isinstance(cfg, dict):
|
||||
cfg = {}
|
||||
|
||||
if "model" not in cfg or not isinstance(cfg["model"], dict):
|
||||
cfg["model"] = {}
|
||||
|
||||
cfg["model"]["model"] = new_model
|
||||
|
||||
try:
|
||||
from ruamel.yaml import YAML
|
||||
r_yaml = YAML()
|
||||
with config_path.open("w", encoding="utf-8") as f:
|
||||
r_yaml.dump(cfg, f)
|
||||
|
||||
# Restart the user-space service to apply changes
|
||||
try:
|
||||
import services.maintenance as maintenance
|
||||
maintenance.restart_service("hermes-gateway")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
@@ -70,9 +70,20 @@ def model_upgrades() -> list[dict]:
|
||||
cmds = " ".join(str(s.get("cmd", "")).lower()
|
||||
for s in (llamaswap.read_config().get("models") or {}).values())
|
||||
out = []
|
||||
|
||||
ROLE_MAP = {
|
||||
"reasoning": "heavy",
|
||||
"agent": "fast",
|
||||
"scout": "fast",
|
||||
"coder": "coder",
|
||||
"vision": "vision"
|
||||
}
|
||||
|
||||
for c in disc.get("categories", []):
|
||||
role = c["role"]
|
||||
if role not in active_roles:
|
||||
disc_role = c["role"]
|
||||
mapped_role = ROLE_MAP.get(disc_role, disc_role)
|
||||
|
||||
if mapped_role not in active_roles:
|
||||
continue
|
||||
|
||||
rec = c.get("recommended")
|
||||
@@ -82,7 +93,7 @@ def model_upgrades() -> list[dict]:
|
||||
stem = base[:-5] if base.endswith("-gguf") else base
|
||||
if base in cmds or (stem and stem in cmds):
|
||||
continue
|
||||
out.append({"role": role, "title": c["title"], "repo": rec})
|
||||
out.append({"role": mapped_role, "title": c["title"], "repo": rec})
|
||||
return out
|
||||
|
||||
|
||||
@@ -168,6 +179,17 @@ def logs(service: str, lines: int = 200, sudo_password: str | None = None) -> di
|
||||
return {"ok": False, "text": "", "err": "Dienst nicht erlaubt."}
|
||||
return {"ok": r["ok"], "text": r["out"] or r["err"]}
|
||||
|
||||
def check_updates_job(sudo_password: str | None = None) -> dict:
|
||||
if err := check_sudo_needs_password(sudo_password):
|
||||
return err
|
||||
|
||||
def on_done():
|
||||
_engine_cache.update(ts=0.0, avail=False)
|
||||
|
||||
cmd = "sudo apt-get update"
|
||||
job_id = jobengine.start_job(["bash", "-c", cmd], "Nach Updates suchen", on_done=on_done, sudo_password=sudo_password)
|
||||
return {"ok": True, "job_id": job_id}
|
||||
|
||||
|
||||
def os_update_job(sudo_password: str | None = None) -> dict:
|
||||
if err := check_sudo_needs_password(sudo_password):
|
||||
|
||||
+20
-13
@@ -132,20 +132,27 @@ def get_engine_version() -> dict:
|
||||
if git_info:
|
||||
return {**git_info, "type": "git"}
|
||||
|
||||
try:
|
||||
binary = os.path.join(engine_path, "llama-server")
|
||||
if not os.path.exists(binary):
|
||||
binary = os.path.join(engine_path, "bin", "llama-server")
|
||||
if not os.path.exists(binary):
|
||||
binary = "llama-server"
|
||||
candidates = [
|
||||
os.path.join(engine_path, "llama-server"),
|
||||
os.path.join(engine_path, "bin", "llama-server"),
|
||||
"/usr/local/bin/llama-server",
|
||||
"/usr/bin/llama-server",
|
||||
"llama-server"
|
||||
]
|
||||
|
||||
for binary in candidates:
|
||||
if binary != "llama-server" and not os.path.exists(binary):
|
||||
continue
|
||||
try:
|
||||
res = subprocess.run([binary, "--version"], capture_output=True, text=True, timeout=2)
|
||||
output = (res.stdout or "").strip() or (res.stderr or "").strip()
|
||||
if output:
|
||||
lines = output.splitlines()
|
||||
ver = lines[0] if lines else "unknown"
|
||||
return {"version_text": ver, "type": "binary"}
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
res = subprocess.run([binary, "--version"], capture_output=True, text=True, timeout=2)
|
||||
if res.returncode == 0:
|
||||
lines = res.stdout.strip().splitlines()
|
||||
ver = lines[0] if lines else "unknown"
|
||||
return {"version_text": ver, "type": "binary"}
|
||||
except Exception:
|
||||
pass
|
||||
return {"type": "unknown"}
|
||||
|
||||
|
||||
|
||||
+1
File diff suppressed because one or more lines are too long
-350
File diff suppressed because one or more lines are too long
-1
File diff suppressed because one or more lines are too long
+375
File diff suppressed because one or more lines are too long
Vendored
+2
-2
@@ -7,8 +7,8 @@
|
||||
<link rel="manifest" href="/manifest.webmanifest" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<title>Mission Control 2.0</title>
|
||||
<script type="module" crossorigin src="/assets/index-Dk6KzQ7B.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-nR6_L3dg.css">
|
||||
<script type="module" crossorigin src="/assets/index-rbtAsh2N.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-DJE5r1SD.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
import { X } from "lucide-react"
|
||||
|
||||
export interface CustomDialogProps {
|
||||
type: "alert" | "confirm" | "prompt"
|
||||
title: string
|
||||
message: string
|
||||
defaultValue?: string
|
||||
onConfirm: (val?: string) => void
|
||||
onCancel?: () => void
|
||||
}
|
||||
|
||||
export function CustomDialog({ type, title, message, defaultValue, onConfirm, onCancel }: CustomDialogProps) {
|
||||
return (
|
||||
<div className="fixed inset-0 z-[99] bg-black/60 backdrop-blur-sm flex items-center justify-center p-4">
|
||||
<div className="w-full max-w-sm rounded-2xl border border-border/80 bg-card p-6 shadow-2xl space-y-4 animate-in fade-in zoom-in-95 duration-200">
|
||||
<div className="flex items-center justify-between border-b border-border/20 pb-2">
|
||||
<h3 className="text-xs font-bold uppercase tracking-wider text-primary">{title}</h3>
|
||||
<button
|
||||
onClick={onCancel || (() => onConfirm())}
|
||||
className="p-1 rounded hover:bg-accent text-muted-foreground hover:text-foreground cursor-pointer"
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground leading-relaxed">{message}</p>
|
||||
|
||||
{type === "prompt" && (
|
||||
<input
|
||||
type="text"
|
||||
id="custom-dialog-input"
|
||||
defaultValue={defaultValue}
|
||||
className="w-full h-9 px-3 text-xs bg-background/40 border border-border/60 rounded-lg outline-none focus:border-primary transition-colors text-foreground"
|
||||
autoFocus
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
const val = (document.getElementById("custom-dialog-input") as HTMLInputElement)?.value
|
||||
onConfirm(val)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="flex justify-end gap-3 pt-2">
|
||||
{(type === "confirm" || type === "prompt") && (
|
||||
<button
|
||||
onClick={onCancel}
|
||||
className="h-8 px-4 flex items-center justify-center text-xs font-semibold text-muted-foreground border border-border/60 bg-background/20 hover:bg-accent rounded-lg transition-colors cursor-pointer"
|
||||
>
|
||||
Abbrechen
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
onClick={() => {
|
||||
const val = type === "prompt"
|
||||
? (document.getElementById("custom-dialog-input") as HTMLInputElement)?.value
|
||||
: undefined
|
||||
onConfirm(val)
|
||||
}}
|
||||
className="h-8 px-4 flex items-center justify-center text-xs font-semibold text-primary-foreground bg-primary hover:bg-primary/95 rounded-lg transition-colors cursor-pointer"
|
||||
>
|
||||
{type === "confirm" ? "Ja, fortfahren" : type === "prompt" ? "Übernehmen" : "OK"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,12 +1,14 @@
|
||||
import { useEffect, useState, useRef } from "react"
|
||||
import { X, RefreshCw, Terminal, Cpu, Server, Shield, Power } from "lucide-react"
|
||||
import { X, RefreshCw, Terminal, Cpu, Server, Shield, Power, Eye, EyeOff, Key, Download, AlertTriangle } from "lucide-react"
|
||||
import { api, type Job, type UpdatesResp } from "@/lib/api"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { CustomDialog } from "./CustomDialog"
|
||||
|
||||
|
||||
interface SystemDrawerProps {
|
||||
open: boolean
|
||||
onClose: () => void
|
||||
defaultTab?: "maintenance" | "logs"
|
||||
defaultTab?: "maintenance" | "logs" | "settings"
|
||||
}
|
||||
|
||||
const SERVICES = [
|
||||
@@ -29,8 +31,68 @@ export function SystemDrawer({ open, onClose, defaultTab = "maintenance" }: Syst
|
||||
const [selectedService, setSelectedService] = useState("llama-swap")
|
||||
const [logs, setLogs] = useState("")
|
||||
const [loadingLogs, setLoadingLogs] = useState(false)
|
||||
const [logStatus, setLogStatus] = useState<string | null>(null)
|
||||
const [restartingServices, setRestartingServices] = useState<Record<string, boolean>>({})
|
||||
const [activeTab, setActiveTab] = useState<"maintenance" | "logs">("maintenance")
|
||||
const [activeTab, setActiveTab] = useState<"maintenance" | "logs" | "settings">("maintenance")
|
||||
const [checkingUpdates, setCheckingUpdates] = useState(false)
|
||||
|
||||
// Custom Dialog State
|
||||
const [dialog, setDialog] = useState<{
|
||||
type: "alert" | "confirm"
|
||||
title: string
|
||||
message: string
|
||||
onConfirm: () => void
|
||||
onCancel?: () => void
|
||||
} | null>(null)
|
||||
|
||||
function showAlert(title: string, message: string, onConfirm?: () => void) {
|
||||
setDialog({
|
||||
type: "alert",
|
||||
title,
|
||||
message,
|
||||
onConfirm: () => {
|
||||
setDialog(null)
|
||||
if (onConfirm) onConfirm()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function showConfirm(title: string, message: string, onConfirm: () => void) {
|
||||
setDialog({
|
||||
type: "confirm",
|
||||
title,
|
||||
message,
|
||||
onConfirm: () => {
|
||||
setDialog(null)
|
||||
onConfirm()
|
||||
},
|
||||
onCancel: () => setDialog(null)
|
||||
})
|
||||
}
|
||||
|
||||
function formatLastCheck(ts?: number | null) {
|
||||
if (!ts) return "Nie"
|
||||
return new Date(ts * 1000).toLocaleString("de-DE", {
|
||||
day: "2-digit",
|
||||
month: "2-digit",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
second: "2-digit"
|
||||
})
|
||||
}
|
||||
|
||||
// Credentials State
|
||||
const [sudoPasswordInput, setSudoPasswordInput] = useState("")
|
||||
const [hfTokenInput, setHfTokenInput] = useState("")
|
||||
const [showSudo, setShowSudo] = useState(false)
|
||||
const [showHf, setShowHf] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
setSudoPasswordInput(localStorage.getItem("mc_sudo_password") || "")
|
||||
setHfTokenInput(localStorage.getItem("mc_hf_token") || "")
|
||||
}
|
||||
}, [open])
|
||||
|
||||
useEffect(() => {
|
||||
if (open && defaultTab) {
|
||||
@@ -54,12 +116,16 @@ export function SystemDrawer({ open, onClose, defaultTab = "maintenance" }: Syst
|
||||
|
||||
function loadServiceLogs(service: string) {
|
||||
setLoadingLogs(true)
|
||||
api<{ ok: boolean; text: string; err?: string }>(`/api/maintenance/logs?service=${service}&lines=150`)
|
||||
setLogStatus(null)
|
||||
api<{ ok: boolean; text: string; err?: string; status?: string }>(`/api/maintenance/logs?service=${service}&lines=150`)
|
||||
.then((res) => {
|
||||
if (res.ok) {
|
||||
setLogs(res.text)
|
||||
} else {
|
||||
setLogs(`Fehler beim Laden der Logs: ${res.err || "Unbekannter Fehler"}`)
|
||||
if (res.status === "incorrect_password" || res.status === "password_required") {
|
||||
setLogStatus(res.status)
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch((e) => setLogs(`Fehler: ${e.message}`))
|
||||
@@ -98,7 +164,7 @@ export function SystemDrawer({ open, onClose, defaultTab = "maintenance" }: Syst
|
||||
loadJobs()
|
||||
setActiveTab("maintenance")
|
||||
} catch (e: any) {
|
||||
alert(`Fehler beim Starten des OS-Updates: ${e.message}`)
|
||||
showAlert("Fehler", `Fehler beim Starten des OS-Updates: ${e.message}`)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -108,19 +174,52 @@ export function SystemDrawer({ open, onClose, defaultTab = "maintenance" }: Syst
|
||||
loadJobs()
|
||||
setActiveTab("maintenance")
|
||||
} catch (e: any) {
|
||||
alert(`Fehler: ${e.message}`)
|
||||
showAlert("Fehler", `Fehler beim Engine-Update: ${e.message}`)
|
||||
}
|
||||
}
|
||||
|
||||
async function triggerCheckUpdates() {
|
||||
setCheckingUpdates(true)
|
||||
try {
|
||||
await api("/api/maintenance/check-updates", { method: "POST" })
|
||||
loadJobs()
|
||||
setActiveTab("maintenance")
|
||||
} catch (e: any) {
|
||||
showAlert("Fehler", `Fehler bei der Update-Suche: ${e.message}`)
|
||||
} finally {
|
||||
setCheckingUpdates(false)
|
||||
}
|
||||
}
|
||||
|
||||
async function triggerModelUpgrade(repo: string, role: string) {
|
||||
try {
|
||||
await api("/api/models/install", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ repo, role })
|
||||
})
|
||||
showAlert("Gestartet", `Modell-Upgrade für '${role}' (${repo}) gestartet.`)
|
||||
loadJobs()
|
||||
setActiveTab("maintenance")
|
||||
} catch (e: any) {
|
||||
showAlert("Fehler", `Fehler beim Starten des Modell-Upgrades: ${e.message}`)
|
||||
}
|
||||
}
|
||||
|
||||
async function triggerReboot() {
|
||||
if (!confirm("Bist du sicher, dass du das gesamte Host-System neu starten willst?")) return
|
||||
try {
|
||||
await api("/api/maintenance/reboot", { method: "POST" })
|
||||
alert("Reboot ausgelöst. System startet neu...")
|
||||
onClose()
|
||||
} catch (e: any) {
|
||||
alert(`Fehler: ${e.message}`)
|
||||
}
|
||||
showConfirm(
|
||||
"Reboot bestätigen",
|
||||
"Bist du sicher, dass du das gesamte Host-System neu starten willst?",
|
||||
async () => {
|
||||
try {
|
||||
await api("/api/maintenance/reboot", { method: "POST" })
|
||||
showAlert("Reboot", "Reboot ausgelöst. System startet neu...", () => {
|
||||
onClose()
|
||||
})
|
||||
} catch (e: any) {
|
||||
showAlert("Fehler", `Fehler beim Reboot: ${e.message}`)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
async function restartService(serviceId: string) {
|
||||
@@ -131,15 +230,16 @@ export function SystemDrawer({ open, onClose, defaultTab = "maintenance" }: Syst
|
||||
body: JSON.stringify({ service: serviceId })
|
||||
})
|
||||
if (res.ok) {
|
||||
alert(`Dienst ${serviceId} wurde erfolgreich neu gestartet.`)
|
||||
if (activeTab === "logs" && selectedService === serviceId) {
|
||||
loadServiceLogs(serviceId)
|
||||
}
|
||||
showAlert("Dienst neu gestartet", `Dienst ${serviceId} wurde erfolgreich neu gestartet.`, () => {
|
||||
if (activeTab === "logs" && selectedService === serviceId) {
|
||||
loadServiceLogs(serviceId)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
alert(`Fehler beim Neustart: ${res.err || "Unbekannter Fehler"}`)
|
||||
showAlert("Fehler", `Fehler beim Neustart: ${res.err || "Unbekannter Fehler"}`)
|
||||
}
|
||||
} catch (e: any) {
|
||||
alert(`Fehler: ${e.message}`)
|
||||
showAlert("Fehler", `Fehler beim Neustart: ${e.message}`)
|
||||
} finally {
|
||||
setRestartingServices(prev => ({ ...prev, [serviceId]: false }))
|
||||
}
|
||||
@@ -150,7 +250,7 @@ export function SystemDrawer({ open, onClose, defaultTab = "maintenance" }: Syst
|
||||
await api(`/api/jobs/${jobId}/cancel`, { method: "POST" })
|
||||
loadJobs()
|
||||
} catch (e: any) {
|
||||
alert(`Fehler beim Abbrechen: ${e.message}`)
|
||||
showAlert("Fehler", `Fehler beim Abbrechen: ${e.message}`)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -210,15 +310,43 @@ export function SystemDrawer({ open, onClose, defaultTab = "maintenance" }: Syst
|
||||
>
|
||||
System-Logs
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab("settings")}
|
||||
className={cn(
|
||||
"flex-1 py-3 text-xs font-semibold uppercase tracking-wider border-b-2 text-center transition-all",
|
||||
activeTab === "settings"
|
||||
? "border-primary text-primary"
|
||||
: "border-transparent text-muted-foreground hover:text-foreground"
|
||||
)}
|
||||
>
|
||||
Einstellungen
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="flex-1 overflow-y-auto p-6 space-y-6">
|
||||
{activeTab === "maintenance" ? (
|
||||
{activeTab === "maintenance" && (
|
||||
<>
|
||||
{/* Quick Actions */}
|
||||
<div className="space-y-3">
|
||||
<h3 className="text-xs font-bold uppercase tracking-wider text-muted-foreground">Wartungsaktionen</h3>
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-xs font-bold uppercase tracking-wider text-muted-foreground">Wartungsaktionen</h3>
|
||||
<div className="flex items-center gap-2">
|
||||
{updates?.last_check && (
|
||||
<span className="text-[9px] text-muted-foreground">
|
||||
Zuletzt gesucht: {formatLastCheck(updates.last_check)}
|
||||
</span>
|
||||
)}
|
||||
<button
|
||||
onClick={triggerCheckUpdates}
|
||||
disabled={checkingUpdates}
|
||||
className="flex items-center gap-1 text-[10px] font-semibold text-primary hover:text-primary/80 transition-colors disabled:opacity-50"
|
||||
>
|
||||
<RefreshCw className={cn("h-3 w-3", checkingUpdates && "animate-spin")} />
|
||||
Nach Updates suchen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<button
|
||||
@@ -256,6 +384,40 @@ export function SystemDrawer({ open, onClose, defaultTab = "maintenance" }: Syst
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Modell Upgrades */}
|
||||
{updates?.model_list && updates.model_list.length > 0 && (
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-xs font-bold uppercase tracking-wider text-muted-foreground">Verfügbare Modell-Upgrades</h3>
|
||||
{updates?.last_check && (
|
||||
<span className="text-[9px] text-muted-foreground">
|
||||
Gesucht: {formatLastCheck(updates.last_check)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
{updates.model_list.map((m) => (
|
||||
<div key={m.role} className="p-3 rounded-xl border border-border/60 bg-background/20 flex flex-col gap-2">
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div>
|
||||
<div className="text-xs font-semibold">{m.title}</div>
|
||||
<div className="text-[10px] font-mono text-muted-foreground">{m.repo}</div>
|
||||
<div className="text-[10px] text-primary font-semibold uppercase mt-0.5">Rolle: {m.role}</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => triggerModelUpgrade(m.repo, m.role)}
|
||||
className="flex items-center gap-1.5 text-[10px] font-semibold text-emerald-400 hover:text-emerald-300 border border-emerald-500/20 bg-emerald-500/5 hover:bg-emerald-500/10 px-2 py-1 rounded-lg transition-colors shrink-0"
|
||||
>
|
||||
<Download className="h-3.5 w-3.5" />
|
||||
Upgrade
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Active Jobs */}
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
@@ -347,7 +509,9 @@ export function SystemDrawer({ open, onClose, defaultTab = "maintenance" }: Syst
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
)}
|
||||
|
||||
{activeTab === "logs" && (
|
||||
// Logs View
|
||||
<div className="flex flex-col h-full space-y-4">
|
||||
{/* Service Select & Restart */}
|
||||
@@ -397,7 +561,23 @@ export function SystemDrawer({ open, onClose, defaultTab = "maintenance" }: Syst
|
||||
ref={logContainerRef}
|
||||
className="flex-1 p-4 overflow-y-auto text-[10px] font-mono text-cyan-300/90 whitespace-pre-wrap select-text leading-relaxed scrollbar-thin"
|
||||
>
|
||||
{loadingLogs && !logs ? (
|
||||
{logStatus === "password_required" || logStatus === "incorrect_password" ? (
|
||||
<div className="flex flex-col items-center justify-center p-6 text-center h-full space-y-3">
|
||||
<AlertTriangle className="h-8 w-8 text-amber-400 animate-pulse animate-duration-1000" />
|
||||
<div className="text-xs font-semibold text-amber-300">
|
||||
{logStatus === "incorrect_password" ? "Falsches Sudo-Passwort hinterlegt." : "Sudo-Passwort für systemd-Dienste erforderlich."}
|
||||
</div>
|
||||
<p className="text-[10px] text-muted-foreground max-w-xs leading-normal">
|
||||
Für das Auslesen der systemd-Logs von {selectedService} werden Root-Rechte benötigt. Hinterlege dein Passwort in den Einstellungen.
|
||||
</p>
|
||||
<button
|
||||
onClick={() => setActiveTab("settings")}
|
||||
className="px-3 py-1.5 text-[10px] font-semibold text-primary-foreground bg-primary hover:bg-primary/95 rounded-md transition-colors mt-2"
|
||||
>
|
||||
Sudo-Passwort eintragen
|
||||
</button>
|
||||
</div>
|
||||
) : loadingLogs && !logs ? (
|
||||
<span className="text-muted-foreground">Lade Logs...</span>
|
||||
) : (
|
||||
logs || <span className="text-muted-foreground">Keine Logeinträge vorhanden.</span>
|
||||
@@ -406,8 +586,109 @@ export function SystemDrawer({ open, onClose, defaultTab = "maintenance" }: Syst
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === "settings" && (
|
||||
<div className="space-y-6">
|
||||
<div className="space-y-2">
|
||||
<h3 className="text-xs font-bold uppercase tracking-wider text-muted-foreground">Zugangsdaten & Schlüssel</h3>
|
||||
<p className="text-[10px] text-muted-foreground leading-normal">
|
||||
Diese Daten werden ausschließlich lokal in deinem Browser (localStorage) gespeichert und bei Bedarf an das Backend übertragen.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Sudo Passwort */}
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs font-semibold flex items-center gap-1.5">
|
||||
<Shield className="h-4 w-4 text-amber-400" />
|
||||
Host Sudo-Passwort
|
||||
</label>
|
||||
<div className="relative">
|
||||
<input
|
||||
type={showSudo ? "text" : "password"}
|
||||
value={sudoPasswordInput}
|
||||
onChange={(e) => setSudoPasswordInput(e.target.value)}
|
||||
placeholder="Sudo-Passwort für System-Operationen"
|
||||
className="w-full h-10 pl-3 pr-10 text-xs bg-background/40 border border-border/60 rounded-lg outline-none focus:border-primary transition-colors text-foreground"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowSudo(!showSudo)}
|
||||
className="absolute inset-y-0 right-0 flex items-center pr-3 text-muted-foreground hover:text-foreground transition-colors"
|
||||
>
|
||||
{showSudo ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
|
||||
</button>
|
||||
</div>
|
||||
<p className="text-[9px] text-muted-foreground leading-normal">
|
||||
Wird benötigt für systemd-Dienste (Llama Swap logs/restart), OS-Update (apt) und Reboot.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* HuggingFace Token */}
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs font-semibold flex items-center gap-1.5">
|
||||
<Key className="h-4 w-4 text-violet-400" />
|
||||
HuggingFace API Token
|
||||
</label>
|
||||
<div className="relative">
|
||||
<input
|
||||
type={showHf ? "text" : "password"}
|
||||
value={hfTokenInput}
|
||||
onChange={(e) => setHfTokenInput(e.target.value)}
|
||||
placeholder="hf_..."
|
||||
className="w-full h-10 pl-3 pr-10 text-xs bg-background/40 border border-border/60 rounded-lg outline-none focus:border-primary transition-colors text-foreground"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowHf(!showHf)}
|
||||
className="absolute inset-y-0 right-0 flex items-center pr-3 text-muted-foreground hover:text-foreground transition-colors"
|
||||
>
|
||||
{showHf ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
|
||||
</button>
|
||||
</div>
|
||||
<p className="text-[9px] text-muted-foreground leading-normal">
|
||||
Erlaubt das Herunterladen von geschützten oder Gate-restricted Modellen direkt über Mission Control.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Buttons */}
|
||||
<div className="flex gap-3 pt-2">
|
||||
<button
|
||||
onClick={() => {
|
||||
localStorage.setItem("mc_sudo_password", sudoPasswordInput);
|
||||
localStorage.setItem("mc_hf_token", hfTokenInput);
|
||||
showAlert("Erfolgreich", "Einstellungen erfolgreich lokal gespeichert.");
|
||||
}}
|
||||
className="flex-1 h-9 flex items-center justify-center text-xs font-semibold text-primary-foreground bg-primary hover:bg-primary/90 rounded-lg transition-colors shadow shadow-primary/10 cursor-pointer"
|
||||
>
|
||||
Speichern
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
setSudoPasswordInput("");
|
||||
setHfTokenInput("");
|
||||
localStorage.removeItem("mc_sudo_password");
|
||||
localStorage.removeItem("mc_hf_token");
|
||||
showAlert("Gelöscht", "Zugangsdaten gelöscht.");
|
||||
}}
|
||||
className="h-9 px-4 flex items-center justify-center text-xs font-semibold text-muted-foreground border border-border/60 bg-background/20 hover:bg-accent rounded-lg transition-colors cursor-pointer"
|
||||
>
|
||||
Zurücksetzen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{dialog && (
|
||||
<CustomDialog
|
||||
type={dialog.type}
|
||||
title={dialog.title}
|
||||
message={dialog.message}
|
||||
onConfirm={dialog.onConfirm}
|
||||
onCancel={dialog.onCancel}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
+45
-1
@@ -1,9 +1,53 @@
|
||||
// Schmaler Fetch-Helfer gegen das MC-2-Backend (/api/*).
|
||||
|
||||
export async function api<T = unknown>(path: string, init?: RequestInit): Promise<T> {
|
||||
const headers = {
|
||||
"Content-Type": "application/json",
|
||||
...init?.headers,
|
||||
} as Record<string, string>
|
||||
|
||||
const sudoPassword = localStorage.getItem("mc_sudo_password")
|
||||
const hfToken = localStorage.getItem("mc_hf_token")
|
||||
|
||||
if (sudoPassword) {
|
||||
headers["X-Sudo-Password"] = sudoPassword
|
||||
}
|
||||
|
||||
let body = init?.body
|
||||
const method = init?.method?.toUpperCase() || "GET"
|
||||
if (method === "POST") {
|
||||
if (typeof body === "string") {
|
||||
try {
|
||||
const data = JSON.parse(body)
|
||||
let changed = false
|
||||
if (sudoPassword && !("sudo_password" in data)) {
|
||||
data["sudo_password"] = sudoPassword
|
||||
changed = true
|
||||
}
|
||||
if (hfToken && !("hf_token" in data)) {
|
||||
data["hf_token"] = hfToken
|
||||
changed = true
|
||||
}
|
||||
if (changed) {
|
||||
body = JSON.stringify(data)
|
||||
}
|
||||
} catch (e) {
|
||||
// ignore JSON parse errors
|
||||
}
|
||||
} else if (!body) {
|
||||
const data: Record<string, any> = {}
|
||||
if (sudoPassword) data["sudo_password"] = sudoPassword
|
||||
if (hfToken) data["hf_token"] = hfToken
|
||||
if (Object.keys(data).length > 0) {
|
||||
body = JSON.stringify(data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const res = await fetch(path, {
|
||||
headers: { "Content-Type": "application/json" },
|
||||
...init,
|
||||
headers,
|
||||
body,
|
||||
})
|
||||
if (!res.ok) throw new Error(`${res.status} ${res.statusText}`)
|
||||
return res.json() as Promise<T>
|
||||
|
||||
@@ -1,14 +1,20 @@
|
||||
import { useEffect, useState, useRef, useCallback } from "react"
|
||||
import { Bot, ExternalLink, Activity, Cpu, Wrench, Shield } from "lucide-react"
|
||||
import { Bot, ExternalLink, Activity, Cpu, Wrench, Shield, Check, X } from "lucide-react"
|
||||
import { api, type AgentStatus } from "@/lib/api"
|
||||
import { cn, resolveExternalUrl } from "@/lib/utils"
|
||||
import { CustomDialog } from "@/components/CustomDialog"
|
||||
|
||||
function Tile({ label, ok, detail, icon: Icon }: { label: string; ok: boolean; detail?: string; icon: any }) {
|
||||
|
||||
function Tile({ label, ok, detail, icon: Icon, onClick }: { label: string; ok: boolean; detail?: string; icon: any; onClick?: () => void }) {
|
||||
return (
|
||||
<div className={cn(
|
||||
"rounded-2xl border bg-card/45 backdrop-blur-md p-5 shadow-lg shadow-black/10 flex flex-col justify-between gap-3 hover:border-primary/40 transition-all duration-300",
|
||||
ok ? "border-border/60" : "border-amber-500/30"
|
||||
)}>
|
||||
<div
|
||||
onClick={onClick}
|
||||
className={cn(
|
||||
"rounded-2xl border bg-card/45 backdrop-blur-md p-5 shadow-lg shadow-black/10 flex flex-col justify-between gap-3 hover:border-primary/40 transition-all duration-300",
|
||||
ok ? "border-border/60" : "border-amber-500/30",
|
||||
onClick && "cursor-pointer hover:bg-card/70"
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-[10px] font-bold uppercase tracking-wider text-muted-foreground">{label}</span>
|
||||
<Icon className={cn("h-4.5 w-4.5", ok ? "text-primary" : "text-amber-500")} />
|
||||
@@ -26,6 +32,18 @@ function Tile({ label, ok, detail, icon: Icon }: { label: string; ok: boolean; d
|
||||
</div>
|
||||
{detail && <div className="text-[10px] font-mono text-muted-foreground truncate max-w-[200px]" title={detail}>{detail}</div>}
|
||||
</div>
|
||||
{onClick && (
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
onClick()
|
||||
}}
|
||||
className="mt-2 flex items-center justify-center gap-1.5 w-full py-1.5 px-3 rounded-lg border border-primary/30 bg-primary/10 hover:bg-primary/20 text-primary text-[10px] font-bold uppercase tracking-wider transition-all cursor-pointer font-space"
|
||||
>
|
||||
<Cpu className="h-3.5 w-3.5" />
|
||||
<span>Gehirn wechseln</span>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -36,6 +54,21 @@ export function AgentView() {
|
||||
|
||||
// Graph UI state
|
||||
const [hoveredNode, setHoveredNode] = useState<string | null>(null)
|
||||
const [showBrainSelect, setShowBrainSelect] = useState(false)
|
||||
const [availableModels, setAvailableModels] = useState<string[]>([])
|
||||
|
||||
// Custom Dialog State
|
||||
const [dialog, setDialog] = useState<{
|
||||
type: "alert" | "confirm"
|
||||
title: string
|
||||
message: string
|
||||
onConfirm?: () => void
|
||||
onCancel?: () => void
|
||||
} | null>(null)
|
||||
|
||||
function showAlert(title: string, message: string, onConfirm?: () => void) {
|
||||
setDialog({ type: "alert", title, message, onConfirm })
|
||||
}
|
||||
|
||||
// Canvas pixel tracking for pixel-perfect connection graph without non-uniform scaling
|
||||
const [dimensions, setDimensions] = useState({ width: 800, height: 360 })
|
||||
@@ -70,8 +103,32 @@ export function AgentView() {
|
||||
api<AgentStatus>("/api/agent/status").then(setS).catch((e) => setError(String(e)))
|
||||
}
|
||||
|
||||
function loadModels() {
|
||||
api<{ models: { name: string }[] }>("/api/models")
|
||||
.then((res) => {
|
||||
const names = res.models.map(m => m.name.split("/").pop()?.replace(".gguf", "") || m.name)
|
||||
setAvailableModels(["auto", "fast", "heavy", ...names])
|
||||
})
|
||||
.catch((e) => console.error("Error loading models", e))
|
||||
}
|
||||
|
||||
async function changeBrainModel(model: string) {
|
||||
try {
|
||||
await api("/api/agent/brain", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ model })
|
||||
})
|
||||
showAlert("Erfolgreich", `Hermes-Gehirn wurde auf '${model}' geändert. Der Gateway-Dienst wurde neu gestartet.`)
|
||||
loadData()
|
||||
setShowBrainSelect(false)
|
||||
} catch (e: any) {
|
||||
showAlert("Fehler", `Fehler beim Wechseln des Gehirns: ${e.message}`)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
loadData()
|
||||
loadModels()
|
||||
const t = setInterval(loadData, 5000)
|
||||
return () => clearInterval(t)
|
||||
}, [])
|
||||
@@ -147,6 +204,7 @@ export function AgentView() {
|
||||
ok={s.gateway_reachable}
|
||||
detail={s.brain_model ? `Model: ${s.brain_model}` : "Model: auto"}
|
||||
icon={Cpu}
|
||||
onClick={() => setShowBrainSelect(true)}
|
||||
/>
|
||||
<Tile
|
||||
label="Verdrahtung"
|
||||
@@ -236,12 +294,13 @@ export function AgentView() {
|
||||
{/* COLUMN 3: Brain & Wiring */}
|
||||
<div
|
||||
className={cn(
|
||||
"absolute z-10 w-44 py-1.5 px-3 -translate-y-1/2 -translate-x-1/2 rounded-xl border flex flex-col justify-center hover:border-primary/50 transition-all text-left shadow select-none",
|
||||
"absolute z-20 w-44 py-1.5 px-3 -translate-y-1/2 -translate-x-1/2 rounded-xl border flex flex-col justify-center hover:border-primary/50 transition-all text-left shadow select-none cursor-pointer",
|
||||
s.gateway_reachable ? "border-emerald-500/50 bg-emerald-500/5 shadow-emerald-500/5" : "border-border/60 bg-card/75"
|
||||
)}
|
||||
style={{ left: "85%", top: "30%" }}
|
||||
onMouseEnter={() => setHoveredNode("brain")}
|
||||
onMouseLeave={() => setHoveredNode(null)}
|
||||
onClick={() => setShowBrainSelect(true)}
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-1 text-muted-foreground">
|
||||
@@ -250,8 +309,13 @@ export function AgentView() {
|
||||
</div>
|
||||
{s.gateway_reachable && <span className="h-1.5 w-1.5 rounded-full bg-emerald-400 animate-pulse" />}
|
||||
</div>
|
||||
<div className="text-[10px] font-mono font-medium truncate mt-0.5 text-foreground font-space" title={s.brain_model}>
|
||||
{s.brain_model || "auto"}
|
||||
<div className="flex items-center justify-between mt-1">
|
||||
<span className="text-[10px] font-mono font-medium truncate text-foreground font-space max-w-[100px]" title={s.brain_model}>
|
||||
{s.brain_model || "auto"}
|
||||
</span>
|
||||
<span className="text-[8px] bg-primary/20 hover:bg-primary/30 border border-primary/30 text-primary px-1.5 py-0.5 rounded uppercase font-bold tracking-wider transition-colors">
|
||||
Ändern
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -358,6 +422,69 @@ export function AgentView() {
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Brain Selection Modal */}
|
||||
{s && showBrainSelect && (
|
||||
<div className="fixed inset-0 z-50 bg-black/60 backdrop-blur-sm flex items-center justify-center p-4">
|
||||
<div className="w-full max-w-md rounded-2xl border border-border/80 bg-card p-6 shadow-2xl space-y-4 animate-in fade-in zoom-in-95 duration-200">
|
||||
<div className="flex items-center justify-between border-b border-border/20 pb-2">
|
||||
<h3 className="text-xs font-bold uppercase tracking-wider text-primary flex items-center gap-1.5">
|
||||
<Cpu className="h-4 w-4" />
|
||||
<span>Hermes-Gehirn konfigurieren</span>
|
||||
</h3>
|
||||
<button
|
||||
onClick={() => setShowBrainSelect(false)}
|
||||
className="p-1 rounded hover:bg-accent text-muted-foreground hover:text-foreground cursor-pointer"
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground leading-relaxed">
|
||||
Das „Gehirn" ist das primäre LLM für Hermes. Wähle ein Modell aus deiner Bibliothek oder nutze ein Gateway-Alias (<code className="text-primary font-semibold">auto</code> / <code className="text-primary font-semibold">fast</code> / <code className="text-primary font-semibold">heavy</code>):
|
||||
</p>
|
||||
|
||||
<div className="space-y-1.5 max-h-60 overflow-y-auto pr-1">
|
||||
{availableModels.map((m) => {
|
||||
const isAlias = ["auto", "fast", "heavy"].includes(m);
|
||||
return (
|
||||
<button
|
||||
key={m}
|
||||
onClick={() => changeBrainModel(m)}
|
||||
className={cn(
|
||||
"w-full text-left px-3 py-2.5 rounded-lg text-xs hover:bg-accent flex items-center justify-between font-mono cursor-pointer border border-border/30",
|
||||
s.brain_model === m || (!s.brain_model && m === "auto")
|
||||
? "text-primary font-bold bg-primary/10 border-primary/30"
|
||||
: "text-foreground bg-background/20"
|
||||
)}
|
||||
>
|
||||
<div className="flex flex-col text-left">
|
||||
<span className="font-semibold truncate max-w-[280px]">
|
||||
{m}
|
||||
</span>
|
||||
<span className="text-[9px] text-muted-foreground mt-0.5">
|
||||
{isAlias ? "Gateway Routing Alias" : "Installiertes GGUF Modell"}
|
||||
</span>
|
||||
</div>
|
||||
{(s.brain_model === m || (!s.brain_model && m === "auto")) && (
|
||||
<Check className="h-4 w-4 shrink-0 text-primary" />
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{dialog && (
|
||||
<CustomDialog
|
||||
type={dialog.type}
|
||||
title={dialog.title}
|
||||
message={dialog.message}
|
||||
onConfirm={() => dialog.onConfirm && dialog.onConfirm()}
|
||||
onCancel={dialog.onCancel}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { useEffect, useState } from "react"
|
||||
import { Bot, Cpu, ExternalLink, Brain, Layers, Plus, ShieldAlert, X, Power, Shield, Download, RefreshCw } from "lucide-react"
|
||||
import { Bot, Cpu, ExternalLink, Brain, Layers, Plus, ShieldAlert, X, Power, Shield, Download, RefreshCw, Check } from "lucide-react"
|
||||
import { api, type SystemStatus, type AgentStatus, type ModelInfo, type Memory, type UpdatesResp, type Job } from "@/lib/api"
|
||||
import { cn, resolveExternalUrl } from "@/lib/utils"
|
||||
import { CustomDialog } from "@/components/CustomDialog"
|
||||
|
||||
|
||||
function gb(b: number) {
|
||||
return (b / 1024 ** 3).toFixed(1)
|
||||
@@ -55,6 +57,55 @@ export function DashboardView() {
|
||||
error?: string
|
||||
}>({ open: false, actionPath: "", actionLabel: "" })
|
||||
|
||||
// Custom Dialog State
|
||||
const [dialog, setDialog] = useState<{
|
||||
type: "alert" | "confirm"
|
||||
title: string
|
||||
message: string
|
||||
onConfirm: () => void
|
||||
onCancel?: () => void
|
||||
} | null>(null)
|
||||
|
||||
const [showBrainSelect, setShowBrainSelect] = useState(false)
|
||||
|
||||
async function changeBrainModel(model: string) {
|
||||
try {
|
||||
await api("/api/agent/brain", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ model })
|
||||
})
|
||||
setDialog({
|
||||
type: "alert",
|
||||
title: "Erfolgreich",
|
||||
message: `Hermes-Gehirn wurde auf '${model}' geändert. Der Gateway-Dienst wurde neu gestartet.`,
|
||||
onConfirm: () => setDialog(null)
|
||||
})
|
||||
loadData()
|
||||
setShowBrainSelect(false)
|
||||
} catch (e: any) {
|
||||
setDialog({
|
||||
type: "alert",
|
||||
title: "Fehler",
|
||||
message: `Fehler beim Wechseln des Gehirns: ${e.message}`,
|
||||
onConfirm: () => setDialog(null)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function showConfirm(title: string, message: string, onConfirm: () => void) {
|
||||
setDialog({
|
||||
type: "confirm",
|
||||
title,
|
||||
message,
|
||||
onConfirm: () => {
|
||||
setDialog(null)
|
||||
onConfirm()
|
||||
},
|
||||
onCancel: () => setDialog(null)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
// Quick Memory Form State
|
||||
const [memContent, setMemContent] = useState("")
|
||||
const [memCat, setMemCat] = useState("stable")
|
||||
@@ -385,7 +436,7 @@ export function DashboardView() {
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={() => { if (confirm("Bist du sicher, dass du das Host-System neu starten willst?")) postAction("/api/maintenance/reboot", "Reboot") }}
|
||||
onClick={() => { showConfirm("Host-System neu starten?", "Bist du sicher, dass du das Host-System neu starten willst?", () => postAction("/api/maintenance/reboot", "Reboot")) }}
|
||||
disabled={loading}
|
||||
className="w-full h-8 flex items-center justify-center gap-1.5 rounded-lg border border-red-500/30 bg-red-500/5 text-red-400 text-[10px] font-bold uppercase hover:bg-red-500/10 transition-all cursor-pointer disabled:opacity-50"
|
||||
>
|
||||
@@ -490,8 +541,18 @@ export function DashboardView() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-3 bg-background/20 rounded-xl border border-border/40">
|
||||
<div className="text-[10px] text-muted-foreground uppercase font-semibold">Aktives Gehirn</div>
|
||||
<div
|
||||
onClick={() => setShowBrainSelect(true)}
|
||||
className="p-3 bg-background/20 rounded-xl border border-border/40 hover:border-primary/45 hover:bg-background/30 transition-all duration-300 cursor-pointer group"
|
||||
>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-[10px] text-muted-foreground uppercase font-semibold">Aktives Gehirn</span>
|
||||
<button
|
||||
className="flex items-center gap-1 text-[9px] font-bold uppercase tracking-wider text-primary border border-primary/20 bg-primary/10 hover:bg-primary/20 px-1.5 py-0.5 rounded transition-all cursor-pointer font-space"
|
||||
>
|
||||
<Cpu className="h-3 w-3" /> Ändern
|
||||
</button>
|
||||
</div>
|
||||
<div className="text-xs font-medium mt-1 font-mono text-primary flex items-center gap-1.5">
|
||||
<Layers className="h-3.5 w-3.5" />
|
||||
{agent.brain_model ? `model: ${agent.brain_model}` : "model: auto"}
|
||||
@@ -628,6 +689,68 @@ export function DashboardView() {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{agent && showBrainSelect && (
|
||||
<div className="fixed inset-0 z-50 bg-black/60 backdrop-blur-sm flex items-center justify-center p-4">
|
||||
<div className="w-full max-w-md rounded-2xl border border-border/80 bg-card p-6 shadow-2xl space-y-4 animate-in fade-in zoom-in-95 duration-200">
|
||||
<div className="flex items-center justify-between border-b border-border/20 pb-2">
|
||||
<h3 className="text-xs font-bold uppercase tracking-wider text-primary flex items-center gap-1.5">
|
||||
<Cpu className="h-4 w-4" />
|
||||
<span>Hermes-Gehirn konfigurieren</span>
|
||||
</h3>
|
||||
<button
|
||||
onClick={() => setShowBrainSelect(false)}
|
||||
className="p-1 rounded hover:bg-accent text-muted-foreground hover:text-foreground cursor-pointer"
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground leading-relaxed">
|
||||
Das „Gehirn" ist das primäre LLM für Hermes. Wähle ein Modell aus deiner Bibliothek oder nutze ein Gateway-Alias (<code className="text-primary font-semibold">auto</code> / <code className="text-primary font-semibold">fast</code> / <code className="text-primary font-semibold">heavy</code>):
|
||||
</p>
|
||||
|
||||
<div className="space-y-1.5 max-h-60 overflow-y-auto pr-1">
|
||||
{(["auto", "fast", "heavy", ...models.map(m => m.name.split("/").pop()?.replace(".gguf", "") || m.name)]).map((m) => {
|
||||
const isAlias = ["auto", "fast", "heavy"].includes(m);
|
||||
return (
|
||||
<button
|
||||
key={m}
|
||||
onClick={() => changeBrainModel(m)}
|
||||
className={cn(
|
||||
"w-full text-left px-3 py-2.5 rounded-lg text-xs hover:bg-accent flex items-center justify-between font-mono cursor-pointer border border-border/30",
|
||||
agent.brain_model === m || (!agent.brain_model && m === "auto")
|
||||
? "text-primary font-bold bg-primary/10 border-primary/30"
|
||||
: "text-foreground bg-background/20"
|
||||
)}
|
||||
>
|
||||
<div className="flex flex-col text-left">
|
||||
<span className="font-semibold truncate max-w-[280px]">
|
||||
{m}
|
||||
</span>
|
||||
<span className="text-[9px] text-muted-foreground mt-0.5">
|
||||
{isAlias ? "Gateway Routing Alias" : "Installiertes GGUF Modell"}
|
||||
</span>
|
||||
</div>
|
||||
{(agent.brain_model === m || (!agent.brain_model && m === "auto")) && (
|
||||
<Check className="h-4 w-4 shrink-0 text-primary" />
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{dialog && (
|
||||
<CustomDialog
|
||||
type={dialog.type}
|
||||
title={dialog.title}
|
||||
message={dialog.message}
|
||||
onConfirm={() => dialog.onConfirm()}
|
||||
onCancel={dialog.onCancel}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
+479
-222
@@ -1,9 +1,10 @@
|
||||
import { useState, useEffect } from "react"
|
||||
import { BookOpen, Layers, Brain, Terminal, Code, RefreshCw, Cpu, Star } from "lucide-react"
|
||||
import { BookOpen, Layers, Brain, Terminal, Code, RefreshCw, Cpu, Star, Compass, Wrench, Shield, ArrowRight } from "lucide-react"
|
||||
import { api, type Health } from "@/lib/api"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
export function GuideView() {
|
||||
const [mainTab, setMainTab] = useState<"connect" | "concepts">("connect")
|
||||
const [activeTab, setActiveTab] = useState<"roocode" | "cursor" | "opencode">("roocode")
|
||||
const [health, setHealth] = useState<Health | null>(null)
|
||||
|
||||
@@ -42,267 +43,523 @@ export function GuideView() {
|
||||
</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>
|
||||
|
||||
{/* Main Mode Tabs */}
|
||||
<div className="flex gap-4 border-b border-border/40 pb-px">
|
||||
<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"
|
||||
onClick={() => setMainTab("connect")}
|
||||
className={cn(
|
||||
"pb-3 text-xs font-bold uppercase tracking-wider border-b-2 px-1 transition-all",
|
||||
mainTab === "connect"
|
||||
? "border-primary text-primary"
|
||||
: "border-transparent text-muted-foreground hover:text-foreground"
|
||||
)}
|
||||
>
|
||||
<RefreshCw className={cn("h-3.5 w-3.5", testing && "animate-spin")} />
|
||||
<span>Testen</span>
|
||||
Editor-Anbindung
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setMainTab("concepts")}
|
||||
className={cn(
|
||||
"pb-3 text-xs font-bold uppercase tracking-wider border-b-2 px-1 transition-all",
|
||||
mainTab === "concepts"
|
||||
? "border-primary text-primary"
|
||||
: "border-transparent text-muted-foreground hover:text-foreground"
|
||||
)}
|
||||
>
|
||||
KI-Wissensdatenbank (Juni 2026)
|
||||
</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>
|
||||
{mainTab === "connect" ? (
|
||||
<>
|
||||
{/* 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>
|
||||
<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>
|
||||
|
||||
<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>
|
||||
|
||||
{/* 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>
|
||||
{/* 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>
|
||||
<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 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>
|
||||
|
||||
<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>
|
||||
{/* 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>
|
||||
|
||||
<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
|
||||
{/* 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>
|
||||
<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://{currentHost}: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 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://{currentHost}: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>
|
||||
)}
|
||||
|
||||
<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)
|
||||
{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>
|
||||
<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-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://{currentHost}: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]">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://{currentHost}: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 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>
|
||||
)}
|
||||
|
||||
<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
|
||||
{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>
|
||||
<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-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://{currentHost}: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]">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://{currentHost}:9001/v1</div>
|
||||
<div><span className="text-muted-foreground/60">Model:</span> auto</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>
|
||||
|
||||
<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>
|
||||
{/* 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) is. 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>
|
||||
</>
|
||||
) : (
|
||||
/* KI-Wissensdatenbank Concepts Tab */
|
||||
<div className="space-y-6">
|
||||
{/* Introduction */}
|
||||
<div className="rounded-2xl border border-border/60 bg-card/45 backdrop-blur-md p-5 shadow-lg shadow-black/10 flex items-start gap-4">
|
||||
<Compass className="h-8 w-8 text-primary shrink-0 mt-0.5" />
|
||||
<div className="space-y-1">
|
||||
<h3 className="text-xs font-bold uppercase tracking-wider text-foreground">Entwickler-Guide: Modernes Agentic Coding (2026)</h3>
|
||||
<p className="text-xs text-muted-foreground leading-normal">
|
||||
Willkommen im Wissenszentrum für dein Mission Control 2 Setup. Hier erfährst du, wie die verschiedenen Technologien (MoE, MCP, Skills, Hermes) zusammenarbeiten und wie du das Maximum aus deinen AI-Prozessabläufen herausholst.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Concepts Grid */}
|
||||
<div className="grid gap-6 md:grid-cols-2">
|
||||
|
||||
{/* MoE Card */}
|
||||
<div className="rounded-2xl border border-border/60 bg-card/45 p-6 shadow-lg shadow-black/10 space-y-4">
|
||||
<div className="flex items-center gap-2 border-b border-border/20 pb-3">
|
||||
<Layers className="h-5 w-5 text-cyan-400" />
|
||||
<div>
|
||||
<h3 className="text-xs font-bold uppercase tracking-wider text-foreground">1. Mixture of Experts (MoE)</h3>
|
||||
<span className="text-[9px] text-cyan-400 font-mono">Effizienz durch Spezialisierung</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground space-y-2 leading-normal">
|
||||
<p>
|
||||
<strong>Was ist das?</strong> Bei traditionellen LLMs wird für jedes Wort das gesamte neuronale Netz aktiviert. Bei MoE besteht das Modell aus mehreren spezialisierten Teilnetzwerken (den <em>Experts</em>). Ein intelligenter <em>Router</em> entscheidet pro Token (Wortteil), welche Experten zur Berechnung herangezogen werden.
|
||||
</p>
|
||||
<p>
|
||||
<strong>Warum in MC2?</strong> So können extrem leistungsstarke Modelle (wie DeepSeek-V3, Mixtral oder Command R+) mit wesentlich geringeren Hardwarekosten ausgeführt werden. Es wird nur ein Bruchteil der Parameter geladen und aktiv berechnet, was Speicherplatz spart und die Inferenz beschleunigt.
|
||||
</p>
|
||||
<div className="p-3 bg-background/25 rounded-xl border border-border/30 font-mono text-[10px] text-foreground">
|
||||
<span className="text-cyan-400">Vorteil:</span> GPT-4-Klasse Performance bei einem Bruchteil der aktiven VRAM-Last auf deinem Homelab!
|
||||
</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>
|
||||
{/* MCP Card */}
|
||||
<div className="rounded-2xl border border-border/60 bg-card/45 p-6 shadow-lg shadow-black/10 space-y-4">
|
||||
<div className="flex items-center gap-2 border-b border-border/20 pb-3">
|
||||
<Compass className="h-5 w-5 text-violet-400" />
|
||||
<div>
|
||||
<h3 className="text-xs font-bold uppercase tracking-wider text-foreground">2. Model Context Protocol (MCP)</h3>
|
||||
<span className="text-[9px] text-violet-400 font-mono">Standardisierte Agenten-Tools</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground space-y-2 leading-normal">
|
||||
<p>
|
||||
<strong>Was ist das?</strong> MCP ist ein offenes Protokoll (initiiert von Anthropic), das festlegt, wie ein KI-Client (z.B. Roo Code auf deinem PC) mit externen Datenquellen und Tools kommuniziert. Es funktioniert wie ein USB-Standard für KI.
|
||||
</p>
|
||||
<p>
|
||||
<strong>Warum in MC2?</strong> MCP trennt den AI-Kern von der Umgebung. Statt für jeden Editor eigene Tools zu schreiben, binden deine Agenten (Roo Code, Hermes) einfach MCP-Server an. Diese Server können Dateien lesen, Websuchen durchführen, Git bedienen oder mit deiner App interagieren.
|
||||
</p>
|
||||
<div className="space-y-1.5 p-3 bg-background/25 rounded-xl border border-border/30 text-[10px]">
|
||||
<div className="font-bold text-foreground">Gute Quellen für MCP Server:</div>
|
||||
<ul className="list-disc pl-4 space-y-1 text-muted-foreground">
|
||||
<li>
|
||||
<a href="https://smithery.ai/" target="_blank" rel="noopener" className="text-primary hover:underline font-semibold">Smithery Registry</a> — Ein Portal zum Suchen und automatischen Installieren von MCP Servern.
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://glama.ai/mcp/servers" target="_blank" rel="noopener" className="text-primary hover:underline font-semibold">Glama MCP Registry</a> — Eine kuratierte, umfangreiche Community-Datenbank von MCP Servern.
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://github.com/modelcontextprotocol/servers" target="_blank" rel="noopener" className="text-primary hover:underline font-semibold">Offizielles Anthropic Repo</a> — Das offizielle Repository mit Standards wie filesystem, postgres, sqlite, brave-search und puppeteer.
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</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>
|
||||
{/* Skills Card */}
|
||||
<div className="rounded-2xl border border-border/60 bg-card/45 p-6 shadow-lg shadow-black/10 space-y-4">
|
||||
<div className="flex items-center gap-2 border-b border-border/20 pb-3">
|
||||
<Brain className="h-5 w-5 text-emerald-400" />
|
||||
<div>
|
||||
<h3 className="text-xs font-bold uppercase tracking-wider text-foreground">3. Agent Skills</h3>
|
||||
<span className="text-[9px] text-emerald-400 font-mono">Modulbasierte Fähigkeiten</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground space-y-2 leading-normal">
|
||||
<p>
|
||||
<strong>Was ist das?</strong> Ein Skill ist ein Verzeichnis mit standardisierten Anweisungen, Scripten und Beispielen, das deine Agenten für spezifische Aufgaben trainiert (z.B. Test-Driven Development, Code-Vereinfachung, API-Design).
|
||||
</p>
|
||||
<p>
|
||||
<strong>Wie benutzt man sie?</strong> Lege einen Skill-Ordner unter <code>.agents/skills/<name></code> in deinem Projekt an. Das Herzstück ist die Datei <code>SKILL.md</code> mit folgendem Aufbau:
|
||||
</p>
|
||||
<pre className="p-2.5 bg-background/25 rounded-lg border border-border/30 font-mono text-[9px] text-foreground overflow-x-auto whitespace-pre">
|
||||
{`---
|
||||
name: tdd-pro
|
||||
description: Drive development with strict TDD practices
|
||||
---
|
||||
# Instructions
|
||||
...`}
|
||||
</pre>
|
||||
<div className="space-y-1.5 p-3 bg-background/25 rounded-xl border border-border/30 text-[10px]">
|
||||
<div className="font-bold text-foreground">Wo gibt es Skills & wo liegen sie?</div>
|
||||
<ul className="list-disc pl-4 space-y-2.5 text-muted-foreground">
|
||||
<li>
|
||||
<strong>skills.sh Registry & CLI:</strong> Das offizielle offene Portal für Agent-Skills (<a href="https://skills.sh/" target="_blank" rel="noopener" className="text-primary hover:underline font-semibold">skills.sh</a>). Du kannst Skills direkt über das Terminal suchen und in deinem Projekt installieren:
|
||||
<div className="mt-1 font-mono text-[9px] bg-background/40 p-2 rounded border border-border/30 text-cyan-300">
|
||||
# Nach Skills suchen:<br />
|
||||
<span className="text-foreground">npx skills find</span><br />
|
||||
# Skill zum aktuellen Projekt hinzufügen:<br />
|
||||
<span className="text-foreground">npx skills add [owner/repo]</span>
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<strong>Globaler Pfad:</strong> <code className="text-foreground select-all">C:\Users\TobisPC\.gemini\config\plugins\agent-skills\skills\</code>. Hier sind deine vorinstallierten, global verfügbaren Skills (wie <i>code-simplification</i>, <i>api-and-interface-design</i>, etc.) abgelegt.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Projekt-Pfad:</strong> <code className="text-foreground select-all">.agents/skills/</code>. Lege diesen Ordner im Root eines beliebigen Projekts an. Dein lokaler Editor-Agent (z.B. Roo Code) liest ihn beim Starten automatisch ein.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Vorlagen / Beispiele:</strong> Kopiere einfach bestehende Skills aus dem globalen Verzeichnis oder erstelle deine eigenen, indem du eine <code>SKILL.md</code> mit YAML-Header (name, description) anlegst.
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Hermes Card */}
|
||||
<div className="rounded-2xl border border-border/60 bg-card/45 p-6 shadow-lg shadow-black/10 space-y-4">
|
||||
<div className="flex items-center gap-2 border-b border-border/20 pb-3">
|
||||
<Cpu className="h-5 w-5 text-indigo-400" />
|
||||
<div>
|
||||
<h3 className="text-xs font-bold uppercase tracking-wider text-foreground">4. Arbeiten mit Hermes</h3>
|
||||
<span className="text-[9px] text-indigo-400 font-mono">Autonomer Box-Agent</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground space-y-2 leading-normal">
|
||||
<p>
|
||||
<strong>Was ist das?</strong> Hermes ist der auf der Box installierte, autonome Hintergrund-Agent. Er verwaltet das Dateisystem und kann über REST (Port 8642) oder eine interaktive ChatUI (Port 8787) gesteuert werden.
|
||||
</p>
|
||||
<p>
|
||||
<strong>Best Practices für Hermes:</strong>
|
||||
</p>
|
||||
<ul className="list-disc pl-4 space-y-1">
|
||||
<li><strong>Chat-Kontext sauber halten:</strong> Starte regelmäßig neue Chats. Zu große Historien verlangsamen den Agenten und führen zu Halluzinationen.</li>
|
||||
<li><strong>Gehirn festlegen:</strong> Konfiguriere im Gateway die Modell-Rolle <code>brain</code> für Hermes, damit er automatisch das passende Modell per Llama Swap lädt.</li>
|
||||
<li><strong>Sandbox umgehen:</strong> Erweitere Hermes' System-Prompt (WebUI-Einstellungen), um Befehle über SSH an deinen Windows-PC zu leiten.</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Tools Best Practices */}
|
||||
<div className="rounded-2xl border border-border/60 bg-card/45 p-6 shadow-lg shadow-black/10 space-y-4 md:col-span-2">
|
||||
<div className="flex items-center gap-2 border-b border-border/20 pb-3">
|
||||
<Wrench className="h-5 w-5 text-amber-400" />
|
||||
<div>
|
||||
<h3 className="text-xs font-bold uppercase tracking-wider text-foreground">5. Richtiges Arbeiten mit Tools (Dateien, Terminal, DevTools)</h3>
|
||||
<span className="text-[9px] text-amber-400 font-mono">Fehler vermeiden & Kosten senken</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid md:grid-cols-3 gap-4 text-xs text-muted-foreground leading-normal">
|
||||
<div className="space-y-1.5 p-3 bg-background/10 rounded-xl border border-border/30">
|
||||
<h4 className="font-bold text-foreground flex items-center gap-1">
|
||||
<Terminal className="h-3.5 w-3.5 text-primary" /> Terminal
|
||||
</h4>
|
||||
<p className="text-[11px]">
|
||||
Verwende nur non-interaktive Befehle. Hänge bei langen Tasks (wie dev-Servern) ein <code>&</code> an oder nutze die integrierte Job-Verwaltung. Verwende niemals interaktive Eingabeaufforderungen.
|
||||
</p>
|
||||
</div>
|
||||
<div className="space-y-1.5 p-3 bg-background/10 rounded-xl border border-border/30">
|
||||
<h4 className="font-bold text-foreground flex items-center gap-1">
|
||||
<Code className="h-3.5 w-3.5 text-cyan-400" /> Dateimanager
|
||||
</h4>
|
||||
<p className="text-[11px]">
|
||||
Überschreibe keine ganzen Dateien, wenn du nur eine Zeile ändern willst. Verwende gezielte Ersetzungs-Tools (wie <code>replace_file_content</code>). Das spart massiv Token-Kosten und beugt Fehlern vor.
|
||||
</p>
|
||||
</div>
|
||||
<div className="space-y-1.5 p-3 bg-background/10 rounded-xl border border-border/30">
|
||||
<h4 className="font-bold text-foreground flex items-center gap-1">
|
||||
<Wrench className="h-3.5 w-3.5 text-violet-400" /> Browser DevTools
|
||||
</h4>
|
||||
<p className="text-[11px]">
|
||||
Koppele deine Debug-Dienste mit dem Chrome-DevTools-Plugin. So kann der Agent Fehler in der Konsole live analysieren und das DOM verifizieren, anstatt blind zu raten.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Autonomie Erklärung */}
|
||||
<div className="rounded-2xl border border-border/60 bg-card/45 p-6 shadow-lg shadow-black/10 space-y-4 md:col-span-2">
|
||||
<div className="flex items-center gap-2 border-b border-border/20 pb-3">
|
||||
<Shield className="h-5 w-5 text-emerald-400" />
|
||||
<div>
|
||||
<h3 className="text-xs font-bold uppercase tracking-wider text-foreground">Wie autonom ist Mission Control 2 wirklich?</h3>
|
||||
<span className="text-[9px] text-emerald-400 font-mono">Die Grenze zwischen Automatisierung und Kontrolle</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground space-y-3 leading-normal">
|
||||
<p>
|
||||
Mission Control 2 ist als <strong>semi-autonomes Gateway</strong> konzipiert. Es besitzt die Fähigkeit, komplexe Workflows komplett selbstständig durchzuführen, unterliegt jedoch strikten Sicherheitsplanken:
|
||||
</p>
|
||||
<div className="grid sm:grid-cols-2 gap-4 pt-1">
|
||||
<div className="space-y-1.5">
|
||||
<h4 className="font-bold text-foreground flex items-center gap-1 text-[11px]">
|
||||
<ArrowRight className="h-3 w-3 text-emerald-400" /> Was läuft vollautomatisch?
|
||||
</h4>
|
||||
<ul className="list-disc pl-4 space-y-1 text-[11px]">
|
||||
<li>Modellaustausch (Routing) per llama-swap je nach Anfragetyp (z.B. Code vs. Reasoning).</li>
|
||||
<li>Hintergrund-Synchronisation und Speicherung von Gedächtniseinträgen (Memory).</li>
|
||||
<li>Modell-Installation und API-Key-Injektionen in Gateway-Verbindungen.</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<h4 className="font-bold text-foreground flex items-center gap-1 text-[11px]">
|
||||
<ArrowRight className="h-3 w-3 text-amber-400" /> Wo ist menschliche Freigabe nötig?
|
||||
</h4>
|
||||
<ul className="list-disc pl-4 space-y-1 text-[11px]">
|
||||
<li><strong>Systembefehle:</strong> Das Ausführen von Terminal-Kommandos auf deinem PC erfordert standardmäßig deine Bestätigung.</li>
|
||||
<li><strong>Kritische Systemeingriffe:</strong> OS-Updates, Engine-Rebuilds und Host-Reboots müssen manuell über das Dashboard ausgelöst werden.</li>
|
||||
<li><strong>Gedächtnis-Löschung:</strong> Permanentes Verwerfen von gesammelten Memorys wird von dir freigegeben.</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-[11px] bg-background/25 p-3 rounded-xl border border-border/30 mt-2">
|
||||
<strong>Fazit:</strong> Der Stack erledigt die Kärrnerarbeit (Modelle tauschen, API-Adapter bereitstellen, Sandbox-Verbindungen herstellen) komplett im Hintergrund. Er agiert als dein persönlicher, treuer Copilot, ohne jemals ungefragt schädliche Operationen auf deinem Hauptsystem auszuführen.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@ import { useEffect, useState } from "react"
|
||||
import { Trash2, Sparkles, User, Scroll, Shield, Tag, Clock, Search, Plus, BookOpen } from "lucide-react"
|
||||
import { api, type DedupeResult, type Memory } from "@/lib/api"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { CustomDialog } from "@/components/CustomDialog"
|
||||
|
||||
|
||||
const CATEGORIES = ["user", "instruction", "stable", "versioned", "ephemeral"]
|
||||
|
||||
@@ -32,6 +34,41 @@ export function MemoryView() {
|
||||
const [error, setError] = useState("")
|
||||
const [deduping, setDeduping] = useState(false)
|
||||
|
||||
// Custom Dialog State
|
||||
const [dialog, setDialog] = useState<{
|
||||
type: "alert" | "confirm"
|
||||
title: string
|
||||
message: string
|
||||
onConfirm: () => void
|
||||
onCancel?: () => void
|
||||
} | null>(null)
|
||||
|
||||
function showAlert(title: string, message: string, onConfirm?: () => void) {
|
||||
setDialog({
|
||||
type: "alert",
|
||||
title,
|
||||
message,
|
||||
onConfirm: () => {
|
||||
setDialog(null)
|
||||
if (onConfirm) onConfirm()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function showConfirm(title: string, message: string, onConfirm: () => void) {
|
||||
setDialog({
|
||||
type: "confirm",
|
||||
title,
|
||||
message,
|
||||
onConfirm: () => {
|
||||
setDialog(null)
|
||||
onConfirm()
|
||||
},
|
||||
onCancel: () => setDialog(null)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
function load() {
|
||||
const params = new URLSearchParams()
|
||||
if (q) params.set("q", q)
|
||||
@@ -66,18 +103,26 @@ export function MemoryView() {
|
||||
body: JSON.stringify({ apply: false }),
|
||||
})
|
||||
if (dry.duplicate_count === 0) {
|
||||
alert("Keine Dubletten gefunden — alles sauber.")
|
||||
showAlert("Ergebnis", "Keine Dubletten gefunden — alles sauber.")
|
||||
return
|
||||
}
|
||||
if (confirm(`${dry.duplicate_count} Dublette(n) in ${dry.groups.length} Gruppe(n) gefunden. Entfernen?`)) {
|
||||
await api("/api/memory/dedupe", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ apply: true })
|
||||
})
|
||||
load()
|
||||
}
|
||||
showConfirm(
|
||||
"Deduplizierung bestätigen",
|
||||
`${dry.duplicate_count} Dublette(n) in ${dry.groups.length} Gruppe(n) gefunden. Entfernen?`,
|
||||
async () => {
|
||||
try {
|
||||
await api("/api/memory/dedupe", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ apply: true })
|
||||
})
|
||||
load()
|
||||
} catch (e: any) {
|
||||
showAlert("Fehler", `Fehler beim Löschen: ${e.message}`)
|
||||
}
|
||||
}
|
||||
)
|
||||
} catch (e: any) {
|
||||
alert(`Fehler: ${e.message}`)
|
||||
showAlert("Fehler", `Fehler bei der Deduplizierung: ${e.message}`)
|
||||
} finally {
|
||||
setDeduping(false)
|
||||
}
|
||||
@@ -239,6 +284,16 @@ export function MemoryView() {
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
|
||||
{dialog && (
|
||||
<CustomDialog
|
||||
type={dialog.type}
|
||||
title={dialog.title}
|
||||
message={dialog.message}
|
||||
onConfirm={dialog.onConfirm}
|
||||
onCancel={dialog.onCancel}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@ import { Download, Search, Trash2, Edit3, Star, Layers, Activity, HardDrive, X,
|
||||
import { api, type DiscoverResp, type Fit, type Job, type ModelInfo, type RoutingResp, type UpdatesResp, type ConnectResp } from "@/lib/api"
|
||||
import { CapsChips } from "@/components/CapsChips"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { CustomDialog } from "@/components/CustomDialog"
|
||||
|
||||
|
||||
function fmtBytes(b?: number) {
|
||||
if (!b) return ""
|
||||
@@ -16,8 +18,9 @@ function fmtEta(s?: number) {
|
||||
return m > 0 ? `${m} min` : `${s} s`
|
||||
}
|
||||
|
||||
function JobsBar() {
|
||||
function JobsBar({ onError }: { onError?: (msg: string) => void }) {
|
||||
const [jobs, setJobs] = useState<Job[]>([])
|
||||
const [errorMsg, setErrorMsg] = useState<string | null>(null)
|
||||
|
||||
function load() {
|
||||
api<{ jobs: Job[] }>("/api/jobs")
|
||||
@@ -36,7 +39,8 @@ function JobsBar() {
|
||||
await api(`/api/jobs/${jobId}/cancel`, { method: "POST" })
|
||||
load()
|
||||
} catch (e: any) {
|
||||
alert(`Fehler: ${e.message}`)
|
||||
if (onError) onError(e.message)
|
||||
else setErrorMsg(e.message)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,11 +87,20 @@ function JobsBar() {
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{errorMsg && (
|
||||
<CustomDialog
|
||||
type="alert"
|
||||
title="Fehler"
|
||||
message={errorMsg}
|
||||
onConfirm={() => setErrorMsg(null)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function fmtSize(b: number | null) {
|
||||
function fmtSize(b?: number | null) {
|
||||
if (!b) return "—"
|
||||
const gb = b / 1024 ** 3
|
||||
return gb >= 1 ? `${gb.toFixed(1)} GB` : `${(b / 1024 ** 2).toFixed(0)} MB`
|
||||
@@ -135,12 +148,68 @@ function Cockpit() {
|
||||
|
||||
// UI state
|
||||
const [activeClient, setActiveClient] = useState<"roocode" | "cursor" | "opencode" | "zed" | "continue" | null>(null)
|
||||
const [activeRoleDrop, setActiveRoleDrop] = useState<string | null>(null)
|
||||
const [activeRoleForAssign, setActiveRoleForAssign] = useState<string | null>(null)
|
||||
const [copied, setCopied] = useState(false)
|
||||
const [hoveredNode, setHoveredNode] = useState<string | null>(null)
|
||||
const [viewMode, setViewMode] = useState<"grid" | "list">("grid")
|
||||
const [filterMode, setFilterMode] = useState<"all" | "in_use">("all")
|
||||
|
||||
// Custom Dialog State
|
||||
const [dialog, setDialog] = useState<{
|
||||
type: "alert" | "confirm" | "prompt"
|
||||
title: string
|
||||
message: string
|
||||
defaultValue?: string
|
||||
onConfirm: (val?: string) => void
|
||||
onCancel?: () => void
|
||||
} | null>(null)
|
||||
|
||||
function showAlert(title: string, message: string, onConfirm?: () => void) {
|
||||
setDialog({
|
||||
type: "alert",
|
||||
title,
|
||||
message,
|
||||
onConfirm: () => {
|
||||
setDialog(null)
|
||||
if (onConfirm) onConfirm()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function showConfirm(title: string, message: string, onConfirm: () => void, onCancel?: () => void) {
|
||||
setDialog({
|
||||
type: "confirm",
|
||||
title,
|
||||
message,
|
||||
onConfirm: () => {
|
||||
setDialog(null)
|
||||
onConfirm()
|
||||
},
|
||||
onCancel: () => {
|
||||
setDialog(null)
|
||||
if (onCancel) onCancel()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function showPrompt(title: string, message: string, defaultValue: string, onConfirm: (val?: string) => void, onCancel?: () => void) {
|
||||
setDialog({
|
||||
type: "prompt",
|
||||
title,
|
||||
message,
|
||||
defaultValue,
|
||||
onConfirm: (val) => {
|
||||
setDialog(null)
|
||||
onConfirm(val)
|
||||
},
|
||||
onCancel: () => {
|
||||
setDialog(null)
|
||||
if (onCancel) onCancel()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
const filteredModels = models.filter((m) => {
|
||||
if (filterMode === "in_use") {
|
||||
return !!m.role || running.includes(m.name)
|
||||
@@ -226,7 +295,7 @@ function Cockpit() {
|
||||
await api(`/api/models/${encodeURIComponent(name)}/load`, { method: "POST" })
|
||||
load()
|
||||
} catch (e: any) {
|
||||
alert(`Fehler beim Laden des Modells: ${e.message}`)
|
||||
showAlert("Fehler", `Fehler beim Laden des Modells: ${e.message}`)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -235,7 +304,7 @@ function Cockpit() {
|
||||
await api(`/api/models/${encodeURIComponent(name)}/unload`, { method: "POST" })
|
||||
load()
|
||||
} catch (e: any) {
|
||||
alert(`Fehler beim Entladen des Modells: ${e.message}`)
|
||||
showAlert("Fehler", `Fehler beim Entladen des Modells: ${e.message}`)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -244,45 +313,55 @@ function Cockpit() {
|
||||
await api("/api/models/unload", { method: "POST" })
|
||||
load()
|
||||
} catch (e: any) {
|
||||
alert(`Fehler beim Entladen aller Modelle: ${e.message}`)
|
||||
showAlert("Fehler", `Fehler beim Entladen aller Modelle: ${e.message}`)
|
||||
}
|
||||
}
|
||||
|
||||
async function handleRoleChange(role: string, modelName: string) {
|
||||
setActiveRoleDrop(null)
|
||||
try {
|
||||
await api(`/api/models/${encodeURIComponent(modelName)}/role`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ role: role || null }),
|
||||
})
|
||||
load()
|
||||
} catch (e) {
|
||||
alert(`Fehler beim Zuweisen der Rolle: ${e}`)
|
||||
} catch (e: any) {
|
||||
showAlert("Fehler", `Fehler beim Zuweisen der Rolle: ${e.message || e}`)
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSetCtx(name: string, cur: number | null) {
|
||||
const v = prompt("Kontextlänge (Tokens):", String(cur || 32768))
|
||||
if (!v) return
|
||||
try {
|
||||
await api(`/api/models/${encodeURIComponent(name)}/ctx`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ ctx: parseInt(v, 10) }),
|
||||
})
|
||||
load()
|
||||
} catch (e) {
|
||||
alert(`Fehler beim Setzen des Kontexts: ${e}`)
|
||||
}
|
||||
showPrompt(
|
||||
"Kontextlänge anpassen",
|
||||
"Gib die gewünschte Kontextlänge in Tokens an:",
|
||||
String(cur || 32768),
|
||||
async (v) => {
|
||||
if (!v) return
|
||||
try {
|
||||
await api(`/api/models/${encodeURIComponent(name)}/ctx`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ ctx: parseInt(v, 10) }),
|
||||
})
|
||||
load()
|
||||
} catch (e: any) {
|
||||
showAlert("Fehler", `Fehler beim Setzen des Kontexts: ${e.message || e}`)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
async function handleDelete(name: string) {
|
||||
if (!confirm(`Modell '${name}' und alle zugehörigen GGUF-Dateien unwiderruflich von der Box löschen?`)) return
|
||||
try {
|
||||
await api(`/api/models/${encodeURIComponent(name)}`, { method: "DELETE" })
|
||||
load()
|
||||
} catch (e) {
|
||||
alert(`Fehler beim Löschen: ${e}`)
|
||||
}
|
||||
showConfirm(
|
||||
"Modell löschen?",
|
||||
`Modell '${name}' und alle zugehörigen GGUF-Dateien unwiderruflich von der Box löschen?`,
|
||||
async () => {
|
||||
try {
|
||||
await api(`/api/models/${encodeURIComponent(name)}`, { method: "DELETE" })
|
||||
load()
|
||||
} catch (e: any) {
|
||||
showAlert("Fehler", `Fehler beim Löschen: ${e.message || e}`)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
async function handleSmartUpgrade(repo: string, role: string, quant: string, toolCapable: boolean) {
|
||||
@@ -291,9 +370,9 @@ function Cockpit() {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ repo, role, quant, jinja: toolCapable }),
|
||||
})
|
||||
alert(`Download für '${repo}' gestartet! Der Fortschritt wird oben angezeigt.`)
|
||||
} catch (e) {
|
||||
alert(`Fehler beim Starten des Upgrades: ${e}`)
|
||||
showAlert("Herunterladen gestartet", `Download für '${repo}' gestartet! Der Fortschritt wird oben angezeigt.`)
|
||||
} catch (e: any) {
|
||||
showAlert("Fehler", `Fehler beim Starten des Upgrades: ${e.message || e}`)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -580,7 +659,7 @@ function Cockpit() {
|
||||
: "border-dashed border-border/40 bg-background/20"
|
||||
)}
|
||||
style={{ left: "90%", top: yPositions[indexMap] }}
|
||||
onClick={() => setActiveRoleDrop(activeRoleDrop === role ? null : role)}
|
||||
onClick={() => setActiveRoleForAssign(role)}
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-[10px] font-bold uppercase tracking-wider text-muted-foreground">{role}</span>
|
||||
@@ -589,32 +668,6 @@ function Cockpit() {
|
||||
<div className="text-[10px] font-mono font-medium truncate mt-0.5 text-foreground max-w-[150px]">
|
||||
{activeModel ? activeModel.name.split("/").pop()?.replace(".gguf", "") : "Keine Zuweisung"}
|
||||
</div>
|
||||
|
||||
{/* Inline drop selection */}
|
||||
{activeRoleDrop === role && (
|
||||
<div className="absolute right-0 top-full mt-1 z-35 w-52 rounded-xl border border-border/80 bg-popover/95 backdrop-blur-md p-1.5 shadow-2xl space-y-1">
|
||||
<div className="text-[9px] uppercase font-bold text-muted-foreground/60 px-2 py-1 select-none">Modell zuweisen:</div>
|
||||
<button
|
||||
onClick={() => handleRoleChange(role, "")}
|
||||
className="w-full text-left px-2.5 py-1.5 rounded-lg text-[10px] hover:bg-accent text-red-400 font-semibold cursor-pointer"
|
||||
>
|
||||
Zuweisung entfernen
|
||||
</button>
|
||||
{models.map((m) => (
|
||||
<button
|
||||
key={m.name}
|
||||
onClick={() => handleRoleChange(role, m.name)}
|
||||
className={cn(
|
||||
"w-full text-left px-2.5 py-1.5 rounded-lg text-[10px] hover:bg-accent flex items-center justify-between font-mono cursor-pointer",
|
||||
m.role === role ? "text-primary font-bold" : "text-foreground"
|
||||
)}
|
||||
>
|
||||
<span className="truncate max-w-[150px]">{m.name.split("/").pop()?.replace(".gguf", "")}</span>
|
||||
{m.role === role && <Check className="h-3 w-3 shrink-0" />}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
@@ -1018,6 +1071,72 @@ function Cockpit() {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Role Assignment Modal */}
|
||||
{activeRoleForAssign && (
|
||||
<div className="fixed inset-0 z-50 bg-black/60 backdrop-blur-sm flex items-center justify-center p-4">
|
||||
<div className="w-full max-w-md rounded-2xl border border-border/80 bg-card p-6 shadow-2xl space-y-4 animate-in fade-in zoom-in-95 duration-200">
|
||||
<div className="flex items-center justify-between border-b border-border/20 pb-2">
|
||||
<h3 className="text-xs font-bold uppercase tracking-wider text-primary">
|
||||
Rolle '{activeRoleForAssign}' konfigurieren
|
||||
</h3>
|
||||
<button
|
||||
onClick={() => setActiveRoleForAssign(null)}
|
||||
className="p-1 rounded hover:bg-accent text-muted-foreground hover:text-foreground cursor-pointer"
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Wähle ein Modell aus deiner Bibliothek für die Rolle <strong className="text-foreground">{activeRoleForAssign}</strong>:
|
||||
</p>
|
||||
|
||||
<div className="space-y-1.5 max-h-60 overflow-y-auto pr-1">
|
||||
<button
|
||||
onClick={() => {
|
||||
handleRoleChange(activeRoleForAssign, "")
|
||||
setActiveRoleForAssign(null)
|
||||
}}
|
||||
className="w-full text-left px-3 py-2 rounded-lg text-xs hover:bg-accent text-red-400 font-semibold cursor-pointer border border-red-500/20 bg-red-500/5 flex items-center justify-between"
|
||||
>
|
||||
<span>Zuweisung entfernen</span>
|
||||
</button>
|
||||
{models.map((m) => (
|
||||
<button
|
||||
key={m.name}
|
||||
onClick={() => {
|
||||
handleRoleChange(activeRoleForAssign, m.name)
|
||||
setActiveRoleForAssign(null)
|
||||
}}
|
||||
className={cn(
|
||||
"w-full text-left px-3 py-2.5 rounded-lg text-xs hover:bg-accent flex items-center justify-between font-mono cursor-pointer border border-border/30",
|
||||
m.role === activeRoleForAssign
|
||||
? "text-primary font-bold bg-primary/10 border-primary/30"
|
||||
: "text-foreground bg-background/20"
|
||||
)}
|
||||
>
|
||||
<div className="flex flex-col text-left">
|
||||
<span className="truncate max-w-[280px] font-semibold">{m.name.split("/").pop()?.replace(".gguf", "")}</span>
|
||||
<span className="text-[9px] text-muted-foreground mt-0.5">{fmtSize(m.size_bytes)} · {m.quant}</span>
|
||||
</div>
|
||||
{m.role === activeRoleForAssign && <Check className="h-4 w-4 shrink-0 text-primary" />}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{dialog && (
|
||||
<CustomDialog
|
||||
type={dialog.type}
|
||||
title={dialog.title}
|
||||
message={dialog.message}
|
||||
defaultValue={dialog.defaultValue}
|
||||
onConfirm={dialog.onConfirm}
|
||||
onCancel={dialog.onCancel}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@ import { useEffect, useState } from "react"
|
||||
import { ExternalLink, RefreshCw, Save, Cpu, HardDrive, Cpu as GpuIcon, Activity } from "lucide-react"
|
||||
import { api, type ServicesResp, type SystemStatus } from "@/lib/api"
|
||||
import { cn, resolveExternalUrl } from "@/lib/utils"
|
||||
import { CustomDialog } from "@/components/CustomDialog"
|
||||
|
||||
|
||||
function gb(b: number) {
|
||||
return (b / 1024 ** 3).toFixed(1)
|
||||
@@ -43,6 +45,27 @@ export function SystemView() {
|
||||
const [backupMsg, setBackupMsg] = useState("")
|
||||
const [restartingServices, setRestartingServices] = useState<Record<string, boolean>>({})
|
||||
|
||||
// Custom Dialog State
|
||||
const [dialog, setDialog] = useState<{
|
||||
type: "alert" | "confirm"
|
||||
title: string
|
||||
message: string
|
||||
onConfirm: () => void
|
||||
onCancel?: () => void
|
||||
} | null>(null)
|
||||
|
||||
function showAlert(title: string, message: string, onConfirm?: () => void) {
|
||||
setDialog({
|
||||
type: "alert",
|
||||
title,
|
||||
message,
|
||||
onConfirm: () => {
|
||||
setDialog(null)
|
||||
if (onConfirm) onConfirm()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function load() {
|
||||
api<SystemStatus>("/api/system/status").then(setS).catch((e) => setError(String(e)))
|
||||
api<ServicesResp>("/api/system/services").then(setSvc).catch(() => {})
|
||||
@@ -72,12 +95,12 @@ export function SystemView() {
|
||||
body: JSON.stringify({ service: serviceId })
|
||||
})
|
||||
if (r.ok) {
|
||||
alert(`Dienst ${serviceId} wurde erfolgreich neu gestartet.`)
|
||||
showAlert("Erfolgreich", `Dienst ${serviceId} wurde erfolgreich neu gestartet.`)
|
||||
} else {
|
||||
alert(`Fehler beim Neustart: ${r.err || "Unbekannter Fehler"}`)
|
||||
showAlert("Fehler beim Neustart", `Fehler beim Neustart: ${r.err || "Unbekannter Fehler"}`)
|
||||
}
|
||||
} catch (e: any) {
|
||||
alert(`Fehler: ${e.message}`)
|
||||
showAlert("Fehler", `Fehler: ${e.message}`)
|
||||
} finally {
|
||||
setRestartingServices(prev => ({ ...prev, [serviceId]: false }))
|
||||
}
|
||||
@@ -234,7 +257,15 @@ export function SystemView() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Maintenance controls inside system view */}
|
||||
{dialog && (
|
||||
<CustomDialog
|
||||
type={dialog.type}
|
||||
title={dialog.title}
|
||||
message={dialog.message}
|
||||
onConfirm={dialog.onConfirm}
|
||||
onCancel={dialog.onCancel}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user