@xcelsior/ui-chat 1.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.
@@ -0,0 +1,54 @@
1
+ import type { Meta, StoryObj } from '@storybook/react';
2
+ import { Chat } from './Chat';
3
+ import type { IChatConfig } from '../types';
4
+
5
+ const meta: Meta<typeof Chat> = {
6
+ title: 'Components/Chat',
7
+ component: Chat,
8
+ parameters: {
9
+ layout: 'fullscreen',
10
+ },
11
+ tags: ['autodocs'],
12
+ };
13
+
14
+ export default meta;
15
+ type Story = StoryObj<typeof Chat>;
16
+
17
+ // Base configuration without user details
18
+ const baseConfig: Omit<IChatConfig, 'currentUser' | 'conversationId' | 'userId'> = {
19
+ websocketUrl: 'wss://dev-ws-chat.xcelsior.co',
20
+ httpApiUrl: 'https://dev-chat.xcelsior.co/api',
21
+ enableEmoji: true,
22
+ enableFileUpload: true,
23
+ enableTypingIndicator: true,
24
+ fileUpload: {
25
+ uploadUrl: `https://dev-chat.xcelsior.co/api/attachments/upload-url`,
26
+ maxFileSize: 2 * 1024 * 1024, // 2MB (default)
27
+ allowedTypes: [
28
+ 'image/jpeg',
29
+ 'image/png',
30
+ 'image/gif',
31
+ 'image/webp',
32
+ 'application/pdf',
33
+ 'application/msword',
34
+ 'text/plain',
35
+ 'text/csv',
36
+ ],
37
+ },
38
+ enableReadReceipts: true,
39
+ apiKey: 'test',
40
+ toast: {
41
+ success: (message: string) => console.log('Success:', message),
42
+ error: (message: string) => console.error('Error:', message),
43
+ info: (message: string) => console.log('Info:', message),
44
+ },
45
+ };
46
+
47
+ /**
48
+ * Default story - Shows pre-chat form first
49
+ */
50
+ export const Default: Story = {
51
+ args: {
52
+ config: baseConfig,
53
+ },
54
+ };
@@ -0,0 +1,194 @@
1
+ import { useCallback, useEffect, useState } from 'react';
2
+ import { ChatWidget } from './ChatWidget';
3
+ import { PreChatForm } from './PreChatForm';
4
+ import type { IChatConfig, IUser } from '../types';
5
+
6
+ interface ChatWidgetWrapperProps {
7
+ /**
8
+ * Base configuration for the chat widget (without user info and conversationId)
9
+ */
10
+ config: Omit<IChatConfig, 'currentUser' | 'conversationId' | 'userId'> & {
11
+ currentUser?: Partial<IUser>;
12
+ conversationId?: string;
13
+ };
14
+ /**
15
+ * Custom className for the wrapper
16
+ */
17
+ className?: string;
18
+ /**
19
+ * Storage key prefix for persisting user data
20
+ * Defaults to 'xcelsior_chat'
21
+ */
22
+ storageKeyPrefix?: string;
23
+ /**
24
+ * Callback when user submits the pre-chat form
25
+ */
26
+ onPreChatSubmit?: (user: IUser) => void;
27
+ }
28
+
29
+ interface StoredUserData {
30
+ name: string;
31
+ email: string;
32
+ conversationId: string;
33
+ timestamp: number;
34
+ }
35
+
36
+ /**
37
+ * Generates a unique session-based ID
38
+ */
39
+ function generateSessionId(): string {
40
+ if (typeof crypto !== 'undefined' && crypto.randomUUID) {
41
+ return crypto.randomUUID();
42
+ }
43
+ // Fallback for older browsers
44
+ return `${Date.now()}-${Math.random().toString(36).substring(2, 15)}`;
45
+ }
46
+
47
+ /**
48
+ * Chat component that handles:
49
+ * - Automatic conversation ID generation
50
+ * - Pre-chat form for collecting user information
51
+ * - Session persistence in localStorage
52
+ */
53
+ export function Chat({
54
+ config,
55
+ className = '',
56
+ storageKeyPrefix = 'xcelsior_chat',
57
+ onPreChatSubmit,
58
+ }: ChatWidgetWrapperProps) {
59
+ const [userInfo, setUserInfo] = useState<IUser | null>(null);
60
+ const [conversationId, setConversationId] = useState<string>('');
61
+ const [isLoading, setIsLoading] = useState(true);
62
+
63
+ // Initialize user data from localStorage or generate new session
64
+ useEffect(() => {
65
+ const initializeSession = () => {
66
+ try {
67
+ // Check if user provided initial data
68
+ if (config.currentUser?.email && config.currentUser?.name) {
69
+ const convId = config.conversationId || generateSessionId();
70
+
71
+ const user: IUser = {
72
+ name: config.currentUser.name,
73
+ email: config.currentUser.email,
74
+ avatar: config.currentUser.avatar,
75
+ type: 'customer',
76
+ status: config.currentUser.status,
77
+ };
78
+
79
+ setUserInfo(user);
80
+ setConversationId(convId);
81
+ setIsLoading(false);
82
+ return;
83
+ }
84
+
85
+ // Try to load from localStorage
86
+ const storedDataJson = localStorage.getItem(`${storageKeyPrefix}_user`);
87
+
88
+ if (storedDataJson) {
89
+ const storedData: StoredUserData = JSON.parse(storedDataJson);
90
+
91
+ // Check if session is still valid (24 hours)
92
+ const isExpired = Date.now() - storedData.timestamp > 24 * 60 * 60 * 1000;
93
+
94
+ if (!isExpired && storedData.email && storedData.name) {
95
+ // Restore session
96
+ const user: IUser = {
97
+ name: storedData.name,
98
+ email: storedData.email,
99
+ type: 'customer',
100
+ status: 'online',
101
+ };
102
+
103
+ setUserInfo(user);
104
+ setConversationId(storedData.conversationId);
105
+ setIsLoading(false);
106
+ return;
107
+ }
108
+ }
109
+
110
+ const convId = config.conversationId || generateSessionId();
111
+ setConversationId(convId);
112
+
113
+ // If we have partial user info, use it
114
+ if (config.currentUser?.email && config.currentUser?.name) {
115
+ const user: IUser = {
116
+ name: config.currentUser.name,
117
+ email: config.currentUser.email,
118
+ avatar: config.currentUser.avatar,
119
+ type: 'customer',
120
+ status: 'online',
121
+ };
122
+ setUserInfo(user);
123
+ }
124
+ } catch (error) {
125
+ console.error('Error initializing chat session:', error);
126
+ // Generate fallback IDs
127
+ setConversationId(config.conversationId || generateSessionId());
128
+ } finally {
129
+ setIsLoading(false);
130
+ }
131
+ };
132
+
133
+ initializeSession();
134
+ }, [config, storageKeyPrefix]);
135
+
136
+ // Handle pre-chat form submission
137
+ const handlePreChatSubmit = useCallback(
138
+ (name: string, email: string) => {
139
+ const convId = conversationId || generateSessionId();
140
+
141
+ const user: IUser = {
142
+ name,
143
+ email,
144
+ type: 'customer',
145
+ status: 'online',
146
+ };
147
+
148
+ // Store in localStorage
149
+ const storageData: StoredUserData = {
150
+ name,
151
+ email,
152
+ conversationId: convId,
153
+ timestamp: Date.now(),
154
+ };
155
+
156
+ try {
157
+ localStorage.setItem(`${storageKeyPrefix}_user`, JSON.stringify(storageData));
158
+ } catch (error) {
159
+ console.error('Error storing user data:', error);
160
+ }
161
+
162
+ setUserInfo(user);
163
+ setConversationId(convId);
164
+ onPreChatSubmit?.(user);
165
+ },
166
+ [conversationId, storageKeyPrefix, onPreChatSubmit]
167
+ );
168
+
169
+ // Show loading state
170
+ if (isLoading) {
171
+ return null; // Or you could show a loading spinner
172
+ }
173
+
174
+ // Show pre-chat form if user info is not available
175
+ if (!userInfo || !userInfo.email || !userInfo.name) {
176
+ return (
177
+ <PreChatForm
178
+ onSubmit={handlePreChatSubmit}
179
+ className={className}
180
+ initialName={config.currentUser?.name}
181
+ initialEmail={config.currentUser?.email}
182
+ />
183
+ );
184
+ }
185
+
186
+ // Show chat widget with complete config
187
+ const fullConfig: IChatConfig = {
188
+ ...config,
189
+ conversationId,
190
+ currentUser: userInfo,
191
+ };
192
+
193
+ return <ChatWidget config={fullConfig} className={className} />;
194
+ }
@@ -0,0 +1,93 @@
1
+ import type { IUser } from '../types';
2
+
3
+ interface ChatHeaderProps {
4
+ agent?: IUser;
5
+ onClose?: () => void;
6
+ onMinimize?: () => void;
7
+ }
8
+
9
+ export function ChatHeader({ agent, onClose, onMinimize }: ChatHeaderProps) {
10
+ return (
11
+ <div className="bg-gradient-to-r from-blue-600 to-purple-600 text-white p-4 flex items-center justify-between">
12
+ <div className="flex items-center gap-3">
13
+ <div className="relative">
14
+ <div className="h-10 w-10 rounded-full bg-white/20 flex items-center justify-center text-lg font-medium">
15
+ {agent?.avatar ? (
16
+ <img
17
+ src={agent.avatar}
18
+ alt={agent.name}
19
+ className="h-10 w-10 rounded-full object-cover"
20
+ />
21
+ ) : (
22
+ '🎧'
23
+ )}
24
+ </div>
25
+ {agent?.status === 'online' && (
26
+ <div className="absolute bottom-0 right-0 h-3 w-3 rounded-full bg-green-500 border-2 border-white" />
27
+ )}
28
+ </div>
29
+ <div>
30
+ <h3 className="font-semibold text-base">{agent?.name || 'Support Team'}</h3>
31
+ <p className="text-xs text-white/80">
32
+ {agent?.status === 'online'
33
+ ? 'Online'
34
+ : agent?.status === 'away'
35
+ ? 'Away'
36
+ : "We'll reply as soon as possible"}
37
+ </p>
38
+ </div>
39
+ </div>
40
+
41
+ <div className="flex items-center gap-2">
42
+ {onMinimize && (
43
+ <button
44
+ type="button"
45
+ onClick={onMinimize}
46
+ className="p-2 hover:bg-white/10 rounded-full transition-colors"
47
+ aria-label="Minimize chat"
48
+ >
49
+ <svg
50
+ className="w-5 h-5"
51
+ fill="none"
52
+ viewBox="0 0 24 24"
53
+ stroke="currentColor"
54
+ aria-hidden="true"
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"
79
+ >
80
+ <title>Close icon</title>
81
+ <path
82
+ strokeLinecap="round"
83
+ strokeLinejoin="round"
84
+ strokeWidth={2}
85
+ d="M6 18L18 6M6 6l12 12"
86
+ />
87
+ </svg>
88
+ </button>
89
+ )}
90
+ </div>
91
+ </div>
92
+ );
93
+ }