@columenlabs/lumen-ui 0.1.300 → 0.1.302
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assistant/copy.js +1 -1
- package/dist/assistant/index.d.ts +1 -1
- package/dist/assistant/index.js +1 -1
- package/dist/assistant/lumen-assistant.d.ts +40 -5
- package/dist/assistant/lumen-assistant.js +153 -23
- package/dist/assistant/usage-history.d.ts +6 -4
- package/dist/assistant/usage-history.js +38 -9
- package/dist/lumen-view.d.ts +2 -1
- package/dist/lumen-view.js +11 -1
- package/package.json +2 -2
package/dist/assistant/copy.js
CHANGED
|
@@ -49,7 +49,7 @@ function itemMarkdown(i, depth, name) {
|
|
|
49
49
|
case 'step': {
|
|
50
50
|
const facts = [
|
|
51
51
|
i.rows !== undefined ? `${i.rows} rows` : '',
|
|
52
|
-
`${(i.ms / 1000).toFixed(1)} s
|
|
52
|
+
i.ms ? `${(i.ms / 1000).toFixed(1)} s` : '',
|
|
53
53
|
i.write ? i.write.status : '',
|
|
54
54
|
].filter(Boolean);
|
|
55
55
|
return `**${i.ok ? 'Tool' : 'Failed'}: ${i.title}** (${facts.join(', ')})${i.error ? `\n\nError: ${i.error}` : ''}`;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { type AssistantAdapter, assistantClient } from './client.js';
|
|
2
2
|
export { ChatImage } from './chat-image.js';
|
|
3
|
-
export { type AssistantCommand, type AssistantMode, type ComposerExtra, LumenAssistant, type LumenAssistantProps, LumenComposer, assistantStatusKey, readAssistantImage, useAssistantStatus, useLumenAssistantDock, } from './lumen-assistant.js';
|
|
3
|
+
export { type AssistantCommand, type AssistantMode, type ComposerExtra, LumenAssistant, type LumenAssistantProps, LumenComposer, QuickReplies, WorkCard, assistantStatusKey, readAssistantImage, useAssistantStatus, useLumenAssistantDock, } from './lumen-assistant.js';
|
|
4
4
|
export { AutoCompactField, assistantAdminSections, assistantSettingsSections } from './settings.js';
|
|
5
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';
|
package/dist/assistant/index.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// `@columenlabs/lumen-ui-types/assistant`.
|
|
4
4
|
export { assistantClient } from './client.js';
|
|
5
5
|
export { ChatImage } from './chat-image.js';
|
|
6
|
-
export { LumenAssistant, LumenComposer, assistantStatusKey, readAssistantImage, useAssistantStatus, useLumenAssistantDock, } from './lumen-assistant.js';
|
|
6
|
+
export { LumenAssistant, LumenComposer, QuickReplies, WorkCard, assistantStatusKey, readAssistantImage, useAssistantStatus, useLumenAssistantDock, } from './lumen-assistant.js';
|
|
7
7
|
export { AutoCompactField, assistantAdminSections, assistantSettingsSections } from './settings.js';
|
|
8
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';
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type AllowanceUnit, type AssistantImage, type AssistantMessage, type AssistantQuote, type AssistantRefNote, type AssistantStatus, type AssistantTask, type AssistantTaskState } from '@columenlabs/lumen-ui-types/assistant';
|
|
1
|
+
import { type AllowanceUnit, type AssistantImage, type AssistantMessage, type AssistantQuote, type AssistantRefNote, type AssistantStatus, type AssistantTask, type AssistantTaskState, type AssistantTaskView, type StepChoices } from '@columenlabs/lumen-ui-types/assistant';
|
|
2
2
|
import { type LucideIcon } from 'lucide-react';
|
|
3
3
|
import { type ReactNode } from 'react';
|
|
4
4
|
import { type LumenOption } from '../lumen-selector.js';
|
|
@@ -65,14 +65,21 @@ export interface LumenAssistantProps {
|
|
|
65
65
|
compactAt?: number;
|
|
66
66
|
/**
|
|
67
67
|
* A task in the chat (`AssistantTask`, the app's work in its own session): the buttons in its
|
|
68
|
-
*
|
|
68
|
+
* card's header, such as Discard and Accept, from the app; `live` while the task's turn runs
|
|
69
|
+
* (the card adds Stop then).
|
|
69
70
|
*/
|
|
70
|
-
taskActions?: (task: AssistantTask) => ReactNode;
|
|
71
|
+
taskActions?: (task: AssistantTask, live: boolean) => ReactNode;
|
|
71
72
|
/**
|
|
72
73
|
* The app's word on where a task stands, ahead of the chat's: it took or dropped the task's result
|
|
73
|
-
* in its own screens.
|
|
74
|
+
* in its own screens. While a task is open, each message the person sends goes to it.
|
|
74
75
|
*/
|
|
75
76
|
taskState?: (task: AssistantTask) => AssistantTaskState | undefined;
|
|
77
|
+
/**
|
|
78
|
+
* The header of a task's card (`AssistantTaskView`): its tone (running, ready, settled, failed,
|
|
79
|
+
* waiting on an answer, stopped), title and the line under it. Without it the card shows the
|
|
80
|
+
* task's label and the chat's word for its state.
|
|
81
|
+
*/
|
|
82
|
+
taskView?: (task: AssistantTask, live: boolean) => AssistantTaskView | undefined;
|
|
76
83
|
}
|
|
77
84
|
/** A composer mode: its toggle's icon and label (also its tooltip and aria-label). */
|
|
78
85
|
export interface AssistantMode {
|
|
@@ -94,9 +101,37 @@ export interface AssistantCommand {
|
|
|
94
101
|
run: (args: string) => string | undefined | Promise<string | undefined>;
|
|
95
102
|
}
|
|
96
103
|
/** The assistant's chat: header, messages and composer. */
|
|
97
|
-
export declare function LumenAssistant({ client, context, onSettings, keepDays, chatScope, onClose, chat: outside, commands, sources, images, modes, imageTool, compactAt, taskActions, taskState, }: LumenAssistantProps): import("react").JSX.Element;
|
|
104
|
+
export declare function LumenAssistant({ client, context, onSettings, keepDays, chatScope, onClose, chat: outside, commands, sources, images, modes, imageTool, compactAt, taskActions, taskState, taskView, }: LumenAssistantProps): import("react").JSX.Element;
|
|
98
105
|
/** An allowance in its unit: `$4.20`, or `350 credits`. */
|
|
99
106
|
export declare const allowanceAmount: (u: AllowanceUnit, n: number) => string;
|
|
107
|
+
/**
|
|
108
|
+
* A piece of the assistant's work as one card: a tinted icon for where it stands, its title and the
|
|
109
|
+
* line under it (`AssistantTaskView`), the app's buttons, Stop while it runs, a thin running bar,
|
|
110
|
+
* and a chevron that shows its steps (`children`). The chat draws each task this way; an app draws
|
|
111
|
+
* its own work the same way (an edit waiting for review) so every piece of work looks alike.
|
|
112
|
+
*/
|
|
113
|
+
export declare function WorkCard({ view, live, state, actions, onStop, steps, open: shownAt, children, }: {
|
|
114
|
+
view: AssistantTaskView;
|
|
115
|
+
live?: boolean;
|
|
116
|
+
/** The task's own state, for `data-state`. */
|
|
117
|
+
state?: string;
|
|
118
|
+
actions?: ReactNode;
|
|
119
|
+
onStop?: () => void;
|
|
120
|
+
/** How many steps `children` holds: no chevron without any. */
|
|
121
|
+
steps?: number;
|
|
122
|
+
/** Starts open; by default it is open while the work runs. */
|
|
123
|
+
open?: boolean;
|
|
124
|
+
children?: ReactNode;
|
|
125
|
+
}): import("react").JSX.Element;
|
|
126
|
+
/**
|
|
127
|
+
* Quick replies under the assistant's question: one row per answer, the suggested one outlined,
|
|
128
|
+
* numbered so a key picks it (when no text box has the focus). With `multi` each row ticks, and
|
|
129
|
+
* Send sends the ticked ones together. They are shortcuts: typing an answer in the box works too.
|
|
130
|
+
*/
|
|
131
|
+
export declare function QuickReplies({ choices, onSend, }: {
|
|
132
|
+
choices: StepChoices;
|
|
133
|
+
onSend: (text: string) => void;
|
|
134
|
+
}): import("react").JSX.Element;
|
|
100
135
|
/** What the composer sends with the text: the tier picked with `#` and the things pointed at with `@`. */
|
|
101
136
|
export interface ComposerExtra {
|
|
102
137
|
tier?: string;
|
|
@@ -6,7 +6,7 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
|
|
|
6
6
|
// with Stop, a queued message and the chat's cost and context under it.
|
|
7
7
|
import { ASSISTANT_IMAGES_MAX, ASSISTANT_IMAGE_CHARS, COMPACT_AT, } from '@columenlabs/lumen-ui-types/assistant';
|
|
8
8
|
import { useQuery, useQueryClient } from '@tanstack/react-query';
|
|
9
|
-
import { ArrowUpIcon, CheckIcon, ChevronRightIcon, CircleAlertIcon, CopyIcon, CornerDownRightIcon, CrosshairIcon, FileTextIcon, GlobeIcon, LightbulbIcon, ListPlusIcon, MonitorIcon, PaperclipIcon, PencilIcon, PlusIcon, RefreshCwIcon, ReplyIcon, SearchIcon, SparklesIcon, SquareIcon, TriangleAlertIcon, Undo2Icon, WrenchIcon, XIcon, } from 'lucide-react';
|
|
9
|
+
import { ArrowUpIcon, CheckCheckIcon, CheckIcon, ChevronDownIcon, ChevronRightIcon, ChevronUpIcon, CircleAlertIcon, CircleCheckIcon, CircleXIcon, CopyIcon, CornerDownRightIcon, CrosshairIcon, FileTextIcon, GlobeIcon, HandIcon, LightbulbIcon, ListPlusIcon, LoaderCircleIcon, MonitorIcon, PaperclipIcon, PencilIcon, PlusIcon, RefreshCwIcon, ReplyIcon, SearchIcon, SparklesIcon, SquareIcon, TriangleAlertIcon, Undo2Icon, WrenchIcon, XIcon, } from 'lucide-react';
|
|
10
10
|
import { Fragment, createContext, useContext, useEffect, useRef, useState, } from 'react';
|
|
11
11
|
import { LumenAssistantButton, LumenAssistantChats, LumenAssistantPanel, assistantOpen, useAssistantHotkey, } from '../assistant-panel.js';
|
|
12
12
|
import { screenMapFor, useAssistantScreen } from '../assistant-screen.js';
|
|
@@ -47,9 +47,9 @@ export function useAssistantStatus(client) {
|
|
|
47
47
|
return useQuery({ queryKey: assistantStatusKey(client), queryFn: () => client.status() });
|
|
48
48
|
}
|
|
49
49
|
export { usd };
|
|
50
|
-
const seconds = (ms) =>
|
|
50
|
+
const seconds = (ms) => !ms ? '' : ms < 1000 ? `${ms} ms` : `${(ms / 1000).toFixed(1)} s`;
|
|
51
51
|
/** The assistant's chat: header, messages and composer. */
|
|
52
|
-
export function LumenAssistant({ client, context, onSettings, keepDays = 30, chatScope, onClose, chat: outside, commands = [], sources = [], images = false, modes = [], imageTool, compactAt = COMPACT_AT, taskActions, taskState, }) {
|
|
52
|
+
export function LumenAssistant({ client, context, onSettings, keepDays = 30, chatScope, onClose, chat: outside, commands = [], sources = [], images = false, modes = [], imageTool, compactAt = COMPACT_AT, taskActions, taskState, taskView, }) {
|
|
53
53
|
const own = useAssistantChat(client);
|
|
54
54
|
const chat = outside ?? own;
|
|
55
55
|
const qc = useQueryClient();
|
|
@@ -80,12 +80,9 @@ export function LumenAssistant({ client, context, onSettings, keepDays = 30, cha
|
|
|
80
80
|
// each new screen call runs here once; how it ended goes with the next message.
|
|
81
81
|
const screen = useAssistantScreen();
|
|
82
82
|
const screenOn = !!screen && (s?.screen ?? 'move') === 'move';
|
|
83
|
-
//
|
|
84
|
-
//
|
|
85
|
-
const
|
|
86
|
-
const openTask = chat.task && (taskState?.(chat.task) ?? chat.task.state) === 'open' && stepped !== chat.task.id
|
|
87
|
-
? chat.task
|
|
88
|
-
: null;
|
|
83
|
+
// While a task is open, every message goes to it (the app decides what that means); the task
|
|
84
|
+
// ends when the app takes or drops its result.
|
|
85
|
+
const openTask = chat.task && (taskState?.(chat.task) ?? chat.task.state) === 'open' ? chat.task : null;
|
|
89
86
|
const extra = () => ({
|
|
90
87
|
...(context ? { context } : {}),
|
|
91
88
|
...(screenOn && screen ? { screen: screenMapFor(screen) } : {}),
|
|
@@ -198,9 +195,9 @@ export function LumenAssistant({ client, context, onSettings, keepDays = 30, cha
|
|
|
198
195
|
title: c.title,
|
|
199
196
|
updated_at: new Date(c.at).toISOString(),
|
|
200
197
|
...(chatScope && allScopes && c.scope ? { where: chatScope.name?.(c.scope) } : {}),
|
|
201
|
-
})) ?? (chats.isFetched ? [] : null), value: chat.chatId ?? undefined, toggle: chatScope && { label: chatScope.label, on: allScopes, onChange: setAllScopes }, 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(ImageTool.Provider, { value: imageTool ?? null, children: _jsx(TaskSlots.Provider, { value: { actions: taskActions, state: taskState }, children: _jsx(Messages, { chat: chat, status: s, onSettings: onSettings, onRetry: (at, user) => chat.retry(at, user, extra()), onFresh: (choice) => chat.fresh(choice, extra())
|
|
198
|
+
})) ?? (chats.isFetched ? [] : null), value: chat.chatId ?? undefined, toggle: chatScope && { label: chatScope.label, on: allScopes, onChange: setAllScopes }, 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(ImageTool.Provider, { value: imageTool ?? null, children: _jsx(TaskSlots.Provider, { value: { actions: taskActions, state: taskState, view: taskView, stop: chat.stop }, children: _jsx(Messages, { chat: chat, status: s, onSettings: onSettings, onRetry: (at, user) => chat.retry(at, user, extra()), onFresh: (choice) => chat.fresh(choice, extra()), onAnswer: (text) => chat.send(text, extra()) }) }) }) }) }), _jsx(LumenComposer, { name: s?.name ?? 'the assistant', busy: chat.busy, queued: chat.queued, queue: chat.queue, canSteer: chat.canSteer, onSteerQueued: chat.steerQueued, onUnqueue: chat.unqueue, disabled: !s || !!s.wait, commands: allCommands, tiers: tiers, sources: allSources, page: context?.label, images: images, modes: modes, onSend: (text, more, how) => chat.send(text, { ...extra(), ...more }, how), onStop: chat.stop, meter: _jsx(ContextDonut, { usage: chat.usage, compactAt: s?.compact_at ?? compactAt, items: chat.items }) })] }));
|
|
202
199
|
}
|
|
203
|
-
function Messages({ chat, status, onSettings, onRetry, onFresh, }) {
|
|
200
|
+
function Messages({ chat, status, onSettings, onRetry, onFresh, onAnswer, }) {
|
|
204
201
|
const end = useRef(null);
|
|
205
202
|
const box = useRef(null);
|
|
206
203
|
const count = JSON.stringify(chat.items).length;
|
|
@@ -219,7 +216,12 @@ function Messages({ chat, status, onSettings, onRetry, onFresh, }) {
|
|
|
219
216
|
status.allowance &&
|
|
220
217
|
status.allowance.request?.status !== 'open'
|
|
221
218
|
? 'Request more'
|
|
222
|
-
: 'Open Settings' }))] })] })), chat.loading ? (_jsxs("p", { className: "flex items-center gap-2 text-muted-foreground text-sm", children: [_jsx(Spinner, {}), " Loading the chat\u2026"] })) : chat.items.length === 0 && !status?.wait ? (_jsxs("p", { className: "pt-8 text-center text-muted-foreground text-sm", children: ["Ask ", status?.name ?? 'the assistant', " anything."] })) : (_jsx(TierNames.Provider, { value: (id) => status?.tiers.find((t) => t.id === id)?.label ?? id, children: _jsx(UndoWrite.Provider, { value: chat.undo, children: _jsx(RetryTurn.Provider, { value: chat.busy || status?.wait ? null : onRetry, children: _jsx(FreshChoice.Provider, { value: chat.busy ? null : onFresh, children: _jsx(Items, { items: chat.items, onSettle: chat.settle, busy: chat.busy, turns: true }) }) }) }) })), chat.busy &&
|
|
219
|
+
: 'Open Settings' }))] })] })), chat.loading ? (_jsxs("p", { className: "flex items-center gap-2 text-muted-foreground text-sm", children: [_jsx(Spinner, {}), " Loading the chat\u2026"] })) : chat.items.length === 0 && !status?.wait ? (_jsxs("p", { className: "pt-8 text-center text-muted-foreground text-sm", children: ["Ask ", status?.name ?? 'the assistant', " anything."] })) : (_jsx(TierNames.Provider, { value: (id) => status?.tiers.find((t) => t.id === id)?.label ?? id, children: _jsx(UndoWrite.Provider, { value: chat.undo, children: _jsx(RetryTurn.Provider, { value: chat.busy || status?.wait ? null : onRetry, children: _jsx(FreshChoice.Provider, { value: chat.busy ? null : onFresh, children: _jsx(Items, { items: chat.items, onSettle: chat.settle, busy: chat.busy, turns: true }) }) }) }) })), !chat.busy &&
|
|
220
|
+
!status?.wait &&
|
|
221
|
+
(() => {
|
|
222
|
+
const c = lastChoices(chat.items);
|
|
223
|
+
return c ? (_jsx("div", { className: "mt-2", children: _jsx(QuickReplies, { choices: c, onSend: onAnswer }, chat.items.length) })) : null;
|
|
224
|
+
})(), chat.busy && !chat.items.some(isLive) && (_jsxs("p", { className: "flex items-center gap-2 py-1 text-muted-foreground text-sm", children: [_jsx(Spinner, {}), " Thinking\u2026"] })), _jsx("div", { ref: end })] }));
|
|
223
225
|
}
|
|
224
226
|
const isLive = (i) => !!i.live || ((i.kind === 'handoff' || i.kind === 'task') && i.items.some(isLive));
|
|
225
227
|
const WAIT = {
|
|
@@ -456,7 +458,13 @@ function ToolStep({ step, live }) {
|
|
|
456
458
|
setUndoing(false);
|
|
457
459
|
}
|
|
458
460
|
};
|
|
459
|
-
|
|
461
|
+
// A change that did not land is a failure too, whatever the call returned.
|
|
462
|
+
const ok = step.ok && w?.status !== 'failed';
|
|
463
|
+
const facts = [
|
|
464
|
+
step.rows !== undefined ? `${step.rows} row${step.rows === 1 ? '' : 's'}` : '',
|
|
465
|
+
seconds(step.ms),
|
|
466
|
+
].filter(Boolean);
|
|
467
|
+
return (_jsxs("p", { "data-slot": "chat-step", "data-ok": ok, className: cn('flex min-w-0 items-center gap-1.5 text-xs', ok ? 'text-muted-foreground' : 'text-loss'), children: [live ? (_jsx(Spinner, { className: "size-3" })) : !ok ? (_jsx(CircleAlertIcon, { className: "size-3.5 shrink-0" })) : w ? (_jsx(PencilIcon, { className: "size-3.5 shrink-0" })) : (_jsx(WrenchIcon, { className: "size-3.5 shrink-0" })), _jsx("span", { className: "truncate", title: step.title, children: step.title }), !live && facts.length > 0 && (_jsx("span", { className: "shrink-0 tabular-nums", children: facts.join(' · ') })), (step.error || w?.error) && (_jsxs("span", { className: "truncate", title: step.error ?? w?.error, children: ["\u00B7 ", step.error ?? w?.error] })), w?.status === 'undone' ? (_jsx("span", { className: "shrink-0", children: "\u00B7 Undone" })) : (w?.undo &&
|
|
460
468
|
undo && (_jsxs(Button, { size: "sm", variant: "ghost", className: "ml-auto h-6 shrink-0 px-2 text-xs", disabled: undoing, onClick: () => void take(), children: [undoing ? _jsx(Spinner, {}) : _jsx(Undo2Icon, {}), "Undo"] })))] }));
|
|
461
469
|
}
|
|
462
470
|
/** The arguments as short lines: `name: value`. */
|
|
@@ -541,28 +549,150 @@ const WHY = {
|
|
|
541
549
|
function HandOff({ item, onSettle, busy, }) {
|
|
542
550
|
return (_jsxs("section", { className: "flex flex-col gap-2 border-l-2 border-primary/40 pl-3", children: [_jsxs("p", { className: "flex items-center gap-1.5 text-xs", children: [_jsx(CornerDownRightIcon, { className: "size-3.5 text-muted-foreground" }), _jsx("span", { className: "font-medium", children: item.label }), _jsxs("span", { className: "text-muted-foreground", children: ["\u00B7 ", WHY[item.why]] })] }), item.task && _jsx("p", { className: "text-muted-foreground text-xs", children: item.task }), _jsx(Items, { items: item.items, onSettle: onSettle, busy: busy })] }));
|
|
543
551
|
}
|
|
544
|
-
/** The app's parts of a task's
|
|
552
|
+
/** The app's parts of a task's card (`taskActions`, `taskState`, `taskView`) and the chat's Stop. */
|
|
545
553
|
const TaskSlots = createContext({});
|
|
554
|
+
/** The words a card says without the app's own view. */
|
|
546
555
|
const TASK_STATE = {
|
|
547
556
|
open: 'Open',
|
|
548
557
|
done: 'Kept',
|
|
549
558
|
dropped: 'Dropped',
|
|
550
559
|
};
|
|
560
|
+
/** Each tone's icon and colour: the icon sits in a tinted square at the head of the card. */
|
|
561
|
+
const TONE = {
|
|
562
|
+
live: {
|
|
563
|
+
icon: LoaderCircleIcon,
|
|
564
|
+
box: 'bg-primary/12 text-primary',
|
|
565
|
+
word: 'text-primary',
|
|
566
|
+
spin: true,
|
|
567
|
+
},
|
|
568
|
+
ready: { icon: CircleCheckIcon, box: 'bg-gain/15 text-gain', word: 'text-gain' },
|
|
569
|
+
done: {
|
|
570
|
+
icon: CheckCheckIcon,
|
|
571
|
+
box: 'bg-muted text-muted-foreground',
|
|
572
|
+
word: 'text-muted-foreground',
|
|
573
|
+
},
|
|
574
|
+
failed: { icon: CircleAlertIcon, box: 'bg-loss/12 text-loss', word: 'text-loss' },
|
|
575
|
+
ask: { icon: HandIcon, box: 'bg-warning/15 text-warning', word: 'text-warning' },
|
|
576
|
+
stopped: {
|
|
577
|
+
icon: CircleXIcon,
|
|
578
|
+
box: 'bg-muted text-muted-foreground',
|
|
579
|
+
word: 'text-muted-foreground',
|
|
580
|
+
},
|
|
581
|
+
};
|
|
582
|
+
/** m:ss since a time, ticking each second while shown. */
|
|
583
|
+
function Clock({ since }) {
|
|
584
|
+
const [now, setNow] = useState(() => Date.now());
|
|
585
|
+
useEffect(() => {
|
|
586
|
+
const t = setInterval(() => setNow(Date.now()), 1000);
|
|
587
|
+
return () => clearInterval(t);
|
|
588
|
+
}, []);
|
|
589
|
+
const s = Math.max(0, Math.floor((now - since) / 1000));
|
|
590
|
+
return (_jsx("span", { className: "tabular-nums", children: `${Math.floor(s / 60)}:${String(s % 60).padStart(2, '0')}` }));
|
|
591
|
+
}
|
|
592
|
+
/** Where a task's card sits among its items: after the person's last message that tools answered. */
|
|
593
|
+
function cardAt(items) {
|
|
594
|
+
const last = items.findLastIndex((i) => i.kind === 'step' || i.kind === 'draft');
|
|
595
|
+
if (last < 0)
|
|
596
|
+
return items.findLastIndex((i) => i.kind === 'user') + 1;
|
|
597
|
+
for (let i = last; i >= 0; i--)
|
|
598
|
+
if (items[i]?.kind === 'user')
|
|
599
|
+
return i + 1;
|
|
600
|
+
return 0;
|
|
601
|
+
}
|
|
551
602
|
/**
|
|
552
|
-
* A
|
|
553
|
-
*
|
|
603
|
+
* A piece of the assistant's work as one card: a tinted icon for where it stands, its title and the
|
|
604
|
+
* line under it (`AssistantTaskView`), the app's buttons, Stop while it runs, a thin running bar,
|
|
605
|
+
* and a chevron that shows its steps (`children`). The chat draws each task this way; an app draws
|
|
606
|
+
* its own work the same way (an edit waiting for review) so every piece of work looks alike.
|
|
607
|
+
*/
|
|
608
|
+
export function WorkCard({ view, live = false, state, actions, onStop, steps = 0, open: shownAt, children, }) {
|
|
609
|
+
const tone = TONE[view.tone];
|
|
610
|
+
const [open, setOpen] = useState(null);
|
|
611
|
+
const shown = (open ?? (shownAt || live)) && steps > 0;
|
|
612
|
+
const Icon = tone.icon;
|
|
613
|
+
return (_jsxs("section", { "data-slot": "chat-task", "data-state": state, "data-tone": view.tone, "aria-label": view.title, className: cn('flex min-w-0 flex-col overflow-hidden rounded-lg border bg-card', view.tone === 'live' && 'border-primary/45', view.tone === 'ready' && 'border-gain/45', view.tone === 'failed' && 'border-loss/45', view.tone === 'ask' && 'border-warning/45'), children: [_jsxs("div", { className: "flex min-w-0 items-center gap-2.5 py-2 pr-1.5 pl-2.5", children: [_jsx("span", { className: cn('grid size-7 shrink-0 place-items-center rounded-md', tone.box), children: _jsx(Icon, { className: cn('size-4', tone.spin && 'animate-spin'), "aria-hidden": true }) }), _jsxs("div", { className: "flex min-w-0 flex-1 flex-col", children: [_jsx("span", { className: "truncate font-medium text-sm", title: view.title, children: view.title }), (view.detail || view.state || (live && view.since)) && (_jsxs("span", { className: "flex min-w-0 gap-1 text-muted-foreground text-xs", children: [view.detail && (_jsx("span", { className: "min-w-0 truncate", title: view.detail, children: view.detail })), view.state && (_jsxs("span", { className: cn('shrink-0', tone.word), children: [view.detail ? '· ' : '', view.state] })), live && view.since ? (_jsxs("span", { className: "shrink-0 tabular-nums", children: ["\u00B7 ", _jsx(Clock, { since: view.since })] })) : null] }))] }), _jsxs("span", { className: "flex shrink-0 items-center gap-1", children: [actions, onStop && (_jsx(IconButton, { icon: SquareIcon, label: "Stop", variant: "outline", size: "icon-sm", onClick: onStop })), steps > 0 && (_jsx(IconButton, { icon: shown ? ChevronUpIcon : ChevronDownIcon, label: shown ? 'Hide steps' : 'Show steps', size: "icon-sm", "aria-expanded": shown, onClick: () => setOpen(!shown) }))] })] }), live && (_jsx("div", { className: "h-0.5 w-full overflow-hidden bg-primary/15", children: _jsx("div", { className: "h-full w-1/3 animate-[lumen-progress_1.4s_ease-in-out_infinite] bg-primary" }) })), shown && _jsx("div", { className: "border-t px-3 py-2", children: children })] }));
|
|
614
|
+
}
|
|
615
|
+
/**
|
|
616
|
+
* A task's turns in the chat (`TaskItem`) as one card: a tinted icon for where it stands, its title
|
|
617
|
+
* and a line under it (the app's `taskView`), the app's buttons (`taskActions`), Stop while it runs
|
|
618
|
+
* and a chevron that shows the steps. The person's messages and the assistant's words stay in the
|
|
619
|
+
* chat around it, so a conversation about the work reads as one; the card sits at the newest turn
|
|
620
|
+
* that ran tools, holding that turn's steps, and earlier turns' steps stay as lines where they ran.
|
|
554
621
|
*/
|
|
555
622
|
function TaskBlock({ item, onSettle, busy, }) {
|
|
556
623
|
const slots = useContext(TaskSlots);
|
|
557
624
|
const state = slots.state?.(item) ?? item.state;
|
|
558
|
-
const
|
|
559
|
-
const
|
|
560
|
-
const
|
|
561
|
-
|
|
625
|
+
const items = item.items;
|
|
626
|
+
const live = items.some(isLive);
|
|
627
|
+
const view = slots.view?.(item, live) ?? {
|
|
628
|
+
tone: live ? 'live' : state === 'open' ? 'ready' : 'done',
|
|
629
|
+
title: item.label,
|
|
630
|
+
state: live ? 'Working' : TASK_STATE[state],
|
|
631
|
+
};
|
|
632
|
+
const at = cardAt(items);
|
|
633
|
+
const steps = items.slice(at).filter((i) => i.kind === 'step' || i.kind === 'draft');
|
|
634
|
+
const after = items.slice(at).filter((i) => i.kind !== 'step' && i.kind !== 'draft');
|
|
635
|
+
const card = (_jsx(WorkCard, { view: view, live: live, state: state, actions: slots.actions?.(item, live), onStop: live ? slots.stop : undefined, steps: steps.length, open: steps.some((x) => x.kind === 'step' && (!x.ok || x.write?.status === 'failed')), children: _jsx(Items, { items: steps, onSettle: onSettle, busy: busy }) }));
|
|
636
|
+
return (_jsxs("div", { "data-slot": "chat-task-turns", className: "flex min-w-0 flex-col gap-3", children: [at > 0 && _jsx(Items, { items: items.slice(0, at), onSettle: onSettle, busy: busy }), card, after.length > 0 && _jsx(Items, { items: after, onSettle: onSettle, busy: busy })] }));
|
|
562
637
|
}
|
|
563
|
-
/** The
|
|
564
|
-
function
|
|
565
|
-
|
|
638
|
+
/** The quick replies the last turn offers (`StepItem.choices`), unless the person answered since. */
|
|
639
|
+
function lastChoices(items) {
|
|
640
|
+
for (let i = items.length - 1; i >= 0; i--) {
|
|
641
|
+
const it = items[i];
|
|
642
|
+
if (it.kind === 'user')
|
|
643
|
+
return null;
|
|
644
|
+
if (it.kind === 'task') {
|
|
645
|
+
const inner = lastChoices(it.items);
|
|
646
|
+
if (inner || it.items.some((x) => x.kind === 'user'))
|
|
647
|
+
return inner;
|
|
648
|
+
continue;
|
|
649
|
+
}
|
|
650
|
+
if (it.kind === 'step' && it.choices)
|
|
651
|
+
return it.choices;
|
|
652
|
+
}
|
|
653
|
+
return null;
|
|
654
|
+
}
|
|
655
|
+
/**
|
|
656
|
+
* Quick replies under the assistant's question: one row per answer, the suggested one outlined,
|
|
657
|
+
* numbered so a key picks it (when no text box has the focus). With `multi` each row ticks, and
|
|
658
|
+
* Send sends the ticked ones together. They are shortcuts: typing an answer in the box works too.
|
|
659
|
+
*/
|
|
660
|
+
export function QuickReplies({ choices, onSend, }) {
|
|
661
|
+
const [picked, setPicked] = useState(() => choices.multi ? choices.options.flatMap((o, n) => (o.on ? [n] : [])) : []);
|
|
662
|
+
const { options, multi, suggested } = choices;
|
|
663
|
+
const pick = (n) => {
|
|
664
|
+
const o = options[n];
|
|
665
|
+
if (!o)
|
|
666
|
+
return;
|
|
667
|
+
if (!multi)
|
|
668
|
+
return onSend(o.label);
|
|
669
|
+
setPicked((p) => (p.includes(n) ? p.filter((x) => x !== n) : [...p, n]));
|
|
670
|
+
};
|
|
671
|
+
const pickRef = useRef(pick);
|
|
672
|
+
pickRef.current = pick;
|
|
673
|
+
useEffect(() => {
|
|
674
|
+
const on = (e) => {
|
|
675
|
+
const t = e.target;
|
|
676
|
+
if (e.metaKey || e.ctrlKey || e.altKey)
|
|
677
|
+
return;
|
|
678
|
+
if (t?.closest('input, textarea, [contenteditable="true"], [role="dialog"]'))
|
|
679
|
+
return;
|
|
680
|
+
const n = Number(e.key);
|
|
681
|
+
if (!Number.isInteger(n) || n < 1 || n > options.length)
|
|
682
|
+
return;
|
|
683
|
+
e.preventDefault();
|
|
684
|
+
pickRef.current(n - 1);
|
|
685
|
+
};
|
|
686
|
+
window.addEventListener('keydown', on);
|
|
687
|
+
return () => window.removeEventListener('keydown', on);
|
|
688
|
+
}, [options.length]);
|
|
689
|
+
return (_jsxs("fieldset", { "data-slot": "chat-choices", "aria-label": "Quick replies", className: "m-0 flex min-w-0 flex-col gap-1.5 border-0 p-0", children: [options.map((o, n) => {
|
|
690
|
+
const on = picked.includes(n);
|
|
691
|
+
return (_jsxs("button", { type: "button", "aria-pressed": multi ? on : undefined, onClick: () => pick(n), className: cn('flex min-w-0 items-center gap-2.5 rounded-lg border bg-card px-2.5 py-2 text-left text-sm hover:bg-muted focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none', n === suggested && !multi && 'border-primary ring-1 ring-primary', on && 'border-primary'), children: [multi ? (_jsxs(_Fragment, { children: [_jsx("span", { className: cn('grid size-4 shrink-0 place-items-center rounded border', on && 'border-primary bg-primary text-primary-foreground'), children: on && _jsx(CheckIcon, { className: "size-3" }) }), _jsx("span", { className: "grid size-4.5 shrink-0 place-items-center rounded-full bg-muted font-medium text-muted-foreground text-xs tabular-nums", children: n + 1 })] })) : (_jsx("kbd", { className: "grid size-4.5 shrink-0 place-items-center rounded bg-muted font-medium text-xs text-muted-foreground", children: n + 1 })), _jsxs("span", { className: "min-w-0 flex-1 truncate", title: o.label, children: [o.label, (o.note || n === suggested) && (_jsx("span", { className: "ml-1.5 text-muted-foreground text-xs", children: o.note ?? 'suggested' }))] })] }, o.label));
|
|
692
|
+
}), multi && (_jsxs("div", { className: "flex items-center justify-end gap-2 text-muted-foreground text-xs", children: [_jsxs("span", { children: [picked.length, " picked"] }), _jsx(IconButton, { icon: ArrowUpIcon, label: "Send picks", variant: "default", size: "icon-sm", disabled: !picked.length, onClick: () => onSend(options
|
|
693
|
+
.filter((_, n) => picked.includes(n))
|
|
694
|
+
.map((o) => o.label)
|
|
695
|
+
.join('; ')) })] }))] }));
|
|
566
696
|
}
|
|
567
697
|
/** The composer's triggers: `@` points at a thing (the `sources`), `#` picks the tier for one message. */
|
|
568
698
|
const POINT = '@';
|
|
@@ -36,10 +36,12 @@ export declare function AssistantChatCost({ client, chatId, busy, }: {
|
|
|
36
36
|
busy?: boolean;
|
|
37
37
|
}): import("react").JSX.Element | null;
|
|
38
38
|
/**
|
|
39
|
-
* This month's use, one row a session (a chat, or every run of one task on one thing): its
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
39
|
+
* This month's use, one row a session (a chat, or every run of one task on one thing): its
|
|
40
|
+
* Category (Chat, Edit, Rebuild: `UsageEntry.category`), Type (the person's API key, their plan
|
|
41
|
+
* sign-in, or the app's credits), Date, Calls and Amount, grouped by Date (first), Category, Type or
|
|
42
|
+
* not at all, filtered by category, type or name, PAGE_SIZE rows a page (a group's totals count all
|
|
43
|
+
* its rows). A row's hover gives its tokens and cost. A sign-in's amount is an estimate: the same
|
|
44
|
+
* tokens at the model's API rates (the plan charges $0).
|
|
43
45
|
*/
|
|
44
46
|
export declare function AssistantUsageTable({ client }: {
|
|
45
47
|
client: AssistantAdapter;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useQuery } from '@tanstack/react-query';
|
|
3
|
-
import { CalendarIcon, ChevronLeftIcon, ChevronRightIcon, CpuIcon, HashIcon, KeyRoundIcon, MessageSquareIcon, MessagesSquareIcon, ReceiptIcon, } from 'lucide-react';
|
|
3
|
+
import { CalendarIcon, ChevronLeftIcon, ChevronRightIcon, CpuIcon, HashIcon, KeyRoundIcon, LayersIcon, MessageSquareIcon, MessagesSquareIcon, ReceiptIcon, } from 'lucide-react';
|
|
4
4
|
import { Fragment, useEffect, useMemo, useState } from 'react';
|
|
5
5
|
import { DetailsTable } from '../details-table.js';
|
|
6
6
|
import { IconButton } from '../icon-button.js';
|
|
@@ -8,6 +8,7 @@ import { InfoLabel } from '../info-tip.js';
|
|
|
8
8
|
import { cn } from '../lib/utils.js';
|
|
9
9
|
import { LumenTokenizedFilter, useLumenFilter } from '../lumen-filter.js';
|
|
10
10
|
import { LumenTable } from '../lumen-view.js';
|
|
11
|
+
import { LumenPageFooter } from '../pager.js';
|
|
11
12
|
import { SettingField } from '../setting-parts.js';
|
|
12
13
|
import { Badge } from '../ui/badge.js';
|
|
13
14
|
import { Label } from '../ui/label.js';
|
|
@@ -280,6 +281,7 @@ function usageRows(h) {
|
|
|
280
281
|
id,
|
|
281
282
|
title: e.title,
|
|
282
283
|
type: 'credits',
|
|
284
|
+
category: s?.category ?? (e.category || (e.kind === 'chat' ? 'Chat' : (e.task ?? 'Run'))),
|
|
283
285
|
at: Math.max(s?.at ?? 0, e.at),
|
|
284
286
|
calls: (s?.calls ?? 0) + e.calls,
|
|
285
287
|
tokens: {
|
|
@@ -320,6 +322,7 @@ function usageRows(h) {
|
|
|
320
322
|
id: `earlier:${type}:${title}`,
|
|
321
323
|
title,
|
|
322
324
|
type,
|
|
325
|
+
category: EARLIER,
|
|
323
326
|
at: 0,
|
|
324
327
|
calls,
|
|
325
328
|
key: type === 'key' ? amount : 0,
|
|
@@ -430,6 +433,17 @@ const USAGE_FIELDS = [
|
|
|
430
433
|
value: (r) => TYPE_LABEL[r.type],
|
|
431
434
|
render: (r) => (_jsx(UsageTip, { r: r, children: _jsx(Badge, { variant: "outline", className: cn('whitespace-nowrap', CHIP_TONE[TYPE_TONE[r.type]]), children: TYPE_LABEL[r.type] }) })),
|
|
432
435
|
},
|
|
436
|
+
{
|
|
437
|
+
key: 'category',
|
|
438
|
+
label: 'Category',
|
|
439
|
+
icon: LayersIcon,
|
|
440
|
+
format: 'text',
|
|
441
|
+
groupable: true,
|
|
442
|
+
filterable: true,
|
|
443
|
+
hideOnPhone: true,
|
|
444
|
+
value: (r) => r.category,
|
|
445
|
+
render: (r) => (_jsx(UsageTip, { r: r, children: _jsx(Badge, { variant: "outline", className: cn('whitespace-nowrap', CHIP_TONE.muted), children: r.category }) })),
|
|
446
|
+
},
|
|
433
447
|
{
|
|
434
448
|
key: 'at',
|
|
435
449
|
label: 'Date',
|
|
@@ -488,27 +502,42 @@ const USAGE_DOMAIN = {
|
|
|
488
502
|
formulas: false,
|
|
489
503
|
};
|
|
490
504
|
const USAGE_FILTER = [
|
|
505
|
+
{ key: 'category', label: 'Category', icon: LayersIcon, values: (r) => [r.category] },
|
|
491
506
|
{ key: 'type', label: 'Type', icon: KeyRoundIcon, values: (r) => [TYPE_LABEL[r.type]] },
|
|
492
507
|
];
|
|
508
|
+
const fieldOf = (key) => USAGE_FIELDS.find((f) => f.key === key) ?? null;
|
|
493
509
|
const GROUPS = [
|
|
494
|
-
{ id: 'type', label: 'Type', field: USAGE_FIELDS[1] },
|
|
495
|
-
{ id: 'none', label: 'None', field: null },
|
|
496
510
|
{ id: 'date', label: 'Date', field: DAY_FIELD },
|
|
511
|
+
{ id: 'category', label: 'Category', field: fieldOf('category') },
|
|
512
|
+
{ id: 'type', label: 'Type', field: fieldOf('type') },
|
|
513
|
+
{ id: 'none', label: 'None', field: null },
|
|
497
514
|
];
|
|
515
|
+
/** Rows a page of the table shows; a group's totals still count every row of it. */
|
|
516
|
+
const PAGE_SIZE = 50;
|
|
498
517
|
/**
|
|
499
|
-
* This month's use, one row a session (a chat, or every run of one task on one thing): its
|
|
500
|
-
*
|
|
501
|
-
*
|
|
502
|
-
*
|
|
518
|
+
* This month's use, one row a session (a chat, or every run of one task on one thing): its
|
|
519
|
+
* Category (Chat, Edit, Rebuild: `UsageEntry.category`), Type (the person's API key, their plan
|
|
520
|
+
* sign-in, or the app's credits), Date, Calls and Amount, grouped by Date (first), Category, Type or
|
|
521
|
+
* not at all, filtered by category, type or name, PAGE_SIZE rows a page (a group's totals count all
|
|
522
|
+
* its rows). A row's hover gives its tokens and cost. A sign-in's amount is an estimate: the same
|
|
523
|
+
* tokens at the model's API rates (the plan charges $0).
|
|
503
524
|
*/
|
|
504
525
|
export function AssistantUsageTable({ client }) {
|
|
505
|
-
const [group, setGroup] = useState('
|
|
526
|
+
const [group, setGroup] = useState('date');
|
|
527
|
+
const [page, setPage] = useState(0);
|
|
506
528
|
const q = useAssistantUsage(client);
|
|
507
529
|
const rows = useMemo(() => (q.data ? usageRows(q.data) : []), [q.data]);
|
|
508
530
|
const filter = useLumenFilter(rows, USAGE_FILTER, { text: (r) => [r.title] });
|
|
531
|
+
const shown = filter.results;
|
|
532
|
+
// A new filter or grouping starts from the first page; a page past the end shows the last.
|
|
533
|
+
const rowSet = shown.map((r) => r.id).join('\u0000');
|
|
534
|
+
// biome-ignore lint/correctness/useExhaustiveDependencies: resets on these changes only
|
|
535
|
+
useEffect(() => setPage(0), [rowSet, group]);
|
|
536
|
+
const pages = Math.max(1, Math.ceil(shown.length / PAGE_SIZE));
|
|
537
|
+
const at = Math.min(page, pages - 1);
|
|
509
538
|
if (!client.usage)
|
|
510
539
|
return null;
|
|
511
540
|
if (!q.data)
|
|
512
541
|
return (_jsx("div", { className: "flex justify-center p-4", children: q.isError ? (_jsx("p", { className: "text-muted-foreground text-sm", children: "Usage didn't load. Try again later." })) : (_jsx(Spinner, {})) }));
|
|
513
|
-
return (_jsxs("div", { className: "flex min-w-0 flex-col gap-2", children: [_jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [_jsx(LumenTokenizedFilter, { filter: filter, label: "Filter usage", placeholder: "Filter: type, or a session name", className: "min-w-48 flex-1", link: null }), _jsx(ToggleGroup, { type: "single", variant: "outline", size: "sm", spacing: 0, value: group, "aria-label": "Group by", onValueChange: (v) => v && setGroup(v), children: GROUPS.map((g) => (_jsx(ToggleGroupItem, { value: g.id, "aria-label": `Group by ${g.label}`, children: g.label }, g.id))) })] }),
|
|
542
|
+
return (_jsxs("div", { className: "flex min-w-0 flex-col gap-2", children: [_jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [_jsx(LumenTokenizedFilter, { filter: filter, label: "Filter usage", placeholder: "Filter: category, type, or a session name", className: "min-w-48 flex-1", link: null }), _jsx(ToggleGroup, { type: "single", variant: "outline", size: "sm", spacing: 0, value: group, "aria-label": "Group by", onValueChange: (v) => v && setGroup(v), children: GROUPS.map((g) => (_jsx(ToggleGroupItem, { value: g.id, "aria-label": `Group by ${g.label}`, children: g.label }, g.id))) })] }), _jsxs("div", { className: "min-w-0", children: [_jsx(LumenTable, { domain: USAGE_DOMAIN, rows: shown, groupBy: GROUPS.find((g) => g.id === group)?.field ?? null, page: { index: at, size: PAGE_SIZE }, sort: { key: 'at', desc: true }, emptyState: rows.length ? undefined : (_jsx("p", { className: "text-muted-foreground text-sm", children: "Nothing used this month." })), onClearFilter: filter.clear }), pages > 1 && (_jsx(LumenPageFooter, { total: shown.length, page: at, size: PAGE_SIZE, onPage: setPage }))] }), _jsxs("p", { className: "text-muted-foreground text-xs", children: ["Sign-in amounts are estimates: your ", PLAN_NAME, " charges $0, so the amount is what the same tokens would cost at the model's API rates. Key and credit amounts are what was charged."] })] }));
|
|
514
543
|
}
|
package/dist/lumen-view.d.ts
CHANGED
|
@@ -213,7 +213,8 @@ export interface LumenTableProps<T> {
|
|
|
213
213
|
rows: T[];
|
|
214
214
|
groupBy?: ViewField<T> | null;
|
|
215
215
|
chips?: ViewChip[];
|
|
216
|
-
/** The page a
|
|
216
|
+
/** The page a table shows; null shows every row. A grouped table pages its rows in group order:
|
|
217
|
+
* a group with rows on the page shows its row there, its count and totals over all its rows. */
|
|
217
218
|
page?: {
|
|
218
219
|
index: number;
|
|
219
220
|
size: number;
|
package/dist/lumen-view.js
CHANGED
|
@@ -761,6 +761,13 @@ function LumenTableRows({ domain, columns = domain.fields, rows, groupBy = null,
|
|
|
761
761
|
}
|
|
762
762
|
return [...m];
|
|
763
763
|
}, [groupBy, sorted, serverGroups]);
|
|
764
|
+
// A grouped table's page (`page`): the rows on it, in group order; every group keeps its totals.
|
|
765
|
+
const paged = useMemo(() => {
|
|
766
|
+
if (!groups || !page || serverGroups)
|
|
767
|
+
return null;
|
|
768
|
+
const start = page.index * page.size;
|
|
769
|
+
return new Set(groups.flatMap(([, ms]) => ms).slice(start, start + page.size));
|
|
770
|
+
}, [groups, page, serverGroups]);
|
|
764
771
|
const span = shown.length + (selection ? 1 : 0) + (add ? 1 : 0);
|
|
765
772
|
const served = new Map((serverGroups?.list ?? []).map((g) => [g.key, g]));
|
|
766
773
|
const startsOpen = (i) => rows.length <= OPEN_ALL || i === 0;
|
|
@@ -959,6 +966,9 @@ function LumenTableRows({ domain, columns = domain.fields, rows, groupBy = null,
|
|
|
959
966
|
(!serverGroups || serverGroups.onToggleAll) && (_jsx(IconButton, { icon: anyClosed ? ChevronsUpDownIcon : ChevronsDownUpIcon, size: "icon-xs", className: "-ml-2 mr-1 align-middle", label: anyClosed ? 'Expand all groups' : 'Collapse all groups', onClick: () => setAll(anyClosed) })), _jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { asChild: true, children: _jsxs(Button, { variant: "ghost", size: "xs", className: cn('font-medium', !(i === 0 && groups) && '-mx-2'), style: { color: colorOf(domain, shown[i]) ?? undefined }, onClick: h.column.getCanSort() ? h.column.getToggleSortingHandler() : undefined, children: [shown[i] ? _jsx(FreshnessMark, { domain: domain, field: shown[i] }) : null, flexRender(h.column.columnDef.header, h.getContext()), _jsx(SortIcon, { dir: h.column.getIsSorted() })] }) }), shown[i] && (domain.groups?.length || freshnessOf(domain, shown[i])) ? (_jsxs(TooltipContent, { children: [domain.groups?.length ? _jsx("div", { children: pathOf(domain, shown[i]) }) : null, freshnessLine(domain, shown[i]) ? (_jsx("div", { children: freshnessLine(domain, shown[i]) })) : null] })) : null] }), resizing && shown[i] && (_jsx(ColumnResizer, { label: shown[i].label, width: widthOf(shown[i]), onWidth: (w) => colWidths.set(h.id, w), onDone: (w) => colWidths.save(h.id, w), onReset: () => colWidths.reset(h.id) }))] }, h.id))), add && (_jsx(TableHead, { className: cn('w-8 pl-1', PINNED_HEAD), children: _jsx(AddColumnMenu, { add: add, fieldOpen: fieldOpen, onFieldOpen: setFieldOpen }) }))] }, hg.id))) }), _jsxs(TableBody, { children: [!rows.length && !serverGroups?.list?.length && (_jsx(TableRow, { "data-slot": "table-empty", className: "hover:bg-transparent", children: _jsx(TableCell, { colSpan: span, className: "py-6 text-center whitespace-normal text-muted-foreground", children: _jsxs("div", { className: "flex flex-col items-center gap-3", children: [emptyState ?? 'No rows match.', onClearFilter && (_jsxs(Button, { variant: "outline", size: "sm", onClick: onClearFilter, children: [_jsx(FilterXIcon, {}), "Clear filters"] }))] }) }) })), groups && groupBy
|
|
960
967
|
? groups.map(([key, members], gi) => {
|
|
961
968
|
const open = isOpen(key, gi);
|
|
969
|
+
const here = paged ? members.filter((m) => paged.has(m)) : members;
|
|
970
|
+
if (!here.length)
|
|
971
|
+
return null;
|
|
962
972
|
return (_jsxs(Fragment, { children: [_jsxs(TableRow, { className: ROLLUP, "data-group": key, "data-rollup": "", children: [selection && (_jsx(TableCell, { "data-card": "select", className: cn('w-8 pr-0', PINNED_GROUP), children: _jsx(Checkbox, { checked: members.every((m) => selection.keys.has(keyOf(m.original)))
|
|
963
973
|
? true
|
|
964
974
|
: members.some((m) => selection.keys.has(keyOf(m.original)))
|
|
@@ -972,7 +982,7 @@ function LumenTableRows({ domain, columns = domain.fields, rows, groupBy = null,
|
|
|
972
982
|
}), children: [_jsx(ChevronRightIcon, { className: cn('transition-transform', open && 'rotate-90') }), _jsx(GroupMark, { field: groupBy, value: key }), _jsx("span", { className: "truncate", children: key }), _jsx(GroupCount, { children: serverGroups
|
|
973
983
|
? (served.get(key)?.n?.toLocaleString() ?? '')
|
|
974
984
|
: members.length })] }), groupActions?.(key, members.map((m) => m.original))] }) }, f.key)) : (_jsx(TableCell, { ...col(f, PINNED_GROUP), children: !serverGroups &&
|
|
975
|
-
rollCell(f, members.map((m) => m.original), 'group') }, f.key))), add && _jsx(TableCell, { className: PINNED_GROUP })] }), open && (serverGroups ? servedRows(members, key) : drawn(
|
|
985
|
+
rollCell(f, members.map((m) => m.original), 'group') }, f.key))), add && _jsx(TableCell, { className: PINNED_GROUP })] }), open && (serverGroups ? servedRows(members, key) : drawn(here, key))] }, key));
|
|
976
986
|
})
|
|
977
987
|
: drawn(sorted, ''), serverGroups?.more && !serverGroups.pages && (_jsx(TableRow, { "data-more": true, children: _jsx(TableCell, { colSpan: span, className: "py-1", children: _jsxs(Button, { variant: "ghost", size: "xs", onClick: serverGroups.more, children: [_jsx(ChevronsDownIcon, {}), "More groups"] }) }) })), hasTotal && rows.length > 0 && (_jsxs(TableRow, { className: cn(ROLLUP, 'border-t-2'), "data-total": true, "data-rollup": "", children: [selection && _jsx(TableCell, {}), rollCells(rows).map((cell, i) => i === 0 && !(shown[0] && rollsUp(shown[0])) ? (_jsx(TableCell, { "data-card": "lead", children: "Total" }, "total")) : (cell)), add && _jsx(TableCell, {})] }))] })] }) })] }));
|
|
978
988
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@columenlabs/lumen-ui",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.302",
|
|
4
4
|
"description": "Lumen: shared React components (frame, views, widgets, charts, drawers).",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
"@codemirror/lang-javascript": "6.2.5",
|
|
50
50
|
"@codemirror/lang-json": "6.0.2",
|
|
51
51
|
"@codemirror/lang-sql": "6.10.0",
|
|
52
|
-
"@columenlabs/lumen-ui-types": "0.1.
|
|
52
|
+
"@columenlabs/lumen-ui-types": "0.1.302",
|
|
53
53
|
"@dnd-kit/core": "6.3.1",
|
|
54
54
|
"@dnd-kit/sortable": "10.0.0",
|
|
55
55
|
"@dnd-kit/utilities": "3.2.2",
|