@columenlabs/lumen-ui 0.1.310 → 0.1.311
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.
|
@@ -110,7 +110,7 @@ export declare const allowanceAmount: (u: AllowanceUnit, n: number) => string;
|
|
|
110
110
|
* and a chevron that shows its steps (`children`). The chat draws each task this way; an app draws
|
|
111
111
|
* its own work the same way (an edit waiting for review) so every piece of work looks alike.
|
|
112
112
|
*/
|
|
113
|
-
export declare function WorkCard({ view, live, state, actions, onStop, steps, open: shownAt, children, }: {
|
|
113
|
+
export declare function WorkCard({ view, live, state, actions, onStop, steps, open: shownAt, rows, children, }: {
|
|
114
114
|
view: AssistantTaskView;
|
|
115
115
|
live?: boolean;
|
|
116
116
|
/** The task's own state, for `data-state`. */
|
|
@@ -121,6 +121,8 @@ export declare function WorkCard({ view, live, state, actions, onStop, steps, op
|
|
|
121
121
|
steps?: number;
|
|
122
122
|
/** Starts open; by default it is open while the work runs. */
|
|
123
123
|
open?: boolean;
|
|
124
|
+
/** `children` are rows with their own padding and lines (a task's steps), drawn edge to edge. */
|
|
125
|
+
rows?: boolean;
|
|
124
126
|
children?: ReactNode;
|
|
125
127
|
}): import("react").JSX.Element;
|
|
126
128
|
/**
|
|
@@ -153,7 +155,13 @@ export declare function readAssistantImage(file: File): Promise<AssistantImage |
|
|
|
153
155
|
* triggers list what can follow them as it is typed: `/` at the start a command, `@` a thing to
|
|
154
156
|
* point at (the `sources`), `#` a tier for this message; a picked tier or thing shows as a chip.
|
|
155
157
|
*/
|
|
156
|
-
export declare function LumenComposer({ name, busy, queued, queue, canSteer, onSteerQueued, onUnqueue, disabled, commands, tiers, sources, page, images: canAttach, modes, onSend, onStop, meter, voice, }: {
|
|
158
|
+
export declare function LumenComposer({ name, busy, queued, queue, canSteer, onSteerQueued, onUnqueue, disabled, commands, tiers, sources, page, images: canAttach, modes, onSend, onStop, meter, voice, queueShown, editQueued, }: {
|
|
159
|
+
/** The queue is drawn under the running task's card instead of above the box. */
|
|
160
|
+
queueShown?: boolean;
|
|
161
|
+
/** Gets this box's Edit, so a queued message shown elsewhere comes back here to edit. */
|
|
162
|
+
editQueued?: {
|
|
163
|
+
current: ((id: string) => void) | null;
|
|
164
|
+
};
|
|
157
165
|
/** A mic beside Send that types what the person says (`VoiceButton`); hidden where the browser can't. */
|
|
158
166
|
voice?: boolean;
|
|
159
167
|
/** Drawn in the icon row after the paperclip and the modes: `LumenAssistant` puts its context donut here. */
|
|
@@ -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, stepLineParts, stepLineText, } from '@columenlabs/lumen-ui-types/assistant';
|
|
8
8
|
import { useQuery, useQueryClient } from '@tanstack/react-query';
|
|
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';
|
|
9
|
+
import { ArrowUpIcon, CheckCheckIcon, CheckIcon, ChevronDownIcon, ChevronRightIcon, ChevronUpIcon, CircleAlertIcon, CircleCheckIcon, CircleHelpIcon, CircleXIcon, CopyIcon, CornerDownRightIcon, CrosshairIcon, FileTextIcon, GlobeIcon, HandIcon, LightbulbIcon, ListPlusIcon, LoaderCircleIcon, MessageSquareTextIcon, 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';
|
|
@@ -52,6 +52,10 @@ const seconds = (ms) => !ms ? '' : ms < 1000 ? `${ms} ms` : `${(ms / 1000).toFix
|
|
|
52
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
|
+
// While the chat ends in an open task, its waiting messages sit under its card, not above the box.
|
|
56
|
+
const tail = chat.items[chat.items.length - 1];
|
|
57
|
+
const queueUnder = tail?.kind === 'task' && (taskState?.(tail) ?? tail.state) === 'open' && chat.queue.length > 0;
|
|
58
|
+
const editQueued = useRef(null);
|
|
55
59
|
const qc = useQueryClient();
|
|
56
60
|
const status = useAssistantStatus(client);
|
|
57
61
|
const [allScopes, setAllScopes] = useAllScopes(client.key);
|
|
@@ -195,7 +199,15 @@ export function LumenAssistant({ client, context, onSettings, keepDays = 30, cha
|
|
|
195
199
|
title: c.title,
|
|
196
200
|
updated_at: new Date(c.at).toISOString(),
|
|
197
201
|
...(chatScope && allScopes && c.scope ? { where: chatScope.name?.(c.scope) } : {}),
|
|
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(
|
|
202
|
+
})) ?? (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(TaskAnswer.Provider, { value: s?.wait ? null : (text) => chat.send(text, extra()), children: _jsx(TaskQueue.Provider, { value: queueUnder
|
|
203
|
+
? {
|
|
204
|
+
queue: chat.queue,
|
|
205
|
+
canSteer: chat.busy && chat.canSteer,
|
|
206
|
+
onSteer: chat.steerQueued,
|
|
207
|
+
onEdit: (id) => editQueued.current?.(id),
|
|
208
|
+
onRemove: (id) => void chat.unqueue(id),
|
|
209
|
+
}
|
|
210
|
+
: null, 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, queueShown: queueUnder, editQueued: editQueued, 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 }) })] }));
|
|
199
211
|
}
|
|
200
212
|
/** How near the end of the chat (px) still counts as at the end, so it keeps following. */
|
|
201
213
|
const FOLLOW_PX = 48;
|
|
@@ -239,7 +251,8 @@ function Messages({ chat, status, onSettings, onRetry, onFresh, onAnswer, }) {
|
|
|
239
251
|
: '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 &&
|
|
240
252
|
!status?.wait &&
|
|
241
253
|
(() => {
|
|
242
|
-
|
|
254
|
+
// A task's question shows its answers in its card (its Follow-up step).
|
|
255
|
+
const c = chat.items[chat.items.length - 1]?.kind === 'task' ? null : lastChoices(chat.items);
|
|
243
256
|
return c ? (_jsx("div", { className: "mt-2", children: _jsx(QuickReplies, { choices: c, onSend: onAnswer }, chat.items.length) })) : null;
|
|
244
257
|
})(), 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 })] }));
|
|
245
258
|
}
|
|
@@ -571,6 +584,10 @@ function HandOff({ item, onSettle, busy, }) {
|
|
|
571
584
|
}
|
|
572
585
|
/** The app's parts of a task's card (`taskActions`, `taskState`, `taskView`) and the chat's Stop. */
|
|
573
586
|
const TaskSlots = createContext({});
|
|
587
|
+
/** Sends a quick reply or an answer in the person's words (null while the chat cannot take one). */
|
|
588
|
+
const TaskAnswer = createContext(null);
|
|
589
|
+
/** Messages waiting for the running task, drawn under its card: Steer now, Edit, Remove. */
|
|
590
|
+
const TaskQueue = createContext(null);
|
|
574
591
|
/** The words a card says without the app's own view. */
|
|
575
592
|
const TASK_STATE = {
|
|
576
593
|
open: 'Open',
|
|
@@ -625,22 +642,138 @@ function cardAt(items) {
|
|
|
625
642
|
* and a chevron that shows its steps (`children`). The chat draws each task this way; an app draws
|
|
626
643
|
* its own work the same way (an edit waiting for review) so every piece of work looks alike.
|
|
627
644
|
*/
|
|
628
|
-
export function WorkCard({ view, live = false, state, actions, onStop, steps = 0, open: shownAt, children, }) {
|
|
645
|
+
export function WorkCard({ view, live = false, state, actions, onStop, steps = 0, open: shownAt, rows = false, children, }) {
|
|
629
646
|
const tone = TONE[view.tone];
|
|
630
647
|
const [open, setOpen] = useState(null);
|
|
631
648
|
const shown = (open ?? (shownAt || live)) && steps > 0;
|
|
632
649
|
const Icon = tone.icon;
|
|
633
|
-
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-
|
|
650
|
+
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-clip 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", { "data-slot": "chat-task-head", className: "sticky -top-3 z-10 flex min-w-0 items-center gap-2.5 rounded-t-lg bg-card 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: cn('border-t', !rows && 'px-3 py-2'), children: children })] }));
|
|
651
|
+
}
|
|
652
|
+
/** A line of the assistant's words as a row's title: its first sentence, without Markdown marks. */
|
|
653
|
+
function firstLine(text) {
|
|
654
|
+
const plain = text
|
|
655
|
+
.replace(/```[\s\S]*?```/g, ' ')
|
|
656
|
+
.replace(/[*_`#>[\]]|\(https?:[^)]*\)/g, '')
|
|
657
|
+
.replace(/\s+/g, ' ')
|
|
658
|
+
.trim();
|
|
659
|
+
const m = plain.match(/^(.{12,}?[.!?:])(\s|$)/);
|
|
660
|
+
return m?.[1] ?? plain;
|
|
661
|
+
}
|
|
662
|
+
/** A step that only looked something up: several in a row fold into one line. */
|
|
663
|
+
const lookup = (i) => i.kind === 'step' && i.ok !== false && !i.write && !i.choices && !i.live && !i.image;
|
|
664
|
+
/**
|
|
665
|
+
* A task's items as the rows of its card. Each turn's words are a step of their own: before its
|
|
666
|
+
* tools they are its Plan, after them its Result, and the last words of a turn that asks are the
|
|
667
|
+
* Follow-up, holding the question's answers. Lookups in a row fold into one line; the person's
|
|
668
|
+
* later messages (answers, steering) are rows too.
|
|
669
|
+
*/
|
|
670
|
+
function taskRows(items, answerable) {
|
|
671
|
+
const rows = [];
|
|
672
|
+
let looked = [];
|
|
673
|
+
let turnHasTools = false;
|
|
674
|
+
let turnStart = 0;
|
|
675
|
+
const flush = () => {
|
|
676
|
+
if (looked.length === 1)
|
|
677
|
+
rows.push({ key: `l${rows.length}`, kind: 'line', item: looked[0] });
|
|
678
|
+
else if (looked.length)
|
|
679
|
+
rows.push({ key: `k${rows.length}`, kind: 'lookups', steps: looked });
|
|
680
|
+
looked = [];
|
|
681
|
+
};
|
|
682
|
+
items.forEach((it, n) => {
|
|
683
|
+
if (it.kind === 'user') {
|
|
684
|
+
flush();
|
|
685
|
+
turnHasTools = false;
|
|
686
|
+
turnStart = rows.length + 1;
|
|
687
|
+
rows.push({ key: `u${n}`, kind: 'you', text: it.text, steered: !!it.steered });
|
|
688
|
+
return;
|
|
689
|
+
}
|
|
690
|
+
if (it.kind === 'step' && it.choices)
|
|
691
|
+
return;
|
|
692
|
+
if (lookup(it)) {
|
|
693
|
+
looked.push(it);
|
|
694
|
+
turnHasTools = true;
|
|
695
|
+
return;
|
|
696
|
+
}
|
|
697
|
+
flush();
|
|
698
|
+
if (it.kind === 'text') {
|
|
699
|
+
if (!it.text.trim())
|
|
700
|
+
return;
|
|
701
|
+
const later = items.slice(n + 1);
|
|
702
|
+
const toolsAfter = later.findIndex((x) => x.kind === 'user');
|
|
703
|
+
const rest = toolsAfter < 0 ? later : later.slice(0, toolsAfter);
|
|
704
|
+
const tag = rest.some((x) => x.kind === 'step' && !x.choices)
|
|
705
|
+
? 'Plan'
|
|
706
|
+
: turnHasTools || rows.slice(turnStart).some((r) => r.kind !== 'words')
|
|
707
|
+
? 'Result'
|
|
708
|
+
: 'Reply';
|
|
709
|
+
rows.push({ key: `t${n}`, kind: 'words', tag, text: it.text });
|
|
710
|
+
return;
|
|
711
|
+
}
|
|
712
|
+
if (it.kind === 'step' || it.kind === 'draft')
|
|
713
|
+
turnHasTools = true;
|
|
714
|
+
rows.push({ key: `i${n}`, kind: 'line', item: it });
|
|
715
|
+
});
|
|
716
|
+
flush();
|
|
717
|
+
// The last words of the chat, while its question waits: the Follow-up, with its answers.
|
|
718
|
+
if (answerable) {
|
|
719
|
+
const last = rows.findLastIndex((r) => r.kind === 'words');
|
|
720
|
+
const r = rows[last];
|
|
721
|
+
if (r?.kind === 'words' && last >= rows.findLastIndex((x) => x.kind === 'you'))
|
|
722
|
+
rows[last] = { ...r, tag: 'Follow-up', choices: answerable };
|
|
723
|
+
}
|
|
724
|
+
return rows;
|
|
725
|
+
}
|
|
726
|
+
const ROW_ICON = {
|
|
727
|
+
line: WrenchIcon,
|
|
728
|
+
lookups: SearchIcon,
|
|
729
|
+
words: LightbulbIcon,
|
|
730
|
+
you: CornerDownRightIcon,
|
|
731
|
+
Plan: LightbulbIcon,
|
|
732
|
+
Result: CircleCheckIcon,
|
|
733
|
+
'Follow-up': CircleHelpIcon,
|
|
734
|
+
Reply: MessageSquareTextIcon,
|
|
735
|
+
};
|
|
736
|
+
/** The person's answer in their own words, under a Follow-up's quick replies. */
|
|
737
|
+
function OwnAnswer({ onSend }) {
|
|
738
|
+
const [text, setText] = useState('');
|
|
739
|
+
const send = () => {
|
|
740
|
+
if (!text.trim())
|
|
741
|
+
return;
|
|
742
|
+
onSend(text.trim());
|
|
743
|
+
setText('');
|
|
744
|
+
};
|
|
745
|
+
return (_jsxs("form", { className: "flex min-w-0 items-center gap-1 rounded-lg border bg-card py-1 pr-1 pl-2.5", onSubmit: (e) => {
|
|
746
|
+
e.preventDefault();
|
|
747
|
+
send();
|
|
748
|
+
}, children: [_jsx("input", { value: text, onChange: (e) => setText(e.target.value), placeholder: "Something else\u2026", "aria-label": "Answer in your own words", className: "min-w-0 flex-1 bg-transparent text-sm outline-none placeholder:text-muted-foreground" }), _jsx(IconButton, { icon: ArrowUpIcon, label: "Send", size: "icon-xs", type: "submit", disabled: !text.trim() })] }));
|
|
749
|
+
}
|
|
750
|
+
/** One row of a task's card: a line that opens to its whole text (one open at a time). */
|
|
751
|
+
function TaskRowView({ row, open, onToggle, onSettle, busy, }) {
|
|
752
|
+
const answer = useContext(TaskAnswer);
|
|
753
|
+
if (row.kind === 'line')
|
|
754
|
+
return (_jsx("div", { className: "border-t px-3 py-1.5 first:border-t-0", children: _jsx(Item, { item: row.item, onSettle: onSettle, busy: busy }) }));
|
|
755
|
+
const tag = row.kind === 'words' ? row.tag : row.kind === 'you' ? (row.steered ? 'Steer' : 'You') : null;
|
|
756
|
+
const Icon = ROW_ICON[row.kind === 'words' ? row.tag : row.kind];
|
|
757
|
+
const now = row.kind === 'words' && row.tag === 'Follow-up';
|
|
758
|
+
const title = row.kind === 'lookups'
|
|
759
|
+
? row.steps.map((x) => x.title).join(' · ')
|
|
760
|
+
: row.kind === 'words'
|
|
761
|
+
? firstLine(row.text)
|
|
762
|
+
: row.text;
|
|
763
|
+
return (_jsxs("div", { "data-slot": "chat-task-row", "data-open": open || undefined, className: "border-t first:border-t-0", children: [_jsxs("button", { type: "button", "aria-expanded": open, onClick: onToggle, className: cn('flex w-full min-w-0 items-center gap-2 px-3 py-1.5 text-left text-sm hover:bg-muted/50 focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none', now && 'bg-primary/5'), children: [_jsx(Icon, { className: cn('size-3.5 shrink-0', now ? 'text-primary' : 'text-muted-foreground'), "aria-hidden": true }), tag && (_jsx("span", { className: cn('shrink-0 text-xs uppercase tracking-wide', now ? 'text-primary' : 'text-muted-foreground'), children: tag })), _jsx("span", { className: cn('min-w-0 flex-1 truncate', !open && !now && 'text-muted-foreground'), title: title, children: title }), row.kind === 'lookups' && (_jsx("span", { className: "shrink-0 text-muted-foreground text-xs tabular-nums", children: row.steps.length })), open ? (_jsx(ChevronDownIcon, { className: "size-3.5 shrink-0 text-muted-foreground", "aria-hidden": true })) : (_jsx(ChevronRightIcon, { className: "size-3.5 shrink-0 text-muted-foreground", "aria-hidden": true }))] }), open && (_jsxs("div", { className: "flex min-w-0 flex-col gap-2 px-3 pt-0.5 pb-2.5", children: [row.kind === 'lookups' ? (_jsx(Items, { items: row.steps, onSettle: onSettle, busy: busy })) : row.kind === 'words' ? (_jsx(Item, { item: { kind: 'text', text: row.text }, onSettle: onSettle, busy: busy })) : (_jsx("p", { className: "text-sm whitespace-pre-wrap [overflow-wrap:anywhere]", children: row.text })), row.kind === 'words' && row.choices && answer && (_jsxs(_Fragment, { children: [_jsx(QuickReplies, { choices: row.choices, onSend: answer }, row.key), _jsx(OwnAnswer, { onSend: answer })] }))] }))] }));
|
|
634
764
|
}
|
|
635
765
|
/**
|
|
636
766
|
* A task's turns in the chat (`TaskItem`) as one card: a tinted icon for where it stands, its title
|
|
637
767
|
* and a line under it (the app's `taskView`), the app's buttons (`taskActions`), Stop while it runs
|
|
638
|
-
* and a chevron that
|
|
639
|
-
*
|
|
640
|
-
*
|
|
768
|
+
* and a chevron that folds its steps. The person's first message sits above it; everything after,
|
|
769
|
+
* the assistant's words included, is a step inside it (`taskRows`). Only the newest step is open:
|
|
770
|
+
* when the next one starts the one before folds, and any step opens again with a tap. Messages
|
|
771
|
+
* queued while it runs wait under it (`TaskQueue`).
|
|
641
772
|
*/
|
|
642
773
|
function TaskBlock({ item, onSettle, busy, }) {
|
|
643
774
|
const slots = useContext(TaskSlots);
|
|
775
|
+
const answer = useContext(TaskAnswer);
|
|
776
|
+
const queue = useContext(TaskQueue);
|
|
644
777
|
const state = slots.state?.(item) ?? item.state;
|
|
645
778
|
const items = item.items;
|
|
646
779
|
const live = items.some(isLive);
|
|
@@ -649,11 +782,19 @@ function TaskBlock({ item, onSettle, busy, }) {
|
|
|
649
782
|
title: item.label,
|
|
650
783
|
state: live ? 'Working' : TASK_STATE[state],
|
|
651
784
|
};
|
|
652
|
-
const
|
|
653
|
-
const
|
|
654
|
-
const
|
|
655
|
-
const
|
|
656
|
-
|
|
785
|
+
const first = items.findIndex((i) => i.kind === 'user');
|
|
786
|
+
const head = items.slice(0, first + 1);
|
|
787
|
+
const inner = items.slice(first + 1);
|
|
788
|
+
const answerable = answer && !busy ? lastChoices(items) : null;
|
|
789
|
+
const rows = taskRows(inner, answerable);
|
|
790
|
+
// The newest row is open unless the person picked another since the rows last changed.
|
|
791
|
+
const [picked, setPicked] = useState(null);
|
|
792
|
+
const newest = rows.length - 1;
|
|
793
|
+
const auto = rows[newest]?.kind === 'line' ? null : newest;
|
|
794
|
+
const open = picked && picked.at === rows.length ? picked.open : auto;
|
|
795
|
+
const failed = inner.some((x) => x.kind === 'step' && (!x.ok || x.write?.status === 'failed'));
|
|
796
|
+
const card = (_jsx(WorkCard, { view: view, live: live, state: state, actions: slots.actions?.(item, live), onStop: live ? slots.stop : undefined, steps: rows.length, open: failed || !!answerable || state === 'open', rows: true, children: rows.map((r, n) => (_jsx(TaskRowView, { row: r, open: open === n, onToggle: () => setPicked({ at: rows.length, open: open === n ? null : n }), onSettle: onSettle, busy: busy }, r.key))) }));
|
|
797
|
+
return (_jsxs("div", { "data-slot": "chat-task-turns", className: "flex min-w-0 flex-col gap-3", children: [head.length > 0 && _jsx(Items, { items: head, onSettle: onSettle, busy: busy }), card, queue && state === 'open' && queue.queue.length > 0 && (_jsx(ComposerQueue, { queue: queue.queue, canSteer: queue.canSteer, onSteer: queue.onSteer, onEdit: queue.onEdit, onRemove: queue.onRemove }))] }));
|
|
657
798
|
}
|
|
658
799
|
/** The quick replies the last turn offers (`StepItem.choices`), unless the person answered since. */
|
|
659
800
|
function lastChoices(items) {
|
|
@@ -767,7 +908,7 @@ const imageUrl = (i) => `data:${i.media_type};base64,${i.data}`;
|
|
|
767
908
|
* triggers list what can follow them as it is typed: `/` at the start a command, `@` a thing to
|
|
768
909
|
* point at (the `sources`), `#` a tier for this message; a picked tier or thing shows as a chip.
|
|
769
910
|
*/
|
|
770
|
-
export function LumenComposer({ name, busy, queued, queue, canSteer = false, onSteerQueued, onUnqueue, disabled, commands = [], tiers = [], sources = [], page, images: canAttach = false, modes = [], onSend, onStop, meter, voice = true, }) {
|
|
911
|
+
export function LumenComposer({ name, busy, queued, queue, canSteer = false, onSteerQueued, onUnqueue, disabled, commands = [], tiers = [], sources = [], page, images: canAttach = false, modes = [], onSend, onStop, meter, voice = true, queueShown = false, editQueued, }) {
|
|
771
912
|
const [text, setText] = useState('');
|
|
772
913
|
const [caret, setCaret] = useState(0);
|
|
773
914
|
const [closed, setClosed] = useState(null);
|
|
@@ -905,7 +1046,9 @@ export function LumenComposer({ name, busy, queued, queue, canSteer = false, onS
|
|
|
905
1046
|
setUse(q.extra.use);
|
|
906
1047
|
requestAnimationFrame(() => box.current?.focus());
|
|
907
1048
|
};
|
|
908
|
-
|
|
1049
|
+
if (editQueued)
|
|
1050
|
+
editQueued.current = onUnqueue ? edit : null;
|
|
1051
|
+
const waiting = queueShown ? [] : (queue ?? (queued ? [{ id: 'queued', message: queued }] : []));
|
|
909
1052
|
const pickCommand = (o) => {
|
|
910
1053
|
const c = commands.find((x) => x.name === o.value);
|
|
911
1054
|
if (!c)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@columenlabs/lumen-ui",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.311",
|
|
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.311",
|
|
53
53
|
"@dnd-kit/core": "6.3.1",
|
|
54
54
|
"@dnd-kit/sortable": "10.0.0",
|
|
55
55
|
"@dnd-kit/utilities": "3.2.2",
|