@columenlabs/lumen-ui 0.1.209 → 0.1.210

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,4 @@
1
- import type { AllowanceRequest, AssistantChat, AssistantChatRow, AssistantEvent, AssistantMessage, AssistantOwnWay, AssistantProvider, AssistantSettingsPatch, AssistantStatus, DraftItem, KeyState, StepItem } from '@columenlabs/lumen-ui-types/assistant';
1
+ import type { AllowanceRequest, AssistantChat, AssistantChatRow, AssistantEvent, AssistantMessage, AssistantOwnWay, AssistantProvider, AssistantSettingsPatch, AssistantStatus, DraftItem, KeyState, StepItem, UsageHistory } from '@columenlabs/lumen-ui-types/assistant';
2
2
  type Test = NonNullable<KeyState['test']>;
3
3
  export interface AssistantAdapter {
4
4
  /** Names the cache its reads keep (two assistants on one page need two keys). */
@@ -34,6 +34,8 @@ export interface AssistantAdapter {
34
34
  chats(): Promise<AssistantChatRow[]>;
35
35
  chat(id: string): Promise<AssistantChat>;
36
36
  removeChat(id: string): Promise<void>;
37
+ /** The person's use in a month (`YYYY-MM`, this month when left out): each model, chat and run. */
38
+ usage?(month?: string): Promise<UsageHistory>;
37
39
  /** One answer: `onEvent` hears each event as it streams; abort `signal` to stop it. */
38
40
  send(msg: AssistantMessage, onEvent: (e: AssistantEvent) => void, signal?: AbortSignal): Promise<void>;
39
41
  settleDraft(chatId: string, draftId: string, action: 'save' | 'dismiss'): Promise<DraftItem>;
@@ -60,6 +60,7 @@ export function assistantClient(base) {
60
60
  removeChat: async (id) => {
61
61
  await send('DELETE', `/chats/${encodeURIComponent(id)}`);
62
62
  },
63
+ usage: (month) => get(`/usage${month ? `?month=${encodeURIComponent(month)}` : ''}`),
63
64
  settleDraft: (chatId, draftId, action) => send('POST', `/chats/${encodeURIComponent(chatId)}/drafts/${encodeURIComponent(draftId)}`, {
64
65
  action,
65
66
  }),
@@ -1,5 +1,6 @@
1
1
  export { type AssistantAdapter, assistantClient } from './client.js';
2
2
  export { type AssistantCommand, type ComposerExtra, LumenAssistant, type LumenAssistantProps, LumenComposer, assistantStatusKey, useAssistantStatus, useLumenAssistantDock, } from './lumen-assistant.js';
3
3
  export { assistantAdminSections, assistantSettingsSections } from './settings.js';
4
+ export { UsageHistoryView, assistantUsageKey, tokens as formatTokens, usd as formatUsd, useAssistantUsage, } from './usage-history.js';
4
5
  export { type AssistantSource, type AssistantSourceItem, type ComposeRequest, type PageSelection, composeAssistant, selectionRef, useAssistantSource, usePageSelection, useShownAssistantSources, } from './sources.js';
5
6
  export { type AssistantChatState, type LiveItem, applyEvent, useAssistantChat, } from './use-assistant-chat.js';
@@ -4,5 +4,6 @@
4
4
  export { assistantClient } from './client.js';
5
5
  export { LumenAssistant, LumenComposer, assistantStatusKey, useAssistantStatus, useLumenAssistantDock, } from './lumen-assistant.js';
6
6
  export { assistantAdminSections, assistantSettingsSections } from './settings.js';
7
+ export { UsageHistoryView, assistantUsageKey, tokens as formatTokens, usd as formatUsd, useAssistantUsage, } from './usage-history.js';
7
8
  export { composeAssistant, selectionRef, useAssistantSource, usePageSelection, useShownAssistantSources, } from './sources.js';
8
9
  export { applyEvent, useAssistantChat, } from './use-assistant-chat.js';
@@ -2,11 +2,12 @@ import type { AllowanceUnit, AssistantMessage, AssistantRefNote, AssistantStatus
2
2
  import { type LumenOption } from '../lumen-selector.js';
3
3
  import type { AssistantAdapter } from './client.js';
4
4
  import { type AssistantSource } from './sources.js';
5
+ import { usd } from './usage-history.js';
5
6
  import { type AssistantChatState } from './use-assistant-chat.js';
6
7
  export declare const assistantStatusKey: (client: AssistantAdapter) => string[];
7
8
  /** The person's assistant status, shared by the panel and Settings. */
8
9
  export declare function useAssistantStatus(client: AssistantAdapter): import("@tanstack/react-query").UseQueryResult<AssistantStatus, Error>;
9
- export declare const usd: (n: number) => string;
10
+ export { usd };
10
11
  export interface LumenAssistantProps {
11
12
  client: AssistantAdapter;
12
13
  /** The page the person is on, sent with each message. */
@@ -1,6 +1,6 @@
1
1
  import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useQuery, useQueryClient } from '@tanstack/react-query';
3
- import { ArrowUpIcon, CheckIcon, ChevronRightIcon, CircleAlertIcon, CopyIcon, CornerDownRightIcon, CrosshairIcon, FileTextIcon, LightbulbIcon, MonitorIcon, PencilIcon, PlusIcon, SparklesIcon, SquareIcon, TriangleAlertIcon, Undo2Icon, WrenchIcon, XIcon, ZapIcon, } from 'lucide-react';
3
+ import { ArrowUpIcon, CheckIcon, ChevronRightIcon, CircleAlertIcon, CopyIcon, CornerDownRightIcon, CrosshairIcon, FileTextIcon, LightbulbIcon, MonitorIcon, PencilIcon, PlusIcon, ReceiptIcon, SparklesIcon, SquareIcon, TriangleAlertIcon, Undo2Icon, WrenchIcon, XIcon, ZapIcon, } from 'lucide-react';
4
4
  import { Fragment, createContext, useContext, useEffect, useRef, useState, } from 'react';
5
5
  import { LumenAssistantButton, LumenAssistantChats, LumenAssistantPanel, assistantOpen, useAssistantHotkey, } from '../assistant-panel.js';
6
6
  import { screenMapFor, useAssistantScreen } from '../assistant-screen.js';
@@ -22,6 +22,7 @@ import { Spinner } from '../ui/spinner.js';
22
22
  import { Textarea } from '../ui/textarea.js';
23
23
  import { chatMarkdown, chatTurns, copyMarkdown, turnMarkdown } from './copy.js';
24
24
  import { composeAssistant, selectionRef, useComposeRequest, usePageSelection, useShownAssistantSources, } from './sources.js';
25
+ import { UsageHistoryView, tokens, usd } from './usage-history.js';
25
26
  import { useAssistantChat } from './use-assistant-chat.js';
26
27
  // The Markdown renderer (tables included) loads with the first answer (lazy-part.tsx).
27
28
  // A tab whose build a deploy replaced shows the answer as plain text instead of reloading mid chat.
@@ -38,12 +39,7 @@ export const assistantStatusKey = (client) => [
38
39
  export function useAssistantStatus(client) {
39
40
  return useQuery({ queryKey: assistantStatusKey(client), queryFn: () => client.status() });
40
41
  }
41
- export const usd = (n) => n === 0 ? '$0' : n < 0.01 ? '<$0.01' : `$${n.toFixed(n < 10 ? 2 : 0)}`;
42
- const tokens = (n) => n < 1000
43
- ? String(n)
44
- : n < 1_000_000
45
- ? `${(n / 1000).toFixed(n < 10_000 ? 1 : 0)}k`
46
- : `${(n / 1e6).toFixed(1)}M`;
42
+ export { usd };
47
43
  const seconds = (ms) => (ms < 1000 ? `${ms} ms` : `${(ms / 1000).toFixed(1)} s`);
48
44
  /** The assistant's chat: header, messages and composer. */
49
45
  export function LumenAssistant({ client, context, onSettings, keepDays = 30, onClose, chat: outside, commands = [], sources = [], }) {
@@ -183,7 +179,7 @@ export function LumenAssistant({ client, context, onSettings, keepDays = 30, onC
183
179
  id: c.id,
184
180
  title: c.title,
185
181
  updated_at: new Date(c.at).toISOString(),
186
- })) ?? (chats.isFetched ? [] : null), value: chat.chatId ?? undefined, note: `Chats are kept ${keepDays} days after their last message.`, onRefresh: () => void chats.refetch(), onOpen: (c) => void chat.open(c.id) }), _jsx(IconButton, { icon: CopyIcon, label: "Copy chat", disabled: !chat.items.length, onClick: () => copyMarkdown(chatMarkdown(chat.title, chat.items, name), 'Chat') }), _jsx(IconButton, { icon: PlusIcon, label: "New chat", disabled: !chat.items.length, onClick: chat.reset }), onClose && (_jsx(IconButton, { icon: XIcon, label: `Close ${s?.name ?? 'assistant'}`, onClick: onClose }))] }), _jsx(AssistantName.Provider, { value: name, children: _jsx(Screen.Provider, { value: controls, children: _jsx(Messages, { chat: chat, status: s, onSettings: onSettings }) }) }), _jsx(LumenComposer, { name: s?.name ?? 'the assistant', busy: chat.busy, queued: chat.queued, disabled: !s || !!s.wait, commands: allCommands, tiers: tiers, sources: allSources, page: context?.label, onSend: (text, more) => chat.send(text, { ...extra(), ...more }), onStop: chat.stop }), _jsx(UsageLine, { usage: chat.usage })] }));
182
+ })) ?? (chats.isFetched ? [] : null), value: chat.chatId ?? undefined, note: `Chats are kept ${keepDays} days after their last message.`, onRefresh: () => void chats.refetch(), onOpen: (c) => void chat.open(c.id) }), _jsx(IconButton, { icon: CopyIcon, label: "Copy chat", disabled: !chat.items.length, onClick: () => copyMarkdown(chatMarkdown(chat.title, chat.items, name), 'Chat') }), _jsx(IconButton, { icon: PlusIcon, label: "New chat", disabled: !chat.items.length, onClick: chat.reset }), onClose && (_jsx(IconButton, { icon: XIcon, label: `Close ${s?.name ?? 'assistant'}`, onClick: onClose }))] }), _jsx(AssistantName.Provider, { value: name, children: _jsx(Screen.Provider, { value: controls, children: _jsx(Messages, { chat: chat, status: s, onSettings: onSettings }) }) }), _jsx(LumenComposer, { name: s?.name ?? 'the assistant', busy: chat.busy, queued: chat.queued, disabled: !s || !!s.wait, commands: allCommands, tiers: tiers, sources: allSources, page: context?.label, onSend: (text, more) => chat.send(text, { ...extra(), ...more }), onStop: chat.stop }), _jsx(UsageLine, { usage: chat.usage, client: client, spent: s?.spend.usd })] }));
187
183
  }
