andy-widget-react 2.3.2 → 3.0.0-next.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.
@@ -1,70 +0,0 @@
1
- import React, { useEffect, MouseEvent } from 'react'
2
-
3
- interface WelcomePopupProps {
4
- message: string
5
- brandColor?: string
6
- onClose: () => void
7
- }
8
-
9
- const WelcomePopup: React.FC<WelcomePopupProps> = ({ message, onClose }) => {
10
- useEffect(() => {
11
- const timer = setTimeout(() => {
12
- onClose()
13
- }, 5000)
14
-
15
- return () => clearTimeout(timer)
16
- }, [onClose])
17
-
18
- const handleBackdropClick = (e: MouseEvent<HTMLDivElement>) => {
19
- if (e.target === e.currentTarget) {
20
- onClose()
21
- }
22
- }
23
-
24
- useEffect(() => {
25
- const handleEscape = (e: KeyboardEvent) => {
26
- if (e.key === 'Escape') {
27
- onClose()
28
- }
29
- }
30
-
31
- document.addEventListener('keydown', handleEscape)
32
- return () => document.removeEventListener('keydown', handleEscape)
33
- }, [onClose])
34
-
35
- return (
36
- <div className='andy-chat-welcome-popup__backdrop' onClick={handleBackdropClick}>
37
- <div className='andy-chat-welcome-popup'>
38
- <button
39
- className='andy-chat-welcome-popup__close'
40
- onClick={onClose}
41
- aria-label='Close welcome message'
42
- type='button'
43
- >
44
- ✕
45
- </button>
46
-
47
- <div className='andy-chat-welcome-popup__icon'>
48
- <svg width='32' height='32' viewBox='0 0 24 24' fill='none' xmlns='http://www.w3.org/2000/svg'>
49
- <path
50
- d='M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z'
51
- fill='currentColor'
52
- />
53
- </svg>
54
- </div>
55
-
56
- <div className='andy-chat-welcome-popup__message'>{message}</div>
57
-
58
- <button className='andy-chat-welcome-popup__button' onClick={onClose} type='button'>
59
- Get Started
60
- </button>
61
-
62
- <div className='andy-chat-welcome-popup__timer'>
63
- <div className='andy-chat-welcome-popup__timer-bar'></div>
64
- </div>
65
- </div>
66
- </div>
67
- )
68
- }
69
-
70
- export default WelcomePopup
@@ -1 +0,0 @@
1
- export { useAndyChat } from './useAndyChat'
@@ -1,312 +0,0 @@
1
- import { useState, useCallback, useRef, useEffect } from 'react'
2
- import {
3
- fetchChatbotConfig,
4
- generateSessionId,
5
- createGreetingMessage,
6
- sendMessageForWidget 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
- import type { SupportOptions } from 'andy-widget-core'
15
- import { useSupportChat } from './useSupportChat'
16
-
17
- const { SESSION_STORAGE_KEY, FORM_DISPLAY_DELAY_MS, PERSIST_SESSION } = CONFIG
18
-
19
- const useSalesAndyChat = (embedId: string, enabled: boolean): UseAndyChatReturn => {
20
- if (enabled) initializeConfig('andy-widget-react')
21
-
22
- const [config, setConfig] = useState<Partial<ChatbotConfig>>({})
23
- const [isLoading, setIsLoading] = useState(true)
24
- const [isOpen, setIsOpen] = useState(false)
25
- const [error, setError] = useState<string | null>(null)
26
-
27
- const [messages, setMessages] = useState<Message[]>([])
28
- const [showingFaq, setShowingFaq] = useState(false)
29
- const [sessionId, setSessionId] = useState(() => generateSessionId())
30
-
31
- const [userInfoCollected, setUserInfoCollected] = useState(false)
32
- const [showingWelcomePopup, setShowingWelcomePopup] = useState(false)
33
- const [showingUserInfoForm, setShowingUserInfoForm] = useState(false)
34
- const [welcomePopupShown, setWelcomePopupShown] = useState(false)
35
-
36
- const activeReplyRef = useRef<AbortController | null>(null)
37
- useEffect(() => () => {
38
- activeReplyRef.current?.abort()
39
- activeReplyRef.current = null
40
- }, [embedId])
41
-
42
- const welcomeTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)
43
- const userInfoTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)
44
-
45
- const initializeChatbot = useCallback(async () => {
46
- if (!enabled) return
47
- if (!embedId) {
48
- throw new Error('Embed ID is required')
49
- }
50
-
51
- try {
52
- setIsLoading(true)
53
- setError(null)
54
-
55
- const rawConfig = await fetchChatbotConfig(embedId)
56
-
57
- const validationResult = validateChatbotConfig(rawConfig)
58
- if (!validationResult.isValid) {
59
- throw new Error(`Invalid configuration: ${validationResult.errors.join(', ')}`)
60
- }
61
-
62
- const normalizedConfig = normalizeConfig(rawConfig as unknown as Record<string, unknown>) as unknown as ChatbotConfig
63
- setConfig(normalizedConfig)
64
-
65
- if (PERSIST_SESSION) {
66
- const storedSessionId = localStorage.getItem(SESSION_STORAGE_KEY)
67
- if (storedSessionId) {
68
- setSessionId(storedSessionId)
69
- } else {
70
- setSessionId(currentSessionId => {
71
- localStorage.setItem(SESSION_STORAGE_KEY, currentSessionId)
72
- return currentSessionId
73
- })
74
- }
75
- }
76
-
77
- if (normalizedConfig.welcomeMessage) {
78
- const greeting = createGreetingMessage(normalizedConfig)
79
- if (greeting) {
80
- const greetingMessage: Message = {
81
- id: Date.now(),
82
- content: greeting,
83
- sender: 'bot',
84
- timestamp: new Date().toISOString()
85
- }
86
- setMessages([greetingMessage])
87
- }
88
- }
89
-
90
- setIsLoading(false)
91
- } catch (err) {
92
- console.error('Failed to initialize chatbot:', err)
93
- setError(err instanceof Error ? err.message : 'Unknown error')
94
- setIsLoading(false)
95
- throw err
96
- }
97
- }, [embedId, enabled])
98
-
99
- const toggleChat = useCallback(() => {
100
- setIsOpen(prev => {
101
- const newIsOpen = !prev
102
-
103
- if (newIsOpen) {
104
- setShowingFaq(false)
105
-
106
- const typedConfig = config as ChatbotConfig
107
- if (typedConfig?.showWelcomePopup?.enabled && !welcomePopupShown) {
108
- setShowingWelcomePopup(true)
109
- setWelcomePopupShown(true)
110
- }
111
-
112
- if (typedConfig?.collectUserInfo?.enabled && !userInfoCollected) {
113
- if (userInfoTimeoutRef.current) {
114
- clearTimeout(userInfoTimeoutRef.current)
115
- }
116
-
117
- userInfoTimeoutRef.current = setTimeout(() => {
118
- setShowingUserInfoForm(true)
119
- }, FORM_DISPLAY_DELAY_MS)
120
- }
121
- } else {
122
- setShowingWelcomePopup(false)
123
- setShowingUserInfoForm(false)
124
-
125
- if (welcomeTimeoutRef.current) {
126
- clearTimeout(welcomeTimeoutRef.current)
127
- }
128
- if (userInfoTimeoutRef.current) {
129
- clearTimeout(userInfoTimeoutRef.current)
130
- }
131
- }
132
-
133
- return newIsOpen
134
- })
135
- }, [config, welcomePopupShown, userInfoCollected])
136
-
137
- const sendMessage = useCallback(
138
- async (messageText: string) => {
139
- if (!enabled || !messageText.trim() || !config || !sessionId || activeReplyRef.current) {
140
- return
141
- }
142
-
143
- const controller = new AbortController()
144
- activeReplyRef.current = controller
145
- const userMessage: Message = {
146
- id: Date.now(),
147
- content: messageText,
148
- sender: 'user',
149
- timestamp: new Date().toISOString()
150
- }
151
-
152
- setMessages(prev => [...prev, userMessage])
153
-
154
- const replyId = Date.now() + 1
155
- const showReply = (content: string, isStreaming: boolean) => {
156
- if (controller.signal.aborted) return
157
- const reply: Message = { id: replyId, content, sender: 'bot', timestamp: new Date().toISOString(), isStreaming }
158
- setMessages(prev => prev.some(message => message.id === replyId)
159
- ? prev.map(message => message.id === replyId ? reply : message)
160
- : [...prev, reply])
161
- }
162
- try {
163
- const response = await sendMessageToAPI(embedId, sessionId, messageText, { signal: controller.signal, onText: text => showReply(text, true) })
164
- if (controller.signal.aborted) return
165
- if (response.status === 'waiting') {
166
- setMessages(prev => prev.filter(message => message.id !== replyId))
167
- return response
168
- }
169
- showReply(response.text, false)
170
-
171
- return response
172
- } catch (err) {
173
- if (controller.signal.aborted) return
174
- console.error('Error sending message:', err)
175
-
176
- const errorMessage: Message = {
177
- id: Date.now() + 1,
178
- content: 'Sorry, I encountered an error. Please try again.',
179
- sender: 'bot',
180
- timestamp: new Date().toISOString(),
181
- isError: true
182
- }
183
-
184
- setMessages(prev => [...prev.filter(message => message.id !== replyId), errorMessage])
185
- throw err
186
- } finally {
187
- if (activeReplyRef.current === controller) activeReplyRef.current = null
188
- }
189
- },
190
- [embedId, sessionId, config, enabled]
191
- )
192
-
193
- const showFaq = useCallback(() => {
194
- setShowingFaq(true)
195
- }, [])
196
-
197
- const hideFaq = useCallback(() => {
198
- setShowingFaq(false)
199
- }, [])
200
-
201
- const showWelcomePopup = useCallback(() => {
202
- setShowingWelcomePopup(true)
203
- }, [])
204
-
205
- const hideWelcomePopup = useCallback(() => {
206
- setShowingWelcomePopup(false)
207
- }, [])
208
-
209
- const showUserInfoForm = useCallback(() => {
210
- setShowingUserInfoForm(true)
211
- }, [])
212
-
213
- const hideUserInfoForm = useCallback(() => {
214
- setShowingUserInfoForm(false)
215
- }, [])
216
-
217
- const submitUserInfo = useCallback(
218
- async (userInfo: UserInfo) => {
219
- if (!enabled) return
220
- try {
221
- await submitUserInfoAPI(embedId, sessionId, userInfo)
222
- setUserInfoCollected(true)
223
- setShowingUserInfoForm(false)
224
-
225
- const confirmMessage: Message = {
226
- id: Date.now(),
227
- content: 'Thank you for providing your information! How can I help you today?',
228
- sender: 'bot',
229
- timestamp: new Date().toISOString()
230
- }
231
-
232
- setMessages(prev => [...prev, confirmMessage])
233
- } catch (err) {
234
- console.error('Error submitting user info:', err)
235
- throw err
236
- }
237
- },
238
- [embedId, sessionId, enabled]
239
- )
240
-
241
- const startNewSession = useCallback(() => {
242
- if (!enabled) return
243
- activeReplyRef.current?.abort()
244
- activeReplyRef.current = null
245
- const newSessionId = generateSessionId()
246
- setSessionId(newSessionId)
247
-
248
- if (PERSIST_SESSION) {
249
- localStorage.setItem(SESSION_STORAGE_KEY, newSessionId)
250
- } else {
251
- localStorage.removeItem(SESSION_STORAGE_KEY)
252
- }
253
-
254
- setMessages([])
255
- setUserInfoCollected(false)
256
- setShowingFaq(false)
257
- setShowingUserInfoForm(false)
258
- setWelcomePopupShown(false)
259
-
260
- const typedConfig = config as ChatbotConfig
261
- if (typedConfig?.welcomeMessage) {
262
- const greeting = createGreetingMessage(typedConfig)
263
- if (greeting) {
264
- const greetingMessage: Message = {
265
- id: Date.now(),
266
- content: greeting,
267
- sender: 'bot',
268
- timestamp: new Date().toISOString()
269
- }
270
- setMessages([greetingMessage])
271
- }
272
- }
273
- }, [config, enabled])
274
-
275
- const addMessage = useCallback((message: Message) => {
276
- setMessages(prev => [...prev, message])
277
- }, [])
278
-
279
- return {
280
- config,
281
- isLoading,
282
- isOpen,
283
- showingFaq,
284
- showingWelcomePopup,
285
- showingUserInfoForm,
286
- messages,
287
- userInfoCollected,
288
- sessionId,
289
- error,
290
-
291
- initializeChatbot,
292
- toggleChat,
293
- sendMessage,
294
- showFaq,
295
- hideFaq,
296
- showWelcomePopup,
297
- hideWelcomePopup,
298
- showUserInfoForm,
299
- hideUserInfoForm,
300
- submitUserInfo,
301
- startNewSession,
302
- addMessage
303
- }
304
- }
305
-
306
- export function useAndyChat(embedId: string): UseAndyChatReturn
307
- export function useAndyChat(options: { mode: 'support'; support: SupportOptions }): ReturnType<typeof useSupportChat>
308
- export function useAndyChat(input: string | { mode: 'support'; support: SupportOptions }) {
309
- const support = useSupportChat(typeof input === 'string' ? null : input.support)
310
- const sales = useSalesAndyChat(typeof input === 'string' ? input : '', typeof input === 'string')
311
- return typeof input === 'string' ? sales : support
312
- }
@@ -1,29 +0,0 @@
1
- import { useEffect, useRef, useState, useSyncExternalStore } from 'react'
2
- import { createSupportController, type SupportOptions, type SupportState } from 'andy-widget-core'
3
-
4
- const empty: SupportState = { config: null, view: 'conversation', messages: [], transcript: [], tickets: [], ticket: null, entries: [], loading: true, busy: false, error: null, submission: null, followup: null, unreadCount: 0 }
5
- const noSubscribe = () => () => {}
6
- const emptySnapshot = () => empty
7
-
8
- /** The host context key changes before a new user/company is allowed to see any previous state. */
9
- export const useSupportChat = (options: SupportOptions | null) => {
10
- const key = options ? JSON.stringify([options.installationId, options.contextKey, options.apiBaseUrl]) : null
11
- const callback = useRef(options?.getRequesterPass)
12
- callback.current = options?.getRequesterPass
13
- const [owned, setOwned] = useState<{ key: string; controller: ReturnType<typeof createSupportController> } | null>(null)
14
- const controller = owned?.key === key ? owned?.controller : null
15
- const state = useSyncExternalStore(controller?.subscribe || noSubscribe, controller?.getSnapshot || emptySnapshot, emptySnapshot)
16
- useEffect(() => {
17
- if (!options || !key) return
18
- const controller = createSupportController({ ...options, getRequesterPass: () => callback.current!() })
19
- setOwned({ key, controller })
20
- void controller.bootstrap()
21
- return () => controller.dispose()
22
- }, [key])
23
- useEffect(() => { controller?.setLanguage(options?.lang) }, [controller, options?.lang])
24
- const ready = !!state.config
25
- useEffect(() => {
26
- if (ready && options?.ticketId) void controller?.inspect(options.ticketId)
27
- }, [controller, ready, options?.ticketId])
28
- return { mode: 'support' as const, state, controller }
29
- }
package/src/index.ts DELETED
@@ -1,67 +0,0 @@
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
- SupportAndyChatProps,
22
- SalesAndyChatProps,
23
- ChatbotConfig,
24
- Message,
25
- FAQItem,
26
- PredefinedQuestion,
27
- WelcomePopupConfig,
28
- CollectUserInfoConfig,
29
- UserInfo,
30
- UseAndyChatReturn,
31
- ChatBubbleProps,
32
- ChatWindowProps,
33
- MessageListProps,
34
- MessageInputProps,
35
- FAQProps,
36
- WelcomePopupProps,
37
- UserInfoFormProps,
38
- FaqItemProps
39
- } from './types'
40
-
41
- // Re-export core utilities for React users who need them
42
- export {
43
- // Design tokens for custom styling
44
- theme,
45
- colors,
46
- typography,
47
- spacing,
48
- borderRadius,
49
- shadows,
50
- zIndex,
51
- animation,
52
- chat,
53
-
54
- // Utility functions
55
- generateSessionId,
56
- parseMarkdown,
57
- sanitizeUrl,
58
-
59
- // Configuration helpers
60
- isValidEmbedId,
61
- getEmbedId,
62
- getApiBaseUrl,
63
- setApiBaseUrl
64
- } from 'andy-widget-core'
65
-
66
- // Default export is the main AndyChat component
67
- export { default } from './AndyChat'
@@ -1,14 +0,0 @@
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
- }