@alexkroman1/aai-ui 0.12.3 → 0.14.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 (203) hide show
  1. package/README.md +253 -0
  2. package/dist/_colors-CpZO-88A.js +78 -0
  3. package/dist/_module-url-C13kAJ87.js +15 -0
  4. package/dist/_page-shell.d.ts +11 -0
  5. package/dist/_recover-run.d.ts +79 -0
  6. package/dist/_repeat-until.d.ts +30 -0
  7. package/dist/_run-controls.d.ts +33 -0
  8. package/dist/_submission-state.d.ts +92 -0
  9. package/dist/_tap-to-talk-state.d.ts +88 -0
  10. package/dist/_utils-Mi-SCFA1.js +47 -0
  11. package/dist/_utils.d.ts +26 -0
  12. package/dist/_web-storage.d.ts +43 -0
  13. package/dist/_workflow-api-ref.d.ts +37 -0
  14. package/dist/_workflow-files.d.ts +34 -0
  15. package/dist/_workflow-test-defs.d.ts +34 -0
  16. package/dist/aai-logo-D8pMVn9_.js +85 -0
  17. package/dist/agent-state-labels.d.ts +65 -0
  18. package/dist/agent-state.d.ts +108 -0
  19. package/dist/audio/capture.d.ts +44 -0
  20. package/dist/audio/index.d.ts +18 -0
  21. package/dist/audio/index.js +431 -0
  22. package/dist/audio/preconnect.d.ts +87 -0
  23. package/dist/audio/voice-io.d.ts +112 -0
  24. package/dist/build-default-client.d.ts +1 -0
  25. package/dist/chat-view-BCbI7gD4.js +396 -0
  26. package/dist/client-config-C7fQrioM.js +139 -0
  27. package/dist/client-config.d.ts +119 -0
  28. package/dist/client-dir.d.ts +44 -0
  29. package/dist/client-dir.js +58 -0
  30. package/dist/client-identity.d.ts +131 -0
  31. package/dist/client-tool.d.ts +51 -0
  32. package/dist/components/_colors.d.ts +73 -0
  33. package/dist/components/_form-readiness.d.ts +56 -0
  34. package/dist/components/_form-values.d.ts +19 -0
  35. package/dist/components/aai-logo.d.ts +12 -0
  36. package/dist/components/audio-result.d.ts +94 -0
  37. package/dist/components/auto-scroll.d.ts +74 -0
  38. package/dist/components/bullet-list.d.ts +74 -0
  39. package/dist/components/button.d.ts +29 -21
  40. package/dist/components/button.js +62 -23
  41. package/dist/components/chat-view.d.ts +24 -18
  42. package/dist/components/chat-view.js +2 -60
  43. package/dist/components/console-shell.d.ts +85 -0
  44. package/dist/components/controls.d.ts +21 -6
  45. package/dist/components/controls.js +31 -19
  46. package/dist/components/conversation-view.d.ts +130 -0
  47. package/dist/components/eyebrow.d.ts +12 -0
  48. package/dist/components/facts.d.ts +81 -0
  49. package/dist/components/form-fields.d.ts +142 -0
  50. package/dist/components/form-types.d.ts +73 -0
  51. package/dist/components/form.d.ts +96 -0
  52. package/dist/components/markdown.d.ts +57 -0
  53. package/dist/components/message-list.d.ts +34 -7
  54. package/dist/components/message-list.js +2 -75
  55. package/dist/components/session-controls.d.ts +125 -0
  56. package/dist/components/session-error-banner.d.ts +69 -0
  57. package/dist/components/session-state-dot.d.ts +111 -0
  58. package/dist/components/sidebar-layout.d.ts +31 -10
  59. package/dist/components/sidebar-layout.js +32 -12
  60. package/dist/components/start-screen.d.ts +17 -6
  61. package/dist/components/start-screen.js +34 -14
  62. package/dist/components/tool-call-block.d.ts +15 -14
  63. package/dist/components/tool-call-block.js +2 -117
  64. package/dist/components/tool-call-row.d.ts +75 -0
  65. package/dist/components/tool-config-context.d.ts +22 -0
  66. package/dist/components/upload-progress.d.ts +76 -0
  67. package/dist/components/url-chips.d.ts +29 -0
  68. package/dist/components/workflow-fields.d.ts +46 -0
  69. package/dist/components/workflow-pending-note.d.ts +81 -0
  70. package/dist/components/workflow-progress.d.ts +81 -0
  71. package/dist/components/workflow-run-error.d.ts +48 -0
  72. package/dist/components/workflow-run-panel.d.ts +79 -0
  73. package/dist/context.d.ts +288 -0
  74. package/dist/context.js +382 -0
  75. package/dist/conversation-log.d.ts +88 -0
  76. package/dist/default-client/assets/_module-url-BX0RuRU2.js +1 -0
  77. package/dist/default-client/assets/audio-mpgipo2Q.js +1 -0
  78. package/dist/default-client/assets/capture-processor-D0q7Qrdd.js +90 -0
  79. package/dist/default-client/assets/index-8ihb1Urm.css +2 -0
  80. package/dist/default-client/assets/index-DPfmSFmc.js +126 -0
  81. package/dist/default-client/assets/playback-processor-CjDbc9iH.js +314 -0
  82. package/dist/default-client/favicon.ico +0 -0
  83. package/dist/default-client/index.html +15 -0
  84. package/dist/default-client-entry.d.ts +6 -0
  85. package/dist/default-client.d.ts +19 -0
  86. package/dist/define-client.d.ts +169 -26
  87. package/dist/define-client.js +209 -55
  88. package/dist/eyebrow-UfmSz9yy.js +27 -0
  89. package/dist/hooks-DN4gHYsj.js +182 -0
  90. package/dist/hooks.d.ts +172 -0
  91. package/dist/hooks.js +3 -0
  92. package/dist/hooks.test-d.d.ts +14 -0
  93. package/dist/inbox-protocol.d.ts +128 -0
  94. package/dist/inbox.d.ts +132 -0
  95. package/dist/index.d.ts +126 -29
  96. package/dist/index.js +5500 -15
  97. package/dist/internal.d.ts +43 -0
  98. package/dist/internal.js +8 -0
  99. package/dist/linked-client.d.ts +70 -0
  100. package/dist/message-list-DHZcLhRA.js +682 -0
  101. package/dist/notice-player.d.ts +70 -0
  102. package/dist/page.d.ts +114 -0
  103. package/dist/phone.d.ts +50 -0
  104. package/dist/route-fetch.d.ts +79 -0
  105. package/dist/session/audio-effects.d.ts +37 -0
  106. package/dist/session/audio-setup.d.ts +57 -0
  107. package/dist/session/audio-state.d.ts +157 -0
  108. package/dist/session/browser-session.d.ts +39 -0
  109. package/dist/session/close.d.ts +26 -0
  110. package/dist/session/dial.d.ts +65 -0
  111. package/dist/session/handshake.d.ts +60 -0
  112. package/dist/session/index.d.ts +17 -0
  113. package/dist/session/index.js +2 -0
  114. package/dist/session/messages.d.ts +63 -0
  115. package/dist/session/mic.d.ts +64 -0
  116. package/dist/session/preconnect.d.ts +48 -0
  117. package/dist/session/reconnect.d.ts +25 -0
  118. package/dist/session/resume-store.d.ts +57 -0
  119. package/dist/session/state.d.ts +146 -0
  120. package/dist/session/ticket.d.ts +44 -0
  121. package/dist/session/types.d.ts +476 -0
  122. package/dist/session/url.d.ts +17 -0
  123. package/dist/session/user-turn.d.ts +55 -0
  124. package/dist/session-D9jM5-TF.js +2140 -0
  125. package/dist/stored-value.d.ts +103 -0
  126. package/dist/tool-call-block-uXtMOUWN.js +204 -0
  127. package/dist/tool-config-context-Es4YUzV2.js +19 -0
  128. package/dist/types.d.ts +272 -34
  129. package/dist/types.js +35 -4
  130. package/dist/upload/files.d.ts +83 -0
  131. package/dist/upload/index.d.ts +15 -0
  132. package/dist/upload/pause.d.ts +31 -0
  133. package/dist/upload/recall.d.ts +67 -0
  134. package/dist/upload/report.d.ts +26 -0
  135. package/dist/upload/session.d.ts +99 -0
  136. package/dist/url-chips-oyy_Eiqh.js +239 -0
  137. package/dist/use-client-runs.d.ts +87 -0
  138. package/dist/use-conversation-log.d.ts +86 -0
  139. package/dist/use-conversation.d.ts +131 -0
  140. package/dist/use-copy.d.ts +74 -0
  141. package/dist/use-download-url.d.ts +84 -0
  142. package/dist/use-flash.d.ts +61 -0
  143. package/dist/use-inbox.d.ts +101 -0
  144. package/dist/use-push-to-talk.d.ts +105 -0
  145. package/dist/use-route-mutation.d.ts +113 -0
  146. package/dist/use-route.d.ts +82 -0
  147. package/dist/use-run-key.d.ts +133 -0
  148. package/dist/use-session-controls.d.ts +69 -0
  149. package/dist/use-session-id.d.ts +57 -0
  150. package/dist/use-tap-to-talk.d.ts +138 -0
  151. package/dist/use-user-transcript-uyHhzy4d.js +73 -0
  152. package/dist/use-user-transcript.d.ts +75 -0
  153. package/dist/use-workflow-form.d.ts +285 -0
  154. package/dist/use-workflow-progress.d.ts +137 -0
  155. package/dist/use-workflow-run-DdhWmbx-.js +697 -0
  156. package/dist/use-workflow-run.d.ts +100 -0
  157. package/dist/use-workflow-runs.d.ts +71 -0
  158. package/dist/use-workflow-stream.d.ts +154 -0
  159. package/dist/use-workflows.d.ts +77 -0
  160. package/dist/workflow-client.d.ts +136 -0
  161. package/dist/workflow-def-types.d.ts +31 -0
  162. package/dist/workflow-events.d.ts +39 -0
  163. package/dist/workflow-field-kind.d.ts +95 -0
  164. package/dist/workflow-status-labels.d.ts +35 -0
  165. package/dist/worklets/_module-url.d.ts +10 -0
  166. package/dist/worklets/_playback-bench-harness.d.ts +181 -0
  167. package/dist/worklets/_playback-bench-host.d.ts +63 -0
  168. package/dist/worklets/_playback-bench-page.d.ts +65 -0
  169. package/dist/worklets/_tts-trace-harness.d.ts +142 -0
  170. package/dist/worklets/_worklet-test-utils.d.ts +27 -0
  171. package/dist/worklets/capture-processor.d.ts +4 -2
  172. package/dist/worklets/capture-processor.js +73 -33
  173. package/dist/worklets/playback-processor.d.ts +4 -2
  174. package/dist/worklets/playback-processor.js +258 -42
  175. package/package.json +59 -32
  176. package/styles.css +152 -30
  177. package/dist/_jsdom-setup.d.ts +0 -0
  178. package/dist/audio.d.ts +0 -45
  179. package/dist/audio.js +0 -121
  180. package/dist/client-context.d.ts +0 -33
  181. package/dist/client-context.js +0 -15
  182. package/dist/client-handler.d.ts +0 -43
  183. package/dist/components/app.d.ts +0 -23
  184. package/dist/components/app.js +0 -41
  185. package/dist/components/error-banner.d.ts +0 -21
  186. package/dist/components/error-banner.js +0 -27
  187. package/dist/components/message-bubble.d.ts +0 -23
  188. package/dist/components/message-bubble.js +0 -35
  189. package/dist/components/state-indicator.d.ts +0 -29
  190. package/dist/components/state-indicator.js +0 -37
  191. package/dist/components/thinking-indicator.d.ts +0 -16
  192. package/dist/components/thinking-indicator.js +0 -34
  193. package/dist/components/tool-icons.d.ts +0 -17
  194. package/dist/components/tool-icons.js +0 -128
  195. package/dist/components/transcript.d.ts +0 -25
  196. package/dist/components/transcript.js +0 -35
  197. package/dist/session-CWB7vmqz.js +0 -429
  198. package/dist/session.d.ts +0 -69
  199. package/dist/session.js +0 -2
  200. package/dist/signals.d.ts +0 -120
  201. package/dist/signals.js +0 -161
  202. package/dist/tsdown.config.d.ts +0 -2
  203. package/dist/types.test-d.d.ts +0 -7
