@alia.onl/sdk 2.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alia.onl/sdk",
3
- "version": "2.0.0",
3
+ "version": "3.0.0",
4
4
  "description": "Alia AI chat SDK — reusable chat UI, voice, and streaming components for Oxy apps",
5
5
  "main": "src/index.ts",
6
6
  "react-native": "src/index.ts",
@@ -25,12 +25,34 @@
25
25
  "react-native-keyboard-controller": ">=1",
26
26
  "react-native-svg": ">=13",
27
27
  "livekit-client": ">=2.0.0",
28
- "expo-audio": ">=0.1.0"
28
+ "expo-audio": ">=0.1.0",
29
+ "nativewind": ">=4",
30
+ "clsx": ">=2",
31
+ "tailwind-merge": ">=2",
32
+ "class-variance-authority": ">=0.7",
33
+ "zeego": ">=3",
34
+ "expo-image": ">=1",
35
+ "expo-image-picker": ">=15",
36
+ "expo-document-picker": ">=12",
37
+ "expo-clipboard": ">=6",
38
+ "lucide-react-native": ">=0.300",
39
+ "@radix-ui/react-dropdown-menu": ">=2"
29
40
  },
30
41
  "peerDependenciesMeta": {
31
42
  "livekit-client": { "optional": true },
32
43
  "expo-audio": { "optional": true },
33
- "react-native-svg": { "optional": true }
44
+ "react-native-svg": { "optional": true },
45
+ "nativewind": { "optional": true },
46
+ "clsx": { "optional": true },
47
+ "tailwind-merge": { "optional": true },
48
+ "class-variance-authority": { "optional": true },
49
+ "zeego": { "optional": true },
50
+ "expo-image": { "optional": true },
51
+ "expo-image-picker": { "optional": true },
52
+ "expo-document-picker": { "optional": true },
53
+ "expo-clipboard": { "optional": true },
54
+ "lucide-react-native": { "optional": true },
55
+ "@radix-ui/react-dropdown-menu": { "optional": true }
34
56
  },
