@columenlabs/lumen-ui 0.1.257 → 0.1.259

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.
@@ -2,6 +2,6 @@ export { type AssistantAdapter, assistantClient } from './client.js';
2
2
  export { ChatImage } from './chat-image.js';
3
3
  export { type AssistantCommand, type AssistantMode, type ComposerExtra, LumenAssistant, type LumenAssistantProps, LumenComposer, assistantStatusKey, readAssistantImage, useAssistantStatus, useLumenAssistantDock, } from './lumen-assistant.js';
4
4
  export { assistantAdminSections, assistantSettingsSections } from './settings.js';
5
- export { AssistantChatCost, UsageHistoryView, assistantUsageKey, tokens as formatTokens, usd as formatUsd, useAssistantUsage, } from './usage-history.js';
5
+ export { AssistantChatCost, AssistantUsageTable, UsageHistoryView, assistantUsageKey, tokens as formatTokens, usd as formatUsd, useAssistantUsage, } from './usage-history.js';
6
6
  export { type AssistantSource, type AssistantSourceItem, type ComposeRequest, type PageSelection, composeAssistant, selectionRef, useAssistantSource, usePageSelection, useShownAssistantSources, } from './sources.js';
7
7
  export { type AssistantChatState, type LiveItem, applyEvent, useAssistantChat, } from './use-assistant-chat.js';
@@ -5,6 +5,6 @@ export { assistantClient } from './client.js';
5
5
  export { ChatImage } from './chat-image.js';
6
6
  export { LumenAssistant, LumenComposer, assistantStatusKey, readAssistantImage, useAssistantStatus, useLumenAssistantDock, } from './lumen-assistant.js';
7
7
  export { assistantAdminSections, assistantSettingsSections } from './settings.js';
8
- export { AssistantChatCost, UsageHistoryView, assistantUsageKey, tokens as formatTokens, usd as formatUsd, useAssistantUsage, } from './usage-history.js';
8
+ export { AssistantChatCost, AssistantUsageTable, UsageHistoryView, assistantUsageKey, tokens as formatTokens, usd as formatUsd, useAssistantUsage, } from './usage-history.js';
9
9
  export { composeAssistant, selectionRef, useAssistantSource, usePageSelection, useShownAssistantSources, } from './sources.js';
10
10
  export { applyEvent, useAssistantChat, } from './use-assistant-chat.js';
@@ -19,7 +19,7 @@ export declare function assistantAdminSections({ client, allowance, providers, }
19
19
  * The assistant's Settings sections: the assistant, then Model providers, Models, Tools, Web and
20
20
  * Spend under it. Pass them to LumenShell's `shared` (or an app's own `settings`).
21
21
  */
