@memori.ai/memori-react 2.0.11 → 2.2.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 (140) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/README.md +74 -26
  3. package/dist/components/Chat/Chat.d.ts +1 -0
  4. package/dist/components/Chat/Chat.js +2 -2
  5. package/dist/components/Chat/Chat.js.map +1 -1
  6. package/dist/components/ChatBubble/ChatBubble.js +1 -1
  7. package/dist/components/ChatBubble/ChatBubble.js.map +1 -1
  8. package/dist/components/ChatInputs/ChatInputs.css +1 -41
  9. package/dist/components/ChatInputs/ChatInputs.d.ts +1 -0
  10. package/dist/components/ChatInputs/ChatInputs.js +9 -3
  11. package/dist/components/ChatInputs/ChatInputs.js.map +1 -1
  12. package/dist/components/FeedbackButtons/FeedbackButtons.js +1 -1
  13. package/dist/components/FeedbackButtons/FeedbackButtons.js.map +1 -1
  14. package/dist/components/MediaWidget/MediaItemWidget.js +1 -12
  15. package/dist/components/MediaWidget/MediaItemWidget.js.map +1 -1
  16. package/dist/components/MemoriWidget/MemoriWidget.d.ts +17 -1
  17. package/dist/components/MemoriWidget/MemoriWidget.js +31 -19
  18. package/dist/components/MemoriWidget/MemoriWidget.js.map +1 -1
  19. package/dist/components/MicrophoneButton/MicrophoneButton.css +101 -0
  20. package/dist/components/MicrophoneButton/MicrophoneButton.d.ts +9 -0
  21. package/dist/components/MicrophoneButton/MicrophoneButton.js +46 -0
  22. package/dist/components/MicrophoneButton/MicrophoneButton.js.map +1 -0
  23. package/dist/components/SettingsDrawer/SettingsDrawer.d.ts +3 -3
  24. package/dist/components/SettingsDrawer/SettingsDrawer.js +8 -6
  25. package/dist/components/SettingsDrawer/SettingsDrawer.js.map +1 -1
  26. package/dist/components/SettingsDrawer/SettingsDrawer.test.js +7 -7
  27. package/dist/components/SettingsDrawer/SettingsDrawer.test.js.map +1 -1
  28. package/dist/components/StartPanel/StartPanel.js +1 -1
  29. package/dist/components/StartPanel/StartPanel.js.map +1 -1
  30. package/dist/components/layouts/FullPage.d.ts +2 -15
  31. package/dist/components/layouts/FullPage.js.map +1 -1
  32. package/dist/components/layouts/Totem.d.ts +2 -15
  33. package/dist/components/layouts/Totem.js.map +1 -1
  34. package/dist/components/ui/Button.d.ts +5 -1
  35. package/dist/components/ui/Button.js +1 -1
  36. package/dist/components/ui/Button.js.map +1 -1
  37. package/dist/components/ui/Tooltip.css +33 -2
  38. package/dist/components/ui/Tooltip.d.ts +2 -1
  39. package/dist/components/ui/Tooltip.js +1 -2
  40. package/dist/components/ui/Tooltip.js.map +1 -1
  41. package/dist/components/ui/Tooltip.test.js +16 -0
  42. package/dist/components/ui/Tooltip.test.js.map +1 -1
  43. package/dist/helpers/configuration.js +1 -1
  44. package/dist/helpers/configuration.js.map +1 -1
  45. package/dist/helpers/media.js +25 -19
  46. package/dist/helpers/media.js.map +1 -1
  47. package/dist/index.d.ts +1 -0
  48. package/dist/index.js +2 -2
  49. package/dist/index.js.map +1 -1
  50. package/dist/locales/en.json +4 -0
  51. package/dist/locales/it.json +4 -0
  52. package/dist/styles.css +3 -2
  53. package/esm/components/Chat/Chat.d.ts +1 -0
  54. package/esm/components/Chat/Chat.js +2 -2
  55. package/esm/components/Chat/Chat.js.map +1 -1
  56. package/esm/components/ChatBubble/ChatBubble.js +1 -1
  57. package/esm/components/ChatBubble/ChatBubble.js.map +1 -1
  58. package/esm/components/ChatInputs/ChatInputs.css +1 -41
  59. package/esm/components/ChatInputs/ChatInputs.d.ts +1 -0
  60. package/esm/components/ChatInputs/ChatInputs.js +9 -3
  61. package/esm/components/ChatInputs/ChatInputs.js.map +1 -1
  62. package/esm/components/FeedbackButtons/FeedbackButtons.js +1 -1
  63. package/esm/components/FeedbackButtons/FeedbackButtons.js.map +1 -1
  64. package/esm/components/MediaWidget/MediaItemWidget.js +1 -12
  65. package/esm/components/MediaWidget/MediaItemWidget.js.map +1 -1
  66. package/esm/components/MemoriWidget/MemoriWidget.d.ts +17 -1
  67. package/esm/components/MemoriWidget/MemoriWidget.js +31 -19
  68. package/esm/components/MemoriWidget/MemoriWidget.js.map +1 -1
  69. package/esm/components/MicrophoneButton/MicrophoneButton.css +101 -0
  70. package/esm/components/MicrophoneButton/MicrophoneButton.d.ts +9 -0
  71. package/esm/components/MicrophoneButton/MicrophoneButton.js +43 -0
  72. package/esm/components/MicrophoneButton/MicrophoneButton.js.map +1 -0
  73. package/esm/components/SettingsDrawer/SettingsDrawer.d.ts +3 -3
  74. package/esm/components/SettingsDrawer/SettingsDrawer.js +9 -7
  75. package/esm/components/SettingsDrawer/SettingsDrawer.js.map +1 -1
  76. package/esm/components/SettingsDrawer/SettingsDrawer.test.js +7 -7
  77. package/esm/components/SettingsDrawer/SettingsDrawer.test.js.map +1 -1
  78. package/esm/components/StartPanel/StartPanel.js +1 -1
  79. package/esm/components/StartPanel/StartPanel.js.map +1 -1
  80. package/esm/components/layouts/FullPage.d.ts +2 -15
  81. package/esm/components/layouts/FullPage.js.map +1 -1
  82. package/esm/components/layouts/Totem.d.ts +2 -15
  83. package/esm/components/layouts/Totem.js.map +1 -1
  84. package/esm/components/ui/Button.d.ts +5 -1
  85. package/esm/components/ui/Button.js +1 -1
  86. package/esm/components/ui/Button.js.map +1 -1
  87. package/esm/components/ui/Tooltip.css +33 -2
  88. package/esm/components/ui/Tooltip.d.ts +2 -1
  89. package/esm/components/ui/Tooltip.js +1 -2
  90. package/esm/components/ui/Tooltip.js.map +1 -1
  91. package/esm/components/ui/Tooltip.test.js +16 -0
  92. package/esm/components/ui/Tooltip.test.js.map +1 -1
  93. package/esm/helpers/configuration.js +1 -1
  94. package/esm/helpers/configuration.js.map +1 -1
  95. package/esm/helpers/media.js +25 -19
  96. package/esm/helpers/media.js.map +1 -1
  97. package/esm/index.d.ts +1 -0
  98. package/esm/index.js +2 -2
  99. package/esm/index.js.map +1 -1
  100. package/esm/locales/en.json +4 -0
  101. package/esm/locales/it.json +4 -0
  102. package/esm/styles.css +3 -2
  103. package/package.json +1 -1
  104. package/src/components/BlockedMemoriBadge/__snapshots__/BlockedMemoriBadge.test.tsx.snap +4 -4
  105. package/src/components/Chat/Chat.tsx +3 -0
  106. package/src/components/Chat/__snapshots__/Chat.test.tsx.snap +6 -6
  107. package/src/components/ChatBubble/ChatBubble.tsx +1 -1
  108. package/src/components/ChatBubble/__snapshots__/ChatBubble.test.tsx.snap +1 -1
  109. package/src/components/ChatInputs/ChatInputs.css +1 -41
  110. package/src/components/ChatInputs/ChatInputs.stories.tsx +50 -3
  111. package/src/components/ChatInputs/ChatInputs.tsx +20 -3
  112. package/src/components/ChatInputs/__snapshots__/ChatInputs.test.tsx.snap +160 -85
  113. package/src/components/FeedbackButtons/FeedbackButtons.tsx +1 -1
  114. package/src/components/Header/Header.stories.tsx +3 -0
  115. package/src/components/MediaWidget/MediaItemWidget.tsx +1 -12
  116. package/src/components/MediaWidget/__snapshots__/MediaItemWidget.test.tsx.snap +6 -6
  117. package/src/components/MediaWidget/__snapshots__/MediaWidget.test.tsx.snap +1 -1
  118. package/src/components/MemoriWidget/MemoriWidget.tsx +53 -18
  119. package/src/components/MicrophoneButton/MicrophoneButton.css +101 -0
  120. package/src/components/MicrophoneButton/MicrophoneButton.stories.tsx +49 -0
  121. package/src/components/MicrophoneButton/MicrophoneButton.tsx +95 -0
  122. package/src/components/SettingsDrawer/SettingsDrawer.stories.tsx +6 -4
  123. package/src/components/SettingsDrawer/SettingsDrawer.test.tsx +14 -14
  124. package/src/components/SettingsDrawer/SettingsDrawer.tsx +57 -25
  125. package/src/components/StartPanel/StartPanel.tsx +3 -3
  126. package/src/components/layouts/FullPage.tsx +2 -16
  127. package/src/components/layouts/Totem.tsx +2 -16
  128. package/src/components/layouts/layouts.stories.tsx +41 -1
  129. package/src/components/ui/Button.tsx +21 -1
  130. package/src/components/ui/Tooltip.css +33 -2
  131. package/src/components/ui/Tooltip.stories.tsx +40 -3
  132. package/src/components/ui/Tooltip.test.tsx +52 -0
  133. package/src/components/ui/Tooltip.tsx +12 -7
  134. package/src/components/ui/__snapshots__/Tooltip.test.tsx.snap +80 -4
  135. package/src/helpers/configuration.ts +1 -1
  136. package/src/helpers/media.ts +29 -23
  137. package/src/index.tsx +3 -0
  138. package/src/locales/en.json +4 -0
  139. package/src/locales/it.json +4 -0
  140. package/src/styles.css +3 -2
