@alia.onl/sdk 5.0.0 → 7.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 +80 -0
- package/LICENSE +202 -0
- package/NOTICE +15 -0
- package/package.json +4 -2
- package/src/components/AliaChatContent.tsx +3 -3
- package/src/components/AliaChatScreen.tsx +3 -3
- package/src/components/AliaChatSheet.tsx +3 -3
- package/src/components/AliaWelcomeMessage.tsx +2 -2
- package/src/components/{AliaMark.tsx → IdentityMark.tsx} +61 -19
- package/src/components/Markdown.tsx +31 -6
- package/src/components/ThinkingIndicator.tsx +2 -2
- package/src/components/ui/button.tsx +2 -2
- package/src/components/ui/chat-text-input.tsx +27 -11
- package/src/components/ui/prompt-input/textarea.tsx +19 -3
- package/src/hooks/useAliaChat.ts +50 -3
- package/src/hooks/useTTS.ts +87 -34
- package/src/hooks/useVoiceRoom.ts +26 -2
- package/src/index.ts +15 -2
- package/src/lib/catalogue.ts +209 -0
- package/src/lib/config.ts +38 -0
- package/src/lib/tool-registry.ts +1 -1
|
@@ -13,7 +13,7 @@ const buttonVariants = cva(
|
|
|
13
13
|
destructive: "bg-destructive web:hover:opacity-90 active:opacity-90",
|
|
14
14
|
outline:
|
|
15
15
|
"border border-input bg-background web:hover:bg-accent web:hover:text-accent-foreground active:bg-accent",
|
|
16
|
-
secondary: "bg-
|
|
16
|
+
secondary: "bg-muted web:hover:opacity-80 active:opacity-80",
|
|
17
17
|
ghost:
|
|
18
18
|
"web:hover:bg-accent web:hover:text-accent-foreground active:bg-accent",
|
|
19
19
|
link: "web:underline-offset-4 web:hover:underline web:focus:underline ",
|
|
@@ -42,7 +42,7 @@ const buttonTextVariants = cva(
|
|
|
42
42
|
destructive: "text-destructive-foreground",
|
|
43
43
|
outline: "group-active:text-accent-foreground",
|
|
44
44
|
secondary:
|
|
45
|
-
"text-
|
|
45
|
+
"text-muted-foreground group-active:text-muted-foreground",
|
|
46
46
|
ghost: "group-active:text-accent-foreground",
|
|
47
47
|
link: "text-primary group-active:underline",
|
|
48
48
|
pill: "text-primary-foreground",
|
|
@@ -21,6 +21,7 @@ declare module "react-native" {
|
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
type ChatTextInputProps = React.ComponentPropsWithoutRef<typeof TextInput> & {
|
|
24
|
+
unstyled?: boolean;
|
|
24
25
|
noFocus?: boolean;
|
|
25
26
|
onEnterPress?: () => void;
|
|
26
27
|
onCompletionKey?: (key: string) => boolean;
|
|
@@ -36,6 +37,7 @@ type ChatTextInputProps = React.ComponentPropsWithoutRef<typeof TextInput> & {
|
|
|
36
37
|
const ChatTextInput = React.forwardRef<TextInput, ChatTextInputProps>(
|
|
37
38
|
({
|
|
38
39
|
className,
|
|
40
|
+
unstyled = false,
|
|
39
41
|
noFocus = false,
|
|
40
42
|
onEnterPress,
|
|
41
43
|
onCompletionKey,
|
|
@@ -54,6 +56,28 @@ const ChatTextInput = React.forwardRef<TextInput, ChatTextInputProps>(
|
|
|
54
56
|
const inputRef = React.useRef<TextInput>(null);
|
|
55
57
|
const wrapperRef = React.useRef<View>(null);
|
|
56
58
|
|
|
59
|
+
// react-native-css observes `style` by reference. Memoizing the composed
|
|
60
|
+
// value prevents its own state update from manufacturing another changed
|
|
61
|
+
// style prop and recursively re-rendering this TextInput on native.
|
|
62
|
+
const inputStyle = React.useMemo(() => [
|
|
63
|
+
style,
|
|
64
|
+
!fillContainer && props.multiline && !disableAutoHeight && ({
|
|
65
|
+
minHeight,
|
|
66
|
+
maxHeight,
|
|
67
|
+
...(Platform.OS === 'web'
|
|
68
|
+
? { overflow: 'auto' as const, fieldSizing: 'content' }
|
|
69
|
+
: {}),
|
|
70
|
+
} as TextStyle & { fieldSizing?: string }),
|
|
71
|
+
fillContainer && { flex: 1, height: '100%' as const },
|
|
72
|
+
], [
|
|
73
|
+
disableAutoHeight,
|
|
74
|
+
fillContainer,
|
|
75
|
+
maxHeight,
|
|
76
|
+
minHeight,
|
|
77
|
+
props.multiline,
|
|
78
|
+
style,
|
|
79
|
+
]);
|
|
80
|
+
|
|
57
81
|
React.useImperativeHandle(ref, () => inputRef.current as TextInput);
|
|
58
82
|
|
|
59
83
|
// Attach paste event listener (web only)
|
|
@@ -135,7 +159,8 @@ const ChatTextInput = React.forwardRef<TextInput, ChatTextInputProps>(
|
|
|
135
159
|
<TextInput
|
|
136
160
|
ref={inputRef}
|
|
137
161
|
className={cn(
|
|
138
|
-
"native:text-md native:leading-[1.25]
|
|
162
|
+
"native:text-md native:leading-[1.25] text-base text-foreground file:border-0 file:bg-transparent file:font-medium placeholder:text-muted-foreground web:flex web:w-full web:py-2 lg:text-sm",
|
|
163
|
+
!unstyled && "rounded-xl border border-input bg-background px-3.5",
|
|
139
164
|
"web:ring-offset-background web:focus-visible:outline-none web:focus-visible:ring-2 web:focus-visible:ring-ring web:focus-visible:ring-offset-2",
|
|
140
165
|
!fillContainer && !props.multiline && "h-9",
|
|
141
166
|
fillContainer && "h-full",
|
|
@@ -147,16 +172,7 @@ const ChatTextInput = React.forwardRef<TextInput, ChatTextInputProps>(
|
|
|
147
172
|
onKeyPress={handleKeyPress}
|
|
148
173
|
onContentSizeChange={handleContentSizeChange}
|
|
149
174
|
scrollEnabled={fillContainer || props.multiline}
|
|
150
|
-
style={
|
|
151
|
-
style,
|
|
152
|
-
!fillContainer && props.multiline && !disableAutoHeight && ({
|
|
153
|
-
minHeight,
|
|
154
|
-
maxHeight,
|
|
155
|
-
overflow: 'auto',
|
|
156
|
-
...(Platform.OS === 'web' ? { fieldSizing: 'content' } : {}),
|
|
157
|
-
} as TextStyle & { overflow: 'auto'; fieldSizing?: string }),
|
|
158
|
-
fillContainer && { flex: 1, height: '100%' },
|
|
159
|
-
]}
|
|
175
|
+
style={inputStyle}
|
|
160
176
|
{...props}
|
|
161
177
|
/>
|
|
162
178
|
</View>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
-
import { View } from "react-native";
|
|
2
|
+
import { StyleSheet, View } from "react-native";
|
|
3
3
|
import { cn } from "../../../lib/utils";
|
|
4
4
|
import { ChatTextInput } from "../chat-text-input";
|
|
5
5
|
import { usePromptInput } from "./context";
|
|
@@ -28,6 +28,11 @@ export function PromptInputTextarea({
|
|
|
28
28
|
handleCompletionKey,
|
|
29
29
|
} = usePromptInput();
|
|
30
30
|
|
|
31
|
+
const inputStyle = React.useMemo(
|
|
32
|
+
() => [style, styles.borderless, isFullscreen && styles.fullscreen],
|
|
33
|
+
[isFullscreen, style],
|
|
34
|
+
);
|
|
35
|
+
|
|
31
36
|
const textInput = (
|
|
32
37
|
<ChatTextInput
|
|
33
38
|
ref={textareaRef}
|
|
@@ -42,12 +47,14 @@ export function PromptInputTextarea({
|
|
|
42
47
|
maxHeight={isFullscreen ? 10000 : maxHeight}
|
|
43
48
|
onImagePaste={onImagePaste}
|
|
44
49
|
fillContainer={isFullscreen}
|
|
50
|
+
unstyled
|
|
45
51
|
className={cn(
|
|
46
|
-
"w-full border-0 bg-transparent text-foreground shadow-none",
|
|
52
|
+
"w-full border-0 bg-transparent text-foreground web:shadow-none",
|
|
47
53
|
isFullscreen ? "px-4 pt-4" : "min-h-[44px] py-3",
|
|
48
54
|
className
|
|
49
55
|
)}
|
|
50
|
-
style={
|
|
56
|
+
style={inputStyle}
|
|
57
|
+
underlineColorAndroid="transparent"
|
|
51
58
|
placeholder={placeholder}
|
|
52
59
|
multiline
|
|
53
60
|
editable={!disabled}
|
|
@@ -62,3 +69,12 @@ export function PromptInputTextarea({
|
|
|
62
69
|
|
|
63
70
|
return textInput;
|
|
64
71
|
}
|
|
72
|
+
|
|
73
|
+
const styles = StyleSheet.create({
|
|
74
|
+
borderless: {
|
|
75
|
+
borderWidth: 0,
|
|
76
|
+
},
|
|
77
|
+
fullscreen: {
|
|
78
|
+
paddingBottom: 100,
|
|
79
|
+
},
|
|
80
|
+
});
|
package/src/hooks/useAliaChat.ts
CHANGED
|
@@ -2,13 +2,22 @@ import React, { useState, useRef, useCallback, useEffect } from 'react';
|
|
|
2
2
|
import { useOxy } from '@oxyhq/services';
|
|
3
3
|
import type { ChatMessage, ToolInvocation } from '../types';
|
|
4
4
|
import { getTextFromContent } from '../lib/content-utils';
|
|
5
|
+
import { resolveModelId } from '../lib/catalogue';
|
|
6
|
+
import { PREFERRED_CHAT_MODEL_ID } from '../lib/config';
|
|
5
7
|
|
|
6
8
|
const API_URL = process.env.EXPO_PUBLIC_ALIA_API_URL ?? 'https://api.alia.onl';
|
|
7
9
|
|
|
8
10
|
export interface UseAliaChatOptions {
|
|
9
11
|
/** Alia API base URL (default: EXPO_PUBLIC_ALIA_API_URL or https://api.alia.onl) */
|
|
10
12
|
apiUrl?: string;
|
|
11
|
-
/**
|
|
13
|
+
/**
|
|
14
|
+
* Alia model to use.
|
|
15
|
+
*
|
|
16
|
+
* Optional, and checked against `GET /catalogue` before a request carries it:
|
|
17
|
+
* an identifier the server no longer offers is replaced rather than sent, so a
|
|
18
|
+
* retirement does not turn into a 400 inside a consumer's app. Omitted, the
|
|
19
|
+
* build's `PREFERRED_CHAT_MODEL_ID` is what gets checked.
|
|
20
|
+
*/
|
|
12
21
|
model?: string;
|
|
13
22
|
/** App context injected as system message so Alia knows which app the user is in */
|
|
14
23
|
clientContext?: string;
|
|
@@ -43,7 +52,7 @@ function generateId(): string {
|
|
|
43
52
|
export function useAliaChat(options: UseAliaChatOptions = {}): UseAliaChatReturn {
|
|
44
53
|
const {
|
|
45
54
|
apiUrl = API_URL,
|
|
46
|
-
model =
|
|
55
|
+
model = PREFERRED_CHAT_MODEL_ID,
|
|
47
56
|
clientContext,
|
|
48
57
|
accessToken: accessTokenProp,
|
|
49
58
|
} = options;
|
|
@@ -214,6 +223,40 @@ export function useAliaChat(options: UseAliaChatOptions = {}): UseAliaChatReturn
|
|
|
214
223
|
toolInvocationsRef.current = [];
|
|
215
224
|
|
|
216
225
|
try {
|
|
226
|
+
/**
|
|
227
|
+
* This SDK stays on `/v1/chat/completions`, and that is a decision
|
|
228
|
+
* rather than an oversight — epic #139 workstream 6.
|
|
229
|
+
*
|
|
230
|
+
* Every other first-party Alia client moved to the product runtime at
|
|
231
|
+
* `POST /alia/chat`, which serves the identical handler. This one cannot
|
|
232
|
+
* follow, for two reasons that are independent of each other.
|
|
233
|
+
*
|
|
234
|
+
* **CORS.** `/v1` is the only surface with the public wildcard policy;
|
|
235
|
+
* everything else falls to the Oxy allowlist
|
|
236
|
+
* (`packages/api/src/index.ts`). Measured 2026-08-19 against the running
|
|
237
|
+
* service: an `OPTIONS /alia/chat` preflight from
|
|
238
|
+
* `https://alia.onl` and `https://console.alia.onl` is answered with a
|
|
239
|
+
* matching `access-control-allow-origin`, and one from an unlisted
|
|
240
|
+
* origin comes back with NO such header, so a browser blocks the
|
|
241
|
+
* request. `OPTIONS /v1/chat/completions` answers `*` for all three.
|
|
242
|
+
* This package is installed by apps on origins Alia does not enumerate,
|
|
243
|
+
* so the product surface would refuse them. Widening the allowlist is
|
|
244
|
+
* not the repair: the narrow policy is what makes `/alia/chat` a product
|
|
245
|
+
* surface, and it is one of the three recorded differences in
|
|
246
|
+
* `packages/api/src/routes/__tests__/v1-compatibility-surface.test.ts`.
|
|
247
|
+
*
|
|
248
|
+
* **Raw source.** `@alia.onl/sdk` publishes `src/`, so consumers' own
|
|
249
|
+
* Metro and tsc compile this line and it changes what every installed
|
|
250
|
+
* copy sends on its NEXT build — not on a release this repository
|
|
251
|
+
* controls. `docs/migration/compatibility-window.md` records the same
|
|
252
|
+
* property for the aliases: installed copies resume sending whatever
|
|
253
|
+
* their source says the moment the service is reachable.
|
|
254
|
+
*
|
|
255
|
+
* The consequence is recorded rather than hidden: `/v1/chat/completions`
|
|
256
|
+
* cannot reach its removal gate while this SDK is a consumer, so
|
|
257
|
+
* migrating it is a browser-CORS decision on the product surface, taken
|
|
258
|
+
* with the SDK's consumers enumerated.
|
|
259
|
+
*/
|
|
217
260
|
const response = await fetch(`${apiUrl}/v1/chat/completions`, {
|
|
218
261
|
method: 'POST',
|
|
219
262
|
headers: {
|
|
@@ -221,7 +264,11 @@ export function useAliaChat(options: UseAliaChatOptions = {}): UseAliaChatReturn
|
|
|
221
264
|
Authorization: `Bearer ${token}`,
|
|
222
265
|
},
|
|
223
266
|
body: JSON.stringify({
|
|
224
|
-
|
|
267
|
+
// Resolved HERE rather than at render: this is the moment a request
|
|
268
|
+
// is about to name a model, and it is the only moment at which
|
|
269
|
+
// sending a retired identifier would actually cost anything. A
|
|
270
|
+
// catalogue that cannot be read leaves the choice alone.
|
|
271
|
+
model: await resolveModelId(apiUrl, model, token, PREFERRED_CHAT_MODEL_ID),
|
|
225
272
|
messages: apiMessages,
|
|
226
273
|
stream: true,
|
|
227
274
|
}),
|
package/src/hooks/useTTS.ts
CHANGED
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
import { useOxy } from '@oxyhq/services';
|
|
12
12
|
import { errorMessage } from '../lib/utils';
|
|
13
13
|
import { create } from 'zustand';
|
|
14
|
+
import { PREFERRED_VOICE_MODEL_ID } from '../lib/config';
|
|
14
15
|
|
|
15
16
|
const API_URL = process.env.EXPO_PUBLIC_ALIA_API_URL ?? 'https://api.alia.onl';
|
|
16
17
|
|
|
@@ -21,6 +22,15 @@ export interface UseTTSOptions {
|
|
|
21
22
|
accessToken?: string;
|
|
22
23
|
voice?: 'male' | 'female';
|
|
23
24
|
tone?: 'brief' | 'chill' | 'default';
|
|
25
|
+
/**
|
|
26
|
+
* Speech model. Defaults to the build's `PREFERRED_VOICE_MODEL_ID`.
|
|
27
|
+
*
|
|
28
|
+
* Not checked against `GET /catalogue`: that surface describes what a chat
|
|
29
|
+
* picker may offer and its resolver filters to `chat_visible` entries, so
|
|
30
|
+
* passing a voice identifier through it would substitute a chat model that
|
|
31
|
+
* cannot speak. See `lib/config.ts`.
|
|
32
|
+
*/
|
|
33
|
+
model?: string;
|
|
24
34
|
}
|
|
25
35
|
|
|
26
36
|
// ============== INLINE TTS STORE ==============
|
|
@@ -52,6 +62,7 @@ const useTTSStore = create<TTSStore>((set) => ({
|
|
|
52
62
|
export function useTTS(options: UseTTSOptions = {}) {
|
|
53
63
|
const apiUrl = options.apiUrl || API_URL;
|
|
54
64
|
const voicePref = options.voice ?? 'female';
|
|
65
|
+
const voiceModel = options.model ?? PREFERRED_VOICE_MODEL_ID;
|
|
55
66
|
const tonePref = options.tone ?? 'default';
|
|
56
67
|
|
|
57
68
|
const { oxyServices } = useOxy();
|
|
@@ -120,7 +131,21 @@ export function useTTS(options: UseTTSOptions = {}) {
|
|
|
120
131
|
reset();
|
|
121
132
|
}, [reset, releasePlayer]);
|
|
122
133
|
|
|
123
|
-
|
|
134
|
+
/**
|
|
135
|
+
* Play a stored clip.
|
|
136
|
+
*
|
|
137
|
+
* `onUnplayable` is how a caller says "I can make this again". Stored audio
|
|
138
|
+
* is not permanent — `tts/` objects age out — and a link to something that is
|
|
139
|
+
* gone fails in the player, not at the request, so the only place to notice
|
|
140
|
+
* is here. Without a fallback the failure is reported; with one, the caller
|
|
141
|
+
* gets to rebuild rather than the person getting an error about a button they
|
|
142
|
+
* pressed twice.
|
|
143
|
+
*/
|
|
144
|
+
const playFromUrl = useCallback((
|
|
145
|
+
audioUrl: string,
|
|
146
|
+
_messageId: string,
|
|
147
|
+
onUnplayable?: () => void,
|
|
148
|
+
) => {
|
|
124
149
|
releasePlayer();
|
|
125
150
|
|
|
126
151
|
(async () => {
|
|
@@ -132,6 +157,10 @@ export function useTTS(options: UseTTSOptions = {}) {
|
|
|
132
157
|
player.addListener('playbackStatusUpdate', (status: AudioStatus) => {
|
|
133
158
|
if (status.error) {
|
|
134
159
|
releasePlayer();
|
|
160
|
+
if (onUnplayable) {
|
|
161
|
+
onUnplayable();
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
135
164
|
setError(status.error);
|
|
136
165
|
return;
|
|
137
166
|
}
|
|
@@ -144,6 +173,10 @@ export function useTTS(options: UseTTSOptions = {}) {
|
|
|
144
173
|
player.play();
|
|
145
174
|
setPlaybackState('playing');
|
|
146
175
|
} catch {
|
|
176
|
+
if (onUnplayable) {
|
|
177
|
+
onUnplayable();
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
147
180
|
setError('Audio playback not available');
|
|
148
181
|
}
|
|
149
182
|
})();
|
|
@@ -183,44 +216,64 @@ export function useTTS(options: UseTTSOptions = {}) {
|
|
|
183
216
|
setActiveMessage(messageId);
|
|
184
217
|
setPlaybackState('loading');
|
|
185
218
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
219
|
+
/**
|
|
220
|
+
* Ask the server to make it, and play what comes back.
|
|
221
|
+
*
|
|
222
|
+
* Hoisted out of the cached branch below so a clip that has aged out of
|
|
223
|
+
* storage can fall back to it. `playFromUrl` is called WITHOUT a fallback
|
|
224
|
+
* here: a freshly made clip that will not play is a real failure, and
|
|
225
|
+
* retrying it would be a loop.
|
|
226
|
+
*/
|
|
227
|
+
const synthesize = async () => {
|
|
228
|
+
const token = getToken();
|
|
229
|
+
if (!token) {
|
|
230
|
+
throw new Error('Not authenticated');
|
|
231
|
+
}
|
|
191
232
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
233
|
+
const response = await fetch(`${apiUrl}/v1/audio/speech`, {
|
|
234
|
+
method: 'POST',
|
|
235
|
+
headers: {
|
|
236
|
+
'Content-Type': 'application/json',
|
|
237
|
+
Authorization: `Bearer ${token}`,
|
|
238
|
+
},
|
|
239
|
+
body: JSON.stringify({
|
|
240
|
+
model: voiceModel,
|
|
241
|
+
input: text,
|
|
242
|
+
voice: getTTSVoice(),
|
|
243
|
+
speed: getTTSSpeed(),
|
|
244
|
+
conversationId,
|
|
245
|
+
messageId,
|
|
246
|
+
}),
|
|
247
|
+
});
|
|
197
248
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
body: JSON.stringify({
|
|
205
|
-
model: 'alia-v1-voice',
|
|
206
|
-
input: text,
|
|
207
|
-
voice: getTTSVoice(),
|
|
208
|
-
speed: getTTSSpeed(),
|
|
209
|
-
conversationId,
|
|
210
|
-
messageId,
|
|
211
|
-
}),
|
|
212
|
-
});
|
|
213
|
-
|
|
214
|
-
if (!response.ok) {
|
|
215
|
-
if (response.status === 504) {
|
|
216
|
-
throw new Error('Request timed out — please try again');
|
|
249
|
+
if (!response.ok) {
|
|
250
|
+
if (response.status === 504) {
|
|
251
|
+
throw new Error('Request timed out — please try again');
|
|
252
|
+
}
|
|
253
|
+
const errData = await response.json().catch(() => ({}));
|
|
254
|
+
throw new Error(errData.error?.message || errData.error || 'TTS failed');
|
|
217
255
|
}
|
|
218
|
-
|
|
219
|
-
|
|
256
|
+
|
|
257
|
+
const data = await response.json();
|
|
258
|
+
playFromUrl(data.audioUrl, messageId);
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* A stored clip plays straight away, and is remade if it has gone.
|
|
263
|
+
*
|
|
264
|
+
* The row keeps its key long after a `tts/` object ages out of storage,
|
|
265
|
+
* so the link is signed, valid, and points at nothing — a failure only
|
|
266
|
+
* the player ever sees. Remaking it is cheaper than keeping every clip
|
|
267
|
+
* forever, and the person notices nothing.
|
|
268
|
+
*/
|
|
269
|
+
if (audioUrl) {
|
|
270
|
+
playFromUrl(audioUrl, messageId, () => {
|
|
271
|
+
void synthesize().catch((e: unknown) => setError(errorMessage(e, 'Failed to read aloud')));
|
|
272
|
+
});
|
|
273
|
+
return;
|
|
220
274
|
}
|
|
221
275
|
|
|
222
|
-
|
|
223
|
-
playFromUrl(data.audioUrl, messageId);
|
|
276
|
+
await synthesize();
|
|
224
277
|
} catch (e: unknown) {
|
|
225
278
|
console.error('[TTS] Error:', e);
|
|
226
279
|
setError(errorMessage(e, 'Failed to read aloud'));
|
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
import { useOxy } from '@oxyhq/services';
|
|
21
21
|
import { errorMessage } from '../lib/utils';
|
|
22
22
|
import type { RoomState, AgentState, VoiceMessage, VoiceToolInvocation } from '../types';
|
|
23
|
+
import { PREFERRED_VOICE_MODEL_ID } from '../lib/config';
|
|
23
24
|
|
|
24
25
|
const API_URL = process.env.EXPO_PUBLIC_ALIA_API_URL ?? 'https://api.alia.onl';
|
|
25
26
|
|
|
@@ -33,6 +34,23 @@ export interface UseVoiceRoomOptions {
|
|
|
33
34
|
apiUrl?: string;
|
|
34
35
|
voicePreference?: 'male' | 'female';
|
|
35
36
|
accessToken?: string;
|
|
37
|
+
/**
|
|
38
|
+
* Voice session model. Defaults to the build's `PREFERRED_VOICE_MODEL_ID`.
|
|
39
|
+
*
|
|
40
|
+
* Not checked against `GET /catalogue`, for the reason given in
|
|
41
|
+
* `lib/config.ts`: the chat resolver filters to `chat_visible` entries and
|
|
42
|
+
* would substitute something that cannot hold a voice session.
|
|
43
|
+
*/
|
|
44
|
+
model?: string;
|
|
45
|
+
/**
|
|
46
|
+
* The agent this session belongs to, when it is a thread with one.
|
|
47
|
+
*
|
|
48
|
+
* Without it the session is ordinary Alia: the API composes the generic
|
|
49
|
+
* prompt and the identity guard says the model's name, so an agent's voice
|
|
50
|
+
* answers as Alia. It is the caller's to supply because only the screen knows
|
|
51
|
+
* whose thread is open.
|
|
52
|
+
*/
|
|
53
|
+
agentId?: string;
|
|
36
54
|
}
|
|
37
55
|
|
|
38
56
|
// ============== TITLE TAG UTILITIES ==============
|
|
@@ -105,6 +123,8 @@ function micDeviceErrorMessage(name: string): string | null {
|
|
|
105
123
|
export function useVoiceRoom(options: UseVoiceRoomOptions = {}) {
|
|
106
124
|
const apiUrl = options.apiUrl || API_URL;
|
|
107
125
|
const voicePref = options.voicePreference ?? 'female';
|
|
126
|
+
const voiceModel = options.model ?? PREFERRED_VOICE_MODEL_ID;
|
|
127
|
+
const agentId = options.agentId;
|
|
108
128
|
|
|
109
129
|
const [roomState, setRoomState] = useState<RoomState>('disconnected');
|
|
110
130
|
const [agentState, setAgentState] = useState<AgentState>('idle');
|
|
@@ -329,8 +349,12 @@ export function useVoiceRoom(options: UseVoiceRoomOptions = {}) {
|
|
|
329
349
|
Authorization: `Bearer ${token}`,
|
|
330
350
|
},
|
|
331
351
|
body: JSON.stringify({
|
|
332
|
-
model:
|
|
352
|
+
model: voiceModel,
|
|
333
353
|
voice: voicePref === 'male' ? 'echo' : 'nova',
|
|
354
|
+
// Omitted rather than sent as undefined: the API branches on the key
|
|
355
|
+
// being a string, and a session with no agent must look exactly as it
|
|
356
|
+
// did before this existed.
|
|
357
|
+
...(agentId ? { agentId } : {}),
|
|
334
358
|
}),
|
|
335
359
|
});
|
|
336
360
|
|
|
@@ -408,7 +432,7 @@ export function useVoiceRoom(options: UseVoiceRoomOptions = {}) {
|
|
|
408
432
|
setRoomState('error');
|
|
409
433
|
cleanup();
|
|
410
434
|
}
|
|
411
|
-
}, [getToken, cleanup, handleDataMessage, apiUrl, voicePref]);
|
|
435
|
+
}, [getToken, cleanup, handleDataMessage, apiUrl, voicePref, voiceModel, agentId]);
|
|
412
436
|
|
|
413
437
|
// ============== DISCONNECT ==============
|
|
414
438
|
|
package/src/index.ts
CHANGED
|
@@ -9,8 +9,8 @@ export { AliaChatScreen } from './components/AliaChatScreen';
|
|
|
9
9
|
export type { AliaChatScreenProps } from './components/AliaChatScreen';
|
|
10
10
|
|
|
11
11
|
// ── Chat UI components ──
|
|
12
|
-
export {
|
|
13
|
-
export type {
|
|
12
|
+
export { IdentityMark } from './components/IdentityMark';
|
|
13
|
+
export type { IdentityMarkProps, IdentityMarkState } from './components/IdentityMark';
|
|
14
14
|
export { ThinkingIndicator } from './components/ThinkingIndicator';
|
|
15
15
|
export { AliaMarkdown } from './components/Markdown';
|
|
16
16
|
export type { AliaMarkdownProps } from './components/Markdown';
|
|
@@ -29,6 +29,19 @@ export type { UseTTSOptions } from './hooks/useTTS';
|
|
|
29
29
|
export { useSpeechToText, useSTTStore } from './hooks/useSpeechToText';
|
|
30
30
|
export type { UseSTTOptions } from './hooks/useSpeechToText';
|
|
31
31
|
|
|
32
|
+
// ── Model catalogue (GET /catalogue) ──
|
|
33
|
+
// Exported so a consumer building its own picker reads the surface the SDK
|
|
34
|
+
// reads, rather than hardcoding an identifier the way this package used to.
|
|
35
|
+
export {
|
|
36
|
+
fetchCatalogue,
|
|
37
|
+
parseCatalogue,
|
|
38
|
+
resolveSelection,
|
|
39
|
+
resolveModelId,
|
|
40
|
+
clearCatalogueCache,
|
|
41
|
+
} from './lib/catalogue';
|
|
42
|
+
export type { CatalogueEntry, CatalogueEntryKind, ModelSelection } from './lib/catalogue';
|
|
43
|
+
export { PREFERRED_CHAT_MODEL_ID, PREFERRED_VOICE_MODEL_ID } from './lib/config';
|
|
44
|
+
|
|
32
45
|
// ── Types ──
|
|
33
46
|
export type {
|
|
34
47
|
ChatMessage,
|