@alexkroman1/aai-ui 0.12.2 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/dist/_react-test-utils.d.ts +86 -0
  2. package/dist/audio.js +4 -5
  3. package/dist/build-default-client.d.ts +1 -0
  4. package/dist/{_components → components}/button.d.ts +4 -4
  5. package/dist/{_components → components}/button.js +28 -5
  6. package/dist/components/chat-view.d.ts +38 -0
  7. package/dist/components/chat-view.js +102 -0
  8. package/dist/{_components → components}/controls.d.ts +1 -1
  9. package/dist/{_components → components}/controls.js +8 -5
  10. package/dist/{_components → components}/message-list.d.ts +2 -2
  11. package/dist/components/message-list.js +149 -0
  12. package/dist/components/sidebar-layout.d.ts +21 -0
  13. package/dist/components/sidebar-layout.js +43 -0
  14. package/dist/{_components → components}/start-screen.d.ts +4 -4
  15. package/dist/{_components → components}/start-screen.js +17 -8
  16. package/dist/{_components → components}/tool-call-block.d.ts +5 -6
  17. package/dist/components/tool-call-block.js +3 -0
  18. package/dist/components/tool-config-context.d.ts +22 -0
  19. package/dist/context.d.ts +23 -0
  20. package/dist/context.js +39 -0
  21. package/dist/default-client/assets/audio-DH9LpexG.js +1 -0
  22. package/dist/default-client/assets/capture-processor-C26lSiVr.js +53 -0
  23. package/dist/default-client/assets/default-client-AeJhjHfj.js +49 -0
  24. package/dist/default-client/assets/default-client-BK1ItCvw.css +2 -0
  25. package/dist/default-client/assets/playback-processor-dSA8Im99.js +101 -0
  26. package/dist/default-client/default-client.html +15 -0
  27. package/dist/default-client.d.ts +2 -0
  28. package/dist/define-client.d.ts +72 -24
  29. package/dist/define-client.js +71 -45
  30. package/dist/hooks.d.ts +5 -0
  31. package/dist/hooks.js +55 -0
  32. package/dist/index.d.ts +15 -37
  33. package/dist/index.js +11 -18
  34. package/dist/session-core.d.ts +70 -0
  35. package/dist/session-core.js +437 -0
  36. package/dist/tool-call-block-CCuChsFm.js +131 -0
  37. package/dist/types.d.ts +19 -22
  38. package/package.json +24 -18
  39. package/styles.css +3 -33
  40. package/dist/_components/app.d.ts +0 -23
  41. package/dist/_components/app.js +0 -41
  42. package/dist/_components/chat-view.d.ts +0 -29
  43. package/dist/_components/chat-view.js +0 -61
  44. package/dist/_components/error-banner.d.ts +0 -21
  45. package/dist/_components/error-banner.js +0 -27
  46. package/dist/_components/message-bubble.d.ts +0 -23
  47. package/dist/_components/message-bubble.js +0 -35
  48. package/dist/_components/message-list.js +0 -76
  49. package/dist/_components/sidebar-layout.d.ts +0 -21
  50. package/dist/_components/sidebar-layout.js +0 -36
  51. package/dist/_components/state-indicator.d.ts +0 -29
  52. package/dist/_components/state-indicator.js +0 -37
  53. package/dist/_components/thinking-indicator.d.ts +0 -16
  54. package/dist/_components/thinking-indicator.js +0 -34
  55. package/dist/_components/tool-call-block.js +0 -118
  56. package/dist/_components/tool-icons.d.ts +0 -17
  57. package/dist/_components/tool-icons.js +0 -128
  58. package/dist/_components/transcript.d.ts +0 -25
  59. package/dist/_components/transcript.js +0 -35
  60. package/dist/client-context.d.ts +0 -33
  61. package/dist/client-context.js +0 -15
  62. package/dist/client-handler.d.ts +0 -43
  63. package/dist/session-CWB7vmqz.js +0 -429
  64. package/dist/session.d.ts +0 -69
  65. package/dist/session.js +0 -2
  66. package/dist/signals.d.ts +0 -120
  67. package/dist/signals.js +0 -161
  68. package/dist/types.test-d.d.ts +0 -7
