@vezlo/assistant-chat 1.0.0
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/LICENSE +661 -0
- package/PACKAGE_README.md +255 -0
- package/README.md +241 -0
- package/lib/api/conversation.d.ts +26 -0
- package/lib/api/conversation.js +52 -0
- package/lib/api/index.d.ts +6 -0
- package/lib/api/index.js +6 -0
- package/lib/api/message.d.ts +30 -0
- package/lib/api/message.js +53 -0
- package/lib/components/Widget.d.ts +11 -0
- package/lib/components/Widget.js +283 -0
- package/lib/components/ui/VezloFooter.d.ts +5 -0
- package/lib/components/ui/VezloFooter.js +7 -0
- package/lib/config/theme.d.ts +23 -0
- package/lib/config/theme.js +31 -0
- package/lib/index.d.ts +5 -0
- package/lib/index.js +6 -0
- package/lib/types/index.d.ts +54 -0
- package/lib/types/index.js +2 -0
- package/lib/utils/index.d.ts +19 -0
- package/lib/utils/index.js +47 -0
- package/package.json +102 -0
- package/public/widget.js +159 -0
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState, useEffect, useRef } from 'react';
|
|
3
|
+
import { createPortal } from 'react-dom';
|
|
4
|
+
import { Send, X, MessageCircle, Bot, ThumbsUp, ThumbsDown } from 'lucide-react';
|
|
5
|
+
import { generateId, formatTimestamp } from '../utils/index.js';
|
|
6
|
+
import { VezloFooter } from './ui/VezloFooter.js';
|
|
7
|
+
import { createConversation, createUserMessage, generateAIResponse } from '../api/index.js';
|
|
8
|
+
export function Widget({ config, isPlayground = false, onOpen, onClose, onMessage, onError, useShadowRoot = false, }) {
|
|
9
|
+
// Use defaultOpen from config, fallback to isPlayground for backward compatibility
|
|
10
|
+
const [isOpen, setIsOpen] = useState(config.defaultOpen ?? isPlayground);
|
|
11
|
+
// Update isOpen when config.defaultOpen changes
|
|
12
|
+
useEffect(() => {
|
|
13
|
+
if (config.defaultOpen !== undefined) {
|
|
14
|
+
setIsOpen(config.defaultOpen);
|
|
15
|
+
}
|
|
16
|
+
}, [config.defaultOpen]);
|
|
17
|
+
const [messages, setMessages] = useState([]);
|
|
18
|
+
const [input, setInput] = useState('');
|
|
19
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
20
|
+
const [messageFeedback, setMessageFeedback] = useState({});
|
|
21
|
+
const [streamingMessage, setStreamingMessage] = useState('');
|
|
22
|
+
const [conversationUuid, setConversationUuid] = useState(null);
|
|
23
|
+
const [isCreatingConversation, setIsCreatingConversation] = useState(false);
|
|
24
|
+
const messagesEndRef = useRef(null);
|
|
25
|
+
const hostRef = useRef(null);
|
|
26
|
+
const shadowRef = useRef(null);
|
|
27
|
+
const shadowMountRef = useRef(null);
|
|
28
|
+
const [shadowReady, setShadowReady] = useState(false);
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
// No global scroll locking for embedded component usage
|
|
31
|
+
return () => { };
|
|
32
|
+
}, [isPlayground]);
|
|
33
|
+
// Initialize Shadow DOM if enabled
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
if (!useShadowRoot)
|
|
36
|
+
return;
|
|
37
|
+
if (!hostRef.current)
|
|
38
|
+
return;
|
|
39
|
+
if (!shadowRef.current) {
|
|
40
|
+
shadowRef.current = hostRef.current.attachShadow({ mode: 'open' });
|
|
41
|
+
// Create a dedicated mount point inside the shadow for React
|
|
42
|
+
const mount = document.createElement('div');
|
|
43
|
+
mount.setAttribute('id', 'vezlo-shadow-mount');
|
|
44
|
+
shadowRef.current.appendChild(mount);
|
|
45
|
+
shadowMountRef.current = mount;
|
|
46
|
+
// Clone existing styles into shadow to ensure Tailwind/host CSS is available
|
|
47
|
+
try {
|
|
48
|
+
const headNodes = Array.from(document.head.querySelectorAll('style, link[rel="stylesheet"]'));
|
|
49
|
+
headNodes.forEach((node) => {
|
|
50
|
+
shadowRef.current?.appendChild(node.cloneNode(true));
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
catch { }
|
|
54
|
+
// Mark shadow as ready to trigger a re-render for the portal
|
|
55
|
+
setShadowReady(true);
|
|
56
|
+
}
|
|
57
|
+
}, [useShadowRoot]);
|
|
58
|
+
// Create conversation when widget opens for the first time
|
|
59
|
+
useEffect(() => {
|
|
60
|
+
const initializeConversation = async () => {
|
|
61
|
+
if (isOpen && !conversationUuid && !isCreatingConversation) {
|
|
62
|
+
setIsCreatingConversation(true);
|
|
63
|
+
try {
|
|
64
|
+
// Create a new conversation
|
|
65
|
+
const userUuid = import.meta.env.VITE_DEFAULT_USER_UUID || 'user-' + generateId().substring(0, 8);
|
|
66
|
+
const companyUuid = import.meta.env.VITE_DEFAULT_COMPANY_UUID || 'company-' + generateId().substring(0, 8);
|
|
67
|
+
const conversation = await createConversation({
|
|
68
|
+
title: 'New Chat',
|
|
69
|
+
user_uuid: userUuid,
|
|
70
|
+
company_uuid: companyUuid,
|
|
71
|
+
});
|
|
72
|
+
setConversationUuid(conversation.uuid);
|
|
73
|
+
console.log('[Widget] Conversation created:', conversation.uuid);
|
|
74
|
+
// Add welcome message after conversation is created
|
|
75
|
+
const welcomeMsg = {
|
|
76
|
+
id: generateId(),
|
|
77
|
+
content: config.welcomeMessage || 'Hello! I\'m your AI assistant. How can I help you today?',
|
|
78
|
+
role: 'assistant',
|
|
79
|
+
timestamp: new Date(),
|
|
80
|
+
};
|
|
81
|
+
setMessages([welcomeMsg]);
|
|
82
|
+
onMessage?.(welcomeMsg);
|
|
83
|
+
}
|
|
84
|
+
catch (error) {
|
|
85
|
+
console.error('[Widget] Failed to create conversation:', error);
|
|
86
|
+
onError?.('Failed to initialize conversation');
|
|
87
|
+
// Still show welcome message even if conversation creation fails
|
|
88
|
+
const welcomeMsg = {
|
|
89
|
+
id: generateId(),
|
|
90
|
+
content: config.welcomeMessage || 'Hello! I\'m your AI assistant. How can I help you today?',
|
|
91
|
+
role: 'assistant',
|
|
92
|
+
timestamp: new Date(),
|
|
93
|
+
};
|
|
94
|
+
setMessages([welcomeMsg]);
|
|
95
|
+
onMessage?.(welcomeMsg);
|
|
96
|
+
}
|
|
97
|
+
finally {
|
|
98
|
+
setIsCreatingConversation(false);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
initializeConversation();
|
|
103
|
+
}, [isOpen, conversationUuid, isCreatingConversation, config.welcomeMessage, onMessage, onError]);
|
|
104
|
+
useEffect(() => {
|
|
105
|
+
// Scroll to bottom when messages change or when streaming
|
|
106
|
+
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
|
107
|
+
}, [messages, streamingMessage]);
|
|
108
|
+
const handleSendMessage = async () => {
|
|
109
|
+
if (!input.trim() || isLoading || !conversationUuid)
|
|
110
|
+
return;
|
|
111
|
+
const userMessageContent = input;
|
|
112
|
+
const userMessage = {
|
|
113
|
+
id: generateId(),
|
|
114
|
+
content: userMessageContent,
|
|
115
|
+
role: 'user',
|
|
116
|
+
timestamp: new Date(),
|
|
117
|
+
};
|
|
118
|
+
setMessages((prev) => [...prev, userMessage]);
|
|
119
|
+
onMessage?.(userMessage);
|
|
120
|
+
setInput('');
|
|
121
|
+
setIsLoading(true);
|
|
122
|
+
try {
|
|
123
|
+
// Step 1: Create user message via API
|
|
124
|
+
const userMessageResponse = await createUserMessage(conversationUuid, {
|
|
125
|
+
content: userMessageContent,
|
|
126
|
+
});
|
|
127
|
+
console.log('[Widget] User message created:', userMessageResponse.uuid);
|
|
128
|
+
// Update the user message with the actual UUID from server
|
|
129
|
+
setMessages((prev) => prev.map((msg) => msg.id === userMessage.id ? { ...msg, id: userMessageResponse.uuid } : msg));
|
|
130
|
+
// Step 2: Generate AI response
|
|
131
|
+
// Keep loading indicator visible until AI response is received
|
|
132
|
+
const aiResponse = await generateAIResponse(userMessageResponse.uuid);
|
|
133
|
+
console.log('[Widget] AI response received:', aiResponse.uuid);
|
|
134
|
+
// Hide loading indicator now that we have the response
|
|
135
|
+
setIsLoading(false);
|
|
136
|
+
// Stream the AI response character by character
|
|
137
|
+
const responseContent = aiResponse.content;
|
|
138
|
+
setStreamingMessage('');
|
|
139
|
+
let currentText = '';
|
|
140
|
+
const streamInterval = setInterval(() => {
|
|
141
|
+
if (currentText.length < responseContent.length) {
|
|
142
|
+
currentText += responseContent[currentText.length];
|
|
143
|
+
setStreamingMessage(currentText);
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
clearInterval(streamInterval);
|
|
147
|
+
// Add the complete message to messages array
|
|
148
|
+
const assistantMessage = {
|
|
149
|
+
id: aiResponse.uuid,
|
|
150
|
+
content: responseContent,
|
|
151
|
+
role: 'assistant',
|
|
152
|
+
timestamp: new Date(aiResponse.created_at),
|
|
153
|
+
};
|
|
154
|
+
setMessages((prev) => [...prev, assistantMessage]);
|
|
155
|
+
onMessage?.(assistantMessage);
|
|
156
|
+
setStreamingMessage('');
|
|
157
|
+
}
|
|
158
|
+
}, 15); // 15ms delay between characters for smooth streaming
|
|
159
|
+
}
|
|
160
|
+
catch (error) {
|
|
161
|
+
console.error('[Widget] Error sending message:', error);
|
|
162
|
+
setIsLoading(false);
|
|
163
|
+
onError?.('Failed to send message');
|
|
164
|
+
// Show error message to user
|
|
165
|
+
const errorMessage = {
|
|
166
|
+
id: generateId(),
|
|
167
|
+
content: 'Sorry, I encountered an error processing your message. Please try again.',
|
|
168
|
+
role: 'assistant',
|
|
169
|
+
timestamp: new Date(),
|
|
170
|
+
};
|
|
171
|
+
setMessages((prev) => [...prev, errorMessage]);
|
|
172
|
+
onMessage?.(errorMessage);
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
const handleKeyPress = (e) => {
|
|
176
|
+
if (e.key === 'Enter' && !e.shiftKey) {
|
|
177
|
+
e.preventDefault();
|
|
178
|
+
handleSendMessage();
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
const handleFeedback = (messageId, type) => {
|
|
182
|
+
setMessageFeedback(prev => ({
|
|
183
|
+
...prev,
|
|
184
|
+
[messageId]: prev[messageId] === type ? null : type
|
|
185
|
+
}));
|
|
186
|
+
};
|
|
187
|
+
const handleOpenWidget = () => {
|
|
188
|
+
setIsOpen(true);
|
|
189
|
+
onOpen?.();
|
|
190
|
+
// Notify parent window (for embed mode)
|
|
191
|
+
if (window.parent && !isPlayground) {
|
|
192
|
+
window.parent.postMessage({ type: 'vezlo-widget-opened' }, '*');
|
|
193
|
+
}
|
|
194
|
+
};
|
|
195
|
+
const handleCloseWidget = () => {
|
|
196
|
+
setIsOpen(false);
|
|
197
|
+
onClose?.();
|
|
198
|
+
// Notify parent window (for embed mode)
|
|
199
|
+
if (window.parent && !isPlayground) {
|
|
200
|
+
window.parent.postMessage({ type: 'vezlo-widget-closed' }, '*');
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
// Fixed positioning styles for embedded usage (non-playground)
|
|
204
|
+
const isBottomRight = config.position !== 'bottom-left';
|
|
205
|
+
const isInIframe = window.parent !== window;
|
|
206
|
+
const containerStyle = isPlayground
|
|
207
|
+
? {
|
|
208
|
+
width: '100%',
|
|
209
|
+
height: '100%',
|
|
210
|
+
display: 'flex',
|
|
211
|
+
alignItems: 'center',
|
|
212
|
+
justifyContent: 'center',
|
|
213
|
+
pointerEvents: 'none',
|
|
214
|
+
}
|
|
215
|
+
: isInIframe
|
|
216
|
+
? {
|
|
217
|
+
// When in iframe, don't add positioning - let iframe handle it
|
|
218
|
+
position: 'relative',
|
|
219
|
+
zIndex: 2147483647,
|
|
220
|
+
pointerEvents: 'none', // container lets clicks pass unless on inner elements
|
|
221
|
+
}
|
|
222
|
+
: {
|
|
223
|
+
position: 'fixed',
|
|
224
|
+
zIndex: 2147483647,
|
|
225
|
+
bottom: 20,
|
|
226
|
+
right: isBottomRight ? 20 : undefined,
|
|
227
|
+
left: !isBottomRight ? 20 : undefined,
|
|
228
|
+
pointerEvents: 'none', // container lets clicks pass unless on inner elements
|
|
229
|
+
};
|
|
230
|
+
const content = (_jsxs("div", { id: "vezlo-widget-root", className: ``, style: containerStyle, children: [_jsx("style", {
|
|
231
|
+
// Using unique, prefixed names to avoid collisions with host styles
|
|
232
|
+
dangerouslySetInnerHTML: {
|
|
233
|
+
__html: `
|
|
234
|
+
@keyframes vezloDotPulse { 0%, 80%, 100% { opacity: .2; transform: scale(0.8);} 40% { opacity: 1; transform: scale(1);} }
|
|
235
|
+
@keyframes vezloCaretBlink { 0%, 50% { opacity: 1; } 51%, 100% { opacity: 0; } }
|
|
236
|
+
`
|
|
237
|
+
} }), !isOpen && (_jsxs("div", { className: "relative animate-fadeIn", style: { pointerEvents: 'auto', width: 64, height: 64 }, children: [_jsx("div", { style: {
|
|
238
|
+
position: 'absolute',
|
|
239
|
+
inset: 0,
|
|
240
|
+
backgroundColor: config.themeColor || '#059669',
|
|
241
|
+
borderRadius: 9999,
|
|
242
|
+
opacity: 0.2,
|
|
243
|
+
filter: 'blur(0px)'
|
|
244
|
+
} }), _jsx("button", { onClick: handleOpenWidget, style: {
|
|
245
|
+
position: 'relative',
|
|
246
|
+
width: 64,
|
|
247
|
+
height: 64,
|
|
248
|
+
borderRadius: 9999,
|
|
249
|
+
color: '#fff',
|
|
250
|
+
background: `linear-gradient(135deg, ${config.themeColor}, ${config.themeColor}dd)`,
|
|
251
|
+
boxShadow: '0 10px 25px rgba(0,0,0,0.20)',
|
|
252
|
+
display: 'flex',
|
|
253
|
+
alignItems: 'center',
|
|
254
|
+
justifyContent: 'center',
|
|
255
|
+
transition: 'transform 0.2s ease, box-shadow 0.2s ease'
|
|
256
|
+
}, onMouseEnter: (e) => {
|
|
257
|
+
e.currentTarget.style.transform = 'scale(1.05)';
|
|
258
|
+
e.currentTarget.style.boxShadow = '0 14px 30px rgba(0,0,0,0.22)';
|
|
259
|
+
}, onMouseLeave: (e) => {
|
|
260
|
+
e.currentTarget.style.transform = 'scale(1)';
|
|
261
|
+
e.currentTarget.style.boxShadow = '0 10px 25px rgba(0,0,0,0.20)';
|
|
262
|
+
}, children: _jsx(MessageCircle, { className: "w-7 h-7" }) })] })), isOpen && (_jsxs("div", { className: "bg-white rounded-2xl shadow-2xl flex flex-col border border-gray-100 overflow-hidden animate-fadeIn", style: {
|
|
263
|
+
pointerEvents: 'auto',
|
|
264
|
+
width: (config.size && config.size.width) ? config.size.width : 420,
|
|
265
|
+
height: (config.size && config.size.height) ? config.size.height : 600
|
|
266
|
+
}, children: [_jsxs("div", { className: "text-white p-4 flex justify-between items-center relative overflow-hidden", style: { background: `linear-gradient(to right, ${config.themeColor}, ${config.themeColor}dd, ${config.themeColor}bb)`, color: '#fff' }, children: [_jsxs("div", { className: "absolute inset-0 opacity-10", children: [_jsx("div", { className: "absolute top-0 left-0 w-full h-full bg-gradient-to-br from-white/20 to-transparent" }), _jsx("div", { className: "absolute bottom-0 right-0 w-32 h-32 bg-white/10 rounded-full -translate-y-8 translate-x-8" })] }), _jsxs("div", { className: "flex items-center gap-3 relative z-10", children: [_jsx("div", { className: "w-10 h-10 bg-white/20 rounded-full flex items-center justify-center backdrop-blur-sm", children: _jsx(Bot, { className: "w-5 h-5 text-white" }) }), _jsxs("div", { children: [_jsx("h3", { className: "font-semibold text-lg", children: config.title }), _jsxs("div", { className: "flex items-center gap-1", children: [_jsx("div", { className: "w-2 h-2 bg-green-400 rounded-full animate-pulse" }), _jsxs("p", { className: "text-xs text-emerald-100", children: ["Online \u2022 ", config.subtitle] })] })] })] }), _jsx("button", { onClick: handleCloseWidget, className: "hover:bg-white/20 rounded-lg p-2 transition-all duration-200 hover:scale-110 relative z-10", children: _jsx(X, { className: "w-5 h-5" }) })] }), _jsxs("div", { className: "flex-1 overflow-y-auto p-4 space-y-4 bg-gradient-to-b from-gray-50 to-gray-100", children: [messages.map((message, index) => (_jsxs("div", { className: `flex ${message.role === 'user' ? 'justify-end' : 'justify-start'} animate-fadeIn`, style: { animationDelay: `${index * 0.1}s` }, children: [message.role === 'assistant' && (_jsx("div", { className: "w-8 h-8 bg-emerald-100 rounded-full flex items-center justify-center flex-shrink-0 mt-1 mr-2", children: _jsx(Bot, { className: "w-4 h-4 text-emerald-600" }) })), _jsxs("div", { className: "flex flex-col max-w-[75%]", children: [_jsx("div", { className: `rounded-2xl px-4 py-3 shadow-sm transition-all duration-200 hover:shadow-md ${message.role === 'user'
|
|
267
|
+
? 'text-white'
|
|
268
|
+
: 'bg-white text-gray-900 border border-gray-200'}`, style: {
|
|
269
|
+
backgroundColor: message.role === 'user' ? config.themeColor : undefined,
|
|
270
|
+
boxShadow: message.role === 'user'
|
|
271
|
+
? `0 4px 12px ${config.themeColor}4D` // 4D is ~30% opacity
|
|
272
|
+
: '0 2px 8px rgba(0, 0, 0, 0.1)'
|
|
273
|
+
}, children: _jsx("p", { className: "text-sm whitespace-pre-wrap break-words leading-relaxed", children: message.content }) }), _jsxs("div", { className: "flex items-center justify-between mt-1", children: [_jsx("p", { className: `text-xs ${message.role === 'user' ? 'text-emerald-100' : 'text-gray-500'}`, children: formatTimestamp(message.timestamp) }), message.role === 'assistant' && (_jsxs("div", { className: "flex items-center gap-1 ml-2", children: [_jsx("button", { onClick: () => handleFeedback(message.id, 'like'), className: `p-1 rounded transition-all duration-200 hover:scale-110 cursor-pointer ${messageFeedback[message.id] === 'like'
|
|
274
|
+
? 'text-green-600'
|
|
275
|
+
: 'text-gray-400 hover:text-green-600'}`, children: _jsx(ThumbsUp, { className: `w-4 h-4 ${messageFeedback[message.id] === 'like' ? 'fill-current' : ''}` }) }), _jsx("button", { onClick: () => handleFeedback(message.id, 'dislike'), className: `p-1 rounded transition-all duration-200 hover:scale-110 cursor-pointer ${messageFeedback[message.id] === 'dislike'
|
|
276
|
+
? 'text-red-600'
|
|
277
|
+
: 'text-gray-400 hover:text-red-600'}`, children: _jsx(ThumbsDown, { className: `w-4 h-4 ${messageFeedback[message.id] === 'dislike' ? 'fill-current' : ''}` }) })] }))] })] })] }, message.id))), streamingMessage && (_jsxs("div", { className: "flex justify-start animate-fadeIn", children: [_jsx("div", { className: "w-8 h-8 bg-emerald-100 rounded-full flex items-center justify-center flex-shrink-0 mt-1 mr-2", children: _jsx(Bot, { className: "w-4 h-4 text-emerald-600" }) }), _jsx("div", { className: "flex flex-col max-w-[75%]", children: _jsx("div", { className: "bg-white text-gray-900 border border-gray-200 rounded-2xl px-4 py-3 shadow-sm", children: _jsxs("p", { className: "text-sm whitespace-pre-wrap break-words leading-relaxed", style: { color: '#111827' }, children: [streamingMessage, _jsx("span", { style: { display: 'inline-block', animation: 'vezloCaretBlink 1s steps(1, end) infinite' }, children: "|" })] }) }) })] })), isLoading && (_jsx("div", { className: "flex justify-start animate-fadeIn", children: _jsx("div", { className: "bg-white border border-gray-200 rounded-2xl px-4 py-3 flex items-center gap-3 shadow-sm", children: _jsxs("div", { className: "flex gap-1", style: { display: 'flex', gap: '4px' }, children: [_jsx("span", { style: { width: 8, height: 8, borderRadius: 9999, backgroundColor: config.themeColor || '#10b981', display: 'inline-block', animation: 'vezloDotPulse 1s infinite ease-in-out', animationDelay: '0s' } }), _jsx("span", { style: { width: 8, height: 8, borderRadius: 9999, backgroundColor: config.themeColor || '#10b981', display: 'inline-block', animation: 'vezloDotPulse 1s infinite ease-in-out', animationDelay: '0.15s' } }), _jsx("span", { style: { width: 8, height: 8, borderRadius: 9999, backgroundColor: config.themeColor || '#10b981', display: 'inline-block', animation: 'vezloDotPulse 1s infinite ease-in-out', animationDelay: '0.3s' } })] }) }) })), _jsx("div", { ref: messagesEndRef })] }), _jsx("div", { className: "border-t border-gray-200 p-4 bg-white", children: _jsxs("div", { className: "flex gap-3", children: [_jsx("input", { type: "text", value: input, onChange: (e) => setInput(e.target.value), onKeyPress: handleKeyPress, placeholder: config.placeholder, disabled: isLoading, className: "flex-1 px-4 py-3 border border-gray-300 rounded-2xl focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent disabled:bg-gray-100 disabled:cursor-not-allowed text-sm transition-all duration-200" }), _jsx("button", { onClick: handleSendMessage, disabled: !input.trim() || isLoading, className: "text-white px-4 py-3 rounded-2xl hover:from-blue-700 hover:to-blue-800 transition-all duration-200 disabled:bg-gray-300 disabled:cursor-not-allowed flex items-center justify-center shadow-lg hover:shadow-xl disabled:shadow-none transform hover:scale-105 disabled:scale-100", style: { background: `linear-gradient(to right, ${config.themeColor}, ${config.themeColor}dd)` }, children: _jsx(Send, { className: "w-4 h-4" }) })] }) }), _jsx("div", { className: "border-t border-gray-200 px-4 py-3 bg-gradient-to-r from-gray-50 to-gray-100", children: _jsx(VezloFooter, { size: "sm" }) })] }))] }));
|
|
278
|
+
if (useShadowRoot) {
|
|
279
|
+
// Ensure host exists in DOM
|
|
280
|
+
return (_jsx("div", { ref: hostRef, style: { all: 'initial' }, children: shadowReady && shadowMountRef.current ? createPortal(content, shadowMountRef.current) : null }));
|
|
281
|
+
}
|
|
282
|
+
return content;
|
|
283
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { THEME } from '../../config/theme.js';
|
|
3
|
+
export function VezloFooter({ size = 'md' }) {
|
|
4
|
+
const iconSize = size === 'sm' ? 'w-3 h-3' : 'w-4 h-4';
|
|
5
|
+
const textSize = size === 'sm' ? 'text-xs' : 'text-sm';
|
|
6
|
+
return (_jsxs("div", { className: "flex items-center justify-center gap-2", children: [_jsx("div", { className: `${iconSize} rounded-sm flex items-center justify-center`, style: { backgroundColor: THEME.primary.hex }, children: _jsx("span", { className: "text-white text-xs font-bold", children: "V" }) }), _jsxs("p", { className: `${textSize} text-gray-600`, children: ["Powered by ", _jsx("span", { className: "font-semibold", style: { color: THEME.primary.hex }, children: "Vezlo" })] })] }));
|
|
7
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Centralized theme configuration
|
|
3
|
+
* Change these values to update colors across the entire application
|
|
4
|
+
*/
|
|
5
|
+
export declare const THEME: {
|
|
6
|
+
readonly primary: {
|
|
7
|
+
readonly hex: "#059669";
|
|
8
|
+
readonly tailwind: "emerald";
|
|
9
|
+
};
|
|
10
|
+
readonly colors: {
|
|
11
|
+
readonly bg: "bg-emerald-600";
|
|
12
|
+
readonly bgHover: "bg-emerald-700";
|
|
13
|
+
readonly bgLight: "bg-emerald-100";
|
|
14
|
+
readonly bgLighter: "bg-emerald-50";
|
|
15
|
+
readonly text: "text-emerald-600";
|
|
16
|
+
readonly textHover: "text-emerald-700";
|
|
17
|
+
readonly textLight: "text-emerald-100";
|
|
18
|
+
readonly border: "border-emerald-600";
|
|
19
|
+
readonly borderLight: "border-emerald-200";
|
|
20
|
+
};
|
|
21
|
+
};
|
|
22
|
+
export declare const getButtonGradient: (color?: string) => string;
|
|
23
|
+
export declare const getHeaderGradient: (color?: string) => string;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Centralized theme configuration
|
|
3
|
+
* Change these values to update colors across the entire application
|
|
4
|
+
*/
|
|
5
|
+
export const THEME = {
|
|
6
|
+
// Primary brand color (emerald)
|
|
7
|
+
primary: {
|
|
8
|
+
hex: '#059669',
|
|
9
|
+
tailwind: 'emerald',
|
|
10
|
+
},
|
|
11
|
+
// Tailwind color variants
|
|
12
|
+
colors: {
|
|
13
|
+
bg: 'bg-emerald-600',
|
|
14
|
+
bgHover: 'bg-emerald-700',
|
|
15
|
+
bgLight: 'bg-emerald-100',
|
|
16
|
+
bgLighter: 'bg-emerald-50',
|
|
17
|
+
text: 'text-emerald-600',
|
|
18
|
+
textHover: 'text-emerald-700',
|
|
19
|
+
textLight: 'text-emerald-100',
|
|
20
|
+
border: 'border-emerald-600',
|
|
21
|
+
borderLight: 'border-emerald-200',
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
// Helper function to get gradient for buttons
|
|
25
|
+
export const getButtonGradient = (color = THEME.primary.hex) => {
|
|
26
|
+
return `linear-gradient(to right, ${color}, ${color}dd)`;
|
|
27
|
+
};
|
|
28
|
+
// Helper function to get header gradient
|
|
29
|
+
export const getHeaderGradient = (color = THEME.primary.hex) => {
|
|
30
|
+
return `linear-gradient(to right, ${color}, ${color}dd, ${color}bb)`;
|
|
31
|
+
};
|
package/lib/index.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { Widget } from './components/Widget.js';
|
|
2
|
+
export type { WidgetProps } from './components/Widget.js';
|
|
3
|
+
export type { WidgetConfig, ChatMessage, ChatSource, ChatState, AssistantServerResponse } from './types/index.js';
|
|
4
|
+
export { generateId, formatTimestamp, validateWidgetConfig, parseSize, getEnvConfig, debugLog } from './utils/index.js';
|
|
5
|
+
export { THEME } from './config/theme.js';
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// Export the main Widget component for library usage
|
|
2
|
+
export { Widget } from './components/Widget.js';
|
|
3
|
+
// Export utilities
|
|
4
|
+
export { generateId, formatTimestamp, validateWidgetConfig, parseSize, getEnvConfig, debugLog } from './utils/index.js';
|
|
5
|
+
// Export theme
|
|
6
|
+
export { THEME } from './config/theme.js';
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
export interface WidgetConfig {
|
|
2
|
+
uuid: string;
|
|
3
|
+
theme: 'light' | 'dark';
|
|
4
|
+
position: 'bottom-right' | 'bottom-left' | 'top-right' | 'top-left';
|
|
5
|
+
size: {
|
|
6
|
+
width: number;
|
|
7
|
+
height: number;
|
|
8
|
+
};
|
|
9
|
+
title: string;
|
|
10
|
+
subtitle?: string;
|
|
11
|
+
placeholder?: string;
|
|
12
|
+
welcomeMessage?: string;
|
|
13
|
+
apiUrl: string;
|
|
14
|
+
apiKey: string;
|
|
15
|
+
themeColor?: string;
|
|
16
|
+
defaultOpen?: boolean;
|
|
17
|
+
}
|
|
18
|
+
export interface ChatMessage {
|
|
19
|
+
id: string;
|
|
20
|
+
content: string;
|
|
21
|
+
role: 'user' | 'assistant';
|
|
22
|
+
timestamp: Date;
|
|
23
|
+
sources?: ChatSource[];
|
|
24
|
+
}
|
|
25
|
+
export interface ChatSource {
|
|
26
|
+
title: string;
|
|
27
|
+
url?: string;
|
|
28
|
+
content?: string;
|
|
29
|
+
}
|
|
30
|
+
export interface ChatState {
|
|
31
|
+
messages: ChatMessage[];
|
|
32
|
+
isLoading: boolean;
|
|
33
|
+
error?: string;
|
|
34
|
+
isOpen: boolean;
|
|
35
|
+
}
|
|
36
|
+
export interface AssistantServerResponse {
|
|
37
|
+
message: string;
|
|
38
|
+
sources?: ChatSource[];
|
|
39
|
+
conversationId?: string;
|
|
40
|
+
}
|
|
41
|
+
export interface WidgetProps {
|
|
42
|
+
config: WidgetConfig;
|
|
43
|
+
onMessage?: (message: ChatMessage) => void;
|
|
44
|
+
onError?: (error: string) => void;
|
|
45
|
+
}
|
|
46
|
+
export interface EnvConfig {
|
|
47
|
+
VITE_ASSISTANT_SERVER_URL: string;
|
|
48
|
+
VITE_ASSISTANT_SERVER_API_KEY: string;
|
|
49
|
+
VITE_WIDGET_DEFAULT_THEME: 'light' | 'dark';
|
|
50
|
+
VITE_WIDGET_DEFAULT_POSITION: string;
|
|
51
|
+
VITE_WIDGET_DEFAULT_SIZE: string;
|
|
52
|
+
VITE_DEV_MODE: boolean;
|
|
53
|
+
VITE_DEBUG_MODE: boolean;
|
|
54
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { type ClassValue } from 'clsx';
|
|
2
|
+
export declare function cn(...inputs: ClassValue[]): string;
|
|
3
|
+
export declare function generateId(): string;
|
|
4
|
+
export declare function formatTimestamp(date: Date): string;
|
|
5
|
+
export declare function validateWidgetConfig(config: any): boolean;
|
|
6
|
+
export declare function parseSize(sizeString: string): {
|
|
7
|
+
width: number;
|
|
8
|
+
height: number;
|
|
9
|
+
};
|
|
10
|
+
export declare function getEnvConfig(): {
|
|
11
|
+
VITE_ASSISTANT_SERVER_URL: any;
|
|
12
|
+
VITE_ASSISTANT_SERVER_API_KEY: any;
|
|
13
|
+
VITE_WIDGET_DEFAULT_THEME: "light" | "dark";
|
|
14
|
+
VITE_WIDGET_DEFAULT_POSITION: any;
|
|
15
|
+
VITE_WIDGET_DEFAULT_SIZE: any;
|
|
16
|
+
VITE_DEV_MODE: boolean;
|
|
17
|
+
VITE_DEBUG_MODE: boolean;
|
|
18
|
+
};
|
|
19
|
+
export declare function debugLog(message: string, data?: any): void;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { clsx } from 'clsx';
|
|
2
|
+
// Utility function for conditional CSS classes
|
|
3
|
+
export function cn(...inputs) {
|
|
4
|
+
return clsx(inputs);
|
|
5
|
+
}
|
|
6
|
+
// Generate unique IDs
|
|
7
|
+
export function generateId() {
|
|
8
|
+
return Math.random().toString(36).substr(2, 9);
|
|
9
|
+
}
|
|
10
|
+
// Format timestamp
|
|
11
|
+
export function formatTimestamp(date) {
|
|
12
|
+
return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
|
13
|
+
}
|
|
14
|
+
// Validate widget configuration
|
|
15
|
+
export function validateWidgetConfig(config) {
|
|
16
|
+
return (config &&
|
|
17
|
+
typeof config.uuid === 'string' &&
|
|
18
|
+
typeof config.apiUrl === 'string' &&
|
|
19
|
+
typeof config.apiKey === 'string' &&
|
|
20
|
+
typeof config.title === 'string');
|
|
21
|
+
}
|
|
22
|
+
// Parse size string (e.g., "350x500") to object
|
|
23
|
+
export function parseSize(sizeString) {
|
|
24
|
+
const [width, height] = sizeString.split('x').map(Number);
|
|
25
|
+
return {
|
|
26
|
+
width: width || 350,
|
|
27
|
+
height: height || 500
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
// Get environment configuration
|
|
31
|
+
export function getEnvConfig() {
|
|
32
|
+
return {
|
|
33
|
+
VITE_ASSISTANT_SERVER_URL: import.meta.env.VITE_ASSISTANT_SERVER_URL || 'http://localhost:3000',
|
|
34
|
+
VITE_ASSISTANT_SERVER_API_KEY: import.meta.env.VITE_ASSISTANT_SERVER_API_KEY || '',
|
|
35
|
+
VITE_WIDGET_DEFAULT_THEME: import.meta.env.VITE_WIDGET_DEFAULT_THEME || 'light',
|
|
36
|
+
VITE_WIDGET_DEFAULT_POSITION: import.meta.env.VITE_WIDGET_DEFAULT_POSITION || 'bottom-right',
|
|
37
|
+
VITE_WIDGET_DEFAULT_SIZE: import.meta.env.VITE_WIDGET_DEFAULT_SIZE || '350x500',
|
|
38
|
+
VITE_DEV_MODE: import.meta.env.VITE_DEV_MODE === 'true',
|
|
39
|
+
VITE_DEBUG_MODE: import.meta.env.VITE_DEBUG_MODE === 'true'
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
// Debug logging
|
|
43
|
+
export function debugLog(message, data) {
|
|
44
|
+
if (getEnvConfig().VITE_DEBUG_MODE) {
|
|
45
|
+
console.log(`[VezloChat] ${message}`, data);
|
|
46
|
+
}
|
|
47
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@vezlo/assistant-chat",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "React component library for AI-powered chat widgets with RAG knowledge base integration and real-time streaming",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "lib/index.js",
|
|
7
|
+
"module": "lib/index.js",
|
|
8
|
+
"types": "lib/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"import": "./lib/index.js",
|
|
12
|
+
"types": "./lib/index.d.ts"
|
|
13
|
+
},
|
|
14
|
+
"./widget.js": "./public/widget.js"
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"lib",
|
|
18
|
+
"public/widget.js",
|
|
19
|
+
"PACKAGE_README.md",
|
|
20
|
+
"LICENSE"
|
|
21
|
+
],
|
|
22
|
+
"scripts": {
|
|
23
|
+
"dev": "vite",
|
|
24
|
+
"build": "tsc -b && npm run build:types && vite build",
|
|
25
|
+
"build:types": "tsc -p tsconfig.lib.json",
|
|
26
|
+
"lint": "eslint .",
|
|
27
|
+
"preview": "vite preview --port 5173 --host 0.0.0.0",
|
|
28
|
+
"clean": "rm -rf dist",
|
|
29
|
+
"start": "npm run preview",
|
|
30
|
+
"vercel-build": "npm run build",
|
|
31
|
+
"prepack": "npm run build"
|
|
32
|
+
},
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"clsx": "^2.1.1",
|
|
35
|
+
"lucide-react": "^0.544.0",
|
|
36
|
+
"react-router-dom": "^7.9.3",
|
|
37
|
+
"tailwindcss": "^4.1.14"
|
|
38
|
+
},
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"@eslint/js": "^9.36.0",
|
|
41
|
+
"@tailwindcss/postcss": "^4.1.14",
|
|
42
|
+
"@tailwindcss/typography": "^0.5.19",
|
|
43
|
+
"@types/node": "^24.6.2",
|
|
44
|
+
"@types/react": "^19.1.16",
|
|
45
|
+
"@types/react-dom": "^19.1.9",
|
|
46
|
+
"@types/react-router-dom": "^5.3.3",
|
|
47
|
+
"@vitejs/plugin-react": "^5.0.4",
|
|
48
|
+
"autoprefixer": "^10.4.21",
|
|
49
|
+
"eslint": "^9.36.0",
|
|
50
|
+
"eslint-plugin-react-hooks": "^5.2.0",
|
|
51
|
+
"eslint-plugin-react-refresh": "^0.4.22",
|
|
52
|
+
"globals": "^16.4.0",
|
|
53
|
+
"postcss": "^8.5.6",
|
|
54
|
+
"typescript": "~5.9.3",
|
|
55
|
+
"typescript-eslint": "^8.45.0",
|
|
56
|
+
"vite": "^7.1.7"
|
|
57
|
+
},
|
|
58
|
+
"keywords": [
|
|
59
|
+
"ai-chatbot",
|
|
60
|
+
"chat-widget",
|
|
61
|
+
"chatbot",
|
|
62
|
+
"knowledge-base",
|
|
63
|
+
"rag",
|
|
64
|
+
"vector-search",
|
|
65
|
+
"embeddings",
|
|
66
|
+
"conversational-ai",
|
|
67
|
+
"llm",
|
|
68
|
+
"ai-assistant",
|
|
69
|
+
"saas-bot",
|
|
70
|
+
"customer-support",
|
|
71
|
+
"react-widget",
|
|
72
|
+
"embeddable-chat",
|
|
73
|
+
"semantic-search",
|
|
74
|
+
"ai-sdk",
|
|
75
|
+
"openai",
|
|
76
|
+
"supabase",
|
|
77
|
+
"pgvector",
|
|
78
|
+
"typescript"
|
|
79
|
+
],
|
|
80
|
+
"author": "Vezlo",
|
|
81
|
+
"license": "AGPL-3.0",
|
|
82
|
+
"peerDependencies": {
|
|
83
|
+
"react": ">=18",
|
|
84
|
+
"react-dom": ">=18"
|
|
85
|
+
},
|
|
86
|
+
"publishConfig": {
|
|
87
|
+
"access": "public"
|
|
88
|
+
},
|
|
89
|
+
"repository": {
|
|
90
|
+
"type": "git",
|
|
91
|
+
"url": "git+https://github.com/vezlo/assistant-chat.git"
|
|
92
|
+
},
|
|
93
|
+
"homepage": "https://github.com/vezlo/assistant-chat#readme",
|
|
94
|
+
"bugs": {
|
|
95
|
+
"url": "https://github.com/vezlo/assistant-chat/issues"
|
|
96
|
+
},
|
|
97
|
+
"engines": {
|
|
98
|
+
"node": ">=20.0.0",
|
|
99
|
+
"npm": ">=9.0.0"
|
|
100
|
+
},
|
|
101
|
+
"readme": "PACKAGE_README.md"
|
|
102
|
+
}
|