import { useEffect, useState } from "react" import { useLocation } from "@tanstack/react-router" import { Search } from "lucide-react" import { Auftraege } from "@/components/cockpit/Auftraege" import { Bestaetigen } from "@/components/cockpit/Bestaetigen" import { Chip, type ChipArt, Zustandsfeld } from "@/components/cockpit/Chip" import { Panel } from "@/components/cockpit/Panel" import { Button } from "@/components/ui/button" import { useAufraeumen, useAufraeumenLoeschen, useJobs, useModellLaden, useModelle, useNutzung, useRadar, useRadarAktion, useRadarSuche, useStart, } from "@/lib/abfragen" import { messwert, RADAR_STATUS_TEXT, rolleVon } from "@/lib/anzeige" import { ApiFehler } from "@/lib/api" import { useLetzteMetrik } from "@/lib/strom" import type { AufraeumKandidat, Modell, Nutzung, RadarKandidat, RadarTest } 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, zwilling, laeuft, nutzer }: { titel: string modell?: Modell zwilling?: 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 // Seit 24.09.: Hirn und Coder sehen über ihren Bild-Zwilling (gleiche Gewichte, ohne Draft). const bilder = Boolean(modell.capabilities?.vision) || Boolean(zwilling) return (
{titel} {laeuft ? "Geladen" : "Auf Abruf"}

{modell.name}

Genutzt von {nutzer}.{modell.spec_active ? " Mit Draft-Beschleunigung." : ""} {zwilling ? " Bilder sieht es über seinen Bild-Zwilling, der nur bei Bedarf lädt." : ""}

{!laeuft && ( )}
) } function NutzungsPanel({ nutzung, laedt, fehler }: { nutzung: Nutzung | undefined; laedt: boolean; fehler: string | null }) { if (!nutzung) { return ( {laedt ? (

Wird gelesen …

) : (

Die Nutzung lässt sich gerade nicht lesen{fehler ? `: ${fehler}` : "."}

)}
) } 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", } const URTEIL_FARBE: Record = { besser: "text-gruen", schlechter: "text-rot-text", gleich: "text-text-2" } type Vergleichszeile = { kandidat: unknown; baseline: unknown; einheit?: string; urteil?: string } /** Die Messwerte hinter dem Urteil: Kandidat gegen das heutige Modell der Rolle. */ function Vergleich({ test }: { test: RadarTest }) { const zeilen = Object.entries(test.vergleich ?? {}) as [string, Vergleichszeile][] if (zeilen.length === 0) return null return (
Messwert Kandidat Heute
{zeilen.map(([name, z]) => (
{name} {messwert(z.kandidat, z.einheit)} {messwert(z.baseline, z.einheit)}
))}
) } function Kandidat({ k, test, heute }: { k: RadarKandidat; test: RadarTest | undefined; heute: string | undefined }) { const aktion = useRadarAktion() const [frage, setFrage] = useState(null) const offen = ["bestanden", "wartet", "neu", "getestet"].includes(k.status) const hirn = k.rolle === "hirn" return (
  • {k.name} {hirn ? "fürs Hirn" : "fürs Coden"} {k.groesse_gb != null && ( // übersprungene Einträge wurden nie vermessen {k.eng ? "passt knapp" : k.passt ? "passt" : "passt nicht"} )} {RADAR_STATUS_TEXT[k.status] ?? k.status}

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

    {test && ["bestanden", "durchgefallen"].includes(k.status) && } {offen && (
    {k.status === "bestanden" && ( )}
    )} setFrage(null)} onJa={() => { if (frage !== "uebernehmen") return // zweiter Klick eines Doppelklicks, der Dialog schließt schon aktion.mutate({ id: k.id, aktion: "uebernehmen", name: k.name }) setFrage(null) }} /> setFrage(null)} onJa={() => { if (frage !== "verwerfen") return aktion.mutate({ id: k.id, aktion: "verwerfen", name: k.name }) setFrage(null) }} />
  • ) } function RadarPanel({ hirn, coder }: { hirn: string | undefined; coder: string | undefined }) { const radar = useRadar() const suche = useRadarSuche() if (radar.isError && !radar.data) { // 404 heißt: Diese Box hat kein Radar. Alles andere ist ein Fehler, kein fehlendes Radar. const fehlt = radar.error instanceof ApiFehler && radar.error.status === 404 return ( {fehlt ? (

    Das Radar ist auf dieser Box noch nicht eingerichtet.

    ) : (

    Das Radar antwortet gerade nicht: {radar.error.message}

    )}
    ) } const d = radar.data return ( suche.mutate(undefined)} disabled={suche.isPending}> {suche.isPending ? "Sucht …" : "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) => ( t.kandidat === k.id)} heute={k.rolle === "hirn" ? hirn : coder} /> ))}
    )} {d && d.tests.length > 0 && (

    Letzte Tests

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

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

    ))}
    )}
    ) } /** Was ungenutzt Platz belegt. Gelöscht wird nur auf Knopfdruck, mit Rückfrage — Löschen ist endgültig. */ function AufraeumenPanel() { const liste = useAufraeumen() const loeschen = useAufraeumenLoeschen() const [frage, setFrage] = useState(null) const kandidaten = liste.data?.kandidaten ?? [] const summe = kandidaten.reduce((s, k) => s + k.groesse_gb, 0) if (liste.isError && !liste.data) { return (

    Was sich aufräumen ließe, lässt sich gerade nicht lesen: {liste.error.message}

    ) } return ( 0 ? bis zu {summe.toLocaleString("de-DE", { maximumFractionDigits: 0 })} GB frei : undefined} >

    Modelle und Ordner, die gerade niemand nutzt. Nichts davon wird von selbst gelöscht, das entscheidest du je Eintrag.

    {liste.isPending ? (

    Wird gelesen …

    ) : kandidaten.length === 0 ? (

    Nichts aufzuräumen.

    ) : (
      {kandidaten.map((k) => (
    • {k.name} {k.art === "eintrag" ? "Eintrag" : "Ordner"} {k.hinweis} {k.groesse_gb.toLocaleString("de-DE")} GB
    • ))}
    )} setFrage(null)} onJa={() => { if (frage) loeschen.mutate(frage) setFrage(null) }} />
    ) } export function ModelleSeite() { const modelle = useModelle() const nutzung = useNutzung() const radar = useRadar() const jobs = useJobs() const [suche, setSuche] = useState(false) // „Ansehen“ im Radar-Kasten der Startseite führt nach /modelle#radar. Der Router springt beim // Rendern selbst zum Anker — da steht hier aber noch „wird gelesen“. Also springen wir, sobald // alles über dem Radar geladen ist und die Seite nicht mehr wächst. const hash = useLocation({ select: (l) => l.hash }) const bereit = !!modelle.data && !nutzung.isPending && !radar.isPending && !jobs.isPending useEffect(() => { if (bereit && hash === "radar") document.getElementById("radar")?.scrollIntoView({ block: "start" }) }, [bereit, hash]) if (modelle.isPending) return // Scheitert nur ein späteres Nachladen, bleibt der letzte Stand stehen. if (!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") // Bild-Zwilling: gleiche Gewichte, eigener Alias (vision bzw. coder-bild). const zwilling = (rolle: Modell | undefined, alias: string) => rolle ? liste.find((m) => m !== rolle && m.aliases.includes(alias) && m.filename === rolle.filename) : undefined const hirnZwilling = zwilling(hirn, "vision") const coderZwilling = zwilling(coder, "coder-bild") const weitere = liste.filter((m) => ![hirn, coder, hirnZwilling, coderZwilling].includes(m)) const zuletzt = nutzung.data?.zuletzt_geladen ?? {} return ( <>
    setSuche(true)}> Modelle selbst suchen} > {weitere.length === 0 &&

    Keine weiteren Einträge.

    }
      {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)} rollenJetzt={{ coder: coder ? { name: coder.name, zwilling: !!coderZwilling, weitere: coder.aliases.filter((a) => a.toLowerCase() !== "coder") } : undefined, }} /> ) }