import { useState } from "react" import { Zap, Plus, X, Check, HardDrive, AlertTriangle } from "lucide-react" import { setGroup, type ModelInfo } from "@/lib/api" import { useModels, useGroups, useSystemStatus, useHermesBrain, useQueryClient, qk } from "@/lib/queries" import { useDialog } from "@/lib/useDialog" import { RoleLabel, roleMeta } from "@/components/models/ModelBadges" import { fmtSize } from "@/lib/format" import { cn } from "@/lib/utils" // „Immer-bereit-Set" = die ko-residente llama-swap-Gruppe `brains`. Ihre Mitglieder // bleiben gemeinsam warm (verdrängen sich nicht). Hier in Klartext verwaltbar, // mit Speicher-Budget und Geländer (Hirn/Gedächtnis nicht entfernbar). export function WarmSetManager() { const qc = useQueryClient() const { data: modelsResp } = useModels(4_000) const { data: groupsResp } = useGroups() const { data: sysStatus } = useSystemStatus() const { data: brain } = useHermesBrain() const { showAlert, dialogElement } = useDialog() const [picking, setPicking] = useState(false) const [busy, setBusy] = useState(false) const models = modelsResp?.models ?? [] const running = modelsResp?.running ?? [] const group = groupsResp?.groups?.brains const members = group?.members ?? [] const byName = (name: string) => models.find((m) => m.name === name) const shortName = (name: string) => name.split("/").pop()?.replace(/\.gguf$/i, "") || name const memberModels = members.map(byName).filter(Boolean) as ModelInfo[] const B = 1024 ** 3 const bud = brain?.budget const weightsGb = memberModels.reduce((a, m) => a + (m.size_bytes || 0), 0) / B const gttTotal = sysStatus?.gpu?.gtt_total || sysStatus?.gpu?.vram_total || 0 const totalGb = bud?.gtt_gb || (gttTotal ? gttTotal / B : 0) // Ehrlicher Fußabdruck inkl. Arbeitsspeicher (KV-Cache) aus dem Budget; sonst nur Gewichte. const reservedGb = bud?.warm_projected_gb ?? weightsGb const pct = totalGb > 0 ? Math.min(100, (reservedGb / totalGb) * 100) : 0 const eligible = models.filter((m) => !members.includes(m.name) && !m.incomplete) async function save(next: string[]) { setBusy(true) try { await setGroup("brains", next, group?.swap ?? false, group?.persist ?? true) qc.invalidateQueries({ queryKey: qk.groups }) qc.invalidateQueries({ queryKey: qk.models }) qc.invalidateQueries({ queryKey: qk.hermesBrain }) } catch (e: any) { showAlert("Fehler", `Immer-bereit-Set konnte nicht geändert werden: ${e?.message || e}`) } finally { setBusy(false) } } function remove(name: string) { const m = byName(name) if (m && roleMeta(m.role).protected) { showAlert("Geschützt", `„${roleMeta(m.role).label}" ist lebenswichtig und bleibt immer bereit.`) return } void save(members.filter((x) => x !== name)) } function add(name: string) { setPicking(false) void save([...members, name]) } if (!group) { return (

Es gibt noch kein Immer-bereit-Set. Es entsteht automatisch, sobald Lucys Hirn zugewiesen ist.

{dialogElement}
) } return (
{/* Mitglieder */}
{memberModels.length === 0 && (
Noch keine Modelle im Set.
)} {memberModels.map((m) => { const isWarm = running.includes(m.name) const prot = roleMeta(m.role).protected return (
{m.role && } {shortName(m.name)} {fmtSize(m.size_bytes)}
{prot ? ( 🔒 ) : ( )}
) })}
{/* Hinzufügen */} {/* Speicher-Budget */}
Reserviert fürs Set {reservedGb.toFixed(1)}{totalGb > 0 ? ` / ${totalGb.toFixed(0)}` : ""} GB
= 85 ? "bg-red-500" : pct >= 65 ? "bg-amber-500" : "bg-teal-500")} style={{ width: `${pct}%` }} />
{bud && !bud.fits && (

Zusammen mit dem größten Gelegenheits-Modell (~{bud.largest_ondemand_gb} GB, z. B. das große Hirn) passt das Set nicht gleichzeitig in den Speicher. Beim Laden wird das Set kurz verdrängt und danach automatisch nachgeladen — kostet ein paar Sekunden.

)} {bud && bud.fits && (

Passt auch neben dem größten Gelegenheits-Modell — nichts wird verdrängt.

)}
{/* Auswahl-Modal */} {picking && (

Dauerhaft bereithalten

Wähle ein Modell, das ohne Ladezeit bereitstehen soll:

{eligible.map((m) => ( ))}
)} {dialogElement}
) } function Header() { return (

Immer-bereit-Set

Diese Modelle hält Lucy immer bereit — sie antworten ohne Ladezeit.

) }