@vezlo/assistant-chat 1.3.0 → 1.5.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/PACKAGE_README.md CHANGED
@@ -90,7 +90,7 @@ This widget requires a running Assistant Server instance. The widget will:
90
90
 
91
91
  1. Create conversations automatically
92
92
  2. Send user messages to the server
93
- 3. Stream AI responses in real-time
93
+ 3. Stream AI responses in real-time using Server-Sent Events (SSE)
94
94
  4. **Realtime Updates**: With `supabaseUrl` and `supabaseAnonKey` configured, the widget receives realtime updates for agent handoff and live message synchronization
95
95
 
96
96
  Configure your Assistant Server URL in your application:
package/README.md CHANGED
@@ -10,7 +10,7 @@ A complete chat widget solution with both a React component library and standalo
10
10
  - **Reusable React Widget**: Install via `npm install @vezlo/assistant-chat`
11
11
  - **TypeScript Support**: Full type definitions included
12
12
  - **Customizable**: Themes, colors, positioning, and behavior
13
- - **Real-time Streaming**: Live AI responses with streaming support
13
+ - **Real-time Streaming**: Live AI responses with Server-Sent Events (SSE) streaming support
14
14
  - **Style Isolation**: Shadow DOM support for conflict-free integration
15
15
  - **📖 [Complete Package Documentation](PACKAGE_README.md)**
16
16
 
@@ -85,7 +85,7 @@ npm run dev
85
85
  - ✅ React component library
86
86
  - ✅ TypeScript support
87
87
  - ✅ Tailwind CSS styling
88
- - ✅ Real-time streaming
88
+ - ✅ Real-time streaming (SSE-based backend streaming)
89
89
  - ✅ **Realtime updates** (agent handoff, live message sync)
90
90
  - ✅ Customizable themes
91
91
  - ✅ Shadow DOM support
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Analytics API Service
3
+ * Handles fetching company analytics data
4
+ */
5
+ export interface CompanyAnalyticsResponse {
6
+ conversations: {
7
+ total: number;
8
+ open: number;
9
+ closed: number;
10
+ };
11
+ users: {
12
+ total_active_users: number;
13
+ };
14
+ messages: {
15
+ total: number;
16
+ user_messages_total: number;
17
+ assistant_messages_total: number;
18
+ agent_messages_total: number;
19
+ };
20
+ feedback: {
21
+ total: number;
22
+ likes: number;
23
+ dislikes: number;
24
+ };
25
+ }
26
+ export declare function getCompanyAnalytics(token: string, apiUrl?: string): Promise<CompanyAnalyticsResponse>;
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Analytics API Service
3
+ * Handles fetching company analytics data
4
+ */
5
+ const DEFAULT_API_BASE_URL = import.meta.env.VITE_ASSISTANT_SERVER_URL || 'http://localhost:3000';
6
+ const parseErrorMessage = async (response) => {
7
+ const data = await response.json().catch(() => ({}));
8
+ return (data.error ||
9
+ data.message ||
10
+ 'Unexpected server error');
11
+ };
12
+ export async function getCompanyAnalytics(token, apiUrl) {
13
+ const API_BASE_URL = apiUrl || DEFAULT_API_BASE_URL;
14
+ const response = await fetch(`${API_BASE_URL}/api/company/analytics`, {
15
+ method: 'GET',
16
+ headers: {
17
+ Accept: 'application/json',
18
+ Authorization: `Bearer ${token}`,
19
+ },
20
+ });
21
+ if (!response.ok) {
22
+ const message = await parseErrorMessage(response);
23
+ throw new Error(message);
24
+ }
25
+ return (await response.json());
26
+ }
@@ -5,3 +5,4 @@
5
5
  export * from './auth.js';
6
6
  export * from './conversation.js';
7
7
  export * from './message.js';
8
+ export * from './analytics.js';
package/lib/api/index.js CHANGED
@@ -5,3 +5,4 @@
5
5
  export * from './auth.js';
6
6
  export * from './conversation.js';
7
7
  export * from './message.js';
8
+ export * from './analytics.js';
@@ -20,11 +20,75 @@ export interface GenerateMessageResponse {
20
20
  status: 'completed' | 'pending' | 'error';
21
21
  created_at: string;
22
22
  }
