stream-chat-react-native-core 9.10.2 → 9.11.0-beta.3

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 (190) hide show
  1. package/lib/commonjs/ai-components/README.md +50 -2
  2. package/lib/commonjs/ai-components/index.js +24 -0
  3. package/lib/commonjs/ai-components/index.js.map +1 -1
  4. package/lib/commonjs/ai-components/message-composer/ComposerView.js +4 -2
  5. package/lib/commonjs/ai-components/message-composer/ComposerView.js.map +1 -1
  6. package/lib/commonjs/ai-components/message-composer/SpeechToTextButton.js +4 -1
  7. package/lib/commonjs/ai-components/message-composer/SpeechToTextButton.js.map +1 -1
  8. package/lib/commonjs/ai-components/native/AIDictation.js +6 -0
  9. package/lib/commonjs/ai-components/native/AIDictation.js.map +1 -1
  10. package/lib/commonjs/ai-components/store/dictation/store.js +4 -2
  11. package/lib/commonjs/ai-components/store/dictation/store.js.map +1 -1
  12. package/lib/commonjs/ai-components/store/dictation/useDictationState.js +4 -2
  13. package/lib/commonjs/ai-components/store/dictation/useDictationState.js.map +1 -1
  14. package/lib/commonjs/ai-components/store/index.js +22 -0
  15. package/lib/commonjs/ai-components/store/index.js.map +1 -1
  16. package/lib/commonjs/ai-components/theme.js +3 -0
  17. package/lib/commonjs/ai-components/theme.js.map +1 -1
  18. package/lib/commonjs/ai-components/transcription/useDictation.js +7 -3
  19. package/lib/commonjs/ai-components/transcription/useDictation.js.map +1 -1
  20. package/lib/commonjs/ai-components/transcription/useDictationAudioLevel.js +32 -0
  21. package/lib/commonjs/ai-components/transcription/useDictationAudioLevel.js.map +1 -0
  22. package/lib/commonjs/ai-components/voice-visualization/VoiceGlow.js +79 -0
  23. package/lib/commonjs/ai-components/voice-visualization/VoiceGlow.js.map +1 -0
  24. package/lib/commonjs/ai-components/voice-visualization/VoiceVisualizationView.js +147 -0
  25. package/lib/commonjs/ai-components/voice-visualization/VoiceVisualizationView.js.map +1 -0
  26. package/lib/commonjs/ai-components/voice-visualization/VoiceWave.js +58 -0
  27. package/lib/commonjs/ai-components/voice-visualization/VoiceWave.js.map +1 -0
  28. package/lib/commonjs/ai-components/voice-visualization/bounce.js +60 -0
  29. package/lib/commonjs/ai-components/voice-visualization/bounce.js.map +1 -0
  30. package/lib/commonjs/ai-components/voice-visualization/driver.js +208 -0
  31. package/lib/commonjs/ai-components/voice-visualization/driver.js.map +1 -0
  32. package/lib/commonjs/ai-components/voice-visualization/glowShader.js +218 -0
  33. package/lib/commonjs/ai-components/voice-visualization/glowShader.js.map +1 -0
  34. package/lib/commonjs/ai-components/voice-visualization/index.js +18 -0
  35. package/lib/commonjs/ai-components/voice-visualization/index.js.map +1 -0
  36. package/lib/commonjs/ai-components/voice-visualization/math.js +70 -0
  37. package/lib/commonjs/ai-components/voice-visualization/math.js.map +1 -0
  38. package/lib/commonjs/ai-components/voice-visualization/palette.js +47 -0
  39. package/lib/commonjs/ai-components/voice-visualization/palette.js.map +1 -0
  40. package/lib/commonjs/ai-components/voice-visualization/simplex.js +77 -0
  41. package/lib/commonjs/ai-components/voice-visualization/simplex.js.map +1 -0
  42. package/lib/commonjs/ai-components/voice-visualization/styles.js +33 -0
  43. package/lib/commonjs/ai-components/voice-visualization/styles.js.map +1 -0
  44. package/lib/commonjs/ai-components/voice-visualization/types.js +4 -0
  45. package/lib/commonjs/ai-components/voice-visualization/types.js.map +1 -0
  46. package/lib/commonjs/ai-components/voice-visualization/useReduceMotion.js +28 -0
  47. package/lib/commonjs/ai-components/voice-visualization/useReduceMotion.js.map +1 -0
  48. package/lib/commonjs/ai-components/voice-visualization/useVoiceVisualization.js +129 -0
  49. package/lib/commonjs/ai-components/voice-visualization/useVoiceVisualization.js.map +1 -0
  50. package/lib/commonjs/ai-components/voice-visualization/waveGeometry.js +124 -0
  51. package/lib/commonjs/ai-components/voice-visualization/waveGeometry.js.map +1 -0
  52. package/lib/commonjs/components/MessageInput/MessageComposer.js +2 -1
  53. package/lib/commonjs/components/MessageInput/MessageComposer.js.map +1 -1
  54. package/lib/commonjs/version.json +1 -1
  55. package/lib/module/ai-components/README.md +50 -2
  56. package/lib/module/ai-components/index.js +24 -0
  57. package/lib/module/ai-components/index.js.map +1 -1
  58. package/lib/module/ai-components/message-composer/ComposerView.js +4 -2
  59. package/lib/module/ai-components/message-composer/ComposerView.js.map +1 -1
  60. package/lib/module/ai-components/message-composer/SpeechToTextButton.js +4 -1
  61. package/lib/module/ai-components/message-composer/SpeechToTextButton.js.map +1 -1
  62. package/lib/module/ai-components/native/AIDictation.js +6 -0
  63. package/lib/module/ai-components/native/AIDictation.js.map +1 -1
  64. package/lib/module/ai-components/store/dictation/store.js +4 -2
  65. package/lib/module/ai-components/store/dictation/store.js.map +1 -1
  66. package/lib/module/ai-components/store/dictation/useDictationState.js +4 -2
  67. package/lib/module/ai-components/store/dictation/useDictationState.js.map +1 -1
  68. package/lib/module/ai-components/store/index.js +22 -0
  69. package/lib/module/ai-components/store/index.js.map +1 -1
  70. package/lib/module/ai-components/theme.js +3 -0
  71. package/lib/module/ai-components/theme.js.map +1 -1
  72. package/lib/module/ai-components/transcription/useDictation.js +7 -3
  73. package/lib/module/ai-components/transcription/useDictation.js.map +1 -1
  74. package/lib/module/ai-components/transcription/useDictationAudioLevel.js +32 -0
  75. package/lib/module/ai-components/transcription/useDictationAudioLevel.js.map +1 -0
  76. package/lib/module/ai-components/voice-visualization/VoiceGlow.js +79 -0
  77. package/lib/module/ai-components/voice-visualization/VoiceGlow.js.map +1 -0
  78. package/lib/module/ai-components/voice-visualization/VoiceVisualizationView.js +147 -0
  79. package/lib/module/ai-components/voice-visualization/VoiceVisualizationView.js.map +1 -0
  80. package/lib/module/ai-components/voice-visualization/VoiceWave.js +58 -0
  81. package/lib/module/ai-components/voice-visualization/VoiceWave.js.map +1 -0
  82. package/lib/module/ai-components/voice-visualization/bounce.js +60 -0
  83. package/lib/module/ai-components/voice-visualization/bounce.js.map +1 -0
  84. package/lib/module/ai-components/voice-visualization/driver.js +208 -0
  85. package/lib/module/ai-components/voice-visualization/driver.js.map +1 -0
  86. package/lib/module/ai-components/voice-visualization/glowShader.js +218 -0
  87. package/lib/module/ai-components/voice-visualization/glowShader.js.map +1 -0
  88. package/lib/module/ai-components/voice-visualization/index.js +18 -0
  89. package/lib/module/ai-components/voice-visualization/index.js.map +1 -0
  90. package/lib/module/ai-components/voice-visualization/math.js +70 -0
  91. package/lib/module/ai-components/voice-visualization/math.js.map +1 -0
  92. package/lib/module/ai-components/voice-visualization/palette.js +47 -0
  93. package/lib/module/ai-components/voice-visualization/palette.js.map +1 -0
  94. package/lib/module/ai-components/voice-visualization/simplex.js +77 -0
  95. package/lib/module/ai-components/voice-visualization/simplex.js.map +1 -0
  96. package/lib/module/ai-components/voice-visualization/styles.js +33 -0
  97. package/lib/module/ai-components/voice-visualization/styles.js.map +1 -0
  98. package/lib/module/ai-components/voice-visualization/types.js +4 -0
  99. package/lib/module/ai-components/voice-visualization/types.js.map +1 -0
  100. package/lib/module/ai-components/voice-visualization/useReduceMotion.js +28 -0
  101. package/lib/module/ai-components/voice-visualization/useReduceMotion.js.map +1 -0
  102. package/lib/module/ai-components/voice-visualization/useVoiceVisualization.js +129 -0
  103. package/lib/module/ai-components/voice-visualization/useVoiceVisualization.js.map +1 -0
  104. package/lib/module/ai-components/voice-visualization/waveGeometry.js +124 -0
  105. package/lib/module/ai-components/voice-visualization/waveGeometry.js.map +1 -0
  106. package/lib/module/components/MessageInput/MessageComposer.js +2 -1
  107. package/lib/module/components/MessageInput/MessageComposer.js.map +1 -1
  108. package/lib/module/version.json +1 -1
  109. package/lib/typescript/ai-components/index.d.ts +3 -1
  110. package/lib/typescript/ai-components/index.d.ts.map +1 -1
  111. package/lib/typescript/ai-components/message-composer/ComposerView.d.ts.map +1 -1
  112. package/lib/typescript/ai-components/native/AIDictation.d.ts +6 -0
  113. package/lib/typescript/ai-components/native/AIDictation.d.ts.map +1 -1
  114. package/lib/typescript/ai-components/store/dictation/store.d.ts +7 -0
  115. package/lib/typescript/ai-components/store/dictation/store.d.ts.map +1 -1
  116. package/lib/typescript/ai-components/store/dictation/useDictationState.d.ts +1 -0
  117. package/lib/typescript/ai-components/store/dictation/useDictationState.d.ts.map +1 -1
  118. package/lib/typescript/ai-components/store/index.d.ts +2 -0
  119. package/lib/typescript/ai-components/store/index.d.ts.map +1 -1
  120. package/lib/typescript/ai-components/theme.d.ts +13 -0
  121. package/lib/typescript/ai-components/theme.d.ts.map +1 -1
  122. package/lib/typescript/ai-components/transcription/useDictation.d.ts +9 -1
  123. package/lib/typescript/ai-components/transcription/useDictation.d.ts.map +1 -1
  124. package/lib/typescript/ai-components/transcription/useDictationAudioLevel.d.ts +11 -0
  125. package/lib/typescript/ai-components/transcription/useDictationAudioLevel.d.ts.map +1 -0
  126. package/lib/typescript/ai-components/voice-visualization/VoiceGlow.d.ts +13 -0
  127. package/lib/typescript/ai-components/voice-visualization/VoiceGlow.d.ts.map +1 -0
  128. package/lib/typescript/ai-components/voice-visualization/VoiceVisualizationView.d.ts +26 -0
  129. package/lib/typescript/ai-components/voice-visualization/VoiceVisualizationView.d.ts.map +1 -0
  130. package/lib/typescript/ai-components/voice-visualization/VoiceWave.d.ts +13 -0
  131. package/lib/typescript/ai-components/voice-visualization/VoiceWave.d.ts.map +1 -0
  132. package/lib/typescript/ai-components/voice-visualization/bounce.d.ts +20 -0
  133. package/lib/typescript/ai-components/voice-visualization/bounce.d.ts.map +1 -0
  134. package/lib/typescript/ai-components/voice-visualization/driver.d.ts +96 -0
  135. package/lib/typescript/ai-components/voice-visualization/driver.d.ts.map +1 -0
  136. package/lib/typescript/ai-components/voice-visualization/glowShader.d.ts +7 -0
  137. package/lib/typescript/ai-components/voice-visualization/glowShader.d.ts.map +1 -0
  138. package/lib/typescript/ai-components/voice-visualization/index.d.ts +4 -0
  139. package/lib/typescript/ai-components/voice-visualization/index.d.ts.map +1 -0
  140. package/lib/typescript/ai-components/voice-visualization/math.d.ts +26 -0
  141. package/lib/typescript/ai-components/voice-visualization/math.d.ts.map +1 -0
  142. package/lib/typescript/ai-components/voice-visualization/palette.d.ts +18 -0
  143. package/lib/typescript/ai-components/voice-visualization/palette.d.ts.map +1 -0
  144. package/lib/typescript/ai-components/voice-visualization/simplex.d.ts +11 -0
  145. package/lib/typescript/ai-components/voice-visualization/simplex.d.ts.map +1 -0
  146. package/lib/typescript/ai-components/voice-visualization/styles.d.ts +24 -0
  147. package/lib/typescript/ai-components/voice-visualization/styles.d.ts.map +1 -0
  148. package/lib/typescript/ai-components/voice-visualization/types.d.ts +45 -0
  149. package/lib/typescript/ai-components/voice-visualization/types.d.ts.map +1 -0
  150. package/lib/typescript/ai-components/voice-visualization/useReduceMotion.d.ts +10 -0
  151. package/lib/typescript/ai-components/voice-visualization/useReduceMotion.d.ts.map +1 -0
  152. package/lib/typescript/ai-components/voice-visualization/useVoiceVisualization.d.ts +18 -0
  153. package/lib/typescript/ai-components/voice-visualization/useVoiceVisualization.d.ts.map +1 -0
  154. package/lib/typescript/ai-components/voice-visualization/waveGeometry.d.ts +25 -0
  155. package/lib/typescript/ai-components/voice-visualization/waveGeometry.d.ts.map +1 -0
  156. package/lib/typescript/components/MessageInput/MessageComposer.d.ts.map +1 -1
  157. package/package.json +2 -2
  158. package/src/ai-components/README.md +50 -2
  159. package/src/ai-components/__tests__/VoiceVisualization.test.tsx +208 -0
  160. package/src/ai-components/__tests__/dictation.test.tsx +58 -1
  161. package/src/ai-components/__tests__/voiceDriver.test.ts +317 -0
  162. package/src/ai-components/__tests__/voicePalette.test.ts +124 -0
  163. package/src/ai-components/index.ts +3 -1
  164. package/src/ai-components/message-composer/ComposerView.tsx +6 -3
  165. package/src/ai-components/message-composer/SpeechToTextButton.tsx +5 -2
  166. package/src/ai-components/native/AIDictation.ts +11 -0
  167. package/src/ai-components/store/dictation/store.ts +9 -0
  168. package/src/ai-components/store/dictation/useDictationState.ts +2 -1
  169. package/src/ai-components/store/index.ts +2 -0
  170. package/src/ai-components/theme.ts +16 -0
  171. package/src/ai-components/transcription/useDictation.ts +16 -3
  172. package/src/ai-components/transcription/useDictationAudioLevel.ts +38 -0
  173. package/src/ai-components/voice-visualization/VoiceGlow.tsx +79 -0
  174. package/src/ai-components/voice-visualization/VoiceVisualizationView.tsx +179 -0
  175. package/src/ai-components/voice-visualization/VoiceWave.tsx +67 -0
  176. package/src/ai-components/voice-visualization/bounce.ts +61 -0
  177. package/src/ai-components/voice-visualization/driver.ts +240 -0
  178. package/src/ai-components/voice-visualization/glowShader.ts +227 -0
  179. package/src/ai-components/voice-visualization/index.ts +3 -0
  180. package/src/ai-components/voice-visualization/math.ts +73 -0
  181. package/src/ai-components/voice-visualization/palette.ts +59 -0
  182. package/src/ai-components/voice-visualization/simplex.ts +92 -0
  183. package/src/ai-components/voice-visualization/styles.ts +42 -0
  184. package/src/ai-components/voice-visualization/types.ts +44 -0
  185. package/src/ai-components/voice-visualization/useReduceMotion.ts +34 -0
  186. package/src/ai-components/voice-visualization/useVoiceVisualization.ts +153 -0
  187. package/src/ai-components/voice-visualization/waveGeometry.ts +137 -0
  188. package/src/components/ChannelPreview/hooks/__tests__/useChannelPreviewPollLabel.test.tsx +1 -1
  189. package/src/components/MessageInput/MessageComposer.tsx +4 -2
  190. package/src/version.json +1 -1
