@columenlabs/lumen-ui 0.1.299 → 0.1.301
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/copy.js
CHANGED
|
@@ -49,7 +49,7 @@ function itemMarkdown(i, depth, name) {
|
|
|
49
49
|
case 'step': {
|
|
50
50
|
const facts = [
|
|
51
51
|
i.rows !== undefined ? `${i.rows} rows` : '',
|
|
52
|
-
`${(i.ms / 1000).toFixed(1)} s
|
|
52
|
+
i.ms ? `${(i.ms / 1000).toFixed(1)} s` : '',
|
|
53
53
|
i.write ? i.write.status : '',
|
|
54
54
|
].filter(Boolean);
|
|
55
55
|
return `**${i.ok ? 'Tool' : 'Failed'}: ${i.title}** (${facts.join(', ')})${i.error ? `\n\nError: ${i.error}` : ''}`;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { type AssistantAdapter, assistantClient } from './client.js';
|
|
2
2
|
export { ChatImage } from './chat-image.js';
|
|
3
|
-
export { type AssistantCommand, type AssistantMode, type ComposerExtra, LumenAssistant, type LumenAssistantProps, LumenComposer, assistantStatusKey, readAssistantImage, useAssistantStatus, useLumenAssistantDock, } from './lumen-assistant.js';
|
|
3
|
+
export { type AssistantCommand, type AssistantMode, type ComposerExtra, LumenAssistant, type LumenAssistantProps, LumenComposer, QuickReplies, WorkCard, assistantStatusKey, readAssistantImage, useAssistantStatus, useLumenAssistantDock, } from './lumen-assistant.js';
|
|
4
4
|
export { AutoCompactField, assistantAdminSections, assistantSettingsSections } from './settings.js';
|
|
5
5
|
export { AssistantChatCost, AssistantUsageTable, UsageHistoryView, assistantUsageKey, tokens as formatTokens, usd as formatUsd, useAssistantUsage, } from './usage-history.js';
|
|
6
6
|
export { type AssistantSource, type AssistantSourceItem, type ComposeRequest, type PageSelection, composeAssistant, selectionRef, useAssistantSource, usePageSelection, useShownAssistantSources, } from './sources.js';
|
package/dist/assistant/index.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// `@columenlabs/lumen-ui-types/assistant`.
|
|
4
4
|
export { assistantClient } from './client.js';
|
|
5
5
|
export { ChatImage } from './chat-image.js';
|
|
6
|
-
export { LumenAssistant, LumenComposer, assistantStatusKey, readAssistantImage, useAssistantStatus, useLumenAssistantDock, } from './lumen-assistant.js';
|
|
6
|
+
export { LumenAssistant, LumenComposer, QuickReplies, WorkCard, assistantStatusKey, readAssistantImage, useAssistantStatus, useLumenAssistantDock, } from './lumen-assistant.js';
|
|
7
7
|
export { AutoCompactField, assistantAdminSections, assistantSettingsSections } from './settings.js';
|
|
8
8
|
export { AssistantChatCost, AssistantUsageTable, UsageHistoryView, assistantUsageKey, tokens as formatTokens, usd as formatUsd, useAssistantUsage, } from './usage-history.js';
|
|
9
9
|
export { composeAssistant, selectionRef, useAssistantSource, usePageSelection, useShownAssistantSources, } from './sources.js';
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type AllowanceUnit, type AssistantImage, type AssistantMessage, type AssistantQuote, type AssistantRefNote, type AssistantStatus, type AssistantTask, type AssistantTaskState } from '@columenlabs/lumen-ui-types/assistant';
|
|
1
|
+
import { type AllowanceUnit, type AssistantImage, type AssistantMessage, type AssistantQuote, type AssistantRefNote, type AssistantStatus, type AssistantTask, type AssistantTaskState, type AssistantTaskView, type StepChoices } 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';
|
|
@@ -65,14 +65,21 @@ export interface LumenAssistantProps {
|
|
|
65
65
|
compactAt?: number;
|
|
66
66
|
/**
|
|
67
67
|
* A task in the chat (`AssistantTask`, the app's work in its own session): the buttons in its
|
|
68
|
-
*
|
|
68
|
+
* card's header, such as Discard and Accept, from the app; `live` while the task's turn runs
|
|
69
|
+
* (the card adds Stop then).
|
|
69
70
|
*/
|
|
70
|
-
taskActions?: (task: AssistantTask) => ReactNode;
|
|
71
|
+
taskActions?: (task: AssistantTask, live: boolean) => ReactNode;
|
|
71
72
|
/**
|
|
72
73
|
* 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.
|
|
74
|
+
* in its own screens. While a task is open, each message the person sends goes to it.
|
|
74
75
|
*/
|
|
75
76
|
taskState?: (task: AssistantTask) => AssistantTaskState | undefined;
|
|
77
|
+
/**
|
|
78
|
+
* The header of a task's card (`AssistantTaskView`): its tone (running, ready, settled, failed,
|
|
79
|
+
* waiting on an answer, stopped), title and the line under it. Without it the card shows the
|
|
80
|
+
* task's label and the chat's word for its state.
|
|
81
|
+
*/
|
|
82
|
+
taskView?: (task: AssistantTask, live: boolean) => AssistantTaskView | undefined;
|
|
76
83
|
}
|
|
77
84
|
/** A composer mode: its toggle's icon and label (also its tooltip and aria-label). */
|
|
78
85
|
export interface AssistantMode {
|
|
@@ -94,9 +101,37 @@ export interface AssistantCommand {
|
|
|
94
101
|
run: (args: string) => string | undefined | Promise<string | undefined>;
|
|
95
102
|
}
|
|
96
103
|
/** The assistant's chat: header, messages and composer. */
|
|
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;
|
|
104
|
+
export declare function LumenAssistant({ client, context, onSettings, keepDays, chatScope, onClose, chat: outside, commands, sources, images, modes, imageTool, compactAt, taskActions, taskState, taskView, }: LumenAssistantProps): import("react").JSX.Element;
|
|
98
105
|
/** An allowance in its unit: `$4.20`, or `350 credits`. */
|
|
99
106
|
export declare const allowanceAmount: (u: AllowanceUnit, n: number) => string;
|
|
107
|
+
/**
|
|
108
|
+
* A piece of the assistant's work as one card: a tinted icon for where it stands, its title and the
|
|
109
|
+
* line under it (`AssistantTaskView`), the app's buttons, Stop while it runs, a thin running bar,
|
|
110
|
+
* and a chevron that shows its steps (`children`). The chat draws each task this way; an app draws
|
|
111
|
+
* its own work the same way (an edit waiting for review) so every piece of work looks alike.
|
|
112
|
+
*/
|
|
113
|
+
export declare function WorkCard({ view, live, state, actions, onStop, steps, open: shownAt, children, }: {
|
|
114
|
+
view: AssistantTaskView;
|
|
115
|
+
live?: boolean;
|
|
116
|
+
/** The task's own state, for `data-state`. */
|
|
117
|
+
state?: string;
|
|
118
|
+
actions?: ReactNode;
|
|
119
|
+
onStop?: () => void;
|
|
120
|
+
/** How many steps `children` holds: no chevron without any. */
|
|
121
|
+
steps?: number;
|
|
122
|
+
/** Starts open; by default it is open while the work runs. */
|
|
123
|
+
open?: boolean;
|
|
124
|
+
children?: ReactNode;
|
|
125
|
+
}): import("react").JSX.Element;
|
|
126
|
+
/**
|
|
127
|
+
* Quick replies under the assistant's question: one row per answer, the suggested one outlined,
|
|
128
|
+
* numbered so a key picks it (when no text box has the focus). With `multi` each row ticks, and
|
|
129
|
+
* Send sends the ticked ones together. They are shortcuts: typing an answer in the box works too.
|
|
130
|
+
*/
|
|
131
|
+
export declare function QuickReplies({ choices, onSend, }: {
|
|
132
|
+
choices: StepChoices;
|
|
133
|
+
onSend: (text: string) => void;
|
|
134
|
+
}): import("react").JSX.Element;
|
|
100
135
|
/** What the composer sends with the text: the tier picked with `#` and the things pointed at with `@`. */
|
|
101
136
|
export interface ComposerExtra {
|
|
102
137
|
tier?: string;
|
|
@@ -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, COMPACT_AT, } 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, PaperclipIcon, PencilIcon, PlusIcon, RefreshCwIcon, ReplyIcon, SearchIcon, SparklesIcon, SquareIcon, TriangleAlertIcon, Undo2Icon, WrenchIcon, XIcon, } from 'lucide-react';
|
|
9
|
+
import { ArrowUpIcon, CheckCheckIcon, CheckIcon, ChevronDownIcon, ChevronRightIcon, ChevronUpIcon, CircleAlertIcon, CircleCheckIcon, CircleXIcon, CopyIcon, CornerDownRightIcon, CrosshairIcon, FileTextIcon, GlobeIcon, HandIcon, LightbulbIcon, ListPlusIcon, LoaderCircleIcon, MonitorIcon, PaperclipIcon, PencilIcon, PlusIcon, RefreshCwIcon, ReplyIcon, SearchIcon, SparklesIcon, SquareIcon, TriangleAlertIcon, Undo2Icon, WrenchIcon, XIcon, } 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';
|
|
@@ -47,9 +47,9 @@ export function useAssistantStatus(client) {
|
|
|
47
47
|
return useQuery({ queryKey: assistantStatusKey(client), queryFn: () => client.status() });
|
|
48
48
|
}
|
|
49
49
|
export { usd };
|
|
50
|
-
const seconds = (ms) =>
|
|
50
|
+
const seconds = (ms) => !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, taskActions, taskState, }) {
|
|
52
|
+
export function LumenAssistant({ client, context, onSettings, keepDays = 30, chatScope, onClose, chat: outside, commands = [], sources = [], images = false, modes = [], imageTool, compactAt = COMPACT_AT, taskActions, taskState, taskView, }) {
|
|
53
53
|
const own = useAssistantChat(client);
|
|
54
54
|
const chat = outside ?? own;
|
|
55
55
|
const qc = useQueryClient();
|
|
@@ -80,12 +80,9 @@ 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
|
-
//
|
|
84
|
-
//
|
|
85
|
-
const
|
|
86
|
-
const openTask = chat.task && (taskState?.(chat.task) ?? chat.task.state) === 'open' && stepped !== chat.task.id
|
|
87
|
-
? chat.task
|
|
88
|
-
: null;
|
|
83
|
+
// While a task is open, every message goes to it (the app decides what that means); the task
|
|
84
|
+
// ends when the app takes or drops its result.
|
|
85
|
+
const openTask = chat.task && (taskState?.(chat.task) ?? chat.task.state) === 'open' ? chat.task : null;
|
|
89
86
|
const extra = () => ({
|
|
90
87
|
...(context ? { context } : {}),
|
|
91
88
|
...(screenOn && screen ? { screen: screenMapFor(screen) } : {}),
|
|
@@ -198,9 +195,9 @@ export function LumenAssistant({ client, context, onSettings, keepDays = 30, cha
|
|
|
198
195
|
title: c.title,
|
|
199
196
|
updated_at: new Date(c.at).toISOString(),
|
|
200
197
|
...(chatScope && allScopes && c.scope ? { where: chatScope.name?.(c.scope) } : {}),
|
|
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())
|
|
198
|
+
})) ?? (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, view: taskView, stop: chat.stop }, children: _jsx(Messages, { chat: chat, status: s, onSettings: onSettings, onRetry: (at, user) => chat.retry(at, user, extra()), onFresh: (choice) => chat.fresh(choice, extra()), onAnswer: (text) => chat.send(text, 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 }) })] }));
|
|
202
199
|
}
|
|
203
|
-
function Messages({ chat, status, onSettings, onRetry, onFresh, }) {
|
|
200
|
+
function Messages({ chat, status, onSettings, onRetry, onFresh, onAnswer, }) {
|
|
204
201
|
const end = useRef(null);
|
|
205
202
|
const box = useRef(null);
|
|
206
203
|
const count = JSON.stringify(chat.items).length;
|
|
@@ -219,7 +216,12 @@ function Messages({ chat, status, onSettings, onRetry, onFresh, }) {
|
|
|
219
216
|
status.allowance &&
|
|
220
217
|
status.allowance.request?.status !== 'open'
|
|
221
218
|
? 'Request more'
|
|
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 &&
|
|
219
|
+
: '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 &&
|
|
220
|
+
!status?.wait &&
|
|
221
|
+
(() => {
|
|
222
|
+
const c = lastChoices(chat.items);
|
|
223
|
+
return c ? (_jsx("div", { className: "mt-2", children: _jsx(QuickReplies, { choices: c, onSend: onAnswer }, chat.items.length) })) : null;
|
|
224
|
+
})(), 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 })] }));
|
|
223
225
|
}
|
|
224
226
|
const isLive = (i) => !!i.live || ((i.kind === 'handoff' || i.kind === 'task') && i.items.some(isLive));
|
|
225
227
|
const WAIT = {
|
|
@@ -330,7 +332,11 @@ function Item({ item, onSettle, busy, }) {
|
|
|
330
332
|
case 'fresh':
|
|
331
333
|
return _jsx(FreshCard, {});
|
|
332
334
|
case 'stopped':
|
|
333
|
-
return _jsx(Rule, { children: item.reason === 'cap'
|
|
335
|
+
return (_jsx(Rule, { children: item.reason === 'cap'
|
|
336
|
+
? "Stopped at this month's spend cap"
|
|
337
|
+
: item.reason === 'limit'
|
|
338
|
+
? 'Stopped at the time limit'
|
|
339
|
+
: 'Stopped' }));
|
|
334
340
|
case 'error':
|
|
335
341
|
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] }));
|
|
336
342
|
}
|
|
@@ -452,7 +458,13 @@ function ToolStep({ step, live }) {
|
|
|
452
458
|
setUndoing(false);
|
|
453
459
|
}
|
|
454
460
|
};
|
|
455
|
-
|
|
461
|
+
// A change that did not land is a failure too, whatever the call returned.
|
|
462
|
+
const ok = step.ok && w?.status !== 'failed';
|
|
463
|
+
const facts = [
|
|
464
|
+
step.rows !== undefined ? `${step.rows} row${step.rows === 1 ? '' : 's'}` : '',
|
|
465
|
+
seconds(step.ms),
|
|
466
|
+
].filter(Boolean);
|
|
467
|
+
return (_jsxs("p", { "data-slot": "chat-step", "data-ok": ok, className: cn('flex min-w-0 items-center gap-1.5 text-xs', ok ? 'text-muted-foreground' : 'text-loss'), children: [live ? (_jsx(Spinner, { className: "size-3" })) : !ok ? (_jsx(CircleAlertIcon, { className: "size-3.5 shrink-0" })) : w ? (_jsx(PencilIcon, { className: "size-3.5 shrink-0" })) : (_jsx(WrenchIcon, { className: "size-3.5 shrink-0" })), _jsx("span", { className: "truncate", title: step.title, children: step.title }), !live && facts.length > 0 && (_jsx("span", { className: "shrink-0 tabular-nums", children: facts.join(' · ') })), (step.error || w?.error) && (_jsxs("span", { className: "truncate", title: step.error ?? w?.error, children: ["\u00B7 ", step.error ?? w?.error] })), w?.status === 'undone' ? (_jsx("span", { className: "shrink-0", children: "\u00B7 Undone" })) : (w?.undo &&
|
|
456
468
|
undo && (_jsxs(Button, { size: "sm", variant: "ghost", className: "ml-auto h-6 shrink-0 px-2 text-xs", disabled: undoing, onClick: () => void take(), children: [undoing ? _jsx(Spinner, {}) : _jsx(Undo2Icon, {}), "Undo"] })))] }));
|
|
457
469
|
}
|
|
458
470
|
/** The arguments as short lines: `name: value`. */
|
|
@@ -537,28 +549,150 @@ const WHY = {
|
|
|
537
549
|
function HandOff({ item, onSettle, busy, }) {
|
|
538
550
|
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 })] }));
|
|
539
551
|
}
|
|
540
|
-
/** The app's parts of a task's
|
|
552
|
+
/** The app's parts of a task's card (`taskActions`, `taskState`, `taskView`) and the chat's Stop. */
|
|
541
553
|
const TaskSlots = createContext({});
|
|
554
|
+
/** The words a card says without the app's own view. */
|
|
542
555
|
const TASK_STATE = {
|
|
543
556
|
open: 'Open',
|
|
544
557
|
done: 'Kept',
|
|
545
558
|
dropped: 'Dropped',
|
|
546
559
|
};
|
|
560
|
+
/** Each tone's icon and colour: the icon sits in a tinted square at the head of the card. */
|
|
561
|
+
const TONE = {
|
|
562
|
+
live: {
|
|
563
|
+
icon: LoaderCircleIcon,
|
|
564
|
+
box: 'bg-primary/12 text-primary',
|
|
565
|
+
word: 'text-primary',
|
|
566
|
+
spin: true,
|
|
567
|
+
},
|
|
568
|
+
ready: { icon: CircleCheckIcon, box: 'bg-gain/15 text-gain', word: 'text-gain' },
|
|
569
|
+
done: {
|
|
570
|
+
icon: CheckCheckIcon,
|
|
571
|
+
box: 'bg-muted text-muted-foreground',
|
|
572
|
+
word: 'text-muted-foreground',
|
|
573
|
+
},
|
|
574
|
+
failed: { icon: CircleAlertIcon, box: 'bg-loss/12 text-loss', word: 'text-loss' },
|
|
575
|
+
ask: { icon: HandIcon, box: 'bg-warning/15 text-warning', word: 'text-warning' },
|
|
576
|
+
stopped: {
|
|
577
|
+
icon: CircleXIcon,
|
|
578
|
+
box: 'bg-muted text-muted-foreground',
|
|
579
|
+
word: 'text-muted-foreground',
|
|
580
|
+
},
|
|
581
|
+
};
|
|
582
|
+
/** m:ss since a time, ticking each second while shown. */
|
|
583
|
+
function Clock({ since }) {
|
|
584
|
+
const [now, setNow] = useState(() => Date.now());
|
|
585
|
+
useEffect(() => {
|
|
586
|
+
const t = setInterval(() => setNow(Date.now()), 1000);
|
|
587
|
+
return () => clearInterval(t);
|
|
588
|
+
}, []);
|
|
589
|
+
const s = Math.max(0, Math.floor((now - since) / 1000));
|
|
590
|
+
return (_jsx("span", { className: "tabular-nums", children: `${Math.floor(s / 60)}:${String(s % 60).padStart(2, '0')}` }));
|
|
591
|
+
}
|
|
592
|
+
/** Where a task's card sits among its items: after the person's last message that tools answered. */
|
|
593
|
+
function cardAt(items) {
|
|
594
|
+
const last = items.findLastIndex((i) => i.kind === 'step' || i.kind === 'draft');
|
|
595
|
+
if (last < 0)
|
|
596
|
+
return items.findLastIndex((i) => i.kind === 'user') + 1;
|
|
597
|
+
for (let i = last; i >= 0; i--)
|
|
598
|
+
if (items[i]?.kind === 'user')
|
|
599
|
+
return i + 1;
|
|
600
|
+
return 0;
|
|
601
|
+
}
|
|
547
602
|
/**
|
|
548
|
-
* A
|
|
549
|
-
*
|
|
603
|
+
* A piece of the assistant's work as one card: a tinted icon for where it stands, its title and the
|
|
604
|
+
* line under it (`AssistantTaskView`), the app's buttons, Stop while it runs, a thin running bar,
|
|
605
|
+
* and a chevron that shows its steps (`children`). The chat draws each task this way; an app draws
|
|
606
|
+
* its own work the same way (an edit waiting for review) so every piece of work looks alike.
|
|
607
|
+
*/
|
|
608
|
+
export function WorkCard({ view, live = false, state, actions, onStop, steps = 0, open: shownAt, children, }) {
|
|
609
|
+
const tone = TONE[view.tone];
|
|
610
|
+
const [open, setOpen] = useState(null);
|
|
611
|
+
const shown = (open ?? (shownAt || live)) && steps > 0;
|
|
612
|
+
const Icon = tone.icon;
|
|
613
|
+
return (_jsxs("section", { "data-slot": "chat-task", "data-state": state, "data-tone": view.tone, "aria-label": view.title, className: cn('flex min-w-0 flex-col overflow-hidden rounded-lg border bg-card', view.tone === 'live' && 'border-primary/45', view.tone === 'ready' && 'border-gain/45', view.tone === 'failed' && 'border-loss/45', view.tone === 'ask' && 'border-warning/45'), children: [_jsxs("div", { className: "flex min-w-0 items-center gap-2.5 py-2 pr-1.5 pl-2.5", children: [_jsx("span", { className: cn('grid size-7 shrink-0 place-items-center rounded-md', tone.box), children: _jsx(Icon, { className: cn('size-4', tone.spin && 'animate-spin'), "aria-hidden": true }) }), _jsxs("div", { className: "flex min-w-0 flex-1 flex-col", children: [_jsx("span", { className: "truncate font-medium text-sm", title: view.title, children: view.title }), (view.detail || view.state || (live && view.since)) && (_jsxs("span", { className: "flex min-w-0 gap-1 text-muted-foreground text-xs", children: [view.detail && (_jsx("span", { className: "min-w-0 truncate", title: view.detail, children: view.detail })), view.state && (_jsxs("span", { className: cn('shrink-0', tone.word), children: [view.detail ? '· ' : '', view.state] })), live && view.since ? (_jsxs("span", { className: "shrink-0 tabular-nums", children: ["\u00B7 ", _jsx(Clock, { since: view.since })] })) : null] }))] }), _jsxs("span", { className: "flex shrink-0 items-center gap-1", children: [actions, onStop && (_jsx(IconButton, { icon: SquareIcon, label: "Stop", variant: "outline", size: "icon-sm", onClick: onStop })), steps > 0 && (_jsx(IconButton, { icon: shown ? ChevronUpIcon : ChevronDownIcon, label: shown ? 'Hide steps' : 'Show steps', size: "icon-sm", "aria-expanded": shown, onClick: () => setOpen(!shown) }))] })] }), live && (_jsx("div", { className: "h-0.5 w-full overflow-hidden bg-primary/15", children: _jsx("div", { className: "h-full w-1/3 animate-[lumen-progress_1.4s_ease-in-out_infinite] bg-primary" }) })), shown && _jsx("div", { className: "border-t px-3 py-2", children: children })] }));
|
|
614
|
+
}
|
|
615
|
+
/**
|
|
616
|
+
* A task's turns in the chat (`TaskItem`) as one card: a tinted icon for where it stands, its title
|
|
617
|
+
* and a line under it (the app's `taskView`), the app's buttons (`taskActions`), Stop while it runs
|
|
618
|
+
* and a chevron that shows the steps. The person's messages and the assistant's words stay in the
|
|
619
|
+
* chat around it, so a conversation about the work reads as one; the card sits at the newest turn
|
|
620
|
+
* that ran tools, holding that turn's steps, and earlier turns' steps stay as lines where they ran.
|
|
550
621
|
*/
|
|
551
622
|
function TaskBlock({ item, onSettle, busy, }) {
|
|
552
623
|
const slots = useContext(TaskSlots);
|
|
553
624
|
const state = slots.state?.(item) ?? item.state;
|
|
554
|
-
const
|
|
555
|
-
const
|
|
556
|
-
const
|
|
557
|
-
|
|
625
|
+
const items = item.items;
|
|
626
|
+
const live = items.some(isLive);
|
|
627
|
+
const view = slots.view?.(item, live) ?? {
|
|
628
|
+
tone: live ? 'live' : state === 'open' ? 'ready' : 'done',
|
|
629
|
+
title: item.label,
|
|
630
|
+
state: live ? 'Working' : TASK_STATE[state],
|
|
631
|
+
};
|
|
632
|
+
const at = cardAt(items);
|
|
633
|
+
const steps = items.slice(at).filter((i) => i.kind === 'step' || i.kind === 'draft');
|
|
634
|
+
const after = items.slice(at).filter((i) => i.kind !== 'step' && i.kind !== 'draft');
|
|
635
|
+
const card = (_jsx(WorkCard, { view: view, live: live, state: state, actions: slots.actions?.(item, live), onStop: live ? slots.stop : undefined, steps: steps.length, open: steps.some((x) => x.kind === 'step' && (!x.ok || x.write?.status === 'failed')), children: _jsx(Items, { items: steps, onSettle: onSettle, busy: busy }) }));
|
|
636
|
+
return (_jsxs("div", { "data-slot": "chat-task-turns", className: "flex min-w-0 flex-col gap-3", children: [at > 0 && _jsx(Items, { items: items.slice(0, at), onSettle: onSettle, busy: busy }), card, after.length > 0 && _jsx(Items, { items: after, onSettle: onSettle, busy: busy })] }));
|
|
558
637
|
}
|
|
559
|
-
/** The
|
|
560
|
-
function
|
|
561
|
-
|
|
638
|
+
/** The quick replies the last turn offers (`StepItem.choices`), unless the person answered since. */
|
|
639
|
+
function lastChoices(items) {
|
|
640
|
+
for (let i = items.length - 1; i >= 0; i--) {
|
|
641
|
+
const it = items[i];
|
|
642
|
+
if (it.kind === 'user')
|
|
643
|
+
return null;
|
|
644
|
+
if (it.kind === 'task') {
|
|
645
|
+
const inner = lastChoices(it.items);
|
|
646
|
+
if (inner || it.items.some((x) => x.kind === 'user'))
|
|
647
|
+
return inner;
|
|
648
|
+
continue;
|
|
649
|
+
}
|
|
650
|
+
if (it.kind === 'step' && it.choices)
|
|
651
|
+
return it.choices;
|
|
652
|
+
}
|
|
653
|
+
return null;
|
|
654
|
+
}
|
|
655
|
+
/**
|
|
656
|
+
* Quick replies under the assistant's question: one row per answer, the suggested one outlined,
|
|
657
|
+
* numbered so a key picks it (when no text box has the focus). With `multi` each row ticks, and
|
|
658
|
+
* Send sends the ticked ones together. They are shortcuts: typing an answer in the box works too.
|
|
659
|
+
*/
|
|
660
|
+
export function QuickReplies({ choices, onSend, }) {
|
|
661
|
+
const [picked, setPicked] = useState(() => choices.multi ? choices.options.flatMap((o, n) => (o.on ? [n] : [])) : []);
|
|
662
|
+
const { options, multi, suggested } = choices;
|
|
663
|
+
const pick = (n) => {
|
|
664
|
+
const o = options[n];
|
|
665
|
+
if (!o)
|
|
666
|
+
return;
|
|
667
|
+
if (!multi)
|
|
668
|
+
return onSend(o.label);
|
|
669
|
+
setPicked((p) => (p.includes(n) ? p.filter((x) => x !== n) : [...p, n]));
|
|
670
|
+
};
|
|
671
|
+
const pickRef = useRef(pick);
|
|
672
|
+
pickRef.current = pick;
|
|
673
|
+
useEffect(() => {
|
|
674
|
+
const on = (e) => {
|
|
675
|
+
const t = e.target;
|
|
676
|
+
if (e.metaKey || e.ctrlKey || e.altKey)
|
|
677
|
+
return;
|
|
678
|
+
if (t?.closest('input, textarea, [contenteditable="true"], [role="dialog"]'))
|
|
679
|
+
return;
|
|
680
|
+
const n = Number(e.key);
|
|
681
|
+
if (!Number.isInteger(n) || n < 1 || n > options.length)
|
|
682
|
+
return;
|
|
683
|
+
e.preventDefault();
|
|
684
|
+
pickRef.current(n - 1);
|
|
685
|
+
};
|
|
686
|
+
window.addEventListener('keydown', on);
|
|
687
|
+
return () => window.removeEventListener('keydown', on);
|
|
688
|
+
}, [options.length]);
|
|
689
|
+
return (_jsxs("fieldset", { "data-slot": "chat-choices", "aria-label": "Quick replies", className: "m-0 flex min-w-0 flex-col gap-1.5 border-0 p-0", children: [options.map((o, n) => {
|
|
690
|
+
const on = picked.includes(n);
|
|
691
|
+
return (_jsxs("button", { type: "button", "aria-pressed": multi ? on : undefined, onClick: () => pick(n), className: cn('flex min-w-0 items-center gap-2.5 rounded-lg border bg-card px-2.5 py-2 text-left text-sm hover:bg-muted focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none', n === suggested && !multi && 'border-primary ring-1 ring-primary', on && 'border-primary'), children: [multi ? (_jsxs(_Fragment, { children: [_jsx("span", { className: cn('grid size-4 shrink-0 place-items-center rounded border', on && 'border-primary bg-primary text-primary-foreground'), children: on && _jsx(CheckIcon, { className: "size-3" }) }), _jsx("span", { className: "grid size-4.5 shrink-0 place-items-center rounded-full bg-muted font-medium text-muted-foreground text-xs tabular-nums", children: n + 1 })] })) : (_jsx("kbd", { className: "grid size-4.5 shrink-0 place-items-center rounded bg-muted font-medium text-xs text-muted-foreground", children: n + 1 })), _jsxs("span", { className: "min-w-0 flex-1 truncate", title: o.label, children: [o.label, (o.note || n === suggested) && (_jsx("span", { className: "ml-1.5 text-muted-foreground text-xs", children: o.note ?? 'suggested' }))] })] }, o.label));
|
|
692
|
+
}), multi && (_jsxs("div", { className: "flex items-center justify-end gap-2 text-muted-foreground text-xs", children: [_jsxs("span", { children: [picked.length, " picked"] }), _jsx(IconButton, { icon: ArrowUpIcon, label: "Send picks", variant: "default", size: "icon-sm", disabled: !picked.length, onClick: () => onSend(options
|
|
693
|
+
.filter((_, n) => picked.includes(n))
|
|
694
|
+
.map((o) => o.label)
|
|
695
|
+
.join('; ')) })] }))] }));
|
|
562
696
|
}
|
|
563
697
|
/** The composer's triggers: `@` points at a thing (the `sources`), `#` picks the tier for one message. */
|
|
564
698
|
const POINT = '@';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@columenlabs/lumen-ui",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.301",
|
|
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.301",
|
|
53
53
|
"@dnd-kit/core": "6.3.1",
|
|
54
54
|
"@dnd-kit/sortable": "10.0.0",
|
|
55
55
|
"@dnd-kit/utilities": "3.2.2",
|