@alexkroman1/aai-ui 1.16.0 → 2.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 (44) hide show
  1. package/dist/audio.d.ts +43 -5
  2. package/dist/audio.js +39 -20
  3. package/dist/chat-view-DKFhxMAT.js +129 -0
  4. package/dist/client-config.d.ts +7 -6
  5. package/dist/components/chat-view.d.ts +1 -8
  6. package/dist/components/chat-view.js +2 -2
  7. package/dist/components/console-shell.d.ts +37 -0
  8. package/dist/components/controls.js +1 -1
  9. package/dist/components/url-chips.d.ts +1 -1
  10. package/dist/components/workflow-view.d.ts +13 -0
  11. package/dist/{controls-BbZcmnJf.js → controls-4OJoekj6.js} +1 -1
  12. package/dist/default-client/assets/audio-Cgviqo9t.js +1 -0
  13. package/dist/default-client/assets/capture-processor-DLHxAIfT.js +90 -0
  14. package/dist/default-client/assets/index-BgbIWnfG.css +2 -0
  15. package/dist/default-client/assets/index-DbKKR3UE.js +62 -0
  16. package/dist/default-client/assets/playback-processor-bMTdFp-8.js +271 -0
  17. package/dist/default-client/assets/rolldown-runtime-BpQH8Ho1.js +1 -0
  18. package/dist/default-client/assets/types-Bpg3ZIZK.js +64 -0
  19. package/dist/default-client/index.html +5 -2
  20. package/dist/define-client-yoGybEYR.js +710 -0
  21. package/dist/define-client.d.ts +0 -9
  22. package/dist/define-client.js +1 -1
  23. package/dist/index.d.ts +3 -4
  24. package/dist/index.js +6 -5
  25. package/dist/{session-core-B64kau_v.js → session-core-BLhiQ18c.js} +6 -0
  26. package/dist/session-core.js +1 -1
  27. package/dist/sync-mic.d.ts +11 -43
  28. package/dist/types.d.ts +24 -1
  29. package/dist/types.js +32 -2
  30. package/dist/worklets/capture-processor.d.ts +1 -1
  31. package/dist/worklets/capture-processor.js +32 -49
  32. package/dist/worklets/playback-processor.d.ts +1 -1
  33. package/dist/worklets/playback-processor.js +125 -9
  34. package/package.json +2 -2
  35. package/dist/chat-view-gi6FccZq.js +0 -193
  36. package/dist/components/sync-chat-view.d.ts +0 -18
  37. package/dist/components/text-controls.d.ts +0 -14
  38. package/dist/default-client/assets/audio-DNDZgEZp.js +0 -1
  39. package/dist/default-client/assets/capture-processor-UlKEKyIW.js +0 -108
  40. package/dist/default-client/assets/index-BogmeUln.css +0 -2
  41. package/dist/default-client/assets/index-BunwIXSP.js +0 -124
  42. package/dist/default-client/assets/playback-processor-C5HVRVbu.js +0 -156
  43. package/dist/define-client-DdpijqAu.js +0 -906
  44. package/dist/sync-vad.d.ts +0 -54
package/dist/audio.d.ts CHANGED
@@ -7,6 +7,29 @@
7
7
  * @throws If the browser cannot decode the payload.
8
8
  */
9
9
  export declare function decodeAudioToPcm16(data: ArrayBuffer, targetRate: number): Promise<Int16Array>;
10
+ /**
11
+ * How much of one turn's playback was covered by concealment rather than
12
+ * received audio — the playback worklet's underrun report, in the shape
13
+ * WebRTC's `inbound-rtp` audio stats use, so the numbers mean the same thing
14
+ * here as in a `getStats()` dump.
15
+ *
16
+ * A turn with `concealmentEvents: 0` never needed its jitter buffer; a turn
17
+ * with a high `silentConcealedSamples` share starved for longer than
18
+ * concealment can plausibly cover, which is a bandwidth problem rather than a
19
+ * buffer-tuning one.
20
+ *
21
+ * @public
22
+ */
23
+ export type PlaybackStats = {
24
+ /** Samples emitted to cover a gap, including the silent ones. */
25
+ concealedSamples: number;
26
+ /** The subset of {@link PlaybackStats.concealedSamples} that were silence. */
27
+ silentConcealedSamples: number;
28
+ /** Distinct underrun episodes, however many render quanta each spanned. */
29
+ concealmentEvents: number;
30
+ /** Episodes that lasted long enough to decay to silence. */
31
+ silentConcealmentEvents: number;
32
+ };
10
33
  /** Configuration for creating a {@link VoiceIO} instance. */