@@ -0,0 +1,317 @@
1
+ import {
2
+ bounceScale,
3
+ createBounce,
4
+ easeOut,
5
+ scaleAt,
6
+ startBounce,
7
+ } from '../voice-visualization/bounce';
8
+ import {
9
+ advanceDriver,
10
+ createDriverState,
11
+ driverReact,
12
+ driverScale,
13
+ ease,
14
+ hoverDriver,
15
+ LEVEL_RESPONSE,
16
+ type LevelResponse,
17
+ NO_TAP,
18
+ tapDriver,
19
+ type VoiceDriverInput,
20
+ type VoiceDriverState,
21
+ } from '../voice-visualization/driver';
22
+ import type { Hsv, VoiceAgentState } from '../voice-visualization/types';
23
+
24
+ // Ported from stream-chat-android's VoiceVisualizationDriverTest and VoiceVisualizationBounceTest,
25
+ // so the platforms keep moving alike.
26
+
27
+ const blue: Hsv = { hue: 218 / 360, saturation: 1, value: 1 };
28
+ const red: Hsv = { hue: 0, saturation: 1, value: 1 };
29
+ const FRAME = 1 / 60;
30
+
31
+ const input = (
32
+ state: VoiceAgentState,
33
+ overrides: Partial<Omit<VoiceDriverInput, 'state'>> = {},
34
+ ): VoiceDriverInput => ({
35
+ audioLevel: null,
36
+ colors: [blue],
37
+ expressivity: 1,
38
+ reduceMotion: false,
39
+ state,
40
+ ...overrides,
41
+ });
42
+
43
+ /** A driver with the input it reads each frame, as the view keeps them. */
44
+ const createDriver = (initial: VoiceDriverInput, response?: LevelResponse) => {
45
+ const driver = { input: initial, state: createDriverState(initial, response) };
46
+ return {
47
+ ...driver,
48
+ advance: (seconds: number) => advanceDriver(driver.state, driver.input, seconds),
49
+ run: (seconds: number) => {
50
+ for (let i = 0; i < Math.floor(seconds / FRAME); i++) {
51
+ advanceDriver(driver.state, driver.input, FRAME);
52
+ }
53
+ },
54
+ setInput: (next: VoiceDriverInput) => {
55
+ driver.input = next;
56
+ },
57
+ get d(): VoiceDriverState {
58
+ return driver.state;
59
+ },
60
+ get input() {
61
+ return driver.input;
62
+ },
63
+ };
64
+ };
65
+
66
+ const expectNear = (actual: number, expected: number, tolerance: number) =>
67
+ expect(Math.abs(actual - expected)).toBeLessThanOrEqual(tolerance);
68
+
69
+ describe('voice visualization driver', () => {
70
+ it('starts settled on the state', () => {
71
+ const { d } = createDriver(input('speaking'));
72
+
73
+ expect(driverReact(d)).toBe(1);
74
+ expect(d.load).toBe(0);
75
+ expect(d.flow).toBe(0);
76
+ expect(d.bright).toBe(1);
77
+ expect(d.time).toBe(0);
78
+ expect(d.count).toBe(1);
79
+ expect(driverScale(d)).toBe(1);
80
+ expect(d.tapAge).toBe(NO_TAP);
81
+ expect(d.colors[0]).toEqual(blue);
82
+ expect(d.colors[4]).toEqual(blue);
83
+ });
84
+
85
+ it('does not share its colors with the caller’s palette', () => {
86
+ const palette = [{ ...blue }];
87
+ const driver = createDriver(input('idle', { colors: palette }));
88
+ driver.setInput(input('idle', { colors: [red] }));
89
+
90
+ driver.run(1);
91
+
92
+ expect(palette[0]).toEqual(blue);
93
+ });
94
+
95
+ it('eases toward the motion of a new state', () => {
96
+ const driver = createDriver(input('idle'));
97
+ driver.setInput(input('connecting'));
98
+
99
+ driver.advance(FRAME);
100
+ expectNear(driver.d.load, 0.08, 1e-4);
101
+ driver.run(2);
102
+ expectNear(driver.d.load, 1, 1e-3);
103
+ expectNear(driverReact(driver.d), 0, 1e-3);
104
+ });
105
+
106
+ it('eases the same at any frame rate', () => {
107
+ const at60 = createDriver(input('idle'));
108
+ const at120 = createDriver(input('idle'));
109
+ at60.setInput(input('speaking'));
110
+ at120.setInput(input('speaking'));
111
+
112
+ for (let i = 0; i < 30; i++) at60.advance(1 / 60);
113
+ for (let i = 0; i < 60; i++) at120.advance(1 / 120);
114
+
115
+ expectNear(driverReact(at60.d), driverReact(at120.d), 1e-4);
116
+ expectNear(at60.d.time, at120.d.time, 1e-2);
117
+ });
118
+
119
+ it('clamps a long frame', () => {
120
+ const driver = createDriver(input('idle'));
121
+
122
+ driver.advance(5);
123
+
124
+ expectNear(driver.d.time, 0.05 * 0.4, 1e-5);
125
+ });
126
+
127
+ it('turns the flow only while thinking', () => {
128
+ const driver = createDriver(input('speaking'));
129
+ driver.run(1);
130
+ expect(driver.d.flowSpin).toBe(0);
131
+
132
+ driver.setInput(input('thinking'));
133
+ driver.run(1);
134
+ expect(driver.d.flowSpin).toBeGreaterThan(0);
135
+ });
136
+
137
+ it('keeps the colors turning in every state', () => {
138
+ const driver = createDriver(input('idle'));
139
+
140
+ driver.run(1);
141
+
142
+ expect(driver.d.orbSpin).toBeGreaterThan(0);
143
+ });
144
+
145
+ it('scales amplitude and speed with expressivity', () => {
146
+ const lively = createDriver(input('speaking', { expressivity: 2 }));
147
+ const still = createDriver(input('speaking', { expressivity: 0 }));
148
+
149
+ expectNear(driverReact(lively.d), 1.85, 1e-5);
150
+ expectNear(driverReact(still.d), 0.15, 1e-5);
151
+ lively.advance(0.05);
152
+ expectNear(lively.d.time, 0.05 * 1.25 * 1.4, 1e-5);
153
+ });
154
+
155
+ it('slows time and ignores the pointer under reduced motion', () => {
156
+ const driver = createDriver(input('idle', { reduceMotion: true }));
157
+
158
+ tapDriver(driver.d, driver.input, 0.1, 0.1);
159
+ hoverDriver(driver.d, driver.input, 0.1, 0.1);
160
+ driver.advance(0.05);
161
+
162
+ expectNear(driver.d.time, 0.05 * 0.07 * 0.4, 1e-6);
163
+ expect(driver.d.tapAge).toBeGreaterThan(NO_TAP);
164
+ expect(driverScale(driver.d)).toBe(1);
165
+ expect(driver.d.hoverAmount).toBe(0);
166
+ });
167
+
168
+ it('stops hovering once motion is reduced', () => {
169
+ const driver = createDriver(input('idle'));
170
+ hoverDriver(driver.d, driver.input, 0.1, 0.1);
171
+ driver.run(1);
172
+
173
+ driver.setInput(input('idle', { reduceMotion: true }));
174
+ driver.run(1);
175
+
176
+ expectNear(driver.d.hoverAmount, 0, 1e-3);
177
+ });
178
+
179
+ it('ripples from a tap and bounces', () => {
180
+ const driver = createDriver(input('idle'));
181
+
182
+ tapDriver(driver.d, driver.input, 0.1, -0.2);
183
+ expect(driver.d.tapX).toBe(0.1);
184
+ expect(driver.d.tapY).toBe(-0.2);
185
+ expect(driver.d.tapAge).toBe(0);
186
+
187
+ driver.advance(0.05);
188
+ expectNear(driver.d.tapAge, 0.05, 1e-6);
189
+ expect(driverScale(driver.d)).toBeLessThan(1);
190
+ driver.run(0.5);
191
+ expect(driverScale(driver.d)).toBe(1);
192
+ });
193
+
194
+ it('eases hovering in and out', () => {
195
+ const driver = createDriver(input('idle'));
196
+
197
+ hoverDriver(driver.d, driver.input, 0.2, 0.1);
198
+ driver.run(1);
199
+ expectNear(driver.d.hoverAmount, 1, 1e-3);
200
+ expect(driver.d.hoverX).toBe(0.2);
201
+
202
+ hoverDriver(driver.d, driver.input, null, null);
203
+ driver.run(1);
204
+ expectNear(driver.d.hoverAmount, 0, 1e-3);
205
+ expect(driver.d.hoverX).toBe(0.2);
206
+ });
207
+
208
+ it('lifts the motion to a live audio level', () => {
209
+ const driver = createDriver(input('idle', { audioLevel: 0.8 }));
210
+
211
+ driver.run(2);
212
+ expectNear(driver.d.mic, 0.8, 1e-3);
213
+ expectNear(driver.d.voice, 1, 1e-3);
214
+ expectNear(driverReact(driver.d), 0.8, 1e-3);
215
+
216
+ driver.setInput(input('idle'));
217
+ driver.run(2);
218
+ expectNear(driver.d.voice, 0, 1e-3);
219
+ expectNear(driverReact(driver.d), 0.12, 1e-3);
220
+ });
221
+
222
+ const settled = (response: LevelResponse, audioLevel: number) => {
223
+ const driver = createDriver(input('idle', { audioLevel }), response);
224
+ driver.run(1);
225
+ return driver;
226
+ };
227
+
228
+ it('catches up with a louder level within a word when smooth', () => {
229
+ const driver = settled(LEVEL_RESPONSE.smooth, 0);
230
+
231
+ driver.setInput(input('idle', { audioLevel: 1 }));
232
+ driver.run(0.1);
233
+ expect(driver.d.mic).toBeGreaterThanOrEqual(0.5);
234
+ expect(driver.d.mic).toBeLessThanOrEqual(0.8);
235
+ driver.run(0.3);
236
+ expect(driver.d.mic).toBeGreaterThan(0.95);
237
+ });
238
+
239
+ it('lets a quieter level fall away over a pause, not a syllable, when smooth', () => {
240
+ const driver = settled(LEVEL_RESPONSE.smooth, 1);
241
+
242
+ driver.setInput(input('idle', { audioLevel: 0 }));
243
+ driver.run(0.1);
244
+ expect(driver.d.mic).toBeGreaterThan(0.7);
245
+ driver.run(0.9);
246
+ expect(driver.d.mic).toBeLessThan(0.1);
247
+ });
248
+
249
+ it('follows a quieter level within a syllable when steady', () => {
250
+ const driver = settled(LEVEL_RESPONSE.steady, 1);
251
+
252
+ driver.setInput(input('idle', { audioLevel: 0 }));
253
+ driver.run(0.1);
254
+ expect(driver.d.mic).toBeLessThan(0.1);
255
+ });
256
+
257
+ it('eases a hue the short way round', () => {
258
+ const driver = createDriver(
259
+ input('idle', { colors: [{ hue: 0.95, saturation: 1, value: 1 }] }),
260
+ );
261
+ driver.setInput(input('idle', { colors: [{ hue: 0.05, saturation: 1, value: 1 }] }));
262
+
263
+ driver.advance(FRAME);
264
+ expectNear(driver.d.colors[0].hue, 0.962, 1e-4);
265
+ driver.run(2);
266
+ expectNear(driver.d.colors[0].hue, 0.05, 1e-3);
267
+ });
268
+
269
+ it('repeats the first color in empty slots and crossfades the count', () => {
270
+ const driver = createDriver(input('idle', { colors: [blue, red] }));
271
+ expect(driver.d.colors[1]).toEqual(red);
272
+ expect(driver.d.colors[2]).toEqual(blue);
273
+
274
+ driver.setInput(input('idle', { colors: [blue, red, red] }));
275
+ driver.advance(FRAME);
276
+ expectNear(driver.d.count, 2.12, 1e-4);
277
+ });
278
+
279
+ it('moves the per-frame share at 60 Hz', () => {
280
+ expectNear(ease(0.08, 1 / 60), 0.08, 1e-5);
281
+ expect(ease(0.08, 0)).toBe(0);
282
+ });
283
+ });
284
+
285
+ describe('voice visualization bounce', () => {
286
+ it('follows the keyframes', () => {
287
+ expect(scaleAt(0)).toBe(1);
288
+ expectNear(scaleAt(0.15), 0.965, 1e-5);
289
+ expectNear(scaleAt(0.3), 0.93, 1e-5);
290
+ expectNear(scaleAt(0.62), 1.03, 1e-5);
291
+ expectNear(scaleAt(1), 1, 1e-5);
292
+ });
293
+
294
+ it('times them with the CSS ease-out', () => {
295
+ expectNear(easeOut(0), 0, 1e-4);
296
+ expectNear(easeOut(0.5), 0.684, 2e-3);
297
+ expectNear(easeOut(1), 1, 1e-4);
298
+ });
299
+
300
+ it('dips, overshoots and settles', () => {
301
+ const bounce = createBounce();
302
+ expect(bounceScale(bounce)).toBe(1);
303
+
304
+ startBounce(bounce);
305
+ const scales = Array.from({ length: 21 }, () => {
306
+ const scale = bounceScale(bounce);
307
+ bounce.age += 1 / 60;
308
+ return scale;
309
+ });
310
+
311
+ expectNear(scales[0], 1, 1e-9);
312
+ expectNear(Math.min(...scales), 0.93, 0.01);
313
+ expectNear(Math.max(...scales), 1.03, 0.01);
314
+ expect(scales.indexOf(Math.min(...scales))).toBeLessThan(scales.indexOf(Math.max(...scales)));
315
+ expect(bounceScale(bounce)).toBe(1);
316
+ });
317
+ });
@@ -0,0 +1,124 @@
1
+ import { audioLevelInput, expressivityInput, speechEnv } from '../voice-visualization/math';
2
+ import { luminance, parseColor, rgbToHsv, toHsvPalette } from '../voice-visualization/palette';
3
+ import { snoise } from '../voice-visualization/simplex';
4
+ import { GLOW_STYLE, WAVE_STYLE } from '../voice-visualization/styles';
5
+
6
+ describe('voice visualization palette', () => {
7
+ it('keeps a color’s full hue, saturation and value', () => {
8
+ const hsv = rgbToHsv([0, 95 / 255, 1]);
9
+
10
+ expect(hsv.hue * 360).toBeCloseTo(217.6, 1);
11
+ expect(hsv.saturation).toBeCloseTo(1, 4);
12
+ expect(hsv.value).toBeCloseTo(1, 4);
13
+ });
14
+
15
+ it('keeps a muted color muted', () => {
16
+ // hsv(108°, 25%, 60%)
17
+ const hsv = rgbToHsv([0.48, 0.6, 0.45]);
18
+
19
+ expect(hsv.hue).toBeCloseTo(0.3, 2);
20
+ expect(hsv.saturation).toBeCloseTo(0.25, 2);
21
+ expect(hsv.value).toBeCloseTo(0.6, 4);
22
+ });
23
+
24
+ it('gives a gray no saturation', () => {
25
+ const hsv = rgbToHsv([0.5, 0.5, 0.5]);
26
+
27
+ expect(hsv.saturation).toBe(0);
28
+ expect(hsv.value).toBe(0.5);
29
+ });
30
+
31
+ it.each([
32
+ ['#005fff', [0, 95 / 255, 1]],
33
+ ['#0af', [0, 170 / 255, 1]],
34
+ ['#005fff80', [0, 95 / 255, 1]],
35
+ ['rgb(0, 95, 255)', [0, 95 / 255, 1]],
36
+ ['rgba(0, 95, 255, 0.5)', [0, 95 / 255, 1]],
37
+ ['hsl(0, 100%, 50%)', [1, 0, 0]],
38
+ ['rebeccapurple', [0.4, 0.2, 0.6]],
39
+ ['#000000', [0, 0, 0]],
40
+ ])('reads %s', (color, expected) => {
41
+ const rgb = parseColor(color);
42
+ expect(rgb).not.toBeNull();
43
+ rgb?.forEach((channel, i) => expect(channel).toBeCloseTo(expected[i], 2));
44
+ });
45
+
46
+ it('skips colors it cannot read', () => {
47
+ expect(parseColor('not-a-color')).toBeNull();
48
+ expect(parseColor('#12345')).toBeNull();
49
+ expect(parseColor('')).toBeNull();
50
+ expect(toHsvPalette(['nope', '#ff0000'])).toHaveLength(1);
51
+ });
52
+
53
+ it('keeps only the first five colors', () => {
54
+ expect(toHsvPalette(Array.from({ length: 7 }, () => '#ff0000'))).toHaveLength(5);
55
+ });
56
+
57
+ it('measures luminance for the light or dark look', () => {
58
+ expect(luminance('#ffffff')).toBeCloseTo(1, 4);
59
+ expect(luminance('#000000')).toBe(0);
60
+ expect(luminance('#1c1e22')).toBeLessThan(0.5);
61
+ expect(luminance('')).toBeNull();
62
+ });
63
+ });
64
+
65
+ describe('voice visualization inputs', () => {
66
+ it('clamps expressivity to 0-2 and replaces one that is not a number', () => {
67
+ expect(expressivityInput(3)).toBe(2);
68
+ expect(expressivityInput(-1)).toBe(0);
69
+ expect(expressivityInput(0.5)).toBe(0.5);
70
+ expect(expressivityInput(Number.NaN)).toBe(1);
71
+ expect(expressivityInput(Number.POSITIVE_INFINITY)).toBe(1);
72
+ expect(expressivityInput(undefined)).toBe(1);
73
+ });
74
+
75
+ it('clamps the audio level to 0-1 and treats one that is not a number as no audio', () => {
76
+ expect(audioLevelInput(2)).toBe(1);
77
+ expect(audioLevelInput(-1)).toBe(0);
78
+ expect(audioLevelInput(0.4)).toBe(0.4);
79
+ expect(audioLevelInput(Number.NaN)).toBeNull();
80
+ expect(audioLevelInput(null)).toBeNull();
81
+ expect(audioLevelInput(undefined)).toBeNull();
82
+ });
83
+ });
84
+
85
+ describe('voice visualization noise', () => {
86
+ it('matches the shader’s simplex noise as a GPU computes it', () => {
87
+ // Read back from the shader's own snoise on a GPU (stream-chat-react's fixtures).
88
+ const gpu: Array<[number, number, number, number]> = [
89
+ [0.3, 21, 0, 0.7696],
90
+ [1.7, 5, 0, 0.208],
91
+ [0.12, 0, 0, 0.049],
92
+ [2.5, 9, 0, 0.3869],
93
+ [10.2, 14, 0, 0.603],
94
+ [3.3, 1.1, 0.9, -0.7541],
95
+ [0.05, 0.05, 0.05, -0.5481],
96
+ ];
97
+ for (const [x, y, z, expected] of gpu) {
98
+ expect(snoise(x, y, z)).toBeCloseTo(expected, 2);
99
+ }
100
+ });
101
+
102
+ it('gives speech phrases and pauses', () => {
103
+ const samples = Array.from({ length: 2000 }, (_, i) => speechEnv(i * 0.01));
104
+
105
+ expect(Math.min(...samples)).toBe(0);
106
+ expect(Math.max(...samples)).toBeGreaterThan(0.6);
107
+ expect(Math.max(...samples)).toBeLessThanOrEqual(1);
108
+ });
109
+ });
110
+
111
+ describe('voice visualization hit areas', () => {
112
+ it('takes the globe within a third of the shorter side', () => {
113
+ expect(GLOW_STYLE.contains(0, 0)).toBe(true);
114
+ expect(GLOW_STYLE.contains(0.2, 0.2)).toBe(true);
115
+ expect(GLOW_STYLE.contains(0.3, 0.3)).toBe(false);
116
+ });
117
+
118
+ it('takes the string across, and only near its line', () => {
119
+ expect(WAVE_STYLE.contains(0.45, 0)).toBe(true);
120
+ expect(WAVE_STYLE.contains(0, 0.1)).toBe(true);
121
+ expect(WAVE_STYLE.contains(0, 0.2)).toBe(false);
122
+ expect(WAVE_STYLE.contains(0.55, 0)).toBe(false);
123
+ });
124
+ });
@@ -5,6 +5,7 @@ export * from './charts';
5
5
  export * from './message-composer';
