@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.
- package/dist/_react-test-utils.d.ts +86 -0
- package/dist/audio.js +4 -5
- package/dist/build-default-client.d.ts +1 -0
- package/dist/{_components → components}/button.d.ts +4 -4
- package/dist/{_components → components}/button.js +28 -5
- package/dist/components/chat-view.d.ts +38 -0
- package/dist/components/chat-view.js +102 -0
- package/dist/{_components → components}/controls.d.ts +1 -1
- package/dist/{_components → components}/controls.js +8 -5
- package/dist/{_components → components}/message-list.d.ts +2 -2
- package/dist/components/message-list.js +149 -0
- package/dist/components/sidebar-layout.d.ts +21 -0
- package/dist/components/sidebar-layout.js +43 -0
- package/dist/{_components → components}/start-screen.d.ts +4 -4
- package/dist/{_components → components}/start-screen.js +17 -8
- package/dist/{_components → components}/tool-call-block.d.ts +5 -6
- package/dist/components/tool-call-block.js +3 -0
- package/dist/components/tool-config-context.d.ts +22 -0
- package/dist/context.d.ts +23 -0
- package/dist/context.js +39 -0
- package/dist/default-client/assets/audio-DH9LpexG.js +1 -0
- package/dist/default-client/assets/capture-processor-C26lSiVr.js +53 -0
- package/dist/default-client/assets/default-client-AeJhjHfj.js +49 -0
- package/dist/default-client/assets/default-client-BK1ItCvw.css +2 -0
- package/dist/default-client/assets/playback-processor-dSA8Im99.js +101 -0
- package/dist/default-client/default-client.html +15 -0
- package/dist/default-client.d.ts +2 -0
- package/dist/define-client.d.ts +72 -24
- package/dist/define-client.js +71 -45
- package/dist/hooks.d.ts +5 -0
- package/dist/hooks.js +55 -0
- package/dist/index.d.ts +15 -37
- package/dist/index.js +11 -18
- package/dist/session-core.d.ts +70 -0
- package/dist/session-core.js +437 -0
- package/dist/tool-call-block-CCuChsFm.js +131 -0
- package/dist/types.d.ts +19 -22
- package/package.json +24 -18
- package/styles.css +3 -33
- package/dist/_components/app.d.ts +0 -23
- package/dist/_components/app.js +0 -41
- package/dist/_components/chat-view.d.ts +0 -29
- package/dist/_components/chat-view.js +0 -61
- package/dist/_components/error-banner.d.ts +0 -21
- package/dist/_components/error-banner.js +0 -27
- package/dist/_components/message-bubble.d.ts +0 -23
- package/dist/_components/message-bubble.js +0 -35
- package/dist/_components/message-list.js +0 -76
- package/dist/_components/sidebar-layout.d.ts +0 -21
- package/dist/_components/sidebar-layout.js +0 -36
- package/dist/_components/state-indicator.d.ts +0 -29
- package/dist/_components/state-indicator.js +0 -37
- package/dist/_components/thinking-indicator.d.ts +0 -16
- package/dist/_components/thinking-indicator.js +0 -34
- package/dist/_components/tool-call-block.js +0 -118
- package/dist/_components/tool-icons.d.ts +0 -17
- package/dist/_components/tool-icons.js +0 -128
- package/dist/_components/transcript.d.ts +0 -25
- package/dist/_components/transcript.js +0 -35
- package/dist/client-context.d.ts +0 -33
- package/dist/client-context.js +0 -15
- package/dist/client-handler.d.ts +0 -43
- package/dist/session-CWB7vmqz.js +0 -429
- package/dist/session.d.ts +0 -69
- package/dist/session.js +0 -2
- package/dist/signals.d.ts +0 -120
- package/dist/signals.js +0 -161
- 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
|
-
|
|
28
|
-
|
|
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.
|
|
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
|
-
"./
|
|
17
|
-
|
|
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.
|
|
21
|
+
"@alexkroman1/aai": "1.0.2"
|
|
25
22
|
},
|
|
26
23
|
"peerDependencies": {
|
|
27
|
-
"
|
|
28
|
-
"
|
|
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
|
-
"@
|
|
38
|
-
"@testing-library/
|
|
39
|
-
"
|
|
40
|
-
"
|
|
41
|
-
"
|
|
42
|
-
"
|
|
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": ">=
|
|
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
|
-
"
|
|
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 .
|
|
63
|
+
"check:attw": "attw --pack --profile esm-only --entrypoints ."
|
|
58
64
|
}
|
|
59
65
|
}
|