SPB Git forge

spb/polyllm

Public
15commits 1branches 0releases
2.2 MBsize
maindefault branch
13 days agolast push
TypeScript 97.4% SQL 1% JavaScript 0.9% CSS 0.6%
9.5 KB · 201 lines tsx
Raw Blame History
1"use client";2import * as React from "react";3import { CalendarRange, X } from "lucide-react";4import { Segmented, ChipRow } from "@/components/ui/segmented";5import { ResponsiveDialog } from "@/components/ui/sheet";6import { Button } from "@/components/ui/button";7import { Input, Field } from "@/components/ui/input";8import { ProviderIcon } from "@/components/brand/provider-icon";9import { providerName } from "@/lib/client/providers";10import { cn } from "@/lib/utils";11import type { ProviderId } from "@/lib/client/types";12import type { UsageRangeKey } from "./types";1314export interface UsageQueryState {15  range: UsageRangeKey;16  from: string; // YYYY-MM-DD when range === "custom"17  to: string;18  provider: ProviderId | "";19  modelKey: string;20  projectId: string;21}2223export const DEFAULT_QUERY: UsageQueryState = { range: "30d", from: "", to: "", provider: "", modelKey: "", projectId: "" };2425const RANGE_OPTIONS: { value: UsageRangeKey; label: string; short: string }[] = [26  { value: "today", label: "Today", short: "Today" },27  { value: "7d", label: "7 days", short: "7d" },28  { value: "30d", label: "30 days", short: "30d" },29  { value: "90d", label: "90 days", short: "90d" },30  { value: "custom", label: "Custom", short: "Custom" },31];3233export function rangeLabel(q: UsageQueryState): string {34  if (q.range === "custom" && q.from && q.to) return `${fmt(q.from)} – ${fmt(q.to)}`;35  if (q.range === "all") return "All time";36  return RANGE_OPTIONS.find((r) => r.value === q.range)?.label ?? "30 days";37}38function fmt(ymd: string): string {39  const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(ymd);40  if (!m) return ymd;41  return new Date(Number(m[1]), Number(m[2]) - 1, Number(m[3])).toLocaleDateString("en-US", { month: "short", day: "numeric" });42}43function today(): string {44  const d = new Date();45  return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;46}4748/** Range segmented control + custom-dates sheet. One filter row scopes every chart below it. */49export function RangePicker({ value, onChange, compact }: { value: UsageQueryState; onChange: (patch: Partial<UsageQueryState>) => void; compact?: boolean }) {50  const [open, setOpen] = React.useState(false);51  const [from, setFrom] = React.useState(value.from);52  const [to, setTo] = React.useState(value.to);53  const max = today();54  const valid = /^\d{4}-\d{2}-\d{2}$/.test(from) && /^\d{4}-\d{2}-\d{2}$/.test(to) && from <= to;5556  const openCustom = () => {57    setFrom(value.from || defaultFrom());58    setTo(value.to || max);59    setOpen(true);60  };61  const apply = () => {62    if (!valid) return;63    onChange({ range: "custom", from, to });64    setOpen(false);65  };6667  return (68    <>69      <Segmented<UsageRangeKey>70        ariaLabel="Time range"71        size={compact ? "sm" : "md"}72        value={value.range}73        onChange={(v) => (v === "custom" ? openCustom() : onChange({ range: v, from: "", to: "" }))}74        options={RANGE_OPTIONS.map((r) => ({ value: r.value, label: r.value === "custom" && value.range === "custom" && value.from ? rangeLabel(value) : compact ? r.short : r.label, icon: r.value === "custom" ? <CalendarRange /> : undefined }))}75      />76      <ResponsiveDialog77        open={open}78        onOpenChange={setOpen}79        size="sm"80        title="Custom range"81        description="Whole days in your local time zone, up to one year."82        footer={83          <div className="flex gap-2">84            <Button variant="ghost" className="flex-1 md:flex-none" onClick={() => setOpen(false)}>85              Cancel86            </Button>87            <Button variant="accent" className="flex-1 md:flex-none" disabled={!valid} onClick={apply}>88              Apply89            </Button>90          </div>91        }92      >93        <div className="grid gap-3 pt-1 sm:grid-cols-2">94          <Field label="From" htmlFor="usage-from">95            <Input id="usage-from" type="date" value={from} max={to || max} onChange={(e) => setFrom(e.target.value)} className="h-11 text-[16px] md:h-9" />96          </Field>97          <Field label="To" htmlFor="usage-to" error={from && to && from > to ? "End before start." : null}>98            <Input id="usage-to" type="date" value={to} min={from || undefined} max={max} onChange={(e) => setTo(e.target.value)} className="h-11 text-[16px] md:h-9" />99          </Field>100        </div>101        <div className="mt-3 flex flex-wrap gap-1.5">102          {[103            { label: "This month", get: () => [max.slice(0, 8) + "01", max] },104            { label: "Last month", get: () => lastMonth() },105            { label: "Last 6 months", get: () => [shiftDays(-182), max] },106            { label: "This year", get: () => [`${max.slice(0, 4)}-01-01`, max] },107          ].map((p) => (108            <button key={p.label} type="button" onClick={() => { const [a, b] = p.get(); setFrom(a); setTo(b); }} className="tap h-8 rounded-full border border-border px-3 text-[12.5px] text-fg-muted hover:border-border-strong hover:text-fg">109              {p.label}110            </button>111          ))}112        </div>113      </ResponsiveDialog>114    </>115  );116}117118function defaultFrom(): string {119  return shiftDays(-29);120}121function shiftDays(n: number): string {122  const d = new Date();123  d.setDate(d.getDate() + n);124  return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;125}126function lastMonth(): [string, string] {127  const d = new Date();128  const first = new Date(d.getFullYear(), d.getMonth() - 1, 1);129  const last = new Date(d.getFullYear(), d.getMonth(), 0);130  const f = (x: Date) => `${x.getFullYear()}-${String(x.getMonth() + 1).padStart(2, "0")}-${String(x.getDate()).padStart(2, "0")}`;131  return [f(first), f(last)];132}133134export interface FacetData {135  providers: ProviderId[];136  models: { modelKey: string; provider: ProviderId; requests: number }[];137  projects: { id: string; name: string; icon: string | null; color: string | null }[];138}139140/** Provider / model / project chips. Chips scroll horizontally on phones; "All" clears each dimension. */141export function FilterChips({ value, onChange, facets, modelLabel }: { value: UsageQueryState; onChange: (patch: Partial<UsageQueryState>) => void; facets: FacetData | null; modelLabel: (key: string) => string }) {142  const providers = facets?.providers ?? [];143  const models = (facets?.models ?? []).filter((m) => !value.provider || m.provider === value.provider).slice(0, 10);144  const projects = facets?.projects ?? [];145  const showProviders = providers.length > 1 || value.provider;146  const showModels = models.length > 1 || value.modelKey;147  const showProjects = projects.length > 0;148  if (!showProviders && !showModels && !showProjects) return null;149  return (150    <div className="space-y-2">151      {showProviders ? (152        <ChipRow<string>153          value={value.provider || "all"}154          onChange={(v) => onChange({ provider: v === "all" ? "" : (v as ProviderId), modelKey: "" })}155          options={[{ value: "all", label: "All providers" }, ...providers.map((p) => ({ value: p, label: providerName(p), icon: <ProviderIcon provider={p} size={13} /> }))]}156          className="-mx-4 px-4 md:mx-0 md:px-0"157        />158      ) : null}159      {showModels ? (160        <ChipRow<string>161          value={value.modelKey || "all"}162          onChange={(v) => onChange({ modelKey: v === "all" ? "" : v })}163          options={[{ value: "all", label: "All models" }, ...models.map((m) => ({ value: m.modelKey, label: modelLabel(m.modelKey), icon: <ProviderIcon provider={m.provider} size={13} />, count: m.requests }))]}164          className="-mx-4 px-4 md:mx-0 md:px-0"165        />166      ) : null}167      {showProjects ? (168        <ChipRow<string>169          value={value.projectId || "all"}170          onChange={(v) => onChange({ projectId: v === "all" ? "" : v })}171          options={[{ value: "all", label: "All projects" }, ...projects.map((p) => ({ value: p.id, label: p.name, icon: p.icon && /^\p{Extended_Pictographic}/u.test(p.icon) ? <span aria-hidden>{p.icon}</span> : <span className="size-2.5 rounded-full" style={{ background: p.color ?? "var(--fg-subtle)" }} aria-hidden /> }))]}172          className="-mx-4 px-4 md:mx-0 md:px-0"173        />174      ) : null}175    </div>176  );177}178179/** Summary of active filters with one-tap clear (shown above the KPIs when anything is narrowed). */180export function ActiveFilters({ value, onChange, modelLabel, projects }: { value: UsageQueryState; onChange: (patch: Partial<UsageQueryState>) => void; modelLabel: (key: string) => string; projects: FacetData["projects"] }) {181  const items: { key: keyof UsageQueryState; label: string }[] = [];182  if (value.provider) items.push({ key: "provider", label: providerName(value.provider) });183  if (value.modelKey) items.push({ key: "modelKey", label: modelLabel(value.modelKey) });184  if (value.projectId) items.push({ key: "projectId", label: projects.find((p) => p.id === value.projectId)?.name ?? "Project" });185  if (!items.length) return null;186  return (187    <div className="flex flex-wrap items-center gap-1.5 text-xs text-fg-muted">188      <span>Filtered by</span>189      {items.map((i) => (190        <button key={i.key} type="button" onClick={() => onChange({ [i.key]: "" } as Partial<UsageQueryState>)} className={cn("tap inline-flex h-7 items-center gap-1 rounded-full bg-bg-muted px-2.5 font-medium text-fg hover:bg-border")} aria-label={`Remove filter ${i.label}`}>191          {i.label}192          <X className="size-3" />193        </button>194      ))}195      <button type="button" onClick={() => onChange({ provider: "", modelKey: "", projectId: "" })} className="tap text-accent hover:underline">196        Clear all197      </button>198    </div>199  );200}201