@columenlabs/lumen-ui 0.1.266 → 0.1.268
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.
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type AllowanceUnit, type AssistantImage, type AssistantMessage, type AssistantQuote, type AssistantRefNote, type AssistantStatus } from '@columenlabs/lumen-ui-types/assistant';
|
|
2
2
|
import { type LucideIcon } from 'lucide-react';
|
|
3
|
+
import { type ReactNode } from 'react';
|
|
3
4
|
import { type LumenOption } from '../lumen-selector.js';
|
|
4
5
|
import type { AssistantAdapter } from './client.js';
|
|
5
6
|
import { type AssistantSource } from './sources.js';
|
|
@@ -58,8 +59,8 @@ export interface LumenAssistantProps {
|
|
|
58
59
|
mode?: string;
|
|
59
60
|
};
|
|
60
61
|
/**
|
|
61
|
-
* Where the server compacts the chat, in tokens (the agent's `compactAt()`),
|
|
62
|
-
*
|
|
62
|
+
* Where the server compacts the chat, in tokens (the agent's `compactAt()`), named in the context
|
|
63
|
+
* donut's hover. 150k by default.
|
|
63
64
|
*/
|
|
64
65
|
compactAt?: number;
|
|
65
66
|
}
|
|
@@ -107,7 +108,9 @@ export declare function readAssistantImage(file: File): Promise<AssistantImage |
|
|
|
107
108
|
* triggers list what can follow them as it is typed: `/` at the start a command, `@` a thing to
|
|
108
109
|
* point at (the `sources`), `#` a tier for this message; a picked tier or thing shows as a chip.
|
|
109
110
|
*/
|
|
110
|
-
export declare function LumenComposer({ name, busy, queued, queue, canSteer, onSteerQueued, onUnqueue, disabled, commands, tiers, sources, page, images: canAttach, modes, onSend, onStop, }: {
|
|
111
|
+
export declare function LumenComposer({ name, busy, queued, queue, canSteer, onSteerQueued, onUnqueue, disabled, commands, tiers, sources, page, images: canAttach, modes, onSend, onStop, meter, }: {
|
|
112
|
+
/** Drawn in the icon row right after the paperclip: `LumenAssistant` puts its context donut here. */
|
|
113
|
+
meter?: ReactNode;
|
|
111
114
|
/** Toggles beside the paperclip (`LumenAssistant` `modes`). */
|
|
112
115
|
modes?: AssistantMode[];
|
|
113
116
|
name: string;
|
|
@@ -23,6 +23,7 @@ import { Button } from '../ui/button.js';
|
|
|
23
23
|
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '../ui/collapsible.js';
|
|
24
24
|
import { InputGroup, InputGroupAddon, InputGroupTextarea } from '../ui/input-group.js';
|
|
25
25
|
import { Spinner } from '../ui/spinner.js';
|
|
26
|
+
import { Tooltip, TooltipContent, TooltipTrigger } from '../ui/tooltip.js';
|
|
26
27
|
import { ChatImage } from './chat-image.js';
|
|
27
28
|
import { chatMarkdown, chatTurns, copyMarkdown, turnMarkdown } from './copy.js';
|
|
28
29
|
import { ImageCard, ImagePending, toolIs } from './image-card.js';
|
|
@@ -190,7 +191,7 @@ export function LumenAssistant({ client, context, onSettings, keepDays = 30, cha
|
|
|
190
191
|
title: c.title,
|
|
191
192
|
updated_at: new Date(c.at).toISOString(),
|
|
192
193
|
...(chatScope && allScopes && c.scope ? { where: chatScope.name?.(c.scope) } : {}),
|
|
193
|
-
})) ?? (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(Messages, { chat: chat, status: s, onSettings: onSettings, onRetry: (at, user) => chat.retry(at, user, extra()), onFresh: (choice) => chat.fresh(choice, 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
|
|
194
|
+
})) ?? (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(Messages, { chat: chat, status: s, onSettings: onSettings, onRetry: (at, user) => chat.retry(at, user, extra()), onFresh: (choice) => chat.fresh(choice, 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: compactAt }) })] }));
|
|
194
195
|
}
|
|
195
196
|
function Messages({ chat, status, onSettings, onRetry, onFresh, }) {
|
|
196
197
|
const end = useRef(null);
|
|
@@ -580,7 +581,7 @@ const imageUrl = (i) => `data:${i.media_type};base64,${i.data}`;
|
|
|
580
581
|
* triggers list what can follow them as it is typed: `/` at the start a command, `@` a thing to
|
|
581
582
|
* point at (the `sources`), `#` a tier for this message; a picked tier or thing shows as a chip.
|
|
582
583
|
*/
|
|
583
|
-
export function LumenComposer({ name, busy, queued, queue, canSteer = false, onSteerQueued, onUnqueue, disabled, commands = [], tiers = [], sources = [], page, images: canAttach = false, modes = [], onSend, onStop, }) {
|
|
584
|
+
export function LumenComposer({ name, busy, queued, queue, canSteer = false, onSteerQueued, onUnqueue, disabled, commands = [], tiers = [], sources = [], page, images: canAttach = false, modes = [], onSend, onStop, meter, }) {
|
|
584
585
|
const [text, setText] = useState('');
|
|
585
586
|
const [caret, setCaret] = useState(0);
|
|
586
587
|
const [closed, setClosed] = useState(null);
|
|
@@ -903,7 +904,7 @@ export function LumenComposer({ name, busy, queued, queue, canSteer = false, onS
|
|
|
903
904
|
if (e.target.files)
|
|
904
905
|
attach(e.target.files);
|
|
905
906
|
e.target.value = '';
|
|
906
|
-
} })] })), modes.map((md) => (_jsx(IconButton, { icon: md.icon, label: md.label, size: "icon-xs", variant: use === md.id ? 'secondary' : 'ghost', "aria-pressed": use === md.id, className: cn(use === md.id && 'text-primary'), disabled: disabled, onClick: () => setUse((u) => (u === md.id ? null : md.id)) }, md.id))), _jsx("div", { className: "ml-auto", children: busy && !text.trim() ? (_jsx(IconButton, { icon: SquareIcon, label: "Stop", variant: "outline", size: "icon-xs", onClick: onStop })) : busy ? (_jsxs("span", { className: "flex gap-1", children: [_jsx(IconButton, { icon: ListPlusIcon, label: canSteer ? 'Queue for after (⌥↩)' : 'Queue for after (↩)', variant: canSteer ? 'outline' : 'default', size: "icon-xs", disabled: disabled, onClick: () => send('queue') }), canSteer && (_jsx(IconButton, { icon: CornerDownRightIcon, label: "Steer now (\u21A9)", variant: "default", size: "icon-xs", disabled: disabled, onClick: () => send('steer') }))] })) : (_jsx(IconButton, { icon: ArrowUpIcon, label: "Send", variant: "default", size: "icon-xs", disabled: !text.trim() || disabled, onClick: () => send() })) })] })] })] }));
|
|
907
|
+
} })] })), meter, modes.map((md) => (_jsx(IconButton, { icon: md.icon, label: md.label, size: "icon-xs", variant: use === md.id ? 'secondary' : 'ghost', "aria-pressed": use === md.id, className: cn(use === md.id && 'text-primary'), disabled: disabled, onClick: () => setUse((u) => (u === md.id ? null : md.id)) }, md.id))), _jsx("div", { className: "ml-auto", children: busy && !text.trim() ? (_jsx(IconButton, { icon: SquareIcon, label: "Stop", variant: "outline", size: "icon-xs", onClick: onStop })) : busy ? (_jsxs("span", { className: "flex gap-1", children: [_jsx(IconButton, { icon: ListPlusIcon, label: canSteer ? 'Queue for after (⌥↩)' : 'Queue for after (↩)', variant: canSteer ? 'outline' : 'default', size: "icon-xs", disabled: disabled, onClick: () => send('queue') }), canSteer && (_jsx(IconButton, { icon: CornerDownRightIcon, label: "Steer now (\u21A9)", variant: "default", size: "icon-xs", disabled: disabled, onClick: () => send('steer') }))] })) : (_jsx(IconButton, { icon: ArrowUpIcon, label: "Send", variant: "default", size: "icon-xs", disabled: !text.trim() || disabled, onClick: () => send() })) })] })] })] }));
|
|
907
908
|
}
|
|
908
909
|
/**
|
|
909
910
|
* What waits above the box while an answer runs: steering messages until the answer reads them,
|
|
@@ -928,15 +929,30 @@ const pointedRef = (p) => p.kind === 'card'
|
|
|
928
929
|
text: textOf(p.el),
|
|
929
930
|
}
|
|
930
931
|
: { kind: p.section.kind, label: p.section.label, id: p.section.id, text: textOf(p.el) };
|
|
932
|
+
/** Context tokens from which the donut turns yellow, and red. */
|
|
933
|
+
const CONTEXT_WATCH = 75_000;
|
|
934
|
+
const CONTEXT_ALERT = 100_000;
|
|
931
935
|
/**
|
|
932
|
-
* How full the chat's context is
|
|
933
|
-
*
|
|
936
|
+
* How full the chat's context is, as a small donut in the composer's icon row (after the paperclip):
|
|
937
|
+
* it fills against `CONTEXT_MAX`, neutral below 75k, yellow from 75k, red from 100k. The hover gives
|
|
938
|
+
* the tokens used, where the chat compacts, and how much of the last turn came from the prompt cache.
|
|
939
|
+
* What a chat cost is not shown here: a person reads their use in Settings.
|
|
934
940
|
*/
|
|
935
|
-
function
|
|
941
|
+
function ContextDonut({ usage, compactAt }) {
|
|
936
942
|
const ctx = usage?.context ?? 0;
|
|
937
943
|
if (ctx <= 0)
|
|
938
944
|
return null;
|
|
939
|
-
|
|
945
|
+
const share = Math.min(1, ctx / CONTEXT_MAX);
|
|
946
|
+
const r = 6;
|
|
947
|
+
const round = 2 * Math.PI * r;
|
|
948
|
+
const tone = ctx >= CONTEXT_ALERT
|
|
949
|
+
? 'text-destructive'
|
|
950
|
+
: ctx >= CONTEXT_WATCH
|
|
951
|
+
? 'text-warning'
|
|
952
|
+
: 'text-muted-foreground';
|
|
953
|
+
const cached = usage && usage.in > 0 ? Math.round((usage.read / usage.in) * 100) : null;
|
|
954
|
+
const said = `Context ${tokens(ctx)} of ${tokens(CONTEXT_MAX)}`;
|
|
955
|
+
return (_jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { asChild: true, children: _jsx("button", { type: "button", "aria-label": said, "data-testid": "context-donut", className: cn('grid size-6 place-items-center rounded-md hover:bg-muted focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none', tone), children: _jsxs("svg", { viewBox: "0 0 16 16", className: "size-4 -rotate-90", "aria-hidden": "true", children: [_jsx("title", { children: said }), _jsx("circle", { cx: "8", cy: "8", r: r, fill: "none", strokeWidth: "2.5", className: "stroke-muted" }), _jsx("circle", { cx: "8", cy: "8", r: r, fill: "none", strokeWidth: "2.5", stroke: "currentColor", strokeDasharray: `${share * round} ${round}` })] }) }) }), _jsxs(TooltipContent, { className: "flex flex-col gap-0.5", children: [_jsx("span", { className: "num", children: said }), _jsxs("span", { className: "num text-muted-foreground", children: ["Compacts at ", tokens(compactAt)] }), cached !== null && (_jsxs("span", { className: "num text-muted-foreground", children: [cached, "% of the last turn read from cache"] }))] })] }));
|
|
940
956
|
}
|
|
941
957
|
/**
|
|
942
958
|
* The assistant docked in LumenShell: put `button` in `headerActions` and `panel` in `panel`.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@columenlabs/lumen-ui",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.268",
|
|
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.268",
|
|
53
53
|
"@dnd-kit/core": "6.3.1",
|
|
54
54
|
"@dnd-kit/sortable": "10.0.0",
|
|
55
55
|
"@dnd-kit/utilities": "3.2.2",
|