23
+ export interface StreamChunkEvent {
24
+ type: 'chunk';
25
+ content: string;
26
+ done?: boolean;
27
+ }
28
+ export interface StreamCompletionEvent {
29
+ type: 'completion';
30
+ uuid: string;
31
+ parent_message_uuid: string;
32
+ status: 'completed';
33
+ created_at: string;
34
+ }
35
+ export interface StreamErrorEvent {
36
+ type: 'error';
37
+ error: string;
38
+ message: string;
39
+ }
40
+ export type StreamEvent = StreamChunkEvent | StreamCompletionEvent | StreamErrorEvent;
41
+ export interface StreamCallbacks {
42
+ onChunk?: (content: string, isDone?: boolean) => void;
43
+ onCompletion?: (data: StreamCompletionEvent) => void;
44
+ onError?: (error: StreamErrorEvent) => void;
45
+ onDone?: () => void;
46
+ }
23
47
  /**
24
48
  * Create a user message in a conversation
25
49
  */
26
50
  export declare function createUserMessage(conversationUuid: string, request: CreateMessageRequest, apiUrl?: string): Promise<MessageResponse>;
27
51
  /**
28
- * Generate AI response for a user message
52
+ * Generate AI response for a user message (legacy - returns full response)
53
+ * @deprecated Use streamAIResponse for better performance
29
54
  */
30
55
  export declare function generateAIResponse(userMessageUuid: string, apiUrl?: string): Promise<GenerateMessageResponse>;
56
+ /**
57
+ * Stream AI response using Server-Sent Events (SSE)
58
+ * This is the recommended approach for real-time streaming
59
+ */
60
+ export declare function streamAIResponse(userMessageUuid: string, callbacks: StreamCallbacks, apiUrl?: string): Promise<void>;
61
+ /**
62
+ * Feedback API
63
+ */
64
+ export interface SubmitFeedbackRequest {
65
+ message_uuid: string;
66
+ rating: 'positive' | 'negative';
67
+ category?: string;
68
+ comment?: string;
69
+ suggested_improvement?: string;
70
+ }
71
+ export interface SubmitFeedbackResponse {
72
+ success: boolean;
73
+ feedback: {
74
+ uuid: string;
75
+ message_uuid: string;
76
+ rating: 'positive' | 'negative';
77
+ category?: string;
78
+ comment?: string;
79
+ suggested_improvement?: string;
80
+ created_at: string;
81
+ };
82
+ }
83
+ export interface DeleteFeedbackResponse {
84
+ success: boolean;
85
+ message: string;
86
+ }
87
+ /**
88
+ * Submit feedback for a message (create or update) - Public API
89
+ */
90
+ export declare function submitFeedback(request: SubmitFeedbackRequest, apiUrl?: string): Promise<SubmitFeedbackResponse>;
91
+ /**
92
+ * Delete/undo feedback for a message - Public API
93
+ */
94
+ export declare function deleteFeedback(feedbackUuid: string, apiUrl?: string): Promise<DeleteFeedbackResponse>;
@@ -30,7 +30,8 @@ export async function createUserMessage(conversationUuid, request, apiUrl) {
30
30
  }
31
31
  }
32
32
  /**
33
- * Generate AI response for a user message
33
+ * Generate AI response for a user message (legacy - returns full response)
34
+ * @deprecated Use streamAIResponse for better performance
34
35
  */
35
36
  export async function generateAIResponse(userMessageUuid, apiUrl) {
36
37
  const API_BASE_URL = apiUrl || DEFAULT_API_BASE_URL;
@@ -53,3 +54,178 @@ export async function generateAIResponse(userMessageUuid, apiUrl) {
53
54
  throw error;
54
55
  }
55
56
  }
