@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.
@@ -0,0 +1,419 @@
1
+ import * as ai from 'ai';
2
+ import { ChatTransport, UIMessage, UIMessageChunk, ToolUIPart, DynamicToolUIPart } from 'ai';
3
+ import { ThreadSummary, ThreadDetail, Persona } from '@dudousxd/nestjs-agent-core';
4
+ import * as react_jsx_runtime from 'react/jsx-runtime';
5
+ import React from 'react';
6
+
7
+ /** Identity surfaced by the backend's `meta` SSE frame / response headers. */
8
+ interface AgentStreamMeta {
9
+ runId: string;
10
+ threadId: string;
11
+ }
12
+ interface AgentChatTransportOptions {
13
+ /**
14
+ * Origin + base path the agent endpoints hang off, e.g.
15
+ * `https://api.example.com`. Endpoints are appended as
16
+ * `${baseUrl}/agent/chat` etc. Defaults to `''` (same origin).
17
+ */
18
+ baseUrl?: string;
19
+ /** Static headers merged into every request (e.g. a tenant ref). */
20
+ headers?: Record<string, string>;
21
+ /**
22
+ * Resolved at request time — use for short-lived bearer tokens that
23
+ * must not be captured once at construction. Merged over `headers`.
24
+ */
25
+ getHeaders?: () => Record<string, string> | Promise<Record<string, string>>;
26
+ /** Forwarded to `fetch` so cookie-based auth/impersonation works. */
27
+ credentials?: RequestCredentials;
28
+ /** Named agent to run the turn (backend `agent` field). */
29
+ agent?: string;
30
+ /**
31
+ * Extra body fields evaluated per send — the hook injects `threadId`,
32
+ * `persona`, and `pageContext` through this. Returned object is spread
33
+ * into the request body after the SDK's own `body`.
34
+ */
35
+ getBody?: () => Record<string, unknown>;
36
+ /**
37
+ * The run id to resume on mount, if any. Returning `undefined` makes
38
+ * `reconnectToStream` resolve to `null` WITHOUT hitting the network —
39
+ * this is the generalized fix for "useChat fires a doomed GET on mount
40
+ * and surfaces its 404". Wire this to the thread's `activeStreamId`.
41
+ */
42
+ getResumeRunId?: () => string | undefined;
43
+ /** Fires whenever a stream emits its `meta` frame. */
44
+ onMeta?: (meta: AgentStreamMeta) => void;
45
+ /** Injectable for tests / non-browser runtimes. Defaults to global fetch. */
46
+ fetch?: typeof fetch;
47
+ }
48
+ /**
49
+ * AI SDK v7 `ChatTransport` for the nestjs-agent backend. POSTs
50
+ * `/agent/chat`, parses the backend's `meta` + `{delta}` + `done` SSE
51
+ * frames, and re-emits them as the v7 UI-message chunk stream
52
+ * (`start` → `text-start` → `text-delta`* → `text-end` → `finish`).
53
+ *
54
+ * The backend hydrates prior history from its store, so only the latest
55
+ * user message text is sent each turn — keeping payloads tiny and
56
+ * preventing the client from corrupting replayed history.
57
+ */
58
+ declare class AgentChatTransport implements ChatTransport<UIMessage> {
59
+ private readonly options;
60
+ private currentRunId;
61
+ private currentThreadId;
62
+ constructor(options?: AgentChatTransportOptions);
63
+ /** Run id of the most recent stream — HITL approve/reject target this. */
64
+ get runId(): string | undefined;
65
+ /** Thread id of the most recent stream. */
66
+ get threadId(): string | undefined;
67
+ sendMessages(options: Parameters<ChatTransport<UIMessage>['sendMessages']>[0]): Promise<ReadableStream<UIMessageChunk>>;
68
+ reconnectToStream(options: Parameters<ChatTransport<UIMessage>['reconnectToStream']>[0]): Promise<ReadableStream<UIMessageChunk> | null>;
69
+ private fetchImpl;
70
+ private baseUrl;
71
+ private resolveHeaders;
72
+ private captureHeaderMeta;
73
+ private recordMeta;
74
+ /**
75
+ * Parse the backend's SSE byte stream and synthesize a valid v7
76
+ * UI-message chunk stream. Recognized backend frames:
77
+ * - `event: meta` `data: {"runId","threadId"}` → records identity
78
+ * - `data: {"delta":"..."}` → `text-delta`
79
+ * - `event: done` `data: {}` → terminates
80
+ */
81
+ private toChunkStream;
82
+ }
83
+
84
+ /** The trimmed persona shape returned by `/agent/threads/personas/catalog`. */
85
+ type PersonaCatalogEntry = Pick<Persona, 'id' | 'label'>;
86
+ /**
87
+ * Thrown by {@link AgentClient} on a non-2xx response. Carries the HTTP `status` so callers can
88
+ * branch (e.g. 403 → "not your thread", 429 → quota) instead of string-matching a generic Error.
89
+ */
90
+ declare class AgentHttpError extends Error {
91
+ readonly status: number;
92
+ readonly method: string;
93
+ readonly path: string;
94
+ constructor(status: number, method: string, path: string, statusText: string);
95
+ }
96
+ interface QuotaToday {
97
+ usedTokens: number;
98
+ }
99
+ interface CancelResult {
100
+ aborted: boolean;
101
+ }
102
+ interface AgentClientOptions {
103
+ /** Origin + base path, e.g. `https://api.example.com`. Defaults to `''`. */
104
+ baseUrl?: string;
105
+ /** Static headers merged into every request. */
106
+ headers?: Record<string, string>;
107
+ /** Resolved per request — for short-lived bearer tokens. */
108
+ getHeaders?: () => Record<string, string> | Promise<Record<string, string>>;
109
+ /** Forwarded to fetch so cookie auth works. */
110
+ credentials?: RequestCredentials;
111
+ /** Injectable for tests / non-browser runtimes. */
112
+ fetch?: typeof fetch;
113
+ }
114
+ /**
115
+ * Framework-agnostic REST client for the nestjs-agent endpoints. Used by
116
+ * `useAgentChat`, but standalone-usable (vanilla fetch, no React).
117
+ */
118
+ declare class AgentClient {
119
+ private readonly options;
120
+ constructor(options?: AgentClientOptions);
121
+ listThreads(): Promise<ThreadSummary[]>;
122
+ getThread(id: string): Promise<ThreadDetail>;
123
+ deleteThread(id: string): Promise<void>;
124
+ forkFromMessage(threadId: string, messageId: string): Promise<ThreadSummary>;
125
+ getPersonaCatalog(): Promise<PersonaCatalogEntry[]>;
126
+ getQuotaToday(): Promise<QuotaToday>;
127
+ cancelStream(runId: string): Promise<CancelResult>;
128
+ approveToolCall(input: {
129
+ toolCallId: string;
130
+ }): Promise<void>;
131
+ rejectToolCall(input: {
132
+ toolCallId: string;
133
+ reason?: string;
134
+ }): Promise<void>;
135
+ private request;
136
+ }
137
+
138
+ interface UseAgentChatOptions {
139
+ /** Origin + base path, e.g. `https://api.example.com`. Defaults to `''`. */
140
+ baseUrl?: string;
141
+ /** Static headers merged into every request. */
142
+ headers?: Record<string, string>;
143
+ /** Resolved per request — for short-lived bearer tokens. */
144
+ getHeaders?: () => Record<string, string> | Promise<Record<string, string>>;
145
+ /** Forwarded to fetch so cookie auth/impersonation works. */
146
+ credentials?: RequestCredentials;
147
+ /** Injectable for tests / non-browser runtimes. */
148
+ fetch?: typeof fetch;
149
+ /** Reuse an existing client instead of constructing one. */
150
+ client?: AgentClient;
151
+ /** Named agent to run each turn. */
152
+ agent?: string;
153
+ /** Thread this chat is bound to. Omitted → backend creates one on send. */
154
+ threadId?: string;
155
+ /** Persisted history to seed `useChat` with (consumed on mount only). */
156
+ initialMessages?: UIMessage[];
157
+ /**
158
+ * Run id of a buffered stream to reconnect to on mount — wire this to the thread's
159
+ * `activeStreamId`. Its presence both gates the SDK's `resume` and names the run, so a normal
160
+ * mount (no id) never fires a doomed resume GET. Omitted → no resume.
161
+ */
162
+ resumeRunId?: string;
163
+ /** Active persona id sent on every turn. */
164
+ persona?: string;
165
+ /** Read at every send to capture a page snapshot for the page-assistant. */
166
+ getPageContext?: () => Record<string, unknown> | null;
167
+ /** Fired after each streamed turn finishes (e.g. to refetch the sidebar). */
168
+ onFinish?: () => void;
169
+ }
170
+ interface AddToolResultArgs {
171
+ tool: string;
172
+ toolCallId: string;
173
+ output: unknown;
174
+ }
175
+ /**
176
+ * Generalized `useChat` wiring for the nestjs-agent backend. Wraps the
177
+ * AI SDK v7 hook with `AgentChatTransport`, plus thread list/load/delete/
178
+ * fork, persona catalog, quota, cancel, and HITL approve/reject — all
179
+ * driven through `AgentClient`. Mirrors flip's `useAdminChat`, generalized.
180
+ */
181
+ declare function useAgentChat(options: UseAgentChatOptions): {
182
+ addToolResult: ({ tool, toolCallId, output }: AddToolResultArgs) => void | PromiseLike<void>;
183
+ runId: string | undefined;
184
+ client: AgentClient;
185
+ threads: ThreadSummary[];
186
+ loadThreads: () => Promise<ThreadSummary[]>;
187
+ loadThread: (id: string) => Promise<ThreadDetail>;
188
+ deleteThread: (id: string) => Promise<void>;
189
+ forkThread: (threadId: string, messageId: string) => Promise<ThreadSummary>;
190
+ personaCatalog: PersonaCatalogEntry[];
191
+ loadPersonaCatalog: () => Promise<PersonaCatalogEntry[]>;
192
+ quota: QuotaToday | null;
193
+ loadQuota: () => Promise<QuotaToday>;
194
+ cancel: () => Promise<void>;
195
+ approve: ({ toolCallId }: {
196
+ toolCallId: string;
197
+ }) => Promise<void>;
198
+ reject: ({ toolCallId, reason, }: {
199
+ toolCallId: string;
200
+ reason?: string;
201
+ }) => Promise<void>;
202
+ regenerate: () => void;
203
+ id: string;
204
+ setMessages: (messages: UIMessage<unknown, ai.UIDataTypes, ai.UITools>[] | ((messages: UIMessage<unknown, ai.UIDataTypes, ai.UITools>[]) => UIMessage<unknown, ai.UIDataTypes, ai.UITools>[])) => void;
205
+ error: Error | undefined;
206
+ stop: () => Promise<void>;
207
+ messages: UIMessage<unknown, ai.UIDataTypes, ai.UITools>[];
208
+ status: ai.ChatStatus;
209
+ sendMessage: (message?: (Omit<UIMessage<unknown, ai.UIDataTypes, ai.UITools>, "id" | "role"> & {
210
+ id?: string;
211
+ role?: "system" | "user" | "assistant";
212
+ } & {
213
+ text?: never;
214
+ files?: never;
215
+ messageId?: string;
216
+ }) | {
217
+ text: string;
218
+ files?: FileList | ai.FileUIPart[];
219
+ metadata?: unknown;
220
+ parts?: never;
221
+ messageId?: string;
222
+ } | {
223
+ files: FileList | ai.FileUIPart[];
224
+ metadata?: unknown;
225
+ parts?: never;
226
+ messageId?: string;
227
+ } | undefined, options?: ai.ChatRequestOptions) => Promise<void>;
228
+ resumeStream: (options?: ai.ChatRequestOptions) => Promise<void>;
229
+ addToolOutput: ai.ChatAddToolOutputFunction<UIMessage<unknown, ai.UIDataTypes, ai.UITools>>;
230
+ addToolApprovalResponse: ai.ChatAddToolApproveResponseFunction;
231
+ clearError: () => void;
232
+ };
233
+
234
+ interface ChatInputProps {
235
+ onSubmit: (text: string) => void;
236
+ disabled?: boolean;
237
+ placeholder?: string;
238
+ rows?: number;
239
+ /** Enable the dictation mic button (Web Speech API). Default `true`. */
240
+ enableSpeech?: boolean;
241
+ /** BCP-47 locale for dictation. Default `en-US`. */
242
+ speechLang?: string;
243
+ /** Label/content for the send button. Default `"Send"`. */
244
+ sendLabel?: React.ReactNode;
245
+ /** Wrapper class. The component ships no styles of its own. */
246
+ className?: string;
247
+ textareaClassName?: string;
248
+ sendButtonClassName?: string;
249
+ micButtonClassName?: string;
250
+ /**
251
+ * Custom mic affordance. Receives `isListening`; return any node. When
252
+ * omitted a plain text button ("Speak" / "Stop") is rendered.
253
+ */
254
+ renderMic?: (state: {
255
+ isListening: boolean;
256
+ }) => React.ReactNode;
257
+ }
258
+ /**
259
+ * Styling-agnostic chat composer. Enter submits, Shift+Enter inserts a
260
+ * newline. Optional dictation streams interim transcripts into the draft
261
+ * and commits finalized chunks. No design-system or icon-library deps —
262
+ * style it entirely through the `*ClassName` props.
263
+ */
264
+ declare function ChatInput({ onSubmit, disabled, placeholder, rows, enableSpeech, speechLang, sendLabel, className, textareaClassName, sendButtonClassName, micButtonClassName, renderMic, }: ChatInputProps): react_jsx_runtime.JSX.Element;
265
+
266
+ /** A tool UI part on a `UIMessage` — a static `tool-*` part or the `dynamic-tool` part. */
267
+ type AnyToolUIPart = ToolUIPart | DynamicToolUIPart;
268
+ type RenderToolPartFn = (part: AnyToolUIPart, key: string) => React.ReactNode;
269
+ /** Render a run of consecutive tool parts as one block (e.g. a collapsible tool group). */
270
+ type RenderToolGroupFn = (parts: AnyToolUIPart[], key: string) => React.ReactNode;
271
+ /** Render a message's text — pass a markdown renderer (e.g. `AgentMarkdown`) here if desired. */
272
+ type RenderTextFn = (text: string, ctx: {
273
+ isStreaming: boolean;
274
+ }) => React.ReactNode;
275
+ /** Server-aggregated usage for an assistant turn, shown as a small inline line. */
276
+ interface MessageUsageInfo {
277
+ inputTokens: number;
278
+ outputTokens: number;
279
+ costUsd: number;
280
+ }
281
+ interface MessageItemClassNames {
282
+ root?: string;
283
+ /** Resolved per role so user/assistant/system bubbles can differ. */
284
+ byRole?: Partial<Record<UIMessage['role'], string>>;
285
+ text?: string;
286
+ /** The action row under the bubble (copy / fork / regenerate / usage / timestamp). */
287
+ actions?: string;
288
+ actionButton?: string;
289
+ usage?: string;
290
+ timestamp?: string;
291
+ editTextarea?: string;
292
+ editActions?: string;
293
+ }
294
+ interface MessageItemProps {
295
+ message: UIMessage;
296
+ renderToolPart?: RenderToolPartFn;
297
+ /** Takes precedence over `renderToolPart` for a run of consecutive tool parts. */
298
+ renderToolGroup?: RenderToolGroupFn;
299
+ renderText?: RenderTextFn;
300
+ classNames?: MessageItemClassNames;
301
+ /** Marks the in-flight assistant bubble (e.g. for a caret animation / deferred markdown render). */
302
+ isStreaming?: boolean;
303
+ /** User bubble gets a pencil → inline textarea; submitting calls back with the new text. */
304
+ editable?: boolean;
305
+ onEditSubmit?: (newText: string) => void | Promise<void>;
306
+ /** Assistant bubble gets a "Regenerate" affordance. */
307
+ regeneratable?: boolean;
308
+ onRegenerate?: () => void | Promise<void>;
309
+ /** When provided, a "Fork" affordance calls back with this message's id. */
310
+ onFork?: (uiMessageId: string) => void | Promise<void>;
311
+ /** Server-aggregated usage for this assistant turn (cost + tokens line). */
312
+ usage?: MessageUsageInfo | null;
313
+ /** ISO timestamp of the persisted message; rendered as a relative-time line when set. */
314
+ createdAt?: string | null;
315
+ }
316
+ /**
317
+ * Styling-agnostic renderer for a single v7 `UIMessage`. Ports flip's full chat-item UX —
318
+ * copy, fork, regenerate, inline edit-and-resubmit, usage line, relative timestamp, and tool
319
+ * grouping — but with ALL styling left to `classNames` and every action surfaced as a callback,
320
+ * so no design-system / icon / date library is pulled in (relative time uses Intl.RelativeTimeFormat).
321
+ */
322
+ declare function MessageItem({ message, renderToolPart, renderToolGroup, renderText, classNames, isStreaming, editable, onEditSubmit, regeneratable, onRegenerate, onFork, usage, createdAt, }: MessageItemProps): react_jsx_runtime.JSX.Element;
323
+ /**
324
+ * "just now" within ±10s (server `created_at` can land a few ms ahead of the client clock), a
325
+ * relative phrase up to a week, then a calendar date. Uses Intl.RelativeTimeFormat — no date-fns.
326
+ */
327
+ declare function formatRelativeTime(date: Date): string;
328
+
329
+ type ChatStatus = 'ready' | 'submitted' | 'streaming' | 'error';
330
+ interface MessageListClassNames {
331
+ root?: string;
332
+ empty?: string;
333
+ loadEarlier?: string;
334
+ typing?: string;
335
+ error?: string;
336
+ errorRetry?: string;
337
+ followUps?: string;
338
+ followUpChip?: string;
339
+ message?: MessageItemClassNames;
340
+ }
341
+ interface MessageListProps {
342
+ messages: UIMessage[];
343
+ status: ChatStatus;
344
+ renderToolPart?: RenderToolPartFn;
345
+ renderToolGroup?: RenderToolGroupFn;
346
+ renderText?: RenderTextFn;
347
+ /** When set, every message gets a "Fork" affordance calling back with its id. */
348
+ onFork?: (uiMessageId: string) => void | Promise<void>;
349
+ /** User messages get an inline edit-and-resubmit affordance. */
350
+ editable?: boolean;
351
+ onEditSubmit?: (uiMessageId: string, newText: string) => void | Promise<void>;
352
+ /** The last assistant message gets a "Regenerate" affordance. */
353
+ regeneratable?: boolean;
354
+ onRegenerate?: (uiMessageId: string) => void | Promise<void>;
355
+ /** Per-message resolvers for the usage line and the persisted timestamp. */
356
+ getUsage?: (message: UIMessage) => MessageUsageInfo | null;
357
+ getCreatedAt?: (message: UIMessage) => string | null;
358
+ /** Shown (with example/follow-up chips) when idle and empty. */
359
+ emptyState?: React.ReactNode;
360
+ /** Node rendered while waiting for the first assistant token. */
361
+ typingIndicator?: React.ReactNode;
362
+ /** AI-generated follow-up prompts under the last assistant message. */
363
+ followUps?: string[] | null;
364
+ onFollowUpClick?: (text: string) => void;
365
+ /** Last turn's error — paired with `onRetry`, renders a retry banner. */
366
+ error?: Error | null;
367
+ onRetry?: () => void | Promise<void>;
368
+ classNames?: MessageListClassNames;
369
+ }
370
+ /**
371
+ * Styling-agnostic message list. Keeps the proven UX — windowed render
372
+ * with "load earlier", a typing indicator, an inline error+retry banner,
373
+ * and follow-up chips — but ships no styles: drive everything through
374
+ * `classNames` and the `emptyState`/`typingIndicator` slots.
375
+ */
376
+ declare function MessageList({ messages, status, renderToolPart, renderToolGroup, renderText, onFork, editable, onEditSubmit, regeneratable, onRegenerate, getUsage, getCreatedAt, emptyState, typingIndicator, followUps, onFollowUpClick, error, onRetry, classNames, }: MessageListProps): react_jsx_runtime.JSX.Element;
377
+
378
+ /**
379
+ * Languages the dictation mic offers. The Web Speech API expects a
380
+ * BCP-47 tag and does NOT auto-detect — the engine forces incoming
381
+ * audio onto the phonemes of whatever `recognition.lang` is set to, so
382
+ * the host app picks the locale explicitly.
383
+ */
384
+ interface SpeechLanguage {
385
+ /** BCP-47 tag fed straight into `SpeechRecognition.lang`. */
386
+ tag: string;
387
+ /** Human label for a dropdown. */
388
+ label: string;
389
+ }
390
+ declare const DEFAULT_SPEECH_LANG = "en-US";
391
+ declare const SPEECH_LANGUAGES: ReadonlyArray<SpeechLanguage>;
392
+ /**
393
+ * Recover the last picked language from localStorage (sync — safe at
394
+ * `useState` init). Falls back to the default if nothing's stored or the
395
+ * stored tag isn't in the offered list.
396
+ */
397
+ declare function loadStoredSpeechLang(): string;
398
+ declare function persistSpeechLang(tag: string): void;
399
+
400
+ interface UseSpeechRecognitionOptions {
401
+ lang?: string;
402
+ onFinalTranscript: (text: string) => void;
403
+ onInterimTranscript?: (text: string) => void;
404
+ }
405
+ interface SpeechRecognitionHook {
406
+ isSupported: boolean;
407
+ isListening: boolean;
408
+ start(): void;
409
+ stop(): void;
410
+ }
411
+ /**
412
+ * Tiny wrapper around the Web Speech API. `start()` opens the mic and
413
+ * streams interim transcripts via `onInterimTranscript`; on a finalized
414
+ * chunk it fires `onFinalTranscript`. `isSupported` is false where the
415
+ * API is missing (e.g. Firefox) — callers should hide the mic button.
416
+ */
417
+ declare function useSpeechRecognition({ lang, onFinalTranscript, onInterimTranscript, }: UseSpeechRecognitionOptions): SpeechRecognitionHook;
418
+
419
+ export { AgentChatTransport, type AgentChatTransportOptions, AgentClient, type AgentClientOptions, AgentHttpError, type AgentStreamMeta, type AnyToolUIPart, type CancelResult, ChatInput, type ChatInputProps, type ChatStatus, DEFAULT_SPEECH_LANG, MessageItem, type MessageItemClassNames, type MessageItemProps, MessageList, type MessageListClassNames, type MessageListProps, type MessageUsageInfo, type PersonaCatalogEntry, type QuotaToday, type RenderTextFn, type RenderToolGroupFn, type RenderToolPartFn, SPEECH_LANGUAGES, type SpeechLanguage, type SpeechRecognitionHook, type UseAgentChatOptions, type UseSpeechRecognitionOptions, formatRelativeTime, loadStoredSpeechLang, persistSpeechLang, useAgentChat, useSpeechRecognition };