@chatwidgetai/chat-widget 0.1.1 → 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.js CHANGED
@@ -69,6 +69,19 @@ async function buildApiError(response, defaultMessage) {
69
69
  class WidgetApiClient {
70
70
  constructor(config) {
71
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;
72
85
  }
73
86
  /**
74
87
  * Get widget configuration
@@ -89,7 +102,15 @@ class WidgetApiClient {
89
102
  }
90
103
  async getOrCreateConversation(conversationId) {
91
104
  const baseUrl = `${this.config.apiUrl}/api/widget/${this.config.widgetId}/conversation`;
92
- const query = conversationId ? `?conversationId=${encodeURIComponent(conversationId)}` : '';
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()}` : '';
93
114
  const response = await fetch(`${baseUrl}${query}`, {
94
115
  method: 'GET',
95
116
  headers: {
@@ -136,6 +157,7 @@ class WidgetApiClient {
136
157
  conversationId: conversationId,
137
158
  message,
138
159
  fileIds,
160
+ timeZone: this.getTimeZone(),
139
161
  }),
140
162
  });
141
163
  if (!response.ok) {
@@ -158,6 +180,7 @@ class WidgetApiClient {
158
180
  conversationId: conversationId,
159
181
  message,
160
182
  fileIds,
183
+ timeZone: this.getTimeZone(),
161
184
  }),
162
185
  });
163
186
  if (!response.ok) {
@@ -234,11 +257,6 @@ class WidgetApiClient {
234
257
  /**
235
258
  * Generate a unique session ID
236
259
  */
237
- function generateSessionId() {
238
- const timestamp = Date.now().toString(36);
239
- const randomStr = Math.random().toString(36).substring(2, 15);
240
- return `session_${timestamp}_${randomStr}`;
241
- }
242
260
  /**
243
261
  * Generate a unique message ID
244
262
  */
@@ -293,6 +311,18 @@ function loadConversation(widgetId) {
293
311
  return null;
294
312
  }
295
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
+ }
296
326
  /**
297
327
  * Check if localStorage is available
298
328
  */
@@ -385,29 +415,43 @@ function useChat(options) {
385
415
  config: null,
386
416
  });
387
417
  const apiClient = react.useRef(new WidgetApiClient({ widgetId, apiKey, apiUrl }));
388
- // Load configuration and conversation on mount
418
+ // Load configuration on mount and hydrate with existing conversation if available
389
419
  react.useEffect(() => {
420
+ let isMounted = true;
390
421
  const initialize = async () => {
391
422
  try {
392
- // Load config
393
423
  const config = await apiClient.current.getConfig();
394
- // Get or create conversation
395
424
  const persistConversation = config.behavior.persistConversation ?? true;
396
- let conversationId;
425
+ let conversationId = '';
426
+ let messages = [];
397
427
  if (persistConversation && isStorageAvailable()) {
398
428
  const stored = loadConversation(widgetId);
399
- conversationId = stored?.conversationId || stored?.sessionId; // Support old sessionId for backwards compatibility
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;
400
443
  }
401
- // Get conversation from backend
402
- const conversation = await apiClient.current.getOrCreateConversation(conversationId);
403
444
  setState(prev => ({
404
445
  ...prev,
405
446
  config,
406
- conversationId: conversation.id,
407
- messages: conversation.messages,
447
+ conversationId,
448
+ messages,
408
449
  }));
409
450
  }
410
451
  catch (error) {
452
+ if (!isMounted) {
453
+ return;
454
+ }
411
455
  const errorInfo = deriveErrorInfo(error);
412
456
  const err = error instanceof Error ? error : new Error(errorInfo.message);
413
457
  setState(prev => ({ ...prev, error: errorInfo.message }));
@@ -415,11 +459,17 @@ function useChat(options) {
415
459
  }
416
460
  };
417
461
  initialize();
462
+ return () => {
463
+ isMounted = false;
464
+ };
418
465
  }, [widgetId, apiKey, apiUrl, onError]);
419
466
  // Save conversation when messages change
420
467
  react.useEffect(() => {
421
468
  const persistConversation = state.config?.behavior.persistConversation ?? true;
422
- if (persistConversation && isStorageAvailable() && state.messages.length > 0) {
469
+ if (persistConversation &&
470
+ isStorageAvailable() &&
471
+ state.messages.length > 0 &&
472
+ state.conversationId) {
423
473
  saveConversation(widgetId, state.conversationId, state.messages);
424
474
  }
425
475
  }, [widgetId, state.messages, state.conversationId, state.config?.behavior.persistConversation]);
@@ -427,13 +477,15 @@ function useChat(options) {
427
477
  * Send a message
428
478
  */
429
479
  const sendMessage = react.useCallback(async (content, files) => {
430
- if (!content.trim() && (!files || files.length === 0))
480
+ const trimmedContent = content.trim();
481
+ const hasFiles = !!files && files.length > 0;
482
+ if (!trimmedContent && !hasFiles)
431
483
  return;
432
484
  const userMessage = {
433
485
  id: generateMessageId(),
434
486
  message: {
435
487
  type: 'human',
436
- content: content.trim(),
488
+ content: trimmedContent,
437
489
  },
438
490
  timestamp: new Date().toISOString(),
439
491
  sources: [],
@@ -448,13 +500,37 @@ function useChat(options) {
448
500
  }));
449
501
  onMessage?.(userMessage);
450
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
+ }
451
527
  // Upload files if provided
452
528
  let fileIds;
453
529
  if (files && files.length > 0) {
454
530
  fileIds = [];
455
531
  for (const file of files) {
456
532
  try {
457
- const uploadedFile = await apiClient.current.uploadFile(state.conversationId, file);
533
+ const uploadedFile = await apiClient.current.uploadFile(conversationId, file);
458
534
  fileIds.push(uploadedFile.id);
459
535
  }
460
536
  catch (uploadError) {
@@ -468,11 +544,11 @@ function useChat(options) {
468
544
  let response;
469
545
  if (useAgent) {
470
546
  // Use agent endpoint - returns ConversationMessage[]
471
- response = await apiClient.current.sendAgentMessage(state.conversationId, content.trim(), fileIds);
547
+ response = await apiClient.current.sendAgentMessage(conversationId, trimmedContent, fileIds);
472
548
  }
473
549
  else {
474
550
  // Use standard chat endpoint
475
- response = await apiClient.current.sendMessage(state.conversationId, content.trim(), fileIds);
551
+ response = await apiClient.current.sendMessage(conversationId, trimmedContent, fileIds);
476
552
  }
477
553
  // Both endpoints now return ConversationMessage[] array (FULL conversation)
478
554
  if (Array.isArray(response)) {
@@ -584,12 +660,12 @@ function useChat(options) {
584
660
  setState(prev => ({
585
661
  ...prev,
586
662
  messages: [],
587
- conversationId: generateSessionId(),
663
+ conversationId: '',
588
664
  error: null,
589
665
  }));
590
666
  const persistConversation = state.config?.behavior.persistConversation ?? true;
591
667
  if (persistConversation && isStorageAvailable()) {
592
- saveConversation(widgetId, generateSessionId(), []);
668
+ clearConversation(widgetId);
593
669
  }
594
670
  }, [widgetId, state.config?.behavior.persistConversation]);
595
671
  /**