@alexkroman1/aai-ui 2.0.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.
- package/dist/_module-url-C_4gRVL0.js +15 -0
- package/dist/audio.d.ts +42 -6
- package/dist/audio.js +79 -69
- package/dist/{chat-view-DKFhxMAT.js → chat-view-C1oJxsWz.js} +4 -4
- package/dist/client-config.d.ts +5 -6
- package/dist/components/chat-view.js +1 -1
- package/dist/components/console-shell.d.ts +2 -2
- package/dist/components/controls.js +1 -1
- package/dist/components/url-chips.d.ts +0 -3
- package/dist/context.d.ts +1 -1
- package/dist/context.js +1 -4
- package/dist/{controls-4OJoekj6.js → controls-DV368uhb.js} +1 -4
- package/dist/default-client/assets/_module-url-BX0RuRU2.js +1 -0
- package/dist/default-client/assets/audio-BGHiiDY_.js +1 -0
- package/dist/default-client/assets/{capture-processor-DLHxAIfT.js → capture-processor-BLPsqnGl.js} +4 -4
- package/dist/default-client/assets/index-DXf7-M0Q.js +244 -0
- package/dist/default-client/assets/index-Dv-Q5VRL.css +2 -0
- package/dist/default-client/assets/{playback-processor-bMTdFp-8.js → playback-processor-B_T_1KQP.js} +28 -21
- package/dist/default-client/index.html +2 -5
- package/dist/define-client-Cyf1jqAl.js +150 -0
- package/dist/define-client.js +1 -1
- package/dist/index.d.ts +1 -3
- package/dist/index.js +7 -6
- package/dist/{session-core-BLhiQ18c.js → session-core-BM3WHeoY.js} +30 -128
- package/dist/session-core-messages.d.ts +0 -4
- package/dist/session-core-types.d.ts +1 -27
- package/dist/session-core.js +1 -1
- package/dist/types.d.ts +1 -1
- package/dist/types.js +2 -2
- package/dist/worklets/_module-url.d.ts +10 -0
- package/dist/worklets/capture-processor.d.ts +2 -2
- package/dist/worklets/capture-processor.js +5 -5
- package/dist/worklets/playback-processor.d.ts +3 -3
- package/dist/worklets/playback-processor.js +31 -24
- package/package.json +2 -2
- package/dist/components/workflow-view.d.ts +0 -13
- package/dist/default-client/assets/audio-Cgviqo9t.js +0 -1
- package/dist/default-client/assets/index-BgbIWnfG.css +0 -2
- package/dist/default-client/assets/index-DbKKR3UE.js +0 -62
- package/dist/default-client/assets/rolldown-runtime-BpQH8Ho1.js +0 -1
- package/dist/default-client/assets/types-Bpg3ZIZK.js +0 -64
- package/dist/define-client-yoGybEYR.js +0 -710
- package/dist/session-core-upload.d.ts +0 -16
- package/dist/sync-mic.d.ts +0 -61
- package/dist/sync-session.d.ts +0 -49
|
@@ -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}
|
package/dist/default-client/assets/{playback-processor-bMTdFp-8.js → playback-processor-B_T_1KQP.js}
RENAMED
|
@@ -1,9 +1,12 @@
|
|
|
1
|
-
import{
|
|
1
|
+
import{i as e}from"./index-DXf7-M0Q.js";import{t}from"./_module-url-BX0RuRU2.js";var n=t(`
|
|
2
2
|
class PlaybackProcessor extends AudioWorkletProcessor {
|
|
3
3
|
constructor(options) {
|
|
4
4
|
super();
|
|
5
5
|
const opts = options.processorOptions || {};
|
|
6
|
-
|
|
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;
|
|
7
10
|
// Fill target for the start of a turn. If 'done' arrives first (short
|
|
8
11
|
// utterance), start immediately instead of waiting for audio that is
|
|
9
12
|
// never coming.
|
|
@@ -16,11 +19,11 @@ class PlaybackProcessor extends AudioWorkletProcessor {
|
|
|
16
19
|
this.concealCapacity = Math.max(1, Math.floor((rate * 40) / 1000));
|
|
17
20
|
this.concealBuf = new Float32Array(this.concealCapacity);
|
|
18
21
|
this.concealDecay = Math.exp(Math.log(${e}) / this.concealCapacity);
|
|
19
|
-
// Float32 ring buffer —
|
|
20
|
-
// the node's lifetime; per-turn state resets via
|
|
21
|
-
// readPos are absolute (monotonic) sample
|
|
22
|
-
// modulo capacity so a
|
|
23
|
-
// writing past the end and going silent.
|
|
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.
|
|
24
27
|
this.capacity = rate * 60;
|
|
25
28
|
this.samples = new Float32Array(this.capacity);
|
|
26
29
|
// Platform endianness probe: the wire format is PCM16 little-endian, so
|
|
@@ -34,7 +37,12 @@ class PlaybackProcessor extends AudioWorkletProcessor {
|
|
|
34
37
|
if (d.event === 'write') {
|
|
35
38
|
this.ingestBytes(d.buffer);
|
|
36
39
|
} else if (d.event === 'interrupt') {
|
|
37
|
-
|
|
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');
|
|
38
46
|
} else if (d.event === 'done') {
|
|
39
47
|
this.isDone = true;
|
|
40
48
|
}
|
|
@@ -44,7 +52,6 @@ class PlaybackProcessor extends AudioWorkletProcessor {
|
|
|
44
52
|
// Reset per-turn state so the node is reusable across replies without
|
|
45
53
|
// reallocating the sample buffer or re-instantiating the worklet.
|
|
46
54
|
resetTurn() {
|
|
47
|
-
this.interrupted = false;
|
|
48
55
|
this.isDone = false;
|
|
49
56
|
this.playing = false;
|
|
50
57
|
// Whether any real audio has been rendered this turn. Separates a turn's
|
|
@@ -105,7 +112,10 @@ class PlaybackProcessor extends AudioWorkletProcessor {
|
|
|
105
112
|
const total = out.length - start;
|
|
106
113
|
const len = this.concealLen;
|
|
107
114
|
let silent = 0;
|
|
108
|
-
|
|
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}) {
|
|
109
119
|
out.fill(0, start);
|
|
110
120
|
silent = total;
|
|
111
121
|
} else {
|
|
@@ -139,10 +149,13 @@ class PlaybackProcessor extends AudioWorkletProcessor {
|
|
|
139
149
|
rememberTail(out, n) {
|
|
140
150
|
const cap = this.concealCapacity;
|
|
141
151
|
const take = Math.min(n, cap);
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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;
|
|
146
159
|
this.concealLen = Math.min(cap, this.concealLen + take);
|
|
147
160
|
// Read the loop oldest-first; once the ring is full the write cursor is
|
|
148
161
|
// the oldest retained sample.
|
|
@@ -210,12 +223,6 @@ class PlaybackProcessor extends AudioWorkletProcessor {
|
|
|
210
223
|
// session), so guard like the capture processor does.
|
|
211
224
|
if (!outputs[0] || !outputs[0][0]) return true;
|
|
212
225
|
const out = outputs[0][0];
|
|
213
|
-
if (this.interrupted) {
|
|
214
|
-
out.fill(0);
|
|
215
|
-
this.stopTurn('interrupt');
|
|
216
|
-
return true;
|
|
217
|
-
}
|
|
218
|
-
|
|
219
226
|
const avail = this.writePos - this.readPos;
|
|
220
227
|
|
|
221
228
|
// Filling: wait for the target. 'done' short-circuits it — what is
|
|
@@ -268,4 +275,4 @@ class PlaybackProcessor extends AudioWorkletProcessor {
|
|
|
268
275
|
}
|
|
269
276
|
|
|
270
277
|
registerProcessor('playback-processor', PlaybackProcessor);
|
|
271
|
-
|
|
278
|
+
`);export{n as default};
|
|
@@ -6,11 +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-
|
|
10
|
-
<link rel="
|
|
11
|
-
<link rel="modulepreload" crossorigin href="./assets/types-Bpg3ZIZK.js">
|
|
12
|
-
<link rel="modulepreload" crossorigin href="./assets/audio-Cgviqo9t.js">
|
|
13
|
-
<link rel="stylesheet" crossorigin href="./assets/index-BgbIWnfG.css">
|
|
9
|
+
<script type="module" crossorigin src="./assets/index-DXf7-M0Q.js"></script>
|
|
10
|
+
<link rel="stylesheet" crossorigin href="./assets/index-Dv-Q5VRL.css">
|
|
14
11
|
</head>
|
|
15
12
|
<body>
|
|
16
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 };
|
package/dist/define-client.js
CHANGED
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";
|
|
@@ -8,7 +9,6 @@ export { StartScreen } from "./components/start-screen.tsx";
|
|
|
8
9
|
export type { ToolDisplayConfig } from "./components/tool-config-context.ts";
|
|
9
10
|
export { ToolConfigContext } from "./components/tool-config-context.ts";
|
|
10
11
|
export { ApiUrlChip, SessionUrlChips, UiUrlChip } from "./components/url-chips.tsx";
|
|
11
|
-
export { WorkflowView } from "./components/workflow-view.tsx";
|
|
12
12
|
export type { Session } from "./context.ts";
|
|
13
13
|
export { SessionProvider, ThemeProvider, useSession, useSessionSelector, useTheme, } from "./context.ts";
|
|
14
14
|
export type { ClientConfig, ClientHandle, } from "./define-client.tsx";
|
|
@@ -16,7 +16,5 @@ export { client } from "./define-client.tsx";
|
|
|
16
16
|
export { useEvent, useToolCallStart, useToolResult } from "./hooks.ts";
|
|
17
17
|
export type { CustomEvent, SessionCore, SessionCoreOptions, SessionSnapshot, } from "./session-core.ts";
|
|
18
18
|
export { createSessionCore } from "./session-core.ts";
|
|
19
|
-
export { CAPTURE_WORKLET_MODULE_URL, createPttRecorder, DEFAULT_SYNC_MIC_SAMPLE_RATE, floatToPcm16, type PttRecorder, } from "./sync-mic.ts";
|
|
20
|
-
export { base64ToPcm16, createSyncSession, pcm16ToBase64, type SyncSession, type SyncSessionOptions, type SyncTurnResult, } from "./sync-session.ts";
|
|
21
19
|
export type { AgentState, ChatMessage, ClientTheme, SessionError, SessionErrorCode, ToolCallInfo, VoiceSessionOptions, WebSocketConstructor, } from "./types.ts";
|
|
22
20
|
export { VOICE_CAPTURE_CONSTRAINTS } from "./types.ts";
|
package/dist/index.js
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
|
-
import {
|
|
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 { t as ChatView } from "./chat-view-
|
|
5
|
-
import { i as UiUrlChip, n as ApiUrlChip, r as SessionUrlChips, t as Controls } from "./controls-
|
|
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";
|
|
6
8
|
import { n as ToolConfigContext } from "./tool-call-block-DIxpG8GM.js";
|
|
7
9
|
import { MessageList } from "./components/message-list.js";
|
|
8
10
|
import { SidebarLayout } from "./components/sidebar-layout.js";
|
|
9
11
|
import { StartScreen } from "./components/start-screen.js";
|
|
10
|
-
import {
|
|
11
|
-
import { t as createSessionCore } from "./session-core-BLhiQ18c.js";
|
|
12
|
+
import { t as createSessionCore } from "./session-core-BM3WHeoY.js";
|
|
12
13
|
import { useEvent, useToolCallStart, useToolResult } from "./hooks.js";
|
|
13
|
-
export { ApiUrlChip, Button,
|
|
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 };
|
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { MIC_SEND_MAX_BUFFERED_BYTES } from "./types.js";
|
|
2
|
+
import { DEFAULT_MAX_HISTORY, WS_OPEN, errorMessage, safeJsonParse } from "@alexkroman1/aai";
|
|
2
3
|
import { ServerMessageSchema, lenientParse } from "@alexkroman1/aai/protocol";
|
|
3
|
-
import { DEFAULT_MAX_HISTORY, FILE_UPLOAD_CHUNK_BYTES, WS_OPEN, errorMessage, safeJsonParse } from "@alexkroman1/aai";
|
|
4
4
|
import ReconnectingWebSocket from "partysocket/ws";
|
|
5
|
-
import { MAX_SYNC_AUDIO_SECONDS } from "@alexkroman1/aai/stt";
|
|
6
5
|
//#region session-core-audio-setup.ts
|
|
7
6
|
/**
|
|
8
7
|
* Audio-path initialization for the voice session core.
|
|
@@ -36,6 +35,7 @@ async function initAudioCapture(conn, msg, deps, fatal) {
|
|
|
36
35
|
const gen = conn.generation;
|
|
37
36
|
const stale = () => conn.generation !== gen || !conn.ws || conn.ws.readyState !== WS_OPEN;
|
|
38
37
|
const reportAudioFailure = (message) => {
|
|
38
|
+
deps.cleanupAudio();
|
|
39
39
|
if (fatal) deps.updateState({
|
|
40
40
|
state: "error",
|
|
41
41
|
error: {
|
|
@@ -45,16 +45,13 @@ async function initAudioCapture(conn, msg, deps, fatal) {
|
|
|
45
45
|
running: false,
|
|
46
46
|
recording: false
|
|
47
47
|
});
|
|
48
|
-
else {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
recording: false
|
|
56
|
-
});
|
|
57
|
-
}
|
|
48
|
+
else deps.updateState({
|
|
49
|
+
error: {
|
|
50
|
+
code: "audio",
|
|
51
|
+
message
|
|
52
|
+
},
|
|
53
|
+
recording: false
|
|
54
|
+
});
|
|
58
55
|
};
|
|
59
56
|
try {
|
|
60
57
|
const [{ createVoiceIO }, captureWorklet, playbackWorklet] = await Promise.all([
|
|
@@ -155,7 +152,7 @@ function appendCapped(list, item, cap) {
|
|
|
155
152
|
* dedup) that previously lived as closure locals in `createSessionCore`.
|
|
156
153
|
*/
|
|
157
154
|
function createMessageHandlers(deps) {
|
|
158
|
-
const { getSnapshot, updateState, conn,
|
|
155
|
+
const { getSnapshot, updateState, conn, cleanupAudio } = deps;
|
|
159
156
|
/** Incremented on each turn boundary -- stale async callbacks compare against this. */
|
|
160
157
|
let handlerGeneration = 0;
|
|
161
158
|
/** Monotonically increasing counter for custom events -- used by useEvent to deduplicate. */
|
|
@@ -213,6 +210,7 @@ function createMessageHandlers(deps) {
|
|
|
213
210
|
} });
|
|
214
211
|
else {
|
|
215
212
|
cleanupAudio();
|
|
213
|
+
conn.generation++;
|
|
216
214
|
updateState({
|
|
217
215
|
state: "error",
|
|
218
216
|
error: {
|
|
@@ -280,7 +278,6 @@ function createMessageHandlers(deps) {
|
|
|
280
278
|
break;
|
|
281
279
|
case "reset":
|
|
282
280
|
handlerGeneration++;
|
|
283
|
-
discardUpload();
|
|
284
281
|
conn.voiceIO?.flush();
|
|
285
282
|
updateState({
|
|
286
283
|
...CLEARED_SESSION_STATE,
|
|
@@ -353,7 +350,6 @@ function createMessageHandlers(deps) {
|
|
|
353
350
|
if (msg.type === "config") return {
|
|
354
351
|
sampleRate: msg.sampleRate,
|
|
355
352
|
ttsSampleRate: msg.ttsSampleRate,
|
|
356
|
-
audioOut: msg.audioOut,
|
|
357
353
|
sid: msg.sessionId
|
|
358
354
|
};
|
|
359
355
|
if (msg.type === "audio_done") {
|
|
@@ -404,84 +400,6 @@ function reconnectPending(socket) {
|
|
|
404
400
|
return socket instanceof ReconnectingWebSocket && socket.shouldReconnect && socket.retryCount < RECONNECT_OPTIONS.maxRetries;
|
|
405
401
|
}
|
|
406
402
|
//#endregion
|
|
407
|
-
//#region session-core-upload.ts
|
|
408
|
-
/**
|
|
409
|
-
* File-upload transcription for the session core (`sendAudioFile`).
|
|
410
|
-
*
|
|
411
|
-
* Split out of `session-core.ts`: this module owns the decode → frame →
|
|
412
|
-
* reliable-send pipeline for uploaded audio, plus the two guards that keep
|
|
413
|
-
* it exclusive — an in-flight lock (no second upload, no mic start
|
|
414
|
-
* mid-upload) and an epoch that in-flight sends compare against so a
|
|
415
|
-
* reset/close/reconnect abandons them instead of streaming a stale clip
|
|
416
|
-
* into the fresh session.
|
|
417
|
-
*/
|
|
418
|
-
function createUploadSender(deps) {
|
|
419
|
-
const { conn, getSnapshot, sendJson } = deps;
|
|
420
|
-
/** True while a `sendAudioFile` upload is decoding or streaming. Blocks the
|
|
421
|
-
* mic (and a second upload) so file bytes never interleave with live audio. */
|
|
422
|
-
let uploadInFlight = false;
|
|
423
|
-
/** Bumped whenever conversation/connection state is discarded (reset, close,
|
|
424
|
-
* reconnect). An in-flight upload compares against it between chunks and
|
|
425
|
-
* aborts instead of streaming a stale clip into the fresh session. */
|
|
426
|
-
let uploadEpoch = 0;
|
|
427
|
-
/** Stream `bytes` to the socket in chunks, waiting out backpressure.
|
|
428
|
-
* Unlike live mic frames (dropped under backpressure — stale speech is
|
|
429
|
-
* worthless), file audio must arrive completely. Aborts if the connection
|
|
430
|
-
* closes or the session is reset (`epoch` moves on) between chunks. */
|
|
431
|
-
async function sendBytesReliably(bytes, epoch) {
|
|
432
|
-
for (let i = 0; i < bytes.byteLength; i += FILE_UPLOAD_CHUNK_BYTES) {
|
|
433
|
-
while (epoch === uploadEpoch && conn.ws && conn.ws.readyState === WS_OPEN && conn.ws.bufferedAmount > MIC_SEND_MAX_BUFFERED_BYTES) await new Promise((r) => setTimeout(r, FILE_SEND_BACKOFF_MS));
|
|
434
|
-
if (epoch !== uploadEpoch) throw new Error("sendAudioFile: session was reset mid-send");
|
|
435
|
-
if (!conn.ws || conn.ws.readyState !== WS_OPEN) throw new Error("sendAudioFile: connection closed mid-send");
|
|
436
|
-
conn.ws.send(bytes.subarray(i, i + FILE_UPLOAD_CHUNK_BYTES));
|
|
437
|
-
}
|
|
438
|
-
}
|
|
439
|
-
/** Validate that an upload may start; returns the session's ready config. */
|
|
440
|
-
function assertUploadReady() {
|
|
441
|
-
const cfg = conn.readyConfig;
|
|
442
|
-
if (!(cfg && conn.ws) || conn.ws.readyState !== WS_OPEN) throw new Error("sendAudioFile: session is not connected");
|
|
443
|
-
const snap = getSnapshot();
|
|
444
|
-
if (snap.audioOut) throw new Error("sendAudioFile is only available in text-only sessions (tts: none()) — voice sessions stream the microphone instead");
|
|
445
|
-
if (snap.recording) throw new Error("sendAudioFile: stop recording before uploading a file");
|
|
446
|
-
if (uploadInFlight) throw new Error("sendAudioFile: another upload is already in progress");
|
|
447
|
-
return cfg;
|
|
448
|
-
}
|
|
449
|
-
async function sendAudioFile(file) {
|
|
450
|
-
const cfg = assertUploadReady();
|
|
451
|
-
uploadInFlight = true;
|
|
452
|
-
const epoch = uploadEpoch;
|
|
453
|
-
try {
|
|
454
|
-
const { decodeAudioToPcm16 } = await import("./audio.js");
|
|
455
|
-
const clip = await decodeAudioToPcm16(await file.arrayBuffer(), cfg.sampleRate);
|
|
456
|
-
if (getSnapshot().recording || conn.audioSetupInFlight) throw new Error("sendAudioFile: stop recording before uploading a file");
|
|
457
|
-
if (epoch !== uploadEpoch) throw new Error("sendAudioFile: session was reset mid-send");
|
|
458
|
-
if (clip.length / cfg.sampleRate <= MAX_SYNC_AUDIO_SECONDS) {
|
|
459
|
-
const bytes = new Uint8Array(clip.buffer, clip.byteOffset, clip.byteLength);
|
|
460
|
-
sendJson({
|
|
461
|
-
type: "transcribe_file_start",
|
|
462
|
-
sampleRate: cfg.sampleRate,
|
|
463
|
-
byteLength: bytes.byteLength
|
|
464
|
-
});
|
|
465
|
-
await sendBytesReliably(bytes, epoch);
|
|
466
|
-
sendJson({ type: "transcribe_file_end" });
|
|
467
|
-
return;
|
|
468
|
-
}
|
|
469
|
-
const padded = new Int16Array(clip.length + cfg.sampleRate);
|
|
470
|
-
padded.set(clip);
|
|
471
|
-
await sendBytesReliably(new Uint8Array(padded.buffer), epoch);
|
|
472
|
-
} finally {
|
|
473
|
-
uploadInFlight = false;
|
|
474
|
-
}
|
|
475
|
-
}
|
|
476
|
-
return {
|
|
477
|
-
sendAudioFile,
|
|
478
|
-
inFlight: () => uploadInFlight,
|
|
479
|
-
discard: () => {
|
|
480
|
-
uploadEpoch++;
|
|
481
|
-
}
|
|
482
|
-
};
|
|
483
|
-
}
|
|
484
|
-
//#endregion
|
|
485
403
|
//#region session-core-url.ts
|
|
486
404
|
/** Build the session WebSocket URL from the platform URL and resume state. */
|
|
487
405
|
function buildWsUrl(platformUrl, resume, sessionId) {
|
|
@@ -524,7 +442,6 @@ function createSessionCore(options) {
|
|
|
524
442
|
contentVersion: 0,
|
|
525
443
|
started: false,
|
|
526
444
|
running: false,
|
|
527
|
-
audioOut: true,
|
|
528
445
|
recording: false,
|
|
529
446
|
apiUrl: buildWsUrl(options.platformUrl, false).toString()
|
|
530
447
|
};
|
|
@@ -570,8 +487,16 @@ function createSessionCore(options) {
|
|
|
570
487
|
};
|
|
571
488
|
let connectionController = null;
|
|
572
489
|
let hasConnected = false;
|
|
490
|
+
/**
|
|
491
|
+
* The session ID to resume: seeded from `options.resumeSessionId`, then
|
|
492
|
+
* kept current from every `config` frame. Reconnect URLs carry it as
|
|
493
|
+
* `?sessionId=<id>` so the server re-registers the SAME session id —
|
|
494
|
+
* that key is what per-session tool state (`ctx.state`) lives under, so
|
|
495
|
+
* a reconnect that omits it gets a fresh session with none of the
|
|
496
|
+
* agent's context, greeting suppression aside.
|
|
497
|
+
*/
|
|
498
|
+
let sessionId = options.resumeSessionId;
|
|
573
499
|
function cleanupAudio() {
|
|
574
|
-
upload.discard();
|
|
575
500
|
conn.audioSetupInFlight = false;
|
|
576
501
|
conn.voiceIO?.close().catch(() => {});
|
|
577
502
|
conn.voiceIO = null;
|
|
@@ -589,16 +514,10 @@ function createSessionCore(options) {
|
|
|
589
514
|
if (conn.ws.bufferedAmount > MIC_SEND_MAX_BUFFERED_BYTES) return;
|
|
590
515
|
conn.ws.send(bytes);
|
|
591
516
|
}
|
|
592
|
-
const upload = createUploadSender({
|
|
593
|
-
conn,
|
|
594
|
-
getSnapshot,
|
|
595
|
-
sendJson
|
|
596
|
-
});
|
|
597
517
|
const { handleMessage, settleWhenAudioDrained } = createMessageHandlers({
|
|
598
518
|
getSnapshot,
|
|
599
519
|
updateState,
|
|
600
520
|
conn,
|
|
601
|
-
discardUpload: upload.discard,
|
|
602
521
|
cleanupAudio
|
|
603
522
|
});
|
|
604
523
|
const audioDeps = {
|
|
@@ -619,20 +538,17 @@ function createSessionCore(options) {
|
|
|
619
538
|
/** React to the server's `config` message: record it, set up the audio
|
|
620
539
|
* path for the session's mode, and replay history on reconnect. */
|
|
621
540
|
function onServerConfig(config) {
|
|
622
|
-
if (config.sid)
|
|
541
|
+
if (config.sid) {
|
|
542
|
+
sessionId = config.sid;
|
|
543
|
+
options.onSessionId?.(config.sid);
|
|
544
|
+
}
|
|
623
545
|
const isReconnect = hasConnected;
|
|
624
546
|
hasConnected = true;
|
|
625
547
|
conn.readyConfig = {
|
|
626
548
|
sampleRate: config.sampleRate,
|
|
627
549
|
ttsSampleRate: config.ttsSampleRate
|
|
628
550
|
};
|
|
629
|
-
|
|
630
|
-
updateState({ audioOut });
|
|
631
|
-
if (audioOut) initAudioCapture(conn, config, audioDeps, true);
|
|
632
|
-
else {
|
|
633
|
-
sendJson({ type: "audio_ready" });
|
|
634
|
-
updateState({ state: "listening" });
|
|
635
|
-
}
|
|
551
|
+
initAudioCapture(conn, config, audioDeps, true);
|
|
636
552
|
if (isReconnect && currentSnapshot.messages.length > 0) sendJson({
|
|
637
553
|
type: "history",
|
|
638
554
|
messages: currentSnapshot.messages.map((m) => ({
|
|
@@ -645,11 +561,12 @@ function createSessionCore(options) {
|
|
|
645
561
|
* The WebSocket URL for the *next* connection attempt. Evaluated per
|
|
646
562
|
* attempt (partysocket takes it as a URL provider), so once the first
|
|
647
563
|
* `config` arrives, every reconnect — automatic or explicit — carries
|
|
648
|
-
*
|
|
564
|
+
* `?sessionId=<id>` and the server resumes the SAME session (id, tool
|
|
565
|
+
* state) instead of minting a new one. `resume=1` remains only as the
|
|
566
|
+
* greeting-suppression fallback for a server whose config carried no id.
|
|
649
567
|
*/
|
|
650
568
|
function currentWsUrl() {
|
|
651
|
-
|
|
652
|
-
return buildWsUrl(options.platformUrl, hasConnected, resumeId).toString();
|
|
569
|
+
return buildWsUrl(options.platformUrl, hasConnected, sessionId).toString();
|
|
653
570
|
}
|
|
654
571
|
/** Open a socket: an injected constructor as-is (tests), or partysocket's
|
|
655
572
|
* reconnecting WebSocket — same interface, plus reconnect-on-close. */
|
|
@@ -729,7 +646,6 @@ function createSessionCore(options) {
|
|
|
729
646
|
sendJson({ type: "cancel" });
|
|
730
647
|
}
|
|
731
648
|
function reset() {
|
|
732
|
-
upload.discard();
|
|
733
649
|
conn.voiceIO?.flush();
|
|
734
650
|
if (conn.ws && conn.ws.readyState === WS_OPEN) {
|
|
735
651
|
sendJson({ type: "reset" });
|
|
@@ -748,17 +664,6 @@ function createSessionCore(options) {
|
|
|
748
664
|
recording: false
|
|
749
665
|
});
|
|
750
666
|
}
|
|
751
|
-
function startRecording() {
|
|
752
|
-
if (currentSnapshot.audioOut || currentSnapshot.recording || conn.audioSetupInFlight || upload.inFlight()) return;
|
|
753
|
-
const cfg = conn.readyConfig;
|
|
754
|
-
if (!(cfg && conn.ws) || conn.ws.readyState !== WS_OPEN) return;
|
|
755
|
-
initAudioCapture(conn, cfg, audioDeps, false);
|
|
756
|
-
}
|
|
757
|
-
function stopRecording() {
|
|
758
|
-
if (currentSnapshot.audioOut || !currentSnapshot.recording) return;
|
|
759
|
-
cleanupAudio();
|
|
760
|
-
updateState({ recording: false });
|
|
761
|
-
}
|
|
762
667
|
function start() {
|
|
763
668
|
updateState({
|
|
764
669
|
started: true,
|
|
@@ -783,9 +688,6 @@ function createSessionCore(options) {
|
|
|
783
688
|
disconnect,
|
|
784
689
|
start,
|
|
785
690
|
toggle,
|
|
786
|
-
startRecording,
|
|
787
|
-
stopRecording,
|
|
788
|
-
sendAudioFile: upload.sendAudioFile,
|
|
789
691
|
[Symbol.dispose]() {
|
|
790
692
|
disconnect();
|
|
791
693
|
}
|
|
@@ -17,8 +17,6 @@ export declare const CLEARED_SESSION_STATE: {
|
|
|
17
17
|
export type SessionConfigMessage = {
|
|
18
18
|
sampleRate: number;
|
|
19
19
|
ttsSampleRate: number;
|
|
20
|
-
/** False for text-only agents (`tts: none()`) — see ReadyConfigSchema. */
|
|
21
|
-
audioOut?: boolean | undefined;
|
|
22
20
|
sid?: string | undefined;
|
|
23
21
|
};
|
|
24
22
|
/** Dependencies the message handlers need from the owning session core. */
|
|
@@ -26,8 +24,6 @@ type MessageHandlerDeps = {
|
|
|
26
24
|
getSnapshot: () => SessionSnapshot;
|
|
27
25
|
updateState: (partial: Partial<SessionSnapshot>) => void;
|
|
28
26
|
conn: ConnState;
|
|
29
|
-
/** Invalidate any in-flight file upload (the upload sender's `discard`). */
|
|
30
|
-
discardUpload: () => void;
|
|
31
27
|
/** Release the microphone/VoiceIO (the session core's `cleanupAudio`). */
|
|
32
28
|
cleanupAudio: () => void;
|
|
33
29
|
};
|