@alia.onl/sdk 10.1.1 → 11.1.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 +67 -0
- package/README.md +24 -0
- package/package.json +4 -6
- package/src/components/AliaAvatar.tsx +66 -0
- package/src/components/AliaChatContent.tsx +236 -112
- package/src/components/AliaChatHeader.tsx +118 -0
- package/src/components/AliaChatMessageList.tsx +197 -260
- package/src/components/AliaChatScreen.tsx +23 -24
- package/src/components/AliaChatSheet.tsx +70 -316
- package/src/components/AliaWelcomeMessage.tsx +47 -50
- package/src/components/AmbientField.tsx +577 -0
- package/src/components/IdentityMark.tsx +10 -2
- package/src/components/Markdown.tsx +116 -40
- package/src/components/PlanPreviewCard.tsx +71 -55
- package/src/components/QuestionCard.tsx +62 -0
- package/src/components/ThinkingBlock.tsx +47 -0
- package/src/components/VoiceModeIcon.tsx +10 -0
- package/src/components/voice/VoiceControls.tsx +22 -73
- package/src/components/voice/VoiceSession.tsx +2 -1
- package/src/hooks/useAliaChat.ts +22 -15
- package/src/hooks/useAmbientWave.ts +35 -5
- package/src/hooks/useSoundEffects.ts +14 -11
- package/src/hooks/useSpeechToText.ts +9 -2
- package/src/hooks/useTTS.ts +163 -156
- package/src/hooks/useVoiceRoom.ts +228 -170
- package/src/index.ts +26 -6
- package/src/lib/catalogue.ts +17 -4
- package/src/lib/chat-stream.ts +25 -7
- package/src/lib/chat-transport.ts +2 -1
- package/src/lib/citations.ts +63 -0
- package/src/lib/content-utils.ts +3 -2
- package/src/lib/image-data-url.ts +15 -0
- package/src/lib/keyboard.tsx +1 -1
- package/src/lib/labels.tsx +81 -0
- package/src/lib/questions.ts +78 -0
- package/src/lib/speech-recognition.native.ts +15 -4
- package/src/lib/speech-recognition.ts +27 -7
- package/src/lib/task-labels.ts +67 -0
- package/src/lib/tool-call-status.ts +28 -0
- package/src/lib/tool-registry.ts +14 -14
- package/src/lib/voice-text.ts +12 -6
- package/src/lib/voice-turn.ts +4 -1
- package/src/lib/work-log-copy.ts +61 -0
- package/src/lib/work-log.ts +378 -0
- package/src/tools.ts +18 -0
- package/src/types.ts +5 -2
- package/src/voice.ts +12 -4
- package/src/components/Reasoning.tsx +0 -152
- package/src/components/ResearchProgressCard.tsx +0 -159
- package/src/components/ThinkingIndicator.tsx +0 -98
- package/src/components/ui/button.tsx +0 -104
- package/src/components/ui/chat-text-input.tsx +0 -184
- package/src/components/ui/dropdown-menu.tsx +0 -2
- package/src/components/ui/dropdown-menu.web.tsx +0 -383
- package/src/components/ui/prompt-input/actions.tsx +0 -28
- package/src/components/ui/prompt-input/add-menu.tsx +0 -83
- package/src/components/ui/prompt-input/attachments.tsx +0 -181
- package/src/components/ui/prompt-input/autocomplete.tsx +0 -168
- package/src/components/ui/prompt-input/context.tsx +0 -71
- package/src/components/ui/prompt-input/mic-button.tsx +0 -53
- package/src/components/ui/prompt-input/prompt-input.tsx +0 -279
- package/src/components/ui/prompt-input/submit-button.tsx +0 -50
- package/src/components/ui/prompt-input/textarea.tsx +0 -80
- package/src/components/ui/text.tsx +0 -29
- package/src/components/voice/AudioWaveVisualizer.tsx +0 -444
- package/src/components/voice/VoiceOverlay.tsx +0 -75
|
@@ -37,8 +37,15 @@ import {
|
|
|
37
37
|
requestSpeechRecognitionPermission,
|
|
38
38
|
startSpeechRecognition,
|
|
39
39
|
} from '../lib/speech-recognition';
|
|
40
|
-
import type {
|
|
41
|
-
|
|
40
|
+
import type {
|
|
41
|
+
SpeechRecognitionFailure,
|
|
42
|
+
SpeechRecognitionSession,
|
|
43
|
+
} from '../lib/speech-recognition-types';
|
|
44
|
+
import {
|
|
45
|
+
speechFailureCode,
|
|
46
|
+
VOICE_ERROR_MESSAGES,
|
|
47
|
+
type VoiceErrorCode,
|
|
48
|
+
} from '../lib/speech-messages';
|
|
42
49
|
import { defaultSpeechLanguage } from '../lib/speech-language';
|
|
43
50
|
import { playSpeechClip, requestSpeechClip, type ProductVoice } from '../lib/speech-synthesis';
|
|
44
51
|
import { VoiceLevelChannel, type VoiceLevelSource } from '../lib/voice-levels';
|
|
@@ -146,7 +153,9 @@ export function useVoiceRoom(options: UseVoiceRoomOptions = {}) {
|
|
|
146
153
|
* the code's English copy, except for `turn-failed`, which carries what the
|
|
147
154
|
* turn sender threw.
|
|
148
155
|
*/
|
|
149
|
-
const [callFailure, setCallFailure] = useState<{ code: VoiceErrorCode; message: string } | null>(
|
|
156
|
+
const [callFailure, setCallFailure] = useState<{ code: VoiceErrorCode; message: string } | null>(
|
|
157
|
+
null,
|
|
158
|
+
);
|
|
150
159
|
/** A turn that failed without ending the call: no answer, or no audio for it. */
|
|
151
160
|
const [turnErrorCode, setTurnErrorCode] = useState<VoiceErrorCode | null>(null);
|
|
152
161
|
const [messages, setMessagesState] = useState<VoiceMessage[]>([]);
|
|
@@ -218,9 +227,14 @@ export function useVoiceRoom(options: UseVoiceRoomOptions = {}) {
|
|
|
218
227
|
return `${sessionPrefixRef.current}-${msgIdRef.current}`;
|
|
219
228
|
};
|
|
220
229
|
|
|
221
|
-
const patchMessage = useCallback(
|
|
222
|
-
|
|
223
|
-
|
|
230
|
+
const patchMessage = useCallback(
|
|
231
|
+
(id: string, patch: Partial<VoiceMessage>) => {
|
|
232
|
+
updateMessages((previous) =>
|
|
233
|
+
previous.map((message) => (message.id === id ? { ...message, ...patch } : message)),
|
|
234
|
+
);
|
|
235
|
+
},
|
|
236
|
+
[updateMessages],
|
|
237
|
+
);
|
|
224
238
|
|
|
225
239
|
const getToken = useCallback((): string | null => {
|
|
226
240
|
const { accessToken } = configRef.current;
|
|
@@ -237,26 +251,32 @@ export function useVoiceRoom(options: UseVoiceRoomOptions = {}) {
|
|
|
237
251
|
|
|
238
252
|
// ============== TEARDOWN ==============
|
|
239
253
|
|
|
240
|
-
const endTurn = useCallback(
|
|
241
|
-
turn
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
if (message
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
254
|
+
const endTurn = useCallback(
|
|
255
|
+
(turn: ActiveTurn) => {
|
|
256
|
+
turn.controller.abort();
|
|
257
|
+
turn.closed = true;
|
|
258
|
+
turn.wake?.();
|
|
259
|
+
if (turnRef.current === turn) turnRef.current = null;
|
|
260
|
+
const message = messagesRef.current.find((candidate) => candidate.id === turn.assistantId);
|
|
261
|
+
if (message?.isStreaming) {
|
|
262
|
+
if (message.content.trim() === '') {
|
|
263
|
+
updateMessages((previous) =>
|
|
264
|
+
previous.filter((candidate) => candidate.id !== turn.assistantId),
|
|
265
|
+
);
|
|
266
|
+
} else {
|
|
267
|
+
patchMessage(turn.assistantId, { isStreaming: false });
|
|
268
|
+
}
|
|
251
269
|
}
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
270
|
+
levels.setPlayback(0);
|
|
271
|
+
},
|
|
272
|
+
[levels, patchMessage, updateMessages],
|
|
273
|
+
);
|
|
255
274
|
|
|
256
275
|
const discardDraft = useCallback(() => {
|
|
257
276
|
const draftId = draftIdRef.current;
|
|
258
277
|
draftIdRef.current = null;
|
|
259
|
-
if (draftId !== null)
|
|
278
|
+
if (draftId !== null)
|
|
279
|
+
updateMessages((previous) => previous.filter((message) => message.id !== draftId));
|
|
260
280
|
}, [updateMessages]);
|
|
261
281
|
|
|
262
282
|
/** Stop everything; the caller decides what state the room is left in. */
|
|
@@ -272,64 +292,79 @@ export function useVoiceRoom(options: UseVoiceRoomOptions = {}) {
|
|
|
272
292
|
levels.reset();
|
|
273
293
|
}, [endTurn, levels]);
|
|
274
294
|
|
|
275
|
-
const fail = useCallback(
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
295
|
+
const fail = useCallback(
|
|
296
|
+
(code: VoiceErrorCode, message: string = VOICE_ERROR_MESSAGES[code]) => {
|
|
297
|
+
teardown();
|
|
298
|
+
discardDraft();
|
|
299
|
+
if (!mountedRef.current) return;
|
|
300
|
+
setCallFailure({ code, message });
|
|
301
|
+
setRoomState('error');
|
|
302
|
+
setAgentStateValue('idle');
|
|
303
|
+
setCurrentSpeaker(null);
|
|
304
|
+
},
|
|
305
|
+
[discardDraft, teardown],
|
|
306
|
+
);
|
|
284
307
|
|
|
285
308
|
// ============== SPEAKING ==============
|
|
286
309
|
|
|
287
|
-
const synthesize = useCallback(
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
310
|
+
const synthesize = useCallback(
|
|
311
|
+
(turn: ActiveTurn, text: string): Promise<string | null> => {
|
|
312
|
+
const token = getToken();
|
|
313
|
+
if (token === null) {
|
|
314
|
+
turn.synthesisFailed = true;
|
|
315
|
+
return Promise.resolve(null);
|
|
316
|
+
}
|
|
317
|
+
const { apiUrl: url, speechModel: model, voicePref: voice } = configRef.current;
|
|
318
|
+
return requestSpeechClip({
|
|
319
|
+
apiUrl: url,
|
|
320
|
+
token,
|
|
321
|
+
model,
|
|
322
|
+
input: text,
|
|
323
|
+
voice,
|
|
324
|
+
signal: turn.controller.signal,
|
|
325
|
+
}).catch((caught: unknown) => {
|
|
296
326
|
if (!turn.controller.signal.aborted) {
|
|
297
327
|
console.error('[useVoiceRoom] Speech synthesis failed:', caught);
|
|
298
328
|
turn.synthesisFailed = true;
|
|
299
329
|
}
|
|
300
330
|
return null;
|
|
301
331
|
});
|
|
302
|
-
|
|
332
|
+
},
|
|
333
|
+
[getToken],
|
|
334
|
+
);
|
|
303
335
|
|
|
304
336
|
/** Play the turn's clips in order as they arrive, fetching one ahead. */
|
|
305
|
-
const playTurn = useCallback(
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
337
|
+
const playTurn = useCallback(
|
|
338
|
+
async (turn: ActiveTurn): Promise<void> => {
|
|
339
|
+
const signal = turn.controller.signal;
|
|
340
|
+
for (let index = 0; ; index += 1) {
|
|
341
|
+
while (index >= turn.clips.length && !turn.closed) {
|
|
342
|
+
await new Promise<void>((resolve) => {
|
|
343
|
+
turn.wake = resolve;
|
|
344
|
+
});
|
|
345
|
+
turn.wake = null;
|
|
346
|
+
}
|
|
347
|
+
if (signal.aborted || index >= turn.clips.length) return;
|
|
348
|
+
const item = turn.clips[index];
|
|
349
|
+
item.clip ??= synthesize(turn, item.text);
|
|
350
|
+
const next = turn.clips[index + 1];
|
|
351
|
+
if (next !== undefined) next.clip ??= synthesize(turn, next.text);
|
|
352
|
+
|
|
353
|
+
const uri = await item.clip;
|
|
354
|
+
if (signal.aborted) return;
|
|
355
|
+
if (uri === null) continue;
|
|
356
|
+
turn.speaking = `${turn.speaking} ${item.text}`;
|
|
357
|
+
setAgent('speaking');
|
|
358
|
+
try {
|
|
359
|
+
await playSpeechClip(uri, { signal, onLevel: (level) => levels.setPlayback(level) });
|
|
360
|
+
} catch (caught: unknown) {
|
|
361
|
+
console.error('[useVoiceRoom] Playback failed:', caught);
|
|
362
|
+
turn.synthesisFailed = true;
|
|
363
|
+
}
|
|
330
364
|
}
|
|
331
|
-
}
|
|
332
|
-
|
|
365
|
+
},
|
|
366
|
+
[levels, setAgent, synthesize],
|
|
367
|
+
);
|
|
333
368
|
|
|
334
369
|
const queueChunks = useCallback((turn: ActiveTurn, final: boolean) => {
|
|
335
370
|
const pending = turn.text.slice(turn.consumed);
|
|
@@ -362,20 +397,23 @@ export function useVoiceRoom(options: UseVoiceRoomOptions = {}) {
|
|
|
362
397
|
}, COMMIT_TIMEOUT_MS);
|
|
363
398
|
}, []);
|
|
364
399
|
|
|
365
|
-
const showDraft = useCallback(
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
400
|
+
const showDraft = useCallback(
|
|
401
|
+
(transcript: string) => {
|
|
402
|
+
const draftId = draftIdRef.current;
|
|
403
|
+
if (draftId !== null) {
|
|
404
|
+
patchMessage(draftId, { content: transcript });
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
const id = nextId();
|
|
408
|
+
draftIdRef.current = id;
|
|
409
|
+
updateMessages((previous) => [
|
|
410
|
+
...previous,
|
|
411
|
+
{ id, role: 'user', content: transcript, timestamp: Date.now(), isStreaming: true },
|
|
412
|
+
]);
|
|
413
|
+
if (mountedRef.current) setCurrentSpeaker('user');
|
|
414
|
+
},
|
|
415
|
+
[patchMessage, updateMessages],
|
|
416
|
+
);
|
|
379
417
|
|
|
380
418
|
const startListening = useCallback(() => {
|
|
381
419
|
if (phaseRef.current === 'off' || mutedRef.current || sessionRef.current !== null) return;
|
|
@@ -436,7 +474,11 @@ export function useVoiceRoom(options: UseVoiceRoomOptions = {}) {
|
|
|
436
474
|
}
|
|
437
475
|
|
|
438
476
|
// An utterance in progress when the engine ended is still an utterance.
|
|
439
|
-
if (
|
|
477
|
+
if (
|
|
478
|
+
(committing || draftIdRef.current !== null) &&
|
|
479
|
+
heard !== '' &&
|
|
480
|
+
turnRef.current === null
|
|
481
|
+
) {
|
|
440
482
|
rapidEndsRef.current = 0;
|
|
441
483
|
runTurnRef.current(heard);
|
|
442
484
|
return;
|
|
@@ -460,99 +502,109 @@ export function useVoiceRoom(options: UseVoiceRoomOptions = {}) {
|
|
|
460
502
|
|
|
461
503
|
// ============== THINKING ==============
|
|
462
504
|
|
|
463
|
-
const runTurn = useCallback(
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
505
|
+
const runTurn = useCallback(
|
|
506
|
+
(text: string) => {
|
|
507
|
+
// Settle the person's words as a message of their own.
|
|
508
|
+
const draftId = draftIdRef.current;
|
|
509
|
+
draftIdRef.current = null;
|
|
510
|
+
if (draftId !== null) {
|
|
511
|
+
patchMessage(draftId, { content: text, isStreaming: false });
|
|
512
|
+
} else {
|
|
513
|
+
updateMessages((previous) => [
|
|
514
|
+
...previous,
|
|
515
|
+
{ id: nextId(), role: 'user', content: text, timestamp: Date.now(), isStreaming: false },
|
|
516
|
+
]);
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
const history: VoiceTurnMessage[] = messagesRef.current
|
|
520
|
+
.filter((message) => message.content.trim() !== '' && message.id !== draftId)
|
|
521
|
+
.map((message) => ({ role: message.role, content: message.content }))
|
|
522
|
+
// The turn itself is `text`, not history.
|
|
523
|
+
.slice(0, draftId === null ? -1 : undefined);
|
|
524
|
+
|
|
525
|
+
const assistantId = nextId();
|
|
470
526
|
updateMessages((previous) => [
|
|
471
527
|
...previous,
|
|
472
|
-
{
|
|
528
|
+
{
|
|
529
|
+
id: assistantId,
|
|
530
|
+
role: 'assistant',
|
|
531
|
+
speaker: 'primary',
|
|
532
|
+
content: '',
|
|
533
|
+
timestamp: Date.now(),
|
|
534
|
+
isStreaming: true,
|
|
535
|
+
},
|
|
473
536
|
]);
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
const history: VoiceTurnMessage[] = messagesRef.current
|
|
477
|
-
.filter((message) => message.content.trim() !== '' && message.id !== draftId)
|
|
478
|
-
.map((message) => ({ role: message.role, content: message.content }))
|
|
479
|
-
// The turn itself is `text`, not history.
|
|
480
|
-
.slice(0, draftId === null ? -1 : undefined);
|
|
481
|
-
|
|
482
|
-
const assistantId = nextId();
|
|
483
|
-
updateMessages((previous) => [
|
|
484
|
-
...previous,
|
|
485
|
-
{ id: assistantId, role: 'assistant', speaker: 'primary', content: '', timestamp: Date.now(), isStreaming: true },
|
|
486
|
-
]);
|
|
487
|
-
|
|
488
|
-
const turn: ActiveTurn = {
|
|
489
|
-
controller: new AbortController(),
|
|
490
|
-
assistantId,
|
|
491
|
-
consumed: 0,
|
|
492
|
-
chunkCount: 0,
|
|
493
|
-
text: '',
|
|
494
|
-
speaking: '',
|
|
495
|
-
clips: [],
|
|
496
|
-
closed: false,
|
|
497
|
-
wake: null,
|
|
498
|
-
synthesisFailed: false,
|
|
499
|
-
};
|
|
500
|
-
turnRef.current = turn;
|
|
501
|
-
setAgent('thinking');
|
|
502
|
-
if (mountedRef.current) setTurnErrorCode(null);
|
|
503
537
|
|
|
504
|
-
|
|
505
|
-
|
|
538
|
+
const turn: ActiveTurn = {
|
|
539
|
+
controller: new AbortController(),
|
|
540
|
+
assistantId,
|
|
541
|
+
consumed: 0,
|
|
542
|
+
chunkCount: 0,
|
|
543
|
+
text: '',
|
|
544
|
+
speaking: '',
|
|
545
|
+
clips: [],
|
|
546
|
+
closed: false,
|
|
547
|
+
wake: null,
|
|
548
|
+
synthesisFailed: false,
|
|
549
|
+
};
|
|
550
|
+
turnRef.current = turn;
|
|
551
|
+
setAgent('thinking');
|
|
552
|
+
if (mountedRef.current) setTurnErrorCode(null);
|
|
553
|
+
|
|
554
|
+
// The microphone stays open through thinking and speaking, for barge-in.
|
|
555
|
+
startListeningRef.current();
|
|
506
556
|
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
557
|
+
const playing = playTurn(turn);
|
|
558
|
+
const signal = turn.controller.signal;
|
|
559
|
+
|
|
560
|
+
void (async () => {
|
|
561
|
+
let lastText = '';
|
|
562
|
+
try {
|
|
563
|
+
await configRef.current.sendTurn({
|
|
564
|
+
text,
|
|
565
|
+
history,
|
|
566
|
+
signal,
|
|
567
|
+
onText: (answerSoFar) => {
|
|
568
|
+
if (signal.aborted) return;
|
|
569
|
+
lastText = answerSoFar;
|
|
570
|
+
turn.text = stripTitleTagsPartial(answerSoFar, { trim: false });
|
|
571
|
+
patchMessage(assistantId, { content: turn.text.trim() });
|
|
572
|
+
queueChunks(turn, false);
|
|
573
|
+
},
|
|
574
|
+
});
|
|
575
|
+
} catch (caught: unknown) {
|
|
576
|
+
if (signal.aborted) return;
|
|
577
|
+
console.error('[useVoiceRoom] Turn failed:', caught);
|
|
578
|
+
fail('turn-failed', errorMessage(caught, VOICE_ERROR_MESSAGES['turn-failed']));
|
|
579
|
+
return;
|
|
580
|
+
}
|
|
526
581
|
if (signal.aborted) return;
|
|
527
|
-
console.error('[useVoiceRoom] Turn failed:', caught);
|
|
528
|
-
fail('turn-failed', errorMessage(caught, VOICE_ERROR_MESSAGES['turn-failed']));
|
|
529
|
-
return;
|
|
530
|
-
}
|
|
531
|
-
if (signal.aborted) return;
|
|
532
|
-
|
|
533
|
-
turn.text = stripTitleTags(lastText, { trim: false });
|
|
534
|
-
patchMessage(assistantId, { content: turn.text.trim(), isStreaming: false });
|
|
535
|
-
queueChunks(turn, true);
|
|
536
|
-
turn.closed = true;
|
|
537
|
-
turn.wake?.();
|
|
538
|
-
await playing;
|
|
539
|
-
if (signal.aborted || turnRef.current !== turn) return;
|
|
540
582
|
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
583
|
+
turn.text = stripTitleTags(lastText, { trim: false });
|
|
584
|
+
patchMessage(assistantId, { content: turn.text.trim(), isStreaming: false });
|
|
585
|
+
queueChunks(turn, true);
|
|
586
|
+
turn.closed = true;
|
|
587
|
+
turn.wake?.();
|
|
588
|
+
await playing;
|
|
589
|
+
if (signal.aborted || turnRef.current !== turn) return;
|
|
590
|
+
|
|
591
|
+
turnRef.current = null;
|
|
592
|
+
levels.setPlayback(0);
|
|
593
|
+
// The session that listened through the answer has heard the answer; a
|
|
594
|
+
// fresh one starts the next utterance clean.
|
|
595
|
+
sessionRef.current?.abort();
|
|
596
|
+
if (turn.text.trim() === '') {
|
|
597
|
+
updateMessages((previous) => previous.filter((message) => message.id !== assistantId));
|
|
598
|
+
if (mountedRef.current) setTurnErrorCode('no-answer');
|
|
599
|
+
} else if (turn.synthesisFailed) {
|
|
600
|
+
if (mountedRef.current) setTurnErrorCode('not-played');
|
|
601
|
+
}
|
|
602
|
+
setAgent('listening');
|
|
603
|
+
startListeningRef.current();
|
|
604
|
+
})();
|
|
605
|
+
},
|
|
606
|
+
[fail, levels, patchMessage, playTurn, queueChunks, setAgent, updateMessages],
|
|
607
|
+
);
|
|
556
608
|
runTurnRef.current = runTurn;
|
|
557
609
|
|
|
558
610
|
// ============== CONNECT ==============
|
|
@@ -564,12 +616,18 @@ export function useVoiceRoom(options: UseVoiceRoomOptions = {}) {
|
|
|
564
616
|
setRoomState('connecting');
|
|
565
617
|
|
|
566
618
|
if (!isSpeechRecognitionAvailable()) {
|
|
567
|
-
setCallFailure({
|
|
619
|
+
setCallFailure({
|
|
620
|
+
code: 'voice-unavailable',
|
|
621
|
+
message: VOICE_ERROR_MESSAGES['voice-unavailable'],
|
|
622
|
+
});
|
|
568
623
|
setRoomState('error');
|
|
569
624
|
return;
|
|
570
625
|
}
|
|
571
626
|
if (getToken() === null) {
|
|
572
|
-
setCallFailure({
|
|
627
|
+
setCallFailure({
|
|
628
|
+
code: 'not-authenticated',
|
|
629
|
+
message: VOICE_ERROR_MESSAGES['not-authenticated'],
|
|
630
|
+
});
|
|
573
631
|
setRoomState('error');
|
|
574
632
|
return;
|
|
575
633
|
}
|
package/src/index.ts
CHANGED
|
@@ -9,15 +9,27 @@ export { AliaChatScreen } from './components/AliaChatScreen';
|
|
|
9
9
|
export type { AliaChatScreenProps } from './components/AliaChatScreen';
|
|
10
10
|
|
|
11
11
|
// ── Chat UI components ──
|
|
12
|
+
export { DEFAULT_LABELS } from './lib/labels';
|
|
13
|
+
export type { AliaChatLabels } from './lib/labels';
|
|
14
|
+
export { VoiceModeIcon } from './components/VoiceModeIcon';
|
|
15
|
+
export { AmbientField, PARALLAX_DURATION, PARALLAX_EASE } from './components/AmbientField';
|
|
16
|
+
export type { AmbientFieldProps } from './components/AmbientField';
|
|
17
|
+
export { AliaAvatar } from './components/AliaAvatar';
|
|
18
|
+
export type { AliaAvatarProps, AliaAvatarState } from './components/AliaAvatar';
|
|
12
19
|
export { IdentityMark } from './components/IdentityMark';
|
|
13
20
|
export type { IdentityMarkProps, IdentityMarkState } from './components/IdentityMark';
|
|
14
|
-
export { ThinkingIndicator } from './components/ThinkingIndicator';
|
|
15
21
|
export { AliaMarkdown } from './components/Markdown';
|
|
16
22
|
export type { AliaMarkdownProps, RenderCodeBlock } from './components/Markdown';
|
|
17
|
-
export { Reasoning, ReasoningTrigger, ReasoningContent } from './components/Reasoning';
|
|
18
23
|
export { AliaWelcomeMessage } from './components/AliaWelcomeMessage';
|
|
19
|
-
export { ResearchProgressCard } from './components/ResearchProgressCard';
|
|
20
24
|
export { PlanPreviewCard } from './components/PlanPreviewCard';
|
|
25
|
+
export { QuestionCard } from './components/QuestionCard';
|
|
26
|
+
export {
|
|
27
|
+
ASK_USER_QUESTION_TOOL,
|
|
28
|
+
askedQuestions,
|
|
29
|
+
formatAnswers,
|
|
30
|
+
isQuestionInvocation,
|
|
31
|
+
} from './lib/questions';
|
|
32
|
+
export type { AskedOption, AskedQuestion, QuestionAnswer } from './lib/questions';
|
|
21
33
|
|
|
22
34
|
// ── Chat hook ──
|
|
23
35
|
export { useAliaChat } from './hooks/useAliaChat';
|
|
@@ -64,13 +76,21 @@ export type {
|
|
|
64
76
|
} from './types';
|
|
65
77
|
|
|
66
78
|
// ── UI Primitives (NativeWind) ──
|
|
67
|
-
export { Button, buttonVariants, buttonTextVariants } from './components/ui/button';
|
|
68
|
-
export type { ButtonProps } from './components/ui/button';
|
|
69
|
-
export { Text, TextClassContext } from './components/ui/text';
|
|
70
79
|
export { cn, formatFileSize } from './lib/utils';
|
|
71
80
|
export { getToolLabel, getToolActiveLabel, getResearchActiveLabel } from './lib/tool-registry';
|
|
72
81
|
export { getTextFromContent, getImagesFromContent } from './lib/content-utils';
|
|
73
82
|
|
|
83
|
+
// ── The turn's work log: tool calls as Bloom's `WebSearch` and `TaskList` ──
|
|
84
|
+
export { isWebInvocation, taskListLog, webSearchLog } from './lib/work-log';
|
|
85
|
+
export type { LiveResearch, TaskListLog, WebSearchLog, WorkLogOptions } from './lib/work-log';
|
|
86
|
+
export { englishWorkLog } from './lib/work-log-copy';
|
|
87
|
+
export type { Translate } from './lib/work-log-copy';
|
|
88
|
+
export { getToolDoneLabel, getToolPillLabel } from './lib/task-labels';
|
|
89
|
+
export { extractCitationSources } from './lib/citations';
|
|
90
|
+
export type { CitationSource } from './lib/citations';
|
|
91
|
+
export { toolCallStatus } from './lib/tool-call-status';
|
|
92
|
+
export type { ToolCallStatus } from './lib/tool-call-status';
|
|
93
|
+
|
|
74
94
|
// ── Picker hooks ──
|
|
75
95
|
export { useImagePicker } from './hooks/useImagePicker';
|
|
76
96
|
export type { ImagePickerAsset } from './hooks/useImagePicker';
|
package/src/lib/catalogue.ts
CHANGED
|
@@ -70,7 +70,9 @@ function asCount(value: unknown): number | null {
|
|
|
70
70
|
}
|
|
71
71
|
|
|
72
72
|
function asTexts(value: unknown): string[] {
|
|
73
|
-
return Array.isArray(value)
|
|
73
|
+
return Array.isArray(value)
|
|
74
|
+
? value.filter((item): item is string => typeof item === 'string')
|
|
75
|
+
: [];
|
|
74
76
|
}
|
|
75
77
|
|
|
76
78
|
function parseEntry(value: unknown): CatalogueEntry | null {
|
|
@@ -100,7 +102,8 @@ function parseEntry(value: unknown): CatalogueEntry | null {
|
|
|
100
102
|
reasoningEfforts: REASONING_EFFORTS.filter((effort) =>
|
|
101
103
|
asTexts(raw.reasoningEfforts).includes(effort),
|
|
102
104
|
),
|
|
103
|
-
pricing:
|
|
105
|
+
pricing:
|
|
106
|
+
input !== null && output !== null ? { inputPerMTok: input, outputPerMTok: output } : null,
|
|
104
107
|
releasedAt: asText(raw.releasedAt),
|
|
105
108
|
featured: raw.featured === true,
|
|
106
109
|
};
|
|
@@ -159,7 +162,12 @@ export function resolveSelection(
|
|
|
159
162
|
catalogue?.entries.find((entry) => entry.id === catalogue.defaultModelId) ?? null;
|
|
160
163
|
|
|
161
164
|
if (requested === undefined) {
|
|
162
|
-
return {
|
|
165
|
+
return {
|
|
166
|
+
requestedId: undefined,
|
|
167
|
+
effectiveId: undefined,
|
|
168
|
+
entry: defaultEntry(),
|
|
169
|
+
source: 'default',
|
|
170
|
+
};
|
|
163
171
|
}
|
|
164
172
|
if (catalogue === undefined) {
|
|
165
173
|
return { requestedId: requested, effectiveId: requested, entry: null, source: 'requested' };
|
|
@@ -168,7 +176,12 @@ export function resolveSelection(
|
|
|
168
176
|
if (entry !== undefined) {
|
|
169
177
|
return { requestedId: requested, effectiveId: requested, entry, source: 'requested' };
|
|
170
178
|
}
|
|
171
|
-
return {
|
|
179
|
+
return {
|
|
180
|
+
requestedId: requested,
|
|
181
|
+
effectiveId: undefined,
|
|
182
|
+
entry: defaultEntry(),
|
|
183
|
+
source: 'replaced',
|
|
184
|
+
};
|
|
172
185
|
}
|
|
173
186
|
|
|
174
187
|
/**
|
package/src/lib/chat-stream.ts
CHANGED
|
@@ -86,10 +86,14 @@ export function readAliaChatFailure(body: unknown): AliaChatFailure {
|
|
|
86
86
|
return {
|
|
87
87
|
...(typeof detail.code === 'string' && detail.code !== '' ? { code: detail.code } : {}),
|
|
88
88
|
...(typeof detail.retryable === 'boolean' ? { retryable: detail.retryable } : {}),
|
|
89
|
-
...(typeof detail.retryAfter === 'number' &&
|
|
89
|
+
...(typeof detail.retryAfter === 'number' &&
|
|
90
|
+
Number.isFinite(detail.retryAfter) &&
|
|
91
|
+
detail.retryAfter >= 0
|
|
90
92
|
? { retryAfter: detail.retryAfter }
|
|
91
93
|
: {}),
|
|
92
|
-
...(typeof detail.reference === 'string' && detail.reference !== ''
|
|
94
|
+
...(typeof detail.reference === 'string' && detail.reference !== ''
|
|
95
|
+
? { reference: detail.reference }
|
|
96
|
+
: {}),
|
|
93
97
|
};
|
|
94
98
|
}
|
|
95
99
|
|
|
@@ -159,7 +163,11 @@ function parsePlanSteps(value: unknown): PlanStep[] {
|
|
|
159
163
|
|
|
160
164
|
return value.map((entry, index) => {
|
|
161
165
|
const step = asObject(entry, `alia.plan_preview steps[${index}]`);
|
|
162
|
-
assertOnlyKeys(
|
|
166
|
+
assertOnlyKeys(
|
|
167
|
+
step,
|
|
168
|
+
['action', 'description', 'toolName'],
|
|
169
|
+
`alia.plan_preview steps[${index}]`,
|
|
170
|
+
);
|
|
163
171
|
return {
|
|
164
172
|
action: asString(step.action, `alia.plan_preview steps[${index}].action`),
|
|
165
173
|
description: asString(step.description, `alia.plan_preview steps[${index}].description`),
|
|
@@ -276,7 +284,8 @@ function validateIgnoredNamedEvent(eventName: string, payload: JsonObject): void
|
|
|
276
284
|
const scopes = new Set(['same_model_revision', 'same_model', 'cross_model']);
|
|
277
285
|
const reason = asString(payload.reason, `${eventName} reason`);
|
|
278
286
|
const scope = asString(payload.scope, `${eventName} scope`);
|
|
279
|
-
if (!reasons.has(reason) || !scopes.has(scope))
|
|
287
|
+
if (!reasons.has(reason) || !scopes.has(scope))
|
|
288
|
+
fail(`${eventName} has an unsupported route switch.`);
|
|
280
289
|
asString(payload.occurredAt, `${eventName} occurredAt`);
|
|
281
290
|
return;
|
|
282
291
|
}
|
|
@@ -378,7 +387,10 @@ function parseOpenAIFrame(payload: unknown): ParsedOpenAIFrame {
|
|
|
378
387
|
asString(error.message, 'OpenAI stream error message');
|
|
379
388
|
// The server's prose is not carried: the code and retryability are what a
|
|
380
389
|
// caller acts on, and the caller words it in its own language.
|
|
381
|
-
throw new AliaChatStreamError(
|
|
390
|
+
throw new AliaChatStreamError(
|
|
391
|
+
'Alia ended the stream with an error.',
|
|
392
|
+
readAliaChatFailure(body),
|
|
393
|
+
);
|
|
382
394
|
}
|
|
383
395
|
assertOnlyKeys(
|
|
384
396
|
body,
|
|
@@ -430,10 +442,16 @@ function parseOpenAIFrame(payload: unknown): ParsedOpenAIFrame {
|
|
|
430
442
|
fail(`choices[${index}].delta.role must be assistant.`);
|
|
431
443
|
}
|
|
432
444
|
if (delta.content !== undefined) {
|
|
433
|
-
events.push({
|
|
445
|
+
events.push({
|
|
446
|
+
kind: 'content',
|
|
447
|
+
content: asString(delta.content, `choices[${index}].delta.content`, true),
|
|
448
|
+
});
|
|
434
449
|
}
|
|
435
450
|
if (delta.reasoning !== undefined) {
|
|
436
|
-
events.push({
|
|
451
|
+
events.push({
|
|
452
|
+
kind: 'reasoning',
|
|
453
|
+
content: asString(delta.reasoning, `choices[${index}].delta.reasoning`),
|
|
454
|
+
});
|
|
437
455
|
}
|
|
438
456
|
if (delta.tool_calls !== undefined) events.push(...parseToolCalls(delta.tool_calls));
|
|
439
457
|
|