@darqlabs/curator-sdk-react 0.4.0 → 0.4.2
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/cjs/CuratorChat.d.ts +46 -4
- package/dist/cjs/CuratorChat.d.ts.map +1 -1
- package/dist/cjs/CuratorChat.js +215 -51
- package/dist/cjs/CuratorChat.js.map +1 -1
- package/dist/cjs/index.d.ts +1 -1
- package/dist/cjs/index.d.ts.map +1 -1
- package/dist/esm/CuratorChat.d.ts +46 -4
- package/dist/esm/CuratorChat.d.ts.map +1 -1
- package/dist/esm/CuratorChat.js +216 -52
- package/dist/esm/CuratorChat.js.map +1 -1
- package/dist/esm/index.d.ts +1 -1
- package/dist/esm/index.d.ts.map +1 -1
- package/package.json +3 -3
|
@@ -11,6 +11,21 @@
|
|
|
11
11
|
*/
|
|
12
12
|
import { type CSSProperties, type ReactNode } from "react";
|
|
13
13
|
import type { AssistantMessage, Curator, CuratorError, Environment, StreamState, StreamPlanProgress } from "@darqlabs/curator-sdk";
|
|
14
|
+
/**
|
|
15
|
+
* A starter prompt offered in the empty thread.
|
|
16
|
+
*
|
|
17
|
+
* A bare string is both the button text and the message sent. The object form
|
|
18
|
+
* separates them, because the two want different lengths: a button reads best
|
|
19
|
+
* at three or four words, while the message that actually starts the
|
|
20
|
+
* conversation is usually a full sentence carrying the context the agent needs.
|
|
21
|
+
* Collapsing them forces a choice between a cramped button and a thin prompt.
|
|
22
|
+
*/
|
|
23
|
+
export type CuratorPrompt = string | {
|
|
24
|
+
/** Button text. */
|
|
25
|
+
label: string;
|
|
26
|
+
/** Message sent on click. Defaults to `label`. */
|
|
27
|
+
prompt?: string;
|
|
28
|
+
};
|
|
14
29
|
export interface CuratorChatProps {
|
|
15
30
|
/** SDK instance. Optional when a `<CuratorProvider>` is mounted. */
|
|
16
31
|
curator?: Curator;
|
|
@@ -26,6 +41,18 @@ export interface CuratorChatProps {
|
|
|
26
41
|
conversationId?: string;
|
|
27
42
|
/** Header title. Default: "Chat". */
|
|
28
43
|
title?: string;
|
|
44
|
+
/**
|
|
45
|
+
* Render without the header.
|
|
46
|
+
*
|
|
47
|
+
* An embedded widget usually sits under a page that has already said what
|
|
48
|
+
* this is; the header then repeats it. CSS can hide it, but hiding a
|
|
49
|
+
* component's own chrome from outside is not a contract — a markup change
|
|
50
|
+
* here would silently un-hide it on someone's page. This is the contract.
|
|
51
|
+
*
|
|
52
|
+
* The close affordance goes with it, so a consumer passing `onClose` while
|
|
53
|
+
* hiding the header owns dismissal themselves.
|
|
54
|
+
*/
|
|
55
|
+
hideHeader?: boolean;
|
|
29
56
|
/** Input placeholder. Default: "Type a message…". */
|
|
30
57
|
placeholder?: string;
|
|
31
58
|
/** Shown above the empty message list as a system prompt. */
|
|
@@ -107,6 +134,19 @@ export interface CuratorChatProps {
|
|
|
107
134
|
* exists.
|
|
108
135
|
*/
|
|
109
136
|
enableAttachments?: boolean;
|
|
137
|
+
/**
|
|
138
|
+
* Starter prompts shown in the empty thread. Clicking one sends it.
|
|
139
|
+
*
|
|
140
|
+
* The blank-thread problem is that a capable agent looks identical to an
|
|
141
|
+
* unconfigured one — nothing on screen says what it can do. These are the
|
|
142
|
+
* cheapest answer: they demonstrate scope and remove the cost of composing a
|
|
143
|
+
* first message.
|
|
144
|
+
*
|
|
145
|
+
* prompts={["Book a shipment", { label: "Track a load", prompt: "Where is my shipment LOG-4173?" }]}
|
|
146
|
+
*
|
|
147
|
+
* Shown only while the thread is empty; they are an entry point, not a menu.
|
|
148
|
+
*/
|
|
149
|
+
prompts?: CuratorPrompt[];
|
|
110
150
|
/**
|
|
111
151
|
* Design tokens — colours, typography, spacing, radii — merged over the
|
|
112
152
|
* palette `theme` selects.
|
|
@@ -131,10 +171,12 @@ declare function AssistantMessage(): import("react").JSX.Element;
|
|
|
131
171
|
/**
|
|
132
172
|
* Consumer-facing token names, mapped to the variables they set.
|
|
133
173
|
*
|
|
134
|
-
*
|
|
135
|
-
*
|
|
136
|
-
*
|
|
137
|
-
*
|
|
174
|
+
* Complete for the axes a consumer can be expected to want: the whole type
|
|
175
|
+
* scale, the whole spacing scale, every colour surface, and the radii. An
|
|
176
|
+
* earlier version exposed only `fontSize` and `radius`, which split each scale
|
|
177
|
+
* down the middle — half reachable by prop, half only by stylesheet. A partial
|
|
178
|
+
* scale is worse than none, because it lets someone change a base size and then
|
|
179
|
+
* discover the secondary sizes did not follow.
|
|
138
180
|
*/
|
|
139
181
|
declare const TOKEN_VARS: Record<string, string>;
|
|
140
182
|
/** Token values a consumer may set. Numbers are treated as pixels. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CuratorChat.d.ts","sourceRoot":"","sources":["../../src/CuratorChat.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,
|
|
1
|
+
{"version":3,"file":"CuratorChat.d.ts","sourceRoot":"","sources":["../../src/CuratorChat.tsx"],"names":[],"mappings":"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"}
|
package/dist/cjs/CuratorChat.js
CHANGED
|
@@ -22,7 +22,7 @@ const Markdown_js_1 = require("./Markdown.js");
|
|
|
22
22
|
const attachmentEnvelope_js_1 = require("./attachmentEnvelope.js");
|
|
23
23
|
const CuratorAttachmentAdapter_js_1 = require("./CuratorAttachmentAdapter.js");
|
|
24
24
|
function CuratorChat(props) {
|
|
25
|
-
const { curator, agent, project, environment, system, conversationId, title = "Chat", placeholder = "Type a message…", welcomeMessage, theme = "light", className, style, onClose, loadingIndicator, onMessage, onState, onPlanProgress, onError, enableRetry = false, enableCopy = false, showTimestamps = true, enableAttachments = false, tokens, acceptedFileTypes, maxFileSize, queueWhileRunning = true, } = props;
|
|
25
|
+
const { curator, agent, project, environment, system, conversationId, title = "Chat", placeholder = "Type a message…", welcomeMessage, theme = "light", className, style, onClose, loadingIndicator, onMessage, onState, onPlanProgress, onError, enableRetry = false, enableCopy = false, showTimestamps = true, enableAttachments = false, hideHeader = false, prompts, tokens, acceptedFileTypes, maxFileSize, queueWhileRunning = true, } = props;
|
|
26
26
|
const contextClient = (0, CuratorProvider_js_1.useCuratorOrNull)();
|
|
27
27
|
const client = curator ?? contextClient;
|
|
28
28
|
if (!client) {
|
|
@@ -75,6 +75,12 @@ function CuratorChat(props) {
|
|
|
75
75
|
// Order is the contract: non-colour defaults, then the theme's palette, then
|
|
76
76
|
// the consumer's tokens, then the raw `style` prop — each able to override
|
|
77
77
|
// everything before it, with `style` still winning as it always has.
|
|
78
|
+
// Layout effect, not effect: the sheet must be in place before the browser
|
|
79
|
+
// paints, or the first frame renders unstyled and visibly settles.
|
|
80
|
+
const hostRef = (0, react_1.useRef)(null);
|
|
81
|
+
useIsomorphicLayoutEffect(() => {
|
|
82
|
+
injectCuratorStyles(hostRef.current);
|
|
83
|
+
}, []);
|
|
78
84
|
const palette = theme === "dark" ? DARK_VARS : LIGHT_VARS;
|
|
79
85
|
const wrapperStyle = {
|
|
80
86
|
...BASE_TOKENS,
|
|
@@ -83,7 +89,7 @@ function CuratorChat(props) {
|
|
|
83
89
|
...containerStyle,
|
|
84
90
|
...style,
|
|
85
91
|
};
|
|
86
|
-
return ((0, jsx_runtime_1.jsxs)("div", { className: className, style: wrapperStyle, "data-curator-chat": true, "data-theme": theme, children: [(0, jsx_runtime_1.jsx)(Header, { title: title, onClose: onClose }), (0, jsx_runtime_1.jsx)(LoadingIndicatorContext.Provider, { value: loadingIndicator ?? (0, jsx_runtime_1.jsx)(TypingDots, {}), children: (0, jsx_runtime_1.jsx)(RetryEnabledContext.Provider, { value: enableRetry, children: (0, jsx_runtime_1.jsx)(CopyEnabledContext.Provider, { value: enableCopy, children: (0, jsx_runtime_1.jsx)(TimestampsEnabledContext.Provider, { value: showTimestamps, children: (0, jsx_runtime_1.jsx)(react_2.AssistantRuntimeProvider, { runtime: runtime, children: (0, jsx_runtime_1.jsxs)(react_2.ThreadPrimitive.Root, { "data-curator-part": "thread", style: threadRootStyle, children: [(0, jsx_runtime_1.jsxs)(react_2.ThreadPrimitive.Viewport, { autoScroll: true, "data-curator-part": "viewport", style: viewportStyle, children: [(0, jsx_runtime_1.
|
|
92
|
+
return ((0, jsx_runtime_1.jsxs)("div", { ref: hostRef, className: className, style: wrapperStyle, "data-curator-chat": true, "data-theme": theme, children: [!hideHeader && (0, jsx_runtime_1.jsx)(Header, { title: title, onClose: onClose }), (0, jsx_runtime_1.jsx)(LoadingIndicatorContext.Provider, { value: loadingIndicator ?? (0, jsx_runtime_1.jsx)(TypingDots, {}), children: (0, jsx_runtime_1.jsx)(RetryEnabledContext.Provider, { value: enableRetry, children: (0, jsx_runtime_1.jsx)(CopyEnabledContext.Provider, { value: enableCopy, children: (0, jsx_runtime_1.jsx)(TimestampsEnabledContext.Provider, { value: showTimestamps, children: (0, jsx_runtime_1.jsx)(react_2.AssistantRuntimeProvider, { runtime: runtime, children: (0, jsx_runtime_1.jsxs)(react_2.ThreadPrimitive.Root, { "data-curator-part": "thread", style: threadRootStyle, children: [(0, jsx_runtime_1.jsxs)(react_2.ThreadPrimitive.Viewport, { autoScroll: true, "data-curator-part": "viewport", style: viewportStyle, children: [(0, jsx_runtime_1.jsxs)(react_2.ThreadPrimitive.Empty, { children: [welcomeMessage ? (0, jsx_runtime_1.jsx)(Welcome, { text: welcomeMessage }) : null, prompts && prompts.length > 0 && (0, jsx_runtime_1.jsx)(Prompts, { prompts: prompts })] }), (0, jsx_runtime_1.jsx)(react_2.ThreadPrimitive.Messages, { components: {
|
|
87
93
|
UserMessage,
|
|
88
94
|
AssistantMessage,
|
|
89
95
|
} })] }), (0, jsx_runtime_1.jsx)(Composer, { placeholder: placeholder, allowQueue: queueWhileRunning, allowAttachments: !!attachmentAdapter })] }) }) }) }) }) }), hidePoweredBy === false && (0, jsx_runtime_1.jsx)(brand_js_1.PoweredByCurator, {})] }));
|
|
@@ -91,6 +97,14 @@ function CuratorChat(props) {
|
|
|
91
97
|
// Context for threading the loading indicator into the message-component
|
|
92
98
|
// callbacks without remounting them on every render. Default is rendered
|
|
93
99
|
// when no consumer-supplied node is provided.
|
|
100
|
+
/**
|
|
101
|
+
* `useLayoutEffect` in the browser, `useEffect` on the server.
|
|
102
|
+
*
|
|
103
|
+
* The widget is embeddable in SSR frameworks, where a bare `useLayoutEffect`
|
|
104
|
+
* logs a warning on every render. The effect it guards does nothing on the
|
|
105
|
+
* server anyway — there is no DOM to inject into.
|
|
106
|
+
*/
|
|
107
|
+
const useIsomorphicLayoutEffect = typeof window !== "undefined" ? react_1.useLayoutEffect : react_1.useEffect;
|
|
94
108
|
const LoadingIndicatorContext = (0, react_1.createContext)(null);
|
|
95
109
|
// Threads the `enableRetry` prop into AssistantMessage, which assistant-ui
|
|
96
110
|
// instantiates itself (no direct prop passing). Default off.
|
|
@@ -107,6 +121,24 @@ function Header({ title, onClose }) {
|
|
|
107
121
|
function Welcome({ text }) {
|
|
108
122
|
return (0, jsx_runtime_1.jsx)("div", { "data-curator-part": "welcome", style: welcomeStyle, children: text });
|
|
109
123
|
}
|
|
124
|
+
/**
|
|
125
|
+
* Starter prompts for an empty thread.
|
|
126
|
+
*
|
|
127
|
+
* Built on `ThreadPrimitive.Suggestion` rather than a plain button so the send
|
|
128
|
+
* goes through the same path as the composer's — including the case where one
|
|
129
|
+
* is clicked while a run is already in flight, which the primitive queues
|
|
130
|
+
* instead of dropping.
|
|
131
|
+
*/
|
|
132
|
+
function Prompts({ prompts }) {
|
|
133
|
+
return ((0, jsx_runtime_1.jsx)("div", { "data-curator-part": "prompts", style: promptsStyle, children: prompts.map((p, i) => {
|
|
134
|
+
const label = typeof p === "string" ? p : p.label;
|
|
135
|
+
const prompt = typeof p === "string" ? p : (p.prompt ?? p.label);
|
|
136
|
+
return ((0, jsx_runtime_1.jsx)(react_2.ThreadPrimitive.Suggestion, { prompt: prompt, send: true, "data-curator-part": "prompt", style: promptStyle,
|
|
137
|
+
// The message differs from the button when the object form is used,
|
|
138
|
+
// so the full text has to be reachable without clicking.
|
|
139
|
+
title: prompt !== label ? prompt : undefined, children: label }, `${label}-${i}`));
|
|
140
|
+
}) }));
|
|
141
|
+
}
|
|
110
142
|
function UserMessage() {
|
|
111
143
|
// Attachments as the message carries them structurally. The envelope path in
|
|
112
144
|
// `UserTextPart` is the fallback for transcripts written before the server
|
|
@@ -159,10 +191,10 @@ function AssistantMessage() {
|
|
|
159
191
|
const indicator = (0, react_1.useContext)(LoadingIndicatorContext);
|
|
160
192
|
const retryEnabled = (0, react_1.useContext)(RetryEnabledContext);
|
|
161
193
|
const copyEnabled = (0, react_1.useContext)(CopyEnabledContext);
|
|
162
|
-
return ((0, jsx_runtime_1.jsxs)(react_2.MessagePrimitive.Root, { "data-curator-part": "message message-assistant", style: rowStartStyle, children: [(0, jsx_runtime_1.jsxs)("div", { "data-curator-part": "bubble bubble-assistant", style: { ...bubbleBaseStyle, ...assistantBubbleStyle }, children: [(0, jsx_runtime_1.jsx)(react_2.MessagePrimitive.If, { hasContent: false, children: indicator }), (0, jsx_runtime_1.jsx)(react_2.MessagePrimitive.Parts, { components: { Text: AssistantTextPart } })] }), (0, jsx_runtime_1.jsx)(Timestamp, { align: "start" }), (retryEnabled || copyEnabled) && (
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
194
|
+
return ((0, jsx_runtime_1.jsxs)(react_2.MessagePrimitive.Root, { "data-curator-part": "message message-assistant", style: rowStartStyle, children: [(0, jsx_runtime_1.jsxs)("div", { "data-curator-part": "bubble bubble-assistant", style: { ...bubbleBaseStyle, ...assistantBubbleStyle }, children: [(0, jsx_runtime_1.jsx)(react_2.MessagePrimitive.If, { hasContent: false, children: indicator }), (0, jsx_runtime_1.jsx)(react_2.MessagePrimitive.Parts, { components: { Text: AssistantTextPart } })] }), (0, jsx_runtime_1.jsxs)("div", { "data-curator-part": "message-meta", style: messageMetaStyle, children: [(0, jsx_runtime_1.jsx)(Timestamp, { align: "start" }), (retryEnabled || copyEnabled) && (
|
|
195
|
+
// `hideWhenRunning` still applies: actions on a half-written reply
|
|
196
|
+
// would copy a fragment.
|
|
197
|
+
(0, jsx_runtime_1.jsxs)(react_2.ActionBarPrimitive.Root, { hideWhenRunning: true, "data-curator-part": "action-bar", style: actionBarStyle, children: [copyEnabled && ((0, jsx_runtime_1.jsxs)(react_2.ActionBarPrimitive.Copy, { "aria-label": "Copy", "data-curator-part": "action-button", style: actionButtonStyle, children: [(0, jsx_runtime_1.jsx)(react_2.MessagePrimitive.If, { copied: true, children: (0, jsx_runtime_1.jsx)(CheckGlyph, {}) }), (0, jsx_runtime_1.jsx)(react_2.MessagePrimitive.If, { copied: false, children: (0, jsx_runtime_1.jsx)(CopyGlyph, {}) })] })), retryEnabled && ((0, jsx_runtime_1.jsx)(react_2.ActionBarPrimitive.Reload, { "aria-label": "Retry", "data-curator-part": "action-button", style: actionButtonStyle, children: (0, jsx_runtime_1.jsx)(RetryGlyph, {}) }))] }))] })] }));
|
|
166
198
|
}
|
|
167
199
|
/**
|
|
168
200
|
* Waiting state for a reply that has not started streaming.
|
|
@@ -349,22 +381,37 @@ const BASE_TOKENS = {
|
|
|
349
381
|
["--curator-space-4"]: "16px",
|
|
350
382
|
["--curator-radius-md"]: "10px",
|
|
351
383
|
["--curator-radius-sm"]: "8px",
|
|
384
|
+
["--curator-border-width"]: "1px",
|
|
352
385
|
};
|
|
353
386
|
/**
|
|
354
387
|
* Consumer-facing token names, mapped to the variables they set.
|
|
355
388
|
*
|
|
356
|
-
*
|
|
357
|
-
*
|
|
358
|
-
*
|
|
359
|
-
*
|
|
389
|
+
* Complete for the axes a consumer can be expected to want: the whole type
|
|
390
|
+
* scale, the whole spacing scale, every colour surface, and the radii. An
|
|
391
|
+
* earlier version exposed only `fontSize` and `radius`, which split each scale
|
|
392
|
+
* down the middle — half reachable by prop, half only by stylesheet. A partial
|
|
393
|
+
* scale is worse than none, because it lets someone change a base size and then
|
|
394
|
+
* discover the secondary sizes did not follow.
|
|
360
395
|
*/
|
|
361
396
|
const TOKEN_VARS = {
|
|
362
397
|
fontFamily: "--curator-font-family",
|
|
363
398
|
fontSize: "--curator-font-size",
|
|
399
|
+
// The rest of the type scale. Exposing `fontSize` alone let a host set a base
|
|
400
|
+
// size while the secondary sizes stayed pinned at 12/11px, which is a worse
|
|
401
|
+
// mismatch than not being able to change any of them.
|
|
402
|
+
fontSizeSm: "--curator-font-size-sm",
|
|
403
|
+
fontSizeXs: "--curator-font-size-xs",
|
|
404
|
+
fontWeightStrong: "--curator-font-weight-strong",
|
|
364
405
|
lineHeight: "--curator-line-height",
|
|
406
|
+
// Density. Every padding and gap in the widget derives from these four.
|
|
407
|
+
space1: "--curator-space-1",
|
|
408
|
+
space2: "--curator-space-2",
|
|
409
|
+
space3: "--curator-space-3",
|
|
410
|
+
space4: "--curator-space-4",
|
|
365
411
|
radius: "--curator-radius",
|
|
366
412
|
radiusMd: "--curator-radius-md",
|
|
367
413
|
radiusSm: "--curator-radius-sm",
|
|
414
|
+
borderWidth: "--curator-border-width",
|
|
368
415
|
background: "--curator-bg",
|
|
369
416
|
surface: "--curator-surface",
|
|
370
417
|
text: "--curator-text",
|
|
@@ -400,6 +447,20 @@ function tokensToVars(tokens) {
|
|
|
400
447
|
}
|
|
401
448
|
return out;
|
|
402
449
|
}
|
|
450
|
+
/**
|
|
451
|
+
* Accent on the USER bubble, neutral on the assistant's.
|
|
452
|
+
*
|
|
453
|
+
* The reverse of what this widget shipped with, and the reverse of what the
|
|
454
|
+
* variable names imply — the assistant bubble used to borrow `--curator-accent`
|
|
455
|
+
* directly. Two problems with that. The assistant writes most of the words, so
|
|
456
|
+
* painting its bubble in the accent colour turns every substantial reply into a
|
|
457
|
+
* slab of saturated colour and makes the machine the loudest thing on screen.
|
|
458
|
+
* And it inverts the convention every messaging UI has taught people to read,
|
|
459
|
+
* where the accent marks *your* messages.
|
|
460
|
+
*
|
|
461
|
+
* Both are now separate variables, so a consumer who preferred the old
|
|
462
|
+
* arrangement sets `assistantBubble` / `userBubble` back.
|
|
463
|
+
*/
|
|
403
464
|
const LIGHT_VARS = {
|
|
404
465
|
["--curator-bg"]: "#ffffff",
|
|
405
466
|
["--curator-surface"]: "#f8fafc",
|
|
@@ -408,13 +469,10 @@ const LIGHT_VARS = {
|
|
|
408
469
|
["--curator-border"]: "rgba(15, 23, 42, 0.08)",
|
|
409
470
|
["--curator-accent"]: "#0f172a",
|
|
410
471
|
["--curator-accent-text"]: "#ffffff",
|
|
411
|
-
["--curator-user-bubble"]: "#
|
|
412
|
-
["--curator-user-bubble-text"]: "#
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
// same value so nothing shifts, but they are now separable.
|
|
416
|
-
["--curator-assistant-bubble"]: "#0f172a",
|
|
417
|
-
["--curator-assistant-bubble-text"]: "#ffffff",
|
|
472
|
+
["--curator-user-bubble"]: "#0f172a",
|
|
473
|
+
["--curator-user-bubble-text"]: "#ffffff",
|
|
474
|
+
["--curator-assistant-bubble"]: "#f1f5f9",
|
|
475
|
+
["--curator-assistant-bubble-text"]: "#0f172a",
|
|
418
476
|
["--curator-header-bg"]: "transparent",
|
|
419
477
|
["--curator-composer-bg"]: "transparent",
|
|
420
478
|
["--curator-focus-ring"]: "rgba(15, 23, 42, 0.16)",
|
|
@@ -429,10 +487,12 @@ const DARK_VARS = {
|
|
|
429
487
|
["--curator-border"]: "rgba(226, 232, 240, 0.1)",
|
|
430
488
|
["--curator-accent"]: "#e2e8f0",
|
|
431
489
|
["--curator-accent-text"]: "#0b1220",
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
["--curator-
|
|
435
|
-
["--curator-
|
|
490
|
+
// Dark mode keeps the same relationship: the user's bubble is the lighter,
|
|
491
|
+
// more saturated one; the assistant sits on a raised neutral surface.
|
|
492
|
+
["--curator-user-bubble"]: "#e2e8f0",
|
|
493
|
+
["--curator-user-bubble-text"]: "#0b1220",
|
|
494
|
+
["--curator-assistant-bubble"]: "#1f2937",
|
|
495
|
+
["--curator-assistant-bubble-text"]: "#e2e8f0",
|
|
436
496
|
["--curator-header-bg"]: "transparent",
|
|
437
497
|
["--curator-composer-bg"]: "transparent",
|
|
438
498
|
["--curator-focus-ring"]: "rgba(226, 232, 240, 0.2)",
|
|
@@ -447,8 +507,8 @@ const containerStyle = {
|
|
|
447
507
|
minHeight: 360,
|
|
448
508
|
background: "var(--curator-bg)",
|
|
449
509
|
color: "var(--curator-text)",
|
|
450
|
-
border: "1px solid var(--curator-border)",
|
|
451
|
-
borderRadius:
|
|
510
|
+
border: "var(--curator-border-width, 1px) solid var(--curator-border)",
|
|
511
|
+
borderRadius: RADIUS_LG,
|
|
452
512
|
overflow: "hidden",
|
|
453
513
|
fontFamily: "var(--curator-font-family)",
|
|
454
514
|
fontSize: FONT_BASE,
|
|
@@ -482,7 +542,7 @@ const headerActionStyle = {
|
|
|
482
542
|
alignItems: "center",
|
|
483
543
|
justifyContent: "center",
|
|
484
544
|
border: "none",
|
|
485
|
-
borderRadius:
|
|
545
|
+
borderRadius: RADIUS_SM,
|
|
486
546
|
background: "transparent",
|
|
487
547
|
color: "var(--curator-muted)",
|
|
488
548
|
cursor: "pointer",
|
|
@@ -505,15 +565,39 @@ const viewportStyle = {
|
|
|
505
565
|
// clips rather than growing a horizontal scrollbar across the whole thread.
|
|
506
566
|
overflowX: "hidden",
|
|
507
567
|
minWidth: 0,
|
|
508
|
-
padding: `${SPACE_4}
|
|
568
|
+
padding: `${SPACE_4} ${SPACE_4} ${SPACE_3}`,
|
|
509
569
|
display: "flex",
|
|
510
570
|
flexDirection: "column",
|
|
511
|
-
gap:
|
|
571
|
+
gap: SPACE_3,
|
|
512
572
|
};
|
|
513
573
|
const welcomeStyle = {
|
|
514
574
|
color: "var(--curator-muted)",
|
|
515
|
-
fontSize:
|
|
516
|
-
padding: `${SPACE_2}
|
|
575
|
+
fontSize: FONT_SM,
|
|
576
|
+
padding: `${SPACE_2} 0`,
|
|
577
|
+
};
|
|
578
|
+
const promptsStyle = {
|
|
579
|
+
display: "flex",
|
|
580
|
+
flexWrap: "wrap",
|
|
581
|
+
gap: 6,
|
|
582
|
+
// Sits under the welcome line rather than beside it: the welcome says what
|
|
583
|
+
// this is, the prompts say what to do, and that reads as a sequence.
|
|
584
|
+
paddingTop: 2,
|
|
585
|
+
};
|
|
586
|
+
const promptStyle = {
|
|
587
|
+
// Outlined, not filled. A filled chip competes with the send button for
|
|
588
|
+
// "primary action", and there are several of these — a row of solid accent
|
|
589
|
+
// blocks in an otherwise empty panel is the loudest thing on the screen.
|
|
590
|
+
border: "1px solid var(--curator-border)",
|
|
591
|
+
borderRadius: RADIUS_SM,
|
|
592
|
+
background: "transparent",
|
|
593
|
+
color: "var(--curator-text)",
|
|
594
|
+
fontFamily: "inherit",
|
|
595
|
+
fontSize: FONT_SM,
|
|
596
|
+
lineHeight: 1.3,
|
|
597
|
+
padding: `6px ${SPACE_2}`,
|
|
598
|
+
cursor: "pointer",
|
|
599
|
+
textAlign: "left",
|
|
600
|
+
maxWidth: "100%",
|
|
517
601
|
};
|
|
518
602
|
const rowStartStyle = {
|
|
519
603
|
// Column so the retry action bar can stack beneath the bubble, both
|
|
@@ -525,26 +609,44 @@ const rowStartStyle = {
|
|
|
525
609
|
minWidth: 0,
|
|
526
610
|
};
|
|
527
611
|
const rowEndStyle = {
|
|
612
|
+
// Column, mirroring the assistant row. It was a plain row, which made the
|
|
613
|
+
// bubble and the timestamp siblings on one line — so a user message rendered
|
|
614
|
+
// as "hello7:19 AM", the stamp jammed against the bubble with no gap, while
|
|
615
|
+
// the assistant's sat neatly underneath. The two sides should differ in
|
|
616
|
+
// alignment, not in structure.
|
|
528
617
|
display: "flex",
|
|
618
|
+
flexDirection: "column",
|
|
619
|
+
alignItems: "flex-end",
|
|
529
620
|
width: "100%",
|
|
530
621
|
// Same `min-width: auto` trap as the bubble: without this the row itself
|
|
531
622
|
// refuses to shrink and the cap on the bubble inside it never binds.
|
|
532
623
|
minWidth: 0,
|
|
533
|
-
|
|
624
|
+
};
|
|
625
|
+
/**
|
|
626
|
+
* The strip under an assistant message: time on the left, actions beside it.
|
|
627
|
+
*
|
|
628
|
+
* `minHeight` is the whole point — it holds the row open at the height the
|
|
629
|
+
* action buttons need, from first render, so neither hovering nor the end of a
|
|
630
|
+
* stream changes the column's height.
|
|
631
|
+
*/
|
|
632
|
+
const messageMetaStyle = {
|
|
633
|
+
display: "flex",
|
|
634
|
+
alignItems: "center",
|
|
635
|
+
gap: 6,
|
|
636
|
+
minHeight: 22,
|
|
637
|
+
marginTop: 2,
|
|
534
638
|
};
|
|
535
639
|
const actionBarStyle = {
|
|
536
640
|
display: "flex",
|
|
537
641
|
alignItems: "center",
|
|
538
|
-
gap:
|
|
539
|
-
marginTop: 4,
|
|
540
|
-
paddingLeft: 2,
|
|
642
|
+
gap: 2,
|
|
541
643
|
};
|
|
542
644
|
const actionButtonStyle = {
|
|
543
645
|
display: "inline-flex",
|
|
544
646
|
alignItems: "center",
|
|
545
647
|
justifyContent: "center",
|
|
546
|
-
width:
|
|
547
|
-
height:
|
|
648
|
+
width: 22,
|
|
649
|
+
height: 22,
|
|
548
650
|
padding: 0,
|
|
549
651
|
border: "none",
|
|
550
652
|
borderRadius: RADIUS_SM,
|
|
@@ -564,7 +666,7 @@ const bubbleBaseStyle = {
|
|
|
564
666
|
// and the whole thread scrolls sideways. This is the fix for that; the
|
|
565
667
|
// wrapping rules below only get a chance to apply once the box can shrink.
|
|
566
668
|
minWidth: 0,
|
|
567
|
-
padding:
|
|
669
|
+
padding: `calc(${SPACE_2} + 2px) ${SPACE_3}`,
|
|
568
670
|
borderRadius: RADIUS_MD,
|
|
569
671
|
whiteSpace: "pre-wrap",
|
|
570
672
|
// `overflow-wrap: anywhere` is the standard property and it breaks inside an
|
|
@@ -592,7 +694,7 @@ const attachmentChipStyle = {
|
|
|
592
694
|
// Tinted from the bubble's own text colour so the chip reads as part of the
|
|
593
695
|
// message in either theme, without needing its own palette token.
|
|
594
696
|
background: "color-mix(in srgb, currentColor 10%, transparent)",
|
|
595
|
-
fontSize:
|
|
697
|
+
fontSize: FONT_SM,
|
|
596
698
|
lineHeight: 1.4,
|
|
597
699
|
verticalAlign: "middle",
|
|
598
700
|
};
|
|
@@ -633,7 +735,7 @@ const queuedRowStyle = {
|
|
|
633
735
|
display: "flex",
|
|
634
736
|
alignItems: "center",
|
|
635
737
|
gap: SPACE_2,
|
|
636
|
-
padding: `${SPACE_2}
|
|
738
|
+
padding: `${SPACE_2} ${SPACE_3} 0`,
|
|
637
739
|
fontSize: FONT_SM,
|
|
638
740
|
color: "var(--curator-muted)",
|
|
639
741
|
minWidth: 0,
|
|
@@ -666,7 +768,6 @@ const timestampStyle = {
|
|
|
666
768
|
fontSize: FONT_XS,
|
|
667
769
|
color: "var(--curator-muted)",
|
|
668
770
|
opacity: 0.7,
|
|
669
|
-
marginTop: 2,
|
|
670
771
|
padding: "0 2px",
|
|
671
772
|
userSelect: "none",
|
|
672
773
|
};
|
|
@@ -714,7 +815,7 @@ const composerRootStyle = {
|
|
|
714
815
|
display: "flex",
|
|
715
816
|
gap: SPACE_2,
|
|
716
817
|
alignItems: "flex-end",
|
|
717
|
-
padding: `${SPACE_3}
|
|
818
|
+
padding: `${SPACE_3} ${SPACE_3} ${SPACE_2}`,
|
|
718
819
|
flexShrink: 0,
|
|
719
820
|
};
|
|
720
821
|
const textareaStyle = {
|
|
@@ -722,7 +823,7 @@ const textareaStyle = {
|
|
|
722
823
|
resize: "none",
|
|
723
824
|
border: "1px solid var(--curator-border)",
|
|
724
825
|
borderRadius: RADIUS_SM,
|
|
725
|
-
padding: `${SPACE_2}
|
|
826
|
+
padding: `${SPACE_2} ${SPACE_3}`,
|
|
726
827
|
background: "var(--curator-bg)",
|
|
727
828
|
color: "var(--curator-text)",
|
|
728
829
|
fontFamily: "inherit",
|
|
@@ -749,16 +850,29 @@ const sendButtonStyle = {
|
|
|
749
850
|
flexShrink: 0,
|
|
750
851
|
cursor: "pointer",
|
|
751
852
|
};
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
853
|
+
/**
|
|
854
|
+
* Stylesheet injection, scoped to the tree the widget is actually in.
|
|
855
|
+
*
|
|
856
|
+
* This used to run at module load and append to `document.head`, which assumes
|
|
857
|
+
* the widget owns the document. It does not when it is embedded in a shadow
|
|
858
|
+
* root: styles in the host document do not cross a shadow boundary, so every
|
|
859
|
+
* rule below was silently lost — the typing dots stopped animating, the prompt
|
|
860
|
+
* hover and action-bar reveal never fired, the composer focus ring vanished,
|
|
861
|
+
* and all markdown formatting went with them. Nothing errors; the widget just
|
|
862
|
+
* quietly renders unstyled in one embedding mode.
|
|
863
|
+
*
|
|
864
|
+
* Injecting into the host element's own `getRootNode()` fixes both cases at
|
|
865
|
+
* once: that resolves to the document normally and to the shadow root when
|
|
866
|
+
* embedded, so a consumer does not have to know which they are in or mirror the
|
|
867
|
+
* sheet themselves.
|
|
868
|
+
*
|
|
869
|
+
* Reported by a consumer who had worked around it downstream by copying the
|
|
870
|
+
* sheet into their shadow root — a workaround nobody should need.
|
|
871
|
+
*/
|
|
872
|
+
const STYLE_PREFIX = "curator-chat-styles";
|
|
873
|
+
/** Bump when the rules change; superseded sheets are removed on inject. */
|
|
874
|
+
const STYLE_ID = `${STYLE_PREFIX}-v6`;
|
|
875
|
+
const STYLE_TEXT = `
|
|
762
876
|
@keyframes curator-wave {
|
|
763
877
|
0%, 60%, 100% { transform: translateY(0); opacity: 0.35; }
|
|
764
878
|
30% { transform: translateY(-4px); opacity: 0.95; }
|
|
@@ -775,6 +889,26 @@ if (typeof document !== "undefined") {
|
|
|
775
889
|
transform: none;
|
|
776
890
|
}
|
|
777
891
|
}
|
|
892
|
+
/* Hidden by opacity rather than by unmounting, so the row keeps its height.
|
|
893
|
+
pointer-events follows visibility so an invisible button is not clickable,
|
|
894
|
+
and focus-within keeps it reachable by keyboard, where there is no hover. */
|
|
895
|
+
[data-curator-chat] [data-curator-part~="action-bar"] {
|
|
896
|
+
opacity: 0;
|
|
897
|
+
pointer-events: none;
|
|
898
|
+
transition: opacity 120ms ease;
|
|
899
|
+
}
|
|
900
|
+
[data-curator-chat] [data-curator-part~="message"]:hover [data-curator-part~="action-bar"],
|
|
901
|
+
[data-curator-chat] [data-curator-part~="action-bar"]:focus-within {
|
|
902
|
+
opacity: 1;
|
|
903
|
+
pointer-events: auto;
|
|
904
|
+
}
|
|
905
|
+
@media (prefers-reduced-motion: reduce) {
|
|
906
|
+
[data-curator-chat] [data-curator-part~="action-bar"] { transition: none; }
|
|
907
|
+
}
|
|
908
|
+
[data-curator-chat] [data-curator-part~="prompt"]:hover {
|
|
909
|
+
background-color: var(--curator-user-bubble);
|
|
910
|
+
border-color: var(--curator-muted);
|
|
911
|
+
}
|
|
778
912
|
[data-curator-chat] button:disabled { opacity: 0.4; cursor: not-allowed; }
|
|
779
913
|
[data-curator-chat] textarea {
|
|
780
914
|
transition: border-color 120ms ease, box-shadow 120ms ease;
|
|
@@ -870,7 +1004,37 @@ if (typeof document !== "undefined") {
|
|
|
870
1004
|
[data-curator-md] th, [data-curator-md] td { border: 1px solid currentColor; padding: 4px 8px; }
|
|
871
1005
|
[data-curator-md] th { font-weight: 600; }
|
|
872
1006
|
`;
|
|
873
|
-
|
|
1007
|
+
/**
|
|
1008
|
+
* Put the sheet in `host`'s root, once per root.
|
|
1009
|
+
*
|
|
1010
|
+
* Runs on mount rather than at import: the root is only knowable from a mounted
|
|
1011
|
+
* element, and a lazily-loaded route chunk can import this module long before —
|
|
1012
|
+
* or well after — the tree it will live in exists.
|
|
1013
|
+
*
|
|
1014
|
+
* Not removed on unmount. Two widgets can share a root, and reference-counting
|
|
1015
|
+
* a `<style>` element costs more than the element does; a shadow root that is
|
|
1016
|
+
* torn down takes its copy with it.
|
|
1017
|
+
*/
|
|
1018
|
+
function injectCuratorStyles(host) {
|
|
1019
|
+
if (!host || typeof document === "undefined")
|
|
1020
|
+
return;
|
|
1021
|
+
const root = host.getRootNode();
|
|
1022
|
+
// A Document takes the sheet in `head`; a ShadowRoot has no head and takes it
|
|
1023
|
+
// directly. Both accept an element created by the owning document.
|
|
1024
|
+
const container = root.head ?? root;
|
|
1025
|
+
if (!container || typeof container.querySelector !== "function")
|
|
1026
|
+
return;
|
|
1027
|
+
if (container.querySelector(`#${STYLE_ID}`))
|
|
1028
|
+
return;
|
|
1029
|
+
// Drop superseded versions in THIS root. Without it an upgraded page would
|
|
1030
|
+
// apply two generations of rules at once, with the older one winning wherever
|
|
1031
|
+
// it happened to be more specific.
|
|
1032
|
+
for (const stale of Array.from(container.querySelectorAll(`[id^="${STYLE_PREFIX}-"]`))) {
|
|
1033
|
+
stale.remove();
|
|
874
1034
|
}
|
|
1035
|
+
const el = document.createElement("style");
|
|
1036
|
+
el.id = STYLE_ID;
|
|
1037
|
+
el.textContent = STYLE_TEXT;
|
|
1038
|
+
container.appendChild(el);
|
|
875
1039
|
}
|
|
876
1040
|
//# sourceMappingURL=CuratorChat.js.map
|