@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
package/src/components/Chat.tsx
CHANGED
|
@@ -1,46 +1,21 @@
|
|
|
1
1
|
import { useCallback, useEffect, useState } from 'react';
|
|
2
2
|
import { ChatWidget } from './ChatWidget';
|
|
3
3
|
import { PreChatForm } from './PreChatForm';
|
|
4
|
+
import { ChatBubbleIcon } from './BrandIcons';
|
|
5
|
+
import { useDraggablePosition } from '../hooks/useDraggablePosition';
|
|
4
6
|
import type { IChatConfig, IUser } from '../types';
|
|
5
7
|
import { useChatWidgetState } from '../hooks/useChatWidgetState';
|
|
6
8
|
import type { ChatWidgetState } from '../hooks/useChatWidgetState';
|
|
7
9
|
|
|
8
10
|
interface ChatWidgetWrapperProps {
|
|
9
|
-
|
|
10
|
-
* Base configuration for the chat widget (without user info and conversationId)
|
|
11
|
-
*/
|
|
12
|
-
config: Omit<IChatConfig, 'currentUser' | 'conversationId' | 'userId'> & {
|
|
13
|
-
currentUser?: Partial<IUser>;
|
|
11
|
+
config: Omit<IChatConfig, 'conversationId'> & {
|
|
14
12
|
conversationId?: string;
|
|
15
13
|
};
|
|
16
|
-
/**
|
|
17
|
-
* Custom className for the wrapper
|
|
18
|
-
*/
|
|
19
14
|
className?: string;
|
|
20
|
-
/**
|
|
21
|
-
* Storage key prefix for persisting user data
|
|
22
|
-
* Defaults to 'xcelsior_chat'
|
|
23
|
-
*/
|
|
24
15
|
storageKeyPrefix?: string;
|
|
25
|
-
/**
|
|
26
|
-
* Callback when user submits the pre-chat form
|
|
27
|
-
*/
|
|
28
16
|
onPreChatSubmit?: (user: IUser) => void;
|
|
29
|
-
/**
|
|
30
|
-
* Controlled state. When provided, the component is controlled.
|
|
31
|
-
* - 'open': Fully open with chat interface
|
|
32
|
-
* - 'minimized': Show bubble button only
|
|
33
|
-
* - 'closed': Fully hidden
|
|
34
|
-
*/
|
|
35
17
|
state?: ChatWidgetState;
|
|
36
|
-
/**
|
|
37
|
-
* Default state for uncontrolled mode
|
|
38
|
-
* @default 'minimized'
|
|
39
|
-
*/
|
|
40
18
|
defaultState?: ChatWidgetState;
|
|
41
|
-
/**
|
|
42
|
-
* Callback when state changes
|
|
43
|
-
*/
|
|
44
19
|
onStateChange?: (state: ChatWidgetState) => void;
|
|
45
20
|
}
|
|
46
21
|
|
|
@@ -51,23 +26,13 @@ interface StoredUserData {
|
|
|
51
26
|
timestamp: number;
|
|
52
27
|
}
|
|
53
28
|
|
|
54
|
-
/**
|
|
55
|
-
* Generates a unique session-based ID
|
|
56
|
-
*/
|
|
57
29
|
function generateSessionId(): string {
|
|
58
30
|
if (typeof crypto !== 'undefined' && crypto.randomUUID) {
|
|
59
31
|
return crypto.randomUUID();
|
|
60
32
|
}
|
|
61
|
-
// Fallback for older browsers
|
|
62
33
|
return `${Date.now()}-${Math.random().toString(36).substring(2, 15)}`;
|
|
63
34
|
}
|
|
64
35
|
|
|
65
|
-
/**
|
|
66
|
-
* Chat component that handles:
|
|
67
|
-
* - Automatic conversation ID generation
|
|
68
|
-
* - Pre-chat form for collecting user information
|
|
69
|
-
* - Session persistence in localStorage
|
|
70
|
-
*/
|
|
71
36
|
export function Chat({
|
|
72
37
|
config,
|
|
73
38
|
className = '',
|
|
@@ -80,22 +45,63 @@ export function Chat({
|
|
|
80
45
|
const [userInfo, setUserInfo] = useState<IUser | null>(null);
|
|
81
46
|
const [conversationId, setConversationId] = useState<string>('');
|
|
82
47
|
const [isLoading, setIsLoading] = useState(true);
|
|
48
|
+
const [isAnimating, setIsAnimating] = useState(false);
|
|
49
|
+
const [showWidget, setShowWidget] = useState(false);
|
|
83
50
|
|
|
84
|
-
|
|
85
|
-
const {
|
|
51
|
+
const identityMode = config.identityCollection || 'progressive';
|
|
52
|
+
const { position, isDragging, showHint, handlers } = useDraggablePosition(config.position);
|
|
53
|
+
|
|
54
|
+
const { currentState, setState: setStateRaw } = useChatWidgetState({
|
|
86
55
|
state,
|
|
87
56
|
defaultState,
|
|
88
57
|
onStateChange,
|
|
89
58
|
});
|
|
90
59
|
|
|
91
|
-
//
|
|
60
|
+
// Wrap setState with animation handling
|
|
61
|
+
const setState = useCallback(
|
|
62
|
+
(newState: ChatWidgetState) => {
|
|
63
|
+
if (newState === 'open' && currentState === 'minimized') {
|
|
64
|
+
// Opening: show widget immediately, trigger animation
|
|
65
|
+
setShowWidget(true);
|
|
66
|
+
setIsAnimating(true);
|
|
67
|
+
setStateRaw(newState);
|
|
68
|
+
// Let the animation class apply on next frame
|
|
69
|
+
requestAnimationFrame(() => {
|
|
70
|
+
requestAnimationFrame(() => {
|
|
71
|
+
setIsAnimating(false);
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
} else if (
|
|
75
|
+
(newState === 'minimized' || newState === 'closed') &&
|
|
76
|
+
currentState === 'open'
|
|
77
|
+
) {
|
|
78
|
+
// Closing: animate out, then change state
|
|
79
|
+
setIsAnimating(true);
|
|
80
|
+
setTimeout(() => {
|
|
81
|
+
setShowWidget(false);
|
|
82
|
+
setIsAnimating(false);
|
|
83
|
+
setStateRaw(newState);
|
|
84
|
+
}, 200);
|
|
85
|
+
} else {
|
|
86
|
+
setStateRaw(newState);
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
[currentState, setStateRaw]
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
// Sync showWidget with state
|
|
93
|
+
useEffect(() => {
|
|
94
|
+
if (currentState === 'open') {
|
|
95
|
+
setShowWidget(true);
|
|
96
|
+
}
|
|
97
|
+
}, [currentState]);
|
|
98
|
+
|
|
99
|
+
// Initialize session
|
|
92
100
|
useEffect(() => {
|
|
93
101
|
const initializeSession = () => {
|
|
94
102
|
try {
|
|
95
|
-
// Check if user provided initial data
|
|
96
103
|
if (config.currentUser?.email && config.currentUser?.name) {
|
|
97
104
|
const convId = config.conversationId || generateSessionId();
|
|
98
|
-
|
|
99
105
|
const user: IUser = {
|
|
100
106
|
name: config.currentUser.name,
|
|
101
107
|
email: config.currentUser.email,
|
|
@@ -103,31 +109,24 @@ export function Chat({
|
|
|
103
109
|
type: 'customer',
|
|
104
110
|
status: config.currentUser.status,
|
|
105
111
|
};
|
|
106
|
-
|
|
107
112
|
setUserInfo(user);
|
|
108
113
|
setConversationId(convId);
|
|
109
114
|
setIsLoading(false);
|
|
110
115
|
return;
|
|
111
116
|
}
|
|
112
117
|
|
|
113
|
-
// Try to load from localStorage
|
|
114
118
|
const storedDataJson = localStorage.getItem(`${storageKeyPrefix}_user`);
|
|
115
|
-
|
|
116
119
|
if (storedDataJson) {
|
|
117
120
|
const storedData: StoredUserData = JSON.parse(storedDataJson);
|
|
118
|
-
|
|
119
|
-
// Check if session is still valid (24 hours)
|
|
120
121
|
const isExpired = Date.now() - storedData.timestamp > 24 * 60 * 60 * 1000;
|
|
121
122
|
|
|
122
123
|
if (!isExpired && storedData.email && storedData.name) {
|
|
123
|
-
// Restore session
|
|
124
124
|
const user: IUser = {
|
|
125
125
|
name: storedData.name,
|
|
126
126
|
email: storedData.email,
|
|
127
127
|
type: 'customer',
|
|
128
128
|
status: 'online',
|
|
129
129
|
};
|
|
130
|
-
|
|
131
130
|
setUserInfo(user);
|
|
132
131
|
setConversationId(storedData.conversationId);
|
|
133
132
|
setIsLoading(false);
|
|
@@ -138,20 +137,11 @@ export function Chat({
|
|
|
138
137
|
const convId = config.conversationId || generateSessionId();
|
|
139
138
|
setConversationId(convId);
|
|
140
139
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
const user: IUser = {
|
|
144
|
-
name: config.currentUser.name,
|
|
145
|
-
email: config.currentUser.email,
|
|
146
|
-
avatar: config.currentUser.avatar,
|
|
147
|
-
type: 'customer',
|
|
148
|
-
status: 'online',
|
|
149
|
-
};
|
|
150
|
-
setUserInfo(user);
|
|
140
|
+
if (identityMode === 'progressive' || identityMode === 'none') {
|
|
141
|
+
setUserInfo(null);
|
|
151
142
|
}
|
|
152
143
|
} catch (error) {
|
|
153
144
|
console.error('Error initializing chat session:', error);
|
|
154
|
-
// Generate fallback IDs
|
|
155
145
|
setConversationId(config.conversationId || generateSessionId());
|
|
156
146
|
} finally {
|
|
157
147
|
setIsLoading(false);
|
|
@@ -159,21 +149,13 @@ export function Chat({
|
|
|
159
149
|
};
|
|
160
150
|
|
|
161
151
|
initializeSession();
|
|
162
|
-
}, [config, storageKeyPrefix]);
|
|
152
|
+
}, [config, storageKeyPrefix, identityMode]);
|
|
163
153
|
|
|
164
|
-
// Handle pre-chat form submission
|
|
165
154
|
const handlePreChatSubmit = useCallback(
|
|
166
155
|
(name: string, email: string) => {
|
|
167
156
|
const convId = conversationId || generateSessionId();
|
|
157
|
+
const user: IUser = { name, email, type: 'customer', status: 'online' };
|
|
168
158
|
|
|
169
|
-
const user: IUser = {
|
|
170
|
-
name,
|
|
171
|
-
email,
|
|
172
|
-
type: 'customer',
|
|
173
|
-
status: 'online',
|
|
174
|
-
};
|
|
175
|
-
|
|
176
|
-
// Store in localStorage
|
|
177
159
|
const storageData: StoredUserData = {
|
|
178
160
|
name,
|
|
179
161
|
email,
|
|
@@ -183,8 +165,8 @@ export function Chat({
|
|
|
183
165
|
|
|
184
166
|
try {
|
|
185
167
|
localStorage.setItem(`${storageKeyPrefix}_user`, JSON.stringify(storageData));
|
|
186
|
-
} catch
|
|
187
|
-
|
|
168
|
+
} catch {
|
|
169
|
+
// Storage unavailable
|
|
188
170
|
}
|
|
189
171
|
|
|
190
172
|
setUserInfo(user);
|
|
@@ -194,35 +176,56 @@ export function Chat({
|
|
|
194
176
|
[conversationId, storageKeyPrefix, onPreChatSubmit]
|
|
195
177
|
);
|
|
196
178
|
|
|
197
|
-
|
|
198
|
-
if (
|
|
199
|
-
return null; // Or you could show a loading spinner
|
|
200
|
-
}
|
|
179
|
+
if (isLoading) return null;
|
|
180
|
+
if (currentState === 'closed') return null;
|
|
201
181
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
}
|
|
182
|
+
const positionClass = position === 'left' ? 'left-4' : 'right-4';
|
|
183
|
+
const primaryColor = config.theme?.primary || '#337eff';
|
|
184
|
+
const primaryStrong = config.theme?.primaryStrong || '#005eff';
|
|
206
185
|
|
|
207
|
-
//
|
|
186
|
+
// FAB button — only show when minimized
|
|
208
187
|
if (currentState === 'minimized') {
|
|
209
188
|
return (
|
|
210
|
-
<div className={`fixed bottom-
|
|
189
|
+
<div className={`fixed bottom-5 ${positionClass} z-50 ${className}`}>
|
|
211
190
|
<button
|
|
212
191
|
type="button"
|
|
213
192
|
onClick={() => setState('open')}
|
|
214
|
-
className=
|
|
193
|
+
className={`group relative rounded-full text-white transition-all duration-300 flex items-center justify-center touch-none select-none ${
|
|
194
|
+
showHint ? 'animate-bounce' : ''
|
|
195
|
+
} ${isDragging ? 'cursor-grabbing' : 'cursor-grab'}`}
|
|
196
|
+
onMouseEnter={(e) => { e.currentTarget.style.transform = 'scale(1.08)'; }}
|
|
197
|
+
onMouseLeave={(e) => { e.currentTarget.style.transform = isDragging ? 'scale(1.1)' : 'scale(1)'; }}
|
|
198
|
+
style={{
|
|
199
|
+
width: 64,
|
|
200
|
+
height: 64,
|
|
201
|
+
background: `linear-gradient(135deg, ${primaryColor}, ${primaryStrong})`,
|
|
202
|
+
boxShadow: [
|
|
203
|
+
`0 4px 24px 0 ${primaryColor}80`,
|
|
204
|
+
`0 8px 32px -4px rgba(0,0,0,0.3)`,
|
|
205
|
+
`inset 0 1px 0 0 rgba(255,255,255,0.2)`,
|
|
206
|
+
].join(', '),
|
|
207
|
+
}}
|
|
215
208
|
aria-label="Open chat"
|
|
209
|
+
{...handlers}
|
|
216
210
|
>
|
|
217
|
-
<
|
|
211
|
+
<ChatBubbleIcon size={28} color="white" className="pointer-events-none" />
|
|
212
|
+
|
|
213
|
+
{/* Pulse ring — subtle blue glow */}
|
|
214
|
+
<span
|
|
215
|
+
className="absolute inset-0 rounded-full animate-ping pointer-events-none"
|
|
216
|
+
style={{
|
|
217
|
+
backgroundColor: primaryColor,
|
|
218
|
+
opacity: 0.15,
|
|
219
|
+
animationDuration: '2.5s',
|
|
220
|
+
}}
|
|
221
|
+
/>
|
|
218
222
|
</button>
|
|
219
223
|
</div>
|
|
220
224
|
);
|
|
221
225
|
}
|
|
222
226
|
|
|
223
|
-
// Open state
|
|
224
|
-
|
|
225
|
-
if (!userInfo || !userInfo.email || !userInfo.name) {
|
|
227
|
+
// Open state — pre-chat form (form mode only)
|
|
228
|
+
if (identityMode === 'form' && (!userInfo || !userInfo.email || !userInfo.name)) {
|
|
226
229
|
return (
|
|
227
230
|
<PreChatForm
|
|
228
231
|
onSubmit={handlePreChatSubmit}
|
|
@@ -235,19 +238,36 @@ export function Chat({
|
|
|
235
238
|
);
|
|
236
239
|
}
|
|
237
240
|
|
|
238
|
-
//
|
|
241
|
+
// Open state — chat widget
|
|
239
242
|
const fullConfig: IChatConfig = {
|
|
240
243
|
...config,
|
|
241
244
|
conversationId,
|
|
242
|
-
currentUser: userInfo,
|
|
245
|
+
currentUser: userInfo || undefined,
|
|
243
246
|
};
|
|
244
247
|
|
|
248
|
+
// Animation styles for open/close
|
|
249
|
+
const widgetAnimationStyle: React.CSSProperties =
|
|
250
|
+
showWidget && !isAnimating
|
|
251
|
+
? {
|
|
252
|
+
opacity: 1,
|
|
253
|
+
transform: 'translateY(0) scale(1)',
|
|
254
|
+
transition: 'opacity 0.25s ease-out, transform 0.25s ease-out',
|
|
255
|
+
}
|
|
256
|
+
: {
|
|
257
|
+
opacity: 0,
|
|
258
|
+
transform: 'translateY(12px) scale(0.97)',
|
|
259
|
+
transition: 'opacity 0.2s ease-in, transform 0.2s ease-in',
|
|
260
|
+
};
|
|
261
|
+
|
|
245
262
|
return (
|
|
246
|
-
<
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
263
|
+
<div style={widgetAnimationStyle}>
|
|
264
|
+
<ChatWidget
|
|
265
|
+
config={fullConfig}
|
|
266
|
+
className={className}
|
|
267
|
+
onClose={() => setState('closed')}
|
|
268
|
+
onMinimize={() => setState('minimized')}
|
|
269
|
+
resolvedPosition={position}
|
|
270
|
+
/>
|
|
271
|
+
</div>
|
|
252
272
|
);
|
|
253
273
|
}
|
|
@@ -1,93 +1,148 @@
|
|
|
1
|
-
import type { IUser } from '../types';
|
|
1
|
+
import type { IChatTheme, IUser } from '../types';
|
|
2
|
+
import { XcelsiorSymbol } from './BrandIcons';
|
|
2
3
|
|
|
3
4
|
interface ChatHeaderProps {
|
|
4
5
|
agent?: IUser;
|
|
5
6
|
onClose?: () => void;
|
|
6
7
|
onMinimize?: () => void;
|
|
8
|
+
theme?: IChatTheme;
|
|
7
9
|
}
|
|
8
10
|
|
|
9
|
-
export function ChatHeader({ agent, onClose, onMinimize }: ChatHeaderProps) {
|
|
11
|
+
export function ChatHeader({ agent, onClose, onMinimize, theme }: ChatHeaderProps) {
|
|
10
12
|
return (
|
|
11
|
-
<div
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
<div
|
|
14
|
+
className="relative text-white overflow-hidden"
|
|
15
|
+
style={{
|
|
16
|
+
flexShrink: 0,
|
|
17
|
+
/* Layered blue smoke/glow effect for premium look */
|
|
18
|
+
background: [
|
|
19
|
+
'radial-gradient(ellipse at 30% 50%, rgba(0,50,255,0.4) 0%, transparent 60%)',
|
|
20
|
+
'radial-gradient(ellipse at 70% 30%, rgba(0,80,255,0.3) 0%, transparent 50%)',
|
|
21
|
+
'radial-gradient(ellipse at 50% 80%, rgba(0,30,200,0.3) 0%, transparent 60%)',
|
|
22
|
+
`linear-gradient(135deg, #0030cc 0%, #001a66 50%, #000d33 100%)`,
|
|
23
|
+
].join(', '),
|
|
24
|
+
}}
|
|
25
|
+
>
|
|
26
|
+
{/* Top glass highlight — subtle light sweep for depth */}
|
|
27
|
+
<div
|
|
28
|
+
className="absolute inset-0 pointer-events-none"
|
|
29
|
+
style={{
|
|
30
|
+
background:
|
|
31
|
+
'linear-gradient(180deg, rgba(255,255,255,0.08) 0%, rgba(255,255,255,0) 50%)',
|
|
32
|
+
}}
|
|
33
|
+
/>
|
|
34
|
+
|
|
35
|
+
<div className="flex items-center justify-between" style={{ padding: '8px 16px' }}>
|
|
36
|
+
<div className="flex items-center gap-3">
|
|
37
|
+
<div className="relative">
|
|
15
38
|
{agent?.avatar ? (
|
|
16
39
|
<img
|
|
17
40
|
src={agent.avatar}
|
|
18
41
|
alt={agent.name}
|
|
19
42
|
className="h-10 w-10 rounded-full object-cover"
|
|
43
|
+
style={{
|
|
44
|
+
boxShadow: '0 2px 8px rgba(0,0,0,0.25)',
|
|
45
|
+
}}
|
|
20
46
|
/>
|
|
21
47
|
) : (
|
|
22
|
-
'
|
|
48
|
+
<div style={{ filter: 'drop-shadow(0 2px 8px rgba(0,0,0,0.3))' }}>
|
|
49
|
+
<XcelsiorSymbol size={28} color="white" />
|
|
50
|
+
</div>
|
|
51
|
+
)}
|
|
52
|
+
{agent?.status === 'online' && (
|
|
53
|
+
<div
|
|
54
|
+
className="absolute -bottom-0.5 -right-0.5 h-3.5 w-3.5 rounded-full"
|
|
55
|
+
style={{
|
|
56
|
+
backgroundColor: theme?.statusPositive || '#1ed473',
|
|
57
|
+
border: '2.5px solid rgba(0,50,180,0.9)',
|
|
58
|
+
boxShadow: `0 0 8px ${theme?.statusPositive || '#1ed473'}80`,
|
|
59
|
+
}}
|
|
60
|
+
/>
|
|
23
61
|
)}
|
|
24
62
|
</div>
|
|
25
|
-
|
|
26
|
-
<
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
63
|
+
<div>
|
|
64
|
+
<h3
|
|
65
|
+
className="font-semibold leading-tight"
|
|
66
|
+
style={{
|
|
67
|
+
fontSize: '15px',
|
|
68
|
+
letterSpacing: '-0.01em',
|
|
69
|
+
}}
|
|
70
|
+
>
|
|
71
|
+
{agent?.name || 'Xcelsior Software'}
|
|
72
|
+
</h3>
|
|
73
|
+
<p
|
|
74
|
+
className="mt-0.5 leading-tight"
|
|
75
|
+
style={{
|
|
76
|
+
fontSize: '12px',
|
|
77
|
+
letterSpacing: '0.015em',
|
|
78
|
+
color: 'rgba(255,255,255,0.6)',
|
|
79
|
+
}}
|
|
80
|
+
>
|
|
81
|
+
{agent?.status === 'online' ? (
|
|
82
|
+
<span className="flex items-center gap-1.5">
|
|
83
|
+
<span
|
|
84
|
+
className="inline-block w-1.5 h-1.5 rounded-full"
|
|
85
|
+
style={{
|
|
86
|
+
backgroundColor: theme?.statusPositive || '#1ed473',
|
|
87
|
+
boxShadow: `0 0 4px ${theme?.statusPositive || '#1ed473'}60`,
|
|
88
|
+
}}
|
|
89
|
+
/>
|
|
90
|
+
Online now
|
|
91
|
+
</span>
|
|
92
|
+
) : agent?.status === 'away' ? (
|
|
93
|
+
'Away'
|
|
94
|
+
) : (
|
|
95
|
+
'We typically reply within minutes'
|
|
96
|
+
)}
|
|
97
|
+
</p>
|
|
98
|
+
</div>
|
|
38
99
|
</div>
|
|
39
|
-
</div>
|
|
40
100
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
<title>Minimize icon</title>
|
|
57
|
-
<path
|
|
58
|
-
strokeLinecap="round"
|
|
59
|
-
strokeLinejoin="round"
|
|
60
|
-
strokeWidth={2}
|
|
61
|
-
d="M20 12H4"
|
|
62
|
-
/>
|
|
63
|
-
</svg>
|
|
64
|
-
</button>
|
|
65
|
-
)}
|
|
66
|
-
{onClose && (
|
|
67
|
-
<button
|
|
68
|
-
type="button"
|
|
69
|
-
onClick={onClose}
|
|
70
|
-
className="p-2 hover:bg-white/10 rounded-full transition-colors"
|
|
71
|
-
aria-label="Close chat"
|
|
72
|
-
>
|
|
73
|
-
<svg
|
|
74
|
-
className="w-5 h-5"
|
|
75
|
-
fill="none"
|
|
76
|
-
viewBox="0 0 24 24"
|
|
77
|
-
stroke="currentColor"
|
|
78
|
-
aria-hidden="true"
|
|
101
|
+
<div className="flex items-center gap-0.5">
|
|
102
|
+
{/* Single minimize button — always minimizes back to FAB bubble */}
|
|
103
|
+
{(onMinimize || onClose) && (
|
|
104
|
+
<button
|
|
105
|
+
type="button"
|
|
106
|
+
onClick={onMinimize || onClose}
|
|
107
|
+
className="p-2 rounded-lg transition-all duration-150"
|
|
108
|
+
style={{ backgroundColor: 'transparent' }}
|
|
109
|
+
onMouseEnter={(e) => {
|
|
110
|
+
e.currentTarget.style.backgroundColor = 'rgba(255,255,255,0.1)';
|
|
111
|
+
}}
|
|
112
|
+
onMouseLeave={(e) => {
|
|
113
|
+
e.currentTarget.style.backgroundColor = 'transparent';
|
|
114
|
+
}}
|
|
115
|
+
aria-label="Minimize chat"
|
|
79
116
|
>
|
|
80
|
-
<
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
117
|
+
<svg
|
|
118
|
+
width="18"
|
|
119
|
+
height="18"
|
|
120
|
+
fill="none"
|
|
121
|
+
viewBox="0 0 24 24"
|
|
122
|
+
stroke="currentColor"
|
|
123
|
+
aria-hidden="true"
|
|
124
|
+
>
|
|
125
|
+
<title>Minimize</title>
|
|
126
|
+
<path
|
|
127
|
+
strokeLinecap="round"
|
|
128
|
+
strokeLinejoin="round"
|
|
129
|
+
strokeWidth={2}
|
|
130
|
+
d="M6 18L18 6M6 6l12 12"
|
|
131
|
+
/>
|
|
132
|
+
</svg>
|
|
133
|
+
</button>
|
|
134
|
+
)}
|
|
135
|
+
</div>
|
|
90
136
|
</div>
|
|
137
|
+
|
|
138
|
+
{/* Bottom edge — subtle gradient line (matches website section dividers) */}
|
|
139
|
+
<div
|
|
140
|
+
className="h-px"
|
|
141
|
+
style={{
|
|
142
|
+
background:
|
|
143
|
+
'linear-gradient(90deg, transparent 5%, rgba(255,255,255,0.12) 30%, rgba(255,255,255,0.12) 70%, transparent 95%)',
|
|
144
|
+
}}
|
|
145
|
+
/>
|
|
91
146
|
</div>
|
|
92
147
|
);
|
|
93
148
|
}
|