@columenlabs/lumen-ui 0.1.217 → 0.1.219
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.
|
@@ -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, GlobeIcon, LightbulbIcon, ListPlusIcon, MonitorIcon, PencilIcon, PlusIcon, ReceiptIcon, SearchIcon, 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, RefreshCwIcon, 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';
|
|
@@ -190,9 +190,9 @@ 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, 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 })] }));
|
|
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, 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 })] }));
|
|
194
194
|
}
|
|
195
|
-
function Messages({ chat, status, onSettings, }) {
|
|
195
|
+
function Messages({ chat, status, onSettings, onRetry, }) {
|
|
196
196
|
const end = useRef(null);
|
|
197
197
|
const count = JSON.stringify(chat.items).length;
|
|
198
198
|
// biome-ignore lint/correctness/useExhaustiveDependencies: scroll to the end as the chat grows
|
|
@@ -210,7 +210,7 @@ function Messages({ chat, status, onSettings, }) {
|
|
|
210
210
|
status.allowance &&
|
|
211
211
|
status.allowance.request?.status !== 'open'
|
|
212
212
|
? 'Request more'
|
|
213
|
-
: 'Open Settings' }))] })] })), chat.loading ? (_jsxs("p", { className: "flex items-center gap-2 text-muted-foreground text-sm", children: [_jsx(Spinner, {}), " Loading the chat\u2026"] })) : chat.items.length === 0 && !status?.wait ? (_jsxs("p", { className: "pt-8 text-center text-muted-foreground text-sm", children: ["Ask ", status?.name ?? 'the assistant', " anything."] })) : (_jsx(TierNames.Provider, { value: (id) => status?.tiers.find((t) => t.id === id)?.label ?? id, children: _jsx(UndoWrite.Provider, { value: chat.undo, children: _jsx(Items, { items: chat.items, onSettle: chat.settle, busy: chat.busy, turns: true }) }) })), chat.busy && !chat.items.some(isLive) && (_jsxs("p", { className: "flex items-center gap-2 py-1 text-muted-foreground text-sm", children: [_jsx(Spinner, {}), " Thinking\u2026"] })), _jsx("div", { ref: end })] }));
|
|
213
|
+
: 'Open Settings' }))] })] })), chat.loading ? (_jsxs("p", { className: "flex items-center gap-2 text-muted-foreground text-sm", children: [_jsx(Spinner, {}), " Loading the chat\u2026"] })) : chat.items.length === 0 && !status?.wait ? (_jsxs("p", { className: "pt-8 text-center text-muted-foreground text-sm", children: ["Ask ", status?.name ?? 'the assistant', " anything."] })) : (_jsx(TierNames.Provider, { value: (id) => status?.tiers.find((t) => t.id === id)?.label ?? id, children: _jsx(UndoWrite.Provider, { value: chat.undo, children: _jsx(RetryTurn.Provider, { value: chat.busy || status?.wait ? null : onRetry, children: _jsx(Items, { items: chat.items, onSettle: chat.settle, busy: chat.busy, turns: true }) }) }) })), chat.busy && !chat.items.some(isLive) && (_jsxs("p", { className: "flex items-center gap-2 py-1 text-muted-foreground text-sm", children: [_jsx(Spinner, {}), " Thinking\u2026"] })), _jsx("div", { ref: end })] }));
|
|
214
214
|
}
|
|
215
215
|
const isLive = (i) => !!i.live || (i.kind === 'handoff' && i.items.some(isLive));
|
|
216
216
|
const WAIT = {
|
|
@@ -237,12 +237,27 @@ const AssistantName = createContext('Assistant');
|
|
|
237
237
|
const TierNames = createContext((id) => id);
|
|
238
238
|
/** Takes back a change made at once (Undo on its line). */
|
|
239
239
|
const UndoWrite = createContext(null);
|
|
240
|
+
/** Sends a failed turn again; null while it can't (an answer runs, nobody can pay). */
|
|
241
|
+
const RetryTurn = createContext(null);
|
|
242
|
+
/** Where a turn's unretried error sits in the chat, when the turn starts with a message. */
|
|
243
|
+
function failedAt(t) {
|
|
244
|
+
if (t.items[0]?.kind !== 'user')
|
|
245
|
+
return null;
|
|
246
|
+
for (let i = t.items.length - 1; i > 0; i--) {
|
|
247
|
+
const it = t.items[i];
|
|
248
|
+
if (it?.kind === 'error')
|
|
249
|
+
return it.retried ? null : t.from + i;
|
|
250
|
+
}
|
|
251
|
+
return null;
|
|
252
|
+
}
|
|
240
253
|
function Items({ items, onSettle, busy, turns, }) {
|
|
241
254
|
const name = useContext(AssistantName);
|
|
255
|
+
const retry = useContext(RetryTurn);
|
|
242
256
|
if (turns)
|
|
243
257
|
return (_jsx("div", { className: "flex flex-col gap-3", children: chatTurns(items).map((t) => {
|
|
244
258
|
const open = busy && t.from + t.items.length === items.length;
|
|
245
|
-
|
|
259
|
+
const failed = retry && !open ? failedAt(t) : null;
|
|
260
|
+
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 && (_jsxs("div", { className: "-mt-2 flex justify-end gap-0.5", children: [retry && failed !== null && (_jsx(IconButton, { icon: RefreshCwIcon, label: "Retry", className: "size-6", onClick: () => retry(failed, t.items[0]) })), _jsx("div", { className: "flex 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
261
|
}) }));
|
|
247
262
|
// Cards drafted one after another sit in one stack, so many of them skim as lines.
|
|
248
263
|
const rows = [];
|
|
@@ -263,10 +278,14 @@ function Items({ items, onSettle, busy, turns, }) {
|
|
|
263
278
|
function Item({ item, onSettle, busy, }) {
|
|
264
279
|
const tierName = useContext(TierNames);
|
|
265
280
|
switch (item.kind) {
|
|
266
|
-
case 'user':
|
|
281
|
+
case 'user': {
|
|
267
282
|
if (item.steered)
|
|
268
283
|
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 })] }));
|
|
269
|
-
|
|
284
|
+
const bubble = (_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] }))] }));
|
|
285
|
+
if (!item.retried)
|
|
286
|
+
return bubble;
|
|
287
|
+
return (_jsxs("div", { className: "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(RefreshCwIcon, { className: "size-3" }), "Retry"] }), bubble] }));
|
|
288
|
+
}
|
|
270
289
|
case 'text':
|
|
271
290
|
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" }))] }));
|
|
272
291
|
case 'step':
|
|
@@ -288,7 +307,7 @@ function Item({ item, onSettle, busy, }) {
|
|
|
288
307
|
case 'stopped':
|
|
289
308
|
return _jsx(Rule, { children: item.reason === 'cap' ? "Stopped at this month's spend cap" : 'Stopped' });
|
|
290
309
|
case 'error':
|
|
291
|
-
return (_jsxs("p", { role:
|
|
310
|
+
return (_jsxs("p", { role: item.retried ? undefined : 'alert', className: cn('flex items-start gap-1.5 text-sm', item.retried ? 'text-muted-foreground' : 'text-loss'), children: [_jsx(CircleAlertIcon, { className: "mt-0.5 size-4 shrink-0" }), item.text] }));
|
|
292
311
|
}
|
|
293
312
|
}
|
|
294
313
|
/** Every screen call in the chat, hand-off sections included. */
|
|
@@ -33,6 +33,13 @@ export interface AssistantChatState {
|
|
|
33
33
|
* next step, else queued for after it.
|
|
34
34
|
*/
|
|
35
35
|
send: (message: string, extra?: Extra, how?: 'steer' | 'queue') => void;
|
|
36
|
+
/**
|
|
37
|
+
* Sends a failed turn again (Retry): `user` is the turn's message, `at` the error it ended in, which
|
|
38
|
+
* then stops offering Retry. Its words, parts, tier and images go again, with `extra` from now.
|
|
39
|
+
*/
|
|
40
|
+
retry: (at: number, user: Extract<ChatItem, {
|
|
41
|
+
kind: 'user';
|
|
42
|
+
}>, extra?: Extra) => void;
|
|
36
43
|
/** A queued message steered into the running answer now. */
|
|
37
44
|
steerQueued: (id: string) => void;
|
|
38
45
|
/** Takes a queued message out; its words and extras come back (Edit puts them in the box). */
|
|
@@ -179,7 +179,7 @@ export function useAssistantChat(client) {
|
|
|
179
179
|
return back.some((x, i) => x !== q[i]) ? back : q;
|
|
180
180
|
});
|
|
181
181
|
}, []);
|
|
182
|
-
const run = useCallback(async (message, extra) => {
|
|
182
|
+
const run = useCallback(async (message, extra, retried) => {
|
|
183
183
|
const ctl = new AbortController();
|
|
184
184
|
abort.current = ctl;
|
|
185
185
|
setBusy(true);
|
|
@@ -193,6 +193,7 @@ export function useAssistantChat(client) {
|
|
|
193
193
|
...(extra?.context ? { context: extra.context.label } : {}),
|
|
194
194
|
...(extra?.refs?.length ? { refs: extra.refs } : {}),
|
|
195
195
|
...(extra?.tier ? { tier: extra.tier } : {}),
|
|
196
|
+
...(retried ? { retried: true } : {}),
|
|
196
197
|
...(extra?.images?.length
|
|
197
198
|
? { images: extra.images.map((i) => `data:${i.media_type};base64,${i.data}`) }
|
|
198
199
|
: {}),
|
|
@@ -299,6 +300,21 @@ export function useAssistantChat(client) {
|
|
|
299
300
|
if (steer)
|
|
300
301
|
void steerIn(m);
|
|
301
302
|
}, [run, canSteer, steerIn]);
|
|
303
|
+
const retry = useCallback((at, user, extra) => {
|
|
304
|
+
if (abort.current)
|
|
305
|
+
return;
|
|
306
|
+
setItems((p) => p.map((x, i) => (i === at && x.kind === 'error' ? { ...x, retried: true } : x)));
|
|
307
|
+
const images = (user.images ?? []).flatMap((src) => {
|
|
308
|
+
const m = /^data:([^;]+);base64,(.*)$/.exec(src);
|
|
309
|
+
return m ? [{ media_type: m[1], data: m[2] }] : [];
|
|
310
|
+
});
|
|
311
|
+
void run(user.text, {
|
|
312
|
+
...extra,
|
|
313
|
+
...(user.refs?.length ? { refs: user.refs } : {}),
|
|
314
|
+
...(user.tier ? { tier: user.tier } : {}),
|
|
315
|
+
...(images.length ? { images } : {}),
|
|
316
|
+
}, true);
|
|
317
|
+
}, [run]);
|
|
302
318
|
const steerQueued = useCallback((id) => {
|
|
303
319
|
const m = queueRef.current.find((x) => x.id === id && !x.steering);
|
|
304
320
|
if (!m || !abort.current)
|
|
@@ -397,6 +413,7 @@ export function useAssistantChat(client) {
|
|
|
397
413
|
canSteer,
|
|
398
414
|
loading,
|
|
399
415
|
send,
|
|
416
|
+
retry,
|
|
400
417
|
steerQueued,
|
|
401
418
|
unqueue,
|
|
402
419
|
stop,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@columenlabs/lumen-ui",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.219",
|
|
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.219",
|
|
53
53
|
"@dnd-kit/core": "6.3.1",
|
|
54
54
|
"@dnd-kit/sortable": "10.0.0",
|
|
55
55
|
"@dnd-kit/utilities": "3.2.2",
|