@chatwidgetai/chat-widget 0.1.0 → 0.1.2
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/dist/api/client.d.ts +12 -0
- package/dist/api/client.d.ts.map +1 -1
- package/dist/components/ChatWidget.d.ts.map +1 -1
- package/dist/components/ChatWindow.d.ts +0 -2
- package/dist/components/ChatWindow.d.ts.map +1 -1
- package/dist/hooks/useChat.d.ts +0 -10
- package/dist/hooks/useChat.d.ts.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.esm.js +243 -242
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +243 -241
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/ai-chat-widget.umd.js +0 -65851
- package/dist/ai-chat-widget.umd.js.map +0 -1
- package/dist/utils/actionExecutor.d.ts +0 -18
- package/dist/utils/actionExecutor.d.ts.map +0 -1
package/dist/index.esm.js
CHANGED
|
@@ -5,9 +5,81 @@ import { useState, useRef, useEffect, useCallback, useMemo } from 'react';
|
|
|
5
5
|
* API Client for Widget Communication
|
|
6
6
|
* Handles all HTTP requests to the widget API
|
|
7
7
|
*/
|
|
8
|
+
class ApiError extends Error {
|
|
9
|
+
constructor(message, status, options) {
|
|
10
|
+
super(message);
|
|
11
|
+
this.name = 'ApiError';
|
|
12
|
+
this.status = status;
|
|
13
|
+
this.details = options?.details;
|
|
14
|
+
this.retryAfterMs = options?.retryAfterMs;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
function parseRetryAfter(headerValue) {
|
|
18
|
+
if (!headerValue)
|
|
19
|
+
return undefined;
|
|
20
|
+
const seconds = Number(headerValue);
|
|
21
|
+
if (!Number.isNaN(seconds) && seconds >= 0) {
|
|
22
|
+
return seconds * 1000;
|
|
23
|
+
}
|
|
24
|
+
const date = Date.parse(headerValue);
|
|
25
|
+
if (!Number.isNaN(date)) {
|
|
26
|
+
return Math.max(0, date - Date.now());
|
|
27
|
+
}
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
|
30
|
+
async function buildApiError(response, defaultMessage) {
|
|
31
|
+
let parsedBody = null;
|
|
32
|
+
let message = defaultMessage;
|
|
33
|
+
try {
|
|
34
|
+
const raw = await response.text();
|
|
35
|
+
if (raw) {
|
|
36
|
+
try {
|
|
37
|
+
parsedBody = JSON.parse(raw);
|
|
38
|
+
if (typeof parsedBody === 'object' && parsedBody !== null) {
|
|
39
|
+
const body = parsedBody;
|
|
40
|
+
message = typeof body.error === 'string'
|
|
41
|
+
? body.error
|
|
42
|
+
: typeof body.message === 'string'
|
|
43
|
+
? body.message
|
|
44
|
+
: defaultMessage;
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
message = raw;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
message = raw;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
// ignore body parsing errors
|
|
57
|
+
}
|
|
58
|
+
let retryAfterMs = parseRetryAfter(response.headers.get('retry-after'));
|
|
59
|
+
if (retryAfterMs === undefined) {
|
|
60
|
+
retryAfterMs = parseRetryAfter(response.headers.get('x-ratelimit-reset'));
|
|
61
|
+
}
|
|
62
|
+
return new ApiError(message || defaultMessage, response.status, {
|
|
63
|
+
details: parsedBody,
|
|
64
|
+
retryAfterMs,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
8
67
|
class WidgetApiClient {
|
|
9
68
|
constructor(config) {
|
|
10
69
|
this.config = config;
|
|
70
|
+
this.detectedTimeZone = WidgetApiClient.detectTimeZone();
|
|
71
|
+
}
|
|
72
|
+
static detectTimeZone() {
|
|
73
|
+
try {
|
|
74
|
+
const tz = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
75
|
+
return tz && tz.trim().length > 0 ? tz : "UTC";
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
return "UTC";
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
getTimeZone() {
|
|
82
|
+
return this.detectedTimeZone;
|
|
11
83
|
}
|
|
12
84
|
/**
|
|
13
85
|
* Get widget configuration
|
|
@@ -21,23 +93,32 @@ class WidgetApiClient {
|
|
|
21
93
|
},
|
|
22
94
|
});
|
|
23
95
|
if (!response.ok) {
|
|
24
|
-
throw
|
|
96
|
+
throw await buildApiError(response, 'Failed to fetch config');
|
|
25
97
|
}
|
|
26
98
|
const json = await response.json();
|
|
27
99
|
return json;
|
|
28
100
|
}
|
|
29
101
|
async getOrCreateConversation(conversationId) {
|
|
30
|
-
const
|
|
102
|
+
const baseUrl = `${this.config.apiUrl}/api/widget/${this.config.widgetId}/conversation`;
|
|
103
|
+
const params = new URLSearchParams();
|
|
104
|
+
if (conversationId) {
|
|
105
|
+
params.set('conversationId', conversationId);
|
|
106
|
+
}
|
|
107
|
+
const timeZone = this.getTimeZone();
|
|
108
|
+
if (timeZone) {
|
|
109
|
+
params.set('timeZone', timeZone);
|
|
110
|
+
}
|
|
111
|
+
const query = params.toString() ? `?${params.toString()}` : '';
|
|
112
|
+
const response = await fetch(`${baseUrl}${query}`, {
|
|
31
113
|
method: 'GET',
|
|
32
114
|
headers: {
|
|
33
115
|
'Authorization': `Bearer ${this.config.apiKey}`,
|
|
34
|
-
}
|
|
116
|
+
},
|
|
35
117
|
});
|
|
36
118
|
if (!response.ok) {
|
|
37
|
-
|
|
38
|
-
throw new Error(error.error || 'Failed to upload file');
|
|
119
|
+
throw await buildApiError(response, 'Failed to load conversation');
|
|
39
120
|
}
|
|
40
|
-
return
|
|
121
|
+
return response.json();
|
|
41
122
|
}
|
|
42
123
|
/**
|
|
43
124
|
* Upload a file
|
|
@@ -54,8 +135,7 @@ class WidgetApiClient {
|
|
|
54
135
|
body: formData,
|
|
55
136
|
});
|
|
56
137
|
if (!response.ok) {
|
|
57
|
-
|
|
58
|
-
throw new Error(error.error || 'Failed to upload file');
|
|
138
|
+
throw await buildApiError(response, 'Failed to upload file');
|
|
59
139
|
}
|
|
60
140
|
const result = await response.json();
|
|
61
141
|
return result.file;
|
|
@@ -75,11 +155,11 @@ class WidgetApiClient {
|
|
|
75
155
|
conversationId: conversationId,
|
|
76
156
|
message,
|
|
77
157
|
fileIds,
|
|
158
|
+
timeZone: this.getTimeZone(),
|
|
78
159
|
}),
|
|
79
160
|
});
|
|
80
161
|
if (!response.ok) {
|
|
81
|
-
|
|
82
|
-
throw new Error(error.error || 'Failed to send message');
|
|
162
|
+
throw await buildApiError(response, 'Failed to send message');
|
|
83
163
|
}
|
|
84
164
|
return response.json();
|
|
85
165
|
}
|
|
@@ -98,11 +178,11 @@ class WidgetApiClient {
|
|
|
98
178
|
conversationId: conversationId,
|
|
99
179
|
message,
|
|
100
180
|
fileIds,
|
|
181
|
+
timeZone: this.getTimeZone(),
|
|
101
182
|
}),
|
|
102
183
|
});
|
|
103
184
|
if (!response.ok) {
|
|
104
|
-
|
|
105
|
-
throw new Error(error.error || `Agent request failed with status ${response.status}`);
|
|
185
|
+
throw await buildApiError(response, `Agent request failed with status ${response.status}`);
|
|
106
186
|
}
|
|
107
187
|
const data = await response.json();
|
|
108
188
|
// Check if response indicates an error
|
|
@@ -114,7 +194,7 @@ class WidgetApiClient {
|
|
|
114
194
|
catch (error) {
|
|
115
195
|
// Enhance error messages
|
|
116
196
|
if (error instanceof TypeError && error.message.includes('fetch')) {
|
|
117
|
-
throw new
|
|
197
|
+
throw new ApiError('Network error: Unable to reach the server', 0);
|
|
118
198
|
}
|
|
119
199
|
throw error;
|
|
120
200
|
}
|
|
@@ -136,13 +216,13 @@ class WidgetApiClient {
|
|
|
136
216
|
}),
|
|
137
217
|
});
|
|
138
218
|
if (!response.ok) {
|
|
139
|
-
const
|
|
219
|
+
const apiError = await buildApiError(response, 'Failed to submit feedback');
|
|
140
220
|
console.error('Feedback submission failed:', {
|
|
141
221
|
status: response.status,
|
|
142
|
-
error:
|
|
222
|
+
error: apiError.details,
|
|
143
223
|
payload: { session_id: sessionId, message_id: messageId, feedback }
|
|
144
224
|
});
|
|
145
|
-
throw
|
|
225
|
+
throw apiError;
|
|
146
226
|
}
|
|
147
227
|
}
|
|
148
228
|
/**
|
|
@@ -175,11 +255,6 @@ class WidgetApiClient {
|
|
|
175
255
|
/**
|
|
176
256
|
* Generate a unique session ID
|
|
177
257
|
*/
|
|
178
|
-
function generateSessionId() {
|
|
179
|
-
const timestamp = Date.now().toString(36);
|
|
180
|
-
const randomStr = Math.random().toString(36).substring(2, 15);
|
|
181
|
-
return `session_${timestamp}_${randomStr}`;
|
|
182
|
-
}
|
|
183
258
|
/**
|
|
184
259
|
* Generate a unique message ID
|
|
185
260
|
*/
|
|
@@ -234,6 +309,18 @@ function loadConversation(widgetId) {
|
|
|
234
309
|
return null;
|
|
235
310
|
}
|
|
236
311
|
}
|
|
312
|
+
/**
|
|
313
|
+
* Clear conversation from localStorage
|
|
314
|
+
*/
|
|
315
|
+
function clearConversation(widgetId) {
|
|
316
|
+
try {
|
|
317
|
+
const key = getStorageKey(widgetId);
|
|
318
|
+
localStorage.removeItem(key);
|
|
319
|
+
}
|
|
320
|
+
catch (error) {
|
|
321
|
+
console.error('Failed to clear conversation:', error);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
237
324
|
/**
|
|
238
325
|
* Check if localStorage is available
|
|
239
326
|
*/
|
|
@@ -250,111 +337,70 @@ function isStorageAvailable() {
|
|
|
250
337
|
}
|
|
251
338
|
|
|
252
339
|
/**
|
|
253
|
-
*
|
|
254
|
-
*
|
|
255
|
-
*/
|
|
256
|
-
/**
|
|
257
|
-
* Execute a client-side action
|
|
340
|
+
* useChat Hook
|
|
341
|
+
* Main state management for chat functionality
|
|
258
342
|
*/
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
case
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
343
|
+
function deriveErrorInfo(error) {
|
|
344
|
+
if (error instanceof ApiError) {
|
|
345
|
+
const retryAfterSeconds = typeof error.retryAfterMs === 'number'
|
|
346
|
+
? Math.max(1, Math.ceil(error.retryAfterMs / 1000))
|
|
347
|
+
: undefined;
|
|
348
|
+
const lowerMessage = (error.message || '').toLowerCase();
|
|
349
|
+
let message;
|
|
350
|
+
switch (error.status) {
|
|
351
|
+
case 429: {
|
|
352
|
+
const isPerUser = lowerMessage.includes('user');
|
|
353
|
+
const base = isPerUser
|
|
354
|
+
? 'You have reached the per-user rate limit.'
|
|
355
|
+
: 'This widget has received too many requests.';
|
|
356
|
+
if (retryAfterSeconds) {
|
|
357
|
+
message = `${base} Please wait ${retryAfterSeconds} second${retryAfterSeconds === 1 ? '' : 's'} before trying again.`;
|
|
358
|
+
}
|
|
359
|
+
else {
|
|
360
|
+
message = `${base} Please wait a moment and try again.`;
|
|
361
|
+
}
|
|
362
|
+
break;
|
|
363
|
+
}
|
|
364
|
+
case 401:
|
|
365
|
+
message = 'Authentication failed. Please refresh the page or verify your API key.';
|
|
366
|
+
break;
|
|
367
|
+
case 403:
|
|
368
|
+
message = 'Access to this widget is restricted. Please contact the site owner if you believe this is an error.';
|
|
369
|
+
break;
|
|
370
|
+
case 404:
|
|
371
|
+
message = 'We could not find this widget. It may have been removed.';
|
|
372
|
+
break;
|
|
271
373
|
default:
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
374
|
+
if (error.status >= 500) {
|
|
375
|
+
message = 'The server encountered an error. Please try again shortly.';
|
|
376
|
+
}
|
|
377
|
+
else if (error.status > 0) {
|
|
378
|
+
message = error.message || 'Something went wrong. Please try again.';
|
|
379
|
+
}
|
|
380
|
+
else {
|
|
381
|
+
message = error.message || 'Unable to connect to the server. Please check your internet connection.';
|
|
382
|
+
}
|
|
277
383
|
}
|
|
384
|
+
return { message, retryAfterSeconds, status: error.status };
|
|
278
385
|
}
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
return {
|
|
294
|
-
}
|
|
295
|
-
try {
|
|
296
|
-
// Validate URL
|
|
297
|
-
new URL(url);
|
|
298
|
-
// Redirect
|
|
299
|
-
window.location.href = url;
|
|
300
|
-
return { success: true, result: { redirected_to: url } };
|
|
301
|
-
}
|
|
302
|
-
catch (error) {
|
|
303
|
-
return { success: false, error: 'Invalid URL' };
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
/**
|
|
307
|
-
* Show a modal
|
|
308
|
-
*/
|
|
309
|
-
function executeShowModal(params) {
|
|
310
|
-
const { title, content } = params;
|
|
311
|
-
if (!title || !content) {
|
|
312
|
-
return { success: false, error: 'Title and content are required' };
|
|
313
|
-
}
|
|
314
|
-
// Use browser's alert as a simple modal
|
|
315
|
-
// In a real implementation, you'd use a proper modal component
|
|
316
|
-
alert(`${title}\n\n${content}`);
|
|
317
|
-
return { success: true, result: { shown: true } };
|
|
318
|
-
}
|
|
319
|
-
/**
|
|
320
|
-
* Copy text to clipboard
|
|
321
|
-
*/
|
|
322
|
-
async function executeCopyToClipboard(params) {
|
|
323
|
-
const text = params.text;
|
|
324
|
-
if (!text) {
|
|
325
|
-
return { success: false, error: 'Text parameter is required' };
|
|
326
|
-
}
|
|
327
|
-
try {
|
|
328
|
-
await navigator.clipboard.writeText(text);
|
|
329
|
-
return { success: true, result: { copied: text } };
|
|
330
|
-
}
|
|
331
|
-
catch (error) {
|
|
332
|
-
return {
|
|
333
|
-
success: false,
|
|
334
|
-
error: 'Failed to copy to clipboard. Please check browser permissions.'
|
|
335
|
-
};
|
|
336
|
-
}
|
|
337
|
-
}
|
|
338
|
-
/**
|
|
339
|
-
* Open chat with a specific message
|
|
340
|
-
*/
|
|
341
|
-
function executeOpenChat(params) {
|
|
342
|
-
const message = params.message;
|
|
343
|
-
if (!message) {
|
|
344
|
-
return { success: false, error: 'Message parameter is required' };
|
|
386
|
+
if (error instanceof Error) {
|
|
387
|
+
const lower = error.message.toLowerCase();
|
|
388
|
+
if (lower.includes('network')) {
|
|
389
|
+
return { message: 'Unable to connect to the server. Please check your internet connection.' };
|
|
390
|
+
}
|
|
391
|
+
if (lower.includes('timeout')) {
|
|
392
|
+
return { message: 'The request timed out. Please try again.' };
|
|
393
|
+
}
|
|
394
|
+
if (lower.includes('unauthorized') || lower.includes('401')) {
|
|
395
|
+
return { message: 'Authentication failed. Please refresh the page or verify your API key.' };
|
|
396
|
+
}
|
|
397
|
+
if (lower.includes('internal server error') || lower.includes('500')) {
|
|
398
|
+
return { message: 'The server encountered an error. Please try again shortly.' };
|
|
399
|
+
}
|
|
400
|
+
return { message: error.message || 'Something went wrong. Please try again.' };
|
|
345
401
|
}
|
|
346
|
-
|
|
347
|
-
const event = new CustomEvent('widget:open-chat', {
|
|
348
|
-
detail: { message }
|
|
349
|
-
});
|
|
350
|
-
window.dispatchEvent(event);
|
|
351
|
-
return { success: true, result: { message } };
|
|
402
|
+
return { message: 'Something went wrong. Please try again.' };
|
|
352
403
|
}
|
|
353
|
-
|
|
354
|
-
/**
|
|
355
|
-
* useChat Hook
|
|
356
|
-
* Main state management for chat functionality
|
|
357
|
-
*/
|
|
358
404
|
function useChat(options) {
|
|
359
405
|
const { widgetId, apiKey, apiUrl, onMessage, onError, } = options;
|
|
360
406
|
const [state, setState] = useState({
|
|
@@ -366,42 +412,62 @@ function useChat(options) {
|
|
|
366
412
|
conversationId: '', // Will be set after loading conversation
|
|
367
413
|
config: null,
|
|
368
414
|
});
|
|
369
|
-
const [pendingAction, setPendingAction] = useState(null);
|
|
370
415
|
const apiClient = useRef(new WidgetApiClient({ widgetId, apiKey, apiUrl }));
|
|
371
|
-
// Load configuration and conversation
|
|
416
|
+
// Load configuration on mount and hydrate with existing conversation if available
|
|
372
417
|
useEffect(() => {
|
|
418
|
+
let isMounted = true;
|
|
373
419
|
const initialize = async () => {
|
|
374
420
|
try {
|
|
375
|
-
// Load config
|
|
376
421
|
const config = await apiClient.current.getConfig();
|
|
377
|
-
// Get or create conversation
|
|
378
422
|
const persistConversation = config.behavior.persistConversation ?? true;
|
|
379
|
-
let conversationId;
|
|
423
|
+
let conversationId = '';
|
|
424
|
+
let messages = [];
|
|
380
425
|
if (persistConversation && isStorageAvailable()) {
|
|
381
426
|
const stored = loadConversation(widgetId);
|
|
382
|
-
|
|
427
|
+
const storedId = stored?.conversationId || stored?.sessionId;
|
|
428
|
+
if (storedId) {
|
|
429
|
+
try {
|
|
430
|
+
const conversation = await apiClient.current.getOrCreateConversation(storedId);
|
|
431
|
+
conversationId = conversation.id;
|
|
432
|
+
messages = conversation.messages;
|
|
433
|
+
}
|
|
434
|
+
catch (conversationError) {
|
|
435
|
+
console.warn('Failed to load existing conversation:', conversationError);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
if (!isMounted) {
|
|
440
|
+
return;
|
|
383
441
|
}
|
|
384
|
-
// Get conversation from backend
|
|
385
|
-
const conversation = await apiClient.current.getOrCreateConversation(conversationId);
|
|
386
442
|
setState(prev => ({
|
|
387
443
|
...prev,
|
|
388
444
|
config,
|
|
389
|
-
conversationId
|
|
390
|
-
messages
|
|
445
|
+
conversationId,
|
|
446
|
+
messages,
|
|
391
447
|
}));
|
|
392
448
|
}
|
|
393
449
|
catch (error) {
|
|
394
|
-
|
|
395
|
-
|
|
450
|
+
if (!isMounted) {
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
const errorInfo = deriveErrorInfo(error);
|
|
454
|
+
const err = error instanceof Error ? error : new Error(errorInfo.message);
|
|
455
|
+
setState(prev => ({ ...prev, error: errorInfo.message }));
|
|
396
456
|
onError?.(err);
|
|
397
457
|
}
|
|
398
458
|
};
|
|
399
459
|
initialize();
|
|
460
|
+
return () => {
|
|
461
|
+
isMounted = false;
|
|
462
|
+
};
|
|
400
463
|
}, [widgetId, apiKey, apiUrl, onError]);
|
|
401
464
|
// Save conversation when messages change
|
|
402
465
|
useEffect(() => {
|
|
403
466
|
const persistConversation = state.config?.behavior.persistConversation ?? true;
|
|
404
|
-
if (persistConversation &&
|
|
467
|
+
if (persistConversation &&
|
|
468
|
+
isStorageAvailable() &&
|
|
469
|
+
state.messages.length > 0 &&
|
|
470
|
+
state.conversationId) {
|
|
405
471
|
saveConversation(widgetId, state.conversationId, state.messages);
|
|
406
472
|
}
|
|
407
473
|
}, [widgetId, state.messages, state.conversationId, state.config?.behavior.persistConversation]);
|
|
@@ -409,13 +475,15 @@ function useChat(options) {
|
|
|
409
475
|
* Send a message
|
|
410
476
|
*/
|
|
411
477
|
const sendMessage = useCallback(async (content, files) => {
|
|
412
|
-
|
|
478
|
+
const trimmedContent = content.trim();
|
|
479
|
+
const hasFiles = !!files && files.length > 0;
|
|
480
|
+
if (!trimmedContent && !hasFiles)
|
|
413
481
|
return;
|
|
414
482
|
const userMessage = {
|
|
415
483
|
id: generateMessageId(),
|
|
416
484
|
message: {
|
|
417
485
|
type: 'human',
|
|
418
|
-
content:
|
|
486
|
+
content: trimmedContent,
|
|
419
487
|
},
|
|
420
488
|
timestamp: new Date().toISOString(),
|
|
421
489
|
sources: [],
|
|
@@ -430,13 +498,37 @@ function useChat(options) {
|
|
|
430
498
|
}));
|
|
431
499
|
onMessage?.(userMessage);
|
|
432
500
|
try {
|
|
501
|
+
let conversationId = state.conversationId;
|
|
502
|
+
if (!conversationId) {
|
|
503
|
+
const conversation = await apiClient.current.getOrCreateConversation();
|
|
504
|
+
conversationId = conversation.id;
|
|
505
|
+
setState(prev => {
|
|
506
|
+
const serverMessages = conversation.messages ?? [];
|
|
507
|
+
if (serverMessages.length === 0) {
|
|
508
|
+
return {
|
|
509
|
+
...prev,
|
|
510
|
+
conversationId,
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
const serverIds = new Set(serverMessages.map(msg => msg.id));
|
|
514
|
+
const mergedMessages = [
|
|
515
|
+
...serverMessages,
|
|
516
|
+
...prev.messages.filter(msg => !serverIds.has(msg.id)),
|
|
517
|
+
];
|
|
518
|
+
return {
|
|
519
|
+
...prev,
|
|
520
|
+
conversationId,
|
|
521
|
+
messages: mergedMessages,
|
|
522
|
+
};
|
|
523
|
+
});
|
|
524
|
+
}
|
|
433
525
|
// Upload files if provided
|
|
434
526
|
let fileIds;
|
|
435
527
|
if (files && files.length > 0) {
|
|
436
528
|
fileIds = [];
|
|
437
529
|
for (const file of files) {
|
|
438
530
|
try {
|
|
439
|
-
const uploadedFile = await apiClient.current.uploadFile(
|
|
531
|
+
const uploadedFile = await apiClient.current.uploadFile(conversationId, file);
|
|
440
532
|
fileIds.push(uploadedFile.id);
|
|
441
533
|
}
|
|
442
534
|
catch (uploadError) {
|
|
@@ -447,15 +539,14 @@ function useChat(options) {
|
|
|
447
539
|
}
|
|
448
540
|
// Determine if widget has actions (use agent endpoint)
|
|
449
541
|
const useAgent = state.config?.behavior.agentic || (state.config?.actions && state.config.actions.length > 0);
|
|
450
|
-
console.log(useAgent);
|
|
451
542
|
let response;
|
|
452
543
|
if (useAgent) {
|
|
453
544
|
// Use agent endpoint - returns ConversationMessage[]
|
|
454
|
-
response = await apiClient.current.sendAgentMessage(
|
|
545
|
+
response = await apiClient.current.sendAgentMessage(conversationId, trimmedContent, fileIds);
|
|
455
546
|
}
|
|
456
547
|
else {
|
|
457
548
|
// Use standard chat endpoint
|
|
458
|
-
response = await apiClient.current.sendMessage(
|
|
549
|
+
response = await apiClient.current.sendMessage(conversationId, trimmedContent, fileIds);
|
|
459
550
|
}
|
|
460
551
|
// Both endpoints now return ConversationMessage[] array (FULL conversation)
|
|
461
552
|
if (Array.isArray(response)) {
|
|
@@ -516,26 +607,9 @@ function useChat(options) {
|
|
|
516
607
|
}
|
|
517
608
|
}
|
|
518
609
|
catch (error) {
|
|
519
|
-
const
|
|
520
|
-
|
|
521
|
-
let userMessage = err.message;
|
|
522
|
-
// Handle specific error types
|
|
523
|
-
if (err.message.includes('Network') || err.message.includes('fetch')) {
|
|
524
|
-
userMessage = 'Unable to connect to the server. Please check your internet connection.';
|
|
525
|
-
}
|
|
526
|
-
else if (err.message.includes('401') || err.message.includes('Unauthorized')) {
|
|
527
|
-
userMessage = 'Authentication failed. Please refresh the page.';
|
|
528
|
-
}
|
|
529
|
-
else if (err.message.includes('500') || err.message.includes('Internal Server Error')) {
|
|
530
|
-
userMessage = 'The server encountered an error. Please try again later.';
|
|
531
|
-
}
|
|
532
|
-
else if (err.message.includes('timeout')) {
|
|
533
|
-
userMessage = 'Request timed out. Please try again.';
|
|
534
|
-
}
|
|
535
|
-
// Use fallback message if configured, otherwise use error message
|
|
610
|
+
const errorInfo = deriveErrorInfo(error);
|
|
611
|
+
const err = error instanceof Error ? error : new Error(errorInfo.message);
|
|
536
612
|
const fallbackMessage = state.config?.behavior?.fallbackMessage;
|
|
537
|
-
const errorMessage = fallbackMessage || userMessage;
|
|
538
|
-
// If fallback message is configured, add it as an assistant message
|
|
539
613
|
if (fallbackMessage) {
|
|
540
614
|
const fallbackAssistantMessage = {
|
|
541
615
|
id: generateMessageId(),
|
|
@@ -551,17 +625,16 @@ function useChat(options) {
|
|
|
551
625
|
messages: [...prev.messages, fallbackAssistantMessage],
|
|
552
626
|
isLoading: false,
|
|
553
627
|
isTyping: false,
|
|
554
|
-
error:
|
|
628
|
+
error: errorInfo.message,
|
|
555
629
|
}));
|
|
556
630
|
onMessage?.(fallbackAssistantMessage);
|
|
557
631
|
}
|
|
558
632
|
else {
|
|
559
|
-
// Show error message as assistant message for better UX
|
|
560
633
|
const errorAssistantMessage = {
|
|
561
634
|
id: generateMessageId(),
|
|
562
635
|
message: {
|
|
563
636
|
type: 'ai',
|
|
564
|
-
content: `⚠️ ${
|
|
637
|
+
content: `⚠️ ${errorInfo.message}`,
|
|
565
638
|
},
|
|
566
639
|
timestamp: new Date().toISOString(),
|
|
567
640
|
sources: [],
|
|
@@ -571,7 +644,7 @@ function useChat(options) {
|
|
|
571
644
|
messages: [...prev.messages, errorAssistantMessage],
|
|
572
645
|
isLoading: false,
|
|
573
646
|
isTyping: false,
|
|
574
|
-
error:
|
|
647
|
+
error: errorInfo.message,
|
|
575
648
|
}));
|
|
576
649
|
onMessage?.(errorAssistantMessage);
|
|
577
650
|
}
|
|
@@ -585,12 +658,12 @@ function useChat(options) {
|
|
|
585
658
|
setState(prev => ({
|
|
586
659
|
...prev,
|
|
587
660
|
messages: [],
|
|
588
|
-
conversationId:
|
|
661
|
+
conversationId: '',
|
|
589
662
|
error: null,
|
|
590
663
|
}));
|
|
591
664
|
const persistConversation = state.config?.behavior.persistConversation ?? true;
|
|
592
665
|
if (persistConversation && isStorageAvailable()) {
|
|
593
|
-
|
|
666
|
+
clearConversation(widgetId);
|
|
594
667
|
}
|
|
595
668
|
}, [widgetId, state.config?.behavior.persistConversation]);
|
|
596
669
|
/**
|
|
@@ -609,81 +682,12 @@ function useChat(options) {
|
|
|
609
682
|
}));
|
|
610
683
|
}
|
|
611
684
|
catch (error) {
|
|
612
|
-
const
|
|
685
|
+
const errorInfo = deriveErrorInfo(error);
|
|
686
|
+
const err = error instanceof Error ? error : new Error(errorInfo.message);
|
|
687
|
+
setState(prev => ({ ...prev, error: errorInfo.message }));
|
|
613
688
|
onError?.(err);
|
|
614
689
|
}
|
|
615
690
|
}, [state.conversationId, onError]);
|
|
616
|
-
/**
|
|
617
|
-
* Approve and execute pending action
|
|
618
|
-
*/
|
|
619
|
-
const approveAction = useCallback(async () => {
|
|
620
|
-
if (!pendingAction)
|
|
621
|
-
return;
|
|
622
|
-
setState(prev => ({ ...prev, isLoading: true, isTyping: true }));
|
|
623
|
-
try {
|
|
624
|
-
// Execute the client action
|
|
625
|
-
const actionResult = await executeClientAction(pendingAction.action);
|
|
626
|
-
// Update the pending action message to show execution result
|
|
627
|
-
const executionMessage = actionResult.success
|
|
628
|
-
? `✓ Action executed successfully`
|
|
629
|
-
: `✗ Action failed: ${actionResult.error}`;
|
|
630
|
-
setState(prev => ({
|
|
631
|
-
...prev,
|
|
632
|
-
messages: prev.messages.map(msg => msg.id === pendingAction.messageId
|
|
633
|
-
? { ...msg, message: { ...msg.message, content: `${msg.message.content}\n\n${executionMessage}` } }
|
|
634
|
-
: msg),
|
|
635
|
-
}));
|
|
636
|
-
// Clear pending action
|
|
637
|
-
setPendingAction(null);
|
|
638
|
-
// TODO: Implement continueAgent in API client if actions are needed
|
|
639
|
-
// For now, just show success message
|
|
640
|
-
const assistantMessage = {
|
|
641
|
-
id: generateMessageId(),
|
|
642
|
-
message: {
|
|
643
|
-
type: 'ai',
|
|
644
|
-
content: 'Action completed successfully.',
|
|
645
|
-
},
|
|
646
|
-
timestamp: new Date().toISOString(),
|
|
647
|
-
sources: [],
|
|
648
|
-
};
|
|
649
|
-
setState(prev => ({
|
|
650
|
-
...prev,
|
|
651
|
-
messages: [...prev.messages, assistantMessage],
|
|
652
|
-
isLoading: false,
|
|
653
|
-
isTyping: false,
|
|
654
|
-
}));
|
|
655
|
-
onMessage?.(assistantMessage);
|
|
656
|
-
}
|
|
657
|
-
catch (error) {
|
|
658
|
-
const err = error instanceof Error ? error : new Error('Failed to execute action');
|
|
659
|
-
setState(prev => ({
|
|
660
|
-
...prev,
|
|
661
|
-
isLoading: false,
|
|
662
|
-
isTyping: false,
|
|
663
|
-
error: err.message,
|
|
664
|
-
messages: prev.messages.map(msg => msg.id === pendingAction.messageId
|
|
665
|
-
? { ...msg, message: { ...msg.message, content: `${msg.message.content}\n\n✗ Failed to execute action` } }
|
|
666
|
-
: msg),
|
|
667
|
-
}));
|
|
668
|
-
setPendingAction(null);
|
|
669
|
-
onError?.(err);
|
|
670
|
-
}
|
|
671
|
-
}, [pendingAction, state.conversationId, onMessage, onError]);
|
|
672
|
-
/**
|
|
673
|
-
* Reject pending action
|
|
674
|
-
*/
|
|
675
|
-
const rejectAction = useCallback(() => {
|
|
676
|
-
if (!pendingAction)
|
|
677
|
-
return;
|
|
678
|
-
// Update message to show rejection
|
|
679
|
-
setState(prev => ({
|
|
680
|
-
...prev,
|
|
681
|
-
messages: prev.messages.map(msg => msg.id === pendingAction.messageId
|
|
682
|
-
? { ...msg, message: { ...msg.message, content: `${msg.message.content}\n\n✗ Action cancelled by user` } }
|
|
683
|
-
: msg),
|
|
684
|
-
}));
|
|
685
|
-
setPendingAction(null);
|
|
686
|
-
}, [pendingAction]);
|
|
687
691
|
return {
|
|
688
692
|
messages: state.messages,
|
|
689
693
|
isLoading: state.isLoading,
|
|
@@ -691,10 +695,7 @@ function useChat(options) {
|
|
|
691
695
|
error: state.error,
|
|
692
696
|
config: state.config,
|
|
693
697
|
conversationId: state.conversationId,
|
|
694
|
-
pendingAction,
|
|
695
698
|
sendMessage,
|
|
696
|
-
approveAction,
|
|
697
|
-
rejectAction,
|
|
698
699
|
clearMessages,
|
|
699
700
|
submitFeedback,
|
|
700
701
|
};
|
|
@@ -21609,7 +21610,7 @@ styleInject(css_248z);
|
|
|
21609
21610
|
|
|
21610
21611
|
const ChatWidget = ({ widgetId, apiKey, apiUrl = window.location.origin, position = 'bottom-right', theme: themeOverride, primaryColor, onOpen, onClose, onMessage, onError, }) => {
|
|
21611
21612
|
const [isOpen, setIsOpen] = useState(false);
|
|
21612
|
-
const { messages, isLoading, isTyping, error, config, sendMessage,
|
|
21613
|
+
const { messages, isLoading, isTyping, error, config, sendMessage, submitFeedback, } = useChat({
|
|
21613
21614
|
widgetId,
|
|
21614
21615
|
apiKey,
|
|
21615
21616
|
apiUrl,
|
|
@@ -21659,11 +21660,11 @@ const ChatWidget = ({ widgetId, apiKey, apiUrl = window.location.origin, positio
|
|
|
21659
21660
|
const handleFeedback = async (messageId, feedback) => {
|
|
21660
21661
|
await submitFeedback(messageId, feedback);
|
|
21661
21662
|
};
|
|
21662
|
-
return (jsx("div", { className: `ai-chat-widget ${effectiveTheme}`, style: customStyles, children: jsx("div", { className: `ai-chat-widget-container ${effectivePosition}`, children: isOpen ? (jsx(ChatWindow, { messages: messages, isLoading: isLoading, isTyping: isTyping, error: error, config: config, onSendMessage: sendMessage,
|
|
21663
|
+
return (jsx("div", { className: `ai-chat-widget ${effectiveTheme}`, style: customStyles, children: jsx("div", { className: `ai-chat-widget-container ${effectivePosition}`, children: isOpen ? (jsx(ChatWindow, { messages: messages, isLoading: isLoading, isTyping: isTyping, error: error, config: config, onSendMessage: sendMessage, onClose: handleToggle, onFeedback: handleFeedback })) : (jsx("button", { className: "ai-chat-button", onClick: handleToggle, "aria-label": "Open chat", style: {
|
|
21663
21664
|
width: config?.appearance.buttonSize || 60,
|
|
21664
21665
|
height: config?.appearance.buttonSize || 60,
|
|
21665
21666
|
}, children: config?.appearance.buttonIcon ? (jsx("span", { className: "ai-chat-button-icon", children: config.appearance.buttonIcon })) : (jsx("svg", { className: "ai-chat-button-svg", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: jsx("path", { d: "M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" }) })) })) }) }));
|
|
21666
21667
|
};
|
|
21667
21668
|
|
|
21668
|
-
export { ChatWidget, useChat };
|
|
21669
|
+
export { ApiError, ChatWidget, useChat };
|
|
21669
21670
|
//# sourceMappingURL=index.esm.js.map
|