@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.
@@ -44141,6 +44141,65 @@
44141
44141
  * API Client for Widget Communication
44142
44142
  * Handles all HTTP requests to the widget API
44143
44143
  */
44144
+ class ApiError extends Error {
44145
+ constructor(message, status, options) {
44146
+ super(message);
44147
+ this.name = 'ApiError';
44148
+ this.status = status;
44149
+ this.details = options?.details;
44150
+ this.retryAfterMs = options?.retryAfterMs;
44151
+ }
44152
+ }
44153
+ function parseRetryAfter(headerValue) {
44154
+ if (!headerValue)
44155
+ return undefined;
44156
+ const seconds = Number(headerValue);
44157
+ if (!Number.isNaN(seconds) && seconds >= 0) {
44158
+ return seconds * 1000;
44159
+ }
44160
+ const date = Date.parse(headerValue);
44161
+ if (!Number.isNaN(date)) {
44162
+ return Math.max(0, date - Date.now());
44163
+ }
44164
+ return undefined;
44165
+ }
44166
+ async function buildApiError(response, defaultMessage) {
44167
+ let parsedBody = null;
44168
+ let message = defaultMessage;
44169
+ try {
44170
+ const raw = await response.text();
44171
+ if (raw) {
44172
+ try {
44173
+ parsedBody = JSON.parse(raw);
44174
+ if (typeof parsedBody === 'object' && parsedBody !== null) {
44175
+ const body = parsedBody;
44176
+ message = typeof body.error === 'string'
44177
+ ? body.error
44178
+ : typeof body.message === 'string'
44179
+ ? body.message
44180
+ : defaultMessage;
44181
+ }
44182
+ else {
44183
+ message = raw;
44184
+ }
44185
+ }
44186
+ catch {
44187
+ message = raw;
44188
+ }
44189
+ }
44190
+ }
44191
+ catch {
44192
+ // ignore body parsing errors
44193
+ }
44194
+ let retryAfterMs = parseRetryAfter(response.headers.get('retry-after'));
44195
+ if (retryAfterMs === undefined) {
44196
+ retryAfterMs = parseRetryAfter(response.headers.get('x-ratelimit-reset'));
44197
+ }
44198
+ return new ApiError(message || defaultMessage, response.status, {
44199
+ details: parsedBody,
44200
+ retryAfterMs,
44201
+ });
44202
+ }
44144
44203
  class WidgetApiClient {
44145
44204
  constructor(config) {
44146
44205
  this.config = config;
@@ -44157,23 +44216,24 @@
44157
44216
  },
44158
44217
  });
44159
44218
  if (!response.ok) {
44160
- throw new Error(`Failed to fetch config: ${response.statusText}`);
44219
+ throw await buildApiError(response, 'Failed to fetch config');
44161
44220
  }
44162
44221
  const json = await response.json();
44163
44222
  return json;
44164
44223
  }
