@columenlabs/lumen-ui 0.1.231 → 0.1.232

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.
@@ -72,6 +72,8 @@ function itemMarkdown(i, depth, name) {
72
72
  ].join('\n\n');
73
73
  case 'advice':
74
74
  return `_${i.error ? 'Could not ask' : 'Asked'} ${i.label} for advice._`;
75
+ case 'fresh':
76
+ return '_Offered a fresh chat for this._';
75
77
  case 'compacted':
76
78
  return '_Compacted._';
77
79
  case 'stopped':
@@ -193,9 +193,9 @@ export function LumenAssistant({ client, context, onSettings, keepDays = 30, cha
193
193
  title: c.title,
194
194
  updated_at: new Date(c.at).toISOString(),
195
195
  ...(chatScope && allScopes && c.scope ? { where: chatScope.name?.(c.scope) } : {}),
196
- })) ?? (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, compactAt: compactAt })] }));
196
+ })) ?? (chats.isFetched ? [] : null), value: chat.chatId ?? undefined, toggle: chatScope && { label: chatScope.label, on: allScopes, onChange: setAllScopes }, note: `Chats are kept ${keepDays} days after their last message.`, onRefresh: () => void chats.refetch(), onOpen: (c) => void chat.open(c.id) }), _jsx(IconButton, { icon: CopyIcon, label: "Copy chat", disabled: !chat.items.length, onClick: () => copyMarkdown(chatMarkdown(chat.title, chat.items, name), 'Chat') }), _jsx(IconButton, { icon: PlusIcon, label: "New chat", disabled: !chat.items.length, onClick: chat.reset }), onClose && (_jsx(IconButton, { icon: XIcon, label: `Close ${s?.name ?? 'assistant'}`, onClick: onClose }))] }), _jsx(AssistantName.Provider, { value: name, children: _jsx(Screen.Provider, { value: controls, children: _jsx(ImageTool.Provider, { value: imageTool ?? null, children: _jsx(Messages, { chat: chat, status: s, onSettings: onSettings, onRetry: (at, user) => chat.retry(at, user, extra()), onFresh: (choice) => chat.fresh(choice, extra()) }) }) }) }), _jsx(LumenComposer, { name: s?.name ?? 'the assistant', busy: chat.busy, queued: chat.queued, queue: chat.queue, canSteer: chat.canSteer, onSteerQueued: chat.steerQueued, onUnqueue: chat.unqueue, disabled: !s || !!s.wait, commands: allCommands, tiers: tiers, sources: allSources, page: context?.label, images: images, modes: modes, onSend: (text, more, how) => chat.send(text, { ...extra(), ...more }, how), onStop: chat.stop }), _jsx(UsageLine, { usage: chat.usage, client: client, spent: s?.spend.usd, compactAt: compactAt })] }));
197
197
  }
198
- function Messages({ chat, status, onSettings, onRetry, }) {
198
+ function Messages({ chat, status, onSettings, onRetry, onFresh, }) {
199
199
  const end = useRef(null);
200
200
  const box = useRef(null);
201
201
  const count = JSON.stringify(chat.items).length;
@@ -214,7 +214,7 @@ function Messages({ chat, status, onSettings, onRetry, }) {
214
214
  status.allowance &&
215
215
  status.allowance.request?.status !== 'open'
216
216
  ? 'Request more'
217
- : '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 })] }));
217
+ : '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(FreshChoice.Provider, { value: chat.busy ? null : onFresh, 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 })] }));
218
218
  }
219
219
  const isLive = (i) => !!i.live || (i.kind === 'handoff' && i.items.some(isLive));
220
220
  const WAIT = {
@@ -243,6 +243,16 @@ const TierNames = createContext((id) => id);
243
243
  /** The app's image tool (`LumenAssistant` `imageTool`). */
244
244
  const ImageTool = createContext(null);
245
245
  const UndoWrite = createContext(null);
246
+ /** Answers the fresh-chat card; null while an answer runs. */
247
+ const FreshChoice = createContext(null);
248
+ /**
249
+ * The app asks before answering: the message above starts new work and this chat is long, so a
250
+ * fresh chat answers it faster and for less. + takes it there, × keeps it here.
251
+ */
252
+ function FreshCard() {
253
+ const choose = useContext(FreshChoice);
254
+ return (_jsxs("div", { "data-slot": "assistant-fresh", className: "flex min-w-0 items-center gap-2 rounded-lg border bg-card py-2 pr-2 pl-3 text-sm", children: [_jsx("span", { className: "min-w-0 flex-1", children: "This looks like new work. Start a fresh chat for it?" }), choose && (_jsxs("span", { className: "flex shrink-0 gap-1", children: [_jsx(IconButton, { icon: XIcon, label: "Keep it in this chat", onClick: () => choose('stay') }), _jsx(IconButton, { icon: PlusIcon, label: "New chat with this message", variant: "default", onClick: () => choose('new') })] }))] }));
255
+ }
246
256
  /** Sends a failed turn again; null while it can't (an answer runs, nobody can pay). */
247
257
  const RetryTurn = createContext(null);
248
258
  /** Where a turn's unretried error sits in the chat, when the turn starts with a message. */
@@ -310,6 +320,8 @@ function Item({ item, onSettle, busy, }) {
310
320
  : `Asked ${item.label}`] }));
311
321
  case 'compacted':
