@dudousxd/nestjs-agent-react 0.5.1 → 0.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +119 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -37,6 +37,125 @@ function Chat() {
|
|
|
37
37
|
}
|
|
38
38
|
```
|
|
39
39
|
|
|
40
|
+
## Bring your own UI
|
|
41
|
+
|
|
42
|
+
The package is **headless by design**. `useAgentChat` (state/streaming/persistence) and `AgentClient`
|
|
43
|
+
(the REST calls) own all the logic; `MessageList`/`ChatInput` — and the `markdown` subpath — are an
|
|
44
|
+
OPTIONAL, styling-agnostic reference implementation on top. A host with its own design system renders
|
|
45
|
+
straight off the hook's return value and never imports `MessageList` at all.
|
|
46
|
+
|
|
47
|
+
### `useAgentChat` on its own
|
|
48
|
+
|
|
49
|
+
```tsx
|
|
50
|
+
import { useAgentChat } from '@dudousxd/nestjs-agent-react';
|
|
51
|
+
import { isTextUIPart, isToolUIPart } from 'ai';
|
|
52
|
+
|
|
53
|
+
function CustomChat({ threadId }: { threadId?: string }) {
|
|
54
|
+
const chat = useAgentChat({
|
|
55
|
+
baseUrl: '/agent',
|
|
56
|
+
// Omit the key entirely when absent — `UseAgentChatOptions` is built with
|
|
57
|
+
// `exactOptionalPropertyTypes`, so an explicit `threadId: undefined` doesn't type-check.
|
|
58
|
+
...(threadId !== undefined ? { threadId } : {}),
|
|
59
|
+
agent: 'support',
|
|
60
|
+
// Reattach to a turn still streaming when the page loaded — survives a refresh.
|
|
61
|
+
resume: true,
|
|
62
|
+
getHeaders: () => ({ 'x-actor-id': currentUser.id }),
|
|
63
|
+
onThreadCreated: (newThreadId) => router.replace(`/chat/${newThreadId}`),
|
|
64
|
+
// Fires once per run when the SERVER is done writing (title + terminal state persisted) —
|
|
65
|
+
// the right signal to refetch a thread list/sidebar; `onFinish` only means "a turn rendered".
|
|
66
|
+
onRunSettled: ({ status }) => {
|
|
67
|
+
if (status === 'completed') refetchThreadList();
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
return (
|
|
72
|
+
<div className="my-chat-shell">
|
|
73
|
+
{chat.messages.map((message) => (
|
|
74
|
+
<div
|
|
75
|
+
key={message.id}
|
|
76
|
+
className={message.role === 'user' ? 'my-bubble-user' : 'my-bubble-assistant'}
|
|
77
|
+
>
|
|
78
|
+
{message.parts.map((part, i) => {
|
|
79
|
+
if (isTextUIPart(part)) return <p key={i}>{part.text}</p>;
|
|
80
|
+
if (isToolUIPart(part)) return <MyToolCard key={i} part={part} />;
|
|
81
|
+
return null;
|
|
82
|
+
})}
|
|
83
|
+
</div>
|
|
84
|
+
))}
|
|
85
|
+
<MyComposer
|
|
86
|
+
disabled={chat.status === 'streaming'}
|
|
87
|
+
onSubmit={(text) => chat.sendMessage({ text })}
|
|
88
|
+
/>
|
|
89
|
+
</div>
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
`chat` is the AI SDK v7 `useChat` return value (`messages`, `status`, `sendMessage`, `stop`, …) spread
|
|
95
|
+
together with the extras: `runId`/`activeRunId`, `client` (the raw `AgentClient`), thread list/CRUD
|
|
96
|
+
(`threads`, `loadThreads`, `loadThread`, `deleteThread`, `forkThread`, `renameThread`, `promoteThread`,
|
|
97
|
+
`truncateFromMessage`), `quota`/`loadQuota`, `cancel`, HITL `approve`/`reject`, and `regenerate`.
|
|
98
|
+
`MyToolCard`'s `part` prop above types as the exported `AnyToolUIPart` (`ToolUIPart | DynamicToolUIPart`
|
|
99
|
+
— `MessageItem` uses the same union for its own `renderToolPart` callback).
|
|
100
|
+
|
|
101
|
+
### The transport, standalone
|
|
102
|
+
|
|
103
|
+
`AgentChatTransport` is a plain AI SDK v7 `ChatTransport` — wire it straight into `useChat` for the SSE
|
|
104
|
+
plumbing alone, with none of `useAgentChat`'s thread/quota/approval state:
|
|
105
|
+
|
|
106
|
+
```ts
|
|
107
|
+
import { useChat } from '@ai-sdk/react';
|
|
108
|
+
import { AgentChatTransport } from '@dudousxd/nestjs-agent-react';
|
|
109
|
+
|
|
110
|
+
const transport = new AgentChatTransport({
|
|
111
|
+
baseUrl: '/agent',
|
|
112
|
+
getHeaders: () => ({ 'x-actor-id': currentUser.id }),
|
|
113
|
+
onMeta: ({ runId, threadId }) => console.log('turn started', runId, threadId),
|
|
114
|
+
});
|
|
115
|
+
const chat = useChat({ transport });
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### Loading persisted history
|
|
119
|
+
|
|
120
|
+
A reloaded thread's `StoredMessage[]` (from `AgentClient.getThread`) needs converting to `UIMessage[]`
|
|
121
|
+
before it can seed `useChat`'s `initialMessages`:
|
|
122
|
+
|
|
123
|
+
```ts
|
|
124
|
+
import { storedThreadToUiMessages } from '@dudousxd/nestjs-agent-react';
|
|
125
|
+
|
|
126
|
+
const detail = await chat.client.getThread(threadId);
|
|
127
|
+
const initialMessages = storedThreadToUiMessages(detail.messages);
|
|
128
|
+
// Feed into useAgentChat({ threadId, initialMessages, ... }) on the mount that owns this thread —
|
|
129
|
+
// `initialMessages` is only read once, on mount.
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
`storedThreadToUiMessages` merges the store's one-row-per-model-iteration turns (a turn with tool
|
|
133
|
+
calls persists as "thinking…" + tool calls, then a separate final-answer row) into ONE `UIMessage` per
|
|
134
|
+
conversational turn, matching how the live stream renders — and stamps `metadata.usage` on any turn it
|
|
135
|
+
merged. For a single already-atomic row, `storedMessageToUiMessage` maps it 1:1 with no merging.
|
|
136
|
+
|
|
137
|
+
### Attachments and the raw client
|
|
138
|
+
|
|
139
|
+
`uploadAttachment` and the rest of `AgentClient` work outside `useAgentChat` too — e.g. from a
|
|
140
|
+
file-picker or a standalone approvals-inbox screen:
|
|
141
|
+
|
|
142
|
+
```ts
|
|
143
|
+
const attachment = await chat.client.uploadAttachment(file); // → MessageAttachment
|
|
144
|
+
await chat.sendMessage({ text: 'what is in this?' }, { body: { attachments: [attachment] } });
|
|
145
|
+
|
|
146
|
+
// Human-in-the-loop, callable from any component — not just the chat screen that raised it:
|
|
147
|
+
await chat.approve({ toolCallId });
|
|
148
|
+
await chat.reject({ toolCallId, reason: 'not now' });
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### What a flip-style host owns
|
|
152
|
+
|
|
153
|
+
Everything visual: bubble layout and markdown rendering, the tool-card UI (branch on
|
|
154
|
+
`isToolUIPart`/`isDynamicToolUIPart`), composer design, and theming/design tokens — including how
|
|
155
|
+
"pending approval" reads in your product. The package never renders a pixel unless you opt into
|
|
156
|
+
`MessageList`/`ChatInput`; what it owns is the protocol: SSE parsing, resume/reconnect, thread CRUD,
|
|
157
|
+
quota, and HITL routing.
|
|
158
|
+
|
|
40
159
|
### Optional markdown subpath
|
|
41
160
|
|
|
42
161
|
```tsx
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dudousxd/nestjs-agent-react",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.2",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "git+https://github.com/DavideCarvalho/nestjs-agent.git",
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
"dist"
|
|
45
45
|
],
|
|
46
46
|
"dependencies": {
|
|
47
|
-
"@dudousxd/nestjs-agent-core": "0.
|
|
47
|
+
"@dudousxd/nestjs-agent-core": "0.9.0"
|
|
48
48
|
},
|
|
49
49
|
"peerDependencies": {
|
|
50
50
|
"@ai-sdk/react": "^4.0.0",
|