Ctrl K
ComunidadGuías…

sections-a.jsx

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
// AI-MONITOR · Page sections.
// Hero → Cluster → Nodes → Models → Telemetry → Footer

const { NODES, MODELS, live, totals, subscribe } = window.AIData;

// Tiny helper: subscribe to live tick and re-render
function useLive() {
  const [, force] = useState(0);
  useEffect(() => subscribe(() => force((n) => n + 1)), []);
}

function fmtBytes(gb) {
  if (gb >= 1024) return `${(gb / 1024).toFixed(1)} TB`;
  return `${Math.round(gb)} GB`;
}
function fmtDuration(sec) {
  const d = Math.floor(sec / 86400);
  const h = Math.floor((sec % 86400) / 3600);
  const m = Math.floor((sec % 3600) / 60);
  return `${d}d ${String(h).padStart(2,"0")}:${String(m).padStart(2,"0")}`;
}
function fmtNumber(n) {
  if (n >= 1e6) return (n / 1e6).toFixed(2) + "M";
  if (n >= 1e3) return (n / 1e3).toFixed(1) + "K";
  return Math.round(n).toString();
}

// ── NAV ─────────────────────────────────────────────────────────────────
function Nav({ onOpenCmd }) {
  const [clock, setClock] = useState(() => new Date());
  const [menuOpen, setMenuOpen] = useState(false);
  const [, force] = useState(0);
  useEffect(() => {
    const id = setInterval(() => setClock(new Date()), 1000);
    return () => clearInterval(id);
  }, []);
  useEffect(() => subscribe(() => force((n) => n + 1)), []);
  // 时区: 默认浏览器本地; hearth.yaml display.timezone 声明则覆盖
  // (helper 在 data.js, 静态 IANA 时区由 /api/config 一次性加载)
  const time = window.AIData.formatTime(clock);
  const date = window.AIData.formatDate(clock);
  const mode = window.AIData.mode;
  const status = window.AIData.status;
  const isLive = status === "live";
  const isReconnect = status === "reconnecting" || status === "connecting";
  const { t } = useLang();
  const close = () => setMenuOpen(false);
  // 真实在线节点计数(后端权威 up)——不再写死 "5/5"
  const _nids = Object.keys(live.nodes);
  const _total = _nids.length;
  const _online = _nids.filter((id) => live.nodes[id].up !== false).length;
  const _degraded = _online < _total;
  return (
    <header className="nav">
      <div className="nav-logo">
        <div className="nav-logo-mark"></div>
        <span>{t("Hearth")}</span>
      </div>
      <nav className={"nav-links" + (menuOpen ? " open" : "")}>
        <a href="#overview" data-active onClick={close}>{t("Overview")}</a>
        <a href="#cluster" onClick={close}>{t("Cluster")}</a>
        <a href="#nodes" onClick={close}>{t("Nodes")}</a>
        <a href="#models" onClick={close}>{t("Models")}</a>
        <a href="#training" onClick={close}>{t("Training")}</a>
        <a href="#telemetry" onClick={close}>{t("Telemetry")}</a>
        <a href="#fabric" onClick={close}>{t("Fabric")}</a>
      </nav>
      <div className="nav-right">
        <span className={"dot" + (isReconnect || (isLive && _degraded) ? " warn" : "")} />
        <span className="nav-status-text">
          {isLive ? `${_online}/${_total} ${t("nodes online")}`
            : isReconnect ? t("reconnecting…")
            : mode === "mock" ? t("mock data · no backend")
            : `${_online}/${_total} ${t("nodes online")}`}
        </span>
        <span className="nav-sep" />
        <span className="nav-mode-badge" title={isLive ? "Streaming /api/stream" : "Local simulator"}
              style={{
                fontFamily: "var(--mono)", fontSize: 9.5, letterSpacing: ".14em",
                padding: "2px 7px", borderRadius: 4,
                background: isLive ? "rgba(48,209,88,.12)" : "rgba(255,214,10,.10)",
                color: isLive ? "var(--ok)" : "var(--warn)",
                border: "0.5px solid " + (isLive ? "rgba(48,209,88,.3)" : "rgba(255,214,10,.3)"),
              }}>
          {isLive ? "LIVE" : "MOCK"}
        </span>
        <span className="nav-sep" />
        <span className="nav-clock">{date} · {time}</span>
        <span className="nav-sep" />
        <LangSwitcher />
        <span className="nav-sep" />
        <button className="nav-search" onClick={onOpenCmd}
          style={{
            display: "inline-flex", alignItems: "center", gap: 8,
            background: "rgba(255,255,255,.04)",
            border: "0.5px solid var(--line)",
            color: "var(--ink-2)",
            font: "500 11px var(--mono)",
            padding: "5px 10px 5px 10px",
            borderRadius: 7,
            cursor: "pointer",
          }}>
          {t("Search")} <span className="kbd">⌘K</span>
        </button>
        {/* 手机端汉堡菜单按钮: 桌面 display:none, 手机 ≤640 显示 */}
        <button className="nav-burger" aria-label="menu" aria-expanded={menuOpen}
          onClick={() => setMenuOpen((v) => !v)}>
          <span /><span /><span />
        </button>
      </div>
    </header>
  );
}

