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.
- package/README.md +79 -0
- package/dist/index.cjs +4688 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.esm.js +4658 -0
- package/dist/index.esm.js.map +1 -0
- package/dist/types/AndyChat.d.ts +4 -0
- package/dist/types/components/ChatBubble.d.ts +9 -0
- package/dist/types/components/ChatWindow.d.ts +11 -0
- package/dist/types/components/FAQ.d.ts +8 -0
- package/dist/types/components/MessageInput.d.ts +8 -0
- package/dist/types/components/MessageList.d.ts +10 -0
- package/dist/types/components/UserInfoForm.d.ts +10 -0
- package/dist/types/components/WelcomePopup.d.ts +8 -0
- package/dist/types/hooks/index.d.ts +1 -0
- package/dist/types/hooks/useAndyChat.d.ts +2 -0
- package/dist/types/index.d.ts +13 -0
- package/dist/types/types/index.d.ts +180 -0
- package/package.json +72 -0
- package/src/AndyChat.tsx +1091 -0
- package/src/components/ChatBubble.tsx +48 -0
- package/src/components/ChatWindow.tsx +53 -0
- package/src/components/FAQ.tsx +48 -0
- package/src/components/MessageInput.tsx +82 -0
- package/src/components/MessageList.tsx +122 -0
- package/src/components/UserInfoForm.tsx +186 -0
- package/src/components/WelcomePopup.tsx +70 -0
- package/src/hooks/index.ts +1 -0
- package/src/hooks/useAndyChat.ts +281 -0
- package/src/index.ts +65 -0
- package/src/styles/main.css +14 -0
- package/src/types/index.ts +222 -0
|
@@ -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'
|