@alexkroman1/aai-ui 1.8.2 → 1.9.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 (62) hide show
  1. package/dist/_colors-DJUordGv.js +19 -0
  2. package/dist/_utils-5cs73OrA.js +16 -0
  3. package/dist/_utils.d.ts +4 -0
  4. package/dist/aai-logo-B8lDmsut.js +84 -0
  5. package/dist/audio.d.ts +9 -2
  6. package/dist/audio.js +41 -24
  7. package/dist/chat-view-C1XbqDk0.js +186 -0
  8. package/dist/components/_colors.d.ts +26 -0
  9. package/dist/components/aai-logo.d.ts +7 -6
  10. package/dist/components/button.d.ts +11 -13
  11. package/dist/components/button.js +7 -16
  12. package/dist/components/chat-view.d.ts +8 -19
  13. package/dist/components/chat-view.js +2 -98
  14. package/dist/components/controls.d.ts +1 -1
  15. package/dist/components/controls.js +2 -39
  16. package/dist/components/eyebrow.d.ts +12 -0
  17. package/dist/components/message-list.d.ts +1 -1
  18. package/dist/components/message-list.js +126 -56
  19. package/dist/components/sidebar-layout.d.ts +1 -1
  20. package/dist/components/start-screen.d.ts +4 -3
  21. package/dist/components/start-screen.js +20 -14
  22. package/dist/components/text-controls.d.ts +14 -0
  23. package/dist/components/tool-call-block.d.ts +5 -3
  24. package/dist/components/tool-call-block.js +1 -1
  25. package/dist/components/url-chips.d.ts +28 -0
  26. package/dist/context.d.ts +30 -4
  27. package/dist/context.js +86 -10
  28. package/dist/controls-BngrPbOC.js +135 -0
  29. package/dist/default-client/assets/audio-Cs-6t_Wd.js +1 -0
  30. package/dist/default-client/assets/capture-processor-C19oBn4L.js +105 -0
  31. package/dist/default-client/assets/index-Bf4ZTNcx.js +73 -0
  32. package/dist/default-client/assets/index-Bzlh9i7w.css +2 -0
  33. package/dist/default-client/assets/playback-processor-BtlzAH78.js +149 -0
  34. package/dist/default-client/index.html +3 -3
  35. package/dist/define-client.d.ts +2 -2
  36. package/dist/define-client.js +18 -24
  37. package/dist/eyebrow-C6ZFuiz6.js +27 -0
  38. package/dist/hooks.js +71 -42
  39. package/dist/index.d.ts +3 -1
  40. package/dist/index.js +6 -5
  41. package/dist/session-core-D3NDaySY.js +652 -0
  42. package/dist/session-core-messages.d.ts +50 -0
  43. package/dist/session-core-types.d.ts +147 -0
  44. package/dist/session-core-upload.d.ts +16 -0
  45. package/dist/session-core.d.ts +2 -68
  46. package/dist/session-core.js +1 -455
  47. package/dist/{tool-call-block-wby_jyoY.js → tool-call-block-7f1GTG-P.js} +33 -31
  48. package/dist/types.d.ts +29 -4
  49. package/dist/types.js +10 -1
  50. package/dist/worklets/capture-processor.d.ts +2 -0
  51. package/dist/worklets/capture-processor.js +80 -25
  52. package/dist/worklets/playback-processor.d.ts +2 -0
  53. package/dist/worklets/playback-processor.js +80 -29
  54. package/package.json +18 -18
  55. package/styles.css +30 -0
  56. package/dist/_react-test-utils.d.ts +0 -86
  57. package/dist/aai-logo-BqFv6JU6.js +0 -21
  58. package/dist/default-client/assets/audio-UowwhmYo.js +0 -1
  59. package/dist/default-client/assets/capture-processor-C26lSiVr.js +0 -53
  60. package/dist/default-client/assets/index-BZqVGR2o.css +0 -2
  61. package/dist/default-client/assets/index-YW9WUhbL.js +0 -49
  62. package/dist/default-client/assets/playback-processor-dSA8Im99.js +0 -101
