@alexkroman1/aai-ui 0.12.3 → 0.13.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 (201) hide show
  1. package/README.md +257 -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 +38 -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-cGFGID-2.js +1 -0
  78. package/dist/default-client/assets/capture-processor-BUABoo3_.js +90 -0
  79. package/dist/default-client/assets/index-8ihb1Urm.css +2 -0
  80. package/dist/default-client/assets/index-BQURVARl.js +126 -0
  81. package/dist/default-client/assets/playback-processor-ABOa11j0.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.d.ts +23 -0
  85. package/dist/define-client.d.ts +162 -26
  86. package/dist/define-client.js +201 -55
  87. package/dist/eyebrow-UfmSz9yy.js +27 -0
  88. package/dist/hooks-DN4gHYsj.js +182 -0
  89. package/dist/hooks.d.ts +172 -0
  90. package/dist/hooks.js +3 -0
  91. package/dist/hooks.test-d.d.ts +14 -0
  92. package/dist/inbox-protocol.d.ts +128 -0
  93. package/dist/inbox.d.ts +132 -0
  94. package/dist/index.d.ts +126 -29
  95. package/dist/index.js +5500 -15
  96. package/dist/internal.d.ts +43 -0
  97. package/dist/internal.js +8 -0
  98. package/dist/linked-client.d.ts +70 -0
  99. package/dist/message-list-DHZcLhRA.js +682 -0
  100. package/dist/notice-player.d.ts +70 -0
  101. package/dist/page.d.ts +114 -0
  102. package/dist/phone.d.ts +50 -0
  103. package/dist/route-fetch.d.ts +79 -0
  104. package/dist/session/audio-effects.d.ts +31 -0
  105. package/dist/session/audio-setup.d.ts +57 -0
  106. package/dist/session/audio-state.d.ts +157 -0
  107. package/dist/session/browser-session.d.ts +30 -0
  108. package/dist/session/close.d.ts +26 -0
  109. package/dist/session/dial.d.ts +65 -0
  110. package/dist/session/handshake.d.ts +60 -0
  111. package/dist/session/index.d.ts +17 -0
  112. package/dist/session/index.js +2 -0
  113. package/dist/session/messages.d.ts +63 -0
  114. package/dist/session/mic.d.ts +64 -0
  115. package/dist/session/preconnect.d.ts +48 -0
  116. package/dist/session/reconnect.d.ts +25 -0
  117. package/dist/session/resume-store.d.ts +57 -0
  118. package/dist/session/state.d.ts +146 -0
  119. package/dist/session/ticket.d.ts +44 -0
  120. package/dist/session/types.d.ts +476 -0
  121. package/dist/session/url.d.ts +17 -0
  122. package/dist/session/user-turn.d.ts +55 -0
  123. package/dist/session-CE0Ipsb3.js +2129 -0
  124. package/dist/stored-value.d.ts +103 -0
  125. package/dist/tool-call-block-uXtMOUWN.js +204 -0
  126. package/dist/tool-config-context-Es4YUzV2.js +19 -0
  127. package/dist/types.d.ts +272 -34
  128. package/dist/types.js +35 -4
  129. package/dist/upload/files.d.ts +83 -0
  130. package/dist/upload/index.d.ts +15 -0
  131. package/dist/upload/pause.d.ts +31 -0
  132. package/dist/upload/recall.d.ts +67 -0
  133. package/dist/upload/report.d.ts +26 -0
  134. package/dist/upload/session.d.ts +99 -0
  135. package/dist/url-chips-oyy_Eiqh.js +239 -0
  136. package/dist/use-client-runs.d.ts +87 -0
  137. package/dist/use-conversation-log.d.ts +86 -0
  138. package/dist/use-conversation.d.ts +131 -0
  139. package/dist/use-copy.d.ts +74 -0
  140. package/dist/use-download-url.d.ts +84 -0
  141. package/dist/use-flash.d.ts +61 -0
  142. package/dist/use-inbox.d.ts +101 -0
  143. package/dist/use-push-to-talk.d.ts +105 -0
  144. package/dist/use-route-mutation.d.ts +113 -0
  145. package/dist/use-route.d.ts +82 -0
  146. package/dist/use-run-key.d.ts +133 -0
  147. package/dist/use-session-controls.d.ts +69 -0
  148. package/dist/use-session-id.d.ts +57 -0
  149. package/dist/use-tap-to-talk.d.ts +138 -0
  150. package/dist/use-user-transcript-uyHhzy4d.js +73 -0
  151. package/dist/use-user-transcript.d.ts +75 -0
  152. package/dist/use-workflow-form.d.ts +285 -0
  153. package/dist/use-workflow-progress.d.ts +137 -0
  154. package/dist/use-workflow-run-DdhWmbx-.js +697 -0
  155. package/dist/use-workflow-run.d.ts +100 -0
  156. package/dist/use-workflow-runs.d.ts +71 -0
  157. package/dist/use-workflow-stream.d.ts +154 -0
  158. package/dist/use-workflows.d.ts +77 -0
  159. package/dist/workflow-client.d.ts +136 -0
  160. package/dist/workflow-def-types.d.ts +31 -0
  161. package/dist/workflow-events.d.ts +39 -0
  162. package/dist/workflow-field-kind.d.ts +95 -0
  163. package/dist/workflow-status-labels.d.ts +35 -0
  164. package/dist/worklets/_module-url.d.ts +10 -0
  165. package/dist/worklets/_playback-bench-harness.d.ts +181 -0
  166. package/dist/worklets/_playback-bench-host.d.ts +63 -0
  167. package/dist/worklets/_playback-bench-page.d.ts +65 -0
  168. package/dist/worklets/_tts-trace-harness.d.ts +142 -0
  169. package/dist/worklets/_worklet-test-utils.d.ts +27 -0
  170. package/dist/worklets/capture-processor.d.ts +4 -2
  171. package/dist/worklets/capture-processor.js +73 -33
  172. package/dist/worklets/playback-processor.d.ts +4 -2
  173. package/dist/worklets/playback-processor.js +258 -42
  174. package/package.json +57 -32
  175. package/styles.css +152 -30
  176. package/dist/audio.d.ts +0 -45
  177. package/dist/audio.js +0 -121
  178. package/dist/client-context.d.ts +0 -33
  179. package/dist/client-context.js +0 -15
  180. package/dist/client-handler.d.ts +0 -43
  181. package/dist/components/app.d.ts +0 -23
  182. package/dist/components/app.js +0 -41
  183. package/dist/components/error-banner.d.ts +0 -21
  184. package/dist/components/error-banner.js +0 -27
  185. package/dist/components/message-bubble.d.ts +0 -23
  186. package/dist/components/message-bubble.js +0 -35
  187. package/dist/components/state-indicator.d.ts +0 -29
  188. package/dist/components/state-indicator.js +0 -37
  189. package/dist/components/thinking-indicator.d.ts +0 -16
  190. package/dist/components/thinking-indicator.js +0 -34
  191. package/dist/components/tool-icons.d.ts +0 -17
  192. package/dist/components/tool-icons.js +0 -128
  193. package/dist/components/transcript.d.ts +0 -25
  194. package/dist/components/transcript.js +0 -35
  195. package/dist/session-CWB7vmqz.js +0 -429
  196. package/dist/session.d.ts +0 -69
  197. package/dist/session.js +0 -2
  198. package/dist/signals.d.ts +0 -120
  199. package/dist/signals.js +0 -161
  200. package/dist/tsdown.config.d.ts +0 -2
  201. package/dist/types.test-d.d.ts +0 -7
