SPB Git forge

spb/llm-api

Public
0commits 0branches 0releases
0 Bsize
maindefault branch
—last push
4.0 KB · 131 lines tsx
Raw Blame History
1"use client";23import { createContext, useContext, useEffect, useMemo, useRef, useState } from "react";4import type { Job, ManagerState, Telemetry } from "./types";56interface LiveEvent {7  seq: number;8  ts: number;9  type: string;10  data: Record<string, unknown>;11}1213interface LiveState {14  connected: boolean;15  manager: ManagerState | null;16  metrics: Telemetry | null;17  metricsHistory: Telemetry[];18  jobs: Record<string, Job>;19  modelEvents: Record<string, Record<string, unknown>>;20  alerts: { ts: number; level: string; message: string }[];21  requests: Record<string, unknown>[];22  version: number; // bumps on model/manager/settings changes so pages can refetch23}2425const Ctx = createContext<LiveState>({26  connected: false,27  manager: null,28  metrics: null,29  metricsHistory: [],30  jobs: {},31  modelEvents: {},32  alerts: [],33  requests: [],34  version: 0,35});3637export function LiveProvider({ children, enabled }: { children: React.ReactNode; enabled: boolean }) {38  const [state, setState] = useState<LiveState>({39    connected: false, manager: null, metrics: null, metricsHistory: [], jobs: {}, modelEvents: {}, alerts: [], requests: [], version: 0,40  });41  const esRef = useRef<EventSource | null>(null);4243  useEffect(() => {44    if (!enabled) return;45    let stopped = false;46    let retry = 1000;47    const connect = () => {48      if (stopped) return;49      const es = new EventSource("/api/events");50      esRef.current = es;51      es.onopen = () => {52        retry = 1000;53        setState((s) => ({ ...s, connected: true }));54      };55      es.onerror = () => {56        es.close();57        setState((s) => ({ ...s, connected: false }));58        if (!stopped) setTimeout(connect, (retry = Math.min(retry * 1.6, 15000)));59      };60      const handle = (ev: MessageEvent) => {61        let e: LiveEvent;62        try {63          e = JSON.parse(ev.data);64        } catch {65          return;66        }67        setState((s) => {68          const n = { ...s };69          switch (e.type) {70            case "snapshot": {71              const d = e.data as { manager: ManagerState; metrics: Telemetry | null; jobs: Job[] };72              n.manager = d.manager;73              if (d.metrics) n.metrics = d.metrics;74              n.jobs = Object.fromEntries((d.jobs || []).map((j) => [j.id, j]));75              n.version++;76              break;77            }78            case "manager":79              n.manager = e.data as unknown as ManagerState;80              n.version++;81              break;82            case "metrics": {83              const t = e.data as unknown as Telemetry;84              n.metrics = t;85              n.metricsHistory = [...s.metricsHistory.slice(-119), t];86              break;87            }88            case "job": {89              const j = e.data as unknown as Job;90              n.jobs = { ...s.jobs, [j.id]: j };91              if (j.status === "completed" && (j.kind === "download" || j.kind === "scan" || j.kind === "benchmark")) n.version++;92              break;93            }94            case "model":95              n.modelEvents = { ...s.modelEvents, [String(e.data.model_id)]: e.data };96              n.version++;97              break;98            case "models":99            case "settings":100            case "server":101              n.version++;102              break;103            case "alert":104              n.alerts = [...s.alerts.slice(-9), { ts: e.ts, level: String(e.data.level), message: String(e.data.message) }];105              break;106            case "request":107              n.requests = [e.data, ...s.requests.slice(0, 49)];108              break;109          }110          return n;111        });112      };113      for (const t of ["snapshot", "manager", "metrics", "job", "model", "models", "settings", "server", "alert", "request"]) {114        es.addEventListener(t, handle as EventListener);115      }116    };117    connect();118    return () => {119      stopped = true;120      esRef.current?.close();121    };122  }, [enabled]);123124  const value = useMemo(() => state, [state]);125  return <Ctx.Provider value={value}>{children}</Ctx.Provider>;126}127128export function useLive() {129  return useContext(Ctx);130}131