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