@alia.onl/sdk 4.0.1 → 5.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/CHANGELOG.md +96 -0
- package/package.json +9 -4
- package/src/components/AliaChatContent.tsx +65 -80
- package/src/components/AliaChatMessageList.tsx +7 -9
- package/src/components/AliaChatScreen.tsx +9 -0
- package/src/components/AliaChatSheet.tsx +9 -1
- package/src/components/PlanPreviewCard.tsx +3 -1
- package/src/components/ResearchProgressCard.tsx +9 -11
- package/src/components/ThinkingIndicator.tsx +2 -2
- package/src/components/ui/dropdown-menu.web.tsx +36 -9
- package/src/components/ui/prompt-input/add-menu.tsx +1 -1
- package/src/components/ui/prompt-input/attachments.tsx +8 -10
- package/src/components/ui/prompt-input/mic-button.tsx +2 -1
- package/src/components/ui/prompt-input/prompt-input.tsx +2 -1
- package/src/components/ui/prompt-input/submit-button.tsx +2 -1
- package/src/components/voice/VoiceControls.tsx +4 -1
- package/src/components/voice/VoiceSession.tsx +88 -0
- package/src/index.ts +7 -0
- package/src/types.ts +32 -0
- package/src/voice.ts +3 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# @alia.onl/sdk
|
|
2
|
+
|
|
3
|
+
## 5.0.0
|
|
4
|
+
|
|
5
|
+
### Breaking
|
|
6
|
+
|
|
7
|
+
- **Voice is now an injected capability on `AliaChatScreen` and
|
|
8
|
+
`AliaChatSheet`.** Pass `voiceSession={VoiceSession}` (from
|
|
9
|
+
`@alia.onl/sdk/voice`) to offer voice calls; without it the chat is text-only
|
|
10
|
+
and the microphone button is not rendered.
|
|
11
|
+
|
|
12
|
+
```tsx
|
|
13
|
+
import { AliaChatScreen } from '@alia.onl/sdk';
|
|
14
|
+
import { VoiceSession } from '@alia.onl/sdk/voice';
|
|
15
|
+
|
|
16
|
+
<AliaChatScreen voiceSession={VoiceSession} />;
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
`voiceSession` may be a `React.lazy` component — the chat renders it inside
|
|
20
|
+
its own `Suspense` boundary.
|
|
21
|
+
|
|
22
|
+
v4.0.0 moved the voice exports out of the root barrel so that text-chat
|
|
23
|
+
consumers would stop compiling `livekit-client`, but `AliaChatContent` still
|
|
24
|
+
imported `useVoiceRoom` and `useAudioLevelMonitor` directly from their
|
|
25
|
+
modules, and `AliaChatScreen`/`AliaChatSheet` reach it. Since this package
|
|
26
|
+
ships raw source, that put the whole LiveKit client (1,204,825 bytes raw,
|
|
27
|
+
~250 KiB gzip) back into every text-chat consumer's module graph. The split
|
|
28
|
+
could not be made real while the shell owned voice, so it no longer does.
|
|
29
|
+
|
|
30
|
+
### Added
|
|
31
|
+
|
|
32
|
+
- `VoiceSession` (`@alia.onl/sdk/voice`) — the LiveKit half of the chat as one
|
|
33
|
+
component. It dials on mount, ends the call on unmount, renders
|
|
34
|
+
`VoiceControls`, and reports room state, agent state, audio amplitude, and the
|
|
35
|
+
transcript back to the chat shell.
|
|
36
|
+
- `VoiceSessionComponent`, `VoiceSessionProps`, and `VoiceSessionState` types,
|
|
37
|
+
exported from both entries.
|
|
38
|
+
|
|
39
|
+
### Fixed
|
|
40
|
+
|
|
41
|
+
- An unexpected room drop now always ends voice mode. It previously required a
|
|
42
|
+
non-empty pre-call transcript, so a dropped call in a fresh chat left the
|
|
43
|
+
voice UI up with no room behind it.
|
|
44
|
+
|
|
45
|
+
### Internal
|
|
46
|
+
|
|
47
|
+
- `bun run --filter @alia.onl/sdk check:entries` walks the real import graph and
|
|
48
|
+
fails if the root entry can reach `livekit-client` (or if the voice entry
|
|
49
|
+
cannot). Wired into CI alongside a new SDK typecheck step.
|
|
50
|
+
|
|
51
|
+
## 4.1.0
|
|
52
|
+
|
|
53
|
+
### Changed
|
|
54
|
+
|
|
55
|
+
- Every `lucide-react-native` import is now a per-icon subpath
|
|
56
|
+
(`lucide-react-native/icons/<name>`) instead of the package barrel, and the
|
|
57
|
+
dependency moved from `^0.562.0` to `^1.24.0` (the first line that exposes
|
|
58
|
+
`./icons/*` in its `exports` map, with per-icon type declarations).
|
|
59
|
+
|
|
60
|
+
The barrel statically re-exports ~1750 icon modules and Metro does not
|
|
61
|
+
tree-shake, so a single named import pulled roughly 1.2 MB of unused icon
|
|
62
|
+
source into every consumer bundle. Measured with a real Metro build of the
|
|
63
|
+
same eight icons: 1755 modules / 1 656 869 bytes via the barrel versus
|
|
64
|
+
16 modules / 18 312 bytes via subpaths.
|
|
65
|
+
|
|
66
|
+
- **Consumers must enable Metro's package-exports resolution.** Set
|
|
67
|
+
`resolver.unstable_enablePackageExports = true` in `metro.config.js`. It is
|
|
68
|
+
the default in Expo SDK 53+ and Metro 0.82+, so most projects already have
|
|
69
|
+
it. Without it the bundle fails at build time with
|
|
70
|
+
`Unable to resolve module lucide-react-native/icons/…` — a loud failure, not
|
|
71
|
+
a silent runtime one.
|
|
72
|
+
|
|
73
|
+
The public API is unchanged: no icon is re-exported from `.` or `./voice`, and
|
|
74
|
+
no exported component, hook, or type was added, removed, or renamed.
|
|
75
|
+
|
|
76
|
+
## 4.0.1
|
|
77
|
+
|
|
78
|
+
- Expose `./package.json` in the `exports` map.
|
|
79
|
+
|
|
80
|
+
## 4.0.0
|
|
81
|
+
|
|
82
|
+
### Breaking
|
|
83
|
+
|
|
84
|
+
- The voice surface moved from the root entry to `@alia.onl/sdk/voice` so
|
|
85
|
+
text-chat consumers no longer pull `livekit-client` through the export
|
|
86
|
+
barrel. Update imports of `AudioWaveVisualizer`, `VoiceOverlay`,
|
|
87
|
+
`VoiceControls`, `useVoiceRoom`, `useAudioLevelMonitor`, `useAudioLevels`,
|
|
88
|
+
`useAmbientWave`, `useSoundEffects`, `useVoiceSoundEffects`, and the
|
|
89
|
+
`RoomState` / `AgentState` / `VoiceMessage` / `VoiceToolInvocation` types.
|
|
90
|
+
|
|
91
|
+
Note that rendering `AliaChatScreen` or `AliaChatSheet` still reaches
|
|
92
|
+
`livekit-client`: `AliaChatContent` imports `useVoiceRoom` and
|
|
93
|
+
`useAudioLevelMonitor` directly. Only consumers composing the leaf
|
|
94
|
+
primitives themselves avoid it.
|
|
95
|
+
- An `exports` map was introduced, so deep imports into package internals no
|
|
96
|
+
longer resolve. Only `.`, `./voice`, and `./package.json` are public.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alia.onl/sdk",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "5.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",
|
|
@@ -13,12 +13,17 @@
|
|
|
13
13
|
},
|
|
14
14
|
"files": [
|
|
15
15
|
"src",
|
|
16
|
-
"tsconfig.json"
|
|
16
|
+
"tsconfig.json",
|
|
17
|
+
"CHANGELOG.md"
|
|
17
18
|
],
|
|
18
19
|
"private": false,
|
|
19
20
|
"publishConfig": {
|
|
20
21
|
"access": "public"
|
|
21
22
|
},
|
|
23
|
+
"scripts": {
|
|
24
|
+
"typecheck": "tsc --noEmit",
|
|
25
|
+
"check:entries": "node scripts/check-entry-isolation.mjs"
|
|
26
|
+
},
|
|
22
27
|
"dependencies": {
|
|
23
28
|
"@radix-ui/react-dropdown-menu": "^2.1.20",
|
|
24
29
|
"@radix-ui/react-icons": "^1.3.2",
|
|
@@ -27,7 +32,7 @@
|
|
|
27
32
|
"@tanstack/react-query-persist-client": "^5.101.2",
|
|
28
33
|
"class-variance-authority": "^0.7.1",
|
|
29
34
|
"clsx": "^2.1.1",
|
|
30
|
-
"lucide-react-native": "^
|
|
35
|
+
"lucide-react-native": "^1.24.0",
|
|
31
36
|
"react-native-markdown-display": "^7.0.2",
|
|
32
37
|
"tailwind-merge": "^3.6.0",
|
|
33
38
|
"zeego": "^3.0.6",
|
|
@@ -36,7 +41,7 @@
|
|
|
36
41
|
"peerDependencies": {
|
|
37
42
|
"react": "^19.2.3",
|
|
38
43
|
"react-native": "^0.86.0",
|
|
39
|
-
"@oxyhq/services": "^
|
|
44
|
+
"@oxyhq/services": "^23.0.1",
|
|
40
45
|
"@react-native-async-storage/async-storage": "^2.2.0",
|
|
41
46
|
"socket.io-client": "^4.8.3",
|
|
42
47
|
"expo-font": "^57.0.0",
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Shared chat content component used by both AliaChatSheet and AliaChatScreen.
|
|
3
3
|
*
|
|
4
|
-
* Owns
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Owns the text chat; voice is an injected capability (`voiceSession`) so that
|
|
5
|
+
* nothing here reaches `livekit-client` at module-evaluation time. Containers
|
|
6
|
+
* only handle layout chrome (sheet modal, full-screen SafeAreaView) and pass a
|
|
7
|
+
* `header` render prop that receives the live mark state.
|
|
7
8
|
*/
|
|
8
9
|
|
|
9
10
|
import React, {
|
|
11
|
+
Suspense,
|
|
10
12
|
useState,
|
|
11
13
|
useCallback,
|
|
12
14
|
useEffect,
|
|
@@ -16,21 +18,22 @@ import React, {
|
|
|
16
18
|
import { View, StyleSheet } from 'react-native';
|
|
17
19
|
import { useSharedValue } from 'react-native-reanimated';
|
|
18
20
|
import type { SharedValue } from 'react-native-reanimated';
|
|
19
|
-
import
|
|
21
|
+
import Volume2 from 'lucide-react-native/icons/volume-2';
|
|
20
22
|
import { useAliaChat, type UseAliaChatOptions } from '../hooks/useAliaChat';
|
|
21
|
-
import { useVoiceRoom } from '../hooks/useVoiceRoom';
|
|
22
|
-
import { useAudioLevelMonitor } from '../hooks/useAudioLevelMonitor';
|
|
23
|
-
import { useAudioLevels } from '../hooks/useAudioLevels';
|
|
24
23
|
import { useTTS } from '../hooks/useTTS';
|
|
25
24
|
import { useAmbientWave } from '../hooks/useAmbientWave';
|
|
26
25
|
import { VoiceOverlay } from './voice/VoiceOverlay';
|
|
27
|
-
import { VoiceControls } from './voice/VoiceControls';
|
|
28
26
|
import { AliaChatMessageList } from './AliaChatMessageList';
|
|
29
27
|
import { AliaWelcomeMessage, type WelcomeSuggestion } from './AliaWelcomeMessage';
|
|
30
28
|
import { PromptInput } from './ui/prompt-input/prompt-input';
|
|
31
29
|
import { Button } from './ui/button';
|
|
32
30
|
import type { AliaMarkState } from './AliaMark';
|
|
33
|
-
import type {
|
|
31
|
+
import type {
|
|
32
|
+
ChatMessage,
|
|
33
|
+
VoiceMessage,
|
|
34
|
+
VoiceSessionComponent,
|
|
35
|
+
VoiceSessionState,
|
|
36
|
+
} from '../types';
|
|
34
37
|
import type { Completion } from './ui/prompt-input/context';
|
|
35
38
|
|
|
36
39
|
export interface AliaChatContentProps {
|
|
@@ -64,6 +67,12 @@ export interface AliaChatContentProps {
|
|
|
64
67
|
primaryColor?: string;
|
|
65
68
|
/** Dark-mode flag — forwarded to the ambient wave overlay. */
|
|
66
69
|
isDarkMode?: boolean;
|
|
70
|
+
/**
|
|
71
|
+
* Voice capability — pass `VoiceSession` from `@alia.onl/sdk/voice` to offer
|
|
72
|
+
* voice calls. Omitted, the chat is text-only and no LiveKit code is reachable
|
|
73
|
+
* from this module. May be a `React.lazy` component.
|
|
74
|
+
*/
|
|
75
|
+
voiceSession?: VoiceSessionComponent;
|
|
67
76
|
}
|
|
68
77
|
|
|
69
78
|
/** Adapt a voice message into the chat message format. */
|
|
@@ -100,6 +109,7 @@ export function AliaChatContent({
|
|
|
100
109
|
header,
|
|
101
110
|
primaryColor,
|
|
102
111
|
isDarkMode,
|
|
112
|
+
voiceSession: VoiceSession,
|
|
103
113
|
}: AliaChatContentProps) {
|
|
104
114
|
// ── Chat ──
|
|
105
115
|
const chatOptions: UseAliaChatOptions = { apiUrl, model, clientContext };
|
|
@@ -119,60 +129,36 @@ export function AliaChatContent({
|
|
|
119
129
|
const internalScrollOffsetY = useSharedValue(0);
|
|
120
130
|
const scrollOffsetY = externalScrollOffsetY ?? internalScrollOffsetY;
|
|
121
131
|
|
|
122
|
-
// ── Voice room ──
|
|
123
|
-
const voiceRoom = useVoiceRoom({ apiUrl });
|
|
124
|
-
const { captureLevel, playbackLevel } = useAudioLevelMonitor(voiceRoom.room, voiceRoom.isConnected);
|
|
125
|
-
const { waveAmplitude } = useAudioLevels({
|
|
126
|
-
captureLevel,
|
|
127
|
-
playbackLevel,
|
|
128
|
-
agentState: voiceRoom.agentState,
|
|
129
|
-
isConnected: voiceRoom.isConnected,
|
|
130
|
-
});
|
|
131
|
-
|
|
132
132
|
// ── TTS ──
|
|
133
133
|
const tts = useTTS({ apiUrl });
|
|
134
134
|
|
|
135
135
|
// ── Voice mode state ──
|
|
136
|
+
// `isVoiceActive` mounts the session; `voiceState` is what the session reports
|
|
137
|
+
// back, and stays null until the call is under way.
|
|
136
138
|
const [isVoiceActive, setIsVoiceActive] = useState(false);
|
|
139
|
+
const [voiceState, setVoiceState] = useState<VoiceSessionState | null>(null);
|
|
137
140
|
const textSnapshotRef = useRef<ChatMessage[]>([]);
|
|
138
141
|
|
|
139
142
|
// Merge voice messages into chat messages
|
|
143
|
+
const voiceMessages = voiceState?.messages;
|
|
140
144
|
useEffect(() => {
|
|
141
|
-
if (!
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
}, [voiceRoom.messages, isVoiceActive, setMessages]);
|
|
145
|
-
|
|
146
|
-
// Auto-deactivate on error
|
|
147
|
-
useEffect(() => {
|
|
148
|
-
if (!isVoiceActive) return;
|
|
149
|
-
if (voiceRoom.error) {
|
|
150
|
-
deactivateVoice();
|
|
151
|
-
} else if (voiceRoom.roomState === 'error') {
|
|
152
|
-
deactivateVoice();
|
|
153
|
-
}
|
|
154
|
-
}, [voiceRoom.error, voiceRoom.roomState, isVoiceActive]);
|
|
155
|
-
|
|
156
|
-
// Auto-deactivate on unexpected disconnection
|
|
157
|
-
useEffect(() => {
|
|
158
|
-
if (!isVoiceActive) return;
|
|
159
|
-
if (voiceRoom.roomState === 'disconnected' && textSnapshotRef.current.length > 0) {
|
|
160
|
-
deactivateVoice();
|
|
161
|
-
}
|
|
162
|
-
}, [voiceRoom.roomState, isVoiceActive]);
|
|
145
|
+
if (!voiceMessages) return;
|
|
146
|
+
setMessages([...textSnapshotRef.current, ...voiceMessages.map(adaptVoiceMessage)]);
|
|
147
|
+
}, [voiceMessages, setMessages]);
|
|
163
148
|
|
|
164
149
|
const activateVoice = useCallback(() => {
|
|
165
|
-
if (isVoiceActive
|
|
150
|
+
if (isVoiceActive) return;
|
|
166
151
|
textSnapshotRef.current = [...messages];
|
|
167
152
|
setIsVoiceActive(true);
|
|
168
|
-
|
|
169
|
-
}, [isVoiceActive, voiceRoom, messages]);
|
|
153
|
+
}, [isVoiceActive, messages]);
|
|
170
154
|
|
|
155
|
+
// Unmounting the session disconnects the room; the transcript merged above
|
|
156
|
+
// stays in the chat.
|
|
171
157
|
const deactivateVoice = useCallback(() => {
|
|
172
|
-
voiceRoom.disconnect();
|
|
173
158
|
setIsVoiceActive(false);
|
|
159
|
+
setVoiceState(null);
|
|
174
160
|
textSnapshotRef.current = [];
|
|
175
|
-
}, [
|
|
161
|
+
}, []);
|
|
176
162
|
|
|
177
163
|
// ── TTS read aloud handler ──
|
|
178
164
|
const handleReadAloud = useCallback(
|
|
@@ -183,10 +169,11 @@ export function AliaChatContent({
|
|
|
183
169
|
);
|
|
184
170
|
|
|
185
171
|
// ── Mark state (drives the header brand mark) ──
|
|
172
|
+
const voiceAgentState = voiceState?.agentState;
|
|
186
173
|
const markState = useMemo<AliaMarkState>(() => {
|
|
187
174
|
if (isVoiceActive) {
|
|
188
|
-
if (
|
|
189
|
-
if (
|
|
175
|
+
if (voiceAgentState === 'speaking') return 'writing';
|
|
176
|
+
if (voiceAgentState === 'thinking') return 'thinking';
|
|
190
177
|
return 'idle';
|
|
191
178
|
}
|
|
192
179
|
if (!isStreaming) return 'idle';
|
|
@@ -195,7 +182,7 @@ export function AliaChatContent({
|
|
|
195
182
|
return 'working';
|
|
196
183
|
if (lastMsg?.thinking) return 'thinking';
|
|
197
184
|
return 'writing';
|
|
198
|
-
}, [messages, isStreaming, isVoiceActive,
|
|
185
|
+
}, [messages, isStreaming, isVoiceActive, voiceAgentState]);
|
|
199
186
|
|
|
200
187
|
const welcomeComponent = useMemo(() => {
|
|
201
188
|
if (!welcomeGreeting || messages.length > 0) return undefined;
|
|
@@ -211,29 +198,32 @@ export function AliaChatContent({
|
|
|
211
198
|
|
|
212
199
|
// ── Ambient wave — one persistent overlay across idle/voice/TTS/STT ──
|
|
213
200
|
const wave = useAmbientWave({
|
|
214
|
-
voice:
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
201
|
+
voice: voiceState
|
|
202
|
+
? {
|
|
203
|
+
isActive: isVoiceActive,
|
|
204
|
+
isConnected: voiceState.isConnected,
|
|
205
|
+
agentState: voiceState.agentState,
|
|
206
|
+
waveAmplitude: voiceState.waveAmplitude,
|
|
207
|
+
}
|
|
208
|
+
: undefined,
|
|
220
209
|
isTTSPlaying: tts.playbackState === 'playing',
|
|
221
210
|
ttsWaveAmplitude: tts.ttsWaveAmplitude,
|
|
222
211
|
isGenerating: isStreaming,
|
|
223
212
|
});
|
|
224
213
|
|
|
225
|
-
// ── Voice activate button for empty submit ──
|
|
214
|
+
// ── Voice activate button for empty submit (only if voice was provided) ──
|
|
226
215
|
const voiceActivateButton = useMemo(
|
|
227
|
-
() =>
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
216
|
+
() =>
|
|
217
|
+
VoiceSession ? (
|
|
218
|
+
<Button
|
|
219
|
+
size="icon"
|
|
220
|
+
onPress={activateVoice}
|
|
221
|
+
className="h-8 w-8 rounded-full"
|
|
222
|
+
>
|
|
223
|
+
<Volume2 size={16} color="white" />
|
|
224
|
+
</Button>
|
|
225
|
+
) : undefined,
|
|
226
|
+
[VoiceSession, activateVoice],
|
|
237
227
|
);
|
|
238
228
|
|
|
239
229
|
return (
|
|
@@ -269,20 +259,15 @@ export function AliaChatContent({
|
|
|
269
259
|
/>
|
|
270
260
|
|
|
271
261
|
{/* Input or Voice Controls */}
|
|
272
|
-
{isVoiceActive ? (
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
onEnableCohost={voiceRoom.enableCohost}
|
|
282
|
-
onDisableCohost={voiceRoom.disableCohost}
|
|
283
|
-
onContinueCohost={voiceRoom.continueCohost}
|
|
284
|
-
onEnd={deactivateVoice}
|
|
285
|
-
/>
|
|
262
|
+
{VoiceSession && isVoiceActive ? (
|
|
263
|
+
/* Suspense so the session may be handed to us as a React.lazy import. */
|
|
264
|
+
<Suspense fallback={null}>
|
|
265
|
+
<VoiceSession
|
|
266
|
+
apiUrl={apiUrl}
|
|
267
|
+
onStateChange={setVoiceState}
|
|
268
|
+
onEnd={deactivateVoice}
|
|
269
|
+
/>
|
|
270
|
+
</Suspense>
|
|
286
271
|
) : (
|
|
287
272
|
<View style={styles.inputContainer}>
|
|
288
273
|
<PromptInput
|
|
@@ -11,15 +11,13 @@ import Animated, {
|
|
|
11
11
|
cancelAnimation,
|
|
12
12
|
type SharedValue,
|
|
13
13
|
} from 'react-native-reanimated';
|
|
14
|
-
import
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
Pencil,
|
|
22
|
-
} from 'lucide-react-native';
|
|
14
|
+
import Copy from 'lucide-react-native/icons/copy';
|
|
15
|
+
import Check from 'lucide-react-native/icons/check';
|
|
16
|
+
import Volume2 from 'lucide-react-native/icons/volume-2';
|
|
17
|
+
import Square from 'lucide-react-native/icons/square';
|
|
18
|
+
import ThumbsUp from 'lucide-react-native/icons/thumbs-up';
|
|
19
|
+
import ThumbsDown from 'lucide-react-native/icons/thumbs-down';
|
|
20
|
+
import Pencil from 'lucide-react-native/icons/pencil';
|
|
23
21
|
import { Text } from './ui/text';
|
|
24
22
|
import { AliaMarkdown } from './Markdown';
|
|
25
23
|
import { ThinkingIndicator } from './ThinkingIndicator';
|
|
@@ -11,6 +11,10 @@
|
|
|
11
11
|
* headerLeft={<BackButton />}
|
|
12
12
|
* />
|
|
13
13
|
* ```
|
|
14
|
+
*
|
|
15
|
+
* Voice is opt-in — pass `voiceSession={VoiceSession}` from
|
|
16
|
+
* `@alia.onl/sdk/voice` to offer calls. Without it the screen is text-only and
|
|
17
|
+
* `livekit-client` never enters the bundle.
|
|
14
18
|
*/
|
|
15
19
|
|
|
16
20
|
import React from 'react';
|
|
@@ -19,6 +23,7 @@ import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
|
19
23
|
import { AliaChatContent } from './AliaChatContent';
|
|
20
24
|
import { AliaMark } from './AliaMark';
|
|
21
25
|
import type { WelcomeSuggestion } from './AliaWelcomeMessage';
|
|
26
|
+
import type { VoiceSessionComponent } from '../types';
|
|
22
27
|
|
|
23
28
|
export interface AliaChatScreenProps {
|
|
24
29
|
/** App context injected into system prompt */
|
|
@@ -41,6 +46,8 @@ export interface AliaChatScreenProps {
|
|
|
41
46
|
primaryColor?: string;
|
|
42
47
|
/** Dark-mode flag — forwarded to the ambient wave overlay. */
|
|
43
48
|
isDarkMode?: boolean;
|
|
49
|
+
/** Voice capability — `VoiceSession` from `@alia.onl/sdk/voice`. */
|
|
50
|
+
voiceSession?: VoiceSessionComponent;
|
|
44
51
|
}
|
|
45
52
|
|
|
46
53
|
export function AliaChatScreen({
|
|
@@ -54,6 +61,7 @@ export function AliaChatScreen({
|
|
|
54
61
|
welcomeSuggestions,
|
|
55
62
|
primaryColor,
|
|
56
63
|
isDarkMode,
|
|
64
|
+
voiceSession,
|
|
57
65
|
}: AliaChatScreenProps) {
|
|
58
66
|
const insets = useSafeAreaInsets();
|
|
59
67
|
|
|
@@ -68,6 +76,7 @@ export function AliaChatScreen({
|
|
|
68
76
|
welcomeSuggestions={welcomeSuggestions}
|
|
69
77
|
primaryColor={primaryColor}
|
|
70
78
|
isDarkMode={isDarkMode}
|
|
79
|
+
voiceSession={voiceSession}
|
|
71
80
|
header={({ markState, hasMessages, clear }) => (
|
|
72
81
|
<View className="flex-row items-center justify-between px-4 py-3">
|
|
73
82
|
<View className="flex-row items-center gap-2.5">
|
|
@@ -32,6 +32,7 @@ import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
|
32
32
|
import { AliaChatContent } from './AliaChatContent';
|
|
33
33
|
import { AliaMark } from './AliaMark';
|
|
34
34
|
import type { WelcomeSuggestion } from './AliaWelcomeMessage';
|
|
35
|
+
import type { VoiceSessionComponent } from '../types';
|
|
35
36
|
|
|
36
37
|
const { height: SCREEN_HEIGHT } = Dimensions.get('window');
|
|
37
38
|
|
|
@@ -58,6 +59,12 @@ export interface AliaChatSheetProps {
|
|
|
58
59
|
primaryColor?: string;
|
|
59
60
|
/** Dark-mode flag — forwarded to the ambient wave overlay (defaults to the sheet's own color scheme). */
|
|
60
61
|
isDarkMode?: boolean;
|
|
62
|
+
/**
|
|
63
|
+
* Voice capability — pass `VoiceSession` from `@alia.onl/sdk/voice` to offer
|
|
64
|
+
* voice calls. Omitted, the sheet is text-only and `livekit-client` never
|
|
65
|
+
* enters the bundle.
|
|
66
|
+
*/
|
|
67
|
+
voiceSession?: VoiceSessionComponent;
|
|
61
68
|
}
|
|
62
69
|
|
|
63
70
|
export interface AliaChatSheetRef {
|
|
@@ -66,7 +73,7 @@ export interface AliaChatSheetRef {
|
|
|
66
73
|
}
|
|
67
74
|
|
|
68
75
|
export const AliaChatSheet = forwardRef<AliaChatSheetRef, AliaChatSheetProps>(
|
|
69
|
-
({ clientContext, model, apiUrl, welcomeGreeting, welcomeSubtitle, welcomeSuggestions, primaryColor, isDarkMode }, ref) => {
|
|
76
|
+
({ clientContext, model, apiUrl, welcomeGreeting, welcomeSubtitle, welcomeSuggestions, primaryColor, isDarkMode, voiceSession }, ref) => {
|
|
70
77
|
const scheme = useColorScheme();
|
|
71
78
|
const isDark = scheme === 'dark';
|
|
72
79
|
const insets = useSafeAreaInsets();
|
|
@@ -250,6 +257,7 @@ export const AliaChatSheet = forwardRef<AliaChatSheetRef, AliaChatSheetProps>(
|
|
|
250
257
|
welcomeSuggestions={welcomeSuggestions}
|
|
251
258
|
primaryColor={primaryColor}
|
|
252
259
|
isDarkMode={isDarkMode ?? isDark}
|
|
260
|
+
voiceSession={voiceSession}
|
|
253
261
|
header={({ markState, hasMessages, clear }) => (
|
|
254
262
|
<View style={styles.header}>
|
|
255
263
|
<View style={styles.headerLeft}>
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { View, Pressable } from 'react-native';
|
|
2
2
|
import { Text } from './ui/text';
|
|
3
3
|
import { Button } from './ui/button';
|
|
4
|
-
import
|
|
4
|
+
import Check from 'lucide-react-native/icons/check';
|
|
5
|
+
import X from 'lucide-react-native/icons/x';
|
|
6
|
+
import ListChecks from 'lucide-react-native/icons/list-checks';
|
|
5
7
|
import Animated from 'react-native-reanimated';
|
|
6
8
|
import { planPreviewEnter } from '../animations';
|
|
7
9
|
import type { PlanStep } from '../types';
|
|
@@ -2,15 +2,13 @@ import { useState } from 'react';
|
|
|
2
2
|
import { View, Pressable } from 'react-native';
|
|
3
3
|
import { Text } from './ui/text';
|
|
4
4
|
import { cn } from '../lib/utils';
|
|
5
|
-
import
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
Loader2,
|
|
13
|
-
} from 'lucide-react-native';
|
|
5
|
+
import Search from 'lucide-react-native/icons/search';
|
|
6
|
+
import BookOpen from 'lucide-react-native/icons/book-open';
|
|
7
|
+
import CircleCheckBig from 'lucide-react-native/icons/circle-check-big';
|
|
8
|
+
import ChevronDown from 'lucide-react-native/icons/chevron-down';
|
|
9
|
+
import ChevronUp from 'lucide-react-native/icons/chevron-up';
|
|
10
|
+
import ExternalLink from 'lucide-react-native/icons/external-link';
|
|
11
|
+
import LoaderCircle from 'lucide-react-native/icons/loader-circle';
|
|
14
12
|
import type { ResearchProgress } from '../types';
|
|
15
13
|
|
|
16
14
|
interface ResearchProgressCardProps {
|
|
@@ -42,7 +40,7 @@ export function ResearchProgressCard({ progress }: ResearchProgressCardProps) {
|
|
|
42
40
|
<View className="flex-row items-center gap-2">
|
|
43
41
|
<View className="w-8 h-8 rounded-full bg-primary/10 items-center justify-center">
|
|
44
42
|
{isComplete ? (
|
|
45
|
-
<
|
|
43
|
+
<CircleCheckBig size={16} className="text-green-500" />
|
|
46
44
|
) : (
|
|
47
45
|
<Search size={16} className="text-primary" />
|
|
48
46
|
)}
|
|
@@ -111,7 +109,7 @@ export function ResearchProgressCard({ progress }: ResearchProgressCardProps) {
|
|
|
111
109
|
{/* Current search query indicator */}
|
|
112
110
|
{progress.currentQuery && !isComplete && (
|
|
113
111
|
<View className="flex-row items-center gap-2 bg-muted/50 rounded-lg px-3 py-1.5">
|
|
114
|
-
<
|
|
112
|
+
<LoaderCircle size={12} className="text-muted-foreground" />
|
|
115
113
|
<Text className="text-xs text-muted-foreground flex-1" numberOfLines={1}>
|
|
116
114
|
{progress.currentQuery}
|
|
117
115
|
</Text>
|
|
@@ -30,7 +30,7 @@ const workingPhrases = [
|
|
|
30
30
|
"Doing the thing...",
|
|
31
31
|
];
|
|
32
32
|
|
|
33
|
-
export function ThinkingIndicator({ isWorking = false, statusText }: { isWorking?: boolean; statusText?: string }) {
|
|
33
|
+
export function ThinkingIndicator({ isWorking = false, statusText, color }: { isWorking?: boolean; statusText?: string; color?: string }) {
|
|
34
34
|
const phrases = isWorking ? workingPhrases : thinkingPhrases;
|
|
35
35
|
const [phraseIndex, setPhraseIndex] = useState(() =>
|
|
36
36
|
Math.floor(Math.random() * phrases.length)
|
|
@@ -84,7 +84,7 @@ export function ThinkingIndicator({ isWorking = false, statusText }: { isWorking
|
|
|
84
84
|
|
|
85
85
|
return (
|
|
86
86
|
<View className="flex-row items-center gap-2 py-2">
|
|
87
|
-
<AliaMark size={16} state={isWorking ? 'working' : 'thinking'}
|
|
87
|
+
<AliaMark size={16} state={isWorking ? 'working' : 'thinking'} color={color} />
|
|
88
88
|
<View className="flex-row items-center">
|
|
89
89
|
<Text className="text-base text-muted-foreground">{shownText}</Text>
|
|
90
90
|
{(statusText || isTyping) && (
|
|
@@ -5,27 +5,54 @@ import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
|
|
|
5
5
|
import { CheckIcon, ChevronRightIcon } from "@radix-ui/react-icons";
|
|
6
6
|
import * as React from "react";
|
|
7
7
|
import type { LucideIcon } from "lucide-react-native";
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
8
|
+
// Per-icon subpaths, never the `lucide-react-native` barrel: the barrel
|
|
9
|
+
// statically re-exports all ~1500 icons, and Metro has no tree shaking, so a
|
|
10
|
+
// single named import pulls ~1.3 MB of unused icon modules into every consumer
|
|
11
|
+
// bundle.
|
|
12
|
+
import Star from "lucide-react-native/icons/star";
|
|
13
|
+
import Pencil from "lucide-react-native/icons/pencil";
|
|
14
|
+
import Trash2 from "lucide-react-native/icons/trash-2";
|
|
15
|
+
import Share2 from "lucide-react-native/icons/share-2";
|
|
16
|
+
import Download from "lucide-react-native/icons/download";
|
|
17
|
+
import Settings from "lucide-react-native/icons/settings";
|
|
18
|
+
import CircleQuestionMark from "lucide-react-native/icons/circle-question-mark";
|
|
19
|
+
import Image from "lucide-react-native/icons/image";
|
|
20
|
+
import FileText from "lucide-react-native/icons/file-text";
|
|
21
|
+
import Search from "lucide-react-native/icons/search";
|
|
22
|
+
import ShoppingBag from "lucide-react-native/icons/shopping-bag";
|
|
23
|
+
import Ellipsis from "lucide-react-native/icons/ellipsis";
|
|
24
|
+
import ExternalLink from "lucide-react-native/icons/external-link";
|
|
25
|
+
import BookOpen from "lucide-react-native/icons/book-open";
|
|
26
|
+
import Globe from "lucide-react-native/icons/globe";
|
|
27
|
+
import PenTool from "lucide-react-native/icons/pen-tool";
|
|
28
|
+
import Sparkles from "lucide-react-native/icons/sparkles";
|
|
29
|
+
import User from "lucide-react-native/icons/user";
|
|
30
|
+
import CreditCard from "lucide-react-native/icons/credit-card";
|
|
31
|
+
import Bell from "lucide-react-native/icons/bell";
|
|
32
|
+
import LogOut from "lucide-react-native/icons/log-out";
|
|
33
|
+
import Folder from "lucide-react-native/icons/folder";
|
|
34
|
+
import Check from "lucide-react-native/icons/check";
|
|
35
|
+
import Brain from "lucide-react-native/icons/brain";
|
|
36
|
+
import Ghost from "lucide-react-native/icons/ghost";
|
|
37
|
+
import Bot from "lucide-react-native/icons/bot";
|
|
38
|
+
import Bookmark from "lucide-react-native/icons/bookmark";
|
|
39
|
+
import TriangleAlert from "lucide-react-native/icons/triangle-alert";
|
|
40
|
+
import Pin from "lucide-react-native/icons/pin";
|
|
14
41
|
|
|
15
42
|
// Map iOS SF Symbol names to Lucide icons for web rendering
|
|
16
43
|
const SF_SYMBOL_MAP: Record<string, LucideIcon> = {
|
|
17
44
|
"star": Star, "star.fill": Star, "pencil": Pencil, "trash": Trash2,
|
|
18
45
|
"square.and.arrow.up": Share2, "arrow.down.doc": Download,
|
|
19
|
-
"gearshape": Settings, "questionmark.circle":
|
|
46
|
+
"gearshape": Settings, "questionmark.circle": CircleQuestionMark,
|
|
20
47
|
"photo": Image, "doc": FileText, "magnifyingglass": Search,
|
|
21
|
-
"bag": ShoppingBag, "ellipsis":
|
|
48
|
+
"bag": ShoppingBag, "ellipsis": Ellipsis, "link": ExternalLink,
|
|
22
49
|
"book": BookOpen, "globe": Globe, "pencil.tip": PenTool,
|
|
23
50
|
"sparkle": Sparkles, "person.circle": User, "creditcard": CreditCard,
|
|
24
51
|
"bell": Bell, "rectangle.portrait.and.arrow.right": LogOut,
|
|
25
52
|
"folder": Folder, "checkmark": Check, "brain": Brain,
|
|
26
53
|
"eye.slash": Ghost, "cpu": Bot,
|
|
27
54
|
"bookmark": Bookmark, "bookmark.fill": Bookmark,
|
|
28
|
-
"exclamationmark.triangle":
|
|
55
|
+
"exclamationmark.triangle": TriangleAlert,
|
|
29
56
|
"pin": Pin, "pin.fill": Pin,
|
|
30
57
|
};
|
|
31
58
|
|
|
@@ -1,15 +1,13 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import { View, Pressable, ScrollView, ActivityIndicator } from "react-native";
|
|
3
3
|
import { Image } from "expo-image";
|
|
4
|
-
import
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
X,
|
|
12
|
-
} from "lucide-react-native";
|
|
4
|
+
import FileText from "lucide-react-native/icons/file-text";
|
|
5
|
+
import FileSpreadsheet from "lucide-react-native/icons/file-spreadsheet";
|
|
6
|
+
import FileCode from "lucide-react-native/icons/file-code";
|
|
7
|
+
import FileArchive from "lucide-react-native/icons/file-archive";
|
|
8
|
+
import FileHeadphone from "lucide-react-native/icons/file-headphone";
|
|
9
|
+
import File from "lucide-react-native/icons/file";
|
|
10
|
+
import X from "lucide-react-native/icons/x";
|
|
13
11
|
import { Text } from "../text";
|
|
14
12
|
import { formatFileSize } from "../../../lib/utils";
|
|
15
13
|
import { usePromptInput, type Attachment } from "./context";
|
|
@@ -48,7 +46,7 @@ function getDocumentIcon(mimeType: string, name: string) {
|
|
|
48
46
|
mimeType.startsWith("audio/") ||
|
|
49
47
|
["mp3", "wav", "ogg", "flac", "aac"].includes(ext)
|
|
50
48
|
)
|
|
51
|
-
return { Icon:
|
|
49
|
+
return { Icon: FileHeadphone, color: "#EC4899", bgColor: "#EC489918" };
|
|
52
50
|
if (
|
|
53
51
|
mimeType === "text/plain" ||
|
|
54
52
|
["txt", "md", "rtf"].includes(ext)
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import React, { useEffect } from "react";
|
|
2
2
|
import { Pressable, ActivityIndicator } from "react-native";
|
|
3
|
-
import
|
|
3
|
+
import Mic from "lucide-react-native/icons/mic";
|
|
4
|
+
import MicOff from "lucide-react-native/icons/mic-off";
|
|
4
5
|
import { cn } from "../../../lib/utils";
|
|
5
6
|
import { useSpeechToText } from "../../../hooks/useSpeechToText";
|
|
6
7
|
import { usePromptInput } from "./context";
|
|
@@ -6,7 +6,8 @@ import {
|
|
|
6
6
|
type ViewStyle,
|
|
7
7
|
} from "react-native";
|
|
8
8
|
import { KeyboardAvoidingView } from "../../../lib/keyboard";
|
|
9
|
-
import
|
|
9
|
+
import Maximize2 from "lucide-react-native/icons/maximize-2";
|
|
10
|
+
import Minimize2 from "lucide-react-native/icons/minimize-2";
|
|
10
11
|
import { cn } from "../../../lib/utils";
|
|
11
12
|
import { PromptInputContext, type Attachment, type Completion } from "./context";
|
|
12
13
|
import { PromptInputTextarea } from "./textarea";
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
-
import
|
|
2
|
+
import ArrowUp from "lucide-react-native/icons/arrow-up";
|
|
3
|
+
import Square from "lucide-react-native/icons/square";
|
|
3
4
|
import { cn } from "../../../lib/utils";
|
|
4
5
|
import { Button } from "../button";
|
|
5
6
|
import { usePromptInput } from "./context";
|
|
@@ -4,7 +4,10 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { View, Text, Pressable, ActivityIndicator, StyleSheet } from 'react-native';
|
|
7
|
-
import
|
|
7
|
+
import Mic from 'lucide-react-native/icons/mic';
|
|
8
|
+
import MicOff from 'lucide-react-native/icons/mic-off';
|
|
9
|
+
import PhoneOff from 'lucide-react-native/icons/phone-off';
|
|
10
|
+
import Users from 'lucide-react-native/icons/users';
|
|
8
11
|
import type { RoomState, AgentState } from '../../types';
|
|
9
12
|
|
|
10
13
|
interface VoiceControlsProps {
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A live LiveKit call, mounted only while voice mode is on.
|
|
3
|
+
*
|
|
4
|
+
* This is the only place the chat UI touches `livekit-client`. The chat shell
|
|
5
|
+
* receives it as the `voiceSession` prop rather than importing it, so a
|
|
6
|
+
* text-only consumer never pulls the LiveKit client into its module graph —
|
|
7
|
+
* which is the whole reason the `@alia.onl/sdk/voice` entry exists.
|
|
8
|
+
*
|
|
9
|
+
* Mounting starts the call; unmounting ends it (`useVoiceRoom` tears the room
|
|
10
|
+
* down on unmount), so the shell only has to decide whether to render it.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { useEffect, useRef } from 'react';
|
|
14
|
+
import { useVoiceRoom } from '../../hooks/useVoiceRoom';
|
|
15
|
+
import { useAudioLevelMonitor } from '../../hooks/useAudioLevelMonitor';
|
|
16
|
+
import { useAudioLevels } from '../../hooks/useAudioLevels';
|
|
17
|
+
import { VoiceControls } from './VoiceControls';
|
|
18
|
+
import type { VoiceSessionProps } from '../../types';
|
|
19
|
+
|
|
20
|
+
export function VoiceSession({ apiUrl, onStateChange, onEnd }: VoiceSessionProps) {
|
|
21
|
+
const {
|
|
22
|
+
room,
|
|
23
|
+
roomState,
|
|
24
|
+
agentState,
|
|
25
|
+
isMuted,
|
|
26
|
+
error,
|
|
27
|
+
messages,
|
|
28
|
+
cohostActive,
|
|
29
|
+
currentSpeaker,
|
|
30
|
+
roundComplete,
|
|
31
|
+
isConnected,
|
|
32
|
+
connect,
|
|
33
|
+
toggleMute,
|
|
34
|
+
enableCohost,
|
|
35
|
+
disableCohost,
|
|
36
|
+
continueCohost,
|
|
37
|
+
} = useVoiceRoom({ apiUrl });
|
|
38
|
+
|
|
39
|
+
const { captureLevel, playbackLevel } = useAudioLevelMonitor(room, isConnected);
|
|
40
|
+
const { waveAmplitude } = useAudioLevels({
|
|
41
|
+
captureLevel,
|
|
42
|
+
playbackLevel,
|
|
43
|
+
agentState,
|
|
44
|
+
isConnected,
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
// Dial out once per mount. The ref keeps a new `connect` identity from
|
|
48
|
+
// placing a second call on top of the live one.
|
|
49
|
+
const dialedRef = useRef(false);
|
|
50
|
+
useEffect(() => {
|
|
51
|
+
if (dialedRef.current) return;
|
|
52
|
+
dialedRef.current = true;
|
|
53
|
+
connect();
|
|
54
|
+
}, [connect]);
|
|
55
|
+
|
|
56
|
+
useEffect(() => {
|
|
57
|
+
onStateChange({ isConnected, agentState, waveAmplitude, messages });
|
|
58
|
+
}, [isConnected, agentState, waveAmplitude, messages, onStateChange]);
|
|
59
|
+
|
|
60
|
+
// End the call on failure, or once a room we actually reached goes away. The
|
|
61
|
+
// "reached" flag is what keeps the pre-connect `disconnected` state — which
|
|
62
|
+
// every session starts in — from ending the call before it begins.
|
|
63
|
+
const reachedRoomRef = useRef(false);
|
|
64
|
+
useEffect(() => {
|
|
65
|
+
if (roomState === 'connected') reachedRoomRef.current = true;
|
|
66
|
+
if (error || roomState === 'error') {
|
|
67
|
+
onEnd();
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
if (roomState === 'disconnected' && reachedRoomRef.current) onEnd();
|
|
71
|
+
}, [error, roomState, onEnd]);
|
|
72
|
+
|
|
73
|
+
return (
|
|
74
|
+
<VoiceControls
|
|
75
|
+
roomState={roomState}
|
|
76
|
+
agentState={agentState}
|
|
77
|
+
isMuted={isMuted}
|
|
78
|
+
cohostActive={cohostActive}
|
|
79
|
+
currentSpeaker={currentSpeaker}
|
|
80
|
+
roundComplete={roundComplete}
|
|
81
|
+
onToggleMute={toggleMute}
|
|
82
|
+
onEnableCohost={enableCohost}
|
|
83
|
+
onDisableCohost={disableCohost}
|
|
84
|
+
onContinueCohost={continueCohost}
|
|
85
|
+
onEnd={onEnd}
|
|
86
|
+
/>
|
|
87
|
+
);
|
|
88
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -40,6 +40,13 @@ export type {
|
|
|
40
40
|
PlanStep,
|
|
41
41
|
} from './types';
|
|
42
42
|
|
|
43
|
+
// ── Voice capability contract (the implementation lives in `./voice`) ──
|
|
44
|
+
export type {
|
|
45
|
+
VoiceSessionComponent,
|
|
46
|
+
VoiceSessionProps,
|
|
47
|
+
VoiceSessionState,
|
|
48
|
+
} from './types';
|
|
49
|
+
|
|
43
50
|
// ── UI Primitives (NativeWind) ──
|
|
44
51
|
export { Button, buttonVariants, buttonTextVariants } from './components/ui/button';
|
|
45
52
|
export type { ButtonProps } from './components/ui/button';
|
package/src/types.ts
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import type { ComponentType } from 'react';
|
|
2
|
+
import type { SharedValue } from 'react-native-reanimated';
|
|
3
|
+
|
|
1
4
|
// ── Welcome / Suggestion Types ──
|
|
2
5
|
|
|
3
6
|
export interface WelcomeSuggestion {
|
|
@@ -93,3 +96,32 @@ export interface VoiceMessage {
|
|
|
93
96
|
isStreaming: boolean;
|
|
94
97
|
toolInvocations?: VoiceToolInvocation[];
|
|
95
98
|
}
|
|
99
|
+
|
|
100
|
+
// ── Voice Session (injected capability) ──
|
|
101
|
+
|
|
102
|
+
/** Everything a live call reports back to the chat shell. */
|
|
103
|
+
export interface VoiceSessionState {
|
|
104
|
+
/** The room is up and the agent is live. */
|
|
105
|
+
isConnected: boolean;
|
|
106
|
+
agentState: AgentState;
|
|
107
|
+
/** Audio amplitude channel that drives the ambient wave overlay. */
|
|
108
|
+
waveAmplitude: SharedValue<number>;
|
|
109
|
+
/** Transcript so far; the shell adapts it into the chat message list. */
|
|
110
|
+
messages: VoiceMessage[];
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export interface VoiceSessionProps {
|
|
114
|
+
/** Alia API base URL. */
|
|
115
|
+
apiUrl?: string;
|
|
116
|
+
/** Called on every room, agent, or transcript change. */
|
|
117
|
+
onStateChange: (state: VoiceSessionState) => void;
|
|
118
|
+
/** The call is over — the user ended it, it errored, or the room dropped. */
|
|
119
|
+
onEnd: () => void;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* The voice capability handed to `AliaChatScreen` / `AliaChatSheet`. Pass
|
|
124
|
+
* `VoiceSession` from `@alia.onl/sdk/voice` to enable voice calls; leave it out
|
|
125
|
+
* and the chat is text-only, with `livekit-client` absent from the module graph.
|
|
126
|
+
*/
|
|
127
|
+
export type VoiceSessionComponent = ComponentType<VoiceSessionProps>;
|
package/src/voice.ts
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
// into their module graph.
|
|
5
5
|
|
|
6
6
|
// ── Voice components ──
|
|
7
|
+
export { VoiceSession } from './components/voice/VoiceSession';
|
|
7
8
|
export { AudioWaveVisualizer } from './components/voice/AudioWaveVisualizer';
|
|
8
9
|
export { VoiceOverlay } from './components/voice/VoiceOverlay';
|
|
9
10
|
export { VoiceControls } from './components/voice/VoiceControls';
|
|
@@ -23,4 +24,6 @@ export type {
|
|
|
23
24
|
AgentState,
|
|
24
25
|
VoiceMessage,
|
|
25
26
|
VoiceToolInvocation,
|
|
27
|
+
VoiceSessionProps,
|
|
28
|
+
VoiceSessionState,
|
|
26
29
|
} from './types';
|