35
57
  "devDependencies": {
36
58
  "typescript": "^5.9.3"
@@ -0,0 +1,313 @@
1
+ /**
2
+ * Shared chat content component used by both AliaChatSheet and AliaChatScreen.
3
+ *
4
+ * Owns all chat + voice state internally. Containers only handle
5
+ * layout chrome (sheet modal, full-screen SafeAreaView, headers).
6
+ */
7
+
8
+ import React, {
9
+ forwardRef,
10
+ useImperativeHandle,
11
+ useState,
12
+ useCallback,
13
+ useEffect,
14
+ useMemo,
15
+ useRef,
16
+ } from 'react';
17
+ import { View, Pressable, StyleSheet } from 'react-native';
18
+ import { useSharedValue, withTiming, Easing } from 'react-native-reanimated';
19
+ import type { SharedValue } from 'react-native-reanimated';
20
+ import { Volume2 } from 'lucide-react-native';
21
+ import { useAliaChat, type UseAliaChatOptions } from '../hooks/useAliaChat';
22
+ import { useVoiceRoom } from '../hooks/useVoiceRoom';
23
+ import { useAudioLevelMonitor } from '../hooks/useAudioLevelMonitor';
24
+ import { useAudioLevels } from '../hooks/useAudioLevels';
25
+ import { useTTS } from '../hooks/useTTS';
26
+ import { useSTTStore } from '../hooks/useSpeechToText';
27
+ import { VoiceOverlay } from './voice/VoiceOverlay';
28
+ import { VoiceControls } from './voice/VoiceControls';
29
+ import { AliaChatMessageList } from './AliaChatMessageList';
30
+ import { PromptInput } from './ui/prompt-input/prompt-input';
31
+ import { Button } from './ui/button';
32
+ import { AliaChatSuggestions } from './AliaChatSuggestions';
33
+ import type { AliaExpression } from './AliaFace';
34
+ import type { AliaChatSuggestion } from '../types';
35
+ import type { ChatMessage } from '../types';
36
+ import type { Completion } from './ui/prompt-input/context';
37
+
38
+ export interface AliaChatContentProps {
39
+ clientContext?: string;
40
+ suggestions?: AliaChatSuggestion[];
41
+ model?: string;
42
+ apiUrl?: string;
43
+ /** Shared value for scroll offset (used by sheet for pan-to-dismiss) */
44
+ scrollOffsetY?: SharedValue<number>;
45
+ /** Injectable autocomplete hook */
46
+ useSuggestions?: (query: string) => { data: Completion[] | undefined; isLoading: boolean };
47
+ /** Called when a suggestion is selected from autocomplete */
48
+ onSuggestionUsed?: (suggestionId: string) => void;
49
+ /** Error handler (e.g. toast) */
50
+ onError?: (message: string) => void;
51
+ }
52
+
53
+ export interface AliaChatContentRef {
54
+ messages: ChatMessage[];
55
+ isStreaming: boolean;
56
+ isVoiceActive: boolean;
57
+ faceExpression: AliaExpression;
58
+ clear: () => void;
59
+ }
60
+
61
+ /** Adapt a voice message into the chat message format. */
62
+ function adaptVoiceMessage(vm: any): ChatMessage {
63
+ return {
64
+ id: vm.id,
65
+ role: vm.role,
66
+ content: vm.content,
67
+ toolInvocations: vm.toolInvocations,
68
+ createdAt: Date.now(),
69
+ source: 'voice' as const,
70
+ speaker: vm.speaker,
71
+ };
72
+ }
73
+
74
+ export const AliaChatContent = forwardRef<AliaChatContentRef, AliaChatContentProps>(
75
+ ({ clientContext, suggestions = [], model, apiUrl, scrollOffsetY: externalScrollOffsetY, useSuggestions, onSuggestionUsed, onError }, ref) => {
76
+ // ── Chat ──
77
+ const chatOptions: UseAliaChatOptions = { apiUrl, model, clientContext };
78
+ const { messages, setMessages, send, isStreaming, stop, clear } = useAliaChat(chatOptions);
79
+
80
+ // ── Input state ──
81
+ const [inputValue, setInputValue] = useState('');
82
+
83
+ const handleSubmit = useCallback(() => {
84
+ const text = inputValue.trim();
85
+ if (!text) return;
86
+ send(text);
87
+ setInputValue('');
88
+ }, [inputValue, send]);
89
+
90
+ // ── Scroll offset (use external if provided, else internal) ──
91
+ const internalScrollOffsetY = useSharedValue(0);
92
+ const scrollOffsetY = externalScrollOffsetY ?? internalScrollOffsetY;
93
+
94
+ // ── Voice room ──
95
+ const voiceRoom = useVoiceRoom({ apiUrl });
96
+ const { captureLevel, playbackLevel } = useAudioLevelMonitor(voiceRoom.room, voiceRoom.isConnected);
97
+ const { waveAmplitude } = useAudioLevels({
98
+ captureLevel,
99
+ playbackLevel,
100
+ agentState: voiceRoom.agentState,
101
+ isConnected: voiceRoom.isConnected,
102
+ });
103
+
104
+ // ── TTS ──
105
+ const tts = useTTS({ apiUrl });
106
+
107
+ // ── STT wave overlay ──
108
+ const sttIsRecording = useSTTStore((s) => s.isRecording);
109
+ const sttMetering = useSTTStore((s) => s.metering);
110
+ const sttWaveAmplitude = useSharedValue(0);
111
+
112
+ useEffect(() => {
113
+ if (!sttIsRecording) {
114
+ sttWaveAmplitude.value = withTiming(0, { duration: 300 });
115
+ return;
116
+ }
117
+ const target = Math.max(0.08, sttMetering);
118
+ const duration = target > sttWaveAmplitude.value ? 60 : 200;
119
+ sttWaveAmplitude.value = withTiming(target, {
120
+ duration,
121
+ easing: Easing.bezier(0.33, 1, 0.68, 1),
122
+ });
123
+ }, [sttIsRecording, sttMetering]);
124
+
125
+ // ── Voice mode state ──
126
+ const [isVoiceActive, setIsVoiceActive] = useState(false);
127
+ const textSnapshotRef = useRef<ChatMessage[]>([]);
128
+
129
+ // Merge voice messages into chat messages
130
+ useEffect(() => {
131
+ if (!isVoiceActive) return;
132
+ const adapted = voiceRoom.messages.map(adaptVoiceMessage);
133
+ setMessages([...textSnapshotRef.current, ...adapted]);
134
+ }, [voiceRoom.messages, isVoiceActive, setMessages]);
135
+
136
+ // Auto-deactivate on error
137
+ useEffect(() => {
138
+ if (!isVoiceActive) return;
139
+ if (voiceRoom.error) {
140
+ deactivateVoice();
141
+ } else if (voiceRoom.roomState === 'error') {
142
+ deactivateVoice();
143
+ }
144
+ }, [voiceRoom.error, voiceRoom.roomState, isVoiceActive]);
145
+
146
+ // Auto-deactivate on unexpected disconnection
147
+ useEffect(() => {
148
+ if (!isVoiceActive) return;
149
+ if (voiceRoom.roomState === 'disconnected' && textSnapshotRef.current.length > 0) {
150
+ deactivateVoice();
151
+ }
152
+ }, [voiceRoom.roomState, isVoiceActive]);
153
+
154
+ const activateVoice = useCallback(() => {
155
+ if (isVoiceActive || voiceRoom.roomState === 'connecting') return;
156
+ textSnapshotRef.current = [...messages];
157
+ setIsVoiceActive(true);
158
+ voiceRoom.connect();
159
+ }, [isVoiceActive, voiceRoom, messages]);
160
+
161
+ const deactivateVoice = useCallback(() => {
162
+ voiceRoom.disconnect();
163
+ setIsVoiceActive(false);
164
+ textSnapshotRef.current = [];
165
+ }, [voiceRoom]);
166
+
167
+ // ── TTS read aloud handler ──
168
+ const handleReadAloud = useCallback(
169
+ (messageId: string, text: string) => {
170
+ tts.readAloud(messageId, text);
171
+ },
172
+ [tts],
173
+ );
174
+
175
+ // ── Face expression ──
176
+ const faceExpression = useMemo<AliaExpression>(() => {
177
+ if (isVoiceActive) {
178
+ if (voiceRoom.agentState === 'speaking') return 'Writing E';
179
+ if (voiceRoom.agentState === 'thinking') return 'Thinking';
180
+ return 'Idle A';
181
+ }
182
+ if (!isStreaming) return 'Idle A';
183
+ const lastMsg = messages[messages.length - 1];
184
+ if (lastMsg?.role === 'assistant' && lastMsg.toolInvocations?.some((t) => t.state === 'call'))
185
+ return 'Searching A';
186
+ if (lastMsg?.thinking) return 'Thinking';
187
+ return 'Writing E';
188
+ }, [messages, isStreaming, isVoiceActive, voiceRoom.agentState]);
189
+
190
+ const showSuggestions = messages.length === 0 && suggestions.length > 0;
191
+
192
+ // ── Overlay state ──
193
+ const showVoiceOverlay = isVoiceActive;
194
+ const showTTSOverlay = !isVoiceActive && tts.playbackState === 'playing';
195
+ const showSTTOverlay = !isVoiceActive && tts.playbackState !== 'playing' && sttIsRecording;
196
+
197
+ // ── Voice activate button for empty submit ──
198
+ const voiceActivateButton = useMemo(
199
+ () => (
200
+ <Button
201
+ size="icon"
202
+ onPress={activateVoice}
203
+ className="h-8 w-8 rounded-full"
204
+ >
205
+ <Volume2 size={16} color="white" />
206
+ </Button>
207
+ ),
208
+ [activateVoice],
209
+ );
210
+
211
+ // ── Expose state to container ──
212
+ useImperativeHandle(
213
+ ref,
214
+ () => ({
215
+ messages,
216
+ isStreaming,
217
+ isVoiceActive,
218
+ faceExpression,
219
+ clear,
220
+ }),
221
+ [messages, isStreaming, isVoiceActive, faceExpression, clear],
222
+ );
223
+
224
+ return (
225
+ <View style={styles.container}>
226
+ {/* Voice/TTS/STT wave overlay */}
227
+ {showVoiceOverlay && (
228
+ <VoiceOverlay
229
+ waveAmplitude={waveAmplitude}
230
+ agentState={voiceRoom.agentState}
231
+ isConnected={voiceRoom.isConnected}
232
+ />
233
+ )}
234
+ {showTTSOverlay && (
235
+ <VoiceOverlay
236
+ waveAmplitude={tts.ttsWaveAmplitude}
237
+ agentState="speaking"
238
+ isConnected={true}
239
+ />
240
+ )}
241
+ {showSTTOverlay && (
242
+ <VoiceOverlay
243
+ waveAmplitude={sttWaveAmplitude}
244
+ agentState="listening"
245
+ isConnected={true}
246
+ />
247
+ )}
248
+
249
+ {/* Suggestions or Messages */}
250
+ {showSuggestions ? (
251
+ <AliaChatSuggestions suggestions={suggestions} onSelect={send} />
252
+ ) : (
253
+ <AliaChatMessageList
254
+ messages={messages}
255
+ isStreaming={isStreaming}
256
+ scrollOffsetY={scrollOffsetY}
257
+ onReadAloud={handleReadAloud}
258
+ ttsActiveMessageId={tts.activeMessageId}
259
+ ttsPlaybackState={tts.playbackState}
260
+ />
261
+ )}
262
+
263
+ {/* Input or Voice Controls */}
264
+ {isVoiceActive ? (
265
+ <VoiceControls
266
+ roomState={voiceRoom.roomState}
267
+ agentState={voiceRoom.agentState}
268
+ isMuted={voiceRoom.isMuted}
269
+ cohostActive={voiceRoom.cohostActive}
270
+ currentSpeaker={voiceRoom.currentSpeaker}
271
+ roundComplete={voiceRoom.roundComplete}
272
+ onToggleMute={voiceRoom.toggleMute}
273
+ onEnableCohost={voiceRoom.enableCohost}
274
+ onDisableCohost={voiceRoom.disableCohost}
275
+ onContinueCohost={voiceRoom.continueCohost}
276
+ onEnd={deactivateVoice}
277
+ />
278
+ ) : (
279
+ <View style={styles.inputContainer}>
280
+ <PromptInput
281
+ value={inputValue}
282
+ onValueChange={setInputValue}
283
+ onSubmit={handleSubmit}
284
+ isLoading={isStreaming}
285
+ onStop={stop}
286
+ emptyAction={voiceActivateButton}
287
+ placeholder="Ask Alia..."
288
+ apiUrl={apiUrl}
289
+ autocomplete={!!useSuggestions}
290
+ useSuggestions={useSuggestions}
291
+ onSuggestionUsed={onSuggestionUsed}
292
+ onError={onError}
293
+ />
294
+ </View>
295
+ )}
296
+ </View>
297
+ );
298
+ },
299
+ );
300
+
301
+ AliaChatContent.displayName = 'AliaChatContent';
302
+
303
+ const styles = StyleSheet.create({
304
+ container: {
305
+ flex: 1,
306
+ position: 'relative',
307
+ },
308
+ inputContainer: {
309
+ paddingHorizontal: 12,
310
+ paddingTop: 8,
311
+ paddingBottom: 8,
312
+ },
313
+ });
@@ -1,33 +1,56 @@
1
- import React, { useState } from 'react';
1
+ import React, { useState, useCallback } from 'react';
2
2
  import {
3
3
  View,
4
4
  TextInput,
5
- TouchableOpacity,
5
+ Pressable,
6
6
  StyleSheet,
7
7
  ActivityIndicator,
8
8
  Platform,
9
9
  } from 'react-native';
10
10
  import { useSafeAreaInsets } from 'react-native-safe-area-context';
11
+ import { useSpeechToText } from '../hooks/useSpeechToText';
11
12
  import { useAliaColors } from '../theme';
12
13
 
13
14
  interface AliaChatInputProps {
14
15
  onSend: (text: string) => void;
15
16
  isStreaming: boolean;
17
+ onStop: () => void;
18
+ onVoiceActivate: () => void;
19
+ apiUrl?: string;
16
20
  }
17
21
 
18
- export function AliaChatInput({ onSend, isStreaming }: AliaChatInputProps) {
22
+ export function AliaChatInput({
23
+ onSend,
24
+ isStreaming,
25
+ onStop,
26
+ onVoiceActivate,
27
+ apiUrl,
28
+ }: AliaChatInputProps) {
19
29
  const [input, setInput] = useState('');
20
30
  const colors = useAliaColors();
21
31
  const insets = useSafeAreaInsets();
22
32
  const isDark = colors.isDark;
33
+ const stt = useSpeechToText({ apiUrl });
23
34
 
24
- const canSend = input.trim().length > 0 && !isStreaming;
35
+ const hasText = input.trim().length > 0;
36
+ const canSend = hasText && !isStreaming;
25
37
 
26
- const handleSend = () => {
38
+ const handleSend = useCallback(() => {
27
39
  if (!canSend) return;
28
40
  onSend(input.trim());
29
41
  setInput('');
30
- };
42
+ }, [canSend, input, onSend]);
43
+
44
+ const handleMicPress = useCallback(async () => {
45
+ if (stt.isRecording) {
46
+ const text = await stt.stopAndTranscribe();
47
+ if (text) {
48
+ setInput((prev) => (prev ? `${prev} ${text}` : text));
49
+ }
50
+ } else if (!stt.isTranscribing) {
51
+ stt.startRecording();
52
+ }
53
+ }, [stt]);
31
54
 
32
55
  return (
33
56
  <View
@@ -55,39 +78,65 @@ export function AliaChatInput({ onSend, isStreaming }: AliaChatInputProps) {
55
78
  maxLength={2000}
56
79
  onSubmitEditing={handleSend}
57
80
  blurOnSubmit={false}
58
- editable={!isStreaming}
59
81
  />
60
- <TouchableOpacity
61
- style={[
62
- styles.sendButton,
63
- { backgroundColor: colors.tint },
64
- !canSend && { opacity: 0.4 },
65
- ]}
66
- onPress={handleSend}
67
- disabled={!canSend}
68
- activeOpacity={0.7}
82
+
83
+ {/* STT mic button */}
84
+ <Pressable
85
+ onPress={handleMicPress}
86
+ disabled={stt.isTranscribing}
87
+ style={styles.micButton}
69
88
  >
70
- {isStreaming ? (
71
- <ActivityIndicator size="small" color="#FFFFFF" />
89
+ {stt.isTranscribing ? (
90
+ <ActivityIndicator size="small" color="#6366f1" />
91
+ ) : stt.isRecording ? (
92
+ <MicOffIcon />
72
93
  ) : (
73
- <ArrowUpIcon />
94
+ <MicIcon color={colors.secondaryText} />
74
95
  )}
75
- </TouchableOpacity>
96
+ </Pressable>
97
+
98
+ {/* Three-state submit button */}
99
+ {isStreaming ? (
100
+ <Pressable
101
+ style={[styles.actionButton, { backgroundColor: colors.tint }]}
102
+ onPress={onStop}
103
+ >
104
+ <StopIcon />
105
+ </Pressable>
106
+ ) : hasText ? (
107
+ <Pressable
108
+ style={[
109
+ styles.actionButton,
110
+ { backgroundColor: colors.tint },
111
+ !canSend && { opacity: 0.4 },
112
+ ]}
113
+ onPress={handleSend}
114
+ disabled={!canSend}
115
+ >
116
+ <ArrowUpIcon />
117
+ </Pressable>
118
+ ) : (
119
+ <Pressable
120
+ style={[styles.actionButton, { backgroundColor: colors.tint }]}
121
+ onPress={onVoiceActivate}
122
+ >
123
+ <SoundIcon />
124
+ </Pressable>
125
+ )}
76
126
  </View>
77
127
  );
78
128
  }
79
129
 
80
- /** Simple arrow-up SVG as a component to avoid icon library dependency */
130
+ // ── Icons (inline SVG on web, View-based on native) ──
131
+
81
132
  function ArrowUpIcon() {
82
- // Use a unicode character for simplicity — works cross-platform
83
133
  return (
84
- <View style={styles.arrowIcon}>
134
+ <View style={styles.iconCenter}>
85
135
  {Platform.OS === 'web' ? (
86
136
  <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="white" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
87
137
  <path d="M12 19V5M5 12l7-7 7 7" />
88
138
  </svg>
89
139
  ) : (
90
- // React Native fallback — simple Text arrow
91
140
  <View style={styles.arrowIconInner}>
92
141
  <View style={[styles.arrowStem, { backgroundColor: '#FFFFFF' }]} />
93
142
  <View style={[styles.arrowHead, { borderColor: '#FFFFFF' }]} />
@@ -97,6 +146,74 @@ function ArrowUpIcon() {
97
146
  );
98
147
  }
99
148
 
149
+ function StopIcon() {
150
+ return (
151
+ <View style={styles.iconCenter}>
152
+ {Platform.OS === 'web' ? (
153
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="white">
154
+ <rect x="4" y="4" width="16" height="16" rx="2" />
155
+ </svg>
156
+ ) : (
157
+ <View style={styles.stopSquare} />
158
+ )}
159
+ </View>
160
+ );
161
+ }
162
+
163
+ function SoundIcon() {
164
+ return (
165
+ <View style={styles.iconCenter}>
166
+ {Platform.OS === 'web' ? (
167
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="white" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
168
+ <path d="M11 5L6 9H2v6h4l5 4V5z" />
169
+ <path d="M15.54 8.46a5 5 0 0 1 0 7.07" />
170
+ <path d="M19.07 4.93a10 10 0 0 1 0 14.14" />
171
+ </svg>
172
+ ) : (
173
+ <View style={styles.soundBars}>
174
+ <View style={[styles.soundBar, styles.soundBarShort]} />
175
+ <View style={[styles.soundBar, styles.soundBarMed]} />
176
+ <View style={[styles.soundBar, styles.soundBarTall]} />
177
+ </View>
178
+ )}
179
+ </View>
180
+ );
181
+ }
182
+
183
+ function MicIcon({ color }: { color: string }) {
184
+ return (
185
+ <View style={styles.iconCenter}>
186
+ {Platform.OS === 'web' ? (
187
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke={color} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
188
+ <rect x="9" y="1" width="6" height="12" rx="3" />
189
+ <path d="M5 10a7 7 0 0 0 14 0" />
190
+ <line x1="12" y1="17" x2="12" y2="21" />
191
+ <line x1="8" y1="21" x2="16" y2="21" />
192
+ </svg>
193
+ ) : (
194
+ <View style={[styles.micDot, { backgroundColor: color }]} />
195
+ )}
196
+ </View>
197
+ );
198
+ }
199
+
200
+ function MicOffIcon() {
201
+ return (
202
+ <View style={styles.iconCenter}>
203
+ {Platform.OS === 'web' ? (
204
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#ef4444" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
205
+ <rect x="9" y="1" width="6" height="12" rx="3" />
206
+ <path d="M5 10a7 7 0 0 0 14 0" />
207
+ <line x1="12" y1="17" x2="12" y2="21" />
208
+ <line x1="1" y1="1" x2="23" y2="23" />
209
+ </svg>
210
+ ) : (
211
+ <View style={[styles.micDot, { backgroundColor: '#ef4444' }]} />
212
+ )}
213
+ </View>
214
+ );
215
+ }
216
+
100
217
  const styles = StyleSheet.create({
101
218
  container: {
102
219
  flexDirection: 'row',
@@ -114,14 +231,20 @@ const styles = StyleSheet.create({
114
231
  borderRadius: 20,
115
232
  maxHeight: 100,
116
233
  },
117
- sendButton: {
234
+ micButton: {
235
+ width: 32,
236
+ height: 40,
237
+ alignItems: 'center',
238
+ justifyContent: 'center',
239
+ },
240
+ actionButton: {
118
241
  width: 40,
119
242
  height: 40,
120
243
  borderRadius: 20,
121
244
  alignItems: 'center',
122
245
  justifyContent: 'center',
123
246
  },
124
- arrowIcon: {
247
+ iconCenter: {
125
248
  width: 18,
126
249
  height: 18,
127
250
  alignItems: 'center',
@@ -144,4 +267,28 @@ const styles = StyleSheet.create({
144
267
  borderLeftWidth: 2.5,
145
268
  transform: [{ rotate: '45deg' }],
146
269
  },
270
+ stopSquare: {
271
+ width: 12,
272
+ height: 12,
273
+ borderRadius: 2,
274
+ backgroundColor: '#FFFFFF',
275
+ },
276
+ soundBars: {
277
+ flexDirection: 'row',
278
+ alignItems: 'flex-end',
279
+ gap: 2,
280
+ },
281
+ soundBar: {
282
+ width: 3,
283
+ backgroundColor: '#FFFFFF',
284
+ borderRadius: 1.5,
285
+ },
286
+ soundBarShort: { height: 6 },
287
+ soundBarMed: { height: 10 },
288
+ soundBarTall: { height: 14 },
289
+ micDot: {
290
+ width: 12,
291
+ height: 12,
292
+ borderRadius: 6,
293
+ },
147
294
  });