@columenlabs/lumen-ui 0.1.212 → 0.1.214
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/chat-image.d.ts +4 -0
- package/dist/assistant/chat-image.js +47 -0
- package/dist/assistant/lumen-assistant.d.ts +19 -8
- package/dist/assistant/lumen-assistant.js +48 -17
- package/dist/assistant/sources.d.ts +5 -5
- package/dist/assistant/sources.js +2 -2
- package/dist/assistant-panel.d.ts +9 -1
- package/dist/assistant-panel.js +13 -4
- package/package.json +3 -2
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
// ChatImage: an image in a chat message. A thumbnail until clicked; then it opens full width in place,
|
|
3
|
+
// where the wheel or a pinch zooms at the pointer, a drag or two fingers pan and a double-click
|
|
4
|
+
// toggles fit and 2× (react-zoom-pan-pinch). Its toolbar zooms, resets, opens the image in a 90%
|
|
5
|
+
// lightbox with the same controls (the kit's Dialog: Esc, × or a click outside closes it) and
|
|
6
|
+
// shrinks it back to the thumbnail.
|
|
7
|
+
import { Maximize2Icon, Minimize2Icon, RotateCcwIcon, XIcon, ZoomInIcon, ZoomOutIcon, } from 'lucide-react';
|
|
8
|
+
import { useState } from 'react';
|
|
9
|
+
import { TransformComponent, TransformWrapper, useControls, useTransformEffect, } from 'react-zoom-pan-pinch';
|
|
10
|
+
import { IconButton } from '../icon-button.js';
|
|
11
|
+
import { cn } from '../lib/utils.js';
|
|
12
|
+
import { Dialog, DialogClose, DialogContent, DialogTitle } from '../ui/dialog.js';
|
|
13
|
+
import { Tooltip, TooltipContent, TooltipTrigger } from '../ui/tooltip.js';
|
|
14
|
+
const MAX_SCALE = 8;
|
|
15
|
+
/** The zoom buttons and the scale between them; `children` are the buttons after a divider. */
|
|
16
|
+
function ZoomBar({ children }) {
|
|
17
|
+
const { zoomIn, zoomOut, resetTransform } = useControls();
|
|
18
|
+
const [scale, setScale] = useState(1);
|
|
19
|
+
useTransformEffect(({ state }) => setScale(state.scale));
|
|
20
|
+
return (_jsxs("div", { className: "absolute top-2 right-2 z-10 flex items-center gap-0.5 rounded-lg border bg-popover/90 p-0.5 text-popover-foreground shadow-sm backdrop-blur-sm", "data-slot": "image-zoom-bar", children: [_jsx(IconButton, { icon: ZoomOutIcon, label: "Zoom out", disabled: scale <= 1.001, onClick: () => zoomOut() }), _jsxs("span", { className: "num min-w-10 text-center text-muted-foreground text-xs", "aria-live": "polite", children: [Math.round(scale * 100), "%"] }), _jsx(IconButton, { icon: ZoomInIcon, label: "Zoom in", disabled: scale >= MAX_SCALE - 0.001, onClick: () => zoomIn() }), _jsx(IconButton, { icon: RotateCcwIcon, label: "Reset zoom", onClick: () => resetTransform() }), _jsx("span", { className: "mx-0.5 h-4 w-px bg-border", "aria-hidden": true }), children] }));
|
|
21
|
+
}
|
|
22
|
+
/** The image at fit (scale 1), with zoom and pan; the wrapper takes the box its parent gives it. */
|
|
23
|
+
function Zoomable({ src, alt, children, }) {
|
|
24
|
+
return (_jsxs(TransformWrapper, { minScale: 1, maxScale: MAX_SCALE, centerZoomedOut: true, wheel: { step: 0.004 }, doubleClick: { mode: 'toggle', step: 1 }, children: [_jsx(ZoomBar, { children: children }), _jsx(TransformComponent, { wrapperClass: "!size-full cursor-grab active:cursor-grabbing", contentClass: "!size-full", children: _jsx("img", { src: src, alt: alt, draggable: false, className: "size-full object-contain" }) })] }));
|
|
25
|
+
}
|
|
26
|
+
export function ChatImage({ src, alt }) {
|
|
27
|
+
const [open, setOpen] = useState(false);
|
|
28
|
+
const [full, setFull] = useState(false);
|
|
29
|
+
const [ratio, setRatio] = useState(null);
|
|
30
|
+
if (!open) {
|
|
31
|
+
return (_jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { asChild: true, children: _jsxs("button", { type: "button", "aria-label": "View larger", onClick: () => setOpen(true), className: "group/thumb relative cursor-zoom-in overflow-hidden rounded-md border outline-none focus-visible:ring-2 focus-visible:ring-ring", children: [_jsx("img", { src: src, alt: alt, className: "max-h-32 max-w-40 object-cover", onLoad: (e) => {
|
|
32
|
+
const { naturalWidth: w, naturalHeight: h } = e.currentTarget;
|
|
33
|
+
if (w && h)
|
|
34
|
+
setRatio(w / h);
|
|
35
|
+
} }), _jsx("span", { className: "absolute top-1 right-1 flex size-6 items-center justify-center rounded-md bg-background/80 opacity-0 transition-opacity group-hover/thumb:opacity-100 group-focus-visible/thumb:opacity-100", children: _jsx(Maximize2Icon, { className: "size-3.5", "aria-hidden": true }) })] }) }), _jsx(TooltipContent, { children: "View larger" })] }));
|
|
36
|
+
}
|
|
37
|
+
return (_jsxs(_Fragment, { children: [_jsx("div", { className: "relative w-full overflow-hidden rounded-md border bg-background", style: { aspectRatio: ratio ?? 4 / 3, maxHeight: '60vh' }, "data-slot": "chat-image", children: _jsxs(Zoomable, { src: src, alt: alt, children: [_jsx(IconButton, { icon: Maximize2Icon, label: "Full screen", onClick: () => setFull(true) }), _jsx(IconButton, { icon: Minimize2Icon, label: "Shrink", onClick: () => {
|
|
38
|
+
setOpen(false);
|
|
39
|
+
setFull(false);
|
|
40
|
+
} })] }) }), _jsx(Dialog, { open: full, onOpenChange: setFull, children: _jsxs(DialogContent, { showCloseButton: false, "aria-describedby": undefined,
|
|
41
|
+
// Focus the box, not its first button: a focused button shows its tooltip, which would take
|
|
42
|
+
// the first Esc.
|
|
43
|
+
onOpenAutoFocus: (e) => {
|
|
44
|
+
e.preventDefault();
|
|
45
|
+
e.currentTarget.focus();
|
|
46
|
+
}, className: cn('h-[90vh] w-[90vw] max-w-none overflow-hidden p-0 sm:max-w-none', 'max-md:h-full max-md:w-full'), children: [_jsx(DialogTitle, { className: "sr-only", children: alt }), _jsx("div", { className: "relative size-full", children: _jsx(Zoomable, { src: src, alt: alt, children: _jsx(DialogClose, { asChild: true, children: _jsx(IconButton, { icon: XIcon, label: "Close" }) }) }) })] }) })] }));
|
|
47
|
+
}
|
|
@@ -16,6 +16,17 @@ export interface LumenAssistantProps {
|
|
|
16
16
|
onSettings?: () => void;
|
|
17
17
|
/** Days chats are kept, for the Chats menu's line. */
|
|
18
18
|
keepDays?: number;
|
|
19
|
+
/**
|
|
20
|
+
* Chats belong to a place in the app (a workspace, a project): the Chats menu lists the
|
|
21
|
+
* `current` one's chats (rows whose `scope` matches) and a switch named `label` shows them all,
|
|
22
|
+
* each with its place's `name`. The switch is kept on this device. A row with no `scope` shows
|
|
23
|
+
* only with the switch on.
|
|
24
|
+
*/
|
|
25
|
+
chatScope?: {
|
|
26
|
+
current: string;
|
|
27
|
+
label: string;
|
|
28
|
+
name?: (scope: string) => string | undefined;
|
|
29
|
+
};
|
|
19
30
|
/** Closes the panel (its ✕). */
|
|
20
31
|
onClose?: () => void;
|
|
21
32
|
/** An outside chat state (the dock shares one between the button and the panel). */
|
|
@@ -25,7 +36,7 @@ export interface LumenAssistantProps {
|
|
|
25
36
|
* kit's: /compact, /clear, /model and /escalate.
|
|
26
37
|
*/
|
|
27
38
|
commands?: AssistantCommand[];
|
|
28
|
-
/** What
|
|
39
|
+
/** What `@` lists everywhere; a page adds its own with `useAssistantSource`. */
|
|
29
40
|
sources?: readonly AssistantSource[];
|
|
30
41
|
/**
|
|
31
42
|
* Images can go with a message: pasted (Cmd+V / Ctrl+V) or dropped on the box, shown as chips
|
|
@@ -47,10 +58,10 @@ export interface AssistantCommand {
|
|
|
47
58
|
run: (args: string) => string | undefined | Promise<string | undefined>;
|
|
48
59
|
}
|
|
49
60
|
/** The assistant's chat: header, messages and composer. */
|
|
50
|
-
export declare function LumenAssistant({ client, context, onSettings, keepDays, onClose, chat: outside, commands, sources, images, }: LumenAssistantProps): import("react").JSX.Element;
|
|
61
|
+
export declare function LumenAssistant({ client, context, onSettings, keepDays, chatScope, onClose, chat: outside, commands, sources, images, }: LumenAssistantProps): import("react").JSX.Element;
|
|
51
62
|
/** An allowance in its unit: `$4.20`, or `350 credits`. */
|
|
52
63
|
export declare const allowanceAmount: (u: AllowanceUnit, n: number) => string;
|
|
53
|
-
/** What the composer sends with the text: the tier picked with
|
|
64
|
+
/** What the composer sends with the text: the tier picked with `#` and the things pointed at with `@`. */
|
|
54
65
|
export interface ComposerExtra {
|
|
55
66
|
tier?: string;
|
|
56
67
|
refs?: AssistantRefNote[];
|
|
@@ -58,26 +69,26 @@ export interface ComposerExtra {
|
|
|
58
69
|
}
|
|
59
70
|
/**
|
|
60
71
|
* The message box: Enter sends, Shift+Enter breaks a line; Stop while an answer runs. Three
|
|
61
|
-
* triggers list what can follow them as it is typed: `/` at the start a command, `@` a
|
|
62
|
-
*
|
|
72
|
+
* triggers list what can follow them as it is typed: `/` at the start a command, `@` a thing to
|
|
73
|
+
* point at (the `sources`), `#` a tier for this message; a picked tier or thing shows as a chip.
|
|
63
74
|
*/
|
|
64
75
|
export declare function LumenComposer({ name, busy, queued, disabled, commands, tiers, sources, page, images: canAttach, onSend, onStop, }: {
|
|
65
76
|
name: string;
|
|
66
77
|
/** Images can be pasted or dropped on the box (`LumenAssistant` `images`). */
|
|
67
78
|
images?: boolean;
|
|
68
|
-
/** The page's name for
|
|
79
|
+
/** The page's name for `@` This page; the document's title by default. */
|
|
69
80
|
page?: string;
|
|
70
81
|
busy: boolean;
|
|
71
82
|
queued: string | null;
|
|
72
83
|
disabled?: boolean;
|
|
73
84
|
commands?: AssistantCommand[];
|
|
74
|
-
/** What
|
|
85
|
+
/** What `#` lists. */
|
|
75
86
|
tiers?: {
|
|
76
87
|
id: string;
|
|
77
88
|
label: string;
|
|
78
89
|
model_label?: string;
|
|
79
90
|
}[];
|
|
80
|
-
/** What
|
|
91
|
+
/** What `@` lists, a group each. */
|
|
81
92
|
sources?: readonly AssistantSource[];
|
|
82
93
|
onSend: (text: string, extra: ComposerExtra) => void;
|
|
83
94
|
onStop: () => void;
|
|
@@ -26,6 +26,7 @@ import { Popover, PopoverContent, PopoverTrigger } from '../ui/popover.js';
|
|
|
26
26
|
import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetTrigger } from '../ui/sheet.js';
|
|
27
27
|
import { Spinner } from '../ui/spinner.js';
|
|
28
28
|
import { Textarea } from '../ui/textarea.js';
|
|
29
|
+
import { ChatImage } from './chat-image.js';
|
|
29
30
|
import { chatMarkdown, chatTurns, copyMarkdown, turnMarkdown } from './copy.js';
|
|
30
31
|
import { composeAssistant, selectionRef, useComposeRequest, usePageSelection, useShownAssistantSources, } from './sources.js';
|
|
31
32
|
import { UsageHistoryView, tokens, usd } from './usage-history.js';
|
|
@@ -48,11 +49,12 @@ export function useAssistantStatus(client) {
|
|
|
48
49
|
export { usd };
|
|
49
50
|
const seconds = (ms) => (ms < 1000 ? `${ms} ms` : `${(ms / 1000).toFixed(1)} s`);
|
|
50
51
|
/** The assistant's chat: header, messages and composer. */
|
|
51
|
-
export function LumenAssistant({ client, context, onSettings, keepDays = 30, onClose, chat: outside, commands = [], sources = [], images = false, }) {
|
|
52
|
+
export function LumenAssistant({ client, context, onSettings, keepDays = 30, chatScope, onClose, chat: outside, commands = [], sources = [], images = false, }) {
|
|
52
53
|
const own = useAssistantChat(client);
|
|
53
54
|
const chat = outside ?? own;
|
|
54
55
|
const qc = useQueryClient();
|
|
55
56
|
const status = useAssistantStatus(client);
|
|
57
|
+
const [allScopes, setAllScopes] = useAllScopes(client.key);
|
|
56
58
|
const chats = useQuery({
|
|
57
59
|
queryKey: ['lumen-assistant', client.key, 'chats'],
|
|
58
60
|
queryFn: () => client.chats(),
|
|
@@ -181,11 +183,14 @@ export function LumenAssistant({ client, context, onSettings, keepDays = 30, onC
|
|
|
181
183
|
allCommands.push(c);
|
|
182
184
|
const shown = useShownAssistantSources();
|
|
183
185
|
const allSources = [...shown, ...sources.filter((x) => !shown.some((y) => y.id === x.id))];
|
|
184
|
-
return (_jsxs("div", { "data-assistant": "", className: "flex h-full min-h-0 flex-col", children: [_jsxs("div", { className: "flex h-12 shrink-0 items-center gap-1 border-b px-3", children: [_jsx(SparklesIcon, { className: "size-4 text-edge" }), _jsx("span", { className: "min-w-0 flex-1 truncate font-medium", children: chat.title || s?.name || 'Assistant' }), s && s.tiers.length > 1 && (_jsx(LumenSelector, { trigger: "chip", size: "sm", label: "Tier", value: s.tier, options: s.tiers.map((t) => ({ value: t.id, label: t.label, detail: t.model_label })), onChange: (t) => void setTier(t) })), _jsx(LumenAssistantChats, { chats: chats.data
|
|
186
|
+
return (_jsxs("div", { "data-assistant": "", className: "flex h-full min-h-0 flex-col", children: [_jsxs("div", { className: "flex h-12 shrink-0 items-center gap-1 border-b px-3", children: [_jsx(SparklesIcon, { className: "size-4 text-edge" }), _jsx("span", { className: "min-w-0 flex-1 truncate font-medium", children: chat.title || s?.name || 'Assistant' }), s && s.tiers.length > 1 && (_jsx(LumenSelector, { trigger: "chip", size: "sm", label: "Tier", value: s.tier, options: s.tiers.map((t) => ({ value: t.id, label: t.label, detail: t.model_label })), onChange: (t) => void setTier(t) })), _jsx(LumenAssistantChats, { chats: chats.data
|
|
187
|
+
?.filter((c) => !chatScope || allScopes || c.scope === chatScope.current)
|
|
188
|
+
.map((c) => ({
|
|
185
189
|
id: c.id,
|
|
186
190
|
title: c.title,
|
|
187
191
|
updated_at: new Date(c.at).toISOString(),
|
|
188
|
-
|
|
192
|
+
...(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(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 })] }));
|
|
189
194
|
}
|
|
190
195
|
function Messages({ chat, status, onSettings, }) {
|
|
191
196
|
const end = useRef(null);
|
|
@@ -228,7 +233,7 @@ function allowanceWait(a) {
|
|
|
228
233
|
}
|
|
229
234
|
/** The assistant's name, for a copied turn's headings. */
|
|
230
235
|
const AssistantName = createContext('Assistant');
|
|
231
|
-
/** A tier's name for a message's
|
|
236
|
+
/** A tier's name for a message's `#` chip. */
|
|
232
237
|
const TierNames = createContext((id) => id);
|
|
233
238
|
/** Takes back a change made at once (Undo on its line). */
|
|
234
239
|
const UndoWrite = createContext(null);
|
|
@@ -259,7 +264,7 @@ function Item({ item, onSettle, busy, }) {
|
|
|
259
264
|
const tierName = useContext(TierNames);
|
|
260
265
|
switch (item.kind) {
|
|
261
266
|
case 'user':
|
|
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:
|
|
267
|
+
return (_jsxs("div", { className: "ml-8 self-end rounded-lg bg-muted px-3 py-2 text-sm whitespace-pre-wrap has-[[data-slot=chat-image]]:ml-0 has-[[data-slot=chat-image]]:w-full", children: [(item.tier || item.refs?.length) && (_jsxs("span", { className: "mb-1 flex flex-wrap gap-1", children: [item.tier && _jsx(ComposerChip, { sign: TIER, label: tierName(item.tier) }), item.refs?.map((r, i) => (_jsx(ComposerChip, { sign: POINT, 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(ChatImage, { src: src, alt: `Pasted ${i + 1}` }, `${i}:${src.length}`))) })) : null, item.text, item.context && (_jsxs("span", { className: "mt-1 block text-muted-foreground text-xs", children: ["on ", item.context] }))] }));
|
|
263
268
|
case 'text':
|
|
264
269
|
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" }))] }));
|
|
265
270
|
case 'step':
|
|
@@ -422,7 +427,10 @@ const WHY = {
|
|
|
422
427
|
function HandOff({ item, onSettle, busy, }) {
|
|
423
428
|
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.usage && (_jsx("span", { className: "ml-auto text-muted-foreground tabular-nums", children: usd(item.usage.usd) }))] }), _jsx("p", { className: "text-muted-foreground text-xs", children: item.task }), _jsx(Items, { items: item.items, onSettle: onSettle, busy: busy })] }));
|
|
424
429
|
}
|
|
425
|
-
/**
|
|
430
|
+
/** The composer's triggers: `@` points at a thing (the `sources`), `#` picks the tier for one message. */
|
|
431
|
+
const POINT = '@';
|
|
432
|
+
const TIER = '#';
|
|
433
|
+
/** A chip over the message box: a tier picked with `#` or a thing pointed at with `@`. */
|
|
426
434
|
function ComposerChip({ sign, label, onRemove, }) {
|
|
427
435
|
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" }) }))] }));
|
|
428
436
|
}
|
|
@@ -459,8 +467,8 @@ async function readImage(file) {
|
|
|
459
467
|
const imageUrl = (i) => `data:${i.media_type};base64,${i.data}`;
|
|
460
468
|
/**
|
|
461
469
|
* The message box: Enter sends, Shift+Enter breaks a line; Stop while an answer runs. Three
|
|
462
|
-
* triggers list what can follow them as it is typed: `/` at the start a command, `@` a
|
|
463
|
-
*
|
|
470
|
+
* triggers list what can follow them as it is typed: `/` at the start a command, `@` a thing to
|
|
471
|
+
* point at (the `sources`), `#` a tier for this message; a picked tier or thing shows as a chip.
|
|
464
472
|
*/
|
|
465
473
|
export function LumenComposer({ name, busy, queued, disabled, commands = [], tiers = [], sources = [], page, images: canAttach = false, onSend, onStop, }) {
|
|
466
474
|
const [text, setText] = useState('');
|
|
@@ -491,13 +499,13 @@ export function LumenComposer({ name, busy, queued, disabled, commands = [], tie
|
|
|
491
499
|
}, [request]);
|
|
492
500
|
const m = /^\/(\S*)(?:\s(.*))?$/s.exec(text);
|
|
493
501
|
const cmd = m && m[2] !== undefined ? commands.find((c) => c.name === m[1]) : undefined;
|
|
494
|
-
// `@` or `#` as the word the caret is in: `pick
|
|
502
|
+
// `@` or `#` as the word the caret is in: `pick @sto|` lists the sources' items matching "sto".
|
|
495
503
|
const word = /(^|\s)([@#])([^\s@#]*)$/.exec(text.slice(0, caret));
|
|
496
504
|
const trigger = word ? word[2] : null;
|
|
497
505
|
const asked = word?.[3] ?? '';
|
|
498
506
|
const open = closed !== `${text}|${caret}`;
|
|
499
507
|
const slash = m && commands.length ? (m[2] === undefined ? 'command' : cmd?.options ? 'arg' : null) : null;
|
|
500
|
-
const at = trigger ===
|
|
508
|
+
const at = trigger === TIER && tiers.length > 1 ? 'tier' : trigger === POINT ? 'point' : null;
|
|
501
509
|
const picking = open ? (slash ?? at) : null;
|
|
502
510
|
const extra = () => ({
|
|
503
511
|
...(tier ? { tier } : {}),
|
|
@@ -585,12 +593,12 @@ export function LumenComposer({ name, busy, queued, disabled, commands = [], tie
|
|
|
585
593
|
box.current?.setSelectionRange(start, start);
|
|
586
594
|
});
|
|
587
595
|
};
|
|
588
|
-
// Each
|
|
596
|
+
// Each `@` row makes its chip when picked, so what it carries (a card's words) is read then.
|
|
589
597
|
const sourceItems = new Map();
|
|
590
598
|
const picked = selection ? selectionRef(selection) : null;
|
|
591
599
|
const added = (r) => refs.some((x) => sameRef(x, r)) ? { disabled: 'Added' } : {};
|
|
592
600
|
const onPage = [];
|
|
593
|
-
if (picking === '
|
|
601
|
+
if (picking === 'point') {
|
|
594
602
|
const pageRef = () => ({
|
|
595
603
|
kind: 'page',
|
|
596
604
|
label: page || document.title || 'This page',
|
|
@@ -649,7 +657,7 @@ export function LumenComposer({ name, busy, queued, disabled, commands = [], tie
|
|
|
649
657
|
}
|
|
650
658
|
if (picked)
|
|
651
659
|
sourceItems.set('selection', () => picked);
|
|
652
|
-
const sourceOptions = picking === '
|
|
660
|
+
const sourceOptions = picking === 'point'
|
|
653
661
|
? [
|
|
654
662
|
...(picked
|
|
655
663
|
? [
|
|
@@ -698,20 +706,20 @@ export function LumenComposer({ name, busy, queued, disabled, commands = [], tie
|
|
|
698
706
|
requestAnimationFrame(() => box.current?.focus());
|
|
699
707
|
};
|
|
700
708
|
const tierLabel = tiers.find((t) => t.id === tier)?.label;
|
|
701
|
-
const hints = [commands.length && '/', tiers.length > 1 &&
|
|
709
|
+
const hints = [commands.length && '/', POINT, tiers.length > 1 && TIER].filter(Boolean);
|
|
702
710
|
return (_jsxs("div", { className: "shrink-0 border-t p-3", children: [queued && (_jsxs("p", { className: "mb-2 truncate text-muted-foreground text-xs", children: ["Queued: ", _jsx("span", { className: "text-foreground", children: queued })] })), picking === 'command' && (_jsx(LumenSelectorList, { handle: list, label: "Commands", className: "mb-2", fuzzy: true, query: m?.[1] ?? '', options: commands.map((c) => ({
|
|
703
711
|
value: c.name,
|
|
704
712
|
label: `/${c.name}`,
|
|
705
713
|
...(c.args ? { detail: c.args } : {}),
|
|
706
714
|
description: c.about,
|
|
707
|
-
})), empty: "No command matches", onPick: pickCommand, onClose: () => setClosed(`${text}|${caret}`) })), picking === 'arg' && cmd?.options && (_jsx(LumenSelectorList, { handle: list, label: `/${cmd.name}`, className: "mb-2", fuzzy: true, query: m?.[2] ?? '', options: cmd.options(), onPick: (o) => void run(cmd, o.value), onClose: () => setClosed(`${text}|${caret}`) })), picking === '
|
|
715
|
+
})), empty: "No command matches", onPick: pickCommand, onClose: () => setClosed(`${text}|${caret}`) })), picking === 'arg' && cmd?.options && (_jsx(LumenSelectorList, { handle: list, label: `/${cmd.name}`, className: "mb-2", fuzzy: true, query: m?.[2] ?? '', options: cmd.options(), onPick: (o) => void run(cmd, o.value), onClose: () => setClosed(`${text}|${caret}`) })), picking === 'tier' && (_jsx(LumenSelectorList, { handle: list, label: "Answer this message on", className: "mb-2", fuzzy: true, query: asked, options: tiers.map((t) => ({
|
|
708
716
|
value: t.id,
|
|
709
717
|
label: t.label,
|
|
710
718
|
...(t.model_label ? { detail: t.model_label } : {}),
|
|
711
719
|
})), empty: "No tier matches", onPick: (o) => {
|
|
712
720
|
setTier(o.value);
|
|
713
721
|
dropWord();
|
|
714
|
-
}, onClose: () => setClosed(`${text}|${caret}`) })), picking === '
|
|
722
|
+
}, onClose: () => setClosed(`${text}|${caret}`) })), picking === 'point' && (_jsx(LumenSelectorList, { handle: list, label: "Point at", className: "mb-2", query: asked, options: sourceOptions, empty: "Nothing matches", onPick: (o) => {
|
|
715
723
|
dropWord();
|
|
716
724
|
if (o.value === 'point') {
|
|
717
725
|
void point();
|
|
@@ -720,7 +728,7 @@ export function LumenComposer({ name, busy, queued, disabled, commands = [], tie
|
|
|
720
728
|
const r = sourceItems.get(o.value)?.();
|
|
721
729
|
if (r)
|
|
722
730
|
setRefs((p) => [...p, r]);
|
|
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:
|
|
731
|
+
}, 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: TIER, label: tierLabel, onRemove: () => setTier(null) })), refs.map((r, i) => (_jsx(ComposerChip, { sign: POINT, 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) => {
|
|
724
732
|
setText(e.target.value);
|
|
725
733
|
setCaret(e.target.selectionStart);
|
|
726
734
|
}, onSelect: (e) => setCaret(e.currentTarget.selectionStart), onPaste: (e) => {
|
|
@@ -843,3 +851,26 @@ export function useLumenAssistantDock(client, o = {
|
|
|
843
851
|
panel: (_jsx(LumenAssistantPanel, { name: name, open: open, onOpenChange: setOpen, children: () => (_jsx(LumenAssistant, { client: client, chat: chat, onClose: () => setOpen(false), ...props })) })),
|
|
844
852
|
};
|
|
845
853
|
}
|
|
854
|
+
/** The Chats menu's All switch (`chatScope`), kept on this device per assistant. */
|
|
855
|
+
function useAllScopes(key) {
|
|
856
|
+
const name = `lumen-assistant:${key}:all-scopes`;
|
|
857
|
+
const [on, setOn] = useState(() => {
|
|
858
|
+
try {
|
|
859
|
+
return localStorage.getItem(name) === '1';
|
|
860
|
+
}
|
|
861
|
+
catch {
|
|
862
|
+
return false;
|
|
863
|
+
}
|
|
864
|
+
});
|
|
865
|
+
const set = (v) => {
|
|
866
|
+
setOn(v);
|
|
867
|
+
try {
|
|
868
|
+
if (v)
|
|
869
|
+
localStorage.setItem(name, '1');
|
|
870
|
+
else
|
|
871
|
+
localStorage.removeItem(name);
|
|
872
|
+
}
|
|
873
|
+
catch { }
|
|
874
|
+
};
|
|
875
|
+
return [on, set];
|
|
876
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { AssistantRefNote } from '@columenlabs/lumen-ui-types/assistant';
|
|
2
2
|
import { type ComponentType } from 'react';
|
|
3
|
-
/** One thing a source holds: a row in the
|
|
3
|
+
/** One thing a source holds: a row in the `@` list, sent with the message as `ref`. */
|
|
4
4
|
export interface AssistantSourceItem {
|
|
5
5
|
/** Unique within its source. */
|
|
6
6
|
id: string;
|
|
@@ -14,7 +14,7 @@ export interface AssistantSourceItem {
|
|
|
14
14
|
/** What goes to the model: its kind, its label and whatever finds it (an id, a path). */
|
|
15
15
|
ref: AssistantRefNote;
|
|
16
16
|
}
|
|
17
|
-
/** A group of things in the
|
|
17
|
+
/** A group of things in the `@` list. */
|
|
18
18
|
export interface AssistantSource {
|
|
19
19
|
/** Unique: a page's source replaces an app source of the same id. */
|
|
20
20
|
id: string;
|
|
@@ -23,10 +23,10 @@ export interface AssistantSource {
|
|
|
23
23
|
icon?: ComponentType<{
|
|
24
24
|
className?: string;
|
|
25
25
|
}>;
|
|
26
|
-
/** What it holds now; `query` is what follows the
|
|
26
|
+
/** What it holds now; `query` is what follows the `@`, for a source that searches. */
|
|
27
27
|
items: (query: string) => readonly AssistantSourceItem[];
|
|
28
28
|
}
|
|
29
|
-
/** Adds a source to the
|
|
29
|
+
/** Adds a source to the `@` list while the calling page is shown. */
|
|
30
30
|
export declare function useAssistantSource(source: AssistantSource | null | undefined): void;
|
|
31
31
|
/** The sources pages have added, newest page last. */
|
|
32
32
|
export declare function useShownAssistantSources(): readonly AssistantSource[];
|
|
@@ -50,5 +50,5 @@ export interface PageSelection {
|
|
|
50
50
|
}
|
|
51
51
|
/** What the person last highlighted on the page outside the assistant, or null. */
|
|
52
52
|
export declare function usePageSelection(): PageSelection | null;
|
|
53
|
-
/** A highlight as
|
|
53
|
+
/** A highlight as an `@` chip: its first words, the whole text and where it sits go to the model. */
|
|
54
54
|
export declare const selectionRef: (s: PageSelection) => AssistantRefNote;
|
|
@@ -11,7 +11,7 @@ const subscribe = (l) => {
|
|
|
11
11
|
listeners.add(l);
|
|
12
12
|
return () => listeners.delete(l);
|
|
13
13
|
};
|
|
14
|
-
/** Adds a source to the
|
|
14
|
+
/** Adds a source to the `@` list while the calling page is shown. */
|
|
15
15
|
export function useAssistantSource(source) {
|
|
16
16
|
const latest = useRef(source);
|
|
17
17
|
latest.current = source;
|
|
@@ -105,7 +105,7 @@ export function usePageSelection() {
|
|
|
105
105
|
};
|
|
106
106
|
}, () => selected, () => null);
|
|
107
107
|
}
|
|
108
|
-
/** A highlight as
|
|
108
|
+
/** A highlight as an `@` chip: its first words, the whole text and where it sits go to the model. */
|
|
109
109
|
export const selectionRef = (s) => ({
|
|
110
110
|
kind: 'selection',
|
|
111
111
|
label: `“${s.text.length > 40 ? `${s.text.slice(0, 40)}…` : s.text}”`,
|
|
@@ -35,17 +35,25 @@ export interface AssistantChat {
|
|
|
35
35
|
title: string;
|
|
36
36
|
/** ISO time of its last message. */
|
|
37
37
|
updated_at: string;
|
|
38
|
+
/** Where it was started, shown before its time (`LumenAssistant` `chatScope`, its switch on). */
|
|
39
|
+
where?: string;
|
|
38
40
|
}
|
|
39
41
|
/**
|
|
40
42
|
* Chats, a dropdown from the panel header: chats grouped by when they were
|
|
41
43
|
* last used, searched by title. A row is read ahead when highlighted, so picking it opens at once.
|
|
42
44
|
*/
|
|
43
|
-
export declare function LumenAssistantChats<C extends AssistantChat>({ chats, value, note, disabled, onOpen, onRefresh, onHighlight, }: {
|
|
45
|
+
export declare function LumenAssistantChats<C extends AssistantChat>({ chats, value, note, toggle, disabled, onOpen, onRefresh, onHighlight, }: {
|
|
44
46
|
/** null while loading. */
|
|
45
47
|
chats: C[] | null;
|
|
46
48
|
value?: string;
|
|
47
49
|
/** A line above the list: how long chats are kept. */
|
|
48
50
|
note?: ReactNode;
|
|
51
|
+
/** A switch at the top of the menu, such as All workspaces (`LumenAssistant` `chatScope`). */
|
|
52
|
+
toggle?: {
|
|
53
|
+
label: string;
|
|
54
|
+
on: boolean;
|
|
55
|
+
onChange: (on: boolean) => void;
|
|
56
|
+
};
|
|
49
57
|
disabled?: boolean;
|
|
50
58
|
onOpen: (chat: C) => void;
|
|
51
59
|
/** The menu opened: read the list again. */
|
package/dist/assistant-panel.js
CHANGED
|
@@ -6,7 +6,7 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
|
|
|
6
6
|
// Open or closed and the width are each browser tab's own (sessionStorage). The
|
|
7
7
|
// product fills the body (the product's assistant: its chat, composer, cards and spend).
|
|
8
8
|
import { HistoryIcon, SparklesIcon } from 'lucide-react';
|
|
9
|
-
import { useEffect, useRef, useState } from 'react';
|
|
9
|
+
import { useEffect, useId, useRef, useState } from 'react';
|
|
10
10
|
import { useIsMobile } from './hooks/use-mobile.js';
|
|
11
11
|
import { cn } from './lib/utils.js';
|
|
12
12
|
import { LumenBoundary } from './lumen-boundary.js';
|
|
@@ -15,7 +15,9 @@ import { ASSISTANT_WIDTH, usePrefs } from './prefs.js';
|
|
|
15
15
|
import { ResizeEdge, tabState } from './side-dock.js';
|
|
16
16
|
import { Button } from './ui/button.js';
|
|
17
17
|
import { Kbd } from './ui/kbd.js';
|
|
18
|
+
import { Label } from './ui/label.js';
|
|
18
19
|
import { Sheet, SheetContent, SheetDescription, SheetTitle } from './ui/sheet.js';
|
|
20
|
+
import { Switch } from './ui/switch.js';
|
|
19
21
|
import { Tooltip, TooltipContent, TooltipTrigger } from './ui/tooltip.js';
|
|
20
22
|
export { tabState };
|
|
21
23
|
/** Whether the assistant is open in this tab. */
|
|
@@ -138,8 +140,9 @@ function whenOf(at, now, group, timeZone) {
|
|
|
138
140
|
* Chats, a dropdown from the panel header: chats grouped by when they were
|
|
139
141
|
* last used, searched by title. A row is read ahead when highlighted, so picking it opens at once.
|
|
140
142
|
*/
|
|
141
|
-
export function LumenAssistantChats({ chats, value, note, disabled, onOpen, onRefresh, onHighlight, }) {
|
|
143
|
+
export function LumenAssistantChats({ chats, value, note, toggle, disabled, onOpen, onRefresh, onHighlight, }) {
|
|
142
144
|
const { timeZone } = usePrefs();
|
|
145
|
+
const toggleId = useId();
|
|
143
146
|
const now = new Date();
|
|
144
147
|
const byId = new Map((chats ?? []).map((c) => [c.id, c]));
|
|
145
148
|
const groups = GROUPS.map((group) => ({
|
|
@@ -148,11 +151,17 @@ export function LumenAssistantChats({ chats, value, note, disabled, onOpen, onRe
|
|
|
148
151
|
const at = new Date(c.updated_at);
|
|
149
152
|
const g = groupOf(at, now, timeZone);
|
|
150
153
|
return g === group
|
|
151
|
-
? [
|
|
154
|
+
? [
|
|
155
|
+
{
|
|
156
|
+
value: c.id,
|
|
157
|
+
label: c.title,
|
|
158
|
+
detail: [c.where, whenOf(at, now, g, timeZone)].filter(Boolean).join(' · '),
|
|
159
|
+
},
|
|
160
|
+
]
|
|
152
161
|
: [];
|
|
153
162
|
}),
|
|
154
163
|
})).filter((g) => g.options.length > 0);
|
|
155
|
-
return (_jsx(LumenSelector, { trigger: "icon", icon: HistoryIcon, label: "Chats", search: true, align: "end", groups: groups, empty: chats === null ? 'Loading…' : 'No chats yet', header: note && _jsx("p", { className: "px-2 pt-1 text-xs text-muted-foreground", children: note }), value: value, disabled: disabled, onOpenChange: (o) => o && onRefresh?.(), onHighlight: (id) => {
|
|
164
|
+
return (_jsx(LumenSelector, { trigger: "icon", icon: HistoryIcon, label: "Chats", search: true, align: "end", groups: groups, empty: chats === null ? 'Loading…' : 'No chats yet', header: (toggle || note) && (_jsxs(_Fragment, { children: [toggle && (_jsxs("div", { className: "flex items-center gap-2 px-2 pt-1.5", children: [_jsx(Label, { htmlFor: toggleId, className: "min-w-0 flex-1 truncate text-xs font-normal", children: toggle.label }), _jsx(Switch, { id: toggleId, checked: toggle.on, onCheckedChange: toggle.onChange, "aria-label": toggle.label })] })), note && _jsx("p", { className: "px-2 pt-1 text-xs text-muted-foreground", children: note })] })), value: value, disabled: disabled, onOpenChange: (o) => o && onRefresh?.(), onHighlight: (id) => {
|
|
156
165
|
const c = byId.get(id);
|
|
157
166
|
if (c)
|
|
158
167
|
onHighlight?.(c);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@columenlabs/lumen-ui",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.214",
|
|
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.214",
|
|
53
53
|
"@dnd-kit/core": "6.3.1",
|
|
54
54
|
"@dnd-kit/sortable": "10.0.0",
|
|
55
55
|
"@dnd-kit/utilities": "3.2.2",
|
|
@@ -75,6 +75,7 @@
|
|
|
75
75
|
"react-intersection-observer": "9.16.0",
|
|
76
76
|
"react-markdown": "10.1.0",
|
|
77
77
|
"react-resizable-panels": "^4.14.3",
|
|
78
|
+
"react-zoom-pan-pinch": "4.2.0",
|
|
78
79
|
"recharts": "^3.10.1",
|
|
79
80
|
"remark-gfm": "4.0.1",
|
|
80
81
|
"shiki": "4.5.0",
|