@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.
- package/dist/_module-url-C_4gRVL0.js +15 -0
- package/dist/audio.d.ts +85 -11
- package/dist/audio.js +112 -83
- package/dist/chat-view-C1oJxsWz.js +129 -0
- package/dist/client-config.d.ts +6 -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 -4
- package/dist/context.d.ts +1 -1
- package/dist/context.js +1 -4
- package/dist/{controls-BbZcmnJf.js → controls-DV368uhb.js} +2 -5
- package/dist/default-client/assets/_module-url-BX0RuRU2.js +1 -0
- package/dist/default-client/assets/audio-BGHiiDY_.js +1 -0
- package/dist/default-client/assets/capture-processor-BLPsqnGl.js +90 -0
- package/dist/default-client/assets/{index-BunwIXSP.js → index-DXf7-M0Q.js} +156 -36
- package/dist/default-client/assets/index-Dv-Q5VRL.css +2 -0
- package/dist/default-client/assets/playback-processor-B_T_1KQP.js +278 -0
- package/dist/default-client/index.html +2 -2
- package/dist/define-client-Cyf1jqAl.js +150 -0
- package/dist/define-client.d.ts +0 -9
- package/dist/define-client.js +1 -1
- package/dist/index.d.ts +2 -5
- package/dist/index.js +7 -5
- package/dist/{session-core-B64kau_v.js → session-core-BM3WHeoY.js} +36 -128
- package/dist/session-core-messages.d.ts +0 -4
- package/dist/session-core-types.d.ts +1 -27
- package/dist/session-core.js +1 -1
- package/dist/types.d.ts +24 -1
- package/dist/types.js +32 -2
- package/dist/worklets/_module-url.d.ts +10 -0
- package/dist/worklets/capture-processor.d.ts +3 -3
- package/dist/worklets/capture-processor.js +35 -52
- package/dist/worklets/playback-processor.d.ts +3 -3
- package/dist/worklets/playback-processor.js +149 -26
- 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/playback-processor-C5HVRVbu.js +0 -156
- package/dist/define-client-DdpijqAu.js +0 -906
- package/dist/session-core-upload.d.ts +0 -16
- package/dist/sync-mic.d.ts +0 -93
- package/dist/sync-session.d.ts +0 -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
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
// the
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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.
|
|
234
|
+
if (avail >= this.fillTarget || this.isDone) {
|
|
128
235
|
this.playing = true;
|
|
129
236
|
} else {
|
|
130
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
161
|
-
const src = URL.createObjectURL(script);
|
|
284
|
+
var playback_processor_default = workletModuleUrl(PlaybackProcessorWorklet);
|
|
162
285
|
//#endregion
|
|
163
|
-
export {
|
|
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": "
|
|
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": "
|
|
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}}
|