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