Feat: Add Hermes brain change buttons and skills.sh guide source
This commit is contained in:
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user