SPB Git forge

spb/llm-api

Public
0commits 0branches 0releases
0 Bsize
maindefault branch
—last push
11.2 KB · 171 lines tsx
Raw Blame History
1"use client";23import Link from "next/link";4import { useEffect, useState } from "react";5import { api } from "@/lib/api";6import { useLive } from "@/lib/events";7import { fmtAgo, fmtDuration, fmtGB, fmtMs, fmtNum } from "@/lib/format";8import type { Model, SystemInfo } from "@/lib/types";9import { Meter, Pill, Sparkline, StatTile, StatusPill } from "@/components/ui";1011interface Stats { totals: { requests: number; prompt_tokens: number; completion_tokens: number; avg_tps: number | null; avg_ttft_ms: number | null; errors: number }; per_model: { model_id: string; requests: number; completion_tokens: number; avg_tps: number | null }[]; all_time: { requests: number; completion_tokens: number; prompt_tokens: number } }1213export default function Dashboard() {14  const live = useLive();15  const [sys, setSys] = useState<SystemInfo | null>(null);16  const [models, setModels] = useState<Model[]>([]);17  const [stats, setStats] = useState<Stats | null>(null);1819  useEffect(() => {20    api.get<SystemInfo>("/api/system").then(setSys).catch(() => {});21    api.get<{ models: Model[] }>("/api/models").then((r) => setModels(r.models)).catch(() => {});22    api.get<{ requests: Stats }>("/api/system/metrics?minutes=60&hours=24").then((r) => setStats(r.requests)).catch(() => {});23  }, [live.version]);2425  const t = live.metrics || sys?.telemetry;26  const hw = sys?.hardware;27  const mgr = live.manager || sys?.manager;28  const loaded = mgr?.loaded || [];29  const cur = loaded.find((w) => w.status === "ready");30  const memHist = live.metricsHistory.map((m) => m.mem_used_gb);31  const gpuHist = live.metricsHistory.map((m) => m.gpu_percent ?? 0);32  const cpuHist = live.metricsHistory.map((m) => m.cpu_percent);33  const budget = sys?.policy.max_model_memory_gb ?? 45;34  const installed = models.filter((m) => m.installed);35  const recentTps = live.requests.map((r) => Number(r.tps) || 0).filter(Boolean).reverse();3637  return (38    <div className="flex flex-col gap-5">39      <div className="flex flex-wrap items-end justify-between gap-3">40        <div>41          <h1 className="text-xl font-semibold tracking-tight">Dashboard</h1>42          <div className="text-sm text-ink-3 mt-0.5">43            {hw ? <>{hw.chip} · {hw.memory_gb.toFixed(0)} GB unified memory · {hw.gpu_cores ?? "—"} GPU cores · {hw.cpu_cores} CPU cores · macOS {hw.os_version}</> : "…"}44          </div>45        </div>46        <div className="flex items-center gap-2 text-xs text-ink-3">47          <Pill tone={t?.mem_pressure_level === "normal" ? "good" : t?.mem_pressure_level === "warning" ? "warn" : "bad"}>Memory {t?.mem_pressure_level ?? "—"}</Pill>48          <Pill tone={t?.thermal_state === "nominal" ? "good" : "warn"}>Thermal {t?.thermal_state ?? "—"}</Pill>49          {t?.swap_used_gb ? <Pill tone="warn">Swap {fmtGB(t.swap_used_gb)}</Pill> : null}50        </div>51      </div>5253      {live.alerts.length > 0 && (54        <div className="card border-[#5a4210] bg-[#1d1708] px-4 py-2.5 text-sm text-[#f0b23a]">{live.alerts[live.alerts.length - 1].message}</div>55      )}5657      {/* Current model */}58      <section className="card p-5">59        <div className="flex flex-wrap items-start justify-between gap-4">60          <div className="min-w-0">61            <div className="label mb-1">Current model</div>62            {cur ? (63              <>64                <Link href={`/models/${cur.model_id}`} className="text-lg font-semibold tracking-tight hover:underline truncate block">{cur.model_id}</Link>65                <div className="text-sm text-ink-3 mt-1 flex flex-wrap gap-x-3 gap-y-1 num">66                  <span>{cur.runtime === "mlx" ? "MLX" : "llama.cpp"}</span>67                  <span>context {Math.round(cur.context / 1024)}K</span>68                  <span>memory {fmtGB(cur.measured_gb || cur.estimate_gb)}</span>69                  <span>loaded in {fmtMs(cur.load_ms)}</span>70                  <span>warm TTFT {fmtMs(cur.warm?.ttft_ms as number)}</span>71                  <span>{cur.requests} requests</span>72                  {cur.in_flight > 0 && <span className="text-accent">{cur.in_flight} in flight</span>}73                  {cur.pinned && <span className="text-violet">pinned</span>}74                </div>75              </>76            ) : Object.keys(mgr?.progress || {}).length ? (77              <div className="text-lg font-semibold tracking-tight text-accent pulse">Loading {Object.keys(mgr!.progress)[0]}… <span className="text-sm text-ink-3 font-normal">{Object.values(mgr!.progress)[0].status} · {Object.values(mgr!.progress)[0].elapsed_seconds}s</span></div>78            ) : (79              <div className="text-lg font-semibold tracking-tight text-ink-3">No model loaded <span className="text-sm font-normal">— the next API request loads it on demand</span></div>80            )}81            {loaded.length > 1 && (82              <div className="mt-2 flex flex-wrap gap-1.5">{loaded.filter((w) => w !== cur).map((w) => <Pill key={w.model_id} tone="accent">{w.model_id} · {fmtGB(w.measured_gb || w.estimate_gb)}</Pill>)}</div>83            )}84          </div>85          <div className="flex gap-2">86            <Link href="/playground" className="btn btn-primary">Open playground</Link>87            <Link href="/models" className="btn">Model library</Link>88          </div>89        </div>90        <div className="mt-4">91          <div className="flex justify-between text-xs text-ink-3 mb-1.5 num">92            <span>Model memory budget · {fmtGB(mgr?.resident_gb ?? 0)} resident of {fmtGB(budget, 0)}</span>93            <span>{t ? `${t.mem_used_gb.toFixed(1)} / ${t.mem_total_gb.toFixed(0)} GB system used` : ""}</span>94          </div>95          <Meter value={mgr?.resident_gb ?? 0} max={budget} tone={(mgr?.resident_gb ?? 0) > budget * 0.9 ? "warn" : "accent"} />96        </div>97      </section>9899      {/* Telemetry */}100      <section className="grid grid-cols-2 lg:grid-cols-4 gap-3">101        <StatTile label="RAM used" value={t ? `${t.mem_used_gb.toFixed(1)} GB` : "—"} sub={t ? `${t.mem_available_gb.toFixed(1)} GB available · pressure ${t.mem_pressure_percent}%` : ""}>102          <Sparkline data={memHist} max={t?.mem_total_gb} unit=" GB" />103        </StatTile>104        <StatTile label="GPU" value={t?.gpu_percent != null ? `${t.gpu_percent.toFixed(0)}%` : "—"} sub={t?.gpu_memory_gb != null ? `${t.gpu_memory_gb.toFixed(1)} GB in use by Metal` : "Device utilization"}>105          <Sparkline data={gpuHist} max={100} color="var(--series-3)" unit="%" />106        </StatTile>107        <StatTile label="CPU" value={t ? `${t.cpu_percent.toFixed(0)}%` : "—"} sub={t ? `load ${t.load_avg.map((x) => x.toFixed(1)).join(" / ")}` : ""}>108          <Sparkline data={cpuHist} max={100} color="var(--series-2)" unit="%" />109        </StatTile>110        <StatTile label="Throughput" value={recentTps.length ? `${recentTps[recentTps.length - 1].toFixed(1)} tok/s` : cur && stats?.per_model.find((p) => p.model_id === cur.model_id)?.avg_tps ? `${stats.per_model.find((p) => p.model_id === cur.model_id)!.avg_tps!.toFixed(1)} tok/s` : "—"} sub="last requests, generation">111          <Sparkline data={recentTps} color="var(--series-7)" unit=" tok/s" />112        </StatTile>113      </section>114115      <section className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-3">116        <StatTile label="Requests (24h)" value={fmtNum(stats?.totals.requests ?? 0)} sub={`${fmtNum(stats?.totals.errors ?? 0)} errors`} />117        <StatTile label="Tokens generated (24h)" value={fmtNum(stats?.totals.completion_tokens ?? 0)} sub={`${fmtNum(stats?.all_time.completion_tokens ?? 0)} all time`} />118        <StatTile label="Avg TTFT" value={fmtMs(stats?.totals.avg_ttft_ms)} sub={`avg ${stats?.totals.avg_tps ? stats.totals.avg_tps.toFixed(1) : "—"} tok/s`} />119        <StatTile label="Installed models" value={installed.length} sub={`${installed.filter((m) => m.runtime === "mlx").length} MLX · ${installed.filter((m) => m.runtime === "llamacpp").length} GGUF`} />120        <StatTile label="Disk free" value={t ? `${t.disk_free_gb.toFixed(0)} GB` : "—"} sub={t ? `of ${t.disk_total_gb.toFixed(0)} GB` : ""} />121        <StatTile label="Uptime" value={fmtDuration(t?.app_uptime_seconds ?? 0)} sub={`system ${fmtDuration(t?.uptime_seconds ?? 0)}`} />122      </section>123124      {/* Models + activity */}125      <section className="grid lg:grid-cols-[1.4fr_1fr] gap-4">126        <div className="card">127          <div className="flex items-center justify-between px-4 py-3 border-b border-border">128            <div className="font-medium text-sm">Models</div>129            <Link href="/models" className="text-xs text-accent hover:underline">All models →</Link>130          </div>131          <div className="overflow-x-auto">132            <table className="tbl">133              <thead><tr><th>Model</th><th>Runtime</th><th className="text-right">Est. RAM</th><th className="text-right">Tok/s</th><th>Status</th></tr></thead>134              <tbody>135                {installed.slice().sort((a, b) => Number(b.loaded) - Number(a.loaded) || Number(b.favorite) - Number(a.favorite) || (b.last_used_at || 0) - (a.last_used_at || 0)).slice(0, 8).map((m) => (136                  <tr key={m.id} className="row-link" onClick={() => (location.href = `/models/${m.id}`)}>137                    <td><div className="font-medium truncate max-w-[260px]">{m.favorite && <span className="text-warn mr-1">★</span>}{m.name}</div><div className="text-xs text-ink-3">{m.family} · {m.quantization}</div></td>138                    <td className="text-ink-2">{m.runtime === "mlx" ? "MLX" : "GGUF"}</td>139                    <td className="text-right num">{fmtGB(m.estimated_ram_gb)}</td>140                    <td className="text-right num">{m.avg_tps ? m.avg_tps.toFixed(1) : "—"}</td>141                    <td><StatusPill status={m.status} /></td>142                  </tr>143                ))}144                {!installed.length && <tr><td colSpan={5} className="text-ink-3 text-center py-8">No models yet — <Link className="text-accent" href="/downloads">download one</Link></td></tr>}145              </tbody>146            </table>147          </div>148        </div>149        <div className="card">150          <div className="px-4 py-3 border-b border-border font-medium text-sm">Live activity</div>151          <ul className="divide-y divide-border text-sm max-h-[420px] overflow-auto">152            {live.requests.slice(0, 20).map((r, i) => (153              <li key={i} className="px-4 py-2.5 flex items-center justify-between gap-3">154                <div className="min-w-0">155                  <div className="truncate">{String(r.model_id)}</div>156                  <div className="text-xs text-ink-3">{String(r.endpoint).replace("/v1/", "")}{r.stream ? " · stream" : ""}</div>157                </div>158                <div className="text-right text-xs num shrink-0">159                  <div className={Number(r.status) >= 400 ? "text-bad" : "text-ink-2"}>{Number(r.status) >= 400 ? `error ${r.status}` : `${fmtNum(Number(r.completion_tokens) || 0)} tok · ${r.tps ? Number(r.tps).toFixed(1) : "—"} tok/s`}</div>160                  <div className="text-ink-3">TTFT {fmtMs(Number(r.ttft_ms))} · {fmtMs(Number(r.total_ms))}</div>161                </div>162              </li>163            ))}164            {!live.requests.length && <li className="px-4 py-8 text-center text-ink-3 text-xs">Requests appear here in real time{cur ? "" : ` · last used ${fmtAgo(installed[0]?.last_used_at)}`}</li>}165          </ul>166        </div>167      </section>168    </div>169  );170}171