11
34
  export type VoiceIOOptions = {
12
35
  /** Sample rate in Hz expected by the STT engine (e.g. 16000). */
@@ -25,13 +48,24 @@ export type VoiceIOOptions = {
25
48
  * transition out of listening/speaking instead of looking healthy forever.
26
49
  */
27
50
  onError?: ((err: Error) => void) | undefined;
51
+ /**
52
+ * Called at the end of any turn whose playback had to conceal a gap. Never
53
+ * called for a clean turn, so it can be wired straight to a warning.
54
+ */
55
+ onPlaybackStats?: ((stats: PlaybackStats) => void) | undefined;
56
+ /**
57
+ * Called once if the microphone delivers nothing but digital silence for
58
+ * the first {@link MIC_SILENCE_PROBE_MS} of capture — a muted or wrong input
59
+ * device, which otherwise looks exactly like a user who hasn't spoken.
60
+ */
61
+ onMicSilent?: (() => void) | undefined;
28
62
  };
29
63
  /**
30
64
  * Audio I/O interface for voice capture and playback.
31
65
  *
32
- * Manages microphone capture via an AudioWorklet, resampling to the STT
33
- * sample rate, and TTS audio playback through a second AudioWorklet. Implements
34
- * {@link AsyncDisposable} for resource cleanup.
66
+ * Manages microphone capture via an AudioWorklet and TTS audio playback
67
+ * through a second AudioWorklet. Implements {@link AsyncDisposable} for
68
+ * resource cleanup.
35
69
  */
36
70
  export type VoiceIO = AsyncDisposable & {
37
71
  /** Enqueue a PCM16 audio buffer for playback through the TTS pipeline. */
@@ -48,8 +82,12 @@ export type VoiceIO = AsyncDisposable & {
48
82
  * Create a {@link VoiceIO} instance that captures microphone audio and
49
83
  * plays back TTS audio using the Web Audio API.
50
84
  *
51
- * The AudioContext runs at the TTS sample rate for playback fidelity.
52
- * Captured audio is resampled to the STT rate when the rates differ.
85
+ * Playback runs on a context at the TTS sample rate for fidelity, and capture
86
+ * on its own context at the STT rate so the *browser* performs the rate
87
+ * conversion with its band-limited resampler. The two collapse into one
88
+ * context when the rates match. A browser that declines either requested rate
89
+ * fails init rather than falling back to converting in the worklet, which
90
+ * would alias.
53
91
  *
54
92
  * @param opts - Voice I/O configuration options.
55
93
  * @returns A promise that resolves to a {@link VoiceIO} handle.
package/dist/audio.js CHANGED
@@ -1,4 +1,4 @@
1
- import { MIC_BUFFER_SECONDS } from "./types.js";
1
+ import { MIC_BUFFER_SECONDS, VOICE_CAPTURE_CONSTRAINTS } from "./types.js";
2
2
  //#region audio.ts
3
3
  /** How often {@link VoiceIO.done} checks that the AudioContext is still rendering. */
4
4
  const DONE_POLL_INTERVAL_MS = 1e3;
@@ -39,55 +39,71 @@ async function decodeAudioToPcm16(data, targetRate) {
39
39
  }
40
40
  return pcm;
41
41
  }
42
+ /** Throw unless the browser honored a requested context sample rate. */
43
+ function assertGranted(granted, requested, side) {
44
+ if (granted === requested) return;
45
+ throw new Error(`Browser refused the ${side} sample rate: asked for ${requested} Hz, got ${granted} Hz`);
46
+ }
42
47
  /**
43
48
  * Create a {@link VoiceIO} instance that captures microphone audio and
44
49
  * plays back TTS audio using the Web Audio API.
45
50
  *
46
- * The AudioContext runs at the TTS sample rate for playback fidelity.
47
- * Captured audio is resampled to the STT rate when the rates differ.
51
+ * Playback runs on a context at the TTS sample rate for fidelity, and capture
52
+ * on its own context at the STT rate so the *browser* performs the rate
53
+ * conversion with its band-limited resampler. The two collapse into one
54
+ * context when the rates match. A browser that declines either requested rate
55
+ * fails init rather than falling back to converting in the worklet, which
56
+ * would alias.
48
57
  *
49
58
  * @param opts - Voice I/O configuration options.
50
59
  * @returns A promise that resolves to a {@link VoiceIO} handle.
51
60
  * @throws If microphone access is denied or AudioWorklet registration fails.
52
61
  */
53
62
  async function createVoiceIO(opts) {
54
- const { sttSampleRate, ttsSampleRate, captureWorkletSrc, playbackWorkletSrc, onMicData, onError } = opts;
55
- const contextRate = ttsSampleRate;
63
+ const { sttSampleRate, ttsSampleRate, captureWorkletSrc, playbackWorkletSrc, onMicData, onError, onPlaybackStats, onMicSilent } = opts;
56
64
  const ctx = new AudioContext({
57
- sampleRate: contextRate,
65
+ sampleRate: ttsSampleRate,
58
66
  latencyHint: "playback"
59
67
  });
68
+ const sharesContext = sttSampleRate === ttsSampleRate;
69
+ const capCtx = sharesContext ? ctx : new AudioContext({
70
+ sampleRate: sttSampleRate,
71
+ latencyHint: "interactive"
72
+ });
73
+ async function closeContexts() {
74
+ const contexts = sharesContext ? [ctx] : [ctx, capCtx];
75
+ await Promise.all(contexts.map((c) => c.close().catch((err) => {
76
+ console.warn("AudioContext close failed:", err);
77
+ })));
78
+ }
60
79
  const streamPromise = navigator.mediaDevices.getUserMedia({ audio: {
61
80
  deviceId: { ideal: "default" },
62
- echoCancellation: true,
63
- noiseSuppression: true,
64
- autoGainControl: true,
65
- voiceIsolation: true
81
+ ...VOICE_CAPTURE_CONSTRAINTS
66
82
  } });
67
83
  let stream;
68
84
  try {
69
85
  [stream] = await Promise.all([
70
86
  streamPromise,
71
87
  ctx.resume(),
72
- ctx.audioWorklet.addModule(captureWorkletSrc),
88
+ capCtx.resume(),
89
+ capCtx.audioWorklet.addModule(captureWorkletSrc),
73
90
  ctx.audioWorklet.addModule(playbackWorkletSrc)
74
91
  ]);
92
+ assertGranted(capCtx.sampleRate, sttSampleRate, "capture");
93
+ assertGranted(ctx.sampleRate, ttsSampleRate, "playback");
75
94
  } catch (err) {
76
95
  streamPromise.then((s) => {
77
96
  for (const t of s.getTracks()) t.stop();
78
97
  }).catch(() => {});
79
- await ctx.close().catch((err) => {
80
- console.warn("AudioContext close failed:", err);
81
- });
98
+ await closeContexts();
82
99
  throw err;
83
100
  }
84
- const mic = ctx.createMediaStreamSource(stream);
85
- const capNode = new AudioWorkletNode(ctx, "capture-processor", {
101
+ const mic = capCtx.createMediaStreamSource(stream);
102
+ const capNode = new AudioWorkletNode(capCtx, "capture-processor", {
86
103
  channelCount: 1,
87
104
  channelCountMode: "explicit",
88
105
  processorOptions: {
89
- contextRate,
90
- sttSampleRate,
106
+ sampleRate: sttSampleRate,
91
107
  bufferSeconds: MIC_BUFFER_SECONDS
92
108
  }
93
109
  });
@@ -101,6 +117,7 @@ async function createVoiceIO(opts) {
101
117
  let onCaptureStopped = null;
102
118
  capNode.port.onmessage = (e) => {
103
119
  if (e.data.event === "chunk") onMicData(e.data.buffer);
120
+ else if (e.data.event === "silent") onMicSilent?.();
104
121
  else if (e.data.event === "stopped") {
105
122
  onCaptureStopped?.();
106
123
  onCaptureStopped = null;
@@ -111,10 +128,12 @@ async function createVoiceIO(opts) {
111
128
  const lifecycle = new AbortController();
112
129
  function ensurePlayNode() {
113
130
  if (playNode) return playNode;
114
- const node = new AudioWorkletNode(ctx, "playback-processor", { processorOptions: { sampleRate: contextRate } });
131
+ const node = new AudioWorkletNode(ctx, "playback-processor", { processorOptions: { sampleRate: ctx.sampleRate } });
115
132
  node.connect(ctx.destination);
116
133
  node.port.onmessage = (e) => {
117
134
  if (e.data.event === "stop") {
135
+ const stats = e.data.stats;
136
+ if (stats && stats.concealedSamples > 0) onPlaybackStats?.(stats);
118
137
  if (e.data.reason === "interrupt") return;
119
138
  onPlaybackStop?.();
120
139
  onPlaybackStop = null;
@@ -179,7 +198,7 @@ async function createVoiceIO(opts) {
179
198
  capNode.disconnect();
180
199
  if (playNode) playNode.disconnect();
181
200
  for (const t of stream.getTracks()) t.stop();
182
- await ctx.close().catch(() => {});
201
+ await closeContexts();
183
202
  },
184
203
  async [Symbol.asyncDispose]() {
185
204
  await io.close();
@@ -0,0 +1,129 @@
1
+ import { useSessionSelector, useTheme } from "./context.js";
2
+ import { r as TEXT_FAINT, t as ERROR_COLOR } from "./_colors-DYX7XRTr.js";
3
+ import { t as AaiLogo } from "./aai-logo-B8lDmsut.js";
4
+ import { t as Eyebrow } from "./eyebrow-C6ZFuiz6.js";
5
+ import { t as Controls } from "./controls-4OJoekj6.js";
6
+ import { MessageList } from "./components/message-list.js";
7
+ import clsx from "clsx";
8
+ import { jsx, jsxs } from "react/jsx-runtime";
9
+ //#region components/console-shell.tsx
10
+ /** @jsxImportSource react */
11
+ /**
12
+ * Indicator dot color per state, on the light refresh palette.
13
+ *
14
+ * @internal
15
+ */
16
+ function stateColor(state, primary) {
17
+ switch (state) {
18
+ case "listening":
19
+ case "speaking":
20
+ case "ready": return primary;
21
+ case "thinking": return "#B98900";
22
+ case "error": return ERROR_COLOR;
23
+ default: return TEXT_FAINT;
24
+ }
25
+ }
26
+ /**
27
+ * The design-system "console" chrome shared by the chat shell and the
28
+ * workflow run surface: a 760px column on the cream page with a header
29
+ * (logo + live-status eyebrow), an optional error banner, the main content
30
+ * on a raised white card, and a footer row beneath it.
31
+ *
32
+ * Extracted so the two default surfaces stay visually identical by
33
+ * construction — they used to be hand-copied down to the same `boxShadow`
34
+ * literal, and drifted.
35
+ *
36
+ * @internal
37
+ */
38
+ function ConsoleShell({ icon, title, state, pulsing, error, children, footer, className }) {
39
+ const theme = useTheme();
40
+ return /* @__PURE__ */ jsxs("div", {
41
+ 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),
42
+ style: {
43
+ background: theme.bg,
44
+ color: theme.text
45
+ },
46
+ children: [
47
+ /* @__PURE__ */ jsxs("div", {
48
+ className: "flex items-center justify-between shrink-0",
49
+ children: [/* @__PURE__ */ jsxs("div", {
50
+ className: "flex items-center gap-3 min-w-0",
51
+ children: [icon ?? /* @__PURE__ */ jsx(AaiLogo, { size: 22 }), title && /* @__PURE__ */ jsx("span", {
52
+ className: "font-aai-serif text-[22px] leading-[1.2] font-normal truncate",
53
+ style: { color: theme.text },
54
+ children: title
55
+ })]
56
+ }), /* @__PURE__ */ jsxs(Eyebrow, {
57
+ className: "shrink-0",
58
+ "data-state": state,
59
+ children: [/* @__PURE__ */ jsx("span", {
60
+ className: "w-[7px] h-[7px] rounded-full",
61
+ style: {
62
+ background: stateColor(state, theme.primary),
63
+ animation: pulsing ? "aai-pulse 1.6s ease-in-out infinite" : "none"
64
+ }
65
+ }), state]
66
+ })]
67
+ }),
68
+ error && /* @__PURE__ */ jsx("div", {
69
+ className: "px-3.5 py-2.5 rounded-aai border text-[13px] leading-[130%] shrink-0",
70
+ style: {
71
+ borderColor: "rgba(179,38,30,0.35)",
72
+ background: "rgba(179,38,30,0.06)",
73
+ color: "#B3261E"
74
+ },
75
+ children: error
76
+ }),
77
+ /* @__PURE__ */ jsx("div", {
78
+ className: "flex flex-col flex-1 min-h-0 border rounded-lg overflow-hidden",
79
+ style: {
80
+ background: theme.surface,
81
+ borderColor: theme.border,
82
+ boxShadow: "0 1px 3px 0 rgb(20 18 12 / 0.06)"
83
+ },
84
+ children
85
+ }),
86
+ footer
87
+ ]
88
+ });
89
+ }
90
+ //#endregion
91
+ //#region components/chat-view.tsx
92
+ const PULSING_STATES = /* @__PURE__ */ new Set(["listening", "speaking"]);
93
+ /**
94
+ * The main chat interface for a voice agent session — the design-system
95
+ * "voice agent console": a 760px column on the cream page with a header
96
+ * (logo + live-status eyebrow), the conversation on a raised white card,
97
+ * and the session controls beneath it.
98
+ *
99
+ * Must be rendered inside a {@link SessionProvider}.
100
+ *
101
+ * @example
102
+ * ```tsx
103
+ * <StartScreen icon="🍕" title="Pizza Palace">
104
+ * <ChatView />
105
+ * </StartScreen>
106
+ * ```
107
+ *
108
+ * @param icon - Optional element rendered in place of the logo in the header.
109
+ * @param title - Optional title string for the header.
110
+ * @param className - Additional CSS class names applied to the root element.
111
+ *
112
+ * @public
113
+ */
114
+ function ChatView({ icon, title, className }) {
115
+ const state = useSessionSelector((s) => s.state);
116
+ const error = useSessionSelector((s) => s.error);
117
+ return /* @__PURE__ */ jsx(ConsoleShell, {
118
+ icon,
119
+ title,
120
+ state,
121
+ pulsing: PULSING_STATES.has(state),
122
+ error: error?.message,
123
+ className,
124
+ footer: /* @__PURE__ */ jsx(Controls, {}),
125
+ children: /* @__PURE__ */ jsx(MessageList, {})
126
+ });
127
+ }
128
+ //#endregion
129
+ export { ConsoleShell as n, ChatView as t };
@@ -2,15 +2,16 @@
2
2
  * Pre-connection client-config lookup.
3
3
  *
4
4
  * `GET client-config` (relative to the agent's base URL — see
5
- * `sdk/client-config.ts` in `@alexkroman1/aai`) tells the default client how
6
- * to talk to the agent before any connection exists, most importantly which
7
- * transport `agent({ transport })` declared. Every failure path — network
8
- * error, 404 from an older server, malformed body — degrades to the
9
- * WebSocket default, so this lookup can never break an existing agent.
5
+ * `sdk/client-config.ts` in `@alexkroman1/aai`) tells the default client
6
+ * what kind of app the agent is before any connection exists a
7
+ * conversational agent (WebSocket chat shell) or a workflow (one-shot run
8
+ * surface). Every failure path — network error, 404 from an older server,
9
+ * malformed body — degrades to the agent default, so this lookup can never
10
+ * break an existing agent.
10
11
  */
11
12
  import { type ClientConfigResponse } from "@alexkroman1/aai/protocol";
12
13
  export type { ClientConfigResponse } from "@alexkroman1/aai/protocol";
13
14
  /** Resolve a relative endpoint path against the agent's base URL. */
14
15
  export declare function buildAgentUrl(platformUrl: string, endpointPath: string): URL;
15
- /** Fetch the agent's client config; any failure yields the WebSocket default. */
16
+ /** Fetch the agent's client config; any failure yields the agent default. */
16
17
  export declare function fetchClientConfig(platformUrl: string, fetchFn?: typeof globalThis.fetch): Promise<ClientConfigResponse>;
@@ -1,12 +1,5 @@
1
+ /** @jsxImportSource react */
1
2
  import type { ReactNode } from "react";
2
- import type { AgentState } from "../types.ts";
3
- /**
4
- * Indicator dot color per state, on the light refresh palette. Shared with
5
- * the sync-transport chat shell's status eyebrow.
6
- *
7
- * @internal
8
- */
9
- export declare function stateColor(state: AgentState, primary: string): string;
10
3
  /**
11
4
  * The main chat interface for a voice agent session — the design-system
12
5
  * "voice agent console": a 760px column on the cream page with a header
@@ -1,3 +1,3 @@
1
1
  import "../context.js";
2
- import { n as stateColor, t as ChatView } from "../chat-view-gi6FccZq.js";
3
- export { ChatView, stateColor };
2
+ import { t as ChatView } from "../chat-view-DKFhxMAT.js";
3
+ export { ChatView };
@@ -0,0 +1,37 @@
1
+ import type { ReactNode } from "react";
2
+ import type { AgentState } from "../types.ts";
3
+ /**
4
+ * Indicator dot color per state, on the light refresh palette.
5
+ *
6
+ * @internal
7
+ */
8
+ export declare function stateColor(state: AgentState, primary: string): string;
9
+ /**
10
+ * The design-system "console" chrome shared by the chat shell and the
11
+ * workflow run surface: a 760px column on the cream page with a header
12
+ * (logo + live-status eyebrow), an optional error banner, the main content
13
+ * on a raised white card, and a footer row beneath it.
14
+ *
15
+ * Extracted so the two default surfaces stay visually identical by
16
+ * construction — they used to be hand-copied down to the same `boxShadow`
17
+ * literal, and drifted.
18
+ *
19
+ * @internal
20
+ */
21
+ export declare function ConsoleShell({ icon, title, state, pulsing, error, children, footer, className, }: {
22
+ /** Element rendered in place of the logo in the header. */
23
+ icon?: ReactNode | undefined;
24
+ /** Title string for the header. */
25
+ title?: string | undefined;
26
+ /** Live status shown in the header eyebrow. */
27
+ state: AgentState;
28
+ /** Whether the status dot pulses. */
29
+ pulsing: boolean;
30
+ /** Error banner text; `null`/`undefined` hides the banner. */
31
+ error?: string | null | undefined;
32
+ /** Card content. */
33
+ children: ReactNode;
34
+ /** Row rendered beneath the card (controls). */
35
+ footer: ReactNode;
36
+ className?: string | undefined;
37
+ }): ReactNode;
@@ -1,3 +1,3 @@
1
1
  import "../context.js";
2
- import { t as Controls } from "../controls-BbZcmnJf.js";
2
+ import { t as Controls } from "../controls-4OJoekj6.js";
3
3
  export { Controls };
@@ -37,7 +37,7 @@ export declare function ApiUrlChip({ className }: {
37
37
  * The UI and API URLs side by side. They answer the same question — "how do I
38
38
  * reach this agent?" — so they belong together and each needs its label to be
39
39
  * told apart. Rendered by the default shell in every session mode (S2S,
40
- * pipeline, text-only).
40
+ * pipeline).
41
41
  *
42
42
  * @public
43
43
  */
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Workflow run surface: push-to-talk or audio upload stages one instruction
3
+ * clip; **Go** runs it as a single sync turn and shows what was heard plus
4
+ * the tool calls that executed — no greeting, no assistant messages.
5
+ *
6
+ * @public
7
+ */
8
+ export declare function WorkflowView({ syncUrl, title, }: {
9
+ /** The workflow server's sync endpoint, e.g. `https://host/slug/sync`. */
10
+ syncUrl: string;
11
+ /** Workflow name shown in the header. */
12
+ title?: string | undefined;
13
+ }): import("react").JSX.Element;
@@ -91,7 +91,7 @@ function ApiUrlChip({ className }) {
91
91
  * The UI and API URLs side by side. They answer the same question — "how do I
92
92
  * reach this agent?" — so they belong together and each needs its label to be
93
93
  * told apart. Rendered by the default shell in every session mode (S2S,
94
- * pipeline, text-only).
94
+ * pipeline).
95
95
  *
96
96
  * @public
97
97
  */
@@ -0,0 +1 @@
1
+ import{n as e}from"./rolldown-runtime-BpQH8Ho1.js";import{d as t,t as n}from"./types-Bpg3ZIZK.js";var r=e({createVoiceIO:()=>l,decodeAudioToPcm16:()=>s}),i=1e3,a=65e3,o=250;async function s(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}function c(e,t,n){if(e!==t)throw Error(`Browser refused the ${n} sample rate: asked for ${t} Hz, got ${e} Hz`)}async function l(e){let{sttSampleRate:r,ttsSampleRate:s,captureWorkletSrc:l,playbackWorkletSrc:u,onMicData:d,onError:f,onPlaybackStats:p,onMicSilent:m}=e,h=new AudioContext({sampleRate:s,latencyHint:`playback`}),g=r===s,_=g?h:new AudioContext({sampleRate:r,latencyHint:`interactive`});async function v(){let e=g?[h]:[h,_];await Promise.all(e.map(e=>e.close().catch(e=>{console.warn(`AudioContext close failed:`,e)})))}let y=navigator.mediaDevices.getUserMedia({audio:{deviceId:{ideal:`default`},...n}}),b;try{[b]=await Promise.all([y,h.resume(),_.resume(),_.audioWorklet.addModule(l),h.audioWorklet.addModule(u)]),c(_.sampleRate,r,`capture`),c(h.sampleRate,s,`playback`)}catch(e){throw y.then(e=>{for(let t of e.getTracks())t.stop()}).catch(()=>{}),await v(),e}let x=_.createMediaStreamSource(b),S=new AudioWorkletNode(_,`capture-processor`,{channelCount:1,channelCountMode:`explicit`,processorOptions:{sampleRate:r,bufferSeconds:t}});x.connect(S),S.onprocessorerror=()=>{let e=Error(`Audio capture worklet crashed`);console.error(`[aai-ui]`,e.message),f?.(e)},S.port.postMessage({event:`start`});let C=null;S.port.onmessage=e=>{e.data.event===`chunk`?d(e.data.buffer):e.data.event===`silent`?m?.():e.data.event===`stopped`&&(C?.(),C=null)};let w=null,T=null,E=new AbortController;function D(){if(w)return w;let e=new AudioWorkletNode(h,`playback-processor`,{processorOptions:{sampleRate:h.sampleRate}});return e.connect(h.destination),e.port.onmessage=e=>{if(e.data.event===`stop`){let t=e.data.stats;if(t&&t.concealedSamples>0&&p?.(t),e.data.reason===`interrupt`)return;T?.(),T=null}},e.onprocessorerror=()=>{let e=Error(`Audio playback worklet crashed`);console.error(`[aai-ui]`,e.message),T?.(),T=null,f?.(e)},w=e,e}let O={enqueue(e){E.signal.aborted||e.byteLength!==0&&D().port.postMessage({event:`write`,buffer:new Uint8Array(e)},[e])},done(){return!w||h.state!==`running`?Promise.resolve():new Promise(e=>{T?.();let t=()=>{clearInterval(n),clearTimeout(r),T===t&&(T=null),e()},n=setInterval(()=>{h.state!==`running`&&t()},i),r=setTimeout(t,a);T=t,w?.port.postMessage({event:`done`})})},flush(){w&&(T?.(),T=null,w.port.postMessage({event:`interrupt`}))},async close(){if(!E.signal.aborted){E.abort(),await new Promise(e=>{let t=setTimeout(e,o);C=()=>{clearTimeout(t),e()},S.port.postMessage({event:`stop`})}),x.disconnect(),S.disconnect(),w&&w.disconnect();for(let e of b.getTracks())e.stop();await v()}},async[Symbol.asyncDispose](){await O.close()}};return O}export{s as n,r as t};
@@ -0,0 +1,90 @@
1
+ import{p as e}from"./types-Bpg3ZIZK.js";var t=`
2
+ class CaptureProcessor extends AudioWorkletProcessor {
3
+ constructor(options) {
4
+ super();
5
+ this.recording = false;
6
+ const opts = options.processorOptions || {};
7
+ // The context runs at the STT rate, so this is both the input and the
8
+ // output rate — there is nothing to convert.
9
+ this.rate = opts.sampleRate || sampleRate;
10
+ // Int16 accumulation buffer: flushed to the main thread as one transferred
11
+ // ArrayBuffer once ~bufferSeconds of samples are batched. Sized 2x the
12
+ // flush target so a whole render quantum always fits before flushing.
13
+ this.targetSamples = Math.max(1, Math.round(this.rate * (opts.bufferSeconds || 0.1)));
14
+ this.pending = new Int16Array(this.targetSamples * 2);
15
+ this.pendingLen = 0;
16
+ // Dead-mic probe: samples left to inspect before concluding the device
17
+ // delivers nothing but digital silence. Only consumed while recording, so
18
+ // the cost disappears after the window (or after the first real sample).
19
+ this.probeSamplesLeft = Math.round(
20
+ (this.rate * (opts.silenceProbeMs ?? ${e})) / 1000,
21
+ );
22
+ this.port.onmessage = (e) => {
23
+ if (e.data.event === 'start') this.recording = true;
24
+ else if (e.data.event === 'stop') {
25
+ // Final flush so the tail of speech isn't dropped on close, then ack
26
+ // so the host knows the tail chunk (if any) has been posted and it is
27
+ // safe to tear the context down.
28
+ this.flush();
29
+ this.recording = false;
30
+ this.port.postMessage({ event: 'stopped' });
31
+ }
32
+ };
33
+ }
34
+
35
+ // Convert Float32 -> Int16 and append to the pending batch. Writes through
36
+ // an Int16Array directly (assignment truncates like DataView.setInt16).
37
+ accumulate(samples) {
38
+ let buf = this.pending;
39
+ if (this.pendingLen + samples.length > buf.length) {
40
+ // Defensive: only reachable if a render quantum outproduces the 1x
41
+ // headroom above the flush target (never with 128-sample quanta).
42
+ const grown = new Int16Array((this.pendingLen + samples.length) * 2);
43
+ grown.set(buf.subarray(0, this.pendingLen));
44
+ this.pending = grown;
45
+ buf = grown;
46
+ }
47
+ for (let i = 0; i < samples.length; i++) {
48
+ const s = Math.max(-1, Math.min(1, samples[i]));
49
+ buf[this.pendingLen++] = s < 0 ? s * 0x8000 : s * 0x7fff;
50
+ }
51
+ }
52
+
53
+ // Post the batched samples as one transferred ArrayBuffer and reset.
54
+ flush() {
55
+ if (this.pendingLen === 0) return;
56
+ const buffer = this.pending.buffer.slice(0, this.pendingLen * 2);
57
+ this.pendingLen = 0;
58
+ this.port.postMessage({ event: 'chunk', buffer }, [buffer]);
59
+ }
60
+
61
+ // Watch the first window of input for any nonzero sample. One is enough to
62
+ // prove the device is live — a real mic in a quiet room still carries a
63
+ // noise floor, so all-zeros means muted, wrong input, or no input at all.
64
+ probeForSilence(channel) {
65
+ for (let i = 0; i < channel.length; i++) {
66
+ if (channel[i] !== 0) {
67
+ this.probeSamplesLeft = 0;
68
+ return;
69
+ }
70
+ }
71
+ this.probeSamplesLeft -= channel.length;
72
+ if (this.probeSamplesLeft <= 0) {
73
+ this.port.postMessage({ event: 'silent' });
74
+ }
75
+ }
76
+
77
+ process(inputs) {
78
+ const input = inputs[0];
79
+ if (!input || !input[0] || !this.recording) return true;
80
+
81
+ if (this.probeSamplesLeft > 0) this.probeForSilence(input[0]);
82
+
83
+ this.accumulate(input[0]);
84
+ if (this.pendingLen >= this.targetSamples) this.flush();
85
+ return true;
86
+ }
87
+ }
88
+
89
+ registerProcessor('capture-processor', CaptureProcessor);
90
+ `,n=new Blob([t],{type:`application/javascript`}),r=URL.createObjectURL(n);export{r as default};
@@ -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);--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{.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-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-\[40\%\]{max-width:40%}.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}.touch-none{touch-action:none}.\[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}@keyframes pulse{50%{opacity:.5}}