@alexkroman1/aai-ui 1.16.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/dist/_module-url-C_4gRVL0.js +15 -0
  2. package/dist/audio.d.ts +85 -11
  3. package/dist/audio.js +112 -83
  4. package/dist/chat-view-C1oJxsWz.js +129 -0
  5. package/dist/client-config.d.ts +6 -6
  6. package/dist/components/chat-view.d.ts +1 -8
  7. package/dist/components/chat-view.js +2 -2
  8. package/dist/components/console-shell.d.ts +37 -0
  9. package/dist/components/controls.js +1 -1
  10. package/dist/components/url-chips.d.ts +1 -4
  11. package/dist/context.d.ts +1 -1
  12. package/dist/context.js +1 -4
  13. package/dist/{controls-BbZcmnJf.js → controls-DV368uhb.js} +2 -5
  14. package/dist/default-client/assets/_module-url-BX0RuRU2.js +1 -0
  15. package/dist/default-client/assets/audio-BGHiiDY_.js +1 -0
  16. package/dist/default-client/assets/capture-processor-BLPsqnGl.js +90 -0
  17. package/dist/default-client/assets/{index-BunwIXSP.js → index-DXf7-M0Q.js} +156 -36
  18. package/dist/default-client/assets/index-Dv-Q5VRL.css +2 -0
  19. package/dist/default-client/assets/playback-processor-B_T_1KQP.js +278 -0
  20. package/dist/default-client/index.html +2 -2
  21. package/dist/define-client-Cyf1jqAl.js +150 -0
  22. package/dist/define-client.d.ts +0 -9
  23. package/dist/define-client.js +1 -1
  24. package/dist/index.d.ts +2 -5
  25. package/dist/index.js +7 -5
  26. package/dist/{session-core-B64kau_v.js → session-core-BM3WHeoY.js} +36 -128
  27. package/dist/session-core-messages.d.ts +0 -4
  28. package/dist/session-core-types.d.ts +1 -27
  29. package/dist/session-core.js +1 -1
  30. package/dist/types.d.ts +24 -1
  31. package/dist/types.js +32 -2
  32. package/dist/worklets/_module-url.d.ts +10 -0
  33. package/dist/worklets/capture-processor.d.ts +3 -3
  34. package/dist/worklets/capture-processor.js +35 -52
  35. package/dist/worklets/playback-processor.d.ts +3 -3
  36. package/dist/worklets/playback-processor.js +149 -26
  37. package/package.json +2 -2
  38. package/dist/chat-view-gi6FccZq.js +0 -193
  39. package/dist/components/sync-chat-view.d.ts +0 -18
  40. package/dist/components/text-controls.d.ts +0 -14
  41. package/dist/default-client/assets/audio-DNDZgEZp.js +0 -1
  42. package/dist/default-client/assets/capture-processor-UlKEKyIW.js +0 -108
  43. package/dist/default-client/assets/index-BogmeUln.css +0 -2
  44. package/dist/default-client/assets/playback-processor-C5HVRVbu.js +0 -156
  45. package/dist/define-client-DdpijqAu.js +0 -906
  46. package/dist/session-core-upload.d.ts +0 -16
  47. package/dist/sync-mic.d.ts +0 -93
  48. package/dist/sync-session.d.ts +0 -49
  49. package/dist/sync-vad.d.ts +0 -54
