Skip to contentVibraUI

Status codes

How every request in the window ended — OK, rate limited or server error — as one bar with its counts and shares; reads statusMix().

Preview

Install

npx shadcn@latest add @vibra/widget-ai-overview-status-mix

Needs the @vibra registry in your components.json — set it up once.

Source

app/ai/components/status-mix.tsx
"use client"

import { formatNumber } from "@/lib/format"
import { ChartCard } from "@/components/ui/chart-card"
import { chartTone } from "@/components/ui/chart-core"
import { PercentageBar } from "@/components/ui/percentage-bar"

import { type StatusRow } from "../data"

// A status code is a meaning, not a category, so it resolves to the semantic
// tokens rather than to a palette slot: --chart-positive for the requests that
// worked, the warning tone for throttling, --chart-negative for failures. The
// words are in the legend, so the colour only ever reinforces them.
const STATUS_COLORS: Record<number, string> = {
  200: chartTone("positive"),
  429: chartTone("warning"),
  500: chartTone("negative"),
}

/** How the window's requests ended, as one bar and its legend. */
export function StatusMix({ rows }: { rows: StatusRow[] }) {
  return (
    <ChartCard data-widget="widget-ai-overview-status-mix" title="Status codes" description="Every request in the window." height={96}>
      <div className="flex flex-col justify-center gap-4 py-2">
        <PercentageBar
          segments={rows.map((row) => ({
            label: `${row.status} ${row.label}`,
            value: row.count,
            color: STATUS_COLORS[row.status],
          }))}
          format={(value, percent) => `${formatNumber(value, { maximumFractionDigits: 0 })} (${percent}%)`}
        />
      </div>
    </ChartCard>
  )
}
app/ai/data.ts
/**
 * What this page reads. Everything on it is `db.aiRequests`, aggregated:
 * spend is the sum of `costCents`, tokens are the two token columns, the
 * latency percentiles are read off the sorted `latencyMs` of the window, and
 * the provider and status splits are counts. The window ends at
 * `REFERENCE_DATE`, never at a clock, so the same 30 days are summarised every
 * time the page renders.
 */
import { formatCompact, formatCurrency, formatDuration, formatNumber, getInitials } from "@/lib/format"
import { db, REFERENCE_DATE, type AiRequest, type Member } from "@/lib/sample-data"

import { dayLabel } from "./vocabulary"

const DAY_MS = 86_400_000
const WINDOW_DAYS = 30

/** The window every number on this page is measured over: 30 days to "now". */
export const WINDOW_FROM = new Date(REFERENCE_DATE.getTime() - WINDOW_DAYS * DAY_MS)

/** The window the page covers, stated in the zone its days are counted in. */
export const WINDOW_LABEL = `${dayLabel.format(WINDOW_FROM)} – ${dayLabel.format(REFERENCE_DATE)} UTC`

const REQUESTS: AiRequest[] = db.aiRequests
  .all()
  .filter((request) => request.at >= WINDOW_FROM && request.at <= REFERENCE_DATE)
  .sort((a, b) => b.at.getTime() - a.at.getTime())

/**
 * The `p`th percentile of `values` by nearest rank — 0.5 is the median. The
 * caller sorts once and asks twice, so this takes an already-sorted list.
 */
function percentile(sorted: number[], p: number): number {
  if (sorted.length === 0) return 0
  const rank = Math.ceil(p * sorted.length)
  return sorted[Math.min(sorted.length - 1, Math.max(0, rank - 1))]
}

const LATENCIES = REQUESTS.map((request) => request.latencyMs).sort((a, b) => a - b)

/** The four headline numbers, already formatted. */
export function headline() {
  const spendCents = REQUESTS.reduce((total, request) => total + request.costCents, 0)
  const promptTokens = REQUESTS.reduce((total, request) => total + request.promptTokens, 0)
  const completionTokens = REQUESTS.reduce((total, request) => total + request.completionTokens, 0)
  const failed = REQUESTS.filter((request) => request.status !== 200).length

  return {
    spend: formatCurrency(spendCents / 100, "USD", { maximumFractionDigits: 0 }),
    requests: formatNumber(REQUESTS.length, { maximumFractionDigits: 0 }),
    failed: formatNumber(failed, { maximumFractionDigits: 0 }),
    tokens: formatCompact(promptTokens + completionTokens),
    tokensIn: formatCompact(promptTokens),
    tokensOut: formatCompact(completionTokens),
    p50: formatDuration(percentile(LATENCIES, 0.5)),
    p95: formatDuration(percentile(LATENCIES, 0.95)),
  }
}

export type LatencyPoint = { date: string; p50: number; p95: number }