package/dist/context.js CHANGED
@@ -1,19 +1,32 @@
1
- import { createContext, createElement, useContext, useSyncExternalStore } from "react";
1
+ import { createContext, createElement, useCallback, useContext, useEffect, useRef, useSyncExternalStore } from "react";
2
2
  //#region context.ts
3
3
  const DEFAULT_THEME = {
4
- bg: "#101010",
5
- primary: "#fab283",
6
- text: "rgba(255, 255, 255, 0.94)",
7
- surface: "#151515",
8
- border: "#282828"
4
+ bg: "#FBF8F2",
5
+ primary: "#3F2BC1",
6
+ text: "#1B1A18",
7
+ surface: "#FFFFFF",
8
+ border: "#DCD7CC"
9
9
  };
10
10
  const SessionCtx = createContext(null);
11
11
  function SessionProvider({ value, children }) {
12
12
  return createElement(SessionCtx.Provider, { value }, children);
13
13
  }
14
- function useSession() {
14
+ /**
15
+ * Return the raw {@link SessionCore} from context without subscribing to
16
+ * snapshot changes. Useful for accessing stable methods (`start`, `toggle`,
17
+ * `reset`, …) from components that select narrow state via
18
+ * {@link useSessionSelector}.
19
+ *
20
+ * Not part of the package's public export surface — internal to aai-ui
21
+ * components.
22
+ */
23
+ function useSessionCore() {
15
24
  const core = useContext(SessionCtx);
16
- if (!core) throw new Error("useSession must be used within <SessionProvider>");
25
+ if (!core) throw new Error("Session hooks must be used within <SessionProvider>");
26
+ return core;
27
+ }
28
+ function useSession() {
29
+ const core = useSessionCore();
17
30
  return {
18
31
  ...useSyncExternalStore(core.subscribe, core.getSnapshot),
19
32
  start: core.start,
@@ -21,19 +34,82 @@ function useSession() {
21
34
  resetState: core.resetState,
22
35
  reset: core.reset,
23
36
  disconnect: core.disconnect,
24
- toggle: core.toggle
37
+ toggle: core.toggle,
38
+ startRecording: core.startRecording,
39
+ stopRecording: core.stopRecording,
40
+ sendAudioFile: core.sendAudioFile
25
41
  };
26
42
  }
43
+ /**
44
+ * Subscribe to a narrow slice of the session snapshot.
45
+ *
46
+ * Unlike {@link useSession} — which re-renders the component on *every*
47
+ * snapshot change — this only triggers a re-render when the selected value
48
+ * changes (per `isEqual`, default `Object.is`). Use it for components that
49
+ * read a single field, e.g. `useSessionSelector((s) => s.running)`.
50
+ *
51
+ * The selector must be pure. It may run on every snapshot change, so keep it
52
+ * cheap; when it returns a derived object, pass a custom `isEqual` to avoid
53
+ * re-renders on referentially-new-but-equal results.
54
+ *
55
+ * @public
56
+ */
57
+ function useSessionSelector(selector, isEqual = Object.is) {
58
+ const core = useSessionCore();
59
+ const selectorRef = useRef(selector);
60
+ selectorRef.current = selector;
61
+ const isEqualRef = useRef(isEqual);
62
+ isEqualRef.current = isEqual;
63
+ const cacheRef = useRef({ hasValue: false });
64
+ const getSelection = useCallback(() => {
65
+ const next = selectorRef.current(core.getSnapshot());
66
+ const cache = cacheRef.current;
67
+ if (cache.hasValue && isEqualRef.current(cache.value, next)) return cache.value;
68
+ cacheRef.current = {
69
+ hasValue: true,
70
+ value: next
71
+ };
72
+ return next;
73
+ }, [core]);
74
+ return useSyncExternalStore(core.subscribe, getSelection);
75
+ }
27
76
  const ThemeCtx = createContext(DEFAULT_THEME);
28
77
  function ThemeProvider({ value, children }) {
29
78
  const merged = value ? {
30
79
  ...DEFAULT_THEME,
31
80
  ...value
32
81
  } : DEFAULT_THEME;
82
+ usePageBackground(merged.bg);
33
83
  return createElement(ThemeCtx.Provider, { value: merged }, children);
34
84
  }
85
+ /**
86
+ * Paint `html`/`body` with the theme background.
87
+ *
88
+ * Components paint `theme.bg` on their own containers, but the page behind
89
+ * them keeps whatever the static `<style>` in index.html set. Any viewport
90
+ * wider (or taller) than the app column then shows that color as a border
91
+ * around the UI — which is how a cream theme ended up letterboxed in black.
92
+ * Driving it from the theme means a custom `client({ theme })` covers the
93
+ * page too, instead of trading one hardcoded color for another.
94
+ */
95
+ function usePageBackground(bg) {
96
+ useEffect(() => {
97
+ if (typeof document === "undefined") return;
98
+ const { body, documentElement: html } = document;
99
+ const previous = {
100
+ body: body.style.background,
101
+ html: html.style.background
102
+ };
103
+ body.style.background = bg;
104
+ html.style.background = bg;
105
+ return () => {
106
+ body.style.background = previous.body;
107
+ html.style.background = previous.html;
108
+ };
109
+ }, [bg]);
110
+ }
35
111
  function useTheme() {
36
112
  return useContext(ThemeCtx);
37
113
  }
38
114
  //#endregion
39
- export { DEFAULT_THEME, SessionProvider, ThemeProvider, useSession, useTheme };
115
+ export { SessionProvider, ThemeProvider, useSession, useSessionCore, useSessionSelector, useTheme };
@@ -0,0 +1,135 @@
1
+ import { useSessionCore, useSessionSelector, useTheme } from "./context.js";
2
+ import { Button } from "./components/button.js";
3
+ import { i as TEXT_MUTED, n as SURFACE_TINT, r as TEXT_FAINT } from "./_colors-DJUordGv.js";
4
+ import clsx from "clsx";
5
+ import { useState } from "react";
6
+ import { jsx, jsxs } from "react/jsx-runtime";
7
+ //#region components/url-chips.tsx
8
+ /** @jsxImportSource react */
9
+ /** How long the "Copied" confirmation replaces the label after a click. */
10
+ const COPIED_FEEDBACK_MS = 1500;
11
+ /**
12
+ * A compact labeled chip showing a URL. Click to copy.
13
+ *
14
+ * The label is what makes a pair of these readable — on its own a bare URL
15
+ * leaves you guessing whether it's the page or the socket.
16
+ */
17
+ function UrlChip({ label, url, hint, testId, className }) {
18
+ const theme = useTheme();
19
+ const [copied, setCopied] = useState(false);
20
+ function copy() {
21
+ navigator.clipboard?.writeText(url).then(() => {
22
+ setCopied(true);
23
+ setTimeout(() => setCopied(false), COPIED_FEEDBACK_MS);
24
+ }).catch(() => {});
25
+ }
26
+ return /* @__PURE__ */ jsxs("button", {
27
+ type: "button",
28
+ onClick: copy,
29
+ title: `${hint} (click to copy)\n${url}`,
30
+ className: `flex items-center gap-1.5 min-w-0 appearance-none m-0 px-2 py-1 rounded-aai border cursor-pointer outline-none text-[11px] leading-none font-aai-mono ${className ?? ""}`,
31
+ style: {
32
+ background: SURFACE_TINT,
33
+ borderColor: theme.border,
34
+ color: TEXT_FAINT
35
+ },
36
+ "data-testid": testId,
37
+ children: [/* @__PURE__ */ jsx("span", {
38
+ className: "uppercase tracking-wide shrink-0",
39
+ style: { color: TEXT_MUTED },
40
+ children: copied ? "Copied" : label
41
+ }), /* @__PURE__ */ jsx("span", {
42
+ className: "truncate",
43
+ "data-testid": `${testId}-url`,
44
+ children: url
45
+ })]
46
+ });
47
+ }
48
+ /** The page this UI is served from — what you'd send someone to talk to the agent. */
49
+ function pageUrl() {
50
+ if (typeof window === "undefined") return "";
51
+ return `${window.location.origin}${window.location.pathname}`;
52
+ }
53
+ /**
54
+ * The session's shareable UI URL.
55
+ *
56
+ * @public
57
+ */
58
+ function UiUrlChip({ className }) {
59
+ return /* @__PURE__ */ jsx(UrlChip, {
60
+ label: "UI",
61
+ url: pageUrl(),
62
+ hint: "Shareable UI",
63
+ testId: "ui-url-chip",
64
+ ...className !== void 0 && { className }
65
+ });
66
+ }
67
+ /**
68
+ * The session's programmatic WebSocket endpoint — the URL a script or backend
69
+ * can connect to directly instead of using this UI.
70
+ *
71
+ * @public
72
+ */
73
+ function ApiUrlChip({ className }) {
74
+ return /* @__PURE__ */ jsx(UrlChip, {
75
+ label: "API",
76
+ url: useSessionSelector((s) => s.apiUrl),
77
+ hint: "API endpoint",
78
+ testId: "api-url-chip",
79
+ ...className !== void 0 && { className }
80
+ });
81
+ }
82
+ /**
83
+ * The UI and API URLs side by side. They answer the same question — "how do I
84
+ * reach this agent?" — so they belong together and each needs its label to be
85
+ * told apart. Rendered by the default shell in every session mode (S2S,
86
+ * pipeline, text-only).
87
+ *
88
+ * @public
89
+ */
90
+ function SessionUrlChips({ className }) {
91
+ return /* @__PURE__ */ jsxs("div", {
92
+ className: `flex items-center gap-1.5 min-w-0 ${className ?? ""}`,
93
+ children: [/* @__PURE__ */ jsx(UiUrlChip, { className: "min-w-0 flex-1" }), /* @__PURE__ */ jsx(ApiUrlChip, { className: "min-w-0 flex-1" })]
94
+ });
95
+ }
96
+ //#endregion
97
+ //#region components/controls.tsx
98
+ /** @jsxImportSource react */
99
+ /**
100
+ * Session control buttons: **Stop / Resume** and **New Conversation**.
101
+ *
102
+ * Reads session state from {@link useSession}. Must be rendered inside a
103
+ * {@link SessionProvider}.
104
+ *
105
+ * @example
106
+ * ```tsx
107
+ * <Controls className="justify-end" />
108
+ * ```
109
+ *
110
+ * @param className - Additional CSS class names applied to the container.
111
+ *
112
+ * @public
113
+ */
114
+ function Controls({ className }) {
115
+ const running = useSessionSelector((s) => s.running);
116
+ const { toggle, reset } = useSessionCore();
117
+ return /* @__PURE__ */ jsxs("div", {
118
+ className: clsx("flex items-center gap-3 shrink-0", className),
119
+ children: [
120
+ /* @__PURE__ */ jsx(Button, {
121
+ variant: "secondary",
122
+ onClick: toggle,
123
+ children: running ? "Stop" : "Resume"
124
+ }),
125
+ /* @__PURE__ */ jsx(Button, {
126
+ variant: "ghost",
127
+ onClick: reset,
128
+ children: "New Conversation"
129
+ }),
130
+ /* @__PURE__ */ jsx(SessionUrlChips, { className: "ml-auto max-w-[60%]" })
131
+ ]
132
+ });
133
+ }
134
+ //#endregion
135
+ export { UiUrlChip as i, ApiUrlChip as n, SessionUrlChips as r, Controls as t };
@@ -0,0 +1 @@
1
+ import{t as e}from"./index-Bf4ZTNcx.js";async function t(e,t){let n=await new OfflineAudioContext(1,1,t).decodeAudioData(e),r=Math.ceil(n.duration*t),i=new OfflineAudioContext(1,r,t),a=i.createBufferSource();a.buffer=n,a.connect(i.destination),a.start();let o=(await i.startRendering()).getChannelData(0),s=new Int16Array(o.length);for(let e=0;e<o.length;e++){let t=Math.max(-1,Math.min(1,o[e]??0));s[e]=t<0?t*32768:t*32767}return s}async function n(t){let{sttSampleRate:n,ttsSampleRate:r,captureWorkletSrc:i,playbackWorkletSrc:a,onMicData:o}=t,s=r,c=new AudioContext({sampleRate:s,latencyHint:`playback`}),l=navigator.mediaDevices.getUserMedia({audio:{deviceId:{ideal:`default`},echoCancellation:!0,noiseSuppression:!0,autoGainControl:!0,voiceIsolation:!0}}),u;try{[u]=await Promise.all([l,c.resume(),c.audioWorklet.addModule(i),c.audioWorklet.addModule(a)])}catch(e){throw l.then(e=>{for(let t of e.getTracks())t.stop()}).catch(()=>{}),await c.close().catch(e=>{console.warn(`AudioContext close failed:`,e)}),e}let d=c.createMediaStreamSource(u),f=new AudioWorkletNode(c,`capture-processor`,{channelCount:1,channelCountMode:`explicit`,processorOptions:{contextRate:s,sttSampleRate:n,bufferSeconds:e}});d.connect(f),f.port.postMessage({event:`start`}),f.port.onmessage=e=>{e.data.event===`chunk`&&o(e.data.buffer)};let p=null,m=null,h=new AbortController;function g(){if(p)return p;let e=new AudioWorkletNode(c,`playback-processor`,{processorOptions:{sampleRate:s}});return e.connect(c.destination),e.port.onmessage=e=>{e.data.event===`stop`&&(m?.(),m=null)},p=e,e}let _={enqueue(e){h.signal.aborted||e.byteLength!==0&&g().port.postMessage({event:`write`,buffer:new Uint8Array(e)},[e])},done(){return!p||c.state!==`running`?Promise.resolve():new Promise(e=>{m=e,p?.port.postMessage({event:`done`})})},flush(){p&&p.port.postMessage({event:`interrupt`})},async close(){if(!h.signal.aborted){h.abort(),f.port.postMessage({event:`stop`}),d.disconnect(),f.disconnect(),p&&p.disconnect();for(let e of u.getTracks())e.stop();await c.close().catch(()=>{})}},async[Symbol.asyncDispose](){await _.close()}};return _}export{n as createVoiceIO,t as decodeAudioToPcm16};
@@ -0,0 +1,105 @@
1
+ var e=new Blob([`
2
+ class CaptureProcessor extends AudioWorkletProcessor {
3
+ constructor(options) {
4
+ super();
5
+ this.recording = false;
6
+ const opts = options.processorOptions || {};
7
+ this.fromRate = opts.contextRate || sampleRate;
8
+ this.toRate = opts.sttSampleRate || sampleRate;
9
+ this.ratio = this.fromRate / this.toRate;
10
+ this.needsResample = this.fromRate !== this.toRate;
11
+ // Streaming-resampler state carried across process() blocks so the output
12
+ // clock doesn't drift and there's no discontinuity at 128-sample block
13
+ // boundaries. \`prev\` is the previous block's last input sample (used to
14
+ // interpolate across the boundary); \`pos\` is the fractional read position,
15
+ // in input samples, relative to an extended [prev, ...input] frame. Start
16
+ // at 1 so the first output is input[0] (not the bogus initial prev).
17
+ this.prev = 0;
18
+ this.pos = 1;
19
+ // Output buffer reused across process() calls -- the render quantum is a
20
+ // fixed size, so allocating per call would churn the realtime audio thread.
21
+ this.resampleBuf = null;
22
+ // Int16 accumulation buffer: flushed to the main thread as one transferred
23
+ // ArrayBuffer once ~bufferSeconds of output samples are batched. Sized 2x
24
+ // the flush target so a whole render quantum always fits before flushing.
25
+ this.targetSamples = Math.max(1, Math.round(this.toRate * (opts.bufferSeconds || 0.1)));
26
+ this.pending = new Int16Array(this.targetSamples * 2);
27
+ this.pendingLen = 0;
28
+ this.port.onmessage = (e) => {
29
+ if (e.data.event === 'start') this.recording = true;
30
+ else if (e.data.event === 'stop') {
31
+ // Final flush so the tail of speech isn't dropped on close.
32
+ this.flush();
33
+ this.recording = false;
34
+ }
35
+ };
36
+ }
37
+
38
+ resample(input) {
39
+ const ratio = this.ratio;
40
+ const n = input.length;
41
+ if (n === 0) return new Float32Array(0);
42
+ // At most ceil(n / ratio) + 1 outputs per block (pos starts in [0, ratio)).
43
+ const maxLen = Math.ceil(n / ratio) + 1;
44
+ if (this.resampleBuf === null || this.resampleBuf.length < maxLen) {
45
+ this.resampleBuf = new Float32Array(maxLen);
46
+ }
47
+ const out = this.resampleBuf;
48
+ // Extended frame: index 0 = prev (previous block's last sample),
49
+ // index k>=1 = input[k-1]. Interpolate at fractional positions stepping by
50
+ // ratio, carrying \`pos\` across calls to preserve the sample clock.
51
+ let count = 0;
52
+ let pos = this.pos;
53
+ while (pos < n) {
54
+ const idx = pos | 0;
55
+ const frac = pos - idx;
56
+ const a = idx === 0 ? this.prev : input[idx - 1];
57
+ const b = input[idx];
58
+ out[count++] = a + frac * (b - a);
59
+ pos += ratio;
60
+ }
61
+ // Shift the origin to this block's last sample for the next call.
62
+ this.prev = input[n - 1];
63
+ this.pos = pos - n;
64
+ return out.subarray(0, count);
65
+ }
66
+
67
+ // Convert Float32 -> Int16 and append to the pending batch. Writes through
68
+ // an Int16Array directly (assignment truncates like DataView.setInt16).
69
+ accumulate(samples) {
70
+ let buf = this.pending;
71
+ if (this.pendingLen + samples.length > buf.length) {
72
+ // Defensive: only reachable if a render quantum outproduces the 1x
73
+ // headroom above the flush target (never with 128-sample quanta).
74
+ const grown = new Int16Array((this.pendingLen + samples.length) * 2);
75
+ grown.set(buf.subarray(0, this.pendingLen));
76
+ this.pending = grown;
77
+ buf = grown;
78
+ }
79
+ for (let i = 0; i < samples.length; i++) {
80
+ const s = Math.max(-1, Math.min(1, samples[i]));
81
+ buf[this.pendingLen++] = s < 0 ? s * 0x8000 : s * 0x7fff;
82
+ }
83
+ }
84
+
85
+ // Post the batched samples as one transferred ArrayBuffer and reset.
86
+ flush() {
87
+ if (this.pendingLen === 0) return;
88
+ const buffer = this.pending.buffer.slice(0, this.pendingLen * 2);
89
+ this.pendingLen = 0;
90
+ this.port.postMessage({ event: 'chunk', buffer }, [buffer]);
91
+ }
92
+
93
+ process(inputs) {
94
+ const input = inputs[0];
95
+ if (!input || !input[0] || !this.recording) return true;
96
+
97
+ const samples = this.needsResample ? this.resample(input[0]) : input[0];
98
+ this.accumulate(samples);
99
+ if (this.pendingLen >= this.targetSamples) this.flush();
100
+ return true;
101
+ }
102
+ }
103
+
104
+ registerProcessor('capture-processor', CaptureProcessor);
105
+ `],{type:`application/javascript`}),t=URL.createObjectURL(e);export{t as default};