@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
@@ -1,18 +1,33 @@
1
+ import { PLAYBACK_BUFFER_SECONDS, PLAYBACK_CONCEAL_FADE_MS, PLAYBACK_CONCEAL_FLOOR, PLAYBACK_JITTER_MS, PLAYBACK_REFILL_MS } from "../types.js";
2
+ import { t as workletModuleUrl } from "../_module-url-C_4gRVL0.js";
1
3
  //#region worklets/playback-processor.ts
2
4
  const PlaybackProcessorWorklet = `
3
5
  class PlaybackProcessor extends AudioWorkletProcessor {
4
6
  constructor(options) {
5
7
  super();
6
- const rate = options.processorOptions?.sampleRate ?? 24000;
7
- // Wait for ~400ms of audio before starting.
8
- // If 'done' arrives first (short utterance), start immediately.
9
- this.jitterSamples = Math.floor(rate * 0.4);
10
- // Float32 ring buffer — 60s at the context sample rate. Allocated once for
11
- // the node's lifetime; per-turn state resets via resetTurn(). writePos and
12
- // readPos are absolute (monotonic) sample counts; the buffer is indexed
13
- // modulo capacity so a reply longer than 60s keeps playing instead of
14
- // writing past the end and going silent.
15
- this.capacity = rate * 60;
8
+ const opts = options.processorOptions || {};
9
+ // The node's context IS the playback context (audio.ts asserts its rate),
10
+ // so the worklet-global sampleRate is authoritative; the option exists
11
+ // for the node-less test harness.
12
+ const rate = opts.sampleRate ?? sampleRate;
13
+ // Fill target for the start of a turn. If 'done' arrives first (short
14
+ // utterance), start immediately instead of waiting for audio that is
15
+ // never coming.
16
+ this.jitterSamples = Math.floor((rate * (opts.jitterMs ?? ${PLAYBACK_JITTER_MS})) / 1000);
17
+ // Fill target after an underrun — see PLAYBACK_REFILL_MS.
18
+ this.refillSamples = Math.floor((rate * (opts.refillMs ?? ${PLAYBACK_REFILL_MS})) / 1000);
19
+ // Concealment source: a ring of the most recently played samples, looped
20
+ // under a decaying gain to cover a gap. Sized to the fade window, with a
21
+ // per-sample decay that reaches the floor exactly at its end.
22
+ this.concealCapacity = Math.max(1, Math.floor((rate * ${PLAYBACK_CONCEAL_FADE_MS}) / 1000));
23
+ this.concealBuf = new Float32Array(this.concealCapacity);
24
+ this.concealDecay = Math.exp(Math.log(${PLAYBACK_CONCEAL_FLOOR}) / this.concealCapacity);
25
+ // Float32 ring buffer — PLAYBACK_BUFFER_SECONDS at the context sample
26
+ // rate. Allocated once for the node's lifetime; per-turn state resets via
27
+ // resetTurn(). writePos and readPos are absolute (monotonic) sample
28
+ // counts; the buffer is indexed modulo capacity so a longer reply keeps
29
+ // playing instead of writing past the end and going silent.
30
+ this.capacity = rate * ${PLAYBACK_BUFFER_SECONDS};
16
31
  this.samples = new Float32Array(this.capacity);
17
32
  // Platform endianness probe: the wire format is PCM16 little-endian, so
18
33
  // the Int16Array fast path in ingestBytes is only valid on LE hosts
@@ -25,7 +40,12 @@ class PlaybackProcessor extends AudioWorkletProcessor {
25
40
  if (d.event === 'write') {
26
41
  this.ingestBytes(d.buffer);
27
42
  } else if (d.event === 'interrupt') {
28
- this.interrupted = true;
43
+ // Applied eagerly, not deferred to the next process(): onmessage and
44
+ // process() never interleave (one audio thread), and a deferred
45
+ // interrupt would let 'write'/'done' frames for the NEXT turn
46
+ // coalesce in ahead of it and be wiped by the reset along with the
47
+ // cancelled turn's audio.
48
+ this.stopTurn('interrupt');
29
49
  } else if (d.event === 'done') {
30
50
  this.isDone = true;
31
51
  }
@@ -35,13 +55,33 @@ class PlaybackProcessor extends AudioWorkletProcessor {
35
55
  // Reset per-turn state so the node is reusable across replies without
36
56
  // reallocating the sample buffer or re-instantiating the worklet.
37
57
  resetTurn() {
38
- this.interrupted = false;
39
58
  this.isDone = false;
40
59
  this.playing = false;
60
+ // Whether any real audio has been rendered this turn. Separates a turn's
61
+ // pre-roll (nothing to extrapolate from, and not a defect) from a
62
+ // mid-turn underrun.
63
+ this.hasPlayed = false;
64
+ this.fillTarget = this.jitterSamples;
41
65
  // Carry-over byte for split samples across chunks
42
66
  this.carry = null;
43
67
  this.writePos = 0;
44
68
  this.readPos = 0;
69
+ // Concealment ring state and the current fade position.
70
+ this.concealLen = 0;
71
+ this.concealWrite = 0;
72
+ this.concealPos = 0;
73
+ this.concealGain = 1;
74
+ // Episode flags, so a multi-quantum gap counts as one event.
75
+ this.concealing = false;
76
+ this.concealedSilence = false;
77
+ // Reported to the host on 'stop'. A fresh object per turn: the one just
78
+ // posted must not be mutated by the next turn.
79
+ this.stats = {
80
+ concealedSamples: 0,
81
+ silentConcealedSamples: 0,
82
+ concealmentEvents: 0,
83
+ silentConcealmentEvents: 0,
84
+ };
45
85
  }
46
86
 
47
87
  // End the current turn: notify the host and rearm for the next reply.
@@ -51,10 +91,82 @@ class PlaybackProcessor extends AudioWorkletProcessor {
51
91
  // stop belongs to: interrupt-stops are dropped host-side (flush() already
52
92
  // settled that turn), so they can never resolve a later turn's done() early.
53
93
  stopTurn(reason) {
54
- this.port.postMessage({ event: 'stop', reason });
94
+ this.port.postMessage({ event: 'stop', reason, stats: this.stats });
55
95
  this.resetTurn();
56
96
  }
57
97
 
98
+ // Cover a quantum (from \`start\`) where real audio should have been.
99
+ //
100
+ // Before the turn's first samples there is nothing to extrapolate from, so
101
+ // the gap is plain silence and counted as nothing — WebRTC likewise only
102
+ // counts concealment once playout has begun. After that, loop the retained
103
+ // tail under a decaying gain: a hard zero-fill is a discontinuity mid-word,
104
+ // which is the click that makes a brief stall sound like breakage.
105
+ coverGap(out, start) {
106
+ if (!this.hasPlayed) {
107
+ out.fill(0, start);
108
+ return;
109
+ }
110
+ if (!this.concealing) {
111
+ this.concealing = true;
112
+ this.concealedSilence = false;
113
+ this.stats.concealmentEvents++;
114
+ }
115
+ const total = out.length - start;
116
+ const len = this.concealLen;
117
+ let silent = 0;
118
+ // Second condition: once the fade has decayed to the floor, every sample
119
+ // the loop would emit is 0 anyway — bulk-fill instead of running 128
120
+ // branchy iterations per quantum for the whole tail of a long stall.
121
+ if (len === 0 || this.concealGain < ${PLAYBACK_CONCEAL_FLOOR}) {
122
+ out.fill(0, start);
123
+ silent = total;
124
+ } else {
125
+ let g = this.concealGain;
126
+ for (let i = start; i < out.length; i++) {
127
+ if (g < ${PLAYBACK_CONCEAL_FLOOR}) {
128
+ // The fade has run out: keep counting the gap, but stop looping a
129
+ // fragment that is now inaudible anyway.
130
+ out[i] = 0;
131
+ silent++;
132
+ continue;
133
+ }
134
+ out[i] = this.concealBuf[this.concealPos] * g;
135
+ this.concealPos = this.concealPos + 1 === len ? 0 : this.concealPos + 1;
136
+ g *= this.concealDecay;
137
+ }
138
+ this.concealGain = g;
139
+ }
140
+ this.stats.concealedSamples += total;
141
+ if (silent > 0) {
142
+ this.stats.silentConcealedSamples += silent;
143
+ if (!this.concealedSilence) {
144
+ this.concealedSilence = true;
145
+ this.stats.silentConcealmentEvents++;
146
+ }
147
+ }
148
+ }
149
+
150
+ // Retain the tail of a rendered quantum as the next gap's concealment
151
+ // source, and close any episode the real audio just ended.
152
+ rememberTail(out, n) {
153
+ const cap = this.concealCapacity;
154
+ const take = Math.min(n, cap);
155
+ // Bulk copies (this runs on every cleanly rendered quantum): the tail is
156
+ // one contiguous source run, landing in at most two ring runs.
157
+ const tail = out.subarray(n - take, n);
158
+ const first = Math.min(take, cap - this.concealWrite);
159
+ this.concealBuf.set(tail.subarray(0, first), this.concealWrite);
160
+ if (take > first) this.concealBuf.set(tail.subarray(first), 0);
161
+ this.concealWrite = (this.concealWrite + take) % cap;
162
+ this.concealLen = Math.min(cap, this.concealLen + take);
163
+ // Read the loop oldest-first; once the ring is full the write cursor is
164
+ // the oldest retained sample.
165
+ this.concealPos = this.concealLen === cap ? this.concealWrite : 0;
166
+ this.concealing = false;
167
+ this.concealGain = 1;
168
+ }
169
+
58
170
  ingestBytes(uint8) {
59
171
  let bytes = uint8;
60
172
 
@@ -114,24 +226,31 @@ class PlaybackProcessor extends AudioWorkletProcessor {
114
226
  // session), so guard like the capture processor does.
115
227
  if (!outputs[0] || !outputs[0][0]) return true;
116
228
  const out = outputs[0][0];
117
- if (this.interrupted) {
118
- out.fill(0);
119
- this.stopTurn('interrupt');
120
- return true;
121
- }
122
-
123
229
  const avail = this.writePos - this.readPos;
124
230
 
125
- // Wait for jitter buffer to fill, unless done (short utterance)
231
+ // Filling: wait for the target. 'done' short-circuits it — what is
232
+ // buffered is all there will be, so there is nothing left to wait for.
126
233
  if (!this.playing) {
127
- if (avail >= this.jitterSamples || this.isDone) {
234
+ if (avail >= this.fillTarget || this.isDone) {
128
235
  this.playing = true;
129
236
  } else {
130
- out.fill(0);
237
+ this.coverGap(out, 0);
131
238
  return true;
132
239
  }
133
240
  }
134
241
 
242
+ // Underrun: this quantum cannot be filled and more audio is still coming.
243
+ // Go back to filling (at the refill target) and cover the gap, leaving
244
+ // readPos untouched — the fragment stays buffered and plays intact once
245
+ // the buffer recovers, instead of being dribbled out a few samples at a
246
+ // time for the rest of the turn.
247
+ if (avail < out.length && !this.isDone) {
248
+ this.playing = false;
249
+ this.fillTarget = this.refillSamples;
250
+ this.coverGap(out, 0);
251
+ return true;
252
+ }
253
+
135
254
  if (avail > 0) {
136
255
  const n = Math.min(avail, out.length);
137
256
  // Copy from the ring buffer, splitting across the wrap boundary.
@@ -140,11 +259,16 @@ class PlaybackProcessor extends AudioWorkletProcessor {
140
259
  out.set(this.samples.subarray(start, start + first), 0);
141
260
  if (n > first) out.set(this.samples.subarray(0, n - first), first);
142
261
  this.readPos += n;
262
+ // Only reachable with n < out.length on the turn's final partial
263
+ // quantum (the underrun branch above catches every other case).
143
264
  out.fill(0, n);
265
+ this.hasPlayed = true;
266
+ this.rememberTail(out, n);
144
267
  return true;
145
268
  }
146
269
 
147
- // No data: output silence, end the turn only when done
270
+ // Drained and done: end the turn. Not reachable mid-turn — an empty
271
+ // buffer with audio still coming is the underrun branch above.
148
272
  out.fill(0);
149
273
  if (this.isDone) {
150
274
  this.stopTurn('done');
@@ -157,7 +281,6 @@ registerProcessor('playback-processor', PlaybackProcessor);
157
281
  `;
