@dudousxd/nestjs-agent-react 0.1.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/index.cjs ADDED
@@ -0,0 +1,1168 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
9
+ var __export = (target, all) => {
10
+ for (var name in all)
11
+ __defProp(target, name, { get: all[name], enumerable: true });
12
+ };
13
+ var __copyProps = (to, from, except, desc) => {
14
+ if (from && typeof from === "object" || typeof from === "function") {
15
+ for (let key of __getOwnPropNames(from))
16
+ if (!__hasOwnProp.call(to, key) && key !== except)
17
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
18
+ }
19
+ return to;
20
+ };
21
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
22
+ // If the importer is in node compatibility mode or this is not an ESM
23
+ // file that has been converted to a CommonJS file using a Babel-
24
+ // compatible transform (i.e. "__esModule" has not been set), then set
25
+ // "default" to the CommonJS "module.exports" for node compatibility.
26
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
27
+ mod
28
+ ));
29
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
30
+
31
+ // src/index.ts
32
+ var src_exports = {};
33
+ __export(src_exports, {
34
+ AgentChatTransport: () => AgentChatTransport,
35
+ AgentClient: () => AgentClient,
36
+ AgentHttpError: () => AgentHttpError,
37
+ ChatInput: () => ChatInput,
38
+ DEFAULT_SPEECH_LANG: () => DEFAULT_SPEECH_LANG,
39
+ MessageItem: () => MessageItem,
40
+ MessageList: () => MessageList,
41
+ SPEECH_LANGUAGES: () => SPEECH_LANGUAGES,
42
+ formatRelativeTime: () => formatRelativeTime,
43
+ loadStoredSpeechLang: () => loadStoredSpeechLang,
44
+ persistSpeechLang: () => persistSpeechLang,
45
+ useAgentChat: () => useAgentChat,
46
+ useSpeechRecognition: () => useSpeechRecognition
47
+ });
48
+ module.exports = __toCommonJS(src_exports);
49
+
50
+ // src/agent-chat-transport.ts
51
+ var HEADER_RUN_ID = "x-agent-run-id";
52
+ var HEADER_THREAD_ID = "x-agent-thread-id";
53
+ var AgentChatTransport = class {
54
+ static {
55
+ __name(this, "AgentChatTransport");
56
+ }
57
+ options;
58
+ currentRunId;
59
+ currentThreadId;
60
+ constructor(options = {}) {
61
+ this.options = options;
62
+ }
63
+ /** Run id of the most recent stream — HITL approve/reject target this. */
64
+ get runId() {
65
+ return this.currentRunId;
66
+ }
67
+ /** Thread id of the most recent stream. */
68
+ get threadId() {
69
+ return this.currentThreadId;
70
+ }
71
+ async sendMessages(options) {
72
+ const lastMessage = options.messages.at(-1);
73
+ const message = lastMessage ? extractText(lastMessage) : "";
74
+ const body = {
75
+ ...this.options.agent !== void 0 ? {
76
+ agent: this.options.agent
77
+ } : {},
78
+ ...this.options.getBody?.() ?? {},
79
+ ...options.body ?? {},
80
+ message
81
+ };
82
+ const response = await this.fetchImpl()(`${this.baseUrl()}/agent/chat`, {
83
+ method: "POST",
84
+ headers: {
85
+ "content-type": "application/json",
86
+ accept: "text/event-stream",
87
+ ...await this.resolveHeaders(options.headers)
88
+ },
89
+ body: JSON.stringify(body),
90
+ ...this.options.credentials !== void 0 ? {
91
+ credentials: this.options.credentials
92
+ } : {},
93
+ ...options.abortSignal ? {
94
+ signal: options.abortSignal
95
+ } : {}
96
+ });
97
+ if (!response.ok || !response.body) {
98
+ throw new Error(`Agent chat request failed: ${response.status} ${response.statusText}`);
99
+ }
100
+ this.captureHeaderMeta(response.headers);
101
+ return this.toChunkStream(response.body);
102
+ }
103
+ async reconnectToStream(options) {
104
+ const runId = this.options.getResumeRunId?.();
105
+ if (runId === void 0) return null;
106
+ const response = await this.fetchImpl()(`${this.baseUrl()}/agent/chat/${encodeURIComponent(runId)}/stream`, {
107
+ method: "GET",
108
+ headers: {
109
+ accept: "text/event-stream",
110
+ ...await this.resolveHeaders(options.headers)
111
+ },
112
+ ...this.options.credentials !== void 0 ? {
113
+ credentials: this.options.credentials
114
+ } : {}
115
+ });
116
+ if (response.status === 404) return null;
117
+ if (!response.ok || !response.body) {
118
+ throw new Error(`Agent stream reconnect failed: ${response.status} ${response.statusText}`);
119
+ }
120
+ this.captureHeaderMeta(response.headers);
121
+ return this.toChunkStream(response.body);
122
+ }
123
+ fetchImpl() {
124
+ return this.options.fetch ?? globalThis.fetch;
125
+ }
126
+ baseUrl() {
127
+ return (this.options.baseUrl ?? "").replace(/\/$/, "");
128
+ }
129
+ async resolveHeaders(perRequest) {
130
+ const dynamic = await this.options.getHeaders?.() ?? {};
131
+ const extra = perRequest instanceof Headers ? Object.fromEntries(perRequest.entries()) : perRequest ?? {};
132
+ return {
133
+ ...this.options.headers,
134
+ ...dynamic,
135
+ ...extra
136
+ };
137
+ }
138
+ captureHeaderMeta(headers) {
139
+ const runId = headers.get(HEADER_RUN_ID);
140
+ const threadId = headers.get(HEADER_THREAD_ID);
141
+ if (runId) this.currentRunId = runId;
142
+ if (threadId) this.currentThreadId = threadId;
143
+ }
144
+ recordMeta(meta) {
145
+ this.currentRunId = meta.runId;
146
+ this.currentThreadId = meta.threadId;
147
+ this.options.onMeta?.(meta);
148
+ }
149
+ /**
150
+ * Parse the backend's SSE byte stream and synthesize a valid v7
151
+ * UI-message chunk stream. Recognized backend frames:
152
+ * - `event: meta` `data: {"runId","threadId"}` → records identity
153
+ * - `data: {"delta":"..."}` → `text-delta`
154
+ * - `event: done` `data: {}` → terminates
155
+ */
156
+ toChunkStream(source) {
157
+ const reader = source.getReader();
158
+ const decoder = new TextDecoder();
159
+ const textId = `txt-${Math.random().toString(36).slice(2)}`;
160
+ let buffer = "";
161
+ let started = false;
162
+ const record = /* @__PURE__ */ __name((meta) => this.recordMeta(meta), "record");
163
+ return new ReadableStream({
164
+ async pull(controller) {
165
+ function ensureStarted() {
166
+ if (started) return;
167
+ started = true;
168
+ controller.enqueue({
169
+ type: "start"
170
+ });
171
+ controller.enqueue({
172
+ type: "start-step"
173
+ });
174
+ controller.enqueue({
175
+ type: "text-start",
176
+ id: textId
177
+ });
178
+ }
179
+ __name(ensureStarted, "ensureStarted");
180
+ function finish() {
181
+ ensureStarted();
182
+ controller.enqueue({
183
+ type: "text-end",
184
+ id: textId
185
+ });
186
+ controller.enqueue({
187
+ type: "finish-step"
188
+ });
189
+ controller.enqueue({
190
+ type: "finish"
191
+ });
192
+ controller.close();
193
+ }
194
+ __name(finish, "finish");
195
+ try {
196
+ while (true) {
197
+ const { done, value } = await reader.read();
198
+ if (done) {
199
+ finish();
200
+ return;
201
+ }
202
+ buffer += decoder.decode(value, {
203
+ stream: true
204
+ });
205
+ let separator = buffer.indexOf("\n\n");
206
+ while (separator !== -1) {
207
+ const rawEvent = buffer.slice(0, separator);
208
+ buffer = buffer.slice(separator + 2);
209
+ const frame = parseSseFrame(rawEvent);
210
+ if (frame.event === "done") {
211
+ finish();
212
+ return;
213
+ }
214
+ if (frame.event === "error") {
215
+ ensureStarted();
216
+ controller.enqueue({
217
+ type: "error",
218
+ errorText: parseErrorText(frame.data)
219
+ });
220
+ controller.close();
221
+ return;
222
+ }
223
+ if (frame.event === "meta") {
224
+ const meta = parseMeta(frame.data);
225
+ if (meta) record(meta);
226
+ } else if (frame.data) {
227
+ const delta = parseDelta(frame.data);
228
+ if (delta) {
229
+ ensureStarted();
230
+ controller.enqueue({
231
+ type: "text-delta",
232
+ id: textId,
233
+ delta
234
+ });
235
+ }
236
+ }
237
+ separator = buffer.indexOf("\n\n");
238
+ }
239
+ }
240
+ } catch (error) {
241
+ controller.enqueue({
242
+ type: "error",
243
+ errorText: error instanceof Error ? error.message : "Agent stream error"
244
+ });
245
+ controller.close();
246
+ }
247
+ },
248
+ cancel() {
249
+ void reader.cancel();
250
+ }
251
+ });
252
+ }
253
+ };
254
+ function parseSseFrame(raw) {
255
+ let event;
256
+ const dataLines = [];
257
+ for (const line of raw.split("\n")) {
258
+ if (line.startsWith("event:")) {
259
+ event = line.slice("event:".length).trim();
260
+ } else if (line.startsWith("data:")) {
261
+ dataLines.push(line.slice("data:".length).trimStart());
262
+ }
263
+ }
264
+ return {
265
+ event,
266
+ data: dataLines.length > 0 ? dataLines.join("\n") : void 0
267
+ };
268
+ }
269
+ __name(parseSseFrame, "parseSseFrame");
270
+ function parseMeta(data) {
271
+ if (!data) return null;
272
+ try {
273
+ const parsed = JSON.parse(data);
274
+ if (parsed !== null && typeof parsed === "object" && "runId" in parsed && "threadId" in parsed && typeof parsed.runId === "string" && typeof parsed.threadId === "string") {
275
+ return {
276
+ runId: parsed.runId,
277
+ threadId: parsed.threadId
278
+ };
279
+ }
280
+ } catch {
281
+ }
282
+ return null;
283
+ }
284
+ __name(parseMeta, "parseMeta");
285
+ function parseErrorText(data) {
286
+ if (!data) return "Agent run failed";
287
+ try {
288
+ const parsed = JSON.parse(data);
289
+ if (parsed !== null && typeof parsed === "object" && "message" in parsed && typeof parsed.message === "string") {
290
+ return parsed.message;
291
+ }
292
+ } catch {
293
+ }
294
+ return "Agent run failed";
295
+ }
296
+ __name(parseErrorText, "parseErrorText");
297
+ function parseDelta(data) {
298
+ try {
299
+ const parsed = JSON.parse(data);
300
+ if (parsed !== null && typeof parsed === "object" && "delta" in parsed && typeof parsed.delta === "string") {
301
+ return parsed.delta;
302
+ }
303
+ } catch {
304
+ }
305
+ return null;
306
+ }
307
+ __name(parseDelta, "parseDelta");
308
+ function extractText(message) {
309
+ let text = "";
310
+ for (const part of message.parts ?? []) {
311
+ if (part.type === "text") text += part.text;
312
+ }
313
+ return text;
314
+ }
315
+ __name(extractText, "extractText");
316
+
317
+ // src/client.ts
318
+ var AgentHttpError = class extends Error {
319
+ static {
320
+ __name(this, "AgentHttpError");
321
+ }
322
+ status;
323
+ method;
324
+ path;
325
+ constructor(status, method, path, statusText) {
326
+ super(`Agent request failed: ${method} ${path} \u2192 ${status} ${statusText}`), this.status = status, this.method = method, this.path = path;
327
+ this.name = "AgentHttpError";
328
+ }
329
+ };
330
+ var AgentClient = class {
331
+ static {
332
+ __name(this, "AgentClient");
333
+ }
334
+ options;
335
+ constructor(options = {}) {
336
+ this.options = options;
337
+ }
338
+ listThreads() {
339
+ return this.request("GET", "/agent/threads");
340
+ }
341
+ getThread(id) {
342
+ return this.request("GET", `/agent/threads/${encodeURIComponent(id)}`);
343
+ }
344
+ deleteThread(id) {
345
+ return this.request("DELETE", `/agent/threads/${encodeURIComponent(id)}`);
346
+ }
347
+ forkFromMessage(threadId, messageId) {
348
+ return this.request("POST", `/agent/threads/${encodeURIComponent(threadId)}/fork-from/${encodeURIComponent(messageId)}`);
349
+ }
350
+ getPersonaCatalog() {
351
+ return this.request("GET", "/agent/threads/personas/catalog");
352
+ }
353
+ getQuotaToday() {
354
+ return this.request("GET", "/agent/quota/today");
355
+ }
356
+ cancelStream(runId) {
357
+ return this.request("POST", `/agent/chat/${encodeURIComponent(runId)}/cancel`);
358
+ }
359
+ approveToolCall(input) {
360
+ return this.request("POST", "/agent/tool-call/approve", input);
361
+ }
362
+ rejectToolCall(input) {
363
+ return this.request("POST", "/agent/tool-call/reject", input);
364
+ }
365
+ async request(method, path, body) {
366
+ const fetchImpl = this.options.fetch ?? globalThis.fetch;
367
+ const baseUrl = (this.options.baseUrl ?? "").replace(/\/$/, "");
368
+ const dynamic = await this.options.getHeaders?.() ?? {};
369
+ const response = await fetchImpl(`${baseUrl}${path}`, {
370
+ method,
371
+ headers: {
372
+ accept: "application/json",
373
+ ...body !== void 0 ? {
374
+ "content-type": "application/json"
375
+ } : {},
376
+ ...this.options.headers,
377
+ ...dynamic
378
+ },
379
+ ...body !== void 0 ? {
380
+ body: JSON.stringify(body)
381
+ } : {},
382
+ ...this.options.credentials !== void 0 ? {
383
+ credentials: this.options.credentials
384
+ } : {}
385
+ });
386
+ if (!response.ok) {
387
+ throw new AgentHttpError(response.status, method, path, response.statusText);
388
+ }
389
+ if (response.status === 204) return void 0;
390
+ const text = await response.text();
391
+ if (!text) return void 0;
392
+ return JSON.parse(text);
393
+ }
394
+ };
395
+
396
+ // src/use-agent-chat.ts
397
+ var import_react = require("@ai-sdk/react");
398
+ var import_react2 = require("react");
399
+ function useAgentChat(options) {
400
+ const latest = (0, import_react2.useRef)(options);
401
+ latest.current = options;
402
+ const [runId, setRunId] = (0, import_react2.useState)(options.resumeRunId);
403
+ const runIdRef = (0, import_react2.useRef)(runId);
404
+ runIdRef.current = runId;
405
+ const regenerateNext = (0, import_react2.useRef)(false);
406
+ const client = (0, import_react2.useMemo)(() => {
407
+ if (options.client) return options.client;
408
+ return new AgentClient({
409
+ ...options.baseUrl !== void 0 ? {
410
+ baseUrl: options.baseUrl
411
+ } : {},
412
+ ...options.credentials !== void 0 ? {
413
+ credentials: options.credentials
414
+ } : {},
415
+ ...options.fetch !== void 0 ? {
416
+ fetch: options.fetch
417
+ } : {},
418
+ getHeaders: /* @__PURE__ */ __name(async () => mergeHeaders(latest.current), "getHeaders")
419
+ });
420
+ }, []);
421
+ const transport = (0, import_react2.useMemo)(() => {
422
+ function onMeta(meta) {
423
+ setRunId(meta.runId);
424
+ }
425
+ __name(onMeta, "onMeta");
426
+ return new AgentChatTransport({
427
+ ...options.baseUrl !== void 0 ? {
428
+ baseUrl: options.baseUrl
429
+ } : {},
430
+ ...options.credentials !== void 0 ? {
431
+ credentials: options.credentials
432
+ } : {},
433
+ ...options.fetch !== void 0 ? {
434
+ fetch: options.fetch
435
+ } : {},
436
+ ...options.agent !== void 0 ? {
437
+ agent: options.agent
438
+ } : {},
439
+ getHeaders: /* @__PURE__ */ __name(async () => mergeHeaders(latest.current), "getHeaders"),
440
+ getBody: /* @__PURE__ */ __name(() => {
441
+ const current = latest.current;
442
+ const pageContext = current.getPageContext?.() ?? null;
443
+ const regenerate2 = regenerateNext.current;
444
+ regenerateNext.current = false;
445
+ return {
446
+ ...current.threadId !== void 0 ? {
447
+ threadId: current.threadId
448
+ } : {},
449
+ ...current.persona !== void 0 ? {
450
+ persona: current.persona
451
+ } : {},
452
+ ...pageContext ? {
453
+ pageContext
454
+ } : {},
455
+ ...regenerate2 ? {
456
+ regenerate: true
457
+ } : {}
458
+ };
459
+ }, "getBody"),
460
+ getResumeRunId: /* @__PURE__ */ __name(() => latest.current.resumeRunId, "getResumeRunId"),
461
+ onMeta
462
+ });
463
+ }, []);
464
+ const chat = (0, import_react.useChat)({
465
+ transport,
466
+ resume: options.resumeRunId !== void 0,
467
+ ...options.threadId !== void 0 ? {
468
+ id: options.threadId
469
+ } : {},
470
+ ...options.initialMessages !== void 0 ? {
471
+ messages: options.initialMessages
472
+ } : {},
473
+ onFinish: /* @__PURE__ */ __name(() => {
474
+ latest.current.onFinish?.();
475
+ }, "onFinish")
476
+ });
477
+ const chatRef = (0, import_react2.useRef)(chat);
478
+ chatRef.current = chat;
479
+ const addToolResult = (0, import_react2.useCallback)(({ tool, toolCallId, output }) => chatRef.current.addToolResult({
480
+ tool,
481
+ toolCallId,
482
+ output
483
+ }), []);
484
+ const [threads, setThreads] = (0, import_react2.useState)([]);
485
+ const [personaCatalog, setPersonaCatalog] = (0, import_react2.useState)([]);
486
+ const [quota, setQuota] = (0, import_react2.useState)(null);
487
+ const loadThreads = (0, import_react2.useCallback)(async () => {
488
+ const list = await client.listThreads();
489
+ setThreads(list);
490
+ return list;
491
+ }, [
492
+ client
493
+ ]);
494
+ const loadThread = (0, import_react2.useCallback)((id) => client.getThread(id), [
495
+ client
496
+ ]);
497
+ const deleteThread = (0, import_react2.useCallback)(async (id) => {
498
+ await client.deleteThread(id);
499
+ setThreads((current) => current.filter((thread) => thread.id !== id));
500
+ }, [
501
+ client
502
+ ]);
503
+ const forkThread = (0, import_react2.useCallback)((threadId, messageId) => client.forkFromMessage(threadId, messageId), [
504
+ client
505
+ ]);
506
+ const loadPersonaCatalog = (0, import_react2.useCallback)(async () => {
507
+ const catalog = await client.getPersonaCatalog();
508
+ setPersonaCatalog(catalog);
509
+ return catalog;
510
+ }, [
511
+ client
512
+ ]);
513
+ const loadQuota = (0, import_react2.useCallback)(async () => {
514
+ const today = await client.getQuotaToday();
515
+ setQuota(today);
516
+ return today;
517
+ }, [
518
+ client
519
+ ]);
520
+ const cancel = (0, import_react2.useCallback)(async () => {
521
+ chatRef.current.stop();
522
+ const activeRunId = runIdRef.current;
523
+ if (activeRunId) {
524
+ try {
525
+ await client.cancelStream(activeRunId);
526
+ } catch {
527
+ }
528
+ }
529
+ }, [
530
+ client
531
+ ]);
532
+ const approve = (0, import_react2.useCallback)(async ({ toolCallId }) => {
533
+ await client.approveToolCall({
534
+ toolCallId
535
+ });
536
+ }, [
537
+ client
538
+ ]);
539
+ const reject = (0, import_react2.useCallback)(async ({ toolCallId, reason }) => {
540
+ await client.rejectToolCall({
541
+ toolCallId,
542
+ ...reason !== void 0 ? {
543
+ reason
544
+ } : {}
545
+ });
546
+ }, [
547
+ client
548
+ ]);
549
+ const regenerate = (0, import_react2.useCallback)(() => {
550
+ regenerateNext.current = true;
551
+ void chatRef.current.regenerate();
552
+ }, []);
553
+ return {
554
+ ...chat,
555
+ addToolResult,
556
+ runId,
557
+ client,
558
+ threads,
559
+ loadThreads,
560
+ loadThread,
561
+ deleteThread,
562
+ forkThread,
563
+ personaCatalog,
564
+ loadPersonaCatalog,
565
+ quota,
566
+ loadQuota,
567
+ cancel,
568
+ approve,
569
+ reject,
570
+ regenerate
571
+ };
572
+ }
573
+ __name(useAgentChat, "useAgentChat");
574
+ async function mergeHeaders(options) {
575
+ const dynamic = await options.getHeaders?.() ?? {};
576
+ return {
577
+ ...options.headers,
578
+ ...dynamic
579
+ };
580
+ }
581
+ __name(mergeHeaders, "mergeHeaders");
582
+
583
+ // src/components/chat-input.tsx
584
+ var import_react4 = __toESM(require("react"), 1);
585
+
586
+ // src/components/use-speech-recognition.ts
587
+ var import_react3 = require("react");
588
+ function getRecognitionCtor() {
589
+ if (typeof window === "undefined") return null;
590
+ const scope = window;
591
+ return scope.SpeechRecognition ?? scope.webkitSpeechRecognition ?? null;
592
+ }
593
+ __name(getRecognitionCtor, "getRecognitionCtor");
594
+ function useSpeechRecognition({ lang = "en-US", onFinalTranscript, onInterimTranscript }) {
595
+ const recognitionRef = (0, import_react3.useRef)(null);
596
+ const [isListening, setIsListening] = (0, import_react3.useState)(false);
597
+ const ctor = (0, import_react3.useMemo)(() => getRecognitionCtor(), []);
598
+ const isSupported = ctor !== null;
599
+ const finalRef = (0, import_react3.useRef)(onFinalTranscript);
600
+ const interimRef = (0, import_react3.useRef)(onInterimTranscript);
601
+ finalRef.current = onFinalTranscript;
602
+ interimRef.current = onInterimTranscript;
603
+ (0, import_react3.useEffect)(() => {
604
+ if (!ctor) return;
605
+ const recognition = new ctor();
606
+ recognition.lang = lang;
607
+ recognition.continuous = false;
608
+ recognition.interimResults = true;
609
+ recognition.onresult = (event) => {
610
+ let interim = "";
611
+ for (let i = event.resultIndex; i < event.results.length; i++) {
612
+ const result = event.results[i];
613
+ if (!result) continue;
614
+ const text = result[0]?.transcript ?? "";
615
+ if (result.isFinal) {
616
+ finalRef.current(text);
617
+ } else {
618
+ interim += text;
619
+ }
620
+ }
621
+ if (interim && interimRef.current) interimRef.current(interim);
622
+ };
623
+ recognition.onerror = (event) => {
624
+ if (event.error !== "no-speech" && event.error !== "aborted") {
625
+ console.warn("SpeechRecognition error", event.error);
626
+ }
627
+ setIsListening(false);
628
+ };
629
+ recognition.onend = () => setIsListening(false);
630
+ recognitionRef.current = recognition;
631
+ return () => {
632
+ try {
633
+ recognition.stop();
634
+ } catch {
635
+ }
636
+ recognitionRef.current = null;
637
+ };
638
+ }, [
639
+ ctor,
640
+ lang
641
+ ]);
642
+ const start = (0, import_react3.useCallback)(() => {
643
+ const recognition = recognitionRef.current;
644
+ if (!recognition) return;
645
+ try {
646
+ recognition.start();
647
+ setIsListening(true);
648
+ } catch (error) {
649
+ console.warn("SpeechRecognition start failed", error);
650
+ }
651
+ }, []);
652
+ const stop = (0, import_react3.useCallback)(() => {
653
+ const recognition = recognitionRef.current;
654
+ if (!recognition) return;
655
+ try {
656
+ recognition.stop();
657
+ } catch {
658
+ }
659
+ }, []);
660
+ return {
661
+ isSupported,
662
+ isListening,
663
+ start,
664
+ stop
665
+ };
666
+ }
667
+ __name(useSpeechRecognition, "useSpeechRecognition");
668
+
669
+ // src/components/chat-input.tsx
670
+ function ChatInput({ onSubmit, disabled, placeholder, rows = 2, enableSpeech = true, speechLang = "en-US", sendLabel = "Send", className, textareaClassName, sendButtonClassName, micButtonClassName, renderMic }) {
671
+ const [value, setValue] = (0, import_react4.useState)("");
672
+ const [interim, setInterim] = (0, import_react4.useState)("");
673
+ const speech = useSpeechRecognition({
674
+ lang: speechLang,
675
+ onFinalTranscript: /* @__PURE__ */ __name((text) => {
676
+ const trimmed = text.trim();
677
+ if (!trimmed) return;
678
+ setValue((prev) => prev.length === 0 ? trimmed : `${prev.trimEnd()} ${trimmed}`);
679
+ setInterim("");
680
+ }, "onFinalTranscript"),
681
+ onInterimTranscript: /* @__PURE__ */ __name((text) => setInterim(text), "onInterimTranscript")
682
+ });
683
+ (0, import_react4.useEffect)(() => {
684
+ if (!speech.isListening) setInterim("");
685
+ }, [
686
+ speech.isListening
687
+ ]);
688
+ function submit() {
689
+ const trimmed = value.trim();
690
+ if (!trimmed) return;
691
+ onSubmit(trimmed);
692
+ setValue("");
693
+ setInterim("");
694
+ }
695
+ __name(submit, "submit");
696
+ function handleKeyDown(event) {
697
+ if (event.key === "Enter" && !event.shiftKey) {
698
+ event.preventDefault();
699
+ submit();
700
+ }
701
+ }
702
+ __name(handleKeyDown, "handleKeyDown");
703
+ function toggleMic() {
704
+ if (speech.isListening) speech.stop();
705
+ else speech.start();
706
+ }
707
+ __name(toggleMic, "toggleMic");
708
+ const displayValue = speech.isListening && interim ? `${value}${value && !value.endsWith(" ") ? " " : ""}${interim}` : value;
709
+ const showMic = enableSpeech && speech.isSupported;
710
+ return /* @__PURE__ */ import_react4.default.createElement("div", {
711
+ className
712
+ }, /* @__PURE__ */ import_react4.default.createElement("textarea", {
713
+ className: textareaClassName,
714
+ rows,
715
+ value: displayValue,
716
+ disabled,
717
+ onChange: /* @__PURE__ */ __name((event) => {
718
+ if (speech.isListening && interim) return;
719
+ setValue(event.target.value);
720
+ }, "onChange"),
721
+ onKeyDown: handleKeyDown,
722
+ placeholder: placeholder ?? "Ask something (Enter to send, Shift+Enter for newline)"
723
+ }), /* @__PURE__ */ import_react4.default.createElement("button", {
724
+ type: "button",
725
+ className: sendButtonClassName,
726
+ onClick: submit,
727
+ disabled: disabled || !value.trim()
728
+ }, sendLabel), showMic ? /* @__PURE__ */ import_react4.default.createElement("button", {
729
+ type: "button",
730
+ className: micButtonClassName,
731
+ onClick: toggleMic,
732
+ disabled,
733
+ "aria-label": speech.isListening ? "Stop dictating" : "Start dictating"
734
+ }, renderMic ? renderMic({
735
+ isListening: speech.isListening
736
+ }) : speech.isListening ? "Stop" : "Speak") : null);
737
+ }
738
+ __name(ChatInput, "ChatInput");
739
+
740
+ // src/components/message-item.tsx
741
+ var import_ai = require("ai");
742
+ var import_react5 = __toESM(require("react"), 1);
743
+ function MessageItem({ message, renderToolPart, renderToolGroup, renderText, classNames, isStreaming = false, editable, onEditSubmit, regeneratable, onRegenerate, onFork, usage, createdAt }) {
744
+ const isUser = message.role === "user";
745
+ const parts = message.parts ?? [];
746
+ const copyableText = extractText2(parts);
747
+ const actionRow = /* @__PURE__ */ import_react5.default.createElement("div", {
748
+ className: classNames?.actions,
749
+ "data-actions-for": message.role
750
+ }, copyableText ? /* @__PURE__ */ import_react5.default.createElement(CopyButton, {
751
+ text: copyableText,
752
+ className: classNames?.actionButton
753
+ }) : null, onFork ? /* @__PURE__ */ import_react5.default.createElement("button", {
754
+ type: "button",
755
+ onClick: /* @__PURE__ */ __name(() => void onFork(message.id), "onClick"),
756
+ className: classNames?.actionButton,
757
+ title: "Fork thread from this message"
758
+ }, "Fork") : null, !isUser && regeneratable && onRegenerate ? /* @__PURE__ */ import_react5.default.createElement("button", {
759
+ type: "button",
760
+ onClick: /* @__PURE__ */ __name(() => void onRegenerate(), "onClick"),
761
+ className: classNames?.actionButton,
762
+ title: "Regenerate response"
763
+ }, "Regenerate") : null, !isUser && usage ? /* @__PURE__ */ import_react5.default.createElement(UsageInline, {
764
+ usage,
765
+ className: classNames?.usage
766
+ }) : null, createdAt ? /* @__PURE__ */ import_react5.default.createElement(TimestampInline, {
767
+ createdAt,
768
+ className: classNames?.timestamp
769
+ }) : null);
770
+ if (isUser && editable && onEditSubmit) {
771
+ return /* @__PURE__ */ import_react5.default.createElement(EditableUserBubble, {
772
+ message,
773
+ onSubmit: onEditSubmit,
774
+ renderText,
775
+ classNames,
776
+ actions: actionRow
777
+ });
778
+ }
779
+ const className = [
780
+ classNames?.root,
781
+ classNames?.byRole?.[message.role]
782
+ ].filter((entry) => entry).join(" ");
783
+ return /* @__PURE__ */ import_react5.default.createElement("div", {
784
+ className: className === "" ? void 0 : className,
785
+ "data-role": message.role,
786
+ "data-streaming": isStreaming ? "true" : void 0
787
+ }, /* @__PURE__ */ import_react5.default.createElement("div", null, renderParts(message, parts, {
788
+ renderText,
789
+ renderToolPart,
790
+ renderToolGroup,
791
+ classNames,
792
+ isStreaming
793
+ })), actionRow);
794
+ }
795
+ __name(MessageItem, "MessageItem");
796
+ function renderParts(message, parts, ctx) {
797
+ const blocks = [];
798
+ let toolBuffer = [];
799
+ let toolGroupCounter = 0;
800
+ let textCounter = 0;
801
+ function flushTools() {
802
+ if (toolBuffer.length === 0) {
803
+ return;
804
+ }
805
+ const key = `${message.id}-tools-${toolGroupCounter++}`;
806
+ if (ctx.renderToolGroup) {
807
+ blocks.push(/* @__PURE__ */ import_react5.default.createElement(import_react5.default.Fragment, {
808
+ key
809
+ }, ctx.renderToolGroup(toolBuffer, key)));
810
+ } else if (ctx.renderToolPart) {
811
+ for (const part of toolBuffer) {
812
+ const partKey = `${message.id}-${part.toolCallId}`;
813
+ blocks.push(/* @__PURE__ */ import_react5.default.createElement(import_react5.default.Fragment, {
814
+ key: partKey
815
+ }, ctx.renderToolPart(part, partKey)));
816
+ }
817
+ }
818
+ toolBuffer = [];
819
+ }
820
+ __name(flushTools, "flushTools");
821
+ for (const part of parts) {
822
+ if ((0, import_ai.isToolUIPart)(part)) {
823
+ toolBuffer.push(part);
824
+ continue;
825
+ }
826
+ flushTools();
827
+ if ((0, import_ai.isTextUIPart)(part)) {
828
+ const key = `${message.id}-text-${textCounter++}`;
829
+ blocks.push(/* @__PURE__ */ import_react5.default.createElement("div", {
830
+ key,
831
+ className: ctx.classNames?.text
832
+ }, ctx.renderText ? ctx.renderText(part.text, {
833
+ isStreaming: ctx.isStreaming
834
+ }) : part.text));
835
+ }
836
+ }
837
+ flushTools();
838
+ return blocks;
839
+ }
840
+ __name(renderParts, "renderParts");
841
+ function EditableUserBubble({ message, onSubmit, renderText, classNames, actions }) {
842
+ const initialText = extractText2(message.parts ?? []);
843
+ const [isEditing, setIsEditing] = (0, import_react5.useState)(false);
844
+ const [draft, setDraft] = (0, import_react5.useState)(initialText);
845
+ const textareaRef = (0, import_react5.useRef)(null);
846
+ (0, import_react5.useEffect)(() => {
847
+ if (isEditing && textareaRef.current) {
848
+ const el = textareaRef.current;
849
+ el.focus();
850
+ el.setSelectionRange(el.value.length, el.value.length);
851
+ }
852
+ }, [
853
+ isEditing
854
+ ]);
855
+ async function handleSave() {
856
+ const next = draft.trim();
857
+ if (!next) {
858
+ return;
859
+ }
860
+ setIsEditing(false);
861
+ await onSubmit(next);
862
+ }
863
+ __name(handleSave, "handleSave");
864
+ if (!isEditing) {
865
+ const rootClass = [
866
+ classNames?.root,
867
+ classNames?.byRole?.user
868
+ ].filter((entry) => entry).join(" ");
869
+ return /* @__PURE__ */ import_react5.default.createElement("div", {
870
+ className: rootClass === "" ? void 0 : rootClass,
871
+ "data-role": "user"
872
+ }, /* @__PURE__ */ import_react5.default.createElement("div", {
873
+ className: classNames?.text
874
+ }, renderText ? renderText(initialText, {
875
+ isStreaming: false
876
+ }) : initialText), /* @__PURE__ */ import_react5.default.createElement("button", {
877
+ type: "button",
878
+ onClick: /* @__PURE__ */ __name(() => {
879
+ setDraft(initialText);
880
+ setIsEditing(true);
881
+ }, "onClick"),
882
+ className: classNames?.actionButton,
883
+ title: "Edit & resubmit"
884
+ }, "Edit"), actions);
885
+ }
886
+ return /* @__PURE__ */ import_react5.default.createElement("div", {
887
+ "data-editing": "true"
888
+ }, /* @__PURE__ */ import_react5.default.createElement("textarea", {
889
+ ref: textareaRef,
890
+ value: draft,
891
+ onChange: /* @__PURE__ */ __name((event) => setDraft(event.target.value), "onChange"),
892
+ onKeyDown: /* @__PURE__ */ __name((event) => {
893
+ if (event.key === "Enter" && !event.shiftKey) {
894
+ event.preventDefault();
895
+ void handleSave();
896
+ }
897
+ if (event.key === "Escape") {
898
+ event.preventDefault();
899
+ setIsEditing(false);
900
+ }
901
+ }, "onKeyDown"),
902
+ rows: Math.min(8, draft.split("\n").length + 1),
903
+ className: classNames?.editTextarea
904
+ }), /* @__PURE__ */ import_react5.default.createElement("div", {
905
+ className: classNames?.editActions
906
+ }, /* @__PURE__ */ import_react5.default.createElement("button", {
907
+ type: "button",
908
+ onClick: /* @__PURE__ */ __name(() => setIsEditing(false), "onClick")
909
+ }, "Cancel"), /* @__PURE__ */ import_react5.default.createElement("button", {
910
+ type: "button",
911
+ onClick: /* @__PURE__ */ __name(() => void handleSave(), "onClick"),
912
+ disabled: !draft.trim()
913
+ }, "Resend")));
914
+ }
915
+ __name(EditableUserBubble, "EditableUserBubble");
916
+ function CopyButton({ text, className }) {
917
+ const [copied, setCopied] = (0, import_react5.useState)(false);
918
+ async function handleCopy() {
919
+ try {
920
+ await navigator.clipboard.writeText(text);
921
+ setCopied(true);
922
+ setTimeout(() => setCopied(false), 1500);
923
+ } catch {
924
+ }
925
+ }
926
+ __name(handleCopy, "handleCopy");
927
+ return /* @__PURE__ */ import_react5.default.createElement("button", {
928
+ type: "button",
929
+ onClick: /* @__PURE__ */ __name(() => void handleCopy(), "onClick"),
930
+ className,
931
+ title: copied ? "Copied!" : "Copy message text"
932
+ }, copied ? "Copied" : "Copy");
933
+ }
934
+ __name(CopyButton, "CopyButton");
935
+ function UsageInline({ usage, className }) {
936
+ const total = usage.inputTokens + usage.outputTokens;
937
+ return /* @__PURE__ */ import_react5.default.createElement("span", {
938
+ className,
939
+ title: `Input ${usage.inputTokens.toLocaleString()} \xB7 Output ${usage.outputTokens.toLocaleString()} tokens`
940
+ }, formatCostUsd(usage.costUsd), " \xB7 ", formatTokensShort(total));
941
+ }
942
+ __name(UsageInline, "UsageInline");
943
+ function TimestampInline({ createdAt, className }) {
944
+ const parsed = new Date(createdAt);
945
+ if (Number.isNaN(parsed.getTime())) {
946
+ return null;
947
+ }
948
+ const absolute = parsed.toLocaleString(void 0, {
949
+ dateStyle: "medium",
950
+ timeStyle: "short"
951
+ });
952
+ return /* @__PURE__ */ import_react5.default.createElement("span", {
953
+ className,
954
+ title: absolute
955
+ }, formatRelativeTime(parsed));
956
+ }
957
+ __name(TimestampInline, "TimestampInline");
958
+ var RELATIVE_TIME = new Intl.RelativeTimeFormat(void 0, {
959
+ numeric: "auto",
960
+ style: "narrow"
961
+ });
962
+ var ONE_WEEK_MS = 7 * 24 * 60 * 60 * 1e3;
963
+ function formatRelativeTime(date) {
964
+ const diffMs = Date.now() - date.getTime();
965
+ if (Math.abs(diffMs) < 1e4) {
966
+ return "just now";
967
+ }
968
+ if (diffMs > ONE_WEEK_MS) {
969
+ return date.toLocaleDateString(void 0, {
970
+ month: "short",
971
+ day: "numeric"
972
+ });
973
+ }
974
+ const minutes = Math.round(-diffMs / 6e4);
975
+ if (Math.abs(minutes) < 60) {
976
+ return RELATIVE_TIME.format(minutes, "minute");
977
+ }
978
+ const hours = Math.round(-diffMs / 36e5);
979
+ if (Math.abs(hours) < 24) {
980
+ return RELATIVE_TIME.format(hours, "hour");
981
+ }
982
+ return RELATIVE_TIME.format(Math.round(-diffMs / 864e5), "day");
983
+ }
984
+ __name(formatRelativeTime, "formatRelativeTime");
985
+ function formatTokensShort(n) {
986
+ if (n < 1e3) {
987
+ return `${n} tokens`;
988
+ }
989
+ if (n < 1e4) {
990
+ return `${(n / 1e3).toFixed(1)}k tokens`;
991
+ }
992
+ return `${Math.round(n / 1e3)}k tokens`;
993
+ }
994
+ __name(formatTokensShort, "formatTokensShort");
995
+ function formatCostUsd(cost) {
996
+ if (cost === 0) {
997
+ return "$0";
998
+ }
999
+ if (cost < 0.01) {
1000
+ return `$${cost.toFixed(4)}`;
1001
+ }
1002
+ if (cost < 1) {
1003
+ return `$${cost.toFixed(3)}`;
1004
+ }
1005
+ return `$${cost.toFixed(2)}`;
1006
+ }
1007
+ __name(formatCostUsd, "formatCostUsd");
1008
+ function extractText2(parts) {
1009
+ const out = [];
1010
+ for (const part of parts) {
1011
+ if ((0, import_ai.isTextUIPart)(part)) {
1012
+ out.push(part.text);
1013
+ }
1014
+ }
1015
+ return out.join("\n\n").trim();
1016
+ }
1017
+ __name(extractText2, "extractText");
1018
+
1019
+ // src/components/message-list.tsx
1020
+ var import_react6 = __toESM(require("react"), 1);
1021
+ var DEFAULT_VISIBLE_COUNT = 50;
1022
+ var LOAD_EARLIER_STEP = 50;
1023
+ function MessageList({ messages, status, renderToolPart, renderToolGroup, renderText, onFork, editable, onEditSubmit, regeneratable, onRegenerate, getUsage, getCreatedAt, emptyState, typingIndicator, followUps, onFollowUpClick, error, onRetry, classNames }) {
1024
+ const [visibleCount, setVisibleCount] = (0, import_react6.useState)(DEFAULT_VISIBLE_COUNT);
1025
+ if (messages.length === 0 && status === "ready") {
1026
+ return /* @__PURE__ */ import_react6.default.createElement("div", {
1027
+ className: classNames?.empty
1028
+ }, emptyState);
1029
+ }
1030
+ const lastMessage = messages.at(-1);
1031
+ const showTypingBubble = (status === "submitted" || status === "streaming") && lastMessage?.role !== "assistant";
1032
+ const showFollowUps = status === "ready" && !showTypingBubble && lastMessage?.role === "assistant" && onFollowUpClick !== void 0 && !!followUps && followUps.length > 0;
1033
+ const visibleStart = Math.max(0, messages.length - visibleCount);
1034
+ const visibleMessages = messages.slice(visibleStart);
1035
+ const hiddenCount = visibleStart;
1036
+ let lastAssistantIndex = -1;
1037
+ for (let i = messages.length - 1; i >= 0; i--) {
1038
+ if (messages[i]?.role === "assistant") {
1039
+ lastAssistantIndex = i;
1040
+ break;
1041
+ }
1042
+ }
1043
+ return /* @__PURE__ */ import_react6.default.createElement("div", {
1044
+ className: classNames?.root
1045
+ }, hiddenCount > 0 ? /* @__PURE__ */ import_react6.default.createElement("button", {
1046
+ type: "button",
1047
+ className: classNames?.loadEarlier,
1048
+ onClick: /* @__PURE__ */ __name(() => setVisibleCount((count) => count + LOAD_EARLIER_STEP), "onClick")
1049
+ }, "Load earlier (", hiddenCount, " more)") : null, visibleMessages.map((message, index) => {
1050
+ const absoluteIndex = visibleStart + index;
1051
+ const isLastAssistant = absoluteIndex === lastAssistantIndex;
1052
+ const usage = getUsage?.(message) ?? null;
1053
+ const createdAt = getCreatedAt?.(message) ?? null;
1054
+ return /* @__PURE__ */ import_react6.default.createElement(MessageItem, {
1055
+ key: message.id,
1056
+ message,
1057
+ ...renderToolPart ? {
1058
+ renderToolPart
1059
+ } : {},
1060
+ ...renderToolGroup ? {
1061
+ renderToolGroup
1062
+ } : {},
1063
+ ...renderText ? {
1064
+ renderText
1065
+ } : {},
1066
+ ...classNames?.message ? {
1067
+ classNames: classNames.message
1068
+ } : {},
1069
+ ...onFork ? {
1070
+ onFork
1071
+ } : {},
1072
+ ...editable ? {
1073
+ editable
1074
+ } : {},
1075
+ ...onEditSubmit ? {
1076
+ onEditSubmit: /* @__PURE__ */ __name((text) => onEditSubmit(message.id, text), "onEditSubmit")
1077
+ } : {},
1078
+ ...regeneratable && isLastAssistant ? {
1079
+ regeneratable
1080
+ } : {},
1081
+ ...onRegenerate ? {
1082
+ onRegenerate: /* @__PURE__ */ __name(() => onRegenerate(message.id), "onRegenerate")
1083
+ } : {},
1084
+ ...usage !== null ? {
1085
+ usage
1086
+ } : {},
1087
+ ...createdAt !== null ? {
1088
+ createdAt
1089
+ } : {},
1090
+ isStreaming: status === "streaming" && isLastAssistant
1091
+ });
1092
+ }), showTypingBubble ? /* @__PURE__ */ import_react6.default.createElement("div", {
1093
+ className: classNames?.typing
1094
+ }, typingIndicator ?? "Thinking\u2026") : null, error && onRetry ? /* @__PURE__ */ import_react6.default.createElement("div", {
1095
+ className: classNames?.error
1096
+ }, /* @__PURE__ */ import_react6.default.createElement("span", null, error.message || "The assistant didn't respond."), /* @__PURE__ */ import_react6.default.createElement("button", {
1097
+ type: "button",
1098
+ className: classNames?.errorRetry,
1099
+ onClick: /* @__PURE__ */ __name(() => void onRetry(), "onClick")
1100
+ }, "Retry")) : null, showFollowUps ? /* @__PURE__ */ import_react6.default.createElement("div", {
1101
+ className: classNames?.followUps
1102
+ }, followUps.map((text) => /* @__PURE__ */ import_react6.default.createElement("button", {
1103
+ key: text,
1104
+ type: "button",
1105
+ className: classNames?.followUpChip,
1106
+ onClick: /* @__PURE__ */ __name(() => onFollowUpClick(text), "onClick")
1107
+ }, text))) : null);
1108
+ }
1109
+ __name(MessageList, "MessageList");
1110
+
1111
+ // src/components/speech-languages.ts
1112
+ var DEFAULT_SPEECH_LANG = "en-US";
1113
+ var SPEECH_LANGUAGES = [
1114
+ {
1115
+ tag: "en-US",
1116
+ label: "English (US)"
1117
+ },
1118
+ {
1119
+ tag: "en-GB",
1120
+ label: "English (UK)"
1121
+ },
1122
+ {
1123
+ tag: "pt-BR",
1124
+ label: "Portuguese (Brazil)"
1125
+ },
1126
+ {
1127
+ tag: "es-ES",
1128
+ label: "Spanish (Spain)"
1129
+ }
1130
+ ];
1131
+ var STORAGE_KEY = "nestjs-agent-chat-speech-lang";
1132
+ function loadStoredSpeechLang() {
1133
+ if (typeof window === "undefined") return DEFAULT_SPEECH_LANG;
1134
+ try {
1135
+ const raw = window.localStorage.getItem(STORAGE_KEY);
1136
+ if (raw && SPEECH_LANGUAGES.some((language) => language.tag === raw)) {
1137
+ return raw;
1138
+ }
1139
+ } catch {
1140
+ }
1141
+ return DEFAULT_SPEECH_LANG;
1142
+ }
1143
+ __name(loadStoredSpeechLang, "loadStoredSpeechLang");
1144
+ function persistSpeechLang(tag) {
1145
+ if (typeof window === "undefined") return;
1146
+ try {
1147
+ window.localStorage.setItem(STORAGE_KEY, tag);
1148
+ } catch {
1149
+ }
1150
+ }
1151
+ __name(persistSpeechLang, "persistSpeechLang");
1152
+ // Annotate the CommonJS export names for ESM import in node:
1153
+ 0 && (module.exports = {
1154
+ AgentChatTransport,
1155
+ AgentClient,
1156
+ AgentHttpError,
1157
+ ChatInput,
1158
+ DEFAULT_SPEECH_LANG,
1159
+ MessageItem,
1160
+ MessageList,
1161
+ SPEECH_LANGUAGES,
1162
+ formatRelativeTime,
1163
+ loadStoredSpeechLang,
1164
+ persistSpeechLang,
1165
+ useAgentChat,
1166
+ useSpeechRecognition
1167
+ });
1168
+ //# sourceMappingURL=index.cjs.map