44165
44224
  async getOrCreateConversation(conversationId) {
44166
- const response = await fetch(`${this.config.apiUrl}/api/widget/${this.config.widgetId}/conversation/${conversationId || '0'}`, {
44225
+ const baseUrl = `${this.config.apiUrl}/api/widget/${this.config.widgetId}/conversation`;
44226
+ const query = conversationId ? `?conversationId=${encodeURIComponent(conversationId)}` : '';
44227
+ const response = await fetch(`${baseUrl}${query}`, {
44167
44228
  method: 'GET',
44168
44229
  headers: {
44169
44230
  'Authorization': `Bearer ${this.config.apiKey}`,
44170
- }
44231
+ },
44171
44232
  });
44172
44233
  if (!response.ok) {
44173
- const error = await response.json().catch(() => ({ error: response.statusText }));
44174
- throw new Error(error.error || 'Failed to upload file');
44234
+ throw await buildApiError(response, 'Failed to load conversation');
44175
44235
  }
44176
- return await response.json();
44236
+ return response.json();
44177
44237
  }
44178
44238
  /**
44179
44239
  * Upload a file
@@ -44190,8 +44250,7 @@
44190
44250
  body: formData,
44191
44251
  });
44192
44252
  if (!response.ok) {
44193
- const error = await response.json().catch(() => ({ error: response.statusText }));
44194
- throw new Error(error.error || 'Failed to upload file');
44253
+ throw await buildApiError(response, 'Failed to upload file');
44195
44254
  }
44196
44255
  const result = await response.json();
44197
44256
  return result.file;
@@ -44214,8 +44273,7 @@
44214
44273
  }),
44215
44274
  });
44216
44275
  if (!response.ok) {
44217
- const error = await response.json().catch(() => ({ error: response.statusText }));
44218
- throw new Error(error.error || 'Failed to send message');
44276
+ throw await buildApiError(response, 'Failed to send message');
44219
44277
  }
44220
44278
  return response.json();
44221
44279
  }
@@ -44237,8 +44295,7 @@
44237
44295
  }),
44238
44296
  });
44239
44297
  if (!response.ok) {
44240
- const error = await response.json().catch(() => ({ error: response.statusText }));
44241
- throw new Error(error.error || `Agent request failed with status ${response.status}`);
44298
+ throw await buildApiError(response, `Agent request failed with status ${response.status}`);
44242
44299
  }
44243
44300
  const data = await response.json();
44244
44301
  // Check if response indicates an error
@@ -44250,7 +44307,7 @@
44250
44307
  catch (error) {
44251
44308
  // Enhance error messages
44252
44309
  if (error instanceof TypeError && error.message.includes('fetch')) {
44253
- throw new Error('Network error: Unable to reach the server');
44310
+ throw new ApiError('Network error: Unable to reach the server', 0);
44254
44311
  }
44255
44312
  throw error;
44256
44313
  }
@@ -44272,13 +44329,13 @@
44272
44329
  }),
44273
44330
  });
44274
44331
  if (!response.ok) {
44275
- const errorData = await response.json().catch(() => ({ error: 'Unknown error' }));
44332
+ const apiError = await buildApiError(response, 'Failed to submit feedback');
44276
44333
  console.error('Feedback submission failed:', {
44277
44334
  status: response.status,
44278
- error: errorData,
44335
+ error: apiError.details,
44279
44336
  payload: { session_id: sessionId, message_id: messageId, feedback }
44280
44337
  });
44281
- throw new Error(errorData.error || 'Failed to submit feedback');
44338
+ throw apiError;
44282
44339
  }
44283
44340
  }
44284
44341
  /**
@@ -44386,111 +44443,70 @@
44386
44443
  }
44387
44444
 
44388
44445
  /**
44389
- * Client Action Executor
44390
- * Handles execution of client-side actions
44391
- */
44392
- /**
44393
- * Execute a client-side action
44446
+ * useChat Hook
44447
+ * Main state management for chat functionality
44394
44448
  */
44395
- async function executeClientAction(action) {
44396
- try {
44397
- console.log(`[ActionExecutor] Executing action: ${action.implementation}`, action.parameters);
44398
- switch (action.implementation) {
44399
- case 'redirect':
44400
- return executeRedirect(action.parameters);
44401
- case 'show_modal':
44402
- return executeShowModal(action.parameters);
44403
- case 'copy_to_clipboard':
44404
- return executeCopyToClipboard(action.parameters);
44405
- case 'open_chat':
44406
- return executeOpenChat(action.parameters);
44449
+ function deriveErrorInfo(error) {
44450
+ if (error instanceof ApiError) {
44451
+ const retryAfterSeconds = typeof error.retryAfterMs === 'number'
44452
+ ? Math.max(1, Math.ceil(error.retryAfterMs / 1000))
44453
+ : undefined;
44454
+ const lowerMessage = (error.message || '').toLowerCase();
44455
+ let message;
44456
+ switch (error.status) {
44457
+ case 429: {
44458
+ const isPerUser = lowerMessage.includes('user');
44459
+ const base = isPerUser
44460
+ ? 'You have reached the per-user rate limit.'
44461
+ : 'This widget has received too many requests.';
44462
+ if (retryAfterSeconds) {
44463
+ message = `${base} Please wait ${retryAfterSeconds} second${retryAfterSeconds === 1 ? '' : 's'} before trying again.`;
44464
+ }
44465
+ else {
44466
+ message = `${base} Please wait a moment and try again.`;
44467
+ }
44468
+ break;
44469
+ }
44470
+ case 401:
44471
+ message = 'Authentication failed. Please refresh the page or verify your API key.';
44472
+ break;
44473
+ case 403:
44474
+ message = 'Access to this widget is restricted. Please contact the site owner if you believe this is an error.';
44475
+ break;
44476
+ case 404:
44477
+ message = 'We could not find this widget. It may have been removed.';
44478
+ break;
44407
44479
  default:
44408
- console.warn(`[ActionExecutor] Unknown action: ${action.implementation}`);
44409
- return {
44410
- success: false,
44411
- error: `Unknown action type: ${action.implementation}`
44412
- };
44480
+ if (error.status >= 500) {
44481
+ message = 'The server encountered an error. Please try again shortly.';
44482
+ }
44483
+ else if (error.status > 0) {
44484
+ message = error.message || 'Something went wrong. Please try again.';
44485
+ }
44486
+ else {
44487
+ message = error.message || 'Unable to connect to the server. Please check your internet connection.';
44488
+ }
44413
44489
  }
44490
+ return { message, retryAfterSeconds, status: error.status };
44414
44491
  }
44415
- catch (error) {
44416
- console.error(`[ActionExecutor] Error executing action:`, error);
44417
- return {
44418
- success: false,
44419
- error: error instanceof Error ? error.message : 'Unknown error'
44420
- };
44421
- }
44422
- }
44423
- /**
44424
- * Redirect to a URL
44425
- */
44426
- function executeRedirect(params) {
44427
- const url = params.url;
44428
- if (!url) {
44429
- return { success: false, error: 'URL parameter is required' };
44430
- }
44431
- try {
44432
- // Validate URL
44433
- new URL(url);
44434
- // Redirect
44435
- window.location.href = url;
44436
- return { success: true, result: { redirected_to: url } };
44437
- }
44438
- catch (error) {
44439
- return { success: false, error: 'Invalid URL' };
44440
- }
44441
- }
44442
- /**
44443
- * Show a modal
44444
- */
44445
- function executeShowModal(params) {
44446
- const { title, content } = params;
44447
- if (!title || !content) {
44448
- return { success: false, error: 'Title and content are required' };
44449
- }
44450
- // Use browser's alert as a simple modal
44451
- // In a real implementation, you'd use a proper modal component
44452
- alert(`${title}\n\n${content}`);
44453
- return { success: true, result: { shown: true } };
44454
- }
44455
- /**
44456
- * Copy text to clipboard
44457
- */
44458
- async function executeCopyToClipboard(params) {
44459
- const text = params.text;
44460
- if (!text) {
44461
- return { success: false, error: 'Text parameter is required' };
44462
- }
44463
- try {
44464
- await navigator.clipboard.writeText(text);
44465
- return { success: true, result: { copied: text } };
44466
- }
44467
- catch (error) {
44468
- return {
44469
- success: false,
44470
- error: 'Failed to copy to clipboard. Please check browser permissions.'
44471
- };
44472
- }
44473
- }
44474
- /**
44475
- * Open chat with a specific message
44476
- */
44477
- function executeOpenChat(params) {
44478
- const message = params.message;
44479
- if (!message) {
44480
- return { success: false, error: 'Message parameter is required' };
44492
+ if (error instanceof Error) {
44493
+ const lower = error.message.toLowerCase();
44494
+ if (lower.includes('network')) {
44495
+ return { message: 'Unable to connect to the server. Please check your internet connection.' };
44496
+ }
44497
+ if (lower.includes('timeout')) {
44498
+ return { message: 'The request timed out. Please try again.' };
44499
+ }
44500
+ if (lower.includes('unauthorized') || lower.includes('401')) {
44501
+ return { message: 'Authentication failed. Please refresh the page or verify your API key.' };
44502
+ }
44503
+ if (lower.includes('internal server error') || lower.includes('500')) {
44504
+ return { message: 'The server encountered an error. Please try again shortly.' };
44505
+ }
44506
+ return { message: error.message || 'Something went wrong. Please try again.' };
44481
44507
  }
44482
- // Dispatch a custom event that the chat widget can listen to
44483
- const event = new CustomEvent('widget:open-chat', {
44484
- detail: { message }
44485
- });
44486
- window.dispatchEvent(event);
44487
- return { success: true, result: { message } };
44508
+ return { message: 'Something went wrong. Please try again.' };
44488
44509
  }
44489
-
44490
- /**
44491
- * useChat Hook
44492
- * Main state management for chat functionality
44493
- */
44494
44510
  function useChat(options) {
44495
44511
  const { widgetId, apiKey, apiUrl, onMessage, onError, } = options;
44496
44512
  const [state, setState] = reactExports.useState({
@@ -44502,7 +44518,6 @@
44502
44518
  conversationId: '', // Will be set after loading conversation
44503
44519
  config: null,
44504
44520
  });
44505
- const [pendingAction, setPendingAction] = reactExports.useState(null);
44506
44521
  const apiClient = reactExports.useRef(new WidgetApiClient({ widgetId, apiKey, apiUrl }));
44507
44522
  // Load configuration and conversation on mount
44508
44523
  reactExports.useEffect(() => {
@@ -44527,8 +44542,9 @@
44527
44542
  }));
44528
44543
  }
44529
44544
  catch (error) {
44530
- const err = error instanceof Error ? error : new Error('Failed to initialize');
44531
- setState(prev => ({ ...prev, error: err.message }));
44545
+ const errorInfo = deriveErrorInfo(error);
44546
+ const err = error instanceof Error ? error : new Error(errorInfo.message);
44547
+ setState(prev => ({ ...prev, error: errorInfo.message }));
44532
44548
  onError?.(err);
44533
44549
  }
44534
44550
  };
@@ -44583,7 +44599,6 @@
44583
44599
  }
44584
44600
  // Determine if widget has actions (use agent endpoint)
44585
44601
  const useAgent = state.config?.behavior.agentic || (state.config?.actions && state.config.actions.length > 0);
44586
- console.log(useAgent);
44587
44602
  let response;
44588
44603
  if (useAgent) {
44589
44604
  // Use agent endpoint - returns ConversationMessage[]
@@ -44652,26 +44667,9 @@
44652
44667
  }
44653
44668
  }
44654
44669
  catch (error) {
44655
- const err = error instanceof Error ? error : new Error('Failed to send message');
44656
- // Determine user-friendly error message
44657
- let userMessage = err.message;
44658
- // Handle specific error types
44659
- if (err.message.includes('Network') || err.message.includes('fetch')) {
44660
- userMessage = 'Unable to connect to the server. Please check your internet connection.';
44661
- }
44662
- else if (err.message.includes('401') || err.message.includes('Unauthorized')) {
44663
- userMessage = 'Authentication failed. Please refresh the page.';
44664
- }
44665
- else if (err.message.includes('500') || err.message.includes('Internal Server Error')) {
44666
- userMessage = 'The server encountered an error. Please try again later.';
44667
- }
44668
- else if (err.message.includes('timeout')) {
44669
- userMessage = 'Request timed out. Please try again.';
44670
- }
44671
- // Use fallback message if configured, otherwise use error message
44670
+ const errorInfo = deriveErrorInfo(error);
44671
+ const err = error instanceof Error ? error : new Error(errorInfo.message);
44672
44672
  const fallbackMessage = state.config?.behavior?.fallbackMessage;
44673
- const errorMessage = fallbackMessage || userMessage;
44674
- // If fallback message is configured, add it as an assistant message
44675
44673
  if (fallbackMessage) {
44676
44674
  const fallbackAssistantMessage = {
44677
44675
  id: generateMessageId(),
@@ -44687,17 +44685,16 @@
44687
44685
  messages: [...prev.messages, fallbackAssistantMessage],
44688
44686
  isLoading: false,
44689
44687
  isTyping: false,
44690
- error: null,
44688
+ error: errorInfo.message,
44691
44689
  }));
44692
44690
  onMessage?.(fallbackAssistantMessage);
44693
44691
  }
