@columenlabs/lumen-ui 0.1.301 → 0.1.302

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.
@@ -36,10 +36,12 @@ export declare function AssistantChatCost({ client, chatId, busy, }: {
36
36
  busy?: boolean;
37
37
  }): import("react").JSX.Element | null;
38
38
  /**
39
- * This month's use, one row a session (a chat, or every run of one task on one thing): 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).
39
+ * This month's use, one row a session (a chat, or every run of one task on one thing): its
40
+ * Category (Chat, Edit, Rebuild: `UsageEntry.category`), Type (the person's API key, their plan
41
+ * sign-in, or the app's credits), Date, Calls and Amount, grouped by Date (first), Category, Type or
42
+ * not at all, filtered by category, type or name, PAGE_SIZE rows a page (a group's totals count all
43
+ * its rows). A row's hover gives its tokens and cost. A sign-in's amount is an estimate: the same
44
+ * tokens at the model's API rates (the plan charges $0).
43
45
  */
44
46
  export declare function AssistantUsageTable({ client }: {
45
47
  client: AssistantAdapter;
@@ -1,6 +1,6 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useQuery } from '@tanstack/react-query';
3
- import { CalendarIcon, ChevronLeftIcon, ChevronRightIcon, CpuIcon, HashIcon, KeyRoundIcon, MessageSquareIcon, MessagesSquareIcon, ReceiptIcon, } from 'lucide-react';
3
+ import { CalendarIcon, ChevronLeftIcon, ChevronRightIcon, CpuIcon, HashIcon, KeyRoundIcon, LayersIcon, MessageSquareIcon, MessagesSquareIcon, ReceiptIcon, } from 'lucide-react';
4
4
  import { Fragment, useEffect, useMemo, useState } from 'react';
5
5
  import { DetailsTable } from '../details-table.js';
6
6
  import { IconButton } from '../icon-button.js';
@@ -8,6 +8,7 @@ import { InfoLabel } from '../info-tip.js';
8
8
  import { cn } from '../lib/utils.js';
9
9
  import { LumenTokenizedFilter, useLumenFilter } from '../lumen-filter.js';
10
10
  import { LumenTable } from '../lumen-view.js';
11
+ import { LumenPageFooter } from '../pager.js';
11
12
  import { SettingField } from '../setting-parts.js';
12
13
  import { Badge } from '../ui/badge.js';
13
14
  import { Label } from '../ui/label.js';
@@ -280,6 +281,7 @@ function usageRows(h) {
280
281
  id,
281
282
  title: e.title,
282
283
  type: 'credits',
284
+ category: s?.category ?? (e.category || (e.kind === 'chat' ? 'Chat' : (e.task ?? 'Run'))),
283
285
  at: Math.max(s?.at ?? 0, e.at),
284
286
  calls: (s?.calls ?? 0) + e.calls,
285
287
  tokens: {
@@ -320,6 +322,7 @@ function usageRows(h) {
320
322
  id: `earlier:${type}:${title}`,
321
323
  title,
322
324
  type,
325
+ category: EARLIER,
323
326
  at: 0,
324
327
  calls,
325
328
  key: type === 'key' ? amount : 0,
@@ -430,6 +433,17 @@ const USAGE_FIELDS = [
430
433
  value: (r) => TYPE_LABEL[r.type],
431
434
  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] }) })),
432
435
  },
