@alexkroman1/aai-ui 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/dist/_components/app.d.ts +5 -0
- package/dist/_components/app.js +23 -0
- package/dist/_components/button.d.ts +11 -0
- package/dist/_components/button.js +22 -0
- package/dist/_components/chat-view.d.ts +5 -0
- package/dist/_components/chat-view.js +37 -0
- package/dist/_components/controls.d.ts +4 -0
- package/dist/_components/controls.js +23 -0
- package/dist/_components/error-banner.d.ts +7 -0
- package/dist/_components/error-banner.js +13 -0
- package/dist/_components/message-bubble.d.ts +7 -0
- package/dist/_components/message-bubble.js +19 -0
- package/dist/_components/message-list.d.ts +4 -0
- package/dist/_components/message-list.js +59 -0
- package/dist/_components/sidebar-layout.d.ts +21 -0
- package/dist/_components/sidebar-layout.js +36 -0
- package/dist/_components/start-screen.d.ts +26 -0
- package/dist/_components/start-screen.js +51 -0
- package/dist/_components/state-indicator.d.ts +7 -0
- package/dist/_components/state-indicator.js +15 -0
- package/dist/_components/thinking-indicator.d.ts +5 -0
- package/dist/_components/thinking-indicator.js +22 -0
- package/dist/_components/tool-call-block.d.ts +7 -0
- package/dist/_components/tool-call-block.js +96 -0
- package/dist/_components/tool-icons.d.ts +17 -0
- package/dist/_components/tool-icons.js +128 -0
- package/dist/_components/transcript.d.ts +7 -0
- package/dist/_components/transcript.js +17 -0
- package/dist/_jsdom-setup.d.ts +0 -0
- package/dist/audio.d.ts +45 -0
- package/dist/audio.js +111 -0
- package/dist/components.d.ts +31 -0
- package/dist/components.js +17 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.js +19 -0
- package/dist/mount-context.d.ts +33 -0
- package/dist/mount-context.js +15 -0
- package/dist/mount.d.ts +51 -0
- package/dist/mount.js +70 -0
- package/dist/session.d.ts +100 -0
- package/dist/session.js +359 -0
- package/dist/signals.d.ts +89 -0
- package/dist/signals.js +138 -0
- package/dist/tsdown.config.d.ts +2 -0
- package/dist/types.d.ts +75 -0
- package/dist/types.js +5 -0
- package/dist/worklets/capture-processor.d.ts +2 -0
- package/dist/worklets/capture-processor.js +57 -0
- package/dist/worklets/playback-processor.d.ts +2 -0
- package/dist/worklets/playback-processor.js +105 -0
- package/package.json +56 -0
- package/styles.css +74 -0
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { jsx, jsxs } from "preact/jsx-runtime";
|
|
2
|
+
//#region _components/tool-icons.tsx
|
|
3
|
+
/** Magnifying glass icon for web_search. */
|
|
4
|
+
function SearchIcon(props) {
|
|
5
|
+
return /* @__PURE__ */ jsxs("svg", {
|
|
6
|
+
viewBox: "0 0 16 16",
|
|
7
|
+
fill: "none",
|
|
8
|
+
stroke: "currentColor",
|
|
9
|
+
"stroke-width": "1.5",
|
|
10
|
+
class: props.class,
|
|
11
|
+
children: [/* @__PURE__ */ jsx("circle", {
|
|
12
|
+
cx: "7",
|
|
13
|
+
cy: "7",
|
|
14
|
+
r: "4.5"
|
|
15
|
+
}), /* @__PURE__ */ jsx("path", {
|
|
16
|
+
d: "M10.5 10.5L14 14",
|
|
17
|
+
"stroke-linecap": "round"
|
|
18
|
+
})]
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
/** External link icon for visit_webpage. */
|
|
22
|
+
function ExternalLinkIcon(props) {
|
|
23
|
+
return /* @__PURE__ */ jsxs("svg", {
|
|
24
|
+
viewBox: "0 0 16 16",
|
|
25
|
+
fill: "none",
|
|
26
|
+
stroke: "currentColor",
|
|
27
|
+
"stroke-width": "1.5",
|
|
28
|
+
class: props.class,
|
|
29
|
+
children: [
|
|
30
|
+
/* @__PURE__ */ jsx("path", {
|
|
31
|
+
d: "M6 2H3a1 1 0 00-1 1v10a1 1 0 001 1h10a1 1 0 001-1v-3",
|
|
32
|
+
"stroke-linecap": "round"
|
|
33
|
+
}),
|
|
34
|
+
/* @__PURE__ */ jsx("path", {
|
|
35
|
+
d: "M9 2h5v5",
|
|
36
|
+
"stroke-linecap": "round",
|
|
37
|
+
"stroke-linejoin": "round"
|
|
38
|
+
}),
|
|
39
|
+
/* @__PURE__ */ jsx("path", {
|
|
40
|
+
d: "M14 2L7 9",
|
|
41
|
+
"stroke-linecap": "round"
|
|
42
|
+
})
|
|
43
|
+
]
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
/** Terminal icon for run_code. */
|
|
47
|
+
function TerminalIcon(props) {
|
|
48
|
+
return /* @__PURE__ */ jsxs("svg", {
|
|
49
|
+
viewBox: "0 0 16 16",
|
|
50
|
+
fill: "none",
|
|
51
|
+
stroke: "currentColor",
|
|
52
|
+
"stroke-width": "1.5",
|
|
53
|
+
class: props.class,
|
|
54
|
+
children: [
|
|
55
|
+
/* @__PURE__ */ jsx("rect", {
|
|
56
|
+
x: "1",
|
|
57
|
+
y: "2",
|
|
58
|
+
width: "14",
|
|
59
|
+
height: "12",
|
|
60
|
+
rx: "1.5"
|
|
61
|
+
}),
|
|
62
|
+
/* @__PURE__ */ jsx("path", {
|
|
63
|
+
d: "M4 6l3 2.5L4 11",
|
|
64
|
+
"stroke-linecap": "round",
|
|
65
|
+
"stroke-linejoin": "round"
|
|
66
|
+
}),
|
|
67
|
+
/* @__PURE__ */ jsx("path", {
|
|
68
|
+
d: "M9 11h3",
|
|
69
|
+
"stroke-linecap": "round"
|
|
70
|
+
})
|
|
71
|
+
]
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
/** Download icon for fetch_json. */
|
|
75
|
+
function DownloadIcon(props) {
|
|
76
|
+
return /* @__PURE__ */ jsxs("svg", {
|
|
77
|
+
viewBox: "0 0 16 16",
|
|
78
|
+
fill: "none",
|
|
79
|
+
stroke: "currentColor",
|
|
80
|
+
"stroke-width": "1.5",
|
|
81
|
+
class: props.class,
|
|
82
|
+
children: [
|
|
83
|
+
/* @__PURE__ */ jsx("path", {
|
|
84
|
+
d: "M8 2v9",
|
|
85
|
+
"stroke-linecap": "round"
|
|
86
|
+
}),
|
|
87
|
+
/* @__PURE__ */ jsx("path", {
|
|
88
|
+
d: "M4.5 8L8 11.5 11.5 8",
|
|
89
|
+
"stroke-linecap": "round",
|
|
90
|
+
"stroke-linejoin": "round"
|
|
91
|
+
}),
|
|
92
|
+
/* @__PURE__ */ jsx("path", {
|
|
93
|
+
d: "M2 13h12",
|
|
94
|
+
"stroke-linecap": "round"
|
|
95
|
+
})
|
|
96
|
+
]
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
/** Chat bubble icon for user_input. */
|
|
100
|
+
function ChatBubbleIcon(props) {
|
|
101
|
+
return /* @__PURE__ */ jsx("svg", {
|
|
102
|
+
viewBox: "0 0 16 16",
|
|
103
|
+
fill: "none",
|
|
104
|
+
stroke: "currentColor",
|
|
105
|
+
"stroke-width": "1.5",
|
|
106
|
+
class: props.class,
|
|
107
|
+
children: /* @__PURE__ */ jsx("path", {
|
|
108
|
+
d: "M2 3a1 1 0 011-1h10a1 1 0 011 1v7a1 1 0 01-1 1H5l-3 3V3z",
|
|
109
|
+
"stroke-linejoin": "round"
|
|
110
|
+
})
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
/** Bolt/lightning icon for default/unknown tools. */
|
|
114
|
+
function BoltIcon(props) {
|
|
115
|
+
return /* @__PURE__ */ jsx("svg", {
|
|
116
|
+
viewBox: "0 0 16 16",
|
|
117
|
+
fill: "none",
|
|
118
|
+
stroke: "currentColor",
|
|
119
|
+
"stroke-width": "1.5",
|
|
120
|
+
class: props.class,
|
|
121
|
+
children: /* @__PURE__ */ jsx("path", {
|
|
122
|
+
d: "M9 1L3 9h5l-1 6 6-8H8l1-6z",
|
|
123
|
+
"stroke-linejoin": "round"
|
|
124
|
+
})
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
//#endregion
|
|
128
|
+
export { BoltIcon, ChatBubbleIcon, DownloadIcon, ExternalLinkIcon, SearchIcon, TerminalIcon };
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type * as preact from "preact";
|
|
2
|
+
import type { Reactive } from "../types.ts";
|
|
3
|
+
/** @public */
|
|
4
|
+
export declare function Transcript({ userUtterance, className, }: {
|
|
5
|
+
userUtterance: Reactive<string | null>;
|
|
6
|
+
className?: string;
|
|
7
|
+
}): preact.JSX.Element | null;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { ThinkingIndicator } from "./thinking-indicator.js";
|
|
2
|
+
import clsx from "clsx";
|
|
3
|
+
import { jsx } from "preact/jsx-runtime";
|
|
4
|
+
//#region _components/transcript.tsx
|
|
5
|
+
/** @public */
|
|
6
|
+
function Transcript({ userUtterance, className }) {
|
|
7
|
+
if (userUtterance.value === null) return null;
|
|
8
|
+
return /* @__PURE__ */ jsx("div", {
|
|
9
|
+
class: clsx("flex flex-col items-end w-full", className),
|
|
10
|
+
children: /* @__PURE__ */ jsx("div", {
|
|
11
|
+
class: "max-w-[min(82%,64ch)] whitespace-pre-wrap wrap-break-word text-sm leading-[150%] text-aai-text-muted bg-aai-surface-faint border border-aai-border px-3 py-2 rounded-aai ml-auto",
|
|
12
|
+
children: userUtterance.value || /* @__PURE__ */ jsx(ThinkingIndicator, {})
|
|
13
|
+
})
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
//#endregion
|
|
17
|
+
export { Transcript };
|
|
File without changes
|
package/dist/audio.d.ts
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/** Configuration for creating a {@link VoiceIO} instance. */
|
|
2
|
+
export type VoiceIOOptions = {
|
|
3
|
+
/** Sample rate in Hz expected by the STT engine (e.g. 16000). */
|
|
4
|
+
sttSampleRate: number;
|
|
5
|
+
/** Sample rate in Hz used by the TTS engine (e.g. 22050). */
|
|
6
|
+
ttsSampleRate: number;
|
|
7
|
+
/** Source URL or data URI for the capture AudioWorklet processor. */
|
|
8
|
+
captureWorkletSrc: string;
|
|
9
|
+
/** Source URL or data URI for the playback AudioWorklet processor. */
|
|
10
|
+
playbackWorkletSrc: string;
|
|
11
|
+
/** Callback invoked with buffered PCM16 microphone data to send to the server. */
|
|
12
|
+
onMicData: (pcm16: ArrayBuffer) => void;
|
|
13
|
+
/** Callback invoked with the current playback position in samples. */
|
|
14
|
+
onPlaybackProgress?: (samplesPlayed: number) => void;
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* Audio I/O interface for voice capture and playback.
|
|
18
|
+
*
|
|
19
|
+
* Manages microphone capture via an AudioWorklet, resampling to the STT
|
|
20
|
+
* sample rate, and TTS audio playback through a second AudioWorklet. Implements
|
|
21
|
+
* {@link AsyncDisposable} for resource cleanup.
|
|
22
|
+
*/
|
|
23
|
+
export type VoiceIO = AsyncDisposable & {
|
|
24
|
+
/** Enqueue a PCM16 audio buffer for playback through the TTS pipeline. */
|
|
25
|
+
enqueue(pcm16Buffer: ArrayBuffer): void;
|
|
26
|
+
/** Signal that all TTS audio for the current turn has been enqueued.
|
|
27
|
+
* Resolves when the worklet has finished playing all buffered audio. */
|
|
28
|
+
done(): Promise<void>;
|
|
29
|
+
/** Immediately stop playback and discard any buffered TTS audio. */
|
|
30
|
+
flush(): void;
|
|
31
|
+
/** Release all audio resources (microphone, AudioContext, worklets). */
|
|
32
|
+
close(): Promise<void>;
|
|
33
|
+
};
|
|
34
|
+
/**
|
|
35
|
+
* Create a {@link VoiceIO} instance that captures microphone audio and
|
|
36
|
+
* plays back TTS audio using the Web Audio API.
|
|
37
|
+
*
|
|
38
|
+
* The AudioContext runs at the TTS sample rate for playback fidelity.
|
|
39
|
+
* Captured audio is resampled to the STT rate when the rates differ.
|
|
40
|
+
*
|
|
41
|
+
* @param opts - Voice I/O configuration options.
|
|
42
|
+
* @returns A promise that resolves to a {@link VoiceIO} handle.
|
|
43
|
+
* @throws If microphone access is denied or AudioWorklet registration fails.
|
|
44
|
+
*/
|
|
45
|
+
export declare function createVoiceIO(opts: VoiceIOOptions): Promise<VoiceIO>;
|
package/dist/audio.js
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { MIC_BUFFER_SECONDS } from "./types.js";
|
|
2
|
+
//#region audio.ts
|
|
3
|
+
/**
|
|
4
|
+
* Create a {@link VoiceIO} instance that captures microphone audio and
|
|
5
|
+
* plays back TTS audio using the Web Audio API.
|
|
6
|
+
*
|
|
7
|
+
* The AudioContext runs at the TTS sample rate for playback fidelity.
|
|
8
|
+
* Captured audio is resampled to the STT rate when the rates differ.
|
|
9
|
+
*
|
|
10
|
+
* @param opts - Voice I/O configuration options.
|
|
11
|
+
* @returns A promise that resolves to a {@link VoiceIO} handle.
|
|
12
|
+
* @throws If microphone access is denied or AudioWorklet registration fails.
|
|
13
|
+
*/
|
|
14
|
+
async function createVoiceIO(opts) {
|
|
15
|
+
const { sttSampleRate, ttsSampleRate, captureWorkletSrc, playbackWorkletSrc, onMicData } = opts;
|
|
16
|
+
const contextRate = ttsSampleRate;
|
|
17
|
+
const ctx = new AudioContext({
|
|
18
|
+
sampleRate: contextRate,
|
|
19
|
+
latencyHint: "playback"
|
|
20
|
+
});
|
|
21
|
+
await ctx.resume();
|
|
22
|
+
const stream = await navigator.mediaDevices.getUserMedia({ audio: {
|
|
23
|
+
sampleRate: contextRate,
|
|
24
|
+
echoCancellation: true,
|
|
25
|
+
noiseSuppression: true,
|
|
26
|
+
autoGainControl: true
|
|
27
|
+
} });
|
|
28
|
+
try {
|
|
29
|
+
await Promise.all([ctx.audioWorklet.addModule(captureWorkletSrc), ctx.audioWorklet.addModule(playbackWorkletSrc)]);
|
|
30
|
+
} catch (err) {
|
|
31
|
+
for (const t of stream.getTracks()) t.stop();
|
|
32
|
+
await ctx.close().catch(() => {});
|
|
33
|
+
throw err;
|
|
34
|
+
}
|
|
35
|
+
const mic = ctx.createMediaStreamSource(stream);
|
|
36
|
+
const capNode = new AudioWorkletNode(ctx, "capture-processor", {
|
|
37
|
+
channelCount: 1,
|
|
38
|
+
channelCountMode: "explicit",
|
|
39
|
+
processorOptions: {
|
|
40
|
+
contextRate,
|
|
41
|
+
sttSampleRate
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
mic.connect(capNode);
|
|
45
|
+
const chunkSizeBytes = Math.floor(sttSampleRate * MIC_BUFFER_SECONDS) * 2;
|
|
46
|
+
const capBuf = new Uint8Array(chunkSizeBytes * 2);
|
|
47
|
+
let capOffset = 0;
|
|
48
|
+
capNode.port.postMessage({ event: "start" });
|
|
49
|
+
capNode.port.onmessage = (e) => {
|
|
50
|
+
if (e.data.event !== "chunk") return;
|
|
51
|
+
const chunk = new Uint8Array(e.data.buffer);
|
|
52
|
+
capBuf.set(chunk, capOffset);
|
|
53
|
+
capOffset += chunk.byteLength;
|
|
54
|
+
if (capOffset >= chunkSizeBytes) {
|
|
55
|
+
onMicData(capBuf.slice(0, capOffset).buffer);
|
|
56
|
+
capOffset = 0;
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
let playNode = null;
|
|
60
|
+
let onPlaybackStop = null;
|
|
61
|
+
const lifecycle = new AbortController();
|
|
62
|
+
const { onPlaybackProgress } = opts;
|
|
63
|
+
function ensurePlayNode() {
|
|
64
|
+
if (playNode) return playNode;
|
|
65
|
+
const node = new AudioWorkletNode(ctx, "playback-processor", { processorOptions: { sampleRate: contextRate } });
|
|
66
|
+
node.connect(ctx.destination);
|
|
67
|
+
node.port.onmessage = (e) => {
|
|
68
|
+
if (e.data.event === "stop") {
|
|
69
|
+
node.disconnect();
|
|
70
|
+
if (playNode === node) playNode = null;
|
|
71
|
+
onPlaybackStop?.();
|
|
72
|
+
onPlaybackStop = null;
|
|
73
|
+
} else if (e.data.event === "progress") onPlaybackProgress?.(e.data.readPos);
|
|
74
|
+
};
|
|
75
|
+
playNode = node;
|
|
76
|
+
return node;
|
|
77
|
+
}
|
|
78
|
+
const io = {
|
|
79
|
+
enqueue(pcm16Buffer) {
|
|
80
|
+
if (lifecycle.signal.aborted) return;
|
|
81
|
+
if (pcm16Buffer.byteLength === 0) return;
|
|
82
|
+
ensurePlayNode().port.postMessage({
|
|
83
|
+
event: "write",
|
|
84
|
+
buffer: new Uint8Array(pcm16Buffer)
|
|
85
|
+
}, [pcm16Buffer]);
|
|
86
|
+
},
|
|
87
|
+
done() {
|
|
88
|
+
if (!playNode) return Promise.resolve();
|
|
89
|
+
return new Promise((resolve) => {
|
|
90
|
+
onPlaybackStop = resolve;
|
|
91
|
+
playNode?.port.postMessage({ event: "done" });
|
|
92
|
+
});
|
|
93
|
+
},
|
|
94
|
+
flush() {
|
|
95
|
+
if (playNode) playNode.port.postMessage({ event: "interrupt" });
|
|
96
|
+
},
|
|
97
|
+
async close() {
|
|
98
|
+
if (lifecycle.signal.aborted) return;
|
|
99
|
+
lifecycle.abort();
|
|
100
|
+
capNode.port.postMessage({ event: "stop" });
|
|
101
|
+
for (const t of stream.getTracks()) t.stop();
|
|
102
|
+
await ctx.close().catch(() => {});
|
|
103
|
+
},
|
|
104
|
+
async [Symbol.asyncDispose]() {
|
|
105
|
+
await io.close();
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
return io;
|
|
109
|
+
}
|
|
110
|
+
//#endregion
|
|
111
|
+
export { createVoiceIO };
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Preact UI components for AAI voice agents.
|
|
3
|
+
*
|
|
4
|
+
* Provides ready-made components, session context, and mount helpers.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* ```tsx
|
|
8
|
+
* import { App, mount } from "@aai/ui/components";
|
|
9
|
+
*
|
|
10
|
+
* mount(App, { target: "#app" });
|
|
11
|
+
* ```
|
|
12
|
+
*/
|
|
13
|
+
export { App } from "./_components/app.tsx";
|
|
14
|
+
export { Button } from "./_components/button.tsx";
|
|
15
|
+
export { ChatView } from "./_components/chat-view.tsx";
|
|
16
|
+
export { Controls } from "./_components/controls.tsx";
|
|
17
|
+
export { ErrorBanner } from "./_components/error-banner.tsx";
|
|
18
|
+
export { MessageBubble } from "./_components/message-bubble.tsx";
|
|
19
|
+
export { MessageList } from "./_components/message-list.tsx";
|
|
20
|
+
export { SidebarLayout } from "./_components/sidebar-layout.tsx";
|
|
21
|
+
export { StartScreen } from "./_components/start-screen.tsx";
|
|
22
|
+
export { StateIndicator } from "./_components/state-indicator.tsx";
|
|
23
|
+
export { ThinkingIndicator } from "./_components/thinking-indicator.tsx";
|
|
24
|
+
export { ToolCallBlock } from "./_components/tool-call-block.tsx";
|
|
25
|
+
export { Transcript } from "./_components/transcript.tsx";
|
|
26
|
+
export type { MountHandle, MountOptions } from "./mount.tsx";
|
|
27
|
+
export { mount } from "./mount.tsx";
|
|
28
|
+
export type { MountConfig, MountTheme } from "./mount-context.ts";
|
|
29
|
+
export { useMountConfig } from "./mount-context.ts";
|
|
30
|
+
export type { SessionSignals } from "./signals.ts";
|
|
31
|
+
export { createSessionControls, SessionProvider, useAutoScroll, useSession, useToolResult, } from "./signals.ts";
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { useMountConfig } from "./mount-context.js";
|
|
2
|
+
import { SessionProvider, createSessionControls, useAutoScroll, useSession, useToolResult } from "./signals.js";
|
|
3
|
+
import { Button } from "./_components/button.js";
|
|
4
|
+
import { Controls } from "./_components/controls.js";
|
|
5
|
+
import { ErrorBanner } from "./_components/error-banner.js";
|
|
6
|
+
import { MessageBubble } from "./_components/message-bubble.js";
|
|
7
|
+
import { ThinkingIndicator } from "./_components/thinking-indicator.js";
|
|
8
|
+
import { ToolCallBlock } from "./_components/tool-call-block.js";
|
|
9
|
+
import { Transcript } from "./_components/transcript.js";
|
|
10
|
+
import { MessageList } from "./_components/message-list.js";
|
|
11
|
+
import { StateIndicator } from "./_components/state-indicator.js";
|
|
12
|
+
import { ChatView } from "./_components/chat-view.js";
|
|
13
|
+
import { StartScreen } from "./_components/start-screen.js";
|
|
14
|
+
import { App } from "./_components/app.js";
|
|
15
|
+
import { SidebarLayout } from "./_components/sidebar-layout.js";
|
|
16
|
+
import { mount } from "./mount.js";
|
|
17
|
+
export { App, Button, ChatView, Controls, ErrorBanner, MessageBubble, MessageList, SessionProvider, SidebarLayout, StartScreen, StateIndicator, ThinkingIndicator, ToolCallBlock, Transcript, createSessionControls, mount, useAutoScroll, useMountConfig, useSession, useToolResult };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser client library for AAI voice agents.
|
|
3
|
+
*
|
|
4
|
+
* Provides WebSocket session management, audio capture/playback,
|
|
5
|
+
* and Preact UI components. For narrower imports, use the sub-path exports:
|
|
6
|
+
*
|
|
7
|
+
* - `@aai/ui/session` — WebSocket session only (no Preact dependency)
|
|
8
|
+
* - `@aai/ui/components` — Preact components, mount helpers, and signals
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```tsx
|
|
12
|
+
* import { App, mount } from "@aai/ui";
|
|
13
|
+
*
|
|
14
|
+
* mount(App, { target: "#app" });
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
export * from "./components.ts";
|
|
18
|
+
export type { VoiceSession } from "./session.ts";
|
|
19
|
+
export { createVoiceSession } from "./session.ts";
|
|
20
|
+
export type { AgentState, Message, Reactive, SessionError, SessionErrorCode, SessionOptions, ToolCallInfo, } from "./types.ts";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { useMountConfig } from "./mount-context.js";
|
|
2
|
+
import { SessionProvider, createSessionControls, useAutoScroll, useSession, useToolResult } from "./signals.js";
|
|
3
|
+
import { Button } from "./_components/button.js";
|
|
4
|
+
import { Controls } from "./_components/controls.js";
|
|
5
|
+
import { ErrorBanner } from "./_components/error-banner.js";
|
|
6
|
+
import { MessageBubble } from "./_components/message-bubble.js";
|
|
7
|
+
import { ThinkingIndicator } from "./_components/thinking-indicator.js";
|
|
8
|
+
import { ToolCallBlock } from "./_components/tool-call-block.js";
|
|
9
|
+
import { Transcript } from "./_components/transcript.js";
|
|
10
|
+
import { MessageList } from "./_components/message-list.js";
|
|
11
|
+
import { StateIndicator } from "./_components/state-indicator.js";
|
|
12
|
+
import { ChatView } from "./_components/chat-view.js";
|
|
13
|
+
import { StartScreen } from "./_components/start-screen.js";
|
|
14
|
+
import { App } from "./_components/app.js";
|
|
15
|
+
import { SidebarLayout } from "./_components/sidebar-layout.js";
|
|
16
|
+
import { createVoiceSession } from "./session.js";
|
|
17
|
+
import { mount } from "./mount.js";
|
|
18
|
+
import "./components.js";
|
|
19
|
+
export { App, Button, ChatView, Controls, ErrorBanner, MessageBubble, MessageList, SessionProvider, SidebarLayout, StartScreen, StateIndicator, ThinkingIndicator, ToolCallBlock, Transcript, createSessionControls, createVoiceSession, mount, useAutoScroll, useMountConfig, useSession, useToolResult };
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Theme overrides for the default UI. Applied as CSS custom properties.
|
|
3
|
+
*
|
|
4
|
+
* @public
|
|
5
|
+
*/
|
|
6
|
+
export type MountTheme = {
|
|
7
|
+
/** Background color. Default: `#101010`. */
|
|
8
|
+
bg?: string;
|
|
9
|
+
/** Primary accent color. Default: `#fab283`. */
|
|
10
|
+
primary?: string;
|
|
11
|
+
/** Main text color. */
|
|
12
|
+
text?: string;
|
|
13
|
+
/** Surface/card color. */
|
|
14
|
+
surface?: string;
|
|
15
|
+
/** Border color. */
|
|
16
|
+
border?: string;
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* Resolved mount-level configuration available to default UI components.
|
|
20
|
+
*
|
|
21
|
+
* @public
|
|
22
|
+
*/
|
|
23
|
+
export type MountConfig = {
|
|
24
|
+
title?: string | undefined;
|
|
25
|
+
theme?: MountTheme | undefined;
|
|
26
|
+
};
|
|
27
|
+
export declare const MountConfigProvider: import("preact").Provider<MountConfig>;
|
|
28
|
+
/**
|
|
29
|
+
* Read mount config (title, theme) from the nearest provider.
|
|
30
|
+
*
|
|
31
|
+
* @public
|
|
32
|
+
*/
|
|
33
|
+
export declare function useMountConfig(): MountConfig;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { createContext } from "preact";
|
|
2
|
+
import { useContext } from "preact/hooks";
|
|
3
|
+
//#region mount-context.ts
|
|
4
|
+
const Ctx = createContext({});
|
|
5
|
+
const MountConfigProvider = Ctx.Provider;
|
|
6
|
+
/**
|
|
7
|
+
* Read mount config (title, theme) from the nearest provider.
|
|
8
|
+
*
|
|
9
|
+
* @public
|
|
10
|
+
*/
|
|
11
|
+
function useMountConfig() {
|
|
12
|
+
return useContext(Ctx);
|
|
13
|
+
}
|
|
14
|
+
//#endregion
|
|
15
|
+
export { MountConfigProvider, useMountConfig };
|
package/dist/mount.d.ts
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { ComponentType } from "preact";
|
|
2
|
+
import { type MountTheme } from "./mount-context.ts";
|
|
3
|
+
import { type VoiceSession } from "./session.ts";
|
|
4
|
+
import { type SessionSignals } from "./signals.ts";
|
|
5
|
+
/**
|
|
6
|
+
* Options for {@link mount}.
|
|
7
|
+
*
|
|
8
|
+
* @public
|
|
9
|
+
*/
|
|
10
|
+
export type MountOptions = {
|
|
11
|
+
/** CSS selector or DOM element to render into. Defaults to `"#app"`. */
|
|
12
|
+
target?: string | HTMLElement;
|
|
13
|
+
/** Base URL of the AAI platform server. Derived from `location.href` by default. */
|
|
14
|
+
platformUrl?: string;
|
|
15
|
+
/** Agent title shown in the header and start screen. */
|
|
16
|
+
title?: string;
|
|
17
|
+
/** Theme color overrides. */
|
|
18
|
+
theme?: MountTheme;
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* Handle returned by {@link mount} for cleanup.
|
|
22
|
+
*
|
|
23
|
+
* Implements `Disposable` so it can be used with `using`.
|
|
24
|
+
*
|
|
25
|
+
* @public
|
|
26
|
+
*/
|
|
27
|
+
export type MountHandle = {
|
|
28
|
+
/** The underlying voice session. */
|
|
29
|
+
session: VoiceSession;
|
|
30
|
+
/** Reactive session controls for the mounted UI. */
|
|
31
|
+
signals: SessionSignals;
|
|
32
|
+
/** Unmount the UI, remove injected styles, and disconnect the session. */
|
|
33
|
+
dispose(): void;
|
|
34
|
+
/** Alias for `dispose` for use with `using`. */
|
|
35
|
+
[Symbol.dispose](): void;
|
|
36
|
+
};
|
|
37
|
+
/**
|
|
38
|
+
* Mount a Preact component with voice session wiring.
|
|
39
|
+
*
|
|
40
|
+
* Creates a {@link VoiceSession}, wraps it in
|
|
41
|
+
* {@link SessionSignals}, and renders the component
|
|
42
|
+
* inside a {@link SessionProvider}.
|
|
43
|
+
*
|
|
44
|
+
* @param Component - The Preact component to render.
|
|
45
|
+
* @param options - Mount options (target element, platform URL).
|
|
46
|
+
* @returns A {@link MountHandle} for cleanup.
|
|
47
|
+
* @throws If the target element is not found in the DOM.
|
|
48
|
+
*
|
|
49
|
+
* @public
|
|
50
|
+
*/
|
|
51
|
+
export declare function mount(Component: ComponentType<any>, options?: MountOptions): MountHandle;
|
package/dist/mount.js
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { MountConfigProvider } from "./mount-context.js";
|
|
2
|
+
import { SessionProvider, createSessionControls } from "./signals.js";
|
|
3
|
+
import { createVoiceSession } from "./session.js";
|
|
4
|
+
import { render } from "preact";
|
|
5
|
+
import { batch, signal } from "@preact/signals";
|
|
6
|
+
import { jsx } from "preact/jsx-runtime";
|
|
7
|
+
//#region mount.tsx
|
|
8
|
+
function resolveContainer(target = "#app") {
|
|
9
|
+
const el = typeof target === "string" ? document.querySelector(target) : target;
|
|
10
|
+
if (!el) throw new Error(`Element not found: ${target}`);
|
|
11
|
+
return el;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Mount a Preact component with voice session wiring.
|
|
15
|
+
*
|
|
16
|
+
* Creates a {@link VoiceSession}, wraps it in
|
|
17
|
+
* {@link SessionSignals}, and renders the component
|
|
18
|
+
* inside a {@link SessionProvider}.
|
|
19
|
+
*
|
|
20
|
+
* @param Component - The Preact component to render.
|
|
21
|
+
* @param options - Mount options (target element, platform URL).
|
|
22
|
+
* @returns A {@link MountHandle} for cleanup.
|
|
23
|
+
* @throws If the target element is not found in the DOM.
|
|
24
|
+
*
|
|
25
|
+
* @public
|
|
26
|
+
*/
|
|
27
|
+
function mount(Component, options) {
|
|
28
|
+
const container = resolveContainer(options?.target);
|
|
29
|
+
const session = createVoiceSession({
|
|
30
|
+
platformUrl: options?.platformUrl ?? globalThis.location.origin + globalThis.location.pathname,
|
|
31
|
+
signal,
|
|
32
|
+
batch
|
|
33
|
+
});
|
|
34
|
+
const signals = createSessionControls(session);
|
|
35
|
+
const mountConfig = {
|
|
36
|
+
title: options?.title,
|
|
37
|
+
theme: options?.theme
|
|
38
|
+
};
|
|
39
|
+
if (options?.theme) {
|
|
40
|
+
const t = options.theme;
|
|
41
|
+
const el = container;
|
|
42
|
+
if (t.bg) el.style.setProperty("--color-aai-bg", t.bg);
|
|
43
|
+
if (t.primary) el.style.setProperty("--color-aai-primary", t.primary);
|
|
44
|
+
if (t.text) el.style.setProperty("--color-aai-text", t.text);
|
|
45
|
+
if (t.surface) el.style.setProperty("--color-aai-surface", t.surface);
|
|
46
|
+
if (t.border) el.style.setProperty("--color-aai-border", t.border);
|
|
47
|
+
}
|
|
48
|
+
render(/* @__PURE__ */ jsx(MountConfigProvider, {
|
|
49
|
+
value: mountConfig,
|
|
50
|
+
children: /* @__PURE__ */ jsx(SessionProvider, {
|
|
51
|
+
value: signals,
|
|
52
|
+
children: /* @__PURE__ */ jsx(Component, {})
|
|
53
|
+
})
|
|
54
|
+
}), container);
|
|
55
|
+
const handle = {
|
|
56
|
+
session,
|
|
57
|
+
signals,
|
|
58
|
+
dispose() {
|
|
59
|
+
render(null, container);
|
|
60
|
+
signals.dispose();
|
|
61
|
+
session.disconnect();
|
|
62
|
+
},
|
|
63
|
+
[Symbol.dispose]() {
|
|
64
|
+
handle.dispose();
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
return handle;
|
|
68
|
+
}
|
|
69
|
+
//#endregion
|
|
70
|
+
export { mount };
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import type { ClientEvent, ReadyConfig } from "@alexkroman1/aai/protocol";
|
|
2
|
+
import type { VoiceIO } from "./audio.ts";
|
|
3
|
+
import type { AgentState, Message, Reactive, SessionError, SessionErrorCode, SessionOptions, ToolCallInfo } from "./types.ts";
|
|
4
|
+
export type { AgentState, Message, Reactive, SessionError, SessionErrorCode, SessionOptions, ToolCallInfo, };
|
|
5
|
+
type BatchFn = (fn: () => void) => void;
|
|
6
|
+
/**
|
|
7
|
+
* A reactive voice session that manages WebSocket communication,
|
|
8
|
+
* audio capture/playback, and agent state transitions.
|
|
9
|
+
*
|
|
10
|
+
* Uses plain JSON text frames and binary audio frames for communication
|
|
11
|
+
* and native WebSocket for the connection.
|
|
12
|
+
*
|
|
13
|
+
* Implements `Disposable` for resource cleanup via `using`.
|
|
14
|
+
*
|
|
15
|
+
* @public
|
|
16
|
+
*/
|
|
17
|
+
export type VoiceSession = {
|
|
18
|
+
/** Current agent state (connecting, listening, thinking, etc.). */
|
|
19
|
+
readonly state: Reactive<AgentState>;
|
|
20
|
+
/** Chat message history for the session. */
|
|
21
|
+
readonly messages: Reactive<Message[]>;
|
|
22
|
+
/** Active tool calls for the current turn. */
|
|
23
|
+
readonly toolCalls: Reactive<ToolCallInfo[]>;
|
|
24
|
+
/**
|
|
25
|
+
* Live user utterance from STT/VAD.
|
|
26
|
+
* `null` = not speaking, `""` = speech detected but no text yet,
|
|
27
|
+
* non-empty string = partial/final transcript text.
|
|
28
|
+
*/
|
|
29
|
+
readonly userUtterance: Reactive<string | null>;
|
|
30
|
+
/**
|
|
31
|
+
* Streaming agent response text.
|
|
32
|
+
* `null` = not speaking, non-empty string = accumulated delta text.
|
|
33
|
+
* Cleared when the final `chat` message arrives.
|
|
34
|
+
*/
|
|
35
|
+
readonly agentUtterance: Reactive<string | null>;
|
|
36
|
+
/** Current session error, or `null` if no error. */
|
|
37
|
+
readonly error: Reactive<SessionError | null>;
|
|
38
|
+
/** Disconnection info, or `null` if connected. */
|
|
39
|
+
readonly disconnected: Reactive<{
|
|
40
|
+
intentional: boolean;
|
|
41
|
+
} | null>;
|
|
42
|
+
/**
|
|
43
|
+
* Open a WebSocket connection to the server and begin audio capture.
|
|
44
|
+
*
|
|
45
|
+
* @param options - Optional connection options. `signal` is an AbortSignal that, when aborted, disconnects the session.
|
|
46
|
+
*/
|
|
47
|
+
connect(options?: {
|
|
48
|
+
signal?: AbortSignal;
|
|
49
|
+
}): void;
|
|
50
|
+
/** Cancel the current agent turn and discard in-flight TTS audio. */
|
|
51
|
+
cancel(): void;
|
|
52
|
+
/** Clear messages, transcript, and error state without disconnecting. */
|
|
53
|
+
resetState(): void;
|
|
54
|
+
/** Reset the session: clear state and reconnect. */
|
|
55
|
+
reset(): void;
|
|
56
|
+
/** Close the WebSocket and release all audio resources. */
|
|
57
|
+
disconnect(): void;
|
|
58
|
+
/** Alias for `disconnect` for use with `using`. */
|
|
59
|
+
[Symbol.dispose](): void;
|
|
60
|
+
};
|
|
61
|
+
/**
|
|
62
|
+
* Handles server→client messages and updates reactive Preact signals
|
|
63
|
+
* accordingly (state transitions, transcripts, messages, audio playback).
|
|
64
|
+
*/
|
|
65
|
+
/** @internal Exported for testing only. */
|
|
66
|
+
export declare class ClientHandler {
|
|
67
|
+
#private;
|
|
68
|
+
constructor(opts: {
|
|
69
|
+
state: Reactive<AgentState>;
|
|
70
|
+
messages: Reactive<Message[]>;
|
|
71
|
+
toolCalls: Reactive<ToolCallInfo[]>;
|
|
72
|
+
userUtterance: Reactive<string | null>;
|
|
73
|
+
agentUtterance: Reactive<string | null>;
|
|
74
|
+
error: Reactive<SessionError | null>;
|
|
75
|
+
voiceIO: () => VoiceIO | null;
|
|
76
|
+
batch: BatchFn;
|
|
77
|
+
});
|
|
78
|
+
/** Single entry point for all server→client session events. */
|
|
79
|
+
event(e: ClientEvent): void;
|
|
80
|
+
playAudioChunk(chunk: Uint8Array): void;
|
|
81
|
+
playAudioDone(): void;
|
|
82
|
+
/**
|
|
83
|
+
* Dispatch an incoming WebSocket message (text or binary).
|
|
84
|
+
*
|
|
85
|
+
* Returns the parsed config if the message is a `config` message,
|
|
86
|
+
* otherwise `null`.
|
|
87
|
+
*/
|
|
88
|
+
handleMessage(data: string | ArrayBuffer): ReadyConfig | null;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Create a voice session that connects to an AAI server via WebSocket.
|
|
92
|
+
*
|
|
93
|
+
* Uses plain JSON text frames and binary audio frames for communication.
|
|
94
|
+
*
|
|
95
|
+
* @param options - Session configuration including the platform server URL.
|
|
96
|
+
* @returns A {@link VoiceSession} handle for controlling the session.
|
|
97
|
+
*
|
|
98
|
+
* @public
|
|
99
|
+
*/
|
|
100
|
+
export declare function createVoiceSession(options: SessionOptions): VoiceSession;
|