@memori.ai/memori-react 8.6.6 → 8.7.0

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.
Files changed (81) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/dist/components/Header/Header.css +24 -0
  3. package/dist/components/Header/Header.d.ts +4 -0
  4. package/dist/components/Header/Header.js +52 -2
  5. package/dist/components/Header/Header.js.map +1 -1
  6. package/dist/components/LoginDrawer/LoginDrawer.css +1372 -238
  7. package/dist/components/MemoriWidget/MemoriWidget.js +42 -25
  8. package/dist/components/MemoriWidget/MemoriWidget.js.map +1 -1
  9. package/dist/components/icons/Logout.d.ts +6 -0
  10. package/dist/components/icons/Logout.js +6 -0
  11. package/dist/components/icons/Logout.js.map +1 -0
  12. package/dist/components/layouts/HiddenChat.js +3 -1
  13. package/dist/components/layouts/HiddenChat.js.map +1 -1
  14. package/dist/components/layouts/hidden-chat.css +4 -0
  15. package/dist/components/ui/Dropdown.css +173 -0
  16. package/dist/components/ui/Dropdown.d.ts +11 -0
  17. package/dist/components/ui/Dropdown.js +33 -0
  18. package/dist/components/ui/Dropdown.js.map +1 -0
  19. package/dist/helpers/error.js +3 -0
  20. package/dist/helpers/error.js.map +1 -1
  21. package/dist/helpers/stt/useSTT.js +0 -56
  22. package/dist/helpers/stt/useSTT.js.map +1 -1
  23. package/dist/helpers/tts/useTTS.js +19 -8
  24. package/dist/helpers/tts/useTTS.js.map +1 -1
  25. package/dist/locales/en.json +48 -1
  26. package/dist/locales/es.json +30 -0
  27. package/dist/locales/fr.json +30 -0
  28. package/dist/locales/it.json +44 -0
  29. package/esm/components/Header/Header.css +24 -0
  30. package/esm/components/Header/Header.d.ts +4 -0
  31. package/esm/components/Header/Header.js +53 -3
  32. package/esm/components/Header/Header.js.map +1 -1
  33. package/esm/components/LoginDrawer/LoginDrawer.css +1372 -238
  34. package/esm/components/MemoriWidget/MemoriWidget.js +42 -25
  35. package/esm/components/MemoriWidget/MemoriWidget.js.map +1 -1
  36. package/esm/components/icons/Logout.d.ts +6 -0
  37. package/esm/components/icons/Logout.js +4 -0
  38. package/esm/components/icons/Logout.js.map +1 -0
  39. package/esm/components/layouts/HiddenChat.js +3 -1
  40. package/esm/components/layouts/HiddenChat.js.map +1 -1
  41. package/esm/components/layouts/hidden-chat.css +4 -0
  42. package/esm/components/ui/Dropdown.css +173 -0
  43. package/esm/components/ui/Dropdown.d.ts +11 -0
  44. package/esm/components/ui/Dropdown.js +30 -0
  45. package/esm/components/ui/Dropdown.js.map +1 -0
  46. package/esm/helpers/error.js +3 -0
  47. package/esm/helpers/error.js.map +1 -1
  48. package/esm/helpers/stt/useSTT.js +0 -56
  49. package/esm/helpers/stt/useSTT.js.map +1 -1
  50. package/esm/helpers/tts/useTTS.js +19 -8
  51. package/esm/helpers/tts/useTTS.js.map +1 -1
  52. package/esm/locales/en.json +48 -1
  53. package/esm/locales/es.json +30 -0
  54. package/esm/locales/fr.json +30 -0
  55. package/esm/locales/it.json +44 -0
  56. package/package.json +2 -2
  57. package/src/__snapshots__/index.test.tsx.snap +19 -0
  58. package/src/components/Header/Header.css +24 -0
  59. package/src/components/Header/Header.test.tsx +15 -1
  60. package/src/components/Header/Header.tsx +147 -10
  61. package/src/components/Header/__snapshots__/Header.test.tsx.snap +53 -37
  62. package/src/components/LoginDrawer/LoginDrawer.css +1372 -238
  63. package/src/components/LoginDrawer/LoginDrawer.test.tsx +12 -1
  64. package/src/components/MemoriWidget/MemoriWidget.tsx +102 -60
  65. package/src/components/icons/Logout.tsx +27 -0
  66. package/src/components/layouts/HiddenChat.tsx +3 -1
  67. package/src/components/layouts/__snapshots__/HiddenChat.test.tsx.snap +1 -1
  68. package/src/components/layouts/hidden-chat.css +4 -0
  69. package/src/components/ui/Dropdown.css +173 -0
  70. package/src/components/ui/Dropdown.tsx +63 -0
  71. package/src/helpers/error.ts +3 -0
  72. package/src/helpers/stt/useSTT.ts +1 -50
  73. package/src/helpers/tts/useTTS.ts +34 -18
  74. package/src/index.stories.tsx +1 -0
  75. package/src/index.test.tsx +17 -0
  76. package/src/locales/en.json +48 -1
  77. package/src/locales/es.json +30 -0
  78. package/src/locales/fr.json +30 -0
  79. package/src/locales/it.json +44 -0
  80. package/src/components/AccountForm/AccountForm.test.tsx +0 -27
  81. package/src/components/AccountForm/__snapshots__/AccountForm.test.tsx.snap +0 -202