6
6
  export * from './StreamingMessageView';
7
7
  export * from './AITypingIndicatorView';
8
+ export * from './voice-visualization';
8
9
 
9
10
  // components
10
11
  export * from './components';
@@ -15,7 +16,8 @@ export * from './contexts';
15
16
  // services
16
17
  export * from './services';
17
18
  export * from './transcription/useDictation';
18
- export { isDictationAvailable } from './native/AIDictation';
19
+ export * from './transcription/useDictationAudioLevel';
20
+ export { isDictationAvailable, type DictationLevel } from './native/AIDictation';
19
21
 
20
22
  // state-stores
21
23
  export * from './store';
@@ -19,7 +19,7 @@ import type {
19
19
  AbstractMediaPickerService,
20
20
  MediaPickerState,
21
21
  } from '../services/media-picker-service/AbstractMediaPickerService';
22
- import { useComposerText } from '../store';
22
+ import { COMPOSER_DICTATION_SOURCE, useComposerText } from '../store';
23
23
  import type { ComposerState } from '../store';
24
24
  import { useDictationState } from '../store/dictation/useDictationState';
25
25
  import { useDictationTranscript } from '../store/dictation/useDictationTranscript';
@@ -100,7 +100,7 @@ export const AIMessageInput = () => {
100
100
  } = useTheme();