158
282
  /** Raw worklet source — exported so tests can evaluate the processor directly. */
159
283
  const playbackProcessorSource = PlaybackProcessorWorklet;
160
- const script = new Blob([PlaybackProcessorWorklet], { type: "application/javascript" });
161
- const src = URL.createObjectURL(script);
284
+ var playback_processor_default = workletModuleUrl(PlaybackProcessorWorklet);
162
285
  //#endregion
163
- export { src as default, playbackProcessorSource };
286
+ export { playback_processor_default as default, playbackProcessorSource };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alexkroman1/aai-ui",
3
- "version": "1.16.0",
3
+ "version": "3.0.0",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "dist",
@@ -20,7 +20,7 @@
20
20
  "clsx": "^2.1.1",
21
21
  "partysocket": "^1.3.0",
22
22
  "use-sync-external-store": "^1.6.0",
23
- "@alexkroman1/aai": "1.16.0"
23
+ "@alexkroman1/aai": "3.0.0"
24
24
  },
25
25
  "peerDependencies": {
26
26
  "react": "^19.0.0",
@@ -1,193 +0,0 @@
1
- import { useSessionCore, useSessionSelector, useTheme } from "./context.js";
2
- import { Button } from "./components/button.js";
3
- import { r as TEXT_FAINT, t as ERROR_COLOR } from "./_colors-DYX7XRTr.js";
4
- import { t as AaiLogo } from "./aai-logo-B8lDmsut.js";
5
- import { r as SessionUrlChips, t as Controls } from "./controls-BbZcmnJf.js";
6
- import { t as Eyebrow } from "./eyebrow-C6ZFuiz6.js";
7
- import { MessageList } from "./components/message-list.js";
8
- import clsx from "clsx";
9
- import { memo, useRef, useState } from "react";
10
- import { jsx, jsxs } from "react/jsx-runtime";
11
- import { errorMessage } from "@alexkroman1/aai";
12
- //#region components/text-controls.tsx
13
- /** @jsxImportSource react */
14
- /**
15
- * Controls for a text-only session (`tts: none()`): a **record** toggle that
16
- * streams the microphone while active, an **upload** button that transcribes
17
- * an audio file, and **New Conversation**. Replies arrive as text in the
18
- * {@link MessageList} — there is no playback side.
19
- *
20
- * Rendered automatically by {@link ChatView} when the server declares the
21
- * session text-only; custom clients can compose it directly.
22
- *
23
- * @public
24
- */
25
- const TextControls = memo(function TextControls({ className }) {
26
- const recording = useSessionSelector((s) => s.recording);
27
- const state = useSessionSelector((s) => s.state);
28
- const core = useSessionCore();
29
- const fileInput = useRef(null);
30
- const [uploading, setUploading] = useState(false);
31
- const [uploadError, setUploadError] = useState(null);
32
- const connected = state !== "disconnected" && state !== "connecting" && state !== "error";
33
- function toggleRecording() {
34
- if (recording) core.stopRecording();
35
- else core.startRecording();
36
- }
37
- function onFileChosen(file) {
38
- if (!file || uploading) return;
39
- setUploading(true);
40
- setUploadError(null);
41
- core.sendAudioFile(file).catch((err) => {
42
- setUploadError(errorMessage(err));
43
- }).finally(() => {
44
- setUploading(false);
45
- if (fileInput.current) fileInput.current.value = "";
46
- });
47
- }
48
- return /* @__PURE__ */ jsxs("div", {
49
- className: clsx("flex flex-col gap-1.5 shrink-0", className),
50
- children: [/* @__PURE__ */ jsxs("div", {
51
- className: "flex items-center gap-2",
52
- children: [
53
- /* @__PURE__ */ jsxs(Button, {
54
- variant: recording ? "default" : "secondary",
55
- onClick: toggleRecording,
56
- disabled: !connected || uploading,
57
- "aria-pressed": recording,
58
- style: recording ? { background: ERROR_COLOR } : void 0,
59
- "data-testid": "record-button",
60
- children: [/* @__PURE__ */ jsx("span", {
61
- className: clsx("w-2 h-2 rounded-full mr-2", recording && "animate-pulse"),
62
- style: { background: recording ? "#fff" : ERROR_COLOR }
63
- }), recording ? "Stop recording" : "Record"]
64
- }),
65
- /* @__PURE__ */ jsx(Button, {
66
- variant: "ghost",
67
- onClick: () => fileInput.current?.click(),
68
- disabled: !connected || uploading,
69
- "data-testid": "upload-button",
70
- children: uploading ? "Transcribing…" : "Upload audio"
71
- }),
72
- /* @__PURE__ */ jsx("input", {
73
- ref: fileInput,
74
- type: "file",
75
- accept: "audio/*",
76
- className: "hidden",
77
- onChange: (e) => onFileChosen(e.target.files?.[0]),
78
- "data-testid": "upload-input"
79
- }),
80
- /* @__PURE__ */ jsx(Button, {
81
- variant: "ghost",
82
- onClick: core.reset,
83
- children: "New Conversation"
84
- }),
85
- /* @__PURE__ */ jsx(SessionUrlChips, { className: "ml-auto max-w-[55%]" })
86
- ]
87
- }), uploadError && /* @__PURE__ */ jsx("div", {
88
- className: "text-[12px]",
89
- style: { color: "#B3261E" },
90
- "data-testid": "upload-error",
91
- children: uploadError
92
- })]
93
- });
94
- });
95
- //#endregion
96
- //#region components/chat-view.tsx
97
- /** @jsxImportSource react */
98
- const PULSING_STATES = /* @__PURE__ */ new Set(["listening", "speaking"]);
99
- /**
100
- * Indicator dot color per state, on the light refresh palette. Shared with
101
- * the sync-transport chat shell's status eyebrow.
102
- *
103
- * @internal
104
- */
105
- function stateColor(state, primary) {
106
- switch (state) {
107
- case "listening":
108
- case "speaking":
109
- case "ready": return primary;
110
- case "thinking": return "#B98900";
111
- case "error": return ERROR_COLOR;
112
- default: return TEXT_FAINT;
113
- }
114
- }
115
- /**
116
- * The main chat interface for a voice agent session — the design-system
117
- * "voice agent console": a 760px column on the cream page with a header
118
- * (logo + live-status eyebrow), the conversation on a raised white card,
119
- * and the session controls beneath it.
120
- *
121
- * Must be rendered inside a {@link SessionProvider}.
122
- *
123
- * @example
124
- * ```tsx
125
- * <StartScreen icon="🍕" title="Pizza Palace">
126
- * <ChatView />
127
- * </StartScreen>
128
- * ```
129
- *
130
- * @param icon - Optional element rendered in place of the logo in the header.
131
- * @param title - Optional title string for the header.
132
- * @param className - Additional CSS class names applied to the root element.
133
- *
134
- * @public
135
- */
136
- function ChatView({ icon, title, className }) {
137
- const state = useSessionSelector((s) => s.state);
138
- const error = useSessionSelector((s) => s.error);
139
- const audioOut = useSessionSelector((s) => s.audioOut);
140
- const theme = useTheme();
141
- const pulsing = PULSING_STATES.has(state);
142
- return /* @__PURE__ */ jsxs("div", {
143
- className: clsx("flex flex-col h-screen w-full max-w-190 mx-auto box-border px-6 py-8 gap-5 font-aai text-sm", className),
144
- style: {
145
- background: theme.bg,
146
- color: theme.text
147
- },
148
- children: [
149
- /* @__PURE__ */ jsxs("div", {
150
- className: "flex items-center justify-between shrink-0",
151
- children: [/* @__PURE__ */ jsxs("div", {
152
- className: "flex items-center gap-3 min-w-0",
153
- children: [icon ?? /* @__PURE__ */ jsx(AaiLogo, { size: 22 }), title && /* @__PURE__ */ jsx("span", {
154
- className: "font-aai-serif text-[22px] leading-[1.2] font-normal truncate",
155
- style: { color: theme.text },
156
- children: title
157
- })]
158
- }), /* @__PURE__ */ jsxs(Eyebrow, {
159
- className: "shrink-0",
160
- "data-state": state,
161
- children: [/* @__PURE__ */ jsx("span", {
162
- className: "w-[7px] h-[7px] rounded-full",
163
- style: {
164
- background: stateColor(state, theme.primary),
165
- animation: pulsing ? "aai-pulse 1.6s ease-in-out infinite" : "none"
166
- }
167
- }), state]
168
- })]
169
- }),
170
- error && /* @__PURE__ */ jsx("div", {
171
- className: "px-3.5 py-2.5 rounded-aai border text-[13px] leading-[130%] shrink-0",
172
- style: {
173
- borderColor: "rgba(179,38,30,0.35)",
174
- background: "rgba(179,38,30,0.06)",
175
- color: "#B3261E"
176
- },
177
- children: error.message
178
- }),
179
- /* @__PURE__ */ jsx("div", {
180
- className: "flex flex-col flex-1 min-h-0 border rounded-lg overflow-hidden",
181
- style: {
182
- background: theme.surface,
183
- borderColor: theme.border,
184
- boxShadow: "0 1px 3px 0 rgb(20 18 12 / 0.06)"
185
- },
186
- children: /* @__PURE__ */ jsx(MessageList, {})
187
- }),
188
- audioOut ? /* @__PURE__ */ jsx(Controls, {}) : /* @__PURE__ */ jsx(TextControls, {})
189
- ]
190
- });
191
- }
192
- //#endregion
193
- export { stateColor as n, TextControls as r, ChatView as t };
@@ -1,18 +0,0 @@
1
- /**
2
- * Sync-transport view: a hands-free VAD-endpointed conversation, transcript
3
- * + reply output, and the endpoint each utterance is POSTed to — one HTTP
4
- * request per turn.
5
- *
6
- * @public
7
- */
8
- export declare function SyncChatView({ syncUrl, title, greeting, }: {
9
- /** The agent server's sync endpoint, e.g. `https://host/slug/sync`. */
10
- syncUrl: string;
11
- /** Agent name shown in the header. */
12
- title?: string | undefined;
13
- /**
14
- * Greeting shown as the card's opening line. Sync turns have no session
15
- * start for the server to speak it on, so it is display-only.
16
- */
17
- greeting?: string | undefined;
18
- }): import("react").JSX.Element;
@@ -1,14 +0,0 @@
1
- /**
2
- * Controls for a text-only session (`tts: none()`): a **record** toggle that
3
- * streams the microphone while active, an **upload** button that transcribes
4
- * an audio file, and **New Conversation**. Replies arrive as text in the
5
- * {@link MessageList} — there is no playback side.
6
- *
7
- * Rendered automatically by {@link ChatView} when the server declares the
8
- * session text-only; custom clients can compose it directly.
9
- *
10
- * @public
11
- */
12
- export declare const TextControls: import("react").MemoExoticComponent<({ className, }: {
13
- className?: string | undefined;
14
- }) => import("react").JSX.Element>;
@@ -1 +0,0 @@
1
- import{t as e}from"./index-BunwIXSP.js";var t=1e3,n=65e3,r=250;async function i(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),c=0;for(let e of o){let t=Math.max(-1,Math.min(1,e));s[c++]=t<0?t*32768:t*32767}return s}async function a(i){let{sttSampleRate:a,ttsSampleRate:o,captureWorkletSrc:s,playbackWorkletSrc:c,onMicData:l,onError:u}=i,d=o,f=new AudioContext({sampleRate:d,latencyHint:`playback`}),p=navigator.mediaDevices.getUserMedia({audio:{deviceId:{ideal:`default`},echoCancellation:!0,noiseSuppression:!0,autoGainControl:!0,voiceIsolation:!0}}),m;try{[m]=await Promise.all([p,f.resume(),f.audioWorklet.addModule(s),f.audioWorklet.addModule(c)])}catch(e){throw p.then(e=>{for(let t of e.getTracks())t.stop()}).catch(()=>{}),await f.close().catch(e=>{console.warn(`AudioContext close failed:`,e)}),e}let h=f.createMediaStreamSource(m),g=new AudioWorkletNode(f,`capture-processor`,{channelCount:1,channelCountMode:`explicit`,processorOptions:{contextRate:d,sttSampleRate:a,bufferSeconds:e}});h.connect(g),g.onprocessorerror=()=>{let e=Error(`Audio capture worklet crashed`);console.error(`[aai-ui]`,e.message),u?.(e)},g.port.postMessage({event:`start`});let _=null;g.port.onmessage=e=>{e.data.event===`chunk`?l(e.data.buffer):e.data.event===`stopped`&&(_?.(),_=null)};let v=null,y=null,b=new AbortController;function x(){if(v)return v;let e=new AudioWorkletNode(f,`playback-processor`,{processorOptions:{sampleRate:d}});return e.connect(f.destination),e.port.onmessage=e=>{if(e.data.event===`stop`){if(e.data.reason===`interrupt`)return;y?.(),y=null}},e.onprocessorerror=()=>{let e=Error(`Audio playback worklet crashed`);console.error(`[aai-ui]`,e.message),y?.(),y=null,u?.(e)},v=e,e}let S={enqueue(e){b.signal.aborted||e.byteLength!==0&&x().port.postMessage({event:`write`,buffer:new Uint8Array(e)},[e])},done(){return!v||f.state!==`running`?Promise.resolve():new Promise(e=>{y?.();let r=()=>{clearInterval(i),clearTimeout(a),y===r&&(y=null),e()},i=setInterval(()=>{f.state!==`running`&&r()},t),a=setTimeout(r,n);y=r,v?.port.postMessage({event:`done`})})},flush(){v&&(y?.(),y=null,v.port.postMessage({event:`interrupt`}))},async close(){if(!b.signal.aborted){b.abort(),await new Promise(e=>{let t=setTimeout(e,r);_=()=>{clearTimeout(t),e()},g.port.postMessage({event:`stop`})}),h.disconnect(),g.disconnect(),v&&v.disconnect();for(let e of m.getTracks())e.stop();await f.close().catch(()=>{})}},async[Symbol.asyncDispose](){await S.close()}};return S}export{a as createVoiceIO,i as decodeAudioToPcm16};
@@ -1,108 +0,0 @@
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, then ack
32
- // so the host knows the tail chunk (if any) has been posted and it is
33
- // safe to tear the context down.
34
- this.flush();
35
- this.recording = false;
36
- this.port.postMessage({ event: 'stopped' });
37
- }
38
- };
39
- }
40
-
41
- resample(input) {
42
- const ratio = this.ratio;
43
- const n = input.length;
44
- if (n === 0) return new Float32Array(0);
45
- // At most ceil(n / ratio) + 1 outputs per block (pos starts in [0, ratio)).
46
- const maxLen = Math.ceil(n / ratio) + 1;
47
- if (this.resampleBuf === null || this.resampleBuf.length < maxLen) {
48
- this.resampleBuf = new Float32Array(maxLen);
49
- }
50
- const out = this.resampleBuf;
51
- // Extended frame: index 0 = prev (previous block's last sample),
52
- // index k>=1 = input[k-1]. Interpolate at fractional positions stepping by
53
- // ratio, carrying \`pos\` across calls to preserve the sample clock.
54
- let count = 0;
55
- let pos = this.pos;
56
- while (pos < n) {
57
- const idx = pos | 0;
58
- const frac = pos - idx;
59
- const a = idx === 0 ? this.prev : input[idx - 1];
60
- const b = input[idx];
61
- out[count++] = a + frac * (b - a);
62
- pos += ratio;
63
- }
64
- // Shift the origin to this block's last sample for the next call.
65
- this.prev = input[n - 1];
66
- this.pos = pos - n;
67
- return out.subarray(0, count);
68
- }
69
-
70
- // Convert Float32 -> Int16 and append to the pending batch. Writes through
71
- // an Int16Array directly (assignment truncates like DataView.setInt16).
72
- accumulate(samples) {
73
- let buf = this.pending;
74
- if (this.pendingLen + samples.length > buf.length) {
75
- // Defensive: only reachable if a render quantum outproduces the 1x
76
- // headroom above the flush target (never with 128-sample quanta).
77
- const grown = new Int16Array((this.pendingLen + samples.length) * 2);
78
- grown.set(buf.subarray(0, this.pendingLen));
79
- this.pending = grown;
80
- buf = grown;
81
- }
82
- for (let i = 0; i < samples.length; i++) {
83
- const s = Math.max(-1, Math.min(1, samples[i]));
84
- buf[this.pendingLen++] = s < 0 ? s * 0x8000 : s * 0x7fff;
85
- }
86
- }
87
-
88
- // Post the batched samples as one transferred ArrayBuffer and reset.
89
- flush() {
90
- if (this.pendingLen === 0) return;
91
- const buffer = this.pending.buffer.slice(0, this.pendingLen * 2);
92
- this.pendingLen = 0;
93
- this.port.postMessage({ event: 'chunk', buffer }, [buffer]);
94
- }
95
-
96
- process(inputs) {
97
- const input = inputs[0];
98
- if (!input || !input[0] || !this.recording) return true;
99
-
100
- const samples = this.needsResample ? this.resample(input[0]) : input[0];
101
- this.accumulate(samples);
102
- if (this.pendingLen >= this.targetSamples) this.flush();
103
- return true;
104
- }
105
- }
106
-
107
- registerProcessor('capture-processor', CaptureProcessor);
108
- `],{type:`application/javascript`}),t=URL.createObjectURL(e);export{t as default};
@@ -1,2 +0,0 @@
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);--animate-pulse:pulse 2s cubic-bezier(.4, 0, .6, 1) infinite;--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{.invisible{visibility:hidden}.absolute{position:absolute}.fixed{position:fixed}.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-1\.5{margin-top:calc(var(--spacing) * 1.5)}.mt-2{margin-top:calc(var(--spacing) * 2)}.mr-2{margin-right:calc(var(--spacing) * 2)}.ml-auto{margin-left:auto}.box-border{box-sizing:border-box}.block{display:block}.flex{display:flex}.hidden{display:none}.inline{display:inline}.inline-flex{display:inline-flex}.h-1\.5{height:calc(var(--spacing) * 1.5)}.h-2{height:calc(var(--spacing) * 2)}.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-2{width:calc(var(--spacing) * 2)}.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-\[55\%\]{max-width:55%}.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}.grow{flex-grow:1}.rotate-90{rotate:90deg}.animate-pulse{animation:var(--animate-pulse)}.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-\[12px\]{font-size:12px}.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}@keyframes pulse{50%{opacity:.5}}