@@ -0,0 +1,314 @@
1
+ import{r as e}from"./index-BQURVARl.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-BQURVARl.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,23 @@
1
+ /** @jsxImportSource react */
2
+ /**
3
+ * The page every agent gets with NO `client.tsx` of its own — and which page
4
+ * follows the agent's declared front door.
5
+ *
6
+ * One bundle serves both, because the CLI serves one directory for every agent
7
+ * (`resolveClientDir`'s fallback to `defaultClientDir()`): the choice cannot be
8
+ * made at build time, so it is made here, from the `page` field
9
+ * `GET client-config` reports precisely so a browser knows before it dials.
10
+ *
11
+ * It used to call `mountClient({})` unconditionally. For a WORKFLOW APP that
12
+ * renders a start screen and then opens a `/websocket` the server declines by
13
+ * design — a dead page — which is why "you do not need a `client.tsx`" was only
14
+ * ever true of a voice agent. Both mounts have a default shell now, so it is
15
+ * true of either.
16
+ *
17
+ * The name is FORWARDED rather than looked up twice: on the platform this
18
+ * endpoint is the broker, so a request able to boot a sandbox should not be
19
+ * issued once here and again by the shell. Both shells treat an explicit name as
20
+ * final and skip their own lookup. An agent that declared none still costs the
21
+ * shell its own request, which is what it cost before.
22
+ */
23
+ import "../styles.css";
@@ -1,57 +1,193 @@
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 type { ClientTheme, VoiceSessionOptions } from "./types.ts";
5
6
  /**
6
- * Options for {@link defineClient}.
7
+ * Configuration passed to {@link mountClient}.
8
+ *
9
+ * The session-forwarded fields are picked from {@link VoiceSessionOptions}
10
+ * (one source of truth for types and docs) rather than re-declared — a
11
+ * re-declared copy is exactly how doc comments drift. It is NOT the session's
12
+ * own options type: that is {@link VoiceSessionOptions}, which
13
+ * `createBrowserSession` takes and which three of these fields come from.
14
+ *
15
+ * @remarks
16
+ * **One flat type, not a union of tiers.** `component` is what decides which
17
+ * shell renders — absent, the default one (StartScreen + ChatView, optional
18
+ * sidebar); present, the caller's own component inside the same providers —
19
+ * and that decision is made at runtime, where every field can be honoured. It
20
+ * used to be a union whose two arms banned each other's fields with `?: never`,
21
+ * and the failure that shape produces is recorded twice in this file's history:
22
+ * `mountClient({ name, component })` and `mountClient({ component, tools })` were both
23
+ * the natural thing to write, both were refused with *"Type 'string' is not
24
+ * assignable to type 'undefined'"*, and both cost a build round each time
25
+ * before the ban was lifted. What was left banned was `sidebar` beside a
26
+ * `component`, which invited the identical failure for a combination
27
+ * {@link mountClient} can simply render.
7
28
  *
8
29
  * @public
9
30
  */
