@xcelsior/ui-chat 1.0.5 → 1.0.7
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 +13 -0
- package/README.md +82 -0
- package/dist/index.d.mts +72 -4
- package/dist/index.d.ts +72 -4
- package/dist/index.js +148 -43
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +149 -45
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/components/Chat.tsx +60 -1
- package/src/components/ChatWidget.tsx +13 -37
- package/src/components/MessageItem.tsx +0 -2
- package/src/components/PreChatForm.tsx +65 -4
- package/src/hooks/useChatWidgetState.ts +68 -0
- package/src/index.tsx +8 -3
package/src/components/Chat.tsx
CHANGED
|
@@ -2,6 +2,8 @@ import { useCallback, useEffect, useState } from 'react';
|
|
|
2
2
|
import { ChatWidget } from './ChatWidget';
|
|
3
3
|
import { PreChatForm } from './PreChatForm';
|
|
4
4
|
import type { IChatConfig, IUser } from '../types';
|
|
5
|
+
import { useChatWidgetState } from '../hooks/useChatWidgetState';
|
|
6
|
+
import type { ChatWidgetState } from '../hooks/useChatWidgetState';
|
|
5
7
|
|
|
6
8
|
interface ChatWidgetWrapperProps {
|
|
7
9
|
/**
|
|
@@ -24,6 +26,22 @@ interface ChatWidgetWrapperProps {
|
|
|
24
26
|
* Callback when user submits the pre-chat form
|
|
25
27
|
*/
|
|
26
28
|
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
|
+
state?: ChatWidgetState;
|
|
36
|
+
/**
|
|
37
|
+
* Default state for uncontrolled mode
|
|
38
|
+
* @default 'minimized'
|
|
39
|
+
*/
|
|
40
|
+
defaultState?: ChatWidgetState;
|
|
41
|
+
/**
|
|
42
|
+
* Callback when state changes
|
|
43
|
+
*/
|
|
44
|
+
onStateChange?: (state: ChatWidgetState) => void;
|
|
27
45
|
}
|
|
28
46
|
|
|
29
47
|
interface StoredUserData {
|
|
@@ -55,11 +73,21 @@ export function Chat({
|
|
|
55
73
|
className = '',
|
|
56
74
|
storageKeyPrefix = 'xcelsior_chat',
|
|
57
75
|
onPreChatSubmit,
|
|
76
|
+
state,
|
|
77
|
+
defaultState = 'minimized',
|
|
78
|
+
onStateChange,
|
|
58
79
|
}: ChatWidgetWrapperProps) {
|
|
59
80
|
const [userInfo, setUserInfo] = useState<IUser | null>(null);
|
|
60
81
|
const [conversationId, setConversationId] = useState<string>('');
|
|
61
82
|
const [isLoading, setIsLoading] = useState(true);
|
|
62
83
|
|
|
84
|
+
// Centralized state management using the custom hook
|
|
85
|
+
const { currentState, setState } = useChatWidgetState({
|
|
86
|
+
state,
|
|
87
|
+
defaultState,
|
|
88
|
+
onStateChange,
|
|
89
|
+
});
|
|
90
|
+
|
|
63
91
|
// Initialize user data from localStorage or generate new session
|
|
64
92
|
useEffect(() => {
|
|
65
93
|
const initializeSession = () => {
|
|
@@ -171,6 +199,28 @@ export function Chat({
|
|
|
171
199
|
return null; // Or you could show a loading spinner
|
|
172
200
|
}
|
|
173
201
|
|
|
202
|
+
// Handle closed state - fully hidden
|
|
203
|
+
if (currentState === 'closed') {
|
|
204
|
+
return null;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Handle minimized state - show bubble button only
|
|
208
|
+
if (currentState === 'minimized') {
|
|
209
|
+
return (
|
|
210
|
+
<div className={`fixed bottom-4 right-4 z-50 ${className}`}>
|
|
211
|
+
<button
|
|
212
|
+
type="button"
|
|
213
|
+
onClick={() => setState('open')}
|
|
214
|
+
className="h-14 w-14 rounded-full bg-gradient-to-r from-blue-600 to-purple-600 text-white shadow-lg hover:shadow-xl transition-all flex items-center justify-center relative"
|
|
215
|
+
aria-label="Open chat"
|
|
216
|
+
>
|
|
217
|
+
<span className="text-2xl">💬</span>
|
|
218
|
+
</button>
|
|
219
|
+
</div>
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Open state - show either pre-chat form or chat widget
|
|
174
224
|
// Show pre-chat form if user info is not available
|
|
175
225
|
if (!userInfo || !userInfo.email || !userInfo.name) {
|
|
176
226
|
return (
|
|
@@ -179,6 +229,8 @@ export function Chat({
|
|
|
179
229
|
className={className}
|
|
180
230
|
initialName={config.currentUser?.name}
|
|
181
231
|
initialEmail={config.currentUser?.email}
|
|
232
|
+
onClose={() => setState('closed')}
|
|
233
|
+
onMinimize={() => setState('minimized')}
|
|
182
234
|
/>
|
|
183
235
|
);
|
|
184
236
|
}
|
|
@@ -190,5 +242,12 @@ export function Chat({
|
|
|
190
242
|
currentUser: userInfo,
|
|
191
243
|
};
|
|
192
244
|
|
|
193
|
-
return
|
|
245
|
+
return (
|
|
246
|
+
<ChatWidget
|
|
247
|
+
config={fullConfig}
|
|
248
|
+
className={className}
|
|
249
|
+
onClose={() => setState('closed')}
|
|
250
|
+
onMinimize={() => setState('minimized')}
|
|
251
|
+
/>
|
|
252
|
+
);
|
|
194
253
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useCallback, useEffect
|
|
1
|
+
import { useCallback, useEffect } from 'react';
|
|
2
2
|
import { useWebSocket } from '../hooks/useWebSocket';
|
|
3
3
|
import { useMessages } from '../hooks/useMessages';
|
|
4
4
|
import { useFileUpload } from '../hooks/useFileUpload';
|
|
@@ -21,6 +21,14 @@ export interface ChatWidgetProps {
|
|
|
21
21
|
* External WebSocket connection (for agents with global connection)
|
|
22
22
|
*/
|
|
23
23
|
externalWebSocket?: WebSocket | null;
|
|
24
|
+
/**
|
|
25
|
+
* Callback when user wants to minimize the widget
|
|
26
|
+
*/
|
|
27
|
+
onMinimize?: () => void;
|
|
28
|
+
/**
|
|
29
|
+
* Callback when user wants to close the widget
|
|
30
|
+
*/
|
|
31
|
+
onClose?: () => void;
|
|
24
32
|
}
|
|
25
33
|
|
|
26
34
|
export function ChatWidget({
|
|
@@ -28,10 +36,9 @@ export function ChatWidget({
|
|
|
28
36
|
className = '',
|
|
29
37
|
variant = 'popover',
|
|
30
38
|
externalWebSocket,
|
|
39
|
+
onMinimize,
|
|
40
|
+
onClose,
|
|
31
41
|
}: ChatWidgetProps) {
|
|
32
|
-
const [isMinimized, setIsMinimized] = useState(false);
|
|
33
|
-
const [isClosed, setIsClosed] = useState(false);
|
|
34
|
-
|
|
35
42
|
const isFullPage = variant === 'fullPage';
|
|
36
43
|
|
|
37
44
|
// Initialize WebSocket connection (or use external one)
|
|
@@ -107,37 +114,6 @@ export function ChatWidget({
|
|
|
107
114
|
}
|
|
108
115
|
}, [websocket.error, config]);
|
|
109
116
|
|
|
110
|
-
// For fullPage variant, ignore minimize/close state
|
|
111
|
-
if (!isFullPage) {
|
|
112
|
-
if (isClosed) {
|
|
113
|
-
return null;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
// Minimized view (floating button) - only for popover
|
|
117
|
-
if (isMinimized) {
|
|
118
|
-
return (
|
|
119
|
-
<div className={`fixed bottom-4 right-4 z-50 ${className}`}>
|
|
120
|
-
<button
|
|
121
|
-
type="button"
|
|
122
|
-
onClick={() => setIsMinimized(false)}
|
|
123
|
-
className="h-14 w-14 rounded-full bg-gradient-to-r from-blue-600 to-purple-600 text-white shadow-lg hover:shadow-xl transition-all flex items-center justify-center relative"
|
|
124
|
-
aria-label="Open chat"
|
|
125
|
-
>
|
|
126
|
-
<span className="text-2xl">💬</span>
|
|
127
|
-
{messages.some(
|
|
128
|
-
(msg) =>
|
|
129
|
-
msg.senderId !== config.currentUser.email && msg.status !== 'read'
|
|
130
|
-
) && (
|
|
131
|
-
<span className="absolute -top-1 -right-1 h-5 w-5 rounded-full bg-red-500 text-white text-xs flex items-center justify-center">
|
|
132
|
-
!
|
|
133
|
-
</span>
|
|
134
|
-
)}
|
|
135
|
-
</button>
|
|
136
|
-
</div>
|
|
137
|
-
);
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
|
|
141
117
|
// Container styles based on variant
|
|
142
118
|
const containerClasses = isFullPage
|
|
143
119
|
? `flex flex-col bg-white dark:bg-gray-900 h-full ${className}`
|
|
@@ -165,8 +141,8 @@ export function ChatWidget({
|
|
|
165
141
|
}
|
|
166
142
|
: undefined
|
|
167
143
|
}
|
|
168
|
-
onMinimize={
|
|
169
|
-
onClose={
|
|
144
|
+
onMinimize={onMinimize}
|
|
145
|
+
onClose={onClose}
|
|
170
146
|
/>
|
|
171
147
|
)}
|
|
172
148
|
|
|
@@ -62,7 +62,6 @@ export function MessageItem({
|
|
|
62
62
|
}`}
|
|
63
63
|
>
|
|
64
64
|
{message.messageType === 'text' && (
|
|
65
|
-
<div className="prose prose-sm dark:prose-invert max-w-none">
|
|
66
65
|
<ReactMarkdown
|
|
67
66
|
components={{
|
|
68
67
|
p: ({ children }) => <p className="mb-0">{children}</p>,
|
|
@@ -90,7 +89,6 @@ export function MessageItem({
|
|
|
90
89
|
>
|
|
91
90
|
{message.content}
|
|
92
91
|
</ReactMarkdown>
|
|
93
|
-
</div>
|
|
94
92
|
)}
|
|
95
93
|
{message.messageType === 'image' && (
|
|
96
94
|
<div>
|
|
@@ -5,6 +5,14 @@ interface PreChatFormProps {
|
|
|
5
5
|
className?: string;
|
|
6
6
|
initialName?: string;
|
|
7
7
|
initialEmail?: string;
|
|
8
|
+
/**
|
|
9
|
+
* Callback when user wants to minimize the form
|
|
10
|
+
*/
|
|
11
|
+
onMinimize: () => void;
|
|
12
|
+
/**
|
|
13
|
+
* Callback when user wants to close the form
|
|
14
|
+
*/
|
|
15
|
+
onClose: () => void;
|
|
8
16
|
}
|
|
9
17
|
|
|
10
18
|
/**
|
|
@@ -15,6 +23,8 @@ export function PreChatForm({
|
|
|
15
23
|
className = '',
|
|
16
24
|
initialName = '',
|
|
17
25
|
initialEmail = '',
|
|
26
|
+
onMinimize,
|
|
27
|
+
onClose,
|
|
18
28
|
}: PreChatFormProps) {
|
|
19
29
|
const [name, setName] = useState(initialName);
|
|
20
30
|
const [email, setEmail] = useState(initialEmail);
|
|
@@ -62,6 +72,7 @@ export function PreChatForm({
|
|
|
62
72
|
}
|
|
63
73
|
};
|
|
64
74
|
|
|
75
|
+
// Show full form
|
|
65
76
|
return (
|
|
66
77
|
<div
|
|
67
78
|
className={`fixed bottom-4 right-4 z-50 flex flex-col bg-white dark:bg-gray-900 rounded-lg shadow-2xl overflow-hidden ${className}`}
|
|
@@ -72,10 +83,60 @@ export function PreChatForm({
|
|
|
72
83
|
>
|
|
73
84
|
{/* Header */}
|
|
74
85
|
<div className="bg-gradient-to-r from-blue-600 to-purple-600 text-white px-6 py-4">
|
|
75
|
-
<
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
86
|
+
<div className="flex items-start justify-between">
|
|
87
|
+
<div className="flex-1">
|
|
88
|
+
<h2 className="text-lg font-semibold">Start a Conversation</h2>
|
|
89
|
+
<p className="text-sm text-blue-100 mt-1">
|
|
90
|
+
Please provide your details to continue
|
|
91
|
+
</p>
|
|
92
|
+
</div>
|
|
93
|
+
<div className="flex gap-2 ml-2">
|
|
94
|
+
{/* Minimize Button */}
|
|
95
|
+
<button
|
|
96
|
+
type="button"
|
|
97
|
+
onClick={onMinimize}
|
|
98
|
+
className="text-white hover:bg-white/20 rounded p-1 transition-colors"
|
|
99
|
+
aria-label="Minimize chat"
|
|
100
|
+
>
|
|
101
|
+
<svg
|
|
102
|
+
className="w-5 h-5"
|
|
103
|
+
fill="none"
|
|
104
|
+
stroke="currentColor"
|
|
105
|
+
viewBox="0 0 24 24"
|
|
106
|
+
>
|
|
107
|
+
<title>Minimize</title>
|
|
108
|
+
<path
|
|
109
|
+
strokeLinecap="round"
|
|
110
|
+
strokeLinejoin="round"
|
|
111
|
+
strokeWidth={2}
|
|
112
|
+
d="M20 12H4"
|
|
113
|
+
/>
|
|
114
|
+
</svg>
|
|
115
|
+
</button>
|
|
116
|
+
{/* Close Button */}
|
|
117
|
+
<button
|
|
118
|
+
type="button"
|
|
119
|
+
onClick={onClose}
|
|
120
|
+
className="text-white hover:bg-white/20 rounded p-1 transition-colors"
|
|
121
|
+
aria-label="Close chat"
|
|
122
|
+
>
|
|
123
|
+
<svg
|
|
124
|
+
className="w-5 h-5"
|
|
125
|
+
fill="none"
|
|
126
|
+
stroke="currentColor"
|
|
127
|
+
viewBox="0 0 24 24"
|
|
128
|
+
>
|
|
129
|
+
<title>Close</title>
|
|
130
|
+
<path
|
|
131
|
+
strokeLinecap="round"
|
|
132
|
+
strokeLinejoin="round"
|
|
133
|
+
strokeWidth={2}
|
|
134
|
+
d="M6 18L18 6M6 6l12 12"
|
|
135
|
+
/>
|
|
136
|
+
</svg>
|
|
137
|
+
</button>
|
|
138
|
+
</div>
|
|
139
|
+
</div>
|
|
79
140
|
</div>
|
|
80
141
|
|
|
81
142
|
{/* Form */}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { useCallback, useState } from 'react';
|
|
2
|
+
|
|
3
|
+
export type ChatWidgetState = 'open' | 'minimized' | 'closed' | 'undefined';
|
|
4
|
+
|
|
5
|
+
export interface UseChatWidgetStateOptions {
|
|
6
|
+
/**
|
|
7
|
+
* Controlled state. When provided, the component is controlled.
|
|
8
|
+
*/
|
|
9
|
+
state?: ChatWidgetState;
|
|
10
|
+
/**
|
|
11
|
+
* Default state for uncontrolled mode
|
|
12
|
+
* @default 'minimized'
|
|
13
|
+
*/
|
|
14
|
+
defaultState?: ChatWidgetState;
|
|
15
|
+
/**
|
|
16
|
+
* Callback when state changes
|
|
17
|
+
*/
|
|
18
|
+
onStateChange?: (state: ChatWidgetState) => void;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface UseChatWidgetStateReturn {
|
|
22
|
+
/**
|
|
23
|
+
* Current state of the widget
|
|
24
|
+
*/
|
|
25
|
+
currentState: ChatWidgetState;
|
|
26
|
+
/**
|
|
27
|
+
* Function to update the state
|
|
28
|
+
*/
|
|
29
|
+
setState: (newState: ChatWidgetState) => void;
|
|
30
|
+
/**
|
|
31
|
+
* Whether the state is controlled
|
|
32
|
+
*/
|
|
33
|
+
isControlled: boolean;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Hook to manage chat widget state (controlled vs uncontrolled)
|
|
38
|
+
* Encapsulates the logic for handling both controlled and uncontrolled state patterns
|
|
39
|
+
*/
|
|
40
|
+
export function useChatWidgetState({
|
|
41
|
+
state: controlledState,
|
|
42
|
+
defaultState = 'minimized',
|
|
43
|
+
onStateChange,
|
|
44
|
+
}: UseChatWidgetStateOptions): UseChatWidgetStateReturn {
|
|
45
|
+
// Handle controlled vs uncontrolled state
|
|
46
|
+
const [uncontrolledState, setUncontrolledState] = useState<ChatWidgetState>(defaultState);
|
|
47
|
+
const isControlled = controlledState !== undefined && controlledState !== 'undefined';
|
|
48
|
+
const currentState = isControlled ? controlledState : uncontrolledState;
|
|
49
|
+
|
|
50
|
+
// State setter that works for both controlled and uncontrolled modes
|
|
51
|
+
const setState = useCallback(
|
|
52
|
+
(newValue: ChatWidgetState) => {
|
|
53
|
+
// Update internal state if uncontrolled
|
|
54
|
+
if (!isControlled) {
|
|
55
|
+
setUncontrolledState(newValue);
|
|
56
|
+
}
|
|
57
|
+
// Always call the change handler if provided
|
|
58
|
+
onStateChange?.(newValue);
|
|
59
|
+
},
|
|
60
|
+
[isControlled, onStateChange]
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
currentState,
|
|
65
|
+
setState,
|
|
66
|
+
isControlled,
|
|
67
|
+
};
|
|
68
|
+
}
|
package/src/index.tsx
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Main components
|
|
2
2
|
export { ChatWidget } from './components/ChatWidget';
|
|
3
|
-
|
|
3
|
+
export type { ChatWidgetProps } from './components/ChatWidget';
|
|
4
4
|
export { Chat } from './components/Chat';
|
|
5
5
|
|
|
6
6
|
// Individual components (for custom implementations)
|
|
@@ -16,6 +16,12 @@ export { useWebSocket } from './hooks/useWebSocket';
|
|
|
16
16
|
export { useMessages } from './hooks/useMessages';
|
|
17
17
|
export { useFileUpload } from './hooks/useFileUpload';
|
|
18
18
|
export { useTypingIndicator } from './hooks/useTypingIndicator';
|
|
19
|
+
export { useChatWidgetState } from './hooks/useChatWidgetState';
|
|
20
|
+
export type {
|
|
21
|
+
ChatWidgetState,
|
|
22
|
+
UseChatWidgetStateOptions,
|
|
23
|
+
UseChatWidgetStateReturn,
|
|
24
|
+
} from './hooks/useChatWidgetState';
|
|
19
25
|
|
|
20
26
|
// Utilities
|
|
21
27
|
export { fetchMessages } from './utils/api';
|
|
@@ -40,7 +46,7 @@ import type {
|
|
|
40
46
|
ConversationChannel,
|
|
41
47
|
} from './types';
|
|
42
48
|
|
|
43
|
-
export {
|
|
49
|
+
export type {
|
|
44
50
|
IUser,
|
|
45
51
|
IMessage,
|
|
46
52
|
IConversation,
|
|
@@ -57,5 +63,4 @@ export {
|
|
|
57
63
|
ConversationStatus,
|
|
58
64
|
ConversationPriority,
|
|
59
65
|
ConversationChannel,
|
|
60
|
-
ChatWidgetProps,
|
|
61
66
|
};
|