@alia.onl/sdk 15.7.0 → 19.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 +255 -12
- package/README.md +241 -155
- package/package.json +17 -5
- package/src/animations.ts +0 -1
- package/src/components/AliaAskProvider.tsx +127 -0
- package/src/components/AliaChatContent.tsx +604 -392
- package/src/components/AliaChatHeader.tsx +14 -38
- package/src/components/AliaChatScreen.tsx +56 -31
- package/src/components/AliaChatSheet.tsx +110 -36
- package/src/components/AliaComposer.tsx +1 -1
- package/src/components/AliaParticipants.tsx +202 -0
- package/src/components/AliaParticipantsHeader.tsx +60 -0
- package/src/components/AliaSharedConversation.tsx +86 -0
- package/src/components/AliaWelcomeMessage.tsx +18 -5
- package/src/components/Image.tsx +12 -0
- package/src/components/Markdown.tsx +498 -14
- package/src/components/PlanPreviewCard.tsx +7 -49
- package/src/components/ReplyMarkdown.tsx +359 -0
- package/src/components/RichBlocks.tsx +195 -0
- package/src/components/canvas/CanvasComponent.tsx +59 -0
- package/src/components/canvas/ChartRenderer.tsx +73 -0
- package/src/components/canvas/CodeRenderer.tsx +24 -0
- package/src/components/canvas/FormRenderer.tsx +101 -0
- package/src/components/canvas/MarkdownRenderer.tsx +14 -0
- package/src/components/canvas/TableRenderer.tsx +47 -0
- package/src/components/thought/FilesAndSources.tsx +120 -0
- package/src/components/thought/ThoughtPanel.tsx +417 -0
- package/src/components/thought/ToolStep.tsx +141 -0
- package/src/components/thread/AgentResultCard.tsx +125 -0
- package/src/components/thread/AgentTaskCard.tsx +127 -0
- package/src/components/thread/AliaThread.tsx +401 -0
- package/src/components/thread/MessageBlockBoundary.tsx +78 -0
- package/src/components/thread/NewConversationOffer.tsx +54 -0
- package/src/components/thread/ThreadRow.tsx +458 -0
- package/src/components/thread/ThreadSearch.tsx +129 -0
- package/src/components/thread/ToolResultCard.tsx +254 -0
- package/src/components/thread/TurnMenu.tsx +54 -0
- package/src/components/thread/TurnStatusLine.tsx +34 -0
- package/src/components/voice/AliaVoiceCall.tsx +21 -0
- package/src/components/voice/VoiceControls.tsx +2 -28
- package/src/components/voice/VoiceSession.tsx +39 -11
- package/src/csp.ts +50 -0
- package/src/hooks/useAliaActivity.ts +3 -3
- package/src/hooks/useAtBottom.ts +51 -0
- package/src/hooks/useAttachMenu.ts +94 -0
- package/src/hooks/useChatComposer.ts +299 -0
- package/src/hooks/useChatVoice.ts +129 -0
- package/src/hooks/useDictation.ts +2 -3
- package/src/hooks/useDocumentPicker.ts +3 -29
- package/src/hooks/useFailedTurnToast.ts +43 -0
- package/src/hooks/useMessageVote.ts +50 -0
- package/src/hooks/useOpenThought.ts +79 -0
- package/src/hooks/usePendingQuestions.ts +2 -2
- package/src/hooks/useReadAloud.ts +92 -0
- package/src/hooks/useShareHandler.ts +24 -0
- package/src/hooks/useSoundEffects.ts +1 -1
- package/src/hooks/useTTS.ts +2 -3
- package/src/hooks/useThoughtScope.ts +59 -0
- package/src/hooks/useThreadRows.ts +145 -0
- package/src/hooks/useThreadView.ts +68 -0
- package/src/hooks/useTimelineWindow.ts +256 -0
- package/src/hooks/useVoiceLauncher.ts +180 -0
- package/src/hooks/useVoiceRoom.ts +2 -6
- package/src/index.ts +43 -29
- package/src/lib/activity-store.ts +1 -1
- package/src/lib/add-menu.ts +7 -0
- package/src/lib/call-thread.ts +33 -0
- package/src/lib/character-runtime.ts +6 -4
- package/src/lib/citations.ts +79 -2
- package/src/lib/clipboard.ts +23 -0
- package/src/lib/content-utils.ts +10 -0
- package/src/lib/copy-en.ts +284 -0
- package/src/lib/copy.ts +32 -0
- package/src/lib/document-picker.ts +31 -0
- package/src/lib/flights.ts +22 -13
- package/src/lib/focus-return.ts +66 -0
- package/src/lib/keyboard.native.tsx +19 -0
- package/src/lib/keyboard.tsx +9 -0
- package/src/lib/labels.tsx +69 -17
- package/src/lib/origins.ts +15 -0
- package/src/lib/participants.ts +86 -0
- package/src/lib/power-level-options.ts +66 -0
- package/src/lib/questions.ts +2 -2
- package/src/lib/task-labels.ts +4 -4
- package/src/lib/tool-call-status.ts +1 -1
- package/src/lib/tool-icons.ts +36 -0
- package/src/lib/tool-renderers.tsx +48 -0
- package/src/lib/turn-state.ts +2 -2
- package/src/lib/voice-audio-engine.ts +14 -158
- package/src/lib/voice-call-request.ts +49 -0
- package/src/lib/voice-call.ts +76 -34
- package/src/lib/voice-controls-labels.ts +31 -0
- package/src/lib/voice-protocol.ts +32 -14
- package/src/lib/voice-worklets.ts +19 -0
- package/src/lib/work-log.ts +6 -6
- package/src/runtime/agents.ts +151 -0
- package/src/runtime/api.ts +123 -0
- package/src/runtime/attachment-utils.ts +96 -0
- package/src/runtime/composer-draft-store.ts +270 -0
- package/src/runtime/config.tsx +186 -0
- package/src/runtime/conversation-store.ts +49 -0
- package/src/runtime/conversations.ts +557 -0
- package/src/runtime/error-utils.ts +104 -0
- package/src/runtime/host-tools.ts +140 -0
- package/src/runtime/index.ts +113 -0
- package/src/runtime/message-history.ts +124 -0
- package/src/runtime/model.ts +39 -0
- package/src/runtime/oxy-files.ts +37 -0
- package/src/runtime/power-levels.ts +66 -0
- package/src/runtime/query-keys.ts +34 -0
- package/src/runtime/random-uuid.ts +25 -0
- package/src/runtime/routes.ts +51 -0
- package/src/runtime/sharing.ts +165 -0
- package/src/runtime/sse-frame-reader.ts +88 -0
- package/src/runtime/stream-events.ts +146 -0
- package/src/runtime/stream-outcome.ts +21 -0
- package/src/runtime/thought-store.ts +103 -0
- package/src/runtime/thread/agent-activity.ts +261 -0
- package/src/runtime/thread/automation-labels.ts +79 -0
- package/src/runtime/thread/conversation-export.ts +135 -0
- package/src/runtime/thread/message-days.ts +120 -0
- package/src/runtime/thread/message-processor.ts +93 -0
- package/src/runtime/thread/task-utils.ts +11 -0
- package/src/runtime/thread/thought-utils.ts +752 -0
- package/src/runtime/thread/thread-history.ts +139 -0
- package/src/runtime/thread/timeline.ts +202 -0
- package/src/runtime/thread/title-tags.ts +8 -0
- package/src/runtime/thread/tool-cards.ts +383 -0
- package/src/runtime/turn-failure.ts +108 -0
- package/src/runtime/types.ts +148 -0
- package/src/runtime/usage-limit-error.ts +44 -0
- package/src/runtime/use-agent-activity.ts +80 -0
- package/src/runtime/use-conversation-stream.ts +1427 -0
- package/src/runtime/use-conversation-thread.ts +556 -0
- package/src/runtime/use-start-conversation.ts +107 -0
- package/src/runtime/use-thread-history.ts +64 -0
- package/src/runtime/use-thread-search.ts +95 -0
- package/src/runtime/use-turn-edit.ts +99 -0
- package/src/tools.ts +2 -2
- package/src/types.ts +18 -47
- package/worklets/voice-processors.js +152 -0
- package/src/components/AliaChatMessageList.tsx +0 -532
- package/src/components/ThinkingBlock.tsx +0 -43
- package/src/hooks/useAliaChat.ts +0 -573
- package/src/lib/api-url.ts +0 -2
- package/src/lib/catalogue.ts +0 -236
- package/src/lib/chat-stream.ts +0 -655
- package/src/lib/chat-transport.ts +0 -98
- package/src/lib/delegation.ts +0 -34
- package/src/lib/work-log-copy.ts +0 -61
package/src/lib/labels.tsx
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { createContext, type ReactNode, useContext, useMemo } from 'react';
|
|
2
2
|
import { VOICE_ERROR_MESSAGES, type VoiceErrorCode } from './speech-messages';
|
|
3
|
-
import {
|
|
3
|
+
import { englishCopy, type Translate } from './copy';
|
|
4
|
+
import { VOICE_CONTROLS_LABELS, type VoiceControlsLabels } from './voice-controls-labels';
|
|
5
|
+
import type { VoiceCloseReason } from './voice-protocol';
|
|
6
|
+
|
|
7
|
+
/** Why a call could not be opened; the host's `canStartVoice` answers with one. */
|
|
8
|
+
export type VoiceBlockReason = 'sign-in' | 'plan-required' | 'credits-exhausted';
|
|
4
9
|
|
|
5
10
|
/** Every string the chat UI draws itself. Bloom's own copy follows `BloomProvider locale`. */
|
|
6
11
|
export interface AliaChatLabels {
|
|
@@ -11,11 +16,21 @@ export interface AliaChatLabels {
|
|
|
11
16
|
more: string;
|
|
12
17
|
chatActions: string;
|
|
13
18
|
// Composer
|
|
14
|
-
|
|
19
|
+
/** The empty composer's prompt, naming the conversation's lead ("Ask Sindi..."). */
|
|
20
|
+
placeholder: (leadName: string) => string;
|
|
15
21
|
addMenuGroup: string;
|
|
16
22
|
addPhotos: string;
|
|
23
|
+
/** The add menu's row for documents, where the lead takes them. */
|
|
24
|
+
addDocuments: string;
|
|
17
25
|
startVoice: string;
|
|
18
|
-
|
|
26
|
+
/** The call bar's words (status, mute, end). */
|
|
27
|
+
voiceControls: VoiceControlsLabels;
|
|
28
|
+
/** Why a call ended without failing; `null` for a hang-up (the person knows). */
|
|
29
|
+
voiceEnded: (reason: VoiceCloseReason) => string | null;
|
|
30
|
+
/** Said once, and shown in the bar, when the call is about to reach its time limit. */
|
|
31
|
+
voiceEndingSoon: string;
|
|
32
|
+
/** Why a call was not opened, told instead of a button that silently does nothing. */
|
|
33
|
+
voiceBlocked: (reason: VoiceBlockReason) => string;
|
|
19
34
|
/** The field's accessible name. */
|
|
20
35
|
composerMessage: string;
|
|
21
36
|
/** The add button's accessible name. */
|
|
@@ -61,30 +76,61 @@ export interface AliaChatLabels {
|
|
|
61
76
|
stopReading: string;
|
|
62
77
|
// Plan
|
|
63
78
|
planTitle: string;
|
|
64
|
-
planApprove: string;
|
|
65
|
-
planCancel: string;
|
|
66
|
-
planApproved: string;
|
|
67
|
-
planCancelled: string;
|
|
68
79
|
// Reasoning
|
|
69
80
|
reasoning: string;
|
|
70
81
|
thoughtFor: (seconds: number) => string;
|
|
71
|
-
// The
|
|
72
|
-
/** The words
|
|
73
|
-
|
|
82
|
+
// The words that are not one fixed field
|
|
83
|
+
/** The words that are not one fixed field (the thread, its cards, the work log), keyed as the Alia app keys them (see `copy.ts`). */
|
|
84
|
+
t: Translate;
|
|
85
|
+
/** The language `t` speaks, as a BCP 47 tag: dates, days and prices in the thread are formatted in it. */
|
|
86
|
+
locale: string;
|
|
87
|
+
// Conversations (the runtime's own words: toasts and the errors it raises)
|
|
88
|
+
/** A conversation's title until it has one. */
|
|
89
|
+
newChat: string;
|
|
90
|
+
/** A turn could not be sent and the composer got its text back. */
|
|
91
|
+
sendFailed: string;
|
|
92
|
+
/** The session ended mid-request. */
|
|
93
|
+
signInRequired: string;
|
|
94
|
+
conversationSaveFailed: string;
|
|
95
|
+
conversationDeleteFailed: string;
|
|
96
|
+
conversationCreateFailed: string;
|
|
97
|
+
conversationClearFailed: string;
|
|
98
|
+
/** An attachment of a kind the chat API does not carry was left out of the turn. */
|
|
99
|
+
attachmentNotSent: (name: string) => string;
|
|
100
|
+
/** A photo whose bytes could not be read was left out of the turn. */
|
|
101
|
+
attachmentUnreadable: (name: string) => string;
|
|
74
102
|
// Research
|
|
75
103
|
}
|
|
76
104
|
|
|
105
|
+
const ENDED_ENGLISH: Partial<Record<VoiceCloseReason, string>> = {
|
|
106
|
+
'call-moved-elsewhere': 'The call continues on your other device',
|
|
107
|
+
'max-duration': 'The call reached its time limit',
|
|
108
|
+
'idle-timeout': 'The call ended after two minutes of silence',
|
|
109
|
+
'credits-exhausted': 'The call ended: you ran out of credits',
|
|
110
|
+
'server-shutdown': 'The call ended while Alia was updating — start a new one',
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const BLOCKED_ENGLISH: Record<VoiceBlockReason, string> = {
|
|
114
|
+
'sign-in': 'Sign in to use voice',
|
|
115
|
+
'plan-required': 'Voice mode needs a paid plan',
|
|
116
|
+
'credits-exhausted': "You've run out of credits",
|
|
117
|
+
};
|
|
118
|
+
|
|
77
119
|
export const DEFAULT_LABELS: AliaChatLabels = {
|
|
78
120
|
clear: 'Clear',
|
|
79
121
|
back: 'Back',
|
|
80
122
|
share: 'Share',
|
|
81
123
|
more: 'More',
|
|
82
124
|
chatActions: 'Chat actions',
|
|
83
|
-
placeholder:
|
|
125
|
+
placeholder: (leadName) => `Ask ${leadName}...`,
|
|
84
126
|
addMenuGroup: 'Add',
|
|
85
127
|
addPhotos: 'Photos',
|
|
128
|
+
addDocuments: 'Files',
|
|
86
129
|
startVoice: 'Start voice',
|
|
87
|
-
|
|
130
|
+
voiceControls: VOICE_CONTROLS_LABELS,
|
|
131
|
+
voiceEnded: (reason) => ENDED_ENGLISH[reason] ?? null,
|
|
132
|
+
voiceEndingSoon: 'One minute left in this call',
|
|
133
|
+
voiceBlocked: (reason) => BLOCKED_ENGLISH[reason],
|
|
88
134
|
composerMessage: 'Message',
|
|
89
135
|
composerAddMenu: 'Add to chat',
|
|
90
136
|
composerModes: 'Power level',
|
|
@@ -114,13 +160,19 @@ export const DEFAULT_LABELS: AliaChatLabels = {
|
|
|
114
160
|
readAloud: 'Read aloud',
|
|
115
161
|
stopReading: 'Stop reading',
|
|
116
162
|
planTitle: "Here's my plan",
|
|
117
|
-
planApprove: 'Approve',
|
|
118
|
-
planCancel: 'Cancel',
|
|
119
|
-
planApproved: 'Plan approved',
|
|
120
|
-
planCancelled: 'Plan cancelled',
|
|
121
163
|
reasoning: 'Reasoning',
|
|
122
164
|
thoughtFor: (seconds) => `Thought for ${seconds} seconds`,
|
|
123
|
-
|
|
165
|
+
t: englishCopy,
|
|
166
|
+
locale: 'en',
|
|
167
|
+
newChat: 'New chat',
|
|
168
|
+
sendFailed: "Your message couldn't be sent",
|
|
169
|
+
signInRequired: 'Sign in to keep chatting',
|
|
170
|
+
conversationSaveFailed: 'Failed to save conversation',
|
|
171
|
+
conversationDeleteFailed: 'Failed to delete conversation',
|
|
172
|
+
conversationCreateFailed: 'Failed to create conversation',
|
|
173
|
+
conversationClearFailed: "The conversation couldn't be cleared",
|
|
174
|
+
attachmentNotSent: (name) => `${name} wasn't sent — only photos can be sent`,
|
|
175
|
+
attachmentUnreadable: (name) => `${name} couldn't be read and wasn't sent`,
|
|
124
176
|
};
|
|
125
177
|
|
|
126
178
|
const LabelsContext = createContext<AliaChatLabels>(DEFAULT_LABELS);
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The two origins the SDK talks to, defined once. Every URL it builds starts from one of them,
|
|
3
|
+
* and `@alia.onl/sdk/csp` (`src/csp.ts`) derives the host's CSP from these same values, so an
|
|
4
|
+
* origin the SDK uses is an origin it declares.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/** Alia's API: `EXPO_PUBLIC_ALIA_API_URL`, then production. Chat, dictation, speech, voice. */
|
|
8
|
+
export const ALIA_API_URL = process.env.EXPO_PUBLIC_ALIA_API_URL ?? 'https://api.alia.onl';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Alia's web app, which serves the files the SDK shares with every host: Bloom's character
|
|
12
|
+
* runtime (`/bloom-character/`) and the voice call's worklets (`/sdk/voice-worklets/`), both
|
|
13
|
+
* with CORS open (`packages/app/worker/index.js`).
|
|
14
|
+
*/
|
|
15
|
+
export const ALIA_WEB_ORIGIN = 'https://alia.onl';
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import type { AvatarCharacter } from '@oxy.so/bloom/agent-avatar';
|
|
2
|
+
import type { AliaAgentProfile } from '../runtime/agents';
|
|
3
|
+
import type { Message } from '../runtime/types';
|
|
4
|
+
import { readAgentCharacter } from './agent-character';
|
|
5
|
+
import { ALIA_AVATAR } from './alia-character';
|
|
6
|
+
|
|
7
|
+
/** Somebody in the conversation who answers: its lead, or an agent the lead consulted. */
|
|
8
|
+
export interface AliaParticipant {
|
|
9
|
+
id: string;
|
|
10
|
+
name: string;
|
|
11
|
+
handle?: string;
|
|
12
|
+
/** The 3D character it is drawn as. */
|
|
13
|
+
character: AvatarCharacter;
|
|
14
|
+
color?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** The conversation's lead: who greets, whose name is on the composer, who the header shows first. */
|
|
18
|
+
export interface AliaLead extends AliaParticipant {
|
|
19
|
+
/** Alia herself, rather than an agent the chat was opened on. */
|
|
20
|
+
isAlia: boolean;
|
|
21
|
+
/** The agent's own greeting; Alia greets with the chat's labels. */
|
|
22
|
+
greeting?: string;
|
|
23
|
+
/** The agent's own starters, offered over the composer of an empty chat. */
|
|
24
|
+
starters: Array<{ title: string; prompt: string }>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Who is in the conversation, as the header draws it: the lead, then whoever has joined. */
|
|
28
|
+
export interface AliaChatParticipants {
|
|
29
|
+
lead: AliaLead;
|
|
30
|
+
/** The consulted agents that have joined the header, in the order they joined. */
|
|
31
|
+
joined: AliaParticipant[];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Alia as a lead. */
|
|
35
|
+
export const ALIA_LEAD: AliaLead = {
|
|
36
|
+
id: 'alia',
|
|
37
|
+
name: 'Alia',
|
|
38
|
+
handle: 'alia',
|
|
39
|
+
character: ALIA_AVATAR,
|
|
40
|
+
isAlia: true,
|
|
41
|
+
starters: [],
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
/** The lead of a chat: the agent it was opened on (`agent={{ id }}`), or Alia. */
|
|
45
|
+
export function leadOf(profile: AliaAgentProfile | null | undefined): AliaLead {
|
|
46
|
+
if (!profile) return ALIA_LEAD;
|
|
47
|
+
return {
|
|
48
|
+
id: profile.id,
|
|
49
|
+
name: profile.name,
|
|
50
|
+
handle: profile.handle,
|
|
51
|
+
character: readAgentCharacter(profile.character),
|
|
52
|
+
color: profile.color,
|
|
53
|
+
isAlia: false,
|
|
54
|
+
greeting: profile.greeting,
|
|
55
|
+
starters: profile.starters,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** A consulted agent as a message carries it (`agentInfo`). */
|
|
60
|
+
export function participantOf(agentInfo: NonNullable<Message['agentInfo']>): AliaParticipant {
|
|
61
|
+
return {
|
|
62
|
+
id: agentInfo.id,
|
|
63
|
+
name: agentInfo.name,
|
|
64
|
+
handle: agentInfo.handle,
|
|
65
|
+
character: readAgentCharacter(agentInfo.character),
|
|
66
|
+
color: agentInfo.color,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* The agents that answered in `messages`, besides the lead, in the order they first did — each
|
|
72
|
+
* with whether its first answer is the one being streamed now (it is joining) or one already
|
|
73
|
+
* written (it was there when the chat opened).
|
|
74
|
+
*/
|
|
75
|
+
export function consultedAgents(
|
|
76
|
+
messages: readonly Message[],
|
|
77
|
+
leadId: string,
|
|
78
|
+
): Array<{ participant: AliaParticipant; live: boolean }> {
|
|
79
|
+
const seen = new Map<string, { participant: AliaParticipant; live: boolean }>();
|
|
80
|
+
for (const message of messages) {
|
|
81
|
+
const info = message.agentInfo;
|
|
82
|
+
if (!info || info.id === leadId || seen.has(info.id)) continue;
|
|
83
|
+
seen.set(info.id, { participant: participantOf(info), live: message.isStreaming === true });
|
|
84
|
+
}
|
|
85
|
+
return [...seen.values()];
|
|
86
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { ComposerPanelPermissionOption } from '@oxy.so/bloom/composer-panel';
|
|
2
|
+
import { RiComputerLine } from '@oxy.so/bloom/icons/RiComputerLine';
|
|
3
|
+
import { RiFireLine } from '@oxy.so/bloom/icons/RiFireLine';
|
|
4
|
+
import { RiFlashlightLine } from '@oxy.so/bloom/icons/RiFlashlightLine';
|
|
5
|
+
import { RiLightbulbLine } from '@oxy.so/bloom/icons/RiLightbulbLine';
|
|
6
|
+
import { RiRocket2Line } from '@oxy.so/bloom/icons/RiRocket2Line';
|
|
7
|
+
import { RiSparkling2Line } from '@oxy.so/bloom/icons/RiSparkling2Line';
|
|
8
|
+
import { RiSpeedUpLine } from '@oxy.so/bloom/icons/RiSpeedUpLine';
|
|
9
|
+
import { RiVipCrownLine } from '@oxy.so/bloom/icons/RiVipCrownLine';
|
|
10
|
+
import { POWER_LEVELS, type PowerLevel } from '../runtime/power-levels';
|
|
11
|
+
import type { Translate } from './copy';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* The power-level selector, in the shape Bloom's composer mode selector takes.
|
|
15
|
+
*
|
|
16
|
+
* Alia's users choose how much power a turn gets, never which hosted model answers it: Oxy picks
|
|
17
|
+
* the model of the chosen level for every request. The selector is `ComposerPill`'s mode picker —
|
|
18
|
+
* Bloom's own radio menu of a label, a short description and an icon per mode — so the seven
|
|
19
|
+
* levels read the same way every Bloom mode selector does. Models running on the person's OWN
|
|
20
|
+
* devices follow the levels: that is their machine, not a model Alia offers, and which devices
|
|
21
|
+
* are connected is the host's to say.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
type IconComponent = ComposerPanelPermissionOption['icon'];
|
|
25
|
+
|
|
26
|
+
/** One glyph per level, rising with the power it stands for. */
|
|
27
|
+
export const POWER_LEVEL_ICONS: Record<PowerLevel, IconComponent> = {
|
|
28
|
+
auto: RiSparkling2Line,
|
|
29
|
+
instant: RiFlashlightLine,
|
|
30
|
+
medium: RiSpeedUpLine,
|
|
31
|
+
high: RiLightbulbLine,
|
|
32
|
+
xhigh: RiFireLine,
|
|
33
|
+
pro: RiRocket2Line,
|
|
34
|
+
ultra: RiVipCrownLine,
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/** A model running on one of the person's own devices, offered after the levels. */
|
|
38
|
+
export interface DeviceModel {
|
|
39
|
+
/** What a request carries as `model` (`local/<runtime>/<model>`). */
|
|
40
|
+
id: string;
|
|
41
|
+
/** The tag the person knows it by, e.g. `llama3.1:8b`. */
|
|
42
|
+
name: string;
|
|
43
|
+
/** Which of their devices is running it. */
|
|
44
|
+
deviceLabel: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** The selector's options: the seven levels, then the person's device models. */
|
|
48
|
+
export function buildPowerLevelOptions(
|
|
49
|
+
t: Translate,
|
|
50
|
+
deviceModels: readonly DeviceModel[],
|
|
51
|
+
): ComposerPanelPermissionOption[] {
|
|
52
|
+
return [
|
|
53
|
+
...POWER_LEVELS.map((level) => ({
|
|
54
|
+
id: level,
|
|
55
|
+
label: t(`powerLevels.${level}.label`),
|
|
56
|
+
description: t(`powerLevels.${level}.description`),
|
|
57
|
+
icon: POWER_LEVEL_ICONS[level],
|
|
58
|
+
})),
|
|
59
|
+
...deviceModels.map((model) => ({
|
|
60
|
+
id: model.id,
|
|
61
|
+
label: model.name,
|
|
62
|
+
description: t('powerLevels.onDevice', { device: model.deviceLabel }),
|
|
63
|
+
icon: RiComputerLine,
|
|
64
|
+
})),
|
|
65
|
+
];
|
|
66
|
+
}
|
package/src/lib/questions.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// call: it is stored with the message, survives a reload, and is answered by the person's
|
|
5
5
|
// next message. Pure functions, no React Native, so a host's models and tests can use them.
|
|
6
6
|
|
|
7
|
-
import type {
|
|
7
|
+
import type { Message, ToolInvocation } from '../runtime/types';
|
|
8
8
|
|
|
9
9
|
/** The tool's name on the wire. */
|
|
10
10
|
export const ASK_USER_QUESTION_TOOL = 'askUserQuestion';
|
|
@@ -83,7 +83,7 @@ export function formatAnswers(questions: AskedQuestion[], answers: QuestionAnswe
|
|
|
83
83
|
* is the answer, so nothing is pending then.
|
|
84
84
|
*/
|
|
85
85
|
export function pendingQuestions(
|
|
86
|
-
messages: readonly Pick<
|
|
86
|
+
messages: readonly Pick<Message, 'role' | 'toolInvocations'>[],
|
|
87
87
|
isStreaming: boolean,
|
|
88
88
|
): AskedQuestion[] {
|
|
89
89
|
const last = messages.at(-1);
|
package/src/lib/task-labels.ts
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { englishCopy, type Translate } from './copy';
|
|
2
2
|
|
|
3
3
|
/** Whether the catalogue names this tool itself; otherwise the label is made from its id. */
|
|
4
4
|
function isNamed(kind: 'pill' | 'done', toolName: string): boolean {
|
|
5
5
|
const key = `tasks.tool.${kind}.${toolName}`;
|
|
6
|
-
return
|
|
6
|
+
return englishCopy(key) !== key;
|
|
7
7
|
}
|
|
8
8
|
|
|
9
9
|
/** A tool's short label, in the reader's language when a translator is given. */
|
|
10
|
-
export function getToolPillLabel(toolName: string, t: Translate =
|
|
10
|
+
export function getToolPillLabel(toolName: string, t: Translate = englishCopy): string {
|
|
11
11
|
if (isNamed('pill', toolName)) return t(`tasks.tool.pill.${toolName}`);
|
|
12
12
|
|
|
13
13
|
// Oxy service tools: oxy_serviceName__toolName → "ServiceName"
|
|
@@ -28,7 +28,7 @@ export function getToolPillLabel(toolName: string, t: Translate = englishWorkLog
|
|
|
28
28
|
* `runningTitle` for this once the task's steps land). Labels that are already names
|
|
29
29
|
* ("Telegram", "Email") read the same either way, so they fall back to the running label.
|
|
30
30
|
*/
|
|
31
|
-
export function getToolDoneLabel(toolName: string, t: Translate =
|
|
31
|
+
export function getToolDoneLabel(toolName: string, t: Translate = englishCopy): string {
|
|
32
32
|
return isNamed('done', toolName)
|
|
33
33
|
? t(`tasks.tool.done.${toolName}`)
|
|
34
34
|
: getToolPillLabel(toolName, t);
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { BloomIconComponent } from '@oxy.so/bloom/icons';
|
|
2
|
+
import { RiCalendarLine } from '@oxy.so/bloom/icons/RiCalendarLine';
|
|
3
|
+
import { RiChat3Line } from '@oxy.so/bloom/icons/RiChat3Line';
|
|
4
|
+
import { RiDatabase2Line } from '@oxy.so/bloom/icons/RiDatabase2Line';
|
|
5
|
+
import { RiFileTextLine } from '@oxy.so/bloom/icons/RiFileTextLine';
|
|
6
|
+
import { RiGlobalLine } from '@oxy.so/bloom/icons/RiGlobalLine';
|
|
7
|
+
import { RiLightbulbFlashLine } from '@oxy.so/bloom/icons/RiLightbulbFlashLine';
|
|
8
|
+
import { RiLink } from '@oxy.so/bloom/icons/RiLink';
|
|
9
|
+
import { RiSearchLine } from '@oxy.so/bloom/icons/RiSearchLine';
|
|
10
|
+
import { RiSendPlaneLine } from '@oxy.so/bloom/icons/RiSendPlaneLine';
|
|
11
|
+
import { RiSettings3Line } from '@oxy.so/bloom/icons/RiSettings3Line';
|
|
12
|
+
import { RiUserLine } from '@oxy.so/bloom/icons/RiUserLine';
|
|
13
|
+
|
|
14
|
+
/** The glyph a tool's step carries in the thought panel, by tool name. */
|
|
15
|
+
const TOOL_ICONS: ReadonlyMap<string, BloomIconComponent> = new Map([
|
|
16
|
+
['webSearch', RiSearchLine],
|
|
17
|
+
['scrapeURL', RiLink],
|
|
18
|
+
['getTimeline', RiCalendarLine],
|
|
19
|
+
['searchKnowledgeBase', RiDatabase2Line],
|
|
20
|
+
['webScraper', RiGlobalLine],
|
|
21
|
+
['browse', RiGlobalLine],
|
|
22
|
+
['sendWhatsAppMessage', RiChat3Line],
|
|
23
|
+
['getWhatsAppChats', RiChat3Line],
|
|
24
|
+
['getWhatsAppMessages', RiChat3Line],
|
|
25
|
+
['sendTelegramMessage', RiSendPlaneLine],
|
|
26
|
+
['getCurrentDate', RiCalendarLine],
|
|
27
|
+
['generateFile', RiFileTextLine],
|
|
28
|
+
['saveUserMemory', RiLightbulbFlashLine],
|
|
29
|
+
['updateUserPreferences', RiSettings3Line],
|
|
30
|
+
['updateUserContext', RiUserLine],
|
|
31
|
+
]);
|
|
32
|
+
|
|
33
|
+
/** A tool's glyph; a tool with none of its own reads as the web it reached. */
|
|
34
|
+
export function getToolIcon(toolName: string): BloomIconComponent {
|
|
35
|
+
return TOOL_ICONS.get(toolName) ?? RiGlobalLine;
|
|
36
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { createContext, type ReactNode, useContext } from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A tool's result as a host's renderer receives it (`docs/agent-embedding.md` §3): the app and the
|
|
5
|
+
* tool its canonical name names, what it was called with and what it returned.
|
|
6
|
+
*/
|
|
7
|
+
export interface AliaToolResult {
|
|
8
|
+
/** The app of `oxy_<app>__<tool>` / `self_<app>__<tool>`. */
|
|
9
|
+
app: string;
|
|
10
|
+
tool: string;
|
|
11
|
+
toolCallId: string;
|
|
12
|
+
input?: Record<string, unknown>;
|
|
13
|
+
output: unknown;
|
|
14
|
+
/** The consulted agent that called it; absent for the lead's own calls. */
|
|
15
|
+
agentId?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* The host's own drawings of catalogue tools' results, keyed `<app>:<tool>` — `homiio:searchListings`
|
|
20
|
+
* for `oxy_homiio__searchListings`. Drawn before the generic tool card, for the lead's calls and a
|
|
21
|
+
* consulted agent's alike.
|
|
22
|
+
*/
|
|
23
|
+
export type AliaToolRenderers = Readonly<Record<string, (result: AliaToolResult) => ReactNode>>;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* `oxy_<app>__<tool>` or `self_<app>__<tool>`: the only names a renderer is keyed from. The app
|
|
27
|
+
* part is an Oxy app slug; the tool part is whatever follows the separator.
|
|
28
|
+
*/
|
|
29
|
+
const CANONICAL_TOOL = /^(?:oxy|self)_([a-z][a-z0-9-]{0,63})__([A-Za-z0-9_-]+)$/;
|
|
30
|
+
|
|
31
|
+
/** The app and tool of a canonical catalogue tool name, or `null` for any other name. */
|
|
32
|
+
export function parseCatalogueTool(toolName: string): { app: string; tool: string } | null {
|
|
33
|
+
const match = CANONICAL_TOOL.exec(toolName);
|
|
34
|
+
return match ? { app: match[1], tool: match[2] } : null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** The renderer key of a tool name (`<app>:<tool>`), or `null` when it is not a catalogue tool. */
|
|
38
|
+
export function toolRendererKey(toolName: string): string | null {
|
|
39
|
+
const parsed = parseCatalogueTool(toolName);
|
|
40
|
+
return parsed ? `${parsed.app}:${parsed.tool}` : null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const RenderersContext = createContext<AliaToolRenderers>({});
|
|
44
|
+
|
|
45
|
+
export const AliaToolRenderersProvider = RenderersContext.Provider;
|
|
46
|
+
|
|
47
|
+
/** The host's renderers, `{}` when it brought none. */
|
|
48
|
+
export const useToolRenderers = () => useContext(RenderersContext);
|
package/src/lib/turn-state.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { Message } from '../runtime/types';
|
|
2
2
|
import type { AliaAvatarState } from '../components/AliaAvatar';
|
|
3
3
|
|
|
4
4
|
/** What a chat turn is doing, from its messages: the one reading the header mark and every button share. */
|
|
5
5
|
export function turnActivityState(
|
|
6
|
-
messages: ReadonlyArray<
|
|
6
|
+
messages: ReadonlyArray<Message>,
|
|
7
7
|
isStreaming: boolean,
|
|
8
8
|
): AliaAvatarState {
|
|
9
9
|
if (!isStreaming) return 'idle';
|
|
@@ -11,12 +11,14 @@
|
|
|
11
11
|
* playback through a local `RTCPeerConnection` loopback) is not built until the
|
|
12
12
|
* measurement says it is needed.
|
|
13
13
|
*
|
|
14
|
-
* The worklets are
|
|
15
|
-
*
|
|
14
|
+
* The worklets are one module served by Alia's web app (`./voice-worklets.ts`),
|
|
15
|
+
* never a `blob:` URL a host's CSP would have to allow as script. Their numbers
|
|
16
|
+
* (stream rate, frame size, prebuffer) are passed as `processorOptions`.
|
|
16
17
|
*/
|
|
17
18
|
|
|
18
19
|
import { levelOfPcm16, pcm16ToFloat } from './pcm';
|
|
19
20
|
import { VOICE_FRAME_SAMPLES, VOICE_SAMPLE_RATE } from './voice-protocol';
|
|
21
|
+
import { VOICE_WORKLETS_URL } from './voice-worklets';
|
|
20
22
|
import {
|
|
21
23
|
VoiceAudioEngineError,
|
|
22
24
|
type VoiceAudioEngine,
|
|
@@ -30,155 +32,6 @@ const PREBUFFER_MS = 80;
|
|
|
30
32
|
/** A burst shorter than the prebuffer still plays once nothing more arrives for this long. */
|
|
31
33
|
const PREBUFFER_IDLE_MS = 100;
|
|
32
34
|
|
|
33
|
-
const CAPTURE_PROCESSOR = `
|
|
34
|
-
class AliaVoiceCapture extends AudioWorkletProcessor {
|
|
35
|
-
constructor() {
|
|
36
|
-
super();
|
|
37
|
-
this.ratio = sampleRate / ${VOICE_SAMPLE_RATE};
|
|
38
|
-
this.t = 0;
|
|
39
|
-
this.prev = 0;
|
|
40
|
-
this.frame = new Int16Array(${VOICE_FRAME_SAMPLES});
|
|
41
|
-
this.filled = 0;
|
|
42
|
-
}
|
|
43
|
-
process(inputs) {
|
|
44
|
-
const input = inputs[0] && inputs[0][0];
|
|
45
|
-
if (!input || input.length === 0) return true;
|
|
46
|
-
const n = input.length;
|
|
47
|
-
while (this.t < n - 1) {
|
|
48
|
-
const i = Math.floor(this.t);
|
|
49
|
-
const f = this.t - i;
|
|
50
|
-
const a = i < 0 ? this.prev : input[i];
|
|
51
|
-
const b = input[i + 1];
|
|
52
|
-
let s = a + (b - a) * f;
|
|
53
|
-
s = s < -1 ? -1 : s > 1 ? 1 : s;
|
|
54
|
-
this.frame[this.filled++] = s < 0 ? s * 0x8000 : s * 0x7fff;
|
|
55
|
-
if (this.filled === this.frame.length) {
|
|
56
|
-
const out = this.frame;
|
|
57
|
-
this.port.postMessage(out.buffer, [out.buffer]);
|
|
58
|
-
this.frame = new Int16Array(${VOICE_FRAME_SAMPLES});
|
|
59
|
-
this.filled = 0;
|
|
60
|
-
}
|
|
61
|
-
this.t += this.ratio;
|
|
62
|
-
}
|
|
63
|
-
this.t -= n;
|
|
64
|
-
this.prev = input[n - 1];
|
|
65
|
-
return true;
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
registerProcessor('alia-voice-capture', AliaVoiceCapture);
|
|
69
|
-
`;
|
|
70
|
-
|
|
71
|
-
const PLAYBACK_PROCESSOR = `
|
|
72
|
-
class AliaVoicePlayback extends AudioWorkletProcessor {
|
|
73
|
-
constructor() {
|
|
74
|
-
super();
|
|
75
|
-
this.step = ${VOICE_SAMPLE_RATE} / sampleRate;
|
|
76
|
-
this.queue = [];
|
|
77
|
-
this.queued = 0;
|
|
78
|
-
this.pos = 0;
|
|
79
|
-
this.buffering = true;
|
|
80
|
-
this.lastChunkAt = 0;
|
|
81
|
-
this.item = null;
|
|
82
|
-
this.played = 0;
|
|
83
|
-
this.sinceReport = 0;
|
|
84
|
-
this.sumSquares = 0;
|
|
85
|
-
this.count = 0;
|
|
86
|
-
this.port.onmessage = (event) => {
|
|
87
|
-
const message = event.data;
|
|
88
|
-
if (message.type === 'chunk') {
|
|
89
|
-
this.queue.push({ itemId: message.itemId, samples: message.samples });
|
|
90
|
-
this.queued += message.samples.length;
|
|
91
|
-
this.lastChunkAt = currentTime;
|
|
92
|
-
} else if (message.type === 'flush') {
|
|
93
|
-
const playedMs = this.item === null ? null : (this.played / ${VOICE_SAMPLE_RATE}) * 1000;
|
|
94
|
-
this.port.postMessage({ type: 'flushed', itemId: this.item, playedMs });
|
|
95
|
-
this.queue = [];
|
|
96
|
-
this.queued = 0;
|
|
97
|
-
this.pos = 0;
|
|
98
|
-
this.buffering = true;
|
|
99
|
-
this.item = null;
|
|
100
|
-
this.played = 0;
|
|
101
|
-
}
|
|
102
|
-
};
|
|
103
|
-
}
|
|
104
|
-
report(force) {
|
|
105
|
-
if (!force && this.sinceReport < sampleRate / 20) return;
|
|
106
|
-
const rms = this.count === 0 ? 0 : Math.sqrt(this.sumSquares / this.count);
|
|
107
|
-
this.port.postMessage({
|
|
108
|
-
type: 'progress',
|
|
109
|
-
itemId: this.item,
|
|
110
|
-
playedMs: (this.played / ${VOICE_SAMPLE_RATE}) * 1000,
|
|
111
|
-
rms,
|
|
112
|
-
});
|
|
113
|
-
this.sinceReport = 0;
|
|
114
|
-
this.sumSquares = 0;
|
|
115
|
-
this.count = 0;
|
|
116
|
-
}
|
|
117
|
-
process(_inputs, outputs) {
|
|
118
|
-
const out = outputs[0][0];
|
|
119
|
-
if (this.buffering) {
|
|
120
|
-
const ready =
|
|
121
|
-
this.queued >= ${(VOICE_SAMPLE_RATE * PREBUFFER_MS) / 1000} ||
|
|
122
|
-
(this.queued > 0 && currentTime - this.lastChunkAt > ${PREBUFFER_IDLE_MS / 1000});
|
|
123
|
-
if (!ready) {
|
|
124
|
-
out.fill(0);
|
|
125
|
-
for (let c = 1; c < outputs[0].length; c++) outputs[0][c].fill(0);
|
|
126
|
-
return true;
|
|
127
|
-
}
|
|
128
|
-
this.buffering = false;
|
|
129
|
-
}
|
|
130
|
-
for (let k = 0; k < out.length; k++) {
|
|
131
|
-
const head = this.queue[0];
|
|
132
|
-
if (!head) {
|
|
133
|
-
out[k] = 0;
|
|
134
|
-
if (!this.buffering) {
|
|
135
|
-
this.buffering = true;
|
|
136
|
-
this.report(true);
|
|
137
|
-
this.port.postMessage({ type: 'idle' });
|
|
138
|
-
}
|
|
139
|
-
continue;
|
|
140
|
-
}
|
|
141
|
-
if (head.itemId !== this.item) {
|
|
142
|
-
this.item = head.itemId;
|
|
143
|
-
this.played = 0;
|
|
144
|
-
this.port.postMessage({ type: 'started', itemId: head.itemId });
|
|
145
|
-
}
|
|
146
|
-
const i = Math.floor(this.pos);
|
|
147
|
-
const f = this.pos - i;
|
|
148
|
-
const a = head.samples[i];
|
|
149
|
-
const b = i + 1 < head.samples.length ? head.samples[i + 1] : a;
|
|
150
|
-
const s = a + (b - a) * f;
|
|
151
|
-
out[k] = s;
|
|
152
|
-
this.sumSquares += s * s;
|
|
153
|
-
this.count++;
|
|
154
|
-
this.pos += this.step;
|
|
155
|
-
const consumed = Math.floor(this.pos);
|
|
156
|
-
if (consumed > 0) {
|
|
157
|
-
const taken = Math.min(consumed, head.samples.length);
|
|
158
|
-
this.played += taken;
|
|
159
|
-
this.queued -= taken;
|
|
160
|
-
if (consumed >= head.samples.length) {
|
|
161
|
-
this.queue.shift();
|
|
162
|
-
this.pos = 0;
|
|
163
|
-
} else {
|
|
164
|
-
head.samples = head.samples.subarray(consumed);
|
|
165
|
-
this.pos -= consumed;
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
for (let c = 1; c < outputs[0].length; c++) outputs[0][c].set(out);
|
|
170
|
-
this.sinceReport += out.length;
|
|
171
|
-
this.report(false);
|
|
172
|
-
return true;
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
registerProcessor('alia-voice-playback', AliaVoicePlayback);
|
|
176
|
-
`;
|
|
177
|
-
|
|
178
|
-
function moduleUrl(source: string): string {
|
|
179
|
-
return URL.createObjectURL(new Blob([source], { type: 'application/javascript' }));
|
|
180
|
-
}
|
|
181
|
-
|
|
182
35
|
function deviceError(caught: unknown): VoiceAudioEngineError {
|
|
183
36
|
const name = (caught as { name?: unknown } | null)?.name;
|
|
184
37
|
if (name === 'NotAllowedError' || name === 'SecurityError' || name === 'PermissionDeniedError')
|
|
@@ -229,16 +82,10 @@ export function createVoiceAudioEngine(): VoiceAudioEngine {
|
|
|
229
82
|
}
|
|
230
83
|
|
|
231
84
|
context = new AudioContextCtor({ latencyHint: 'interactive' });
|
|
232
|
-
const captureUrl = moduleUrl(CAPTURE_PROCESSOR);
|
|
233
|
-
const playbackUrl = moduleUrl(PLAYBACK_PROCESSOR);
|
|
234
85
|
try {
|
|
235
|
-
await context.audioWorklet.addModule(
|
|
236
|
-
await context.audioWorklet.addModule(playbackUrl);
|
|
86
|
+
await context.audioWorklet.addModule(VOICE_WORKLETS_URL);
|
|
237
87
|
} catch {
|
|
238
88
|
throw new VoiceAudioEngineError('voice-unavailable');
|
|
239
|
-
} finally {
|
|
240
|
-
URL.revokeObjectURL(captureUrl);
|
|
241
|
-
URL.revokeObjectURL(playbackUrl);
|
|
242
89
|
}
|
|
243
90
|
await context.resume().catch(() => undefined);
|
|
244
91
|
|
|
@@ -247,6 +94,10 @@ export function createVoiceAudioEngine(): VoiceAudioEngine {
|
|
|
247
94
|
numberOfInputs: 1,
|
|
248
95
|
numberOfOutputs: 0,
|
|
249
96
|
channelCount: 1,
|
|
97
|
+
processorOptions: {
|
|
98
|
+
streamSampleRate: VOICE_SAMPLE_RATE,
|
|
99
|
+
frameSamples: VOICE_FRAME_SAMPLES,
|
|
100
|
+
},
|
|
250
101
|
});
|
|
251
102
|
capture.port.onmessage = (event: MessageEvent<ArrayBuffer>) => {
|
|
252
103
|
if (stopped) return;
|
|
@@ -260,6 +111,11 @@ export function createVoiceAudioEngine(): VoiceAudioEngine {
|
|
|
260
111
|
numberOfInputs: 0,
|
|
261
112
|
numberOfOutputs: 1,
|
|
262
113
|
outputChannelCount: [context.destination.channelCount || 2],
|
|
114
|
+
processorOptions: {
|
|
115
|
+
streamSampleRate: VOICE_SAMPLE_RATE,
|
|
116
|
+
prebufferMs: PREBUFFER_MS,
|
|
117
|
+
prebufferIdleMs: PREBUFFER_IDLE_MS,
|
|
118
|
+
},
|
|
263
119
|
});
|
|
264
120
|
playback.port.onmessage = (event: MessageEvent) => {
|
|
265
121
|
const message = event.data as
|