@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 +91 -0
- package/README.md +234 -0
- package/package.json +9 -13
- package/src/components/AliaChatContent.tsx +0 -1
- package/src/components/AliaChatMessageList.tsx +0 -4
- package/src/components/ResearchProgressCard.tsx +3 -0
- package/src/components/ui/prompt-input/mic-button.tsx +2 -3
- package/src/components/ui/prompt-input/prompt-input.tsx +1 -4
- package/src/components/voice/VoiceControls.tsx +3 -54
- package/src/components/voice/VoiceSession.tsx +6 -19
- package/src/hooks/useAliaChat.ts +16 -1
- package/src/hooks/useAudioLevelMonitor.ts +26 -122
- package/src/hooks/useSpeechToText.ts +165 -150
- package/src/hooks/useTTS.ts +29 -29
- package/src/hooks/useVoiceRoom.ts +553 -409
- package/src/index.ts +0 -14
- package/src/lib/audio-level.ts +3 -2
- package/src/lib/chat-transport.ts +11 -0
- package/src/lib/speech-language.ts +20 -0
- package/src/lib/speech-messages.ts +31 -0
- package/src/lib/speech-recognition-types.ts +75 -0
- package/src/lib/speech-recognition.native.ts +171 -0
- package/src/lib/speech-recognition.ts +279 -0
- package/src/lib/speech-synthesis.ts +122 -0
- package/src/lib/voice-levels.ts +60 -0
- package/src/lib/voice-text.ts +192 -0
- package/src/lib/voice-turn.ts +87 -0
- package/src/types.ts +3 -3
- package/src/voice.ts +7 -3
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": "
|
|
4
|
-
"description": "Alia AI chat SDK
|
|
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
|
|
49
|
-
"@oxy.so/services": "^
|
|
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.
|
|
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
|
}
|
|
@@ -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
|
|
13
|
+
export function PromptInputMicButton({ className }: PromptInputMicButtonProps) {
|
|
15
14
|
const { value, setValue, onError } = usePromptInput();
|
|
16
|
-
const stt = useSpeechToText(
|
|
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
|
|
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
|
|
3
|
-
* Contains status text
|
|
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
|
|
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
|
|
2
|
+
* A live voice call, mounted only while voice mode is on.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
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`
|
|
10
|
-
*
|
|
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
|
);
|
package/src/hooks/useAliaChat.ts
CHANGED
|
@@ -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
|
-
/**
|
|
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.
|