44694
44692
  else {
44695
- // Show error message as assistant message for better UX
44696
44693
  const errorAssistantMessage = {
44697
44694
  id: generateMessageId(),
44698
44695
  message: {
44699
44696
  type: 'ai',
44700
- content: `⚠️ ${errorMessage}`,
44697
+ content: `⚠️ ${errorInfo.message}`,
44701
44698
  },
44702
44699
  timestamp: new Date().toISOString(),
44703
44700
  sources: [],
@@ -44707,7 +44704,7 @@
44707
44704
  messages: [...prev.messages, errorAssistantMessage],
44708
44705
  isLoading: false,
44709
44706
  isTyping: false,
44710
- error: errorMessage,
44707
+ error: errorInfo.message,
44711
44708
  }));
44712
44709
  onMessage?.(errorAssistantMessage);
44713
44710
  }
@@ -44745,81 +44742,12 @@
44745
44742
  }));
44746
44743
  }
44747
44744
  catch (error) {
44748
- const err = error instanceof Error ? error : new Error('Failed to submit feedback');
44745
+ const errorInfo = deriveErrorInfo(error);
44746
+ const err = error instanceof Error ? error : new Error(errorInfo.message);
44747
+ setState(prev => ({ ...prev, error: errorInfo.message }));
44749
44748
  onError?.(err);
44750
44749
  }