436
+ {
437
+ key: 'category',
438
+ label: 'Category',
439
+ icon: LayersIcon,
440
+ format: 'text',
441
+ groupable: true,
442
+ filterable: true,
443
+ hideOnPhone: true,
444
+ value: (r) => r.category,
445
+ render: (r) => (_jsx(UsageTip, { r: r, children: _jsx(Badge, { variant: "outline", className: cn('whitespace-nowrap', CHIP_TONE.muted), children: r.category }) })),
446
+ },
433
447
  {
434
448
  key: 'at',
435
449
  label: 'Date',
@@ -488,27 +502,42 @@ const USAGE_DOMAIN = {
488
502
  formulas: false,
489
503
  };
490
504
  const USAGE_FILTER = [
505
+ { key: 'category', label: 'Category', icon: LayersIcon, values: (r) => [r.category] },
491
506
  { key: 'type', label: 'Type', icon: KeyRoundIcon, values: (r) => [TYPE_LABEL[r.type]] },
492
507
  ];
508
+ const fieldOf = (key) => USAGE_FIELDS.find((f) => f.key === key) ?? null;
493
509
  const GROUPS = [
494
- { id: 'type', label: 'Type', field: USAGE_FIELDS[1] },
495
- { id: 'none', label: 'None', field: null },
496
510
  { id: 'date', label: 'Date', field: DAY_FIELD },
511
+ { id: 'category', label: 'Category', field: fieldOf('category') },
512
+ { id: 'type', label: 'Type', field: fieldOf('type') },
513
+ { id: 'none', label: 'None', field: null },
497
514
  ];
515
+ /** Rows a page of the table shows; a group's totals still count every row of it. */
516
+ const PAGE_SIZE = 50;
498
517
  /**
499
- * This month's use, one row a session (a chat, or every run of one task on one thing): its Type (the person's API key, their plan sign-in, or
500
- * the app's credits), Date, Calls and Amount, grouped by Type, Date or not at all, filtered by
501
- * type or name. A row's hover gives its tokens and cost. A sign-in's amount is an estimate: the
502
- * same tokens at the model's API rates (the plan charges $0).
518
+ * This month's use, one row a session (a chat, or every run of one task on one thing): its
519
+ * Category (Chat, Edit, Rebuild: `UsageEntry.category`), Type (the person's API key, their plan
520
+ * sign-in, or the app's credits), Date, Calls and Amount, grouped by Date (first), Category, Type or
521
+ * not at all, filtered by category, type or name, PAGE_SIZE rows a page (a group's totals count all
522
+ * its rows). A row's hover gives its tokens and cost. A sign-in's amount is an estimate: the same
523
+ * tokens at the model's API rates (the plan charges $0).
503
524
  */
504
525
  export function AssistantUsageTable({ client }) {
505
- const [group, setGroup] = useState('type');
526
+ const [group, setGroup] = useState('date');
527
+ const [page, setPage] = useState(0);
506
528
  const q = useAssistantUsage(client);
507
529
  const rows = useMemo(() => (q.data ? usageRows(q.data) : []), [q.data]);
508
530
  const filter = useLumenFilter(rows, USAGE_FILTER, { text: (r) => [r.title] });
531
+ const shown = filter.results;
532
+ // A new filter or grouping starts from the first page; a page past the end shows the last.
533
+ const rowSet = shown.map((r) => r.id).join('\u0000');
534
+ // biome-ignore lint/correctness/useExhaustiveDependencies: resets on these changes only
535
+ useEffect(() => setPage(0), [rowSet, group]);
536
+ const pages = Math.max(1, Math.ceil(shown.length / PAGE_SIZE));
537
+ const at = Math.min(page, pages - 1);
509
538
  if (!client.usage)
510
539
  return null;
511
540
  if (!q.data)
512
541
  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, {})) }));
513
- 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("div", { className: "min-w-0", children: _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."] })] }));
542
+ 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: category, 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))) })] }), _jsxs("div", { className: "min-w-0", children: [_jsx(LumenTable, { domain: USAGE_DOMAIN, rows: shown, groupBy: GROUPS.find((g) => g.id === group)?.field ?? null, page: { index: at, size: PAGE_SIZE }, 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 }), pages > 1 && (_jsx(LumenPageFooter, { total: shown.length, page: at, size: PAGE_SIZE, onPage: setPage }))] }), _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."] })] }));
514
543
  }