@@ -0,0 +1,173 @@
1
+ .memori-dropdown {
2
+ position: relative;
3
+ display: inline-block;
4
+ }
5
+
6
+ .memori-dropdown--trigger {
7
+ cursor: pointer;
8
+ }
9
+
10
+ .memori-dropdown--content {
11
+ position: absolute;
12
+ z-index: 10001;
13
+ overflow: hidden;
14
+ min-width: 280px;
15
+ max-width: 320px;
16
+ border: 1px solid rgba(0, 0, 0, 0.08);
17
+ border-radius: 12px;
18
+ animation: dropdownFadeIn 0.2s ease-out;
19
+ background: white;
20
+ box-shadow: 0 10px 25px rgba(0, 0, 0, 0.15), 0 4px 10px rgba(0, 0, 0, 0.1);
21
+ }
22
+
23
+ @keyframes dropdownFadeIn {
24
+ from {
25
+ opacity: 0;
26
+ transform: translateY(-8px) scale(0.95);
27
+ }
28
+ to {
29
+ opacity: 1;
30
+ transform: translateY(0) scale(1);
31
+ }
32
+ }
33
+
34
+ /* Placement variants */
35
+ .memori-dropdown--content--bottom-right {
36
+ top: 100%;
37
+ right: 0;
38
+ margin-top: 8px;
39
+ }
40
+
41
+ .memori-dropdown--content--bottom-left {
42
+ top: 100%;
43
+ left: 0;
44
+ margin-top: 8px;
45
+ }
46
+
47
+ .memori-dropdown--content--top-right {
48
+ right: 0;
49
+ bottom: 100%;
50
+ margin-bottom: 8px;
51
+ }
52
+
53
+ .memori-dropdown--content--top-left {
54
+ bottom: 100%;
55
+ left: 0;
56
+ margin-bottom: 8px;
57
+ }
58
+
59
+ /* User profile dropdown specific styles */
60
+ .memori-dropdown--user-profile {
61
+ padding: 16px;
62
+ /* border-bottom: 1px solid #f1f5f9; */
63
+ }
64
+
65
+ .memori-dropdown--user-info {
66
+ display: flex;
67
+ align-items: center;
68
+ gap: 12px;
69
+ }
70
+
71
+ .memori-dropdown--avatar {
72
+ width: 48px;
73
+ height: 48px;
74
+ border: 2px solid #e2e8f0;
75
+ border-radius: 50%;
76
+ object-fit: cover;
77
+ }
78
+
79
+ .memori-dropdown--avatar-placeholder {
80
+ display: flex;
81
+ width: 40px;
82
+ height: 40px;
83
+ align-items: center;
84
+ justify-content: center;
85
+ border: 2px solid #e2e8f0;
86
+ border-radius: 50%;
87
+ background: var(--memori-primary);
88
+ color: white;
89
+ font-size: 16px;
90
+ font-weight: 600;
91
+ }
92
+
93
+ .memori-dropdown--user-details {
94
+ min-width: 0;
95
+ flex: 1;
96
+ }
97
+
98
+ .memori-dropdown--user-name {
99
+ overflow: hidden;
100
+ margin: 0;
101
+ color: #1e293b;
102
+ font-size: 14px;
103
+ font-weight: 600;
104
+ text-align: start;
105
+ text-overflow: ellipsis;
106
+ white-space: nowrap;
107
+ }
108
+
109
+ .memori-dropdown--user-email {
110
+ overflow: hidden;
111
+ margin: 2px 0 0 0;
112
+ color: #64748b;
113
+ font-size: 12px;
114
+ text-align: start;
115
+ text-overflow: ellipsis;
116
+ white-space: nowrap;
117
+ }
118
+
119
+ .memori-dropdown--user-badge {
120
+ border-radius: 12px;
121
+ margin-top: 6px;
122
+ color: rgb(119, 119, 119);
123
+ font-size: 11px;
124
+ font-weight: 500;
125
+ text-align: start;
126
+ }
127
+
128
+ .memori-dropdown--actions {
129
+ padding: 8px 0;
130
+ }
131
+
132
+ .memori-dropdown--action-button {
133
+ display: flex;
134
+ width: 100%;
135
+ align-items: center;
136
+ padding: 12px 16px;
137
+ border: none;
138
+ background: none;
139
+ color: #374151;
140
+ cursor: pointer;
141
+ font-size: 14px;
142
+ gap: 8px;
143
+ text-align: left;
144
+ transition: background-color 0.2s ease;
145
+ }
146
+
147
+ .memori-dropdown--action-button:hover {
148
+ background: #f8fafc;
149
+ }
150
+
151
+ .memori-dropdown--action-button--logout {
152
+ border-top: 1px solid #f1f5f9;
153
+ margin-top: 4px;
154
+ color: #dc2626;
155
+ }
156
+
157
+ .memori-dropdown--action-button--logout:hover {
158
+ background: #fef2f2;
159
+ }
160
+
161
+ .memori-dropdown--action-icon {
162
+ width: 16px;
163
+ height: 16px;
164
+ flex-shrink: 0;
165
+ }
166
+
167
+ /* Responsive adjustments */
168
+ @media (max-width: 640px) {
169
+ .memori-dropdown--content {
170
+ min-width: 260px;
171
+ max-width: 280px;
172
+ }
173
+ }
@@ -0,0 +1,63 @@
1
+ import React, { useRef, useEffect, useState } from 'react';
2
+ import cx from 'classnames';
3
+
4
+ export interface Props {
5
+ open?: boolean;
6
+ onClose?: () => void;
7
+ children?: React.ReactNode;
8
+ className?: string;
9
+ trigger?: React.ReactNode;
10
+ placement?: 'bottom-left' | 'bottom-right' | 'top-left' | 'top-right';
11
+ }
12
+
13
+ const Dropdown: React.FC<Props> = ({
14
+ open = false,
15
+ onClose,
16
+ children,
17
+ className,
18
+ trigger,
19
+ placement = 'bottom-right',
20
+ }) => {
21
+ const dropdownRef = useRef<HTMLDivElement>(null);
22
+ const [isOpen, setIsOpen] = useState(open);
23
+
24
+ useEffect(() => {
25
+ setIsOpen(open);
26
+ }, [open]);
27
+
28
+ useEffect(() => {
29
+ const handleClickOutside = (event: MouseEvent) => {
30
+ if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
31
+ setIsOpen(false);
32
+ onClose?.();
33
+ }
34
+ };
35
+
36
+ if (isOpen) {
37
+ document.addEventListener('mousedown', handleClickOutside);
38
+ }
39
+
40
+ return () => {
41
+ document.removeEventListener('mousedown', handleClickOutside);
42
+ };
43
+ }, [isOpen, onClose]);
44
+
45
+ const handleTriggerClick = () => {
46
+ setIsOpen(!isOpen);
47
+ };
48
+
49
+ return (
50
+ <div className={cx('memori-dropdown', className)} ref={dropdownRef}>
51
+ <div className="memori-dropdown--trigger" onClick={handleTriggerClick}>
52
+ {trigger}
53
+ </div>
54
+ {isOpen && (
55
+ <div className={cx('memori-dropdown--content', `memori-dropdown--content--${placement}`)}>
56
+ {children}
57
+ </div>
58
+ )}
59
+ </div>
60
+ );
61
+ };
62
+
63
+ export default Dropdown;
@@ -50,6 +50,9 @@ const errors = {
50
50
  SESSION_NOT_FOUND: -101,
51
51
  SESSION_IS_NOT_ADMINISTRATIVE: -102,
52
52
  SESSION_EXPIRED: -103,
53
+ SESSION_OTP_NOT_FOUND: -107,
54
+ SESSION_OTP_EXPIRED: -108,
55
+ SESSION_MISSING_TEMPORARY_TOKEN: -109,
53
56
 
54
57
  MEMORI_MISSING_CONFIGURATION: -201,
55
58
  MEMORI_CONFIGURATION_NOT_FOUND: -202,
@@ -83,13 +83,11 @@ async function convertToWav(audioBlob: Blob): Promise<Blob> {
83
83
 
84
84
  resolve(wavBlob);
85
85
  } catch (error) {
86
- console.error('Error converting audio to WAV:', error);
87
86
  reject(error);
88
87
  }
89
88
  };
