@alexkroman1/aai-ui 0.9.3 → 0.10.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/_components/app.d.ts +19 -1
- package/dist/_components/app.js +19 -1
- package/dist/_components/button.d.ts +43 -4
- package/dist/_components/button.js +33 -9
- package/dist/_components/chat-view.d.ts +25 -1
- package/dist/_components/chat-view.js +25 -1
- package/dist/_components/controls.d.ts +15 -1
- package/dist/_components/controls.js +15 -1
- package/dist/_components/error-banner.d.ts +15 -1
- package/dist/_components/error-banner.js +15 -1
- package/dist/_components/message-bubble.d.ts +19 -3
- package/dist/_components/message-bubble.js +17 -1
- package/dist/_components/message-list.d.ts +18 -1
- package/dist/_components/message-list.js +20 -3
- package/dist/_components/state-indicator.d.ts +23 -1
- package/dist/_components/state-indicator.js +23 -1
- package/dist/_components/thinking-indicator.d.ts +12 -1
- package/dist/_components/thinking-indicator.js +23 -11
- package/dist/_components/tool-call-block.d.ts +21 -1
- package/dist/_components/tool-call-block.js +33 -11
- package/dist/_components/transcript.d.ts +19 -1
- package/dist/_components/transcript.js +19 -1
- package/dist/_sqlite-stub.d.ts +3 -0
- package/dist/audio.js +13 -3
- package/dist/client-handler.d.ts +43 -0
- package/dist/index.d.ts +23 -6
- package/dist/index.js +3 -4
- package/dist/mount.d.ts +4 -0
- package/dist/mount.js +7 -4
- package/dist/session-D0RdWlWH.js +451 -0
- package/dist/session.d.ts +5 -36
- package/dist/session.js +1 -358
- package/dist/signals.d.ts +48 -0
- package/dist/signals.js +84 -25
- package/dist/types.d.ts +19 -5
- package/package.json +12 -10
- package/styles.css +2 -3
- package/dist/components.d.ts +0 -31
- package/dist/components.js +0 -17
package/dist/session.js
CHANGED
|
@@ -1,359 +1,2 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { errorMessage } from "@alexkroman1/aai/utils";
|
|
3
|
-
//#region session.ts
|
|
4
|
-
/** Built-in non-reactive container (plain mutable wrapper). */
|
|
5
|
-
function plainReactive(initial) {
|
|
6
|
-
return { value: initial };
|
|
7
|
-
}
|
|
8
|
-
/** No-op batch — just calls the function. */
|
|
9
|
-
function plainBatch(fn) {
|
|
10
|
-
fn();
|
|
11
|
-
}
|
|
12
|
-
/**
|
|
13
|
-
* Handles server→client messages and updates reactive Preact signals
|
|
14
|
-
* accordingly (state transitions, transcripts, messages, audio playback).
|
|
15
|
-
*/
|
|
16
|
-
/** @internal Exported for testing only. */
|
|
17
|
-
var ClientHandler = class {
|
|
18
|
-
#state;
|
|
19
|
-
#messages;
|
|
20
|
-
#toolCalls;
|
|
21
|
-
#userUtterance;
|
|
22
|
-
#agentUtterance;
|
|
23
|
-
#error;
|
|
24
|
-
#voiceIO;
|
|
25
|
-
#batch;
|
|
26
|
-
/** Incremented on each turn boundary — stale async callbacks compare against this. */
|
|
27
|
-
#generation = 0;
|
|
28
|
-
constructor(opts) {
|
|
29
|
-
this.#state = opts.state;
|
|
30
|
-
this.#messages = opts.messages;
|
|
31
|
-
this.#toolCalls = opts.toolCalls;
|
|
32
|
-
this.#userUtterance = opts.userUtterance;
|
|
33
|
-
this.#agentUtterance = opts.agentUtterance;
|
|
34
|
-
this.#error = opts.error;
|
|
35
|
-
this.#voiceIO = opts.voiceIO;
|
|
36
|
-
this.#batch = opts.batch;
|
|
37
|
-
}
|
|
38
|
-
/** Single entry point for all server→client session events. */
|
|
39
|
-
event(e) {
|
|
40
|
-
switch (e.type) {
|
|
41
|
-
case "speech_started":
|
|
42
|
-
this.#userUtterance.value = "";
|
|
43
|
-
break;
|
|
44
|
-
case "speech_stopped": break;
|
|
45
|
-
case "transcript":
|
|
46
|
-
this.#userUtterance.value = e.text;
|
|
47
|
-
break;
|
|
48
|
-
case "turn":
|
|
49
|
-
this.#generation++;
|
|
50
|
-
this.#batch(() => {
|
|
51
|
-
this.#userUtterance.value = null;
|
|
52
|
-
this.#messages.value = [...this.#messages.value, {
|
|
53
|
-
role: "user",
|
|
54
|
-
content: e.text
|
|
55
|
-
}];
|
|
56
|
-
this.#state.value = "thinking";
|
|
57
|
-
});
|
|
58
|
-
break;
|
|
59
|
-
case "chat_delta":
|
|
60
|
-
this.#agentUtterance.value = this.#agentUtterance.value ? `${this.#agentUtterance.value} ${e.text}` : e.text;
|
|
61
|
-
break;
|
|
62
|
-
case "chat":
|
|
63
|
-
this.#batch(() => {
|
|
64
|
-
this.#agentUtterance.value = null;
|
|
65
|
-
this.#messages.value = [...this.#messages.value, {
|
|
66
|
-
role: "assistant",
|
|
67
|
-
content: e.text
|
|
68
|
-
}];
|
|
69
|
-
});
|
|
70
|
-
break;
|
|
71
|
-
case "tool_call_start":
|
|
72
|
-
this.#toolCalls.value = [...this.#toolCalls.value, {
|
|
73
|
-
toolCallId: e.toolCallId,
|
|
74
|
-
toolName: e.toolName,
|
|
75
|
-
args: e.args,
|
|
76
|
-
status: "pending",
|
|
77
|
-
afterMessageIndex: this.#messages.value.length - 1
|
|
78
|
-
}];
|
|
79
|
-
break;
|
|
80
|
-
case "tool_call_done": {
|
|
81
|
-
const tcs = this.#toolCalls.value;
|
|
82
|
-
const idx = tcs.findIndex((tc) => tc.toolCallId === e.toolCallId);
|
|
83
|
-
if (idx !== -1) {
|
|
84
|
-
const updated = [...tcs];
|
|
85
|
-
const existing = updated[idx];
|
|
86
|
-
if (existing) updated[idx] = {
|
|
87
|
-
...existing,
|
|
88
|
-
status: "done",
|
|
89
|
-
result: e.result
|
|
90
|
-
};
|
|
91
|
-
this.#toolCalls.value = updated;
|
|
92
|
-
}
|
|
93
|
-
break;
|
|
94
|
-
}
|
|
95
|
-
case "tts_done":
|
|
96
|
-
this.#state.value = "listening";
|
|
97
|
-
break;
|
|
98
|
-
case "cancelled":
|
|
99
|
-
this.#generation++;
|
|
100
|
-
this.#voiceIO()?.flush();
|
|
101
|
-
this.#batch(() => {
|
|
102
|
-
this.#userUtterance.value = null;
|
|
103
|
-
this.#agentUtterance.value = null;
|
|
104
|
-
this.#state.value = "listening";
|
|
105
|
-
});
|
|
106
|
-
break;
|
|
107
|
-
case "reset":
|
|
108
|
-
this.#generation++;
|
|
109
|
-
this.#voiceIO()?.flush();
|
|
110
|
-
this.#batch(() => {
|
|
111
|
-
this.#messages.value = [];
|
|
112
|
-
this.#toolCalls.value = [];
|
|
113
|
-
this.#userUtterance.value = null;
|
|
114
|
-
this.#agentUtterance.value = null;
|
|
115
|
-
this.#error.value = null;
|
|
116
|
-
this.#state.value = "listening";
|
|
117
|
-
});
|
|
118
|
-
break;
|
|
119
|
-
case "error":
|
|
120
|
-
console.error("Agent error:", e.message);
|
|
121
|
-
this.#batch(() => {
|
|
122
|
-
this.#error.value = {
|
|
123
|
-
code: e.code,
|
|
124
|
-
message: e.message
|
|
125
|
-
};
|
|
126
|
-
this.#state.value = "error";
|
|
127
|
-
});
|
|
128
|
-
break;
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
playAudioChunk(chunk) {
|
|
132
|
-
if (this.#state.value === "error") return;
|
|
133
|
-
if (this.#state.value !== "speaking") this.#state.value = "speaking";
|
|
134
|
-
this.#voiceIO()?.enqueue(chunk.buffer);
|
|
135
|
-
}
|
|
136
|
-
playAudioDone() {
|
|
137
|
-
const gen = this.#generation;
|
|
138
|
-
const io = this.#voiceIO();
|
|
139
|
-
if (io) io.done().then(() => {
|
|
140
|
-
if (this.#generation !== gen) return;
|
|
141
|
-
this.#state.value = "listening";
|
|
142
|
-
}).catch(() => {});
|
|
143
|
-
else this.#state.value = "listening";
|
|
144
|
-
}
|
|
145
|
-
/**
|
|
146
|
-
* Dispatch an incoming WebSocket message (text or binary).
|
|
147
|
-
*
|
|
148
|
-
* Returns the parsed config if the message is a `config` message,
|
|
149
|
-
* otherwise `null`.
|
|
150
|
-
*/
|
|
151
|
-
handleMessage(data) {
|
|
152
|
-
if (data instanceof ArrayBuffer) {
|
|
153
|
-
this.playAudioChunk(new Uint8Array(data));
|
|
154
|
-
return null;
|
|
155
|
-
}
|
|
156
|
-
let msg;
|
|
157
|
-
try {
|
|
158
|
-
const parsed = ServerMessageSchema.safeParse(JSON.parse(data));
|
|
159
|
-
if (!parsed.success) return null;
|
|
160
|
-
msg = parsed.data;
|
|
161
|
-
} catch {
|
|
162
|
-
return null;
|
|
163
|
-
}
|
|
164
|
-
if (msg.type === "config") {
|
|
165
|
-
const { type: _, ...config } = msg;
|
|
166
|
-
return config;
|
|
167
|
-
}
|
|
168
|
-
if (msg.type === "audio_done") {
|
|
169
|
-
this.playAudioDone();
|
|
170
|
-
return null;
|
|
171
|
-
}
|
|
172
|
-
this.event(msg);
|
|
173
|
-
return null;
|
|
174
|
-
}
|
|
175
|
-
};
|
|
176
|
-
/**
|
|
177
|
-
* Create a voice session that connects to an AAI server via WebSocket.
|
|
178
|
-
*
|
|
179
|
-
* Uses plain JSON text frames and binary audio frames for communication.
|
|
180
|
-
*
|
|
181
|
-
* @param options - Session configuration including the platform server URL.
|
|
182
|
-
* @returns A {@link VoiceSession} handle for controlling the session.
|
|
183
|
-
*
|
|
184
|
-
* @public
|
|
185
|
-
*/
|
|
186
|
-
function createVoiceSession(options) {
|
|
187
|
-
const reactive = options.signal ?? plainReactive;
|
|
188
|
-
const batchFn = options.batch ?? plainBatch;
|
|
189
|
-
const state = reactive("disconnected");
|
|
190
|
-
const messages = reactive([]);
|
|
191
|
-
const toolCalls = reactive([]);
|
|
192
|
-
const userUtterance = reactive(null);
|
|
193
|
-
const agentUtterance = reactive(null);
|
|
194
|
-
const error = reactive(null);
|
|
195
|
-
const disconnected = reactive(null);
|
|
196
|
-
let ws = null;
|
|
197
|
-
let voiceIO = null;
|
|
198
|
-
let connectionController = null;
|
|
199
|
-
let hasConnected = false;
|
|
200
|
-
let audioSetupInFlight = false;
|
|
201
|
-
function cleanupAudio() {
|
|
202
|
-
audioSetupInFlight = false;
|
|
203
|
-
voiceIO?.close();
|
|
204
|
-
voiceIO = null;
|
|
205
|
-
}
|
|
206
|
-
function resetState() {
|
|
207
|
-
batchFn(() => {
|
|
208
|
-
messages.value = [];
|
|
209
|
-
toolCalls.value = [];
|
|
210
|
-
userUtterance.value = null;
|
|
211
|
-
agentUtterance.value = null;
|
|
212
|
-
error.value = null;
|
|
213
|
-
});
|
|
214
|
-
}
|
|
215
|
-
function send(msg) {
|
|
216
|
-
if (ws && ws.readyState === WebSocket.OPEN) ws.send(JSON.stringify(msg));
|
|
217
|
-
}
|
|
218
|
-
function sendBinary(data) {
|
|
219
|
-
if (ws && ws.readyState === WebSocket.OPEN) ws.send(data);
|
|
220
|
-
}
|
|
221
|
-
async function handleReady(msg) {
|
|
222
|
-
if (audioSetupInFlight) return;
|
|
223
|
-
audioSetupInFlight = true;
|
|
224
|
-
try {
|
|
225
|
-
const [{ createVoiceIO }, captureWorklet, playbackWorklet] = await Promise.all([
|
|
226
|
-
import("./audio.js"),
|
|
227
|
-
import("./worklets/capture-processor.js").then((m) => m.default),
|
|
228
|
-
import("./worklets/playback-processor.js").then((m) => m.default)
|
|
229
|
-
]);
|
|
230
|
-
const io = await createVoiceIO({
|
|
231
|
-
sttSampleRate: msg.sampleRate,
|
|
232
|
-
ttsSampleRate: msg.ttsSampleRate,
|
|
233
|
-
captureWorkletSrc: captureWorklet,
|
|
234
|
-
playbackWorkletSrc: playbackWorklet,
|
|
235
|
-
onMicData: (pcm16) => {
|
|
236
|
-
try {
|
|
237
|
-
sendBinary(pcm16);
|
|
238
|
-
} catch {}
|
|
239
|
-
}
|
|
240
|
-
});
|
|
241
|
-
if (!ws || ws.readyState !== WebSocket.OPEN) {
|
|
242
|
-
io.close();
|
|
243
|
-
return;
|
|
244
|
-
}
|
|
245
|
-
voiceIO = io;
|
|
246
|
-
send({ type: "audio_ready" });
|
|
247
|
-
state.value = "listening";
|
|
248
|
-
} catch (err) {
|
|
249
|
-
if (!ws || ws.readyState !== WebSocket.OPEN) return;
|
|
250
|
-
batchFn(() => {
|
|
251
|
-
error.value = {
|
|
252
|
-
code: "audio",
|
|
253
|
-
message: `Microphone access failed: ${errorMessage(err)}`
|
|
254
|
-
};
|
|
255
|
-
state.value = "error";
|
|
256
|
-
});
|
|
257
|
-
} finally {
|
|
258
|
-
audioSetupInFlight = false;
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
function connect(opts) {
|
|
262
|
-
disconnected.value = null;
|
|
263
|
-
state.value = "connecting";
|
|
264
|
-
connectionController?.abort();
|
|
265
|
-
cleanupAudio();
|
|
266
|
-
ws?.close();
|
|
267
|
-
ws = null;
|
|
268
|
-
const controller = new AbortController();
|
|
269
|
-
connectionController = controller;
|
|
270
|
-
const { signal: sig } = controller;
|
|
271
|
-
if (opts?.signal) opts.signal.addEventListener("abort", () => disconnect(), { signal: sig });
|
|
272
|
-
const base = options.platformUrl;
|
|
273
|
-
const wsUrl = new URL("websocket", base.endsWith("/") ? base : `${base}/`);
|
|
274
|
-
wsUrl.protocol = wsUrl.protocol === "https:" ? "wss:" : "ws:";
|
|
275
|
-
if (hasConnected) wsUrl.searchParams.set("resume", "1");
|
|
276
|
-
const socket = new WebSocket(wsUrl.toString());
|
|
277
|
-
socket.binaryType = "arraybuffer";
|
|
278
|
-
ws = socket;
|
|
279
|
-
const handler = new ClientHandler({
|
|
280
|
-
state,
|
|
281
|
-
messages,
|
|
282
|
-
toolCalls,
|
|
283
|
-
userUtterance,
|
|
284
|
-
agentUtterance,
|
|
285
|
-
error,
|
|
286
|
-
voiceIO: () => voiceIO,
|
|
287
|
-
batch: batchFn
|
|
288
|
-
});
|
|
289
|
-
socket.addEventListener("open", () => {
|
|
290
|
-
state.value = "ready";
|
|
291
|
-
}, { signal: sig });
|
|
292
|
-
socket.addEventListener("message", (event) => {
|
|
293
|
-
const msgEvent = event;
|
|
294
|
-
const config = handler.handleMessage(msgEvent.data);
|
|
295
|
-
if (config) {
|
|
296
|
-
const isReconnect = hasConnected;
|
|
297
|
-
hasConnected = true;
|
|
298
|
-
handleReady(config);
|
|
299
|
-
if (isReconnect && messages.value.length > 0) send({
|
|
300
|
-
type: "history",
|
|
301
|
-
messages: messages.value.map((m) => ({
|
|
302
|
-
role: m.role,
|
|
303
|
-
text: m.content
|
|
304
|
-
}))
|
|
305
|
-
});
|
|
306
|
-
}
|
|
307
|
-
}, { signal: sig });
|
|
308
|
-
socket.addEventListener("close", () => {
|
|
309
|
-
if (sig.aborted) return;
|
|
310
|
-
controller.abort();
|
|
311
|
-
disconnected.value = { intentional: false };
|
|
312
|
-
cleanupAudio();
|
|
313
|
-
state.value = "disconnected";
|
|
314
|
-
}, { signal: sig });
|
|
315
|
-
}
|
|
316
|
-
function cancel() {
|
|
317
|
-
voiceIO?.flush();
|
|
318
|
-
state.value = "listening";
|
|
319
|
-
send({ type: "cancel" });
|
|
320
|
-
}
|
|
321
|
-
function reset() {
|
|
322
|
-
voiceIO?.flush();
|
|
323
|
-
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
324
|
-
send({ type: "reset" });
|
|
325
|
-
return;
|
|
326
|
-
}
|
|
327
|
-
resetState();
|
|
328
|
-
disconnect();
|
|
329
|
-
connect();
|
|
330
|
-
}
|
|
331
|
-
function disconnect() {
|
|
332
|
-
connectionController?.abort();
|
|
333
|
-
connectionController = null;
|
|
334
|
-
cleanupAudio();
|
|
335
|
-
ws?.close();
|
|
336
|
-
ws = null;
|
|
337
|
-
state.value = "disconnected";
|
|
338
|
-
disconnected.value = { intentional: true };
|
|
339
|
-
}
|
|
340
|
-
return {
|
|
341
|
-
state,
|
|
342
|
-
messages,
|
|
343
|
-
toolCalls,
|
|
344
|
-
userUtterance,
|
|
345
|
-
agentUtterance,
|
|
346
|
-
error,
|
|
347
|
-
disconnected,
|
|
348
|
-
connect,
|
|
349
|
-
cancel,
|
|
350
|
-
resetState,
|
|
351
|
-
reset,
|
|
352
|
-
disconnect,
|
|
353
|
-
[Symbol.dispose]() {
|
|
354
|
-
disconnect();
|
|
355
|
-
}
|
|
356
|
-
};
|
|
357
|
-
}
|
|
358
|
-
//#endregion
|
|
1
|
+
import { n as ClientHandler, t as createVoiceSession } from "./session-D0RdWlWH.js";
|
|
359
2
|
export { ClientHandler, createVoiceSession };
|
package/dist/signals.d.ts
CHANGED
|
@@ -73,12 +73,60 @@ export declare function useSession(): SessionSignals;
|
|
|
73
73
|
*
|
|
74
74
|
* Automatically resets tracking when the session is reset (toolCalls cleared).
|
|
75
75
|
*
|
|
76
|
+
* @example
|
|
77
|
+
* ```tsx
|
|
78
|
+
* // Filter by tool name with a typed result:
|
|
79
|
+
* useToolResult<Recipe>("get_recipe", (result, toolCall) => {
|
|
80
|
+
* setRecipe(result);
|
|
81
|
+
* });
|
|
82
|
+
*
|
|
83
|
+
* // Receive all tool results (untyped):
|
|
84
|
+
* useToolResult((toolName, result, toolCall) => {
|
|
85
|
+
* console.log(toolName, result);
|
|
86
|
+
* });
|
|
87
|
+
* ```
|
|
88
|
+
*
|
|
89
|
+
* @public
|
|
90
|
+
*/
|
|
91
|
+
export declare function useToolResult<R = unknown>(toolName: string, callback: (result: R, toolCall: ToolCallInfo) => void): void;
|
|
92
|
+
/**
|
|
93
|
+
* Hook that fires a callback exactly once for each newly completed tool call.
|
|
94
|
+
*
|
|
76
95
|
* @param callback - Called once per completed tool call with the tool name,
|
|
77
96
|
* parsed result, and full {@link ToolCallInfo}.
|
|
78
97
|
*
|
|
79
98
|
* @public
|
|
80
99
|
*/
|
|
81
100
|
export declare function useToolResult(callback: (toolName: string, result: unknown, toolCall: ToolCallInfo) => void): void;
|
|
101
|
+
/**
|
|
102
|
+
* Hook that fires a callback when a new tool call starts (status: "pending").
|
|
103
|
+
*
|
|
104
|
+
* Use this to show a loading/skeleton UI immediately when the agent invokes
|
|
105
|
+
* a tool, rather than waiting for the full result. The callback receives the
|
|
106
|
+
* tool name, parsed arguments, and the full {@link ToolCallInfo}.
|
|
107
|
+
*
|
|
108
|
+
* @param callback - Called once per new tool call at start time.
|
|
109
|
+
*
|
|
110
|
+
* @public
|
|
111
|
+
*/
|
|
112
|
+
export declare function useToolCallStart(callback: (toolName: string, args: Record<string, unknown>, toolCall: ToolCallInfo) => void): void;
|
|
113
|
+
/**
|
|
114
|
+
* Hook that fires a callback for each intermediate update pushed by a tool
|
|
115
|
+
* via `ctx.sendUpdate()`.
|
|
116
|
+
*
|
|
117
|
+
* Use this to render progressive/streaming data from a tool before it
|
|
118
|
+
* finishes — e.g. showing a recipe preview card while nutrition data is
|
|
119
|
+
* still loading.
|
|
120
|
+
*
|
|
121
|
+
* The callback fires once per update. The `data` argument is the parsed
|
|
122
|
+
* JSON update (or raw string if parsing fails).
|
|
123
|
+
*
|
|
124
|
+
* @param callback - Called once per intermediate update with the tool name,
|
|
125
|
+
* parsed data, and full {@link ToolCallInfo}.
|
|
126
|
+
*
|
|
127
|
+
* @public
|
|
128
|
+
*/
|
|
129
|
+
export declare function useToolCallUpdate(callback: (toolName: string, data: unknown, toolCall: ToolCallInfo) => void): void;
|
|
82
130
|
/**
|
|
83
131
|
* Auto-scroll a container to the bottom when messages, tool calls,
|
|
84
132
|
* or utterances change. Returns a ref to attach to a sentinel `<div>`
|
package/dist/signals.js
CHANGED
|
@@ -80,40 +80,99 @@ function tryParseJSON(str) {
|
|
|
80
80
|
return str;
|
|
81
81
|
}
|
|
82
82
|
}
|
|
83
|
+
function isNewCompletedCall(tc, seen, filterName) {
|
|
84
|
+
if (tc.status !== "done" || !tc.result) return false;
|
|
85
|
+
if (seen.has(tc.toolCallId)) return false;
|
|
86
|
+
if (filterName && tc.toolName !== filterName) return false;
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
89
|
+
function useToolResult(toolNameOrCallback, maybeCallback) {
|
|
90
|
+
const filterName = typeof toolNameOrCallback === "string" ? toolNameOrCallback : void 0;
|
|
91
|
+
const callback = typeof toolNameOrCallback === "function" ? toolNameOrCallback : (_name, result, tc) => maybeCallback?.(result, tc);
|
|
92
|
+
const { session } = useSession();
|
|
93
|
+
const seenRef = useRef(/* @__PURE__ */ new Set());
|
|
94
|
+
const cbRef = useRef(callback);
|
|
95
|
+
cbRef.current = callback;
|
|
96
|
+
useEffect(() => effect(() => {
|
|
97
|
+
const toolCalls = session.toolCalls.value;
|
|
98
|
+
if (toolCalls.length === 0) {
|
|
99
|
+
seenRef.current.clear();
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
for (const tc of toolCalls) {
|
|
103
|
+
if (!isNewCompletedCall(tc, seenRef.current, filterName)) continue;
|
|
104
|
+
seenRef.current.add(tc.toolCallId);
|
|
105
|
+
cbRef.current(tc.toolName, tryParseJSON(tc.result), tc);
|
|
106
|
+
}
|
|
107
|
+
}), [session]);
|
|
108
|
+
}
|
|
83
109
|
/**
|
|
84
|
-
* Hook that fires a callback
|
|
85
|
-
*
|
|
86
|
-
* Handles deduplication internally — safe to use with `useState` setters
|
|
87
|
-
* without worrying about duplicates. The `result` argument is the parsed
|
|
88
|
-
* JSON result from the tool (or the raw string if parsing fails).
|
|
110
|
+
* Hook that fires a callback when a new tool call starts (status: "pending").
|
|
89
111
|
*
|
|
90
|
-
*
|
|
112
|
+
* Use this to show a loading/skeleton UI immediately when the agent invokes
|
|
113
|
+
* a tool, rather than waiting for the full result. The callback receives the
|
|
114
|
+
* tool name, parsed arguments, and the full {@link ToolCallInfo}.
|
|
91
115
|
*
|
|
92
|
-
* @param callback - Called once per
|
|
93
|
-
* parsed result, and full {@link ToolCallInfo}.
|
|
116
|
+
* @param callback - Called once per new tool call at start time.
|
|
94
117
|
*
|
|
95
118
|
* @public
|
|
96
119
|
*/
|
|
97
|
-
function
|
|
120
|
+
function useToolCallStart(callback) {
|
|
98
121
|
const { session } = useSession();
|
|
99
122
|
const seenRef = useRef(/* @__PURE__ */ new Set());
|
|
100
123
|
const cbRef = useRef(callback);
|
|
101
124
|
cbRef.current = callback;
|
|
102
|
-
useEffect(() => {
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
125
|
+
useEffect(() => effect(() => {
|
|
126
|
+
const toolCalls = session.toolCalls.value;
|
|
127
|
+
if (toolCalls.length === 0) {
|
|
128
|
+
seenRef.current.clear();
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
for (const tc of toolCalls) {
|
|
132
|
+
if (seenRef.current.has(tc.toolCallId)) continue;
|
|
133
|
+
seenRef.current.add(tc.toolCallId);
|
|
134
|
+
cbRef.current(tc.toolName, tc.args, tc);
|
|
135
|
+
}
|
|
136
|
+
}), [session]);
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Hook that fires a callback for each intermediate update pushed by a tool
|
|
140
|
+
* via `ctx.sendUpdate()`.
|
|
141
|
+
*
|
|
142
|
+
* Use this to render progressive/streaming data from a tool before it
|
|
143
|
+
* finishes — e.g. showing a recipe preview card while nutrition data is
|
|
144
|
+
* still loading.
|
|
145
|
+
*
|
|
146
|
+
* The callback fires once per update. The `data` argument is the parsed
|
|
147
|
+
* JSON update (or raw string if parsing fails).
|
|
148
|
+
*
|
|
149
|
+
* @param callback - Called once per intermediate update with the tool name,
|
|
150
|
+
* parsed data, and full {@link ToolCallInfo}.
|
|
151
|
+
*
|
|
152
|
+
* @public
|
|
153
|
+
*/
|
|
154
|
+
function useToolCallUpdate(callback) {
|
|
155
|
+
const { session } = useSession();
|
|
156
|
+
const countRef = useRef(/* @__PURE__ */ new Map());
|
|
157
|
+
const cbRef = useRef(callback);
|
|
158
|
+
cbRef.current = callback;
|
|
159
|
+
useEffect(() => effect(() => {
|
|
160
|
+
const toolCalls = session.toolCalls.value;
|
|
161
|
+
if (toolCalls.length === 0) {
|
|
162
|
+
countRef.current.clear();
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
for (const tc of toolCalls) processNewUpdates(tc, countRef.current, cbRef.current);
|
|
166
|
+
}), [session]);
|
|
167
|
+
}
|
|
168
|
+
function processNewUpdates(tc, seenCounts, cb) {
|
|
169
|
+
const seen = seenCounts.get(tc.toolCallId) ?? 0;
|
|
170
|
+
if (tc.updates.length <= seen) return;
|
|
171
|
+
for (let i = seen; i < tc.updates.length; i++) {
|
|
172
|
+
const raw = tc.updates[i];
|
|
173
|
+
if (raw !== void 0) cb(tc.toolName, tryParseJSON(raw), tc);
|
|
174
|
+
}
|
|
175
|
+
seenCounts.set(tc.toolCallId, tc.updates.length);
|
|
117
176
|
}
|
|
118
177
|
/**
|
|
119
178
|
* Auto-scroll a container to the bottom when messages, tool calls,
|
|
@@ -135,4 +194,4 @@ function useAutoScroll() {
|
|
|
135
194
|
return ref;
|
|
136
195
|
}
|
|
137
196
|
//#endregion
|
|
138
|
-
export { SessionProvider, createSessionControls, useAutoScroll, useSession, useToolResult };
|
|
197
|
+
export { SessionProvider, createSessionControls, useAutoScroll, useSession, useToolCallStart, useToolCallUpdate, useToolResult };
|
package/dist/types.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { SessionErrorCode } from "@alexkroman1/aai/protocol";
|
|
1
2
|
/** Microphone buffer duration in seconds before sending to the server. */
|
|
2
3
|
export declare const MIC_BUFFER_SECONDS = 0.1;
|
|
3
4
|
/**
|
|
@@ -11,7 +12,7 @@ export type AgentState = "disconnected" | "connecting" | "ready" | "listening" |
|
|
|
11
12
|
*
|
|
12
13
|
* @public
|
|
13
14
|
*/
|
|
14
|
-
export type
|
|
15
|
+
export type ChatMessage = {
|
|
15
16
|
/** The sender of the message. */
|
|
16
17
|
role: "user" | "assistant";
|
|
17
18
|
/** The text content of the message. */
|
|
@@ -28,11 +29,12 @@ export type ToolCallInfo = {
|
|
|
28
29
|
args: Record<string, unknown>;
|
|
29
30
|
status: "pending" | "done";
|
|
30
31
|
result?: string | undefined;
|
|
32
|
+
/** Intermediate updates pushed by the tool via `ctx.sendUpdate()`. */
|
|
33
|
+
updates: string[];
|
|
31
34
|
/** Index in the messages array where this tool call should appear. */
|
|
32
35
|
afterMessageIndex: number;
|
|
33
36
|
};
|
|
34
|
-
|
|
35
|
-
export type { SessionErrorCode };
|
|
37
|
+
export type { SessionErrorCode } from "@alexkroman1/aai/protocol";
|
|
36
38
|
/**
|
|
37
39
|
* Error reported by the voice session.
|
|
38
40
|
*
|
|
@@ -57,7 +59,7 @@ export type Reactive<T> = {
|
|
|
57
59
|
*
|
|
58
60
|
* @public
|
|
59
61
|
*/
|
|
60
|
-
export type
|
|
62
|
+
export type VoiceSessionOptions = {
|
|
61
63
|
/** Base URL of the AAI platform server. */
|
|
62
64
|
platformUrl: string;
|
|
63
65
|
/**
|
|
@@ -65,11 +67,23 @@ export type SessionOptions = {
|
|
|
65
67
|
* Defaults to a plain mutable wrapper. Pass `signal` from
|
|
66
68
|
* `@preact/signals` for automatic Preact component re-rendering.
|
|
67
69
|
*/
|
|
68
|
-
|
|
70
|
+
reactiveFactory?: <T>(initial: T) => Reactive<T>;
|
|
69
71
|
/**
|
|
70
72
|
* Function to batch multiple reactive updates.
|
|
71
73
|
* Defaults to calling the function directly. Pass `batch` from
|
|
72
74
|
* `@preact/signals` for optimized batched updates.
|
|
73
75
|
*/
|
|
74
76
|
batch?: (fn: () => void) => void;
|
|
77
|
+
/**
|
|
78
|
+
* Called when the server sends a session ID in the config message.
|
|
79
|
+
* Use this to store the ID (e.g. in localStorage) for reconnection
|
|
80
|
+
* via `resumeSessionId`.
|
|
81
|
+
*/
|
|
82
|
+
onSessionId?: ((sessionId: string) => void) | undefined;
|
|
83
|
+
/**
|
|
84
|
+
* Session ID from a previous connection. When set, the server will
|
|
85
|
+
* attempt to restore persisted session state (if the agent has
|
|
86
|
+
* `persistence` enabled).
|
|
87
|
+
*/
|
|
88
|
+
resumeSessionId?: string | undefined;
|
|
75
89
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alexkroman1/aai-ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"files": [
|
|
6
6
|
"dist",
|
|
@@ -17,16 +17,11 @@
|
|
|
17
17
|
"source": "./session.ts",
|
|
18
18
|
"types": "./dist/session.d.ts",
|
|
19
19
|
"import": "./dist/session.js"
|
|
20
|
-
},
|
|
21
|
-
"./components": {
|
|
22
|
-
"source": "./components.ts",
|
|
23
|
-
"types": "./dist/components.d.ts",
|
|
24
|
-
"import": "./dist/components.js"
|
|
25
20
|
}
|
|
26
21
|
},
|
|
27
22
|
"dependencies": {
|
|
28
23
|
"clsx": "^2.1.1",
|
|
29
|
-
"@alexkroman1/aai": "0.
|
|
24
|
+
"@alexkroman1/aai": "0.10.0"
|
|
30
25
|
},
|
|
31
26
|
"peerDependencies": {
|
|
32
27
|
"@preact/signals": "^2.8.2",
|
|
@@ -43,14 +38,21 @@
|
|
|
43
38
|
"@testing-library/preact": "^3.2.4",
|
|
44
39
|
"jsdom": "^29.0.1",
|
|
45
40
|
"preact": "^10.29.0",
|
|
46
|
-
"tsdown": "^0.21.
|
|
41
|
+
"tsdown": "^0.21.5"
|
|
47
42
|
},
|
|
48
43
|
"engines": {
|
|
49
|
-
"node": ">=22"
|
|
44
|
+
"node": ">=22.6"
|
|
45
|
+
},
|
|
46
|
+
"repository": {
|
|
47
|
+
"type": "git",
|
|
48
|
+
"url": "https://github.com/alexkroman/agent.git",
|
|
49
|
+
"directory": "packages/aai-ui"
|
|
50
50
|
},
|
|
51
51
|
"scripts": {
|
|
52
52
|
"build": "tsdown && tsc -p tsconfig.build.json",
|
|
53
53
|
"typecheck": "tsc --noEmit",
|
|
54
|
-
"lint": "biome check ."
|
|
54
|
+
"lint": "biome check .",
|
|
55
|
+
"check:api": "api-extractor run -c api-extractor.json",
|
|
56
|
+
"check:attw": "attw --pack --profile esm-only --entrypoints . ./session"
|
|
55
57
|
}
|
|
56
58
|
}
|
package/styles.css
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
@import url("https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&family=IBM+Plex+Mono:wght@400;500&display=swap");
|
|
2
1
|
@import "tailwindcss";
|
|
3
2
|
@source "./";
|
|
4
3
|
@source "../dist/ui/";
|
|
@@ -24,8 +23,8 @@
|
|
|
24
23
|
--color-aai-state-speaking: #e06c75;
|
|
25
24
|
--color-aai-state-error: #e06c75;
|
|
26
25
|
--radius-aai: 6px;
|
|
27
|
-
--font-aai:
|
|
28
|
-
--font-aai-mono: "
|
|
26
|
+
--font-aai: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
|
|
27
|
+
--font-aai-mono: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
|
|
29
28
|
}
|
|
30
29
|
|
|
31
30
|
@layer base {
|