@@ -0,0 +1,437 @@
1
+ import { WS_OPEN, errorMessage } from "@alexkroman1/aai";
2
+ import { ReadyConfigSchema, ServerMessageSchema, lenientParse } from "@alexkroman1/aai/protocol";
3
+ //#region session-core.ts
4
+ /**
5
+ * Framework-agnostic voice session core.
6
+ *
7
+ * Manages WebSocket communication, audio capture/playback, and agent state
8
+ * transitions using a subscribe/getSnapshot pattern compatible with React's
9
+ * `useSyncExternalStore` and other external store consumers.
10
+ *
11
+ * No dependency on React, Preact, or any UI framework.
12
+ */
13
+ /**
14
+ * Initialize audio capture and playback after the server sends a ready config.
15
+ *
16
+ * Lifecycle: dynamically import audio modules -> request microphone access ->
17
+ * register AudioWorklet processors -> create a `VoiceIO` instance -> send
18
+ * `audio_ready` to the server -> transition state to `"listening"`.
19
+ *
20
+ * Uses the connection `generation` counter to detect if `connect()` was called
21
+ * while awaiting async operations; if so, the stale VoiceIO is closed immediately
22
+ * to prevent it from being assigned to a newer connection.
23
+ *
24
+ * On failure (e.g. microphone permission denied, WebSocket closed mid-setup),
25
+ * sets the error state and transitions to `"disconnected"`.
26
+ */
27
+ async function initAudioCapture(conn, msg, deps) {
28
+ if (conn.audioSetupInFlight) return;
29
+ conn.audioSetupInFlight = true;
30
+ const gen = conn.generation;
31
+ try {
32
+ const [{ createVoiceIO }, captureWorklet, playbackWorklet] = await Promise.all([
33
+ import("./audio.js"),
34
+ import("./worklets/capture-processor.js").then((m) => m.default),
35
+ import("./worklets/playback-processor.js").then((m) => m.default)
36
+ ]);
37
+ const io = await createVoiceIO({
38
+ sttSampleRate: msg.sampleRate,
39
+ ttsSampleRate: msg.ttsSampleRate,
40
+ captureWorkletSrc: captureWorklet,
41
+ playbackWorkletSrc: playbackWorklet,
42
+ onMicData: (pcm16) => {
43
+ try {
44
+ deps.sendBinary(pcm16);
45
+ } catch {
46
+ console.debug("[aai-ui] sendBinary dropped: connection closed");
47
+ }
48
+ }
49
+ });
50
+ if (conn.generation !== gen || !conn.ws || conn.ws.readyState !== WS_OPEN) {
51
+ io.close();
52
+ return;
53
+ }
54
+ conn.voiceIO = io;
55
+ deps.send({ type: "audio_ready" });
56
+ deps.updateState({ state: "listening" });
57
+ } catch (err) {
58
+ if (conn.generation !== gen || !conn.ws || conn.ws.readyState !== WS_OPEN) return;
59
+ deps.updateState({
60
+ state: "error",
61
+ error: {
62
+ code: "audio",
63
+ message: `Microphone access failed: ${errorMessage(err)}`
64
+ },
65
+ running: false
66
+ });
67
+ } finally {
68
+ conn.audioSetupInFlight = false;
69
+ }
70
+ }
71
+ function buildWsUrl(platformUrl, resume, sessionId) {
72
+ const wsUrl = new URL("websocket", platformUrl.endsWith("/") ? platformUrl : `${platformUrl}/`);
73
+ wsUrl.protocol = wsUrl.protocol === "https:" ? "wss:" : "ws:";
74
+ if (sessionId) wsUrl.searchParams.set("sessionId", sessionId);
75
+ else if (resume) wsUrl.searchParams.set("resume", "1");
76
+ return wsUrl;
77
+ }
78
+ /**
79
+ * Create a framework-agnostic voice session core that connects to an AAI
80
+ * server via WebSocket.
81
+ *
82
+ * Uses a subscribe/getSnapshot pattern for state management, compatible with
83
+ * React's `useSyncExternalStore` and other external store integrations.
84
+ *
85
+ * @param options - Session configuration including the platform server URL.
86
+ * @returns A {@link SessionCore} handle for controlling the session.
87
+ *
88
+ * @public
89
+ */
90
+ function createSessionCore(options) {
91
+ const WS = options.WebSocket ?? WebSocket;
92
+ let currentSnapshot = {
93
+ state: "disconnected",
94
+ messages: [],
95
+ toolCalls: [],
96
+ userTranscript: null,
97
+ agentTranscript: null,
98
+ error: null,
99
+ started: false,
100
+ running: false
101
+ };
102
+ const subscribers = /* @__PURE__ */ new Set();
103
+ function notify() {
104
+ for (const sub of subscribers) sub();
105
+ }
106
+ function updateState(partial) {
107
+ currentSnapshot = {
108
+ ...currentSnapshot,
109
+ ...partial
110
+ };
111
+ notify();
112
+ }
113
+ function getSnapshot() {
114
+ return currentSnapshot;
115
+ }
116
+ function subscribe(callback) {
117
+ subscribers.add(callback);
118
+ return () => {
119
+ subscribers.delete(callback);
120
+ };
121
+ }
122
+ const conn = {
123
+ ws: null,
124
+ voiceIO: null,
125
+ audioSetupInFlight: false,
126
+ generation: 0
127
+ };
128
+ let connectionController = null;
129
+ let hasConnected = false;
130
+ function cleanupAudio() {
131
+ conn.audioSetupInFlight = false;
132
+ conn.voiceIO?.close();
133
+ conn.voiceIO = null;
134
+ }
135
+ function resetState() {
136
+ updateState({
137
+ messages: [],
138
+ toolCalls: [],
139
+ userTranscript: null,
140
+ agentTranscript: null,
141
+ error: null
142
+ });
143
+ }
144
+ function send(msg) {
145
+ if (conn.ws && conn.ws.readyState === WS_OPEN) conn.ws.send(JSON.stringify(msg));
146
+ }
147
+ function sendBinary(data) {
148
+ if (conn.ws && conn.ws.readyState === WS_OPEN) conn.ws.send(data);
149
+ }
150
+ const audioDeps = {
151
+ send,
152
+ sendBinary,
153
+ updateState
154
+ };
155
+ /** Incremented on each turn boundary -- stale async callbacks compare against this. */
156
+ let handlerGeneration = 0;
157
+ function handleUserTranscriptEvent(text) {
158
+ handlerGeneration++;
159
+ updateState({
160
+ userTranscript: null,
161
+ messages: [...currentSnapshot.messages, {
162
+ role: "user",
163
+ content: text
164
+ }],
165
+ state: "thinking"
166
+ });
167
+ }
168
+ function handleAgentTranscriptEvent(text) {
169
+ updateState({
170
+ agentTranscript: null,
171
+ messages: [...currentSnapshot.messages, {
172
+ role: "assistant",
173
+ content: text
174
+ }]
175
+ });
176
+ }
177
+ /** Single entry point for all server->client session events. */
178
+ function handleEvent(e) {
179
+ if (currentSnapshot.state === "error" && e.type !== "error") updateState({
180
+ state: "disconnected",
181
+ error: null
182
+ });
183
+ switch (e.type) {
184
+ case "speech_started":
185
+ updateState({ userTranscript: "" });
186
+ break;
187
+ case "speech_stopped": break;
188
+ case "user_transcript":
189
+ handleUserTranscriptEvent(e.text);
190
+ break;
191
+ case "agent_transcript":
192
+ handleAgentTranscriptEvent(e.text);
193
+ break;
194
+ case "tool_call":
195
+ updateState({ toolCalls: [...currentSnapshot.toolCalls, {
196
+ callId: e.toolCallId,
197
+ name: e.toolName,
198
+ args: e.args,
199
+ status: "pending",
200
+ afterMessageIndex: currentSnapshot.messages.length - 1
201
+ }] });
202
+ break;
203
+ case "tool_call_done": {
204
+ const tcs = currentSnapshot.toolCalls;
205
+ const idx = tcs.findIndex((tc) => tc.callId === e.toolCallId);
206
+ if (idx !== -1) {
207
+ const updated = [...tcs];
208
+ const existing = updated[idx];
209
+ if (existing) updated[idx] = {
210
+ ...existing,
211
+ status: "done",
212
+ result: e.result
213
+ };
214
+ updateState({ toolCalls: updated });
215
+ }
216
+ break;
217
+ }
218
+ case "reply_done":
219
+ updateState({ state: "listening" });
220
+ break;
221
+ case "cancelled":
222
+ handlerGeneration++;
223
+ conn.voiceIO?.flush();
224
+ updateState({
225
+ userTranscript: null,
226
+ agentTranscript: null,
227
+ state: "listening"
228
+ });
229
+ break;
230
+ case "reset":
231
+ handlerGeneration++;
232
+ conn.voiceIO?.flush();
233
+ updateState({
234
+ messages: [],
235
+ toolCalls: [],
236
+ userTranscript: null,
237
+ agentTranscript: null,
238
+ error: null,
239
+ state: "listening"
240
+ });
241
+ break;
242
+ case "error":
243
+ console.error("Agent error:", e.message);
244
+ updateState({
245
+ state: "error",
246
+ error: {
247
+ code: e.code,
248
+ message: e.message
249
+ },
250
+ running: false
251
+ });
252
+ break;
253
+ default: break;
254
+ }
255
+ }
256
+ /** Enqueue a PCM16 audio chunk for playback. Transitions state to `"speaking"` on the first chunk. */
257
+ function playAudioChunk(chunk) {
258
+ if (currentSnapshot.state === "disconnected" && currentSnapshot.error !== null) return;
259
+ if (currentSnapshot.state !== "speaking") updateState({ state: "speaking" });
260
+ if (chunk.buffer instanceof ArrayBuffer) conn.voiceIO?.enqueue(chunk.buffer);
261
+ }
262
+ /**
263
+ * Signal that the server has finished sending audio for this turn.
264
+ * Waits for the audio queue to drain, then transitions state to `"listening"`.
265
+ * Uses the `handlerGeneration` counter to discard stale completions from interrupted turns.
266
+ */
267
+ function playAudioDone() {
268
+ const gen = handlerGeneration;
269
+ const io = conn.voiceIO;
270
+ if (io) io.done().then(() => {
271
+ if (handlerGeneration !== gen) return;
272
+ updateState({ state: "listening" });
273
+ }).catch((err) => {
274
+ console.warn("Audio playback done failed:", err);
275
+ });
276
+ else updateState({ state: "listening" });
277
+ }
278
+ /** Parse a JSON text frame into a ServerMessage, or null if invalid/unknown. */
279
+ function parseTextFrame(data) {
280
+ try {
281
+ const result = lenientParse(ServerMessageSchema, JSON.parse(data));
282
+ if (!result.ok) {
283
+ if (result.malformed) console.warn("Ignoring invalid server message:", result.error);
284
+ return null;
285
+ }
286
+ return result.data;
287
+ } catch {
288
+ return null;
289
+ }
290
+ }
291
+ /** Try to extract a ReadyConfig from a config message, or null. */
292
+ function parseConfig(msg) {
293
+ const { type: _, sessionId, ...config } = msg;
294
+ const parsed = ReadyConfigSchema.safeParse(config);
295
+ if (!parsed.success) {
296
+ console.warn("Unsupported server config:", parsed.error.message);
297
+ return null;
298
+ }
299
+ return sessionId ? {
300
+ ...parsed.data,
301
+ sessionId
302
+ } : parsed.data;
303
+ }
304
+ /**
305
+ * Dispatch an incoming WebSocket message (text or binary).
306
+ *
307
+ * Returns the parsed config if the message is a `config` message,
308
+ * otherwise `null`.
309
+ */
310
+ function handleMessage(data) {
311
+ if (data instanceof ArrayBuffer) {
312
+ playAudioChunk(new Uint8Array(data));
313
+ return null;
314
+ }
315
+ const msg = parseTextFrame(data);
316
+ if (!msg) return null;
317
+ if (msg.type === "config") return parseConfig(msg);
318
+ if (msg.type === "audio_done") {
319
+ playAudioDone();
320
+ return null;
321
+ }
322
+ handleEvent(msg);
323
+ return null;
324
+ }
325
+ function connect(opts) {
326
+ updateState({
327
+ state: "connecting",
328
+ error: null
329
+ });
330
+ connectionController?.abort();
331
+ cleanupAudio();
332
+ conn.ws?.close();
333
+ conn.ws = null;
334
+ conn.generation++;
335
+ const controller = new AbortController();
336
+ connectionController = controller;
337
+ const { signal: sig } = controller;
338
+ if (opts?.signal) opts.signal.addEventListener("abort", () => disconnect(), { signal: sig });
339
+ const resumeId = !hasConnected ? options.resumeSessionId : void 0;
340
+ const socket = new WS(buildWsUrl(options.platformUrl, hasConnected, resumeId).toString());
341
+ socket.binaryType = "arraybuffer";
342
+ conn.ws = socket;
343
+ socket.addEventListener("open", () => {
344
+ updateState({ state: "ready" });
345
+ }, { signal: sig });
346
+ socket.addEventListener("message", (event) => {
347
+ const config = handleMessage(event.data);
348
+ if (config) {
349
+ if (config.sessionId) options.onSessionId?.(config.sessionId);
350
+ const isReconnect = hasConnected;
351
+ hasConnected = true;
352
+ initAudioCapture(conn, config, audioDeps).catch((err) => {
353
+ audioDeps.updateState({
354
+ state: "error",
355
+ error: {
356
+ code: "audio",
357
+ message: `Audio capture failed: ${errorMessage(err)}`
358
+ },
359
+ running: false
360
+ });
361
+ });
362
+ if (isReconnect && currentSnapshot.messages.length > 0) send({
363
+ type: "history",
364
+ messages: currentSnapshot.messages.map((m) => ({
365
+ role: m.role,
366
+ content: m.content
367
+ }))
368
+ });
369
+ }
370
+ }, { signal: sig });
371
+ socket.addEventListener("close", () => {
372
+ if (sig.aborted) return;
373
+ controller.abort();
374
+ cleanupAudio();
375
+ updateState({
376
+ state: "disconnected",
377
+ running: false
378
+ });
379
+ }, { signal: sig });
380
+ }
381
+ function cancel() {
382
+ conn.voiceIO?.flush();
383
+ updateState({ state: "listening" });
384
+ send({ type: "cancel" });
385
+ }
386
+ function reset() {
387
+ conn.voiceIO?.flush();
388
+ if (conn.ws && conn.ws.readyState === WS_OPEN) {
389
+ send({ type: "reset" });
390
+ return;
391
+ }
392
+ resetState();
393
+ disconnect();
394
+ connect();
395
+ }
396
+ function disconnect() {
397
+ connectionController?.abort();
398
+ connectionController = null;
399
+ cleanupAudio();
400
+ conn.ws?.close();
401
+ conn.ws = null;
402
+ updateState({
403
+ state: "disconnected",
404
+ running: false
405
+ });
406
+ }
407
+ function start() {
408
+ updateState({
409
+ started: true,
410
+ running: true
411
+ });
412
+ connect();
413
+ }
414
+ function toggle() {
415
+ if (currentSnapshot.running) disconnect();
416
+ else {
417
+ updateState({ running: true });
418
+ connect();
419
+ }
420
+ }
421
+ return {
422
+ getSnapshot,
423
+ subscribe,
424
+ connect,
425
+ cancel,
426
+ resetState,
427
+ reset,
428
+ disconnect,
429
+ start,
430
+ toggle,
431
+ [Symbol.dispose]() {
432
+ disconnect();
433
+ }
434
+ };
435
+ }
436
+ //#endregion
437
+ export { createSessionCore };
@@ -0,0 +1,131 @@
1
+ import { useTheme } from "./context.js";
2
+ import clsx from "clsx";
3
+ import { createContext, useContext, useMemo, useState } from "react";
4
+ import { jsx, jsxs } from "react/jsx-runtime";
5
+ //#region components/tool-config-context.ts
6
+ /**
7
+ * Context for tool display configuration.
8
+ * Provided by `client` or manually via `ToolConfigContext.Provider`.
9
+ *
10
+ * @public
11
+ */
12
+ const ToolConfigContext = createContext({});
13
+ /**
14
+ * Read tool display configuration from the nearest `ToolConfigContext.Provider`.
15
+ *
16
+ * @internal
17
+ */
18
+ function useToolConfig() {
19
+ return useContext(ToolConfigContext);
20
+ }
21
+ //#endregion
22
+ //#region components/tool-call-block.tsx
23
+ /** @jsxImportSource react */
24
+ function formatResult(result) {
25
+ try {
26
+ return JSON.stringify(JSON.parse(result), null, 2);
27
+ } catch {
28
+ return result;
29
+ }
30
+ }
31
+ /**
32
+ * Renders a tool invocation with an optional icon/emoji, title, subtitle, and a
33
+ * collapsible result viewer.
34
+ *
35
+ * Tool display is configured via `ToolConfigContext`. If no config is found
36
+ * for a tool name, the raw tool name is shown as the title.
37
+ *
38
+ * While the tool call is pending a shimmer animation is shown. Once
39
+ * complete, clicking the block expands the formatted JSON result.
40
+ *
41
+ * @example
42
+ * ```tsx
43
+ * <ToolCallBlock toolCall={toolCall} />
44
+ * ```
45
+ *
46
+ * @param toolCall - The tool call to render (see {@link ToolCallInfo}).
47
+ * @param className - Additional CSS class names.
48
+ *
49
+ * @public
50
+ */
51
+ function ToolCallBlock({ toolCall, className }) {
52
+ const [isOpen, setIsOpen] = useState(false);
53
+ const theme = useTheme();
54
+ const config = useToolConfig()[toolCall.name];
55
+ const isPending = toolCall.status === "pending";
56
+ const title = config?.label || toolCall.name;
57
+ const icon = config?.icon;
58
+ const canExpand = !isPending && Boolean(toolCall.result);
59
+ const formatted = useMemo(() => toolCall.result ? formatResult(toolCall.result) : "", [toolCall.result]);
60
+ const subtitle = useMemo(() => {
61
+ const args = toolCall.args;
62
+ if (toolCall.name === "run_code" && args.code) {
63
+ const firstLine = String(args.code).split("\n")[0] ?? "";
64
+ return firstLine.length > 80 ? `${firstLine.slice(0, 80)}...` : firstLine;
65
+ }
66
+ for (const key of [
67
+ "query",
68
+ "url",
69
+ "question"
70
+ ]) if (args[key]) return String(args[key]);
71
+ const summary = JSON.stringify(args);
72
+ return summary.length > 80 ? `${summary.slice(0, 80)}...` : summary;
73
+ }, [toolCall.name, toolCall.args]);
74
+ return /* @__PURE__ */ jsxs("div", {
75
+ className: clsx("flex flex-col", className),
76
+ children: [/* @__PURE__ */ jsxs("button", {
77
+ type: "button",
78
+ "aria-expanded": canExpand ? isOpen : void 0,
79
+ disabled: isPending,
80
+ className: clsx("flex items-center gap-2 px-3 py-2 rounded-aai border select-none text-left w-full", canExpand && "cursor-pointer"),
81
+ style: {
82
+ borderColor: theme.border,
83
+ background: "rgba(255,255,255,0.031)"
84
+ },
85
+ onClick: () => {
86
+ if (canExpand) setIsOpen(!isOpen);
87
+ },
88
+ children: [
89
+ icon && /* @__PURE__ */ jsx("span", {
90
+ className: "w-4 h-4 shrink-0 text-center leading-4",
91
+ children: icon
92
+ }),
93
+ /* @__PURE__ */ jsx("span", {
94
+ className: clsx("text-sm font-medium", isPending && "tool-shimmer"),
95
+ style: { color: theme.text },
96
+ children: title
97
+ }),
98
+ /* @__PURE__ */ jsx("span", {
99
+ className: "text-sm truncate flex-1 min-w-0",
100
+ style: { color: "rgba(255,255,255,0.422)" },
101
+ children: subtitle
102
+ }),
103
+ canExpand && /* @__PURE__ */ jsx("span", {
104
+ className: "text-xs shrink-0",
105
+ style: { color: "rgba(255,255,255,0.422)" },
106
+ children: isOpen ? "▾" : "▸"
107
+ })
108
+ ]
109
+ }), isOpen && /* @__PURE__ */ jsxs("div", {
110
+ className: "border-x border-b rounded-b-aai max-h-64 overflow-auto",
111
+ style: {
112
+ borderColor: theme.border,
113
+ background: theme.surface
114
+ },
115
+ children: [toolCall.name === "run_code" && Boolean(toolCall.args.code) && /* @__PURE__ */ jsx("pre", {
116
+ className: "text-xs p-2 whitespace-pre-wrap border-b font-mono",
117
+ style: {
118
+ color: theme.text,
119
+ borderColor: theme.border
120
+ },
121
+ children: String(toolCall.args.code)
122
+ }), formatted && /* @__PURE__ */ jsx("pre", {
123
+ className: "text-xs p-2 whitespace-pre-wrap",
124
+ style: { color: "rgba(255,255,255,0.422)" },
125
+ children: formatted
126
+ })]
127
+ })]
128
+ });
129
+ }
130
+ //#endregion
131
+ export { ToolConfigContext as n, ToolCallBlock as t };
package/dist/types.d.ts CHANGED
@@ -24,8 +24,8 @@ export type ChatMessage = {
24
24
  * @public
25
25
  */
26
26
  export type ToolCallInfo = {
27
- toolCallId: string;
28
- toolName: string;
27
+ callId: string;
28
+ name: string;
29
29
  args: Record<string, unknown>;
30
30
  status: "pending" | "done";
31
31
  result?: string | undefined;
@@ -44,14 +44,6 @@ export type SessionError = {
44
44
  /** A human-readable description of the error. */
45
45
  readonly message: string;
46
46
  };
47
- /**
48
- * Minimal reactive container. Compatible with `@preact/signals` `Signal<T>`.
49
- *
50
- * @public
51
- */
52
- export type Reactive<T> = {
53
- value: T;
54
- };
55
47
  /**
56
48
  * Options for creating a voice session.
57
49
  *
@@ -60,18 +52,6 @@ export type Reactive<T> = {
60
52
  export type VoiceSessionOptions = {
61
53
  /** Base URL of the AAI platform server. */
62
54
  platformUrl: string;
63
- /**
64
- * Factory for creating reactive state containers.
65
- * Defaults to a plain mutable wrapper. Pass `signal` from
66
- * `@preact/signals` for automatic Preact component re-rendering.
67
- */
68
- reactiveFactory?: <T>(initial: T) => Reactive<T>;
69
- /**
70
- * Function to batch multiple reactive updates.
71
- * Defaults to calling the function directly. Pass `batch` from
72
- * `@preact/signals` for optimized batched updates.
73
- */
74
- batch?: (fn: () => void) => void;
75
55
  /**
76
56
  * Called when the server sends a session ID in the config message.
77
57
  * Use this to store the ID (e.g. in localStorage) for reconnection
@@ -99,3 +79,20 @@ export type WebSocketConstructor = {
99
79
  new (url: string | URL, protocols?: string | string[]): WebSocket;
100
80
  readonly OPEN: number;
101
81
  };
82
+ /**
83
+ * Theme color overrides for the AAI UI components.
84
+ *
85
+ * @public
86
+ */
87
+ export type ClientTheme = {
88
+ /** Background color. Default: `#101010`. */
89
+ bg?: string;
90
+ /** Primary accent color. Default: `#fab283`. */
91
+ primary?: string;
92
+ /** Main text color. */
93
+ text?: string;
94
+ /** Surface/card color. */
95
+ surface?: string;
96
+ /** Border color. */
97
+ border?: string;
98
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alexkroman1/aai-ui",
3
- "version": "0.12.2",
3
+ "version": "1.0.2",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "dist",
@@ -13,19 +13,16 @@
13
13
  "import": "./dist/index.js"
14
14
  },
15
15
  "./styles.css": "./styles.css",
16
- "./session": {
17
- "@dev/source": "./session.ts",
18
- "types": "./dist/session.d.ts",
19
- "import": "./dist/session.js"
20
- }
16
+ "./package.json": "./package.json",
17
+ "./default-client/*": "./dist/default-client/*"
21
18
  },
22
19
  "dependencies": {
23
20
  "clsx": "^2.1.1",
24
- "@alexkroman1/aai": "0.12.2"
21
+ "@alexkroman1/aai": "1.0.2"
25
22
  },
26
23
  "peerDependencies": {
27
- "@preact/signals": "^2.8.2",
28
- "preact": "^10.29.0",
24
+ "react": "^19.0.0",
25
+ "react-dom": "^19.0.0",
29
26
  "tailwindcss": "^4.2.1"
30
27
  },
31
28
  "peerDependenciesMeta": {
@@ -34,15 +31,22 @@
34
31
  }
35
32
  },
