@columenlabs/lumen-ui 0.1.234 → 0.1.236

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.
@@ -17,7 +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
+ import { OwnKeyUsage, UsageHistoryView } from './usage-history.js';
21
21
  const EFFORTS = [
22
22
  { value: 'default', label: 'Model default' },
23
23
  { value: 'low', label: 'Low' },
@@ -117,12 +117,20 @@ const wayTest = (k, plan = false) => k?.test
117
117
  : null;
118
118
  /** A provider's mark: its initial on a tile. */
119
119
  const mark = (label) => _jsx(ConnectionLogo, { text: label.charAt(0) });
120
+ /**
121
+ * Whether the app lets this person use `w` of their own on `p` (the app's `ownWays`, the same
122
+ * rule the server pays by). A way it does not allow never shows, even when one is still stored:
123
+ * the server would not run on it, so Settings never says it works.
124
+ */
125
+ const mayOwn = (p, w) => !p.own_ways || p.own_ways.includes(w);
126
+ /** Whether the person has a way of their own on `p` that the app lets them use. */
127
+ const ownUsable = (p) => (mayOwn(p, 'key') && p.own.set) || (mayOwn(p, 'plan') && !!p.plan?.set);
120
128
  /** One provider, collapsed to a row: the person's own key and, where it has one, their plan sign-in. */
121
129
  function OwnProviderKey({ client, p }) {
122
130
  const { apply, refresh } = useApply(client);
123
131
  const ways = [];
124
- const may = (w) => !p.own_ways || p.own_ways.includes(w);
125
- if (p.signin === 'chatgpt' && (may('plan') || p.plan?.set))
132
+ const may = (w) => mayOwn(p, w);
133
+ if (p.signin === 'chatgpt' && may('plan'))
126
134
  ways.push({
127
135
  id: 'plan',
128
136
  label: 'ChatGPT plan',
@@ -139,7 +147,7 @@ function OwnProviderKey({ client, p }) {
139
147
  removeText: 'Your API key, if you set one, runs from then on.',
140
148
  removeLabel: 'Sign out',
141
149
  });
142
- if (may('key') || p.own.set)
150
+ if (may('key'))
143
151
  ways.push({
144
152
  id: 'key',
145
153
  label: 'API key',
@@ -161,14 +169,14 @@ function OwnProviderKey({ client, p }) {
161
169
  function Providers({ client }) {
162
170
  const { apply } = useApply(client);
163
171
  return (_jsx(WithStatus, { client: client, children: (s) => {
164
- // A provider the app gives this person no way of their own on shows no row, unless one is
165
- // still set (so it can be removed).
166
- const rows = s.providers.filter((p) => !p.own_ways || p.own_ways.length > 0 || p.own.set || p.plan?.set);
167
- const ownSet = rows.some((p) => p.own.set || p.plan?.set);
172
+ // A provider the app gives this person no way of their own on shows no row: what shows here
173
+ // is exactly what the server may pay with.
174
+ const rows = s.providers.filter((p) => !p.own_ways || p.own_ways.length > 0);
175
+ const ownSet = rows.some(ownUsable);
168
176
  return (_jsxs("div", { className: "flex flex-col", children: [_jsx(LumenExpandList, { children: rows.map((p) => (_jsx(OwnProviderKey, { client: client, p: p }, p.id))) }), ownSet && (_jsx(Row, { label: "Answers run on", info: "Your own key or plan first, with the shared credits behind it; or the shared credits first.", children: _jsx(LumenSelector, { label: "Answers run on", size: "sm", className: "w-44", value: s.pay, options: [
169
177
  { value: 'own', label: 'Your own first' },
170
178
  { value: 'app', label: 'Shared credits first' },
171
- ], onChange: (pay) => void apply('pay', () => client.settings({ pay: pay })) }) })), ownSet && (_jsx(Row, { 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)) }) }))] }));
179
+ ], onChange: (pay) => void apply('pay', () => client.settings({ pay: pay })) }) })), ownSet && (_jsx(Row, { 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)) }) })), _jsx(OwnKeyUsage, { client: client, always: ownSet })] }));
172
180
  } }));
173
181
  }
174
182
  /** One provider's shared key, which an admin sets for everyone. */
@@ -13,3 +13,12 @@ export declare function UsageHistoryView({ client, className, }: {
13
13
  client: AssistantAdapter;
14
14
  className?: string;
15
15
  }): import("react").JSX.Element | null;
