@alexkroman1/aai-ui 1.9.2 → 1.11.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.
Files changed (48) hide show
  1. package/dist/{_colors-DJUordGv.js → _colors-DYX7XRTr.js} +8 -0
  2. package/dist/{_utils-5cs73OrA.js → _utils-CN1yVgYS.js} +2 -5
  3. package/dist/audio.d.ts +6 -0
  4. package/dist/audio.js +62 -9
  5. package/dist/{chat-view-C1XbqDk0.js → chat-view-u3yBAlig.js} +16 -14
  6. package/dist/components/_colors.d.ts +0 -4
  7. package/dist/components/chat-view.js +1 -1
  8. package/dist/components/controls.d.ts +2 -2
  9. package/dist/components/controls.js +1 -1
  10. package/dist/components/message-list.d.ts +2 -2
  11. package/dist/components/message-list.js +70 -64
  12. package/dist/components/start-screen.js +1 -1
  13. package/dist/components/text-controls.d.ts +2 -2
  14. package/dist/components/tool-call-block.d.ts +6 -2
  15. package/dist/components/tool-call-block.js +1 -1
  16. package/dist/context.d.ts +3 -11
  17. package/dist/context.js +9 -22
  18. package/dist/{controls-BngrPbOC.js → controls-B2EPUJDU.js} +12 -9
  19. package/dist/default-client/assets/audio-CSwpo0gM.js +1 -0
  20. package/dist/default-client/assets/{capture-processor-C19oBn4L.js → capture-processor-UlKEKyIW.js} +4 -1
  21. package/dist/default-client/assets/index-B2bISVbm.css +2 -0
  22. package/dist/default-client/assets/index-BTqSueyz.js +94 -0
  23. package/dist/default-client/assets/{playback-processor-BtlzAH78.js → playback-processor-C5HVRVbu.js} +11 -4
  24. package/dist/default-client/index.html +2 -2
  25. package/dist/define-client.js +3 -3
  26. package/dist/hooks.js +5 -2
  27. package/dist/index.d.ts +4 -0
  28. package/dist/index.js +354 -5
  29. package/dist/{session-core-D3NDaySY.js → session-core-KUoY-0JO.js} +251 -114
  30. package/dist/session-core-audio-setup.d.ts +36 -0
  31. package/dist/session-core-messages.d.ts +12 -0
  32. package/dist/session-core-reconnect.d.ts +19 -0
  33. package/dist/session-core-url.d.ts +2 -0
  34. package/dist/session-core.js +1 -1
  35. package/dist/sync-mic.d.ts +49 -0
  36. package/dist/sync-session.d.ts +49 -0
  37. package/dist/sync-vad.d.ts +54 -0
  38. package/dist/{tool-call-block-7f1GTG-P.js → tool-call-block-DIxpG8GM.js} +11 -6
  39. package/dist/types.d.ts +12 -14
  40. package/dist/types.js +1 -13
  41. package/dist/worklets/capture-processor.d.ts +1 -1
  42. package/dist/worklets/capture-processor.js +4 -1
  43. package/dist/worklets/playback-processor.d.ts +1 -1
  44. package/dist/worklets/playback-processor.js +11 -4
  45. package/package.json +8 -5
  46. package/dist/default-client/assets/audio-Cs-6t_Wd.js +0 -1
  47. package/dist/default-client/assets/index-Bf4ZTNcx.js +0 -73
  48. package/dist/default-client/assets/index-Bzlh9i7w.css +0 -2