// ── HERO ────────────────────────────────────────────────────────────────
function Hero() {
  useLive();
  const { t } = useLang();
  const cluster = live.cluster;

  // For the headline ticker: rolling running tokens count
  return (
    <section className="hero" id="overview">
      <div className="hero-eyebrow reveal in">
        <span className="live"><span className="dot" />LIVE · 1.2 s</span>
        <span>{t("Home AI Compute Center")}</span>
      </div>
      <h1 className="hero-title reveal in">
        {t("Home")}<br />
        <em>{t("Compute Center")}</em>
      </h1>
      <p className="hero-sub reveal in">
        {t("A unified telemetry surface for your home AI compute cluster — auto-discovers models, surfaces real metrics from vLLM / llama.cpp / LiteLLM, honestly labels what backends don't expose. Every token, every watt — in real time.")}
      </p>
      <div className="hero-meta reveal in">
        <span>{t("UPTIME")} <b>{fmtDuration(cluster.uptimeSec)}</b></span>
        <span>{t("REQUESTS")} <b>{fmtNumber(cluster.reqTotal)}</b></span>
        <span>{t("TOKENS SERVED")} <b>{fmtNumber(cluster.tokTotal)}</b></span>
        <span>{t("SCRAPE")} <b>15 s · Prometheus 2.55</b></span>
      </div>

      <div className="stat-row">
        <HeroStat
          label="AI Compute · FP4"
          value={totals.pflopsFp4}
          decimals={2}
          unit="PFLOPS"
          spark={cluster.tps}
          color="var(--accent)"
          sub={<>{Math.round(totals.totalFp16)} <small>TFLOPS BF16</small></>}
        />
        <HeroStat
          label="Unified Memory"
          value={totals.totalVram}
          unit="GB"
          spark={cluster.kv}
          color="var(--violet)"
          sub={(() => {
            // 按 hearth.yaml `kind` 字段动态分类: discrete = 消费级独显, 其他 = 数据中心 SoC (Spark/Apple Silicon 等)
            const discrete = NODES.filter((n) => (n.kind || "discrete") === "discrete").length;
            const datacenter = NODES.length - discrete;
            const parts = [];
            if (discrete > 0) parts.push(`${discrete}× consumer`);
            if (datacenter > 0) parts.push(`${datacenter}× datacenter`);
            return <>{NODES.length} GPUs{parts.length ? " · " + parts.join(" + ") : ""}</>;
          })()}
        />
        <HeroStat
          label="CPU Cores"
          value={totals.totalCores}
          unit="cores"
          spark={[]}
          color="var(--teal)"
          fake={true}
          sub={<>{totals.totalThreads} threads · 1× x86 + 4× ARM</>}
        />
        <HeroStat
          label="System RAM"
          value={totals.totalRam}
          unit="GB"
          spark={[]}
          color="var(--ok)"
          fake={true}
          sub={<>DDR5 + LPDDR5X unified</>}
        />
        <HeroStat
          label="Storage"
          value={totals.totalDisk / 1024}
          unit="TB"
          decimals={0}
          spark={[]}
          color="var(--hot)"
          fake={true}
          sub={<>5× 4 TB NVMe Gen4 · 38% used</>}
        />
      </div>

      <div style={{ marginTop: 24, display: "flex", justifyContent: "space-between", alignItems: "flex-end", flexWrap: "wrap", gap: 16 }}>
        <div className="hero-meta" style={{ marginTop: 0 }}>
          <span><span className="dot" /> Live throughput</span>
          <span>TOKENS/SEC <b className="num">{cluster.tpsNow.toFixed(0)}</b></span>
          <span>REQUESTS/SEC <b className="num">{cluster.rpsNow.toFixed(2)}</b></span>
          <span>KV-CACHE <b className="num">{cluster.kvNow.toFixed(0)}%</b></span>
          <span>CLUSTER POWER <b className="num">{(cluster.powNow / 1000).toFixed(2)} kW</b></span>
          <span>HOTTEST GPU <b className="num">{cluster.tempNow.toFixed(0)}°C</b></span>
        </div>
      </div>
    </section>
  );
}