101
101
  const styles = useStyles();
102
102
 
103
- const { error, isRecording } = useDictationState();
103
+ const { error, isRecording, source } = useDictationState();
104
104
  const { text } = useComposerText();
105
105
  const { transcript } = useDictationTranscript();
106
106
  const { setText } = useMessageComposerContext();
@@ -109,10 +109,13 @@ export const AIMessageInput = () => {
109
109
  useEffect(() => {
110
110
  const transcriptChanged = transcript !== previousTranscript.current;
111
111
  previousTranscript.current = transcript;
112
+ // Only dictation from the composer's own button fills the input; dictation started elsewhere
113
+ // (a voice mode, say) must not overwrite what the user typed.
114
+ if (source !== COMPOSER_DICTATION_SOURCE) return;
112
115
  if (!error && (isRecording || transcriptChanged)) {
113
116
  setText(transcript);
114
117
  }
115
- }, [isRecording, transcript, error, setText]);
118
+ }, [isRecording, transcript, error, setText, source]);
116
119
 
117
120
  return (
118
121
  <TextInput
@@ -9,7 +9,7 @@ import { Mic } from '../internal/icons/Mic';
9
9
 
10
10
  import { isDictationAvailable, warnIfDictationUnavailable } from '../native/AIDictation';
11
11
  import type { DictationStartOptions } from '../native-specs/NativeAIDictation';
12
- import { dictationStore, type DictationStoreState } from '../store';
12
+ import { COMPOSER_DICTATION_SOURCE, dictationStore, type DictationStoreState } from '../store';
13
13
  import { useDictation } from '../transcription/useDictation';
14
14
 
15
15
  const selector = ({ isRecording }: DictationStoreState) => ({
@@ -33,6 +33,9 @@ export const SpeechToTextButton = (props: SpeechToTextButtonProps) => {
33
33
  return <AvailableSpeechToTextButton {...props} />;
34
34
  };
35
35
 
36
+ // Marks this button's dictation as the composer's, so `ComposerView` fills its input with it.
37
+ const DICTATION_OPTIONS = { source: COMPOSER_DICTATION_SOURCE };
38
+
36
39
  const AvailableSpeechToTextButton = ({ options }: SpeechToTextButtonProps) => {
37
40
  const {
38
41
  theme: {
@@ -41,7 +44,7 @@ const AvailableSpeechToTextButton = ({ options }: SpeechToTextButtonProps) => {
41
44
  },
42
45
  } = useTheme();
43
46
 
44
- const { cancel, toggle } = useDictation(options);
47
+ const { cancel, toggle } = useDictation(options, DICTATION_OPTIONS);
45
48
  const { isRecording } = useStateStore(dictationStore, selector);
46
49
 
47
50
  useEffect(() => {
@@ -11,6 +11,10 @@ import NativeAIDictation, {
11
11
  const ON_STATE_EVENT = 'AIDictationOnState';
12
12
  const ON_RESULT_EVENT = 'AIDictationOnResult';
13
13
  const ON_ERROR_EVENT = 'AIDictationOnError';
14
+ const ON_LEVEL_EVENT = 'AIDictationOnLevel';
15
+
16
+ /** The microphone's level while dictation listens, from 0 to 1. */
17
+ export type DictationLevel = { level: number };
14
18
 
15
19
  let eventEmitter: NativeEventEmitter | undefined;
16
20
  const getEventEmitter = () => {
@@ -74,4 +78,11 @@ export const AIDictation = {
74
78
  );
75
79
  return sub ? () => sub.remove() : noop;
76
80
  },
81
+ /** The microphone's level while dictation listens, about 10 to 30 times a second. */
82
+ addLevelListener(listener: (payload: DictationLevel) => void) {
83
+ const sub = getEventEmitter()?.addListener(ON_LEVEL_EVENT, (event) =>
84
+ listener(event as DictationLevel),
85
+ );
86
+ return sub ? () => sub.remove() : noop;
87
+ },
77
88
  };
@@ -7,6 +7,11 @@ export type DictationStoreState = {
7
7
  transcript: string;
8
8
  isRecording: boolean;
9
9
  error?: DictationError | null;
10
+ /**
11
+ * Who started the current dictation, as passed to `useDictation`, or `null`. Lets a consumer
12
+ * such as `ComposerView` pick up only the transcripts meant for it.
13
+ */
14
+ source?: string | null;
10
15
  };
11
16
 
12
17
  const DEFAULT_STATE: DictationStoreState = {
@@ -14,8 +19,12 @@ const DEFAULT_STATE: DictationStoreState = {
14
19
  transcript: '',
15
20
  isRecording: false,
16
21
  error: null,
22
+ source: null,
17
23
  };
18
24
 
25
+ /** The `source` of dictation started by the composer's own `SpeechToTextButton`. */
26
+ export const COMPOSER_DICTATION_SOURCE = 'composer';
27
+
19
28
  export const dictationStore = new StateStore<DictationStoreState>(DEFAULT_STATE);
20
29
 
21
30
  const reset = () => dictationStore.next(DEFAULT_STATE);
@@ -2,10 +2,11 @@ import { useStateStore } from '@stream-io/state-store/react-bindings';
2
2
 
3
3
  import { dictationStore, type DictationStoreState } from './store';
4
4
 
5
- const selector = ({ state, isRecording, error }: DictationStoreState) => ({
5
+ const selector = ({ state, isRecording, error, source }: DictationStoreState) => ({
6
6
  state,
7
7
  isRecording,
8
8
  error,
9
+ source,
9
10
  });
10
11
 
11
12
  export const useDictationState = () => useStateStore(dictationStore, selector);
@@ -9,3 +9,5 @@ export * from './composer/useComposerText';
9
9
 
10
10
  // dictation
11
11
  export * from './dictation/store';
12
+ export * from './dictation/useDictationState';
13
+ export * from './dictation/useDictationTranscript';
@@ -56,6 +56,19 @@ export type Theme = {
56
56
  aiTypingIndicatorView: {
57
57
  text: TextStyle;
58
58
  };
59
+ /**
60
+ * `VoiceGlow` and `VoiceWave`. Optional, so a hand-built `Theme` without it keeps type-checking;
61
+ * the visualizations fall back to the default accent then.
62
+ */
63
+ voiceVisualization?: {
64
+ /** One to five colors, used when a visualization is given no `colors` of its own. */
65
+ colors: string[];
66
+ /**
67
+ * The color the visualizations sit on, which tells them to draw for a light or a dark
68
+ * background. Defaults to the system's color scheme.
69
+ */
70
+ backdropColor?: string;
71
+ };
59
72
  };
60
73
 
61
74
  export const defaultTheme: Theme = {
@@ -135,4 +148,7 @@ export const defaultTheme: Theme = {
135
148
  aiTypingIndicatorView: {
136
149
  text: {},
137
150
  },
151
+ voiceVisualization: {
152
+ colors: [colors.accent_blue],
153
+ },
138
154
  };
@@ -27,7 +27,19 @@ const selector = ({ isRecording }: DictationStoreState) => ({
27
27
  });
28
28
 
29
29
  // TODO: add proper jsdocs
30
- export const useDictation = (defaultOptions: DictationStartOptions = DEFAULT_OPTIONS) => {
30
+ export type UseDictationOptions = {
31
+ /**
32
+ * Who is dictating, stored as the dictation store's `source` when dictation starts. The
33
+ * `ComposerView` only fills its input with dictation whose source is the composer's own, so
34
+ * dictation started elsewhere (a voice mode, say) leaves the composer's text alone.
35
+ */
36
+ source?: string;
37
+ };
38
+
39
+ export const useDictation = (
40
+ defaultOptions: DictationStartOptions = DEFAULT_OPTIONS,
41
+ { source }: UseDictationOptions = {},
42
+ ) => {
31
43
  const { isRecording } = useStateStore(dictationStore, selector);
32
44
 
33
45
  useEffect(() => {
@@ -65,11 +77,12 @@ export const useDictation = (defaultOptions: DictationStartOptions = DEFAULT_OPT
65
77
  code: 'NO_PERMISSION',
66
78
  message: 'Microphone permission not granted',
67
79
  },
80
+ source: source ?? null,
68
81
  });
69
82
  return;
70
83
  }
71
84
 
72
- dictationStore.partialNext({ error: null, transcript: '' });
85
+ dictationStore.partialNext({ error: null, source: source ?? null, transcript: '' });
73
86
 
74
87
  const opts: DictationStartOptions = {
75
88
  ...defaultOptions,
@@ -78,7 +91,7 @@ export const useDictation = (defaultOptions: DictationStartOptions = DEFAULT_OPT
78
91
 
79
92
  AIDictation.start(opts);
80
93
  },
81
- [defaultOptions],
94
+ [defaultOptions, source],
82
95
  );
83
96
 
84
97
  const stop = useCallback(() => {