44751
44750
  }, [state.conversationId, onError]);
44752
- /**
44753
- * Approve and execute pending action
44754
- */
44755
- const approveAction = reactExports.useCallback(async () => {
44756
- if (!pendingAction)
44757
- return;
44758
- setState(prev => ({ ...prev, isLoading: true, isTyping: true }));
44759
- try {
44760
- // Execute the client action
44761
- const actionResult = await executeClientAction(pendingAction.action);
44762
- // Update the pending action message to show execution result
44763
- const executionMessage = actionResult.success
44764
- ? `✓ Action executed successfully`
44765
- : `✗ Action failed: ${actionResult.error}`;
44766
- setState(prev => ({
44767
- ...prev,
44768
- messages: prev.messages.map(msg => msg.id === pendingAction.messageId
44769
- ? { ...msg, message: { ...msg.message, content: `${msg.message.content}\n\n${executionMessage}` } }
44770
- : msg),
44771
- }));
44772
- // Clear pending action
44773
- setPendingAction(null);
44774
- // TODO: Implement continueAgent in API client if actions are needed
44775
- // For now, just show success message
44776
- const assistantMessage = {
44777
- id: generateMessageId(),
44778
- message: {
44779
- type: 'ai',
44780
- content: 'Action completed successfully.',
44781
- },
44782
- timestamp: new Date().toISOString(),
44783
- sources: [],
44784
- };
44785
- setState(prev => ({
44786
- ...prev,
44787
- messages: [...prev.messages, assistantMessage],
44788
- isLoading: false,
44789
- isTyping: false,
44790
- }));
44791
- onMessage?.(assistantMessage);
44792
- }
44793
- catch (error) {
44794
- const err = error instanceof Error ? error : new Error('Failed to execute action');
44795
- setState(prev => ({
44796
- ...prev,
44797
- isLoading: false,
44798
- isTyping: false,
44799
- error: err.message,
44800
- messages: prev.messages.map(msg => msg.id === pendingAction.messageId
44801
- ? { ...msg, message: { ...msg.message, content: `${msg.message.content}\n\n✗ Failed to execute action` } }
44802
- : msg),
44803
- }));
44804
- setPendingAction(null);
44805
- onError?.(err);
44806
- }
44807
- }, [pendingAction, state.conversationId, onMessage, onError]);
44808
- /**
44809
- * Reject pending action
44810
- */
44811
- const rejectAction = reactExports.useCallback(() => {
44812
- if (!pendingAction)
44813
- return;
44814
- // Update message to show rejection
44815
- setState(prev => ({
44816
- ...prev,
44817
- messages: prev.messages.map(msg => msg.id === pendingAction.messageId
44818
- ? { ...msg, message: { ...msg.message, content: `${msg.message.content}\n\n✗ Action cancelled by user` } }
44819
- : msg),
44820
- }));
44821
- setPendingAction(null);
44822
- }, [pendingAction]);
44823
44751
  return {
44824
44752
  messages: state.messages,
44825
44753
  isLoading: state.isLoading,
@@ -44827,10 +44755,7 @@
44827
44755
  error: state.error,
44828
44756
  config: state.config,
44829
44757
  conversationId: state.conversationId,
44830
- pendingAction,
44831
44758
  sendMessage,
44832
- approveAction,
44833
- rejectAction,
44834
44759
  clearMessages,
44835
44760
  submitFeedback,
44836
44761
  };