@@ -0,0 +1,2 @@
1
+ /*! tailwindcss v4.3.3 | MIT License | https://tailwindcss.com */
2
+ @layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-border-style:solid;--tw-leading:initial;--tw-font-weight:initial;--tw-tracking:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-duration:initial;--tw-ease:initial}}}@layer theme{:root,:host{--font-sans:-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";--font-mono:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;--spacing:.25rem;--text-xs:.75rem;--text-xs--line-height:calc(1 / .75);--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--font-weight-normal:400;--font-weight-medium:500;--tracking-wide:.025em;--radius-md:.375rem;--radius-lg:.5rem;--radius-xl:.75rem;--ease-in-out:cubic-bezier(.4, 0, .2, 1);--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono);--font-aai:"Monument Grotesk", "ABC Monument Grotesk", ui-sans-serif, system-ui, -apple-system, sans-serif;--font-aai-serif:"Source Serif 4", "Source Serif Pro", Charter, "Iowan Old Style", Georgia, serif;--font-aai-mono:"JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, monospace;--radius-aai:4px}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring:where(:not(iframe)){outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab, red, red)){::placeholder{color:color-mix(in oklab, currentcolor 50%, transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}html,body{margin:0;padding:0}}@layer components;@layer utilities{.collapse{visibility:collapse}.invisible{visibility:hidden}.absolute{position:absolute}.relative{position:relative}.static{position:static}.container{width:100%}@media (width>=40rem){.container{max-width:40rem}}@media (width>=48rem){.container{max-width:48rem}}@media (width>=64rem){.container{max-width:64rem}}@media (width>=80rem){.container{max-width:80rem}}@media (width>=96rem){.container{max-width:96rem}}.m-0{margin:0}.mx-auto{margin-inline:auto}.mt-2{margin-top:calc(var(--spacing) * 2)}.ml-auto{margin-left:auto}.box-border{box-sizing:border-box}.block{display:block}.flex{display:flex}.grid{display:grid}.inline{display:inline}.inline-flex{display:inline-flex}.h-1\.5{height:calc(var(--spacing) * 1.5)}.h-4{height:calc(var(--spacing) * 4)}.h-9{height:calc(var(--spacing) * 9)}.h-11{height:calc(var(--spacing) * 11)}.h-\[7px\]{height:7px}.h-screen{height:100vh}.max-h-64{max-height:calc(var(--spacing) * 64)}.min-h-0{min-height:0}.min-h-5{min-height:calc(var(--spacing) * 5)}.w-1\.5{width:calc(var(--spacing) * 1.5)}.w-4{width:calc(var(--spacing) * 4)}.w-\[7px\]{width:7px}.w-fit{width:fit-content}.w-full{width:100%}.max-w-75{max-width:calc(var(--spacing) * 75)}.max-w-105{max-width:calc(var(--spacing) * 105)}.max-w-190{max-width:calc(var(--spacing) * 190)}.max-w-\[60\%\]{max-width:60%}.max-w-\[82\%\]{max-width:82%}.max-w-\[min\(78\%\,64ch\)\]{max-width:min(78%,64ch)}.min-w-0{min-width:0}.flex-1{flex:1}.shrink-0{flex-shrink:0}.rotate-90{rotate:90deg}.cursor-pointer{cursor:pointer}.\[scrollbar-width\:none\]{scrollbar-width:none}.appearance-none{appearance:none}.flex-col{flex-direction:column}.items-center{align-items:center}.items-end{align-items:flex-end}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.justify-end{justify-content:flex-end}.gap-1{gap:var(--spacing)}.gap-1\.5{gap:calc(var(--spacing) * 1.5)}.gap-2{gap:calc(var(--spacing) * 2)}.gap-2\.5{gap:calc(var(--spacing) * 2.5)}.gap-3{gap:calc(var(--spacing) * 3)}.gap-4{gap:calc(var(--spacing) * 4)}.gap-5{gap:calc(var(--spacing) * 5)}.truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.overflow-auto{overflow:auto}.overflow-hidden{overflow:hidden}.overflow-y-auto{overflow-y:auto}.rounded-aai{border-radius:var(--radius-aai)}.rounded-full{border-radius:3.40282e38px}.rounded-lg{border-radius:var(--radius-lg)}.rounded-md{border-radius:var(--radius-md)}.rounded-xl{border-radius:var(--radius-xl)}.border{border-style:var(--tw-border-style);border-width:1px}.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.border-none{--tw-border-style:none;border-style:none}.bg-transparent{background-color:#0000}.p-7{padding:calc(var(--spacing) * 7)}.px-2{padding-inline:calc(var(--spacing) * 2)}.px-3\.5{padding-inline:calc(var(--spacing) * 3.5)}.px-5{padding-inline:calc(var(--spacing) * 5)}.px-6{padding-inline:calc(var(--spacing) * 6)}.px-7{padding-inline:calc(var(--spacing) * 7)}.px-10{padding-inline:calc(var(--spacing) * 10)}.py-1{padding-block:var(--spacing)}.py-2\.5{padding-block:calc(var(--spacing) * 2.5)}.py-3{padding-block:calc(var(--spacing) * 3)}.py-8{padding-block:calc(var(--spacing) * 8)}.py-12{padding-block:calc(var(--spacing) * 12)}.text-center{text-align:center}.text-left{text-align:left}.font-aai{font-family:var(--font-aai)}.font-aai-mono{font-family:var(--font-aai-mono)}.font-aai-serif{font-family:var(--font-aai-serif)}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.text-\[10px\]{font-size:10px}.text-\[11px\]{font-size:11px}.text-\[13px\]{font-size:13px}.text-\[15px\]{font-size:15px}.text-\[22px\]{font-size:22px}.text-\[32px\]{font-size:32px}.leading-4{--tw-leading:calc(var(--spacing) * 4);line-height:calc(var(--spacing) * 4)}.leading-\[1\.2\]{--tw-leading:1.2;line-height:1.2}.leading-\[1\.15\]{--tw-leading:1.15;line-height:1.15}.leading-\[22px\]{--tw-leading:22px;line-height:22px}.leading-\[23px\]{--tw-leading:23px;line-height:23px}.leading-\[130\%\]{--tw-leading:130%;line-height:130%}.leading-none{--tw-leading:1;line-height:1}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-normal{--tw-font-weight:var(--font-weight-normal);font-weight:var(--font-weight-normal)}.tracking-\[-0\.2px\]{--tw-tracking:-.2px;letter-spacing:-.2px}.tracking-\[1\.2px\]{--tw-tracking:1.2px;letter-spacing:1.2px}.tracking-\[1\.4px\]{--tw-tracking:1.4px;letter-spacing:1.4px}.tracking-wide{--tw-tracking:var(--tracking-wide);letter-spacing:var(--tracking-wide)}.text-balance{text-wrap:balance}.wrap-break-word{overflow-wrap:break-word}.whitespace-nowrap{white-space:nowrap}.whitespace-pre-wrap{white-space:pre-wrap}.uppercase{text-transform:uppercase}.ring{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.filter{filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.transition{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter,display,content-visibility,overlay,pointer-events;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-colors{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-transform{transition-property:transform,translate,scale,rotate;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.duration-150{--tw-duration:.15s;transition-duration:.15s}.ease-in-out{--tw-ease:var(--ease-in-out);transition-timing-function:var(--ease-in-out)}.outline-none{--tw-outline-style:none;outline-style:none}.select-none{-webkit-user-select:none;user-select:none}.disabled\:cursor-not-allowed:disabled{cursor:not-allowed}.disabled\:opacity-50:disabled{opacity:.5}@media (width>=40rem){.sm\:px-16{padding-inline:calc(var(--spacing) * 16)}.sm\:py-14{padding-block:calc(var(--spacing) * 14)}}}@keyframes aai-pulse{0%,to{opacity:1;transform:scale(1)}50%{opacity:.45;transform:scale(.82)}}@keyframes aai-bounce{0%,80%,to{opacity:.3;transform:scale(.8)}40%{opacity:1;transform:scale(1)}}@keyframes aai-shimmer{0%{background-position:-200% 0}to{background-position:200% 0}}.tool-shimmer{-webkit-text-fill-color:transparent;background:linear-gradient(90deg,currentColor 25%,#0000 50%,currentColor 75%) 0 0/200% 100%;-webkit-background-clip:text;background-clip:text;animation:2s infinite aai-shimmer}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-leading{syntax:"*";inherits:false}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-tracking{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"<length>";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false}@property --tw-duration{syntax:"*";inherits:false}@property --tw-ease{syntax:"*";inherits:false}
@@ -0,0 +1,278 @@
1
+ import{i as e}from"./index-DXf7-M0Q.js";import{t}from"./_module-url-BX0RuRU2.js";var n=t(`
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.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
+ // Fill target for the start of a turn. If 'done' arrives first (short
11
+ // utterance), start immediately instead of waiting for audio that is
12
+ // never coming.
13
+ this.jitterSamples = Math.floor((rate * (opts.jitterMs ?? 400)) / 1000);
14
+ // Fill target after an underrun — see PLAYBACK_REFILL_MS.
15
+ this.refillSamples = Math.floor((rate * (opts.refillMs ?? 200)) / 1000);
16
+ // Concealment source: a ring of the most recently played samples, looped
17
+ // under a decaying gain to cover a gap. Sized to the fade window, with a
18
+ // per-sample decay that reaches the floor exactly at its end.
19
+ this.concealCapacity = Math.max(1, Math.floor((rate * 40) / 1000));
20
+ this.concealBuf = new Float32Array(this.concealCapacity);
21
+ this.concealDecay = Math.exp(Math.log(${e}) / this.concealCapacity);
22
+ // Float32 ring buffer — PLAYBACK_BUFFER_SECONDS at the context sample
23
+ // rate. Allocated once for the node's lifetime; per-turn state resets via
24
+ // resetTurn(). writePos and readPos are absolute (monotonic) sample
25
+ // counts; the buffer is indexed modulo capacity so a longer reply keeps
26
+ // playing instead of writing past the end and going silent.
27
+ this.capacity = rate * 60;
28
+ this.samples = new Float32Array(this.capacity);
29
+ // Platform endianness probe: the wire format is PCM16 little-endian, so
30
+ // the Int16Array fast path in ingestBytes is only valid on LE hosts
31
+ // (every shipping browser target; the DataView path is the fallback).
32
+ this.littleEndian = new Uint8Array(new Uint16Array([1]).buffer)[0] === 1;
33
+ this.resetTurn();
34
+
35
+ this.port.onmessage = (e) => {
36
+ const d = e.data;
37
+ if (d.event === 'write') {
38
+ this.ingestBytes(d.buffer);
39
+ } else if (d.event === 'interrupt') {
40
+ // Applied eagerly, not deferred to the next process(): onmessage and
41
+ // process() never interleave (one audio thread), and a deferred
42
+ // interrupt would let 'write'/'done' frames for the NEXT turn
43
+ // coalesce in ahead of it and be wiped by the reset along with the
44
+ // cancelled turn's audio.
45
+ this.stopTurn('interrupt');
46
+ } else if (d.event === 'done') {
47
+ this.isDone = true;
48
+ }
49
+ };
50
+ }
51
+
52
+ // Reset per-turn state so the node is reusable across replies without
53
+ // reallocating the sample buffer or re-instantiating the worklet.
54
+ resetTurn() {
55
+ this.isDone = false;
56
+ this.playing = false;
57
+ // Whether any real audio has been rendered this turn. Separates a turn's
58
+ // pre-roll (nothing to extrapolate from, and not a defect) from a
59
+ // mid-turn underrun.
60
+ this.hasPlayed = false;
61
+ this.fillTarget = this.jitterSamples;
62
+ // Carry-over byte for split samples across chunks
63
+ this.carry = null;
64
+ this.writePos = 0;
65
+ this.readPos = 0;
66
+ // Concealment ring state and the current fade position.
67
+ this.concealLen = 0;
68
+ this.concealWrite = 0;
69
+ this.concealPos = 0;
70
+ this.concealGain = 1;
71
+ // Episode flags, so a multi-quantum gap counts as one event.
72
+ this.concealing = false;
73
+ this.concealedSilence = false;
74
+ // Reported to the host on 'stop'. A fresh object per turn: the one just
75
+ // posted must not be mutated by the next turn.
76
+ this.stats = {
77
+ concealedSamples: 0,
78
+ silentConcealedSamples: 0,
79
+ concealmentEvents: 0,
80
+ silentConcealmentEvents: 0,
81
+ };
82
+ }
83
+
84
+ // End the current turn: notify the host and rearm for the next reply.
85
+ // Must NOT return false from process() — a processor that stops is dead
86
+ // for good, forcing a new node (and buffer) per reply.
87
+ // \`reason\` ('interrupt' | 'done') tells the host which turn boundary this
88
+ // stop belongs to: interrupt-stops are dropped host-side (flush() already
89
+ // settled that turn), so they can never resolve a later turn's done() early.
90
+ stopTurn(reason) {
91
+ this.port.postMessage({ event: 'stop', reason, stats: this.stats });
92
+ this.resetTurn();
93
+ }
94
+
95
+ // Cover a quantum (from \`start\`) where real audio should have been.
96
+ //
97
+ // Before the turn's first samples there is nothing to extrapolate from, so
98
+ // the gap is plain silence and counted as nothing — WebRTC likewise only
99
+ // counts concealment once playout has begun. After that, loop the retained
100
+ // tail under a decaying gain: a hard zero-fill is a discontinuity mid-word,
101
+ // which is the click that makes a brief stall sound like breakage.
102
+ coverGap(out, start) {
103
+ if (!this.hasPlayed) {
104
+ out.fill(0, start);
105
+ return;
106
+ }
107
+ if (!this.concealing) {
108
+ this.concealing = true;
109
+ this.concealedSilence = false;
110
+ this.stats.concealmentEvents++;
111
+ }
112
+ const total = out.length - start;
113
+ const len = this.concealLen;
114
+ let silent = 0;
115
+ // Second condition: once the fade has decayed to the floor, every sample
116
+ // the loop would emit is 0 anyway — bulk-fill instead of running 128
117
+ // branchy iterations per quantum for the whole tail of a long stall.
118
+ if (len === 0 || this.concealGain < ${e}) {
119
+ out.fill(0, start);
120
+ silent = total;
121
+ } else {
122
+ let g = this.concealGain;
123
+ for (let i = start; i < out.length; i++) {
124
+ if (g < ${e}) {
125
+ // The fade has run out: keep counting the gap, but stop looping a
126
+ // fragment that is now inaudible anyway.
127
+ out[i] = 0;
128
+ silent++;
129
+ continue;
130
+ }
131
+ out[i] = this.concealBuf[this.concealPos] * g;
132
+ this.concealPos = this.concealPos + 1 === len ? 0 : this.concealPos + 1;
133
+ g *= this.concealDecay;
134
+ }
135
+ this.concealGain = g;
136
+ }
137
+ this.stats.concealedSamples += total;
138
+ if (silent > 0) {
139
+ this.stats.silentConcealedSamples += silent;
140
+ if (!this.concealedSilence) {
141
+ this.concealedSilence = true;
142
+ this.stats.silentConcealmentEvents++;
143
+ }
144
+ }
145
+ }
146
+
147
+ // Retain the tail of a rendered quantum as the next gap's concealment
148
+ // source, and close any episode the real audio just ended.
149
+ rememberTail(out, n) {
150
+ const cap = this.concealCapacity;
151
+ const take = Math.min(n, cap);
152
+ // Bulk copies (this runs on every cleanly rendered quantum): the tail is
153
+ // one contiguous source run, landing in at most two ring runs.
154
+ const tail = out.subarray(n - take, n);
155
+ const first = Math.min(take, cap - this.concealWrite);
156
+ this.concealBuf.set(tail.subarray(0, first), this.concealWrite);
157
+ if (take > first) this.concealBuf.set(tail.subarray(first), 0);
158
+ this.concealWrite = (this.concealWrite + take) % cap;
159
+ this.concealLen = Math.min(cap, this.concealLen + take);
160
+ // Read the loop oldest-first; once the ring is full the write cursor is
161
+ // the oldest retained sample.
162
+ this.concealPos = this.concealLen === cap ? this.concealWrite : 0;
163
+ this.concealing = false;
164
+ this.concealGain = 1;
165
+ }
166
+
167
+ ingestBytes(uint8) {
168
+ let bytes = uint8;
169
+
170
+ if (this.carry !== null) {
171
+ const merged = new Uint8Array(1 + bytes.length);
172
+ merged[0] = this.carry;
173
+ merged.set(bytes, 1);
174
+ bytes = merged;
175
+ this.carry = null;
176
+ }
177
+
178
+ if (bytes.length % 2 !== 0) {
179
+ this.carry = bytes[bytes.length - 1];
180
+ bytes = bytes.subarray(0, bytes.length - 1);
181
+ }
182
+
183
+ if (bytes.length === 0) return;
184
+ const numSamples = bytes.length / 2;
185
+ const cap = this.capacity;
186
+ const samples = this.samples;
187
+ if (this.littleEndian && (bytes.byteOffset & 1) === 0) {
188
+ // Fast path: 2-byte-aligned LE bytes wrap directly as an Int16Array;
189
+ // copy wrap-aware in at most two runs with no per-sample DataView call
190
+ // or modulo. This runs on the realtime audio thread.
191
+ const int16 = new Int16Array(bytes.buffer, bytes.byteOffset, numSamples);
192
+ let src = 0;
193
+ let dst = this.writePos % cap;
194
+ while (src < numSamples) {
195
+ const run = Math.min(numSamples - src, cap - dst);
196
+ for (let j = 0; j < run; j++) {
197
+ samples[dst + j] = int16[src + j] / 0x8000;
198
+ }
199
+ src += run;
200
+ dst = 0;
201
+ }
202
+ } else {
203
+ // Odd byte offset (or big-endian host): fall back to per-sample reads.
204
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.length);
205
+ let dst = this.writePos % cap;
206
+ for (let i = 0; i < numSamples; i++) {
207
+ samples[dst] = view.getInt16(i * 2, true) / 0x8000;
208
+ dst++;
209
+ if (dst === cap) dst = 0;
210
+ }
211
+ }
212
+ this.writePos += numSamples;
213
+ // If the producer outran the consumer by more than the buffer holds, drop
214
+ // the oldest unplayed audio rather than reading samples we've overwritten.
215
+ if (this.writePos - this.readPos > this.capacity) {
216
+ this.readPos = this.writePos - this.capacity;
217
+ }
218
+ }
219
+
220
+ process(inputs, outputs) {
221
+ // No output wired up yet — nothing to render this quantum. Throwing here
222
+ // would permanently kill the processor (the node is persistent per
223
+ // session), so guard like the capture processor does.
224
+ if (!outputs[0] || !outputs[0][0]) return true;
225
+ const out = outputs[0][0];
226
+ const avail = this.writePos - this.readPos;
227
+
228
+ // Filling: wait for the target. 'done' short-circuits it — what is
229
+ // buffered is all there will be, so there is nothing left to wait for.
230
+ if (!this.playing) {
231
+ if (avail >= this.fillTarget || this.isDone) {
232
+ this.playing = true;
233
+ } else {
234
+ this.coverGap(out, 0);
235
+ return true;
236
+ }
237
+ }
238
+
239
+ // Underrun: this quantum cannot be filled and more audio is still coming.
240
+ // Go back to filling (at the refill target) and cover the gap, leaving
241
+ // readPos untouched — the fragment stays buffered and plays intact once
242
+ // the buffer recovers, instead of being dribbled out a few samples at a
243
+ // time for the rest of the turn.
244
+ if (avail < out.length && !this.isDone) {
245
+ this.playing = false;
246
+ this.fillTarget = this.refillSamples;
247
+ this.coverGap(out, 0);
248
+ return true;
249
+ }
250
+
251
+ if (avail > 0) {
252
+ const n = Math.min(avail, out.length);
253
+ // Copy from the ring buffer, splitting across the wrap boundary.
254
+ const start = this.readPos % this.capacity;
255
+ const first = Math.min(n, this.capacity - start);
256
+ out.set(this.samples.subarray(start, start + first), 0);
257
+ if (n > first) out.set(this.samples.subarray(0, n - first), first);
258
+ this.readPos += n;
259
+ // Only reachable with n < out.length on the turn's final partial
260
+ // quantum (the underrun branch above catches every other case).
261
+ out.fill(0, n);
262
+ this.hasPlayed = true;
263
+ this.rememberTail(out, n);
264
+ return true;
265
+ }
266
+
267
+ // Drained and done: end the turn. Not reachable mid-turn — an empty
268
+ // buffer with audio still coming is the underrun branch above.
269
+ out.fill(0);
270
+ if (this.isDone) {
271
+ this.stopTurn('done');
272
+ }
273
+ return true;
274
+ }
275
+ }
276
+
277
+ registerProcessor('playback-processor', PlaybackProcessor);
278
+ `);export{n as default};
@@ -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-BunwIXSP.js"></script>
10
- <link rel="stylesheet" crossorigin href="./assets/index-BogmeUln.css">
9
+ <script type="module" crossorigin src="./assets/index-DXf7-M0Q.js"></script>
10
+ <link rel="stylesheet" crossorigin href="./assets/index-Dv-Q5VRL.css">
11
11
  </head>
12
12
  <body>
13
13
  <main id="app"></main>
@@ -0,0 +1,150 @@
1
+ import { SessionProvider, ThemeProvider } from "./context.js";
2
+ import { t as ChatView } from "./chat-view-C1oJxsWz.js";
3
+ import { n as ToolConfigContext } from "./tool-call-block-DIxpG8GM.js";
4
+ import { SidebarLayout } from "./components/sidebar-layout.js";
5
+ import { StartScreen } from "./components/start-screen.js";
6
+ import { t as createSessionCore } from "./session-core-BM3WHeoY.js";
7
+ import { CLIENT_CONFIG_PATH, ClientConfigResponseSchema } from "@alexkroman1/aai/protocol";
8
+ import { createElement, useEffect, useState } from "react";
9
+ import { jsx } from "react/jsx-runtime";
10
+ import { flushSync } from "react-dom";
11
+ import { createRoot } from "react-dom/client";
12
+ //#region client-config.ts
13
+ /**
14
+ * Pre-connection client-config lookup.
15
+ *
16
+ * `GET client-config` (relative to the agent's base URL — see
17
+ * `sdk/client-config.ts` in `@alexkroman1/aai`) gives the default client the
18
+ * agent's display name and greeting before any connection exists. Every
19
+ * failure path — network error, 404 from an older server, malformed body —
20
+ * degrades to the empty default, so this lookup can never break an existing
21
+ * agent.
22
+ */
23
+ /** Resolve a relative endpoint path against the agent's base URL. */
24
+ function buildAgentUrl(platformUrl, endpointPath) {
25
+ return new URL(endpointPath, platformUrl.endsWith("/") ? platformUrl : `${platformUrl}/`);
26
+ }
27
+ const AGENT_DEFAULT = {};
28
+ /** Fetch the agent's client config; any failure yields the agent default. */
29
+ async function fetchClientConfig(platformUrl, fetchFn) {
30
+ const doFetch = fetchFn ?? ((input, init) => globalThis.fetch(input, init));
31
+ try {
32
+ const resp = await doFetch(buildAgentUrl(platformUrl, CLIENT_CONFIG_PATH).href);
33
+ if (!resp.ok) return AGENT_DEFAULT;
34
+ const parsed = ClientConfigResponseSchema.safeParse(await resp.json());
35
+ return parsed.success ? parsed.data : AGENT_DEFAULT;
36
+ } catch {
37
+ return AGENT_DEFAULT;
38
+ }
39
+ }
40
+ //#endregion
41
+ //#region define-client.tsx
42
+ /** @jsxImportSource react */
43
+ function resolveContainer(target = "#app") {
44
+ if (typeof target !== "string") return target;
45
+ const el = document.querySelector(target);
46
+ if (!el) throw new Error(`Element not found: ${target}`);
47
+ return el;
48
+ }
49
+ /**
50
+ * Default shell rendered in config tier.
51
+ * Wraps StartScreen → (SidebarLayout →) ChatView.
52
+ */
53
+ function DefaultShell({ name, Sidebar, sidebarWidth }) {
54
+ const chat = /* @__PURE__ */ jsx(ChatView, { title: name });
55
+ return /* @__PURE__ */ jsx(StartScreen, {
56
+ title: name,
57
+ children: Sidebar ? /* @__PURE__ */ jsx(SidebarLayout, {
58
+ sidebar: /* @__PURE__ */ jsx(Sidebar, {}),
59
+ sidebarWidth,
60
+ children: chat
61
+ }) : chat
62
+ });
63
+ }
64
+ /**
65
+ * Config-tier root: fetches the server-declared display name via
66
+ * `GET client-config` and renders the chat shell. The shell renders
67
+ * immediately — optimistically — while the lookup is in flight; servers
68
+ * without the endpoint (every lookup failure resolves to the empty default)
69
+ * work exactly as before.
70
+ */
71
+ function DefaultRoot({ platformUrl, name, Sidebar, sidebarWidth }) {
72
+ const [resolved, setResolved] = useState(null);
73
+ useEffect(() => {
74
+ let cancelled = false;
75
+ fetchClientConfig(platformUrl).then((cfg) => {
76
+ if (!cancelled) setResolved(cfg);
77
+ });
78
+ return () => {
79
+ cancelled = true;
80
+ };
81
+ }, [platformUrl]);
82
+ return /* @__PURE__ */ jsx(DefaultShell, {
83
+ name: name ?? resolved?.name,
84
+ Sidebar,
85
+ sidebarWidth
86
+ });
87
+ }
88
+ /**
89
+ * Define and mount a client UI for a voice agent.
90
+ *
91
+ * **Tier 1 (config-only):** Pass options without `component` to get the
92
+ * default shell (StartScreen + ChatView, optional sidebar).
93
+ *
94
+ * **Tier 2 (custom component):** Pass `component` to render a fully custom
95
+ * root component inside the providers.
96
+ *
97
+ * @example Tier 1
98
+ * ```tsx
99
+ * client({
100
+ * name: "Pizza Ordering",
101
+ * theme: { bg: "#1a1a1a", primary: "#e55" },
102
+ * sidebar: OrderPanel,
103
+ * tools: { add_pizza: { icon: "🍕", label: "Adding pizza" } },
104
+ * });
105
+ * ```
106
+ *
107
+ * @example Tier 2
108
+ * ```tsx
109
+ * client({ component: MyCustomApp });
110
+ * ```
111
+ *
112
+ * @returns A {@link ClientHandle} for cleanup.
113
+ * @throws If the target element is not found in the DOM.
114
+ *
115
+ * @public
116
+ */
117
+ function client(config) {
118
+ const container = resolveContainer(config.target);
119
+ const platformUrl = config.platformUrl ?? globalThis.location.origin + globalThis.location.pathname;
120
+ const session = createSessionCore({
121
+ platformUrl,
122
+ onSessionId: config.onSessionId,
123
+ resumeSessionId: config.resumeSessionId,
124
+ WebSocket: config.WebSocket
125
+ });
126
+ const rootNode = config.component ? createElement(config.component) : createElement(DefaultRoot, {
127
+ platformUrl,
128
+ name: config.name,
129
+ Sidebar: config.sidebar,
130
+ sidebarWidth: config.sidebarWidth
131
+ });
132
+ const toolConfig = config.tools ?? {};
133
+ const root = createRoot(container);
134
+ flushSync(() => {
135
+ root.render(createElement(ToolConfigContext.Provider, { value: toolConfig }, createElement(ThemeProvider, { value: config.theme }, createElement(SessionProvider, { value: session }, rootNode))));
136
+ });
137
+ const handle = {
138
+ session,
139
+ dispose() {
140
+ root.unmount();
141
+ session[Symbol.dispose]();
142
+ },
143
+ [Symbol.dispose]() {
144
+ handle.dispose();
145
+ }
146
+ };
147
+ return handle;
148
+ }
149
+ //#endregion
150
+ export { buildAgentUrl as n, fetchClientConfig as r, client as t };
@@ -21,15 +21,6 @@ type BaseOptions = {
21
21
  resumeSessionId?: string;
22
22
  /** WebSocket constructor override. Passed through to session options. */
23
23
  WebSocket?: WebSocketConstructor;
24
- /**
25
- * Transport the UI talks over. Unset (the default) asks the server via
26
- * `GET client-config`, so `agent({ transport })` decides — an agent that
27
- * declared `transport: "sync"` gets the sync shell (HTTP turns, no
28
- * WebSocket) with no custom client needed. An explicit value here skips
29
- * the lookup. Only the config tier branches on it; a custom `component`
30
- * owns its own transport.
31
- */
32
- transport?: "websocket" | "sync";
33
24
  };
34
25
  /**
35
26
  * Tier 1: Config-only options. Renders the default shell (StartScreen + ChatView).
@@ -1,4 +1,4 @@
1
- import { t as client } from "./define-client-DdpijqAu.js";
1
+ import { t as client } from "./define-client-Cyf1jqAl.js";
2
2
  import "./context.js";
3
3
  import "./components/sidebar-layout.js";
4
4
  import "./components/start-screen.js";
package/dist/index.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ export { floatToPcm16 } from "./audio.ts";
1
2
  export { buildAgentUrl, type ClientConfigResponse, fetchClientConfig, } from "./client-config.ts";
2
3
  export { Button } from "./components/button.tsx";
3
4
  export { ChatView } from "./components/chat-view.tsx";
@@ -5,8 +6,6 @@ export { Controls } from "./components/controls.tsx";
5
6
  export { MessageList } from "./components/message-list.tsx";
6
7
  export { SidebarLayout } from "./components/sidebar-layout.tsx";
7
8
  export { StartScreen } from "./components/start-screen.tsx";
8
- export { SyncChatView } from "./components/sync-chat-view.tsx";
9
- export { TextControls } from "./components/text-controls.tsx";
10
9
  export type { ToolDisplayConfig } from "./components/tool-config-context.ts";
11
10
  export { ToolConfigContext } from "./components/tool-config-context.ts";
12
11
  export { ApiUrlChip, SessionUrlChips, UiUrlChip } from "./components/url-chips.tsx";
@@ -17,7 +16,5 @@ export { client } from "./define-client.tsx";
17
16
  export { useEvent, useToolCallStart, useToolResult } from "./hooks.ts";
18
17
  export type { CustomEvent, SessionCore, SessionCoreOptions, SessionSnapshot, } from "./session-core.ts";
19
18
  export { createSessionCore } from "./session-core.ts";
20
- export { CAPTURE_WORKLET_MODULE_URL, createPttRecorder, DEFAULT_SYNC_MIC_SAMPLE_RATE, floatToPcm16, type PttRecorder, type SyncMicrophone, type SyncMicrophoneOptions, startSyncMicrophone, } from "./sync-mic.ts";
21
- export { base64ToPcm16, createSyncSession, pcm16ToBase64, type SyncSession, type SyncSessionOptions, type SyncTurnResult, } from "./sync-session.ts";
22
- export { createUtteranceDetector, type UtteranceDetector, type UtteranceDetectorOptions, } from "./sync-vad.ts";
23
19
  export type { AgentState, ChatMessage, ClientTheme, SessionError, SessionErrorCode, ToolCallInfo, VoiceSessionOptions, WebSocketConstructor, } from "./types.ts";
20
+ export { VOICE_CAPTURE_CONSTRAINTS } from "./types.ts";
package/dist/index.js CHANGED
@@ -1,12 +1,14 @@
1
- import { a as pcm16ToBase64, c as createPttRecorder, d as createUtteranceDetector, f as buildAgentUrl, i as createSyncSession, l as floatToPcm16, n as SyncChatView, o as CAPTURE_WORKLET_MODULE_URL, p as fetchClientConfig, r as base64ToPcm16, s as DEFAULT_SYNC_MIC_SAMPLE_RATE, t as client, u as startSyncMicrophone } from "./define-client-DdpijqAu.js";
1
+ import { VOICE_CAPTURE_CONSTRAINTS } from "./types.js";
2
+ import { floatToPcm16 } from "./audio.js";
3
+ import { n as buildAgentUrl, r as fetchClientConfig, t as client } from "./define-client-Cyf1jqAl.js";
2
4
  import { SessionProvider, ThemeProvider, useSession, useSessionSelector, useTheme } from "./context.js";
3
5
  import { Button } from "./components/button.js";
4
- import { i as UiUrlChip, n as ApiUrlChip, r as SessionUrlChips, t as Controls } from "./controls-BbZcmnJf.js";
6
+ import { t as ChatView } from "./chat-view-C1oJxsWz.js";
7
+ import { i as UiUrlChip, n as ApiUrlChip, r as SessionUrlChips, t as Controls } from "./controls-DV368uhb.js";
5
8
  import { n as ToolConfigContext } from "./tool-call-block-DIxpG8GM.js";
6
9
  import { MessageList } from "./components/message-list.js";
7
- import { r as TextControls, t as ChatView } from "./chat-view-gi6FccZq.js";
8
10
  import { SidebarLayout } from "./components/sidebar-layout.js";
9
11
  import { StartScreen } from "./components/start-screen.js";
10
- import { t as createSessionCore } from "./session-core-B64kau_v.js";
12
+ import { t as createSessionCore } from "./session-core-BM3WHeoY.js";
11
13
  import { useEvent, useToolCallStart, useToolResult } from "./hooks.js";
12
- export { ApiUrlChip, Button, CAPTURE_WORKLET_MODULE_URL, ChatView, Controls, DEFAULT_SYNC_MIC_SAMPLE_RATE, MessageList, SessionProvider, SessionUrlChips, SidebarLayout, StartScreen, SyncChatView, TextControls, ThemeProvider, ToolConfigContext, UiUrlChip, base64ToPcm16, buildAgentUrl, client, createPttRecorder, createSessionCore, createSyncSession, createUtteranceDetector, fetchClientConfig, floatToPcm16, pcm16ToBase64, startSyncMicrophone, useEvent, useSession, useSessionSelector, useTheme, useToolCallStart, useToolResult };
14
+ export { ApiUrlChip, Button, ChatView, Controls, MessageList, SessionProvider, SessionUrlChips, SidebarLayout, StartScreen, ThemeProvider, ToolConfigContext, UiUrlChip, VOICE_CAPTURE_CONSTRAINTS, buildAgentUrl, client, createSessionCore, fetchClientConfig, floatToPcm16, useEvent, useSession, useSessionSelector, useTheme, useToolCallStart, useToolResult };