@columenlabs/lumen-ui 0.1.258 → 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';
|
package/dist/assistant/index.js
CHANGED
|
@@ -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
|
|
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
|
|
159
|
-
const amount = (key, plan, title) => plan > 0 ? (
|
|
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.
|
|
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
|
|
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}:
|
|
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 =
|
|
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.
|
|
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.
|
|
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",
|