SPB Git

Repository Public

models.tsx 3.1 KB 59 lines tsx

1'use client';2import { useState } from 'react';3import { useQueryClient } from '@tanstack/react-query';4import type { LocalModel } from '@cr/shared';5import { api, errorMessage } from '@/lib/api';6import { qk, useModels } from '@/lib/queries';7import { fmtDateTime, fmtNum } from '@/lib/format';8import { toast } from '@/stores/toasts';9import { Button } from '@/components/ui/button';10import { StatusDot } from '@/components/ui/badge';11import { EmptyState, ErrorState, Section, Spinner } from '@/components/ui/misc';12import { useBrand } from '@/stores/branding';1314export function LocalModels() {15  const brand = useBrand();16  const q = useModels();17  const qc = useQueryClient();18  const [testing, setTesting] = useState<string | null>(null);19  const test = async (m: LocalModel) => {20    setTesting(m.id);21    try {22      // ModelTest (server): a real small completion; null latency/throughput = not measurable23      const r = await api.post<{ ok: boolean; checkedAt: string; latencyMs: number | null; tokensPerSec: number | null; error: string | null }>(`/cluster/models/${encodeURIComponent(m.id)}/test`);24      qc.setQueryData<LocalModel[]>(qk.models, (l) => l?.map((x) => (x.id === m.id ? { ...x, health: r.ok ? 'ONLINE' : 'OFFLINE', checkedAt: r.checkedAt, latencyMs: r.latencyMs, tokensPerSec: r.tokensPerSec } : x)));25      if (!r.ok) toast.warning('Model test failed', r.error ?? 'The endpoint did not answer.');26    } catch (e) {27      toast.error('Model test failed', errorMessage(e));28    } finally {29      setTesting(null);30    }31  };32  return (33    <Section title="Local models" aside={`Local model inference is separate from the ${brand.agentName} runtime`}>34      {q.isPending ? <div className="flex justify-center p-6"><Spinner /></div>35      : q.isError ? <ErrorState error={q.error} onRetry={() => q.refetch()} />36      : q.data.length === 0 ? <EmptyState title="No local model endpoints configured" className="py-8" />37      : (38        <ul className="divide-y divide-line" data-testid="models">39          {q.data.map((m) => (40            <li key={m.id} className="flex flex-wrap items-center gap-x-5 gap-y-1.5 px-4 py-2.5 text-xs">41              <StatusDot health={m.health} withLabel />42              <div className="min-w-0 flex-1 basis-40">43                <p className="truncate text-[13px] font-medium text-text">{m.name}</p>44                <p className="truncate text-[11px] text-muted">{m.host}{m.engine ? ` · ${m.engine}` : ''} · {m.endpoint}</p>45              </div>46              <dl className="tnum flex gap-5">47                <div><dt className="text-[11px] text-muted">Latency</dt><dd>{m.latencyMs == null ? 'not measured' : `${fmtNum(m.latencyMs, 0)} ms`}</dd></div>48                <div><dt className="text-[11px] text-muted">Throughput</dt><dd>{m.tokensPerSec == null ? 'not measured' : `${fmtNum(m.tokensPerSec, 1)} tok/s`}</dd></div>49                <div><dt className="text-[11px] text-muted">Checked</dt><dd>{m.checkedAt ? fmtDateTime(m.checkedAt) : 'never'}</dd></div>50              </dl>51              <Button size="sm" loading={testing === m.id} onClick={() => test(m)}>Run test</Button>52            </li>53          ))}54        </ul>55      )}56    </Section>57  );58}59
Files main
Menu

Theme