@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.
@@ -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 <ChatWidget config={fullConfig} className={className} />;
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, useState } from 'react';
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={() => setIsMinimized(true)}
169
- onClose={() => setIsClosed(true)}
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
- <h2 className="text-lg font-semibold">Start a Conversation</h2>
76
- <p className="text-sm text-blue-100 mt-1">
77
- Please provide your details to continue
78
- </p>
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
- import type { ChatWidgetProps } from './components/ChatWidget';
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
  };