@columenlabs/lumen-ui 0.1.225 → 0.1.226
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 +7 -1
- package/dist/assistant/index.d.ts +2 -1
- package/dist/assistant/index.js +2 -1
- package/dist/assistant/lumen-assistant.d.ts +7 -1
- package/dist/assistant/lumen-assistant.js +30 -9
- package/dist/assistant/reply-selection.d.ts +8 -0
- package/dist/assistant/reply-selection.js +79 -0
- package/dist/assistant/sources.d.ts +3 -1
- package/dist/assistant/use-assistant-chat.js +2 -0
- package/dist/whats-new.d.ts +6 -1
- package/dist/whats-new.js +7 -5
- package/package.json +2 -2
package/dist/assistant/copy.js
CHANGED
|
@@ -36,7 +36,13 @@ function itemMarkdown(i, depth, name) {
|
|
|
36
36
|
const refs = i.refs?.length
|
|
37
37
|
? `\n\nPointing at: ${i.refs.map((r) => r.label).join(', ')}`
|
|
38
38
|
: '';
|
|
39
|
-
|
|
39
|
+
const quote = i.quote
|
|
40
|
+
? `${i.quote.text
|
|
41
|
+
.split('\n')
|
|
42
|
+
.map((l) => `> ${l}`)
|
|
43
|
+
.join('\n')}\n\n`
|
|
44
|
+
: '';
|
|
45
|
+
return `${h} You\n\n${quote}${i.text}${refs}`;
|
|
40
46
|
}
|
|
41
47
|
case 'text':
|
|
42
48
|
return `${h} ${name}\n\n${i.text}`;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export { type AssistantAdapter, assistantClient } from './client.js';
|
|
2
|
-
export {
|
|
2
|
+
export { ChatImage } from './chat-image.js';
|
|
3
|
+
export { type AssistantCommand, type ComposerExtra, LumenAssistant, type LumenAssistantProps, LumenComposer, assistantStatusKey, readAssistantImage, useAssistantStatus, useLumenAssistantDock, } from './lumen-assistant.js';
|
|
3
4
|
export { assistantAdminSections, assistantSettingsSections } from './settings.js';
|
|
4
5
|
export { UsageHistoryView, assistantUsageKey, tokens as formatTokens, usd as formatUsd, useAssistantUsage, } from './usage-history.js';
|
|
5
6
|
export { type AssistantSource, type AssistantSourceItem, type ComposeRequest, type PageSelection, composeAssistant, selectionRef, useAssistantSource, usePageSelection, useShownAssistantSources, } from './sources.js';
|
package/dist/assistant/index.js
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
// routes of `@columenlabs/lumen-ai/assistant`; the shapes both share are
|
|
3
3
|
// `@columenlabs/lumen-ui-types/assistant`.
|
|
4
4
|
export { assistantClient } from './client.js';
|
|
5
|
-
export {
|
|
5
|
+
export { ChatImage } from './chat-image.js';
|
|
6
|
+
export { LumenAssistant, LumenComposer, assistantStatusKey, readAssistantImage, useAssistantStatus, useLumenAssistantDock, } from './lumen-assistant.js';
|
|
6
7
|
export { assistantAdminSections, assistantSettingsSections } from './settings.js';
|
|
7
8
|
export { UsageHistoryView, assistantUsageKey, tokens as formatTokens, usd as formatUsd, useAssistantUsage, } from './usage-history.js';
|
|
8
9
|
export { composeAssistant, selectionRef, useAssistantSource, usePageSelection, useShownAssistantSources, } from './sources.js';
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type AllowanceUnit, type AssistantImage, type AssistantMessage, type AssistantRefNote, type AssistantStatus } from '@columenlabs/lumen-ui-types/assistant';
|
|
1
|
+
import { type AllowanceUnit, type AssistantImage, type AssistantMessage, type AssistantQuote, type AssistantRefNote, type AssistantStatus } from '@columenlabs/lumen-ui-types/assistant';
|
|
2
2
|
import { type LumenOption } from '../lumen-selector.js';
|
|
3
3
|
import type { AssistantAdapter } from './client.js';
|
|
4
4
|
import { type AssistantSource } from './sources.js';
|
|
@@ -66,7 +66,13 @@ export interface ComposerExtra {
|
|
|
66
66
|
tier?: string;
|
|
67
67
|
refs?: AssistantRefNote[];
|
|
68
68
|
images?: AssistantImage[];
|
|
69
|
+
quote?: AssistantQuote;
|
|
69
70
|
}
|
|
71
|
+
/**
|
|
72
|
+
* A pasted, dropped or picked image, ready to send: scaled to `IMAGE_EDGE` on a canvas and saved as JPEG
|
|
73
|
+
* (PNG when it has few pixels to lose), as base64. `null` when the browser cannot read it.
|
|
74
|
+
*/
|
|
75
|
+
export declare function readAssistantImage(file: File): Promise<AssistantImage | null>;
|
|
70
76
|
/**
|
|
71
77
|
* The message box: Enter sends, Shift+Enter breaks a line; Stop while an answer runs. While it
|
|
72
78
|
* runs, words in the box go into it with Enter (Steer, where `canSteer`) or wait for it to end
|
|
@@ -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, } 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, PencilIcon, PlusIcon, ReceiptIcon, RefreshCwIcon, SearchIcon, SparklesIcon, SquareIcon, TriangleAlertIcon, Undo2Icon, WrenchIcon, XIcon, ZapIcon, } from 'lucide-react';
|
|
9
|
+
import { ArrowUpIcon, CheckIcon, ChevronRightIcon, CircleAlertIcon, CopyIcon, CornerDownRightIcon, CrosshairIcon, FileTextIcon, GlobeIcon, LightbulbIcon, ListPlusIcon, MonitorIcon, PaperclipIcon, PencilIcon, PlusIcon, ReceiptIcon, RefreshCwIcon, ReplyIcon, SearchIcon, SparklesIcon, SquareIcon, TriangleAlertIcon, Undo2Icon, WrenchIcon, XIcon, ZapIcon, } 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';
|
|
@@ -28,6 +28,7 @@ import { Spinner } from '../ui/spinner.js';
|
|
|
28
28
|
import { Textarea } from '../ui/textarea.js';
|
|
29
29
|
import { ChatImage } from './chat-image.js';
|
|
30
30
|
import { chatMarkdown, chatTurns, copyMarkdown, turnMarkdown } from './copy.js';
|
|
31
|
+
import { ReplyToSelection } from './reply-selection.js';
|
|
31
32
|
import { composeAssistant, selectionRef, useComposeRequest, usePageSelection, useShownAssistantSources, } from './sources.js';
|
|
32
33
|
import { UsageHistoryView, tokens, usd } from './usage-history.js';
|
|
33
34
|
import { useAssistantChat, } from './use-assistant-chat.js';
|
|
@@ -194,6 +195,7 @@ export function LumenAssistant({ client, context, onSettings, keepDays = 30, cha
|
|
|
194
195
|
}
|
|
195
196
|
function Messages({ chat, status, onSettings, onRetry, }) {
|
|
196
197
|
const end = useRef(null);
|
|
198
|
+
const box = useRef(null);
|
|
197
199
|
const count = JSON.stringify(chat.items).length;
|
|
198
200
|
// biome-ignore lint/correctness/useExhaustiveDependencies: scroll to the end as the chat grows
|
|
199
201
|
useEffect(() => {
|
|
@@ -202,7 +204,7 @@ function Messages({ chat, status, onSettings, onRetry, }) {
|
|
|
202
204
|
// The app's own link to Settings, else the shell's, at the section that answers the wait.
|
|
203
205
|
const shellSettings = useOpenSettings();
|
|
204
206
|
const settings = onSettings ? (_section) => onSettings() : shellSettings;
|
|
205
|
-
return (_jsxs("div", { className: "min-h-0 flex-1 overflow-y-auto px-3 py-3", children: [status?.wait && (_jsxs(Alert, { className: "mb-3", children: [_jsx(CircleAlertIcon, {}), _jsxs(AlertDescription, { className: "flex flex-col items-start gap-2", children: [status.wait === 'app_cap' && status.allowance
|
|
207
|
+
return (_jsxs("div", { ref: box, className: "relative min-h-0 flex-1 overflow-y-auto px-3 py-3", children: [_jsx(ReplyToSelection, { box: box }), status?.wait && (_jsxs(Alert, { className: "mb-3", children: [_jsx(CircleAlertIcon, {}), _jsxs(AlertDescription, { className: "flex flex-col items-start gap-2", children: [status.wait === 'app_cap' && status.allowance
|
|
206
208
|
? allowanceWait(status.allowance)
|
|
207
209
|
: WAIT[status.wait](status.admin, status.providers.find((p) => p.id === status.wait_provider)?.label ?? 'API'), settings && (_jsx(Button, { size: "sm", variant: "outline", onClick: () => settings(status.wait === 'app_cap' && status.allowance
|
|
208
210
|
? 'assistant-allowance'
|
|
@@ -281,13 +283,13 @@ function Item({ item, onSettle, busy, }) {
|
|
|
281
283
|
case 'user': {
|
|
282
284
|
if (item.steered)
|
|
283
285
|
return (_jsxs("div", { className: "ml-8 flex flex-col items-end gap-0.5 self-end", children: [_jsxs("span", { className: "flex items-center gap-1 text-primary text-xs", children: [_jsx(CornerDownRightIcon, { className: "size-3" }), "Steered"] }), _jsx("div", { className: "rounded-lg border border-primary/40 bg-muted px-3 py-2 text-sm whitespace-pre-wrap break-words", children: item.text })] }));
|
|
284
|
-
const bubble = (_jsxs("div", { className: "ml-8 self-end rounded-lg bg-muted px-3 py-2 text-sm whitespace-pre-wrap has-[[data-slot=chat-image]]:ml-0 has-[[data-slot=chat-image]]:w-full", children: [(item.tier || item.refs?.length) && (_jsxs("span", { className: "mb-1 flex flex-wrap gap-1", children: [item.tier && _jsx(ComposerChip, { sign: TIER, label: tierName(item.tier) }), item.refs?.map((r, i) => (_jsx(ComposerChip, { sign: POINT, label: r.label }, `${r.kind}:${r.label}:${i}`)))] })), item.images?.length ? (_jsx("span", { className: "mb-1 flex flex-wrap gap-1.5", children: item.images.map((src, i) => (_jsx(ChatImage, { src: src, alt: `Pasted ${i + 1}` }, `${i}:${src.length}`))) })) : null, item.text, item.context && (_jsxs("span", { className: "mt-1 block text-muted-foreground text-xs", children: ["on ", item.context] }))] }));
|
|
286
|
+
const bubble = (_jsxs("div", { "data-chat-from": "user", className: "ml-8 self-end rounded-lg bg-muted px-3 py-2 text-sm whitespace-pre-wrap has-[[data-slot=chat-image]]:ml-0 has-[[data-slot=chat-image]]:w-full", children: [item.quote && _jsx(QuoteLine, { quote: item.quote }), (item.tier || item.refs?.length) && (_jsxs("span", { className: "mb-1 flex flex-wrap gap-1", children: [item.tier && _jsx(ComposerChip, { sign: TIER, label: tierName(item.tier) }), item.refs?.map((r, i) => (_jsx(ComposerChip, { sign: POINT, label: r.label }, `${r.kind}:${r.label}:${i}`)))] })), item.images?.length ? (_jsx("span", { className: "mb-1 flex flex-wrap gap-1.5", children: item.images.map((src, i) => (_jsx(ChatImage, { src: src, alt: `Pasted ${i + 1}` }, `${i}:${src.length}`))) })) : null, item.text, item.context && (_jsxs("span", { className: "mt-1 block text-muted-foreground text-xs", children: ["on ", item.context] }))] }));
|
|
285
287
|
if (!item.retried)
|
|
286
288
|
return bubble;
|
|
287
289
|
return (_jsxs("div", { className: "flex flex-col items-end gap-0.5 self-end", children: [_jsxs("span", { className: "flex items-center gap-1 text-primary text-xs", children: [_jsx(RefreshCwIcon, { className: "size-3" }), "Retry"] }), bubble] }));
|
|
288
290
|
}
|
|
289
291
|
case 'text':
|
|
290
|
-
return (_jsxs("div", { className: "prose-sm text-sm leading-relaxed [&_code]:font-code [&_code]:text-xs [&_li]:ml-4 [&_ol]:list-decimal [&_p]:my-1.5 [&_ul]:list-disc", children: [_jsx(ChatMarkdown, { children: item.text }), item.live && (_jsx("span", { className: "ml-0.5 inline-block h-3.5 w-1.5 animate-pulse bg-foreground/60 align-middle" }))] }));
|
|
292
|
+
return (_jsxs("div", { "data-chat-from": "assistant", className: "prose-sm text-sm leading-relaxed [&_code]:font-code [&_code]:text-xs [&_li]:ml-4 [&_ol]:list-decimal [&_p]:my-1.5 [&_ul]:list-disc", children: [_jsx(ChatMarkdown, { children: item.text }), item.live && (_jsx("span", { className: "ml-0.5 inline-block h-3.5 w-1.5 animate-pulse bg-foreground/60 align-middle" }))] }));
|
|
291
293
|
case 'step':
|
|
292
294
|
return _jsx(Step, { step: item, live: !!item.live });
|
|
293
295
|
case 'draft':
|
|
@@ -512,13 +514,20 @@ const TIER = '#';
|
|
|
512
514
|
function ComposerChip({ sign, label, onRemove, }) {
|
|
513
515
|
return (_jsxs("span", { className: "inline-flex max-w-full items-center gap-1 rounded-md border bg-background px-1.5 py-0.5 text-xs", children: [_jsx("span", { className: "text-muted-foreground", children: sign }), _jsx("span", { className: "truncate", children: label }), onRemove && (_jsx("button", { type: "button", "aria-label": `Remove ${label}`, className: "text-muted-foreground hover:text-foreground", onClick: onRemove, children: _jsx(XIcon, { className: "size-3" }) }))] }));
|
|
514
516
|
}
|
|
517
|
+
/**
|
|
518
|
+
* A quote a message replies to: the reply icon and the quoted words. Over the box it is one line
|
|
519
|
+
* with an × (`onRemove`); in a sent message up to two lines. The whole text is in its hover.
|
|
520
|
+
*/
|
|
521
|
+
function QuoteLine({ quote, onRemove }) {
|
|
522
|
+
return (_jsxs("span", { "data-slot": "chat-quote", className: cn('flex min-w-0 items-start gap-1.5 rounded-md bg-background/60 px-2 py-1 text-muted-foreground text-xs whitespace-normal', onRemove ? 'mb-2 items-center bg-muted py-0.5 pr-0.5' : 'mb-1.5'), title: quote.text, children: [_jsx(ReplyIcon, { className: cn('size-3.5 shrink-0', !onRemove && 'mt-0.5'), "aria-hidden": true }), _jsx("span", { className: cn('min-w-0 flex-1', onRemove ? 'truncate' : 'line-clamp-2 break-words'), children: quote.text }), onRemove && (_jsx(IconButton, { icon: XIcon, label: "Remove quote", size: "icon-xs", onClick: onRemove }))] }));
|
|
523
|
+
}
|
|
515
524
|
/** The longest edge an image is sent at (what the models read best; larger only costs more). */
|
|
516
525
|
const IMAGE_EDGE = 1568;
|
|
517
526
|
/**
|
|
518
|
-
* A pasted or
|
|
527
|
+
* A pasted, dropped or picked image, ready to send: scaled to `IMAGE_EDGE` on a canvas and saved as JPEG
|
|
519
528
|
* (PNG when it has few pixels to lose), as base64. `null` when the browser cannot read it.
|
|
520
529
|
*/
|
|
521
|
-
async function
|
|
530
|
+
export async function readAssistantImage(file) {
|
|
522
531
|
try {
|
|
523
532
|
const bmp = await createImageBitmap(file);
|
|
524
533
|
const k = Math.min(1, IMAGE_EDGE / Math.max(bmp.width, bmp.height));
|
|
@@ -558,8 +567,10 @@ export function LumenComposer({ name, busy, queued, queue, canSteer = false, onS
|
|
|
558
567
|
const [tier, setTier] = useState(null);
|
|
559
568
|
const [refs, setRefs] = useState([]);
|
|
560
569
|
const [images, setImages] = useState([]);
|
|
570
|
+
const [quote, setQuote] = useState(null);
|
|
561
571
|
const list = useRef(null);
|
|
562
572
|
const box = useRef(null);
|
|
573
|
+
const filePick = useRef(null);
|
|
563
574
|
const selection = usePageSelection();
|
|
564
575
|
// An app's compose request (`composeAssistant`): its chips and text, ready to finish.
|
|
565
576
|
const [request, take] = useComposeRequest();
|
|
@@ -572,6 +583,8 @@ export function LumenComposer({ name, busy, queued, queue, canSteer = false, onS
|
|
|
572
583
|
setRefs((p) => [...p, ...(request.refs ?? [])]);
|
|
573
584
|
if (request.tier)
|
|
574
585
|
setTier(request.tier);
|
|
586
|
+
if (request.quote)
|
|
587
|
+
setQuote(request.quote);
|
|
575
588
|
if (request.text !== undefined) {
|
|
576
589
|
setText(request.text);
|
|
577
590
|
setCaret(request.text.length);
|
|
@@ -592,6 +605,7 @@ export function LumenComposer({ name, busy, queued, queue, canSteer = false, onS
|
|
|
592
605
|
...(tier ? { tier } : {}),
|
|
593
606
|
...(refs.length ? { refs } : {}),
|
|
594
607
|
...(images.length ? { images } : {}),
|
|
608
|
+
...(quote ? { quote } : {}),
|
|
595
609
|
});
|
|
596
610
|
const clear = () => {
|
|
597
611
|
setText('');
|
|
@@ -599,6 +613,7 @@ export function LumenComposer({ name, busy, queued, queue, canSteer = false, onS
|
|
|
599
613
|
setTier(null);
|
|
600
614
|
setRefs([]);
|
|
601
615
|
setImages([]);
|
|
616
|
+
setQuote(null);
|
|
602
617
|
};
|
|
603
618
|
/** Adds the image files among `files`; says so when one is left out. Returns whether any was an image. */
|
|
604
619
|
const attach = (files) => {
|
|
@@ -606,7 +621,7 @@ export function LumenComposer({ name, busy, queued, queue, canSteer = false, onS
|
|
|
606
621
|
if (!canAttach || !picked.length)
|
|
607
622
|
return false;
|
|
608
623
|
void (async () => {
|
|
609
|
-
const read = (await Promise.all(picked.map(
|
|
624
|
+
const read = (await Promise.all(picked.map(readAssistantImage))).filter((x) => !!x);
|
|
610
625
|
if (read.length < picked.length)
|
|
611
626
|
lumenToast.error("That image couldn't be read. Try a PNG or JPEG.");
|
|
612
627
|
setImages((p) => {
|
|
@@ -662,6 +677,8 @@ export function LumenComposer({ name, busy, queued, queue, canSteer = false, onS
|
|
|
662
677
|
setRefs(q.extra.refs);
|
|
663
678
|
if (q.extra?.images?.length)
|
|
664
679
|
setImages(q.extra.images);
|
|
680
|
+
if (q.extra?.quote)
|
|
681
|
+
setQuote(q.extra.quote);
|
|
665
682
|
requestAnimationFrame(() => box.current?.focus());
|
|
666
683
|
};
|
|
667
684
|
const waiting = queue ?? (queued ? [{ id: 'queued', message: queued }] : []);
|
|
@@ -825,7 +842,7 @@ export function LumenComposer({ name, busy, queued, queue, canSteer = false, onS
|
|
|
825
842
|
const r = sourceItems.get(o.value)?.();
|
|
826
843
|
if (r)
|
|
827
844
|
setRefs((p) => [...p, r]);
|
|
828
|
-
}, onClose: () => setClosed(`${text}|${caret}`) })), images.length > 0 && (_jsx("div", { className: "mb-2 flex flex-wrap gap-1.5", "aria-label": "Images going with your message", children: images.map((img, i) => (_jsxs("span", { className: "relative", children: [_jsx("img", { src: imageUrl(img), alt: `Pasted ${i + 1}`, className: "size-12 rounded-md border object-cover" }), _jsx("button", { type: "button", "aria-label": `Remove image ${i + 1}`, className: "absolute -top-1.5 -right-1.5 grid size-4 place-items-center rounded-full border bg-background text-muted-foreground hover:text-foreground", onClick: () => setImages((p) => p.filter((_, j) => j !== i)), children: _jsx(XIcon, { className: "size-3" }) })] }, `${i}:${img.data.length}`))) })), (tierLabel || refs.length > 0) && (_jsxs("div", { className: "mb-2 flex flex-wrap gap-1", children: [tierLabel && (_jsx(ComposerChip, { sign: TIER, label: tierLabel, onRemove: () => setTier(null) })), refs.map((r, i) => (_jsx(ComposerChip, { sign: POINT, label: r.label, onRemove: () => setRefs((p) => p.filter((_, j) => j !== i)) }, `${r.kind}:${r.label}:${i}`)))] })), _jsxs("div", { className: "relative", children: [_jsx(Textarea, { ref: box, "aria-label": `Message ${name}`, placeholder: `Ask ${name}…${hints.length ? ` (${hints.join(' ')})` : ''}`, className: cn('max-h-48 min-h-11 resize-none', busy && text.trim() && canSteer ? 'pr-18' : 'pr-11'), value: text, disabled: disabled, onChange: (e) => {
|
|
845
|
+
}, onClose: () => setClosed(`${text}|${caret}`) })), quote && _jsx(QuoteLine, { quote: quote, onRemove: () => setQuote(null) }), images.length > 0 && (_jsx("div", { className: "mb-2 flex flex-wrap gap-1.5", "aria-label": "Images going with your message", children: images.map((img, i) => (_jsxs("span", { className: "relative", children: [_jsx("img", { src: imageUrl(img), alt: `Pasted ${i + 1}`, className: "size-12 rounded-md border object-cover" }), _jsx("button", { type: "button", "aria-label": `Remove image ${i + 1}`, className: "absolute -top-1.5 -right-1.5 grid size-4 place-items-center rounded-full border bg-background text-muted-foreground hover:text-foreground", onClick: () => setImages((p) => p.filter((_, j) => j !== i)), children: _jsx(XIcon, { className: "size-3" }) })] }, `${i}:${img.data.length}`))) })), (tierLabel || refs.length > 0) && (_jsxs("div", { className: "mb-2 flex flex-wrap gap-1", children: [tierLabel && (_jsx(ComposerChip, { sign: TIER, label: tierLabel, onRemove: () => setTier(null) })), refs.map((r, i) => (_jsx(ComposerChip, { sign: POINT, label: r.label, onRemove: () => setRefs((p) => p.filter((_, j) => j !== i)) }, `${r.kind}:${r.label}:${i}`)))] })), _jsxs("div", { className: "relative", children: [_jsx(Textarea, { ref: box, "aria-label": `Message ${name}`, placeholder: `Ask ${name}…${hints.length ? ` (${hints.join(' ')})` : ''}`, className: cn('max-h-48 min-h-11 resize-none', busy && text.trim() && canSteer ? 'pr-18' : 'pr-11', canAttach && 'pb-9'), value: text, disabled: disabled, onChange: (e) => {
|
|
829
846
|
setText(e.target.value);
|
|
830
847
|
setCaret(e.target.selectionStart);
|
|
831
848
|
}, onSelect: (e) => setCaret(e.currentTarget.selectionStart), onPaste: (e) => {
|
|
@@ -845,7 +862,11 @@ export function LumenComposer({ name, busy, queued, queue, canSteer = false, onS
|
|
|
845
862
|
e.preventDefault();
|
|
846
863
|
send(e.altKey ? 'queue' : undefined);
|
|
847
864
|
}
|
|
848
|
-
} }),
|
|
865
|
+
} }), canAttach && (_jsxs("div", { className: "absolute bottom-1.5 left-1.5", children: [_jsx(IconButton, { icon: PaperclipIcon, label: "Attach images", size: "icon-xs", disabled: disabled, onClick: () => filePick.current?.click() }), _jsx("input", { ref: filePick, type: "file", accept: "image/*", multiple: true, hidden: true, onChange: (e) => {
|
|
866
|
+
if (e.target.files)
|
|
867
|
+
attach(e.target.files);
|
|
868
|
+
e.target.value = '';
|
|
869
|
+
} })] })), _jsx("div", { className: "absolute right-1.5 bottom-1.5", children: busy && !text.trim() ? (_jsx(IconButton, { icon: SquareIcon, label: "Stop", variant: "outline", size: "icon-xs", onClick: onStop })) : busy ? (_jsxs("span", { className: "flex gap-1", children: [_jsx(IconButton, { icon: ListPlusIcon, label: canSteer ? 'Queue for after (⌥↩)' : 'Queue for after (↩)', variant: canSteer ? 'outline' : 'default', size: "icon-xs", disabled: disabled, onClick: () => send('queue') }), canSteer && (_jsx(IconButton, { icon: CornerDownRightIcon, label: "Steer now (\u21A9)", variant: "default", size: "icon-xs", disabled: disabled, onClick: () => send('steer') }))] })) : (_jsx(IconButton, { icon: ArrowUpIcon, label: "Send", variant: "default", size: "icon-xs", disabled: !text.trim() || disabled, onClick: () => send() })) })] })] }));
|
|
849
870
|
}
|
|
850
871
|
/**
|
|
851
872
|
* What waits above the box while an answer runs: steering messages until the answer reads them,
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type RefObject } from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* The Reply bar over selected chat text. `box` is the chat's scrolling box, positioned; the bar
|
|
4
|
+
* sits in it, above the selection (below it when the top is out of view).
|
|
5
|
+
*/
|
|
6
|
+
export declare function ReplyToSelection({ box }: {
|
|
7
|
+
box: RefObject<HTMLElement | null>;
|
|
8
|
+
}): import("react").JSX.Element | null;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
// Reply to a piece of the chat: text selected inside one message (the person's or the assistant's)
|
|
3
|
+
// gets a small bar above it, Reply and Copy. Reply puts the selection over the composer as a quote
|
|
4
|
+
// (`composeAssistant`), which goes with the next message.
|
|
5
|
+
import { ASSISTANT_QUOTE_MAX } from '@columenlabs/lumen-ui-types/assistant';
|
|
6
|
+
import { CopyIcon, ReplyIcon } from 'lucide-react';
|
|
7
|
+
import { useEffect, useState } from 'react';
|
|
8
|
+
import { IconButton } from '../icon-button.js';
|
|
9
|
+
import { lumenToast } from '../lumen-toast.js';
|
|
10
|
+
import { composeAssistant } from './sources.js';
|
|
11
|
+
/** The bar's size, to keep it inside the chat's box. */
|
|
12
|
+
const BAR_W = 72;
|
|
13
|
+
const BAR_H = 40;
|
|
14
|
+
/** The message (`data-chat-from`) a node sits in, when it is inside `box`. */
|
|
15
|
+
function messageOf(node, box) {
|
|
16
|
+
const el = node instanceof Element ? node : node?.parentElement;
|
|
17
|
+
const m = el?.closest('[data-chat-from]') ?? null;
|
|
18
|
+
return m && box.contains(m) ? m : null;
|
|
19
|
+
}
|
|
20
|
+
/** The selection as a quote and where its bar goes in `box`, or null when it is not one message's text. */
|
|
21
|
+
function pickedIn(box) {
|
|
22
|
+
const sel = document.getSelection();
|
|
23
|
+
if (!sel || sel.isCollapsed || !sel.rangeCount)
|
|
24
|
+
return null;
|
|
25
|
+
const from = messageOf(sel.anchorNode, box);
|
|
26
|
+
if (!from || from !== messageOf(sel.focusNode, box))
|
|
27
|
+
return null;
|
|
28
|
+
const text = sel.toString().trim().slice(0, ASSISTANT_QUOTE_MAX);
|
|
29
|
+
if (!text)
|
|
30
|
+
return null;
|
|
31
|
+
const r = sel.getRangeAt(0).getBoundingClientRect();
|
|
32
|
+
const b = box.getBoundingClientRect();
|
|
33
|
+
const above = r.top - b.top - BAR_H - 4;
|
|
34
|
+
const top = (above >= 0 ? above : r.bottom - b.top + 4) + box.scrollTop;
|
|
35
|
+
const mid = r.left - b.left + r.width / 2 - BAR_W / 2;
|
|
36
|
+
const left = Math.max(4, Math.min(mid, b.width - BAR_W - 4));
|
|
37
|
+
return {
|
|
38
|
+
quote: { text, from: from.dataset.chatFrom === 'user' ? 'user' : 'assistant' },
|
|
39
|
+
top,
|
|
40
|
+
left,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* The Reply bar over selected chat text. `box` is the chat's scrolling box, positioned; the bar
|
|
45
|
+
* sits in it, above the selection (below it when the top is out of view).
|
|
46
|
+
*/
|
|
47
|
+
export function ReplyToSelection({ box }) {
|
|
48
|
+
const [picked, setPicked] = useState(null);
|
|
49
|
+
useEffect(() => {
|
|
50
|
+
let frame = 0;
|
|
51
|
+
const read = () => {
|
|
52
|
+
cancelAnimationFrame(frame);
|
|
53
|
+
frame = requestAnimationFrame(() => {
|
|
54
|
+
const el = box.current;
|
|
55
|
+
setPicked(el ? pickedIn(el) : null);
|
|
56
|
+
});
|
|
57
|
+
};
|
|
58
|
+
document.addEventListener('selectionchange', read);
|
|
59
|
+
return () => {
|
|
60
|
+
cancelAnimationFrame(frame);
|
|
61
|
+
document.removeEventListener('selectionchange', read);
|
|
62
|
+
};
|
|
63
|
+
}, [box]);
|
|
64
|
+
if (!picked)
|
|
65
|
+
return null;
|
|
66
|
+
const done = () => {
|
|
67
|
+
document.getSelection()?.removeAllRanges();
|
|
68
|
+
setPicked(null);
|
|
69
|
+
};
|
|
70
|
+
return (_jsxs("div", { role: "toolbar", "aria-label": "Selected text", className: "absolute z-10 flex gap-0.5 rounded-lg border bg-popover p-0.5 shadow-md", style: { top: picked.top, left: picked.left },
|
|
71
|
+
// A press on the bar keeps the selection.
|
|
72
|
+
onMouseDown: (e) => e.preventDefault(), children: [_jsx(IconButton, { icon: ReplyIcon, label: "Reply", onClick: () => {
|
|
73
|
+
composeAssistant({ quote: picked.quote });
|
|
74
|
+
done();
|
|
75
|
+
} }), _jsx(IconButton, { icon: CopyIcon, label: "Copy", onClick: () => {
|
|
76
|
+
void navigator.clipboard.writeText(picked.quote.text).then(() => lumenToast.success('Copied'), () => lumenToast.error("Couldn't copy. Copy it with your keyboard instead."));
|
|
77
|
+
done();
|
|
78
|
+
} })] }));
|
|
79
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AssistantRefNote } from '@columenlabs/lumen-ui-types/assistant';
|
|
1
|
+
import type { AssistantQuote, AssistantRefNote } from '@columenlabs/lumen-ui-types/assistant';
|
|
2
2
|
import { type ComponentType } from 'react';
|
|
3
3
|
/** One thing a source holds: a row in the `@` list, sent with the message as `ref`. */
|
|
4
4
|
export interface AssistantSourceItem {
|
|
@@ -35,6 +35,8 @@ export interface ComposeRequest {
|
|
|
35
35
|
text?: string;
|
|
36
36
|
refs?: AssistantRefNote[];
|
|
37
37
|
tier?: string;
|
|
38
|
+
/** A piece of the chat to reply to: it shows above the box with an ×. */
|
|
39
|
+
quote?: AssistantQuote;
|
|
38
40
|
}
|
|
39
41
|
/**
|
|
40
42
|
* Opens the assistant with `refs` as chips in its composer and `text` typed, focused for the person
|
|
@@ -192,6 +192,7 @@ export function useAssistantChat(client) {
|
|
|
192
192
|
at: Date.now(),
|
|
193
193
|
...(extra?.context ? { context: extra.context.label } : {}),
|
|
194
194
|
...(extra?.refs?.length ? { refs: extra.refs } : {}),
|
|
195
|
+
...(extra?.quote ? { quote: extra.quote } : {}),
|
|
195
196
|
...(extra?.tier ? { tier: extra.tier } : {}),
|
|
196
197
|
...(retried ? { retried: true } : {}),
|
|
197
198
|
...(extra?.images?.length
|
|
@@ -311,6 +312,7 @@ export function useAssistantChat(client) {
|
|
|
311
312
|
void run(user.text, {
|
|
312
313
|
...extra,
|
|
313
314
|
...(user.refs?.length ? { refs: user.refs } : {}),
|
|
315
|
+
...(user.quote ? { quote: user.quote } : {}),
|
|
314
316
|
...(user.tier ? { tier: user.tier } : {}),
|
|
315
317
|
...(images.length ? { images } : {}),
|
|
316
318
|
}, true);
|
package/dist/whats-new.d.ts
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
import { type ReactNode } from 'react';
|
|
2
|
-
export declare function WhatsNew({ api, apps, everyone, }: {
|
|
2
|
+
export declare function WhatsNew({ api, apps, everyone, label, }: {
|
|
3
3
|
api: string;
|
|
4
4
|
/** Each app's chip label by id, in the order groups list them; left out: no app chips. */
|
|
5
5
|
apps?: Record<string, ReactNode>;
|
|
6
6
|
/** The chip of a takeaway about no one app. */
|
|
7
7
|
everyone?: string;
|
|
8
|
+
/**
|
|
9
|
+
* Status-bar text that opens the notes (the app's version): no spark and no "What's new" words;
|
|
10
|
+
* the tooltip and aria-label still say What's new. Left out: the spark and those words.
|
|
11
|
+
*/
|
|
12
|
+
label?: ReactNode;
|
|
8
13
|
}): import("react").JSX.Element | null;
|
package/dist/whats-new.js
CHANGED
|
@@ -42,7 +42,7 @@ async function page(api, before) {
|
|
|
42
42
|
throw new Error(`releases ${r.status}`);
|
|
43
43
|
return (await r.json()).releases;
|
|
44
44
|
}
|
|
45
|
-
export function WhatsNew({ api, apps, everyone = 'Global', }) {
|
|
45
|
+
export function WhatsNew({ api, apps, everyone = 'Global', label = null, }) {
|
|
46
46
|
const [open, setOpen] = useState(false);
|
|
47
47
|
const { timeZone } = usePrefs();
|
|
48
48
|
const q = useInfiniteQuery({
|
|
@@ -53,13 +53,15 @@ export function WhatsNew({ api, apps, everyone = 'Global', }) {
|
|
|
53
53
|
staleTime: 30 * 60_000,
|
|
54
54
|
retry: false,
|
|
55
55
|
});
|
|
56
|
-
// Hidden while loading, on an error, and when the host has no releases API (404).
|
|
57
|
-
|
|
56
|
+
// Hidden while loading, on an error, and when the host has no releases API (404). A label (the
|
|
57
|
+
// app's version) stays on the bar through that and opens the notes once they load.
|
|
58
|
+
const ready = !!q.data && q.data.pages[0] !== null;
|
|
59
|
+
if (!ready && label == null)
|
|
58
60
|
return null;
|
|
59
|
-
const releases = q.data
|
|
61
|
+
const releases = ready ? (q.data?.pages.flatMap((p) => p ?? []) ?? []) : [];
|
|
60
62
|
const latest = releases[0];
|
|
61
63
|
const day = (at) => new Date(at).toLocaleDateString('en-US', { month: 'short', day: 'numeric', timeZone });
|
|
62
|
-
return (_jsxs(_Fragment, { children: [_jsxs(Button, { variant: "ghost", size: "sm", className: "h-6 gap-1 px-1.5 text-muted-foreground text-xs", onClick: () => setOpen(true), title: latest ? `What's new · ${latest.tag}` : "What's new", "aria-label": "What's new", children: [_jsx(SparklesIcon, { className: "size-3.5" }), _jsx("span", { className: "max-md:hidden", children: "What's new" })] }), _jsxs(LumenDrawer, { open: open, onClose: () => setOpen(false), title: "What's new", description: "Key takeaways of each release, newest first", footerStart: releases.length ? `${releases.length} releases` : undefined, footer: _jsxs(_Fragment, { children: [q.hasNextPage && (_jsx(Button, { variant: "outline", size: "sm", disabled: q.isFetchingNextPage, onClick: () => q.fetchNextPage(), children: "Show more" })), _jsx(Button, { size: "sm", onClick: () => setOpen(false), children: "Close" })] }), children: [!releases.length && (_jsx("p", { className: "py-2 text-muted-foreground text-sm", children: "No releases yet. Each release's key takeaways show here once it ships." })), releases.map((r, i) => (_jsx(DrawerSection, { title: r.tag, hint: day(r.published_at), defaultOpen: i === 0, children: r.takeaways.length ? (_jsx(Notes, { takeaways: r.takeaways, apps: apps, everyone: everyone })) : (_jsx("p", { className: "py-1 pl-1 text-muted-foreground text-sm", children: "Nothing new for you in this one." })) }, r.tag)))] })] }));
|
|
64
|
+
return (_jsxs(_Fragment, { children: [label != null ? (_jsx("button", { type: "button", className: "-mx-1 flex-none rounded px-1 tabular-nums hover:bg-muted hover:text-foreground", onClick: () => ready && setOpen(true), title: "What's new", "aria-label": "What's new", children: label })) : (_jsxs(Button, { variant: "ghost", size: "sm", className: "h-6 gap-1 px-1.5 text-muted-foreground text-xs", onClick: () => setOpen(true), title: latest ? `What's new · ${latest.tag}` : "What's new", "aria-label": "What's new", children: [_jsx(SparklesIcon, { className: "size-3.5" }), _jsx("span", { className: "max-md:hidden", children: "What's new" })] })), _jsxs(LumenDrawer, { open: open, onClose: () => setOpen(false), title: "What's new", description: "Key takeaways of each release, newest first", footerStart: releases.length ? `${releases.length} releases` : undefined, footer: _jsxs(_Fragment, { children: [q.hasNextPage && (_jsx(Button, { variant: "outline", size: "sm", disabled: q.isFetchingNextPage, onClick: () => q.fetchNextPage(), children: "Show more" })), _jsx(Button, { size: "sm", onClick: () => setOpen(false), children: "Close" })] }), children: [!releases.length && (_jsx("p", { className: "py-2 text-muted-foreground text-sm", children: "No releases yet. Each release's key takeaways show here once it ships." })), releases.map((r, i) => (_jsx(DrawerSection, { title: r.tag, hint: day(r.published_at), defaultOpen: i === 0, children: r.takeaways.length ? (_jsx(Notes, { takeaways: r.takeaways, apps: apps, everyone: everyone })) : (_jsx("p", { className: "py-1 pl-1 text-muted-foreground text-sm", children: "Nothing new for you in this one." })) }, r.tag)))] })] }));
|
|
63
65
|
}
|
|
64
66
|
/** One release's takeaways: a flat list, or one sub-section per app once one app has many. */
|
|
65
67
|
function Notes({ takeaways, apps, everyone, }) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@columenlabs/lumen-ui",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.226",
|
|
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.226",
|
|
53
53
|
"@dnd-kit/core": "6.3.1",
|
|
54
54
|
"@dnd-kit/sortable": "10.0.0",
|
|
55
55
|
"@dnd-kit/utilities": "3.2.2",
|