@columenlabs/lumen-ui 0.1.226 → 0.1.227
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 +7 -0
- package/dist/assistant/chat-image.js +11 -7
- package/dist/assistant/image-card.d.ts +11 -0
- package/dist/assistant/image-card.js +61 -0
- package/dist/assistant/index.d.ts +1 -1
- package/dist/assistant/lumen-assistant.d.ts +25 -2
- package/dist/assistant/lumen-assistant.js +34 -9
- package/dist/assistant/sources.d.ts +7 -1
- package/package.json +2 -2
|
@@ -2,3 +2,10 @@ export declare function ChatImage({ src, alt }: {
|
|
|
2
2
|
src: string;
|
|
3
3
|
alt: string;
|
|
4
4
|
}): import("react").JSX.Element;
|
|
5
|
+
/** An image over 90% of the screen with zoom and pan (the kit's Dialog: Esc, × or a click outside closes it). */
|
|
6
|
+
export declare function ImageLightbox({ src, alt, open, onOpenChange, }: {
|
|
7
|
+
src: string;
|
|
8
|
+
alt: string;
|
|
9
|
+
open: boolean;
|
|
10
|
+
onOpenChange: (open: boolean) => void;
|
|
11
|
+
}): import("react").JSX.Element;
|
|
@@ -40,11 +40,15 @@ export function ChatImage({ src, alt }) {
|
|
|
40
40
|
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: () => {
|
|
41
41
|
setOpen(false);
|
|
42
42
|
setFull(false);
|
|
43
|
-
} })] }) }), _jsx(
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
43
|
+
} })] }) }), _jsx(ImageLightbox, { src: src, alt: alt, open: full, onOpenChange: setFull })] }));
|
|
44
|
+
}
|
|
45
|
+
/** An image over 90% of the screen with zoom and pan (the kit's Dialog: Esc, × or a click outside closes it). */
|
|
46
|
+
export function ImageLightbox({ src, alt, open, onOpenChange, }) {
|
|
47
|
+
return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { showCloseButton: false, "aria-describedby": undefined,
|
|
48
|
+
// Focus the box, not its first button: a focused button shows its tooltip, which would take
|
|
49
|
+
// the first Esc.
|
|
50
|
+
onOpenAutoFocus: (e) => {
|
|
51
|
+
e.preventDefault();
|
|
52
|
+
e.currentTarget.focus();
|
|
53
|
+
}, 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, panAtFit: true, children: _jsx(DialogClose, { asChild: true, children: _jsx(IconButton, { icon: XIcon, label: "Close" }) }) }) })] }) }));
|
|
50
54
|
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { StepImage, StepItem } from '@columenlabs/lumen-ui-types/assistant';
|
|
2
|
+
/** Whether a tool's name as the model called it (`group__tool`) is `tool`. */
|
|
3
|
+
export declare const toolIs: (called: string, tool: string) => boolean;
|
|
4
|
+
export declare function ImageCard({ step, image, mode, }: {
|
|
5
|
+
step: StepItem;
|
|
6
|
+
image: StepImage;
|
|
7
|
+
/** The composer mode Make another turns on. */
|
|
8
|
+
mode?: string;
|
|
9
|
+
}): import("react").JSX.Element;
|
|
10
|
+
/** The image tool's line while it runs. */
|
|
11
|
+
export declare function ImagePending(): import("react").JSX.Element;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { DownloadIcon, ImageIcon, ImagePlusIcon, Maximize2Icon, RefreshCwIcon } from 'lucide-react';
|
|
3
|
+
import { useState } from 'react';
|
|
4
|
+
import { IconButton } from '../icon-button.js';
|
|
5
|
+
import { lumenToast } from '../lumen-toast.js';
|
|
6
|
+
import { ImageLightbox } from './chat-image.js';
|
|
7
|
+
import { composeAssistant } from './sources.js';
|
|
8
|
+
/** Whether a tool's name as the model called it (`group__tool`) is `tool`. */
|
|
9
|
+
export const toolIs = (called, tool) => called === tool || called.endsWith(`__${tool}`);
|
|
10
|
+
/** The image as the composer sends it, or null when it cannot be read. */
|
|
11
|
+
async function asReference(url) {
|
|
12
|
+
try {
|
|
13
|
+
const res = await fetch(url, {
|
|
14
|
+
credentials: 'same-origin',
|
|
15
|
+
signal: AbortSignal.timeout(15_000),
|
|
16
|
+
});
|
|
17
|
+
if (!res.ok)
|
|
18
|
+
return null;
|
|
19
|
+
const blob = await res.blob();
|
|
20
|
+
const data = await new Promise((ok, fail) => {
|
|
21
|
+
const f = new FileReader();
|
|
22
|
+
f.onload = () => ok(String(f.result));
|
|
23
|
+
f.onerror = () => fail(f.error);
|
|
24
|
+
f.readAsDataURL(blob);
|
|
25
|
+
});
|
|
26
|
+
const m = /^data:(image\/(?:png|jpeg|webp|gif));base64,(.*)$/.exec(data);
|
|
27
|
+
return m ? { media_type: m[1], data: m[2] } : null;
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
export function ImageCard({ step, image, mode, }) {
|
|
34
|
+
const [zoom, setZoom] = useState(false);
|
|
35
|
+
const [busy, setBusy] = useState(false);
|
|
36
|
+
const alt = image.alt || step.title;
|
|
37
|
+
const withImage = async (then) => {
|
|
38
|
+
setBusy(true);
|
|
39
|
+
const img = await asReference(image.url);
|
|
40
|
+
setBusy(false);
|
|
41
|
+
if (img)
|
|
42
|
+
then(img);
|
|
43
|
+
else
|
|
44
|
+
lumenToast.error("That image couldn't be read. Try again.");
|
|
45
|
+
};
|
|
46
|
+
return (_jsxs("div", { className: "flex w-full min-w-0 flex-col gap-1.5", "data-slot": "chat-made-image", children: [_jsxs("div", { className: "relative w-full max-w-sm overflow-hidden rounded-lg border bg-muted", children: [_jsx("img", { src: image.url, alt: alt, className: "block w-full" }), _jsxs("div", { className: "absolute top-2 right-2 flex gap-1 rounded-lg border bg-popover/90 p-0.5 text-popover-foreground shadow-sm backdrop-blur-sm", children: [_jsx(IconButton, { icon: Maximize2Icon, label: "Zoom", onClick: () => setZoom(true) }), _jsx(IconButton, { icon: ImagePlusIcon, label: "Use as reference", disabled: busy, onClick: () => void withImage((img) => composeAssistant({ images: [img] })) }), _jsx(IconButton, { icon: DownloadIcon, label: "Download", onClick: () => {
|
|
47
|
+
const a = document.createElement('a');
|
|
48
|
+
a.href = image.url;
|
|
49
|
+
a.download = 'image.png';
|
|
50
|
+
a.click();
|
|
51
|
+
} }), _jsx(IconButton, { icon: RefreshCwIcon, label: "Make another", disabled: busy, onClick: () => void withImage((img) => composeAssistant({
|
|
52
|
+
text: 'Make another version of this image.',
|
|
53
|
+
images: [img],
|
|
54
|
+
...(mode ? { use: mode } : {}),
|
|
55
|
+
send: true,
|
|
56
|
+
})) })] })] }), image.cost && _jsxs("span", { className: "text-muted-foreground text-xs", children: ["Image \u00B7 ", image.cost] }), _jsx(ImageLightbox, { src: image.url, alt: alt, open: zoom, onOpenChange: setZoom })] }));
|
|
57
|
+
}
|
|
58
|
+
/** The image tool's line while it runs. */
|
|
59
|
+
export function ImagePending() {
|
|
60
|
+
return (_jsx("div", { className: "flex w-full min-w-0 flex-col gap-1.5", children: _jsxs("output", { className: "flex aspect-[400/260] w-full max-w-sm animate-pulse flex-col items-center justify-center gap-2 rounded-lg border border-dashed bg-muted/50 text-muted-foreground", children: [_jsx(ImageIcon, { className: "size-7", "aria-hidden": true }), _jsx("span", { className: "text-foreground text-sm", children: "Making the image" }), _jsx("span", { className: "text-xs", children: "Usually 10 to 20 s" })] }) }));
|
|
61
|
+
}
|
|
@@ -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 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, assistantStatusKey, readAssistantImage, useAssistantStatus, useLumenAssistantDock, } from './lumen-assistant.js';
|
|
4
4
|
export { assistantAdminSections, assistantSettingsSections } from './settings.js';
|
|
5
5
|
export { 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';
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type AllowanceUnit, type AssistantImage, type AssistantMessage, type AssistantQuote, type AssistantRefNote, type AssistantStatus } from '@columenlabs/lumen-ui-types/assistant';
|
|
2
|
+
import { type LucideIcon } from 'lucide-react';
|
|
2
3
|
import { type LumenOption } from '../lumen-selector.js';
|
|
3
4
|
import type { AssistantAdapter } from './client.js';
|
|
4
5
|
import { type AssistantSource } from './sources.js';
|
|
@@ -43,6 +44,25 @@ export interface LumenAssistantProps {
|
|
|
43
44
|
* until it is sent. Turn it on when the app's chat route passes `images` on to the agent.
|
|
44
45
|
*/
|
|
45
46
|
images?: boolean;
|
|
47
|
+
/**
|
|
48
|
+
* Toggles beside the paperclip, one per mode ("Make an image"): one turned on goes with the next
|
|
49
|
+
* message as its `use`, and the app's plan reads it (call the tool that makes an image).
|
|
50
|
+
*/
|
|
51
|
+
modes?: AssistantMode[];
|
|
52
|
+
/**
|
|
53
|
+
* The app's tool that makes images: while it runs its line is a "Making the image" box, and its
|
|
54
|
+
* card's Make another sends again with `mode` on.
|
|
55
|
+
*/
|
|
56
|
+
imageTool?: {
|
|
57
|
+
tool: string;
|
|
58
|
+
mode?: string;
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
/** A composer mode: its toggle's icon and label (also its tooltip and aria-label). */
|
|
62
|
+
export interface AssistantMode {
|
|
63
|
+
id: string;
|
|
64
|
+
icon: LucideIcon;
|
|
65
|
+
label: string;
|
|
46
66
|
}
|
|
47
67
|
/** A slash command: typing `/` in the composer lists them. */
|
|
48
68
|
export interface AssistantCommand {
|
|
@@ -58,7 +78,7 @@ export interface AssistantCommand {
|
|
|
58
78
|
run: (args: string) => string | undefined | Promise<string | undefined>;
|
|
59
79
|
}
|
|
60
80
|
/** The assistant's chat: header, messages and composer. */
|
|
61
|
-
export declare function LumenAssistant({ client, context, onSettings, keepDays, chatScope, onClose, chat: outside, commands, sources, images, }: LumenAssistantProps): import("react").JSX.Element;
|
|
81
|
+
export declare function LumenAssistant({ client, context, onSettings, keepDays, chatScope, onClose, chat: outside, commands, sources, images, modes, imageTool, }: LumenAssistantProps): import("react").JSX.Element;
|
|
62
82
|
/** An allowance in its unit: `$4.20`, or `350 credits`. */
|
|
63
83
|
export declare const allowanceAmount: (u: AllowanceUnit, n: number) => string;
|
|
64
84
|
/** What the composer sends with the text: the tier picked with `#` and the things pointed at with `@`. */
|
|
@@ -67,6 +87,7 @@ export interface ComposerExtra {
|
|
|
67
87
|
refs?: AssistantRefNote[];
|
|
68
88
|
images?: AssistantImage[];
|
|
69
89
|
quote?: AssistantQuote;
|
|
90
|
+
use?: string;
|
|
70
91
|
}
|
|
71
92
|
/**
|
|
72
93
|
* A pasted, dropped or picked image, ready to send: scaled to `IMAGE_EDGE` on a canvas and saved as JPEG
|
|
@@ -81,7 +102,9 @@ export declare function readAssistantImage(file: File): Promise<AssistantImage |
|
|
|
81
102
|
* triggers list what can follow them as it is typed: `/` at the start a command, `@` a thing to
|
|
82
103
|
* point at (the `sources`), `#` a tier for this message; a picked tier or thing shows as a chip.
|
|
83
104
|
*/
|
|
84
|
-
export declare function LumenComposer({ name, busy, queued, queue, canSteer, onSteerQueued, onUnqueue, disabled, commands, tiers, sources, page, images: canAttach, onSend, onStop, }: {
|
|
105
|
+
export declare function LumenComposer({ name, busy, queued, queue, canSteer, onSteerQueued, onUnqueue, disabled, commands, tiers, sources, page, images: canAttach, modes, onSend, onStop, }: {
|
|
106
|
+
/** Toggles beside the paperclip (`LumenAssistant` `modes`). */
|
|
107
|
+
modes?: AssistantMode[];
|
|
85
108
|
name: string;
|
|
86
109
|
/** Images can be pasted or dropped on the box (`LumenAssistant` `images`). */
|
|
87
110
|
images?: boolean;
|
|
@@ -28,6 +28,7 @@ import { Spinner } from '../ui/spinner.js';
|
|
|
28
28
|
import { Textarea } from '../ui/textarea.js';
|
|
29
29
|
import { ChatImage } from './chat-image.js';
|
|
30
30
|
import { chatMarkdown, chatTurns, copyMarkdown, turnMarkdown } from './copy.js';
|
|
31
|
+
import { ImageCard, ImagePending, toolIs } from './image-card.js';
|
|
31
32
|
import { ReplyToSelection } from './reply-selection.js';
|
|
32
33
|
import { composeAssistant, selectionRef, useComposeRequest, usePageSelection, useShownAssistantSources, } from './sources.js';
|
|
33
34
|
import { UsageHistoryView, tokens, usd } from './usage-history.js';
|
|
@@ -50,7 +51,7 @@ export function useAssistantStatus(client) {
|
|
|
50
51
|
export { usd };
|
|
51
52
|
const seconds = (ms) => (ms < 1000 ? `${ms} ms` : `${(ms / 1000).toFixed(1)} s`);
|
|
52
53
|
/** The assistant's chat: header, messages and composer. */
|
|
53
|
-
export function LumenAssistant({ client, context, onSettings, keepDays = 30, chatScope, onClose, chat: outside, commands = [], sources = [], images = false, }) {
|
|
54
|
+
export function LumenAssistant({ client, context, onSettings, keepDays = 30, chatScope, onClose, chat: outside, commands = [], sources = [], images = false, modes = [], imageTool, }) {
|
|
54
55
|
const own = useAssistantChat(client);
|
|
55
56
|
const chat = outside ?? own;
|
|
56
57
|
const qc = useQueryClient();
|
|
@@ -191,7 +192,7 @@ export function LumenAssistant({ client, context, onSettings, keepDays = 30, cha
|
|
|
191
192
|
title: c.title,
|
|
192
193
|
updated_at: new Date(c.at).toISOString(),
|
|
193
194
|
...(chatScope && allScopes && c.scope ? { where: chatScope.name?.(c.scope) } : {}),
|
|
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(Messages, { chat: chat, status: s, onSettings: onSettings, onRetry: (at, user) => chat.retry(at, user, 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, onSend: (text, more, how) => chat.send(text, { ...extra(), ...more }, how), onStop: chat.stop }), _jsx(UsageLine, { usage: chat.usage, client: client, spent: s?.spend.usd })] }));
|
|
195
|
+
})) ?? (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()) }) }) }) }), _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 }), _jsx(UsageLine, { usage: chat.usage, client: client, spent: s?.spend.usd })] }));
|
|
195
196
|
}
|
|
196
197
|
function Messages({ chat, status, onSettings, onRetry, }) {
|
|
197
198
|
const end = useRef(null);
|
|
@@ -238,6 +239,8 @@ const AssistantName = createContext('Assistant');
|
|
|
238
239
|
/** A tier's name for a message's `#` chip. */
|
|
239
240
|
const TierNames = createContext((id) => id);
|
|
240
241
|
/** Takes back a change made at once (Undo on its line). */
|
|
242
|
+
/** The app's image tool (`LumenAssistant` `imageTool`). */
|
|
243
|
+
const ImageTool = createContext(null);
|
|
241
244
|
const UndoWrite = createContext(null);
|
|
242
245
|
/** Sends a failed turn again; null while it can't (an answer runs, nobody can pay). */
|
|
243
246
|
const RetryTurn = createContext(null);
|
|
@@ -400,6 +403,11 @@ function WebSources({ items }) {
|
|
|
400
403
|
return (_jsx("ol", { "aria-label": "Sources", className: "flex min-w-0 flex-wrap gap-1.5", children: pages.map((p, n) => (_jsx("li", { className: "min-w-0 max-w-full", children: _jsxs("a", { href: p.url, target: "_blank", rel: "noopener noreferrer", title: p.title ? `${p.title} · ${p.url}` : p.url, className: "flex h-6 min-w-0 items-center gap-1.5 rounded-full border pr-2.5 pl-1 text-xs hover:bg-muted", children: [_jsx("span", { className: "grid size-4 shrink-0 place-items-center rounded-full bg-primary/15 font-medium text-primary text-xs tabular-nums", children: n + 1 }), _jsx("span", { className: "truncate", children: hostOf(p.url) })] }) }, p.url))) }));
|
|
401
404
|
}
|
|
402
405
|
function Step({ step, live }) {
|
|
406
|
+
const images = useContext(ImageTool);
|
|
407
|
+
if (step.image)
|
|
408
|
+
return _jsx(ImageCard, { step: step, image: step.image, mode: images?.mode });
|
|
409
|
+
if (live && images && toolIs(step.tool, images.tool))
|
|
410
|
+
return _jsx(ImagePending, {});
|
|
403
411
|
if (step.web)
|
|
404
412
|
return _jsx(WebStep, { step: step, live: live });
|
|
405
413
|
return _jsx(ToolStep, { step: step, live: live });
|
|
@@ -560,7 +568,7 @@ const imageUrl = (i) => `data:${i.media_type};base64,${i.data}`;
|
|
|
560
568
|
* triggers list what can follow them as it is typed: `/` at the start a command, `@` a thing to
|
|
561
569
|
* point at (the `sources`), `#` a tier for this message; a picked tier or thing shows as a chip.
|
|
562
570
|
*/
|
|
563
|
-
export function LumenComposer({ name, busy, queued, queue, canSteer = false, onSteerQueued, onUnqueue, disabled, commands = [], tiers = [], sources = [], page, images: canAttach = false, onSend, onStop, }) {
|
|
571
|
+
export function LumenComposer({ name, busy, queued, queue, canSteer = false, onSteerQueued, onUnqueue, disabled, commands = [], tiers = [], sources = [], page, images: canAttach = false, modes = [], onSend, onStop, }) {
|
|
564
572
|
const [text, setText] = useState('');
|
|
565
573
|
const [caret, setCaret] = useState(0);
|
|
566
574
|
const [closed, setClosed] = useState(null);
|
|
@@ -568,6 +576,7 @@ export function LumenComposer({ name, busy, queued, queue, canSteer = false, onS
|
|
|
568
576
|
const [refs, setRefs] = useState([]);
|
|
569
577
|
const [images, setImages] = useState([]);
|
|
570
578
|
const [quote, setQuote] = useState(null);
|
|
579
|
+
const [use, setUse] = useState(null);
|
|
571
580
|
const list = useRef(null);
|
|
572
581
|
const box = useRef(null);
|
|
573
582
|
const filePick = useRef(null);
|
|
@@ -579,6 +588,18 @@ export function LumenComposer({ name, busy, queued, queue, canSteer = false, onS
|
|
|
579
588
|
if (!request)
|
|
580
589
|
return;
|
|
581
590
|
take();
|
|
591
|
+
// Make another: sent at once, the box left as it is.
|
|
592
|
+
if (request.send && request.text) {
|
|
593
|
+
onSend(request.text, {
|
|
594
|
+
...(request.images?.length ? { images: request.images } : {}),
|
|
595
|
+
...(request.use ? { use: request.use } : {}),
|
|
596
|
+
});
|
|
597
|
+
return;
|
|
598
|
+
}
|
|
599
|
+
if (request.images?.length)
|
|
600
|
+
setImages((p) => [...p, ...(request.images ?? [])].slice(0, ASSISTANT_IMAGES_MAX));
|
|
601
|
+
if (request.use)
|
|
602
|
+
setUse(request.use);
|
|
582
603
|
if (request.refs?.length)
|
|
583
604
|
setRefs((p) => [...p, ...(request.refs ?? [])]);
|
|
584
605
|
if (request.tier)
|
|
@@ -606,6 +627,7 @@ export function LumenComposer({ name, busy, queued, queue, canSteer = false, onS
|
|
|
606
627
|
...(refs.length ? { refs } : {}),
|
|
607
628
|
...(images.length ? { images } : {}),
|
|
608
629
|
...(quote ? { quote } : {}),
|
|
630
|
+
...(use ? { use } : {}),
|
|
609
631
|
});
|
|
610
632
|
const clear = () => {
|
|
611
633
|
setText('');
|
|
@@ -614,6 +636,7 @@ export function LumenComposer({ name, busy, queued, queue, canSteer = false, onS
|
|
|
614
636
|
setRefs([]);
|
|
615
637
|
setImages([]);
|
|
616
638
|
setQuote(null);
|
|
639
|
+
setUse(null);
|
|
617
640
|
};
|
|
618
641
|
/** Adds the image files among `files`; says so when one is left out. Returns whether any was an image. */
|
|
619
642
|
const attach = (files) => {
|
|
@@ -679,6 +702,8 @@ export function LumenComposer({ name, busy, queued, queue, canSteer = false, onS
|
|
|
679
702
|
setImages(q.extra.images);
|
|
680
703
|
if (q.extra?.quote)
|
|
681
704
|
setQuote(q.extra.quote);
|
|
705
|
+
if (q.extra?.use)
|
|
706
|
+
setUse(q.extra.use);
|
|
682
707
|
requestAnimationFrame(() => box.current?.focus());
|
|
683
708
|
};
|
|
684
709
|
const waiting = queue ?? (queued ? [{ id: 'queued', message: queued }] : []);
|
|
@@ -842,7 +867,7 @@ export function LumenComposer({ name, busy, queued, queue, canSteer = false, onS
|
|
|
842
867
|
const r = sourceItems.get(o.value)?.();
|
|
843
868
|
if (r)
|
|
844
869
|
setRefs((p) => [...p, r]);
|
|
845
|
-
}, onClose: () => setClosed(`${text}|${caret}`) })), quote && _jsx(QuoteLine, { quote: quote, onRemove: () => setQuote(null) }), 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: cn('max-h-48 min-h-11 resize-none', busy && text.trim() && canSteer ? 'pr-18' : 'pr-11', canAttach && 'pb-9'), value: text, disabled: disabled, onChange: (e) => {
|
|
870
|
+
}, onClose: () => setClosed(`${text}|${caret}`) })), quote && _jsx(QuoteLine, { quote: quote, onRemove: () => setQuote(null) }), 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: cn('max-h-48 min-h-11 resize-none', busy && text.trim() && canSteer ? 'pr-18' : 'pr-11', (canAttach || modes.length > 0) && 'pb-9'), value: text, disabled: disabled, onChange: (e) => {
|
|
846
871
|
setText(e.target.value);
|
|
847
872
|
setCaret(e.target.selectionStart);
|
|
848
873
|
}, onSelect: (e) => setCaret(e.currentTarget.selectionStart), onPaste: (e) => {
|
|
@@ -862,11 +887,11 @@ export function LumenComposer({ name, busy, queued, queue, canSteer = false, onS
|
|
|
862
887
|
e.preventDefault();
|
|
863
888
|
send(e.altKey ? 'queue' : undefined);
|
|
864
889
|
}
|
|
865
|
-
} }), canAttach && (_jsxs("div", { className: "absolute bottom-1.5 left-1.5", children: [_jsx(IconButton, { icon: PaperclipIcon, label: "Attach images", size: "icon-xs", disabled: disabled, onClick: () => filePick.current?.click() }), _jsx("input", { ref: filePick, type: "file", accept: "image/*", multiple: true, hidden: true, onChange: (e) => {
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
890
|
+
} }), (canAttach || modes.length > 0) && (_jsxs("div", { className: "absolute bottom-1.5 left-1.5 flex items-center gap-0.5", children: [canAttach && (_jsxs(_Fragment, { children: [_jsx(IconButton, { icon: PaperclipIcon, label: "Attach images", size: "icon-xs", disabled: disabled, onClick: () => filePick.current?.click() }), _jsx("input", { ref: filePick, type: "file", accept: "image/*", multiple: true, hidden: true, onChange: (e) => {
|
|
891
|
+
if (e.target.files)
|
|
892
|
+
attach(e.target.files);
|
|
893
|
+
e.target.value = '';
|
|
894
|
+
} })] })), 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: "absolute right-1.5 bottom-1.5", 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() })) })] })] }));
|
|
870
895
|
}
|
|
871
896
|
/**
|
|
872
897
|
* What waits above the box while an answer runs: steering messages until the answer reads them,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AssistantQuote, AssistantRefNote } from '@columenlabs/lumen-ui-types/assistant';
|
|
1
|
+
import type { AssistantImage, AssistantQuote, AssistantRefNote } from '@columenlabs/lumen-ui-types/assistant';
|
|
2
2
|
import { type ComponentType } from 'react';
|
|
3
3
|
/** One thing a source holds: a row in the `@` list, sent with the message as `ref`. */
|
|
4
4
|
export interface AssistantSourceItem {
|
|
@@ -37,6 +37,12 @@ export interface ComposeRequest {
|
|
|
37
37
|
tier?: string;
|
|
38
38
|
/** A piece of the chat to reply to: it shows above the box with an ×. */
|
|
39
39
|
quote?: AssistantQuote;
|
|
40
|
+
/** Images to send with the message (an image card's Use as reference). */
|
|
41
|
+
images?: AssistantImage[];
|
|
42
|
+
/** A composer mode to turn on (`LumenAssistant` `modes`), by id. */
|
|
43
|
+
use?: string;
|
|
44
|
+
/** Send at once with `text`, leaving the box as it is (an image card's Make another). */
|
|
45
|
+
send?: boolean;
|
|
40
46
|
}
|
|
41
47
|
/**
|
|
42
48
|
* Opens the assistant with `refs` as chips in its composer and `text` typed, focused for the person
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@columenlabs/lumen-ui",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.227",
|
|
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.227",
|
|
53
53
|
"@dnd-kit/core": "6.3.1",
|
|
54
54
|
"@dnd-kit/sortable": "10.0.0",
|
|
55
55
|
"@dnd-kit/utilities": "3.2.2",
|