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,48 @@
1
+ import React from 'react'
2
+ import { CHAT_BUBBLE_DEFAULT_ICON_URL } from 'andy-widget-core'
3
+ import type { ChatbotConfig } from '../types'
4
+
5
+ interface ChatBubbleProps {
6
+ isOpen: boolean
7
+ config: ChatbotConfig | null
8
+ onClick: () => void
9
+ }
10
+
11
+ const ChatBubble: React.FC<ChatBubbleProps> = ({ isOpen, config, onClick }) => {
12
+ const bubbleClasses = ['andy-bubble', isOpen && 'andy-bubble--open'].filter(Boolean).join(' ')
13
+
14
+ return (
15
+ <div
16
+ className={bubbleClasses}
17
+ onClick={onClick}
18
+ role='button'
19
+ tabIndex={0}
20
+ aria-label={isOpen ? 'Close chat' : 'Open chat'}
21
+ onKeyDown={e => {
22
+ if (e.key === 'Enter' || e.key === ' ') {
23
+ e.preventDefault()
24
+ onClick()
25
+ }
26
+ }}
27
+ >
28
+ {isOpen ? (
29
+ <svg
30
+ className='andy-chat-bubble__icon'
31
+ width='28'
32
+ height='28'
33
+ viewBox='0 0 24 24'
34
+ fill='currentColor'
35
+ xmlns='http://www.w3.org/2000/svg'
36
+ >
37
+ <path d='M7.41 8.59L12 13.17l4.59-4.58L18 10l-6 6-6-6 1.41-1.41z' />
38
+ </svg>
39
+ ) : config?.logoUrl ? (
40
+ <img src={config.logoUrl} alt='Chatbot Logo' className='andy-chat-bubble__logo' />
41
+ ) : (
42
+ <img src={CHAT_BUBBLE_DEFAULT_ICON_URL} alt='Chat' className='andy-chat-bubble__icon' />
43
+ )}
44
+ </div>
45
+ )
46
+ }
47
+
48
+ export default ChatBubble
@@ -0,0 +1,53 @@
1
+ import React from 'react'
2
+ import MessageList from './MessageList'
3
+ import MessageInput from './MessageInput'
4
+ import { FOOTER_TEXT, FOOTER_URL } from 'andy-widget-core'
5
+ import type { ChatbotConfig, Message } from '../types'
6
+
7
+ interface ChatWindowProps {
8
+ config: ChatbotConfig | null
9
+ messages: Message[]
10
+ isLoading: boolean
11
+ onClose: () => void
12
+ onSendMessage: (message: string) => Promise<void> | void
13
+ }
14
+
15
+ const ChatWindow: React.FC<ChatWindowProps> = ({ config, messages, isLoading, onClose, onSendMessage }) => {
16
+ const windowClasses = ['andy-chatbot', 'open'].filter(Boolean).join(' ')
17
+
18
+ return (
19
+ <div className={windowClasses}>
20
+ <div className='andy-header'>
21
+ <div className='andy-title'>{config?.headline || 'Chat with our AI ✨'}</div>
22
+ <div className='andy-subtitle'>{config?.description || 'Ask any question and our AI will answer'}</div>
23
+
24
+ <div className='andy-close' onClick={onClose} role='button' tabIndex={0}>
25
+ ✕
26
+ </div>
27
+ </div>
28
+
29
+ <div className='andy-messages' id='andy-messages'>
30
+ <MessageList
31
+ messages={messages}
32
+ isLoading={isLoading}
33
+ logoUrl={config?.logoUrl}
34
+ botName={config?.name}
35
+ />
36
+ </div>
37
+
38
+ <div className='andy-input-container'>
39
+ <MessageInput onSendMessage={onSendMessage} disabled={isLoading} placeholder='Type your message here...' />
40
+ </div>
41
+
42
+ {config?.showBranding !== false && (
43
+ <div className='andy-footer'>
44
+ <a href={FOOTER_URL} target='_blank' rel='noopener noreferrer'>
45
+ {FOOTER_TEXT}
46
+ </a>
47
+ </div>
48
+ )}
49
+ </div>
50
+ )
51
+ }
52
+
53
+ export default ChatWindow
@@ -0,0 +1,48 @@
1
+ import React from 'react'
2
+ import type { FAQItem } from '../types'
3
+
4
+ interface FAQProps {
5
+ faqs?: FAQItem[]
6
+ onBack: () => void
7
+ }
8
+
9
+ const FAQ: React.FC<FAQProps> = ({ faqs = [], onBack }) => {
10
+ return (
11
+ <div className='andy-chat-faq'>
12
+ <button className='andy-chat-faq__back' onClick={onBack} type='button' aria-label='Back to chat'>
13
+ <svg viewBox='0 0 24 24' fill='none' xmlns='http://www.w3.org/2000/svg' width='16' height='16'>
14
+ <path d='M20 11H7.83l5.59-5.59L12 4l-8 8 8 8 1.41-1.41L7.83 13H20v-2z' fill='currentColor' />
15
+ </svg>
16
+ Back to chat
17
+ </button>
18
+
19
+ <div className='andy-chat-faq__title'>Frequently Asked Questions</div>
20
+
21
+ <div className='andy-chat-faq__list'>
22
+ {faqs.length > 0 ? (
23
+ faqs.map((faq, index) => (
24
+ <div key={index} className='andy-chat-faq__item'>
25
+ <div className='andy-chat-faq__question'>{faq.question}</div>
26
+ <div className='andy-chat-faq__answer'>{faq.answer}</div>
27
+ </div>
28
+ ))
29
+ ) : (
30
+ <div className='andy-chat-faq__empty'>
31
+ <div className='andy-chat-faq__empty-icon'>
32
+ <svg width='48' height='48' viewBox='0 0 24 24' fill='none' xmlns='http://www.w3.org/2000/svg'>
33
+ <path
34
+ d='M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 17h-2v-2h2v2zm2.07-7.75l-.9.92C13.45 12.9 13 13.5 13 15h-2v-.5c0-1.1.45-2.1 1.17-2.83l1.24-1.26c.37-.36.59-.86.59-1.41 0-1.1-.9-2-2-2s-2 .9-2 2H8c0-2.21 1.79-4 4-4s4 1.79 4 4c0 .88-.36 1.68-.93 2.25z'
35
+ fill='currentColor'
36
+ opacity='0.3'
37
+ />
38
+ </svg>
39
+ </div>
40
+ <div className='andy-chat-faq__empty-text'>No frequently asked questions available at this time.</div>
41
+ </div>
42
+ )}
43
+ </div>
44
+ </div>
45
+ )
46
+ }
47
+
48
+ export default FAQ
@@ -0,0 +1,82 @@
1
+ import React, { useState, useRef, KeyboardEvent, ChangeEvent, FormEvent } from 'react'
2
+
3
+ interface MessageInputProps {
4
+ onSendMessage: (message: string) => Promise<void> | void
5
+ disabled?: boolean
6
+ placeholder?: string
7
+ }
8
+
9
+ const MessageInput: React.FC<MessageInputProps> = ({
10
+ onSendMessage,
11
+ disabled = false,
12
+ placeholder = 'Type your message here...'
13
+ }) => {
14
+ const [message, setMessage] = useState('')
15
+ const [isSubmitting, setIsSubmitting] = useState(false)
16
+ const inputRef = useRef<HTMLInputElement>(null)
17
+
18
+ const handleSubmit = async (e: FormEvent<HTMLFormElement> | KeyboardEvent<HTMLInputElement>) => {
19
+ e.preventDefault()
20
+
21
+ const trimmedMessage = message.trim()
22
+ if (!trimmedMessage || disabled || isSubmitting) {
23
+ return
24
+ }
25
+
26
+ try {
27
+ setIsSubmitting(true)
28
+ await onSendMessage(trimmedMessage)
29
+ setMessage('')
30
+
31
+ if (inputRef.current) {
32
+ inputRef.current.focus()
33
+ }
34
+ } catch (error) {
35
+ console.error('Error sending message:', error)
36
+ } finally {
37
+ setIsSubmitting(false)
38
+ }
39
+ }
40
+
41
+ const handleInputChange = (e: ChangeEvent<HTMLInputElement>) => {
42
+ setMessage(e.target.value)
43
+ }
44
+
45
+ const handleKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {
46
+ if (e.key === 'Enter' && !e.shiftKey) {
47
+ e.preventDefault()
48
+ handleSubmit(e)
49
+ }
50
+ }
51
+
52
+ const isInputDisabled = disabled || isSubmitting
53
+ const canSend = message.trim().length > 0 && !isInputDisabled
54
+
55
+ return (
56
+ <div className='andy-input'>
57
+ <input
58
+ ref={inputRef}
59
+ type='text'
60
+ value={message}
61
+ onChange={handleInputChange}
62
+ onKeyDown={handleKeyDown}
63
+ placeholder={placeholder}
64
+ disabled={isInputDisabled}
65
+ maxLength={1000}
66
+ autoComplete='off'
67
+ id='andy-input'
68
+ />
69
+ <button type='button' onClick={() => handleSubmit({ preventDefault: () => {} } as FormEvent<HTMLFormElement>)} disabled={!canSend} id='andy-send'>
70
+ {isSubmitting ? (
71
+ '...'
72
+ ) : (
73
+ <svg width='18' height='18' viewBox='0 0 24 24' fill='none' xmlns='http://www.w3.org/2000/svg'>
74
+ <path d='M2.01 21L23 12 2.01 3 2 10l15 2-15 2z' fill='white' />
75
+ </svg>
76
+ )}
77
+ </button>
78
+ </div>
79
+ )
80
+ }
81
+
82
+ export default MessageInput
@@ -0,0 +1,122 @@
1
+ import React, { useEffect, useRef } from 'react'
2
+ import { parseMarkdown } from 'andy-widget-core'
3
+ import type { Message } from '../types'
4
+
5
+ interface MessageProps {
6
+ message: Message
7
+ logoUrl?: string
8
+ botName?: string
9
+ }
10
+
11
+ const MessageComponent: React.FC<MessageProps> = ({ message, botName }) => {
12
+ const isBot = message.sender === 'bot'
13
+
14
+ const containerClasses = ['andy-message-container', `andy-message-container--${message.sender}`]
15
+ .filter(Boolean)
16
+ .join(' ')
17
+
18
+ const messageClasses = ['andy-message', `andy-message--${message.sender}`].filter(Boolean).join(' ')
19
+
20
+ const messageContent = isBot ? parseMarkdown(message.content) : message.content
21
+
22
+ if (isBot) {
23
+ return (
24
+ <div className={containerClasses}>
25
+ <div className='andy-bot-header'>
26
+ <div className='andy-bot-header__icon'>
27
+ <svg
28
+ width='16'
29
+ height='16'
30
+ viewBox='0 0 24 24'
31
+ fill='none'
32
+ xmlns='http://www.w3.org/2000/svg'
33
+ stroke='currentColor'
34
+ strokeWidth='2'
35
+ strokeLinecap='round'
36
+ strokeLinejoin='round'
37
+ >
38
+ <path d='M12 6V2H8' />
39
+ <path d='m8 18-4 4V8a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2Z' />
40
+ <path d='M2 12h2' />
41
+ <path d='M9 11v2' />
42
+ <path d='M15 11v2' />
43
+ <path d='M20 12h2' />
44
+ </svg>
45
+ </div>
46
+ <span className='andy-bot-header__name'>{botName || 'Andy bot'}</span>
47
+ </div>
48
+ <div className={messageClasses}>
49
+ <div className='andy-message__content' dangerouslySetInnerHTML={{ __html: messageContent }} />
50
+ </div>
51
+ </div>
52
+ )
53
+ }
54
+
55
+ return (
56
+ <div className={containerClasses}>
57
+ <div className={messageClasses}>
58
+ <div className='andy-message__content'>
59
+ <p>{messageContent}</p>
60
+ </div>
61
+ </div>
62
+ </div>
63
+ )
64
+ }
65
+
66
+ const TypingIndicator: React.FC = () => (
67
+ <div className='andy-message-container andy-message-container--bot andy-message-container--typing'>
68
+ <div className='andy-message andy-message--bot andy-message--typing'>
69
+ <div className='andy-typing-indicator'>
70
+ <div className='andy-typing-indicator__dot'></div>
71
+ <div className='andy-typing-indicator__dot'></div>
72
+ <div className='andy-typing-indicator__dot'></div>
73
+ </div>
74
+ </div>
75
+ </div>
76
+ )
77
+
78
+ interface MessageListProps {
79
+ messages: Message[]
80
+ isLoading: boolean
81
+ logoUrl?: string
82
+ botName?: string
83
+ }
84
+
85
+ const MessageList: React.FC<MessageListProps> = ({ messages, isLoading, logoUrl, botName }) => {
86
+ const messagesEndRef = useRef<HTMLDivElement>(null)
87
+
88
+ useEffect(() => {
89
+ if (messagesEndRef.current) {
90
+ messagesEndRef.current.scrollIntoView({
91
+ behavior: 'smooth',
92
+ block: 'end'
93
+ })
94
+ }
95
+ }, [messages, isLoading])
96
+
97
+ return (
98
+ <>
99
+ {messages.map(message => (
100
+ <MessageComponent
101
+ key={message.id || message.timestamp}
102
+ message={message}
103
+ logoUrl={logoUrl}
104
+ botName={botName}
105
+ />
106
+ ))}
107
+
108
+ {isLoading && <TypingIndicator />}
109
+
110
+ {messages.length === 0 && !isLoading && (
111
+ <div className='andy-empty-state'>
112
+ <div className='andy-empty-state__icon'>💬</div>
113
+ <div className='andy-empty-state__text'>Start a conversation by typing a message below</div>
114
+ </div>
115
+ )}
116
+
117
+ <div ref={messagesEndRef} />
118
+ </>
119
+ )
120
+ }
121
+
122
+ export default MessageList
@@ -0,0 +1,186 @@
1
+ import React, { useState, useEffect, MouseEvent, ChangeEvent, FormEvent } from 'react'
2
+ import type { UserInfo } from '../types'
3
+
4
+ interface UserInfoFormProps {
5
+ headerText?: string
6
+ brandColor?: string
7
+ onSubmit: (data: UserInfo) => Promise<void> | void
8
+ onClose: () => void
9
+ }
10
+
11
+ interface FormErrors {
12
+ name?: string
13
+ email?: string
14
+ submit?: string
15
+ }
16
+
17
+ const UserInfoForm: React.FC<UserInfoFormProps> = ({ headerText, onSubmit, onClose }) => {
18
+ const [formData, setFormData] = useState<UserInfo>({
19
+ name: '',
20
+ email: ''
21
+ })
22
+ const [errors, setErrors] = useState<FormErrors>({})
23
+ const [isSubmitting, setIsSubmitting] = useState(false)
24
+
25
+ const validateForm = (): boolean => {
26
+ const newErrors: FormErrors = {}
27
+
28
+ if (!formData.name.trim()) {
29
+ newErrors.name = 'Name is required'
30
+ } else if (formData.name.trim().length < 2) {
31
+ newErrors.name = 'Name must be at least 2 characters'
32
+ }
33
+
34
+ if (!formData.email.trim()) {
35
+ newErrors.email = 'Email is required'
36
+ } else {
37
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
38
+ if (!emailRegex.test(formData.email)) {
39
+ newErrors.email = 'Please enter a valid email address'
40
+ }
41
+ }
42
+
43
+ setErrors(newErrors)
44
+ return Object.keys(newErrors).length === 0
45
+ }
46
+
47
+ const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
48
+ e.preventDefault()
49
+
50
+ if (!validateForm() || isSubmitting) {
51
+ return
52
+ }
53
+
54
+ try {
55
+ setIsSubmitting(true)
56
+ await onSubmit(formData)
57
+ } catch (error) {
58
+ console.error('Error submitting user info:', error)
59
+ setErrors({
60
+ submit: 'Failed to save your information. Please try again.'
61
+ })
62
+ } finally {
63
+ setIsSubmitting(false)
64
+ }
65
+ }
66
+
67
+ const handleInputChange = (field: keyof UserInfo) => (e: ChangeEvent<HTMLInputElement>) => {
68
+ setFormData(prev => ({
69
+ ...prev,
70
+ [field]: e.target.value
71
+ }))
72
+
73
+ if (errors[field]) {
74
+ setErrors(prev => ({
75
+ ...prev,
76
+ [field]: undefined
77
+ }))
78
+ }
79
+ }
80
+
81
+ useEffect(() => {
82
+ const handleEscape = (e: KeyboardEvent) => {
83
+ if (e.key === 'Escape') {
84
+ onClose()
85
+ }
86
+ }
87
+
88
+ document.addEventListener('keydown', handleEscape)
89
+ return () => document.removeEventListener('keydown', handleEscape)
90
+ }, [onClose])
91
+
92
+ const handleBackdropClick = (e: MouseEvent<HTMLDivElement>) => {
93
+ if (e.target === e.currentTarget) {
94
+ onClose()
95
+ }
96
+ }
97
+
98
+ return (
99
+ <div className='andy-chat-user-form__backdrop' onClick={handleBackdropClick}>
100
+ <div className='andy-chat-user-form'>
101
+ <button className='andy-chat-user-form__close' onClick={onClose} aria-label='Close form' type='button'>
102
+ ✕
103
+ </button>
104
+
105
+ <div className='andy-chat-user-form__header'>
106
+ <div className='andy-chat-user-form__icon'>
107
+ <svg width='24' height='24' viewBox='0 0 24 24' fill='none' xmlns='http://www.w3.org/2000/svg'>
108
+ <path
109
+ d='M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z'
110
+ fill='currentColor'
111
+ />
112
+ </svg>
113
+ </div>
114
+ <div className='andy-chat-user-form__title'>{headerText || "Let's get to know you better"}</div>
115
+ <div className='andy-chat-user-form__subtitle'>Please provide your information to continue</div>
116
+ </div>
117
+
118
+ <form className='andy-chat-user-form__form' onSubmit={handleSubmit}>
119
+ <div className='andy-chat-user-form__field'>
120
+ <label htmlFor='andy-user-name' className='andy-chat-user-form__label'>
121
+ Name *
122
+ </label>
123
+ <input
124
+ id='andy-user-name'
125
+ type='text'
126
+ className={`andy-chat-user-form__input ${errors.name ? 'andy-chat-user-form__input--error' : ''}`}
127
+ value={formData.name}
128
+ onChange={handleInputChange('name')}
129
+ placeholder='Enter your name'
130
+ disabled={isSubmitting}
131
+ maxLength={100}
132
+ />
133
+ {errors.name && <div className='andy-chat-user-form__error'>{errors.name}</div>}
134
+ </div>
135
+
136
+ <div className='andy-chat-user-form__field'>
137
+ <label htmlFor='andy-user-email' className='andy-chat-user-form__label'>
138
+ Email *
139
+ </label>
140
+ <input
141
+ id='andy-user-email'
142
+ type='email'
143
+ className={`andy-chat-user-form__input ${errors.email ? 'andy-chat-user-form__input--error' : ''}`}
144
+ value={formData.email}
145
+ onChange={handleInputChange('email')}
146
+ placeholder='Enter your email'
147
+ disabled={isSubmitting}
148
+ maxLength={100}
149
+ />
150
+ {errors.email && <div className='andy-chat-user-form__error'>{errors.email}</div>}
151
+ </div>
152
+
153
+ {errors.submit && <div className='andy-chat-user-form__submit-error'>{errors.submit}</div>}
154
+
155
+ <div className='andy-chat-user-form__actions'>
156
+ <button
157
+ type='button'
158
+ className='andy-chat-user-form__button andy-chat-user-form__button--secondary'
159
+ onClick={onClose}
160
+ disabled={isSubmitting}
161
+ >
162
+ Skip for now
163
+ </button>
164
+ <button
165
+ type='submit'
166
+ className='andy-chat-user-form__button andy-chat-user-form__button--primary'
167
+ disabled={isSubmitting}
168
+ >
169
+ {isSubmitting ? (
170
+ <span className='andy-chat-user-form__loading'>
171
+ <span className='andy-chat-user-form__loading-dot'></span>
172
+ <span className='andy-chat-user-form__loading-dot'></span>
173
+ <span className='andy-chat-user-form__loading-dot'></span>
174
+ </span>
175
+ ) : (
176
+ 'Continue'
177
+ )}
178
+ </button>
179
+ </div>
180
+ </form>
181
+ </div>
182
+ </div>
183
+ )
184
+ }
185
+
186
+ export default UserInfoForm
@@ -0,0 +1,70 @@
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
@@ -0,0 +1 @@
1
+ export { useAndyChat } from './useAndyChat'