@columenlabs/lumen-ui 0.1.286 → 0.1.287
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 +5 -0
- package/dist/assistant/client.js +3 -0
- package/dist/assistant/copy.js +5 -0
- package/dist/assistant/lumen-assistant.d.ts +12 -2
- package/dist/assistant/lumen-assistant.js +35 -3
- package/dist/assistant/use-assistant-chat.d.ts +3 -1
- package/dist/assistant/use-assistant-chat.js +71 -8
- package/package.json +2 -2
|
@@ -52,6 +52,11 @@ export interface AssistantAdapter {
|
|
|
52
52
|
steer?(chatId: string, message: string): Promise<{
|
|
53
53
|
steered: boolean;
|
|
54
54
|
}>;
|
|
55
|
+
/**
|
|
56
|
+
* Stops the answer running in `chatId` on the server: what it said and made so far is kept, and
|
|
57
|
+
* the answer's stream ends with `stopped`. Without it Stop only stops reading the stream.
|
|
58
|
+
*/
|
|
59
|
+
stop?(chatId: string): Promise<void>;
|
|
55
60
|
settleDraft(chatId: string, draftId: string, action: 'save' | 'dismiss'): Promise<DraftItem>;
|
|
56
61
|
/** Takes back a change the assistant made at once. */
|
|
57
62
|
undoWrite(chatId: string, stepId: string): Promise<StepItem>;
|
package/dist/assistant/client.js
CHANGED
|
@@ -76,6 +76,9 @@ export function assistantClient(base) {
|
|
|
76
76
|
...(amount !== undefined ? { amount } : {}),
|
|
77
77
|
}),
|
|
78
78
|
steer: (chatId, message) => send('POST', `/chats/${encodeURIComponent(chatId)}/steer`, { message }),
|
|
79
|
+
async stop(chatId) {
|
|
80
|
+
await send('POST', `/chats/${encodeURIComponent(chatId)}/stop`);
|
|
81
|
+
},
|
|
79
82
|
async send(msg, onEvent, signal) {
|
|
80
83
|
const url = `${root}/chat`;
|
|
81
84
|
const res = await fetch(url, {
|
package/dist/assistant/copy.js
CHANGED
|
@@ -72,6 +72,11 @@ function itemMarkdown(i, depth, name) {
|
|
|
72
72
|
]
|
|
73
73
|
.filter(Boolean)
|
|
74
74
|
.join('\n\n');
|
|
75
|
+
case 'task':
|
|
76
|
+
return [
|
|
77
|
+
`${h} ${i.label} (${i.state})`,
|
|
78
|
+
...i.items.map((x) => itemMarkdown(x, depth + 1, name)),
|
|
79
|
+
].join('\n\n');
|
|
75
80
|
case 'advice':
|
|
76
81
|
return `_${i.error ? 'Could not ask' : 'Asked'} ${i.label} for advice._`;
|
|
77
82
|
case 'fresh':
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type AllowanceUnit, type AssistantImage, type AssistantMessage, type AssistantQuote, type AssistantRefNote, type AssistantStatus } from '@columenlabs/lumen-ui-types/assistant';
|
|
1
|
+
import { type AllowanceUnit, type AssistantImage, type AssistantMessage, type AssistantQuote, type AssistantRefNote, type AssistantStatus, type AssistantTask, type AssistantTaskState } from '@columenlabs/lumen-ui-types/assistant';
|
|
2
2
|
import { type LucideIcon } from 'lucide-react';
|
|
3
3
|
import { type ReactNode } from 'react';
|
|
4
4
|
import { type LumenOption } from '../lumen-selector.js';
|
|
@@ -63,6 +63,16 @@ export interface LumenAssistantProps {
|
|
|
63
63
|
* yellow at 75% of it and red at it. The person's Auto-compact (status `compact_at`) wins; 150k by default.
|
|
64
64
|
*/
|
|
65
65
|
compactAt?: number;
|
|
66
|
+
/**
|
|
67
|
+
* A task in the chat (`AssistantTask`, the app's work in its own session): the buttons in its
|
|
68
|
+
* block's header, such as Keep and Drop, from the app.
|
|
69
|
+
*/
|
|
70
|
+
taskActions?: (task: AssistantTask) => ReactNode;
|
|
71
|
+
/**
|
|
72
|
+
* The app's word on where a task stands, ahead of the chat's: it took or dropped the task's result
|
|
73
|
+
* in its own screens. A task no longer open drops its chip over the box.
|
|
74
|
+
*/
|
|
75
|
+
taskState?: (task: AssistantTask) => AssistantTaskState | undefined;
|
|
66
76
|
}
|
|
67
77
|
/** A composer mode: its toggle's icon and label (also its tooltip and aria-label). */
|
|
68
78
|
export interface AssistantMode {
|
|
@@ -84,7 +94,7 @@ export interface AssistantCommand {
|
|
|
84
94
|
run: (args: string) => string | undefined | Promise<string | undefined>;
|
|
85
95
|
}
|
|
86
96
|
/** The assistant's chat: header, messages and composer. */
|
|
87
|
-
export declare function LumenAssistant({ client, context, onSettings, keepDays, chatScope, onClose, chat: outside, commands, sources, images, modes, imageTool, compactAt, }: LumenAssistantProps): import("react").JSX.Element;
|
|
97
|
+
export declare function LumenAssistant({ client, context, onSettings, keepDays, chatScope, onClose, chat: outside, commands, sources, images, modes, imageTool, compactAt, taskActions, taskState, }: LumenAssistantProps): import("react").JSX.Element;
|
|
88
98
|
/** An allowance in its unit: `$4.20`, or `350 credits`. */
|
|
89
99
|
export declare const allowanceAmount: (u: AllowanceUnit, n: number) => string;
|
|
90
100
|
/** What the composer sends with the text: the tier picked with `#` and the things pointed at with `@`. */
|
|
@@ -49,7 +49,7 @@ export function useAssistantStatus(client) {
|
|
|
49
49
|
export { usd };
|
|
50
50
|
const seconds = (ms) => (ms < 1000 ? `${ms} ms` : `${(ms / 1000).toFixed(1)} s`);
|
|
51
51
|
/** The assistant's chat: header, messages and composer. */
|
|
52
|
-
export function LumenAssistant({ client, context, onSettings, keepDays = 30, chatScope, onClose, chat: outside, commands = [], sources = [], images = false, modes = [], imageTool, compactAt = COMPACT_AT, }) {
|
|
52
|
+
export function LumenAssistant({ client, context, onSettings, keepDays = 30, chatScope, onClose, chat: outside, commands = [], sources = [], images = false, modes = [], imageTool, compactAt = COMPACT_AT, taskActions, taskState, }) {
|
|
53
53
|
const own = useAssistantChat(client);
|
|
54
54
|
const chat = outside ?? own;
|
|
55
55
|
const qc = useQueryClient();
|
|
@@ -80,9 +80,16 @@ export function LumenAssistant({ client, context, onSettings, keepDays = 30, cha
|
|
|
80
80
|
// each new screen call runs here once; how it ended goes with the next message.
|
|
81
81
|
const screen = useAssistantScreen();
|
|
82
82
|
const screenOn = !!screen && (s?.screen ?? 'move') === 'move';
|
|
83
|
+
// The open task's chip over the box: the next message goes to the task (the app decides) until
|
|
84
|
+
// the person steps out with its ×, or the task ends.
|
|
85
|
+
const [stepped, setStepped] = useState(null);
|
|
86
|
+
const openTask = chat.task && (taskState?.(chat.task) ?? chat.task.state) === 'open' && stepped !== chat.task.id
|
|
87
|
+
? chat.task
|
|
88
|
+
: null;
|
|
83
89
|
const extra = () => ({
|
|
84
90
|
...(context ? { context } : {}),
|
|
85
91
|
...(screenOn && screen ? { screen: screenMapFor(screen) } : {}),
|
|
92
|
+
...(openTask ? { task: openTask.id } : {}),
|
|
86
93
|
});
|
|
87
94
|
const ran = useRef(new Set());
|
|
88
95
|
const ended = chat.screenEnded;
|
|
@@ -191,7 +198,7 @@ export function LumenAssistant({ client, context, onSettings, keepDays = 30, cha
|
|
|
191
198
|
title: c.title,
|
|
192
199
|
updated_at: new Date(c.at).toISOString(),
|
|
193
200
|
...(chatScope && allScopes && c.scope ? { where: chatScope.name?.(c.scope) } : {}),
|
|
194
|
-
})) ?? (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, meter: _jsx(ContextDonut, { usage: chat.usage, compactAt: s?.compact_at ?? compactAt, items: chat.items }) })] }));
|
|
201
|
+
})) ?? (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(TaskSlots.Provider, { value: { actions: taskActions, state: taskState }, children: _jsx(Messages, { chat: chat, status: s, onSettings: onSettings, onRetry: (at, user) => chat.retry(at, user, extra()), onFresh: (choice) => chat.fresh(choice, extra()) }) }) }) }) }), openTask && (_jsx("div", { className: "flex px-3 pt-1", children: _jsx(TaskChip, { task: openTask, onLeave: () => setStepped(openTask.id) }) })), _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, meter: _jsx(ContextDonut, { usage: chat.usage, compactAt: s?.compact_at ?? compactAt, items: chat.items }) })] }));
|
|
195
202
|
}
|
|
196
203
|
function Messages({ chat, status, onSettings, onRetry, onFresh, }) {
|
|
197
204
|
const end = useRef(null);
|
|
@@ -214,7 +221,7 @@ function Messages({ chat, status, onSettings, onRetry, onFresh, }) {
|
|
|
214
221
|
? 'Request more'
|
|
215
222
|
: '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 })] }));
|
|
216
223
|
}
|
|
217
|
-
const isLive = (i) => !!i.live || (i.kind === 'handoff' && i.items.some(isLive));
|
|
224
|
+
const isLive = (i) => !!i.live || ((i.kind === 'handoff' || i.kind === 'task') && i.items.some(isLive));
|
|
218
225
|
const WAIT = {
|
|
219
226
|
not_connected: (admin, provider) => admin
|
|
220
227
|
? `Add an ${provider} key in Settings ▸ Model providers, or pick another model in Models.`
|
|
@@ -310,6 +317,8 @@ function Item({ item, onSettle, busy, }) {
|
|
|
310
317
|
return _jsx(ScreenChip, { item: item });
|
|
311
318
|
case 'handoff':
|
|
312
319
|
return _jsx(HandOff, { item: item, onSettle: onSettle, busy: busy });
|
|
320
|
+
case 'task':
|
|
321
|
+
return _jsx(TaskBlock, { item: item, onSettle: onSettle, busy: busy });
|
|
313
322
|
case 'advice':
|
|
314
323
|
return (_jsxs("p", { className: "flex items-center gap-1.5 text-muted-foreground text-xs", children: [item.live ? _jsx(Spinner, { className: "size-3" }) : _jsx(LightbulbIcon, { className: "size-3.5" }), item.live
|
|
315
324
|
? `Asking ${item.label}…`
|
|
@@ -528,6 +537,29 @@ const WHY = {
|
|
|
528
537
|
function HandOff({ item, onSettle, busy, }) {
|
|
529
538
|
return (_jsxs("section", { className: "flex flex-col gap-2 border-l-2 border-primary/40 pl-3", children: [_jsxs("p", { className: "flex items-center gap-1.5 text-xs", children: [_jsx(CornerDownRightIcon, { className: "size-3.5 text-muted-foreground" }), _jsx("span", { className: "font-medium", children: item.label }), _jsxs("span", { className: "text-muted-foreground", children: ["\u00B7 ", WHY[item.why]] })] }), item.task && _jsx("p", { className: "text-muted-foreground text-xs", children: item.task }), _jsx(Items, { items: item.items, onSettle: onSettle, busy: busy })] }));
|
|
530
539
|
}
|
|
540
|
+
/** The app's parts of a task's block (`taskActions`, `taskState`). */
|
|
541
|
+
const TaskSlots = createContext({});
|
|
542
|
+
const TASK_STATE = {
|
|
543
|
+
open: 'Open',
|
|
544
|
+
done: 'Kept',
|
|
545
|
+
dropped: 'Dropped',
|
|
546
|
+
};
|
|
547
|
+
/**
|
|
548
|
+
* A task's turns in the chat (`TaskItem`): one block with its label and state; while open the
|
|
549
|
+
* app's buttons (`taskActions`) sit in its header, once ended it folds to that one line.
|
|
550
|
+
*/
|
|
551
|
+
function TaskBlock({ item, onSettle, busy, }) {
|
|
552
|
+
const slots = useContext(TaskSlots);
|
|
553
|
+
const state = slots.state?.(item) ?? item.state;
|
|
554
|
+
const live = item.items.some(isLive);
|
|
555
|
+
const [open, setOpen] = useState(null);
|
|
556
|
+
const shown = open ?? (state === 'open' || live);
|
|
557
|
+
return (_jsxs("section", { "data-slot": "chat-task", "data-state": state, className: "flex min-w-0 flex-col gap-2 rounded-md border border-primary/30 bg-primary/5 px-3 py-2", children: [_jsxs("div", { className: "flex min-w-0 items-center gap-1.5 text-xs", children: [_jsx(WrenchIcon, { className: "size-3.5 shrink-0 text-muted-foreground", "aria-hidden": true }), _jsx("button", { type: "button", className: "min-w-0 flex-1 truncate text-left font-medium", title: item.label, "aria-expanded": shown, onClick: () => setOpen(!shown), children: item.label }), state === 'open' ? (!live && slots.actions?.(item)) : (_jsxs("span", { className: "shrink-0 text-muted-foreground", children: [state === 'done' ? _jsx(CheckIcon, { className: "inline size-3.5", "aria-hidden": true }) : null, ' ', TASK_STATE[state]] }))] }), shown && _jsx(Items, { items: item.items, onSettle: onSettle, busy: busy })] }));
|
|
558
|
+
}
|
|
559
|
+
/** The open task's chip over the box: the next message goes to it; its × steps out. */
|
|
560
|
+
function TaskChip({ task, onLeave }) {
|
|
561
|
+
return (_jsxs("span", { "data-slot": "chat-task-chip", className: "inline-flex min-w-0 max-w-full items-center gap-1 rounded-md border border-primary/30 bg-primary/5 px-1.5 py-0.5 text-xs", title: `Your next message goes to ${task.label}`, children: [_jsx(WrenchIcon, { className: "size-3 shrink-0 text-muted-foreground", "aria-hidden": true }), _jsx("span", { className: "truncate", children: task.label }), _jsx(IconButton, { icon: XIcon, label: "Step out of this task", size: "icon-xs", onClick: onLeave })] }));
|
|
562
|
+
}
|
|
531
563
|
/** The composer's triggers: `@` points at a thing (the `sources`), `#` picks the tier for one message. */
|
|
532
564
|
const POINT = '@';
|
|
533
565
|
const TIER = '#';
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AssistantEvent, AssistantMessage, AssistantScreenOutcome, ChatItem, ChatUsage, DraftItem, StepItem } from '@columenlabs/lumen-ui-types/assistant';
|
|
1
|
+
import type { AssistantEvent, AssistantMessage, AssistantScreenOutcome, AssistantTask, ChatItem, ChatUsage, DraftItem, StepItem } from '@columenlabs/lumen-ui-types/assistant';
|
|
2
2
|
import type { AssistantAdapter } from './client.js';
|
|
3
3
|
/**
|
|
4
4
|
* An item as the panel draws it: a text still streaming, a step still running. `writing` marks a
|
|
@@ -32,6 +32,8 @@ export interface AssistantChatState {
|
|
|
32
32
|
canSteer: boolean;
|
|
33
33
|
/** The chat is loading. */
|
|
34
34
|
loading: boolean;
|
|
35
|
+
/** The newest task open in this chat (`AssistantTask`): the composer offers to keep the next message on it. */
|
|
36
|
+
task: AssistantTask | null;
|
|
35
37
|
/**
|
|
36
38
|
* Sends now, or while an answer runs: `steer` (the default where the adapter can) into it at its
|
|
37
39
|
* next step, else queued for after it.
|
|
@@ -46,8 +46,13 @@ function resendOf(user) {
|
|
|
46
46
|
...(images.length ? { images } : {}),
|
|
47
47
|
};
|
|
48
48
|
}
|
|
49
|
-
/**
|
|
50
|
-
|
|
49
|
+
/**
|
|
50
|
+
* Where an event lands: the chat, the task block this answer runs in, or the last hand-off section
|
|
51
|
+
* when it came from inside one.
|
|
52
|
+
*/
|
|
53
|
+
function sinkOf(all, handoff) {
|
|
54
|
+
const top = all[all.length - 1];
|
|
55
|
+
const items = top?.kind === 'task' && top.live ? top.items : all;
|
|
51
56
|
if (!handoff)
|
|
52
57
|
return items;
|
|
53
58
|
for (let i = items.length - 1; i >= 0; i--) {
|
|
@@ -59,7 +64,10 @@ function sinkOf(items, handoff) {
|
|
|
59
64
|
}
|
|
60
65
|
/** A copy of the items with one event applied (hand-off sections copied too). */
|
|
61
66
|
export function applyEvent(prev, e) {
|
|
62
|
-
const
|
|
67
|
+
const copy = (i) => i.kind === 'handoff' || i.kind === 'task'
|
|
68
|
+
? { ...i, items: i.items.map(copy) }
|
|
69
|
+
: i;
|
|
70
|
+
const items = prev.map(copy);
|
|
63
71
|
const inside = 'handoff' in e.data && e.data.handoff === true;
|
|
64
72
|
const sink = sinkOf(items, inside);
|
|
65
73
|
const last = sink[sink.length - 1];
|
|
@@ -115,10 +123,31 @@ export function applyEvent(prev, e) {
|
|
|
115
123
|
break;
|
|
116
124
|
}
|
|
117
125
|
case 'handoff':
|
|
118
|
-
|
|
126
|
+
sink.push({ ...e.data, items: [] });
|
|
119
127
|
break;
|
|
128
|
+
case 'task': {
|
|
129
|
+
// This answer runs in a task: the message just sent joins the task's block when the block is
|
|
130
|
+
// right above it, else a new block opens under it; what the answer says lands in the block.
|
|
131
|
+
const user = items.findLastIndex((i) => i.kind === 'user' && !i.steered);
|
|
132
|
+
const above = user > 0 ? items[user - 1] : undefined;
|
|
133
|
+
if (user >= 0 &&
|
|
134
|
+
user === items.length - 1 &&
|
|
135
|
+
above?.kind === 'task' &&
|
|
136
|
+
above.id === e.data.id) {
|
|
137
|
+
const u = items.splice(user, 1)[0];
|
|
138
|
+
items[user - 1] = {
|
|
139
|
+
...above,
|
|
140
|
+
...e.data,
|
|
141
|
+
items: [...above.items, u],
|
|
142
|
+
live: true,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
else
|
|
146
|
+
items.push({ kind: 'task', ...e.data, items: [], live: true });
|
|
147
|
+
break;
|
|
148
|
+
}
|
|
120
149
|
case 'handoff_done': {
|
|
121
|
-
const h = [...
|
|
150
|
+
const h = [...sink].reverse().find((i) => i.kind === 'handoff');
|
|
122
151
|
if (h)
|
|
123
152
|
h.usage = e.data.usage;
|
|
124
153
|
break;
|
|
@@ -164,16 +193,23 @@ export function applyEvent(prev, e) {
|
|
|
164
193
|
const settled = (items) => items.map((i) => {
|
|
165
194
|
if (i.kind === 'handoff')
|
|
166
195
|
return { ...i, items: settled(i.items) };
|
|
196
|
+
if (i.kind === 'task') {
|
|
197
|
+
const { live: _l, ...rest } = i;
|
|
198
|
+
return { ...rest, items: settled(i.items) };
|
|
199
|
+
}
|
|
167
200
|
if (!i.live || i.kind === 'screen')
|
|
168
201
|
return i;
|
|
169
202
|
const { live: _l, writing: _w, ...rest } = i;
|
|
170
203
|
return rest;
|
|
171
204
|
});
|
|
205
|
+
/** How long Stop waits for the server to end the answer before it stops reading the stream. */
|
|
206
|
+
const STOP_WAIT_MS = 8_000;
|
|
172
207
|
export function useAssistantChat(client) {
|
|
173
208
|
const [chatId, setChatId] = useState(null);
|
|
174
209
|
const [title, setTitle] = useState('');
|
|
175
210
|
const [items, setItems] = useState([]);
|
|
176
211
|
const [usage, setUsage] = useState(null);
|
|
212
|
+
const [task, setTask] = useState(null);
|
|
177
213
|
const [busy, setBusy] = useState(false);
|
|
178
214
|
const [loading, setLoading] = useState(false);
|
|
179
215
|
const [queue, setQueue] = useState([]);
|
|
@@ -244,6 +280,10 @@ export function useAssistantChat(client) {
|
|
|
244
280
|
}
|
|
245
281
|
else if (e.event === 'usage')
|
|
246
282
|
setUsage(e.data);
|
|
283
|
+
else if (e.event === 'task') {
|
|
284
|
+
setTask(e.data.state === 'open' ? e.data : null);
|
|
285
|
+
setItems((p) => applyEvent(p, e));
|
|
286
|
+
}
|
|
247
287
|
else if (e.event === 'done')
|
|
248
288
|
setUsage(e.data.usage);
|
|
249
289
|
else if (e.event === 'steered') {
|
|
@@ -351,6 +391,7 @@ export function useAssistantChat(client) {
|
|
|
351
391
|
setTitle('');
|
|
352
392
|
setItems([]);
|
|
353
393
|
setUsage(null);
|
|
394
|
+
setTask(null);
|
|
354
395
|
void run(user.text, { ...extra, ...resendOf(user) });
|
|
355
396
|
}
|
|
356
397
|
else {
|
|
@@ -377,7 +418,26 @@ export function useAssistantChat(client) {
|
|
|
377
418
|
setQueue((q) => q.filter((x) => x.id !== id));
|
|
378
419
|
return m;
|
|
379
420
|
}, []);
|
|
380
|
-
|
|
421
|
+
// Stop asks the server to end the answer, and the stream ends there with what was said and made
|
|
422
|
+
// so far, so nothing it finished is lost. An app with no stop route (or one that does not
|
|
423
|
+
// answer in a few seconds) stops reading the stream instead.
|
|
424
|
+
const stop = useCallback(() => {
|
|
425
|
+
const ctl = abort.current;
|
|
426
|
+
const id = idRef.current;
|
|
427
|
+
if (!ctl)
|
|
428
|
+
return;
|
|
429
|
+
if (!client.stop || !id)
|
|
430
|
+
return void ctl.abort();
|
|
431
|
+
const late = setTimeout(() => {
|
|
432
|
+
if (abort.current === ctl)
|
|
433
|
+
ctl.abort();
|
|
434
|
+
}, STOP_WAIT_MS);
|
|
435
|
+
client.stop(id).catch(() => {
|
|
436
|
+
clearTimeout(late);
|
|
437
|
+
if (abort.current === ctl)
|
|
438
|
+
ctl.abort();
|
|
439
|
+
});
|
|
440
|
+
}, [client]);
|
|
381
441
|
const reset = useCallback(() => {
|
|
382
442
|
abort.current?.abort();
|
|
383
443
|
setQueue([]);
|
|
@@ -386,6 +446,7 @@ export function useAssistantChat(client) {
|
|
|
386
446
|
setTitle('');
|
|
387
447
|
setItems([]);
|
|
388
448
|
setUsage(null);
|
|
449
|
+
setTask(null);
|
|
389
450
|
}, []);
|
|
390
451
|
const open = useCallback(async (id) => {
|
|
391
452
|
abort.current?.abort();
|
|
@@ -405,6 +466,7 @@ export function useAssistantChat(client) {
|
|
|
405
466
|
: i;
|
|
406
467
|
}));
|
|
407
468
|
setUsage(c.usage ?? null);
|
|
469
|
+
setTask(c.task ?? null);
|
|
408
470
|
}
|
|
409
471
|
catch (err) {
|
|
410
472
|
setItems([{ kind: 'error', text: errorText(err) }]);
|
|
@@ -417,7 +479,7 @@ export function useAssistantChat(client) {
|
|
|
417
479
|
const replace = useCallback((next) => {
|
|
418
480
|
const swap = (list) => list.map((i) => i.kind === next.kind && i.id === next.id
|
|
419
481
|
? next
|
|
420
|
-
: i.kind === 'handoff'
|
|
482
|
+
: i.kind === 'handoff' || i.kind === 'task'
|
|
421
483
|
? { ...i, items: swap(i.items) }
|
|
422
484
|
: i);
|
|
423
485
|
setItems(swap);
|
|
@@ -441,7 +503,7 @@ export function useAssistantChat(client) {
|
|
|
441
503
|
const outcome = { status: o.status, ...(o.text ? { text: o.text } : {}) };
|
|
442
504
|
const mark = (list) => list.map((i) => i.kind === 'screen' && i.id === o.id
|
|
443
505
|
? { kind: 'screen', id: i.id, label: i.label, moves: i.moves, outcome }
|
|
444
|
-
: i.kind === 'handoff'
|
|
506
|
+
: i.kind === 'handoff' || i.kind === 'task'
|
|
445
507
|
? { ...i, items: mark(i.items) }
|
|
446
508
|
: i);
|
|
447
509
|
setItems(mark);
|
|
@@ -456,6 +518,7 @@ export function useAssistantChat(client) {
|
|
|
456
518
|
queue,
|
|
457
519
|
canSteer,
|
|
458
520
|
loading,
|
|
521
|
+
task,
|
|
459
522
|
send,
|
|
460
523
|
retry,
|
|
461
524
|
steerQueued,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@columenlabs/lumen-ui",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.287",
|
|
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.287",
|
|
53
53
|
"@dnd-kit/core": "6.3.1",
|
|
54
54
|
"@dnd-kit/sortable": "10.0.0",
|
|
55
55
|
"@dnd-kit/utilities": "3.2.2",
|