@columenlabs/lumen-ui 0.1.258 → 0.1.260
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.
- package/dist/assistant/client.d.ts +8 -1
- package/dist/assistant/client.js +4 -0
- package/dist/assistant/index.d.ts +1 -1
- package/dist/assistant/index.js +1 -1
- package/dist/assistant/settings.d.ts +7 -2
- package/dist/assistant/settings.js +128 -7
- package/dist/assistant/usage-history.d.ts +10 -1
- package/dist/assistant/usage-history.js +175 -8
- package/package.json +2 -2
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AllowanceRequest, AssistantChat, AssistantChatRow, AssistantEvent, AssistantMessage, AssistantOwnWay, AssistantProvider, AssistantSettingsPatch, AssistantStatus, DraftItem, KeyState, StepItem, UsageHistory } from '@columenlabs/lumen-ui-types/assistant';
|
|
1
|
+
import type { AllowanceRequest, AssistantChat, AssistantChatRow, AssistantConnectorInput, 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). */
|
|
@@ -31,6 +31,13 @@ export interface AssistantAdapter {
|
|
|
31
31
|
client_secret: string;
|
|
32
32
|
} | null): Promise<AssistantStatus>;
|
|
33
33
|
testScrapeql(): Promise<Test>;
|
|
34
|
+
/** Admin: adds a connector (an MCP server), kept only when its test lists its tools. */
|
|
35
|
+
addConnector?(input: AssistantConnectorInput): Promise<AssistantStatus>;
|
|
36
|
+
/** Admin: changes one; left out, `headers` keeps the ones saved. */
|
|
37
|
+
saveConnector?(id: string, input: AssistantConnectorInput): Promise<AssistantStatus>;
|
|
38
|
+
removeConnector?(id: string): Promise<AssistantStatus>;
|
|
39
|
+
/** Admin: lists its tools again. */
|
|
40
|
+
testConnector?(id: string): Promise<Test>;
|
|
34
41
|
chats(): Promise<AssistantChatRow[]>;
|
|
35
42
|
chat(id: string): Promise<AssistantChat>;
|
|
36
43
|
removeChat(id: string): Promise<void>;
|
package/dist/assistant/client.js
CHANGED
|
@@ -55,6 +55,10 @@ export function assistantClient(base) {
|
|
|
55
55
|
signOut: (p) => send('DELETE', `/my-signin?provider=${p}`),
|
|
56
56
|
scrapeql: (token) => token ? send('PUT', '/web/scrapeql', token) : send('DELETE', '/web/scrapeql'),
|
|
57
57
|
testScrapeql: () => send('POST', '/web/test'),
|
|
58
|
+
addConnector: (input) => send('POST', '/connectors', input),
|
|
59
|
+
saveConnector: (id, input) => send('PUT', `/connectors/${encodeURIComponent(id)}`, input),
|
|
60
|
+
removeConnector: (id) => send('DELETE', `/connectors/${encodeURIComponent(id)}`),
|
|
61
|
+
testConnector: (id) => send('POST', `/connectors/${encodeURIComponent(id)}/test`),
|
|
58
62
|
chats: () => get('/chats'),
|
|
59
63
|
chat: (id) => get(`/chats/${encodeURIComponent(id)}`),
|
|
60
64
|
removeChat: async (id) => {
|
|
@@ -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';
|
|
@@ -4,10 +4,12 @@ import type { AssistantAdapter } from './client.js';
|
|
|
4
4
|
* The admin's section: the app's shared keys and each person's monthly allowance on them. Put it
|
|
5
5
|
* where the app keeps its admin settings, shown only to admins.
|
|
6
6
|
*/
|
|
7
|
-
export declare function assistantAdminSections({ client, allowance, providers, }: {
|
|
7
|
+
export declare function assistantAdminSections({ client, allowance, providers, connectors, }: {
|
|
8
8
|
client: AssistantAdapter;
|
|
9
9
|
/** Add Allowance requests (the app set `allowance` on the server). */
|
|
10
10
|
allowance?: boolean;
|
|
11
|
+
/** Add Connectors: outside MCP servers the assistant may use (the app set `connectors` on the server). */
|
|
12
|
+
connectors?: boolean;
|
|
11
13
|
/** Give each provider's shared key a section of its own (listed under Allowance) instead of one
|
|
12
14
|
* Shared model keys section: the app's providers, in order. */
|
|
13
15
|
providers?: {
|
|
@@ -19,7 +21,7 @@ export declare function assistantAdminSections({ client, allowance, providers, }
|
|
|
19
21
|
* The assistant's Settings sections: the assistant, then Model providers, Models, Tools, Web and
|
|
20
22
|
* Spend under it. Pass them to LumenShell's `shared` (or an app's own `settings`).
|
|
21
23
|
*/
|
|
22
|
-
export declare function assistantSettingsSections({ client, name, web, allowance, }: {
|
|
24
|
+
export declare function assistantSettingsSections({ client, name, web, allowance, usage, }: {
|
|
23
25
|
client: AssistantAdapter;
|
|
24
26
|
/** The assistant's name, the parent section's title. */
|
|
25
27
|
name: string;
|
|
@@ -27,4 +29,7 @@ export declare function assistantSettingsSections({ client, name, web, allowance
|
|
|
27
29
|
web?: boolean;
|
|
28
30
|
/** Show Allowance: what is left on the shared keys, and Request more (`allowance` on the server). */
|
|
29
31
|
allowance?: boolean;
|
|
32
|
+
/** Show Usage: this month's chats and runs by who paid (key, sign-in or credits); Model
|
|
33
|
+
* providers then leaves out its own spend table. */
|
|
34
|
+
usage?: boolean;
|
|
30
35
|
}): 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, PlugIcon, 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
|
/**
|
|
@@ -312,6 +312,104 @@ function Web({ client }) {
|
|
|
312
312
|
const { apply } = useApply(client);
|
|
313
313
|
return (_jsx(WithStatus, { client: client, children: (s) => (_jsxs("div", { className: "flex flex-col", children: [_jsx(SettingField, { label: "Read the web", info: "A plain fetch of public pages and APIs from the server, never the app's own pages.", children: _jsx(Switch, { "aria-label": "Read the web", checked: s.web.fetch, onCheckedChange: (fetch) => void apply('web', () => client.settings({ web: { fetch } })) }) }), _jsx(ScrapeqlBlock, { client: client, s: s })] })) }));
|
|
314
314
|
}
|
|
315
|
+
// ---------------------------------------------------------------- connectors
|
|
316
|
+
const CONNECTOR_ACCESS_LABELS = [
|
|
317
|
+
{ value: 'off', label: 'Off' },
|
|
318
|
+
{ value: 'read', label: 'Read' },
|
|
319
|
+
{ value: 'draft', label: 'Read + draft' },
|
|
320
|
+
];
|
|
321
|
+
/** Name, address and up to two sign-in headers; a pair left empty is not sent. */
|
|
322
|
+
const headerFields = (prefix = '') => [
|
|
323
|
+
{ id: `${prefix}h1`, label: 'Header', placeholder: 'CF-Access-Client-Id' },
|
|
324
|
+
{ id: `${prefix}v1`, label: 'Value', secret: true },
|
|
325
|
+
{ id: `${prefix}h2`, label: 'Header', placeholder: 'CF-Access-Client-Secret' },
|
|
326
|
+
{ id: `${prefix}v2`, label: 'Value', secret: true },
|
|
327
|
+
];
|
|
328
|
+
const headersOf = (v) => {
|
|
329
|
+
const out = {};
|
|
330
|
+
for (const i of [1, 2]) {
|
|
331
|
+
const k = v[`h${i}`]?.trim();
|
|
332
|
+
const val = v[`v${i}`]?.trim();
|
|
333
|
+
if (k && val)
|
|
334
|
+
out[k] = val;
|
|
335
|
+
}
|
|
336
|
+
return out;
|
|
337
|
+
};
|
|
338
|
+
function ConnectorRow({ client, c }) {
|
|
339
|
+
const { apply, refresh } = useApply(client);
|
|
340
|
+
const reads = c.tools.filter((t) => t.read).length;
|
|
341
|
+
const access = (v) => apply('connector', () => client.saveConnector(c.id, {
|
|
342
|
+
name: c.name,
|
|
343
|
+
url: c.url,
|
|
344
|
+
access: v,
|
|
345
|
+
}));
|
|
346
|
+
return (_jsx(LumenConnection, { id: c.id, name: c.name, logo: _jsx(ConnectionLogo, { icon: PlugIcon }), meta: `${c.tools.length} tool${c.tools.length === 1 ? '' : 's'}${c.access === 'off' ? ' · off' : ''}`, ways: [
|
|
347
|
+
{
|
|
348
|
+
id: 'headers',
|
|
349
|
+
label: 'Sign-in headers',
|
|
350
|
+
icon: KeyRoundIcon,
|
|
351
|
+
info: 'Sent with every call to this server, such as a Cloudflare Access service token. Kept sealed on the server and never shown again.',
|
|
352
|
+
set: true,
|
|
353
|
+
ident: c.headers.length ? c.headers.join(', ') : 'None',
|
|
354
|
+
test: c.test
|
|
355
|
+
? {
|
|
356
|
+
at: c.test.at,
|
|
357
|
+
ok: c.test.ok,
|
|
358
|
+
detail: c.test.detail,
|
|
359
|
+
error: c.test.ok ? null : c.test.detail,
|
|
360
|
+
}
|
|
361
|
+
: null,
|
|
362
|
+
fields: [{ id: 'url', label: 'Address', placeholder: c.url }, ...headerFields()],
|
|
363
|
+
addLabel: 'Replace',
|
|
364
|
+
onSave: (v) => apply('connector', () => client.saveConnector(c.id, {
|
|
365
|
+
name: c.name,
|
|
366
|
+
url: v.url?.trim() || c.url,
|
|
367
|
+
headers: headersOf(v),
|
|
368
|
+
}), 'Connector saved'),
|
|
369
|
+
...(client.testConnector
|
|
370
|
+
? {
|
|
371
|
+
onTest: () => client.testConnector(c.id).finally(refresh),
|
|
372
|
+
}
|
|
373
|
+
: {}),
|
|
374
|
+
onRemove: () => apply('connector', () => client.removeConnector(c.id), 'Connector removed'),
|
|
375
|
+
removeTitle: `Remove ${c.name}?`,
|
|
376
|
+
removeText: 'The assistant stops using its tools.',
|
|
377
|
+
},
|
|
378
|
+
], lines: [
|
|
379
|
+
{ label: 'Address', value: c.url, mono: true },
|
|
380
|
+
{
|
|
381
|
+
label: 'Tools',
|
|
382
|
+
info: 'The assistant finds these only when it needs them, so they cost nothing until used.',
|
|
383
|
+
value: c.tools.length
|
|
384
|
+
? `${c.tools.map((t) => t.title ?? t.name).join(', ')} (${reads} read-only)`
|
|
385
|
+
: 'None listed',
|
|
386
|
+
},
|
|
387
|
+
], children: _jsx(SettingField, { label: "Use", info: "Read uses the tools the server marks read-only. Read + draft also lets the others make a card the person saves or dismisses.", children: _jsx(ToggleGroup, { type: "single", variant: "outline", size: "sm", spacing: 0, value: c.access, "aria-label": `Use ${c.name}`, onValueChange: (v) => v && void access(v), children: CONNECTOR_ACCESS_LABELS.map((a) => (_jsx(ToggleGroupItem, { value: a.value, children: a.label }, a.value))) }) }) }));
|
|
388
|
+
}
|
|
389
|
+
/** Admin: the app's connectors, each an MCP server, and a row to add one. */
|
|
390
|
+
function Connectors({ client }) {
|
|
391
|
+
const { apply } = useApply(client);
|
|
392
|
+
return (_jsx(WithStatus, { client: client, children: (s) => (_jsxs(LumenExpandList, { children: [(s.connectors ?? []).map((c) => (_jsx(ConnectorRow, { client: client, c: c }, c.id))), _jsx(LumenConnection, { id: "connector-new", name: "Add connector", logo: _jsx(ConnectionLogo, { icon: PlugIcon }), meta: "Any MCP server over HTTP", ways: [
|
|
393
|
+
{
|
|
394
|
+
id: 'new',
|
|
395
|
+
label: 'MCP server',
|
|
396
|
+
icon: PlugIcon,
|
|
397
|
+
info: 'Its address (Streamable HTTP) and the headers it signs in with. It is tested before it is kept.',
|
|
398
|
+
set: false,
|
|
399
|
+
fields: [
|
|
400
|
+
{ id: 'name', label: 'Name', placeholder: 'Web search' },
|
|
401
|
+
{ id: 'url', label: 'Address', placeholder: 'https://example.com/mcp' },
|
|
402
|
+
...headerFields(),
|
|
403
|
+
],
|
|
404
|
+
addLabel: 'Add connector',
|
|
405
|
+
onSave: (v) => apply('connector', () => client.addConnector({
|
|
406
|
+
name: v.name?.trim() ?? '',
|
|
407
|
+
url: v.url?.trim() ?? '',
|
|
408
|
+
headers: headersOf(v),
|
|
409
|
+
}), 'Connector added'),
|
|
410
|
+
},
|
|
411
|
+
] })] })) }));
|
|
412
|
+
}
|
|
315
413
|
// ---------------------------------------------------------------- spend
|
|
316
414
|
function Spend({ client }) {
|
|
317
415
|
return (_jsx(WithStatus, { client: client, children: (s) => {
|
|
@@ -414,7 +512,7 @@ function AllowanceRequests({ client }) {
|
|
|
414
512
|
* The admin's section: the app's shared keys and each person's monthly allowance on them. Put it
|
|
415
513
|
* where the app keeps its admin settings, shown only to admins.
|
|
416
514
|
*/
|
|
417
|
-
export function assistantAdminSections({ client, allowance = false, providers, }) {
|
|
515
|
+
export function assistantAdminSections({ client, allowance = false, providers, connectors = false, }) {
|
|
418
516
|
const keys = providers
|
|
419
517
|
? [
|
|
420
518
|
{
|
|
@@ -455,13 +553,24 @@ export function assistantAdminSections({ client, allowance = false, providers, }
|
|
|
455
553
|
},
|
|
456
554
|
]
|
|
457
555
|
: []),
|
|
556
|
+
...(connectors
|
|
557
|
+
? [
|
|
558
|
+
{
|
|
559
|
+
id: 'assistant-connectors',
|
|
560
|
+
title: 'Connectors',
|
|
561
|
+
icon: PlugIcon,
|
|
562
|
+
description: 'Outside services the assistant may use',
|
|
563
|
+
content: _jsx(Connectors, { client: client }),
|
|
564
|
+
},
|
|
565
|
+
]
|
|
566
|
+
: []),
|
|
458
567
|
];
|
|
459
568
|
}
|
|
460
569
|
/**
|
|
461
570
|
* The assistant's Settings sections: the assistant, then Model providers, Models, Tools, Web and
|
|
462
571
|
* Spend under it. Pass them to LumenShell's `shared` (or an app's own `settings`).
|
|
463
572
|
*/
|
|
464
|
-
export function assistantSettingsSections({ client, name, web = true, allowance = false, }) {
|
|
573
|
+
export function assistantSettingsSections({ client, name, web = true, allowance = false, usage = false, }) {
|
|
465
574
|
return [
|
|
466
575
|
{
|
|
467
576
|
id: 'assistant',
|
|
@@ -476,7 +585,7 @@ export function assistantSettingsSections({ client, name, web = true, allowance
|
|
|
476
585
|
icon: KeyRoundIcon,
|
|
477
586
|
sub: true,
|
|
478
587
|
description: 'Your own key for each provider',
|
|
479
|
-
content: _jsx(Providers, { client: client }),
|
|
588
|
+
content: _jsx(Providers, { client: client, usage: usage }),
|
|
480
589
|
},
|
|
481
590
|
{
|
|
482
591
|
id: 'assistant-models',
|
|
@@ -514,6 +623,18 @@ export function assistantSettingsSections({ client, name, web = true, allowance
|
|
|
514
623
|
description: 'What each model, chat and run cost',
|
|
515
624
|
content: _jsx(Spend, { client: client }),
|
|
516
625
|
},
|
|
626
|
+
...(usage
|
|
627
|
+
? [
|
|
628
|
+
{
|
|
629
|
+
id: 'assistant-usage',
|
|
630
|
+
title: 'Usage',
|
|
631
|
+
icon: ChartColumnIcon,
|
|
632
|
+
sub: true,
|
|
633
|
+
description: 'Every chat and run this month, and who paid',
|
|
634
|
+
content: _jsx(AssistantUsageTable, { client: client }),
|
|
635
|
+
},
|
|
636
|
+
]
|
|
637
|
+
: []),
|
|
517
638
|
...(allowance
|
|
518
639
|
? [
|
|
519
640
|
{
|
|
@@ -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.260",
|
|
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.260",
|
|
53
53
|
"@dnd-kit/core": "6.3.1",
|
|
54
54
|
"@dnd-kit/sortable": "10.0.0",
|
|
55
55
|
"@dnd-kit/utilities": "3.2.2",
|