@xcelsior/ui-chat 1.0.8 → 2.0.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/index.d.mts +69 -69
- package/dist/index.d.ts +69 -69
- package/dist/index.js +2458 -627
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +2457 -628
- package/dist/index.mjs.map +1 -1
- package/package.json +6 -5
- package/src/components/BrandIcons.stories.tsx +95 -0
- package/src/components/BrandIcons.tsx +84 -0
- package/src/components/Chat.stories.tsx +149 -16
- package/src/components/Chat.tsx +116 -96
- package/src/components/ChatHeader.tsx +124 -69
- package/src/components/ChatInput.tsx +253 -104
- package/src/components/ChatWidget.tsx +209 -63
- package/src/components/ConversationRating.stories.tsx +33 -0
- package/src/components/ConversationRating.tsx +156 -0
- package/src/components/MarkdownMessage.tsx +202 -0
- package/src/components/MessageItem.stories.tsx +253 -55
- package/src/components/MessageItem.tsx +223 -60
- package/src/components/MessageList.tsx +164 -35
- package/src/components/PreChatForm.tsx +236 -96
- package/src/components/ThinkingIndicator.tsx +370 -0
- package/src/components/TypingIndicator.tsx +27 -11
- package/src/hooks/useDraggablePosition.ts +91 -0
- package/src/hooks/useMessages.ts +12 -13
- package/src/hooks/useResizableWidget.ts +324 -0
- package/src/index.tsx +5 -0
- package/src/types.ts +51 -5
- package/src/utils/markdown-styles.ts +140 -0
- package/storybook-static/assets/BrandIcons-Cjy5INAp.js +4 -0
- package/storybook-static/assets/BrandIcons.stories-BeVC6svr.js +64 -0
- package/storybook-static/assets/Chat.stories-J_Yp51wU.js +803 -0
- package/storybook-static/assets/Color-YHDXOIA2-BMnd3YrF.js +1 -0
- package/storybook-static/assets/ConversationRating.stories-B5_QddHN.js +12 -0
- package/storybook-static/assets/DocsRenderer-CFRXHY34-i_W8iCu9.js +575 -0
- package/storybook-static/assets/MessageItem-DAaKZ9s9.js +14 -0
- package/storybook-static/assets/MessageItem.stories-Ckr1_scc.js +255 -0
- package/storybook-static/assets/ToastContext-Bty1K7ya.js +1 -0
- package/storybook-static/assets/chunk-XP5HYGXS-BpfKkqn7.js +1 -0
- package/storybook-static/assets/en-US-BukEqXxE.js +1 -0
- package/storybook-static/assets/entry-preview-docs-DHohToDm.js +46 -0
- package/storybook-static/assets/entry-preview-oDnntGcx.js +2 -0
- package/storybook-static/assets/iframe-CGBtu2Se.js +211 -0
- package/storybook-static/assets/index--qcDGAq6.js +1 -0
- package/storybook-static/assets/index-BLHw34Di.js +24 -0
- package/storybook-static/assets/index-B_4m48Mv.js +1 -0
- package/storybook-static/assets/index-DgH-xKnr.js +11 -0
- package/storybook-static/assets/index-DrFu-skq.js +6 -0
- package/storybook-static/assets/index-DrdPSA1J.js +240 -0
- package/storybook-static/assets/index-jvNEZhzf.js +1 -0
- package/storybook-static/assets/index-yBjzXJbu.js +9 -0
- package/storybook-static/assets/jsx-runtime-Cf8x2fCZ.js +9 -0
- package/storybook-static/assets/preview-B8lJiyuQ.js +34 -0
- package/storybook-static/assets/preview-BBWR9nbA.js +1 -0
- package/storybook-static/assets/preview-BRpahs9B.js +2 -0
- package/storybook-static/assets/preview-BWzBA1C2.js +396 -0
- package/storybook-static/assets/preview-CvbIS5ZJ.js +1 -0
- package/storybook-static/assets/preview-DD_OYowb.js +1 -0
- package/storybook-static/assets/preview-DGUiP6tS.js +7 -0
- package/storybook-static/assets/preview-DHQbi4pV.js +1 -0
- package/storybook-static/assets/preview-DUOvJmsz.js +1 -0
- package/storybook-static/assets/preview-DcGwT3kv.css +1 -0
- package/storybook-static/assets/preview-DwI0w3cI.js +1 -0
- package/storybook-static/assets/react-18-CALspjOX.js +1 -0
- package/storybook-static/assets/test-utils-BE0XkMtV.js +9 -0
- package/storybook-static/favicon.svg +1 -0
- package/storybook-static/iframe.html +666 -0
- package/storybook-static/index.html +177 -0
- package/storybook-static/index.json +1 -0
- package/storybook-static/nunito-sans-bold-italic.woff2 +0 -0
- package/storybook-static/nunito-sans-bold.woff2 +0 -0
- package/storybook-static/nunito-sans-italic.woff2 +0 -0
- package/storybook-static/nunito-sans-regular.woff2 +0 -0
- package/storybook-static/project.json +1 -0
- package/storybook-static/sb-addons/essentials-actions-3/manager-bundle.js +3 -0
- package/storybook-static/sb-addons/essentials-backgrounds-5/manager-bundle.js +12 -0
- package/storybook-static/sb-addons/essentials-controls-2/manager-bundle.js +405 -0
- package/storybook-static/sb-addons/essentials-docs-4/manager-bundle.js +245 -0
- package/storybook-static/sb-addons/essentials-measure-8/manager-bundle.js +3 -0
- package/storybook-static/sb-addons/essentials-outline-9/manager-bundle.js +3 -0
- package/storybook-static/sb-addons/essentials-toolbars-7/manager-bundle.js +3 -0
- package/storybook-static/sb-addons/essentials-viewport-6/manager-bundle.js +3 -0
- package/storybook-static/sb-addons/interactions-10/manager-bundle.js +222 -0
- package/storybook-static/sb-addons/links-1/manager-bundle.js +3 -0
- package/storybook-static/sb-addons/storybook-core-core-server-presets-0/common-manager-bundle.js +3 -0
- package/storybook-static/sb-common-assets/favicon.svg +1 -0
- package/storybook-static/sb-common-assets/nunito-sans-bold-italic.woff2 +0 -0
- package/storybook-static/sb-common-assets/nunito-sans-bold.woff2 +0 -0
- package/storybook-static/sb-common-assets/nunito-sans-italic.woff2 +0 -0
- package/storybook-static/sb-common-assets/nunito-sans-regular.woff2 +0 -0
- package/storybook-static/sb-manager/globals-module-info.js +1052 -0
- package/storybook-static/sb-manager/globals-runtime.js +42127 -0
- package/storybook-static/sb-manager/globals.js +48 -0
- package/storybook-static/sb-manager/runtime.js +12048 -0
- package/.turbo/turbo-build.log +0 -22
- package/.turbo/turbo-lint.log +0 -5
|
@@ -1,10 +1,21 @@
|
|
|
1
1
|
import { useEffect, useRef, useState } from 'react';
|
|
2
2
|
import { createPortal } from 'react-dom';
|
|
3
|
-
import { Button, TextArea } from '@xcelsior/design-system';
|
|
4
3
|
import Picker from '@emoji-mart/react';
|
|
5
4
|
import type { IChatConfig } from '../types';
|
|
6
5
|
import type { UseFileUploadReturn } from '../hooks/useFileUpload';
|
|
7
6
|
|
|
7
|
+
/** Detect if a hex/rgb color is "light" (luminance > 0.5) */
|
|
8
|
+
function isLightColor(color: string): boolean {
|
|
9
|
+
let r = 0, g = 0, b = 0;
|
|
10
|
+
if (color.startsWith('#')) {
|
|
11
|
+
const hex = color.replace('#', '');
|
|
12
|
+
r = parseInt(hex.substring(0, 2), 16);
|
|
13
|
+
g = parseInt(hex.substring(2, 4), 16);
|
|
14
|
+
b = parseInt(hex.substring(4, 6), 16);
|
|
15
|
+
}
|
|
16
|
+
return (0.299 * r + 0.587 * g + 0.114 * b) / 255 > 0.5;
|
|
17
|
+
}
|
|
18
|
+
|
|
8
19
|
interface ChatInputProps {
|
|
9
20
|
onSend: (message: string) => void;
|
|
10
21
|
onTyping?: (isTyping: boolean) => void;
|
|
@@ -27,6 +38,7 @@ export function ChatInput({
|
|
|
27
38
|
top: number;
|
|
28
39
|
left: number;
|
|
29
40
|
} | null>(null);
|
|
41
|
+
const [isFocused, setIsFocused] = useState(false);
|
|
30
42
|
const textAreaRef = useRef<HTMLTextAreaElement>(null);
|
|
31
43
|
const emojiPickerRef = useRef<HTMLDivElement>(null);
|
|
32
44
|
const emojiButtonRef = useRef<HTMLButtonElement>(null);
|
|
@@ -38,6 +50,13 @@ export function ChatInput({
|
|
|
38
50
|
const enableEmoji = config.enableEmoji ?? true;
|
|
39
51
|
const enableFileUpload = config.enableFileUpload ?? true;
|
|
40
52
|
|
|
53
|
+
// Theme tokens — aligned with Xcelsior website design system
|
|
54
|
+
const bgColor = config.theme?.background || '#00001a';
|
|
55
|
+
const isLightTheme = isLightColor(bgColor);
|
|
56
|
+
const textColor = config.theme?.text || (isLightTheme ? '#1a1a2e' : '#f7f7f8');
|
|
57
|
+
const textMuted = config.theme?.textMuted || (isLightTheme ? 'rgba(0,0,0,0.4)' : 'rgba(247,247,248,0.45)');
|
|
58
|
+
const primaryColor = config.theme?.primary || '#337eff';
|
|
59
|
+
|
|
41
60
|
// Load emoji data
|
|
42
61
|
useEffect(() => {
|
|
43
62
|
if (!enableEmoji) return;
|
|
@@ -123,7 +142,7 @@ export function ChatInput({
|
|
|
123
142
|
startTypingTimeoutRef.current = setTimeout(() => {
|
|
124
143
|
onTyping(true);
|
|
125
144
|
isTypingRef.current = true;
|
|
126
|
-
}, 300);
|
|
145
|
+
}, 300);
|
|
127
146
|
}
|
|
128
147
|
|
|
129
148
|
// Set timeout to stop typing indicator after inactivity
|
|
@@ -132,7 +151,7 @@ export function ChatInput({
|
|
|
132
151
|
onTyping(false);
|
|
133
152
|
isTypingRef.current = false;
|
|
134
153
|
}
|
|
135
|
-
}, 1500);
|
|
154
|
+
}, 1500);
|
|
136
155
|
}
|
|
137
156
|
};
|
|
138
157
|
|
|
@@ -146,14 +165,12 @@ export function ChatInput({
|
|
|
146
165
|
|
|
147
166
|
// Stop typing indicator
|
|
148
167
|
if (onTyping) {
|
|
149
|
-
// Clear all typing-related timeouts
|
|
150
168
|
if (typingTimeoutRef.current) {
|
|
151
169
|
clearTimeout(typingTimeoutRef.current);
|
|
152
170
|
}
|
|
153
171
|
if (startTypingTimeoutRef.current) {
|
|
154
172
|
clearTimeout(startTypingTimeoutRef.current);
|
|
155
173
|
}
|
|
156
|
-
// Send stop typing event if currently typing
|
|
157
174
|
if (isTypingRef.current) {
|
|
158
175
|
onTyping(false);
|
|
159
176
|
isTypingRef.current = false;
|
|
@@ -214,114 +231,241 @@ export function ChatInput({
|
|
|
214
231
|
}
|
|
215
232
|
};
|
|
216
233
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
<div className="relative">
|
|
221
|
-
<TextArea
|
|
222
|
-
ref={textAreaRef}
|
|
223
|
-
value={message}
|
|
224
|
-
onChange={(e) => handleTyping(e.target.value)}
|
|
225
|
-
onKeyDown={handleKeyDown}
|
|
226
|
-
placeholder="Type a message..."
|
|
227
|
-
rows={1}
|
|
228
|
-
className="resize-none pr-24 pl-4 py-3 rounded-full bg-gray-100 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 text-sm leading-5 placeholder-gray-500 dark:placeholder-gray-400"
|
|
229
|
-
disabled={disabled}
|
|
230
|
-
/>
|
|
231
|
-
|
|
232
|
-
{/* Actions inside the input on the right */}
|
|
233
|
-
<div className="absolute right-12 top-1/2 -translate-y-1/2 flex items-center gap-1">
|
|
234
|
-
{enableEmoji && (
|
|
235
|
-
<div className="relative">
|
|
236
|
-
<button
|
|
237
|
-
ref={emojiButtonRef}
|
|
238
|
-
type="button"
|
|
239
|
-
onClick={() => {
|
|
240
|
-
if (!showEmojiPicker && emojiButtonRef.current) {
|
|
241
|
-
const rect =
|
|
242
|
-
emojiButtonRef.current.getBoundingClientRect();
|
|
243
|
-
setEmojiPickerPosition({
|
|
244
|
-
top: rect.top - 450,
|
|
245
|
-
left: rect.left - 290,
|
|
246
|
-
});
|
|
247
|
-
}
|
|
248
|
-
setShowEmojiPicker((v) => !v);
|
|
249
|
-
}}
|
|
250
|
-
className="p-1.5 rounded-full hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors"
|
|
251
|
-
disabled={disabled}
|
|
252
|
-
aria-label="Add emoji"
|
|
253
|
-
>
|
|
254
|
-
<span className="text-lg">😊</span>
|
|
255
|
-
</button>
|
|
256
|
-
</div>
|
|
257
|
-
)}
|
|
258
|
-
|
|
259
|
-
{enableFileUpload && fileUpload.canUpload && (
|
|
260
|
-
<>
|
|
261
|
-
<button
|
|
262
|
-
type="button"
|
|
263
|
-
onClick={() => fileInputRef.current?.click()}
|
|
264
|
-
className="p-1.5 rounded-full hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors"
|
|
265
|
-
disabled={disabled || fileUpload.isUploading}
|
|
266
|
-
aria-label="Attach file"
|
|
267
|
-
>
|
|
268
|
-
{fileUpload.isUploading ? (
|
|
269
|
-
<span className="text-lg animate-spin">⏳</span>
|
|
270
|
-
) : (
|
|
271
|
-
<span className="text-lg">📎</span>
|
|
272
|
-
)}
|
|
273
|
-
</button>
|
|
274
|
-
<input
|
|
275
|
-
ref={fileInputRef}
|
|
276
|
-
type="file"
|
|
277
|
-
accept="image/*,application/pdf,.doc,.docx"
|
|
278
|
-
className="hidden"
|
|
279
|
-
onChange={handleFileSelect}
|
|
280
|
-
/>
|
|
281
|
-
</>
|
|
282
|
-
)}
|
|
283
|
-
</div>
|
|
234
|
+
const canSend = message.trim().length > 0 && !disabled;
|
|
235
|
+
|
|
236
|
+
const placeholderColor = isLightTheme ? 'rgba(0,0,0,0.35)' : 'rgba(247,247,248,0.3)';
|
|
284
237
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
238
|
+
return (
|
|
239
|
+
<div
|
|
240
|
+
className="px-4 py-3"
|
|
241
|
+
style={{
|
|
242
|
+
borderTop: isLightTheme
|
|
243
|
+
? '1px solid rgba(0,0,0,0.06)'
|
|
244
|
+
: '1px solid rgba(255,255,255,0.06)',
|
|
245
|
+
}}
|
|
246
|
+
>
|
|
247
|
+
<div
|
|
248
|
+
className="relative flex items-center rounded-full transition-all duration-200"
|
|
249
|
+
style={{
|
|
250
|
+
backgroundColor: isLightTheme
|
|
251
|
+
? 'rgba(112,115,124,0.08)'
|
|
252
|
+
: 'rgba(112,115,124,0.12)',
|
|
253
|
+
outline: isFocused ? `2px solid ${primaryColor}` : 'none',
|
|
254
|
+
outlineOffset: '-1px',
|
|
255
|
+
border: isLightTheme
|
|
256
|
+
? '1px solid rgba(0,0,0,0.1)'
|
|
257
|
+
: '1px solid rgba(255,255,255,0.08)',
|
|
258
|
+
backdropFilter: 'blur(32px)',
|
|
259
|
+
}}
|
|
260
|
+
>
|
|
261
|
+
{/* Placeholder color via inline style element */}
|
|
262
|
+
<style>{`.xchat-input::placeholder { color: ${placeholderColor}; }`}</style>
|
|
263
|
+
<textarea
|
|
264
|
+
ref={textAreaRef}
|
|
265
|
+
value={message}
|
|
266
|
+
onChange={(e) => handleTyping(e.target.value)}
|
|
267
|
+
onKeyDown={handleKeyDown}
|
|
268
|
+
onFocus={() => setIsFocused(true)}
|
|
269
|
+
onBlur={() => setIsFocused(false)}
|
|
270
|
+
placeholder="Type a message..."
|
|
271
|
+
rows={1}
|
|
272
|
+
className="xchat-input flex-1 resize-none bg-transparent"
|
|
273
|
+
style={{
|
|
274
|
+
color: textColor,
|
|
275
|
+
border: 'none',
|
|
276
|
+
outline: 'none',
|
|
277
|
+
boxShadow: 'none',
|
|
278
|
+
WebkitAppearance: 'none',
|
|
279
|
+
padding: '10px 0 10px 20px',
|
|
280
|
+
minHeight: '42px',
|
|
281
|
+
maxHeight: '120px',
|
|
282
|
+
caretColor: primaryColor,
|
|
283
|
+
fontSize: '14px',
|
|
284
|
+
lineHeight: '20px',
|
|
285
|
+
letterSpacing: '0.006em',
|
|
286
|
+
}}
|
|
287
|
+
disabled={disabled}
|
|
288
|
+
/>
|
|
289
|
+
|
|
290
|
+
{/* Action buttons row */}
|
|
291
|
+
<div className="flex items-center gap-1 px-2 shrink-0">
|
|
292
|
+
{enableEmoji && (
|
|
293
|
+
<button
|
|
294
|
+
ref={emojiButtonRef}
|
|
295
|
+
type="button"
|
|
296
|
+
onClick={() => {
|
|
297
|
+
if (!showEmojiPicker && emojiButtonRef.current) {
|
|
298
|
+
const rect = emojiButtonRef.current.getBoundingClientRect();
|
|
299
|
+
setEmojiPickerPosition({
|
|
300
|
+
top: rect.top - 450,
|
|
301
|
+
left: rect.left - 290,
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
setShowEmojiPicker((v) => !v);
|
|
305
|
+
}}
|
|
306
|
+
className="p-1.5 rounded-lg transition-all duration-150"
|
|
307
|
+
style={{
|
|
308
|
+
color: textMuted,
|
|
309
|
+
backgroundColor: 'transparent',
|
|
310
|
+
}}
|
|
311
|
+
onMouseEnter={(e) => {
|
|
312
|
+
e.currentTarget.style.backgroundColor = isLightTheme ? 'rgba(0,0,0,0.06)' : 'rgba(255,255,255,0.06)';
|
|
313
|
+
e.currentTarget.style.color = textColor;
|
|
314
|
+
}}
|
|
315
|
+
onMouseLeave={(e) => {
|
|
316
|
+
e.currentTarget.style.backgroundColor = 'transparent';
|
|
317
|
+
e.currentTarget.style.color = textMuted;
|
|
318
|
+
}}
|
|
319
|
+
disabled={disabled}
|
|
320
|
+
aria-label="Add emoji"
|
|
293
321
|
>
|
|
294
|
-
<
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
322
|
+
<svg
|
|
323
|
+
width="18"
|
|
324
|
+
height="18"
|
|
325
|
+
viewBox="0 0 24 24"
|
|
326
|
+
fill="none"
|
|
327
|
+
stroke="currentColor"
|
|
328
|
+
strokeWidth="1.75"
|
|
329
|
+
strokeLinecap="round"
|
|
330
|
+
strokeLinejoin="round"
|
|
331
|
+
aria-hidden="true"
|
|
332
|
+
>
|
|
333
|
+
<title>Emoji</title>
|
|
334
|
+
<circle cx="12" cy="12" r="10" />
|
|
335
|
+
<path d="M8 14s1.5 2 4 2 4-2 4-2" />
|
|
336
|
+
<line x1="9" y1="9" x2="9.01" y2="9" />
|
|
337
|
+
<line x1="15" y1="9" x2="15.01" y2="9" />
|
|
338
|
+
</svg>
|
|
339
|
+
</button>
|
|
340
|
+
)}
|
|
341
|
+
|
|
342
|
+
{enableFileUpload && fileUpload.canUpload && (
|
|
343
|
+
<>
|
|
344
|
+
<button
|
|
345
|
+
type="button"
|
|
346
|
+
onClick={() => fileInputRef.current?.click()}
|
|
347
|
+
className="p-1.5 rounded-lg transition-all duration-150"
|
|
348
|
+
style={{
|
|
349
|
+
color: textMuted,
|
|
350
|
+
backgroundColor: 'transparent',
|
|
351
|
+
}}
|
|
352
|
+
onMouseEnter={(e) => {
|
|
353
|
+
e.currentTarget.style.backgroundColor =
|
|
354
|
+
'rgba(255,255,255,0.06)';
|
|
355
|
+
e.currentTarget.style.color = textColor;
|
|
356
|
+
}}
|
|
357
|
+
onMouseLeave={(e) => {
|
|
358
|
+
e.currentTarget.style.backgroundColor = 'transparent';
|
|
359
|
+
e.currentTarget.style.color = textMuted;
|
|
360
|
+
}}
|
|
361
|
+
disabled={disabled || fileUpload.isUploading}
|
|
362
|
+
aria-label="Attach file"
|
|
363
|
+
>
|
|
364
|
+
{fileUpload.isUploading ? (
|
|
365
|
+
<svg
|
|
366
|
+
width="18"
|
|
367
|
+
height="18"
|
|
368
|
+
viewBox="0 0 24 24"
|
|
369
|
+
fill="none"
|
|
370
|
+
stroke="currentColor"
|
|
371
|
+
strokeWidth="1.75"
|
|
372
|
+
className="animate-spin"
|
|
373
|
+
aria-hidden="true"
|
|
374
|
+
>
|
|
375
|
+
<title>Uploading</title>
|
|
376
|
+
<path d="M21 12a9 9 0 11-6.219-8.56" />
|
|
377
|
+
</svg>
|
|
378
|
+
) : (
|
|
379
|
+
<svg
|
|
380
|
+
width="18"
|
|
381
|
+
height="18"
|
|
382
|
+
viewBox="0 0 24 24"
|
|
383
|
+
fill="none"
|
|
384
|
+
stroke="currentColor"
|
|
385
|
+
strokeWidth="1.75"
|
|
304
386
|
strokeLinecap="round"
|
|
305
387
|
strokeLinejoin="round"
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
388
|
+
aria-hidden="true"
|
|
389
|
+
>
|
|
390
|
+
<title>Attach file</title>
|
|
391
|
+
<path d="M21.44 11.05l-9.19 9.19a6 6 0 01-8.49-8.49l9.19-9.19a4 4 0 015.66 5.66l-9.2 9.19a2 2 0 01-2.83-2.83l8.49-8.48" />
|
|
392
|
+
</svg>
|
|
393
|
+
)}
|
|
394
|
+
</button>
|
|
395
|
+
<input
|
|
396
|
+
ref={fileInputRef}
|
|
397
|
+
type="file"
|
|
398
|
+
accept="image/*,application/pdf,.doc,.docx"
|
|
399
|
+
className="hidden"
|
|
400
|
+
onChange={handleFileSelect}
|
|
401
|
+
/>
|
|
402
|
+
</>
|
|
403
|
+
)}
|
|
404
|
+
|
|
405
|
+
{/* Send button — gradient pill matching website CTA style */}
|
|
406
|
+
<button
|
|
407
|
+
type="button"
|
|
408
|
+
onClick={handleSend}
|
|
409
|
+
disabled={!canSend}
|
|
410
|
+
className="p-1.5 rounded-lg transition-all duration-200"
|
|
411
|
+
style={{
|
|
412
|
+
background: canSend
|
|
413
|
+
? `linear-gradient(135deg, ${primaryColor}, ${config.theme?.primaryStrong || '#005eff'})`
|
|
414
|
+
: 'transparent',
|
|
415
|
+
color: canSend ? '#ffffff' : textMuted,
|
|
416
|
+
opacity: canSend ? 1 : 0.35,
|
|
417
|
+
cursor: canSend ? 'pointer' : 'default',
|
|
418
|
+
boxShadow: canSend
|
|
419
|
+
? `0 2px 8px -2px ${primaryColor}60`
|
|
420
|
+
: 'none',
|
|
421
|
+
}}
|
|
422
|
+
aria-label="Send message"
|
|
423
|
+
>
|
|
424
|
+
<svg
|
|
425
|
+
width="18"
|
|
426
|
+
height="18"
|
|
427
|
+
viewBox="0 0 24 24"
|
|
428
|
+
fill="none"
|
|
429
|
+
stroke="currentColor"
|
|
430
|
+
strokeWidth="2"
|
|
431
|
+
strokeLinecap="round"
|
|
432
|
+
strokeLinejoin="round"
|
|
433
|
+
aria-hidden="true"
|
|
434
|
+
>
|
|
435
|
+
<title>Send</title>
|
|
436
|
+
<line x1="22" y1="2" x2="11" y2="13" />
|
|
437
|
+
<polygon points="22 2 15 22 11 13 2 9 22 2" />
|
|
438
|
+
</svg>
|
|
439
|
+
</button>
|
|
313
440
|
</div>
|
|
314
441
|
</div>
|
|
315
442
|
|
|
443
|
+
{/* Upload progress */}
|
|
316
444
|
{fileUpload.isUploading && (
|
|
317
|
-
<div className="mt-2">
|
|
318
|
-
<div
|
|
445
|
+
<div className="mt-2 px-1">
|
|
446
|
+
<div
|
|
447
|
+
className="w-full rounded-full overflow-hidden"
|
|
448
|
+
style={{
|
|
449
|
+
height: 3,
|
|
450
|
+
backgroundColor: isLightTheme ? 'rgba(0,0,0,0.06)' : 'rgba(255,255,255,0.06)',
|
|
451
|
+
}}
|
|
452
|
+
>
|
|
319
453
|
<div
|
|
320
|
-
className="
|
|
321
|
-
style={{
|
|
454
|
+
className="h-full rounded-full transition-all duration-300"
|
|
455
|
+
style={{
|
|
456
|
+
width: `${fileUpload.uploadProgress}%`,
|
|
457
|
+
background: `linear-gradient(90deg, ${primaryColor}, ${config.theme?.primaryStrong || '#005eff'})`,
|
|
458
|
+
}}
|
|
322
459
|
/>
|
|
323
460
|
</div>
|
|
324
|
-
<p
|
|
461
|
+
<p
|
|
462
|
+
className="mt-1"
|
|
463
|
+
style={{
|
|
464
|
+
fontSize: '11px',
|
|
465
|
+
letterSpacing: '0.019em',
|
|
466
|
+
color: textMuted,
|
|
467
|
+
}}
|
|
468
|
+
>
|
|
325
469
|
Uploading... {fileUpload.uploadProgress}%
|
|
326
470
|
</p>
|
|
327
471
|
</div>
|
|
@@ -335,11 +479,16 @@ export function ChatInput({
|
|
|
335
479
|
createPortal(
|
|
336
480
|
<div
|
|
337
481
|
ref={emojiPickerRef}
|
|
338
|
-
className="fixed rounded-
|
|
482
|
+
className="fixed rounded-xl overflow-hidden"
|
|
339
483
|
style={{
|
|
340
484
|
top: `${emojiPickerPosition.top}px`,
|
|
341
485
|
left: `${emojiPickerPosition.left}px`,
|
|
342
486
|
zIndex: 9999,
|
|
487
|
+
boxShadow: [
|
|
488
|
+
'inset 0 0 0 0.5px rgba(255,255,255,0.06)',
|
|
489
|
+
'inset 0 1px 0 0 rgba(255,255,255,0.1)',
|
|
490
|
+
'0 16px 48px -8px rgba(0,0,0,0.5)',
|
|
491
|
+
].join(', '),
|
|
343
492
|
}}
|
|
344
493
|
>
|
|
345
494
|
<Picker
|
|
@@ -353,7 +502,7 @@ export function ChatInput({
|
|
|
353
502
|
navPosition="bottom"
|
|
354
503
|
perLine={8}
|
|
355
504
|
searchPosition="sticky"
|
|
356
|
-
theme="
|
|
505
|
+
theme="dark"
|
|
357
506
|
/>
|
|
358
507
|
</div>,
|
|
359
508
|
document.body
|