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.
- package/lib/commonjs/ai-components/README.md +50 -2
- package/lib/commonjs/ai-components/index.js +24 -0
- package/lib/commonjs/ai-components/index.js.map +1 -1
- package/lib/commonjs/ai-components/message-composer/ComposerView.js +4 -2
- package/lib/commonjs/ai-components/message-composer/ComposerView.js.map +1 -1
- package/lib/commonjs/ai-components/message-composer/SpeechToTextButton.js +4 -1
- package/lib/commonjs/ai-components/message-composer/SpeechToTextButton.js.map +1 -1
- package/lib/commonjs/ai-components/native/AIDictation.js +6 -0
- package/lib/commonjs/ai-components/native/AIDictation.js.map +1 -1
- package/lib/commonjs/ai-components/store/dictation/store.js +4 -2
- package/lib/commonjs/ai-components/store/dictation/store.js.map +1 -1
- package/lib/commonjs/ai-components/store/dictation/useDictationState.js +4 -2
- package/lib/commonjs/ai-components/store/dictation/useDictationState.js.map +1 -1
- package/lib/commonjs/ai-components/store/index.js +22 -0
- package/lib/commonjs/ai-components/store/index.js.map +1 -1
- package/lib/commonjs/ai-components/theme.js +3 -0
- package/lib/commonjs/ai-components/theme.js.map +1 -1
- package/lib/commonjs/ai-components/transcription/useDictation.js +7 -3
- package/lib/commonjs/ai-components/transcription/useDictation.js.map +1 -1
- package/lib/commonjs/ai-components/transcription/useDictationAudioLevel.js +32 -0
- package/lib/commonjs/ai-components/transcription/useDictationAudioLevel.js.map +1 -0
- package/lib/commonjs/ai-components/voice-visualization/VoiceGlow.js +79 -0
- package/lib/commonjs/ai-components/voice-visualization/VoiceGlow.js.map +1 -0
- package/lib/commonjs/ai-components/voice-visualization/VoiceVisualizationView.js +147 -0
- package/lib/commonjs/ai-components/voice-visualization/VoiceVisualizationView.js.map +1 -0
- package/lib/commonjs/ai-components/voice-visualization/VoiceWave.js +58 -0
- package/lib/commonjs/ai-components/voice-visualization/VoiceWave.js.map +1 -0
- package/lib/commonjs/ai-components/voice-visualization/bounce.js +60 -0
- package/lib/commonjs/ai-components/voice-visualization/bounce.js.map +1 -0
- package/lib/commonjs/ai-components/voice-visualization/driver.js +208 -0
- package/lib/commonjs/ai-components/voice-visualization/driver.js.map +1 -0
- package/lib/commonjs/ai-components/voice-visualization/glowShader.js +218 -0
- package/lib/commonjs/ai-components/voice-visualization/glowShader.js.map +1 -0
- package/lib/commonjs/ai-components/voice-visualization/index.js +18 -0
- package/lib/commonjs/ai-components/voice-visualization/index.js.map +1 -0
- package/lib/commonjs/ai-components/voice-visualization/math.js +70 -0
- package/lib/commonjs/ai-components/voice-visualization/math.js.map +1 -0
- package/lib/commonjs/ai-components/voice-visualization/palette.js +47 -0
- package/lib/commonjs/ai-components/voice-visualization/palette.js.map +1 -0
- package/lib/commonjs/ai-components/voice-visualization/simplex.js +77 -0
- package/lib/commonjs/ai-components/voice-visualization/simplex.js.map +1 -0
- package/lib/commonjs/ai-components/voice-visualization/styles.js +33 -0
- package/lib/commonjs/ai-components/voice-visualization/styles.js.map +1 -0
- package/lib/commonjs/ai-components/voice-visualization/types.js +4 -0
- package/lib/commonjs/ai-components/voice-visualization/types.js.map +1 -0
- package/lib/commonjs/ai-components/voice-visualization/useReduceMotion.js +28 -0
- package/lib/commonjs/ai-components/voice-visualization/useReduceMotion.js.map +1 -0
- package/lib/commonjs/ai-components/voice-visualization/useVoiceVisualization.js +129 -0
- package/lib/commonjs/ai-components/voice-visualization/useVoiceVisualization.js.map +1 -0
- package/lib/commonjs/ai-components/voice-visualization/waveGeometry.js +124 -0
- package/lib/commonjs/ai-components/voice-visualization/waveGeometry.js.map +1 -0
- package/lib/commonjs/components/MessageInput/MessageComposer.js +2 -1
- package/lib/commonjs/components/MessageInput/MessageComposer.js.map +1 -1
- package/lib/commonjs/version.json +1 -1
- package/lib/module/ai-components/README.md +50 -2
- package/lib/module/ai-components/index.js +24 -0
- package/lib/module/ai-components/index.js.map +1 -1
- package/lib/module/ai-components/message-composer/ComposerView.js +4 -2
- package/lib/module/ai-components/message-composer/ComposerView.js.map +1 -1
- package/lib/module/ai-components/message-composer/SpeechToTextButton.js +4 -1
- package/lib/module/ai-components/message-composer/SpeechToTextButton.js.map +1 -1
- package/lib/module/ai-components/native/AIDictation.js +6 -0
- package/lib/module/ai-components/native/AIDictation.js.map +1 -1
- package/lib/module/ai-components/store/dictation/store.js +4 -2
- package/lib/module/ai-components/store/dictation/store.js.map +1 -1
- package/lib/module/ai-components/store/dictation/useDictationState.js +4 -2
- package/lib/module/ai-components/store/dictation/useDictationState.js.map +1 -1
- package/lib/module/ai-components/store/index.js +22 -0
- package/lib/module/ai-components/store/index.js.map +1 -1
- package/lib/module/ai-components/theme.js +3 -0
- package/lib/module/ai-components/theme.js.map +1 -1
- package/lib/module/ai-components/transcription/useDictation.js +7 -3
- package/lib/module/ai-components/transcription/useDictation.js.map +1 -1
- package/lib/module/ai-components/transcription/useDictationAudioLevel.js +32 -0
- package/lib/module/ai-components/transcription/useDictationAudioLevel.js.map +1 -0
- package/lib/module/ai-components/voice-visualization/VoiceGlow.js +79 -0
- package/lib/module/ai-components/voice-visualization/VoiceGlow.js.map +1 -0
- package/lib/module/ai-components/voice-visualization/VoiceVisualizationView.js +147 -0
- package/lib/module/ai-components/voice-visualization/VoiceVisualizationView.js.map +1 -0
- package/lib/module/ai-components/voice-visualization/VoiceWave.js +58 -0
- package/lib/module/ai-components/voice-visualization/VoiceWave.js.map +1 -0
- package/lib/module/ai-components/voice-visualization/bounce.js +60 -0
- package/lib/module/ai-components/voice-visualization/bounce.js.map +1 -0
- package/lib/module/ai-components/voice-visualization/driver.js +208 -0
- package/lib/module/ai-components/voice-visualization/driver.js.map +1 -0
- package/lib/module/ai-components/voice-visualization/glowShader.js +218 -0
- package/lib/module/ai-components/voice-visualization/glowShader.js.map +1 -0
- package/lib/module/ai-components/voice-visualization/index.js +18 -0
- package/lib/module/ai-components/voice-visualization/index.js.map +1 -0
- package/lib/module/ai-components/voice-visualization/math.js +70 -0
- package/lib/module/ai-components/voice-visualization/math.js.map +1 -0
- package/lib/module/ai-components/voice-visualization/palette.js +47 -0
- package/lib/module/ai-components/voice-visualization/palette.js.map +1 -0
- package/lib/module/ai-components/voice-visualization/simplex.js +77 -0
- package/lib/module/ai-components/voice-visualization/simplex.js.map +1 -0
- package/lib/module/ai-components/voice-visualization/styles.js +33 -0
- package/lib/module/ai-components/voice-visualization/styles.js.map +1 -0
- package/lib/module/ai-components/voice-visualization/types.js +4 -0
- package/lib/module/ai-components/voice-visualization/types.js.map +1 -0
- package/lib/module/ai-components/voice-visualization/useReduceMotion.js +28 -0
- package/lib/module/ai-components/voice-visualization/useReduceMotion.js.map +1 -0
- package/lib/module/ai-components/voice-visualization/useVoiceVisualization.js +129 -0
- package/lib/module/ai-components/voice-visualization/useVoiceVisualization.js.map +1 -0
- package/lib/module/ai-components/voice-visualization/waveGeometry.js +124 -0
- package/lib/module/ai-components/voice-visualization/waveGeometry.js.map +1 -0
- package/lib/module/components/MessageInput/MessageComposer.js +2 -1
- package/lib/module/components/MessageInput/MessageComposer.js.map +1 -1
- package/lib/module/version.json +1 -1
- package/lib/typescript/ai-components/index.d.ts +3 -1
- package/lib/typescript/ai-components/index.d.ts.map +1 -1
- package/lib/typescript/ai-components/message-composer/ComposerView.d.ts.map +1 -1
- package/lib/typescript/ai-components/native/AIDictation.d.ts +6 -0
- package/lib/typescript/ai-components/native/AIDictation.d.ts.map +1 -1
- package/lib/typescript/ai-components/store/dictation/store.d.ts +7 -0
- package/lib/typescript/ai-components/store/dictation/store.d.ts.map +1 -1
- package/lib/typescript/ai-components/store/dictation/useDictationState.d.ts +1 -0
- package/lib/typescript/ai-components/store/dictation/useDictationState.d.ts.map +1 -1
- package/lib/typescript/ai-components/store/index.d.ts +2 -0
- package/lib/typescript/ai-components/store/index.d.ts.map +1 -1
- package/lib/typescript/ai-components/theme.d.ts +13 -0
- package/lib/typescript/ai-components/theme.d.ts.map +1 -1
- package/lib/typescript/ai-components/transcription/useDictation.d.ts +9 -1
- package/lib/typescript/ai-components/transcription/useDictation.d.ts.map +1 -1
- package/lib/typescript/ai-components/transcription/useDictationAudioLevel.d.ts +11 -0
- package/lib/typescript/ai-components/transcription/useDictationAudioLevel.d.ts.map +1 -0
- package/lib/typescript/ai-components/voice-visualization/VoiceGlow.d.ts +13 -0
- package/lib/typescript/ai-components/voice-visualization/VoiceGlow.d.ts.map +1 -0
- package/lib/typescript/ai-components/voice-visualization/VoiceVisualizationView.d.ts +26 -0
- package/lib/typescript/ai-components/voice-visualization/VoiceVisualizationView.d.ts.map +1 -0
- package/lib/typescript/ai-components/voice-visualization/VoiceWave.d.ts +13 -0
- package/lib/typescript/ai-components/voice-visualization/VoiceWave.d.ts.map +1 -0
- package/lib/typescript/ai-components/voice-visualization/bounce.d.ts +20 -0
- package/lib/typescript/ai-components/voice-visualization/bounce.d.ts.map +1 -0
- package/lib/typescript/ai-components/voice-visualization/driver.d.ts +96 -0
- package/lib/typescript/ai-components/voice-visualization/driver.d.ts.map +1 -0
- package/lib/typescript/ai-components/voice-visualization/glowShader.d.ts +7 -0
- package/lib/typescript/ai-components/voice-visualization/glowShader.d.ts.map +1 -0
- package/lib/typescript/ai-components/voice-visualization/index.d.ts +4 -0
- package/lib/typescript/ai-components/voice-visualization/index.d.ts.map +1 -0
- package/lib/typescript/ai-components/voice-visualization/math.d.ts +26 -0
- package/lib/typescript/ai-components/voice-visualization/math.d.ts.map +1 -0
- package/lib/typescript/ai-components/voice-visualization/palette.d.ts +18 -0
- package/lib/typescript/ai-components/voice-visualization/palette.d.ts.map +1 -0
- package/lib/typescript/ai-components/voice-visualization/simplex.d.ts +11 -0
- package/lib/typescript/ai-components/voice-visualization/simplex.d.ts.map +1 -0
- package/lib/typescript/ai-components/voice-visualization/styles.d.ts +24 -0
- package/lib/typescript/ai-components/voice-visualization/styles.d.ts.map +1 -0
- package/lib/typescript/ai-components/voice-visualization/types.d.ts +45 -0
- package/lib/typescript/ai-components/voice-visualization/types.d.ts.map +1 -0
- package/lib/typescript/ai-components/voice-visualization/useReduceMotion.d.ts +10 -0
- package/lib/typescript/ai-components/voice-visualization/useReduceMotion.d.ts.map +1 -0
- package/lib/typescript/ai-components/voice-visualization/useVoiceVisualization.d.ts +18 -0
- package/lib/typescript/ai-components/voice-visualization/useVoiceVisualization.d.ts.map +1 -0
- package/lib/typescript/ai-components/voice-visualization/waveGeometry.d.ts +25 -0
- package/lib/typescript/ai-components/voice-visualization/waveGeometry.d.ts.map +1 -0
- package/lib/typescript/components/MessageInput/MessageComposer.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/ai-components/README.md +50 -2
- package/src/ai-components/__tests__/VoiceVisualization.test.tsx +208 -0
- package/src/ai-components/__tests__/dictation.test.tsx +58 -1
- package/src/ai-components/__tests__/voiceDriver.test.ts +317 -0
- package/src/ai-components/__tests__/voicePalette.test.ts +124 -0
- package/src/ai-components/index.ts +3 -1
- package/src/ai-components/message-composer/ComposerView.tsx +6 -3
- package/src/ai-components/message-composer/SpeechToTextButton.tsx +5 -2
- package/src/ai-components/native/AIDictation.ts +11 -0
- package/src/ai-components/store/dictation/store.ts +9 -0
- package/src/ai-components/store/dictation/useDictationState.ts +2 -1
- package/src/ai-components/store/index.ts +2 -0
- package/src/ai-components/theme.ts +16 -0
- package/src/ai-components/transcription/useDictation.ts +16 -3
- package/src/ai-components/transcription/useDictationAudioLevel.ts +38 -0
- package/src/ai-components/voice-visualization/VoiceGlow.tsx +79 -0
- package/src/ai-components/voice-visualization/VoiceVisualizationView.tsx +179 -0
- package/src/ai-components/voice-visualization/VoiceWave.tsx +67 -0
- package/src/ai-components/voice-visualization/bounce.ts +61 -0
- package/src/ai-components/voice-visualization/driver.ts +240 -0
- package/src/ai-components/voice-visualization/glowShader.ts +227 -0
- package/src/ai-components/voice-visualization/index.ts +3 -0
- package/src/ai-components/voice-visualization/math.ts +73 -0
- package/src/ai-components/voice-visualization/palette.ts +59 -0
- package/src/ai-components/voice-visualization/simplex.ts +92 -0
- package/src/ai-components/voice-visualization/styles.ts +42 -0
- package/src/ai-components/voice-visualization/types.ts +44 -0
- package/src/ai-components/voice-visualization/useReduceMotion.ts +34 -0
- package/src/ai-components/voice-visualization/useVoiceVisualization.ts +153 -0
- package/src/ai-components/voice-visualization/waveGeometry.ts +137 -0
- package/src/components/ChannelPreview/hooks/__tests__/useChannelPreviewPollLabel.test.tsx +1 -1
- package/src/components/MessageInput/MessageComposer.tsx +4 -2
- 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
|
|
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);
|
|
@@ -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
|
|
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(() => {
|