36
33
  "devDependencies": {
37
- "@preact/signals": "^2.8.2",
38
- "@testing-library/preact": "^3.2.4",
39
- "jsdom": "^29.0.1",
40
- "preact": "^10.29.0",
41
- "tsdown": "^0.21.5",
42
- "vitest": "^4.1.1"
34
+ "@tailwindcss/vite": "^4.2.1",
35
+ "@testing-library/dom": "^10.0.0",
36
+ "@testing-library/react": "^16.3.0",
37
+ "@types/react": "^19.0.0",
38
+ "@types/react-dom": "^19.0.0",
39
+ "@vitejs/plugin-react": "^6.0.1",
40
+ "jsdom": "^29.0.2",
41
+ "react": "^19.0.0",
42
+ "react-dom": "^19.0.0",
43
+ "tsdown": "^0.21.7",
44
+ "tsx": "^4.21.0",
45
+ "vite": "^8.0.3",
46
+ "vitest": "^4.1.3"
43
47
  },
44
48
  "engines": {
45
- "node": ">=22.6"
49
+ "node": ">=24"
46
50
  },
47
51
  "repository": {
48
52
  "type": "git",
@@ -50,10 +54,12 @@
50
54
  "directory": "packages/aai-ui"
51
55
  },
52
56
  "scripts": {
53
- "build": "tsdown && tsc -p tsconfig.build.json",
57
+ "test": "vitest run",
58
+ "test:coverage": "vitest run --coverage",
59
+ "build": "tsdown && tsc -p tsconfig.build.json && tsx build-default-client.ts",
54
60
  "typecheck": "tsc --noEmit",
55
61
  "lint": "biome check .",
56
62
  "check:publint": "publint",
57
- "check:attw": "attw --pack --profile esm-only --entrypoints . ./session"
63
+ "check:attw": "attw --pack --profile esm-only --entrypoints ."
58
64
  }
59
65
  }