10
- export type ClientOptions = {
31
+ export type ClientConfig = Pick<VoiceSessionOptions, "onSessionId" | "resumeSessionId" | "location" | "phone" | "client" | "token" | "WebSocket"> & {
11
32
  /** CSS selector or DOM element to render into. Defaults to `"#app"`. */
12
33
  target?: string | HTMLElement;
13
34
  /** Base URL of the AAI platform server. Derived from `location.href` by default. */
14
35
  platformUrl?: string;
15
- /** Agent title shown in the header and start screen. */
16
- title?: string;
17
36
  /** Theme color overrides. */
18
37
  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;
38
+ /**
39
+ * Full custom component to render instead of the default shell.
40
+ *
41
+ * It is rendered inside the same providers the default shell gets, so every
42
+ * session hook, `useTheme` and the tool display config work in it unchanged.
43
+ */
44
+ component?: ComponentType;
45
+ /**
46
+ * Agent name shown in the header and start screen — and, with a `component`,
47
+ * the page title, there being no shell header to put it in. Left out, the
48
+ * default shell asks the agent for its own declared name.
49
+ */
50
+ name?: string;
51
+ /**
52
+ * Optional sidebar component rendered alongside the main pane.
53
+ *
54
+ * Beside a `component` it is the custom component that becomes the main pane,
55
+ * in the same {@link SidebarLayout} the default shell uses.
56
+ */
57
+ sidebar?: ComponentType;
58
+ /** CSS width of the sidebar. Defaults to `"18rem"`. */
59
+ sidebarWidth?: string;
60
+ /**
61
+ * Which side the sidebar sits on. Defaults to `"left"`.
62
+ *
63
+ * Routed through the same {@link SidebarLayout} whether the main pane is the
64
+ * default shell or a `component`, for the reason `sidebar` itself is: the two
65
+ * branches build the same layout and a field honoured by only one of them is
66
+ * the shape this config used to have.
67
+ */
68
+ sidebarPosition?: "left" | "right";
69
+ /**
70
+ * Element rendered in place of the AAI logo — on the start card, and in the
71
+ * shell header once the session begins.
72
+ *
73
+ * Both, because they are one mark: an agent whose start screen shows a slice
74
+ * of pizza and whose header shows our logo reads as two products.
75
+ */
76
+ icon?: ReactNode;
77
+ /** A line under the title on the start card. */
78
+ subtitle?: string;
79
+ /** Label of the start CTA. Defaults to `"Start Conversation"`. */
80
+ buttonText?: string;
81
+ /**
82
+ * Tool display config: icon and label overrides keyed by tool name.
83
+ *
84
+ * Honoured with a custom `component` too: {@link mountClient} installs it into
85
+ * `ToolConfigContext`, and the consumer is `ToolCallBlock` — which a custom
86
+ * component renders as soon as it uses `MessageList` or `ChatView`, the usual
87
+ * way to build one.
88
+ */
89
+ tools?: ToolDisplayConfig;
25
90
  };
26
91
  /**
27
- * Handle returned by {@link defineClient} for cleanup.
92
+ * Handle returned by {@link mountClient} for cleanup.
28
93
  *
29
94
  * Implements `Disposable` so it can be used with `using`.
30
95
  *
31
96
  * @public
32
97
  */
33
98
  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. */
99
+ /** The underlying session core. */
100
+ session: BrowserSession;
101
+ /** Unmount the UI and disconnect the session. */
102
+ dispose(): void;
103
+ /** Alias for `dispose` for use with `using`. */
104
+ [Symbol.dispose](): void;
105
+ };
106
+ /**
107
+ * The element a mount renders into.
108
+ *
109
+ * Exported so `mountPage()` resolves its target the same way `mountClient()` does — the
110
+ * default selector and the "element not found" sentence are part of what an
111
+ * author has learned, and a second copy is how the two mounts come to disagree
112
+ * about which one they were given.
113
+ *
114
+ * @internal
115
+ */
116
+ export declare function resolveContainer(target?: string | HTMLElement): HTMLElement;
117
+ /**
118
+ * What a mount hands back: unmount, and `using` support.
119
+ *
120
+ * @internal
121
+ */
122
+ export type MountedRoot = {
123
+ /** Unmount the React tree (and, for `mountClient()`, dispose the session). */
39
124
  dispose(): void;
40
125
  /** Alias for `dispose` for use with `using`. */
41
126
  [Symbol.dispose](): void;
42
127
  };
