@alexkroman1/aai-ui 1.4.4 → 1.5.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.
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
<title>aai</title>
|
|
7
7
|
<link rel="icon" href="data:," />
|
|
8
8
|
<style>html, body { background: #101010; margin: 0; }</style>
|
|
9
|
-
<script type="module" crossorigin src="./assets/index-
|
|
9
|
+
<script type="module" crossorigin src="./assets/index-BlMvrUC4.js"></script>
|
|
10
10
|
<link rel="stylesheet" crossorigin href="./assets/index-BZqVGR2o.css">
|
|
11
11
|
</head>
|
|
12
12
|
<body>
|
package/dist/session-core.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { WS_OPEN, errorMessage } from "@alexkroman1/aai";
|
|
2
|
-
import {
|
|
2
|
+
import { ServerMessageSchema, lenientParse } from "@alexkroman1/aai/protocol";
|
|
3
3
|
//#region session-core.ts
|
|
4
4
|
/**
|
|
5
5
|
* Framework-agnostic voice session core.
|
|
@@ -10,6 +10,14 @@ import { ReadyConfigSchema, ServerMessageSchema, lenientParse } from "@alexkroma
|
|
|
10
10
|
*
|
|
11
11
|
* No dependency on React, Preact, or any UI framework.
|
|
12
12
|
*/
|
|
13
|
+
/** Cap on `customEvents` retained in the session snapshot to avoid unbounded growth. */
|
|
14
|
+
const MAX_CUSTOM_EVENTS = 200;
|
|
15
|
+
/** Cap on `messages` retained in the session snapshot; mirrors host-side DEFAULT_MAX_HISTORY. */
|
|
16
|
+
const MAX_MESSAGES = 200;
|
|
17
|
+
function appendCapped(list, item, cap) {
|
|
18
|
+
const next = [...list, item];
|
|
19
|
+
return next.length > cap ? next.slice(-cap) : next;
|
|
20
|
+
}
|
|
13
21
|
/**
|
|
14
22
|
* Initialize audio capture and playback after the server sends a ready config.
|
|
15
23
|
*
|
|
@@ -41,9 +49,9 @@ async function initAudioCapture(conn, msg, deps) {
|
|
|
41
49
|
playbackWorkletSrc: playbackWorklet,
|
|
42
50
|
onMicData: (pcm16) => {
|
|
43
51
|
try {
|
|
44
|
-
deps.
|
|
52
|
+
deps.sendAudio(new Uint8Array(pcm16));
|
|
45
53
|
} catch {
|
|
46
|
-
console.debug("[aai-ui]
|
|
54
|
+
console.debug("[aai-ui] sendAudio dropped: connection closed");
|
|
47
55
|
}
|
|
48
56
|
}
|
|
49
57
|
});
|
|
@@ -52,7 +60,7 @@ async function initAudioCapture(conn, msg, deps) {
|
|
|
52
60
|
return;
|
|
53
61
|
}
|
|
54
62
|
conn.voiceIO = io;
|
|
55
|
-
deps.
|
|
63
|
+
deps.sendJson({ type: "audio_ready" });
|
|
56
64
|
deps.updateState({ state: "listening" });
|
|
57
65
|
} catch (err) {
|
|
58
66
|
if (conn.generation !== gen || !conn.ws || conn.ws.readyState !== WS_OPEN) return;
|
|
@@ -143,39 +151,46 @@ function createSessionCore(options) {
|
|
|
143
151
|
error: null
|
|
144
152
|
});
|
|
145
153
|
}
|
|
146
|
-
function
|
|
154
|
+
function sendJson(msg) {
|
|
147
155
|
if (conn.ws && conn.ws.readyState === WS_OPEN) conn.ws.send(JSON.stringify(msg));
|
|
148
156
|
}
|
|
149
|
-
function
|
|
150
|
-
if (conn.ws && conn.ws.readyState === WS_OPEN) conn.ws.send(
|
|
157
|
+
function sendAudio(bytes) {
|
|
158
|
+
if (conn.ws && conn.ws.readyState === WS_OPEN) conn.ws.send(bytes);
|
|
151
159
|
}
|
|
152
160
|
const audioDeps = {
|
|
153
|
-
|
|
154
|
-
|
|
161
|
+
sendJson,
|
|
162
|
+
sendAudio,
|
|
155
163
|
updateState
|
|
156
164
|
};
|
|
157
165
|
/** Incremented on each turn boundary -- stale async callbacks compare against this. */
|
|
158
166
|
let handlerGeneration = 0;
|
|
159
167
|
/** Monotonically increasing counter for custom events -- used by useEvent to deduplicate. */
|
|
160
168
|
let customEventSeq = 0;
|
|
169
|
+
function appendCustomEvent(name, data) {
|
|
170
|
+
updateState({ customEvents: appendCapped(currentSnapshot.customEvents, {
|
|
171
|
+
id: ++customEventSeq,
|
|
172
|
+
event: name,
|
|
173
|
+
data
|
|
174
|
+
}, MAX_CUSTOM_EVENTS) });
|
|
175
|
+
}
|
|
161
176
|
function handleUserTranscriptEvent(text) {
|
|
162
177
|
handlerGeneration++;
|
|
163
178
|
updateState({
|
|
164
179
|
userTranscript: null,
|
|
165
|
-
messages:
|
|
180
|
+
messages: appendCapped(currentSnapshot.messages, {
|
|
166
181
|
role: "user",
|
|
167
182
|
content: text
|
|
168
|
-
}
|
|
183
|
+
}, MAX_MESSAGES),
|
|
169
184
|
state: "thinking"
|
|
170
185
|
});
|
|
171
186
|
}
|
|
172
187
|
function handleAgentTranscriptEvent(text) {
|
|
173
188
|
updateState({
|
|
174
189
|
agentTranscript: null,
|
|
175
|
-
messages:
|
|
190
|
+
messages: appendCapped(currentSnapshot.messages, {
|
|
176
191
|
role: "assistant",
|
|
177
192
|
content: text
|
|
178
|
-
}
|
|
193
|
+
}, MAX_MESSAGES)
|
|
179
194
|
});
|
|
180
195
|
}
|
|
181
196
|
/** Single entry point for all server->client session events. */
|
|
@@ -199,7 +214,7 @@ function createSessionCore(options) {
|
|
|
199
214
|
updateState({ toolCalls: [...currentSnapshot.toolCalls, {
|
|
200
215
|
callId: e.toolCallId,
|
|
201
216
|
name: e.toolName,
|
|
202
|
-
args: e.args,
|
|
217
|
+
args: e.args ?? {},
|
|
203
218
|
status: "pending",
|
|
204
219
|
afterMessageIndex: currentSnapshot.messages.length - 1
|
|
205
220
|
}] });
|
|
@@ -245,11 +260,7 @@ function createSessionCore(options) {
|
|
|
245
260
|
});
|
|
246
261
|
break;
|
|
247
262
|
case "custom_event":
|
|
248
|
-
|
|
249
|
-
id: ++customEventSeq,
|
|
250
|
-
event: e.event,
|
|
251
|
-
data: e.data
|
|
252
|
-
}] });
|
|
263
|
+
appendCustomEvent(e.event, e.data);
|
|
253
264
|
break;
|
|
254
265
|
case "error":
|
|
255
266
|
console.error("Agent error:", e.message);
|
|
@@ -262,6 +273,7 @@ function createSessionCore(options) {
|
|
|
262
273
|
running: false
|
|
263
274
|
});
|
|
264
275
|
break;
|
|
276
|
+
case "idle_timeout": break;
|
|
265
277
|
default: break;
|
|
266
278
|
}
|
|
267
279
|
}
|
|
@@ -269,7 +281,7 @@ function createSessionCore(options) {
|
|
|
269
281
|
function playAudioChunk(chunk) {
|
|
270
282
|
if (currentSnapshot.state === "disconnected" && currentSnapshot.error !== null) return;
|
|
271
283
|
if (currentSnapshot.state !== "speaking") updateState({ state: "speaking" });
|
|
272
|
-
|
|
284
|
+
conn.voiceIO?.enqueue(chunk.buffer);
|
|
273
285
|
}
|
|
274
286
|
/**
|
|
275
287
|
* Signal that the server has finished sending audio for this turn.
|
|
@@ -287,52 +299,47 @@ function createSessionCore(options) {
|
|
|
287
299
|
});
|
|
288
300
|
else updateState({ state: "listening" });
|
|
289
301
|
}
|
|
290
|
-
/** Parse a JSON text frame into a ServerMessage, or null if invalid/unknown. */
|
|
291
|
-
function parseTextFrame(data) {
|
|
292
|
-
try {
|
|
293
|
-
const result = lenientParse(ServerMessageSchema, JSON.parse(data));
|
|
294
|
-
if (!result.ok) {
|
|
295
|
-
if (result.malformed) console.warn("Ignoring invalid server message:", result.error);
|
|
296
|
-
return null;
|
|
297
|
-
}
|
|
298
|
-
return result.data;
|
|
299
|
-
} catch {
|
|
300
|
-
return null;
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
/** Try to extract a ReadyConfig from a config message, or null. */
|
|
304
|
-
function parseConfig(msg) {
|
|
305
|
-
const { type: _, sessionId, ...config } = msg;
|
|
306
|
-
const parsed = ReadyConfigSchema.safeParse(config);
|
|
307
|
-
if (!parsed.success) {
|
|
308
|
-
console.warn("Unsupported server config:", parsed.error.message);
|
|
309
|
-
return null;
|
|
310
|
-
}
|
|
311
|
-
return sessionId ? {
|
|
312
|
-
...parsed.data,
|
|
313
|
-
sessionId
|
|
314
|
-
} : parsed.data;
|
|
315
|
-
}
|
|
316
302
|
/**
|
|
317
|
-
* Dispatch an incoming WebSocket message
|
|
303
|
+
* Dispatch an incoming WebSocket message.
|
|
304
|
+
*
|
|
305
|
+
* Binary frames carry raw PCM16 audio chunks. Text frames are JSON-encoded
|
|
306
|
+
* {@link ServerMessage} values validated via Zod.
|
|
318
307
|
*
|
|
319
308
|
* Returns the parsed config if the message is a `config` message,
|
|
320
|
-
* otherwise `
|
|
309
|
+
* otherwise `undefined`.
|
|
321
310
|
*/
|
|
322
311
|
function handleMessage(data) {
|
|
323
312
|
if (data instanceof ArrayBuffer) {
|
|
324
313
|
playAudioChunk(new Uint8Array(data));
|
|
325
|
-
return
|
|
314
|
+
return;
|
|
326
315
|
}
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
316
|
+
if (typeof data !== "string") {
|
|
317
|
+
console.warn("session-core: non-string, non-binary frame received; dropping");
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
let raw;
|
|
321
|
+
try {
|
|
322
|
+
raw = JSON.parse(data);
|
|
323
|
+
} catch {
|
|
324
|
+
console.warn("session-core: invalid JSON; dropping");
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
const parsed = lenientParse(ServerMessageSchema, raw);
|
|
328
|
+
if (!parsed.ok) {
|
|
329
|
+
if (parsed.malformed) console.warn("session-core: malformed server message", parsed.error);
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
const msg = parsed.data;
|
|
333
|
+
if (msg.type === "config") return {
|
|
334
|
+
sampleRate: msg.sampleRate,
|
|
335
|
+
ttsSampleRate: msg.ttsSampleRate,
|
|
336
|
+
sid: msg.sessionId
|
|
337
|
+
};
|
|
330
338
|
if (msg.type === "audio_done") {
|
|
331
339
|
playAudioDone();
|
|
332
|
-
return
|
|
340
|
+
return;
|
|
333
341
|
}
|
|
334
342
|
handleEvent(msg);
|
|
335
|
-
return null;
|
|
336
343
|
}
|
|
337
344
|
function connect(opts) {
|
|
338
345
|
updateState({
|
|
@@ -358,7 +365,7 @@ function createSessionCore(options) {
|
|
|
358
365
|
socket.addEventListener("message", (event) => {
|
|
359
366
|
const config = handleMessage(event.data);
|
|
360
367
|
if (config) {
|
|
361
|
-
if (config.
|
|
368
|
+
if (config.sid) options.onSessionId?.(config.sid);
|
|
362
369
|
const isReconnect = hasConnected;
|
|
363
370
|
hasConnected = true;
|
|
364
371
|
initAudioCapture(conn, config, audioDeps).catch((err) => {
|
|
@@ -371,7 +378,7 @@ function createSessionCore(options) {
|
|
|
371
378
|
running: false
|
|
372
379
|
});
|
|
373
380
|
});
|
|
374
|
-
if (isReconnect && currentSnapshot.messages.length > 0)
|
|
381
|
+
if (isReconnect && currentSnapshot.messages.length > 0) sendJson({
|
|
375
382
|
type: "history",
|
|
376
383
|
messages: currentSnapshot.messages.map((m) => ({
|
|
377
384
|
role: m.role,
|
|
@@ -393,12 +400,12 @@ function createSessionCore(options) {
|
|
|
393
400
|
function cancel() {
|
|
394
401
|
conn.voiceIO?.flush();
|
|
395
402
|
updateState({ state: "listening" });
|
|
396
|
-
|
|
403
|
+
sendJson({ type: "cancel" });
|
|
397
404
|
}
|
|
398
405
|
function reset() {
|
|
399
406
|
conn.voiceIO?.flush();
|
|
400
407
|
if (conn.ws && conn.ws.readyState === WS_OPEN) {
|
|
401
|
-
|
|
408
|
+
sendJson({ type: "reset" });
|
|
402
409
|
return;
|
|
403
410
|
}
|
|
404
411
|
resetState();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alexkroman1/aai-ui",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"files": [
|
|
6
6
|
"dist",
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
},
|
|
19
19
|
"dependencies": {
|
|
20
20
|
"clsx": "^2.1.1",
|
|
21
|
-
"@alexkroman1/aai": "1.
|
|
21
|
+
"@alexkroman1/aai": "1.5.0"
|
|
22
22
|
},
|
|
23
23
|
"peerDependencies": {
|
|
24
24
|
"react": "^19.0.0",
|