57
+ /**
58
+ * Stream AI response using Server-Sent Events (SSE)
59
+ * This is the recommended approach for real-time streaming
60
+ */
61
+ export async function streamAIResponse(userMessageUuid, callbacks, apiUrl) {
62
+ const API_BASE_URL = apiUrl || DEFAULT_API_BASE_URL;
63
+ try {
64
+ const response = await fetch(`${API_BASE_URL}/api/messages/${userMessageUuid}/generate`, {
65
+ method: 'POST',
66
+ headers: {
67
+ 'Accept': 'text/event-stream',
68
+ },
69
+ });
70
+ if (!response.ok) {
71
+ // Try to parse error as JSON first
72
+ try {
73
+ const errorData = await response.json();
74
+ callbacks.onError?.({
75
+ type: 'error',
76
+ error: 'Failed to generate response',
77
+ message: errorData.message || `HTTP ${response.status}`,
78
+ });
79
+ }
80
+ catch {
81
+ // If not JSON, use status text
82
+ callbacks.onError?.({
83
+ type: 'error',
84
+ error: 'Failed to generate response',
85
+ message: `HTTP ${response.status}: ${response.statusText}`,
86
+ });
87
+ }
88
+ return;
89
+ }
90
+ if (!response.body) {
91
+ callbacks.onError?.({
92
+ type: 'error',
93
+ error: 'No response body',
94
+ message: 'Server did not return a response stream',
95
+ });
96
+ return;
97
+ }
98
+ const reader = response.body.getReader();
99
+ const decoder = new TextDecoder();
100
+ let buffer = '';
101
+ try {
102
+ while (true) {
103
+ const { done, value } = await reader.read();
104
+ if (done) {
105
+ break;
106
+ }
107
+ // Decode chunk and add to buffer
108
+ buffer += decoder.decode(value, { stream: true });
109
+ // Process complete SSE messages (lines ending with \n\n)
110
+ const lines = buffer.split('\n\n');
111
+ buffer = lines.pop() || ''; // Keep incomplete line in buffer
112
+ for (const line of lines) {
113
+ if (line.startsWith('data: ')) {
114
+ const data = line.slice(6); // Remove 'data: ' prefix
115
+ // Check for [DONE] marker
116
+ if (data.trim() === '[DONE]') {
117
+ callbacks.onDone?.();
118
+ return;
119
+ }
120
+ try {
121
+ const event = JSON.parse(data);
122
+ switch (event.type) {
123
+ case 'chunk':
124
+ callbacks.onChunk?.(event.content, event.done);
125
+ break;
126
+ case 'completion':
127
+ callbacks.onCompletion?.(event);
128
+ callbacks.onDone?.();
129
+ return;
130
+ case 'error':
131
+ callbacks.onError?.(event);
132
+ return;
133
+ }
134
+ }
135
+ catch (parseError) {
136
+ console.warn('[Message API] Failed to parse SSE event:', data, parseError);
137
+ // Continue processing other events
138
+ }
139
+ }
140
+ }
141
+ }
142
+ // Process any remaining buffer
143
+ if (buffer.trim()) {
144
+ const lines = buffer.split('\n\n');
145
+ for (const line of lines) {
146
+ if (line.startsWith('data: ')) {
147
+ const data = line.slice(6);
148
+ if (data.trim() === '[DONE]') {
149
+ callbacks.onDone?.();
150
+ return;
151
+ }
152
+ try {
153
+ const event = JSON.parse(data);
154
+ if (event.type === 'completion') {
155
+ callbacks.onCompletion?.(event);
156
+ }
157
+ else if (event.type === 'error') {
158
+ callbacks.onError?.(event);
159
+ }
160
+ }
161
+ catch (parseError) {
162
+ console.warn('[Message API] Failed to parse final SSE event:', parseError);
163
+ }
164
+ }
165
+ }
166
+ }
167
+ callbacks.onDone?.();
168
+ }
169
+ finally {
170
+ reader.releaseLock();
171
+ }
172
+ }
173
+ catch (error) {
174
+ console.error('[Message API] Error streaming AI response:', error);
175
+ callbacks.onError?.({
176
+ type: 'error',
177
+ error: 'Stream error',
178
+ message: error instanceof Error ? error.message : 'Unknown error occurred',
179
+ });
180
+ }
181
+ }
182
+ /**
183
+ * Submit feedback for a message (create or update) - Public API
184
+ */
185
+ export async function submitFeedback(request, apiUrl) {
186
+ const API_BASE_URL = apiUrl || DEFAULT_API_BASE_URL;
187
+ try {
188
+ const response = await fetch(`${API_BASE_URL}/api/feedback`, {
189
+ method: 'POST',
190
+ headers: {
191
+ 'Content-Type': 'application/json',
192
+ 'Accept': 'application/json',
193
+ },
194
+ body: JSON.stringify(request),
195
+ });
196
+ if (!response.ok) {
197
+ const errorData = await response.json().catch(() => ({}));
198
+ throw new Error(errorData.message || `Failed to submit feedback: ${response.status}`);
199
+ }
200
+ const data = await response.json();
201
+ return data;
202
+ }
203
+ catch (error) {
204
+ console.error('[Message API] Error submitting feedback:', error);
205
+ throw error;
206
+ }
207
+ }
208
+ /**
209
+ * Delete/undo feedback for a message - Public API
210
+ */
211
+ export async function deleteFeedback(feedbackUuid, apiUrl) {
212
+ const API_BASE_URL = apiUrl || DEFAULT_API_BASE_URL;
213
+ try {
214
+ const response = await fetch(`${API_BASE_URL}/api/feedback/${feedbackUuid}`, {
215
+ method: 'DELETE',
216
+ headers: {
217
+ 'Accept': 'application/json',
218
+ },
219
+ });
220
+ if (!response.ok) {
221
+ const errorData = await response.json().catch(() => ({}));
222
+ throw new Error(errorData.message || `Failed to delete feedback: ${response.status}`);
223
+ }
224
+ const data = await response.json();
225
+ return data;
226
+ }
227
+ catch (error) {
228
+ console.error('[Message API] Error deleting feedback:', error);
229
+ throw error;
230
+ }
231
+ }
@@ -4,7 +4,8 @@ import { createPortal } from 'react-dom';
4
4
  import { Send, X, MessageCircle, Bot, ThumbsUp, ThumbsDown } from 'lucide-react';
