@columenlabs/lumen-ui 0.1.209 → 0.1.211
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 +3 -1
- package/dist/assistant/client.js +1 -0
- package/dist/assistant/index.d.ts +1 -0
- package/dist/assistant/index.js +1 -0
- package/dist/assistant/lumen-assistant.d.ts +13 -4
- package/dist/assistant/lumen-assistant.js +102 -18
- package/dist/assistant/settings.js +5 -1
- package/dist/assistant/usage-history.d.ts +15 -0
- package/dist/assistant/usage-history.js +88 -0
- package/dist/assistant/use-assistant-chat.js +3 -0
- package/package.json +2 -2
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AllowanceRequest, AssistantChat, AssistantChatRow, AssistantEvent, AssistantMessage, AssistantOwnWay, AssistantProvider, AssistantSettingsPatch, AssistantStatus, DraftItem, KeyState, StepItem } from '@columenlabs/lumen-ui-types/assistant';
|
|
1
|
+
import type { AllowanceRequest, AssistantChat, AssistantChatRow, 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). */
|
|
@@ -34,6 +34,8 @@ export interface AssistantAdapter {
|
|
|
34
34
|
chats(): Promise<AssistantChatRow[]>;
|
|
35
35
|
chat(id: string): Promise<AssistantChat>;
|
|
36
36
|
removeChat(id: string): Promise<void>;
|
|
37
|
+
/** The person's use in a month (`YYYY-MM`, this month when left out): each model, chat and run. */
|
|
38
|
+
usage?(month?: string): Promise<UsageHistory>;
|
|
37
39
|
/** One answer: `onEvent` hears each event as it streams; abort `signal` to stop it. */
|
|
38
40
|
send(msg: AssistantMessage, onEvent: (e: AssistantEvent) => void, signal?: AbortSignal): Promise<void>;
|
|
39
41
|
settleDraft(chatId: string, draftId: string, action: 'save' | 'dismiss'): Promise<DraftItem>;
|
package/dist/assistant/client.js
CHANGED
|
@@ -60,6 +60,7 @@ export function assistantClient(base) {
|
|
|
60
60
|
removeChat: async (id) => {
|
|
61
61
|
await send('DELETE', `/chats/${encodeURIComponent(id)}`);
|
|
62
62
|
},
|
|
63
|
+
usage: (month) => get(`/usage${month ? `?month=${encodeURIComponent(month)}` : ''}`),
|
|
63
64
|
settleDraft: (chatId, draftId, action) => send('POST', `/chats/${encodeURIComponent(chatId)}/drafts/${encodeURIComponent(draftId)}`, {
|
|
64
65
|
action,
|
|
65
66
|
}),
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export { type AssistantAdapter, assistantClient } from './client.js';
|
|
2
2
|
export { type AssistantCommand, type ComposerExtra, LumenAssistant, type LumenAssistantProps, LumenComposer, assistantStatusKey, useAssistantStatus, useLumenAssistantDock, } from './lumen-assistant.js';
|
|
3
3
|
export { assistantAdminSections, assistantSettingsSections } from './settings.js';
|
|
4
|
+
export { UsageHistoryView, assistantUsageKey, tokens as formatTokens, usd as formatUsd, useAssistantUsage, } from './usage-history.js';
|
|
4
5
|
export { type AssistantSource, type AssistantSourceItem, type ComposeRequest, type PageSelection, composeAssistant, selectionRef, useAssistantSource, usePageSelection, useShownAssistantSources, } from './sources.js';
|
|
5
6
|
export { type AssistantChatState, type LiveItem, applyEvent, useAssistantChat, } from './use-assistant-chat.js';
|
package/dist/assistant/index.js
CHANGED
|
@@ -4,5 +4,6 @@
|
|
|
4
4
|
export { assistantClient } from './client.js';
|
|
5
5
|
export { LumenAssistant, LumenComposer, assistantStatusKey, useAssistantStatus, useLumenAssistantDock, } from './lumen-assistant.js';
|
|
6
6
|
export { assistantAdminSections, assistantSettingsSections } from './settings.js';
|
|
7
|
+
export { UsageHistoryView, assistantUsageKey, tokens as formatTokens, usd as formatUsd, useAssistantUsage, } from './usage-history.js';
|
|
7
8
|
export { composeAssistant, selectionRef, useAssistantSource, usePageSelection, useShownAssistantSources, } from './sources.js';
|
|
8
9
|
export { applyEvent, useAssistantChat, } from './use-assistant-chat.js';
|
|
@@ -1,12 +1,13 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type AllowanceUnit, type AssistantImage, type AssistantMessage, type AssistantRefNote, type AssistantStatus } from '@columenlabs/lumen-ui-types/assistant';
|
|
2
2
|
import { type LumenOption } from '../lumen-selector.js';
|
|
3
3
|
import type { AssistantAdapter } from './client.js';
|
|
4
4
|
import { type AssistantSource } from './sources.js';
|
|
5
|
+
import { usd } from './usage-history.js';
|
|
5
6
|
import { type AssistantChatState } from './use-assistant-chat.js';
|
|
6
7
|
export declare const assistantStatusKey: (client: AssistantAdapter) => string[];
|
|
7
8
|
/** The person's assistant status, shared by the panel and Settings. */
|
|
8
9
|
export declare function useAssistantStatus(client: AssistantAdapter): import("@tanstack/react-query").UseQueryResult<AssistantStatus, Error>;
|
|
9
|
-
export
|
|
10
|
+
export { usd };
|
|
10
11
|
export interface LumenAssistantProps {
|
|
11
12
|
client: AssistantAdapter;
|
|
12
13
|
/** The page the person is on, sent with each message. */
|
|
@@ -26,6 +27,11 @@ export interface LumenAssistantProps {
|
|
|
26
27
|
commands?: AssistantCommand[];
|
|
27
28
|
/** What `#` lists everywhere; a page adds its own with `useAssistantSource`. */
|
|
28
29
|
sources?: readonly AssistantSource[];
|
|
30
|
+
/**
|
|
31
|
+
* Images can go with a message: pasted (Cmd+V / Ctrl+V) or dropped on the box, shown as chips
|
|
32
|
+
* until it is sent. Turn it on when the app's chat route passes `images` on to the agent.
|
|
33
|
+
*/
|
|
34
|
+
images?: boolean;
|
|
29
35
|
}
|
|
30
36
|
/** A slash command: typing `/` in the composer lists them. */
|
|
31
37
|
export interface AssistantCommand {
|
|
@@ -41,21 +47,24 @@ export interface AssistantCommand {
|
|
|
41
47
|
run: (args: string) => string | undefined | Promise<string | undefined>;
|
|
42
48
|
}
|
|
43
49
|
/** The assistant's chat: header, messages and composer. */
|
|
44
|
-
export declare function LumenAssistant({ client, context, onSettings, keepDays, onClose, chat: outside, commands, sources, }: LumenAssistantProps): import("react").JSX.Element;
|
|
50
|
+
export declare function LumenAssistant({ client, context, onSettings, keepDays, onClose, chat: outside, commands, sources, images, }: LumenAssistantProps): import("react").JSX.Element;
|
|
45
51
|
/** An allowance in its unit: `$4.20`, or `350 credits`. */
|
|
46
52
|
export declare const allowanceAmount: (u: AllowanceUnit, n: number) => string;
|
|
47
53
|
/** What the composer sends with the text: the tier picked with `@` and the things pointed at with `#`. */
|
|
48
54
|
export interface ComposerExtra {
|
|
49
55
|
tier?: string;
|
|
50
56
|
refs?: AssistantRefNote[];
|
|
57
|
+
images?: AssistantImage[];
|
|
51
58
|
}
|
|
52
59
|
/**
|
|
53
60
|
* The message box: Enter sends, Shift+Enter breaks a line; Stop while an answer runs. Three
|
|
54
61
|
* triggers list what can follow them as it is typed: `/` at the start a command, `@` a tier for
|
|
55
62
|
* this message, `#` a thing to point at (the `sources`); a picked tier or thing shows as a chip.
|
|
56
63
|
*/
|
|
57
|
-
export declare function LumenComposer({ name, busy, queued, disabled, commands, tiers, sources, page, onSend, onStop, }: {
|
|
64
|
+
export declare function LumenComposer({ name, busy, queued, disabled, commands, tiers, sources, page, images: canAttach, onSend, onStop, }: {
|
|
58
65
|
name: string;
|
|
66
|
+
/** Images can be pasted or dropped on the box (`LumenAssistant` `images`). */
|
|
67
|
+
images?: boolean;
|
|
59
68
|
/** The page's name for `#` This page; the document's title by default. */
|
|
60
69
|
page?: string;
|
|
61
70
|
busy: boolean;
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
// LumenAssistant: the chat that fills LumenAssistantPanel, for any app's assistant served by
|
|
3
|
+
// `@columenlabs/lumen-ai/assistant`. A header with the tier, Chats and New chat; the chat itself
|
|
4
|
+
// (the person's messages, the answer as it streams, each tool call as a line, drafted changes as
|
|
5
|
+
// cards to Save or Dismiss, work handed to a higher tier in its own section); and the composer,
|
|
6
|
+
// with Stop, a queued message and the chat's cost and context under it.
|
|
7
|
+
import { ASSISTANT_IMAGES_MAX, ASSISTANT_IMAGE_CHARS, } from '@columenlabs/lumen-ui-types/assistant';
|
|
2
8
|
import { useQuery, useQueryClient } from '@tanstack/react-query';
|
|
3
|
-
import { ArrowUpIcon, CheckIcon, ChevronRightIcon, CircleAlertIcon, CopyIcon, CornerDownRightIcon, CrosshairIcon, FileTextIcon, LightbulbIcon, MonitorIcon, PencilIcon, PlusIcon, SparklesIcon, SquareIcon, TriangleAlertIcon, Undo2Icon, WrenchIcon, XIcon, ZapIcon, } from 'lucide-react';
|
|
9
|
+
import { ArrowUpIcon, CheckIcon, ChevronRightIcon, CircleAlertIcon, CopyIcon, CornerDownRightIcon, CrosshairIcon, FileTextIcon, LightbulbIcon, MonitorIcon, PencilIcon, PlusIcon, ReceiptIcon, SparklesIcon, SquareIcon, TriangleAlertIcon, Undo2Icon, WrenchIcon, XIcon, ZapIcon, } from 'lucide-react';
|
|
4
10
|
import { Fragment, createContext, useContext, useEffect, useRef, useState, } from 'react';
|
|
5
11
|
import { LumenAssistantButton, LumenAssistantChats, LumenAssistantPanel, assistantOpen, useAssistantHotkey, } from '../assistant-panel.js';
|
|
6
12
|
import { screenMapFor, useAssistantScreen } from '../assistant-screen.js';
|
|
@@ -22,6 +28,7 @@ import { Spinner } from '../ui/spinner.js';
|
|
|
22
28
|
import { Textarea } from '../ui/textarea.js';
|
|
23
29
|
import { chatMarkdown, chatTurns, copyMarkdown, turnMarkdown } from './copy.js';
|
|
24
30
|
import { composeAssistant, selectionRef, useComposeRequest, usePageSelection, useShownAssistantSources, } from './sources.js';
|
|
31
|
+
import { UsageHistoryView, tokens, usd } from './usage-history.js';
|
|
25
32
|
import { useAssistantChat } from './use-assistant-chat.js';
|
|
26
33
|
// The Markdown renderer (tables included) loads with the first answer (lazy-part.tsx).
|
|
27
34
|
// A tab whose build a deploy replaced shows the answer as plain text instead of reloading mid chat.
|
|
@@ -38,15 +45,10 @@ export const assistantStatusKey = (client) => [
|
|
|
38
45
|
export function useAssistantStatus(client) {
|
|
39
46
|
return useQuery({ queryKey: assistantStatusKey(client), queryFn: () => client.status() });
|
|
40
47
|
}
|
|
41
|
-
export
|
|
42
|
-
const tokens = (n) => n < 1000
|
|
43
|
-
? String(n)
|
|
44
|
-
: n < 1_000_000
|
|
45
|
-
? `${(n / 1000).toFixed(n < 10_000 ? 1 : 0)}k`
|
|
46
|
-
: `${(n / 1e6).toFixed(1)}M`;
|
|
48
|
+
export { usd };
|
|
47
49
|
const seconds = (ms) => (ms < 1000 ? `${ms} ms` : `${(ms / 1000).toFixed(1)} s`);
|
|
48
50
|
/** The assistant's chat: header, messages and composer. */
|
|
49
|
-
export function LumenAssistant({ client, context, onSettings, keepDays = 30, onClose, chat: outside, commands = [], sources = [], }) {
|
|
51
|
+
export function LumenAssistant({ client, context, onSettings, keepDays = 30, onClose, chat: outside, commands = [], sources = [], images = false, }) {
|
|
50
52
|
const own = useAssistantChat(client);
|
|
51
53
|
const chat = outside ?? own;
|
|
52
54
|
const qc = useQueryClient();
|
|
@@ -183,7 +185,7 @@ export function LumenAssistant({ client, context, onSettings, keepDays = 30, onC
|
|
|
183
185
|
id: c.id,
|
|
184
186
|
title: c.title,
|
|
185
187
|
updated_at: new Date(c.at).toISOString(),
|
|
186
|
-
})) ?? (chats.isFetched ? [] : null), value: chat.chatId ?? undefined, note: `Chats are kept ${keepDays} days after their last message.`, onRefresh: () => void chats.refetch(), onOpen: (c) => void chat.open(c.id) }), _jsx(IconButton, { icon: CopyIcon, label: "Copy chat", disabled: !chat.items.length, onClick: () => copyMarkdown(chatMarkdown(chat.title, chat.items, name), 'Chat') }), _jsx(IconButton, { icon: PlusIcon, label: "New chat", disabled: !chat.items.length, onClick: chat.reset }), onClose && (_jsx(IconButton, { icon: XIcon, label: `Close ${s?.name ?? 'assistant'}`, onClick: onClose }))] }), _jsx(AssistantName.Provider, { value: name, children: _jsx(Screen.Provider, { value: controls, children: _jsx(Messages, { chat: chat, status: s, onSettings: onSettings }) }) }), _jsx(LumenComposer, { name: s?.name ?? 'the assistant', busy: chat.busy, queued: chat.queued, disabled: !s || !!s.wait, commands: allCommands, tiers: tiers, sources: allSources, page: context?.label, onSend: (text, more) => chat.send(text, { ...extra(), ...more }), onStop: chat.stop }), _jsx(UsageLine, { usage: chat.usage })] }));
|
|
188
|
+
})) ?? (chats.isFetched ? [] : null), value: chat.chatId ?? undefined, note: `Chats are kept ${keepDays} days after their last message.`, onRefresh: () => void chats.refetch(), onOpen: (c) => void chat.open(c.id) }), _jsx(IconButton, { icon: CopyIcon, label: "Copy chat", disabled: !chat.items.length, onClick: () => copyMarkdown(chatMarkdown(chat.title, chat.items, name), 'Chat') }), _jsx(IconButton, { icon: PlusIcon, label: "New chat", disabled: !chat.items.length, onClick: chat.reset }), onClose && (_jsx(IconButton, { icon: XIcon, label: `Close ${s?.name ?? 'assistant'}`, onClick: onClose }))] }), _jsx(AssistantName.Provider, { value: name, children: _jsx(Screen.Provider, { value: controls, children: _jsx(Messages, { chat: chat, status: s, onSettings: onSettings }) }) }), _jsx(LumenComposer, { name: s?.name ?? 'the assistant', busy: chat.busy, queued: chat.queued, disabled: !s || !!s.wait, commands: allCommands, tiers: tiers, sources: allSources, page: context?.label, images: images, onSend: (text, more) => chat.send(text, { ...extra(), ...more }), onStop: chat.stop }), _jsx(UsageLine, { usage: chat.usage, client: client, spent: s?.spend.usd })] }));
|
|
187
189
|
}
|
|
188
190
|
function Messages({ chat, status, onSettings, }) {
|
|
189
191
|
const end = useRef(null);
|
|
@@ -257,7 +259,7 @@ function Item({ item, onSettle, busy, }) {
|
|
|
257
259
|
const tierName = useContext(TierNames);
|
|
258
260
|
switch (item.kind) {
|
|
259
261
|
case 'user':
|
|
260
|
-
return (_jsxs("div", { className: "ml-8 self-end rounded-lg bg-muted px-3 py-2 text-sm whitespace-pre-wrap", children: [(item.tier || item.refs?.length) && (_jsxs("span", { className: "mb-1 flex flex-wrap gap-1", children: [item.tier && _jsx(ComposerChip, { sign: "@", label: tierName(item.tier) }), item.refs?.map((r, i) => (_jsx(ComposerChip, { sign: "#", label: r.label }, `${r.kind}:${r.label}:${i}`)))] })), item.text, item.context && (_jsxs("span", { className: "mt-1 block text-muted-foreground text-xs", children: ["on ", item.context] }))] }));
|
|
262
|
+
return (_jsxs("div", { className: "ml-8 self-end rounded-lg bg-muted px-3 py-2 text-sm whitespace-pre-wrap", children: [(item.tier || item.refs?.length) && (_jsxs("span", { className: "mb-1 flex flex-wrap gap-1", children: [item.tier && _jsx(ComposerChip, { sign: "@", label: tierName(item.tier) }), item.refs?.map((r, i) => (_jsx(ComposerChip, { sign: "#", label: r.label }, `${r.kind}:${r.label}:${i}`)))] })), item.images?.length ? (_jsx("span", { className: "mb-1 flex flex-wrap gap-1.5", children: item.images.map((src, i) => (_jsx("img", { src: src, alt: `Pasted ${i + 1}`, className: "max-h-32 max-w-40 rounded-md border object-cover" }, `${i}:${src.length}`))) })) : null, item.text, item.context && (_jsxs("span", { className: "mt-1 block text-muted-foreground text-xs", children: ["on ", item.context] }))] }));
|
|
261
263
|
case 'text':
|
|
262
264
|
return (_jsxs("div", { className: "prose-sm text-sm leading-relaxed [&_code]:font-code [&_code]:text-xs [&_li]:ml-4 [&_ol]:list-decimal [&_p]:my-1.5 [&_ul]:list-disc", children: [_jsx(ChatMarkdown, { children: item.text }), item.live && (_jsx("span", { className: "ml-0.5 inline-block h-3.5 w-1.5 animate-pulse bg-foreground/60 align-middle" }))] }));
|
|
263
265
|
case 'step':
|
|
@@ -424,17 +426,49 @@ function HandOff({ item, onSettle, busy, }) {
|
|
|
424
426
|
function ComposerChip({ sign, label, onRemove, }) {
|
|
425
427
|
return (_jsxs("span", { className: "inline-flex max-w-full items-center gap-1 rounded-md border bg-background px-1.5 py-0.5 text-xs", children: [_jsx("span", { className: "text-muted-foreground", children: sign }), _jsx("span", { className: "truncate", children: label }), onRemove && (_jsx("button", { type: "button", "aria-label": `Remove ${label}`, className: "text-muted-foreground hover:text-foreground", onClick: onRemove, children: _jsx(XIcon, { className: "size-3" }) }))] }));
|
|
426
428
|
}
|
|
429
|
+
/** The longest edge an image is sent at (what the models read best; larger only costs more). */
|
|
430
|
+
const IMAGE_EDGE = 1568;
|
|
431
|
+
/**
|
|
432
|
+
* A pasted or dropped image, ready to send: scaled to `IMAGE_EDGE` on a canvas and saved as JPEG
|
|
433
|
+
* (PNG when it has few pixels to lose), as base64. `null` when the browser cannot read it.
|
|
434
|
+
*/
|
|
435
|
+
async function readImage(file) {
|
|
436
|
+
try {
|
|
437
|
+
const bmp = await createImageBitmap(file);
|
|
438
|
+
const k = Math.min(1, IMAGE_EDGE / Math.max(bmp.width, bmp.height));
|
|
439
|
+
const canvas = document.createElement('canvas');
|
|
440
|
+
canvas.width = Math.max(1, Math.round(bmp.width * k));
|
|
441
|
+
canvas.height = Math.max(1, Math.round(bmp.height * k));
|
|
442
|
+
const g = canvas.getContext('2d');
|
|
443
|
+
if (!g)
|
|
444
|
+
return null;
|
|
445
|
+
g.fillStyle = '#fff';
|
|
446
|
+
g.fillRect(0, 0, canvas.width, canvas.height);
|
|
447
|
+
g.drawImage(bmp, 0, 0, canvas.width, canvas.height);
|
|
448
|
+
bmp.close();
|
|
449
|
+
const png = canvas.toDataURL('image/png');
|
|
450
|
+
const url = png.length < 400_000 ? png : canvas.toDataURL('image/jpeg', 0.85);
|
|
451
|
+
const [head, data] = url.split(',');
|
|
452
|
+
const media_type = head?.includes('png') ? 'image/png' : 'image/jpeg';
|
|
453
|
+
return data ? { media_type, data } : null;
|
|
454
|
+
}
|
|
455
|
+
catch {
|
|
456
|
+
return null;
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
const imageUrl = (i) => `data:${i.media_type};base64,${i.data}`;
|
|
427
460
|
/**
|
|
428
461
|
* The message box: Enter sends, Shift+Enter breaks a line; Stop while an answer runs. Three
|
|
429
462
|
* triggers list what can follow them as it is typed: `/` at the start a command, `@` a tier for
|
|
430
463
|
* this message, `#` a thing to point at (the `sources`); a picked tier or thing shows as a chip.
|
|
431
464
|
*/
|
|
432
|
-
export function LumenComposer({ name, busy, queued, disabled, commands = [], tiers = [], sources = [], page, onSend, onStop, }) {
|
|
465
|
+
export function LumenComposer({ name, busy, queued, disabled, commands = [], tiers = [], sources = [], page, images: canAttach = false, onSend, onStop, }) {
|
|
433
466
|
const [text, setText] = useState('');
|
|
434
467
|
const [caret, setCaret] = useState(0);
|
|
435
468
|
const [closed, setClosed] = useState(null);
|
|
436
469
|
const [tier, setTier] = useState(null);
|
|
437
470
|
const [refs, setRefs] = useState([]);
|
|
471
|
+
const [images, setImages] = useState([]);
|
|
438
472
|
const list = useRef(null);
|
|
439
473
|
const box = useRef(null);
|
|
440
474
|
const selection = usePageSelection();
|
|
@@ -468,12 +502,40 @@ export function LumenComposer({ name, busy, queued, disabled, commands = [], tie
|
|
|
468
502
|
const extra = () => ({
|
|
469
503
|
...(tier ? { tier } : {}),
|
|
470
504
|
...(refs.length ? { refs } : {}),
|
|
505
|
+
...(images.length ? { images } : {}),
|
|
471
506
|
});
|
|
472
507
|
const clear = () => {
|
|
473
508
|
setText('');
|
|
474
509
|
setCaret(0);
|
|
475
510
|
setTier(null);
|
|
476
511
|
setRefs([]);
|
|
512
|
+
setImages([]);
|
|
513
|
+
};
|
|
514
|
+
/** Adds the image files among `files`; says so when one is left out. Returns whether any was an image. */
|
|
515
|
+
const attach = (files) => {
|
|
516
|
+
const picked = [...files].filter((f) => f.type.startsWith('image/'));
|
|
517
|
+
if (!canAttach || !picked.length)
|
|
518
|
+
return false;
|
|
519
|
+
void (async () => {
|
|
520
|
+
const read = (await Promise.all(picked.map(readImage))).filter((x) => !!x);
|
|
521
|
+
if (read.length < picked.length)
|
|
522
|
+
lumenToast.error("That image couldn't be read. Try a PNG or JPEG.");
|
|
523
|
+
setImages((p) => {
|
|
524
|
+
const next = [...p];
|
|
525
|
+
let chars = next.reduce((n, x) => n + x.data.length, 0);
|
|
526
|
+
for (const img of read) {
|
|
527
|
+
if (next.length >= ASSISTANT_IMAGES_MAX ||
|
|
528
|
+
chars + img.data.length > ASSISTANT_IMAGE_CHARS) {
|
|
529
|
+
lumenToast.error(`Up to ${ASSISTANT_IMAGES_MAX} images go with one message.`);
|
|
530
|
+
break;
|
|
531
|
+
}
|
|
532
|
+
next.push(img);
|
|
533
|
+
chars += img.data.length;
|
|
534
|
+
}
|
|
535
|
+
return next;
|
|
536
|
+
});
|
|
537
|
+
})();
|
|
538
|
+
return true;
|
|
477
539
|
};
|
|
478
540
|
const run = async (c, args) => {
|
|
479
541
|
const keep = extra();
|
|
@@ -658,10 +720,20 @@ export function LumenComposer({ name, busy, queued, disabled, commands = [], tie
|
|
|
658
720
|
const r = sourceItems.get(o.value)?.();
|
|
659
721
|
if (r)
|
|
660
722
|
setRefs((p) => [...p, r]);
|
|
661
|
-
}, onClose: () => setClosed(`${text}|${caret}`) })), (tierLabel || refs.length > 0) && (_jsxs("div", { className: "mb-2 flex flex-wrap gap-1", children: [tierLabel && _jsx(ComposerChip, { sign: "@", label: tierLabel, onRemove: () => setTier(null) }), refs.map((r, i) => (_jsx(ComposerChip, { sign: "#", label: r.label, onRemove: () => setRefs((p) => p.filter((_, j) => j !== i)) }, `${r.kind}:${r.label}:${i}`)))] })), _jsxs("div", { className: "relative", children: [_jsx(Textarea, { ref: box, "aria-label": `Message ${name}`, placeholder: `Ask ${name}…${hints.length ? ` (${hints.join(' ')})` : ''}`, className: "max-h-48 min-h-11 resize-none pr-11", value: text, disabled: disabled, onChange: (e) => {
|
|
723
|
+
}, onClose: () => setClosed(`${text}|${caret}`) })), images.length > 0 && (_jsx("div", { className: "mb-2 flex flex-wrap gap-1.5", "aria-label": "Images going with your message", children: images.map((img, i) => (_jsxs("span", { className: "relative", children: [_jsx("img", { src: imageUrl(img), alt: `Pasted ${i + 1}`, className: "size-12 rounded-md border object-cover" }), _jsx("button", { type: "button", "aria-label": `Remove image ${i + 1}`, className: "absolute -top-1.5 -right-1.5 grid size-4 place-items-center rounded-full border bg-background text-muted-foreground hover:text-foreground", onClick: () => setImages((p) => p.filter((_, j) => j !== i)), children: _jsx(XIcon, { className: "size-3" }) })] }, `${i}:${img.data.length}`))) })), (tierLabel || refs.length > 0) && (_jsxs("div", { className: "mb-2 flex flex-wrap gap-1", children: [tierLabel && _jsx(ComposerChip, { sign: "@", label: tierLabel, onRemove: () => setTier(null) }), refs.map((r, i) => (_jsx(ComposerChip, { sign: "#", label: r.label, onRemove: () => setRefs((p) => p.filter((_, j) => j !== i)) }, `${r.kind}:${r.label}:${i}`)))] })), _jsxs("div", { className: "relative", children: [_jsx(Textarea, { ref: box, "aria-label": `Message ${name}`, placeholder: `Ask ${name}…${hints.length ? ` (${hints.join(' ')})` : ''}`, className: "max-h-48 min-h-11 resize-none pr-11", value: text, disabled: disabled, onChange: (e) => {
|
|
662
724
|
setText(e.target.value);
|
|
663
725
|
setCaret(e.target.selectionStart);
|
|
664
|
-
}, onSelect: (e) => setCaret(e.currentTarget.selectionStart),
|
|
726
|
+
}, onSelect: (e) => setCaret(e.currentTarget.selectionStart), onPaste: (e) => {
|
|
727
|
+
// An image on the clipboard becomes a chip; text pastes as usual.
|
|
728
|
+
if (!e.clipboardData.getData('text/plain') && attach(e.clipboardData.files))
|
|
729
|
+
e.preventDefault();
|
|
730
|
+
}, onDragOver: (e) => {
|
|
731
|
+
if (canAttach && [...e.dataTransfer.items].some((x) => x.type.startsWith('image/')))
|
|
732
|
+
e.preventDefault();
|
|
733
|
+
}, onDrop: (e) => {
|
|
734
|
+
if (attach(e.dataTransfer.files))
|
|
735
|
+
e.preventDefault();
|
|
736
|
+
}, onKeyDown: (e) => {
|
|
665
737
|
if (picking && list.current?.key(e))
|
|
666
738
|
return;
|
|
667
739
|
if (e.key === 'Enter' && !e.shiftKey && !e.nativeEvent.isComposing) {
|
|
@@ -711,17 +783,29 @@ function UsageTable({ usage }) {
|
|
|
711
783
|
return (_jsxs(Fragment, { children: [_jsx("span", { className: "truncate font-medium", title: p.label, children: r.tier }), _jsx("span", { className: "text-muted-foreground", children: r.kind }), _jsx("span", { className: "num text-right", children: usd(p.usd) }), _jsxs("span", { className: "num text-right", children: [tokens(p.in), " (", hitPct(p), "%)"] }), _jsx("span", { className: "num text-right", children: tokens(p.out) }), _jsx("span", { className: "num text-right", children: p.calls })] }, p.label));
|
|
712
784
|
}), _jsx("span", { className: cn(total, 'font-semibold'), children: "Total" }), _jsx("span", { className: total }), _jsx("span", { className: cn(total, 'num text-right font-semibold'), children: usd(usage.usd) }), _jsxs("span", { className: cn(total, 'num text-right'), children: [tokens(usage.in), " (", hitPct(usage), "%)"] }), _jsx("span", { className: cn(total, 'num text-right'), children: tokens(usage.out) }), _jsx("span", { className: cn(total, 'num text-right'), children: usage.calls })] }));
|
|
713
785
|
}
|
|
786
|
+
/**
|
|
787
|
+
* This month's total of every chat and run; it opens the month's history (each model, chat and
|
|
788
|
+
* run), a sheet on a phone.
|
|
789
|
+
*/
|
|
790
|
+
function MonthTotal({ client, spent, phone, className, }) {
|
|
791
|
+
const trigger = (_jsxs("button", { type: "button", className: cn('flex shrink-0 items-center gap-1 tabular-nums hover:text-foreground', className), "aria-label": "Usage this month", title: "Usage this month: every chat and run", children: [_jsx(ReceiptIcon, { className: "size-3", "aria-hidden": true }), _jsxs("span", { children: ["This month ", _jsx("span", { className: "num", children: usd(spent) })] })] }));
|
|
792
|
+
return phone ? (_jsxs(Sheet, { children: [_jsx(SheetTrigger, { asChild: true, children: trigger }), _jsxs(SheetContent, { side: "bottom", children: [_jsx(SheetHeader, { children: _jsx(SheetTitle, { children: "Usage" }) }), _jsx(UsageHistoryView, { client: client, className: "px-4 pb-6" })] })] })) : (_jsxs(Popover, { children: [_jsx(PopoverTrigger, { asChild: true, children: trigger }), _jsx(PopoverContent, { align: "end", className: "w-md max-w-[92vw] p-3", children: _jsx(UsageHistoryView, { client: client }) })] }));
|
|
793
|
+
}
|
|
714
794
|
/**
|
|
715
795
|
* The chat's cost, tokens in and out and cache hit (a pop-up splits them per part, a sheet on a
|
|
716
|
-
* phone),
|
|
796
|
+
* phone), how full the context is, and this month's total (`MonthTotal`).
|
|
717
797
|
*/
|
|
718
|
-
function UsageLine({ usage }) {
|
|
798
|
+
function UsageLine({ usage, client, spent, }) {
|
|
719
799
|
const phone = useIsMobile();
|
|
720
|
-
|
|
800
|
+
const month = !!client.usage && spent !== undefined && spent > 0;
|
|
801
|
+
if ((!usage || !usage.calls) && !month)
|
|
721
802
|
return null;
|
|
722
|
-
const ctx = usage
|
|
803
|
+
const ctx = usage?.context ?? 0;
|
|
804
|
+
const total = month ? (_jsx(MonthTotal, { client: client, spent: spent ?? 0, phone: phone, className: ctx > 0 ? '' : 'ml-auto' })) : null;
|
|
805
|
+
if (!usage || !usage.calls)
|
|
806
|
+
return (_jsx("div", { className: "flex shrink-0 items-center gap-3 px-3 pb-2 text-muted-foreground text-xs", children: total }));
|
|
723
807
|
const trigger = (_jsxs("button", { type: "button", className: "flex min-w-0 items-center gap-2 tabular-nums hover:text-foreground", "aria-label": "This chat's usage", children: [_jsxs("span", { className: "num truncate", children: [usd(usage.usd), " \u00B7 \u2191", tokens(usage.in), " \u2193", tokens(usage.out)] }), _jsxs("span", { className: "num flex shrink-0 items-center gap-0.5", title: "Read from cache", children: [_jsx(ZapIcon, { className: "size-3", "aria-hidden": true }), hitPct(usage), "%"] })] }));
|
|
724
|
-
return (_jsxs("div", { className: "flex shrink-0 items-center gap-3 px-3 pb-2 text-muted-foreground text-xs", children: [phone ? (_jsxs(Sheet, { children: [_jsx(SheetTrigger, { asChild: true, children: trigger }), _jsxs(SheetContent, { side: "bottom", children: [_jsx(SheetHeader, { children: _jsx(SheetTitle, { children: "This chat's usage" }) }), _jsx("div", { className: "overflow-x-auto px-4 pb-6", children: _jsx(UsageTable, { usage: usage }) })] })] })) : (_jsxs(Popover, { children: [_jsx(PopoverTrigger, { asChild: true, children: trigger }), _jsx(PopoverContent, { align: "start", className: "w-auto p-2", children: _jsx(UsageTable, { usage: usage }) })] })), ctx > 0 && (_jsxs("span", { className: "ml-auto flex items-center gap-1.5", title: `Context ${tokens(ctx)} of ${tokens(CONTEXT_MAX)}`, children: [_jsxs("span", { className: "relative h-1 w-16 overflow-hidden rounded-full bg-muted", children: [_jsx("span", { className: cn('absolute inset-y-0 left-0', ctx > COMPACT_AT ? 'bg-warning' : 'bg-primary/60'), style: { width: `${Math.min(100, (ctx / CONTEXT_MAX) * 100)}%` } }), _jsx("span", { className: "absolute inset-y-0 w-px bg-foreground/40", style: { left: `${(COMPACT_AT / CONTEXT_MAX) * 100}%` } })] }), _jsx("span", { className: "tabular-nums", children: tokens(ctx) })] }))] }));
|
|
808
|
+
return (_jsxs("div", { className: "flex shrink-0 items-center gap-3 px-3 pb-2 text-muted-foreground text-xs", children: [phone ? (_jsxs(Sheet, { children: [_jsx(SheetTrigger, { asChild: true, children: trigger }), _jsxs(SheetContent, { side: "bottom", children: [_jsx(SheetHeader, { children: _jsx(SheetTitle, { children: "This chat's usage" }) }), _jsx("div", { className: "overflow-x-auto px-4 pb-6", children: _jsx(UsageTable, { usage: usage }) })] })] })) : (_jsxs(Popover, { children: [_jsx(PopoverTrigger, { asChild: true, children: trigger }), _jsx(PopoverContent, { align: "start", className: "w-auto p-2", children: _jsx(UsageTable, { usage: usage }) })] })), ctx > 0 && (_jsxs("span", { className: "ml-auto flex items-center gap-1.5", title: `Context ${tokens(ctx)} of ${tokens(CONTEXT_MAX)}`, children: [_jsxs("span", { className: "relative h-1 w-16 overflow-hidden rounded-full bg-muted", children: [_jsx("span", { className: cn('absolute inset-y-0 left-0', ctx > COMPACT_AT ? 'bg-warning' : 'bg-primary/60'), style: { width: `${Math.min(100, (ctx / CONTEXT_MAX) * 100)}%` } }), _jsx("span", { className: "absolute inset-y-0 w-px bg-foreground/40", style: { left: `${(COMPACT_AT / CONTEXT_MAX) * 100}%` } })] }), _jsx("span", { className: "tabular-nums", children: tokens(ctx) })] })), total] }));
|
|
725
809
|
}
|
|
726
810
|
/**
|
|
727
811
|
* The assistant docked in LumenShell: put `button` in `headerActions` and `panel` in `panel`.
|
|
@@ -17,6 +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
21
|
const EFFORTS = [
|
|
21
22
|
{ value: 'default', label: 'Model default' },
|
|
22
23
|
{ value: 'low', label: 'Low' },
|
|
@@ -266,6 +267,9 @@ function Web({ client }) {
|
|
|
266
267
|
function Spend({ client }) {
|
|
267
268
|
return (_jsx(WithStatus, { client: client, children: (s) => {
|
|
268
269
|
const sp = s.spend;
|
|
270
|
+
// With the history route: each month, model, chat and run (`UsageHistoryView`).
|
|
271
|
+
if (client.usage)
|
|
272
|
+
return (_jsxs("div", { className: "flex flex-col gap-2", children: [sp.cap != null && sp.payer && (_jsx("p", { className: "text-muted-foreground text-sm", children: `${usd(sp.cap)} a month on ${sp.payer === 'app' ? 'the app key' : 'your key'}` })), _jsx(UsageHistoryView, { client: client })] }));
|
|
269
273
|
return (_jsxs("div", { className: "flex flex-col gap-3", children: [_jsxs("div", { className: "flex items-baseline gap-2", children: [_jsx("span", { className: "font-semibold text-2xl tabular-nums", children: usd(sp.usd) }), _jsxs("span", { className: "text-muted-foreground text-sm", children: [sp.month, sp.cap != null &&
|
|
270
274
|
sp.payer &&
|
|
271
275
|
` · ${usd(sp.cap)} cap on ${sp.payer === 'app' ? 'the app key' : 'your key'}`] })] }), sp.rows.length ? (_jsx("div", { className: "flex flex-col text-sm", children: sp.rows.map((r) => (_jsxs("div", { className: "flex items-center gap-2 border-b py-2 last:border-b-0", children: [_jsx("span", { className: "min-w-0 flex-1 truncate", children: r.label }), _jsx("span", { className: "text-muted-foreground text-xs", children: r.payer === 'app' ? 'Shared key' : 'Your key' }), _jsxs("span", { className: "w-16 text-right text-muted-foreground text-xs tabular-nums", children: [r.calls, " calls"] }), _jsx("span", { className: "w-16 text-right tabular-nums", children: usd(r.usd) })] }, `${r.label}:${r.payer}`))) })) : (_jsx("p", { className: "text-muted-foreground text-sm", children: "Nothing spent this month." }))] }));
|
|
@@ -437,7 +441,7 @@ export function assistantSettingsSections({ client, name, web = true, allowance
|
|
|
437
441
|
title: 'Spend',
|
|
438
442
|
icon: ReceiptIcon,
|
|
439
443
|
sub: true,
|
|
440
|
-
description: 'What
|
|
444
|
+
description: 'What each model, chat and run cost',
|
|
441
445
|
content: _jsx(Spend, { client: client }),
|
|
442
446
|
},
|
|
443
447
|
...(allowance
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { UsageHistory } from '@columenlabs/lumen-ui-types/assistant';
|
|
2
|
+
import type { AssistantAdapter } from './client.js';
|
|
3
|
+
export declare const usd: (n: number) => string;
|
|
4
|
+
export declare const tokens: (n: number) => string;
|
|
5
|
+
export declare const assistantUsageKey: (client: AssistantAdapter, month?: string) => string[];
|
|
6
|
+
/** One month of the person's use (this month when `month` is left out); off when the adapter has no `usage`. */
|
|
7
|
+
export declare function useAssistantUsage(client: AssistantAdapter, month?: string, enabled?: boolean): import("@tanstack/react-query").UseQueryResult<UsageHistory, Error>;
|
|
8
|
+
/**
|
|
9
|
+
* A month of use: the total, each model (cost, tokens in and out, calls) and each chat and run,
|
|
10
|
+
* newest first, with ‹ › to step through the months that have any.
|
|
11
|
+
*/
|
|
12
|
+
export declare function UsageHistoryView({ client, className, }: {
|
|
13
|
+
client: AssistantAdapter;
|
|
14
|
+
className?: string;
|
|
15
|
+
}): import("react").JSX.Element | null;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useQuery } from '@tanstack/react-query';
|
|
3
|
+
import { ChevronLeftIcon, ChevronRightIcon, CpuIcon, MessageSquareIcon } from 'lucide-react';
|
|
4
|
+
import { Fragment, useState } from 'react';
|
|
5
|
+
import { IconButton } from '../icon-button.js';
|
|
6
|
+
import { cn } from '../lib/utils.js';
|
|
7
|
+
import { Spinner } from '../ui/spinner.js';
|
|
8
|
+
export const usd = (n) => n === 0 ? '$0' : n < 0.01 ? '<$0.01' : `$${n.toFixed(n < 10 ? 2 : 0)}`;
|
|
9
|
+
export const tokens = (n) => n < 1000
|
|
10
|
+
? String(n)
|
|
11
|
+
: n < 1_000_000
|
|
12
|
+
? `${(n / 1000).toFixed(n < 10_000 ? 1 : 0)}k`
|
|
13
|
+
: `${(n / 1e6).toFixed(1)}M`;
|
|
14
|
+
export const assistantUsageKey = (client, month) => [
|
|
15
|
+
'lumen-assistant',
|
|
16
|
+
client.key,
|
|
17
|
+
'usage',
|
|
18
|
+
month ?? 'now',
|
|
19
|
+
];
|
|
20
|
+
/** One month of the person's use (this month when `month` is left out); off when the adapter has no `usage`. */
|
|
21
|
+
export function useAssistantUsage(client, month, enabled = true) {
|
|
22
|
+
return useQuery({
|
|
23
|
+
queryKey: assistantUsageKey(client, month),
|
|
24
|
+
queryFn: () => client.usage(month),
|
|
25
|
+
enabled: enabled && !!client.usage,
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
/** `2026-11` → `November 2026`. */
|
|
29
|
+
const monthName = (m) => {
|
|
30
|
+
const [y, mo] = m.split('-').map(Number);
|
|
31
|
+
return new Date(y ?? 2000, (mo ?? 1) - 1, 1).toLocaleDateString(undefined, {
|
|
32
|
+
month: 'long',
|
|
33
|
+
year: 'numeric',
|
|
34
|
+
});
|
|
35
|
+
};
|
|
36
|
+
/** Today's time, else the day. */
|
|
37
|
+
const when = (at) => {
|
|
38
|
+
const d = new Date(at);
|
|
39
|
+
return d.toDateString() === new Date().toDateString()
|
|
40
|
+
? d.toLocaleTimeString(undefined, { hour: 'numeric', minute: '2-digit' })
|
|
41
|
+
: d.toLocaleDateString(undefined, { month: 'short', day: 'numeric' });
|
|
42
|
+
};
|
|
43
|
+
/** The model of a part, from its label (`Steady · Sonnet 5.5` → `Sonnet 5.5`). */
|
|
44
|
+
const modelOf = (p) => p.label.split(' · ').slice(1).join(' · ') || p.label;
|
|
45
|
+
/** Rows of one month summed per model (each tier and payer of one model is one row). */
|
|
46
|
+
function perModel(rows) {
|
|
47
|
+
const out = new Map();
|
|
48
|
+
for (const r of rows) {
|
|
49
|
+
const k = r.model ?? modelOf(r);
|
|
50
|
+
const p = out.get(k);
|
|
51
|
+
if (!p)
|
|
52
|
+
out.set(k, { ...r, label: modelOf(r) });
|
|
53
|
+
else {
|
|
54
|
+
p.calls += r.calls;
|
|
55
|
+
p.in += r.in;
|
|
56
|
+
p.read += r.read;
|
|
57
|
+
p.out += r.out;
|
|
58
|
+
p.usd += r.usd;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return [...out.values()].sort((a, b) => b.usd - a.usd);
|
|
62
|
+
}
|
|
63
|
+
const entryHover = (e) => [
|
|
64
|
+
e.title,
|
|
65
|
+
...e.parts.map((p) => `${p.label}: ${usd(p.usd)} · ${tokens(p.in)} in · ${tokens(p.out)} out · ${p.calls} calls`),
|
|
66
|
+
].join('\n');
|
|
67
|
+
function Entry({ e }) {
|
|
68
|
+
const Icon = e.kind === 'chat' ? MessageSquareIcon : CpuIcon;
|
|
69
|
+
return (_jsxs("li", { className: "flex min-w-0 items-center gap-2 py-1.5 text-xs", title: entryHover(e), children: [_jsx(Icon, { className: "size-3.5 shrink-0 text-muted-foreground", "aria-hidden": true }), _jsx("span", { className: "min-w-0 flex-1 truncate", children: e.title }), _jsx("time", { className: "shrink-0 text-muted-foreground", children: when(e.at) }), _jsxs("span", { className: "num w-20 shrink-0 text-right text-muted-foreground", children: ["\u2191", tokens(e.in), " \u2193", tokens(e.out)] }), _jsx("span", { className: "num w-14 shrink-0 text-right", children: usd(e.usd) })] }));
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* A month of use: the total, each model (cost, tokens in and out, calls) and each chat and run,
|
|
73
|
+
* newest first, with ‹ › to step through the months that have any.
|
|
74
|
+
*/
|
|
75
|
+
export function UsageHistoryView({ client, className, }) {
|
|
76
|
+
const [month, setMonth] = useState(undefined);
|
|
77
|
+
const q = useAssistantUsage(client, month);
|
|
78
|
+
const h = q.data;
|
|
79
|
+
const at = h ? h.months.indexOf(h.month) : -1;
|
|
80
|
+
const older = h && at >= 0 ? h.months[at + 1] : undefined;
|
|
81
|
+
const newer = h && at > 0 ? h.months[at - 1] : undefined;
|
|
82
|
+
if (!client.usage)
|
|
83
|
+
return null;
|
|
84
|
+
if (!h)
|
|
85
|
+
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, {})) }));
|
|
86
|
+
const models = perModel(h.rows);
|
|
87
|
+
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
|
+
}
|
|
@@ -168,6 +168,9 @@ export function useAssistantChat(client) {
|
|
|
168
168
|
...(extra?.context ? { context: extra.context.label } : {}),
|
|
169
169
|
...(extra?.refs?.length ? { refs: extra.refs } : {}),
|
|
170
170
|
...(extra?.tier ? { tier: extra.tier } : {}),
|
|
171
|
+
...(extra?.images?.length
|
|
172
|
+
? { images: extra.images.map((i) => `data:${i.media_type};base64,${i.data}`) }
|
|
173
|
+
: {}),
|
|
171
174
|
},
|
|
172
175
|
]);
|
|
173
176
|
// Every screen outcome not yet sent goes with this message; kept again if it fails.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@columenlabs/lumen-ui",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.211",
|
|
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.211",
|
|
53
53
|
"@dnd-kit/core": "6.3.1",
|
|
54
54
|
"@dnd-kit/sortable": "10.0.0",
|
|
55
55
|
"@dnd-kit/utilities": "3.2.2",
|