@columenlabs/lumen-ui 0.1.77 → 0.1.79

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.
@@ -0,0 +1,240 @@
1
+ import { useCallback, useEffect, useRef, useState } from 'react';
2
+ import { errorText } from '../integrations.js';
3
+ /** Where an event lands: the chat, or the last hand-off section when it came from inside one. */
4
+ function sinkOf(items, handoff) {
5
+ if (!handoff)
6
+ return items;
7
+ for (let i = items.length - 1; i >= 0; i--) {
8
+ const it = items[i];
9
+ if (it?.kind === 'handoff')
10
+ return it.items;
11
+ }
12
+ return items;
13
+ }
14
+ /** A copy of the items with one event applied (hand-off sections copied too). */
15
+ export function applyEvent(prev, e) {
16
+ const items = prev.map((i) => i.kind === 'handoff' ? { ...i, items: [...i.items] } : i);
17
+ const inside = 'handoff' in e.data && e.data.handoff === true;
18
+ const sink = sinkOf(items, inside);
19
+ const last = sink[sink.length - 1];
20
+ switch (e.event) {
21
+ case 'text':
22
+ if (last?.kind === 'text' && last.live)
23
+ sink[sink.length - 1] = { ...last, text: last.text + e.data.delta };
24
+ else
25
+ sink.push({ kind: 'text', text: e.data.delta, live: true });
26
+ break;
27
+ case 'text_end':
28
+ if (last?.kind === 'text')
29
+ sink[sink.length - 1] = { kind: 'text', text: last.text };
30
+ break;
31
+ case 'step_start':
32
+ sink.push({
33
+ kind: 'step',
34
+ id: e.data.id,
35
+ tool: e.data.tool,
36
+ title: e.data.title,
37
+ ok: true,
38
+ ms: 0,
39
+ live: true,
40
+ });
41
+ break;
42
+ case 'step': {
43
+ const { handoff: _h, ...step } = e.data;
44
+ const at = sink.findIndex((i) => i.kind === 'step' && i.id === step.id);
45
+ if (at >= 0)
46
+ sink[at] = step;
47
+ else
48
+ sink.push(step);
49
+ break;
50
+ }
51
+ case 'draft': {
52
+ const { handoff: _h, ...draft } = e.data;
53
+ sink.push(draft);
54
+ break;
55
+ }
56
+ case 'handoff':
57
+ items.push({ ...e.data, items: [] });
58
+ break;
59
+ case 'handoff_done': {
60
+ const h = [...items].reverse().find((i) => i.kind === 'handoff');
61
+ if (h)
62
+ h.usage = e.data.usage;
63
+ break;
64
+ }
65
+ case 'advice':
66
+ items.push({ kind: 'advice', tier: e.data.tier, label: e.data.label, live: true });
67
+ break;
68
+ case 'advice_done': {
69
+ const at = items.findLastIndex((i) => i.kind === 'advice');
70
+ const a = items[at];
71
+ if (a?.kind === 'advice')
72
+ items[at] = {
73
+ kind: 'advice',
74
+ tier: a.tier,
75
+ label: a.label,
76
+ ...(e.data.error ? { error: e.data.error } : {}),
77
+ };
78
+ break;
79
+ }
80
+ case 'compacted':
81
+ items.push({ kind: 'compacted' });
82
+ break;
83
+ case 'stopped':
84
+ items.push({ kind: 'stopped', ...(e.data.reason ? { reason: e.data.reason } : {}) });
85
+ break;
86
+ case 'error':
87
+ items.push({
88
+ kind: 'error',
89
+ text: e.data.message,
90
+ ...(e.data.code ? { code: e.data.code } : {}),
91
+ });
92
+ break;
93
+ }
94
+ return items;
95
+ }
96
+ /** Nothing streams any more: a text or step cut by Stop ends where it was. */
97
+ const settled = (items) => items.map((i) => {
98
+ if (i.kind === 'handoff')
99
+ return { ...i, items: settled(i.items) };
100
+ if (!i.live)
101
+ return i;
102
+ const { live: _l, ...rest } = i;
103
+ return rest;
104
+ });
105
+ export function useAssistantChat(client) {
106
+ const [chatId, setChatId] = useState(null);
107
+ const [title, setTitle] = useState('');
108
+ const [items, setItems] = useState([]);
109
+ const [usage, setUsage] = useState(null);
110
+ const [busy, setBusy] = useState(false);
111
+ const [loading, setLoading] = useState(false);
112
+ const [queued, setQueued] = useState(null);
113
+ const abort = useRef(null);
114
+ const idRef = useRef(null);
115
+ idRef.current = chatId;
116
+ const run = useCallback(async (message, extra) => {
117
+ const ctl = new AbortController();
118
+ abort.current = ctl;
119
+ setBusy(true);
120
+ if (message !== '/compact')
121
+ setItems((p) => [
122
+ ...p,
123
+ {
124
+ kind: 'user',
125
+ text: message,
126
+ at: Date.now(),
127
+ ...(extra?.context ? { context: extra.context.label } : {}),
128
+ ...(extra?.refs?.length ? { refs: extra.refs } : {}),
129
+ ...(extra?.tier ? { tier: extra.tier } : {}),
130
+ },
131
+ ]);
132
+ try {
133
+ await client.send({ message, ...(idRef.current ? { chat_id: idRef.current } : {}), ...extra }, (e) => {
134
+ if (e.event === 'chat') {
135
+ idRef.current = e.data.id;
136
+ setChatId(e.data.id);
137
+ setTitle(e.data.title);
138
+ }
139
+ else if (e.event === 'usage')
140
+ setUsage(e.data);
141
+ else if (e.event === 'done')
142
+ setUsage(e.data.usage);
143
+ else
144
+ setItems((p) => applyEvent(p, e));
145
+ }, ctl.signal);
146
+ }
147
+ catch (err) {
148
+ if (ctl.signal.aborted)
149
+ setItems((p) => [...p, { kind: 'stopped' }]);
150
+ else
151
+ setItems((p) => [...p, { kind: 'error', text: errorText(err) }]);
152
+ }
153
+ finally {
154
+ setItems(settled);
155
+ abort.current = null;
156
+ setBusy(false);
157
+ }
158
+ }, [client]);
159
+ // A queued message goes once the answer ends.
160
+ useEffect(() => {
161
+ if (busy || !queued)
162
+ return;
163
+ setQueued(null);
164
+ void run(queued.message, queued.extra);
165
+ }, [busy, queued, run]);
166
+ const send = useCallback((message, extra) => {
167
+ const text = message.trim();
168
+ if (!text)
169
+ return;
170
+ if (abort.current)
171
+ setQueued({ message: text, ...(extra ? { extra } : {}) });
172
+ else
173
+ void run(text, extra);
174
+ }, [run]);
175
+ const stop = useCallback(() => abort.current?.abort(), []);
176
+ const reset = useCallback(() => {
177
+ abort.current?.abort();
178
+ setQueued(null);
179
+ setChatId(null);
180
+ idRef.current = null;
181
+ setTitle('');
182
+ setItems([]);
183
+ setUsage(null);
184
+ }, []);
185
+ const open = useCallback(async (id) => {
186
+ abort.current?.abort();
187
+ setQueued(null);
188
+ setLoading(true);
189
+ try {
190
+ const c = await client.chat(id);
191
+ idRef.current = c.id;
192
+ setChatId(c.id);
193
+ setTitle(c.title);
194
+ setItems(c.items);
195
+ setUsage(c.usage ?? null);
196
+ }
197
+ catch (err) {
198
+ setItems([{ kind: 'error', text: errorText(err) }]);
199
+ }
200
+ finally {
201
+ setLoading(false);
202
+ }
203
+ }, [client]);
204
+ // Puts a settled card or an undone line back in place, in a hand-off section too.
205
+ const replace = useCallback((next) => {
206
+ const swap = (list) => list.map((i) => i.kind === next.kind && i.id === next.id
207
+ ? next
208
+ : i.kind === 'handoff'
209
+ ? { ...i, items: swap(i.items) }
210
+ : i);
211
+ setItems(swap);
212
+ }, []);
213
+ const settle = useCallback(async (draft, action) => {
214
+ const id = idRef.current;
215
+ if (!id)
216
+ return;
217
+ replace(await client.settleDraft(id, draft.id, action));
218
+ }, [client, replace]);
219
+ const undo = useCallback(async (step) => {
220
+ const id = idRef.current;
221
+ if (!id)
222
+ return;
223
+ replace(await client.undoWrite(id, step.id));
224
+ }, [client, replace]);
225
+ return {
226
+ chatId,
227
+ title,
228
+ items,
229
+ usage,
230
+ busy,
231
+ queued: queued?.message ?? null,
232
+ loading,
233
+ send,
234
+ stop,
235
+ open,
236
+ reset,
237
+ settle,
238
+ undo,
239
+ };
240
+ }
@@ -0,0 +1,21 @@
1
+ import { type ReactNode } from 'react';
2
+ /** Rows that start collapsed; opening one closes the others. `defaultOpen` names a row open at first. */
3
+ export declare function LumenExpandList({ defaultOpen, className, children, }: {
4
+ defaultOpen?: string | null;
5
+ className?: string;
6
+ children: ReactNode;
7
+ }): import("react").JSX.Element;
8
+ /**
9
+ * One row that expands in place. Inside a LumenExpandList it opens by `id`; `open` and
10
+ * `onOpenChange` make it controlled; alone it keeps its own state (`defaultOpen`).
11
+ */
12
+ export declare function LumenExpandRow({ id, name, meta, badge, open: openProp, onOpenChange, defaultOpen, children, }: {
13
+ id?: string;
14
+ name: ReactNode;
15
+ meta?: ReactNode;
16
+ badge?: ReactNode;
17
+ open?: boolean;
18
+ onOpenChange?: (open: boolean) => void;
19
+ defaultOpen?: boolean;
20
+ children: ReactNode;
21
+ }): import("react").JSX.Element;
@@ -0,0 +1,33 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ // A stack of rows that start collapsed and expand in place: a settings page's providers, channels or
3
+ // accounts, each row's name, a line about it and a badge, its own settings inside. One row is open
4
+ // at a time within a LumenExpandList.
5
+ import { ChevronRightIcon } from 'lucide-react';
6
+ import { createContext, useContext, useState } from 'react';
7
+ import { cn } from './lib/utils.js';
8
+ import { Collapsible, CollapsibleContent, CollapsibleTrigger } from './ui/collapsible.js';
9
+ const ListCtx = createContext(null);
10
+ /** Rows that start collapsed; opening one closes the others. `defaultOpen` names a row open at first. */
11
+ export function LumenExpandList({ defaultOpen = null, className, children, }) {
12
+ const [open, set] = useState(defaultOpen);
13
+ return (_jsx(ListCtx.Provider, { value: { open, set }, children: _jsx("div", { className: cn('grid grid-cols-1', className), children: children }) }));
14
+ }
15
+ /**
16
+ * One row that expands in place. Inside a LumenExpandList it opens by `id`; `open` and
17
+ * `onOpenChange` make it controlled; alone it keeps its own state (`defaultOpen`).
18
+ */
19
+ export function LumenExpandRow({ id, name, meta, badge, open: openProp, onOpenChange, defaultOpen = false, children, }) {
20
+ const list = useContext(ListCtx);
21
+ const [own, setOwn] = useState(defaultOpen);
22
+ const inList = list && id !== undefined;
23
+ const open = openProp ?? (inList ? list.open === id : own);
24
+ const change = (o) => {
25
+ if (onOpenChange)
26
+ onOpenChange(o);
27
+ else if (inList)
28
+ list.set(o ? id : null);
29
+ else
30
+ setOwn(o);
31
+ };
32
+ return (_jsxs(Collapsible, { open: open, onOpenChange: change, className: "border-b last:border-b-0", children: [_jsx(CollapsibleTrigger, { asChild: true, children: _jsxs("button", { type: "button", className: "flex w-full items-center gap-3 px-1 py-3 text-left hover:bg-muted/50", children: [_jsxs("span", { className: "min-w-0 flex-1", children: [_jsx("span", { className: "block font-medium", children: name }), meta && _jsx("span", { className: "block truncate text-xs text-muted-foreground", children: meta })] }), badge, _jsx(ChevronRightIcon, { className: cn('size-4 shrink-0 text-muted-foreground transition-transform', open && 'rotate-90') })] }) }), _jsx(CollapsibleContent, { className: "grid grid-cols-1 gap-5 px-1 pt-1 pb-5", children: children })] }));
33
+ }
package/dist/index.d.ts CHANGED
@@ -49,6 +49,7 @@ export { type Crumb, IconButton, type IconButtonProps, Page, PageBody, PageHeade
49
49
  export { DEFAULT_PREFS, deviceTimeZone, type Prefs, TIME_ZONES, type Theme, type TimeZoneId, getPrefs, resolvedTheme, setPref, timeZoneLabel, usePrefs, ASSISTANT_WIDTH, } from './prefs.js';
50
50
  export { KIT, type KitConfig, configureLumen } from './kit-config.js';
51
51
  export { ConnectionBlock, ErrorLine, IntegrationRow, KeyTestLine, type KeyTestResult, TestBadge, errorText, useClock, } from './integrations.js';
52
+ export { LumenExpandList, LumenExpandRow } from './expand-list.js';
52
53
  export { LumenPager } from './pager.js';
53
54
  export { createDeskQueryClient } from './query-client.js';
54
55
  export { refreshHeaders, withRefreshToken } from './refresh.js';
package/dist/index.js CHANGED
@@ -56,6 +56,7 @@ export { IconButton, Page, PageBody, PageHeader, } from './page.js';
56
56
  export { DEFAULT_PREFS, deviceTimeZone, TIME_ZONES, getPrefs, resolvedTheme, setPref, timeZoneLabel, usePrefs, ASSISTANT_WIDTH, } from './prefs.js';
57
57
  export { KIT, configureLumen } from './kit-config.js';
58
58
  export { ConnectionBlock, ErrorLine, IntegrationRow, KeyTestLine, TestBadge, errorText, useClock, } from './integrations.js';
59
+ export { LumenExpandList, LumenExpandRow } from './expand-list.js';
59
60
  export { LumenPager } from './pager.js';
60
61
  export { createDeskQueryClient } from './query-client.js';
61
62
  export { refreshHeaders, withRefreshToken } from './refresh.js';
@@ -1,4 +1,5 @@
1
1
  import { type ReactNode } from 'react';
2
+ import { LumenExpandRow } from './expand-list.js';
2
3
  /** A key's last test: when, whether it worked, and what it read or why it failed. */
3
4
  export interface KeyTestResult {
4
5
  at: string;
@@ -13,15 +14,8 @@ export declare const errorText: (e: unknown) => string;
13
14
  export declare function ErrorLine({ error }: {
14
15
  error: unknown;
15
16
  }): import("react").JSX.Element | null;
16
- /** One integration: a row that expands in place. */
17
- export declare function IntegrationRow({ name, meta, badge, open, onOpenChange, children, }: {
18
- name: string;
19
- meta: string;
20
- badge: ReactNode;
21
- open: boolean;
22
- onOpenChange: (open: boolean) => void;
23
- children: ReactNode;
24
- }): import("react").JSX.Element;
17
+ /** One integration: a row that expands in place (LumenExpandRow, kept under its old name). */
18
+ export declare const IntegrationRow: typeof LumenExpandRow;
25
19
  /**
26
20
  * One linked connection (a bank, a broker) and its accounts (an app banks and
27
21
  * An app a broker link brokers alike). Collapsed by default, showing its name, `meta` and status; a
@@ -4,6 +4,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
4
4
  // the Test line that runs it again. A saved key says nothing; each row shows whether it works.
5
5
  import { AlertTriangleIcon, CheckIcon, ChevronRightIcon, RefreshCwIcon } from 'lucide-react';
6
6
  import { useState } from 'react';
7
+ import { LumenExpandRow } from './expand-list.js';
7
8
  import { IconButton } from './icon-button.js';
8
9
  import { cn } from './lib/utils.js';
9
10
  import { usePrefs } from './prefs.js';
@@ -32,10 +33,8 @@ export function ErrorLine({ error }) {
32
33
  return null;
33
34
  return (_jsx("p", { role: "alert", className: "text-xs text-loss", children: errorText(error) }));
34
35
  }
35
- /** One integration: a row that expands in place. */
36
- export function IntegrationRow({ name, meta, badge, open, onOpenChange, children, }) {
37
- return (_jsxs(Collapsible, { open: open, onOpenChange: onOpenChange, className: "border-b last:border-b-0", children: [_jsx(CollapsibleTrigger, { asChild: true, children: _jsxs("button", { type: "button", className: "flex w-full items-center gap-3 px-1 py-3 text-left hover:bg-muted/50", children: [_jsxs("span", { className: "min-w-0 flex-1", children: [_jsx("span", { className: "block font-medium", children: name }), _jsx("span", { className: "block truncate text-xs text-muted-foreground", children: meta })] }), badge, _jsx(ChevronRightIcon, { className: cn('size-4 shrink-0 text-muted-foreground transition-transform', open && 'rotate-90') })] }) }), _jsx(CollapsibleContent, { className: "grid grid-cols-1 gap-5 px-1 pt-1 pb-5", children: children })] }));
38
- }
36
+ /** One integration: a row that expands in place (LumenExpandRow, kept under its old name). */
37
+ export const IntegrationRow = LumenExpandRow;
39
38
  /**
40
39
  * One linked connection (a bank, a broker) and its accounts (an app banks and
41
40
  * An app a broker link brokers alike). Collapsed by default, showing its name, `meta` and status; a
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@columenlabs/lumen-ui",
3
- "version": "0.1.77",
3
+ "version": "0.1.79",
4
4
  "description": "Lumen: shared React components (frame, views, widgets, charts, drawers).",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -19,6 +19,10 @@
19
19
  "types": "./dist/people/index.d.ts",
20
20
  "default": "./dist/people/index.js"
21
21
  },
22
+ "./assistant": {
23
+ "types": "./dist/assistant/index.d.ts",
24
+ "default": "./dist/assistant/index.js"
25
+ },
22
26
  "./theme.css": "./dist/theme.css"
23
27
  },
24
28
  "files": [
@@ -45,7 +49,7 @@
45
49
  "@codemirror/lang-javascript": "6.2.5",
46
50
  "@codemirror/lang-json": "6.0.2",
47
51
  "@codemirror/lang-sql": "6.10.0",
48
- "@columenlabs/lumen-ui-types": "0.1.77",
52
+ "@columenlabs/lumen-ui-types": "0.1.79",
49
53
  "@dnd-kit/core": "6.3.1",
50
54
  "@dnd-kit/sortable": "10.0.0",
51
55
  "@dnd-kit/utilities": "3.2.2",