function HeroStat({ label, value, decimals = 0, unit, spark, color, sub, fake }) {
  const [ref, v] = useCountUp(value, 1400);
  return (
    <div className="stat">
      <div className="stat-label">{label}</div>
      <div ref={ref} className="stat-value">
        {v.toFixed(decimals)} <span className="stat-unit">{unit}</span>
      </div>
      <div className="stat-sub">{sub}</div>
      <div className="stat-spark">
        {fake ? <FakeSpark color={color} /> : <Sparkline data={spark} color={color} />}
      </div>
    </div>
  );
}
// Static "system" sparkline for fields that don't have time-series (cores/ram/disk)
function FakeSpark({ color }) {
  const [ref, { w }] = useElementSize();
  const H = 28;
  const data = useMemo(() => Array.from({ length: 28 }, (_, i) => 50 + Math.sin(i * 0.7) * 6 + Math.cos(i * 0.3) * 4), []);
  if (!w) return <div ref={ref} style={{ width: "100%", height: H }} />;
  return <div ref={ref}><Sparkline data={data} color={color} /></div>;
}

// ── CLUSTER ────────────────────────────────────────────────────────────
function Cluster() {
  useLive();
  const { t } = useLang();
  const [hmMetric, setHmMetric] = useState("GPU");
  const hmKey = hmMetric === "VRAM" ? "vram" : "gpu";
  // Build aggregate utilization series and per-resource breakdown
  // GPU utilization aggregate
  const tps = live.cluster.tps;
  const pow = live.cluster.pow;
  const temp = live.cluster.temp;
  const kv = live.cluster.kv;

  // Resource pool composition (VRAM)
  const usedVram = NODES.reduce((a, n) => a + n.gpu.mem * (live.nodes[n.id].vram.now / 100), 0);
  const freeVram = totals.totalVram - usedVram;

  // Build heatmap rows
  const hmRows = NODES.map((n) => ({
    label: n.name,
    data: live.nodes[n.id][hmKey].hist,
  }));

  return (
    <section className="page reveal" id="cluster">
      <div className="eyebrow"><span className="num">02</span>{t("Cluster · live telemetry")}</div>
      <h2>{t("Real-time pulse of the ")}<em>{t("entire fabric.")}</em></h2>
      <p className="lede">
        {t("Aggregated utilization, throughput, and thermals across all {n} nodes — sampled at 1.2 s, retained for 7 days in Prometheus, surfaced here as a single coherent picture.", { n: NODES.length })}
      </p>

      <div className="grid g-3" style={{ marginBottom: 16 }}>
        <PulseCard title={t("Token throughput")} sub={t("tokens / sec · all models")} data={tps} color="var(--accent)" unit=" t/s" max={Math.max(...tps) * 1.3 || 1000} />
        <PulseCard title={t("Cluster power draw")} sub={t("watts · all PSUs")} data={pow} color="var(--hot)" unit=" W" max={Math.max(...pow) * 1.3 || 2000} />
        <PulseCard title={t("KV-cache pressure")} sub={t("% utilization, weighted mean")} data={kv} color="var(--violet)" unit="%" max={100} />
      </div>

      <div className="grid" style={{ gridTemplateColumns: "1.4fr 1fr", gap: "var(--gap)" }}>
        <div className="card">
          <div className="card-head">
            <div>
              <div className="card-title">{t("Per-node")} {hmMetric} · {t("heatmap")}</div>
              <div className="card-sub">{hmMetric === "VRAM" ? t("VRAM usage · last 60 ticks · 0–100%") : t("Accelerator activity · last 60 ticks · 0–100%")}</div>
            </div>
            <div className="btn-seg" role="tablist">
              {["GPU","VRAM"].map((k) => (
                <button key={k} data-on={hmMetric === k ? "1" : "0"} onClick={() => setHmMetric(k)}>{k}</button>
              ))}
            </div>
          </div>
          <div className="card-body">
            <Heatmap rows={hmRows} columns={30} />
            <div style={{ display: "flex", justifyContent: "space-between", marginTop: 16, fontFamily: "var(--mono)", fontSize: 10.5, color: "var(--ink-3)" }}>
              <span>60 ticks ago</span>
              <span style={{ display: "flex", gap: 12, alignItems: "center" }}>
                <Swatch hue={145} />0–35
                <Swatch hue={210} />35–65
                <Swatch hue={28}  />65–85
                <Swatch hue={5}   />85+ critical
              </span>
              <span>now</span>
            </div>
          </div>
        </div>

        <div className="card">
          <div className="card-head">
            <div>
              <div className="card-title">{t("Resource pools")}</div>
              <div className="card-sub">{t("Aggregate capacity vs. live usage")}</div>
            </div>
          </div>
          <div className="card-body" style={{ display: "flex", flexDirection: "column", gap: 18 }}>
            <PoolBar label="GPU Compute (FP4)" used={live.cluster.tpsNow * 0.001 * totals.pflopsFp4} cap={totals.pflopsFp4} unit=" PFLOPS" segs={[
              { label: "qwen3", value: 0.30 * live.cluster.tpsNow * 0.001 * totals.pflopsFp4, color: "var(--accent)" },
              { label: "deepseek", value: 0.24 * live.cluster.tpsNow * 0.001 * totals.pflopsFp4, color: "var(--violet)" },
              { label: "gpt-oss", value: 0.16 * live.cluster.tpsNow * 0.001 * totals.pflopsFp4, color: "var(--teal)" },
              { label: "other", value: 0.12 * live.cluster.tpsNow * 0.001 * totals.pflopsFp4, color: "var(--pink)" },
              { label: "free", value: totals.pflopsFp4 - 0.82 * live.cluster.tpsNow * 0.001 * totals.pflopsFp4, color: "rgba(255,255,255,.08)" },
            ]} />
            <PoolBar label="VRAM / Unified" used={usedVram} cap={totals.totalVram} unit=" GB" segs={[
              ...MODELS.filter((m) => m.state === "serving").map((m, i) => ({
                label: m.id, value: m.vram, color: ["var(--accent)","var(--violet)","var(--teal)","var(--hot)","var(--ok)","var(--pink)","#5ac8fa","#ffd60a"][i % 8],
              })),
              { label: "free", value: Math.max(0, totals.totalVram - MODELS.filter(m=>m.state==="serving").reduce((a,m)=>a+m.vram,0)), color: "rgba(255,255,255,.08)" },
            ]} />
            <PoolBar label="System RAM" used={totals.totalRam * 0.42} cap={totals.totalRam} unit=" GB" segs={[
              { label: "kernel", value: totals.totalRam * 0.06, color: "var(--ink-4)" },
              { label: "containers", value: totals.totalRam * 0.21, color: "var(--accent)" },
              { label: "buffers", value: totals.totalRam * 0.08, color: "var(--teal)" },
              { label: "page-cache", value: totals.totalRam * 0.07, color: "var(--violet)" },
              { label: "free", value: totals.totalRam * 0.58, color: "rgba(255,255,255,.08)" },
            ]} />
            <PoolBar label="Storage" used={totals.totalDisk * 0.38} cap={totals.totalDisk} unit=" GB" segs={[
              { label: "models", value: totals.totalDisk * 0.22, color: "var(--violet)" },
              { label: "datasets", value: totals.totalDisk * 0.09, color: "var(--accent)" },
              { label: "logs/traces", value: totals.totalDisk * 0.04, color: "var(--teal)" },
              { label: "system", value: totals.totalDisk * 0.03, color: "var(--ink-4)" },
              { label: "free", value: totals.totalDisk * 0.62, color: "rgba(255,255,255,.08)" },
            ]} />
          </div>
        </div>
      </div>

      <div className="grid g-3" style={{ marginTop: 16 }}>
        <SmallStat label="Peak GPU temp"     value={cluster_peak()} unit="°C" sub="hot node under load" color="var(--hot)" />
        <SmallStat label="Avg power / token" value={(live.cluster.powNow / Math.max(1, live.cluster.tpsNow)).toFixed(2)} unit=" J/tok" sub="lower is better · target ≤ 1.4" color="var(--accent)" />
        <SmallStat label="P95 latency (chat)" value={Math.round(weightedP95())} unit=" ms" sub="weighted across serving models" color="var(--violet)" />
      </div>
    </section>
  );
}

