@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.
- package/CHANGELOG.md +33 -0
- package/dist/components/Header/Header.css +24 -0
- package/dist/components/Header/Header.d.ts +4 -0
- package/dist/components/Header/Header.js +52 -2
- package/dist/components/Header/Header.js.map +1 -1
- package/dist/components/LoginDrawer/LoginDrawer.css +1372 -238
- package/dist/components/MemoriWidget/MemoriWidget.js +42 -25
- package/dist/components/MemoriWidget/MemoriWidget.js.map +1 -1
- package/dist/components/icons/Logout.d.ts +6 -0
- package/dist/components/icons/Logout.js +6 -0
- package/dist/components/icons/Logout.js.map +1 -0
- package/dist/components/layouts/HiddenChat.js +3 -1
- package/dist/components/layouts/HiddenChat.js.map +1 -1
- package/dist/components/layouts/hidden-chat.css +4 -0
- package/dist/components/ui/Dropdown.css +173 -0
- package/dist/components/ui/Dropdown.d.ts +11 -0
- package/dist/components/ui/Dropdown.js +33 -0
- package/dist/components/ui/Dropdown.js.map +1 -0
- package/dist/helpers/error.js +3 -0
- package/dist/helpers/error.js.map +1 -1
- package/dist/helpers/stt/useSTT.js +0 -56
- package/dist/helpers/stt/useSTT.js.map +1 -1
- package/dist/helpers/tts/useTTS.js +19 -8
- package/dist/helpers/tts/useTTS.js.map +1 -1
- package/dist/locales/en.json +48 -1
- package/dist/locales/es.json +30 -0
- package/dist/locales/fr.json +30 -0
- package/dist/locales/it.json +44 -0
- package/esm/components/Header/Header.css +24 -0
- package/esm/components/Header/Header.d.ts +4 -0
- package/esm/components/Header/Header.js +53 -3
- package/esm/components/Header/Header.js.map +1 -1
- package/esm/components/LoginDrawer/LoginDrawer.css +1372 -238
- package/esm/components/MemoriWidget/MemoriWidget.js +42 -25
- package/esm/components/MemoriWidget/MemoriWidget.js.map +1 -1
- package/esm/components/icons/Logout.d.ts +6 -0
- package/esm/components/icons/Logout.js +4 -0
- package/esm/components/icons/Logout.js.map +1 -0
- package/esm/components/layouts/HiddenChat.js +3 -1
- package/esm/components/layouts/HiddenChat.js.map +1 -1
- package/esm/components/layouts/hidden-chat.css +4 -0
- package/esm/components/ui/Dropdown.css +173 -0
- package/esm/components/ui/Dropdown.d.ts +11 -0
- package/esm/components/ui/Dropdown.js +30 -0
- package/esm/components/ui/Dropdown.js.map +1 -0
- package/esm/helpers/error.js +3 -0
- package/esm/helpers/error.js.map +1 -1
- package/esm/helpers/stt/useSTT.js +0 -56
- package/esm/helpers/stt/useSTT.js.map +1 -1
- package/esm/helpers/tts/useTTS.js +19 -8
- package/esm/helpers/tts/useTTS.js.map +1 -1
- package/esm/locales/en.json +48 -1
- package/esm/locales/es.json +30 -0
- package/esm/locales/fr.json +30 -0
- package/esm/locales/it.json +44 -0
- package/package.json +2 -2
- package/src/__snapshots__/index.test.tsx.snap +19 -0
- package/src/components/Header/Header.css +24 -0
- package/src/components/Header/Header.test.tsx +15 -1
- package/src/components/Header/Header.tsx +147 -10
- package/src/components/Header/__snapshots__/Header.test.tsx.snap +53 -37
- package/src/components/LoginDrawer/LoginDrawer.css +1372 -238
- package/src/components/LoginDrawer/LoginDrawer.test.tsx +12 -1
- package/src/components/MemoriWidget/MemoriWidget.tsx +102 -60
- package/src/components/icons/Logout.tsx +27 -0
- package/src/components/layouts/HiddenChat.tsx +3 -1
- package/src/components/layouts/__snapshots__/HiddenChat.test.tsx.snap +1 -1
- package/src/components/layouts/hidden-chat.css +4 -0
- package/src/components/ui/Dropdown.css +173 -0
- package/src/components/ui/Dropdown.tsx +63 -0
- package/src/helpers/error.ts +3 -0
- package/src/helpers/stt/useSTT.ts +1 -50
- package/src/helpers/tts/useTTS.ts +34 -18
- package/src/index.stories.tsx +1 -0
- package/src/index.test.tsx +17 -0
- package/src/locales/en.json +48 -1
- package/src/locales/es.json +30 -0
- package/src/locales/fr.json +30 -0
- package/src/locales/it.json +44 -0
- package/src/components/AccountForm/AccountForm.test.tsx +0 -27
- 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;
|
package/src/helpers/error.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
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
|
|
package/src/index.stories.tsx
CHANGED
package/src/index.test.tsx
CHANGED
|
@@ -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
|
+
});
|
package/src/locales/en.json
CHANGED
|
@@ -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",
|