import { useState } from "react" import { Search } from "lucide-react" import { Chip, type ChipArt, Zustandsfeld } from "@/components/cockpit/Chip" import { Panel } from "@/components/cockpit/Panel" import { Button } from "@/components/ui/button" import { useModellLaden, useModelle, useNutzung, useRadar, useRadarAktion, useRadarSuche, useStart, } from "@/lib/abfragen" import { rolleVon } from "@/lib/anzeige" import { useLetzteMetrik } from "@/lib/strom" import type { Modell, Nutzung, RadarKandidat } from "@/lib/typen" import { flugplanZeit, gb, gbText, zahl } from "@/lib/zeit" import { ModellSuche } from "./ModellSuche" /** Grenze aus der Erfahrung (Memory „llama-swap: nur EINE Gruppe resident“): darüber OOM. */ const GRENZE_GB = 115 function Speicher({ modelle, laufend }: { modelle: Modell[]; laufend: string[] }) { const m = useLetzteMetrik() const start = useStart() const gesamt = gb(m?.ram_total ?? start.data?.box.ram_total) ?? 122.7 const belegt = gb(m?.ram_used ?? start.data?.box.ram_used) ?? 0 const geladen = modelle.filter((x) => laufend.includes(x.name)) const dateien = geladen.reduce((s, x) => s + (gb(x.size_bytes) ?? 0), 0) const rest = Math.max(0, belegt - dateien) const pct = (g: number) => `${Math.min(100, (g / gesamt) * 100)}%` return ( {Math.round(belegt)} von {Math.round(gesamt)} GB belegt · Grenze ~{GRENZE_GB} GB}>
Geladene Modelle: {Math.round(dateien)} GB Dateien Cache, Dienste, System: {Math.round(rest)} GB Ab hier wird es eng
) } function Wert({ name, wert }: { name: string; wert: string }) { return (
{name}
{wert}
) } function Rolle({ titel, modell, laeuft, nutzer }: { titel: string; modell?: Modell; laeuft: boolean; nutzer: string }) { const laden = useModellLaden() if (!modell) { return (
{titel}Frei

Nicht belegt

Nur belegen, wenn ein Modell hier echten Mehrwert bringt. Das Radar meldet Kandidaten.

) } const kontext = modell.ctx ? Math.round(modell.ctx / Math.max(1, modell.parallel_slots) / 1024) : null const bilder = Boolean(modell.capabilities?.vision) return (
{titel} {laeuft ? "Geladen" : "Auf Abruf"}

{modell.name}

Genutzt von {nutzer}.{modell.spec_active ? " Mit Draft-Beschleunigung." : ""}

{!laeuft && ( )}
) } function NutzungsPanel({ nutzung }: { nutzung: Nutzung | undefined }) { if (!nutzung) return null const max = Math.max(1, ...nutzung.absender.map((a) => a.anfragen)) const stunden = nutzung.letzte_24h.map((s) => Object.values(s.je_absender).reduce((a, b) => a + b, 0)) const maxStunde = Math.max(1, ...stunden) return ( letzte {nutzung.tage} Tage}> {nutzung.absender.length === 0 &&

Keine Anfragen aufgezeichnet.

}
    {nutzung.absender.map((a) => (
  • {a.name}{zahl(a.anfragen)}
  • ))}
Anfragen je Stunde, letzte 24 h {stunden.map((n, i) => { const h = n ? Math.max(3, (n / maxStunde) * 50) : 2 return })} {[0, 6, 12, 18].map((s) => ( {s} ))}

Anfragen an llama-swap. NerdQuiz geht direkt dorthin, alles andere über MC2.

) } const STATUS_ART: Record = { bestanden: "gruen", durchgefallen: "rot", wartet: "cyan", neu: "cyan", getestet: "grau", uebernommen: "gruen", verworfen: "grau", } function Kandidat({ k }: { k: RadarKandidat }) { const aktion = useRadarAktion() const offen = ["bestanden", "wartet", "neu"].includes(k.status) return (
  • {k.name} {k.rolle === "hirn" ? "fürs Hirn" : "fürs Coden"} {k.eng ? "passt knapp" : k.passt ? "passt" : "passt nicht"} {k.status}

    {k.groesse_gb != null ? `${k.groesse_gb.toLocaleString("de-DE")} GB. ` : ""} {k.begruendung}

    {offen && (
    {k.status === "bestanden" && ( )}
    )}
  • ) } function RadarPanel() { const radar = useRadar() const suche = useRadarSuche() if (radar.isError) { return (

    Das Radar ist auf dieser Box noch nicht eingerichtet.

    ) } const d = radar.data return ( suche.mutate(undefined)} disabled={suche.isPending}>Jetzt suchen} >

    Testet nachts zwischen 00:30 und 02:30 selbst, höchstens einen Kandidaten pro Woche. {d?.naechster_test ? ` Nächster Test: ${flugplanZeit(d.naechster_test)}.` : ""}

    {!d ? (

    Wird gelesen …

    ) : d.kandidaten.length === 0 ? (

    Nichts Besseres in Sicht.

    ) : (
      {d.kandidaten.map((k) => )}
    )} {d && d.tests.length > 0 && (

    Letzte Tests

    {d.tests.slice(0, 3).map((t) => (

    {flugplanZeit(t.datum)} · {t.zusammenfassung}

    ))}
    )}
    ) } export function ModelleSeite() { const modelle = useModelle() const nutzung = useNutzung() const [suche, setSuche] = useState(false) if (modelle.isPending) return if (modelle.error || !modelle.data) return const liste = modelle.data.models const laufend = modelle.data.running const hirn = liste.find((m) => rolleVon(m) === "hirn") const coder = liste.find((m) => rolleVon(m) === "coder") const weitere = liste.filter((m) => m !== hirn && m !== coder) const zuletzt = nutzung.data?.zuletzt_geladen ?? {} return ( <>
    setSuche(true)}> Modelle selbst suchen} >
      {weitere.map((m) => (
    • {m.name} {m.aliases.length ? m.aliases.join(", ") : "ohne Rolle"} {gbText(m.size_bytes)} {laufend.includes(m.name) ? "geladen" : zuletzt[m.name] ? `zuletzt ${flugplanZeit(zuletzt[m.name])}` : "länger nicht geladen"}
    • ))}
    setSuche(false)} /> ) }