@xcelsior/ui-chat 1.0.7 → 2.0.0
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/CHANGELOG.md +10 -0
- 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 +222 -59
- 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-lint.log +0 -5
|
@@ -3,32 +3,44 @@ import { useWebSocket } from '../hooks/useWebSocket';
|
|
|
3
3
|
import { useMessages } from '../hooks/useMessages';
|
|
4
4
|
import { useFileUpload } from '../hooks/useFileUpload';
|
|
5
5
|
import { useTypingIndicator } from '../hooks/useTypingIndicator';
|
|
6
|
+
import { useResizableWidget } from '../hooks/useResizableWidget';
|
|
6
7
|
import { ChatHeader } from './ChatHeader';
|
|
7
8
|
import { MessageList } from './MessageList';
|
|
8
9
|
import { ChatInput } from './ChatInput';
|
|
9
|
-
import type { IChatConfig, IMessage } from '../types';
|
|
10
|
+
import type { IChatConfig, IMessage, IUser } from '../types';
|
|
10
11
|
|
|
11
12
|
export interface ChatWidgetProps {
|
|
12
13
|
config: IChatConfig;
|
|
13
14
|
className?: string;
|
|
14
|
-
/**
|
|
15
|
-
* Variant of the chat widget:
|
|
16
|
-
* - 'popover': Fixed positioned floating widget (default)
|
|
17
|
-
* - 'fullPage': Full page layout that fills the container
|
|
18
|
-
*/
|
|
19
15
|
variant?: 'popover' | 'fullPage';
|
|
20
|
-
/**
|
|
21
|
-
* External WebSocket connection (for agents with global connection)
|
|
22
|
-
*/
|
|
23
16
|
externalWebSocket?: WebSocket | null;
|
|
24
|
-
/**
|
|
25
|
-
* Callback when user wants to minimize the widget
|
|
26
|
-
*/
|
|
27
17
|
onMinimize?: () => void;
|
|
28
|
-
/**
|
|
29
|
-
* Callback when user wants to close the widget
|
|
30
|
-
*/
|
|
31
18
|
onClose?: () => void;
|
|
19
|
+
/** Resolved position (left or right) from the parent Chat component */
|
|
20
|
+
resolvedPosition?: 'left' | 'right';
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Generate an anonymous user for progressive identity mode */
|
|
24
|
+
function getAnonymousUser(): IUser {
|
|
25
|
+
let anonId: string;
|
|
26
|
+
try {
|
|
27
|
+
const stored = localStorage.getItem('xcelsior-chat-anon-id');
|
|
28
|
+
if (stored) {
|
|
29
|
+
anonId = stored;
|
|
30
|
+
} else {
|
|
31
|
+
anonId = `anon-${crypto.randomUUID?.() || Math.random().toString(36).slice(2)}`;
|
|
32
|
+
localStorage.setItem('xcelsior-chat-anon-id', anonId);
|
|
33
|
+
}
|
|
34
|
+
} catch {
|
|
35
|
+
anonId = `anon-${Math.random().toString(36).slice(2)}`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return {
|
|
39
|
+
name: 'Visitor',
|
|
40
|
+
email: `${anonId}@anonymous.xcelsior.co`,
|
|
41
|
+
type: 'customer',
|
|
42
|
+
status: 'online',
|
|
43
|
+
};
|
|
32
44
|
}
|
|
33
45
|
|
|
34
46
|
export function ChatWidget({
|
|
@@ -38,25 +50,32 @@ export function ChatWidget({
|
|
|
38
50
|
externalWebSocket,
|
|
39
51
|
onMinimize,
|
|
40
52
|
onClose,
|
|
53
|
+
resolvedPosition = 'right',
|
|
41
54
|
}: ChatWidgetProps) {
|
|
42
55
|
const isFullPage = variant === 'fullPage';
|
|
43
56
|
|
|
44
|
-
//
|
|
45
|
-
const
|
|
57
|
+
// Resolve user — support anonymous mode for progressive identity
|
|
58
|
+
const effectiveUser = config.currentUser || getAnonymousUser();
|
|
59
|
+
const effectiveConfig = { ...config, currentUser: effectiveUser };
|
|
46
60
|
|
|
47
|
-
|
|
48
|
-
const { messages, addMessage, isLoading, loadMore, hasMore, isLoadingMore } = useMessages(
|
|
49
|
-
websocket,
|
|
50
|
-
config
|
|
51
|
-
);
|
|
61
|
+
const websocket = useWebSocket(effectiveConfig, externalWebSocket);
|
|
52
62
|
|
|
53
|
-
|
|
54
|
-
|
|
63
|
+
const { messages, addMessage, isLoading, loadMore, hasMore, isLoadingMore, isBotThinking } =
|
|
64
|
+
useMessages(websocket, effectiveConfig);
|
|
55
65
|
|
|
56
|
-
|
|
66
|
+
const fileUpload = useFileUpload(config.apiKey, config.fileUpload);
|
|
57
67
|
const { isTyping, typingUsers } = useTypingIndicator(websocket);
|
|
58
68
|
|
|
59
|
-
|
|
69
|
+
const { width, height, isResizing, isNearEdge, containerResizeProps } = useResizableWidget({
|
|
70
|
+
initialWidth: 380,
|
|
71
|
+
initialHeight: 580,
|
|
72
|
+
minWidth: 320,
|
|
73
|
+
minHeight: 400,
|
|
74
|
+
maxWidth: 800,
|
|
75
|
+
maxHeight: 900,
|
|
76
|
+
enabled: !isFullPage,
|
|
77
|
+
});
|
|
78
|
+
|
|
60
79
|
const handleSendMessage = useCallback(
|
|
61
80
|
(content: string) => {
|
|
62
81
|
if (!websocket.isConnected) {
|
|
@@ -64,41 +83,33 @@ export function ChatWidget({
|
|
|
64
83
|
return;
|
|
65
84
|
}
|
|
66
85
|
|
|
67
|
-
// Create optimistic message
|
|
68
86
|
const optimisticMessage: IMessage = {
|
|
69
87
|
id: `temp-${Date.now()}`,
|
|
70
88
|
conversationId: config.conversationId || '',
|
|
71
|
-
senderId:
|
|
72
|
-
senderType:
|
|
89
|
+
senderId: effectiveUser.email,
|
|
90
|
+
senderType: effectiveUser.type,
|
|
73
91
|
content,
|
|
74
92
|
messageType: 'text',
|
|
75
93
|
createdAt: new Date().toISOString(),
|
|
76
94
|
status: 'sent',
|
|
77
95
|
};
|
|
78
96
|
|
|
79
|
-
// Add to local state immediately (optimistic update)
|
|
80
97
|
addMessage(optimisticMessage);
|
|
81
98
|
|
|
82
|
-
// Send via WebSocket
|
|
83
99
|
websocket.sendMessage('sendMessage', {
|
|
84
100
|
conversationId: config.conversationId,
|
|
85
101
|
content,
|
|
86
102
|
messageType: 'text',
|
|
87
103
|
});
|
|
88
104
|
|
|
89
|
-
// Call callback
|
|
90
105
|
config.onMessageSent?.(optimisticMessage);
|
|
91
106
|
},
|
|
92
|
-
[websocket, config, addMessage]
|
|
107
|
+
[websocket, config, addMessage, effectiveUser]
|
|
93
108
|
);
|
|
94
109
|
|
|
95
|
-
// Handle typing indicator
|
|
96
110
|
const handleTyping = useCallback(
|
|
97
111
|
(isTyping: boolean) => {
|
|
98
|
-
if (!websocket.isConnected || config.enableTypingIndicator === false)
|
|
99
|
-
return;
|
|
100
|
-
}
|
|
101
|
-
|
|
112
|
+
if (!websocket.isConnected || config.enableTypingIndicator === false) return;
|
|
102
113
|
websocket.sendMessage('typing', {
|
|
103
114
|
conversationId: config.conversationId,
|
|
104
115
|
isTyping,
|
|
@@ -107,35 +118,100 @@ export function ChatWidget({
|
|
|
107
118
|
[websocket, config]
|
|
108
119
|
);
|
|
109
120
|
|
|
110
|
-
// Handle errors
|
|
111
121
|
useEffect(() => {
|
|
112
122
|
if (websocket.error) {
|
|
113
123
|
config.toast?.error(websocket.error.message || 'An error occurred');
|
|
114
124
|
}
|
|
115
125
|
}, [websocket.error, config]);
|
|
116
126
|
|
|
117
|
-
//
|
|
118
|
-
const
|
|
119
|
-
|
|
120
|
-
|
|
127
|
+
// Theme tokens — aligned with Xcelsior website design system
|
|
128
|
+
const bgColor = config.theme?.background || '#00001a';
|
|
129
|
+
const bgAlt = config.theme?.backgroundAlt || '#02164a';
|
|
130
|
+
const textColor = config.theme?.text || '#f7f7f8';
|
|
131
|
+
const primaryColor = config.theme?.primary || '#337eff';
|
|
132
|
+
const textMuted = config.theme?.textMuted || 'rgba(247,247,248,0.45)';
|
|
121
133
|
|
|
122
|
-
|
|
123
|
-
|
|
134
|
+
// Detect light vs dark theme based on background luminance
|
|
135
|
+
const isLightTheme = (() => {
|
|
136
|
+
if (!bgColor.startsWith('#')) return false;
|
|
137
|
+
const hex = bgColor.replace('#', '');
|
|
138
|
+
const r = parseInt(hex.substring(0, 2), 16);
|
|
139
|
+
const g = parseInt(hex.substring(2, 4), 16);
|
|
140
|
+
const b = parseInt(hex.substring(4, 6), 16);
|
|
141
|
+
return (0.299 * r + 0.587 * g + 0.114 * b) / 255 > 0.5;
|
|
142
|
+
})();
|
|
143
|
+
|
|
144
|
+
// Position-aware container styles
|
|
145
|
+
const positionClass = resolvedPosition === 'left' ? 'left-4' : 'right-4';
|
|
146
|
+
|
|
147
|
+
const containerStyle: React.CSSProperties = isFullPage
|
|
148
|
+
? { backgroundColor: bgColor, color: textColor }
|
|
149
|
+
: {
|
|
150
|
+
position: 'relative',
|
|
151
|
+
width,
|
|
152
|
+
height,
|
|
153
|
+
maxHeight: 'calc(100vh - 100px)',
|
|
154
|
+
backgroundColor: bgColor,
|
|
155
|
+
color: textColor,
|
|
156
|
+
borderRadius: 16,
|
|
157
|
+
/* Xcelsior card pattern: inset box-shadow borders instead of border-image */
|
|
158
|
+
boxShadow: isLightTheme
|
|
159
|
+
? [
|
|
160
|
+
'0 25px 60px -12px rgba(0,0,0,0.15)',
|
|
161
|
+
'0 0 0 1px rgba(0,0,0,0.08)',
|
|
162
|
+
].join(', ')
|
|
163
|
+
: [
|
|
164
|
+
'inset 0 0 0 0.5px rgba(255,255,255,0.06)',
|
|
165
|
+
'inset 0 1px 0 0 rgba(255,255,255,0.12)',
|
|
166
|
+
'0 25px 60px -12px rgba(0,0,0,0.6)',
|
|
167
|
+
'0 0 40px -8px rgba(51,126,255,0.15)',
|
|
168
|
+
].join(', '),
|
|
169
|
+
userSelect: isResizing ? 'none' : undefined,
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
/* Dot grid texture matching the Xcelsior website background */
|
|
173
|
+
const dotGridBg: React.CSSProperties = !isFullPage
|
|
174
|
+
? {
|
|
175
|
+
backgroundImage: isLightTheme
|
|
176
|
+
? `radial-gradient(circle, rgba(0,0,0,0.04) 1px, transparent 1px)`
|
|
177
|
+
: `radial-gradient(circle, rgba(255,255,255,0.03) 1px, transparent 1px)`,
|
|
178
|
+
backgroundSize: '24px 24px',
|
|
179
|
+
}
|
|
180
|
+
: {};
|
|
181
|
+
|
|
182
|
+
// Subtle edge glow when user hovers near a resize edge
|
|
183
|
+
// Uses outline (not clipped by overflow:hidden) for visibility
|
|
184
|
+
const edgeHintStyle: React.CSSProperties = !isFullPage && isNearEdge
|
|
185
|
+
? {
|
|
186
|
+
outline: isLightTheme
|
|
187
|
+
? '2px solid rgba(0,94,255,0.25)'
|
|
188
|
+
: '2px solid rgba(51,126,255,0.35)',
|
|
189
|
+
outlineOffset: -1,
|
|
190
|
+
transition: 'outline 150ms ease',
|
|
191
|
+
}
|
|
124
192
|
: {
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
193
|
+
outline: '2px solid transparent',
|
|
194
|
+
outlineOffset: -1,
|
|
195
|
+
transition: 'outline 150ms ease',
|
|
128
196
|
};
|
|
129
197
|
|
|
130
198
|
return (
|
|
131
|
-
<div
|
|
199
|
+
<div
|
|
200
|
+
className={
|
|
201
|
+
isFullPage
|
|
202
|
+
? `flex flex-col h-full ${className}`
|
|
203
|
+
: `fixed bottom-20 ${positionClass} z-50 flex flex-col overflow-hidden ${className}`
|
|
204
|
+
}
|
|
205
|
+
style={{ ...containerStyle, ...dotGridBg, ...edgeHintStyle }}
|
|
206
|
+
{...(!isFullPage ? containerResizeProps : {})}
|
|
207
|
+
>
|
|
132
208
|
{!isFullPage && (
|
|
133
209
|
<ChatHeader
|
|
134
210
|
agent={
|
|
135
|
-
|
|
211
|
+
effectiveUser.type === 'customer'
|
|
136
212
|
? {
|
|
137
213
|
email: 'contact@xcelsior.co',
|
|
138
|
-
name: '
|
|
214
|
+
name: 'Xcelsior Software',
|
|
139
215
|
type: 'agent',
|
|
140
216
|
status: websocket.isConnected ? 'online' : 'offline',
|
|
141
217
|
}
|
|
@@ -143,21 +219,49 @@ export function ChatWidget({
|
|
|
143
219
|
}
|
|
144
220
|
onMinimize={onMinimize}
|
|
145
221
|
onClose={onClose}
|
|
222
|
+
theme={config.theme}
|
|
146
223
|
/>
|
|
147
224
|
)}
|
|
148
225
|
|
|
149
226
|
{/* Connection Status */}
|
|
150
227
|
{!websocket.isConnected && (
|
|
151
|
-
<div
|
|
228
|
+
<div
|
|
229
|
+
className="px-4 py-2"
|
|
230
|
+
style={{
|
|
231
|
+
backgroundColor: isLightTheme ? 'rgba(255,169,56,0.08)' : 'rgba(255,169,56,0.06)',
|
|
232
|
+
borderBottom: `1px solid rgba(255,169,56,${isLightTheme ? '0.15' : '0.12'})`,
|
|
233
|
+
}}
|
|
234
|
+
>
|
|
152
235
|
<div className="flex items-center gap-2">
|
|
153
|
-
<div
|
|
154
|
-
|
|
236
|
+
<div
|
|
237
|
+
className="w-1.5 h-1.5 rounded-full animate-pulse"
|
|
238
|
+
style={{ backgroundColor: config.theme?.statusCaution || '#ffa938' }}
|
|
239
|
+
/>
|
|
240
|
+
<span
|
|
241
|
+
style={{
|
|
242
|
+
fontSize: '12px',
|
|
243
|
+
letterSpacing: '0.015em',
|
|
244
|
+
color: config.theme?.statusCaution || '#ffa938',
|
|
245
|
+
}}
|
|
246
|
+
>
|
|
155
247
|
Reconnecting...
|
|
156
248
|
</span>
|
|
157
249
|
<button
|
|
158
250
|
type="button"
|
|
159
251
|
onClick={websocket.reconnect}
|
|
160
|
-
className="ml-auto
|
|
252
|
+
className="ml-auto transition-opacity"
|
|
253
|
+
style={{
|
|
254
|
+
fontSize: '12px',
|
|
255
|
+
letterSpacing: '0.015em',
|
|
256
|
+
color: config.theme?.statusCaution || '#ffa938',
|
|
257
|
+
opacity: 0.7,
|
|
258
|
+
}}
|
|
259
|
+
onMouseEnter={(e) => {
|
|
260
|
+
e.currentTarget.style.opacity = '1';
|
|
261
|
+
}}
|
|
262
|
+
onMouseLeave={(e) => {
|
|
263
|
+
e.currentTarget.style.opacity = '0.7';
|
|
264
|
+
}}
|
|
161
265
|
>
|
|
162
266
|
Retry
|
|
163
267
|
</button>
|
|
@@ -169,8 +273,20 @@ export function ChatWidget({
|
|
|
169
273
|
{isLoading ? (
|
|
170
274
|
<div className="flex-1 flex items-center justify-center">
|
|
171
275
|
<div className="text-center">
|
|
172
|
-
<div
|
|
173
|
-
|
|
276
|
+
<div
|
|
277
|
+
className="w-7 h-7 border-2 border-t-transparent rounded-full animate-spin mx-auto mb-3"
|
|
278
|
+
style={{
|
|
279
|
+
borderColor: primaryColor,
|
|
280
|
+
borderTopColor: 'transparent',
|
|
281
|
+
}}
|
|
282
|
+
/>
|
|
283
|
+
<p
|
|
284
|
+
style={{
|
|
285
|
+
fontSize: '12px',
|
|
286
|
+
letterSpacing: '0.015em',
|
|
287
|
+
color: textMuted,
|
|
288
|
+
}}
|
|
289
|
+
>
|
|
174
290
|
Loading messages...
|
|
175
291
|
</p>
|
|
176
292
|
</div>
|
|
@@ -178,13 +294,16 @@ export function ChatWidget({
|
|
|
178
294
|
) : (
|
|
179
295
|
<MessageList
|
|
180
296
|
messages={messages}
|
|
181
|
-
currentUser={
|
|
297
|
+
currentUser={effectiveUser}
|
|
182
298
|
isTyping={isTyping}
|
|
183
299
|
typingUser={typingUsers[0]}
|
|
184
300
|
autoScroll={true}
|
|
185
301
|
onLoadMore={loadMore}
|
|
186
302
|
hasMore={hasMore}
|
|
187
303
|
isLoadingMore={isLoadingMore}
|
|
304
|
+
theme={config.theme}
|
|
305
|
+
onQuickAction={handleSendMessage}
|
|
306
|
+
isBotThinking={isBotThinking}
|
|
188
307
|
/>
|
|
189
308
|
)}
|
|
190
309
|
|
|
@@ -192,19 +311,46 @@ export function ChatWidget({
|
|
|
192
311
|
<ChatInput
|
|
193
312
|
onSend={handleSendMessage}
|
|
194
313
|
onTyping={handleTyping}
|
|
195
|
-
config={
|
|
314
|
+
config={effectiveConfig}
|
|
196
315
|
fileUpload={fileUpload}
|
|
197
316
|
disabled={!websocket.isConnected}
|
|
198
317
|
/>
|
|
199
318
|
|
|
200
|
-
{/* Powered by footer
|
|
319
|
+
{/* Powered by footer */}
|
|
201
320
|
{!isFullPage && (
|
|
202
|
-
<div
|
|
203
|
-
|
|
204
|
-
|
|
321
|
+
<div
|
|
322
|
+
className="px-4 py-1.5 text-center"
|
|
323
|
+
style={{
|
|
324
|
+
borderTop: isLightTheme
|
|
325
|
+
? '1px solid rgba(0,0,0,0.06)'
|
|
326
|
+
: '1px solid rgba(255,255,255,0.06)',
|
|
327
|
+
backgroundColor: isLightTheme
|
|
328
|
+
? 'rgba(0,0,0,0.03)'
|
|
329
|
+
: 'rgba(0,0,0,0.2)',
|
|
330
|
+
}}
|
|
331
|
+
>
|
|
332
|
+
<p
|
|
333
|
+
style={{
|
|
334
|
+
fontSize: '10px',
|
|
335
|
+
letterSpacing: '0.025em',
|
|
336
|
+
color: isLightTheme
|
|
337
|
+
? 'rgba(0,0,0,0.35)'
|
|
338
|
+
: 'rgba(247,247,248,0.28)',
|
|
339
|
+
}}
|
|
340
|
+
>
|
|
341
|
+
Powered by{' '}
|
|
342
|
+
<span style={{
|
|
343
|
+
fontWeight: 600,
|
|
344
|
+
color: isLightTheme
|
|
345
|
+
? 'rgba(0,0,0,0.5)'
|
|
346
|
+
: 'rgba(247,247,248,0.45)',
|
|
347
|
+
}}>
|
|
348
|
+
Xcelsior
|
|
349
|
+
</span>
|
|
205
350
|
</p>
|
|
206
351
|
</div>
|
|
207
352
|
)}
|
|
353
|
+
|
|
208
354
|
</div>
|
|
209
355
|
);
|
|
210
356
|
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react';
|
|
2
|
+
import { ConversationRating } from './ConversationRating';
|
|
3
|
+
|
|
4
|
+
const meta: Meta<typeof ConversationRating> = {
|
|
5
|
+
title: 'Components/ConversationRating',
|
|
6
|
+
component: ConversationRating,
|
|
7
|
+
parameters: {
|
|
8
|
+
layout: 'padded',
|
|
9
|
+
},
|
|
10
|
+
tags: ['autodocs'],
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export default meta;
|
|
14
|
+
type Story = StoryObj<typeof ConversationRating>;
|
|
15
|
+
|
|
16
|
+
/** Default state — shows thumbs up/down buttons */
|
|
17
|
+
export const Default: Story = {
|
|
18
|
+
args: {
|
|
19
|
+
onRate: (rating) => console.log('Rated:', rating),
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/** In a dark container (matching chat widget) */
|
|
24
|
+
export const InDarkContainer: Story = {
|
|
25
|
+
render: (args) => (
|
|
26
|
+
<div className="max-w-md p-4 bg-gray-900 rounded-lg">
|
|
27
|
+
<ConversationRating {...args} />
|
|
28
|
+
</div>
|
|
29
|
+
),
|
|
30
|
+
args: {
|
|
31
|
+
onRate: (rating) => console.log('Rated:', rating),
|
|
32
|
+
},
|
|
33
|
+
};
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
import type { IChatTheme } from '../types';
|
|
3
|
+
|
|
4
|
+
interface ConversationRatingProps {
|
|
5
|
+
onRate: (rating: 'positive' | 'negative') => void;
|
|
6
|
+
theme?: IChatTheme;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function ConversationRating({ onRate, theme }: ConversationRatingProps) {
|
|
10
|
+
const [rated, setRated] = useState<'positive' | 'negative' | null>(null);
|
|
11
|
+
const [hovering, setHovering] = useState<'positive' | 'negative' | null>(null);
|
|
12
|
+
|
|
13
|
+
const textMuted = theme?.textMuted || 'rgba(247,247,248,0.45)';
|
|
14
|
+
const textColor = theme?.text || '#f7f7f8';
|
|
15
|
+
const positiveColor = theme?.statusPositive || '#1ed473';
|
|
16
|
+
const negativeColor = theme?.statusNegative || '#ff6363';
|
|
17
|
+
|
|
18
|
+
const handleRate = (rating: 'positive' | 'negative') => {
|
|
19
|
+
setRated(rating);
|
|
20
|
+
onRate(rating);
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
if (rated) {
|
|
24
|
+
return (
|
|
25
|
+
<div
|
|
26
|
+
className="flex items-center justify-center gap-2 py-2.5 px-4 rounded-xl mx-4 my-2"
|
|
27
|
+
style={{
|
|
28
|
+
backgroundColor: 'rgba(255,255,255,0.03)',
|
|
29
|
+
boxShadow: 'inset 0 0 0 0.5px rgba(255,255,255,0.06)',
|
|
30
|
+
}}
|
|
31
|
+
>
|
|
32
|
+
<svg
|
|
33
|
+
width="14"
|
|
34
|
+
height="14"
|
|
35
|
+
viewBox="0 0 24 24"
|
|
36
|
+
fill="none"
|
|
37
|
+
stroke={positiveColor}
|
|
38
|
+
strokeWidth="2"
|
|
39
|
+
strokeLinecap="round"
|
|
40
|
+
strokeLinejoin="round"
|
|
41
|
+
aria-hidden="true"
|
|
42
|
+
>
|
|
43
|
+
<title>Thanks</title>
|
|
44
|
+
<polyline points="20 6 9 17 4 12" />
|
|
45
|
+
</svg>
|
|
46
|
+
<span
|
|
47
|
+
style={{
|
|
48
|
+
fontSize: '12px',
|
|
49
|
+
letterSpacing: '0.015em',
|
|
50
|
+
color: textMuted,
|
|
51
|
+
}}
|
|
52
|
+
>
|
|
53
|
+
Thanks for your feedback!
|
|
54
|
+
</span>
|
|
55
|
+
</div>
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return (
|
|
60
|
+
<div
|
|
61
|
+
className="flex flex-col items-center gap-2.5 py-3 px-4 rounded-xl mx-4 my-2"
|
|
62
|
+
style={{
|
|
63
|
+
backgroundColor: 'rgba(255,255,255,0.03)',
|
|
64
|
+
boxShadow: 'inset 0 0 0 0.5px rgba(255,255,255,0.06)',
|
|
65
|
+
}}
|
|
66
|
+
>
|
|
67
|
+
<p
|
|
68
|
+
style={{
|
|
69
|
+
fontSize: '12px',
|
|
70
|
+
letterSpacing: '0.015em',
|
|
71
|
+
color: textMuted,
|
|
72
|
+
}}
|
|
73
|
+
>
|
|
74
|
+
How was your experience?
|
|
75
|
+
</p>
|
|
76
|
+
<div className="flex gap-2.5">
|
|
77
|
+
{/* Positive rating button — ghost pill matching website button style */}
|
|
78
|
+
<button
|
|
79
|
+
type="button"
|
|
80
|
+
onClick={() => handleRate('positive')}
|
|
81
|
+
onMouseEnter={() => setHovering('positive')}
|
|
82
|
+
onMouseLeave={() => setHovering(null)}
|
|
83
|
+
className="flex items-center gap-1.5 px-3.5 py-1.5 rounded-full font-medium transition-all duration-200"
|
|
84
|
+
style={{
|
|
85
|
+
fontSize: '12px',
|
|
86
|
+
letterSpacing: '0.015em',
|
|
87
|
+
boxShadow:
|
|
88
|
+
hovering === 'positive'
|
|
89
|
+
? `inset 0 0 0 1px ${positiveColor}40, 0 0 12px -4px ${positiveColor}25`
|
|
90
|
+
: 'inset 0 0 0 0.5px rgba(255,255,255,0.08)',
|
|
91
|
+
backgroundColor:
|
|
92
|
+
hovering === 'positive'
|
|
93
|
+
? `${positiveColor}10`
|
|
94
|
+
: 'rgba(255,255,255,0.02)',
|
|
95
|
+
color: hovering === 'positive' ? positiveColor : textColor,
|
|
96
|
+
}}
|
|
97
|
+
aria-label="Rate positive"
|
|
98
|
+
>
|
|
99
|
+
<svg
|
|
100
|
+
width="14"
|
|
101
|
+
height="14"
|
|
102
|
+
viewBox="0 0 24 24"
|
|
103
|
+
fill="none"
|
|
104
|
+
stroke="currentColor"
|
|
105
|
+
strokeWidth="2"
|
|
106
|
+
strokeLinecap="round"
|
|
107
|
+
strokeLinejoin="round"
|
|
108
|
+
aria-hidden="true"
|
|
109
|
+
>
|
|
110
|
+
<title>Thumbs up</title>
|
|
111
|
+
<path d="M14 9V5a3 3 0 0 0-3-3l-4 9v11h11.28a2 2 0 0 0 2-1.7l1.38-9a2 2 0 0 0-2-2.3zM7 22H4a2 2 0 0 1-2-2v-7a2 2 0 0 1 2-2h3" />
|
|
112
|
+
</svg>
|
|
113
|
+
Helpful
|
|
114
|
+
</button>
|
|
115
|
+
{/* Negative rating button */}
|
|
116
|
+
<button
|
|
117
|
+
type="button"
|
|
118
|
+
onClick={() => handleRate('negative')}
|
|
119
|
+
onMouseEnter={() => setHovering('negative')}
|
|
120
|
+
onMouseLeave={() => setHovering(null)}
|
|
121
|
+
className="flex items-center gap-1.5 px-3.5 py-1.5 rounded-full font-medium transition-all duration-200"
|
|
122
|
+
style={{
|
|
123
|
+
fontSize: '12px',
|
|
124
|
+
letterSpacing: '0.015em',
|
|
125
|
+
boxShadow:
|
|
126
|
+
hovering === 'negative'
|
|
127
|
+
? `inset 0 0 0 1px ${negativeColor}40, 0 0 12px -4px ${negativeColor}25`
|
|
128
|
+
: 'inset 0 0 0 0.5px rgba(255,255,255,0.08)',
|
|
129
|
+
backgroundColor:
|
|
130
|
+
hovering === 'negative'
|
|
131
|
+
? `${negativeColor}10`
|
|
132
|
+
: 'rgba(255,255,255,0.02)',
|
|
133
|
+
color: hovering === 'negative' ? negativeColor : textColor,
|
|
134
|
+
}}
|
|
135
|
+
aria-label="Rate negative"
|
|
136
|
+
>
|
|
137
|
+
<svg
|
|
138
|
+
width="14"
|
|
139
|
+
height="14"
|
|
140
|
+
viewBox="0 0 24 24"
|
|
141
|
+
fill="none"
|
|
142
|
+
stroke="currentColor"
|
|
143
|
+
strokeWidth="2"
|
|
144
|
+
strokeLinecap="round"
|
|
145
|
+
strokeLinejoin="round"
|
|
146
|
+
aria-hidden="true"
|
|
147
|
+
>
|
|
148
|
+
<title>Thumbs down</title>
|
|
149
|
+
<path d="M10 15V19a3 3 0 0 0 3 3l4-9V2H5.72a2 2 0 0 0-2 1.7l-1.38 9a2 2 0 0 0 2 2.3zm7-13h2.67A2.31 2.31 0 0 1 22 4v7a2.31 2.31 0 0 1-2.33 2H17" />
|
|
150
|
+
</svg>
|
|
151
|
+
Not helpful
|
|
152
|
+
</button>
|
|
153
|
+
</div>
|
|
154
|
+
</div>
|
|
155
|
+
);
|
|
156
|
+
}
|