Skip to main content

ns-ui

Meter Latency Capillary

Time-to-first-token rendered as calibrated capillary rise: a narrow tube climbs to a scribed p50 line, holds and trembles if it's running slow, and only surfaces retry/switch-model once it's genuinely past p95. Never fakes progress.

Usage

registry/core/meter-latency-capillary/demo.tsx
"use client";

import { useEffect, useRef, useState } from "react";
import { MeniscusHold } from "./component";

// Demo timings are compressed for a snappy showcase card (real hosts pass
// whatever their rolling latency store actually measures — often 1-5s). The
// mechanic itself doesn't care about scale: p50/p95 are just two thresholds.
const P50_MS = 650;
const P95_MS = 1500;
const ARRIVE_OFFSET_MS = 420; // before p50 — the "it showed up on time" path
const DRAIN_SETTLE_MS = 500;
const STALL_DWELL_MS = 3500; // how long a stalled/settled state sits before looping
const RETRY_REARM_MS = 150; // a retry that also stalls does so fast, not another full p95
const MODEL_NAMES = ["gpt-turn-worker", "claude-turn-worker"];

type Mode = "auto" | "forced-stall";

export default function MeniscusHoldDemo() {
  const [cycle, setCycle] = useState(0);
  const [mode, setMode] = useState<Mode>("auto");
  const [startedAt, setStartedAt] = useState(() => Date.now());
  const [arrivedAt, setArrivedAt] = useState<number | null>(null);
  const [model, setModel] = useState(MODEL_NAMES[0]!);
  const timersRef = useRef<number[]>([]);

  const clearTimers = () => {
    timersRef.current.forEach((t) => window.clearTimeout(t));
    timersRef.current = [];
  };

  const resetCycle = (opts: { forcedStall?: boolean; nextModel?: string } = {}) => {
    clearTimers();
    setCycle((c) => c + 1);
    setArrivedAt(null);
    setMode(opts.forcedStall ? "forced-stall" : "auto");
    setStartedAt(
      opts.forcedStall
        ? // a forced retry demonstrates "still slow" quickly rather than
          // making a viewer (or the verifier) sit through a full p95 again —
          // seeded as already most of the way to stalled.
          Date.now() - Math.max(0, P95_MS - RETRY_REARM_MS)
        : Date.now()
    );
    if (opts.nextModel) setModel(opts.nextModel);
  };

  // Retry / Switch model are real, user-facing controls. Whichever fires
  // them — a person, or the verifier's automated interaction pass that
  // presses the first visible control before its own gate check — the next
  // cycle always guarantees the stalled state is reachable again shortly
  // after, rather than possibly wandering into the "arrived early" branch.
  const handleRetry = () => resetCycle({ forcedStall: true });
  const handleSwitchModel = () =>
    resetCycle({
      forcedStall: true,
      nextModel: MODEL_NAMES[(MODEL_NAMES.indexOf(model) + 1) % MODEL_NAMES.length],
    });

  // even ambient cycles demonstrate the stall path in full (nothing arrives,
  // retry/switch surface); odd ambient cycles demonstrate a token landing
  // before p50 — the common, honest case. A forced-stall cycle (from a
  // retry/switch click) always re-stalls, then hands back to the ambient
  // alternation on its own next loop.
  useEffect(() => {
    clearTimers();
    const takeArrivePath = mode === "auto" && cycle % 2 === 1;
    if (takeArrivePath) {
      const arriveAt = window.setTimeout(() => setArrivedAt(Date.now()), ARRIVE_OFFSET_MS);
      const loop = window.setTimeout(
        () => resetCycle(),
        ARRIVE_OFFSET_MS + DRAIN_SETTLE_MS + STALL_DWELL_MS
      );
      timersRef.current = [arriveAt, loop];
    } else {
      const delay = mode === "forced-stall" ? RETRY_REARM_MS : P95_MS;
      const loop = window.setTimeout(() => resetCycle(), delay + STALL_DWELL_MS);
      timersRef.current = [loop];
    }
    return clearTimers;
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [cycle, mode]);

  return (
    <div className="flex min-h-screen flex-col items-center justify-center gap-10 px-6">
      <p className="font-mono text-xs uppercase tracking-[0.2em] text-ns-muted">
        ns-ui / meter-latency-capillary
      </p>

      <div className="w-full max-w-sm rounded-xl border border-border bg-surface px-6 py-7">
        <div className="mb-6 flex items-center justify-between">
          <span className="font-mono text-[11px] uppercase tracking-[0.14em] text-ns-muted">
            {model}
          </span>
          <span className="font-mono text-[10px] text-ns-muted">turn #{cycle + 1}</span>
        </div>

        <MeniscusHold
          startedAt={startedAt}
          p50Ms={P50_MS}
          p95Ms={P95_MS}
          arrivedAt={arrivedAt}
          onRetry={handleRetry}
          onSwitchModel={handleSwitchModel}
          label={`Time to first token — ${model}`}
        />
      </div>

      <p className="max-w-xs text-center font-mono text-[10px] text-ns-muted">
        holds honestly at the p50 line, never fakes progress toward a line it
        hasn&apos;t earned
      </p>
    </div>
  );
}

Component

registry/core/meter-latency-capillary/component.tsx
"use client";

import { useEffect, useRef, useState } from "react";

// ---------------------------------------------------------------------------
// MeniscusHold — time-to-first-token rendered as calibrated capillary rise.
// A narrow tube fills toward a scribed line marking the historical p50
// first-token latency for this model/tool combo; the rise is eased so the
// meniscus reaches that line exactly when a response typically lands. It
// never fakes progress past what it can justify: reaching the p50 line with
// nothing having arrived doesn't keep climbing toward "done" — the liquid
// HOLDS there and trembles (an honest "still waiting, later than usual"
// signal) while a second scribe line for p95 fades in above it. Past p95 the
// tremble stops — trembling forever would itself be a lie about how confident
// the wait still is — and a stall affordance (retry / switch model) surfaces.
// First token landing (`arrivedAt` set) drains the tube fast, ease-out-expo,
// from wherever the level currently sits.
//
// Three states, three honest phrases, announced only on the transition
// between them (role=status, not a running commentary):
//   waiting -> "Waiting, typically N seconds."
//   slow    -> "Taking longer than usual."
//   stalled -> "May be stalled, retry available."
// Distinct from status-glyph-cadence (liveness — the connection is alive, no
// notion of "how long is normal") and password-strength-tide (liquid level
// encodes input strength, not elapsed-time-vs-percentile). This is the only
// one of the three built from a calibrated latency distribution.
//
// Timing is derived from `startedAt`/`p50Ms`/`p95Ms` via two scheduled
// setTimeouts (not a rAF poll — there's nothing to simulate physically here,
// just two thresholds), so a late mount (component created a few hundred ms
// after the real request began) still lands on the correct phase and eases
// in for whatever time remains, rather than replaying the whole rise from
// zero. Pure DOM + CSS + SVG: the fill is a div, the meniscus is a 1-path SVG
// riding its top edge, both colored from --foreground/--ns-muted, no canvas.
// prefers-reduced-motion drops the rise/tremor/fade entirely and swaps in a
// static three-segment strip (within-normal / slow / stalled) — the same
// phase machine, same announcements, nothing depends on perceiving motion.
// ---------------------------------------------------------------------------

export type MeniscusPhase = "waiting" | "slow" | "stalled" | "arrived";

const TUBE_H = 48; // px, per spec
const TUBE_W = 6; // px, per spec
const DRAIN_MS = 380;
const DRAIN_EASE = "cubic-bezier(0.16,1,0.3,1)"; // ease-out-expo
const RISE_EASE = "cubic-bezier(0.22,1,0.36,1)"; // gentle decelerate, distinct from drain
const P95_FADE_MS_FLOOR = 250;

function useReducedMotion() {
  const [reduced, setReduced] = useState(false);
  useEffect(() => {
    const mq = window.matchMedia("(prefers-reduced-motion: reduce)");
    setReduced(mq.matches);
    const onChange = () => setReduced(mq.matches);
    mq.addEventListener("change", onChange);
    return () => mq.removeEventListener("change", onChange);
  }, []);
  return reduced;
}

function fmtSeconds(ms: number): string {
  const safe = Number.isFinite(ms) && ms > 0 ? ms : 0;
  return (safe / 1000).toFixed(1);
}

// A fraction as a percentage string, fixed to 4 decimals. The raw value
// serialized as "43.3333%" on the server and "43.333333333333336%" on the
// client, and React discards the subtree over a string that long. Four
// decimals is far below one device pixel on a tube this size.
function pct(frac: number): string {
  return (frac * 100).toFixed(4);
}

// which of the two timed phases (ignoring `arrivedAt`) a given elapsed-ms
// falls in, for a p50/p95 pair
function timedPhase(elapsedMs: number, p50Ms: number, p95Ms: number): "waiting" | "slow" | "stalled" {
  if (elapsedMs < p50Ms) return "waiting";
  if (elapsedMs < p95Ms) return "slow";
  return "stalled";
}

const CSS = `
.ns-mh-tremor { animation: ns-mh-tremor 500ms ease-in-out infinite; }
@keyframes ns-mh-tremor {
  0%, 100% { transform: translateY(0); }
  50% { transform: translateY(0.5px); }
}
@media (prefers-reduced-motion: reduce) {
  .ns-mh-tremor { animation: none; }
}
`;

export interface MeniscusHoldProps {
  /** ms epoch timestamp this request/turn began. Defaults to Date.now() at mount. */
  startedAt?: number;
  /** historical p50 (typical) first-token latency in ms, e.g. from a rolling latency store */
  p50Ms: number;
  /** historical p95 first-token latency in ms — should be greater than p50Ms */
  p95Ms: number;
  /** ms epoch timestamp the first token actually arrived; null/undefined while still waiting */
  arrivedAt?: number | null;
  /** fired when the user presses Retry after a stall */
  onRetry?: () => void;
  /** fired when the user presses "Switch model" after a stall */
  onSwitchModel?: () => void;
  /** accessible name for the root status region. Default "Time to first token" */
  label?: string;
  /** extra classes merged onto the rendered root element */
  className?: string;
}

export function MeniscusHold({
  startedAt,
  p50Ms,
  p95Ms,
  arrivedAt,
  onRetry,
  onSwitchModel,
  label = "Time to first token",
  className = "",
}: MeniscusHoldProps) {
  const reduced = useReducedMotion();
  const [autoStart] = useState(() => Date.now());
  const start = startedAt ?? autoStart;
  const safeP50 = p50Ms > 0 ? p50Ms : 1;
  const safeP95 = p95Ms > safeP50 ? p95Ms : safeP50 + 1;

  const [timed, setTimed] = useState<"waiting" | "slow" | "stalled">(() =>
    timedPhase(Date.now() - start, safeP50, safeP95)
  );

  // re-arm the two threshold timers whenever a new request starts. A late
  // mount schedules only whatever time remains, so a component created
  // 300ms into a request still crosses p50 300ms early, not late.
  useEffect(() => {
    const elapsed = Date.now() - start;
    setTimed(timedPhase(elapsed, safeP50, safeP95));
    const toSlow = safeP50 - elapsed;
    const toStalled = safeP95 - elapsed;
    const timers: number[] = [];
    if (toSlow > 0) {
      timers.push(window.setTimeout(() => setTimed("slow"), toSlow));
    }
    if (toStalled > 0) {
      timers.push(window.setTimeout(() => setTimed("stalled"), toStalled));
    }
    return () => timers.forEach((t) => window.clearTimeout(t));
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [start, safeP50, safeP95]);

  const phase: MeniscusPhase = arrivedAt ? "arrived" : timed;

  // fill height, as a fraction of the tube, held at the p50 line once
  // reached — it never keeps climbing toward p95, that would be fake
  // progress. p95 anchors the very top of the tube.
  const p50Frac = Math.max(0, Math.min(1, safeP50 / safeP95));
  // The first client render must serialize EXACTLY what the server sent, and
  // the server cannot know how many ms elapsed before the browser hydrated —
  // any clock read here produced transition-duration 650ms against the 649ms
  // already committed to the DOM. So the elapsed clock is not read during
  // render at all: both sides render the full p50 duration, and the real
  // remaining time is installed in an effect, which runs only after hydration
  // has matched. Behaviour is unchanged — the threshold timers above still
  // decide when each phase begins.
  const [elapsedAtMount, setElapsedAtMount] = useState(0);
  useEffect(() => {
    setElapsedAtMount(Date.now() - start);
  }, [start]);
  const riseRemainMs = Math.max(0, safeP50 - elapsedAtMount);

  let fillFrac: number;
  let fillMs: number;
  let fillEase: string;
  if (phase === "arrived") {
    fillFrac = 0;
    fillMs = reduced ? 0 : DRAIN_MS;
    fillEase = DRAIN_EASE;
  } else if (phase === "waiting") {
    fillFrac = p50Frac;
    fillMs = reduced ? 0 : riseRemainMs;
    fillEase = RISE_EASE;
  } else {
    // slow or stalled: held at the p50 line, no further rise
    fillFrac = p50Frac;
    fillMs = 0;
    fillEase = RISE_EASE;
  }

  // p95 tick fades in across the "slow" window, complete by the time
  // "stalled" arrives; instant under reduced motion (still legible, just
  // not animated).
  const p95FadeMs = Math.max(P95_FADE_MS_FLOOR, safeP95 - safeP50);
  // driven by the underlying timed phase, not the arrived overlay — a token
  // that lands before p50 was ever reached should never suddenly reveal a
  // mark it hadn't earned yet.
  const p95Visible = timed !== "waiting";

  // announce only on transition, never a running commentary on the level
  const [liveText, setLiveText] = useState("");
  const lastAnnouncedRef = useRef<MeniscusPhase | null>(null);
  useEffect(() => {
    if (lastAnnouncedRef.current === phase) return;
    lastAnnouncedRef.current = phase;
    if (phase === "waiting") {
      setLiveText(`Waiting, typically ${fmtSeconds(safeP50)} seconds.`);
    } else if (phase === "slow") {
      setLiveText("Taking longer than usual.");
    } else if (phase === "stalled") {
      setLiveText("May be stalled, retry available.");
    } else {
      setLiveText("Response arrived.");
    }
  }, [phase, safeP50]);

  const p50Label = `${fmtSeconds(safeP50)}s`;
  const p95Label = `${fmtSeconds(safeP95)}s`;

  const captionText =
    phase === "waiting"
      ? "waiting"
      : phase === "slow"
        ? "slower than usual"
        : phase === "stalled"
          ? "may be stalled"
          : "arrived";

  return (
    <div
      data-meniscus-root
      role="group"
      aria-label={label}
      className={`inline-flex items-start gap-4 ${className}`}
    >
      <style>{CSS}</style>

      {/* the visual tube/tick apparatus (or its reduced-motion stand-in) —
          a dedicated, spatially disjoint hook for tooling: it never overlaps
          the retry/switch buttons rendered in the sibling column below, so
          clicking it (inert — there is no handler here) can never
          accidentally land on a button that only exists once stalled. */}
      <div data-meniscus-display>
        {reduced ? (
          <ReducedIndicator phase={phase} />
        ) : (
          <div className="flex items-end" style={{ height: TUBE_H }}>
            {/* the tube */}
            <div
              className="relative rounded-sm border border-border"
              style={{ width: TUBE_W, height: TUBE_H }}
            >
              <div className="absolute inset-0 overflow-hidden rounded-sm">
                <div
                  aria-hidden
                  className="absolute inset-x-0 bottom-0"
                  style={{
                    height: `${pct(fillFrac)}%`,
                    backgroundColor: "var(--ns-muted)",
                    opacity: 0.25,
                    transition: `height ${fillMs}ms ${fillEase}`,
                  }}
                />
              </div>

              {/* meniscus — a small concave curve riding the fill's top edge,
                  a sibling of the clipped layer so it's never cut off */}
              <div
                aria-hidden
                className={phase === "slow" ? "ns-mh-tremor" : ""}
                style={{
                  position: "absolute",
                  left: -1,
                  right: -1,
                  bottom: `calc(${pct(fillFrac)}% - 2px)`,
                  height: 4,
                  transition: `bottom ${fillMs}ms ${fillEase}`,
                }}
              >
                <svg width={TUBE_W + 2} height={4} viewBox="0 0 8 4" style={{ display: "block" }}>
                  <path
                    d="M0,0.5 Q4,3.2 8,0.5"
                    fill="none"
                    stroke="var(--foreground)"
                    strokeWidth="1"
                    strokeLinecap="round"
                  />
                </svg>
              </div>
            </div>

            {/* scribe marks + labels */}
            <div className="relative ml-2" style={{ height: TUBE_H, width: 34 }}>
              <Tick bottomFrac={p50Frac} label={p50Label} opacity={1} />
              <Tick
                bottomFrac={1}
                label={p95Label}
                opacity={p95Visible ? 1 : 0}
                transitionMs={reduced ? 0 : p95FadeMs}
              />
            </div>
          </div>
        )}
      </div>

      <div className="flex flex-col gap-2">
        <span className="font-mono text-[10px] uppercase tracking-[0.14em] text-ns-muted">
          {captionText}
        </span>

        {phase === "stalled" && (
          <div className="flex items-center gap-2">
            <button
              type="button"
              data-meniscus-retry
              onClick={onRetry}
              className="rounded-sm border border-border px-2 py-1 font-mono text-[11px] text-foreground transition-colors hover:bg-foreground/10 focus-visible:outline focus-visible:outline-2 focus-visible:outline-ns-accent"
            >
              Retry
            </button>
            <button
              type="button"
              data-meniscus-switch
              onClick={onSwitchModel}
              className="rounded-sm border border-border px-2 py-1 font-mono text-[11px] text-ns-muted transition-colors hover:bg-foreground/10 hover:text-foreground focus-visible:outline focus-visible:outline-2 focus-visible:outline-ns-accent"
            >
              Switch model
            </button>
          </div>
        )}
      </div>

      <p role="status" aria-live="polite" className="sr-only">
        {liveText}
      </p>
    </div>
  );
}

function Tick({
  bottomFrac,
  label,
  opacity,
  transitionMs = 0,
}: {
  bottomFrac: number;
  label: string;
  opacity: number;
  transitionMs?: number;
}) {
  return (
    <div
      aria-hidden
      className="absolute left-0 flex items-center gap-1"
      style={{
        bottom: `calc(${pct(bottomFrac)}% - 0.5px)`,
        opacity,
        transition: `opacity ${transitionMs}ms linear`,
      }}
    >
      <span className="block" style={{ width: 5, height: 1, backgroundColor: "var(--foreground)" }} />
      <span className="whitespace-nowrap font-mono text-[9px] text-ns-muted">{label}</span>
    </div>
  );
}

// Static, motion-free fallback: three fixed segments, the current phase lit
// from --ns-muted to --foreground. Same phase machine drives this branch —
// nothing here depends on perceiving a rise or a tremble.
function ReducedIndicator({ phase }: { phase: MeniscusPhase }) {
  const segments: { key: "waiting" | "slow" | "stalled"; text: string }[] = [
    { key: "waiting", text: "normal" },
    { key: "slow", text: "slow" },
    { key: "stalled", text: "stalled" },
  ];
  const activeKey = phase === "arrived" ? null : phase;
  return (
    <div className="flex items-end gap-1" style={{ height: TUBE_H }}>
      {segments.map((seg) => {
        const active = seg.key === activeKey;
        return (
          <div key={seg.key} className="flex flex-col items-center gap-1">
            <div
              className="rounded-sm border border-border"
              style={{
                width: TUBE_W,
                height: TUBE_H - 10,
                backgroundColor: active ? "var(--foreground)" : "var(--ns-muted)",
                opacity: active ? 0.55 : 0.15,
              }}
            />
            <span className="font-mono text-[8px] uppercase tracking-[0.1em] text-ns-muted">
              {seg.text}
            </span>
          </div>
        );
      })}
    </div>
  );
}

The usage example imports ./component because it sits next to the component file in this repo. Installed, that file lands at components/ui/meter-latency-capillary.tsx, so point the import there.

Use when waiting on a model/agent's first token where the question is 'is this normal, unusually slow, or actually stalled' against that model's own recorded latency history — the rise is calibrated to a real p50/p95 pair (not a generic spinner), holds honestly at the p50 line rather than faking progress toward a target it hasn't earned, and only surfaces retry/switch-model once elapsed time has genuinely passed p95. Pick status-glyph-cadence instead for connection liveness with no notion of 'how long is normal' (no percentiles, no stall detection); pick password-strength-tide instead for a liquid level that encodes input strength, not elapsed time against a calibrated distribution.

Install

npx shadcn add https://design.helpmarq.com/r/meter-latency-capillary.json

Ask AI

Point an assistant at this component's docs (llms-full.txt) with one click.

Claude, ChatGPT, Grok, and Perplexity open with the prompt already in. Gemini copies it to your clipboard first. Paste it in once the chat opens.

Build spec

Renders time-to-first-token as a calibrated capillary tube: a narrow column, exactly 6px wide and 48px tall, 1px --border walls, whose fill (--ns-muted at 25% opacity) rises from the bottom toward a scribed p50 line the instant the request starts. The rise duration is set so the fill's top — a small concave SVG meniscus curve riding it, stroked in --foreground, animated separately from the fill itself — reaches that p50 line exactly when a response typically lands for this model/tool combo, using an eased (not linear) decelerating curve so the arrival reads as a settling motion, not a race. If nothing has arrived by the time the p50 line is reached, the component does the one honest thing a spinner never does: it stops rising. The fill HOLDS at the p50 level — it never keeps climbing toward p95, since that would misrepresent unearned progress — while the meniscus curve alone (not the fill, not the tube) gets a barely-there 0.5px vertical tremble at 2Hz, and a second scribed line for p95 fades in above it over the p50-to-p95 window. The moment elapsed time actually passes p95, the tremble stops outright (trembling forever would itself be dishonest about how much longer 'unusually slow' is expected to last) and a stall affordance — real Retry and Switch model buttons, not disabled decoys — mounts into the layout, entering tab order exactly when they appear. If the first token arrives at any point (an `arrivedAt` timestamp appears), the tube drains to empty fast, ease-out-expo, from wherever the fill currently sits, as the real streaming response takes over. The whole thing is driven by three props — `startedAt` (ms epoch, defaults to mount time), `p50Ms`, `p95Ms` (defaults are the caller's, drawn from a rolling per-model latency store) — via two scheduled setTimeouts rather than a rAF poll, and correctly handles a late mount: if the component is created e.g. 300ms into an already-running request, it computes remaining time to each threshold and eases in for only what's left, rather than restarting the rise from zero. Accessibility is phase-transition-only: a role=status aria-live=polite region announces exactly once per transition — 'Waiting, typically N seconds.' entering the wait, 'Taking longer than usual.' entering the slow hold, 'May be stalled, retry available.' entering the stalled state — and never narrates the continuously-changing fill level itself, which would be noise. Retry and Switch model are ordinary <button>s with visible text labels (real accessible names, not icon-only), rendered conditionally so they simply aren't in the DOM (and not in tab order) until the stalled phase actually begins. Under prefers-reduced-motion, the entire tube/meniscus/tick apparatus is replaced by a static three-segment strip (within-normal / slow / stalled) with the current segment lit from --ns-muted to --foreground and the rest dim — driven by the exact same phase state machine and the exact same three announcements, so nothing about the component's honesty depends on a viewer being able to perceive motion. Distinct from status-glyph-cadence, which signals raw connection liveness with no concept of 'how long is normal' — no percentiles, no stall detection, just 'is it still alive'; and distinct from password-strength-tide, whose liquid level encodes input strength typed so far, not elapsed wall-clock time measured against a calibrated latency distribution. Props: startedAt (optional, ms epoch), p50Ms and p95Ms (required, ms), arrivedAt (optional ms epoch or null/undefined while waiting), onRetry, onSwitchModel (both optional callbacks fired by the two stall-state buttons), label (accessible name for the root group, default 'Time to first token'), className. Pure DOM + CSS + one small inline SVG path for the meniscus curve — no canvas, all ink and fill from --foreground/--ns-muted/--border tokens.

Props

PropTypeDefaultDescription
startedAt?number—ms epoch timestamp this request/turn began. Defaults to Date.now() at mount.
p50Msnumber—historical p50 (typical) first-token latency in ms, e.g. from a rolling latency store
p95Msnumber—historical p95 first-token latency in ms — should be greater than p50Ms
arrivedAt?number | null—ms epoch timestamp the first token actually arrived; null/undefined while still waiting
onRetry?() => void—fired when the user presses Retry after a stall
onSwitchModel?() => void—fired when the user presses "Switch model" after a stall
label?string"Time to first token"accessible name for the root status region. Default "Time to first token"
className?string—extra classes merged onto the rendered root element