@darqlabs/curator-sdk-react 0.3.4 → 0.4.1
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/CuratorAdapter.d.ts +8 -1
- package/dist/cjs/CuratorAdapter.d.ts.map +1 -1
- package/dist/cjs/CuratorAdapter.js +38 -5
- package/dist/cjs/CuratorAdapter.js.map +1 -1
- package/dist/cjs/CuratorAttachmentAdapter.d.ts +87 -0
- package/dist/cjs/CuratorAttachmentAdapter.d.ts.map +1 -0
- package/dist/cjs/CuratorAttachmentAdapter.js +132 -0
- package/dist/cjs/CuratorAttachmentAdapter.js.map +1 -0
- package/dist/cjs/CuratorChat.d.ts +80 -0
- package/dist/cjs/CuratorChat.d.ts.map +1 -1
- package/dist/cjs/CuratorChat.js +330 -52
- package/dist/cjs/CuratorChat.js.map +1 -1
- package/dist/cjs/index.d.ts +3 -1
- package/dist/cjs/index.d.ts.map +1 -1
- package/dist/cjs/index.js +5 -1
- package/dist/cjs/index.js.map +1 -1
- package/dist/esm/CuratorAdapter.d.ts +8 -1
- package/dist/esm/CuratorAdapter.d.ts.map +1 -1
- package/dist/esm/CuratorAdapter.js +38 -5
- package/dist/esm/CuratorAdapter.js.map +1 -1
- package/dist/esm/CuratorAttachmentAdapter.d.ts +87 -0
- package/dist/esm/CuratorAttachmentAdapter.d.ts.map +1 -0
- package/dist/esm/CuratorAttachmentAdapter.js +128 -0
- package/dist/esm/CuratorAttachmentAdapter.js.map +1 -0
- package/dist/esm/CuratorChat.d.ts +80 -0
- package/dist/esm/CuratorChat.d.ts.map +1 -1
- package/dist/esm/CuratorChat.js +330 -52
- package/dist/esm/CuratorChat.js.map +1 -1
- package/dist/esm/index.d.ts +3 -1
- package/dist/esm/index.d.ts.map +1 -1
- package/dist/esm/index.js +3 -0
- package/dist/esm/index.js.map +1 -1
- package/package.json +3 -3
package/dist/esm/CuratorChat.js
CHANGED
|
@@ -17,8 +17,9 @@ import { createCuratorAdapter } from "./CuratorAdapter.js";
|
|
|
17
17
|
import { PoweredByCurator } from "./brand.js";
|
|
18
18
|
import { Markdown } from "./Markdown.js";
|
|
19
19
|
import { splitAttachmentEnvelope, formatAttachmentSize } from "./attachmentEnvelope.js";
|
|
20
|
+
import { CuratorAttachmentAdapter } from "./CuratorAttachmentAdapter.js";
|
|
20
21
|
export function CuratorChat(props) {
|
|
21
|
-
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, queueWhileRunning = true, } = props;
|
|
22
|
+
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;
|
|
22
23
|
const contextClient = useCuratorOrNull();
|
|
23
24
|
const client = curator ?? contextClient;
|
|
24
25
|
if (!client) {
|
|
@@ -31,8 +32,17 @@ export function CuratorChat(props) {
|
|
|
31
32
|
const a = client.agent(agent, { project, environment, system });
|
|
32
33
|
return conversationId ? a.conversation(conversationId) : a.chat();
|
|
33
34
|
}, [client, agent, project, environment, system, conversationId]);
|
|
34
|
-
|
|
35
|
-
|
|
35
|
+
// Rebuilt with the chat handle: the upload map is per-conversation, and a
|
|
36
|
+
// handle swap means the pending references belong to a transcript that is no
|
|
37
|
+
// longer on screen.
|
|
38
|
+
const attachmentAdapter = useMemo(() => enableAttachments
|
|
39
|
+
? new CuratorAttachmentAdapter(client, {
|
|
40
|
+
accept: acceptedFileTypes,
|
|
41
|
+
maxFileSize,
|
|
42
|
+
})
|
|
43
|
+
: undefined, [client, chat, enableAttachments, acceptedFileTypes, maxFileSize]);
|
|
44
|
+
const adapter = useMemo(() => createCuratorAdapter(chat, { onMessage, onState, onPlanProgress, onError }, attachmentAdapter), [chat, onMessage, onState, onPlanProgress, onError, attachmentAdapter]);
|
|
45
|
+
const runtime = useLocalRuntime(adapter, attachmentAdapter ? { adapters: { attachments: attachmentAdapter } } : undefined);
|
|
36
46
|
// Branding is a server-resolved entitlement (org subscription tier), not a
|
|
37
47
|
// client prop — the widget runs on the customer's own page, so a local
|
|
38
48
|
// boolean would be trivially flippable. We fetch the deployment config on
|
|
@@ -59,12 +69,21 @@ export function CuratorChat(props) {
|
|
|
59
69
|
cancelled = true;
|
|
60
70
|
};
|
|
61
71
|
}, [client, agent, project, environment, system]);
|
|
72
|
+
// Order is the contract: non-colour defaults, then the theme's palette, then
|
|
73
|
+
// the consumer's tokens, then the raw `style` prop — each able to override
|
|
74
|
+
// everything before it, with `style` still winning as it always has.
|
|
62
75
|
const palette = theme === "dark" ? DARK_VARS : LIGHT_VARS;
|
|
63
|
-
const wrapperStyle = {
|
|
64
|
-
|
|
76
|
+
const wrapperStyle = {
|
|
77
|
+
...BASE_TOKENS,
|
|
78
|
+
...palette,
|
|
79
|
+
...tokensToVars(tokens),
|
|
80
|
+
...containerStyle,
|
|
81
|
+
...style,
|
|
82
|
+
};
|
|
83
|
+
return (_jsxs("div", { className: className, style: wrapperStyle, "data-curator-chat": true, "data-theme": theme, children: [!hideHeader && _jsx(Header, { title: title, onClose: onClose }), _jsx(LoadingIndicatorContext.Provider, { value: loadingIndicator ?? _jsx(TypingDots, {}), children: _jsx(RetryEnabledContext.Provider, { value: enableRetry, children: _jsx(CopyEnabledContext.Provider, { value: enableCopy, children: _jsx(TimestampsEnabledContext.Provider, { value: showTimestamps, children: _jsx(AssistantRuntimeProvider, { runtime: runtime, children: _jsxs(ThreadPrimitive.Root, { "data-curator-part": "thread", style: threadRootStyle, children: [_jsxs(ThreadPrimitive.Viewport, { autoScroll: true, "data-curator-part": "viewport", style: viewportStyle, children: [_jsxs(ThreadPrimitive.Empty, { children: [welcomeMessage ? _jsx(Welcome, { text: welcomeMessage }) : null, prompts && prompts.length > 0 && _jsx(Prompts, { prompts: prompts })] }), _jsx(ThreadPrimitive.Messages, { components: {
|
|
65
84
|
UserMessage,
|
|
66
85
|
AssistantMessage,
|
|
67
|
-
} })] }), _jsx(Composer, { placeholder: placeholder, allowQueue: queueWhileRunning })] }) }) }) }) }) }), hidePoweredBy === false && _jsx(PoweredByCurator, {})] }));
|
|
86
|
+
} })] }), _jsx(Composer, { placeholder: placeholder, allowQueue: queueWhileRunning, allowAttachments: !!attachmentAdapter })] }) }) }) }) }) }), hidePoweredBy === false && _jsx(PoweredByCurator, {})] }));
|
|
68
87
|
}
|
|
69
88
|
// Context for threading the loading indicator into the message-component
|
|
70
89
|
// callbacks without remounting them on every render. Default is rendered
|
|
@@ -79,14 +98,44 @@ const CopyEnabledContext = createContext(false);
|
|
|
79
98
|
const TimestampsEnabledContext = createContext(true);
|
|
80
99
|
// ── header ──────────────────────────────────────────────────────
|
|
81
100
|
function Header({ title, onClose }) {
|
|
82
|
-
return (_jsxs("header", { style: headerStyle, children: [_jsx("h2", { style: titleStyle, children: title }), onClose && (_jsx("button", { type: "button", onClick: onClose, "aria-label": "Close chat", style: headerActionStyle, children: _jsx(HeaderCloseGlyph, {}) }))] }));
|
|
101
|
+
return (_jsxs("header", { "data-curator-part": "header", style: headerStyle, children: [_jsx("h2", { "data-curator-part": "title", style: titleStyle, children: title }), onClose && (_jsx("button", { type: "button", onClick: onClose, "aria-label": "Close chat", "data-curator-part": "header-action", style: headerActionStyle, children: _jsx(HeaderCloseGlyph, {}) }))] }));
|
|
83
102
|
}
|
|
84
103
|
// ── message primitives (custom UI on top of assistant-ui state) ─
|
|
85
104
|
function Welcome({ text }) {
|
|
86
|
-
return _jsx("div", { style: welcomeStyle, children: text });
|
|
105
|
+
return _jsx("div", { "data-curator-part": "welcome", style: welcomeStyle, children: text });
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Starter prompts for an empty thread.
|
|
109
|
+
*
|
|
110
|
+
* Built on `ThreadPrimitive.Suggestion` rather than a plain button so the send
|
|
111
|
+
* goes through the same path as the composer's — including the case where one
|
|
112
|
+
* is clicked while a run is already in flight, which the primitive queues
|
|
113
|
+
* instead of dropping.
|
|
114
|
+
*/
|
|
115
|
+
function Prompts({ prompts }) {
|
|
116
|
+
return (_jsx("div", { "data-curator-part": "prompts", style: promptsStyle, children: prompts.map((p, i) => {
|
|
117
|
+
const label = typeof p === "string" ? p : p.label;
|
|
118
|
+
const prompt = typeof p === "string" ? p : (p.prompt ?? p.label);
|
|
119
|
+
return (_jsx(ThreadPrimitive.Suggestion, { prompt: prompt, send: true, "data-curator-part": "prompt", style: promptStyle,
|
|
120
|
+
// The message differs from the button when the object form is used,
|
|
121
|
+
// so the full text has to be reachable without clicking.
|
|
122
|
+
title: prompt !== label ? prompt : undefined, children: label }, `${label}-${i}`));
|
|
123
|
+
}) }));
|
|
87
124
|
}
|
|
88
125
|
function UserMessage() {
|
|
89
|
-
|
|
126
|
+
// Attachments as the message carries them structurally. The envelope path in
|
|
127
|
+
// `UserTextPart` is the fallback for transcripts written before the server
|
|
128
|
+
// stored them as a field; both render the same chip.
|
|
129
|
+
const attachments = useAuiState((s) => s.message?.attachments) ?? [];
|
|
130
|
+
const hasText = useAuiState((s) => s.message?.content?.some((p) => p.type === "text" && p.text.length > 0) ?? false);
|
|
131
|
+
return (_jsxs(MessagePrimitive.Root, { "data-curator-part": "message message-user", style: rowEndStyle, children: [_jsxs("div", { "data-curator-part": "bubble bubble-user", style: { ...bubbleBaseStyle, ...userBubbleStyle }, children: [attachments.map((a, i) => (_jsx(SentAttachmentChip, { name: a.name,
|
|
132
|
+
// A file sent with no caption is the common case; the chip is then
|
|
133
|
+
// the whole bubble and must not carry a top margin.
|
|
134
|
+
first: i === 0 && !hasText }, a.id))), _jsx(MessagePrimitive.Parts, { components: { Text: UserTextPart } })] }), _jsx(Timestamp, { align: "end" })] }));
|
|
135
|
+
}
|
|
136
|
+
/** One attachment on a sent message. Shared by the structural and envelope paths. */
|
|
137
|
+
function SentAttachmentChip({ name, size, mimeType, first, }) {
|
|
138
|
+
return (_jsxs("span", { "data-curator-part": "attachment-chip", style: { ...attachmentChipStyle, marginTop: first ? 0 : 6 }, title: mimeType ? `${name} (${mimeType})` : name, children: [_jsx(PaperclipGlyph, {}), _jsx("span", { "data-curator-part": "attachment-name", style: attachmentNameStyle, children: name }), size !== undefined && size > 0 && (_jsx("span", { "data-curator-part": "attachment-meta", style: attachmentMetaStyle, children: formatAttachmentSize(size) }))] }));
|
|
90
139
|
}
|
|
91
140
|
/**
|
|
92
141
|
* Local-time stamp under a message.
|
|
@@ -104,7 +153,7 @@ function Timestamp({ align }) {
|
|
|
104
153
|
const date = createdAt instanceof Date ? createdAt : new Date(createdAt);
|
|
105
154
|
if (Number.isNaN(date.getTime()))
|
|
106
155
|
return null;
|
|
107
|
-
return (_jsx("time", { dateTime: date.toISOString(), title: date.toLocaleString(), style: {
|
|
156
|
+
return (_jsx("time", { "data-curator-part": "timestamp", dateTime: date.toISOString(), title: date.toLocaleString(), style: {
|
|
108
157
|
...timestampStyle,
|
|
109
158
|
alignSelf: align === "end" ? "flex-end" : "flex-start",
|
|
110
159
|
}, children: date.toLocaleTimeString(undefined, { hour: "numeric", minute: "2-digit" }) }));
|
|
@@ -115,10 +164,7 @@ const UserTextPart = ({ text }) => {
|
|
|
115
164
|
// in the message, but showing it to the person who sent the message is a leak
|
|
116
165
|
// and it is what renders as a wall of JSON. See attachmentEnvelope.ts.
|
|
117
166
|
const { text: visible, attachments } = splitAttachmentEnvelope(text);
|
|
118
|
-
return (_jsxs(_Fragment, { children: [visible, attachments.map((a, i) => (
|
|
119
|
-
// The name can be long; the tooltip gives the full value without
|
|
120
|
-
// widening the bubble.
|
|
121
|
-
title: `${a.filename} (${a.mimeType})`, children: [_jsx(PaperclipGlyph, {}), _jsx("span", { style: attachmentNameStyle, children: a.filename }), a.size > 0 && (_jsx("span", { style: attachmentMetaStyle, children: formatAttachmentSize(a.size) }))] }, `${a.filename}-${i}`)))] }));
|
|
167
|
+
return (_jsxs(_Fragment, { children: [visible, attachments.map((a, i) => (_jsx(SentAttachmentChip, { name: a.filename, size: a.size, mimeType: a.mimeType, first: !visible && i === 0 }, `${a.filename}-${i}`)))] }));
|
|
122
168
|
};
|
|
123
169
|
/** Small paperclip, sized to the chip's line-height. */
|
|
124
170
|
function PaperclipGlyph() {
|
|
@@ -128,10 +174,10 @@ function AssistantMessage() {
|
|
|
128
174
|
const indicator = useContext(LoadingIndicatorContext);
|
|
129
175
|
const retryEnabled = useContext(RetryEnabledContext);
|
|
130
176
|
const copyEnabled = useContext(CopyEnabledContext);
|
|
131
|
-
return (_jsxs(MessagePrimitive.Root, { style: rowStartStyle, children: [_jsxs("div", { style: { ...bubbleBaseStyle, ...assistantBubbleStyle }, children: [_jsx(MessagePrimitive.If, { hasContent: false, children: indicator }), _jsx(MessagePrimitive.Parts, { components: { Text: AssistantTextPart } })] }), _jsx(Timestamp, { align: "start" }), (retryEnabled || copyEnabled) && (
|
|
177
|
+
return (_jsxs(MessagePrimitive.Root, { "data-curator-part": "message message-assistant", style: rowStartStyle, children: [_jsxs("div", { "data-curator-part": "bubble bubble-assistant", style: { ...bubbleBaseStyle, ...assistantBubbleStyle }, children: [_jsx(MessagePrimitive.If, { hasContent: false, children: indicator }), _jsx(MessagePrimitive.Parts, { components: { Text: AssistantTextPart } })] }), _jsx(Timestamp, { align: "start" }), (retryEnabled || copyEnabled) && (
|
|
132
178
|
// `autohide="not-last"` shows the bar only on the trailing assistant
|
|
133
179
|
// message; `hideWhenRunning` keeps it away while a reply streams.
|
|
134
|
-
_jsxs(ActionBarPrimitive.Root, { autohide: "not-last", hideWhenRunning: true, style: actionBarStyle, children: [copyEnabled && (_jsxs(ActionBarPrimitive.Copy, { "aria-label": "Copy", style: actionButtonStyle, children: [_jsx(MessagePrimitive.If, { copied: true, children: _jsx(CheckGlyph, {}) }), _jsx(MessagePrimitive.If, { copied: false, children: _jsx(CopyGlyph, {}) })] })), retryEnabled && (_jsx(ActionBarPrimitive.Reload, { "aria-label": "Retry", style: actionButtonStyle, children: _jsx(RetryGlyph, {}) }))] }))] }));
|
|
180
|
+
_jsxs(ActionBarPrimitive.Root, { autohide: "not-last", hideWhenRunning: true, "data-curator-part": "action-bar", style: actionBarStyle, children: [copyEnabled && (_jsxs(ActionBarPrimitive.Copy, { "aria-label": "Copy", "data-curator-part": "action-button", style: actionButtonStyle, children: [_jsx(MessagePrimitive.If, { copied: true, children: _jsx(CheckGlyph, {}) }), _jsx(MessagePrimitive.If, { copied: false, children: _jsx(CopyGlyph, {}) })] })), retryEnabled && (_jsx(ActionBarPrimitive.Reload, { "aria-label": "Retry", "data-curator-part": "action-button", style: actionButtonStyle, children: _jsx(RetryGlyph, {}) }))] }))] }));
|
|
135
181
|
}
|
|
136
182
|
/**
|
|
137
183
|
* Waiting state for a reply that has not started streaming.
|
|
@@ -143,7 +189,7 @@ function AssistantMessage() {
|
|
|
143
189
|
* the indicator still says "busy" without the movement.
|
|
144
190
|
*/
|
|
145
191
|
function TypingDots() {
|
|
146
|
-
return (_jsxs("span", { style: typingDotsStyle, role: "status", "aria-label": "Assistant is thinking", children: [_jsx(Dot, { delay: 0 }), _jsx(Dot, { delay: 160 }), _jsx(Dot, { delay: 320 })] }));
|
|
192
|
+
return (_jsxs("span", { "data-curator-part": "typing-dots", style: typingDotsStyle, role: "status", "aria-label": "Assistant is thinking", children: [_jsx(Dot, { delay: 0 }), _jsx(Dot, { delay: 160 }), _jsx(Dot, { delay: 320 })] }));
|
|
147
193
|
}
|
|
148
194
|
function Dot({ delay }) {
|
|
149
195
|
return (_jsx("span", { className: "curator-dot", style: {
|
|
@@ -158,12 +204,18 @@ function Dot({ delay }) {
|
|
|
158
204
|
}
|
|
159
205
|
const AssistantTextPart = ({ text }) => (_jsx(Markdown, { content: text }));
|
|
160
206
|
// ── composer ────────────────────────────────────────────────────
|
|
161
|
-
function Composer({ placeholder, allowQueue, }) {
|
|
207
|
+
function Composer({ placeholder, allowQueue, allowAttachments, }) {
|
|
162
208
|
const aui = useAui();
|
|
163
209
|
const isRunning = useAuiState((s) => s.thread.isRunning);
|
|
210
|
+
// Composer attachments live in assistant-ui's state, not ours — the runtime
|
|
211
|
+
// owns the upload lifecycle, so reading it here keeps one source of truth.
|
|
212
|
+
// State is read-only here; mutations go through `aui.thread.composer()`,
|
|
213
|
+
// which is a factory rather than an object — hence the call.
|
|
214
|
+
const attachments = useAuiState((s) => s.composer?.attachments) ?? [];
|
|
164
215
|
const [text, setText] = useState("");
|
|
165
216
|
const [queued, setQueued] = useState(null);
|
|
166
217
|
const taRef = useRef(null);
|
|
218
|
+
const fileRef = useRef(null);
|
|
167
219
|
// Guards the window between dispatching a queued message and `isRunning`
|
|
168
220
|
// flipping back to true. Without it the flush effect sees "not running" on
|
|
169
221
|
// the very next render and sends the same text again.
|
|
@@ -186,7 +238,9 @@ function Composer({ placeholder, allowQueue, }) {
|
|
|
186
238
|
}, [isRunning, queued, send]);
|
|
187
239
|
const submit = useCallback(() => {
|
|
188
240
|
const value = text.trim();
|
|
189
|
-
|
|
241
|
+
// An attachment with no caption is a complete message. Requiring text here
|
|
242
|
+
// is what made a dropped document impossible to send.
|
|
243
|
+
if (!value && attachments.length === 0)
|
|
190
244
|
return;
|
|
191
245
|
setText("");
|
|
192
246
|
if (taRef.current)
|
|
@@ -199,7 +253,7 @@ function Composer({ placeholder, allowQueue, }) {
|
|
|
199
253
|
return;
|
|
200
254
|
}
|
|
201
255
|
send(value);
|
|
202
|
-
}, [text, isRunning, allowQueue, send]);
|
|
256
|
+
}, [text, attachments.length, isRunning, allowQueue, send]);
|
|
203
257
|
const onKeyDown = (e) => {
|
|
204
258
|
if (e.key === "Enter" && !e.shiftKey && !e.nativeEvent.isComposing) {
|
|
205
259
|
e.preventDefault();
|
|
@@ -215,15 +269,41 @@ function Composer({ placeholder, allowQueue, }) {
|
|
|
215
269
|
// `disabled` is never set on the input: being able to type mid-reply is the
|
|
216
270
|
// point. Send is disabled only when there is nothing to send, or when a
|
|
217
271
|
// message is already held.
|
|
218
|
-
const canSubmit = text.trim().length > 0
|
|
219
|
-
|
|
272
|
+
const canSubmit = (text.trim().length > 0 || attachments.length > 0) &&
|
|
273
|
+
!(isRunning && allowQueue && queued !== null);
|
|
274
|
+
const onPickFiles = async (files) => {
|
|
275
|
+
if (!files)
|
|
276
|
+
return;
|
|
277
|
+
// Sequential rather than parallel: `addAttachment` mutates composer state,
|
|
278
|
+
// and the adapter's per-file rejection message is easier to attribute when
|
|
279
|
+
// they arrive in order.
|
|
280
|
+
for (const file of Array.from(files)) {
|
|
281
|
+
try {
|
|
282
|
+
await aui.thread.composer().addAttachment(file);
|
|
283
|
+
}
|
|
284
|
+
catch {
|
|
285
|
+
// The adapter reports rejection on the chip; a throw here would take
|
|
286
|
+
// the rest of the selection down with it.
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
if (fileRef.current)
|
|
290
|
+
fileRef.current.value = "";
|
|
291
|
+
};
|
|
292
|
+
return (_jsxs("div", { "data-curator-part": "composer", style: composerWrapStyle, children: [queued !== null && (_jsxs("div", { "data-curator-part": "queued-row", style: queuedRowStyle, children: [_jsx("span", { "data-curator-part": "queued-label", style: queuedLabelStyle, children: "Sending next" }), _jsx("span", { "data-curator-part": "queued-text", style: queuedTextStyle, title: queued, children: queued }), _jsx("button", { type: "button", onClick: () => setQueued(null), "aria-label": "Cancel queued message", "data-curator-part": "queued-cancel", style: queuedCancelStyle, children: _jsx(HeaderCloseGlyph, {}) })] })), attachments.length > 0 && (_jsx("div", { "data-curator-part": "pending-attachments", style: pendingRowStyle, children: attachments.map((a) => {
|
|
293
|
+
const failed = a.status?.type === "incomplete";
|
|
294
|
+
const uploading = a.status?.type === "running";
|
|
295
|
+
return (_jsxs("span", { "data-curator-part": "attachment-chip attachment-chip-pending", "data-curator-status": a.status?.type, style: {
|
|
296
|
+
...attachmentChipStyle,
|
|
297
|
+
...(failed ? { color: "var(--curator-error)" } : {}),
|
|
298
|
+
}, title: a.status?.type === "incomplete" ? a.status.message ?? a.name : a.name, children: [_jsx(PaperclipGlyph, {}), _jsx("span", { "data-curator-part": "attachment-name", style: attachmentNameStyle, children: a.name }), uploading && _jsx("span", { "data-curator-part": "attachment-meta", style: attachmentMetaStyle, children: "uploading\u2026" }), failed && _jsx("span", { "data-curator-part": "attachment-meta", style: attachmentMetaStyle, children: "failed" }), _jsx("button", { type: "button", onClick: () => void aui.thread.composer().attachment({ id: a.id }).remove(), "aria-label": `Remove ${a.name}`, "data-curator-part": "chip-remove", style: chipRemoveStyle, children: _jsx(HeaderCloseGlyph, {}) })] }, a.id));
|
|
299
|
+
}) })), _jsxs("div", { "data-curator-part": "composer-root", style: composerRootStyle, children: [allowAttachments && (_jsxs(_Fragment, { children: [_jsx("input", { ref: fileRef, type: "file", multiple: true, onChange: (e) => void onPickFiles(e.target.files), style: { display: "none" }, "aria-hidden": "true", tabIndex: -1 }), _jsx("button", { type: "button", onClick: () => fileRef.current?.click(), "aria-label": "Attach a file", "data-curator-part": "attach-button", style: attachButtonStyle, children: _jsx(PaperclipGlyph, {}) })] })), _jsx("textarea", { ref: (el) => {
|
|
220
300
|
taRef.current = el;
|
|
221
301
|
if (el)
|
|
222
302
|
autosize(el);
|
|
223
303
|
}, value: text, onChange: (e) => {
|
|
224
304
|
setText(e.target.value);
|
|
225
305
|
autosize(e.target);
|
|
226
|
-
}, onKeyDown: onKeyDown, placeholder: placeholder, rows: 1, style: textareaStyle, "aria-label": "Message" }), _jsx("button", { type: "button", onClick: submit, disabled: !canSubmit, "aria-label": isRunning && allowQueue ? "Queue message" : "Send", style: sendButtonStyle, children: _jsx(SendGlyph, {}) })] })] }));
|
|
306
|
+
}, onKeyDown: onKeyDown, placeholder: placeholder, rows: 1, "data-curator-part": "composer-input", style: textareaStyle, "aria-label": "Message" }), _jsx("button", { type: "button", onClick: submit, disabled: !canSubmit, "aria-label": isRunning && allowQueue ? "Queue message" : "Send", "data-curator-part": "send-button", style: sendButtonStyle, children: _jsx(SendGlyph, {}) })] })] }));
|
|
227
307
|
}
|
|
228
308
|
// ── glyphs ──────────────────────────────────────────────────────
|
|
229
309
|
function HeaderCloseGlyph() {
|
|
@@ -242,16 +322,126 @@ function CheckGlyph() {
|
|
|
242
322
|
return (_jsx("svg", { width: "13", height: "13", viewBox: "0 0 24 24", fill: "none", "aria-hidden": "true", children: _jsx("path", { d: "m5 13 4 4L19 7", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round" }) }));
|
|
243
323
|
}
|
|
244
324
|
// ── design tokens ───────────────────────────────────────────────
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
325
|
+
/**
|
|
326
|
+
* Every value below is a CSS variable reference, not a number.
|
|
327
|
+
*
|
|
328
|
+
* They used to be module constants baked into 27 inline style objects, which
|
|
329
|
+
* made typography, spacing and two of the three radii unreachable from outside
|
|
330
|
+
* the package: a widget whose font did not match the surrounding site could
|
|
331
|
+
* only be fixed by forking. Referencing variables here means the defaults ship
|
|
332
|
+
* in `BASE_TOKENS` and a consumer overrides any of them — via the `tokens`
|
|
333
|
+
* prop, or plain CSS on `[data-curator-chat]`.
|
|
334
|
+
*
|
|
335
|
+
* The `var(--x, fallback)` form is deliberate: the widget still renders
|
|
336
|
+
* correctly if the container's variables are stripped by an aggressive CSS
|
|
337
|
+
* reset on the host page.
|
|
338
|
+
*/
|
|
339
|
+
const SPACE_2 = "var(--curator-space-2, 8px)";
|
|
340
|
+
const SPACE_3 = "var(--curator-space-3, 12px)";
|
|
341
|
+
const SPACE_4 = "var(--curator-space-4, 16px)";
|
|
342
|
+
const FONT_BASE = "var(--curator-font-size, 14px)";
|
|
343
|
+
const FONT_SM = "var(--curator-font-size-sm, 12px)";
|
|
250
344
|
/** Timestamps only — small enough to recede, large enough to stay legible. */
|
|
251
|
-
const FONT_XS =
|
|
252
|
-
const RADIUS_LG =
|
|
253
|
-
const RADIUS_MD =
|
|
254
|
-
const RADIUS_SM =
|
|
345
|
+
const FONT_XS = "var(--curator-font-size-xs, 11px)";
|
|
346
|
+
const RADIUS_LG = "var(--curator-radius, 14px)";
|
|
347
|
+
const RADIUS_MD = "var(--curator-radius-md, 10px)";
|
|
348
|
+
const RADIUS_SM = "var(--curator-radius-sm, 8px)";
|
|
349
|
+
/**
|
|
350
|
+
* Defaults for everything that is not a colour. Colours live in the palettes
|
|
351
|
+
* below because they are the one axis that genuinely differs between light and
|
|
352
|
+
* dark; these do not.
|
|
353
|
+
*/
|
|
354
|
+
const BASE_TOKENS = {
|
|
355
|
+
["--curator-font-family"]: "ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif",
|
|
356
|
+
["--curator-font-size"]: "14px",
|
|
357
|
+
["--curator-font-size-sm"]: "12px",
|
|
358
|
+
["--curator-font-size-xs"]: "11px",
|
|
359
|
+
["--curator-line-height"]: "1.5",
|
|
360
|
+
["--curator-font-weight-strong"]: "600",
|
|
361
|
+
["--curator-space-1"]: "4px",
|
|
362
|
+
["--curator-space-2"]: "8px",
|
|
363
|
+
["--curator-space-3"]: "12px",
|
|
364
|
+
["--curator-space-4"]: "16px",
|
|
365
|
+
["--curator-radius-md"]: "10px",
|
|
366
|
+
["--curator-radius-sm"]: "8px",
|
|
367
|
+
};
|
|
368
|
+
/**
|
|
369
|
+
* Consumer-facing token names, mapped to the variables they set.
|
|
370
|
+
*
|
|
371
|
+
* Complete for the axes a consumer can be expected to want: the whole type
|
|
372
|
+
* scale, the whole spacing scale, every colour surface, and the radii. An
|
|
373
|
+
* earlier version exposed only `fontSize` and `radius`, which split each scale
|
|
374
|
+
* down the middle — half reachable by prop, half only by stylesheet. A partial
|
|
375
|
+
* scale is worse than none, because it lets someone change a base size and then
|
|
376
|
+
* discover the secondary sizes did not follow.
|
|
377
|
+
*/
|
|
378
|
+
const TOKEN_VARS = {
|
|
379
|
+
fontFamily: "--curator-font-family",
|
|
380
|
+
fontSize: "--curator-font-size",
|
|
381
|
+
// The rest of the type scale. Exposing `fontSize` alone let a host set a base
|
|
382
|
+
// size while the secondary sizes stayed pinned at 12/11px, which is a worse
|
|
383
|
+
// mismatch than not being able to change any of them.
|
|
384
|
+
fontSizeSm: "--curator-font-size-sm",
|
|
385
|
+
fontSizeXs: "--curator-font-size-xs",
|
|
386
|
+
fontWeightStrong: "--curator-font-weight-strong",
|
|
387
|
+
lineHeight: "--curator-line-height",
|
|
388
|
+
// Density. Every padding and gap in the widget derives from these four.
|
|
389
|
+
space1: "--curator-space-1",
|
|
390
|
+
space2: "--curator-space-2",
|
|
391
|
+
space3: "--curator-space-3",
|
|
392
|
+
space4: "--curator-space-4",
|
|
393
|
+
radius: "--curator-radius",
|
|
394
|
+
radiusMd: "--curator-radius-md",
|
|
395
|
+
radiusSm: "--curator-radius-sm",
|
|
396
|
+
background: "--curator-bg",
|
|
397
|
+
surface: "--curator-surface",
|
|
398
|
+
text: "--curator-text",
|
|
399
|
+
muted: "--curator-muted",
|
|
400
|
+
border: "--curator-border",
|
|
401
|
+
accent: "--curator-accent",
|
|
402
|
+
accentText: "--curator-accent-text",
|
|
403
|
+
userBubble: "--curator-user-bubble",
|
|
404
|
+
userBubbleText: "--curator-user-bubble-text",
|
|
405
|
+
assistantBubble: "--curator-assistant-bubble",
|
|
406
|
+
assistantBubbleText: "--curator-assistant-bubble-text",
|
|
407
|
+
headerBackground: "--curator-header-bg",
|
|
408
|
+
composerBackground: "--curator-composer-bg",
|
|
409
|
+
focusRing: "--curator-focus-ring",
|
|
410
|
+
error: "--curator-error",
|
|
411
|
+
};
|
|
412
|
+
/**
|
|
413
|
+
* Turn a `tokens` object into inline custom properties.
|
|
414
|
+
*
|
|
415
|
+
* Numbers become `px` because every numeric token here is a length, and
|
|
416
|
+
* `radius: 8` reads better at the call site than `radius: "8px"`. A string is
|
|
417
|
+
* passed through untouched so `radius: "0.5rem"` still works.
|
|
418
|
+
*/
|
|
419
|
+
function tokensToVars(tokens) {
|
|
420
|
+
if (!tokens)
|
|
421
|
+
return {};
|
|
422
|
+
const out = {};
|
|
423
|
+
for (const [key, value] of Object.entries(tokens)) {
|
|
424
|
+
const cssVar = TOKEN_VARS[key];
|
|
425
|
+
if (!cssVar || value === undefined)
|
|
426
|
+
continue;
|
|
427
|
+
out[cssVar] = typeof value === "number" ? `${value}px` : value;
|
|
428
|
+
}
|
|
429
|
+
return out;
|
|
430
|
+
}
|
|
431
|
+
/**
|
|
432
|
+
* Accent on the USER bubble, neutral on the assistant's.
|
|
433
|
+
*
|
|
434
|
+
* The reverse of what this widget shipped with, and the reverse of what the
|
|
435
|
+
* variable names imply — the assistant bubble used to borrow `--curator-accent`
|
|
436
|
+
* directly. Two problems with that. The assistant writes most of the words, so
|
|
437
|
+
* painting its bubble in the accent colour turns every substantial reply into a
|
|
438
|
+
* slab of saturated colour and makes the machine the loudest thing on screen.
|
|
439
|
+
* And it inverts the convention every messaging UI has taught people to read,
|
|
440
|
+
* where the accent marks *your* messages.
|
|
441
|
+
*
|
|
442
|
+
* Both are now separate variables, so a consumer who preferred the old
|
|
443
|
+
* arrangement sets `assistantBubble` / `userBubble` back.
|
|
444
|
+
*/
|
|
255
445
|
const LIGHT_VARS = {
|
|
256
446
|
["--curator-bg"]: "#ffffff",
|
|
257
447
|
["--curator-surface"]: "#f8fafc",
|
|
@@ -260,9 +450,15 @@ const LIGHT_VARS = {
|
|
|
260
450
|
["--curator-border"]: "rgba(15, 23, 42, 0.08)",
|
|
261
451
|
["--curator-accent"]: "#0f172a",
|
|
262
452
|
["--curator-accent-text"]: "#ffffff",
|
|
263
|
-
["--curator-user-bubble"]: "#
|
|
453
|
+
["--curator-user-bubble"]: "#0f172a",
|
|
454
|
+
["--curator-user-bubble-text"]: "#ffffff",
|
|
455
|
+
["--curator-assistant-bubble"]: "#f1f5f9",
|
|
456
|
+
["--curator-assistant-bubble-text"]: "#0f172a",
|
|
457
|
+
["--curator-header-bg"]: "transparent",
|
|
458
|
+
["--curator-composer-bg"]: "transparent",
|
|
459
|
+
["--curator-focus-ring"]: "rgba(15, 23, 42, 0.16)",
|
|
264
460
|
["--curator-error"]: "#b91c1c",
|
|
265
|
-
["--curator-radius"]:
|
|
461
|
+
["--curator-radius"]: "14px",
|
|
266
462
|
};
|
|
267
463
|
const DARK_VARS = {
|
|
268
464
|
["--curator-bg"]: "#0b1220",
|
|
@@ -272,9 +468,17 @@ const DARK_VARS = {
|
|
|
272
468
|
["--curator-border"]: "rgba(226, 232, 240, 0.1)",
|
|
273
469
|
["--curator-accent"]: "#e2e8f0",
|
|
274
470
|
["--curator-accent-text"]: "#0b1220",
|
|
275
|
-
|
|
471
|
+
// Dark mode keeps the same relationship: the user's bubble is the lighter,
|
|
472
|
+
// more saturated one; the assistant sits on a raised neutral surface.
|
|
473
|
+
["--curator-user-bubble"]: "#e2e8f0",
|
|
474
|
+
["--curator-user-bubble-text"]: "#0b1220",
|
|
475
|
+
["--curator-assistant-bubble"]: "#1f2937",
|
|
476
|
+
["--curator-assistant-bubble-text"]: "#e2e8f0",
|
|
477
|
+
["--curator-header-bg"]: "transparent",
|
|
478
|
+
["--curator-composer-bg"]: "transparent",
|
|
479
|
+
["--curator-focus-ring"]: "rgba(226, 232, 240, 0.2)",
|
|
276
480
|
["--curator-error"]: "#fca5a5",
|
|
277
|
-
["--curator-radius"]:
|
|
481
|
+
["--curator-radius"]: "14px",
|
|
278
482
|
};
|
|
279
483
|
const containerStyle = {
|
|
280
484
|
display: "flex",
|
|
@@ -287,9 +491,9 @@ const containerStyle = {
|
|
|
287
491
|
border: "1px solid var(--curator-border)",
|
|
288
492
|
borderRadius: "var(--curator-radius)",
|
|
289
493
|
overflow: "hidden",
|
|
290
|
-
fontFamily: "
|
|
494
|
+
fontFamily: "var(--curator-font-family)",
|
|
291
495
|
fontSize: FONT_BASE,
|
|
292
|
-
lineHeight: 1.5,
|
|
496
|
+
lineHeight: "var(--curator-line-height, 1.5)",
|
|
293
497
|
boxSizing: "border-box",
|
|
294
498
|
};
|
|
295
499
|
const headerStyle = {
|
|
@@ -342,15 +546,39 @@ const viewportStyle = {
|
|
|
342
546
|
// clips rather than growing a horizontal scrollbar across the whole thread.
|
|
343
547
|
overflowX: "hidden",
|
|
344
548
|
minWidth: 0,
|
|
345
|
-
padding: `${SPACE_4}
|
|
549
|
+
padding: `${SPACE_4} ${SPACE_4} ${SPACE_3}`,
|
|
346
550
|
display: "flex",
|
|
347
551
|
flexDirection: "column",
|
|
348
|
-
gap:
|
|
552
|
+
gap: SPACE_3,
|
|
349
553
|
};
|
|
350
554
|
const welcomeStyle = {
|
|
351
555
|
color: "var(--curator-muted)",
|
|
352
|
-
fontSize:
|
|
353
|
-
padding: `${SPACE_2}
|
|
556
|
+
fontSize: FONT_SM,
|
|
557
|
+
padding: `${SPACE_2} 0`,
|
|
558
|
+
};
|
|
559
|
+
const promptsStyle = {
|
|
560
|
+
display: "flex",
|
|
561
|
+
flexWrap: "wrap",
|
|
562
|
+
gap: 6,
|
|
563
|
+
// Sits under the welcome line rather than beside it: the welcome says what
|
|
564
|
+
// this is, the prompts say what to do, and that reads as a sequence.
|
|
565
|
+
paddingTop: 2,
|
|
566
|
+
};
|
|
567
|
+
const promptStyle = {
|
|
568
|
+
// Outlined, not filled. A filled chip competes with the send button for
|
|
569
|
+
// "primary action", and there are several of these — a row of solid accent
|
|
570
|
+
// blocks in an otherwise empty panel is the loudest thing on the screen.
|
|
571
|
+
border: "1px solid var(--curator-border)",
|
|
572
|
+
borderRadius: RADIUS_SM,
|
|
573
|
+
background: "transparent",
|
|
574
|
+
color: "var(--curator-text)",
|
|
575
|
+
fontFamily: "inherit",
|
|
576
|
+
fontSize: FONT_SM,
|
|
577
|
+
lineHeight: 1.3,
|
|
578
|
+
padding: `6px ${SPACE_2}`,
|
|
579
|
+
cursor: "pointer",
|
|
580
|
+
textAlign: "left",
|
|
581
|
+
maxWidth: "100%",
|
|
354
582
|
};
|
|
355
583
|
const rowStartStyle = {
|
|
356
584
|
// Column so the retry action bar can stack beneath the bubble, both
|
|
@@ -362,12 +590,18 @@ const rowStartStyle = {
|
|
|
362
590
|
minWidth: 0,
|
|
363
591
|
};
|
|
364
592
|
const rowEndStyle = {
|
|
593
|
+
// Column, mirroring the assistant row. It was a plain row, which made the
|
|
594
|
+
// bubble and the timestamp siblings on one line — so a user message rendered
|
|
595
|
+
// as "hello7:19 AM", the stamp jammed against the bubble with no gap, while
|
|
596
|
+
// the assistant's sat neatly underneath. The two sides should differ in
|
|
597
|
+
// alignment, not in structure.
|
|
365
598
|
display: "flex",
|
|
599
|
+
flexDirection: "column",
|
|
600
|
+
alignItems: "flex-end",
|
|
366
601
|
width: "100%",
|
|
367
602
|
// Same `min-width: auto` trap as the bubble: without this the row itself
|
|
368
603
|
// refuses to shrink and the cap on the bubble inside it never binds.
|
|
369
604
|
minWidth: 0,
|
|
370
|
-
justifyContent: "flex-end",
|
|
371
605
|
};
|
|
372
606
|
const actionBarStyle = {
|
|
373
607
|
display: "flex",
|
|
@@ -401,7 +635,7 @@ const bubbleBaseStyle = {
|
|
|
401
635
|
// and the whole thread scrolls sideways. This is the fix for that; the
|
|
402
636
|
// wrapping rules below only get a chance to apply once the box can shrink.
|
|
403
637
|
minWidth: 0,
|
|
404
|
-
padding:
|
|
638
|
+
padding: `calc(${SPACE_2} + 2px) ${SPACE_3}`,
|
|
405
639
|
borderRadius: RADIUS_MD,
|
|
406
640
|
whiteSpace: "pre-wrap",
|
|
407
641
|
// `overflow-wrap: anywhere` is the standard property and it breaks inside an
|
|
@@ -429,7 +663,7 @@ const attachmentChipStyle = {
|
|
|
429
663
|
// Tinted from the bubble's own text colour so the chip reads as part of the
|
|
430
664
|
// message in either theme, without needing its own palette token.
|
|
431
665
|
background: "color-mix(in srgb, currentColor 10%, transparent)",
|
|
432
|
-
fontSize:
|
|
666
|
+
fontSize: FONT_SM,
|
|
433
667
|
lineHeight: 1.4,
|
|
434
668
|
verticalAlign: "middle",
|
|
435
669
|
};
|
|
@@ -444,12 +678,12 @@ const attachmentMetaStyle = {
|
|
|
444
678
|
};
|
|
445
679
|
const userBubbleStyle = {
|
|
446
680
|
background: "var(--curator-user-bubble)",
|
|
447
|
-
color: "var(--curator-text)",
|
|
681
|
+
color: "var(--curator-user-bubble-text, var(--curator-text))",
|
|
448
682
|
borderBottomRightRadius: 4,
|
|
449
683
|
};
|
|
450
684
|
const assistantBubbleStyle = {
|
|
451
|
-
background: "var(--curator-accent)",
|
|
452
|
-
color: "var(--curator-accent-text)",
|
|
685
|
+
background: "var(--curator-assistant-bubble, var(--curator-accent))",
|
|
686
|
+
color: "var(--curator-assistant-bubble-text, var(--curator-accent-text))",
|
|
453
687
|
borderBottomLeftRadius: 4,
|
|
454
688
|
};
|
|
455
689
|
const typingDotsStyle = {
|
|
@@ -470,7 +704,7 @@ const queuedRowStyle = {
|
|
|
470
704
|
display: "flex",
|
|
471
705
|
alignItems: "center",
|
|
472
706
|
gap: SPACE_2,
|
|
473
|
-
padding: `${SPACE_2}
|
|
707
|
+
padding: `${SPACE_2} ${SPACE_3} 0`,
|
|
474
708
|
fontSize: FONT_SM,
|
|
475
709
|
color: "var(--curator-muted)",
|
|
476
710
|
minWidth: 0,
|
|
@@ -503,15 +737,55 @@ const timestampStyle = {
|
|
|
503
737
|
fontSize: FONT_XS,
|
|
504
738
|
color: "var(--curator-muted)",
|
|
505
739
|
opacity: 0.7,
|
|
506
|
-
marginTop:
|
|
740
|
+
marginTop: 4,
|
|
507
741
|
padding: "0 2px",
|
|
508
742
|
userSelect: "none",
|
|
509
743
|
};
|
|
744
|
+
/** Pending attachments sit above the input, on their own row, like the queue row. */
|
|
745
|
+
const pendingRowStyle = {
|
|
746
|
+
display: "flex",
|
|
747
|
+
flexWrap: "wrap",
|
|
748
|
+
gap: 6,
|
|
749
|
+
paddingLeft: SPACE_3,
|
|
750
|
+
paddingRight: SPACE_3,
|
|
751
|
+
paddingBottom: 6,
|
|
752
|
+
};
|
|
753
|
+
/** Sized to match the send button so the composer's two ends balance. */
|
|
754
|
+
const attachButtonStyle = {
|
|
755
|
+
display: "inline-flex",
|
|
756
|
+
alignItems: "center",
|
|
757
|
+
justifyContent: "center",
|
|
758
|
+
flexShrink: 0,
|
|
759
|
+
width: 32,
|
|
760
|
+
height: 32,
|
|
761
|
+
marginBottom: 2,
|
|
762
|
+
border: "none",
|
|
763
|
+
borderRadius: RADIUS_SM,
|
|
764
|
+
background: "transparent",
|
|
765
|
+
color: "var(--curator-muted)",
|
|
766
|
+
cursor: "pointer",
|
|
767
|
+
padding: 0,
|
|
768
|
+
};
|
|
769
|
+
/** The chip's own dismiss control; smaller than the header's close button. */
|
|
770
|
+
const chipRemoveStyle = {
|
|
771
|
+
display: "inline-flex",
|
|
772
|
+
alignItems: "center",
|
|
773
|
+
justifyContent: "center",
|
|
774
|
+
width: 14,
|
|
775
|
+
height: 14,
|
|
776
|
+
marginLeft: 2,
|
|
777
|
+
padding: 0,
|
|
778
|
+
border: "none",
|
|
779
|
+
background: "transparent",
|
|
780
|
+
color: "inherit",
|
|
781
|
+
cursor: "pointer",
|
|
782
|
+
opacity: 0.7,
|
|
783
|
+
};
|
|
510
784
|
const composerRootStyle = {
|
|
511
785
|
display: "flex",
|
|
512
786
|
gap: SPACE_2,
|
|
513
787
|
alignItems: "flex-end",
|
|
514
|
-
padding: `${SPACE_3}
|
|
788
|
+
padding: `${SPACE_3} ${SPACE_3} ${SPACE_2}`,
|
|
515
789
|
flexShrink: 0,
|
|
516
790
|
};
|
|
517
791
|
const textareaStyle = {
|
|
@@ -519,7 +793,7 @@ const textareaStyle = {
|
|
|
519
793
|
resize: "none",
|
|
520
794
|
border: "1px solid var(--curator-border)",
|
|
521
795
|
borderRadius: RADIUS_SM,
|
|
522
|
-
padding: `${SPACE_2}
|
|
796
|
+
padding: `${SPACE_2} ${SPACE_3}`,
|
|
523
797
|
background: "var(--curator-bg)",
|
|
524
798
|
color: "var(--curator-text)",
|
|
525
799
|
fontFamily: "inherit",
|
|
@@ -572,6 +846,10 @@ if (typeof document !== "undefined") {
|
|
|
572
846
|
transform: none;
|
|
573
847
|
}
|
|
574
848
|
}
|
|
849
|
+
[data-curator-chat] [data-curator-part~="prompt"]:hover {
|
|
850
|
+
background-color: var(--curator-user-bubble);
|
|
851
|
+
border-color: var(--curator-muted);
|
|
852
|
+
}
|
|
575
853
|
[data-curator-chat] button:disabled { opacity: 0.4; cursor: not-allowed; }
|
|
576
854
|
[data-curator-chat] textarea {
|
|
577
855
|
transition: border-color 120ms ease, box-shadow 120ms ease;
|