@chatwidgetai/chat-widget 0.1.0 → 0.1.1

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.js CHANGED
@@ -7,6 +7,65 @@ 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;
@@ -23,23 +82,24 @@ class WidgetApiClient {
23
82
  },
24
83
  });
25
84
  if (!response.ok) {
26
- throw new Error(`Failed to fetch config: ${response.statusText}`);
85
+ throw await buildApiError(response, 'Failed to fetch config');
27
86
  }
28
87
  const json = await response.json();
29
88
  return json;
30
89
  }
31
90
  async getOrCreateConversation(conversationId) {
32
- const response = await fetch(`${this.config.apiUrl}/api/widget/${this.config.widgetId}/conversation/${conversationId || '0'}`, {
91
+ const baseUrl = `${this.config.apiUrl}/api/widget/${this.config.widgetId}/conversation`;
92
+ const query = conversationId ? `?conversationId=${encodeURIComponent(conversationId)}` : '';
93
+ const response = await fetch(`${baseUrl}${query}`, {
33
94
  method: 'GET',
34
95
  headers: {
35
96
  'Authorization': `Bearer ${this.config.apiKey}`,
36
- }
97
+ },
37
98
  });
38
99
  if (!response.ok) {
39
- const error = await response.json().catch(() => ({ error: response.statusText }));
40
- throw new Error(error.error || 'Failed to upload file');
100
+ throw await buildApiError(response, 'Failed to load conversation');
41
101
  }
42
- return await response.json();
102
+ return response.json();
43
103
  }
44
104
  /**
45
105
  * Upload a file
@@ -56,8 +116,7 @@ class WidgetApiClient {
56
116
  body: formData,
57
117
  });
58
118
  if (!response.ok) {
59
- const error = await response.json().catch(() => ({ error: response.statusText }));
60
- throw new Error(error.error || 'Failed to upload file');
119
+ throw await buildApiError(response, 'Failed to upload file');
61
120
  }
62
121
  const result = await response.json();
63
122
  return result.file;
@@ -80,8 +139,7 @@ class WidgetApiClient {
80
139
  }),
81
140
  });
82
141
  if (!response.ok) {
83
- const error = await response.json().catch(() => ({ error: response.statusText }));
84
- throw new Error(error.error || 'Failed to send message');
142
+ throw await buildApiError(response, 'Failed to send message');
85
143
  }
86
144
  return response.json();
87
145
  }
@@ -103,8 +161,7 @@ class WidgetApiClient {
103
161
  }),
104
162
  });
105
163
  if (!response.ok) {
106
- const error = await response.json().catch(() => ({ error: response.statusText }));
107
- throw new Error(error.error || `Agent request failed with status ${response.status}`);
164
+ throw await buildApiError(response, `Agent request failed with status ${response.status}`);
108
165
  }
109
166
  const data = await response.json();
110
167
  // Check if response indicates an error
@@ -116,7 +173,7 @@ class WidgetApiClient {
116
173
  catch (error) {
117
174
  // Enhance error messages
118
175
  if (error instanceof TypeError && error.message.includes('fetch')) {
119
- throw new Error('Network error: Unable to reach the server');
176
+ throw new ApiError('Network error: Unable to reach the server', 0);
120
177
  }
121
178
  throw error;
122
179
  }
@@ -138,13 +195,13 @@ class WidgetApiClient {
138
195
  }),
139
196
  });
140
197
  if (!response.ok) {
141
- const errorData = await response.json().catch(() => ({ error: 'Unknown error' }));
198
+ const apiError = await buildApiError(response, 'Failed to submit feedback');
142
199
  console.error('Feedback submission failed:', {
143
200
  status: response.status,
144
- error: errorData,
201
+ error: apiError.details,
145
202
  payload: { session_id: sessionId, message_id: messageId, feedback }
146
203
  });
147
- throw new Error(errorData.error || 'Failed to submit feedback');
204
+ throw apiError;
148
205
  }
149
206
  }
150
207
  /**
@@ -252,111 +309,70 @@ function isStorageAvailable() {
252
309
  }
253
310
 
254
311
  /**
255
- * Client Action Executor
256
- * Handles execution of client-side actions
257
- */
258
- /**
259
- * Execute a client-side action
312
+ * useChat Hook
313
+ * Main state management for chat functionality
260
314
  */
261
- async function executeClientAction(action) {
262
- try {
263
- console.log(`[ActionExecutor] Executing action: ${action.implementation}`, action.parameters);
264
- switch (action.implementation) {
265
- case 'redirect':
266
- return executeRedirect(action.parameters);
267
- case 'show_modal':
268
- return executeShowModal(action.parameters);
269
- case 'copy_to_clipboard':
270
- return executeCopyToClipboard(action.parameters);
271
- case 'open_chat':
272
- return executeOpenChat(action.parameters);
315
+ function deriveErrorInfo(error) {
316
+ if (error instanceof ApiError) {
317
+ const retryAfterSeconds = typeof error.retryAfterMs === 'number'
318
+ ? Math.max(1, Math.ceil(error.retryAfterMs / 1000))
319
+ : undefined;
320
+ const lowerMessage = (error.message || '').toLowerCase();
321
+ let message;
322
+ switch (error.status) {
323
+ case 429: {
324
+ const isPerUser = lowerMessage.includes('user');
325
+ const base = isPerUser
326
+ ? 'You have reached the per-user rate limit.'
327
+ : 'This widget has received too many requests.';
328
+ if (retryAfterSeconds) {
329
+ message = `${base} Please wait ${retryAfterSeconds} second${retryAfterSeconds === 1 ? '' : 's'} before trying again.`;
330
+ }
331
+ else {
332
+ message = `${base} Please wait a moment and try again.`;
333
+ }
334
+ break;
335
+ }
336
+ case 401:
337
+ message = 'Authentication failed. Please refresh the page or verify your API key.';
338
+ break;
339
+ case 403:
340
+ message = 'Access to this widget is restricted. Please contact the site owner if you believe this is an error.';
341
+ break;
342
+ case 404:
343
+ message = 'We could not find this widget. It may have been removed.';
344
+ break;
273
345
  default:
274
- console.warn(`[ActionExecutor] Unknown action: ${action.implementation}`);
275
- return {
276
- success: false,
277
- error: `Unknown action type: ${action.implementation}`
278
- };
346
+ if (error.status >= 500) {
347
+ message = 'The server encountered an error. Please try again shortly.';
348
+ }
349
+ else if (error.status > 0) {
350
+ message = error.message || 'Something went wrong. Please try again.';
351
+ }
352
+ else {
353
+ message = error.message || 'Unable to connect to the server. Please check your internet connection.';
354
+ }
279
355
  }
356
+ return { message, retryAfterSeconds, status: error.status };
280
357
  }
281
- catch (error) {
282
- console.error(`[ActionExecutor] Error executing action:`, error);
283
- return {
284
- success: false,
285
- error: error instanceof Error ? error.message : 'Unknown error'
286
- };
287
- }
288
- }
289
- /**
290
- * Redirect to a URL
291
- */
292
- function executeRedirect(params) {
293
- const url = params.url;
294
- if (!url) {
295
- return { success: false, error: 'URL parameter is required' };
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' };
358
+ if (error instanceof Error) {
359
+ const lower = error.message.toLowerCase();
360
+ if (lower.includes('network')) {
361
+ return { message: 'Unable to connect to the server. Please check your internet connection.' };
362
+ }
363
+ if (lower.includes('timeout')) {
364
+ return { message: 'The request timed out. Please try again.' };
365
+ }
366
+ if (lower.includes('unauthorized') || lower.includes('401')) {
367
+ return { message: 'Authentication failed. Please refresh the page or verify your API key.' };
368
+ }
369
+ if (lower.includes('internal server error') || lower.includes('500')) {
370
+ return { message: 'The server encountered an error. Please try again shortly.' };
371
+ }
372
+ return { message: error.message || 'Something went wrong. Please try again.' };
347
373
  }
348
- // Dispatch a custom event that the chat widget can listen to
349
- const event = new CustomEvent('widget:open-chat', {
350
- detail: { message }
351
- });
352
- window.dispatchEvent(event);
353
- return { success: true, result: { message } };
374
+ return { message: 'Something went wrong. Please try again.' };
354
375
  }
355
-
356
- /**
357
- * useChat Hook
358
- * Main state management for chat functionality
359
- */
360
376
  function useChat(options) {
361
377
  const { widgetId, apiKey, apiUrl, onMessage, onError, } = options;
362
378
  const [state, setState] = react.useState({
@@ -368,7 +384,6 @@ function useChat(options) {
368
384
  conversationId: '', // Will be set after loading conversation
369
385
  config: null,
370
386
  });
371
- const [pendingAction, setPendingAction] = react.useState(null);
372
387
  const apiClient = react.useRef(new WidgetApiClient({ widgetId, apiKey, apiUrl }));
373
388
  // Load configuration and conversation on mount
374
389
  react.useEffect(() => {
@@ -393,8 +408,9 @@ function useChat(options) {
393
408
  }));
394
409
  }
395
410
  catch (error) {
396
- const err = error instanceof Error ? error : new Error('Failed to initialize');
397
- setState(prev => ({ ...prev, error: err.message }));
411
+ const errorInfo = deriveErrorInfo(error);
412
+ const err = error instanceof Error ? error : new Error(errorInfo.message);
413
+ setState(prev => ({ ...prev, error: errorInfo.message }));
398
414
  onError?.(err);
399
415
  }
400
416
  };
@@ -449,7 +465,6 @@ function useChat(options) {
449
465
  }
450
466
  // Determine if widget has actions (use agent endpoint)
451
467
  const useAgent = state.config?.behavior.agentic || (state.config?.actions && state.config.actions.length > 0);
452
- console.log(useAgent);
453
468
  let response;
454
469
  if (useAgent) {
455
470
  // Use agent endpoint - returns ConversationMessage[]
@@ -518,26 +533,9 @@ function useChat(options) {
518
533
  }
519
534
  }
520
535
  catch (error) {
521
- const err = error instanceof Error ? error : new Error('Failed to send message');
522
- // Determine user-friendly error message
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
536
+ const errorInfo = deriveErrorInfo(error);
537
+ const err = error instanceof Error ? error : new Error(errorInfo.message);
538
538
  const fallbackMessage = state.config?.behavior?.fallbackMessage;
539
- const errorMessage = fallbackMessage || userMessage;
540
- // If fallback message is configured, add it as an assistant message
541
539
  if (fallbackMessage) {
542
540
  const fallbackAssistantMessage = {
543
541
  id: generateMessageId(),
@@ -553,17 +551,16 @@ function useChat(options) {
553
551
  messages: [...prev.messages, fallbackAssistantMessage],
554
552
  isLoading: false,
555
553
  isTyping: false,
556
- error: null,
554
+ error: errorInfo.message,
557
555
  }));
558
556
  onMessage?.(fallbackAssistantMessage);
559
557
  }
560
558
  else {
561
- // Show error message as assistant message for better UX
562
559
  const errorAssistantMessage = {
563
560
  id: generateMessageId(),
564
561
  message: {
565
562
  type: 'ai',
566
- content: `⚠️ ${errorMessage}`,
563
+ content: `⚠️ ${errorInfo.message}`,
567
564
  },
568
565
  timestamp: new Date().toISOString(),
569
566
  sources: [],
@@ -573,7 +570,7 @@ function useChat(options) {
573
570
  messages: [...prev.messages, errorAssistantMessage],
574
571
  isLoading: false,
575
572
  isTyping: false,
576
- error: errorMessage,
573
+ error: errorInfo.message,
577
574
  }));
578
575
  onMessage?.(errorAssistantMessage);
579
576
  }
@@ -611,81 +608,12 @@ function useChat(options) {
611
608
  }));
612
609
  }
613
610
  catch (error) {
614
- const err = error instanceof Error ? error : new Error('Failed to submit feedback');
611
+ const errorInfo = deriveErrorInfo(error);
612
+ const err = error instanceof Error ? error : new Error(errorInfo.message);
613
+ setState(prev => ({ ...prev, error: errorInfo.message }));
615
614
  onError?.(err);
616
615
  }
617
616
  }, [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
617
  return {
690
618
  messages: state.messages,
691
619
  isLoading: state.isLoading,
@@ -693,10 +621,7 @@ function useChat(options) {
693
621
  error: state.error,
694
622
  config: state.config,
695
623
  conversationId: state.conversationId,
696
- pendingAction,
697
624
  sendMessage,
698
- approveAction,
699
- rejectAction,
700
625
  clearMessages,
701
626
  submitFeedback,
702
627
  };
@@ -21611,7 +21536,7 @@ styleInject(css_248z);
21611
21536
 
21612
21537
  const ChatWidget = ({ widgetId, apiKey, apiUrl = window.location.origin, position = 'bottom-right', theme: themeOverride, primaryColor, onOpen, onClose, onMessage, onError, }) => {
21613
21538
  const [isOpen, setIsOpen] = react.useState(false);
21614
- const { messages, isLoading, isTyping, error, config, sendMessage, approveAction, rejectAction, submitFeedback, } = useChat({
21539
+ const { messages, isLoading, isTyping, error, config, sendMessage, submitFeedback, } = useChat({
21615
21540
  widgetId,
21616
21541
  apiKey,
21617
21542
  apiUrl,
@@ -21661,12 +21586,13 @@ const ChatWidget = ({ widgetId, apiKey, apiUrl = window.location.origin, positio
21661
21586
  const handleFeedback = async (messageId, feedback) => {
21662
21587
  await submitFeedback(messageId, feedback);
21663
21588
  };
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, onApproveAction: approveAction, onRejectAction: rejectAction, onClose: handleToggle, onFeedback: handleFeedback })) : (jsxRuntime.jsx("button", { className: "ai-chat-button", onClick: handleToggle, "aria-label": "Open chat", style: {
21589
+ 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
21590
  width: config?.appearance.buttonSize || 60,
21666
21591
  height: config?.appearance.buttonSize || 60,
21667
21592
  }, 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
21593
  };
21669
21594
 
21595
+ exports.ApiError = ApiError;
21670
21596
  exports.ChatWidget = ChatWidget;
21671
21597
  exports.useChat = useChat;
21672
21598
  //# sourceMappingURL=index.js.map