@@ -46,8 +46,11 @@ class PlaybackProcessor extends AudioWorkletProcessor {
46
46
  // End the current turn: notify the host and rearm for the next reply.
47
47
  // Must NOT return false from process() — a processor that stops is dead
48
48
  // for good, forcing a new node (and buffer) per reply.
49
- stopTurn() {
50
- this.port.postMessage({ event: 'stop' });
49
+ // \`reason\` ('interrupt' | 'done') tells the host which turn boundary this
50
+ // stop belongs to: interrupt-stops are dropped host-side (flush() already
51
+ // settled that turn), so they can never resolve a later turn's done() early.
52
+ stopTurn(reason) {
53
+ this.port.postMessage({ event: 'stop', reason });
51
54
  this.resetTurn();
52
55
  }
53
56
 
@@ -105,10 +108,14 @@ class PlaybackProcessor extends AudioWorkletProcessor {
105
108
  }
106
109
 
107
110
  process(inputs, outputs) {
111
+ // No output wired up yet — nothing to render this quantum. Throwing here
112
+ // would permanently kill the processor (the node is persistent per
113
+ // session), so guard like the capture processor does.
114
+ if (!outputs[0] || !outputs[0][0]) return true;
108
115
  const out = outputs[0][0];
109
116
  if (this.interrupted) {
110
117
  out.fill(0);
111
- this.stopTurn();
118
+ this.stopTurn('interrupt');
112
119
  return true;
113
120
  }
114
121
 
@@ -139,7 +146,7 @@ class PlaybackProcessor extends AudioWorkletProcessor {
139
146
  // No data: output silence, end the turn only when done
140
147
  out.fill(0);
141
148
  if (this.isDone) {
142
- this.stopTurn();
149
+ this.stopTurn('done');
143
150
  }
144
151
  return true;
145
152
  }
@@ -6,8 +6,8 @@
6
6
  <title>aai</title>
7
7
  <link rel="icon" href="data:," />
8
8
  <style>html, body { background: #FBF8F2; margin: 0; }</style>
9
- <script type="module" crossorigin src="./assets/index-Bf4ZTNcx.js"></script>
10
- <link rel="stylesheet" crossorigin href="./assets/index-Bzlh9i7w.css">
9
+ <script type="module" crossorigin src="./assets/index-BTqSueyz.js"></script>
10
+ <link rel="stylesheet" crossorigin href="./assets/index-B2bISVbm.css">
11
11
  </head>
12
12
  <body>
13
13
  <main id="app"></main>
@@ -1,9 +1,9 @@
1
1
  import { SessionProvider, ThemeProvider } from "./context.js";
2
- import { n as ToolConfigContext } from "./tool-call-block-7f1GTG-P.js";
3
- import { t as ChatView } from "./chat-view-C1XbqDk0.js";
2
+ import { n as ToolConfigContext } from "./tool-call-block-DIxpG8GM.js";
3
+ import { t as ChatView } from "./chat-view-u3yBAlig.js";
4
4
  import { SidebarLayout } from "./components/sidebar-layout.js";
5
5
  import { StartScreen } from "./components/start-screen.js";
6
- import { t as createSessionCore } from "./session-core-D3NDaySY.js";
6
+ import { t as createSessionCore } from "./session-core-KUoY-0JO.js";
7
7
  import { createElement } from "react";
8
8
  import { jsx } from "react/jsx-runtime";
9
9
  import { flushSync } from "react-dom";
package/dist/hooks.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { useSessionSelector } from "./context.js";
2
- import { n as tryParseJSON } from "./_utils-5cs73OrA.js";
2
+ import { n as tryParseJSON } from "./_utils-CN1yVgYS.js";
3
3
  import { useEffect, useRef } from "react";
4
4
  //#region hooks.ts
5
5
  /** Index of the first item whose sequence number is above the watermark (tail scan from the end). */
@@ -51,13 +51,16 @@ function useToolCallEffect(status, args, fire) {
51
51
  seq: 0,
52
52
  fired: /* @__PURE__ */ new Set()
53
53
  });
54
+ const mountedRef = useRef(false);
54
55
  const callbackRef = useRef(callback);
55
56
  callbackRef.current = callback;
56
57
  const fireRef = useRef(fire);
57
58
  fireRef.current = fire;
58
59
  useEffect(() => {
60
+ const firstRun = !mountedRef.current;
61
+ mountedRef.current = true;
59
62
  processToolCallTail(toolCalls, cursorRef.current, (tc) => status === "pending" || tc.status === status, (tc) => {
60
- if (tc.status !== status) return;
63
+ if (tc.status !== status && firstRun) return;
61
64
  if (filterName && tc.name !== filterName) return;
62
65
  fireRef.current(callbackRef.current, tc, filterName !== null);
63
66
  });
package/dist/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  export { Button } from "./components/button.tsx";
2
2
  export { ChatView } from "./components/chat-view.tsx";
3
+ export { Controls } from "./components/controls.tsx";
3
4
  export { MessageList } from "./components/message-list.tsx";
4
5
  export { SidebarLayout } from "./components/sidebar-layout.tsx";
5
6
  export { StartScreen } from "./components/start-screen.tsx";
@@ -14,4 +15,7 @@ export { client } from "./define-client.tsx";
14
15
  export { useEvent, useToolCallStart, useToolResult } from "./hooks.ts";
15
16
  export type { CustomEvent, SessionCore, SessionCoreOptions, SessionSnapshot, } from "./session-core.ts";
16
17
  export { createSessionCore } from "./session-core.ts";
18
+ export { CAPTURE_WORKLET_DATA_URI, DEFAULT_SYNC_MIC_SAMPLE_RATE, floatToPcm16, type SyncMicrophone, type SyncMicrophoneOptions, startSyncMicrophone, } from "./sync-mic.ts";
19
+ export { base64ToPcm16, createSyncSession, pcm16ToBase64, type SyncSession, type SyncSessionOptions, type SyncTurnResult, } from "./sync-session.ts";
20
+ export { createUtteranceDetector, type UtteranceDetector, type UtteranceDetectorOptions, } from "./sync-vad.ts";
17
21
  export type { AgentState, ChatMessage, ClientTheme, SessionError, SessionErrorCode, ToolCallInfo, VoiceSessionOptions, WebSocketConstructor, } from "./types.ts";
package/dist/index.js CHANGED
@@ -1,12 +1,361 @@
1
1
  import { SessionProvider, ThemeProvider, useSession, useSessionSelector, useTheme } from "./context.js";
2
2
  import { Button } from "./components/button.js";
3
- import { i as UiUrlChip, n as ApiUrlChip, r as SessionUrlChips } from "./controls-BngrPbOC.js";
4
- import { n as ToolConfigContext } from "./tool-call-block-7f1GTG-P.js";
3
+ import { i as UiUrlChip, n as ApiUrlChip, r as SessionUrlChips, t as Controls } from "./controls-B2EPUJDU.js";
4
+ import { n as ToolConfigContext } from "./tool-call-block-DIxpG8GM.js";
5
5
  import { MessageList } from "./components/message-list.js";
6
- import { n as TextControls, t as ChatView } from "./chat-view-C1XbqDk0.js";
6
+ import { n as TextControls, t as ChatView } from "./chat-view-u3yBAlig.js";
7
7
  import { SidebarLayout } from "./components/sidebar-layout.js";
8
8
  import { StartScreen } from "./components/start-screen.js";
9
- import { t as createSessionCore } from "./session-core-D3NDaySY.js";
9
+ import { t as createSessionCore } from "./session-core-KUoY-0JO.js";
10
10
  import { client } from "./define-client.js";
11
11
  import { useEvent, useToolCallStart, useToolResult } from "./hooks.js";
12
- export { ApiUrlChip, Button, ChatView, MessageList, SessionProvider, SessionUrlChips, SidebarLayout, StartScreen, TextControls, ThemeProvider, ToolConfigContext, UiUrlChip, client, createSessionCore, useEvent, useSession, useSessionSelector, useTheme, useToolCallStart, useToolResult };
12
+ import { errorMessage, safeJsonParse } from "@alexkroman1/aai";
13
+ import { SyncTurnResponseSchema } from "@alexkroman1/aai/protocol";
14
+ //#region sync-vad.ts
15
+ const DEFAULT_SPEECH_RMS = .015;
16
+ const DEFAULT_MAX_UTTERANCE_MS = 3e4;
17
+ function rmsOf(frame) {
18
+ if (frame.length === 0) return 0;
19
+ let sum = 0;
20
+ for (const s of frame) {
21
+ const f = s / 32768;
22
+ sum += f * f;
23
+ }
24
+ return Math.sqrt(sum / frame.length);
25
+ }
26
+ function concat(frames) {
27
+ let total = 0;
28
+ for (const f of frames) total += f.length;
29
+ const out = new Int16Array(total);
30
+ let offset = 0;
31
+ for (const f of frames) {
32
+ out.set(f, offset);
33
+ offset += f.length;
34
+ }
35
+ return out;
36
+ }
37
+ /** Create an energy-based {@link UtteranceDetector}. */
38
+ function createUtteranceDetector(opts) {
39
+ const { sampleRate, speechRms = DEFAULT_SPEECH_RMS, minSpeechMs = 150, hangoverMs = 700, prerollMs = 300, maxUtteranceMs = DEFAULT_MAX_UTTERANCE_MS } = opts;
40
+ const msOf = (frame) => frame.length / sampleRate * 1e3;
41
+ let preroll = [];
42
+ let prerollTotalMs = 0;
43
+ let utterance = [];
44
+ let utteranceMs = 0;
45
+ let speaking = false;
46
+ /** Consecutive voiced ms while still a candidate (below minSpeechMs). */
47
+ let candidateVoicedMs = 0;
48
+ /** Consecutive silent ms while speaking (toward hangoverMs). */
49
+ let silenceMs = 0;
50
+ function toIdle() {
51
+ utterance = [];
52
+ utteranceMs = 0;
53
+ speaking = false;
54
+ candidateVoicedMs = 0;
55
+ silenceMs = 0;
56
+ }
57
+ function pushPreroll(frame) {
58
+ preroll.push(frame);
59
+ prerollTotalMs += msOf(frame);
60
+ while (preroll.length > 0 && prerollTotalMs - msOf(preroll[0]) >= prerollMs) prerollTotalMs -= msOf(preroll.shift());
61
+ }
62
+ function beginCandidate(frame) {
63
+ utterance = [...preroll, frame];
64
+ utteranceMs = prerollTotalMs + msOf(frame);
65
+ preroll = [];
66
+ prerollTotalMs = 0;
67
+ candidateVoicedMs = msOf(frame);
68
+ }
69
+ function finalize() {
70
+ const out = concat(utterance);
71
+ toIdle();
72
+ return out;
73
+ }
74
+ function pushIdle(frame, voiced) {
75
+ if (!voiced) {
76
+ pushPreroll(frame);
77
+ return;
78
+ }
79
+ beginCandidate(frame);
80
+ }
81
+ function pushCandidate(frame, voiced) {
82
+ if (!voiced) {
83
+ for (const f of utterance) pushPreroll(f);
84
+ toIdle();
85
+ return;
86
+ }
87
+ utterance.push(frame);
88
+ utteranceMs += msOf(frame);
89
+ candidateVoicedMs += msOf(frame);
90
+ }
91
+ function pushSpeaking(frame, voiced) {
92
+ utterance.push(frame);
93
+ utteranceMs += msOf(frame);
94
+ silenceMs = voiced ? 0 : silenceMs + msOf(frame);
95
+ return silenceMs >= hangoverMs ? finalize() : null;
96
+ }
97
+ return {
98
+ get speaking() {
99
+ return speaking;
100
+ },
101
+ push(frame) {
102
+ if (frame.length === 0) return null;
103
+ const voiced = rmsOf(frame) >= speechRms;
104
+ let closed = null;
105
+ if (speaking) closed = pushSpeaking(frame, voiced);
106
+ else if (utterance.length > 0) pushCandidate(frame, voiced);
107
+ else pushIdle(frame, voiced);
108
+ if (closed) return closed;
109
+ if (!speaking && candidateVoicedMs >= minSpeechMs) speaking = true;
110
+ if (speaking && utteranceMs >= maxUtteranceMs) return finalize();
111
+ return null;
112
+ },
113
+ flush() {
114
+ const out = speaking ? finalize() : null;
115
+ this.reset();
116
+ return out;
117
+ },
118
+ reset() {
119
+ preroll = [];
120
+ prerollTotalMs = 0;
121
+ toIdle();
122
+ }
123
+ };
124
+ }
125
+ //#endregion
126
+ //#region sync-mic.ts
127
+ /**
128
+ * WebRTC microphone capture for sync mode.
129
+ *
130
+ * Captures voice through `getUserMedia` with the WebRTC voice-processing
131
+ * constraints (echo cancellation, noise suppression, auto gain — the
132
+ * processing that makes the energy VAD in `sync-vad.ts` reliable), runs an
133
+ * AudioWorklet that batches raw frames to the main thread, feeds them
134
+ * through the utterance detector, and hands each completed utterance to
135
+ * the sync session as one HTTP turn. No WebSocket anywhere on the path.
136
+ *
137
+ * The worklet module ships inline as a data URI, so sync mode needs no
138
+ * separately-served processor file.
139
+ */
140
+ /** Default capture rate — what the STT providers expect. */
141
+ const DEFAULT_SYNC_MIC_SAMPLE_RATE = 16e3;
142
+ /** ~128 ms at 16 kHz: few messages per second, fine-enough VAD granularity. */
143
+ const CAPTURE_BATCH_SAMPLES = 2048;
144
+ /** Data URI form of the capture processor (no served asset, no blob URL). */
145
+ const CAPTURE_WORKLET_DATA_URI = `data:application/javascript;charset=utf-8,${encodeURIComponent(`
146
+ registerProcessor("aai-sync-capture", class extends AudioWorkletProcessor {
147
+ constructor(options) {
148
+ super();
149
+ const batch = (options && options.processorOptions && options.processorOptions.batchSamples) || ${CAPTURE_BATCH_SAMPLES};
150
+ this.buf = new Float32Array(batch);
151
+ this.len = 0;
152
+ }
153
+ process(inputs) {
154
+ const ch = inputs[0] && inputs[0][0];
155
+ if (!ch) return true;
156
+ let read = 0;
157
+ while (read < ch.length) {
158
+ const n = Math.min(ch.length - read, this.buf.length - this.len);
159
+ this.buf.set(ch.subarray(read, read + n), this.len);
160
+ this.len += n;
161
+ read += n;
162
+ if (this.len === this.buf.length) {
163
+ const out = this.buf;
164
+ this.port.postMessage({ event: "chunk", samples: out }, [out.buffer]);
165
+ this.buf = new Float32Array(out.length);
166
+ this.len = 0;
167
+ }
168
+ }
169
+ return true;
170
+ }
171
+ });
172
+ `)}`;
173
+ /** Clamp-and-convert one Float32 capture batch to PCM16. */
174
+ function floatToPcm16(samples) {
175
+ const pcm = new Int16Array(samples.length);
176
+ let i = 0;
177
+ for (const sample of samples) {
178
+ const s = Math.max(-1, Math.min(1, sample));
179
+ pcm[i++] = s < 0 ? s * 32768 : s * 32767;
180
+ }
181
+ return pcm;
182
+ }
183
+ /**
184
+ * Open the microphone and stream endpointed utterances into a sync session.
185
+ *
186
+ * @throws If microphone access is denied or worklet registration fails.
187
+ */
188
+ async function startSyncMicrophone(opts) {
189
+ const sampleRate = opts.sampleRate ?? 16e3;
190
+ const detector = createUtteranceDetector({
191
+ sampleRate,
192
+ ...opts.vad
193
+ });
194
+ const fail = (err) => {
195
+ opts.onError?.(err instanceof Error ? err : new Error(errorMessage(err)));
196
+ };
197
+ const streamPromise = navigator.mediaDevices.getUserMedia({ audio: {
198
+ echoCancellation: true,
199
+ noiseSuppression: true,
200
+ autoGainControl: true
201
+ } });
202
+ const ctx = new AudioContext({
203
+ sampleRate,
204
+ latencyHint: "interactive"
205
+ });
206
+ let stream;
207
+ try {
208
+ [stream] = await Promise.all([
209
+ streamPromise,
210
+ ctx.resume(),
211
+ ctx.audioWorklet.addModule(CAPTURE_WORKLET_DATA_URI)
212
+ ]);
213
+ } catch (err) {
214
+ streamPromise.then((s) => {
215
+ for (const t of s.getTracks()) t.stop();
216
+ }).catch(() => {});
217
+ await ctx.close().catch(() => {});
218
+ throw err;
219
+ }
220
+ const mic = ctx.createMediaStreamSource(stream);
221
+ const node = new AudioWorkletNode(ctx, "aai-sync-capture", {
222
+ channelCount: 1,
223
+ channelCountMode: "explicit",
224
+ processorOptions: { batchSamples: CAPTURE_BATCH_SAMPLES }
225
+ });
226
+ mic.connect(node);
227
+ let stopped = false;
228
+ let wasSpeaking = false;
229
+ function dispatch(utterance) {
230
+ if (!utterance) return;
231
+ opts.onSpeechEnd?.();
232
+ opts.session.sendPcm16(utterance, sampleRate).catch(fail);
233
+ }
234
+ function handleFrame(samples) {
235
+ if (stopped) return;
236
+ dispatch(detector.push(floatToPcm16(samples)));
237
+ if (detector.speaking && !wasSpeaking) opts.onSpeechStart?.();
238
+ wasSpeaking = detector.speaking;
239
+ }
240
+ node.port.onmessage = (e) => {
241
+ const data = e.data;
242
+ if (data.event === "chunk" && data.samples) handleFrame(data.samples);
243
+ };
244
+ node.onprocessorerror = () => fail(/* @__PURE__ */ new Error("Sync capture worklet crashed"));
245
+ return {
246
+ get speaking() {
247
+ return detector.speaking;
248
+ },
249
+ async stop() {
250
+ if (stopped) return;
251
+ stopped = true;
252
+ dispatch(detector.flush());
253
+ mic.disconnect();
254
+ node.disconnect();
255
+ for (const t of stream.getTracks()) t.stop();
256
+ await ctx.close().catch(() => {});
257
+ }
258
+ };
259
+ }
260
+ //#endregion
261
+ //#region sync-session.ts
262
+ /**
263
+ * Sync-mode browser session — HTTP turns, no WebSocket.
264
+ *
265
+ * The client half of the server's `POST /sync` endpoint (see
266
+ * `host/sync-turn.ts` in `@alexkroman1/aai`): each turn is one request
267
+ * carrying committed text or one endpointed utterance of PCM16 audio plus
268
+ * the conversation history, answered with the transcript, the reply text,
269
+ * and (when the agent's TTS provider supports one-shot synthesis) the
270
+ * spoken reply. The server holds no session state — this object owns the
271
+ * history and replays it every turn.
272
+ *
273
+ * Microphone capture and utterance endpointing live in `sync-mic.ts` /
274
+ * `sync-vad.ts`; this module is transport only, so it also runs in
275
+ * non-browser clients that bring their own audio.
276
+ */
277
+ /** Base64-encode PCM16 samples (chunked — `btoa` takes a binary string). */
278
+ function pcm16ToBase64(pcm) {
279
+ const bytes = new Uint8Array(pcm.buffer, pcm.byteOffset, pcm.byteLength);
280
+ let binary = "";
281
+ const CHUNK = 32768;
282
+ for (let i = 0; i < bytes.length; i += CHUNK) binary += String.fromCharCode(...bytes.subarray(i, i + CHUNK));
283
+ return btoa(binary);
284
+ }
285
+ /** Decode base64 PCM16LE (the sync response's `audio` field) into samples. */
286
+ function base64ToPcm16(base64) {
287
+ const binary = atob(base64);
288
+ const samples = binary.length >> 1;
289
+ const bytes = new Uint8Array(samples * 2);
290
+ for (let i = 0; i < bytes.length; i++) bytes[i] = binary.charCodeAt(i);
291
+ return new Int16Array(bytes.buffer, 0, samples);
292
+ }
293
+ /** Create a {@link SyncSession} against a sync-mode agent server. */
294
+ function createSyncSession(opts) {
295
+ const fetchFn = opts.fetch ?? ((input, init) => globalThis.fetch(input, init));
296
+ const history = [];
297
+ let queue = Promise.resolve();
298
+ async function parseTurn(resp) {
299
+ if (!resp.ok) {
300
+ const detail = safeJsonParse(await resp.text().catch(() => "")) ?? {};
301
+ throw new Error(`Sync turn failed: HTTP ${resp.status}${detail.error ? ` (${detail.error})` : ""}`);
302
+ }
303
+ const parsed = SyncTurnResponseSchema.safeParse(await resp.json());
304
+ if (!parsed.success) throw new Error("Sync turn failed: malformed server response");
305
+ return parsed.data;
306
+ }
307
+ async function runTurn(body) {
308
+ try {
309
+ const turn = await parseTurn(await fetchFn(opts.url, {
310
+ method: "POST",
311
+ headers: { "Content-Type": "application/json" },
312
+ body: JSON.stringify({
313
+ ...body,
314
+ history: [...history]
315
+ })
316
+ }));
317
+ history.push({
318
+ role: "user",
319
+ content: turn.transcript
320
+ });
321
+ if (turn.reply.length > 0) history.push({
322
+ role: "assistant",
323
+ content: turn.reply
324
+ });
325
+ const result = {
326
+ ...turn,
327
+ pcm: turn.audio !== void 0 ? base64ToPcm16(turn.audio) : null
328
+ };
329
+ opts.onTurn?.(result);
330
+ return result;
331
+ } catch (err) {
332
+ const wrapped = err instanceof Error ? err : new Error(errorMessage(err));
333
+ opts.onError?.(wrapped);
334
+ throw wrapped;
335
+ }
336
+ }
337
+ function enqueue(body) {
338
+ const next = queue.then(() => runTurn(body), () => runTurn(body));
339
+ queue = next.catch(() => void 0);
340
+ return next;
341
+ }
342
+ return {
343
+ get history() {
344
+ return history;
345
+ },
346
+ sendText(text) {
347
+ return enqueue({ text });
348
+ },
349
+ sendPcm16(pcm, sampleRate) {
350
+ return enqueue({
351
+ audio: pcm16ToBase64(pcm),
352
+ sampleRate
353
+ });
354
+ },
355
+ reset() {
356
+ history.length = 0;
357
+ }
358
+ };
359
+ }
360
+ //#endregion
361
+ export { ApiUrlChip, Button, CAPTURE_WORKLET_DATA_URI, ChatView, Controls, DEFAULT_SYNC_MIC_SAMPLE_RATE, MessageList, SessionProvider, SessionUrlChips, SidebarLayout, StartScreen, TextControls, ThemeProvider, ToolConfigContext, UiUrlChip, base64ToPcm16, client, createSessionCore, createSyncSession, createUtteranceDetector, floatToPcm16, pcm16ToBase64, startSyncMicrophone, useEvent, useSession, useSessionSelector, useTheme, useToolCallStart, useToolResult };