@@ -65741,7 +65666,7 @@
65741
65666
 
65742
65667
  const ChatWidget = ({ widgetId, apiKey, apiUrl = window.location.origin, position = 'bottom-right', theme: themeOverride, primaryColor, onOpen, onClose, onMessage, onError, }) => {
65743
65668
  const [isOpen, setIsOpen] = reactExports.useState(false);
65744
- const { messages, isLoading, isTyping, error, config, sendMessage, approveAction, rejectAction, submitFeedback, } = useChat({
65669
+ const { messages, isLoading, isTyping, error, config, sendMessage, submitFeedback, } = useChat({
65745
65670
  widgetId,
65746
65671
  apiKey,
65747
65672
  apiUrl,
@@ -65791,7 +65716,7 @@
65791
65716
  const handleFeedback = async (messageId, feedback) => {
65792
65717
  await submitFeedback(messageId, feedback);
65793
65718
  };
65794
- return (jsxRuntimeExports.jsx("div", { className: `ai-chat-widget ${effectiveTheme}`, style: customStyles, children: jsxRuntimeExports.jsx("div", { className: `ai-chat-widget-container ${effectivePosition}`, children: isOpen ? (jsxRuntimeExports.jsx(ChatWindow, { messages: messages, isLoading: isLoading, isTyping: isTyping, error: error, config: config, onSendMessage: sendMessage, onApproveAction: approveAction, onRejectAction: rejectAction, onClose: handleToggle, onFeedback: handleFeedback })) : (jsxRuntimeExports.jsx("button", { className: "ai-chat-button", onClick: handleToggle, "aria-label": "Open chat", style: {
65719
+ return (jsxRuntimeExports.jsx("div", { className: `ai-chat-widget ${effectiveTheme}`, style: customStyles, children: jsxRuntimeExports.jsx("div", { className: `ai-chat-widget-container ${effectivePosition}`, children: isOpen ? (jsxRuntimeExports.jsx(ChatWindow, { messages: messages, isLoading: isLoading, isTyping: isTyping, error: error, config: config, onSendMessage: sendMessage, onClose: handleToggle, onFeedback: handleFeedback })) : (jsxRuntimeExports.jsx("button", { className: "ai-chat-button", onClick: handleToggle, "aria-label": "Open chat", style: {
65795
65720
  width: config?.appearance.buttonSize || 60,
65796
65721
  height: config?.appearance.buttonSize || 60,
65797
65722
  }, children: config?.appearance.buttonIcon ? (jsxRuntimeExports.jsx("span", { className: "ai-chat-button-icon", children: config.appearance.buttonIcon })) : (jsxRuntimeExports.jsx("svg", { className: "ai-chat-button-svg", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: jsxRuntimeExports.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" }) })) })) }) }));