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 && (
laden.mutate(modell.name)} disabled={laden.isPending}>
Jetzt laden
)}
)
}
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.
}
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" && (
aktion.mutate({ id: k.id, aktion: "uebernehmen", name: k.name })}>
Übernehmen
)}
aktion.mutate({ id: k.id, aktion: "verwerfen", name: k.name })}>
Verwerfen
)}
)
}
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)} />
>
)
}