@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.
- package/dist/audio.d.ts +43 -5
- package/dist/audio.js +39 -20
- package/dist/chat-view-DKFhxMAT.js +129 -0
- package/dist/client-config.d.ts +7 -6
- package/dist/components/chat-view.d.ts +1 -8
- package/dist/components/chat-view.js +2 -2
- package/dist/components/console-shell.d.ts +37 -0
- package/dist/components/controls.js +1 -1
- package/dist/components/url-chips.d.ts +1 -1
- package/dist/components/workflow-view.d.ts +13 -0
- package/dist/{controls-BbZcmnJf.js → controls-4OJoekj6.js} +1 -1
- package/dist/default-client/assets/audio-Cgviqo9t.js +1 -0
- package/dist/default-client/assets/capture-processor-DLHxAIfT.js +90 -0
- package/dist/default-client/assets/index-BgbIWnfG.css +2 -0
- package/dist/default-client/assets/index-DbKKR3UE.js +62 -0
- package/dist/default-client/assets/playback-processor-bMTdFp-8.js +271 -0
- package/dist/default-client/assets/rolldown-runtime-BpQH8Ho1.js +1 -0
- package/dist/default-client/assets/types-Bpg3ZIZK.js +64 -0
- package/dist/default-client/index.html +5 -2
- package/dist/define-client-yoGybEYR.js +710 -0
- package/dist/define-client.d.ts +0 -9
- package/dist/define-client.js +1 -1
- package/dist/index.d.ts +3 -4
- package/dist/index.js +6 -5
- package/dist/{session-core-B64kau_v.js → session-core-BLhiQ18c.js} +6 -0
- package/dist/session-core.js +1 -1
- package/dist/sync-mic.d.ts +11 -43
- package/dist/types.d.ts +24 -1
- package/dist/types.js +32 -2
- package/dist/worklets/capture-processor.d.ts +1 -1
- package/dist/worklets/capture-processor.js +32 -49
- package/dist/worklets/playback-processor.d.ts +1 -1
- package/dist/worklets/playback-processor.js +125 -9
- package/package.json +2 -2
- package/dist/chat-view-gi6FccZq.js +0 -193
- package/dist/components/sync-chat-view.d.ts +0 -18
- package/dist/components/text-controls.d.ts +0 -14
- package/dist/default-client/assets/audio-DNDZgEZp.js +0 -1
- package/dist/default-client/assets/capture-processor-UlKEKyIW.js +0 -108
- package/dist/default-client/assets/index-BogmeUln.css +0 -2
- package/dist/default-client/assets/index-BunwIXSP.js +0 -124
- package/dist/default-client/assets/playback-processor-C5HVRVbu.js +0 -156
- package/dist/define-client-DdpijqAu.js +0 -906
- 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
|
|
33
|
-
*
|
|
34
|
-
*
|
|
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
|
-
*
|
|
52
|
-
*
|
|
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
|
-
*
|
|
47
|
-
*
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
80
|
-
console.warn("AudioContext close failed:", err);
|
|
81
|
-
});
|
|
98
|
+
await closeContexts();
|
|
82
99
|
throw err;
|
|
83
100
|
}
|
|
84
|
-
const mic =
|
|
85
|
-
const capNode = new AudioWorkletNode(
|
|
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
|
-
|
|
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:
|
|
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
|
|
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 };
|
package/dist/client-config.d.ts
CHANGED
|
@@ -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
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* error, 404 from an older server,
|
|
9
|
-
*
|
|
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
|
|
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 {
|
|
3
|
-
export { ChatView
|
|
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;
|
|
@@ -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
|
|
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
|
|
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}}
|