128
+ /**
129
+ * Render `node` into `container` and hand back its teardown.
130
+ *
131
+ * Shared by {@link mountClient} and `mountPage()` because the plumbing is identical and
132
+ * the two decisions in it are not obvious enough to be re-derived: the root is
133
+ * created here rather than by the caller so nothing else can hold one, and the
134
+ * render is `flushSync` so the mount is observable to the caller's NEXT
135
+ * STATEMENT (and to a test) rather than scheduled — `mountClient()` returns a handle
136
+ * whose session is expected to be live, and `mountPage()` returns one a caller may
137
+ * dispose immediately.
138
+ *
139
+ * `onDispose` runs after the unmount, which is the order `mountClient()` needs: the
140
+ * tree comes down before the session under it goes away.
141
+ *
142
+ * @internal
143
+ */
144
+ export declare function mountRoot(container: HTMLElement, node: ReactNode, onDispose?: () => void): MountedRoot;
43
145
  /**
44
146
  * Define and mount a client UI for a voice agent.
45
147
  *
46
- * Creates a {@link VoiceSession}, wraps it in
47
- * {@link SessionSignals}, and renders the component
48
- * inside a {@link SessionProvider}.
148
+ * **Config only:** leave `component` out and the default shell renders
149
+ * (StartScreen + ChatView, optional sidebar).
150
+ *
151
+ * **A custom component:** pass `component` and it is rendered inside the same
152
+ * providers instead of the default shell — beside a `sidebar` if one is given,
153
+ * in the same {@link SidebarLayout}. A provided `name` then also sets
154
+ * `document.title`, there being no shell header to show it in.
155
+ *
156
+ * Mounts into `target` — a CSS selector or DOM element, defaulting to
157
+ * `"#app"` — and throws `Element not found: <target>` when the selector
158
+ * matches nothing.
159
+ *
160
+ * @example The default shell
161
+ * ```tsx
162
+ * import { mountClient } from "@alexkroman1/aai-ui";
163
+ *
164
+ * function OrderPanel() {
165
+ * return <div>Cart</div>;
166
+ * }
167
+ *
168
+ * mountClient({
169
+ * name: "Pizza Ordering",
170
+ * theme: { bg: "#1a1a1a", primary: "#e55" },
171
+ * sidebar: OrderPanel,
172
+ * tools: { add_pizza: { icon: "🍕", label: "Adding pizza" } },
173
+ * });
174
+ * ```
175
+ *
176
+ * @example A custom component
177
+ * ```tsx
178
+ * import { mountClient, useSession } from "@alexkroman1/aai-ui";
179
+ *
180
+ * function MyCustomApp() {
181
+ * const session = useSession();
182
+ * return <div>{session.state}</div>;
183
+ * }
184
+ *
185
+ * mountClient({ component: MyCustomApp });
186
+ * ```
49
187
  *
50
- * @param Component - The Preact component to render.
51
- * @param options - Client options (target element, platform URL, theme).
52
188
  * @returns A {@link ClientHandle} for cleanup.
53
189
  * @throws If the target element is not found in the DOM.
54
190
  *
55
191
  * @public
56
192
  */
57
- export declare function defineClient(Component: ComponentType<any>, options?: ClientOptions): ClientHandle;
193
+ export declare function mountClient(config: ClientConfig): ClientHandle;