312
322
  return _jsx(Rule, { children: "Compacted: earlier messages are sent as a summary" });
323
+ case 'fresh':
324
+ return _jsx(FreshCard, {});
313
325
  case 'stopped':
314
326
  return _jsx(Rule, { children: item.reason === 'cap' ? "Stopped at this month's spend cap" : 'Stopped' });
315
327
  case 'error':
@@ -52,6 +52,11 @@ export interface AssistantChatState {
52
52
  undo: (step: StepItem) => Promise<void>;
53
53
  /** A screen call ended in the browser: shown on its chip, and sent with the next message. */
54
54
  screenEnded: (outcome: AssistantScreenOutcome) => void;
55
+ /**
56
+ * Answers the fresh-chat card (`fresh` item): `new` sends its message in a new chat, `stay`
57
+ * sends it here again, kept in this chat.
58
+ */
59
+ fresh: (choice: 'new' | 'stay', extra?: Extra) => void;
55
60
  }
56
61
  /** A copy of the items with one event applied (hand-off sections copied too). */
57
62
  export declare function applyEvent(prev: LiveItem[], e: AssistantEvent): LiveItem[];
@@ -33,6 +33,19 @@ function keepOutcomes(m) {
33
33
  }
34
34
  let queuedSeq = 0;
35
35
  const queuedId = () => `q${++queuedSeq}`;
36
+ /** What a sent message carried besides its words, to send it again (Retry, a fresh chat). */
37
+ function resendOf(user) {
38
+ const images = (user.images ?? []).flatMap((src) => {
39
+ const m = /^data:([^;]+);base64,(.*)$/.exec(src);
40
+ return m ? [{ media_type: m[1], data: m[2] }] : [];
41
+ });
42
+ return {
43
+ ...(user.refs?.length ? { refs: user.refs } : {}),
44
+ ...(user.quote ? { quote: user.quote } : {}),
45
+ ...(user.tier ? { tier: user.tier } : {}),
46
+ ...(images.length ? { images } : {}),
47
+ };
48
+ }
36
49
  /** Where an event lands: the chat, or the last hand-off section when it came from inside one. */
37
50
  function sinkOf(items, handoff) {
38
51
  if (!handoff)
@@ -119,6 +132,9 @@ export function applyEvent(prev, e) {
119
132
  case 'compacted':
120
133
  items.push({ kind: 'compacted' });
121
134
  break;
135
+ case 'fresh':
136
+ items.push({ kind: 'fresh' });
137
+ break;
122
138
  case 'stopped':
123
139
  items.push({ kind: 'stopped', ...(e.data.reason ? { reason: e.data.reason } : {}) });
124
140
  break;
@@ -305,17 +321,34 @@ export function useAssistantChat(client) {
305
321
  if (abort.current)
306
322
  return;
307
323
  setItems((p) => p.map((x, i) => (i === at && x.kind === 'error' ? { ...x, retried: true } : x)));
308
- const images = (user.images ?? []).flatMap((src) => {
309
- const m = /^data:([^;]+);base64,(.*)$/.exec(src);
310
- return m ? [{ media_type: m[1], data: m[2] }] : [];
311
- });
312
- void run(user.text, {
313
- ...extra,
314
- ...(user.refs?.length ? { refs: user.refs } : {}),
315
- ...(user.quote ? { quote: user.quote } : {}),
316
- ...(user.tier ? { tier: user.tier } : {}),
317
- ...(images.length ? { images } : {}),
318
- }, true);
324
+ void run(user.text, { ...extra, ...resendOf(user) }, true);
325
+ }, [run]);
326
+ const itemsRef = useRef([]);
327
+ itemsRef.current = items;
328
+ const fresh = useCallback((choice, extra) => {
329
+ if (abort.current)
330
+ return;
331
+ const list = itemsRef.current;
332
+ const card = list.findLastIndex((i) => i.kind === 'fresh');
333
+ const at = list.findLastIndex((i, n) => n < card && i.kind === 'user');
334
+ const user = list[at];
335
+ if (card < 0 || user?.kind !== 'user')
336
+ return;
337
+ if (choice === 'new') {
338
+ // A new chat, as New chat makes one, and the message is its first.
339
+ setQueue([]);
340
+ setChatId(null);
341
+ idRef.current = null;
342
+ setTitle('');
343
+ setItems([]);
344
+ setUsage(null);
345
+ void run(user.text, { ...extra, ...resendOf(user) });
346
+ }
347
+ else {
348
+ // The message goes again in its place, without the card.
349
+ setItems((p) => p.slice(0, at));
350
+ void run(user.text, { ...extra, ...resendOf(user), stay: true });
351
+ }
319
352
  }, [run]);
320
353
  const steerQueued = useCallback((id) => {
321
354
  const m = queueRef.current.find((x) => x.id === id && !x.steering);
@@ -424,5 +457,6 @@ export function useAssistantChat(client) {
424
457
  settle,
425
458
  undo,
426
459
  screenEnded,
460
+ fresh,
427
461
  };
428
462
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@columenlabs/lumen-ui",
3
- "version": "0.1.231",
3
+ "version": "0.1.232",
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.231",
52
+ "@columenlabs/lumen-ui-types": "0.1.232",
53
53
  "@dnd-kit/core": "6.3.1",
54
54
  "@dnd-kit/sortable": "10.0.0",
55
55
  "@dnd-kit/utilities": "3.2.2",