@@ -0,0 +1,314 @@
1
+ import{r as e}from"./index-DPfmSFmc.js";import{t}from"./_module-url-BX0RuRU2.js";var n=`
2
+ class PlaybackProcessor extends AudioWorkletProcessor {
3
+ constructor(options) {
4
+ super();
5
+ const opts = options.processorOptions || {};
6
+ // The node's context IS the playback context (audio/voice-io.ts asserts its rate),
7
+ // so the worklet-global sampleRate is authoritative; the option exists
8
+ // for the node-less test harness.
9
+ const rate = opts.sampleRate ?? sampleRate;
10
+ // Kept, because every derived quantity below reads it and \`bufferedMs\` in
11
+ // process() is the one that used to read the worklet global instead. Those
12
+ // agree in production (the option is unset, so \`rate\` IS \`sampleRate\`) and
13
+ // diverge only in the node-less harness — which is the one place a spec
14
+ // could ever assert on the reported backlog, so the divergence made the
15
+ // number untestable rather than wrong.
16
+ this.rate = rate;
17
+ // The one fill target — for the start of a turn and for recovery after an
18
+ // underrun alike. If 'done' arrives first (short utterance), start
19
+ // immediately instead of waiting for audio that is never coming.
20
+ this.fillSamples = Math.floor((rate * (opts.fillMs ?? 200)) / 1000);
21
+ // Concealment source: a ring of the most recently played samples, looped
22
+ // under a decaying gain to cover a gap. Sized to the fade window, with a
23
+ // per-sample decay that reaches the floor exactly at its end.
24
+ this.concealCapacity = Math.max(1, Math.floor((rate * 40) / 1000));
25
+ this.concealBuf = new Float32Array(this.concealCapacity);
26
+ this.concealDecay = Math.exp(Math.log(${e}) / this.concealCapacity);
27
+ // Float32 ring buffer — PLAYBACK_BUFFER_SECONDS at the context sample
28
+ // rate. Allocated once for the node's lifetime; per-turn state resets via
29
+ // resetTurn(). writePos and readPos are absolute (monotonic) sample
30
+ // counts; the buffer is indexed modulo capacity so a longer reply keeps
31
+ // playing instead of writing past the end and going silent.
32
+ this.capacity = rate * 60;
33
+ this.samples = new Float32Array(this.capacity);
34
+ // Cadence for the 'progress' report in process(), counted in samples so
35
+ // the hot path never reads a clock. Survives resetTurn(): the host clamps
36
+ // upward only, so a stale count costs at most one extra report.
37
+ this.reportIntervalSamples = Math.floor((rate * 500) / 1000);
38
+ this.sinceReportSamples = 0;
39
+ // Platform endianness probe: the wire format is PCM16 little-endian, so
40
+ // the Int16Array fast path in ingestBytes is only valid on LE hosts
41
+ // (every shipping browser target; the DataView path is the fallback).
42
+ this.littleEndian = new Uint8Array(new Uint16Array([1]).buffer)[0] === 1;
43
+ this.resetTurn();
44
+
45
+ this.port.onmessage = (e) => {
46
+ const d = e.data;
47
+ if (d.event === 'write') {
48
+ this.ingestBytes(d.buffer);
49
+ } else if (d.event === 'interrupt') {
50
+ // Applied eagerly, not deferred to the next process(): onmessage and
51
+ // process() never interleave (one audio thread), and a deferred
52
+ // interrupt would let 'write'/'done' frames for the NEXT turn
53
+ // coalesce in ahead of it and be wiped by the reset along with the
54
+ // cancelled turn's audio.
55
+ this.stopTurn('interrupt');
56
+ } else if (d.event === 'done') {
57
+ this.isDone = true;
58
+ // Echoed back on this turn's 'stop' so the host can tell WHICH turn
59
+ // drained: a stop already in flight when a barge-in lands would
60
+ // otherwise settle the next turn's done() the moment it arrives.
61
+ this.doneTurn = d.turn ?? null;
62
+ }
63
+ };
64
+ }
65
+
66
+ // Reset per-turn state so the node is reusable across replies without
67
+ // reallocating the sample buffer or re-instantiating the worklet.
68
+ resetTurn() {
69
+ this.isDone = false;
70
+ // Host turn id carried by this turn's 'done' (null until one arrives).
71
+ this.doneTurn = null;
72
+ this.playing = false;
73
+ // Whether any real audio has been rendered this turn. Separates a turn's
74
+ // pre-roll (nothing to extrapolate from, and not a defect) from a
75
+ // mid-turn underrun.
76
+ this.hasPlayed = false;
77
+ // Carry-over byte for split samples across chunks
78
+ this.carry = null;
79
+ this.writePos = 0;
80
+ this.readPos = 0;
81
+ // Concealment ring state and the current fade position.
82
+ this.concealLen = 0;
83
+ this.concealWrite = 0;
84
+ this.concealPos = 0;
85
+ this.concealGain = 1;
86
+ // Episode flags, so a multi-quantum gap counts as one event.
87
+ this.concealing = false;
88
+ this.concealedSilence = false;
89
+ // Reported to the host on 'stop'. A fresh object per turn: the one just
90
+ // posted must not be mutated by the next turn.
91
+ this.stats = {
92
+ concealedSamples: 0,
93
+ silentConcealedSamples: 0,
94
+ concealmentEvents: 0,
95
+ silentConcealmentEvents: 0,
96
+ };
97
+ }
98
+
99
+ // End the current turn: notify the host and rearm for the next reply.
100
+ // Must NOT return false from process() — a processor that stops is dead
101
+ // for good, forcing a new node (and buffer) per reply.
102
+ // \`reason\` ('interrupt' | 'done') tells the host which turn boundary this
103
+ // stop belongs to — interrupt-stops are dropped host-side, flush() having
104
+ // already settled that turn — and \`turn\` names WHICH turn drained (the id
105
+ // this turn's 'done' carried), so a drain-stop still in flight when a
106
+ // barge-in lands cannot settle the next turn's done() early.
107
+ stopTurn(reason) {
108
+ this.port.postMessage({ event: 'stop', reason, turn: this.doneTurn, stats: this.stats });
109
+ this.resetTurn();
110
+ }
111
+
112
+ // Cover a quantum (from \`start\`) where real audio should have been.
113
+ //
114
+ // Before the turn's first samples there is nothing to extrapolate from, so
115
+ // the gap is plain silence and counted as nothing — WebRTC likewise only
116
+ // counts concealment once playout has begun. After that, loop the retained
117
+ // tail under a decaying gain: a hard zero-fill is a discontinuity mid-word,
118
+ // which is the click that makes a brief stall sound like breakage.
119
+ coverGap(out, start) {
120
+ if (!this.hasPlayed) {
121
+ out.fill(0, start);
122
+ return;
123
+ }
124
+ if (!this.concealing) {
125
+ this.concealing = true;
126
+ this.concealedSilence = false;
127
+ this.stats.concealmentEvents++;
128
+ }
129
+ const total = out.length - start;
130
+ const len = this.concealLen;
131
+ let silent = 0;
132
+ // Second condition: once the fade has decayed to the floor, every sample
133
+ // the loop would emit is 0 anyway — bulk-fill instead of running 128
134
+ // branchy iterations per quantum for the whole tail of a long stall.
135
+ if (len === 0 || this.concealGain < ${e}) {
136
+ out.fill(0, start);
137
+ silent = total;
138
+ } else {
139
+ let g = this.concealGain;
140
+ for (let i = start; i < out.length; i++) {
141
+ if (g < ${e}) {
142
+ // The fade has run out: keep counting the gap, but stop looping a
143
+ // fragment that is now inaudible anyway.
144
+ out[i] = 0;
145
+ silent++;
146
+ continue;
147
+ }
148
+ out[i] = this.concealBuf[this.concealPos] * g;
149
+ this.concealPos = this.concealPos + 1 === len ? 0 : this.concealPos + 1;
150
+ g *= this.concealDecay;
151
+ }
152
+ this.concealGain = g;
153
+ }
154
+ this.stats.concealedSamples += total;
155
+ if (silent > 0) {
156
+ this.stats.silentConcealedSamples += silent;
157
+ if (!this.concealedSilence) {
158
+ this.concealedSilence = true;
159
+ this.stats.silentConcealmentEvents++;
160
+ }
161
+ }
162
+ }
163
+
164
+ // Retain the tail of a rendered quantum as the next gap's concealment
165
+ // source, and close any episode the real audio just ended.
166
+ rememberTail(out, n) {
167
+ const cap = this.concealCapacity;
168
+ const take = Math.min(n, cap);
169
+ // Bulk copies (this runs on every cleanly rendered quantum): the tail is
170
+ // one contiguous source run, landing in at most two ring runs.
171
+ const tail = out.subarray(n - take, n);
172
+ const first = Math.min(take, cap - this.concealWrite);
173
+ this.concealBuf.set(tail.subarray(0, first), this.concealWrite);
174
+ if (take > first) this.concealBuf.set(tail.subarray(first), 0);
175
+ this.concealWrite = (this.concealWrite + take) % cap;
176
+ this.concealLen = Math.min(cap, this.concealLen + take);
177
+ // Read the loop oldest-first; once the ring is full the write cursor is
178
+ // the oldest retained sample.
179
+ this.concealPos = this.concealLen === cap ? this.concealWrite : 0;
180
+ this.concealing = false;
181
+ this.concealGain = 1;
182
+ }
183
+
184
+ ingestBytes(uint8) {
185
+ let bytes = uint8;
186
+
187
+ if (this.carry !== null) {
188
+ const merged = new Uint8Array(1 + bytes.length);
189
+ merged[0] = this.carry;
190
+ merged.set(bytes, 1);
191
+ bytes = merged;
192
+ this.carry = null;
193
+ }
194
+
195
+ if (bytes.length % 2 !== 0) {
196
+ this.carry = bytes[bytes.length - 1];
197
+ bytes = bytes.subarray(0, bytes.length - 1);
198
+ }
199
+
200
+ if (bytes.length === 0) return;
201
+ const numSamples = bytes.length / 2;
202
+ const cap = this.capacity;
203
+ const samples = this.samples;
204
+ if (this.littleEndian && (bytes.byteOffset & 1) === 0) {
205
+ // Fast path: 2-byte-aligned LE bytes wrap directly as an Int16Array;
206
+ // copy wrap-aware in at most two runs with no per-sample DataView call
207
+ // or modulo. This runs on the realtime audio thread.
208
+ const int16 = new Int16Array(bytes.buffer, bytes.byteOffset, numSamples);
209
+ let src = 0;
210
+ let dst = this.writePos % cap;
211
+ while (src < numSamples) {
212
+ const run = Math.min(numSamples - src, cap - dst);
213
+ for (let j = 0; j < run; j++) {
214
+ samples[dst + j] = int16[src + j] / 0x8000;
215
+ }
216
+ src += run;
217
+ dst = 0;
218
+ }
219
+ } else {
220
+ // Odd byte offset (or big-endian host): fall back to per-sample reads.
221
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.length);
222
+ let dst = this.writePos % cap;
223
+ for (let i = 0; i < numSamples; i++) {
224
+ samples[dst] = view.getInt16(i * 2, true) / 0x8000;
225
+ dst++;
226
+ if (dst === cap) dst = 0;
227
+ }
228
+ }
229
+ this.writePos += numSamples;
230
+ // If the producer outran the consumer by more than the buffer holds, drop
231
+ // the oldest unplayed audio rather than reading samples we've overwritten.
232
+ if (this.writePos - this.readPos > this.capacity) {
233
+ this.readPos = this.writePos - this.capacity;
234
+ }
235
+ }
236
+
237
+ process(inputs, outputs) {
238
+ // No output wired up yet — nothing to render this quantum. Throwing here
239
+ // would permanently kill the processor (the node is persistent per
240
+ // session), so guard like the capture processor does.
241
+ if (!outputs[0] || !outputs[0][0]) return true;
242
+ const out = outputs[0][0];
243
+ const avail = this.writePos - this.readPos;
244
+
245
+ // Report the unplayed backlog to the host (\`playback_progress\`). This IS
246
+ // the closed-loop signal: the host otherwise models playback open-loop —
247
+ // every forwarded chunk assumed to start playing on arrival at exactly
248
+ // 1.0x — and cannot see a buffer that has run ahead of the wall clock.
249
+ // Measured against a client draining at 0.6x, the host declared the line
250
+ // silent while it still held seconds of the reply, and then opened the
251
+ // speaking-edge gate over speech the caller had not heard.
252
+ //
253
+ // Sent from process() rather than a timer because this is the only place
254
+ // that sees the buffer on the audio thread; the counter is in QUANTA, so
255
+ // it costs an integer compare per render and no clock read. Silence is not
256
+ // reported: an empty buffer is what the host already assumes.
257
+ this.sinceReportSamples += out.length;
258
+ if (this.sinceReportSamples >= this.reportIntervalSamples) {
259
+ this.sinceReportSamples = 0;
260
+ if (avail > 0) {
261
+ this.port.postMessage({ event: 'progress', bufferedMs: (avail / this.rate) * 1000 });
262
+ }
263
+ }
264
+
265
+ // Filling: wait for the target. 'done' short-circuits it — what is
266
+ // buffered is all there will be, so there is nothing left to wait for.
267
+ if (!this.playing) {
268
+ if (avail >= this.fillSamples || this.isDone) {
269
+ this.playing = true;
270
+ } else {
271
+ this.coverGap(out, 0);
272
+ return true;
273
+ }
274
+ }
275
+
276
+ // Underrun: this quantum cannot be filled and more audio is still coming.
277
+ // Go back to filling and cover the gap, leaving
278
+ // readPos untouched — the fragment stays buffered and plays intact once
279
+ // the buffer recovers, instead of being dribbled out a few samples at a
280
+ // time for the rest of the turn.
281
+ if (avail < out.length && !this.isDone) {
282
+ this.playing = false;
283
+ this.coverGap(out, 0);
284
+ return true;
285
+ }
286
+
287
+ if (avail > 0) {
288
+ const n = Math.min(avail, out.length);
289
+ // Copy from the ring buffer, splitting across the wrap boundary.
290
+ const start = this.readPos % this.capacity;
291
+ const first = Math.min(n, this.capacity - start);
292
+ out.set(this.samples.subarray(start, start + first), 0);
293
+ if (n > first) out.set(this.samples.subarray(0, n - first), first);
294
+ this.readPos += n;
295
+ // Only reachable with n < out.length on the turn's final partial
296
+ // quantum (the underrun branch above catches every other case).
297
+ out.fill(0, n);
298
+ this.hasPlayed = true;
299
+ this.rememberTail(out, n);
300
+ return true;
301
+ }
302
+
303
+ // Drained and done: end the turn. Not reachable mid-turn — an empty
304
+ // buffer with audio still coming is the underrun branch above.
305
+ out.fill(0);
306
+ if (this.isDone) {
307
+ this.stopTurn('done');
308
+ }
309
+ return true;
310
+ }
311
+ }
312
+
313
+ registerProcessor('playback-processor', PlaybackProcessor);
314
+ `,r=t(n);export{r as default};
Binary file
@@ -0,0 +1,15 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
6
+ <title>aai</title>
7
+ <link rel="icon" href="./favicon.ico" />
8
+ <style>html,body{background:#fbf8f2;margin:0}</style>
9
+ <script type="module" crossorigin src="./assets/index-DPfmSFmc.js"></script>
10
+ <link rel="stylesheet" crossorigin href="./assets/index-8ihb1Urm.css">
11
+ </head>
12
+ <body>
13
+ <main id="app"></main>
14
+ </body>
15
+ </html>
@@ -0,0 +1,6 @@
1
+ /**
2
+ * The prebuilt default client's bundle entry (`index.html` loads it): the
3
+ * stylesheet plus one call. The choice of page lives in `default-client.tsx`,
4
+ * where a spec can drive it without importing a side effect.
5
+ */
6
+ import "../styles.css";
@@ -0,0 +1,19 @@
1
+ /** The two mounts the default client chooses between, injectable for a spec. */
2
+ export type DefaultClientMounts = {
3
+ mountClient: (config: {
4
+ name?: string;
5
+ }) => unknown;
6
+ mountPage: (config: {
7
+ name?: string;
8
+ }) => unknown;
9
+ };
10
+ /**
11
+ * Look up the agent's front door and mount the matching shell.
12
+ *
13
+ * A function rather than the module's own side effect so the CHOICE can be
14
+ * driven with fake mounts; `default-client-entry.tsx` is the bundle entry that
15
+ * calls it. The lookup already degrades to "the agent declared nothing" on
16
+ * every failure path — which resolves to `page: "voice"`, the front door this
17
+ * can always mount.
18
+ */
19
+ export declare function bootDefaultClient(mounts?: DefaultClientMounts): Promise<void>;
@@ -1,57 +1,200 @@
1
- import type { ComponentType } from "preact";
2
- import { type ClientTheme } from "./client-context.ts";
3
- import { type VoiceSession, type WebSocketConstructor } from "./session.ts";
4
- import { type SessionSignals } from "./signals.ts";
1
+ /** @jsxImportSource react */
2
+ import { type ComponentType, type ReactNode } from "react";
3
+ import { type ToolDisplayConfig } from "./components/tool-config-context.ts";
4
+ import type { BrowserSession } from "./session/index.ts";
5
+ import { createBrowserSession } from "./session/index.ts";
6
+ import type { ClientTheme, VoiceSessionOptions } from "./types.ts";
5
7
  /**
6
- * Options for {@link defineClient}.
8
+ * Configuration passed to {@link mountClient}.
9
+ *
10
+ * The session-forwarded fields are picked from {@link VoiceSessionOptions}
11
+ * (one source of truth for types and docs) rather than re-declared — a
12
+ * re-declared copy is exactly how doc comments drift. It is NOT the session's
13
+ * own options type: that is {@link VoiceSessionOptions}, which
14
+ * `createBrowserSession` takes and which three of these fields come from.
15
+ *
16
+ * @remarks
17
+ * **One flat type, not a union of tiers.** `component` is what decides which
18
+ * shell renders — absent, the default one (StartScreen + ChatView, optional
19
+ * sidebar); present, the caller's own component inside the same providers —
20
+ * and that decision is made at runtime, where every field can be honoured. It
21
+ * used to be a union whose two arms banned each other's fields with `?: never`,
22
+ * and the failure that shape produces is recorded twice in this file's history:
23
+ * `mountClient({ name, component })` and `mountClient({ component, tools })` were both
24
+ * the natural thing to write, both were refused with *"Type 'string' is not
25
+ * assignable to type 'undefined'"*, and both cost a build round each time
26
+ * before the ban was lifted. What was left banned was `sidebar` beside a
27
+ * `component`, which invited the identical failure for a combination
28
+ * {@link mountClient} can simply render.
7
29
  *
8
30
  * @public
9
31
  */
10
- export type ClientOptions = {
32
+ export type ClientConfig = Pick<VoiceSessionOptions, "onSessionId" | "resumeSessionId" | "location" | "phone" | "client" | "token" | "WebSocket"> & {
11
33
  /** CSS selector or DOM element to render into. Defaults to `"#app"`. */
12
34
  target?: string | HTMLElement;
13
35
  /** Base URL of the AAI platform server. Derived from `location.href` by default. */
14
36
  platformUrl?: string;
15
- /** Agent title shown in the header and start screen. */
16
- title?: string;
17
37
  /** Theme color overrides. */
18
38
  theme?: ClientTheme;
19
- /** Called when the server sends a session ID. Store it for reconnection. */
20
- onSessionId?: ((sessionId: string) => void) | undefined;
21
- /** Session ID from a previous connection for resuming persisted state. */
22
- resumeSessionId?: string | undefined;
23
- /** WebSocket constructor override. Passed through to VoiceSessionOptions. */
24
- WebSocket?: WebSocketConstructor | undefined;
39
+ /**
40
+ * Full custom component to render instead of the default shell.
41
+ *
42
+ * It is rendered inside the same providers the default shell gets, so every
43
+ * session hook, `useTheme` and the tool display config work in it unchanged.
44
+ */
45
+ component?: ComponentType;
46
+ /**
47
+ * Agent name shown in the header and start screen — and, with a `component`,
48
+ * the page title, there being no shell header to put it in. Left out, the
49
+ * default shell asks the agent for its own declared name.
50
+ */
51
+ name?: string;
52
+ /**
53
+ * Optional sidebar component rendered alongside the main pane.
54
+ *
55
+ * Beside a `component` it is the custom component that becomes the main pane,
56
+ * in the same {@link SidebarLayout} the default shell uses.
57
+ */
58
+ sidebar?: ComponentType;
59
+ /** CSS width of the sidebar. Defaults to `"18rem"`. */
60
+ sidebarWidth?: string;
61
+ /**
62
+ * Which side the sidebar sits on. Defaults to `"left"`.
63
+ *
64
+ * Routed through the same {@link SidebarLayout} whether the main pane is the
65
+ * default shell or a `component`, for the reason `sidebar` itself is: the two
66
+ * branches build the same layout and a field honoured by only one of them is
67
+ * the shape this config used to have.
68
+ */
69
+ sidebarPosition?: "left" | "right";
70
+ /**
71
+ * Element rendered in place of the AAI logo — on the start card, and in the
72
+ * shell header once the session begins.
73
+ *
74
+ * Both, because they are one mark: an agent whose start screen shows a slice
75
+ * of pizza and whose header shows our logo reads as two products.
76
+ */
77
+ icon?: ReactNode;
78
+ /** A line under the title on the start card. */
79
+ subtitle?: string;
80
+ /** Label of the start CTA. Defaults to `"Start Conversation"`. */
81
+ buttonText?: string;
82
+ /**
83
+ * Tool display config: icon and label overrides keyed by tool name.
84
+ *
85
+ * Honoured with a custom `component` too: {@link mountClient} installs it into
86
+ * `ToolConfigContext`, and the consumer is `ToolCallBlock` — which a custom
87
+ * component renders as soon as it uses `MessageList` or `ChatView`, the usual
88
+ * way to build one.
89
+ */
90
+ tools?: ToolDisplayConfig;
25
91
  };
26
92
  /**
27
- * Handle returned by {@link defineClient} for cleanup.
93
+ * Handle returned by {@link mountClient} for cleanup.
28
94
  *
29
95
  * Implements `Disposable` so it can be used with `using`.
30
96
  *
31
97
  * @public
32
98
  */
33
99
  export type ClientHandle = {
34
- /** The underlying voice session. */
35
- session: VoiceSession;
36
- /** Reactive session controls for the mounted UI. */
37
- signals: SessionSignals;
38
- /** Unmount the UI, remove injected styles, and disconnect the session. */
100
+ /** The underlying session core. */
101
+ session: BrowserSession;
102
+ /** Unmount the UI and disconnect the session. */
103
+ dispose(): void;
104
+ /** Alias for `dispose` for use with `using`. */
105
+ [Symbol.dispose](): void;
106
+ };
107
+ /**
108
+ * The element a mount renders into.
109
+ *
110
+ * Exported so `mountPage()` resolves its target the same way `mountClient()` does — the
111
+ * default selector and the "element not found" sentence are part of what an
112
+ * author has learned, and a second copy is how the two mounts come to disagree
113
+ * about which one they were given.
114
+ *
115
+ * @internal
116
+ */
117
+ export declare function resolveContainer(target?: string | HTMLElement): HTMLElement;
118
+ /**
119
+ * What a mount hands back: unmount, and `using` support.
120
+ *
121
+ * @internal
122
+ */
123
+ export type MountedRoot = {
124
+ /** Unmount the React tree (and, for `mountClient()`, dispose the session). */
39
125
  dispose(): void;
40
126
  /** Alias for `dispose` for use with `using`. */
41
127
  [Symbol.dispose](): void;
42
128
  };
129
+ /**
130
+ * Render `node` into `container` and hand back its teardown.
131
+ *
132
+ * Shared by {@link mountClient} and `mountPage()` because the plumbing is identical and
133
+ * the two decisions in it are not obvious enough to be re-derived: the root is
134
+ * created here rather than by the caller so nothing else can hold one, and the
135
+ * render is `flushSync` so the mount is observable to the caller's NEXT
136
+ * STATEMENT (and to a test) rather than scheduled — `mountClient()` returns a handle
137
+ * whose session is expected to be live, and `mountPage()` returns one a caller may
138
+ * dispose immediately.
139
+ *
140
+ * `onDispose` runs after the unmount, which is the order `mountClient()` needs: the
141
+ * tree comes down before the session under it goes away.
142
+ *
143
+ * @internal
144
+ */
145
+ export declare function mountRoot(container: HTMLElement, node: ReactNode, onDispose?: () => void): MountedRoot;
43
146
  /**
44
147
  * Define and mount a client UI for a voice agent.
45
148
  *
46
- * Creates a {@link VoiceSession}, wraps it in
47
- * {@link SessionSignals}, and renders the component
48
- * inside a {@link SessionProvider}.
149
+ * **Config only:** leave `component` out and the default shell renders
150
+ * (StartScreen + ChatView, optional sidebar).
151
+ *
152
+ * **A custom component:** pass `component` and it is rendered inside the same
153
+ * providers instead of the default shell — beside a `sidebar` if one is given,
154
+ * in the same {@link SidebarLayout}. A provided `name` then also sets
155
+ * `document.title`, there being no shell header to show it in.
156
+ *
157
+ * Mounts into `target` — a CSS selector or DOM element, defaulting to
158
+ * `"#app"` — and throws `Element not found: <target>` when the selector
159
+ * matches nothing.
160
+ *
161
+ * @example The default shell
162
+ * ```tsx
163
+ * import { mountClient } from "@alexkroman1/aai-ui";
164
+ *
165
+ * function OrderPanel() {
166
+ * return <div>Cart</div>;
167
+ * }
168
+ *
169
+ * mountClient({
170
+ * name: "Pizza Ordering",
171
+ * theme: { bg: "#1a1a1a", primary: "#e55" },
172
+ * sidebar: OrderPanel,
173
+ * tools: { add_pizza: { icon: "🍕", label: "Adding pizza" } },
174
+ * });
175
+ * ```
176
+ *
177
+ * @example A custom component
178
+ * ```tsx
179
+ * import { mountClient, useSession } from "@alexkroman1/aai-ui";
180
+ *
181
+ * function MyCustomApp() {
182
+ * const session = useSession();
183
+ * return <div>{session.state}</div>;
184
+ * }
185
+ *
186
+ * mountClient({ component: MyCustomApp });
187
+ * ```
49
188
  *
50
- * @param Component - The Preact component to render.
51
- * @param options - Client options (target element, platform URL, theme).
52
189
  * @returns A {@link ClientHandle} for cleanup.
53
190
  * @throws If the target element is not found in the DOM.
54
191
  *
55
192
  * @public
56
193
  */
57
- export declare function defineClient(Component: ComponentType<any>, options?: ClientOptions): ClientHandle;
194
+ export declare function mountClient(config: ClientConfig): ClientHandle;
195
+ /**
196
+ * {@link mountClient} with the session factory handed in. Not on the barrel:
197
+ * it is the seam a spec drives with a fake core rather than replacing the
198
+ * session module.
199
+ */
200
+ export declare function mountClientWith(config: ClientConfig, createSession: typeof createBrowserSession): ClientHandle;