5
5
  import { generateId, formatTimestamp } from '../utils/index.js';
6
6
  import { VezloFooter } from './ui/VezloFooter.js';
7
- import { createConversation, createUserMessage, generateAIResponse } from '../api/index.js';
7
+ import { createConversation, createUserMessage, streamAIResponse } from '../api/index.js';
8
+ import { submitFeedback, deleteFeedback } from '../api/message.js';
8
9
  import { subscribeToConversations } from '../services/conversationRealtime.js';
9
10
  import { THEME } from '../config/theme.js';
10
11
  export function Widget({ config, isPlayground = false, onOpen, onClose, onMessage, onError, useShadowRoot = false, }) {
@@ -20,6 +21,7 @@ export function Widget({ config, isPlayground = false, onOpen, onClose, onMessag
20
21
  const [input, setInput] = useState('');
21
22
  const [isLoading, setIsLoading] = useState(false);
22
23
  const [messageFeedback, setMessageFeedback] = useState({});
24
+ const [messageFeedbackUuids, setMessageFeedbackUuids] = useState({});
23
25
  const [streamingMessage, setStreamingMessage] = useState('');
24
26
  const [conversationUuid, setConversationUuid] = useState(null);
25
27
  const [companyUuid, setCompanyUuid] = useState(null);
@@ -30,6 +32,7 @@ export function Widget({ config, isPlayground = false, onOpen, onClose, onMessag
30
32
  const hostRef = useRef(null);
31
33
  const shadowRef = useRef(null);
32
34
  const shadowMountRef = useRef(null);
35
+ const inputRef = useRef(null);
33
36
  const [shadowReady, setShadowReady] = useState(false);
34
37
  useEffect(() => {
35
38
  // No global scroll locking for embedded component usage
@@ -105,6 +108,13 @@ export function Widget({ config, isPlayground = false, onOpen, onClose, onMessag
105
108
  };
106
109
  initializeConversation();
107
110
  }, [isOpen, conversationUuid, isCreatingConversation, config.welcomeMessage, onMessage, onError]);
111
+ // Auto-focus input when it becomes enabled (isLoading becomes false)
112
+ useEffect(() => {
113
+ if (isOpen && !isLoading && !conversationClosed && inputRef.current) {
114
+ // Focus immediately when input becomes enabled
115
+ inputRef.current.focus();
116
+ }
117
+ }, [isLoading, isOpen, conversationClosed]);
108
118
  // Subscribe to realtime updates for agent messages
109
119
  useEffect(() => {
110
120
  if (!companyUuid || !conversationUuid) {
@@ -166,34 +176,76 @@ export function Widget({ config, isPlayground = false, onOpen, onClose, onMessag
166
176
  setMessages((prev) => prev.map((msg) => msg.id === userMessage.id ? { ...msg, id: userMessageResponse.uuid } : msg));
167
177
  // Step 2: Generate AI response (only if agent hasn't joined)
168
178
  if (!agentJoined) {
169
- // Keep loading indicator visible until AI response is received
170
- const aiResponse = await generateAIResponse(userMessageResponse.uuid, config.apiUrl);
171
- console.log('[Widget] AI response received:', aiResponse.uuid);
172
- // Hide loading indicator now that we have the response
173
- setIsLoading(false);
174
- // Stream the AI response character by character
175
- const responseContent = aiResponse.content;
179
+ // Initialize streaming state
176
180
  setStreamingMessage('');
177
- let currentText = '';
178
- const streamInterval = setInterval(() => {
179
- if (currentText.length < responseContent.length) {
180
- currentText += responseContent[currentText.length];
181
- setStreamingMessage(currentText);
182
- }
183
- else {
184
- clearInterval(streamInterval);
185
- // Add the complete message to messages array
186
- const assistantMessage = {
187
- id: aiResponse.uuid,
188
- content: responseContent,
181
+ let accumulatedContent = '';
182
+ let hasReceivedChunks = false;
183
+ let streamingComplete = false;
184
+ const tempMessageId = `streaming-${userMessageResponse.uuid}`;
185
+ // Stream AI response using SSE
186
+ await streamAIResponse(userMessageResponse.uuid, {
187
+ onChunk: (chunk, isDone) => {
188
+ // Hide loading indicator on first chunk (streaming started)
189
+ if (!hasReceivedChunks) {
190
+ hasReceivedChunks = true;
191
+ setIsLoading(false);
192
+ }
193
+ // Accumulate content and update streaming message
194
+ if (chunk) {
195
+ accumulatedContent += chunk;
196
+ setStreamingMessage(accumulatedContent);
197
+ }
198
+ // If this is the final chunk (done=true), convert to message immediately
199
+ if (isDone && !streamingComplete) {
200
+ streamingComplete = true;
201
+ // Add message to array with temp ID (shows timestamp/icons)
202
+ const tempMessage = {
203
+ id: tempMessageId,
204
+ content: accumulatedContent,
205
+ role: 'assistant',
206
+ timestamp: new Date(),
207
+ };
208
+ setStreamingMessage('');
209
+ setMessages((prev) => [...prev, tempMessage]);
210
+ }
211
+ },
212
+ onCompletion: (completionData) => {
213
+ // Store real UUID in _realUuid field (for feedback) without changing id (no jerk)
214
+ setMessages((prev) => prev.map((msg) => msg.id === tempMessageId
215
+ ? { ...msg, _realUuid: completionData.uuid }
216
+ : msg));
217
+ setIsLoading(false);
218
+ const finalMessage = {
219
+ id: completionData.uuid,
220
+ content: accumulatedContent,
189
221
  role: 'assistant',
190
- timestamp: new Date(aiResponse.created_at),
222
+ timestamp: new Date(completionData.created_at),
191
223
  };
192
- setMessages((prev) => [...prev, assistantMessage]);
193
- onMessage?.(assistantMessage);
224
+ onMessage?.(finalMessage);
225
+ // Log completion (no content in event anymore)
226
+ console.log('[Widget] AI response completed:', {
227
+ uuid: completionData.uuid,
228
+ parent_message_uuid: completionData.parent_message_uuid,
229
+ status: completionData.status,
230
+ created_at: completionData.created_at,
231
+ accumulated_content_length: accumulatedContent.length
232
+ });
233
+ },
234
+ onError: (errorData) => {
235
+ // Hide loading indicator
236
+ setIsLoading(false);
237
+ // Clear streaming message
194
238
  setStreamingMessage('');
195
- }
196
- }, 15); // 15ms delay between characters for smooth streaming
239
+ // Show error to user
240
+ const errorMessage = errorData.message || errorData.error || 'Failed to generate response';
241
+ onError?.(errorMessage);
242
+ console.error('[Widget] AI response error:', errorData);
243
+ },
244
+ onDone: () => {
245
+ // Ensure loading is hidden
246
+ setIsLoading(false);
247
+ },
248
+ }, config.apiUrl);
197
249
  }
198
250
  else {
199
251
  // Agent has joined, don't generate AI response
@@ -237,11 +289,88 @@ export function Widget({ config, isPlayground = false, onOpen, onClose, onMessag
237
289
  handleSendMessage();
238
290
  }
239
291
  };
240
- const handleFeedback = (messageId, type) => {
241
- setMessageFeedback(prev => ({
242
- ...prev,
243
- [messageId]: prev[messageId] === type ? null : type
244
- }));
292
+ const handleFeedback = async (messageId, type) => {
293
+ // Find the message to get the real UUID (might be stored in _realUuid)
294
+ const message = messages.find(m => m.id === messageId);
295
+ // Safety check: Don't proceed if message has temp ID and no real UUID yet
296
+ if (!message?._realUuid && messageId.startsWith('streaming-')) {
297
+ console.warn('[Widget] Cannot submit feedback: Message UUID not yet available');
298
+ return;
299
+ }
300
+ const realUuid = message?._realUuid || messageId; // Use real UUID if available, fallback to ID
301
+ const currentFeedback = messageFeedback[messageId];
302
+ const feedbackUuid = messageFeedbackUuids[messageId];
303
+ // Map UI types to backend rating values
304
+ const rating = type === 'like' ? 'positive' : 'negative';
305
+ // Optimistic UI update - update immediately for better UX
306
+ const previousFeedback = currentFeedback;
307
+ const previousUuid = feedbackUuid;
308
+ // If clicking the same rating again, delete (undo)
309
+ if (currentFeedback === type && feedbackUuid) {
310
+ // Optimistically update UI (remove feedback)
311
+ setMessageFeedback(prev => ({
312
+ ...prev,
313
+ [messageId]: null
314
+ }));
315
+ setMessageFeedbackUuids(prev => {
316
+ const updated = { ...prev };
317
+ delete updated[messageId];
318
+ return updated;
319
+ });
320
+ // Call API in background
321
+ try {
322
+ await deleteFeedback(feedbackUuid, config.apiUrl);
323
+ }
324
+ catch (error) {
325
+ console.error('[Widget] Error deleting feedback:', error);
326
+ // Revert UI state on error
327
+ setMessageFeedback(prev => ({
328
+ ...prev,
329
+ [messageId]: previousFeedback
330
+ }));
331
+ setMessageFeedbackUuids(prev => ({
332
+ ...prev,
333
+ [messageId]: previousUuid
334
+ }));
335
+ }
336
+ }
337
+ else {
338
+ // Optimistically update UI (add/change feedback)
339
+ setMessageFeedback(prev => ({
340
+ ...prev,
341
+ [messageId]: type
342
+ }));
343
+ // Call API in background
344
+ try {
345
+ const response = await submitFeedback({
346
+ message_uuid: realUuid,
347
+ rating,
348
+ }, config.apiUrl);
349
+ // Update with real feedback UUID from server
350
+ setMessageFeedbackUuids(prev => ({
351
+ ...prev,
352
+ [messageId]: response.feedback.uuid
353
+ }));
354
+ }
355
+ catch (error) {
356
+ console.error('[Widget] Error submitting feedback:', error);
357
+ // Revert UI state on error
358
+ setMessageFeedback(prev => ({
359
+ ...prev,
360
+ [messageId]: previousFeedback
361
+ }));
362
+ setMessageFeedbackUuids(prev => {
363
+ if (previousUuid) {
364
+ return { ...prev, [messageId]: previousUuid };
365
+ }
366
+ else {
367
+ const updated = { ...prev };
368
+ delete updated[messageId];
369
+ return updated;
370
+ }
371
+ });
372
+ }
373
+ }
245
374
  };
246
375
  const handleOpenWidget = () => {
247
376
  setIsOpen(true);
@@ -311,7 +440,8 @@ export function Widget({ config, isPlayground = false, onOpen, onClose, onMessag
311
440
  display: 'flex',
312
441
  alignItems: 'center',
313
442
  justifyContent: 'center',
314
- transition: 'transform 0.2s ease, box-shadow 0.2s ease'
443
+ transition: 'transform 0.2s ease, box-shadow 0.2s ease',
444
+ cursor: 'pointer'
315
445
  }, onMouseEnter: (e) => {
316
446
  e.currentTarget.style.transform = 'scale(1.05)';
317
447
  e.currentTarget.style.boxShadow = '0 14px 30px rgba(0,0,0,0.22)';
@@ -322,7 +452,7 @@ export function Widget({ config, isPlayground = false, onOpen, onClose, onMessag
322
452
  pointerEvents: 'auto',
323
453
  width: (config.size && config.size.width) ? config.size.width : 420,
324
454
  height: (config.size && config.size.height) ? config.size.height : 600
325
- }, children: [_jsxs("div", { className: "text-white p-4 flex justify-between items-center relative overflow-hidden", style: { background: `linear-gradient(to right, ${config.themeColor || THEME.primary.hex}, ${config.themeColor || THEME.primary.hex}dd, ${config.themeColor || THEME.primary.hex}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-12 h-12 bg-white/20 rounded-2xl flex items-center justify-center backdrop-blur-sm flex-shrink-0", children: _jsx(Bot, { className: "w-6 h-6 text-white" }) }), _jsxs("div", { className: "min-w-0 flex-1", children: [_jsx("h3", { className: "font-semibold text-lg leading-tight truncate", children: config.title }), _jsxs("div", { className: "flex items-center gap-1.5 mt-0.5", children: [_jsx("div", { className: "w-2 h-2 bg-green-400 rounded-full animate-pulse flex-shrink-0" }), _jsxs("p", { className: "text-xs text-white/90 truncate", 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-white", children: [messages.map((message, index) => (_jsx("div", { className: `flex ${message.role === 'system'
455
+ }, children: [_jsxs("div", { className: "text-white p-4 flex justify-between items-center relative overflow-hidden", style: { background: `linear-gradient(to right, ${config.themeColor || THEME.primary.hex}, ${config.themeColor || THEME.primary.hex}dd, ${config.themeColor || THEME.primary.hex}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-12 h-12 bg-white/20 rounded-2xl flex items-center justify-center backdrop-blur-sm flex-shrink-0", children: _jsx(Bot, { className: "w-6 h-6 text-white" }) }), _jsxs("div", { className: "min-w-0 flex-1", children: [_jsx("h3", { className: "font-semibold text-lg leading-tight truncate", children: config.title }), _jsxs("div", { className: "flex items-center gap-1.5 mt-0.5", children: [_jsx("div", { className: "w-2 h-2 bg-green-400 rounded-full animate-pulse flex-shrink-0" }), _jsxs("p", { className: "text-xs text-white/90 truncate", 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 cursor-pointer", 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-white", children: [messages.map((message, index) => (_jsx("div", { className: `flex ${message.role === 'system'
326
456
  ? 'justify-center'
327
457
  : message.role === 'user'
328
458
  ? 'justify-end'
@@ -335,11 +465,22 @@ export function Widget({ config, isPlayground = false, onOpen, onClose, onMessag
335
465
  boxShadow: message.role === 'user'
336
466
  ? `0 4px 12px ${(config.themeColor || THEME.primary.hex)}4D` // 4D is ~30% opacity
337
467
  : '0 2px 8px rgba(0, 0, 0, 0.1)'
338
- }, 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'
339
- ? 'text-green-600'
340
- : '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'
341
- ? 'text-red-600'
342
- : '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-50 rounded-full flex items-center justify-center flex-shrink-0 mt-1 mr-2 border border-emerald-100", 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 || THEME.primary.hex, display: 'inline-block', animation: 'vezloDotPulse 1s infinite ease-in-out', animationDelay: '0s' } }), _jsx("span", { style: { width: 8, height: 8, borderRadius: 9999, backgroundColor: config.themeColor || THEME.primary.hex, 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 || THEME.primary.hex, display: 'inline-block', animation: 'vezloDotPulse 1s infinite ease-in-out', animationDelay: '0.3s' } })] }) }) })), _jsx("div", { ref: messagesEndRef })] }), conversationClosed && (_jsx("div", { className: "border-t border-gray-200 p-4 bg-white flex justify-center", children: _jsx("button", { onClick: handleStartNewChat, disabled: isCreatingConversation, className: "px-6 py-3 bg-gradient-to-r from-emerald-600 to-emerald-500 text-white text-sm font-medium rounded-lg hover:from-emerald-700 hover:to-emerald-600 transition-all duration-200 shadow-md hover:shadow-lg transform hover:scale-105 disabled:opacity-50 disabled:cursor-not-allowed disabled:transform-none cursor-pointer", children: isCreatingConversation ? 'Starting new chat...' : 'Start New Chat' }) })), !conversationClosed && (_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: conversationClosed
468
+ }, 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' && (() => {
469
+ // Check if message has real UUID (not temp ID)
470
+ // Disable if: message has temp ID (starts with 'streaming-') AND no _realUuid yet
471
+ const isTempId = message.id?.startsWith('streaming-') || false;
472
+ const hasRealUuid = !!message._realUuid;
473
+ const isDisabled = isTempId && !hasRealUuid;
474
+ return (_jsxs("div", { className: "flex items-center gap-1 ml-2", children: [_jsx("button", { onClick: () => !isDisabled && handleFeedback(message.id, 'like'), disabled: isDisabled, className: `p-1 rounded transition-all duration-200 ${isDisabled
475
+ ? 'opacity-40 cursor-not-allowed'
476
+ : 'hover:scale-110 cursor-pointer'} ${messageFeedback[message.id] === 'like'
477
+ ? 'text-green-600'
478
+ : 'text-gray-400 hover:text-green-600'}`, title: isDisabled ? 'Waiting for message to be saved...' : 'Like this response', children: _jsx(ThumbsUp, { className: `w-4 h-4 ${messageFeedback[message.id] === 'like' ? 'fill-current' : ''}` }) }), _jsx("button", { onClick: () => !isDisabled && handleFeedback(message.id, 'dislike'), disabled: isDisabled, className: `p-1 rounded transition-all duration-200 ${isDisabled
479
+ ? 'opacity-40 cursor-not-allowed'
480
+ : 'hover:scale-110 cursor-pointer'} ${messageFeedback[message.id] === 'dislike'
481
+ ? 'text-red-600'
482
+ : 'text-gray-400 hover:text-red-600'}`, title: isDisabled ? 'Waiting for message to be saved...' : 'Dislike this response', children: _jsx(ThumbsDown, { className: `w-4 h-4 ${messageFeedback[message.id] === 'dislike' ? 'fill-current' : ''}` }) })] }));
483
+ })()] })] })] })) }, message.id))), streamingMessage && (_jsxs("div", { className: "flex justify-start animate-fadeIn", children: [_jsx("div", { className: "w-8 h-8 bg-emerald-50 rounded-full flex items-center justify-center flex-shrink-0 mt-1 mr-2 border border-emerald-100", 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 || THEME.primary.hex, display: 'inline-block', animation: 'vezloDotPulse 1s infinite ease-in-out', animationDelay: '0s' } }), _jsx("span", { style: { width: 8, height: 8, borderRadius: 9999, backgroundColor: config.themeColor || THEME.primary.hex, 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 || THEME.primary.hex, display: 'inline-block', animation: 'vezloDotPulse 1s infinite ease-in-out', animationDelay: '0.3s' } })] }) }) })), _jsx("div", { ref: messagesEndRef })] }), conversationClosed && (_jsx("div", { className: "border-t border-gray-200 p-4 bg-white flex justify-center", children: _jsx("button", { onClick: handleStartNewChat, disabled: isCreatingConversation, className: "px-6 py-3 bg-gradient-to-r from-emerald-600 to-emerald-500 text-white text-sm font-medium rounded-lg hover:from-emerald-700 hover:to-emerald-600 transition-all duration-200 shadow-md hover:shadow-lg transform hover:scale-105 disabled:opacity-50 disabled:cursor-not-allowed disabled:transform-none cursor-pointer", children: isCreatingConversation ? 'Starting new chat...' : 'Start New Chat' }) })), !conversationClosed && (_jsx("div", { className: "border-t border-gray-200 p-4 bg-white", children: _jsxs("div", { className: "flex gap-3", children: [_jsx("input", { ref: inputRef, type: "text", value: input, onChange: (e) => setInput(e.target.value), onKeyPress: handleKeyPress, placeholder: conversationClosed
343
484
  ? 'Conversation closed. Start a new chat to continue.'
344
485
  : config.placeholder, disabled: isLoading || conversationClosed, className: "flex-1 px-4 py-3 border border-gray-300 rounded-2xl focus:outline-none focus:ring-2 focus:ring-emerald-500 focus:border-transparent disabled:bg-gray-100 disabled:cursor-not-allowed text-sm transition-all duration-200 placeholder:text-gray-400" }), _jsx("button", { onClick: handleSendMessage, disabled: !input.trim() || isLoading || conversationClosed, className: "text-white px-4 py-3 rounded-2xl 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 min-w-[48px]", style: {
345
486
  background: `linear-gradient(to right, ${config.themeColor || THEME.primary.hex}, ${config.themeColor || THEME.primary.hex}dd)`,
@@ -11,6 +11,11 @@ function getClient(supabaseUrl, supabaseAnonKey) {
11
11
  if (!client || clientUrl !== supabaseUrl || clientKey !== supabaseAnonKey) {
12
12
  try {
13
13
  client = createClient(supabaseUrl, supabaseAnonKey, {
14
+ auth: {
15
+ persistSession: false, // Prevent multiple auth instances warning
16
+ autoRefreshToken: false,
17
+ detectSessionInUrl: false
18
+ },
14
19
  realtime: { params: { eventsPerSecond: 2 } },
15
20
  });
16
21
  clientUrl = supabaseUrl;
@@ -23,6 +23,7 @@ export interface ChatMessage {
23
23
  role: 'user' | 'assistant' | 'system';
24
24
  timestamp: Date;
25
25
  sources?: ChatSource[];
26
+ _realUuid?: string;
26
27
  }
27
28
  export interface ChatSource {
28
29
  title: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vezlo/assistant-chat",
3
- "version": "1.3.0",
3
+ "version": "1.5.0",
4
4
  "description": "React component library for AI-powered chat widgets with RAG knowledge base integration, realtime updates, and human agent support",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",