andy-widget-react 2.0.5

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,281 @@
1
+ import { useState, useCallback, useRef } from 'react'
2
+ import {
3
+ fetchChatbotConfig,
4
+ generateSessionId,
5
+ createGreetingMessage,
6
+ sendMessage as sendMessageToAPI,
7
+ submitUserInfo as submitUserInfoAPI,
8
+ initializeConfig,
9
+ validateChatbotConfig,
10
+ normalizeConfig,
11
+ CONFIG
12
+ } from 'andy-widget-core'
13
+ import type { ChatbotConfig, Message, UserInfo, UseAndyChatReturn } from '../types'
14
+
15
+ const { SESSION_STORAGE_KEY, FORM_DISPLAY_DELAY_MS, PERSIST_SESSION } = CONFIG
16
+
17
+ export const useAndyChat = (embedId: string): UseAndyChatReturn => {
18
+ initializeConfig('andy-widget-react')
19
+
20
+ const [config, setConfig] = useState<Partial<ChatbotConfig>>({})
21
+ const [isLoading, setIsLoading] = useState(true)
22
+ const [isOpen, setIsOpen] = useState(false)
23
+ const [error, setError] = useState<string | null>(null)
24
+
25
+ const [messages, setMessages] = useState<Message[]>([])
26
+ const [showingFaq, setShowingFaq] = useState(false)
27
+ const [sessionId, setSessionId] = useState(() => generateSessionId())
28
+
29
+ const [userInfoCollected, setUserInfoCollected] = useState(false)
30
+ const [showingWelcomePopup, setShowingWelcomePopup] = useState(false)
31
+ const [showingUserInfoForm, setShowingUserInfoForm] = useState(false)
32
+ const [welcomePopupShown, setWelcomePopupShown] = useState(false)
33
+
34
+ const welcomeTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)
35
+ const userInfoTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)
36
+
37
+ const initializeChatbot = useCallback(async () => {
38
+ if (!embedId) {
39
+ throw new Error('Embed ID is required')
40
+ }
41
+
42
+ try {
43
+ setIsLoading(true)
44
+ setError(null)
45
+
46
+ const rawConfig = await fetchChatbotConfig(embedId)
47
+
48
+ const validationResult = validateChatbotConfig(rawConfig)
49
+ if (!validationResult.isValid) {
50
+ throw new Error(`Invalid configuration: ${validationResult.errors.join(', ')}`)
51
+ }
52
+
53
+ const normalizedConfig = normalizeConfig(rawConfig as unknown as Record<string, unknown>) as unknown as ChatbotConfig
54
+ setConfig(normalizedConfig)
55
+
56
+ if (PERSIST_SESSION) {
57
+ const storedSessionId = localStorage.getItem(SESSION_STORAGE_KEY)
58
+ if (storedSessionId) {
59
+ setSessionId(storedSessionId)
60
+ } else {
61
+ setSessionId(currentSessionId => {
62
+ localStorage.setItem(SESSION_STORAGE_KEY, currentSessionId)
63
+ return currentSessionId
64
+ })
65
+ }
66
+ }
67
+
68
+ if (normalizedConfig.welcomeMessage) {
69
+ const greeting = createGreetingMessage(normalizedConfig)
70
+ if (greeting) {
71
+ const greetingMessage: Message = {
72
+ id: Date.now(),
73
+ content: greeting,
74
+ sender: 'bot',
75
+ timestamp: new Date().toISOString()
76
+ }
77
+ setMessages([greetingMessage])
78
+ }
79
+ }
80
+
81
+ setIsLoading(false)
82
+ } catch (err) {
83
+ console.error('Failed to initialize chatbot:', err)
84
+ setError(err instanceof Error ? err.message : 'Unknown error')
85
+ setIsLoading(false)
86
+ throw err
87
+ }
88
+ }, [embedId])
89
+
90
+ const toggleChat = useCallback(() => {
91
+ setIsOpen(prev => {
92
+ const newIsOpen = !prev
93
+
94
+ if (newIsOpen) {
95
+ setShowingFaq(false)
96
+
97
+ const typedConfig = config as ChatbotConfig
98
+ if (typedConfig?.showWelcomePopup?.enabled && !welcomePopupShown) {
99
+ setShowingWelcomePopup(true)
100
+ setWelcomePopupShown(true)
101
+ }
102
+
103
+ if (typedConfig?.collectUserInfo?.enabled && !userInfoCollected) {
104
+ if (userInfoTimeoutRef.current) {
105
+ clearTimeout(userInfoTimeoutRef.current)
106
+ }
107
+
108
+ userInfoTimeoutRef.current = setTimeout(() => {
109
+ setShowingUserInfoForm(true)
110
+ }, FORM_DISPLAY_DELAY_MS)
111
+ }
112
+ } else {
113
+ setShowingWelcomePopup(false)
114
+ setShowingUserInfoForm(false)
115
+
116
+ if (welcomeTimeoutRef.current) {
117
+ clearTimeout(welcomeTimeoutRef.current)
118
+ }
119
+ if (userInfoTimeoutRef.current) {
120
+ clearTimeout(userInfoTimeoutRef.current)
121
+ }
122
+ }
123
+
124
+ return newIsOpen
125
+ })
126
+ }, [config, welcomePopupShown, userInfoCollected])
127
+
128
+ const sendMessage = useCallback(
129
+ async (messageText: string) => {
130
+ if (!messageText.trim() || !config || !sessionId) {
131
+ return
132
+ }
133
+
134
+ const userMessage: Message = {
135
+ id: Date.now(),
136
+ content: messageText,
137
+ sender: 'user',
138
+ timestamp: new Date().toISOString()
139
+ }
140
+
141
+ setMessages(prev => [...prev, userMessage])
142
+
143
+ try {
144
+ const response = await sendMessageToAPI(embedId, sessionId, messageText)
145
+
146
+ const botMessage: Message = {
147
+ id: Date.now() + 1,
148
+ content: response || 'I apologize, but I encountered an error processing your request.',
149
+ sender: 'bot',
150
+ timestamp: new Date().toISOString()
151
+ }
152
+
153
+ setMessages(prev => [...prev, botMessage])
154
+
155
+ return response
156
+ } catch (err) {
157
+ console.error('Error sending message:', err)
158
+
159
+ const errorMessage: Message = {
160
+ id: Date.now() + 1,
161
+ content: 'Sorry, I encountered an error. Please try again.',
162
+ sender: 'bot',
163
+ timestamp: new Date().toISOString(),
164
+ isError: true
165
+ }
166
+
167
+ setMessages(prev => [...prev, errorMessage])
168
+ throw err
169
+ }
170
+ },
171
+ [embedId, sessionId, config]
172
+ )
173
+
174
+ const showFaq = useCallback(() => {
175
+ setShowingFaq(true)
176
+ }, [])
177
+
178
+ const hideFaq = useCallback(() => {
179
+ setShowingFaq(false)
180
+ }, [])
181
+
182
+ const showWelcomePopup = useCallback(() => {
183
+ setShowingWelcomePopup(true)
184
+ }, [])
185
+
186
+ const hideWelcomePopup = useCallback(() => {
187
+ setShowingWelcomePopup(false)
188
+ }, [])
189
+
190
+ const showUserInfoForm = useCallback(() => {
191
+ setShowingUserInfoForm(true)
192
+ }, [])
193
+
194
+ const hideUserInfoForm = useCallback(() => {
195
+ setShowingUserInfoForm(false)
196
+ }, [])
197
+
198
+ const submitUserInfo = useCallback(
199
+ async (userInfo: UserInfo) => {
200
+ try {
201
+ await submitUserInfoAPI(embedId, sessionId, userInfo)
202
+ setUserInfoCollected(true)
203
+ setShowingUserInfoForm(false)
204
+
205
+ const confirmMessage: Message = {
206
+ id: Date.now(),
207
+ content: 'Thank you for providing your information! How can I help you today?',
208
+ sender: 'bot',
209
+ timestamp: new Date().toISOString()
210
+ }
211
+
212
+ setMessages(prev => [...prev, confirmMessage])
213
+ } catch (err) {
214
+ console.error('Error submitting user info:', err)
215
+ throw err
216
+ }
217
+ },
218
+ [embedId, sessionId]
219
+ )
220
+
221
+ const startNewSession = useCallback(() => {
222
+ const newSessionId = generateSessionId()
223
+ setSessionId(newSessionId)
224
+
225
+ if (PERSIST_SESSION) {
226
+ localStorage.setItem(SESSION_STORAGE_KEY, newSessionId)
227
+ } else {
228
+ localStorage.removeItem(SESSION_STORAGE_KEY)
229
+ }
230
+
231
+ setMessages([])
232
+ setUserInfoCollected(false)
233
+ setShowingFaq(false)
234
+ setShowingUserInfoForm(false)
235
+ setWelcomePopupShown(false)
236
+
237
+ const typedConfig = config as ChatbotConfig
238
+ if (typedConfig?.welcomeMessage) {
239
+ const greeting = createGreetingMessage(typedConfig)
240
+ if (greeting) {
241
+ const greetingMessage: Message = {
242
+ id: Date.now(),
243
+ content: greeting,
244
+ sender: 'bot',
245
+ timestamp: new Date().toISOString()
246
+ }
247
+ setMessages([greetingMessage])
248
+ }
249
+ }
250
+ }, [config])
251
+
252
+ const addMessage = useCallback((message: Message) => {
253
+ setMessages(prev => [...prev, message])
254
+ }, [])
255
+
256
+ return {
257
+ config,
258
+ isLoading,
259
+ isOpen,
260
+ showingFaq,
261
+ showingWelcomePopup,
262
+ showingUserInfoForm,
263
+ messages,
264
+ userInfoCollected,
265
+ sessionId,
266
+ error,
267
+
268
+ initializeChatbot,
269
+ toggleChat,
270
+ sendMessage,
271
+ showFaq,
272
+ hideFaq,
273
+ showWelcomePopup,
274
+ hideWelcomePopup,
275
+ showUserInfoForm,
276
+ hideUserInfoForm,
277
+ submitUserInfo,
278
+ startNewSession,
279
+ addMessage
280
+ }
281
+ }
package/src/index.ts ADDED
@@ -0,0 +1,65 @@
1
+ // Main export for AndyChat React package
2
+ export { default as AndyChat } from './AndyChat'
3
+ // Back-compat: older integrations import { Andy } from 'andy-widget-react'
4
+ export { default as Andy } from './AndyChat'
5
+
6
+ // Export the custom hook for advanced usage
7
+ export { useAndyChat } from './hooks/useAndyChat'
8
+
9
+ // Export individual components for advanced customization
10
+ export { default as ChatBubble } from './components/ChatBubble'
11
+ export { default as ChatWindow } from './components/ChatWindow'
12
+ export { default as MessageList } from './components/MessageList'
13
+ export { default as MessageInput } from './components/MessageInput'
14
+ export { default as FAQ } from './components/FAQ'
15
+ export { default as WelcomePopup } from './components/WelcomePopup'
16
+ export { default as UserInfoForm } from './components/UserInfoForm'
17
+
18
+ // Export types for consumers
19
+ export type {
20
+ AndyChatProps,
21
+ ChatbotConfig,
22
+ Message,
23
+ FAQItem,
24
+ PredefinedQuestion,
25
+ WelcomePopupConfig,
26
+ CollectUserInfoConfig,
27
+ UserInfo,
28
+ UseAndyChatReturn,
29
+ ChatBubbleProps,
30
+ ChatWindowProps,
31
+ MessageListProps,
32
+ MessageInputProps,
33
+ FAQProps,
34
+ WelcomePopupProps,
35
+ UserInfoFormProps,
36
+ FaqItemProps
37
+ } from './types'
38
+
39
+ // Re-export core utilities for React users who need them
40
+ export {
41
+ // Design tokens for custom styling
42
+ theme,
43
+ colors,
44
+ typography,
45
+ spacing,
46
+ borderRadius,
47
+ shadows,
48
+ zIndex,
49
+ animation,
50
+ chat,
51
+
52
+ // Utility functions
53
+ generateSessionId,
54
+ parseMarkdown,
55
+ sanitizeUrl,
56
+
57
+ // Configuration helpers
58
+ isValidEmbedId,
59
+ getEmbedId,
60
+ getApiBaseUrl,
61
+ setApiBaseUrl
62
+ } from 'andy-widget-core'
63
+
64
+ // Default export is the main AndyChat component
65
+ export { default } from './AndyChat'
@@ -0,0 +1,14 @@
1
+ /* AndyChat React Styles */
2
+ /* Placeholder CSS file - can be expanded with custom styles */
3
+
4
+ .andychat-react-container {
5
+ /* Container styles */
6
+ }
7
+
8
+ .andychat-bubble {
9
+ /* Chat bubble styles - handled inline for now */
10
+ }
11
+
12
+ .andychat-window {
13
+ /* Chat window styles - handled inline for now */
14
+ }
@@ -0,0 +1,222 @@
1
+ /**
2
+ * Type definitions for andy-widget-react
3
+ */
4
+
5
+ // Welcome popup configuration
6
+ export interface WelcomePopupConfig {
7
+ enabled: boolean
8
+ seconds: number
9
+ message?: string
10
+ }
11
+
12
+ // User info collection configuration
13
+ export interface CollectUserInfoConfig {
14
+ enabled: boolean
15
+ message?: string
16
+ }
17
+
18
+ // Predefined question
19
+ export interface PredefinedQuestion {
20
+ order: number
21
+ question: string
22
+ }
23
+
24
+ // FAQ item
25
+ export interface FAQItem {
26
+ question: string
27
+ answer: string
28
+ }
29
+
30
+ // Core chatbot configuration (matches API response)
31
+ export interface ChatbotConfig {
32
+ id: string | null
33
+ name: string
34
+ theme: 'light' | 'dark' | 'auto'
35
+ logoUrl: string
36
+ headline: string
37
+ brandColor: string
38
+ description: string
39
+ orientation: 'left' | 'right'
40
+ bubbleStyle: 'icon' | 'pill'
41
+ bubbleLabel: string | null
42
+ useBrandColor: boolean
43
+ defaultLang: 'en' | 'es'
44
+ showBranding: boolean
45
+ welcomeMessage: string
46
+ showWelcomePopup: WelcomePopupConfig
47
+ collectUserInfo: CollectUserInfoConfig
48
+ predefinedQuestions: PredefinedQuestion[]
49
+ faqs: FAQItem[]
50
+ }
51
+
52
+ // Message in the chat
53
+ export interface Message {
54
+ id: number
55
+ content: string
56
+ sender: 'user' | 'bot'
57
+ timestamp: string
58
+ isForm?: boolean
59
+ isLoading?: boolean
60
+ isError?: boolean
61
+ }
62
+
63
+ // User info for form submission
64
+ export interface UserInfo {
65
+ name: string
66
+ email: string
67
+ }
68
+
69
+ // Main AndyChat component props
70
+ export interface AndyChatProps {
71
+ /** Required chatbot embed ID (not required if previewMode is true) */
72
+ embedId?: string
73
+ /** Optional tooltip text for the chat bubble */
74
+ tooltip?: string
75
+ /** Optional bot name (overrides API value) */
76
+ name?: string
77
+ /** Optional headline/subtitle (overrides API value) */
78
+ headline?: string
79
+ /** Optional description (overrides API value) */
80
+ description?: string
81
+ /** Optional welcome message (overrides API value) */
82
+ welcomeMessage?: string
83
+ /** Optional custom label for pill-style bubble (overrides default) */
84
+ bubbleLabel?: string
85
+ /** Optional theme ('light' | 'dark' | 'auto') */
86
+ theme?: 'light' | 'dark' | 'auto'
87
+ /** Optional position ('left' | 'right') */
88
+ orientation?: 'left' | 'right'
89
+ /** Optional brand color hex (e.g., '#3B82F6') */
90
+ brandColor?: string
91
+ /** Optional bubble style ('icon' | 'pill') */
92
+ bubbleStyle?: 'icon' | 'pill'
93
+ /** Optional whether to use brand color */
94
+ useBrandColor?: boolean
95
+ /** Optional logo URL */
96
+ logoUrl?: string
97
+ /** If true, skip API fetch and use props directly as config */
98
+ previewMode?: boolean
99
+ /** If true, start with chat window open */
100
+ initialOpen?: boolean
101
+ }
102
+
103
+ // FAQ Item component props
104
+ export interface FaqItemProps {
105
+ question: string
106
+ answer: string
107
+ }
108
+
109
+ // Chat Bubble component props
110
+ export interface ChatBubbleProps {
111
+ isOpen: boolean
112
+ onClick: () => void
113
+ tooltip?: string
114
+ logoUrl?: string
115
+ bubbleStyle?: 'icon' | 'pill'
116
+ bubbleLabel?: string
117
+ theme?: 'light' | 'dark' | 'auto'
118
+ useBrandColor?: boolean
119
+ }
120
+
121
+ // Chat Window component props
122
+ export interface ChatWindowProps {
123
+ isOpen: boolean
124
+ config: ChatbotConfig | null
125
+ messages: Message[]
126
+ isLoading: boolean
127
+ showPredefinedQuestions: boolean
128
+ isCollectingUserInfo: boolean
129
+ onClose: () => void
130
+ onSendMessage: (message: string) => void
131
+ onPredefinedQuestion: (question: string) => void
132
+ onUserInfoSubmit: (e: React.FormEvent<HTMLFormElement>) => void
133
+ }
134
+
135
+ // Message List component props
136
+ export interface MessageListProps {
137
+ messages: Message[]
138
+ config: ChatbotConfig | null
139
+ isLoading: boolean
140
+ isCollectingUserInfo: boolean
141
+ onUserInfoSubmit: (e: React.FormEvent<HTMLFormElement>) => void
142
+ }
143
+
144
+ // Message Input component props
145
+ export interface MessageInputProps {
146
+ onSendMessage: (message: string) => void
147
+ disabled?: boolean
148
+ placeholder?: string
149
+ }
150
+
151
+ // FAQ component props
152
+ export interface FAQProps {
153
+ faqs: FAQItem[]
154
+ onBack: () => void
155
+ }
156
+
157
+ // Welcome Popup component props
158
+ export interface WelcomePopupProps {
159
+ message: string
160
+ orientation?: 'left' | 'right'
161
+ onClose: () => void
162
+ }
163
+
164
+ // User Info Form component props
165
+ export interface UserInfoFormProps {
166
+ message?: string
167
+ onSubmit: (e: React.FormEvent<HTMLFormElement>) => void
168
+ }
169
+
170
+ // useAndyChat hook return type
171
+ export interface UseAndyChatReturn {
172
+ // State
173
+ config: Partial<ChatbotConfig>
174
+ isLoading: boolean
175
+ isOpen: boolean
176
+ showingFaq: boolean
177
+ showingWelcomePopup: boolean
178
+ showingUserInfoForm: boolean
179
+ messages: Message[]
180
+ userInfoCollected: boolean
181
+ sessionId: string
182
+ error: string | null
183
+
184
+ // Actions
185
+ initializeChatbot: () => Promise<void>
186
+ toggleChat: () => void
187
+ sendMessage: (messageText: string) => Promise<unknown>
188
+ showFaq: () => void
189
+ hideFaq: () => void
190
+ showWelcomePopup: () => void
191
+ hideWelcomePopup: () => void
192
+ showUserInfoForm: () => void
193
+ hideUserInfoForm: () => void
194
+ submitUserInfo: (userInfo: UserInfo) => Promise<void>
195
+ startNewSession: () => void
196
+ addMessage: (message: Message) => void
197
+ }
198
+
199
+ // Stylesheet options for getFullStylesheet
200
+ export interface StylesheetOptions {
201
+ color?: string
202
+ useBrandColor?: boolean
203
+ theme?: 'light' | 'dark' | 'auto'
204
+ orientation?: 'left' | 'right'
205
+ }
206
+
207
+ // Host class options for getHostClasses
208
+ export interface HostClassOptions {
209
+ theme?: 'light' | 'dark' | 'auto'
210
+ orientation?: 'left' | 'right'
211
+ }
212
+
213
+ // Keyboard detector listener callback
214
+ export interface KeyboardListenerCallback {
215
+ (state: { isOpen: boolean; height: number }): void
216
+ }
217
+
218
+ // Keyboard detector interface
219
+ export interface KeyboardDetector {
220
+ addListener: (callback: KeyboardListenerCallback) => void
221
+ removeListener: (callback: KeyboardListenerCallback) => void
222
+ }