90
89
 
91
90
  fileReader.onerror = () => {
92
- console.error('Failed to read audio file');
93
91
  reject(new Error('Failed to read audio file'));
94
92
  };
95
93
 
@@ -232,15 +230,10 @@ export function useSTT(
232
230
  backgroundNoiseRef.current = 0;
233
231
  audioActivityHistoryRef.current = [];
234
232
 
235
- } else {
236
- console.warn('🎤 [INIT] AudioContext not supported in this browser');
237
233
  }
238
234
  } catch (err) {
239
235
  // Silence detection initialization failed but we can continue
240
- console.error('🎤 [INIT] Silence detection initialization failed:', err);
241
236
  }
242
- } else {
243
- console.log('🎤 [INIT] Continuous recording disabled, skipping silence detection setup');
244
237
  }
245
238
 
246
239
  // Format selection based on provider with Safari compatibility
@@ -316,8 +309,6 @@ export function useSTT(
316
309
  if (options.onTranscriptionComplete) {
317
310
  options.onTranscriptionComplete(result);
318
311
  }
319
- } else {
320
- console.log('No meaningful text transcribed, skipping message processing');
321
312
  }
322
313
 
323
314
  setRecordingState('idle');
@@ -510,13 +501,10 @@ export function useSTT(
510
501
  * Start recording audio
511
502
  */
512
503
  const startRecording = useCallback(async (): Promise<void> => {
513
- console.log('🎤 [START] Starting recording...');
514
- console.log('🎤 [START] Mounted:', isMountedRef.current, 'Muted:', microphoneMuted, 'State:', recordingState);
515
504
 
516
505
  // Prevent immediate restart after stopping (cooldown period)
517
506
  const timeSinceLastStop = Date.now() - lastStopTimeRef.current;
518
507
  if (timeSinceLastStop < 1000) { // 1 second cooldown
519
- console.log('🎤 [START] Too soon after last stop, waiting...', timeSinceLastStop + 'ms');
520
508
  return;
521
509
  }
522
510
 
@@ -524,35 +512,28 @@ export function useSTT(
524
512
  microphoneMuted ||
525
513
  recordingState === 'recording'
526
514
  ) {
527
- console.log('🎤 [START] Cannot start recording - conditions not met');
528
515
  return;
529
516
  }
530
517
 
531
518
  if (!hasUserActivatedRecord) {
532
- console.log('🎤 [START] Setting user activated record flag');
533
519
  setHasUserActivatedRecord(true);
534
520
  }
535
521
 
536
522
  try {
537
523
  setError(null);
538
524
  setRecordingState('recording');
539
- console.log('🎤 [START] Recording state set to recording');
540
525
 
541
526
  // Initialize recording if needed
542
527
  if (!mediaRecorderRef.current) {
543
- console.log('🎤 [START] MediaRecorder not initialized, initializing...');
544
528
  const initialized = await initializeRecording();
545
529
  if (!initialized) {
546
- console.error('🎤 [START] Failed to initialize recording');
547
530
  return;
548
531
  }
549
- console.log('🎤 [START] Recording initialized successfully');
550
532
  }
551
533
 
552
534
  // Reset chunks and start recording
553
535
  chunksRef.current = [];
554
536
  isRecordingRef.current = true;
555
- console.log('🎤 [START] Reset chunks and set recording flag');
556
537
 
557
538
  if (
558
539
  mediaRecorderRef.current &&
@@ -564,27 +545,17 @@ export function useSTT(
564
545
  const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
565
546
  const timeslice = isSafari ? 500 : 100; // 500ms for Safari, 100ms for others
566
547
 
567
- console.log(`🎤 [START] Starting MediaRecorder with ${timeslice}ms timeslice (Safari: ${isSafari})`);
568
- console.log('🎤 [START] Continuous recording enabled:', options.continuousRecording);
569
-
570
548
  mediaRecorderRef.current.start(timeslice);
571
549
  setIsListening(true);
572
- console.log('🎤 [START] MediaRecorder started, listening state set to true');
573
550
 
574
551
  // Start silence detection if continuous recording is enabled
575
552
  if (options.continuousRecording) {
576
- console.log('🎤 [START] Starting silence detection for continuous recording');
577
553
  startSilenceDetection();
578
- } else {
579
- console.log('🎤 [START] Continuous recording disabled, skipping silence detection');
580
554
  }
581
- } else {
582
- console.log('🎤 [START] MediaRecorder not available or not inactive, state:', mediaRecorderRef.current?.state);
583
555
  }
584
556
  } catch (err) {
585
557
  const errorMsg =
586
558
  err instanceof Error ? err : new Error('Failed to start recording');
587
- console.error('🎤 [START] Error starting recording:', errorMsg);
588
559
  setError(errorMsg);
589
560
  setRecordingState('error');
590
561
  isRecordingRef.current = false;
@@ -606,16 +577,11 @@ export function useSTT(
606
577
  * Stop recording audio
607
578
  */
608
579
  const stopRecording = useCallback((): void => {
609
- console.log('🛑 [STOP] Stop recording called');
610
- console.log('🛑 [STOP] isRecordingRef:', isRecordingRef.current, 'continuousRecording:', options.continuousRecording);
611
-
612
580
  if (!isRecordingRef.current) {
613
- console.log('🛑 [STOP] Not currently recording, ignoring stop request');
614
581
  return;
615
582
  }
616
583
 
617
584
  try {
618
- console.log('🛑 [STOP] Setting listening to false');
619
585
  setIsListening(false);
620
586
 
621
587
  // Record the stop time for cooldown
@@ -623,27 +589,18 @@ export function useSTT(
623
589
 
624
590
  // Stop silence detection only if continuous recording was enabled
625
591
  if (options.continuousRecording) {
626
- console.log('🛑 [STOP] Stopping silence detection for continuous recording');
627
592
  stopSilenceDetection();
628
- } else {
629
- console.log('🛑 [STOP] Continuous recording disabled, skipping silence detection stop');
630
593
  }
631
594
 
632
595
  if (
633
596
  mediaRecorderRef.current &&
634
597
  mediaRecorderRef.current.state === 'recording'
635
598
  ) {
636
- console.log('🛑 [STOP] Stopping MediaRecorder');
637
599
  mediaRecorderRef.current.stop();
638
- } else {
639
- console.log('🛑 [STOP] MediaRecorder not available or not recording, state:', mediaRecorderRef.current?.state);
640
600
  }
641
-
642
- console.log('🛑 [STOP] Recording stop completed');
643
601
  } catch (err) {
644
602
  const errorMsg =
645
603
  err instanceof Error ? err : new Error('Failed to stop recording');
646
- console.error('🛑 [STOP] Error stopping recording:', errorMsg);
647
604
  setError(errorMsg);
648
605
  setRecordingState('error');
649
606
  isRecordingRef.current = false;
@@ -658,16 +615,10 @@ export function useSTT(
658
615
  * Toggle recording state
659
616
  */
660
617
  const toggleRecording = useCallback(async (): Promise<void> => {
661
- console.log('🔄 [TOGGLE] Toggle recording called, current state:', recordingState);
662
-
663
618
  if (recordingState === 'recording') {
664
- console.log('🔄 [TOGGLE] Currently recording, stopping...');
665
619
  stopRecording();
666
620
  } else if (recordingState === 'idle') {
667
- console.log('🔄 [TOGGLE] Currently idle, starting recording...');
668
621
  await startRecording();
669
- } else {
670
- console.log('🔄 [TOGGLE] Cannot toggle from state:', recordingState);
671
622
  }
672
623
  }, [recordingState, startRecording, stopRecording]);
673
624
 
@@ -712,7 +663,7 @@ export function useSTT(
712
663
  audioContextRef.current.close();
713
664
  }
714
665
  } catch (error) {
715
- console.warn('Error closing AudioContext:', error);
666
+ // Ignore AudioContext close errors
716
667
  }
717
668
  audioContextRef.current = null;
718
669
  }
@@ -73,6 +73,7 @@ export function useTTS(
73
73
  const isSpeakingRef = useRef<boolean>(false);
74
74
  const timeoutRef = useRef<NodeJS.Timeout | null>(null);
75
75
  const isMountedRef = useRef<boolean>(true);
76
+ const currentChunkAudioRef = useRef<HTMLAudioElement | null>(null);
76
77
  const apiUrl = options.apiUrl || '/api/tts';
77
78
 
78
79
  // Load viseme data into the queue
@@ -88,7 +89,6 @@ export function useTTS(
88
89
  visemeLoadedRef.current = true;
89
90
  return true;
90
91
  } else {
91
- console.warn('[useTTS] No viseme data available');
92
92
  return false;
93
93
  }
94
94
  },
@@ -98,9 +98,6 @@ export function useTTS(
98
98
  // Create audio wrapper for viseme processing
99
99
  const createAudioWrapper = useCallback(() => {
100
100
  if (!audioRef.current) {
101
- console.warn(
102
- '[useTTS] Cannot create audio wrapper: audio element is null'
103
- );
104
101
  return null;
105
102
  }
106
103
 
@@ -159,7 +156,6 @@ export function useTTS(
159
156
  * Performs a complete cleanup of audio and viseme resources
160
157
  */
161
158
  const cleanup = useCallback(() => {
162
- console.log('[useTTS] Cleaning up');
163
159
  if (timeoutRef.current) {
164
160
  clearTimeout(timeoutRef.current);
165
161
  timeoutRef.current = null;
@@ -177,6 +173,11 @@ export function useTTS(
177
173
  audioRef.current = null;
178
174
  }
179
175
 
176
+ // Clear chunk audio reference
177
+ if (currentChunkAudioRef.current) {
178
+ currentChunkAudioRef.current = null;
179
+ }
180
+
180
181
  visemeLoadedRef.current = false;
181
182
  // Don't reset isSpeakingRef here - let the speak function manage it
182
183
  }, [stopProcessing]);
@@ -185,20 +186,23 @@ export function useTTS(
185
186
  * Stops audio playback and cleans up
186
187
  */
187
188
  const stop = useCallback((): void => {
188
- console.log('[useTTS] Stopping audio playback');
189
-
189
+ // Stop the main audio element
190
190
  if (audioRef.current) {
191
191
  audioRef.current.pause();
192
192
  audioRef.current.currentTime = 0;
193
193
  }
194
194
 
195
+ // Stop the current chunk audio element if it exists
196
+ if (currentChunkAudioRef.current) {
197
+ currentChunkAudioRef.current.pause();
198
+ currentChunkAudioRef.current.currentTime = 0;
199
+ currentChunkAudioRef.current = null;
200
+ }
201
+
195
202
  setIsPlaying(false);
196
203
  cleanup();
197
-
198
- // Only reset speaking flag after cleanup
199
204
  isSpeakingRef.current = false;
200
205
 
201
- // Dispatch custom event to notify MemoriWidget that audio has ended
202
206
  const e = new CustomEvent('MemoriAudioEnded');
203
207
  document.dispatchEvent(e);
204
208
  }, [cleanup]);
@@ -276,6 +280,9 @@ const speakChunk = useCallback(async (chunkText: string): Promise<void> => {
276
280
  // Crea un nuovo Audio element per riprodurre il chunk corrente
277
281
  const audio = new Audio(audioUrl);
278
282
 
283
+ // Track the current chunk audio element
284
+ currentChunkAudioRef.current = audio;
285
+
279
286
  return new Promise<void>((resolve, reject) => {
280
287
  // Quando l'audio è pronto per essere riprodotto
281
288
  audio.oncanplaythrough = async () => {
@@ -297,12 +304,20 @@ const speakChunk = useCallback(async (chunkText: string): Promise<void> => {
297
304
  // Quando l'audio termina di riprodurre
298
305
  audio.onended = () => {
299
306
  URL.revokeObjectURL(audioUrl);
307
+ // Clear the current chunk audio reference
308
+ if (currentChunkAudioRef.current === audio) {
309
+ currentChunkAudioRef.current = null;
310
+ }
300
311
  // Risolve la Promise quando l'audio termina di riprodurre
301
312
  resolve();
302
313
  };
303
314
 
304
315
  audio.onerror = () => {
305
316
  URL.revokeObjectURL(audioUrl);
317
+ // Clear the current chunk audio reference
318
+ if (currentChunkAudioRef.current === audio) {
319
+ currentChunkAudioRef.current = null;
320
+ }
306
321
  // Rifiuta la Promise se l'audio fallisce
307
322
  reject(new Error('Audio playback failed'));
308
323
  };
@@ -326,12 +341,16 @@ const speak = useCallback(
326
341
  return;
327
342
  }
328
343
 
329
- // If speaker is muted, completely disable TTS functionality
330
- if (speakerMuted) {
331
- console.log('[useTTS] TTS disabled - speaker is muted');
332
- emitEndSpeakEvent();
333
- return;
344
+ // If speaker is muted, completely disable TTS functionality
345
+ if (speakerMuted) {
346
+ // Still set hasUserActivatedSpeak to true when audio is disabled
347
+ // so the chat can start properly
348
+ if (!hasUserActivatedSpeak) {
349
+ setHasUserActivatedSpeak(true);
334
350
  }
351
+ emitEndSpeakEvent();
352
+ return;
353
+ }
335
354
 
336
355
  // Stop any existing playback first (before checking/setting speaking flag)
337
356
  if (isPlaying) {
@@ -356,7 +375,6 @@ const speak = useCallback(
356
375
 
357
376
  // CHUNKING LOGIC: Dividi il testo in chunk se necessario
358
377
  const chunks = createChunks(text, 800);
359
- console.log(`[useTTS] Processing ${chunks.length} chunks for text length: ${text.length}`);
360
378
 
361
379
  // Riproduci tutti i chunk in sequenza
362
380
  // Il loop itera su ogni chunk di testo che deve essere riprodotto
@@ -366,7 +384,6 @@ const speak = useCallback(
366
384
  break; // Interrompe il loop se il componente viene smontato
367
385
  }
368
386
 
369
- console.log(`[useTTS] Playing chunk ${i + 1}/${chunks.length}`);
370
387
  // Attende che il chunk corrente venga riprodotto prima di passare al successivo
371
388
  await speakChunk(chunks[i]);
372
389
 
@@ -388,7 +405,6 @@ const speak = useCallback(
388
405
  document.dispatchEvent(e);
389
406
 
390
407
  } catch (err) {
391
- console.error('[useTTS] Error during speech synthesis:', err);
392
408
  setIsPlaying(false);
393
409
  isSpeakingRef.current = false;
394
410
 
@@ -409,5 +409,6 @@ Test.args = {
409
409
  uiLang: 'EN',
410
410
  spokenLang: 'EN',
411
411
  showLogin: true,
412
+ enableAudio: false,
412
413
  integrationID: '2b37c25d-8cf9-456f-b9ee-2c27dc4d54fc',
413
414
  };
@@ -129,3 +129,20 @@ it('renders client with whiteListedDomains on not allowed domains', () => {
129
129
  );
130
130
  expect(container).toMatchSnapshot();
131
131
  });
132
+
133
+ it('renders client with audio disabled', () => {
134
+ const { container } = render(
135
+ <Memori
136
+ memoriID={memori.memoriID}
137
+ ownerUserID={memori.ownerUserID}
138
+ tenantID={tenant.tenantID}
139
+ integration={{
140
+ ...integration,
141
+ customData: JSON.stringify({}),
142
+ }}
143
+ enableAudio={false}
144
+ autoStart={true}
145
+ />
146
+ );
147
+ expect(container).toMatchSnapshot();
148
+ });
@@ -332,6 +332,33 @@
332
332
  "otpInvalid": "Invalid verification code",
333
333
  "otpError": "Error validating code. Please try again.",
334
334
  "otpSuccess": "Login successful!",
335
+ "otpEmailTitle": "Enter your email",
336
+ "otpEmailDescription": "Enter your email address to receive a verification code",
337
+ "otpCodeDescription": "Enter the 4-digit code sent to {{email}}",
338
+ "emailPlaceholder": "Enter your email",
339
+ "sendOtp": "Send code",
340
+ "resendOtp": "Resend code",
341
+ "resending": "Resending...",
342
+ "backToEmail": "Back to email",
343
+ "otpSent": "Verification code sent!",
344
+ "otpResent": "Code resent successfully!",
345
+ "otpSendError": "Error sending code. Please try again.",
346
+ "emailRequired": "Email required",
347
+ "emailInvalid": "Please enter a valid email address",
348
+ "otpNotFound": "Verification code not found",
349
+ "otpExpired": "Verification code has expired",
350
+ "otpMissing": "Verification code is required",
351
+ "otpSuccessMessage": "Verification successful! Redirecting...",
352
+ "otpSuccessDescription": "Your account has been verified successfully. You will be redirected shortly.",
353
+ "otpHelp": "Enter the 4-digit code from your email",
354
+ "emailHelp": "We'll send you a verification code",
355
+ "userFetchError": "Error loading user data. Please try again.",
356
+ "otpStep1Title": "Enter Email",
357
+ "otpStep2Title": "Check Email",
358
+ "otpStep3Title": "Enter Code",
359
+ "featureFast": "Fast",
360
+ "featureSecure": "Secure",
361
+ "featureMobile": "Mobile",
335
362
  "password": "Password",
336
363
  "newPassword": "New password",
337
364
  "confirmPassword": "Confirm Password",
@@ -360,7 +387,24 @@
360
387
  "termsOfService": "Terms of Service",
361
388
  "pAndCUAccepted": "I accept the terms of service about Deep Thought",
362
389
  "editAccount": "Edit account",
363
- "save": "Save"
390
+ "save": "Save",
391
+ "welcomeUser": "Welcome User",
392
+ "verified": "Verified",
393
+ "premiumUser": "Premium",
394
+ "quickActions": "Quick Actions",
395
+ "editProfile": "Edit Profile",
396
+ "notifications": "Notifications",
397
+ "help": "Help",
398
+ "logoutConfirm": "Are you sure you want to logout?",
399
+ "comingSoon": "Coming soon!",
400
+ "accountOverview": "Account Overview",
401
+ "notSet": "Not set",
402
+ "accepted": "Accepted",
403
+ "pending": "Pending",
404
+ "termsStatus": "Terms Status",
405
+ "enabled": "Enabled",
406
+ "disabled": "Disabled",
407
+ "advancedFeatures": "Advanced Features"
364
408
  },
365
409
  "chatLogs": {
366
410
  "anyMessage": "Any message",
@@ -439,6 +483,9 @@
439
483
  "SESSION_NOT_FOUND": "Session not found",
440
484
  "SESSION_IS_NOT_ADMINISTRATIVE": "Non-administrative session",
441
485
  "SESSION_EXPIRED": "Session expired",
486
+ "SESSION_OTP_NOT_FOUND": "Verification code not found",
487
+ "SESSION_OTP_EXPIRED": "Verification code has expired",
488
+ "SESSION_MISSING_TEMPORARY_TOKEN": "Verification code is required",
442
489
 
443
490
  "MEMORI_MISSING_CONFIGURATION": "Agent: missing configuration",
444
491
  "MEMORI_CONFIGURATION_NOT_FOUND": "Agent: configuration not found",