@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/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 new Error(`Failed to fetch config: ${response.statusText}`);
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 response = await fetch(`${this.config.apiUrl}/api/widget/${this.config.widgetId}/conversation/${conversationId || '0'}`, {
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
- const error = await response.json().catch(() => ({ error: response.statusText }));
38
- throw new Error(error.error || 'Failed to upload file');
119
+ throw await buildApiError(response, 'Failed to load conversation');
39
120
  }
40
- return await response.json();
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
- const error = await response.json().catch(() => ({ error: response.statusText }));
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
- const error = await response.json().catch(() => ({ error: response.statusText }));
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
- const error = await response.json().catch(() => ({ error: response.statusText }));
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 Error('Network error: Unable to reach the server');
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 errorData = await response.json().catch(() => ({ error: 'Unknown error' }));
219
+ const apiError = await buildApiError(response, 'Failed to submit feedback');
140
220
  console.error('Feedback submission failed:', {
141
221
  status: response.status,
142
- error: errorData,
222
+ error: apiError.details,
143
223
  payload: { session_id: sessionId, message_id: messageId, feedback }
144
224
  });
145
- throw new Error(errorData.error || 'Failed to submit feedback');
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
- * Client Action Executor
254
- * Handles execution of client-side actions
255
- */
256
- /**
257
- * Execute a client-side action
340
+ * useChat Hook
341
+ * Main state management for chat functionality
258
342
  */
259
- async function executeClientAction(action) {
260
- try {
261
- console.log(`[ActionExecutor] Executing action: ${action.implementation}`, action.parameters);
262
- switch (action.implementation) {
263
- case 'redirect':
264
- return executeRedirect(action.parameters);
265
- case 'show_modal':
266
- return executeShowModal(action.parameters);
267
- case 'copy_to_clipboard':
268
- return executeCopyToClipboard(action.parameters);
269
- case 'open_chat':
270
- return executeOpenChat(action.parameters);
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
- console.warn(`[ActionExecutor] Unknown action: ${action.implementation}`);
273
- return {
274
- success: false,
275
- error: `Unknown action type: ${action.implementation}`
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
- catch (error) {
280
- console.error(`[ActionExecutor] Error executing action:`, error);
281
- return {
282
- success: false,
283
- error: error instanceof Error ? error.message : 'Unknown error'
284
- };
285
- }
286
- }
287
- /**
288
- * Redirect to a URL
289
- */
290
- function executeRedirect(params) {
291
- const url = params.url;
292
- if (!url) {
293
- return { success: false, error: 'URL parameter is required' };
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
- // Dispatch a custom event that the chat widget can listen to
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 on mount
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
- conversationId = stored?.conversationId || stored?.sessionId; // Support old sessionId for backwards compatibility
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: conversation.id,
390
- messages: conversation.messages,
445
+ conversationId,
446
+ messages,
391
447
  }));
392
448
  }
393
449
  catch (error) {
394
- const err = error instanceof Error ? error : new Error('Failed to initialize');
395
- setState(prev => ({ ...prev, error: err.message }));
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 && isStorageAvailable() && state.messages.length > 0) {
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
- if (!content.trim() && (!files || files.length === 0))
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: content.trim(),
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(state.conversationId, file);
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(state.conversationId, content.trim(), fileIds);
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(state.conversationId, content.trim(), fileIds);
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 err = error instanceof Error ? error : new Error('Failed to send message');
520
- // Determine user-friendly error message
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: null,
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: `⚠️ ${errorMessage}`,
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: errorMessage,
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: generateSessionId(),
661
+ conversationId: '',
589
662
  error: null,
590
663
  }));
591
664
  const persistConversation = state.config?.behavior.persistConversation ?? true;
592
665
  if (persistConversation && isStorageAvailable()) {
593
- saveConversation(widgetId, generateSessionId(), []);
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 err = error instanceof Error ? error : new Error('Failed to submit feedback');
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, approveAction, rejectAction, submitFeedback, } = useChat({
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, onApproveAction: approveAction, onRejectAction: rejectAction, onClose: handleToggle, onFeedback: handleFeedback })) : (jsx("button", { className: "ai-chat-button", onClick: handleToggle, "aria-label": "Open chat", style: {
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