@columenlabs/lumen-ui 0.1.230 → 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':
@@ -121,7 +121,8 @@ const mark = (label) => _jsx(ConnectionLogo, { text: label.charAt(0) });
121
121
  function OwnProviderKey({ client, p }) {
122
122
  const { apply, refresh } = useApply(client);
123
123
  const ways = [];
124
- if (p.signin === 'chatgpt')
124
+ const may = (w) => !p.own_ways || p.own_ways.includes(w);
125
+ if (p.signin === 'chatgpt' && (may('plan') || p.plan?.set))
125
126
  ways.push({
126
127
  id: 'plan',
127
128
  label: 'ChatGPT plan',
@@ -138,27 +139,37 @@ function OwnProviderKey({ client, p }) {
138
139
  removeText: 'Your API key, if you set one, runs from then on.',
139
140
  removeLabel: 'Sign out',
140
141
  });
141
- ways.push({
142
- id: 'key',
143
- label: 'API key',
144
- icon: KeyRoundIcon,
145
- info: `Your own ${p.label} key, billed by ${p.label}.`,
146
- set: p.own.set,
147
- ident: p.own.hint,
148
- test: wayTest(p.own),
149
- fields: [{ id: 'key', label: `${p.label} API key`, secret: true, prefix: p.prefix }],
150
- getUrl: p.keys_url,
151
- onSave: (v) => apply(`own:${p.id}`, () => client.ownKey(p.id, v.key ?? ''), 'Key saved'),
152
- onTest: () => client.testOwnKey(p.id, 'key').finally(refresh),
153
- onRemove: () => apply(`own:${p.id}`, () => client.ownKey(p.id, null), 'Key removed'),
154
- removeTitle: `Remove your ${p.label} key?`,
155
- });
142
+ if (may('key') || p.own.set)
143
+ ways.push({
144
+ id: 'key',
145
+ label: 'API key',
146
+ icon: KeyRoundIcon,
147
+ info: `Your own ${p.label} key, billed by ${p.label}.`,
148
+ set: p.own.set,
149
+ ident: p.own.hint,
150
+ test: wayTest(p.own),
151
+ fields: [{ id: 'key', label: `${p.label} API key`, secret: true, prefix: p.prefix }],
152
+ getUrl: p.keys_url,
153
+ onSave: (v) => apply(`own:${p.id}`, () => client.ownKey(p.id, v.key ?? ''), 'Key saved'),
154
+ onTest: () => client.testOwnKey(p.id, 'key').finally(refresh),
155
+ onRemove: () => apply(`own:${p.id}`, () => client.ownKey(p.id, null), 'Key removed'),
156
+ removeTitle: `Remove your ${p.label} key?`,
157
+ });
156
158
  return (_jsx(LumenConnection, { id: p.id, name: p.label, logo: mark(p.label), ways: ways, use: p.use, onUse: (use) => apply(`use:${p.id}`, () => client.settings({ own_use: { [p.id]: use } })) }));
157
159
  }
158
160
  /** A person's own keys, one row a provider, and what they may spend on them. */
159
161
  function Providers({ client }) {
160
162
  const { apply } = useApply(client);
161
- return (_jsx(WithStatus, { client: client, children: (s) => (_jsxs("div", { className: "flex flex-col", children: [_jsx(LumenExpandList, { children: s.providers.map((p) => (_jsx(OwnProviderKey, { client: client, p: p }, p.id))) }), s.providers.some((p) => p.own.set || p.plan?.set) && (_jsx(Row, { label: "Your keys may spend", info: "On your keys, every provider together.", children: _jsx(CapInput, { label: "Your monthly cap", value: s.caps.own, onSave: (n) => void apply('own', () => client.ownCap(n)) }) }))] })) }));
163
+ return (_jsx(WithStatus, { client: client, children: (s) => {
164
+ // A provider the app gives this person no way of their own on shows no row, unless one is
165
+ // still set (so it can be removed).
166
+ const rows = s.providers.filter((p) => !p.own_ways || p.own_ways.length > 0 || p.own.set || p.plan?.set);
167
+ const ownSet = rows.some((p) => p.own.set || p.plan?.set);
168
+ return (_jsxs("div", { className: "flex flex-col", children: [_jsx(LumenExpandList, { children: rows.map((p) => (_jsx(OwnProviderKey, { client: client, p: p }, p.id))) }), ownSet && (_jsx(Row, { label: "Answers run on", info: "Your own key or plan first, with the shared credits behind it; or the shared credits first.", children: _jsx(LumenSelector, { label: "Answers run on", size: "sm", className: "w-44", value: s.pay, options: [
169
+ { value: 'own', label: 'Your own first' },
170
+ { value: 'app', label: 'Shared credits first' },
171
+ ], onChange: (pay) => void apply('pay', () => client.settings({ pay: pay })) }) })), ownSet && (_jsx(Row, { label: "Your keys may spend", info: "On your keys, every provider together.", children: _jsx(CapInput, { label: "Your monthly cap", value: s.caps.own, onSave: (n) => void apply('own', () => client.ownCap(n)) }) }))] }));
172
+ } }));
162
173
  }
163
174
  /** One provider's shared key, which an admin sets for everyone. */
164
175
  function SharedProviderKey({ client, p }) {
@@ -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.230",
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.230",
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",