16
+ /**
17
+ * Spend on the person's own keys and plans over the last 1, 7 or 30 days: one row a chat or run
18
+ * (Session, Date, Amount), newest first, and a Total of the rows. A session counts in the range its
19
+ * last use falls in. Shown when `always` (an own key or plan is set) or any session was paid on one.
20
+ */
21
+ export declare function OwnKeyUsage({ client, always }: {
22
+ client: AssistantAdapter;
23
+ always?: boolean;
24
+ }): import("react").JSX.Element | null;
@@ -2,9 +2,13 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useQuery } from '@tanstack/react-query';
3
3
  import { ChevronLeftIcon, ChevronRightIcon, CpuIcon, MessageSquareIcon } from 'lucide-react';
4
4
  import { Fragment, useState } from 'react';
5
+ import { DetailsTable } from '../details-table.js';
5
6
  import { IconButton } from '../icon-button.js';
7
+ import { InfoLabel } from '../info-tip.js';
6
8
  import { cn } from '../lib/utils.js';
9
+ import { Label } from '../ui/label.js';
7
10
  import { Spinner } from '../ui/spinner.js';
11
+ import { ToggleGroup, ToggleGroupItem } from '../ui/toggle-group.js';
8
12
  export const usd = (n) => n === 0 ? '$0' : n < 0.01 ? '<$0.01' : `$${n.toFixed(n < 10 ? 2 : 0)}`;
9
13
  export const tokens = (n) => n < 1000
10
14
  ? String(n)
@@ -86,3 +90,45 @@ export function UsageHistoryView({ client, className, }) {
86
90
  const models = perModel(h.rows);
87
91
  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
92
  }
93
+ /** The ranges the own-key table offers, in days. */
94
+ const OWN_RANGES = [
95
+ { id: '1d', days: 1 },
96
+ { id: '7d', days: 7 },
97
+ { id: '30d', days: 30 },
98
+ ];
99
+ /**
100
+ * Spend on the person's own keys and plans over the last 1, 7 or 30 days: one row a chat or run
101
+ * (Session, Date, Amount), newest first, and a Total of the rows. A session counts in the range its
102
+ * last use falls in. Shown when `always` (an own key or plan is set) or any session was paid on one.
103
+ */
104
+ export function OwnKeyUsage({ client, always }) {
105
+ const [range, setRange] = useState('7d');
106
+ const now = useAssistantUsage(client).data;
107
+ // The month before, for a range that reaches back past this month's start.
108
+ const before = now?.months[now.months.indexOf(now.month) + 1];
109
+ const prev = useAssistantUsage(client, before, !!before && range === '30d').data;
110
+ if (!now)
111
+ return null;
112
+ const all = [...now.entries, ...(range === '30d' ? (prev?.entries ?? []) : [])].filter((e) => (e.own_usd ?? 0) > 0);
113
+ if (!always && !all.length)
114
+ return null;
115
+ const days = OWN_RANGES.find((r) => r.id === range)?.days ?? 7;
116
+ const since = Date.now() - days * 86_400_000;
117
+ const seen = new Set();
118
+ const sessions = all
119
+ .filter((e) => e.at >= since && !seen.has(e.id) && seen.add(e.id))
120
+ .sort((a, b) => b.at - a.at);
121
+ const total = sessions.reduce((a, e) => a + (e.own_usd ?? 0), 0);
122
+ return (_jsxs("div", { "data-slot": "setting-row", className: "flex flex-col gap-2 border-b py-3 last:border-b-0", children: [_jsxs("div", { className: "flex items-center gap-4", children: [_jsx(Label, { className: "min-w-0 flex-1", children: _jsx(InfoLabel, { info: "Each chat and run your own key or plan paid for, by its last use.", children: "Spent on your keys" }) }), _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 ? (_jsx(DetailsTable, { head: [
123
+ { label: 'Session', grow: true },
124
+ { label: 'Date', hideOnPhone: true },
125
+ { label: 'Amount', right: true },
126
+ ], rows: sessions.map((e) => ({
127
+ key: e.id,
128
+ cells: [
129
+ _jsx("span", { className: "block truncate", title: e.title, children: e.title }, "n"),
130
+ _jsx("time", { dateTime: new Date(e.at).toISOString(), title: new Date(e.at).toLocaleString(), children: when(e.at) }, "t"),
131
+ usd(e.own_usd ?? 0),
132
+ ],
133
+ })), foot: ['Total', _jsx("span", { className: "max-md:hidden" }, "d"), usd(total)] })) : (_jsxs("p", { className: "text-muted-foreground text-sm", children: ["Nothing spent on your keys in the last ", range, "."] }))] }));
134
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@columenlabs/lumen-ui",
3
- "version": "0.1.234",
3
+ "version": "0.1.236",
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.234",
52
+ "@columenlabs/lumen-ui-types": "0.1.236",
53
53
  "@dnd-kit/core": "6.3.1",
54
54
  "@dnd-kit/sortable": "10.0.0",
55
55
  "@dnd-kit/utilities": "3.2.2",