@@ -213,7 +213,8 @@ export interface LumenTableProps<T> {
213
213
  rows: T[];
214
214
  groupBy?: ViewField<T> | null;
215
215
  chips?: ViewChip[];
216
- /** The page a flat table shows; null shows every row. */
216
+ /** The page a table shows; null shows every row. A grouped table pages its rows in group order:
217
+ * a group with rows on the page shows its row there, its count and totals over all its rows. */
217
218
  page?: {
218
219
  index: number;
219
220
  size: number;
@@ -761,6 +761,13 @@ function LumenTableRows({ domain, columns = domain.fields, rows, groupBy = null,
761
761
  }
762
762
  return [...m];
763
763
  }, [groupBy, sorted, serverGroups]);
764
+ // A grouped table's page (`page`): the rows on it, in group order; every group keeps its totals.
765
+ const paged = useMemo(() => {
766
+ if (!groups || !page || serverGroups)
767
+ return null;
768
+ const start = page.index * page.size;
769
+ return new Set(groups.flatMap(([, ms]) => ms).slice(start, start + page.size));
770
+ }, [groups, page, serverGroups]);
764
771
  const span = shown.length + (selection ? 1 : 0) + (add ? 1 : 0);
765
772
  const served = new Map((serverGroups?.list ?? []).map((g) => [g.key, g]));
766
773
  const startsOpen = (i) => rows.length <= OPEN_ALL || i === 0;
@@ -959,6 +966,9 @@ function LumenTableRows({ domain, columns = domain.fields, rows, groupBy = null,
959
966
  (!serverGroups || serverGroups.onToggleAll) && (_jsx(IconButton, { icon: anyClosed ? ChevronsUpDownIcon : ChevronsDownUpIcon, size: "icon-xs", className: "-ml-2 mr-1 align-middle", label: anyClosed ? 'Expand all groups' : 'Collapse all groups', onClick: () => setAll(anyClosed) })), _jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { asChild: true, children: _jsxs(Button, { variant: "ghost", size: "xs", className: cn('font-medium', !(i === 0 && groups) && '-mx-2'), style: { color: colorOf(domain, shown[i]) ?? undefined }, onClick: h.column.getCanSort() ? h.column.getToggleSortingHandler() : undefined, children: [shown[i] ? _jsx(FreshnessMark, { domain: domain, field: shown[i] }) : null, flexRender(h.column.columnDef.header, h.getContext()), _jsx(SortIcon, { dir: h.column.getIsSorted() })] }) }), shown[i] && (domain.groups?.length || freshnessOf(domain, shown[i])) ? (_jsxs(TooltipContent, { children: [domain.groups?.length ? _jsx("div", { children: pathOf(domain, shown[i]) }) : null, freshnessLine(domain, shown[i]) ? (_jsx("div", { children: freshnessLine(domain, shown[i]) })) : null] })) : null] }), resizing && shown[i] && (_jsx(ColumnResizer, { label: shown[i].label, width: widthOf(shown[i]), onWidth: (w) => colWidths.set(h.id, w), onDone: (w) => colWidths.save(h.id, w), onReset: () => colWidths.reset(h.id) }))] }, h.id))), add && (_jsx(TableHead, { className: cn('w-8 pl-1', PINNED_HEAD), children: _jsx(AddColumnMenu, { add: add, fieldOpen: fieldOpen, onFieldOpen: setFieldOpen }) }))] }, hg.id))) }), _jsxs(TableBody, { children: [!rows.length && !serverGroups?.list?.length && (_jsx(TableRow, { "data-slot": "table-empty", className: "hover:bg-transparent", children: _jsx(TableCell, { colSpan: span, className: "py-6 text-center whitespace-normal text-muted-foreground", children: _jsxs("div", { className: "flex flex-col items-center gap-3", children: [emptyState ?? 'No rows match.', onClearFilter && (_jsxs(Button, { variant: "outline", size: "sm", onClick: onClearFilter, children: [_jsx(FilterXIcon, {}), "Clear filters"] }))] }) }) })), groups && groupBy
960
967
  ? groups.map(([key, members], gi) => {
961
968
  const open = isOpen(key, gi);
969
+ const here = paged ? members.filter((m) => paged.has(m)) : members;
970
+ if (!here.length)
971
+ return null;
962
972
  return (_jsxs(Fragment, { children: [_jsxs(TableRow, { className: ROLLUP, "data-group": key, "data-rollup": "", children: [selection && (_jsx(TableCell, { "data-card": "select", className: cn('w-8 pr-0', PINNED_GROUP), children: _jsx(Checkbox, { checked: members.every((m) => selection.keys.has(keyOf(m.original)))
963
973
  ? true
964
974
  : members.some((m) => selection.keys.has(keyOf(m.original)))
@@ -972,7 +982,7 @@ function LumenTableRows({ domain, columns = domain.fields, rows, groupBy = null,
972
982
  }), children: [_jsx(ChevronRightIcon, { className: cn('transition-transform', open && 'rotate-90') }), _jsx(GroupMark, { field: groupBy, value: key }), _jsx("span", { className: "truncate", children: key }), _jsx(GroupCount, { children: serverGroups
973
983
  ? (served.get(key)?.n?.toLocaleString() ?? '')
974
984
  : members.length })] }), groupActions?.(key, members.map((m) => m.original))] }) }, f.key)) : (_jsx(TableCell, { ...col(f, PINNED_GROUP), children: !serverGroups &&
975
- rollCell(f, members.map((m) => m.original), 'group') }, f.key))), add && _jsx(TableCell, { className: PINNED_GROUP })] }), open && (serverGroups ? servedRows(members, key) : drawn(members, key))] }, key));
985
+ rollCell(f, members.map((m) => m.original), 'group') }, f.key))), add && _jsx(TableCell, { className: PINNED_GROUP })] }), open && (serverGroups ? servedRows(members, key) : drawn(here, key))] }, key));
976
986
  })
977
987
  : drawn(sorted, ''), serverGroups?.more && !serverGroups.pages && (_jsx(TableRow, { "data-more": true, children: _jsx(TableCell, { colSpan: span, className: "py-1", children: _jsxs(Button, { variant: "ghost", size: "xs", onClick: serverGroups.more, children: [_jsx(ChevronsDownIcon, {}), "More groups"] }) }) })), hasTotal && rows.length > 0 && (_jsxs(TableRow, { className: cn(ROLLUP, 'border-t-2'), "data-total": true, "data-rollup": "", children: [selection && _jsx(TableCell, {}), rollCells(rows).map((cell, i) => i === 0 && !(shown[0] && rollsUp(shown[0])) ? (_jsx(TableCell, { "data-card": "lead", children: "Total" }, "total")) : (cell)), add && _jsx(TableCell, {})] }))] })] }) })] }));
978
988
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@columenlabs/lumen-ui",
3
- "version": "0.1.301",
3
+ "version": "0.1.302",
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.301",
52
+ "@columenlabs/lumen-ui-types": "0.1.302",
53
53
  "@dnd-kit/core": "6.3.1",
54
54
  "@dnd-kit/sortable": "10.0.0",
55
55
  "@dnd-kit/utilities": "3.2.2",