/** P50 and P95 for every day in the window, oldest first. */
export function latencyByDay(): LatencyPoint[] {
  const buckets = new Map<string, number[]>()
  for (const request of REQUESTS) {
    const key = request.at.toISOString().slice(0, 10)
    const bucket = buckets.get(key)
    if (bucket) bucket.push(request.latencyMs)
    else buckets.set(key, [request.latencyMs])
  }

  return [...buckets.entries()]
    .sort(([a], [b]) => a.localeCompare(b))
    .map(([date, values]) => {
      const sorted = [...values].sort((a, b) => a - b)
      return { date, p50: percentile(sorted, 0.5), p95: percentile(sorted, 0.95) }
    })
}

export type ProviderRow = { provider: string; spend: number; requests: number }

/** What each provider cost, in whole currency units, biggest bill first. */
export function providerMix(): ProviderRow[] {
  const totals = new Map<string, { spend: number; requests: number }>()
  for (const request of REQUESTS) {
    const row = totals.get(request.provider) ?? { spend: 0, requests: 0 }
    row.spend += request.costCents
    row.requests += 1
    totals.set(request.provider, row)
  }

  return [...totals.entries()]
    .map(([provider, row]) => ({
      provider,
      spend: Math.round(row.spend / 100),
      requests: row.requests,
    }))
    .sort((a, b) => b.spend - a.spend)
}

export type StatusRow = { status: number; label: string; count: number }

// What each code means here, in the order a reader wants them.
const STATUS_LABELS: Record<number, string> = {
  200: "OK",
  429: "Rate limited",
  500: "Server error",
}

/** How the window's requests ended, most common first. */
export function statusMix(): StatusRow[] {
  const counts = new Map<number, number>()
  for (const request of REQUESTS) {
    counts.set(request.status, (counts.get(request.status) ?? 0) + 1)
  }
  return [...counts.entries()]
    .map(([status, count]) => ({ status, label: STATUS_LABELS[status] ?? "Other", count }))
    .sort((a, b) => b.count - a.count)
}

/**
 * A trace is a request — the log row already carries every column the table and
 * the panel show, so the page reads `AiRequest` rather than copying it.
 */
export type Trace = AiRequest

/** Which slice of the log the table shows. */
export type TraceView = "recent" | "slowest" | "errors"

const TRACE_LIMIT = 12

/** The twelve traces the chosen view puts first. */
export function traces(view: TraceView): Trace[] {
  const rows =
    view === "errors"
      ? REQUESTS.filter((request) => request.status !== 200)
      : view === "slowest"
        ? [...REQUESTS].sort((a, b) => b.latencyMs - a.latencyMs)
        : REQUESTS

  return rows.slice(0, TRACE_LIMIT)
}

/**
 * Every view's twelve at once, for the table: it switches between them in the
 * browser, and the panel opens on a row it already holds.
 */
export function tracesByView(): Record<TraceView, Trace[]> {
  return { recent: traces("recent"), slowest: traces("slowest"), errors: traces("errors") }
}

function ownerRow(): Member {
  return db.members.all().find((member) => member.role === "owner") ?? db.members.all()[0]
}

export function currentUser() {
  const owner = ownerRow()
  return { name: owner.name, email: owner.email, initials: getInitials(owner.name), avatarUrl: owner.avatarUrl }
}

export function shellNotifications() {
  return db.notifications
    .all()
    .sort((a, b) => b.at.getTime() - a.at.getTime())
    .slice(0, 6)
    .map(({ id, title, description, at, read, href }) => ({ id, title, description, at, read, href }))
}
app/ai/vocabulary.ts
/**
 * How this page writes a day and a moment. Vocabulary, not data — neither
 * reads `db` — so the islands import them from here rather than from
 * `data.ts`, which reads the store at module scope and must never reach the
 * browser.
 */

/**
 * The days are bucketed by UTC date, so they are named in UTC too — a local
 * formatter would move every point one day earlier west of Greenwich, and
 * `formatDate` has no `timeZone` to pin it with.
 */
export const dayLabel = new Intl.DateTimeFormat("en-US", {
  month: "short",
  day: "numeric",
  timeZone: "UTC",
})

/**
 * When a request happened. Pinned to UTC like the day labels, because the
 * table and the chart have to agree: a row dated in the reader's zone would
 * sit under a different day's point for anyone west of Greenwich.
 */
export const traceTime = new Intl.DateTimeFormat("en-US", {
  month: "short",
  day: "numeric",
  hour: "numeric",
  minute: "2-digit",
  timeZone: "UTC",
})

/**
 * What each provider in the log is called. The providers are fictional —
 * three hosted labs and Northwind's own in-house models — like every AI name
 * in the kit.
 */
export const PROVIDER_NAMES: Record<string, string> = {
  sparrowmere: "Sparrowmere Labs",
  galewright: "Galewright AI",
  ternstead: "Ternstead AI",
  northwind: "Northwind",
}

Its page

On its page the card sits among the rest of the dashboard and shares its range and its data with them.

From the AI cost and latency page