@alia.onl/sdk 1.0.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +37 -3
- package/src/components/AliaChatContent.tsx +313 -0
- package/src/components/AliaChatInput.tsx +174 -27
- package/src/components/AliaChatMessageList.tsx +220 -34
- package/src/components/AliaChatScreen.tsx +132 -0
- package/src/components/AliaChatSheet.tsx +36 -49
- package/src/components/AliaChatSuggestions.tsx +1 -1
- package/src/components/AliaFace.tsx +402 -0
- package/src/components/Markdown.tsx +288 -0
- package/src/components/Reasoning.tsx +154 -0
- package/src/components/ThinkingIndicator.tsx +134 -0
- package/src/components/ui/button.tsx +104 -0
- package/src/components/ui/chat-text-input.tsx +164 -0
- package/src/components/ui/dropdown-menu.tsx +2 -0
- package/src/components/ui/dropdown-menu.web.tsx +355 -0
- package/src/components/ui/prompt-input/actions.tsx +28 -0
- package/src/components/ui/prompt-input/add-menu.tsx +83 -0
- package/src/components/ui/prompt-input/attachments.tsx +183 -0
- package/src/components/ui/prompt-input/autocomplete.tsx +168 -0
- package/src/components/ui/prompt-input/context.tsx +71 -0
- package/src/components/ui/prompt-input/mic-button.tsx +53 -0
- package/src/components/ui/prompt-input/prompt-input.tsx +276 -0
- package/src/components/ui/prompt-input/submit-button.tsx +49 -0
- package/src/components/ui/prompt-input/textarea.tsx +64 -0
- package/src/components/ui/text.tsx +29 -0
- package/src/components/voice/AudioWaveVisualizer.tsx +377 -0
- package/src/components/voice/VoiceControls.tsx +180 -0
- package/src/components/voice/VoiceOverlay.tsx +43 -0
- package/src/hooks/useAliaChat.ts +278 -91
- package/src/hooks/useAudioLevelMonitor.ts +142 -0
- package/src/hooks/useAudioLevels.ts +51 -0
- package/src/hooks/useDocumentPicker.ts +37 -0
- package/src/hooks/useImagePicker.ts +38 -0
- package/src/hooks/useSoundEffects.ts +128 -0
- package/src/hooks/useSpeechToText.ts +182 -0
- package/src/hooks/useTTS.ts +233 -0
- package/src/hooks/useVoiceRoom.ts +420 -0
- package/src/index.ts +71 -6
- package/src/lib/keyboard.native.tsx +6 -0
- package/src/lib/keyboard.tsx +25 -0
- package/src/lib/utils.ts +14 -0
- package/src/optional-deps.d.ts +86 -0
- package/src/theme.ts +25 -2
- package/src/types.ts +63 -1
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { Text as RNText, type TextProps as RNTextProps } from "react-native";
|
|
3
|
+
import { cn } from "../../lib/utils";
|
|
4
|
+
|
|
5
|
+
const TextClassContext = React.createContext<string | undefined>(undefined);
|
|
6
|
+
|
|
7
|
+
export type TextProps = RNTextProps & {
|
|
8
|
+
className?: string;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const Text = React.forwardRef<RNText, TextProps>(
|
|
12
|
+
({ className, ...props }, ref) => {
|
|
13
|
+
const textClass = React.useContext(TextClassContext);
|
|
14
|
+
return (
|
|
15
|
+
<RNText
|
|
16
|
+
className={cn(
|
|
17
|
+
"text-base leading-7 text-foreground web:select-text font-sans",
|
|
18
|
+
textClass,
|
|
19
|
+
className
|
|
20
|
+
)}
|
|
21
|
+
ref={ref}
|
|
22
|
+
{...props}
|
|
23
|
+
/>
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
);
|
|
27
|
+
Text.displayName = "Text";
|
|
28
|
+
|
|
29
|
+
export { Text, TextClassContext };
|
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
import React, { useEffect } from 'react';
|
|
2
|
+
import { View, Platform, useWindowDimensions } from 'react-native';
|
|
3
|
+
import Svg, { Path } from 'react-native-svg';
|
|
4
|
+
import Animated, {
|
|
5
|
+
useSharedValue,
|
|
6
|
+
useAnimatedProps,
|
|
7
|
+
useAnimatedStyle,
|
|
8
|
+
withTiming,
|
|
9
|
+
withRepeat,
|
|
10
|
+
withSequence,
|
|
11
|
+
Easing,
|
|
12
|
+
type SharedValue,
|
|
13
|
+
} from 'react-native-reanimated';
|
|
14
|
+
import type { AgentState } from '../../types';
|
|
15
|
+
|
|
16
|
+
const AnimatedPath = Animated.createAnimatedComponent(Path);
|
|
17
|
+
|
|
18
|
+
const GLOW_HEIGHT = 350;
|
|
19
|
+
const NUM_POINTS = 8;
|
|
20
|
+
|
|
21
|
+
// Ocean-style wave layers: fill from bottom to a surface level, crests undulate
|
|
22
|
+
interface WaveLayerConfig {
|
|
23
|
+
color: string;
|
|
24
|
+
surfaceY: number; // surface level as % from top (50 = middle)
|
|
25
|
+
waveHeight: number; // base crest height in px
|
|
26
|
+
maxWaveBoost: number; // extra crest height from audio
|
|
27
|
+
phaseOffset: number;
|
|
28
|
+
speed: number; // ms for one full horizontal cycle
|
|
29
|
+
blur: number;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const WAVE_LAYERS: WaveLayerConfig[] = [
|
|
33
|
+
// Deepest layer - dark blue, highest surface (closest to top)
|
|
34
|
+
{
|
|
35
|
+
color: 'rgba(12, 74, 110, 0.8)',
|
|
36
|
+
surfaceY: 35,
|
|
37
|
+
waveHeight: 14,
|
|
38
|
+
maxWaveBoost: 120,
|
|
39
|
+
phaseOffset: 0,
|
|
40
|
+
speed: 7000,
|
|
41
|
+
blur: 30,
|
|
42
|
+
},
|
|
43
|
+
// Mid layer - ocean blue
|
|
44
|
+
{
|
|
45
|
+
color: 'rgba(14, 165, 233, 0.45)',
|
|
46
|
+
surfaceY: 42,
|
|
47
|
+
waveHeight: 16,
|
|
48
|
+
maxWaveBoost: 140,
|
|
49
|
+
phaseOffset: Math.PI * 0.8,
|
|
50
|
+
speed: 5500,
|
|
51
|
+
blur: 22,
|
|
52
|
+
},
|
|
53
|
+
// Front layer - bright cyan, lowest surface
|
|
54
|
+
{
|
|
55
|
+
color: 'rgba(56, 189, 248, 0.35)',
|
|
56
|
+
surfaceY: 50,
|
|
57
|
+
waveHeight: 20,
|
|
58
|
+
maxWaveBoost: 150,
|
|
59
|
+
phaseOffset: Math.PI * 1.5,
|
|
60
|
+
speed: 4500,
|
|
61
|
+
blur: 16,
|
|
62
|
+
},
|
|
63
|
+
// Accent layer - indigo/purple
|
|
64
|
+
{
|
|
65
|
+
color: 'rgba(99, 102, 241, 0.25)',
|
|
66
|
+
surfaceY: 38,
|
|
67
|
+
waveHeight: 12,
|
|
68
|
+
maxWaveBoost: 100,
|
|
69
|
+
phaseOffset: Math.PI * 0.3,
|
|
70
|
+
speed: 8000,
|
|
71
|
+
blur: 28,
|
|
72
|
+
},
|
|
73
|
+
];
|
|
74
|
+
|
|
75
|
+
// Background glow blobs for ambient color
|
|
76
|
+
interface BlobConfig {
|
|
77
|
+
color: string;
|
|
78
|
+
baseWidth: number;
|
|
79
|
+
baseHeight: number;
|
|
80
|
+
x: number;
|
|
81
|
+
y: number;
|
|
82
|
+
blur: number;
|
|
83
|
+
amplitudeScale: number;
|
|
84
|
+
phaseSpeed: number;
|
|
85
|
+
phaseDirection: 1 | -1;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const BLOBS: BlobConfig[] = [
|
|
89
|
+
{
|
|
90
|
+
color: '#0c4a6e',
|
|
91
|
+
baseWidth: 320,
|
|
92
|
+
baseHeight: 200,
|
|
93
|
+
x: 50,
|
|
94
|
+
y: 55,
|
|
95
|
+
blur: 80,
|
|
96
|
+
amplitudeScale: 1.0,
|
|
97
|
+
phaseSpeed: 4000,
|
|
98
|
+
phaseDirection: 1,
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
color: '#0ea5e9',
|
|
102
|
+
baseWidth: 220,
|
|
103
|
+
baseHeight: 140,
|
|
104
|
+
x: 38,
|
|
105
|
+
y: 65,
|
|
106
|
+
blur: 60,
|
|
107
|
+
amplitudeScale: 1.2,
|
|
108
|
+
phaseSpeed: 3500,
|
|
109
|
+
phaseDirection: -1,
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
color: '#6366f1',
|
|
113
|
+
baseWidth: 180,
|
|
114
|
+
baseHeight: 160,
|
|
115
|
+
x: 70,
|
|
116
|
+
y: 55,
|
|
117
|
+
blur: 70,
|
|
118
|
+
amplitudeScale: 0.7,
|
|
119
|
+
phaseSpeed: 4500,
|
|
120
|
+
phaseDirection: -1,
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
color: '#a855f7',
|
|
124
|
+
baseWidth: 130,
|
|
125
|
+
baseHeight: 110,
|
|
126
|
+
x: 78,
|
|
127
|
+
y: 65,
|
|
128
|
+
blur: 60,
|
|
129
|
+
amplitudeScale: 0.5,
|
|
130
|
+
phaseSpeed: 5500,
|
|
131
|
+
phaseDirection: 1,
|
|
132
|
+
},
|
|
133
|
+
];
|
|
134
|
+
|
|
135
|
+
// Colors per agent state for waves — speaking blends blue + purple
|
|
136
|
+
const WAVE_COLORS: Record<AgentState, WaveLayerConfig['color'][]> = {
|
|
137
|
+
idle: [
|
|
138
|
+
'rgba(12, 74, 110, 0.8)',
|
|
139
|
+
'rgba(14, 165, 233, 0.45)',
|
|
140
|
+
'rgba(56, 189, 248, 0.35)',
|
|
141
|
+
'rgba(99, 102, 241, 0.25)',
|
|
142
|
+
],
|
|
143
|
+
listening: [
|
|
144
|
+
'rgba(12, 74, 110, 0.8)',
|
|
145
|
+
'rgba(14, 165, 233, 0.45)',
|
|
146
|
+
'rgba(56, 189, 248, 0.35)',
|
|
147
|
+
'rgba(99, 102, 241, 0.25)',
|
|
148
|
+
],
|
|
149
|
+
thinking: [
|
|
150
|
+
'rgba(120, 80, 20, 0.75)',
|
|
151
|
+
'rgba(234, 179, 8, 0.45)',
|
|
152
|
+
'rgba(251, 191, 36, 0.35)',
|
|
153
|
+
'rgba(253, 224, 71, 0.25)',
|
|
154
|
+
],
|
|
155
|
+
speaking: [
|
|
156
|
+
'rgba(30, 40, 100, 0.8)',
|
|
157
|
+
'rgba(99, 102, 241, 0.5)',
|
|
158
|
+
'rgba(147, 51, 234, 0.4)',
|
|
159
|
+
'rgba(56, 189, 248, 0.3)',
|
|
160
|
+
],
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
const BLOB_COLORS: Record<AgentState, string[]> = {
|
|
164
|
+
idle: ['#0c4a6e', '#0ea5e9', '#6366f1', '#a855f7'],
|
|
165
|
+
listening: ['#0c4a6e', '#0ea5e9', '#6366f1', '#a855f7'],
|
|
166
|
+
thinking: ['#92400e', '#eab308', '#f59e0b', '#fbbf24'],
|
|
167
|
+
speaking: ['#1e2870', '#6366f1', '#9333ea', '#0ea5e9'],
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
interface AudioWaveVisualizerProps {
|
|
171
|
+
waveAmplitude: SharedValue<number>;
|
|
172
|
+
agentState: AgentState;
|
|
173
|
+
isConnected: boolean;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export function AudioWaveVisualizer({
|
|
177
|
+
waveAmplitude,
|
|
178
|
+
agentState = 'idle',
|
|
179
|
+
isConnected,
|
|
180
|
+
}: AudioWaveVisualizerProps) {
|
|
181
|
+
const { width: screenWidth } = useWindowDimensions();
|
|
182
|
+
const state = WAVE_COLORS[agentState] ? agentState : 'idle';
|
|
183
|
+
|
|
184
|
+
return (
|
|
185
|
+
<View
|
|
186
|
+
style={{
|
|
187
|
+
width: screenWidth,
|
|
188
|
+
height: GLOW_HEIGHT,
|
|
189
|
+
position: 'relative',
|
|
190
|
+
}}
|
|
191
|
+
>
|
|
192
|
+
{/* Background glow blobs */}
|
|
193
|
+
{BLOBS.map((blob, i) => (
|
|
194
|
+
<GlowBlob
|
|
195
|
+
key={`blob-${i}`}
|
|
196
|
+
blob={blob}
|
|
197
|
+
color={BLOB_COLORS[state][i]}
|
|
198
|
+
waveAmplitude={waveAmplitude}
|
|
199
|
+
isConnected={isConnected}
|
|
200
|
+
screenWidth={screenWidth}
|
|
201
|
+
/>
|
|
202
|
+
))}
|
|
203
|
+
|
|
204
|
+
{/* Ocean wave layers — filled from surface down to bottom, with blur */}
|
|
205
|
+
{WAVE_LAYERS.map((layer, i) => (
|
|
206
|
+
<OceanWave
|
|
207
|
+
key={`wave-${i}`}
|
|
208
|
+
layer={layer}
|
|
209
|
+
color={WAVE_COLORS[state][i]}
|
|
210
|
+
waveAmplitude={waveAmplitude}
|
|
211
|
+
isConnected={isConnected}
|
|
212
|
+
screenWidth={screenWidth}
|
|
213
|
+
/>
|
|
214
|
+
))}
|
|
215
|
+
</View>
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Ocean wave: surface undulates horizontally, filled from surface to bottom
|
|
220
|
+
function OceanWave({
|
|
221
|
+
layer,
|
|
222
|
+
color,
|
|
223
|
+
waveAmplitude,
|
|
224
|
+
isConnected,
|
|
225
|
+
screenWidth,
|
|
226
|
+
}: {
|
|
227
|
+
layer: WaveLayerConfig;
|
|
228
|
+
color: string;
|
|
229
|
+
waveAmplitude: SharedValue<number>;
|
|
230
|
+
isConnected: boolean;
|
|
231
|
+
screenWidth: number;
|
|
232
|
+
}) {
|
|
233
|
+
const phase = useSharedValue(0);
|
|
234
|
+
|
|
235
|
+
useEffect(() => {
|
|
236
|
+
phase.value = 0;
|
|
237
|
+
phase.value = withRepeat(
|
|
238
|
+
withTiming(Math.PI * 2, { duration: layer.speed, easing: Easing.linear }),
|
|
239
|
+
-1,
|
|
240
|
+
false,
|
|
241
|
+
);
|
|
242
|
+
}, []);
|
|
243
|
+
|
|
244
|
+
const animatedProps = useAnimatedProps(() => {
|
|
245
|
+
const amp = waveAmplitude.value;
|
|
246
|
+
// Surface Y position (fixed, doesn't move with audio)
|
|
247
|
+
const surfaceY = (layer.surfaceY / 100) * GLOW_HEIGHT;
|
|
248
|
+
// Wave crest height: base + audio boost
|
|
249
|
+
const crestHeight = layer.waveHeight + amp * layer.maxWaveBoost;
|
|
250
|
+
const currentPhase = phase.value + layer.phaseOffset;
|
|
251
|
+
const segW = screenWidth / NUM_POINTS;
|
|
252
|
+
|
|
253
|
+
// Start at bottom-left corner
|
|
254
|
+
let d = `M 0 ${GLOW_HEIGHT}`;
|
|
255
|
+
|
|
256
|
+
// Up to first surface point
|
|
257
|
+
const firstY = surfaceY + Math.sin(currentPhase) * crestHeight;
|
|
258
|
+
d += ` L 0 ${firstY}`;
|
|
259
|
+
|
|
260
|
+
// Draw undulating surface across the width
|
|
261
|
+
for (let i = 0; i < NUM_POINTS; i++) {
|
|
262
|
+
const x1 = (i + 1) * segW;
|
|
263
|
+
const y0 =
|
|
264
|
+
surfaceY +
|
|
265
|
+
Math.sin(currentPhase + (i / NUM_POINTS) * Math.PI * 2) * crestHeight;
|
|
266
|
+
const y1 =
|
|
267
|
+
surfaceY +
|
|
268
|
+
Math.sin(currentPhase + ((i + 1) / NUM_POINTS) * Math.PI * 2) * crestHeight;
|
|
269
|
+
|
|
270
|
+
const cp1x = i * segW + segW * 0.5;
|
|
271
|
+
const cp2x = x1 - segW * 0.5;
|
|
272
|
+
|
|
273
|
+
d += ` C ${cp1x} ${y0}, ${cp2x} ${y1}, ${x1} ${y1}`;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Close: down to bottom-right, across to bottom-left
|
|
277
|
+
d += ` L ${screenWidth} ${GLOW_HEIGHT} Z`;
|
|
278
|
+
|
|
279
|
+
return { d };
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
const webBlurStyle =
|
|
283
|
+
Platform.OS === 'web'
|
|
284
|
+
? ({
|
|
285
|
+
filter: `blur(${layer.blur}px)`,
|
|
286
|
+
WebkitFilter: `blur(${layer.blur}px)`,
|
|
287
|
+
} as any)
|
|
288
|
+
: {};
|
|
289
|
+
|
|
290
|
+
return (
|
|
291
|
+
<View
|
|
292
|
+
style={{
|
|
293
|
+
position: 'absolute',
|
|
294
|
+
top: 0,
|
|
295
|
+
left: 0,
|
|
296
|
+
right: 0,
|
|
297
|
+
bottom: 0,
|
|
298
|
+
opacity: isConnected ? 1 : 0,
|
|
299
|
+
...webBlurStyle,
|
|
300
|
+
pointerEvents: 'none',
|
|
301
|
+
}}
|
|
302
|
+
>
|
|
303
|
+
<Svg width={screenWidth} height={GLOW_HEIGHT}>
|
|
304
|
+
<AnimatedPath animatedProps={animatedProps} fill={color} />
|
|
305
|
+
</Svg>
|
|
306
|
+
</View>
|
|
307
|
+
);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// Ambient glow blob
|
|
311
|
+
function GlowBlob({
|
|
312
|
+
blob,
|
|
313
|
+
color,
|
|
314
|
+
waveAmplitude,
|
|
315
|
+
isConnected,
|
|
316
|
+
screenWidth,
|
|
317
|
+
}: {
|
|
318
|
+
blob: BlobConfig;
|
|
319
|
+
color: string;
|
|
320
|
+
waveAmplitude: SharedValue<number>;
|
|
321
|
+
isConnected: boolean;
|
|
322
|
+
screenWidth: number;
|
|
323
|
+
}) {
|
|
324
|
+
const drift = useSharedValue(0);
|
|
325
|
+
|
|
326
|
+
useEffect(() => {
|
|
327
|
+
drift.value = 0;
|
|
328
|
+
drift.value = withRepeat(
|
|
329
|
+
withSequence(
|
|
330
|
+
withTiming(1, {
|
|
331
|
+
duration: blob.phaseSpeed,
|
|
332
|
+
easing: Easing.bezier(0.37, 0, 0.63, 1),
|
|
333
|
+
}),
|
|
334
|
+
withTiming(0, {
|
|
335
|
+
duration: blob.phaseSpeed,
|
|
336
|
+
easing: Easing.bezier(0.37, 0, 0.63, 1),
|
|
337
|
+
}),
|
|
338
|
+
),
|
|
339
|
+
-1,
|
|
340
|
+
false,
|
|
341
|
+
);
|
|
342
|
+
}, []);
|
|
343
|
+
|
|
344
|
+
const animatedStyle = useAnimatedStyle(() => {
|
|
345
|
+
const amp = waveAmplitude.value;
|
|
346
|
+
const scale = 1 + amp * 0.3 * blob.amplitudeScale;
|
|
347
|
+
const driftOffset = (drift.value - 0.5) * 20 * blob.phaseDirection;
|
|
348
|
+
const opacity = isConnected ? 0.45 + amp * 0.35 * blob.amplitudeScale : 0;
|
|
349
|
+
|
|
350
|
+
const w = blob.baseWidth * scale;
|
|
351
|
+
const h = blob.baseHeight * scale;
|
|
352
|
+
const left = (blob.x / 100) * screenWidth - w / 2 + driftOffset;
|
|
353
|
+
const top = (blob.y / 100) * GLOW_HEIGHT - h / 2;
|
|
354
|
+
|
|
355
|
+
return {
|
|
356
|
+
position: 'absolute' as const,
|
|
357
|
+
width: w,
|
|
358
|
+
height: h,
|
|
359
|
+
left,
|
|
360
|
+
top,
|
|
361
|
+
borderRadius: w / 2,
|
|
362
|
+
backgroundColor: color,
|
|
363
|
+
opacity,
|
|
364
|
+
transform: [{ scale }],
|
|
365
|
+
};
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
const webBlurStyle =
|
|
369
|
+
Platform.OS === 'web'
|
|
370
|
+
? ({
|
|
371
|
+
filter: `blur(${blob.blur}px)`,
|
|
372
|
+
WebkitFilter: `blur(${blob.blur}px)`,
|
|
373
|
+
} as any)
|
|
374
|
+
: {};
|
|
375
|
+
|
|
376
|
+
return <Animated.View style={[animatedStyle, webBlurStyle]} />;
|
|
377
|
+
}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bottom voice control bar that replaces PromptInput when voice mode is active.
|
|
3
|
+
* Contains status text, mute/cohost/end buttons, and "Continue" for cohost rounds.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { View, Text, Pressable, ActivityIndicator, StyleSheet } from 'react-native';
|
|
7
|
+
import type { RoomState, AgentState } from '../../types';
|
|
8
|
+
import { useAliaColors } from '../../theme';
|
|
9
|
+
|
|
10
|
+
interface VoiceControlsProps {
|
|
11
|
+
roomState: RoomState;
|
|
12
|
+
agentState: AgentState;
|
|
13
|
+
isMuted: boolean;
|
|
14
|
+
cohostActive: boolean;
|
|
15
|
+
currentSpeaker: 'primary' | 'cohost' | 'user' | null;
|
|
16
|
+
roundComplete: boolean;
|
|
17
|
+
onToggleMute: () => void;
|
|
18
|
+
onEnableCohost: () => void;
|
|
19
|
+
onDisableCohost: () => void;
|
|
20
|
+
onContinueCohost: () => void;
|
|
21
|
+
onEnd: () => void;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function getStatusText(
|
|
25
|
+
roomState: RoomState,
|
|
26
|
+
agentState: AgentState,
|
|
27
|
+
isMuted: boolean,
|
|
28
|
+
cohostActive: boolean,
|
|
29
|
+
currentSpeaker: string | null,
|
|
30
|
+
): string {
|
|
31
|
+
if (roomState === 'connecting') return 'Connecting...';
|
|
32
|
+
if (roomState === 'connected') {
|
|
33
|
+
if (cohostActive && currentSpeaker === 'cohost') return 'Cohost speaking...';
|
|
34
|
+
if (cohostActive && currentSpeaker === 'primary') return 'Alia speaking...';
|
|
35
|
+
if (agentState === 'listening') return isMuted ? 'Muted' : 'Listening...';
|
|
36
|
+
if (agentState === 'thinking') return 'Thinking...';
|
|
37
|
+
if (agentState === 'speaking') return 'Speaking...';
|
|
38
|
+
return 'Connected';
|
|
39
|
+
}
|
|
40
|
+
return '';
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function VoiceControls({
|
|
44
|
+
roomState,
|
|
45
|
+
agentState,
|
|
46
|
+
isMuted,
|
|
47
|
+
cohostActive,
|
|
48
|
+
currentSpeaker,
|
|
49
|
+
roundComplete,
|
|
50
|
+
onToggleMute,
|
|
51
|
+
onEnableCohost,
|
|
52
|
+
onDisableCohost,
|
|
53
|
+
onContinueCohost,
|
|
54
|
+
onEnd,
|
|
55
|
+
}: VoiceControlsProps) {
|
|
56
|
+
const colors = useAliaColors();
|
|
57
|
+
const statusText = getStatusText(roomState, agentState, isMuted, cohostActive, currentSpeaker);
|
|
58
|
+
|
|
59
|
+
return (
|
|
60
|
+
<View style={styles.container}>
|
|
61
|
+
{statusText ? (
|
|
62
|
+
<Text style={[styles.statusText, { color: colors.text }]}>
|
|
63
|
+
{statusText}
|
|
64
|
+
</Text>
|
|
65
|
+
) : null}
|
|
66
|
+
|
|
67
|
+
{roundComplete && (
|
|
68
|
+
<Pressable
|
|
69
|
+
onPress={onContinueCohost}
|
|
70
|
+
style={styles.continueButton}
|
|
71
|
+
>
|
|
72
|
+
<Text style={styles.continueText}>Continue conversation</Text>
|
|
73
|
+
</Pressable>
|
|
74
|
+
)}
|
|
75
|
+
|
|
76
|
+
{roomState === 'connected' && (
|
|
77
|
+
<View style={styles.buttonRow}>
|
|
78
|
+
<View style={styles.buttonWrapper}>
|
|
79
|
+
<Pressable
|
|
80
|
+
onPress={onToggleMute}
|
|
81
|
+
style={[
|
|
82
|
+
styles.button,
|
|
83
|
+
{ backgroundColor: isMuted ? '#ef4444' : 'rgba(255,255,255,0.15)' },
|
|
84
|
+
]}
|
|
85
|
+
>
|
|
86
|
+
<Text style={styles.buttonIcon}>
|
|
87
|
+
{isMuted ? '\u{1F507}' : '\u{1F3A4}'}
|
|
88
|
+
</Text>
|
|
89
|
+
</Pressable>
|
|
90
|
+
<Text style={styles.buttonLabel}>
|
|
91
|
+
{isMuted ? 'Unmute' : 'Mute'}
|
|
92
|
+
</Text>
|
|
93
|
+
</View>
|
|
94
|
+
|
|
95
|
+
<View style={styles.buttonWrapper}>
|
|
96
|
+
<Pressable
|
|
97
|
+
onPress={cohostActive ? onDisableCohost : onEnableCohost}
|
|
98
|
+
style={[
|
|
99
|
+
styles.button,
|
|
100
|
+
{ backgroundColor: cohostActive ? '#8b5cf6' : 'rgba(255,255,255,0.15)' },
|
|
101
|
+
]}
|
|
102
|
+
>
|
|
103
|
+
<Text style={styles.buttonIcon}>{'\u{1F465}'}</Text>
|
|
104
|
+
</Pressable>
|
|
105
|
+
<Text style={styles.buttonLabel}>
|
|
106
|
+
{cohostActive ? 'Solo' : 'Cohost'}
|
|
107
|
+
</Text>
|
|
108
|
+
</View>
|
|
109
|
+
|
|
110
|
+
<View style={styles.buttonWrapper}>
|
|
111
|
+
<Pressable
|
|
112
|
+
onPress={onEnd}
|
|
113
|
+
style={[styles.button, { backgroundColor: '#ef4444' }]}
|
|
114
|
+
>
|
|
115
|
+
<Text style={styles.endIcon}>{'\u2715'}</Text>
|
|
116
|
+
</Pressable>
|
|
117
|
+
<Text style={styles.buttonLabel}>End</Text>
|
|
118
|
+
</View>
|
|
119
|
+
</View>
|
|
120
|
+
)}
|
|
121
|
+
|
|
122
|
+
{roomState === 'connecting' && (
|
|
123
|
+
<ActivityIndicator size="large" color="#38bdf8" />
|
|
124
|
+
)}
|
|
125
|
+
</View>
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const styles = StyleSheet.create({
|
|
130
|
+
container: {
|
|
131
|
+
alignItems: 'center',
|
|
132
|
+
paddingBottom: 24,
|
|
133
|
+
paddingTop: 16,
|
|
134
|
+
},
|
|
135
|
+
statusText: {
|
|
136
|
+
fontSize: 18,
|
|
137
|
+
fontWeight: '500',
|
|
138
|
+
marginBottom: 16,
|
|
139
|
+
},
|
|
140
|
+
buttonRow: {
|
|
141
|
+
flexDirection: 'row',
|
|
142
|
+
alignItems: 'center',
|
|
143
|
+
gap: 32,
|
|
144
|
+
},
|
|
145
|
+
buttonWrapper: {
|
|
146
|
+
alignItems: 'center',
|
|
147
|
+
gap: 8,
|
|
148
|
+
},
|
|
149
|
+
button: {
|
|
150
|
+
width: 56,
|
|
151
|
+
height: 56,
|
|
152
|
+
borderRadius: 28,
|
|
153
|
+
alignItems: 'center',
|
|
154
|
+
justifyContent: 'center',
|
|
155
|
+
},
|
|
156
|
+
buttonIcon: {
|
|
157
|
+
fontSize: 24,
|
|
158
|
+
},
|
|
159
|
+
endIcon: {
|
|
160
|
+
fontSize: 24,
|
|
161
|
+
color: '#ffffff',
|
|
162
|
+
fontWeight: '700',
|
|
163
|
+
},
|
|
164
|
+
buttonLabel: {
|
|
165
|
+
color: '#8E8E93',
|
|
166
|
+
fontSize: 12,
|
|
167
|
+
},
|
|
168
|
+
continueButton: {
|
|
169
|
+
marginBottom: 16,
|
|
170
|
+
paddingHorizontal: 20,
|
|
171
|
+
paddingVertical: 8,
|
|
172
|
+
borderRadius: 24,
|
|
173
|
+
backgroundColor: 'rgba(139, 92, 246, 0.3)',
|
|
174
|
+
},
|
|
175
|
+
continueText: {
|
|
176
|
+
color: '#a78bfa',
|
|
177
|
+
fontSize: 14,
|
|
178
|
+
fontWeight: '500',
|
|
179
|
+
},
|
|
180
|
+
});
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Subtle voice mode overlay that renders the AudioWaveVisualizer
|
|
3
|
+
* behind the conversation message list. Preserves the normal chat theme
|
|
4
|
+
* with waves at reduced opacity for a seamless visual experience.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { View } from 'react-native';
|
|
8
|
+
import Animated, { FadeIn, FadeOut } from 'react-native-reanimated';
|
|
9
|
+
import { AudioWaveVisualizer } from './AudioWaveVisualizer';
|
|
10
|
+
import type { SharedValue } from 'react-native-reanimated';
|
|
11
|
+
import type { AgentState } from '../../types';
|
|
12
|
+
|
|
13
|
+
interface VoiceOverlayProps {
|
|
14
|
+
waveAmplitude: SharedValue<number>;
|
|
15
|
+
agentState: AgentState;
|
|
16
|
+
isConnected: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function VoiceOverlay({ waveAmplitude, agentState, isConnected }: VoiceOverlayProps) {
|
|
20
|
+
return (
|
|
21
|
+
<Animated.View
|
|
22
|
+
entering={FadeIn.duration(400)}
|
|
23
|
+
exiting={FadeOut.duration(300)}
|
|
24
|
+
style={{
|
|
25
|
+
position: 'absolute',
|
|
26
|
+
bottom: 0,
|
|
27
|
+
left: 0,
|
|
28
|
+
right: 0,
|
|
29
|
+
height: 350,
|
|
30
|
+
zIndex: 5,
|
|
31
|
+
pointerEvents: 'none',
|
|
32
|
+
}}
|
|
33
|
+
>
|
|
34
|
+
<View style={{ opacity: 0.35 }}>
|
|
35
|
+
<AudioWaveVisualizer
|
|
36
|
+
waveAmplitude={waveAmplitude}
|
|
37
|
+
agentState={agentState}
|
|
38
|
+
isConnected={isConnected}
|
|
39
|
+
/>
|
|
40
|
+
</View>
|
|
41
|
+
</Animated.View>
|
|
42
|
+
);
|
|
43
|
+
}
|