@columenlabs/lumen-ui 0.1.215 → 0.1.217
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assistant/client.d.ts +7 -0
- package/dist/assistant/client.js +1 -0
- package/dist/assistant/lumen-assistant.d.ts +14 -4
- package/dist/assistant/lumen-assistant.js +98 -12
- package/dist/assistant/use-assistant-chat.d.ts +24 -2
- package/dist/assistant/use-assistant-chat.js +106 -16
- package/package.json +2 -2
|
@@ -38,6 +38,13 @@ export interface AssistantAdapter {
|
|
|
38
38
|
usage?(month?: string): Promise<UsageHistory>;
|
|
39
39
|
/** One answer: `onEvent` hears each event as it streams; abort `signal` to stop it. */
|
|
40
40
|
send(msg: AssistantMessage, onEvent: (e: AssistantEvent) => void, signal?: AbortSignal): Promise<void>;
|
|
41
|
+
/**
|
|
42
|
+
* A message for the answer running in `chatId`: it joins at the answer's next step. `steered:
|
|
43
|
+
* false` (or no method) and the panel queues it for after the answer instead.
|
|
44
|
+
*/
|
|
45
|
+
steer?(chatId: string, message: string): Promise<{
|
|
46
|
+
steered: boolean;
|
|
47
|
+
}>;
|
|
41
48
|
settleDraft(chatId: string, draftId: string, action: 'save' | 'dismiss'): Promise<DraftItem>;
|
|
42
49
|
/** Takes back a change the assistant made at once. */
|
|
43
50
|
undoWrite(chatId: string, stepId: string): Promise<StepItem>;
|
package/dist/assistant/client.js
CHANGED
|
@@ -71,6 +71,7 @@ export function assistantClient(base) {
|
|
|
71
71
|
action,
|
|
72
72
|
...(amount !== undefined ? { amount } : {}),
|
|
73
73
|
}),
|
|
74
|
+
steer: (chatId, message) => send('POST', `/chats/${encodeURIComponent(chatId)}/steer`, { message }),
|
|
74
75
|
async send(msg, onEvent, signal) {
|
|
75
76
|
const url = `${root}/chat`;
|
|
76
77
|
const res = await fetch(url, {
|
|
@@ -3,7 +3,7 @@ import { type LumenOption } from '../lumen-selector.js';
|
|
|
3
3
|
import type { AssistantAdapter } from './client.js';
|
|
4
4
|
import { type AssistantSource } from './sources.js';
|
|
5
5
|
import { usd } from './usage-history.js';
|
|
6
|
-
import { type AssistantChatState } from './use-assistant-chat.js';
|
|
6
|
+
import { type AssistantChatState, type QueuedMessage } from './use-assistant-chat.js';
|
|
7
7
|
export declare const assistantStatusKey: (client: AssistantAdapter) => string[];
|
|
8
8
|
/** The person's assistant status, shared by the panel and Settings. */
|
|
9
9
|
export declare function useAssistantStatus(client: AssistantAdapter): import("@tanstack/react-query").UseQueryResult<AssistantStatus, Error>;
|
|
@@ -68,18 +68,28 @@ export interface ComposerExtra {
|
|
|
68
68
|
images?: AssistantImage[];
|
|
69
69
|
}
|
|
70
70
|
/**
|
|
71
|
-
* The message box: Enter sends, Shift+Enter breaks a line; Stop while an answer runs.
|
|
71
|
+
* The message box: Enter sends, Shift+Enter breaks a line; Stop while an answer runs. While it
|
|
72
|
+
* runs, words in the box go into it with Enter (Steer, where `canSteer`) or wait for it to end
|
|
73
|
+
* with Alt+Enter (Queue); the `queue` lists above the box, each with Steer now, Edit and Remove.
|
|
74
|
+
* Three
|
|
72
75
|
* triggers list what can follow them as it is typed: `/` at the start a command, `@` a thing to
|
|
73
76
|
* point at (the `sources`), `#` a tier for this message; a picked tier or thing shows as a chip.
|
|
74
77
|
*/
|
|
75
|
-
export declare function LumenComposer({ name, busy, queued, disabled, commands, tiers, sources, page, images: canAttach, onSend, onStop, }: {
|
|
78
|
+
export declare function LumenComposer({ name, busy, queued, queue, canSteer, onSteerQueued, onUnqueue, disabled, commands, tiers, sources, page, images: canAttach, onSend, onStop, }: {
|
|
76
79
|
name: string;
|
|
77
80
|
/** Images can be pasted or dropped on the box (`LumenAssistant` `images`). */
|
|
78
81
|
images?: boolean;
|
|
79
82
|
/** The page's name for `@` This page; the document's title by default. */
|
|
80
83
|
page?: string;
|
|
81
84
|
busy: boolean;
|
|
85
|
+
/** The first queued message; `queue` lists them all, with their actions. */
|
|
82
86
|
queued: string | null;
|
|
87
|
+
queue?: QueuedMessage[];
|
|
88
|
+
/** Enter steers a message into the running answer; Alt+Enter queues it. */
|
|
89
|
+
canSteer?: boolean;
|
|
90
|
+
onSteerQueued?: (id: string) => void;
|
|
91
|
+
/** Takes a queued message out; Edit puts its words back in the box. */
|
|
92
|
+
onUnqueue?: (id: string) => QueuedMessage | undefined;
|
|
83
93
|
disabled?: boolean;
|
|
84
94
|
commands?: AssistantCommand[];
|
|
85
95
|
/** What `#` lists. */
|
|
@@ -90,7 +100,7 @@ export declare function LumenComposer({ name, busy, queued, disabled, commands,
|
|
|
90
100
|
}[];
|
|
91
101
|
/** What `@` lists, a group each. */
|
|
92
102
|
sources?: readonly AssistantSource[];
|
|
93
|
-
onSend: (text: string, extra: ComposerExtra) => void;
|
|
103
|
+
onSend: (text: string, extra: ComposerExtra, how?: 'steer' | 'queue') => void;
|
|
94
104
|
onStop: () => void;
|
|
95
105
|
}): import("react").JSX.Element;
|
|
96
106
|
/**
|
|
@@ -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, } 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, LightbulbIcon, MonitorIcon, PencilIcon, PlusIcon, ReceiptIcon, SparklesIcon, SquareIcon, TriangleAlertIcon, Undo2Icon, WrenchIcon, XIcon, ZapIcon, } from 'lucide-react';
|
|
9
|
+
import { ArrowUpIcon, CheckIcon, ChevronRightIcon, CircleAlertIcon, CopyIcon, CornerDownRightIcon, CrosshairIcon, FileTextIcon, GlobeIcon, LightbulbIcon, ListPlusIcon, MonitorIcon, PencilIcon, PlusIcon, ReceiptIcon, SearchIcon, SparklesIcon, SquareIcon, TriangleAlertIcon, Undo2Icon, WrenchIcon, XIcon, ZapIcon, } 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';
|
|
@@ -30,7 +30,7 @@ import { ChatImage } from './chat-image.js';
|
|
|
30
30
|
import { chatMarkdown, chatTurns, copyMarkdown, turnMarkdown } from './copy.js';
|
|
31
31
|
import { composeAssistant, selectionRef, useComposeRequest, usePageSelection, useShownAssistantSources, } from './sources.js';
|
|
32
32
|
import { UsageHistoryView, tokens, usd } from './usage-history.js';
|
|
33
|
-
import { useAssistantChat } from './use-assistant-chat.js';
|
|
33
|
+
import { useAssistantChat, } from './use-assistant-chat.js';
|
|
34
34
|
// The Markdown renderer (tables included) loads with the first answer (lazy-part.tsx).
|
|
35
35
|
// A tab whose build a deploy replaced shows the answer as plain text instead of reloading mid chat.
|
|
36
36
|
const ChatMarkdown = lazyPart(() => import('./chat-markdown.js').then((m) => m.default), null, ({ children }) => _jsx("p", { className: "whitespace-pre-wrap", children: children }));
|
|
@@ -190,7 +190,7 @@ export function LumenAssistant({ client, context, onSettings, keepDays = 30, cha
|
|
|
190
190
|
title: c.title,
|
|
191
191
|
updated_at: new Date(c.at).toISOString(),
|
|
192
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 })] }));
|
|
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, 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 })] }));
|
|
194
194
|
}
|
|
195
195
|
function Messages({ chat, status, onSettings, }) {
|
|
196
196
|
const end = useRef(null);
|
|
@@ -242,7 +242,7 @@ function Items({ items, onSettle, busy, turns, }) {
|
|
|
242
242
|
if (turns)
|
|
243
243
|
return (_jsx("div", { className: "flex flex-col gap-3", children: chatTurns(items).map((t) => {
|
|
244
244
|
const open = busy && t.from + t.items.length === items.length;
|
|
245
|
-
return (_jsxs("div", { "data-slot": "assistant-turn", className: "group/turn flex flex-col gap-3", children: [_jsx(Items, { items: t.items, onSettle: onSettle, busy: busy }), !open && (_jsx("div", { className: "-mt-2 flex justify-end opacity-0 transition-opacity group-hover/turn:opacity-100 focus-within:opacity-100 [@media(hover:none)]:opacity-100", children: _jsx(IconButton, { icon: CopyIcon, label: "Copy this turn", className: "size-6 text-muted-foreground", onClick: () => copyMarkdown(turnMarkdown(t.items, name), 'Turn') }) }))] }, t.from));
|
|
245
|
+
return (_jsxs("div", { "data-slot": "assistant-turn", className: "group/turn flex flex-col gap-3", children: [_jsx(Items, { items: t.items, onSettle: onSettle, busy: busy }), !open && _jsx(WebSources, { items: t.items }), !open && (_jsx("div", { className: "-mt-2 flex justify-end opacity-0 transition-opacity group-hover/turn:opacity-100 focus-within:opacity-100 [@media(hover:none)]:opacity-100", children: _jsx(IconButton, { icon: CopyIcon, label: "Copy this turn", className: "size-6 text-muted-foreground", onClick: () => copyMarkdown(turnMarkdown(t.items, name), 'Turn') }) }))] }, t.from));
|
|
246
246
|
}) }));
|
|
247
247
|
// Cards drafted one after another sit in one stack, so many of them skim as lines.
|
|
248
248
|
const rows = [];
|
|
@@ -264,6 +264,8 @@ function Item({ item, onSettle, busy, }) {
|
|
|
264
264
|
const tierName = useContext(TierNames);
|
|
265
265
|
switch (item.kind) {
|
|
266
266
|
case 'user':
|
|
267
|
+
if (item.steered)
|
|
268
|
+
return (_jsxs("div", { className: "ml-8 flex flex-col items-end gap-0.5 self-end", children: [_jsxs("span", { className: "flex items-center gap-1 text-primary text-xs", children: [_jsx(CornerDownRightIcon, { className: "size-3" }), "Steered"] }), _jsx("div", { className: "rounded-lg border border-primary/40 bg-muted px-3 py-2 text-sm whitespace-pre-wrap break-words", children: item.text })] }));
|
|
267
269
|
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] }))] }));
|
|
268
270
|
case 'text':
|
|
269
271
|
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" }))] }));
|
|
@@ -324,7 +326,64 @@ function ScreenChip({ item }) {
|
|
|
324
326
|
return (_jsxs("div", { "data-slot": "assistant-screen", "data-status": status, className: "flex min-w-0 items-center gap-2 rounded-lg border bg-muted/30 px-3 py-1.5 text-muted-foreground text-xs", children: [status === 'running' ? (_jsx(Spinner, { className: "size-3.5" })) : status === 'done' ? (_jsx(CheckIcon, { className: "size-3.5 shrink-0 text-edge" })) : status === 'failed' ? (_jsx(TriangleAlertIcon, { className: "size-3.5 shrink-0 text-loss" })) : status === 'undone' ? (_jsx(Undo2Icon, { className: "size-3.5 shrink-0" })) : (_jsx(MonitorIcon, { className: "size-3.5 shrink-0" })), _jsxs("span", { className: "min-w-0 flex-1 truncate", title: `${item.label} · ${movesText(item.moves)}`, children: [_jsx("span", { className: cn(status === 'undone' ? 'line-through' : 'text-foreground'), children: item.label }), said && _jsxs("span", { children: [" \u00B7 ", said] })] }), status === 'done' && c?.canUndo(item.id) && (_jsx(IconButton, { icon: Undo2Icon, label: "Undo", size: "icon-xs", onClick: () => c.undo(item.id) })), status === 'waiting' && c && (_jsx(IconButton, { icon: MonitorIcon, label: "Go now", variant: "outline", size: "icon-xs", onClick: () => c.go(item) }))] }));
|
|
325
327
|
}
|
|
326
328
|
const Rule = ({ children }) => (_jsx("p", { className: "flex items-center gap-2 text-muted-foreground text-xs before:h-px before:flex-1 before:bg-border after:h-px after:flex-1 after:bg-border", children: children }));
|
|
329
|
+
/** A page's host without `www.`, or the address when it has none. */
|
|
330
|
+
function hostOf(url) {
|
|
331
|
+
try {
|
|
332
|
+
return new URL(url).hostname.replace(/^www\./, '');
|
|
333
|
+
}
|
|
334
|
+
catch {
|
|
335
|
+
return url;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
/** A page's path after its host, for a read's line. */
|
|
339
|
+
function pathOf(url) {
|
|
340
|
+
try {
|
|
341
|
+
const u = new URL(url);
|
|
342
|
+
const p = decodeURIComponent(u.pathname).replace(/\/$/, '');
|
|
343
|
+
return p.split('/').pop() || '';
|
|
344
|
+
}
|
|
345
|
+
catch {
|
|
346
|
+
return '';
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
/** A web step: a search, its results folded under it, or one page read. */
|
|
350
|
+
function WebStep({ step, live }) {
|
|
351
|
+
const web = step.web ?? {};
|
|
352
|
+
const results = web.results ?? [];
|
|
353
|
+
const icon = live ? (_jsx(Spinner, { className: "size-3" })) : !step.ok ? (_jsx(CircleAlertIcon, { className: "size-3.5 shrink-0" })) : web.read ? (_jsx(GlobeIcon, { className: "size-3.5 shrink-0" })) : (_jsx(SearchIcon, { className: "size-3.5 shrink-0" }));
|
|
354
|
+
const line = (_jsxs(_Fragment, { children: [icon, web.tried ? (_jsxs("span", { className: "truncate", title: web.tried.url, children: ["Could not open ", _jsx("span", { className: "text-foreground", children: hostOf(web.tried.url) })] })) : web.read ? (_jsxs("span", { className: "truncate", title: web.read.title ?? web.read.url, children: ["Read ", _jsx("span", { className: "text-foreground", children: hostOf(web.read.url) }), pathOf(web.read.url) && ` · ${pathOf(web.read.url)}`] })) : (_jsxs("span", { className: "truncate", title: web.query, children: ["Searched the web", web.query ? ': ' : '', web.query && _jsx("span", { className: "text-foreground", children: web.query })] })), !live && !web.read && !web.tried && step.ok && (_jsxs("span", { className: "ml-auto shrink-0 tabular-nums", children: [results.length, " result", results.length === 1 ? '' : 's'] })), step.error && _jsxs("span", { className: "truncate", children: ["\u00B7 ", step.error] })] }));
|
|
355
|
+
const cls = cn('flex min-w-0 items-center gap-1.5 text-xs', step.ok ? 'text-muted-foreground' : 'text-loss');
|
|
356
|
+
if (!results.length)
|
|
357
|
+
return _jsx("p", { className: cls, children: line });
|
|
358
|
+
return (_jsxs(Collapsible, { children: [_jsxs(CollapsibleTrigger, { className: cn(cls, 'group/web w-full text-left'), children: [line, _jsx(ChevronRightIcon, { className: "size-3.5 shrink-0 transition-transform group-data-[state=open]/web:rotate-90" })] }), _jsx(CollapsibleContent, { children: _jsx("ul", { className: "mt-1 ml-1.5 flex flex-col gap-0.5 border-l pl-3 text-muted-foreground text-xs", children: results.map((r) => (_jsx("li", { className: "min-w-0 truncate", children: _jsxs("a", { href: r.url, target: "_blank", rel: "noopener noreferrer", title: r.url, className: "hover:underline", children: [_jsx("span", { className: "text-foreground", children: r.title || hostOf(r.url) }), " \u00B7", ' ', hostOf(r.url)] }) }, r.url))) }) })] }));
|
|
359
|
+
}
|
|
360
|
+
/** The pages a turn read, once each, in the order read: the numbers its answer cites. */
|
|
361
|
+
function turnPages(items) {
|
|
362
|
+
const pages = [];
|
|
363
|
+
const walk = (list) => {
|
|
364
|
+
for (const i of list) {
|
|
365
|
+
if (i.kind === 'handoff')
|
|
366
|
+
walk(i.items);
|
|
367
|
+
else if (i.kind === 'step' && i.web?.read && !pages.some((p) => p.url === i.web?.read?.url))
|
|
368
|
+
pages.push(i.web.read);
|
|
369
|
+
}
|
|
370
|
+
};
|
|
371
|
+
walk(items);
|
|
372
|
+
return pages;
|
|
373
|
+
}
|
|
374
|
+
/** The pages a turn read, as numbered chips under its answer; each opens in a new tab. */
|
|
375
|
+
function WebSources({ items }) {
|
|
376
|
+
const pages = turnPages(items);
|
|
377
|
+
if (!pages.length)
|
|
378
|
+
return null;
|
|
379
|
+
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))) }));
|
|
380
|
+
}
|
|
327
381
|
function Step({ step, live }) {
|
|
382
|
+
if (step.web)
|
|
383
|
+
return _jsx(WebStep, { step: step, live: live });
|
|
384
|
+
return _jsx(ToolStep, { step: step, live: live });
|
|
385
|
+
}
|
|
386
|
+
function ToolStep({ step, live }) {
|
|
328
387
|
const undo = useContext(UndoWrite);
|
|
329
388
|
const [undoing, setUndoing] = useState(false);
|
|
330
389
|
const w = step.write;
|
|
@@ -466,11 +525,14 @@ async function readImage(file) {
|
|
|
466
525
|
}
|
|
467
526
|
const imageUrl = (i) => `data:${i.media_type};base64,${i.data}`;
|
|
468
527
|
/**
|
|
469
|
-
* The message box: Enter sends, Shift+Enter breaks a line; Stop while an answer runs.
|
|
528
|
+
* The message box: Enter sends, Shift+Enter breaks a line; Stop while an answer runs. While it
|
|
529
|
+
* runs, words in the box go into it with Enter (Steer, where `canSteer`) or wait for it to end
|
|
530
|
+
* with Alt+Enter (Queue); the `queue` lists above the box, each with Steer now, Edit and Remove.
|
|
531
|
+
* Three
|
|
470
532
|
* triggers list what can follow them as it is typed: `/` at the start a command, `@` a thing to
|
|
471
533
|
* point at (the `sources`), `#` a tier for this message; a picked tier or thing shows as a chip.
|
|
472
534
|
*/
|
|
473
|
-
export function LumenComposer({ name, busy, queued, disabled, commands = [], tiers = [], sources = [], page, images: canAttach = false, onSend, onStop, }) {
|
|
535
|
+
export function LumenComposer({ name, busy, queued, queue, canSteer = false, onSteerQueued, onUnqueue, disabled, commands = [], tiers = [], sources = [], page, images: canAttach = false, onSend, onStop, }) {
|
|
474
536
|
const [text, setText] = useState('');
|
|
475
537
|
const [caret, setCaret] = useState(0);
|
|
476
538
|
const [closed, setClosed] = useState(null);
|
|
@@ -557,7 +619,7 @@ export function LumenComposer({ name, busy, queued, disabled, commands = [], tie
|
|
|
557
619
|
lumenToast.error(errorText(e));
|
|
558
620
|
}
|
|
559
621
|
};
|
|
560
|
-
const send = () => {
|
|
622
|
+
const send = (how) => {
|
|
561
623
|
if (!text.trim() || disabled)
|
|
562
624
|
return;
|
|
563
625
|
const c = m ? commands.find((x) => x.name === m[1]) : undefined;
|
|
@@ -565,9 +627,25 @@ export function LumenComposer({ name, busy, queued, disabled, commands = [], tie
|
|
|
565
627
|
void run(c, m?.[2] ?? '');
|
|
566
628
|
return;
|
|
567
629
|
}
|
|
568
|
-
onSend(text, extra());
|
|
630
|
+
onSend(text, extra(), busy ? (how ?? (canSteer ? 'steer' : 'queue')) : undefined);
|
|
569
631
|
clear();
|
|
570
632
|
};
|
|
633
|
+
// Edit: a queued message back in the box, its chips with it.
|
|
634
|
+
const edit = (id) => {
|
|
635
|
+
const q = onUnqueue?.(id);
|
|
636
|
+
if (!q)
|
|
637
|
+
return;
|
|
638
|
+
setText(q.message);
|
|
639
|
+
setCaret(q.message.length);
|
|
640
|
+
if (q.extra?.tier)
|
|
641
|
+
setTier(q.extra.tier);
|
|
642
|
+
if (q.extra?.refs?.length)
|
|
643
|
+
setRefs(q.extra.refs);
|
|
644
|
+
if (q.extra?.images?.length)
|
|
645
|
+
setImages(q.extra.images);
|
|
646
|
+
requestAnimationFrame(() => box.current?.focus());
|
|
647
|
+
};
|
|
648
|
+
const waiting = queue ?? (queued ? [{ id: 'queued', message: queued }] : []);
|
|
571
649
|
const pickCommand = (o) => {
|
|
572
650
|
const c = commands.find((x) => x.name === o.value);
|
|
573
651
|
if (!c)
|
|
@@ -707,7 +785,7 @@ export function LumenComposer({ name, busy, queued, disabled, commands = [], tie
|
|
|
707
785
|
};
|
|
708
786
|
const tierLabel = tiers.find((t) => t.id === tier)?.label;
|
|
709
787
|
const hints = [commands.length && '/', POINT, tiers.length > 1 && TIER].filter(Boolean);
|
|
710
|
-
return (_jsxs("div", { className: "shrink-0 border-t p-3", children: [
|
|
788
|
+
return (_jsxs("div", { className: "shrink-0 border-t p-3", children: [waiting.length > 0 && (_jsx(ComposerQueue, { queue: waiting, canSteer: busy && canSteer && !!onSteerQueued, onSteer: (id) => onSteerQueued?.(id), onEdit: onUnqueue ? edit : undefined, onRemove: onUnqueue ? (id) => void onUnqueue(id) : undefined })), picking === 'command' && (_jsx(LumenSelectorList, { handle: list, label: "Commands", className: "mb-2", fuzzy: true, query: m?.[1] ?? '', options: commands.map((c) => ({
|
|
711
789
|
value: c.name,
|
|
712
790
|
label: `/${c.name}`,
|
|
713
791
|
...(c.args ? { detail: c.args } : {}),
|
|
@@ -728,7 +806,7 @@ export function LumenComposer({ name, busy, queued, disabled, commands = [], tie
|
|
|
728
806
|
const r = sourceItems.get(o.value)?.();
|
|
729
807
|
if (r)
|
|
730
808
|
setRefs((p) => [...p, r]);
|
|
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:
|
|
809
|
+
}, 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: cn('max-h-48 min-h-11 resize-none', busy && text.trim() && canSteer ? 'pr-18' : 'pr-11'), value: text, disabled: disabled, onChange: (e) => {
|
|
732
810
|
setText(e.target.value);
|
|
733
811
|
setCaret(e.target.selectionStart);
|
|
734
812
|
}, onSelect: (e) => setCaret(e.currentTarget.selectionStart), onPaste: (e) => {
|
|
@@ -746,9 +824,17 @@ export function LumenComposer({ name, busy, queued, disabled, commands = [], tie
|
|
|
746
824
|
return;
|
|
747
825
|
if (e.key === 'Enter' && !e.shiftKey && !e.nativeEvent.isComposing) {
|
|
748
826
|
e.preventDefault();
|
|
749
|
-
send();
|
|
827
|
+
send(e.altKey ? 'queue' : undefined);
|
|
750
828
|
}
|
|
751
|
-
} }), _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 })) : (_jsx(IconButton, { icon:
|
|
829
|
+
} }), _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() })) })] })] }));
|
|
830
|
+
}
|
|
831
|
+
/**
|
|
832
|
+
* What waits above the box while an answer runs: steering messages until the answer reads them,
|
|
833
|
+
* then queued ones in order, each with Steer now, Edit (back in the box) and Remove.
|
|
834
|
+
*/
|
|
835
|
+
function ComposerQueue({ queue, canSteer, onSteer, onEdit, onRemove, }) {
|
|
836
|
+
const waiting = queue.filter((q) => !q.steering).length;
|
|
837
|
+
return (_jsxs("div", { className: "mb-2 flex flex-col gap-1", children: [waiting > 0 && (_jsxs("p", { className: "flex items-center gap-1.5 text-muted-foreground text-xs", children: [_jsx(ListPlusIcon, { className: "size-3.5" }), "Queued \u00B7 ", waiting] })), queue.map((q) => (_jsxs("div", { className: "flex h-8 min-w-0 items-center gap-1 rounded-md border bg-background pr-1 pl-2 text-sm", children: [q.steering && (_jsx(CornerDownRightIcon, { "aria-label": "Steering: Sage reads it at its next step", className: "size-3.5 shrink-0 animate-pulse text-primary" })), _jsx("span", { className: cn('min-w-0 flex-1 truncate', q.steering && 'text-muted-foreground'), title: q.message, children: q.message }), !q.steering && canSteer && (_jsx(IconButton, { icon: CornerDownRightIcon, label: "Steer now", size: "icon-xs", onClick: () => onSteer(q.id) })), !q.steering && onEdit && (_jsx(IconButton, { icon: PencilIcon, label: "Edit", size: "icon-xs", onClick: () => onEdit(q.id) })), !q.steering && onRemove && (_jsx(IconButton, { icon: XIcon, label: "Remove", size: "icon-xs", onClick: () => onRemove(q.id) }))] }, q.id)))] }));
|
|
752
838
|
}
|
|
753
839
|
// The same thing pointed at twice: its kind and what finds it, whatever its words read then.
|
|
754
840
|
const refKey = (r) => r.kind === 'page'
|
|
@@ -4,6 +4,15 @@ import type { AssistantAdapter } from './client.js';
|
|
|
4
4
|
export type LiveItem = ChatItem & {
|
|
5
5
|
live?: boolean;
|
|
6
6
|
};
|
|
7
|
+
type Extra = Omit<AssistantMessage, 'message' | 'chat_id'>;
|
|
8
|
+
/** A message sent while an answer runs: waiting for it to end, or steered into it. */
|
|
9
|
+
export interface QueuedMessage {
|
|
10
|
+
id: string;
|
|
11
|
+
message: string;
|
|
12
|
+
extra?: Extra;
|
|
13
|
+
/** Sent into the running answer, not read by it yet. */
|
|
14
|
+
steering?: boolean;
|
|
15
|
+
}
|
|
7
16
|
export interface AssistantChatState {
|
|
8
17
|
chatId: string | null;
|
|
9
18
|
title: string;
|
|
@@ -11,11 +20,23 @@ export interface AssistantChatState {
|
|
|
11
20
|
usage: ChatUsage | null;
|
|
12
21
|
/** An answer is streaming. */
|
|
13
22
|
busy: boolean;
|
|
14
|
-
/**
|
|
23
|
+
/** The first message waiting for the answer to end (`queue` has them all). */
|
|
15
24
|
queued: string | null;
|
|
25
|
+
/** Messages sent while the answer runs, in order: steering ones first wait to be read. */
|
|
26
|
+
queue: QueuedMessage[];
|
|
27
|
+
/** The adapter takes a message into a running answer (Steer). */
|
|
28
|
+
canSteer: boolean;
|
|
16
29
|
/** The chat is loading. */
|
|
17
30
|
loading: boolean;
|
|
18
|
-
|
|
31
|
+
/**
|
|
32
|
+
* Sends now, or while an answer runs: `steer` (the default where the adapter can) into it at its
|
|
33
|
+
* next step, else queued for after it.
|
|
34
|
+
*/
|
|
35
|
+
send: (message: string, extra?: Extra, how?: 'steer' | 'queue') => void;
|
|
36
|
+
/** A queued message steered into the running answer now. */
|
|
37
|
+
steerQueued: (id: string) => void;
|
|
38
|
+
/** Takes a queued message out; its words and extras come back (Edit puts them in the box). */
|
|
39
|
+
unqueue: (id: string) => QueuedMessage | undefined;
|
|
19
40
|
stop: () => void;
|
|
20
41
|
open: (id: string) => Promise<void>;
|
|
21
42
|
reset: () => void;
|
|
@@ -28,3 +49,4 @@ export interface AssistantChatState {
|
|
|
28
49
|
/** A copy of the items with one event applied (hand-off sections copied too). */
|
|
29
50
|
export declare function applyEvent(prev: LiveItem[], e: AssistantEvent): LiveItem[];
|
|
30
51
|
export declare function useAssistantChat(client: AssistantAdapter): AssistantChatState;
|
|
52
|
+
export {};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
2
|
+
import { ApiError } from '../api-fetch.js';
|
|
2
3
|
import { errorText } from '../integrations.js';
|
|
3
4
|
/** The browser's IANA zone, sent with each message for the assistant's bearings. */
|
|
4
5
|
function browserZone() {
|
|
@@ -30,6 +31,8 @@ function keepOutcomes(m) {
|
|
|
30
31
|
}
|
|
31
32
|
catch { }
|
|
32
33
|
}
|
|
34
|
+
let queuedSeq = 0;
|
|
35
|
+
const queuedId = () => `q${++queuedSeq}`;
|
|
33
36
|
/** Where an event lands: the chat, or the last hand-off section when it came from inside one. */
|
|
34
37
|
function sinkOf(items, handoff) {
|
|
35
38
|
if (!handoff)
|
|
@@ -148,12 +151,34 @@ export function useAssistantChat(client) {
|
|
|
148
151
|
const [usage, setUsage] = useState(null);
|
|
149
152
|
const [busy, setBusy] = useState(false);
|
|
150
153
|
const [loading, setLoading] = useState(false);
|
|
151
|
-
const [
|
|
154
|
+
const [queue, setQueue] = useState([]);
|
|
155
|
+
// An adapter whose app has no steer route: Steer hides and messages queue.
|
|
156
|
+
const [steerOff, setSteerOff] = useState(false);
|
|
152
157
|
const abort = useRef(null);
|
|
153
158
|
const outcomes = useRef(null);
|
|
154
159
|
outcomes.current ??= readOutcomes();
|
|
155
160
|
const idRef = useRef(null);
|
|
156
161
|
idRef.current = chatId;
|
|
162
|
+
const queueRef = useRef([]);
|
|
163
|
+
queueRef.current = queue;
|
|
164
|
+
/** Steering messages not read (all, or those `texts`) go back to waiting, first in line. */
|
|
165
|
+
const unsteer = useCallback((texts) => {
|
|
166
|
+
setQueue((q) => {
|
|
167
|
+
const left = [...(texts ?? [])];
|
|
168
|
+
const back = q.map((x) => {
|
|
169
|
+
if (!x.steering)
|
|
170
|
+
return x;
|
|
171
|
+
if (texts) {
|
|
172
|
+
const at = left.indexOf(x.message);
|
|
173
|
+
if (at < 0)
|
|
174
|
+
return x;
|
|
175
|
+
left.splice(at, 1);
|
|
176
|
+
}
|
|
177
|
+
return { ...x, steering: false };
|
|
178
|
+
});
|
|
179
|
+
return back.some((x, i) => x !== q[i]) ? back : q;
|
|
180
|
+
});
|
|
181
|
+
}, []);
|
|
157
182
|
const run = useCallback(async (message, extra) => {
|
|
158
183
|
const ctl = new AbortController();
|
|
159
184
|
abort.current = ctl;
|
|
@@ -194,6 +219,19 @@ export function useAssistantChat(client) {
|
|
|
194
219
|
setUsage(e.data);
|
|
195
220
|
else if (e.event === 'done')
|
|
196
221
|
setUsage(e.data.usage);
|
|
222
|
+
else if (e.event === 'steered') {
|
|
223
|
+
// Read by the answer here: the line moves from the queue into the chat.
|
|
224
|
+
setQueue((q) => {
|
|
225
|
+
const at = q.findIndex((m) => m.steering && m.message === e.data.text);
|
|
226
|
+
return at < 0 ? q : [...q.slice(0, at), ...q.slice(at + 1)];
|
|
227
|
+
});
|
|
228
|
+
setItems((p) => [
|
|
229
|
+
...settled(p),
|
|
230
|
+
{ kind: 'user', text: e.data.text, at: e.data.at, steered: true },
|
|
231
|
+
]);
|
|
232
|
+
}
|
|
233
|
+
else if (e.event === 'unsteered')
|
|
234
|
+
unsteer(e.data.texts);
|
|
197
235
|
else
|
|
198
236
|
setItems((p) => applyEvent(p, e));
|
|
199
237
|
}, ctl.signal);
|
|
@@ -211,30 +249,78 @@ export function useAssistantChat(client) {
|
|
|
211
249
|
}
|
|
212
250
|
finally {
|
|
213
251
|
setItems(settled);
|
|
252
|
+
// Steers the answer never read (Stop, an error) wait for the next answer instead.
|
|
253
|
+
unsteer();
|
|
214
254
|
abort.current = null;
|
|
215
255
|
setBusy(false);
|
|
216
256
|
}
|
|
217
|
-
}, [client]);
|
|
218
|
-
// A queued message goes once the answer ends.
|
|
257
|
+
}, [client, unsteer]);
|
|
258
|
+
// A queued message goes once the answer ends, one an answer.
|
|
219
259
|
useEffect(() => {
|
|
220
|
-
if (busy
|
|
260
|
+
if (busy)
|
|
261
|
+
return;
|
|
262
|
+
const next = queue[0];
|
|
263
|
+
if (!next || next.steering)
|
|
221
264
|
return;
|
|
222
|
-
|
|
223
|
-
void run(
|
|
224
|
-
}, [busy,
|
|
225
|
-
const
|
|
265
|
+
setQueue((q) => q.slice(1));
|
|
266
|
+
void run(next.message, next.extra);
|
|
267
|
+
}, [busy, queue, run]);
|
|
268
|
+
const canSteer = !!client.steer && !steerOff;
|
|
269
|
+
// Into the running answer; refused (none runs, the model reads none) it waits in the queue.
|
|
270
|
+
const steerIn = useCallback(async (m) => {
|
|
271
|
+
const id = idRef.current;
|
|
272
|
+
let steered = false;
|
|
273
|
+
if (id && client.steer && abort.current)
|
|
274
|
+
try {
|
|
275
|
+
steered = (await client.steer(id, m.message)).steered;
|
|
276
|
+
}
|
|
277
|
+
catch (err) {
|
|
278
|
+
if (err instanceof ApiError && [404, 405, 410].includes(err.status))
|
|
279
|
+
setSteerOff(true);
|
|
280
|
+
}
|
|
281
|
+
if (!steered)
|
|
282
|
+
setQueue((q) => q.map((x) => (x.id === m.id ? { ...x, steering: false } : x)));
|
|
283
|
+
}, [client]);
|
|
284
|
+
const send = useCallback((message, extra, how) => {
|
|
226
285
|
const text = message.trim();
|
|
227
286
|
if (!text)
|
|
228
287
|
return;
|
|
229
|
-
if (abort.current)
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
288
|
+
if (!abort.current)
|
|
289
|
+
return void run(text, extra);
|
|
290
|
+
const steer = (how ?? 'steer') === 'steer' && canSteer && !!idRef.current;
|
|
291
|
+
const m = {
|
|
292
|
+
id: queuedId(),
|
|
293
|
+
message: text,
|
|
294
|
+
...(extra ? { extra } : {}),
|
|
295
|
+
...(steer ? { steering: true } : {}),
|
|
296
|
+
};
|
|
297
|
+
// Steering ones sit first: they are read before anything queued after the answer.
|
|
298
|
+
setQueue((q) => steer ? [...q.filter((x) => x.steering), m, ...q.filter((x) => !x.steering)] : [...q, m]);
|
|
299
|
+
if (steer)
|
|
300
|
+
void steerIn(m);
|
|
301
|
+
}, [run, canSteer, steerIn]);
|
|
302
|
+
const steerQueued = useCallback((id) => {
|
|
303
|
+
const m = queueRef.current.find((x) => x.id === id && !x.steering);
|
|
304
|
+
if (!m || !abort.current)
|
|
305
|
+
return;
|
|
306
|
+
const on = { ...m, steering: true };
|
|
307
|
+
setQueue((q) => [
|
|
308
|
+
...q.filter((x) => x.steering),
|
|
309
|
+
on,
|
|
310
|
+
...q.filter((x) => !x.steering && x.id !== id),
|
|
311
|
+
]);
|
|
312
|
+
void steerIn(on);
|
|
313
|
+
}, [steerIn]);
|
|
314
|
+
const unqueue = useCallback((id) => {
|
|
315
|
+
const m = queueRef.current.find((x) => x.id === id && !x.steering);
|
|
316
|
+
if (m)
|
|
317
|
+
setQueue((q) => q.filter((x) => x.id !== id));
|
|
318
|
+
return m;
|
|
319
|
+
}, []);
|
|
234
320
|
const stop = useCallback(() => abort.current?.abort(), []);
|
|
235
321
|
const reset = useCallback(() => {
|
|
236
322
|
abort.current?.abort();
|
|
237
|
-
|
|
323
|
+
setQueue([]);
|
|
238
324
|
setChatId(null);
|
|
239
325
|
idRef.current = null;
|
|
240
326
|
setTitle('');
|
|
@@ -243,7 +329,7 @@ export function useAssistantChat(client) {
|
|
|
243
329
|
}, []);
|
|
244
330
|
const open = useCallback(async (id) => {
|
|
245
331
|
abort.current?.abort();
|
|
246
|
-
|
|
332
|
+
setQueue([]);
|
|
247
333
|
setLoading(true);
|
|
248
334
|
try {
|
|
249
335
|
const c = await client.chat(id);
|
|
@@ -306,9 +392,13 @@ export function useAssistantChat(client) {
|
|
|
306
392
|
items,
|
|
307
393
|
usage,
|
|
308
394
|
busy,
|
|
309
|
-
queued:
|
|
395
|
+
queued: queue[0]?.message ?? null,
|
|
396
|
+
queue,
|
|
397
|
+
canSteer,
|
|
310
398
|
loading,
|
|
311
399
|
send,
|
|
400
|
+
steerQueued,
|
|
401
|
+
unqueue,
|
|
312
402
|
stop,
|
|
313
403
|
open,
|
|
314
404
|
reset,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@columenlabs/lumen-ui",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.217",
|
|
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.217",
|
|
53
53
|
"@dnd-kit/core": "6.3.1",
|
|
54
54
|
"@dnd-kit/sortable": "10.0.0",
|
|
55
55
|
"@dnd-kit/utilities": "3.2.2",
|