188
184
  function Messages({ chat, status, onSettings, }) {
189
185
  const end = useRef(null);
@@ -711,17 +707,29 @@ function UsageTable({ usage }) {
711
707
  return (_jsxs(Fragment, { children: [_jsx("span", { className: "truncate font-medium", title: p.label, children: r.tier }), _jsx("span", { className: "text-muted-foreground", children: r.kind }), _jsx("span", { className: "num text-right", children: usd(p.usd) }), _jsxs("span", { className: "num text-right", children: [tokens(p.in), " (", hitPct(p), "%)"] }), _jsx("span", { className: "num text-right", children: tokens(p.out) }), _jsx("span", { className: "num text-right", children: p.calls })] }, p.label));
712
708
  }), _jsx("span", { className: cn(total, 'font-semibold'), children: "Total" }), _jsx("span", { className: total }), _jsx("span", { className: cn(total, 'num text-right font-semibold'), children: usd(usage.usd) }), _jsxs("span", { className: cn(total, 'num text-right'), children: [tokens(usage.in), " (", hitPct(usage), "%)"] }), _jsx("span", { className: cn(total, 'num text-right'), children: tokens(usage.out) }), _jsx("span", { className: cn(total, 'num text-right'), children: usage.calls })] }));
713
709
  }
710
+ /**
711
+ * This month's total of every chat and run; it opens the month's history (each model, chat and
712
+ * run), a sheet on a phone.
713
+ */
714
+ function MonthTotal({ client, spent, phone, className, }) {
715
+ const trigger = (_jsxs("button", { type: "button", className: cn('flex shrink-0 items-center gap-1 tabular-nums hover:text-foreground', className), "aria-label": "Usage this month", title: "Usage this month: every chat and run", children: [_jsx(ReceiptIcon, { className: "size-3", "aria-hidden": true }), _jsxs("span", { children: ["This month ", _jsx("span", { className: "num", children: usd(spent) })] })] }));
716
+ return phone ? (_jsxs(Sheet, { children: [_jsx(SheetTrigger, { asChild: true, children: trigger }), _jsxs(SheetContent, { side: "bottom", children: [_jsx(SheetHeader, { children: _jsx(SheetTitle, { children: "Usage" }) }), _jsx(UsageHistoryView, { client: client, className: "px-4 pb-6" })] })] })) : (_jsxs(Popover, { children: [_jsx(PopoverTrigger, { asChild: true, children: trigger }), _jsx(PopoverContent, { align: "end", className: "w-md max-w-[92vw] p-3", children: _jsx(UsageHistoryView, { client: client }) })] }));
717
+ }
714
718
  /**
715
719
  * The chat's cost, tokens in and out and cache hit (a pop-up splits them per part, a sheet on a
716
- * phone), and how full the context is.
720
+ * phone), how full the context is, and this month's total (`MonthTotal`).
717
721
  */
