SPB Git

Repository Public

page.tsx 9.6 KB 156 lines tsx

1'use client';2import Link from 'next/link';3import { ArrowRight, Plus } from 'lucide-react';4import { useMissions, useModels, useSpend, useSystem, useWaits } from '@/lib/queries';5import { useUi } from '@/stores/ui';6import { useNow } from '@/hooks/use-viewport';7import { isActive } from '@/lib/status';8import { fmtTime, fmtUsd } from '@/lib/format';9import { Button } from '@/components/ui/button';10import { EmptyState, ErrorState, Skeleton } from '@/components/ui/misc';11import { IllusAllClear, IllusMission } from '@/components/ui/illustrations';12import { WaitCard } from '@/components/waits/wait-card';13import { MissionCard, MissionRow } from '@/components/missions/mission-card';14import { BudgetBar } from '@/components/missions/bits';15import { HealthDot, HealthStrip, healthItems } from '@/components/system/health';1617function SectionTitle({ children, count, aside, tone }: { children: React.ReactNode; count?: number; aside?: React.ReactNode; tone?: 'info' }) {18  return (19    <div className="mb-3 flex items-center gap-2.5">20      <h2 className="text-[13px] font-bold uppercase tracking-[0.07em] text-text-2">{children}</h2>21      {count != null && count > 0 && <span className={tone === 'info' ? 'tnum flex h-5 min-w-5 items-center justify-center rounded-full bg-info-ink px-1.5 text-[11px] font-bold text-info-fg' : 'tnum flex h-5 min-w-5 items-center justify-center rounded-full bg-surface-2 px-1.5 text-[11px] font-semibold text-text-2'}>{count}</span>}22      <span className="flex-1" />23      {aside}24    </div>25  );26}2728export default function HomePage() {29  const waits = useWaits();30  const missions = useMissions();31  const models = useModels().data ?? [];32  const spend = useSpend();33  const system = useSystem();34  const now = useNow(15_000);35  const openNew = useUi((s) => s.openNewMission);36  const active = (missions.data ?? []).filter((m) => isActive(m.status));37  const recent = (missions.data ?? []).filter((m) => !isActive(m.status)).slice(0, 4);38  const pending = waits.data ?? [];39  const s = spend.data;40  const left = s ? s.dailyBudgetUsd - s.todayUsd : null;41  const health = healthItems(system.data, now);42  const problems = health.filter((h) => h.ok === false);43  const unknown = health.filter((h) => h.ok == null).length;4445  return (46    <div className="h-full overflow-y-auto" data-testid="home">47      <div className="mx-auto grid w-full max-w-6xl gap-x-8 gap-y-7 px-4 pb-32 pt-2 md:px-8 md:pb-12 md:pt-7 lg:grid-cols-[minmax(0,1fr)_320px]">48        <div className="flex min-w-0 flex-col gap-8">49          <section aria-label="Needs you" data-testid="needs-you">50            <SectionTitle count={pending.length} tone="info" aside={pending.length > 0 ? <Link href="/approvals/" className="inline-flex items-center gap-1 text-xs font-medium text-accent hover:underline">Open inbox <ArrowRight className="size-3" aria-hidden /></Link> : null}>51              Needs you52            </SectionTitle>53            {waits.isError ? (54              <div className="card"><ErrorState error={waits.error} onRetry={() => waits.refetch()} /></div>55            ) : waits.isPending ? (56              <Skeleton className="h-36 rounded-md" />57            ) : pending.length === 0 ? (58              <div className="card flex items-center gap-4 px-5 py-4" data-testid="needs-you-empty">59                <div className="shrink-0 scale-[.8]"><IllusAllClear /></div>60                <div className="min-w-0">61                  <p className="text-[15px] font-semibold tracking-tight">Nothing needs you</p>62                  <p className="mt-0.5 text-[13px] leading-relaxed text-text-2">Approvals and questions from running agents appear here the moment they are raised.</p>63                </div>64              </div>65            ) : (66              <div className="flex flex-col gap-3">67                {pending.map((w) => <WaitCard key={w.id} wait={w} compact />)}68              </div>69            )}70          </section>7172          {/* phones and tablets: spend and health as two compact tiles right after what needs the owner */}73          <div className="-my-2 grid grid-cols-2 gap-3 lg:hidden" data-testid="home-strip">74            <Link href="/spend/" className="card pressable px-3.5 py-2.5">75              <p className="text-[11px] font-semibold uppercase tracking-wide text-muted">Today</p>76              <p className="tnum mt-0.5 truncate text-[17px] font-bold tracking-tight">{fmtUsd(s?.todayUsd)} <span className="text-xs font-normal text-muted">/ {fmtUsd(s?.dailyBudgetUsd, { compact: true })}</span></p>77              <BudgetBar spent={s?.todayUsd} budget={s?.dailyBudgetUsd} className="mt-1.5" label="Daily cap used" />78            </Link>79            <Link href="/system/" className="card pressable px-3.5 py-2.5">80              <p className="text-[11px] font-semibold uppercase tracking-wide text-muted">Health</p>81              <p className="mt-0.5 truncate text-[15px] font-semibold tracking-tight">{problems.length > 0 ? `${problems.length} problem${problems.length === 1 ? '' : 's'}` : unknown === health.length ? 'Unknown' : unknown > 0 ? 'Partly known' : 'All systems OK'}</p>82              <p className="mt-1.5 flex items-center gap-1.5" aria-hidden>83                {health.map((h) => <HealthDot key={h.key} ok={h.ok} />)}84                <span className="truncate text-[11px] text-muted">{problems[0]?.label ?? 'worker · db · keys'}</span>85              </p>86            </Link>87          </div>88          <section aria-label="Running missions" data-testid="running">89            <SectionTitle count={active.length} aside={<Button size="sm" variant="brand" className="h-8 rounded-full px-3.5 font-semibold md:hidden" onClick={() => openNew()} data-testid="new-mission-page"><Plus className="size-3.5" strokeWidth={2.6} /> New mission</Button>}>90              Active missions91            </SectionTitle>92            {missions.isError ? (93              <div className="card"><ErrorState error={missions.error} onRetry={() => missions.refetch()} /></div>94            ) : missions.isPending ? (95              <div className="grid gap-3 md:grid-cols-2"><Skeleton className="h-36 rounded-md" /><Skeleton className="h-36 rounded-md" /></div>96            ) : active.length === 0 ? (97              <div className="card">98                <EmptyState99                  illustration={<IllusMission />}100                  title="No mission is running"101                  description="Give an agent an objective, a budget and a project. It works in a sandbox and comes back to you when it needs a decision."102                  action={<Button variant="brand" className="h-9 rounded-full px-5 font-semibold pointer-coarse:h-12" onClick={() => openNew()} data-testid="empty-new-mission"><Plus className="size-4" strokeWidth={2.6} /> New mission</Button>}103                  className="py-9"104                />105              </div>106            ) : (107              <div className="grid gap-3 md:grid-cols-2">108                {active.map((m) => <MissionCard key={m.id} mission={m} models={models} />)}109              </div>110            )}111          </section>112113          {recent.length > 0 && (114            <section aria-label="Recently finished">115              <SectionTitle aside={<Link href="/missions/" className="inline-flex items-center gap-1 text-xs font-medium text-accent hover:underline">All missions <ArrowRight className="size-3" aria-hidden /></Link>}>Recently finished</SectionTitle>116              <div className="card divide-y divide-line overflow-hidden">117                {recent.map((m) => <MissionRow key={m.id} mission={m} models={models} />)}118              </div>119            </section>120          )}121        </div>122123        <aside className="hidden min-w-0 flex-col gap-7 lg:flex">124          <section aria-label="Spend today" data-testid="spend-today">125            <SectionTitle aside={<Link href="/spend/" className="inline-flex items-center gap-1 text-xs font-medium text-accent hover:underline">Details <ArrowRight className="size-3" aria-hidden /></Link>}>Today</SectionTitle>126            <div className="card p-4">127              {spend.isError ? (128                <ErrorState error={spend.error} onRetry={() => spend.refetch()} className="py-4" />129              ) : (130                <>131                  <p className="tnum flex items-baseline gap-2">132                    <span className="text-[30px] font-bold leading-none tracking-tight" data-testid="today-usd">{fmtUsd(s?.todayUsd)}</span>133                    <span className="text-[13px] text-muted">of {fmtUsd(s?.dailyBudgetUsd, { compact: true })} daily cap</span>134                  </p>135                  <BudgetBar spent={s?.todayUsd} budget={s?.dailyBudgetUsd} className="mt-3 h-2" label="Daily cap used" />136                  <p className="tnum mt-2 text-xs text-muted">137                    {left == null ? '—' : left > 0 ? `${fmtUsd(left)} left before new model calls stop for the day` : 'Cap reached — model calls are stopped until tomorrow'}138                  </p>139                </>140              )}141            </div>142          </section>143144          <section aria-label="System health" data-testid="health">145            <SectionTitle aside={<Link href="/system/" className="inline-flex items-center gap-1 text-xs font-medium text-accent hover:underline">System <ArrowRight className="size-3" aria-hidden /></Link>}>Health</SectionTitle>146            <div className="card p-3">147              {system.isError ? <ErrorState error={system.error} onRetry={() => system.refetch()} className="py-4" /> : <HealthStrip items={health} />}148              <p className="tnum mt-2 px-1 text-[11px] text-muted">{system.data?.collectedAt ? `as of ${fmtTime(system.data.collectedAt)}` : system.isPending ? 'checking…' : 'collection time unknown'}</p>149            </div>150          </section>151        </aside>152      </div>153    </div>154  );155}156
Files main
Menu

Theme