22
- export declare function assistantSettingsSections({ client, name, web, allowance, }: {
22
+ export declare function assistantSettingsSections({ client, name, web, allowance, usage, }: {
23
23
  client: AssistantAdapter;
24
24
  /** The assistant's name, the parent section's title. */
25
25
  name: string;
@@ -27,4 +27,7 @@ export declare function assistantSettingsSections({ client, name, web, allowance
27
27
  web?: boolean;
28
28
  /** Show Allowance: what is left on the shared keys, and Request more (`allowance` on the server). */
29
29
  allowance?: boolean;
30
+ /** Show Usage: this month's chats and runs by who paid (key, sign-in or credits); Model
31
+ * providers then leaves out its own spend table. */
32
+ usage?: boolean;
30
33
  }): SettingsSectionDef[];
@@ -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 { CoinsIcon, GlobeIcon, InboxIcon, KeyRoundIcon, LayersIcon, Link2Icon, LogInIcon, ReceiptIcon, SendIcon, SparklesIcon, WrenchIcon, } from 'lucide-react';
3
+ import { ChartColumnIcon, CoinsIcon, GlobeIcon, InboxIcon, KeyRoundIcon, LayersIcon, Link2Icon, LogInIcon, ReceiptIcon, SendIcon, SparklesIcon, WrenchIcon, } from 'lucide-react';
4
4
  import { useId, useState } from 'react';
5
5
  import { ConnectionLogo, LumenConnection, } from '../connection.js';
6
6
  import { LumenExpandList, LumenExpandRow } from '../expand-list.js';
@@ -18,7 +18,7 @@ import { Label } from '../ui/label.js';
18
18
  import { Switch } from '../ui/switch.js';
19
19
  import { ToggleGroup, ToggleGroupItem } from '../ui/toggle-group.js';
20
20
  import { allowanceAmount, assistantStatusKey, usd, useAssistantStatus } from './lumen-assistant.js';
21
- import { OwnKeyUsage, UsageHistoryView } from './usage-history.js';
21
+ import { AssistantUsageTable, OwnKeyUsage, UsageHistoryView } from './usage-history.js';
22
22
  const EFFORTS = [
23
23
  { value: 'default', label: 'Model default' },
24
24
  { value: 'low', label: 'Low' },
@@ -163,14 +163,14 @@ function OwnProviderKey({ client, p }) {
163
163
  return (_jsx(LumenConnection, { id: p.id, name: p.label, ways: ways, use: p.use, onUse: (use) => apply(`use:${p.id}`, () => client.settings({ own_use: { [p.id]: use } })) }));
164
164
  }
165
165
  /** A person's own keys, one row a provider, and what they may spend on them. */
166
- function Providers({ client }) {
166
+ function Providers({ client, usage }) {
167
167
  const { apply } = useApply(client);
168
168
  return (_jsx(WithStatus, { client: client, children: (s) => {
169
169
  // A provider the app gives this person no way of their own on shows no row: what shows here
170
170
  // is exactly what the server may pay with.
171
171
  const rows = s.providers.filter((p) => !p.own_ways || p.own_ways.length > 0);
172
172
  const ownSet = rows.some(ownUsable);
173
- return (_jsxs("div", { className: "flex flex-col", children: [_jsx(LumenExpandList, { children: rows.map((p) => (_jsx(OwnProviderKey, { client: client, p: p }, p.id))) }), ownSet && (_jsx(SettingField, { label: "Answers run on", info: "Your own key or plan first, with the shared credits behind it; or the shared credits first.", children: _jsxs(ToggleGroup, { type: "single", variant: "outline", size: "sm", spacing: 0, value: s.pay, "aria-label": "Answers run on", onValueChange: (pay) => pay && void apply('pay', () => client.settings({ pay: pay })), children: [_jsx(ToggleGroupItem, { value: "own", children: "Your own first" }), _jsx(ToggleGroupItem, { value: "app", children: "Credits first" })] }) })), ownSet && (_jsx(SettingField, { label: "Your keys may spend", info: "On your keys, every provider together.", children: _jsx(CapInput, { label: "Your monthly cap", value: s.caps.own, onSave: (n) => void apply('own', () => client.ownCap(n)) }) })), ownSet && _jsx(OwnCapUse, { s: s }), _jsx(OwnKeyUsage, { client: client, always: ownSet, usable: rows.filter(ownUsable).map((p) => p.id) })] }));
173
+ return (_jsxs("div", { className: "flex flex-col", children: [_jsx(LumenExpandList, { children: rows.map((p) => (_jsx(OwnProviderKey, { client: client, p: p }, p.id))) }), ownSet && (_jsx(SettingField, { label: "Answers run on", info: "Your own key or plan first, with the shared credits behind it; or the shared credits first.", children: _jsxs(ToggleGroup, { type: "single", variant: "outline", size: "sm", spacing: 0, value: s.pay, "aria-label": "Answers run on", onValueChange: (pay) => pay && void apply('pay', () => client.settings({ pay: pay })), children: [_jsx(ToggleGroupItem, { value: "own", children: "Your own first" }), _jsx(ToggleGroupItem, { value: "app", children: "Credits first" })] }) })), ownSet && (_jsx(SettingField, { label: "Your keys may spend", info: "On your keys, every provider together.", children: _jsx(CapInput, { label: "Your monthly cap", value: s.caps.own, onSave: (n) => void apply('own', () => client.ownCap(n)) }) })), ownSet && _jsx(OwnCapUse, { s: s }), !usage && (_jsx(OwnKeyUsage, { client: client, always: ownSet, usable: rows.filter(ownUsable).map((p) => p.id) }))] }));
174
174
  } }));
175
175
  }
176
176
  /**
@@ -461,7 +461,7 @@ export function assistantAdminSections({ client, allowance = false, providers, }
461
461
  * The assistant's Settings sections: the assistant, then Model providers, Models, Tools, Web and
462
462
  * Spend under it. Pass them to LumenShell's `shared` (or an app's own `settings`).
463
463
  */
464
- export function assistantSettingsSections({ client, name, web = true, allowance = false, }) {
464
+ export function assistantSettingsSections({ client, name, web = true, allowance = false, usage = false, }) {
465
465
  return [
466
466
  {
467
467
  id: 'assistant',
@@ -476,7 +476,7 @@ export function assistantSettingsSections({ client, name, web = true, allowance
476
476
  icon: KeyRoundIcon,
477
477
  sub: true,
478
478
  description: 'Your own key for each provider',
479
- content: _jsx(Providers, { client: client }),
479
+ content: _jsx(Providers, { client: client, usage: usage }),
480
480
  },
481
481
  {
482
482
  id: 'assistant-models',
@@ -514,6 +514,18 @@ export function assistantSettingsSections({ client, name, web = true, allowance
514
514
  description: 'What each model, chat and run cost',
515
515
  content: _jsx(Spend, { client: client }),
516
516
  },
517
+ ...(usage
518
+ ? [
519
+ {
520
+ id: 'assistant-usage',
521
+ title: 'Usage',
522
+ icon: ChartColumnIcon,
523
+ sub: true,
524
+ description: 'Every chat and run this month, and who paid',
525
+ content: _jsx(AssistantUsageTable, { client: client }),
526
+ },
527
+ ]
528
+ : []),
517
529
  ...(allowance
518
530
  ? [
519
531
  {
@@ -26,7 +26,7 @@ export declare function OwnKeyUsage({ client, always, usable, }: {
26
26
  }): import("react").JSX.Element | null;
27
27
  /**
28
28
  * What one chat has cost so far, as a chip for the assistant's header: what was charged (credits or
29
- * the person's key) plus a plan sign-in's calls at API rates, shown ≈ when the plan's part is in it.
29
+ * the person's key) plus a plan sign-in's calls at API rates (an estimate, said in its hover).
30
30
  * Nothing while the chat has no history line. The hover splits it.
31
31
  */
32
32
  export declare function AssistantChatCost({ client, chatId, busy, }: {
@@ -35,3 +35,12 @@ export declare function AssistantChatCost({ client, chatId, busy, }: {
35
35
  /** The chat is answering: the chip reads its history again when the answer ends. */
36
36
  busy?: boolean;
37
37
  }): import("react").JSX.Element | null;
38
+ /**
39
+ * This month's use, one row a chat or run: its Type (the person's API key, their plan sign-in, or
40
+ * the app's credits), Date, Calls and Amount, grouped by Type, Date or not at all, filtered by
41
+ * type or name. A row's hover gives its tokens and cost. A sign-in's amount is an estimate: the
42
+ * same tokens at the model's API rates (the plan charges $0).
43
+ */
44
+ export declare function AssistantUsageTable({ client }: {
45
+ client: AssistantAdapter;
46
+ }): import("react").JSX.Element | null;
@@ -1,17 +1,20 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useQuery } from '@tanstack/react-query';
3
- import { ChevronLeftIcon, ChevronRightIcon, CpuIcon, MessageSquareIcon } from 'lucide-react';
4
- import { Fragment, useEffect, useState } from 'react';
3
+ import { CalendarIcon, ChevronLeftIcon, ChevronRightIcon, CpuIcon, HashIcon, KeyRoundIcon, MessageSquareIcon, ReceiptIcon, } from 'lucide-react';
4
+ import { Fragment, useEffect, useMemo, useState } from 'react';
5
5
  import { DetailsTable } from '../details-table.js';
6
6
  import { IconButton } from '../icon-button.js';
7
7
  import { InfoLabel } from '../info-tip.js';
8
8
  import { cn } from '../lib/utils.js';
9
+ import { LumenTokenizedFilter, useLumenFilter } from '../lumen-filter.js';
10
+ import { LumenTable } from '../lumen-view.js';
9
11
  import { SettingField } from '../setting-parts.js';
10
12
  import { Badge } from '../ui/badge.js';
11
13
  import { Label } from '../ui/label.js';
12
14
  import { Spinner } from '../ui/spinner.js';
13
15
  import { ToggleGroup, ToggleGroupItem } from '../ui/toggle-group.js';
14
16
  import { Tooltip, TooltipContent, TooltipTrigger } from '../ui/tooltip.js';
17
+ import { CHIP_TONE } from '../view-model.js';
15
18
  export const usd = (n) => n === 0 ? '$0' : n < 0.01 ? '<$0.01' : `$${n.toFixed(n < 10 ? 2 : 0)}`;
16
19
  export const tokens = (n) => n < 1000
17
20
  ? String(n)
@@ -155,10 +158,10 @@ export function OwnKeyUsage({ client, always, usable, }) {
155
158
  const named = (e) => e.own_plan_calls ? `${e.title} · ${PLAN_NAME}, ${calls(e.own_plan_calls)}` : e.title;
156
159
  const total = keyTotal + planTotal + (planEarlier ? planListEarlier : 0);
157
160
  const anyPlan = planTotal > 0 || (planEarlier > 0 && planListEarlier > 0);
158
- // An amount with plan calls in it reads ≈: the plan's part is what API rates would charge.
159
- const amount = (key, plan, title) => plan > 0 ? (_jsxs("span", { title: title, children: ["\u2248", usd(key + plan)] }, "a")) : (usd(key));
161
+ // An amount with plan calls in it is an estimate: the plan's part is what API rates would charge.
162
+ const amount = (key, plan, title) => plan > 0 ? (_jsx("span", { title: title, children: usd(key + plan) }, "a")) : (usd(key));
160
163
  const planTitle = (key, plan) => `${key > 0 ? `${usd(key)} on your key + ` : ''}about ${usd(plan)} at API rates on your ${PLAN_NAME}, which charged $0`;
161
- return (_jsxs("div", { className: "flex flex-col gap-2 border-t border-border/60 pb-3 first:border-t-0", children: [_jsx(SettingField, { label: "Spent on your keys", info: "Each chat and run your own key or plan paid for, by its last use. A \u2248 amount prices plan calls at API rates; the plan charges $0.", className: "border-t-0", end: _jsx(ToggleGroup, { type: "single", variant: "outline", size: "sm", spacing: 0, value: range, "aria-label": "Range", onValueChange: (v) => v && setRange(v), children: OWN_RANGES.map((r) => (_jsx(ToggleGroupItem, { value: r.id, "aria-label": `Last ${r.id}`, children: r.id }, r.id))) }) }), sessions.length || untracked || gone || planEarlier ? (_jsx(DetailsTable, { head: [
164
+ return (_jsxs("div", { className: "flex flex-col gap-2 border-t border-border/60 pb-3 first:border-t-0", children: [_jsx(SettingField, { label: "Spent on your keys", info: "Each chat and run your own key or plan paid for, by its last use. Plan amounts are estimates at API rates; the plan charges $0.", className: "border-t-0", end: _jsx(ToggleGroup, { type: "single", variant: "outline", size: "sm", spacing: 0, value: range, "aria-label": "Range", onValueChange: (v) => v && setRange(v), children: OWN_RANGES.map((r) => (_jsx(ToggleGroupItem, { value: r.id, "aria-label": `Last ${r.id}`, children: r.id }, r.id))) }) }), sessions.length || untracked || gone || planEarlier ? (_jsx(DetailsTable, { head: [
162
165
  { label: 'Session', grow: true },
163
166
  { label: 'Date', hideOnPhone: true },
164
167
  { label: 'Amount', right: true },
@@ -217,7 +220,7 @@ export function OwnKeyUsage({ client, always, usable, }) {
217
220
  }
218
221
  /**
219
222
  * What one chat has cost so far, as a chip for the assistant's header: what was charged (credits or
220
- * the person's key) plus a plan sign-in's calls at API rates, shown ≈ when the plan's part is in it.
223
+ * the person's key) plus a plan sign-in's calls at API rates (an estimate, said in its hover).
221
224
  * Nothing while the chat has no history line. The hover splits it.
222
225
  */
223
226
  export function AssistantChatCost({ client, chatId, busy, }) {
@@ -241,9 +244,173 @@ export function AssistantChatCost({ client, chatId, busy, }) {
241
244
  own > 0 ? `${usd(own)} on your key` : null,
242
245
  charged - own >= 0.005 ? `${usd(charged - own)} on credits` : null,
243
246
  e.own_plan_calls
244
- ? `${e.own_plan_calls === 1 ? '1 call' : `${e.own_plan_calls} calls`} on your ${PLAN_NAME}: ≈${usd(plan)} at API rates (charged $0)`
247
+ ? `${e.own_plan_calls === 1 ? '1 call' : `${e.own_plan_calls} calls`} on your ${PLAN_NAME}: ${usd(plan)} estimate at API rates (charged $0)`
245
248
  : null,
246
249
  ].filter(Boolean);
247
- const text = `${plan > 0 ? '≈' : ''}${usd(total)}`;
250
+ const text = usd(total);
248
251
  return (_jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { asChild: true, children: _jsx("button", { type: "button", "aria-label": `This chat cost ${text}`, className: "shrink-0 rounded-md outline-none focus-visible:ring-3 focus-visible:ring-ring/50", children: _jsx(Badge, { variant: "outline", className: "num whitespace-nowrap", children: text }) }) }), _jsx(TooltipContent, { children: _jsxs("div", { className: "flex flex-col gap-0.5", children: [_jsx("span", { className: "font-medium", children: "This chat" }), lines.map((l) => (_jsx("span", { children: l }, l)))] }) })] }));
249
252
  }
253
+ const TYPE_LABEL = {
254
+ key: 'API key',
255
+ plan: 'Sign-in',
256
+ credits: 'Credits',
257
+ };
258
+ const TYPE_TONE = { key: 'sky', plan: 'amber', credits: 'muted' };
259
+ /** Dollars with no `<` or `≈` (the table's note says which amounts are estimates). */
260
+ const money = (n) => (n === 0 ? '$0' : `$${n.toFixed(n < 10 ? 2 : 0)}`);
261
+ const amountOf = (r) => r.key + r.credits + r.plan;
262
+ const EARLIER = 'Earlier';
263
+ /** This month's rows: every chat and run, then the spend no chat or run carries, by who paid. */
264
+ function usageRows(h) {
265
+ const out = h.entries.map((e) => {
266
+ const key = e.own_usd ?? 0;
267
+ const plan = e.own_plan_usd ?? 0;
268
+ const type = plan > 0 || e.own_plan_calls ? 'plan' : key > 0 ? 'key' : 'credits';
269
+ return {
270
+ id: e.id,
271
+ title: e.title,
272
+ type,
273
+ at: e.at,
274
+ calls: e.calls,
275
+ tokens: { in: e.in, read: e.read, out: e.out },
276
+ key,
277
+ credits: Math.max(0, e.usd - key),
278
+ plan,
279
+ };
280
+ });
281
+ const sum = (xs) => xs.reduce((a, n) => a + n, 0);
282
+ const earlier = (type, title, calls, amount) => amount >= 0.01 &&
283
+ out.push({
284
+ id: `earlier:${type}:${title}`,
285
+ title,
286
+ type,
287
+ at: 0,
288
+ calls,
289
+ key: type === 'key' ? amount : 0,
290
+ credits: type === 'credits' ? amount : 0,
291
+ plan: type === 'plan' ? amount : 0,
292
+ });
293
+ const own = h.rows.filter((r) => r.payer === 'own' && !r.free);
294
+ const plans = own.filter((r) => r.way === 'plan' || (!r.way && r.usd < 0.0005));
295
+ const keys = own.filter((r) => !plans.includes(r));
296
+ const app = h.rows.filter((r) => r.payer !== 'own' && !r.free);
297
+ earlier('key', 'Earlier this month, not per chat', Math.max(0, sum(keys.map((r) => r.calls)) - sum(h.entries.filter((e) => e.own_usd).map((e) => e.calls))), sum(keys.map((r) => r.usd)) - sum(h.entries.map((e) => e.own_usd ?? 0)));
298
+ earlier('plan', 'Earlier this month, not per chat', Math.max(0, sum(plans.map((r) => r.calls)) - sum(h.entries.map((e) => e.own_plan_calls ?? 0))), sum(plans.map((r) => r.list_usd ?? 0)) - sum(h.entries.map((e) => e.own_plan_usd ?? 0)));
299
+ earlier('credits', 'Earlier this month, not per chat', Math.max(0, sum(app.map((r) => r.calls)) -
300
+ sum(h.entries.filter((e) => e.usd > (e.own_usd ?? 0)).map((e) => e.calls))), sum(app.map((r) => r.usd)) - sum(h.entries.map((e) => Math.max(0, e.usd - (e.own_usd ?? 0)))));
301
+ return out;
302
+ }
303
+ /** A row's hover: its tokens (cache reads as a share of input) and what it cost, by who paid. */
304
+ function UsageTip({ r, children }) {
305
+ const t = r.tokens;
306
+ const lines = [
307
+ ...(t
308
+ ? [
309
+ ['Input', tokens(t.in)],
310
+ ['Cache read', `${tokens(t.read)} · ${t.in ? Math.round((t.read / t.in) * 100) : 0}%`],
311
+ ['Output', tokens(t.out)],
312
+ ]
313
+ : []),
314
+ ...(r.key > 0 ? [['Cost (your key)', money(r.key)]] : []),
315
+ ...(r.credits > 0 || (r.type === 'credits' && !r.plan)
316
+ ? [['Credits', money(r.credits)]]
317
+ : []),
318
+ ...(r.plan > 0 || r.type === 'plan'
319
+ ? [['Estimate (API rates)', money(r.plan)]]
320
+ : []),
321
+ ];
322
+ return (_jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { asChild: true, children: children }), _jsx(TooltipContent, { className: "max-w-72", children: _jsxs("div", { className: "flex flex-col gap-1", children: [_jsx("span", { className: "truncate font-medium", children: r.title }), _jsx("dl", { className: "grid grid-cols-[auto_auto] gap-x-4 gap-y-0.5", children: lines.map(([k, v]) => (_jsxs(Fragment, { children: [_jsx("dt", { className: "opacity-75", children: k }), _jsx("dd", { className: "num text-right", children: v })] }, k))) }), !t && _jsx("span", { className: "opacity-75", children: "Spent before each chat kept its own count." })] }) })] }));
323
+ }
324
+ const dayOf = (at) => at ? new Date(at).toLocaleDateString(undefined, { month: 'short', day: 'numeric' }) : EARLIER;
325
+ const USAGE_FIELDS = [
326
+ {
327
+ key: 'title',
328
+ label: 'Session',
329
+ icon: MessageSquareIcon,
330
+ format: 'text',
331
+ value: (r) => r.title,
332
+ render: (r) => (_jsx(UsageTip, { r: r, children: _jsx("span", {
333
+ // biome-ignore lint/a11y/noNoninteractiveTabindex: a keyboard reaches the row's hover
334
+ tabIndex: 0, className: cn('rounded-sm outline-none focus-visible:ring-3 focus-visible:ring-ring/50', !r.at && 'text-muted-foreground'), children: r.title }) })),
335
+ },
336
+ {
337
+ key: 'type',
338
+ label: 'Type',
339
+ icon: KeyRoundIcon,
340
+ format: 'text',
341
+ groupable: true,
342
+ filterable: true,
343
+ value: (r) => TYPE_LABEL[r.type],
344
+ render: (r) => (_jsx(UsageTip, { r: r, children: _jsx(Badge, { variant: "outline", className: cn('whitespace-nowrap', CHIP_TONE[TYPE_TONE[r.type]]), children: TYPE_LABEL[r.type] }) })),
345
+ },
346
+ {
347
+ key: 'at',
348
+ label: 'Date',
349
+ icon: CalendarIcon,
350
+ format: 'integer',
351
+ hideOnPhone: true,
352
+ value: (r) => r.at,
353
+ render: (r) => (_jsx(UsageTip, { r: r, children: r.at ? (_jsx("time", { dateTime: new Date(r.at).toISOString(), className: "text-muted-foreground", children: when(r.at) })) : (_jsx("span", { className: "text-muted-foreground", children: EARLIER })) })),
354
+ },
355
+ {
356
+ key: 'calls',
357
+ label: 'Calls',
358
+ icon: HashIcon,
359
+ format: 'integer',
360
+ hideOnPhone: true,
361
+ aggregate: 'sum',
362
+ value: (r) => r.calls,
363
+ render: (r) => (_jsx(UsageTip, { r: r, children: _jsx("span", { children: r.calls }) })),
364
+ },
365
+ {
366
+ key: 'amount',
367
+ label: 'Amount',
368
+ icon: ReceiptIcon,
369
+ format: 'price',
370
+ aggregate: 'sum',
371
+ value: (r) => amountOf(r),
372
+ render: (r) => (_jsx(UsageTip, { r: r, children: _jsx("span", { children: money(amountOf(r)) }) })),
373
+ rollRender: (rows) => money(rows.reduce((a, r) => a + amountOf(r), 0)),
374
+ },
375
+ ];
376
+ /** Grouping by date: each day, then the spend from before chats kept their own count. */
377
+ const DAY_FIELD = {
378
+ key: 'day',
379
+ label: 'Date',
380
+ icon: CalendarIcon,
381
+ format: 'text',
382
+ groupable: true,
383
+ value: (r) => dayOf(r.at),
384
+ };
385
+ const USAGE_DOMAIN = {
386
+ id: 'assistant-usage',
387
+ fields: USAGE_FIELDS,
388
+ rowKey: (r) => r.id,
389
+ text: (r) => [r.title],
390
+ formulas: false,
391
+ };
392
+ const USAGE_FILTER = [
393
+ { key: 'type', label: 'Type', icon: KeyRoundIcon, values: (r) => [TYPE_LABEL[r.type]] },
394
+ ];
395
+ const GROUPS = [
396
+ { id: 'type', label: 'Type', field: USAGE_FIELDS[1] },
397
+ { id: 'none', label: 'None', field: null },
398
+ { id: 'date', label: 'Date', field: DAY_FIELD },
399
+ ];
400
+ /**
401
+ * This month's use, one row a chat or run: its Type (the person's API key, their plan sign-in, or
402
+ * the app's credits), Date, Calls and Amount, grouped by Type, Date or not at all, filtered by
403
+ * type or name. A row's hover gives its tokens and cost. A sign-in's amount is an estimate: the
404
+ * same tokens at the model's API rates (the plan charges $0).
405
+ */
406
+ export function AssistantUsageTable({ client }) {
407
+ const [group, setGroup] = useState('type');
408
+ const q = useAssistantUsage(client);
409
+ const rows = useMemo(() => (q.data ? usageRows(q.data) : []), [q.data]);
410
+ const filter = useLumenFilter(rows, USAGE_FILTER, { text: (r) => [r.title] });
411
+ if (!client.usage)
412
+ return null;
413
+ if (!q.data)
414
+ 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, {})) }));
415
+ return (_jsxs("div", { className: "flex min-w-0 flex-col gap-2", children: [_jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [_jsx(LumenTokenizedFilter, { filter: filter, label: "Filter usage", placeholder: "Filter: type, or a session name", className: "min-w-48 flex-1", link: null }), _jsx(ToggleGroup, { type: "single", variant: "outline", size: "sm", spacing: 0, value: group, "aria-label": "Group by", onValueChange: (v) => v && setGroup(v), children: GROUPS.map((g) => (_jsx(ToggleGroupItem, { value: g.id, "aria-label": `Group by ${g.label}`, children: g.label }, g.id))) })] }), _jsx(LumenTable, { domain: USAGE_DOMAIN, rows: filter.results, groupBy: GROUPS.find((g) => g.id === group)?.field ?? null, sort: { key: 'at', desc: true }, emptyState: rows.length ? undefined : (_jsx("p", { className: "text-muted-foreground text-sm", children: "Nothing used this month." })), onClearFilter: filter.clear }), _jsxs("p", { className: "text-muted-foreground text-xs", children: ["Sign-in amounts are estimates: your ", PLAN_NAME, " charges $0, so the amount is what the same tokens would cost at the model's API rates. Key and credit amounts are what was charged."] })] }));
416
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@columenlabs/lumen-ui",
3
- "version": "0.1.257",
3
+ "version": "0.1.259",
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.257",
52
+ "@columenlabs/lumen-ui-types": "0.1.259",
53
53
  "@dnd-kit/core": "6.3.1",
54
54
  "@dnd-kit/sortable": "10.0.0",
55
55
  "@dnd-kit/utilities": "3.2.2",