718
- function UsageLine({ usage }) {
722
+ function UsageLine({ usage, client, spent, }) {
719
723
  const phone = useIsMobile();
720
- if (!usage || !usage.calls)
724
+ const month = !!client.usage && spent !== undefined && spent > 0;
725
+ if ((!usage || !usage.calls) && !month)
721
726
  return null;
722
- const ctx = usage.context ?? 0;
727
+ const ctx = usage?.context ?? 0;
728
+ const total = month ? (_jsx(MonthTotal, { client: client, spent: spent ?? 0, phone: phone, className: ctx > 0 ? '' : 'ml-auto' })) : null;
729
+ if (!usage || !usage.calls)
730
+ return (_jsx("div", { className: "flex shrink-0 items-center gap-3 px-3 pb-2 text-muted-foreground text-xs", children: total }));
723
731
  const trigger = (_jsxs("button", { type: "button", className: "flex min-w-0 items-center gap-2 tabular-nums hover:text-foreground", "aria-label": "This chat's usage", children: [_jsxs("span", { className: "num truncate", children: [usd(usage.usd), " \u00B7 \u2191", tokens(usage.in), " \u2193", tokens(usage.out)] }), _jsxs("span", { className: "num flex shrink-0 items-center gap-0.5", title: "Read from cache", children: [_jsx(ZapIcon, { className: "size-3", "aria-hidden": true }), hitPct(usage), "%"] })] }));
724
- return (_jsxs("div", { className: "flex shrink-0 items-center gap-3 px-3 pb-2 text-muted-foreground text-xs", children: [phone ? (_jsxs(Sheet, { children: [_jsx(SheetTrigger, { asChild: true, children: trigger }), _jsxs(SheetContent, { side: "bottom", children: [_jsx(SheetHeader, { children: _jsx(SheetTitle, { children: "This chat's usage" }) }), _jsx("div", { className: "overflow-x-auto px-4 pb-6", children: _jsx(UsageTable, { usage: usage }) })] })] })) : (_jsxs(Popover, { children: [_jsx(PopoverTrigger, { asChild: true, children: trigger }), _jsx(PopoverContent, { align: "start", className: "w-auto p-2", children: _jsx(UsageTable, { usage: usage }) })] })), ctx > 0 && (_jsxs("span", { className: "ml-auto flex items-center gap-1.5", title: `Context ${tokens(ctx)} of ${tokens(CONTEXT_MAX)}`, children: [_jsxs("span", { className: "relative h-1 w-16 overflow-hidden rounded-full bg-muted", children: [_jsx("span", { className: cn('absolute inset-y-0 left-0', ctx > COMPACT_AT ? 'bg-warning' : 'bg-primary/60'), style: { width: `${Math.min(100, (ctx / CONTEXT_MAX) * 100)}%` } }), _jsx("span", { className: "absolute inset-y-0 w-px bg-foreground/40", style: { left: `${(COMPACT_AT / CONTEXT_MAX) * 100}%` } })] }), _jsx("span", { className: "tabular-nums", children: tokens(ctx) })] }))] }));
732
+ return (_jsxs("div", { className: "flex shrink-0 items-center gap-3 px-3 pb-2 text-muted-foreground text-xs", children: [phone ? (_jsxs(Sheet, { children: [_jsx(SheetTrigger, { asChild: true, children: trigger }), _jsxs(SheetContent, { side: "bottom", children: [_jsx(SheetHeader, { children: _jsx(SheetTitle, { children: "This chat's usage" }) }), _jsx("div", { className: "overflow-x-auto px-4 pb-6", children: _jsx(UsageTable, { usage: usage }) })] })] })) : (_jsxs(Popover, { children: [_jsx(PopoverTrigger, { asChild: true, children: trigger }), _jsx(PopoverContent, { align: "start", className: "w-auto p-2", children: _jsx(UsageTable, { usage: usage }) })] })), ctx > 0 && (_jsxs("span", { className: "ml-auto flex items-center gap-1.5", title: `Context ${tokens(ctx)} of ${tokens(CONTEXT_MAX)}`, children: [_jsxs("span", { className: "relative h-1 w-16 overflow-hidden rounded-full bg-muted", children: [_jsx("span", { className: cn('absolute inset-y-0 left-0', ctx > COMPACT_AT ? 'bg-warning' : 'bg-primary/60'), style: { width: `${Math.min(100, (ctx / CONTEXT_MAX) * 100)}%` } }), _jsx("span", { className: "absolute inset-y-0 w-px bg-foreground/40", style: { left: `${(COMPACT_AT / CONTEXT_MAX) * 100}%` } })] }), _jsx("span", { className: "tabular-nums", children: tokens(ctx) })] })), total] }));
725
733
  }
726
734
  /**
727
735
  * The assistant docked in LumenShell: put `button` in `headerActions` and `panel` in `panel`.
@@ -17,6 +17,7 @@ import { Label } from '../ui/label.js';
17
17
  import { Switch } from '../ui/switch.js';
18
18
  import { ToggleGroup, ToggleGroupItem } from '../ui/toggle-group.js';
19
19
  import { allowanceAmount, assistantStatusKey, usd, useAssistantStatus } from './lumen-assistant.js';
20
+ import { UsageHistoryView } from './usage-history.js';
20
21
  const EFFORTS = [
21
22
  { value: 'default', label: 'Model default' },
22
23
  { value: 'low', label: 'Low' },
@@ -266,6 +267,9 @@ function Web({ client }) {
266
267
  function Spend({ client }) {
267
268
  return (_jsx(WithStatus, { client: client, children: (s) => {
268
269
  const sp = s.spend;
270
+ // With the history route: each month, model, chat and run (`UsageHistoryView`).
271
+ if (client.usage)
272
+ return (_jsxs("div", { className: "flex flex-col gap-2", children: [sp.cap != null && sp.payer && (_jsx("p", { className: "text-muted-foreground text-sm", children: `${usd(sp.cap)} a month on ${sp.payer === 'app' ? 'the app key' : 'your key'}` })), _jsx(UsageHistoryView, { client: client })] }));
269
273
  return (_jsxs("div", { className: "flex flex-col gap-3", children: [_jsxs("div", { className: "flex items-baseline gap-2", children: [_jsx("span", { className: "font-semibold text-2xl tabular-nums", children: usd(sp.usd) }), _jsxs("span", { className: "text-muted-foreground text-sm", children: [sp.month, sp.cap != null &&
270
274
  sp.payer &&
271
275
  ` · ${usd(sp.cap)} cap on ${sp.payer === 'app' ? 'the app key' : 'your key'}`] })] }), sp.rows.length ? (_jsx("div", { className: "flex flex-col text-sm", children: sp.rows.map((r) => (_jsxs("div", { className: "flex items-center gap-2 border-b py-2 last:border-b-0", children: [_jsx("span", { className: "min-w-0 flex-1 truncate", children: r.label }), _jsx("span", { className: "text-muted-foreground text-xs", children: r.payer === 'app' ? 'Shared key' : 'Your key' }), _jsxs("span", { className: "w-16 text-right text-muted-foreground text-xs tabular-nums", children: [r.calls, " calls"] }), _jsx("span", { className: "w-16 text-right tabular-nums", children: usd(r.usd) })] }, `${r.label}:${r.payer}`))) })) : (_jsx("p", { className: "text-muted-foreground text-sm", children: "Nothing spent this month." }))] }));
@@ -437,7 +441,7 @@ export function assistantSettingsSections({ client, name, web = true, allowance
437
441
  title: 'Spend',
438
442
  icon: ReceiptIcon,
439
443
  sub: true,
440
- description: 'What answers cost this month',
444
+ description: 'What each model, chat and run cost',
441
445
  content: _jsx(Spend, { client: client }),
442
446
  },
443
447
  ...(allowance
@@ -0,0 +1,15 @@
1
+ import type { UsageHistory } from '@columenlabs/lumen-ui-types/assistant';
2
+ import type { AssistantAdapter } from './client.js';
3
+ export declare const usd: (n: number) => string;
4
+ export declare const tokens: (n: number) => string;
5
+ export declare const assistantUsageKey: (client: AssistantAdapter, month?: string) => string[];
6
+ /** One month of the person's use (this month when `month` is left out); off when the adapter has no `usage`. */
7
+ export declare function useAssistantUsage(client: AssistantAdapter, month?: string, enabled?: boolean): import("@tanstack/react-query").UseQueryResult<UsageHistory, Error>;
8
+ /**
9
+ * A month of use: the total, each model (cost, tokens in and out, calls) and each chat and run,
10
+ * newest first, with ‹ › to step through the months that have any.
11
+ */
12
+ export declare function UsageHistoryView({ client, className, }: {
13
+ client: AssistantAdapter;
14
+ className?: string;
15
+ }): import("react").JSX.Element | null;
@@ -0,0 +1,88 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useQuery } from '@tanstack/react-query';
3
+ import { ChevronLeftIcon, ChevronRightIcon, CpuIcon, MessageSquareIcon } from 'lucide-react';
4
+ import { Fragment, useState } from 'react';
5
+ import { IconButton } from '../icon-button.js';
6
+ import { cn } from '../lib/utils.js';
7
+ import { Spinner } from '../ui/spinner.js';
8
+ export const usd = (n) => n === 0 ? '$0' : n < 0.01 ? '<$0.01' : `$${n.toFixed(n < 10 ? 2 : 0)}`;
9
+ export const tokens = (n) => n < 1000
10
+ ? String(n)
11
+ : n < 1_000_000
12
+ ? `${(n / 1000).toFixed(n < 10_000 ? 1 : 0)}k`
13
+ : `${(n / 1e6).toFixed(1)}M`;
14
+ export const assistantUsageKey = (client, month) => [
15
+ 'lumen-assistant',
16
+ client.key,
17
+ 'usage',
18
+ month ?? 'now',
19
+ ];
20
+ /** One month of the person's use (this month when `month` is left out); off when the adapter has no `usage`. */
21
+ export function useAssistantUsage(client, month, enabled = true) {
22
+ return useQuery({
23
+ queryKey: assistantUsageKey(client, month),
24
+ queryFn: () => client.usage(month),
25
+ enabled: enabled && !!client.usage,
26
+ });
27
+ }
28
+ /** `2026-11` → `November 2026`. */
29
+ const monthName = (m) => {
30
+ const [y, mo] = m.split('-').map(Number);
31
+ return new Date(y ?? 2000, (mo ?? 1) - 1, 1).toLocaleDateString(undefined, {
32
+ month: 'long',
33
+ year: 'numeric',
34
+ });
35
+ };
36
+ /** Today's time, else the day. */
37
+ const when = (at) => {
38
+ const d = new Date(at);
39
+ return d.toDateString() === new Date().toDateString()
40
+ ? d.toLocaleTimeString(undefined, { hour: 'numeric', minute: '2-digit' })
41
+ : d.toLocaleDateString(undefined, { month: 'short', day: 'numeric' });
42
+ };
43
+ /** The model of a part, from its label (`Steady · Sonnet 5.5` → `Sonnet 5.5`). */
44
+ const modelOf = (p) => p.label.split(' · ').slice(1).join(' · ') || p.label;
45
+ /** Rows of one month summed per model (each tier and payer of one model is one row). */
46
+ function perModel(rows) {
47
+ const out = new Map();
48
+ for (const r of rows) {
49
+ const k = r.model ?? modelOf(r);
50
+ const p = out.get(k);
51
+ if (!p)
52
+ out.set(k, { ...r, label: modelOf(r) });
53
+ else {
54
+ p.calls += r.calls;
55
+ p.in += r.in;
56
+ p.read += r.read;
57
+ p.out += r.out;
58
+ p.usd += r.usd;
59
+ }
60
+ }
61
+ return [...out.values()].sort((a, b) => b.usd - a.usd);
62
+ }
63
+ const entryHover = (e) => [
64
+ e.title,
65
+ ...e.parts.map((p) => `${p.label}: ${usd(p.usd)} · ${tokens(p.in)} in · ${tokens(p.out)} out · ${p.calls} calls`),
66
+ ].join('\n');
67
+ function Entry({ e }) {
68
+ const Icon = e.kind === 'chat' ? MessageSquareIcon : CpuIcon;
69
+ return (_jsxs("li", { className: "flex min-w-0 items-center gap-2 py-1.5 text-xs", title: entryHover(e), children: [_jsx(Icon, { className: "size-3.5 shrink-0 text-muted-foreground", "aria-hidden": true }), _jsx("span", { className: "min-w-0 flex-1 truncate", children: e.title }), _jsx("time", { className: "shrink-0 text-muted-foreground", children: when(e.at) }), _jsxs("span", { className: "num w-20 shrink-0 text-right text-muted-foreground", children: ["\u2191", tokens(e.in), " \u2193", tokens(e.out)] }), _jsx("span", { className: "num w-14 shrink-0 text-right", children: usd(e.usd) })] }));
70
+ }
71
+ /**
72
+ * A month of use: the total, each model (cost, tokens in and out, calls) and each chat and run,
73
+ * newest first, with ‹ › to step through the months that have any.
74
+ */
75
+ export function UsageHistoryView({ client, className, }) {
76
+ const [month, setMonth] = useState(undefined);
77
+ const q = useAssistantUsage(client, month);
78
+ const h = q.data;
79
+ const at = h ? h.months.indexOf(h.month) : -1;
80
+ const older = h && at >= 0 ? h.months[at + 1] : undefined;
81
+ const newer = h && at > 0 ? h.months[at - 1] : undefined;
82
+ if (!client.usage)
83
+ return null;
84
+ if (!h)
85
+ return (_jsx("div", { className: "flex justify-center p-4", children: q.isError ? (_jsx("p", { className: "text-muted-foreground text-sm", children: "Usage didn't load. Try again later." })) : (_jsx(Spinner, {})) }));
86
+ const models = perModel(h.rows);
87
+ return (_jsxs("div", { className: cn('flex min-w-0 flex-col gap-3', className), children: [_jsxs("div", { className: "flex items-center gap-1", children: [_jsx(IconButton, { icon: ChevronLeftIcon, label: "Earlier month", disabled: !older, onClick: () => setMonth(older) }), _jsx("span", { className: "min-w-0 flex-1 truncate text-center font-medium text-sm", children: monthName(h.month) }), _jsx(IconButton, { icon: ChevronRightIcon, label: "Later month", disabled: !newer, onClick: () => setMonth(newer) })] }), _jsxs("div", { className: "flex items-baseline gap-2", children: [_jsx("span", { className: "num font-semibold text-2xl", children: usd(h.usd) }), _jsx("span", { className: "text-muted-foreground text-xs", children: h.entries.length === 1 ? '1 chat or run' : `${h.entries.length} chats and runs` })] }), models.length > 0 && (_jsxs("div", { className: "grid grid-cols-[minmax(0,1fr)_auto_auto_auto_auto] gap-x-3 gap-y-0.5 text-xs", children: [_jsx("span", { className: "text-muted-foreground", children: "Model" }), _jsx("span", { className: "text-right text-muted-foreground", children: "Cost" }), _jsx("span", { className: "text-right text-muted-foreground", children: "In" }), _jsx("span", { className: "text-right text-muted-foreground", children: "Out" }), _jsx("span", { className: "text-right text-muted-foreground", children: "Calls" }), models.map((m) => (_jsxs(Fragment, { children: [_jsx("span", { className: "truncate font-medium", title: m.label, children: m.label }), _jsx("span", { className: "num text-right", children: usd(m.usd) }), _jsx("span", { className: "num text-right", children: tokens(m.in) }), _jsx("span", { className: "num text-right", children: tokens(m.out) }), _jsx("span", { className: "num text-right", children: m.calls })] }, m.label)))] })), h.entries.length ? (_jsx("ul", { className: "-my-1.5 max-h-72 divide-y overflow-y-auto", children: h.entries.map((e) => (_jsx(Entry, { e: e }, e.id))) })) : (_jsx("p", { className: "text-muted-foreground text-sm", children: "Nothing used this month." }))] }));
88
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@columenlabs/lumen-ui",
3
- "version": "0.1.209",
3
+ "version": "0.1.210",
4
4
  "description": "Lumen: shared React components (frame, views, widgets, charts, drawers).",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -49,7 +49,7 @@
49
49
  "@codemirror/lang-javascript": "6.2.5",
50
50
  "@codemirror/lang-json": "6.0.2",
51
51
  "@codemirror/lang-sql": "6.10.0",
52
- "@columenlabs/lumen-ui-types": "0.1.209",
52
+ "@columenlabs/lumen-ui-types": "0.1.210",
53
53
  "@dnd-kit/core": "6.3.1",
54
54
  "@dnd-kit/sortable": "10.0.0",
55
55
  "@dnd-kit/utilities": "3.2.2",