@tanstack/ai-react 0.22.4 → 0.24.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/README.md +11 -15
- package/dist/esm/chat-ui/chat-input.d.ts +57 -0
- package/dist/esm/chat-ui/chat-input.js +124 -0
- package/dist/esm/chat-ui/chat-input.js.map +1 -0
- package/dist/esm/chat-ui/chat-message.d.ts +91 -0
- package/dist/esm/chat-ui/chat-message.js +157 -0
- package/dist/esm/chat-ui/chat-message.js.map +1 -0
- package/dist/esm/chat-ui/chat-messages.d.ts +33 -0
- package/dist/esm/chat-ui/chat-messages.js +44 -0
- package/dist/esm/chat-ui/chat-messages.js.map +1 -0
- package/dist/esm/chat-ui/chat.d.ts +53 -0
- package/dist/esm/chat-ui/chat.js +57 -0
- package/dist/esm/chat-ui/chat.js.map +1 -0
- package/dist/esm/chat-ui/create-chat-hook.d.ts +48 -0
- package/dist/esm/chat-ui/create-chat-hook.js +57 -0
- package/dist/esm/chat-ui/create-chat-hook.js.map +1 -0
- package/dist/esm/chat-ui/create-ui-contexts.d.ts +29 -0
- package/dist/esm/chat-ui/create-ui-contexts.js +36 -0
- package/dist/esm/chat-ui/create-ui-contexts.js.map +1 -0
- package/dist/esm/chat-ui/create-ui.d.ts +153 -0
- package/dist/esm/chat-ui/create-ui.js +294 -0
- package/dist/esm/chat-ui/create-ui.js.map +1 -0
- package/dist/esm/chat-ui/markdown-plugins.d.ts +15 -0
- package/dist/esm/chat-ui/markdown-plugins.js +28 -0
- package/dist/esm/chat-ui/markdown-plugins.js.map +1 -0
- package/dist/esm/chat-ui/text-part.d.ts +57 -0
- package/dist/esm/chat-ui/text-part.js +46 -0
- package/dist/esm/chat-ui/text-part.js.map +1 -0
- package/dist/esm/chat-ui/thinking-part.d.ts +41 -0
- package/dist/esm/chat-ui/thinking-part.js +73 -0
- package/dist/esm/chat-ui/thinking-part.js.map +1 -0
- package/dist/esm/chat-ui/tool-approval.d.ts +52 -0
- package/dist/esm/chat-ui/tool-approval.js +83 -0
- package/dist/esm/chat-ui/tool-approval.js.map +1 -0
- package/dist/esm/chat-ui/tool-result-content.d.ts +18 -0
- package/dist/esm/chat-ui/tool-result-content.js +19 -0
- package/dist/esm/chat-ui/tool-result-content.js.map +1 -0
- package/dist/esm/create-chat-hook.d.ts +35 -0
- package/dist/esm/create-chat-hook.js +37 -0
- package/dist/esm/create-chat-hook.js.map +1 -0
- package/dist/esm/index.d.ts +3 -0
- package/dist/esm/index.js +3 -1
- package/dist/esm/ui.d.ts +10 -0
- package/dist/esm/ui.js +11 -0
- package/dist/esm/use-web-mcp-tools.d.ts +27 -0
- package/dist/esm/use-web-mcp-tools.js +36 -0
- package/dist/esm/use-web-mcp-tools.js.map +1 -0
- package/package.json +18 -4
- package/src/chat-ui/chat-input.tsx +177 -0
- package/src/chat-ui/chat-message.tsx +271 -0
- package/src/chat-ui/chat-messages.tsx +89 -0
- package/src/chat-ui/chat.tsx +98 -0
- package/src/chat-ui/create-chat-hook.tsx +103 -0
- package/src/chat-ui/create-ui-contexts.tsx +57 -0
- package/src/chat-ui/create-ui.tsx +761 -0
- package/src/chat-ui/markdown-plugins.ts +48 -0
- package/src/chat-ui/text-part.tsx +97 -0
- package/src/chat-ui/thinking-part.tsx +83 -0
- package/src/chat-ui/tool-approval.tsx +132 -0
- package/src/chat-ui/tool-result-content.ts +32 -0
- package/src/create-chat-hook.ts +67 -0
- package/src/index.ts +3 -0
- package/src/ui.ts +43 -0
- package/src/use-web-mcp-tools.ts +59 -0
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
import { ThinkingPart } from './thinking-part'
|
|
2
|
+
import { toolResultContentToString } from './tool-result-content'
|
|
3
|
+
import type { ReactNode } from 'react'
|
|
4
|
+
import type { UIMessage } from '../types'
|
|
5
|
+
|
|
6
|
+
export interface ToolCallRenderProps {
|
|
7
|
+
id: string
|
|
8
|
+
name: string
|
|
9
|
+
arguments: string
|
|
10
|
+
state: string
|
|
11
|
+
approval?: any
|
|
12
|
+
output?: any
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface ChatMessageProps {
|
|
16
|
+
/** The message to render */
|
|
17
|
+
message: UIMessage
|
|
18
|
+
/** Base CSS class name */
|
|
19
|
+
className?: string
|
|
20
|
+
/** Additional className for user messages */
|
|
21
|
+
userClassName?: string
|
|
22
|
+
/** Additional className for assistant messages */
|
|
23
|
+
assistantClassName?: string
|
|
24
|
+
/** Custom renderer for text parts */
|
|
25
|
+
textPartRenderer?: (props: { content: string }) => ReactNode
|
|
26
|
+
/** Custom renderer for thinking parts */
|
|
27
|
+
thinkingPartRenderer?: (props: {
|
|
28
|
+
content: string
|
|
29
|
+
isComplete?: boolean
|
|
30
|
+
}) => ReactNode
|
|
31
|
+
/** Named tool renderers - use the tool name as the key */
|
|
32
|
+
toolsRenderer?: Record<string, (props: ToolCallRenderProps) => ReactNode>
|
|
33
|
+
/** Default tool renderer when tool name not found in toolsRenderer */
|
|
34
|
+
defaultToolRenderer?: (props: ToolCallRenderProps) => ReactNode
|
|
35
|
+
/** Custom renderer for tool result parts */
|
|
36
|
+
toolResultRenderer?: (props: {
|
|
37
|
+
toolCallId: string
|
|
38
|
+
content: string
|
|
39
|
+
state: string
|
|
40
|
+
}) => ReactNode
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* @deprecated Use `createChatUI()` Message instead. Deprecated in 0.9.0. Removed in 1.0.0.
|
|
45
|
+
*
|
|
46
|
+
* Message component - renders a single message with all its parts
|
|
47
|
+
*
|
|
48
|
+
* This component natively understands TanStack AI's parts-based message format:
|
|
49
|
+
* - thinking parts: rendered as collapsible thinking/reasoning sections (auto-collapses when complete)
|
|
50
|
+
* - text parts: rendered as content
|
|
51
|
+
* - tool-call parts: rendered with state, approvals, etc.
|
|
52
|
+
* - tool-result parts: rendered with results
|
|
53
|
+
*
|
|
54
|
+
* @example Basic usage
|
|
55
|
+
* ```tsx
|
|
56
|
+
* <Chat.Message message={message} />
|
|
57
|
+
* ```
|
|
58
|
+
*
|
|
59
|
+
* @example With role-based styling
|
|
60
|
+
* ```tsx
|
|
61
|
+
* <ChatMessage
|
|
62
|
+
* message={message}
|
|
63
|
+
* className="flex"
|
|
64
|
+
* userClassName="justify-end"
|
|
65
|
+
* assistantClassName="justify-start"
|
|
66
|
+
* />
|
|
67
|
+
* ```
|
|
68
|
+
*
|
|
69
|
+
* @example With custom thinking renderer
|
|
70
|
+
* ```tsx
|
|
71
|
+
* <ChatMessage
|
|
72
|
+
* message={message}
|
|
73
|
+
* thinkingPartRenderer={({ content, isComplete }) => (
|
|
74
|
+
* <details open={!isComplete}>
|
|
75
|
+
* <summary>💭 Thinking...</summary>
|
|
76
|
+
* <pre>{content}</pre>
|
|
77
|
+
* </details>
|
|
78
|
+
* )}
|
|
79
|
+
* />
|
|
80
|
+
* ```
|
|
81
|
+
*
|
|
82
|
+
* @example With named tool renderers
|
|
83
|
+
* ```tsx
|
|
84
|
+
* <ChatMessage
|
|
85
|
+
* message={message}
|
|
86
|
+
* toolsRenderer={{
|
|
87
|
+
* recommendGuitar: ({ id, arguments: args }) => <GuitarCard {...JSON.parse(args)} />,
|
|
88
|
+
* weatherLookup: ({ id, arguments: args }) => <WeatherWidget {...JSON.parse(args)} />,
|
|
89
|
+
* }}
|
|
90
|
+
* defaultToolRenderer={() => null}
|
|
91
|
+
* />
|
|
92
|
+
* ```
|
|
93
|
+
*/
|
|
94
|
+
export function ChatMessage({
|
|
95
|
+
message,
|
|
96
|
+
className = '',
|
|
97
|
+
userClassName = '',
|
|
98
|
+
assistantClassName = '',
|
|
99
|
+
textPartRenderer,
|
|
100
|
+
thinkingPartRenderer,
|
|
101
|
+
toolsRenderer,
|
|
102
|
+
defaultToolRenderer,
|
|
103
|
+
toolResultRenderer,
|
|
104
|
+
}: ChatMessageProps) {
|
|
105
|
+
// Combine classes based on role
|
|
106
|
+
const roleClassName =
|
|
107
|
+
message.role === 'user' ? userClassName : assistantClassName
|
|
108
|
+
|
|
109
|
+
const combinedClassName = [className, roleClassName].filter(Boolean).join(' ')
|
|
110
|
+
|
|
111
|
+
return (
|
|
112
|
+
<div
|
|
113
|
+
className={combinedClassName || undefined}
|
|
114
|
+
data-message-id={message.id}
|
|
115
|
+
data-message-role={message.role}
|
|
116
|
+
data-message-created={message.createdAt?.toISOString()}
|
|
117
|
+
>
|
|
118
|
+
{message.parts.map((part, index) => {
|
|
119
|
+
// Check if thinking is complete (if there's a text part after this thinking part)
|
|
120
|
+
const isThinkingComplete =
|
|
121
|
+
part.type === 'thinking' &&
|
|
122
|
+
message.parts.slice(index + 1).some((p) => p.type === 'text')
|
|
123
|
+
|
|
124
|
+
return (
|
|
125
|
+
<MessagePart
|
|
126
|
+
key={`${message.id}-part-${index}`}
|
|
127
|
+
part={part}
|
|
128
|
+
isThinkingComplete={isThinkingComplete}
|
|
129
|
+
textPartRenderer={textPartRenderer}
|
|
130
|
+
thinkingPartRenderer={thinkingPartRenderer}
|
|
131
|
+
toolsRenderer={toolsRenderer}
|
|
132
|
+
defaultToolRenderer={defaultToolRenderer}
|
|
133
|
+
toolResultRenderer={toolResultRenderer}
|
|
134
|
+
/>
|
|
135
|
+
)
|
|
136
|
+
})}
|
|
137
|
+
</div>
|
|
138
|
+
)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function MessagePart({
|
|
142
|
+
part,
|
|
143
|
+
isThinkingComplete,
|
|
144
|
+
textPartRenderer,
|
|
145
|
+
thinkingPartRenderer,
|
|
146
|
+
toolsRenderer,
|
|
147
|
+
defaultToolRenderer,
|
|
148
|
+
toolResultRenderer,
|
|
149
|
+
}: {
|
|
150
|
+
part: UIMessage['parts'][number]
|
|
151
|
+
isThinkingComplete?: boolean
|
|
152
|
+
textPartRenderer?: ChatMessageProps['textPartRenderer']
|
|
153
|
+
thinkingPartRenderer?: ChatMessageProps['thinkingPartRenderer']
|
|
154
|
+
toolsRenderer?: ChatMessageProps['toolsRenderer']
|
|
155
|
+
defaultToolRenderer?: ChatMessageProps['defaultToolRenderer']
|
|
156
|
+
toolResultRenderer?: ChatMessageProps['toolResultRenderer']
|
|
157
|
+
}) {
|
|
158
|
+
// Text part
|
|
159
|
+
if (part.type === 'text') {
|
|
160
|
+
if (textPartRenderer) {
|
|
161
|
+
return <>{textPartRenderer({ content: part.content })}</>
|
|
162
|
+
}
|
|
163
|
+
return (
|
|
164
|
+
<div data-part-type="text" data-part-content>
|
|
165
|
+
{part.content}
|
|
166
|
+
</div>
|
|
167
|
+
)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Thinking part
|
|
171
|
+
if (part.type === 'thinking') {
|
|
172
|
+
if (thinkingPartRenderer) {
|
|
173
|
+
return (
|
|
174
|
+
<>
|
|
175
|
+
{thinkingPartRenderer({
|
|
176
|
+
content: part.content,
|
|
177
|
+
isComplete: isThinkingComplete,
|
|
178
|
+
})}
|
|
179
|
+
</>
|
|
180
|
+
)
|
|
181
|
+
}
|
|
182
|
+
return (
|
|
183
|
+
<ThinkingPart content={part.content} isComplete={isThinkingComplete} />
|
|
184
|
+
)
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Tool call part
|
|
188
|
+
if (part.type === 'tool-call') {
|
|
189
|
+
const toolProps: ToolCallRenderProps = {
|
|
190
|
+
id: part.id,
|
|
191
|
+
name: part.name,
|
|
192
|
+
arguments: part.arguments,
|
|
193
|
+
state: part.state,
|
|
194
|
+
approval: part.approval,
|
|
195
|
+
output: part.output,
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Check if there's a specific renderer for this tool
|
|
199
|
+
if (toolsRenderer?.[part.name]) {
|
|
200
|
+
return <>{toolsRenderer[part.name]?.(toolProps)}</>
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Use default tool renderer if provided
|
|
204
|
+
if (defaultToolRenderer) {
|
|
205
|
+
return <>{defaultToolRenderer(toolProps)}</>
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Fallback to built-in default renderer
|
|
209
|
+
return (
|
|
210
|
+
<div
|
|
211
|
+
data-part-type="tool-call"
|
|
212
|
+
data-tool-name={part.name}
|
|
213
|
+
data-tool-state={part.state}
|
|
214
|
+
data-tool-id={part.id}
|
|
215
|
+
>
|
|
216
|
+
<div data-tool-header>
|
|
217
|
+
<strong>{part.name}</strong>
|
|
218
|
+
<span data-tool-state-badge>{part.state}</span>
|
|
219
|
+
</div>
|
|
220
|
+
{part.arguments && (
|
|
221
|
+
<div data-tool-arguments>
|
|
222
|
+
<pre>{part.arguments}</pre>
|
|
223
|
+
</div>
|
|
224
|
+
)}
|
|
225
|
+
{part.approval && (
|
|
226
|
+
<div data-tool-approval>
|
|
227
|
+
{part.approval.approved !== undefined
|
|
228
|
+
? part.approval.approved
|
|
229
|
+
? '✓ Approved'
|
|
230
|
+
: '✗ Denied'
|
|
231
|
+
: '⏳ Awaiting approval...'}
|
|
232
|
+
</div>
|
|
233
|
+
)}
|
|
234
|
+
{part.output && (
|
|
235
|
+
<div data-tool-output>
|
|
236
|
+
<pre>{JSON.stringify(part.output, null, 2)}</pre>
|
|
237
|
+
</div>
|
|
238
|
+
)}
|
|
239
|
+
</div>
|
|
240
|
+
)
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Tool result part
|
|
244
|
+
if (part.type === 'tool-result') {
|
|
245
|
+
const toolResultContent = toolResultContentToString(part.content)
|
|
246
|
+
|
|
247
|
+
if (toolResultRenderer) {
|
|
248
|
+
return (
|
|
249
|
+
<>
|
|
250
|
+
{toolResultRenderer({
|
|
251
|
+
toolCallId: part.toolCallId,
|
|
252
|
+
content: toolResultContent,
|
|
253
|
+
state: part.state,
|
|
254
|
+
})}
|
|
255
|
+
</>
|
|
256
|
+
)
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
return (
|
|
260
|
+
<div
|
|
261
|
+
data-part-type="tool-result"
|
|
262
|
+
data-tool-call-id={part.toolCallId}
|
|
263
|
+
data-tool-result-state={part.state}
|
|
264
|
+
>
|
|
265
|
+
<div data-tool-result-content>{toolResultContent}</div>
|
|
266
|
+
</div>
|
|
267
|
+
)
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return null
|
|
271
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { useEffect, useRef } from 'react'
|
|
2
|
+
import { useChatContext } from './chat'
|
|
3
|
+
import { ChatMessage } from './chat-message'
|
|
4
|
+
import type { ReactNode } from 'react'
|
|
5
|
+
import type { UIMessage } from '../types'
|
|
6
|
+
|
|
7
|
+
/** @deprecated Use `createChatUI()` Messages instead. Deprecated in 0.9.0. Removed in 1.0.0. */
|
|
8
|
+
export interface ChatMessagesProps {
|
|
9
|
+
/** Custom render function for each message */
|
|
10
|
+
children?: (message: UIMessage, index: number) => ReactNode
|
|
11
|
+
/** CSS class name */
|
|
12
|
+
className?: string
|
|
13
|
+
/** Element to show when there are no messages */
|
|
14
|
+
emptyState?: ReactNode
|
|
15
|
+
/** Element to show while loading the first message */
|
|
16
|
+
loadingState?: ReactNode
|
|
17
|
+
/** Custom error renderer */
|
|
18
|
+
errorState?: (props: {
|
|
19
|
+
error: Error
|
|
20
|
+
reload: () => Promise<void>
|
|
21
|
+
}) => ReactNode
|
|
22
|
+
/** Auto-scroll to bottom on new messages */
|
|
23
|
+
autoScroll?: boolean
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* @deprecated Use `createChatUI()` Messages instead. Deprecated in 0.9.0. Removed in 1.0.0.
|
|
28
|
+
*
|
|
29
|
+
* Messages container - renders all messages in the conversation
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* ```tsx
|
|
33
|
+
* <Chat.Messages>
|
|
34
|
+
* {(message) => <Chat.Message message={message} />}
|
|
35
|
+
* </Chat.Messages>
|
|
36
|
+
* ```
|
|
37
|
+
*/
|
|
38
|
+
export function ChatMessages({
|
|
39
|
+
children,
|
|
40
|
+
className,
|
|
41
|
+
emptyState,
|
|
42
|
+
loadingState,
|
|
43
|
+
errorState,
|
|
44
|
+
autoScroll = true,
|
|
45
|
+
}: ChatMessagesProps) {
|
|
46
|
+
const { messages, isLoading, error, reload } = useChatContext()
|
|
47
|
+
const containerRef = useRef<HTMLDivElement>(null)
|
|
48
|
+
|
|
49
|
+
// Auto-scroll to bottom on new messages
|
|
50
|
+
useEffect(() => {
|
|
51
|
+
if (autoScroll && containerRef.current) {
|
|
52
|
+
containerRef.current.scrollTop = containerRef.current.scrollHeight
|
|
53
|
+
}
|
|
54
|
+
}, [messages, autoScroll])
|
|
55
|
+
|
|
56
|
+
// Error state
|
|
57
|
+
if (error && errorState) {
|
|
58
|
+
return <>{errorState({ error, reload })}</>
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Loading state (only show if no messages yet)
|
|
62
|
+
if (isLoading && messages.length === 0 && loadingState) {
|
|
63
|
+
return <>{loadingState}</>
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Empty state
|
|
67
|
+
if (messages.length === 0 && emptyState) {
|
|
68
|
+
return <>{emptyState}</>
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return (
|
|
72
|
+
<div
|
|
73
|
+
ref={containerRef}
|
|
74
|
+
className={className}
|
|
75
|
+
data-chat-messages
|
|
76
|
+
data-message-count={messages.length}
|
|
77
|
+
>
|
|
78
|
+
{messages.map((message, index) =>
|
|
79
|
+
children ? (
|
|
80
|
+
<div key={message.id} data-message-id={message.id}>
|
|
81
|
+
{children(message, index)}
|
|
82
|
+
</div>
|
|
83
|
+
) : (
|
|
84
|
+
<ChatMessage key={message.id} message={message} />
|
|
85
|
+
),
|
|
86
|
+
)}
|
|
87
|
+
</div>
|
|
88
|
+
)
|
|
89
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { createContext, useContext } from 'react'
|
|
2
|
+
import type { ConnectionAdapter } from '@tanstack/ai-client'
|
|
3
|
+
import { useChat } from '../use-chat'
|
|
4
|
+
import type { ReactNode } from 'react'
|
|
5
|
+
import type { UIMessage, UseChatReturn } from '../types'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Chat context - provides chat state to all child components
|
|
9
|
+
*/
|
|
10
|
+
const ChatContext = createContext<UseChatReturn | null>(null)
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Hook to access chat context
|
|
14
|
+
* @throws Error if used outside of Chat component
|
|
15
|
+
*/
|
|
16
|
+
/** @deprecated Use `createChatUI().useChatContext()` instead. Deprecated in 0.9.0. Removed in 1.0.0. */
|
|
17
|
+
export function useChatContext() {
|
|
18
|
+
const context = useContext(ChatContext)
|
|
19
|
+
if (!context) {
|
|
20
|
+
throw new Error(
|
|
21
|
+
"Chat components must be wrapped in <Chat>. Make sure you're using Chat.Messages, Chat.Input, etc. inside a <Chat> component.",
|
|
22
|
+
)
|
|
23
|
+
}
|
|
24
|
+
return context
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** @deprecated Use `createChatUI()` Chat/Provider instead. Deprecated in 0.9.0. Removed in 1.0.0. */
|
|
28
|
+
export interface ChatProps {
|
|
29
|
+
/** Child components (Chat.Messages, Chat.Input, etc.) */
|
|
30
|
+
children: ReactNode
|
|
31
|
+
/** CSS class name for the root element */
|
|
32
|
+
className?: string
|
|
33
|
+
/** Connection adapter for communicating with your API */
|
|
34
|
+
connection: ConnectionAdapter
|
|
35
|
+
/** Initial messages to display */
|
|
36
|
+
initialMessages?: Array<UIMessage>
|
|
37
|
+
/** Custom message ID generator */
|
|
38
|
+
id?: string
|
|
39
|
+
/** Additional body data to send with requests */
|
|
40
|
+
body?: any
|
|
41
|
+
/** Callback when a response is received */
|
|
42
|
+
onResponse?: (response?: Response) => void | Promise<void>
|
|
43
|
+
/** Callback when each chunk arrives */
|
|
44
|
+
onChunk?: (chunk: any) => void
|
|
45
|
+
/** Callback when a message is complete */
|
|
46
|
+
onFinish?: (message: UIMessage) => void
|
|
47
|
+
/** Callback when an error occurs */
|
|
48
|
+
onError?: (error: Error) => void
|
|
49
|
+
/** Custom tool components registry */
|
|
50
|
+
tools?: Record<string, React.ComponentType<{ input: any; output?: any }>>
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* @deprecated Since 0.9.0. Use `createChatHook()` from `@tanstack/ai-react/ui` instead.
|
|
55
|
+
* See https://tanstack.com/ai/latest/docs/ui/react
|
|
56
|
+
* Removed in 1.0.0.
|
|
57
|
+
*
|
|
58
|
+
* Root Chat component - provides context for all chat subcomponents
|
|
59
|
+
*
|
|
60
|
+
* @example
|
|
61
|
+
* ```tsx
|
|
62
|
+
* <Chat connection={fetchServerSentEvents("/api/chat")}>
|
|
63
|
+
* <Chat.Messages />
|
|
64
|
+
* <Chat.Input />
|
|
65
|
+
* </Chat>
|
|
66
|
+
* ```
|
|
67
|
+
*/
|
|
68
|
+
export function Chat({
|
|
69
|
+
children,
|
|
70
|
+
className,
|
|
71
|
+
connection,
|
|
72
|
+
initialMessages,
|
|
73
|
+
id,
|
|
74
|
+
body,
|
|
75
|
+
onResponse,
|
|
76
|
+
onChunk,
|
|
77
|
+
onFinish,
|
|
78
|
+
onError,
|
|
79
|
+
}: ChatProps) {
|
|
80
|
+
const chat = useChat({
|
|
81
|
+
connection,
|
|
82
|
+
...(initialMessages !== undefined ? { initialMessages } : {}),
|
|
83
|
+
...(id !== undefined ? { id } : {}),
|
|
84
|
+
...(body !== undefined ? { body } : {}),
|
|
85
|
+
...(onResponse !== undefined ? { onResponse } : {}),
|
|
86
|
+
...(onChunk !== undefined ? { onChunk } : {}),
|
|
87
|
+
...(onFinish !== undefined ? { onFinish } : {}),
|
|
88
|
+
...(onError !== undefined ? { onError } : {}),
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
return (
|
|
92
|
+
<ChatContext.Provider value={chat}>
|
|
93
|
+
<div className={className} data-chat-root>
|
|
94
|
+
{children}
|
|
95
|
+
</div>
|
|
96
|
+
</ChatContext.Provider>
|
|
97
|
+
)
|
|
98
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { useMemo, useRef } from 'react'
|
|
2
|
+
import type { ComponentType } from 'react'
|
|
3
|
+
import type { InferredClientContext } from '@tanstack/ai-client'
|
|
4
|
+
import type {
|
|
5
|
+
ChatUIInterruptsOf,
|
|
6
|
+
ChatUISchemaOf,
|
|
7
|
+
ChatUIToolsOf,
|
|
8
|
+
} from '@tanstack/ai-client/ui'
|
|
9
|
+
import { useChat as useUnboundChat } from '../use-chat'
|
|
10
|
+
import type { UseChatOptions } from '../types'
|
|
11
|
+
import { createChatUI } from './create-ui'
|
|
12
|
+
import type { ChatUIFactoryConfig, ChatUIHost, InputProps } from './create-ui'
|
|
13
|
+
|
|
14
|
+
type HeadlessOptions<TOptions> = UseChatOptions<
|
|
15
|
+
ChatUIToolsOf<TOptions>,
|
|
16
|
+
ChatUISchemaOf<TOptions>,
|
|
17
|
+
InferredClientContext<ChatUIToolsOf<TOptions>>,
|
|
18
|
+
ChatUIInterruptsOf<TOptions>
|
|
19
|
+
>
|
|
20
|
+
|
|
21
|
+
type ChatInstanceOverrides<TOptions> = {
|
|
22
|
+
threadId?: string
|
|
23
|
+
live?: boolean
|
|
24
|
+
forwardedProps?: Record<string, any>
|
|
25
|
+
body?: Record<string, any>
|
|
26
|
+
initialMessages?: HeadlessOptions<TOptions>['initialMessages']
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Bind chat options and UI widgets once at module scope.
|
|
31
|
+
*
|
|
32
|
+
* This matches Form `createFormHook` and Table `createTableHook`. The
|
|
33
|
+
* returned `useAppChat` creates a chat instance. `chat.AppChat` renders it.
|
|
34
|
+
* `useChatContext` reads that instance. Part and interrupt widgets take
|
|
35
|
+
* `part` / `interrupt` as props. Do not read those from context.
|
|
36
|
+
*
|
|
37
|
+
* Pass instance overrides such as `threadId` into `useAppChat()`.
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* ```tsx
|
|
41
|
+
* const { useAppChat, useChatContext } = createChatHook({
|
|
42
|
+
* options: chatOptions,
|
|
43
|
+
* components: { layout: ChatLayout, message: ChatMessage },
|
|
44
|
+
* partsComponents: { fallback: FallbackPart },
|
|
45
|
+
* toolsComponents: { getWeather: WeatherTool, purchaseItem: PurchaseTool },
|
|
46
|
+
* interruptsComponents: { generic: { choosePlan: ChoosePlan } },
|
|
47
|
+
* })
|
|
48
|
+
*
|
|
49
|
+
* function Support() {
|
|
50
|
+
* const chat = useAppChat({ threadId: 'support-1' })
|
|
51
|
+
* return <chat.AppChat />
|
|
52
|
+
* }
|
|
53
|
+
* ```
|
|
54
|
+
*/
|
|
55
|
+
export function createChatHook<
|
|
56
|
+
const TOptions,
|
|
57
|
+
TInput extends ComponentType<any> | undefined =
|
|
58
|
+
| ComponentType<InputProps<NoInfer<TOptions>>>
|
|
59
|
+
| undefined,
|
|
60
|
+
>({
|
|
61
|
+
options,
|
|
62
|
+
...chatComponents
|
|
63
|
+
}: {
|
|
64
|
+
options: TOptions
|
|
65
|
+
} & ChatUIFactoryConfig<NoInfer<TOptions>, TInput>) {
|
|
66
|
+
const ui = createChatUI(
|
|
67
|
+
options,
|
|
68
|
+
chatComponents as ChatUIFactoryConfig<NoInfer<TOptions>, TInput>,
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
function useAppChat(overrides?: ChatInstanceOverrides<TOptions>) {
|
|
72
|
+
const chat = (
|
|
73
|
+
overrides
|
|
74
|
+
? useUnboundChat({
|
|
75
|
+
...(options as HeadlessOptions<TOptions>),
|
|
76
|
+
...overrides,
|
|
77
|
+
})
|
|
78
|
+
: useUnboundChat(options as HeadlessOptions<TOptions>)
|
|
79
|
+
) as ChatUIHost<TOptions>
|
|
80
|
+
|
|
81
|
+
// `useChat` returns a fresh object on every render. The mixed Chat
|
|
82
|
+
// component must stay stable, or React remounts the tree on each
|
|
83
|
+
// update. Read the current instance from a ref.
|
|
84
|
+
const chatRef = useRef(chat)
|
|
85
|
+
chatRef.current = chat
|
|
86
|
+
|
|
87
|
+
const AppChat = useMemo(() => {
|
|
88
|
+
function AppChatImpl() {
|
|
89
|
+
return <ui.Chat chat={chatRef.current} />
|
|
90
|
+
}
|
|
91
|
+
return AppChatImpl
|
|
92
|
+
}, [])
|
|
93
|
+
|
|
94
|
+
return Object.assign(chat, { AppChat }) as ChatUIHost<TOptions> & {
|
|
95
|
+
AppChat: ComponentType
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return {
|
|
100
|
+
useAppChat,
|
|
101
|
+
useChatContext: ui.useChatContext,
|
|
102
|
+
}
|
|
103
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { createContext, useContext } from 'react'
|
|
2
|
+
import type { Context } from 'react'
|
|
3
|
+
import type {
|
|
4
|
+
ChatUIInterrupt,
|
|
5
|
+
ChatUISelectedPart,
|
|
6
|
+
} from '@tanstack/ai-client/ui'
|
|
7
|
+
import type { UseChatReturn } from '../types'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Scoped chat, part, and interrupt contexts plus matching context hooks.
|
|
11
|
+
* Pass these into {@link createChatHook} when widgets live in other files, or
|
|
12
|
+
* when two chat trees nest and must not share the default contexts.
|
|
13
|
+
*/
|
|
14
|
+
export interface ChatUIContexts {
|
|
15
|
+
chatContext: Context<UseChatReturn<any, any, any> | null>
|
|
16
|
+
partContext: Context<ChatUISelectedPart | null>
|
|
17
|
+
interruptContext: Context<ChatUIInterrupt | null>
|
|
18
|
+
useChatContext: () => UseChatReturn<any, any, any>
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Create a fresh set of chat UI contexts. This matches Form
|
|
23
|
+
* `createFormHookContexts` and Table `createTableHookContexts`.
|
|
24
|
+
*
|
|
25
|
+
* Most apps can skip this. `createChatHook` uses shared module contexts by
|
|
26
|
+
* default. The hooks on this object are the escape hatch for a widget file
|
|
27
|
+
* that cannot import the `createChatHook` result (circular import), or for
|
|
28
|
+
* nested chats that need isolated providers.
|
|
29
|
+
*
|
|
30
|
+
* Prefer the `useChatContext` hook returned from `createChatHook` when you
|
|
31
|
+
* can import that result. That hook knows the `chatComponents` types. Part
|
|
32
|
+
* and interrupt widgets take `part` / `interrupt` as props.
|
|
33
|
+
*/
|
|
34
|
+
export function createChatHookContexts(): ChatUIContexts {
|
|
35
|
+
const chatContext = createContext<UseChatReturn<any, any, any> | null>(null)
|
|
36
|
+
const partContext = createContext<ChatUISelectedPart | null>(null)
|
|
37
|
+
const interruptContext = createContext<ChatUIInterrupt | null>(null)
|
|
38
|
+
|
|
39
|
+
function useChatContext() {
|
|
40
|
+
const chat = useContext(chatContext)
|
|
41
|
+
if (!chat) {
|
|
42
|
+
throw new Error(
|
|
43
|
+
'`useChatContext` must be used within `UI.Provider` or `UI.Chat`.',
|
|
44
|
+
)
|
|
45
|
+
}
|
|
46
|
+
return chat
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
chatContext,
|
|
51
|
+
partContext,
|
|
52
|
+
interruptContext,
|
|
53
|
+
useChatContext,
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export const defaultChatUIContexts = createChatHookContexts()
|