TypeScript 97.4%
SQL 1%
JavaScript 0.9%
CSS 0.6%
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