@@ -145,12 +145,28 @@ let speechSynthesizer: SpeechSynthesizer | null;
145
145
  let audioDestination: SpeakerAudioDestination;
146
146
  let audioContext: IAudioContext;
147
147
 
148
+ export interface LayoutProps {
149
+ header?: JSX.Element | null;
150
+ avatar: JSX.Element;
151
+ chat?: JSX.Element | null;
152
+ startPanel: JSX.Element;
153
+ integrationStyle?: JSX.Element | null;
154
+ integrationBackground?: JSX.Element | null;
155
+ changeMode?: JSX.Element | null;
156
+ poweredBy?: JSX.Element | null;
157
+ sessionId?: string;
158
+ hasUserActivatedSpeak?: boolean;
159
+ showInstruct?: boolean;
160
+ loading?: boolean;
161
+ }
162
+
148
163
  export interface Props {
149
164
  memori: Memori;
150
165
  memoriConfigs?: MemoriConfig[];
151
166
  memoriLang?: string;
152
167
  integration?: Integration;
153
168
  layout?: 'DEFAULT' | 'FULLPAGE' | 'TOTEM';
169
+ customLayout?: React.FC<LayoutProps>;
154
170
  showShare?: boolean;
155
171
  showInstruct?: boolean;
156
172
  showInputs?: boolean;
@@ -184,6 +200,7 @@ const MemoriWidget = ({
184
200
  memoriLang,
185
201
  integration,
186
202
  layout = 'DEFAULT',
203
+ customLayout,
187
204
  showInstruct = false,
188
205
  showShare = true,
189
206
  preview = false,
@@ -234,18 +251,19 @@ const MemoriWidget = ({
234
251
  ?.find(c => c.memoriConfigID === memori.memoriConfigurationID)
235
252
  ?.culture?.split('-')?.[0]
236
253
  ?.toUpperCase();
237
- // eslint-disable-next-line
254
+ const integrationConfig = integration?.customData
255
+ ? JSON.parse(integration.customData)
256
+ : null;
257
+ const isMultilanguageEnabled = !!integrationConfig?.multilanguage;
238
258
  const [userLang, setUserLang] = useState(
239
- memoriLang ??
259
+ integrationConfig?.lang ??
260
+ memoriLang ??
240
261
  memori?.culture?.split('-')?.[0] ??
241
262
  language ??
263
+ integrationConfig?.uiLang ??
242
264
  i18n.language ??
243
265
  'IT'
244
266
  );
245
- const integrationConfig = integration?.customData
246
- ? JSON.parse(integration.customData)
247
- : null;
248
- const isMultilanguageEnabled = !!integrationConfig?.multilanguage;
249
267
 
250
268
  const [loading, setLoading] = useState(false);
251
269
  const [memoriTyping, setMemoriTyping] = useState(false);
@@ -258,7 +276,7 @@ const MemoriWidget = ({
258
276
  const [showPositionDrawer, setShowPositionDrawer] = useState(false);
259
277
  const [showSettingsDrawer, setShowSettingsDrawer] = useState(false);
260
278
  const [muteSpeaker, setMuteSpeaker] = useState(false);
261
- const [continuousSpeech, setContinuousSpeech] = useState(true);
279
+ const [continuousSpeech, setContinuousSpeech] = useState(false);
262
280
  const [continuousSpeechTimeout, setContinuousSpeechTimeout] = useState(2);
263
281
  const [isPlayingAudio, setIsPlayingAudio] = useState(false);
264
282
  const [controlsPosition, setControlsPosition] = useState<'center' | 'bottom'>(
@@ -272,9 +290,16 @@ const MemoriWidget = ({
272
290
 
273
291
  useEffect(() => {
274
292
  let defaultControlsPosition: 'center' | 'bottom' = 'bottom';
275
- if (window.innerWidth < 768) {
293
+ let microphoneMode = getLocalConfig<string>(
294
+ 'microphoneMode',
295
+ 'HOLD_TO_TALK'
296
+ );
297
+
298
+ if (window.innerWidth <= 768) {
276
299
  // on mobile, default position is bottom
277
300
  defaultControlsPosition = 'bottom';
301
+ // on mobile, keep only HOLD_TO_TALK mode
302
+ microphoneMode = 'HOLD_TO_TALK';
278
303
  } else if (
279
304
  window.matchMedia('(orientation: portrait)').matches ||
280
305
  window.innerHeight > window.innerWidth
@@ -287,7 +312,7 @@ const MemoriWidget = ({
287
312
  }
288
313
 
289
314
  setMuteSpeaker(getLocalConfig('muteSpeaker', false));
290
- setContinuousSpeech(getLocalConfig('continuousSpeech', true));
315
+ setContinuousSpeech(microphoneMode === 'CONTINUOUS');
291
316
  setContinuousSpeechTimeout(getLocalConfig('continuousSpeechTimeout', 2));
292
317
  setControlsPosition(
293
318
  getLocalConfig('controlsPosition', defaultControlsPosition)
@@ -927,6 +952,10 @@ const MemoriWidget = ({
927
952
  }
928
953
  }
929
954
 
955
+ if (memori.enableCompletions) {
956
+ timeout = timeout + 60;
957
+ }
958
+
930
959
  let uiTimeout = setTimeout(handleTimeout, timeout * 1000);
931
960
  setUserInteractionTimeout(uiTimeout);
932
961
  timeoutRef.current = uiTimeout;
@@ -1425,6 +1454,7 @@ const MemoriWidget = ({
1425
1454
 
1426
1455
  clearListening();
1427
1456
  setTranscript('');
1457
+ resetTranscript();
1428
1458
 
1429
1459
  try {
1430
1460
  navigator.mediaDevices
@@ -1481,6 +1511,8 @@ const MemoriWidget = ({
1481
1511
  recognizer.sessionStopped = (_s, _e) => {
1482
1512
  stopListening();
1483
1513
  };
1514
+
1515
+ resetTranscript();
1484
1516
  recognizer.startContinuousRecognitionAsync();
1485
1517
  })
1486
1518
  .catch(console.error);
@@ -1578,7 +1610,8 @@ const MemoriWidget = ({
1578
1610
  'sendOnEnter',
1579
1611
  'keypress'
1580
1612
  );
1581
- setSendOnEnter(stored);
1613
+ if (window.innerWidth <= 768) setSendOnEnter('click');
1614
+ else setSendOnEnter(stored);
1582
1615
  }, []);
1583
1616
  useEffect(() => {
1584
1617
  setLocalConfig('sendOnEnter', sendOnEnter);
@@ -2200,6 +2233,7 @@ const MemoriWidget = ({
2200
2233
  selectReceiverTag={selectReceiverTag}
2201
2234
  preview={preview}
2202
2235
  sendOnEnter={sendOnEnter}
2236
+ microphoneMode={continuousSpeech ? 'CONTINUOUS' : 'HOLD_TO_TALK'}
2203
2237
  setSendOnEnter={setSendOnEnter}
2204
2238
  attachmentsMenuOpen={attachmentsMenuOpen}
2205
2239
  setAttachmentsMenuOpen={setAttachmentsMenuOpen}
@@ -2255,12 +2289,13 @@ const MemoriWidget = ({
2255
2289
 
2256
2290
  const poweredBy = <PoweredBy tenant={tenant} userLang={userLang} />;
2257
2291
 
2258
- const Layout =
2259
- selectedLayout === 'TOTEM'
2260
- ? TotemLayout
2261
- : selectedLayout === 'FULLPAGE'
2262
- ? FullPageLayout
2263
- : FullPageLayout;
2292
+ const Layout = customLayout
2293
+ ? customLayout
2294
+ : selectedLayout === 'TOTEM'
2295
+ ? TotemLayout
2296
+ : selectedLayout === 'FULLPAGE'
2297
+ ? FullPageLayout
2298
+ : FullPageLayout;
2264
2299
 
2265
2300
  return (
2266
2301
  <div
@@ -2387,9 +2422,9 @@ const MemoriWidget = ({
2387
2422
  layout={selectedLayout}
2388
2423
  open={!!showSettingsDrawer}
2389
2424
  onClose={() => setShowSettingsDrawer(false)}
2390
- continuousSpeech={continuousSpeech}
2425
+ microphoneMode={continuousSpeech ? 'CONTINUOUS' : 'HOLD_TO_TALK'}
2391
2426
  continuousSpeechTimeout={continuousSpeechTimeout}
2392
- setContinuousSpeech={setContinuousSpeech}
2427
+ setMicrophoneMode={mode => setContinuousSpeech(mode === 'CONTINUOUS')}
2393
2428
  setContinuousSpeechTimeout={setContinuousSpeechTimeout}
2394
2429
  controlsPosition={controlsPosition}
2395
2430
  setControlsPosition={setControlsPosition}
@@ -0,0 +1,101 @@
1
+ .memori-mic-btn-tooltip.memori-tooltip.memori-tooltip--align-topLeft:not(.memori-tooltip--disabled).memori-tooltip--visible .memori-tooltip--content,
2
+ .memori-mic-btn-tooltip.memori-tooltip.memori-tooltip--align-topLeft:not(.memori-tooltip--disabled):not(.memori-tooltip--visible):hover .memori-tooltip--content {
3
+ touch-action: none;
4
+ transform: translateY(-180%) translateX(27%);
5
+ -webkit-user-select: none;
6
+ user-select: none;
7
+ }
8
+
9
+ .memori-chat-inputs--mic {
10
+ z-index: 1;
11
+ margin-left: 0.33rem;
12
+ touch-action: none;
13
+ -webkit-user-select: none;
14
+ user-select: none;
15
+ }
16
+
17
+ .memori-chat-inputs--mic svg {
18
+ color: var(--memori-primary-text, #fff);
19
+ font-size: 1em;
20
+ touch-action: none;
21
+ -webkit-user-select: none;
22
+ user-select: none;
23
+ }
24
+
25
+ .memori-chat-inputs--mic:hover,
26
+ .memori-chat-inputs--mic:active,
27
+ .memori-chat-inputs--mic:focus {
28
+ border-color: var(--memori-primary) !important;
29
+ color: var(--memori-primary-text, #fff);
30
+ }
31
+
32
+ .memori-chat-inputs--mic:active,
33
+ .memori-chat-inputs--mic:focus {
34
+ box-shadow: 0 0.2rem 0.33rem var(--memori-primary) !important;
35
+ }
36
+
37
+ .memori-chat-inputs--mic:not(.memori-chat-inputs--mic--listening):active,
38
+ .memori-chat-inputs--mic:not(.memori-chat-inputs--mic--listening):focus {
39
+ color: var(--memori-primary) !important;
40
+ }
41
+
42
+ @keyframes micBtnActivePulse {
43
+ 0% {
44
+ transform: scale(1.25);
45
+ }
46
+
47
+ 25% {
48
+ transform: scale(1.4);
49
+ }
50
+
51
+ 50% {
52
+ transform: scale(1.3);
53
+ }
54
+
55
+ 75% {
56
+ transform: scale(1.4);
57
+ }
58
+
59
+ 100% {
60
+ transform: scale(1.2);
61
+ }
62
+ }
63
+
64
+ .memori-chat-inputs--mic.memori-chat-inputs--mic--listening {
65
+ position: relative;
66
+ color: red !important;
67
+ transform: scale(1.5);
68
+ }
69
+
70
+ .memori-chat-inputs--mic.memori-chat-inputs--mic--listening::before,
71
+ .memori-chat-inputs--mic.memori-chat-inputs--mic--listening::after {
72
+ position: absolute;
73
+ z-index: -1;
74
+ top: 0;
75
+ left: 0;
76
+ width: 100%;
77
+ height: 100%;
78
+ border-radius: 50%;
79
+ animation: micBtnActivePulse 2s infinite;
80
+ background: var(--memori-primary);
81
+ content: "";
82
+ opacity: 0.2;
83
+ }
84
+
85
+ .memori-chat-inputs--mic.memori-chat-inputs--mic--listening::after {
86
+ top: 5%;
87
+ left: 5%;
88
+ width: 90%;
89
+ height: 90%;
90
+ animation-delay: 0.3s;
91
+ opacity: 0.3;
92
+ }
93
+
94
+ .memori-chat-inputs--mic.memori-chat-inputs--mic--listening:active,
95
+ .memori-chat-inputs--mic.memori-chat-inputs--mic--listening:focus {
96
+ border-color: red !important;
97
+ }
98
+
99
+ .memori-chat-inputs--mic.memori-chat-inputs--mic--listening svg {
100
+ color: red !important;
101
+ }
@@ -0,0 +1,49 @@
1
+ import React from 'react';
2
+ import { Meta, Story } from '@storybook/react';
3
+ import MicrophoneButton, { Props } from './MicrophoneButton';
4
+
5
+ import './MicrophoneButton.css';
6
+
7
+ const meta: Meta = {
8
+ title: 'Widget/Microphone Button',
9
+ component: MicrophoneButton,
10
+ argTypes: {
11
+ disabled: {
12
+ control: {
13
+ type: 'boolean',
14
+ },
15
+ },
16
+ },
17
+ parameters: {
18
+ controls: { expanded: true },
19
+ },
20
+ };
21
+
22
+ export default meta;
23
+
24
+ const Template: Story<Props> = args => {
25
+ const [listening, setListening] = React.useState(args.listening);
26
+ const startListening = () => setListening(true);
27
+ const stopListening = () => setListening(false);
28
+
29
+ return (
30
+ <div style={{ paddingTop: '10rem', textAlign: 'right' }}>
31
+ <MicrophoneButton
32
+ {...args}
33
+ listening={listening}
34
+ startListening={startListening}
35
+ stopListening={stopListening}
36
+ />
37
+ </div>
38
+ );
39
+ };
40
+
41
+ // By passing using the Args format for exported stories, you can control the props for a component for reuse in a test
42
+ // https://storybook.js.org/docs/react/workflows/unit-testing
43
+ export const Default = Template.bind({});
44
+ Default.args = {
45
+ listening: false,
46
+ stopAudio: () => {},
47
+ startListening: () => {},
48
+ stopListening: () => {},
49
+ };
@@ -0,0 +1,95 @@
1
+ import React, { useState, useEffect, useRef } from 'react';
2
+ import { Props as ChatInputProps } from '../ChatInputs/ChatInputs';
3
+ import Microphone from '../icons/Microphone';
4
+ import Button from '../ui/Button';
5
+ import Tooltip from '../ui/Tooltip';
6
+ import cx from 'classnames';
7
+ import { useTranslation } from 'react-i18next';
8
+
9
+ export interface Props {
10
+ listening?: ChatInputProps['listening'];
11
+ stopAudio: ChatInputProps['stopAudio'];
12
+ startListening: ChatInputProps['startListening'];
13
+ stopListening: ChatInputProps['stopListening'];
14
+ }
15
+
16
+ const MicrophoneButton = ({
17
+ listening,
18
+ stopAudio,
19
+ startListening,
20
+ stopListening,
21
+ }: Props) => {
22
+ const { t } = useTranslation();
23
+ const [micBtnTooltip, setMicBtnTooltip] = useState<string | undefined>();
24
+
25
+ const intervalRef = useRef<any>(null);
26
+
27
+ const startHold = (
28
+ e:
29
+ | React.TouchEvent<HTMLButtonElement>
30
+ | React.MouseEvent<Element, MouseEvent>
31
+ ) => {
32
+ e.preventDefault();
33
+
34
+ setMicBtnTooltip(t('write_and_speak.holdToSpeak') || 'Hold to record');
35
+
36
+ if (intervalRef.current) return;
37
+ intervalRef.current = setTimeout(() => {
38
+ stopAudio();
39
+ setMicBtnTooltip(
40
+ t('write_and_speak.releaseToEndListening') || 'Release to end listening'
41
+ );
42
+ startListening();
43
+ }, 300);
44
+ };
45
+
46
+ const stopHold = () => {
47
+ if (intervalRef.current) {
48
+ clearTimeout(intervalRef.current);
49
+ intervalRef.current = null;
50
+ }
51
+
52
+ stopListening();
53
+ setMicBtnTooltip(undefined);
54
+ };
55
+
56
+ useEffect(() => {
57
+ return () => stopHold();
58
+ }, []);
59
+
60
+ return (
61
+ <Tooltip
62
+ visible={!!micBtnTooltip}
63
+ content={
64
+ <span>
65
+ {micBtnTooltip ||
66
+ t('write_and_speak.pressAndHoldToSpeak') ||
67
+ 'Press and hold to speak'}
68
+ </span>
69
+ }
70
+ align="topLeft"
71
+ className="memori-mic-btn-tooltip"
72
+ >
73
+ <Button
74
+ primary
75
+ className={cx('memori-chat-inputs--mic', {
76
+ 'memori-chat-inputs--mic--listening': listening,
77
+ })}
78
+ title={
79
+ listening
80
+ ? t('write_and_speak.micButtonPopoverListening') || 'Listening'
81
+ : t('write_and_speak.micButtonPopover') || 'Start listening'
82
+ }
83
+ onMouseDown={startHold}
84
+ onTouchStart={startHold}
85
+ onMouseUp={stopHold}
86
+ onMouseLeave={stopHold}
87
+ onTouchEnd={stopHold}
88
+ shape="circle"
89
+ icon={<Microphone />}
90
+ />
91
+ </Tooltip>
92
+ );
93
+ };
94
+
95
+ export default MicrophoneButton;
@@ -21,15 +21,17 @@ const meta: Meta = {
21
21
  export default meta;
22
22
 
23
23
  const Template: Story<Props> = args => {
24
- const [continuousSpeech, setContinuousSpeech] = React.useState(false);
24
+ const [microphoneMode, setMicrophoneMode] = React.useState<
25
+ 'HOLD_TO_TALK' | 'CONTINUOUS'
26
+ >('HOLD_TO_TALK');
25
27
  const [controlsPosition, setControlsPosition] = React.useState<
26
- 'bottom' | 'center' | 'hidden'
28
+ 'bottom' | 'center'
27
29
  >('bottom');
28
30
  return (
29
31
  <SettingsDrawer
30
32
  {...args}
31
- continuousSpeech={continuousSpeech}
32
- setContinuousSpeech={setContinuousSpeech}
33
+ microphoneMode={microphoneMode}
34
+ setMicrophoneMode={setMicrophoneMode}
33
35
  controlsPosition={controlsPosition}
34
36
  setControlsPosition={setControlsPosition}
35
37
  />
@@ -16,9 +16,9 @@ it('renders SettingsDrawer unchanged', () => {
16
16
  <SettingsDrawer
17
17
  open={false}
18
18
  onClose={jest.fn()}
19
- continuousSpeech={false}
19
+ microphoneMode="HOLD_TO_TALK"
20
+ setMicrophoneMode={jest.fn()}
20
21
  continuousSpeechTimeout={2}
21
- setContinuousSpeech={jest.fn()}
22
22
  setContinuousSpeechTimeout={jest.fn()}
23
23
  controlsPosition="bottom"
24
24
  setControlsPosition={jest.fn()}
@@ -34,9 +34,9 @@ it('renders SettingsDrawer open unchanged', () => {
34
34
  <SettingsDrawer
35
35
  open={true}
36
36
  onClose={jest.fn()}
37
- continuousSpeech={false}
37
+ microphoneMode="HOLD_TO_TALK"
38
+ setMicrophoneMode={jest.fn()}
38
39
  continuousSpeechTimeout={2}
39
- setContinuousSpeech={jest.fn()}
40
40
  setContinuousSpeechTimeout={jest.fn()}
41
41
  controlsPosition="bottom"
42
42
  setControlsPosition={jest.fn()}
@@ -52,9 +52,9 @@ it('renders SettingsDrawer open with continuous speech enabled unchanged', () =>
52
52
  <SettingsDrawer
53
53
  open={true}
54
54
  onClose={jest.fn()}
55
- continuousSpeech={true}
55
+ microphoneMode="CONTINUOUS"
56
+ setMicrophoneMode={jest.fn()}
56
57
  continuousSpeechTimeout={2}
57
- setContinuousSpeech={jest.fn()}
58
58
  setContinuousSpeechTimeout={jest.fn()}
59
59
  controlsPosition="bottom"
60
60
  setControlsPosition={jest.fn()}
@@ -70,9 +70,9 @@ it('renders SettingsDrawer open with non-default continuous speech timeout uncha
70
70
  <SettingsDrawer
71
71
  open={true}
72
72
  onClose={jest.fn()}
73
- continuousSpeech={false}
73
+ microphoneMode="CONTINUOUS"
74
+ setMicrophoneMode={jest.fn()}
74
75
  continuousSpeechTimeout={10}
75
- setContinuousSpeech={jest.fn()}
76
76
  setContinuousSpeechTimeout={jest.fn()}
77
77
  controlsPosition="bottom"
78
78
  setControlsPosition={jest.fn()}
@@ -89,9 +89,9 @@ it('renders SettingsDrawer for totem layout open unchanged', () => {
89
89
  layout="TOTEM"
90
90
  open={true}
91
91
  onClose={jest.fn()}
92
- continuousSpeech={false}
92
+ microphoneMode="HOLD_TO_TALK"
93
+ setMicrophoneMode={jest.fn()}
93
94
  continuousSpeechTimeout={2}
94
- setContinuousSpeech={jest.fn()}
95
95
  setContinuousSpeechTimeout={jest.fn()}
96
96
  controlsPosition="bottom"
97
97
  setControlsPosition={jest.fn()}
@@ -108,9 +108,9 @@ it('renders SettingsDrawer for totem layout open with controls at center unchang
108
108
  layout="TOTEM"
109
109
  open={true}
110
110
  onClose={jest.fn()}
111
- continuousSpeech={false}
111
+ microphoneMode="HOLD_TO_TALK"
112
+ setMicrophoneMode={jest.fn()}
112
113
  continuousSpeechTimeout={2}
113
- setContinuousSpeech={jest.fn()}
114
114
  setContinuousSpeechTimeout={jest.fn()}
115
115
  controlsPosition="center"
116
116
  setControlsPosition={jest.fn()}
@@ -127,9 +127,9 @@ it('renders SettingsDrawer for totem layout with continuous speech and hide emis
127
127
  layout="TOTEM"
128
128
  open={true}
129
129
  onClose={jest.fn()}
130
- continuousSpeech={true}
130
+ microphoneMode="HOLD_TO_TALK"
131
+ setMicrophoneMode={jest.fn()}
131
132
  continuousSpeechTimeout={2}
132
- setContinuousSpeech={jest.fn()}
133
133
  setContinuousSpeechTimeout={jest.fn()}
134
134
  controlsPosition="bottom"
135
135
  setControlsPosition={jest.fn()}
@@ -11,9 +11,9 @@ export interface Props {
11
11
  open: boolean;
12
12
  layout?: 'FULLPAGE' | 'TOTEM' | 'DEFAULT';
13
13
  onClose: () => void;
14
- continuousSpeech?: boolean;
14
+ microphoneMode?: 'HOLD_TO_TALK' | 'CONTINUOUS';
15
15
  continuousSpeechTimeout?: number;
16
- setContinuousSpeech: (value: boolean) => void;
16
+ setMicrophoneMode: (value: 'HOLD_TO_TALK' | 'CONTINUOUS') => void;
17
17
  setContinuousSpeechTimeout: (value: number) => void;
18
18
  controlsPosition?: 'center' | 'bottom';
19
19
  setControlsPosition: (value: 'center' | 'bottom') => void;
@@ -27,9 +27,9 @@ const SettingsDrawer = ({
27
27
  open,
28
28
  layout = 'DEFAULT',
29
29
  onClose,
30
- continuousSpeech,
30
+ microphoneMode = 'HOLD_TO_TALK',
31
31
  continuousSpeechTimeout,
32
- setContinuousSpeech,
32
+ setMicrophoneMode,
33
33
  setContinuousSpeechTimeout,
34
34
  controlsPosition,
35
35
  setControlsPosition,
@@ -46,31 +46,63 @@ const SettingsDrawer = ({
46
46
  title={t('widget.settings') || 'Settings'}
47
47
  description={t('write_and_speak.settingsHeaderLabel')}
48
48
  >
49
- <div className="memori-settings-drawer--field">
50
- <Checkbox
51
- label={t('write_and_speak.continuousSpeechLabel')}
52
- name="continuousSpeech"
53
- checked={continuousSpeech}
54
- onChange={e => {
55
- setContinuousSpeech(e.target.checked);
56
- setLocalConfig('continuousSpeech', e.target.checked);
57
- }}
58
- />
59
- </div>
60
-
61
- <div className="memori-settings-drawer--field">
62
- <Select
63
- label={t('write_and_speak.secondsLabel') || 'Seconds'}
64
- placeholder={t('write_and_speak.secondsLabel') || 'Seconds'}
65
- options={silenceSeconds.map(s => ({ value: s, label: s }))}
66
- value={continuousSpeechTimeout}
49
+ <div className="memori-settings-drawer--field controls">
50
+ <label htmlFor="#microphoneMode">
51
+ {t('write_and_speak.microphoneMode') || 'Microphone mode'}:
52
+ </label>
53
+ <RadioGroup
54
+ id="microphoneMode"
55
+ name="microphoneMode"
56
+ value={microphoneMode}
57
+ defaultValue={microphoneMode}
58
+ className="memori-settings-drawer--microphoneMode-radio"
67
59
  onChange={value => {
68
- setContinuousSpeechTimeout(value);
69
- setLocalConfig('continuousSpeechTimeout', value);
60
+ let micMode =
61
+ value === 'CONTINUOUS' ? 'CONTINUOUS' : 'HOLD_TO_TALK';
62
+
63
+ setMicrophoneMode(micMode as 'CONTINUOUS' | 'HOLD_TO_TALK');
64
+ setLocalConfig('microphoneMode', micMode);
70
65
  }}
71
- />
66
+ >
67
+ <RadioGroup.Option
68
+ value="HOLD_TO_TALK"
69
+ className="memori-settings-drawer--microphoneMode-radio-button"
70
+ >
71
+ {({ checked }) => (
72
+ <Button primary={checked}>
73
+ {t('write_and_speak.holdToSpeak') || 'Hold to speak'}
74
+ </Button>
75
+ )}
76
+ </RadioGroup.Option>
77
+ <RadioGroup.Option
78
+ value="CONTINUOUS"
79
+ className="memori-settings-drawer--microphoneMode-radio-button"
80
+ >
81
+ {({ checked }) => (
82
+ <Button primary={checked}>
83
+ {t('write_and_speak.continuousSpeechLabel') ||
84
+ 'Continuous speech'}
85
+ </Button>
86
+ )}
87
+ </RadioGroup.Option>
88
+ </RadioGroup>
72
89
  </div>
73
90
 
91
+ {microphoneMode === 'CONTINUOUS' && (
92
+ <div className="memori-settings-drawer--field">
93
+ <Select
94
+ label={t('write_and_speak.secondsLabel') || 'Seconds'}
95
+ placeholder={t('write_and_speak.secondsLabel') || 'Seconds'}
96
+ options={silenceSeconds.map(s => ({ value: s, label: s }))}
97
+ value={continuousSpeechTimeout}
98
+ onChange={value => {
99
+ setContinuousSpeechTimeout(value);
100
+ setLocalConfig('continuousSpeechTimeout', value);
101
+ }}
102
+ />
103
+ </div>
104
+ )}
105
+
74
106
  {layout === 'TOTEM' && (
75
107
  <>
76
108
  <div className="memori-settings-drawer--field controls">
@@ -102,7 +102,7 @@ const StartPanel: React.FC<Props> = ({
102
102
  {!!gamificationLevel?.badge?.length && (
103
103
  <div className="memori--gamification-badge">
104
104
  <Tooltip
105
- alignLeft
105
+ align="left"
106
106
  content={`${t('gamification.level')} ${
107
107
  gamificationLevel.badge
108
108
  }, ${gamificationLevel.points} ${t('gamification.points')}`}
@@ -119,7 +119,7 @@ const StartPanel: React.FC<Props> = ({
119
119
  )}
120
120
  {!!memori.enableCompletions && (
121
121
  <div className="memori--completions-enabled">
122
- <Tooltip alignLeft content={t('completionsEnabled')}>
122
+ <Tooltip align="left" content={t('completionsEnabled')}>
123
123
  <span aria-label={t('completionsEnabled') || 'Completions'}>
124
124
  <AI />
125
125
  </span>
@@ -128,7 +128,7 @@ const StartPanel: React.FC<Props> = ({
128
128
  )}
129
129
  {!!memori.nsfw && (
130
130
  <div className="memori--nsfw">
131
- <Tooltip alignLeft content={t('nsfw')}>
131
+ <Tooltip align="left" content={t('nsfw')}>
132
132
  <span title={t('nsfw') || 'NSFW'}>🔞</span>
133
133
  </Tooltip>
134
134
  </div>
@@ -1,22 +1,8 @@
1
1
  import React from 'react';
2
2
  import Spin from '../ui/Spin';
3
+ import { LayoutProps } from '../MemoriWidget/MemoriWidget';
3
4
 
4
- export interface Props {
5
- header?: JSX.Element | null;
6
- avatar: JSX.Element;
7
- chat?: JSX.Element | null;
8
- startPanel: JSX.Element;
9
- integrationStyle?: JSX.Element | null;
10
- integrationBackground?: JSX.Element | null;
11
- changeMode?: JSX.Element | null;
12
- poweredBy?: JSX.Element | null;
13
- sessionId?: string;
14
- hasUserActivatedSpeak?: boolean;
15
- showInstruct?: boolean;
16
- loading?: boolean;
17
- }
18
-
19
- const FullPageLayout: React.FC<Props> = ({
5
+ const FullPageLayout: React.FC<LayoutProps> = ({
20
6
  header,
21
7
  avatar,
22
8
  chat,