function cluster_peak() { return Math.round(Math.max(...NODES.map((n) => live.nodes[n.id].tempGpu.now))); }
function weightedP95() {
  const serving = MODELS.filter((m) => m.state === "serving" && m.kind === "chat");
  const totalRps = serving.reduce((a, m) => a + live.models[m.id].rps.now, 0) || 1;
  return serving.reduce((a, m) => a + m.p95 * live.models[m.id].rps.now / totalRps, 0);
}

function Swatch({ hue }) {
  return <span style={{ display: "inline-block", width: 12, height: 8, borderRadius: 2, background: `oklch(0.62 0.18 ${hue})` }} />;
}

function PulseCard({ title, sub, data, color, unit, max }) {
  const now = data[data.length - 1] || 0;
  return (
    <div className="card">
      <div className="card-head">
        <div>
          <div className="card-title">{title}</div>
          <div className="card-sub">{sub}</div>
        </div>
        <div className="num" style={{ fontSize: 22, fontFamily: "var(--display)", fontWeight: 500, letterSpacing: "-.02em" }}>
          {now.toFixed(unit === "%" ? 0 : 1)}<small style={{ fontSize: 11, color: "var(--ink-3)", fontWeight: 400, fontFamily: "var(--mono)", marginLeft: 3 }}>{unit}</small>
        </div>
      </div>
      <div className="card-body" style={{ padding: "10px 6px 6px" }}>
        <AreaChart series={[data]} colors={[color]} height={140} yMax={max} unit={unit} padding={{l:36,r:14,t:10,b:18}} ticks={3} />
      </div>
    </div>
  );
}

