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