@alia.onl/sdk 7.2.7 → 8.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 CHANGED
@@ -1,5 +1,96 @@
1
1
  # @alia.onl/sdk
2
2
 
3
+ ## Unreleased
4
+
5
+ ### The consumer-backend path is documented
6
+
7
+ Documentation only; no code, wire or default changed, and no version is cut for
8
+ it.
9
+
10
+ The package now has a README. It says which endpoint `useAliaChat` calls and
11
+ why it is `/v1/chat/completions` rather than `/alia/chat`: this package ships
12
+ raw source and compiles into apps on origins Alia does not enumerate, and
13
+ `/alia/chat` answers CORS preflights only for Alia's own origins. It then
14
+ documents the path that works from any origin today — `useAliaChat({ apiUrl })`
15
+ pointed at the consumer's own backend, which calls `POST /alia/chat`
16
+ server-to-server, forwards the user's Oxy token, and streams the SSE body back
17
+ unchanged — with a minimal relay. The `apiUrl` option's JSDoc says the same in
18
+ short. Issue #244 records the two other shapes and why neither is available
19
+ yet.
20
+
21
+ ## 8.0.0
22
+
23
+ ### Voice runs on the device
24
+
25
+ Dictation and voice calls work again. Both used Alia endpoints that called
26
+ speech providers directly — `POST /v1/voice/transcribe` and a LiveKit room from
27
+ `POST /v1/voice/token` — and both have been refusing since Alia's inference
28
+ moved behind Oxy, which serves chat and speech synthesis but no transcription or
29
+ realtime session. Those endpoints are removed from the API.
30
+
31
+ - **`useSpeechToText`** recognizes on the device (Web Speech API on web,
32
+ `expo-speech-recognition` on iOS/Android). Its shape is unchanged —
33
+ `startRecording`, `stopAndTranscribe`, `cancel`, the three states,
34
+ `useSTTStore` metering — and it gains `lang` and `isSupported`.
35
+ - **`useVoiceRoom`** is a turn loop: on-device listening with end-of-utterance
36
+ detection, each utterance sent through the chat path (`sendTurn`, by default
37
+ `createAliaVoiceTurnSender`, which posts `/v1/chat/completions` with
38
+ `responseMode: 'voice'`), the answer spoken sentence by sentence through
39
+ `/v1/audio/speech`, and barge-in. `connect`, `disconnect`, `roomState`,
40
+ `agentState`, `messages`, `isMuted`, `toggleMute` and `error` keep their
41
+ meaning; it adds `turnError` and the options `sendTurn`, `chatModel`, `lang`,
42
+ `endOfUtteranceMs` and `bargeIn`.
43
+ - The call asks `/v1/audio/speech` for the product voices `male` / `female`;
44
+ it no longer sends upstream voice names.
45
+
46
+ **Breaking:**
47
+
48
+ - `livekit-client` is no longer a peer dependency; `expo-speech-recognition`
49
+ is a new one (the root entry's dictation reaches it on native), and a native
50
+ app needs a new build with its config plugin.
51
+ - `useVoiceRoom().room` is a `VoiceLevelSource` (live capture and playback
52
+ levels), not a LiveKit `Room`; `useAudioLevelMonitor` takes that.
53
+ - `livekit-client` is gone from the package entirely; nothing in either entry
54
+ reaches it (`check:entries` asserts that).
55
+ - The cohost is removed. `useVoiceRoom` no longer returns `cohostActive`,
56
+ `roundComplete`, `enableCohost`, `disableCohost` or `continueCohost`, and
57
+ `currentSpeaker` is `'primary' | 'user' | null`. `VoiceControls` no longer
58
+ takes `cohostActive`, `currentSpeaker`, `roundComplete`, `onEnableCohost`,
59
+ `onDisableCohost` or `onContinueCohost`, and draws no cohost button or
60
+ "Continue conversation" prompt. `ChatMessage.speaker` and
61
+ `VoiceMessage.speaker` are `'primary'` only, and the message list draws no
62
+ "Cohost" label.
63
+ - `useSpeechToText` no longer takes `apiUrl` or `accessToken`; dictation never
64
+ talks to the Alia API. Pass `lang` or nothing.
65
+ - The pre-Bloom composer is no longer public. `PromptInput`,
66
+ `PromptInputTextarea`, `PromptInputActions`, `PromptInputSubmitButton`,
67
+ `PromptInputMicButton`, `PromptInputAddMenu`, `PromptInputAttachments`,
68
+ `PromptInputAutocomplete`, `PromptInputContext`, `usePromptInput`,
69
+ `useIsFullscreen`, `ChatTextInput` and the types `PromptInputProps`,
70
+ `PromptInputContextType`, `Attachment` and `Completion` are removed from the
71
+ root entry. `AliaChatScreen` / `AliaChatSheet` still draw their composer;
72
+ build your own from the primitives if you composed one from these parts.
73
+ - Dependencies trimmed: `@tanstack/react-query`,
74
+ `@tanstack/react-query-persist-client` and
75
+ `@tanstack/query-async-storage-persister` are no longer dependencies (no
76
+ module imported them), and `socket.io-client` and `expo-font` are no longer
77
+ peer dependencies (likewise unused). Keep them in your app if your app uses
78
+ them itself.
79
+
80
+ **Server side, alongside this release:** the `alia_sk_*` developer keys are
81
+ retired and refused by the Alia API. The SDK never used them — it sends the
82
+ signed-in user's Oxy session — but a backend that relays the SDK's requests
83
+ with such a key must forward the user's Oxy token instead (see the README).
84
+
85
+ ## 7.2.8
86
+
87
+ ### One Oxy runtime and a native-only notifications boundary
88
+
89
+ The Alia app now consumes the canonical `@oxy.so/*` packages throughout, so
90
+ the provider and SDK hooks resolve the same Services context. Its notification
91
+ setup also loads `expo-notifications` only after confirming a native platform;
92
+ web no longer evaluates Expo's unsupported device push-token listener.
93
+
3
94
  ## 7.2.7
4
95
 
5
96
  ### Compatible Oxy 1.0 peers
package/README.md ADDED
@@ -0,0 +1,234 @@
1
+ # @alia.onl/sdk
2
+
3
+ Alia's chat SDK for React Native and Expo apps built on Oxy: the chat sheet and
4
+ screen, the streaming hook, speech in and out, and the primitives they are made
5
+ of. It renders inside the one `OxyProvider` your app already mounts and sends
6
+ the signed-in user's Oxy session with every request; it never holds a token of
7
+ its own.
8
+
9
+ The package ships **raw TypeScript source** — `main`, `types` and every
10
+ `exports` entry point at `src/`. Your bundler compiles it with the rest of your
11
+ app, which is why the section on endpoints below matters more than it would for
12
+ a prebuilt library.
13
+
14
+ ## Install
15
+
16
+ ```sh
17
+ bun add @alia.onl/sdk
18
+ ```
19
+
20
+ `react`, `react-native`, `@oxy.so/core`, `@oxy.so/services` and the Expo modules
21
+ listed under `peerDependencies` in `package.json` are yours to install. Metro
22
+ must resolve package `exports` (`resolver.unstable_enablePackageExports = true`,
23
+ the default in Expo SDK 53+), because every icon import is a subpath.
24
+
25
+ Two entry points:
26
+
27
+ - `@alia.onl/sdk` — text chat, dictation, read-aloud, catalogue, primitives.
28
+ - `@alia.onl/sdk/voice` — `VoiceSession` and `useVoiceRoom`, the voice call.
29
+ Pass `VoiceSession` to the shell as `voiceSession` to offer calls; leave it
30
+ out for text-only chat and the call is never compiled.
31
+
32
+ ## Speech
33
+
34
+ Dictation (`useSpeechToText`) and the voice call (`useVoiceRoom`) recognize
35
+ speech **on the device**: the Web Speech API in browsers that have it (Chromium
36
+ and Safari — not Firefox, where `useSpeechToText().isSupported` is `false`), and
37
+ [`expo-speech-recognition`](https://github.com/jamsch/expo-speech-recognition)
38
+ on iOS and Android. No audio is sent to Alia; whether the platform's recognizer
39
+ uses a network service of its own is the platform's (Chrome's does).
40
+
41
+ On native, add `expo-speech-recognition` (a peer dependency, like `expo-audio`:
42
+ the root entry's mic button reaches it) and its config plugin to your app, then
43
+ make a new native build:
44
+
45
+ ```json
46
+ ["expo-speech-recognition", {
47
+ "microphonePermission": "Allow $(PRODUCT_NAME) to hear what you dictate and say in voice calls.",
48
+ "speechRecognitionPermission": "Allow $(PRODUCT_NAME) to turn your speech into text."
49
+ }]
50
+ ```
51
+
52
+ A voice call is a turn loop, not a realtime session: it listens until the
53
+ person pauses, sends what they said through the ordinary chat path
54
+ (`POST /v1/chat/completions` with `responseMode: "voice"`, or your own
55
+ `sendTurn`), and speaks the answer sentence by sentence through
56
+ `POST /v1/audio/speech` in the product voice (`male` / `female`). Talking over
57
+ the answer interrupts it. Pass `lang` to both hooks to follow your app's
58
+ language.
59
+
60
+ ## Use
61
+
62
+ ```tsx
63
+ import { AliaChatScreen } from '@alia.onl/sdk';
64
+
65
+ export function Assistant() {
66
+ return <AliaChatScreen />;
67
+ }
68
+ ```
69
+
70
+ Or the hook on its own, with your own UI:
71
+
72
+ ```tsx
73
+ import { useAliaChat } from '@alia.onl/sdk';
74
+
75
+ const { messages, send, isStreaming, stop, error } = useAliaChat({
76
+ clientContext: 'The user is looking at their order history.',
77
+ });
78
+ ```
79
+
80
+ `useAliaChat` streams a turn as Server-Sent Events and switches on Alia's
81
+ product events — `alia.reasoning`, `alia.tool_result`, `alia.research_progress`,
82
+ `alia.plan_preview`, delegated-agent answers — into `ChatMessage` fields your
83
+ components render. Those events are the product runtime's, not part of the Oxy
84
+ generic inference contract, and this package is written against them on
85
+ purpose: it is a **product** client.
86
+
87
+ ## Which endpoint the SDK calls, and why
88
+
89
+ The hook sends its turns to `POST {apiUrl}/v1/chat/completions`, with `apiUrl`
90
+ defaulting to `EXPO_PUBLIC_ALIA_API_URL` or `https://api.alia.onl`. It also
91
+ reads `GET {apiUrl}/catalogue` once per base URL, to check the requested model
92
+ identifier against what the server offers.
93
+
94
+ That is the compatibility surface, not the product route. `POST /alia/chat` is
95
+ the same handler with the same authentication, and it is where Alia's own
96
+ clients go — but the two mounts answer browsers differently:
97
+
98
+ | Preflight | `access-control-allow-origin` |
99
+ | --- | --- |
100
+ | `OPTIONS /alia/chat` from `https://alia.onl` or `https://console.alia.onl` | that origin, with credentials |
101
+ | `OPTIONS /alia/chat` from an origin Alia does not enumerate | **absent** |
102
+ | `OPTIONS /v1/chat/completions` from anywhere | `*` |
103
+
104
+ `/alia/chat` takes Alia's exact-origin allowlist (`packages/api/src/lib/cors-origins.ts`);
105
+ `/v1` is public CORS. Because this package compiles into *your* app on *your*
106
+ origin, defaulting the hook to `/alia/chat` would fail every consumer's
107
+ preflight on web. So it stays on `/v1/chat/completions`, which is Alia's
108
+ **permanent product API** — it does not sunset
109
+ ([ADR 0010](https://github.com/OxyHQ/Alia/blob/main/docs/adr/0010-alia-keeps-a-product-api-credentials-come-from-oxy-console.md)).
110
+ Generic model access is a different product, Kaana through Oxy; this package
111
+ is written against Alia's, and the credential a consumer presents is an Oxy
112
+ one — the signed-in user's session today. A change of default to `/alia/chat`
113
+ would be a semver-major of this package, would reach only consumers who upgrade
114
+ **and** rebuild, and is not needed to escape a sunset; it is taken only if the
115
+ per-application origin policy below makes it worthwhile
116
+ ([#244](https://github.com/OxyHQ/Alia/issues/244), closed on that decision).
117
+
118
+ Native apps are not subject to CORS at all — React Native sends no `Origin`
119
+ header — so the table above is about web builds and the backend path below.
120
+
121
+ ## Using the SDK from your own product: the consumer-backend path
122
+
123
+ The supported way for an application outside Alia's origins to use this SDK
124
+ today is to put your own backend between the SDK and Alia. Your backend
125
+ receives the SDK's request, calls Alia's product route server-to-server — a
126
+ server sends no `Origin` header, so `/alia/chat` answers it on its credential
127
+ alone — and streams the SSE body back unchanged. Then point the hook at that
128
+ backend:
129
+
130
+ ```tsx
131
+ useAliaChat({ apiUrl: 'https://your-backend.example' });
132
+ ```
133
+
134
+ The hook keeps its paths, so your backend answers
135
+ `POST /v1/chat/completions` and `GET /catalogue` under whatever base URL you
136
+ give it. Nothing about the stream changes: the SDK's parser reads the bytes Alia
137
+ wrote.
138
+
139
+ **Which credential.** The SDK attaches the signed-in user's Oxy access token as
140
+ `Authorization: Bearer …` on the request your backend receives. Forward that
141
+ header as it is. `POST /alia/chat` authenticates with
142
+ `authenticateTokenOrApiKey` (`packages/api/src/routes/chat.ts`,
143
+ `packages/api/src/middleware/auth.ts`), which accepts an Oxy session token —
144
+ verified with Oxy, the same way it would be if the browser had called Alia
145
+ directly — and the turn is metered to that user's Alia entitlement, the same as
146
+ a direct call. The other credentials the middleware recognises are
147
+ Alia-internal. There is no consumer-application credential for this route yet — that is Oxy Applications' to issue (OxyHQ/oxy#972).
148
+
149
+ A minimal relay, Node 18+ and Express:
150
+
151
+ ```ts
152
+ import express from 'express';
153
+
154
+ const ALIA = 'https://api.alia.onl';
155
+ const app = express();
156
+ app.use(express.json({ limit: '1mb' }));
157
+
158
+ /** Relay one request to Alia and stream the answer back unchanged. */
159
+ async function relay(
160
+ req: express.Request,
161
+ res: express.Response,
162
+ method: 'GET' | 'POST',
163
+ path: string,
164
+ ): Promise<void> {
165
+ const controller = new AbortController();
166
+ req.on('close', () => controller.abort());
167
+
168
+ const authorization = req.get('authorization');
169
+ const upstream = await fetch(`${ALIA}${path}`, {
170
+ method,
171
+ headers: {
172
+ ...(authorization ? { Authorization: authorization } : {}),
173
+ Accept: req.get('accept') ?? 'application/json',
174
+ ...(method === 'POST' ? { 'Content-Type': 'application/json' } : {}),
175
+ },
176
+ body: method === 'POST' ? JSON.stringify(req.body) : undefined,
177
+ signal: controller.signal,
178
+ });
179
+
180
+ res.status(upstream.status);
181
+ res.setHeader('Content-Type', upstream.headers.get('content-type') ?? 'application/json');
182
+ res.setHeader('Cache-Control', 'no-cache');
183
+ res.setHeader('X-Accel-Buffering', 'no'); // keep proxies from buffering the SSE stream
184
+ res.flushHeaders();
185
+
186
+ if (upstream.body === null) {
187
+ res.end();
188
+ return;
189
+ }
190
+ try {
191
+ for await (const chunk of upstream.body) res.write(chunk);
192
+ } catch (error) {
193
+ if (!controller.signal.aborted) throw error;
194
+ } finally {
195
+ res.end();
196
+ }
197
+ }
198
+
199
+ // The SDK's turn: answered by Alia's PRODUCT route, server-to-server.
200
+ app.post('/v1/chat/completions', (req, res) => relay(req, res, 'POST', '/alia/chat'));
201
+
202
+ // The SDK's model check. Without this the hook still works — an unreadable
203
+ // catalogue leaves the requested identifier alone — but it never learns what
204
+ // the server offers.
205
+ app.get('/catalogue', (req, res) => relay(req, res, 'GET', '/catalogue'));
206
+
207
+ app.listen(3000);
208
+ ```
209
+
210
+ The request body the SDK sends is `{ model, messages, stream: true }` and the
211
+ response Alia writes is `text/event-stream`; the relay forwards both without
212
+ reading them. CORS between your web app and your backend is yours to configure,
213
+ as it would be for any of your own routes.
214
+
215
+ **The trade-off.** This is one more hop, and one more process in the path of a
216
+ stream. Some consumers will not want it, which is why it is documented as the
217
+ path that works today rather than chosen as the only one. The other two shapes
218
+ — a CORS policy on `/alia/chat` for registered consumer origins, and the SDK
219
+ default moving to `/alia/chat` in a major — are recorded in #244: the first is
220
+ blocked on Oxy Applications carrying an origin registry (OxyHQ/oxy#972), and
221
+ the second is an adoption window rather than a switch, for the raw-source
222
+ reason above.
223
+
224
+ ## Scripts
225
+
226
+ ```sh
227
+ bun run typecheck # tsc --noEmit
228
+ bun run test # vitest
229
+ bun run check:entries # the root entry must not reach the voice call
230
+ ```
231
+
232
+ ## Licence
233
+
234
+ Apache-2.0. See `LICENSE` and `NOTICE`.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@alia.onl/sdk",
3
- "version": "7.2.7",
4
- "description": "Alia AI chat SDK — reusable chat UI, voice, and streaming components for Oxy apps",
3
+ "version": "8.0.0",
4
+ "description": "Alia AI chat SDK \u2014 reusable chat UI, voice, and streaming components for Oxy apps",
5
5
  "main": "src/index.ts",
6
6
  "react-native": "src/index.ts",
7
7
  "source": "src/index.ts",
@@ -31,9 +31,6 @@
31
31
  "dependencies": {
32
32
  "@radix-ui/react-dropdown-menu": "^2.1.20",
33
33
  "@radix-ui/react-icons": "^1.3.2",
34
- "@tanstack/query-async-storage-persister": "^5.101.2",
35
- "@tanstack/react-query": "^5.102.8",
36
- "@tanstack/react-query-persist-client": "^5.102.8",
37
34
  "class-variance-authority": "^0.7.1",
38
35
  "clsx": "^2.1.1",
39
36
  "lucide-react-native": "^1.24.0",
@@ -45,25 +42,23 @@
45
42
  "peerDependencies": {
46
43
  "react": "^19.2.3",
47
44
  "react-native": "^0.86.0",
48
- "@oxy.so/core": "^1.0.1",
49
- "@oxy.so/services": "^1.0.1",
45
+ "@oxy.so/core": "^1.7.0",
46
+ "@oxy.so/services": "^3.0.0 || ^4.0.0",
50
47
  "@react-native-async-storage/async-storage": "^2.2.0",
51
- "socket.io-client": "^4.8.3",
52
- "expo-font": "^57.0.0",
53
48
  "expo-blur": "^57.0.0",
54
49
  "react-native-reanimated": "^4.5.0",
55
50
  "react-native-gesture-handler": "^2.32.0",
56
51
  "react-native-safe-area-context": "^5.8.0",
57
52
  "react-native-keyboard-controller": "^1.21.9",
58
- "react-native-svg": "^15.15.4",
59
- "livekit-client": "^2.20.1",
53
+ "react-native-svg": "^15.15.5",
60
54
  "expo-audio": "^57.0.0",
61
55
  "expo-image": "^57.0.0",
62
56
  "expo-image-picker": "^57.0.2",
63
57
  "expo-document-picker": "^57.0.0",
64
58
  "expo-haptics": "^57.0.0",
65
59
  "nativewind": "^5.0.0-preview.3",
66
- "expo-clipboard": "^57.0.0"
60
+ "expo-clipboard": "^57.0.0",
61
+ "expo-speech-recognition": "^57.1.0"
67
62
  },
68
63
  "peerDependenciesMeta": {
69
64
  "nativewind": {
@@ -77,6 +72,7 @@
77
72
  "@types/react-test-renderer": "^19.1.0",
78
73
  "react-test-renderer": "19.2.3",
79
74
  "typescript": "^5.9.3",
80
- "vitest": "^4.1.10"
75
+ "vitest": "^4.1.10",
76
+ "expo-speech-recognition": "~57.1.0"
81
77
  }
82
78
  }
@@ -286,7 +286,6 @@ export function AliaChatContent({
286
286
  onStop={stop}
287
287
  emptyAction={voiceActivateButton}
288
288
  placeholder="Ask Alia..."
289
- apiUrl={apiUrl}
290
289
  autocomplete={!!useSuggestions}
291
290
  useSuggestions={useSuggestions}
292
291
  onSuggestionUsed={onSuggestionUsed}
@@ -337,10 +337,6 @@ const AssistantMessage = React.memo(function AssistantMessage({
337
337
  {/* Message Content */}
338
338
  {(hasContent || message.isStreaming) && (
339
339
  <View className="flex-col items-start gap-0.5">
340
- {/* Voice cohost label */}
341
- {message.source === 'voice' && message.speaker === 'cohost' && (
342
- <Text className="text-xs text-indigo-400 mb-0.5">Cohost</Text>
343
- )}
344
340
  {/* Agent identity */}
345
341
  {message.agentInfo && (
346
342
  <View className="flex-row items-center gap-2 mb-0.5">
@@ -23,6 +23,9 @@ const PHASE_LABELS: Record<string, string> = {
23
23
  follow_up: 'Follow-up research',
24
24
  finalizing: 'Polishing report',
25
25
  complete: 'Research complete',
26
+ // The engine finished searching but could not write the final answer; the
27
+ // message carries what was found, and this label keeps the card honest.
28
+ failed: 'Research incomplete',
26
29
  };
27
30
 
28
31
  const PHASE_ORDER = ['decomposing', 'searching', 'synthesizing', 'follow_up', 'finalizing', 'complete'];
@@ -8,12 +8,11 @@ import { usePromptInput } from "./context";
8
8
 
9
9
  export type PromptInputMicButtonProps = {
10
10
  className?: string;
11
- apiUrl?: string;
12
11
  };
13
12
 
14
- export function PromptInputMicButton({ className, apiUrl }: PromptInputMicButtonProps) {
13
+ export function PromptInputMicButton({ className }: PromptInputMicButtonProps) {
15
14
  const { value, setValue, onError } = usePromptInput();
16
- const stt = useSpeechToText({ apiUrl });
15
+ const stt = useSpeechToText();
17
16
 
18
17
  useEffect(() => {
19
18
  if (stt.error) {
@@ -49,8 +49,6 @@ export type PromptInputProps = {
49
49
  onSuggestionUsed?: (suggestionId: string) => void;
50
50
  // Injectable error handler (replaces toast)
51
51
  onError?: (message: string) => void;
52
- // Mic button API URL
53
- apiUrl?: string;
54
52
  } & Omit<React.ComponentProps<typeof View>, "children">;
55
53
 
56
54
  export function PromptInput({
@@ -77,7 +75,6 @@ export function PromptInput({
77
75
  useSuggestions,
78
76
  onSuggestionUsed,
79
77
  onError,
80
- apiUrl,
81
78
  ...props
82
79
  }: PromptInputProps) {
83
80
  const [internalValue, setInternalValue] = useState(value || "");
@@ -182,7 +179,7 @@ export function PromptInput({
182
179
  {actionsLeft ?? <PromptInputAddMenu />}
183
180
  </View>
184
181
  <View className="flex-row items-center gap-1.5">
185
- <PromptInputMicButton apiUrl={apiUrl} />
182
+ <PromptInputMicButton />
186
183
  <PromptInputSubmitButton
187
184
  isLoading={isLoading}
188
185
  onStop={onStop}
@@ -1,26 +1,19 @@
1
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.
2
+ * Bottom voice control bar that replaces the composer when voice mode is active.
3
+ * Contains status text and mute/end buttons.
4
4
  */
5
5
 
6
6
  import { View, Text, Pressable, ActivityIndicator, StyleSheet } from 'react-native';
7
7
  import Mic from 'lucide-react-native/icons/mic';
8
8
  import MicOff from 'lucide-react-native/icons/mic-off';
9
9
  import PhoneOff from 'lucide-react-native/icons/phone-off';
10
- import Users from 'lucide-react-native/icons/users';
11
10
  import type { RoomState, AgentState } from '../../types';
12
11
 
13
12
  interface VoiceControlsProps {
14
13
  roomState: RoomState;
15
14
  agentState: AgentState;
16
15
  isMuted: boolean;
17
- cohostActive: boolean;
18
- currentSpeaker: 'primary' | 'cohost' | 'user' | null;
19
- roundComplete: boolean;
20
16
  onToggleMute: () => void;
21
- onEnableCohost: () => void;
22
- onDisableCohost: () => void;
23
- onContinueCohost: () => void;
24
17
  onEnd: () => void;
25
18
  /** Override theme primary color (resolved hex/hsl value, not CSS var) */
26
19
  primaryColor?: string;
@@ -30,13 +23,9 @@ function getStatusText(
30
23
  roomState: RoomState,
31
24
  agentState: AgentState,
32
25
  isMuted: boolean,
33
- cohostActive: boolean,
34
- currentSpeaker: string | null,
35
26
  ): string {
36
27
  if (roomState === 'connecting') return 'Connecting...';
37
28
  if (roomState === 'connected') {
38
- if (cohostActive && currentSpeaker === 'cohost') return 'Cohost speaking...';
39
- if (cohostActive && currentSpeaker === 'primary') return 'Alia speaking...';
40
29
  if (agentState === 'listening') return isMuted ? 'Muted' : 'Listening...';
41
30
  if (agentState === 'thinking') return 'Thinking...';
42
31
  if (agentState === 'speaking') return 'Speaking...';
@@ -49,17 +38,11 @@ export function VoiceControls({
49
38
  roomState,
50
39
  agentState,
51
40
  isMuted,
52
- cohostActive,
53
- currentSpeaker,
54
- roundComplete,
55
41
  onToggleMute,
56
- onEnableCohost,
57
- onDisableCohost,
58
- onContinueCohost,
59
42
  onEnd,
60
43
  primaryColor,
61
44
  }: VoiceControlsProps) {
62
- const statusText = getStatusText(roomState, agentState, isMuted, cohostActive, currentSpeaker);
45
+ const statusText = getStatusText(roomState, agentState, isMuted);
63
46
 
64
47
  return (
65
48
  <View style={styles.container}>
@@ -69,18 +52,6 @@ export function VoiceControls({
69
52
  </Text>
70
53
  ) : null}
71
54
 
72
- {roundComplete && (
73
- <Pressable
74
- onPress={onContinueCohost}
75
- className="bg-primary/30"
76
- style={[styles.continueButton, primaryColor ? { backgroundColor: primaryColor + '4D' } : undefined]}
77
- >
78
- <Text className="text-sm font-medium text-primary" style={primaryColor ? { color: primaryColor } : undefined}>
79
- Continue conversation
80
- </Text>
81
- </Pressable>
82
- )}
83
-
84
55
  {roomState === 'connected' && (
85
56
  <View style={styles.buttonRow}>
86
57
  <View style={styles.buttonWrapper}>
@@ -100,22 +71,6 @@ export function VoiceControls({
100
71
  </Text>
101
72
  </View>
102
73
 
103
- <View style={styles.buttonWrapper}>
104
- <Pressable
105
- onPress={cohostActive ? onDisableCohost : onEnableCohost}
106
- className={cohostActive ? 'bg-primary' : 'bg-muted'}
107
- style={[
108
- styles.button,
109
- cohostActive && primaryColor ? { backgroundColor: primaryColor } : undefined,
110
- ]}
111
- >
112
- <Users size={24} color="white" />
113
- </Pressable>
114
- <Text className="text-xs text-muted-foreground">
115
- {cohostActive ? 'Solo' : 'Cohost'}
116
- </Text>
117
- </View>
118
-
119
74
  <View style={styles.buttonWrapper}>
120
75
  <Pressable
121
76
  onPress={onEnd}
@@ -157,10 +112,4 @@ const styles = StyleSheet.create({
157
112
  alignItems: 'center',
158
113
  justifyContent: 'center',
159
114
  },
160
- continueButton: {
161
- marginBottom: 16,
162
- paddingHorizontal: 20,
163
- paddingVertical: 8,
164
- borderRadius: 24,
165
- },
166
115
  });
@@ -1,13 +1,12 @@
1
1
  /**
2
- * A live LiveKit call, mounted only while voice mode is on.
2
+ * A live voice call, mounted only while voice mode is on.
3
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.
4
+ * The chat shell receives it as the `voiceSession` prop rather than importing
5
+ * it, so a text-only consumer never compiles the voice loop, speech playback or
6
+ * the call UI — which is the reason the `@alia.onl/sdk/voice` entry exists.
8
7
  *
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.
8
+ * Mounting starts the call; unmounting ends it (`useVoiceRoom` stops listening
9
+ * and speaking on unmount), so the shell only has to decide whether to render it.
11
10
  */
12
11
 
13
12
  import { useEffect, useRef } from 'react';
@@ -25,15 +24,9 @@ export function VoiceSession({ apiUrl, onStateChange, onEnd }: VoiceSessionProps
25
24
  isMuted,
26
25
  error,
27
26
  messages,
28
- cohostActive,
29
- currentSpeaker,
30
- roundComplete,
31
27
  isConnected,
32
28
  connect,
33
29
  toggleMute,
34
- enableCohost,
35
- disableCohost,
36
- continueCohost,
37
30
  } = useVoiceRoom({ apiUrl });
38
31
 
39
32
  const { captureLevel, playbackLevel } = useAudioLevelMonitor(room, isConnected);
@@ -75,13 +68,7 @@ export function VoiceSession({ apiUrl, onStateChange, onEnd }: VoiceSessionProps
75
68
  roomState={roomState}
76
69
  agentState={agentState}
77
70
  isMuted={isMuted}
78
- cohostActive={cohostActive}
79
- currentSpeaker={currentSpeaker}
80
- roundComplete={roundComplete}
81
71
  onToggleMute={toggleMute}
82
- onEnableCohost={enableCohost}
83
- onDisableCohost={disableCohost}
84
- onContinueCohost={continueCohost}
85
72
  onEnd={onEnd}
86
73
  />
87
74
  );
@@ -10,7 +10,22 @@ import type { AliaChatStreamEvent } from '../lib/chat-stream';
10
10
  const API_URL = process.env.EXPO_PUBLIC_ALIA_API_URL ?? 'https://api.alia.onl';
11
11
 
12
12
  export interface UseAliaChatOptions {
13
- /** Alia API base URL (default: EXPO_PUBLIC_ALIA_API_URL or https://api.alia.onl) */
13
+ /**
14
+ * Base URL the hook sends its requests to. Defaults to
15
+ * `EXPO_PUBLIC_ALIA_API_URL`, then `https://api.alia.onl`.
16
+ *
17
+ * The hook appends its own paths — `POST /v1/chat/completions` for a turn and
18
+ * `GET /catalogue` for the model check — so the value is a host, not a route.
19
+ *
20
+ * Point this at YOUR OWN backend to use the SDK from an origin Alia does not
21
+ * enumerate: Alia's product route, `POST /alia/chat`, answers CORS preflights
22
+ * only for Alia's own origins, while a server-to-server call carries no
23
+ * `Origin` header and is answered on its credential alone. Your backend
24
+ * receives the SDK's request, forwards the `Authorization` header it carries
25
+ * (the signed-in user's Oxy access token) to `/alia/chat`, and streams the
26
+ * SSE body back unchanged. The README's *consumer-backend path* section has
27
+ * the minimal relay; issue #244 records why this is the supported shape today.
28
+ */
14
29
  apiUrl?: string;
15
30
  /**
16
31
  * Kaana routing profile to use.