function PoolBar({ label, used, cap, unit, segs }) {
  const pct = (used / cap) * 100;
  return (
    <div>
      <div style={{ display: "flex", justifyContent: "space-between", marginBottom: 6, alignItems: "baseline" }}>
        <span className="metric-l">{label}</span>
        <span className="num" style={{ fontSize: 12, color: "var(--ink-2)" }}>
          <b style={{ color: "var(--ink)", fontWeight: 500 }}>{used.toFixed(used < 10 ? 2 : 0)}</b>
          <span style={{ color: "var(--ink-4)" }}> / {cap.toFixed(0)}{unit}</span>
          <span style={{ marginLeft: 8, color: "var(--ink-3)" }}>{pct.toFixed(0)}%</span>
        </span>
      </div>
      <StackedBar segments={segs} height={8} total={cap} />
      <div style={{ display: "flex", flexWrap: "wrap", gap: 10, marginTop: 8, fontFamily: "var(--mono)", fontSize: 10, color: "var(--ink-3)" }}>
        {segs.filter((s) => s.label !== "free").map((s, i) => (
          <span key={i} style={{ display: "inline-flex", alignItems: "center", gap: 5 }}>
            <i style={{ width: 8, height: 8, borderRadius: 2, background: s.color }} />
            {s.label} <b style={{ color: "var(--ink-2)", fontWeight: 500 }}>{s.value.toFixed(s.value < 10 ? 1 : 0)}</b>
          </span>
        ))}
      </div>
    </div>
  );
}

function SmallStat({ label, value, unit, sub, color }) {
  return (
    <div className="card" style={{ padding: 18 }}>
      <div className="metric-l" style={{ marginBottom: 12 }}>{label}</div>
      <div className="num" style={{ font: "500 28px/1 var(--display)", letterSpacing: "-.022em", color: "var(--ink)" }}>
        {value}<span style={{ fontSize: 13, color: "var(--ink-3)", marginLeft: 3, fontFamily: "var(--mono)" }}>{unit}</span>
      </div>
      <div style={{ marginTop: 10, fontFamily: "var(--mono)", fontSize: 10.5, color: "var(--ink-3)" }}>{sub}</div>
    </div>
  );
}

Object.assign(window, { Nav, Hero, Cluster, useLive, fmtBytes, fmtDuration, fmtNumber });