@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.
Files changed (64) hide show
  1. package/README.md +11 -15
  2. package/dist/esm/chat-ui/chat-input.d.ts +57 -0
  3. package/dist/esm/chat-ui/chat-input.js +124 -0
  4. package/dist/esm/chat-ui/chat-input.js.map +1 -0
  5. package/dist/esm/chat-ui/chat-message.d.ts +91 -0
  6. package/dist/esm/chat-ui/chat-message.js +157 -0
  7. package/dist/esm/chat-ui/chat-message.js.map +1 -0
  8. package/dist/esm/chat-ui/chat-messages.d.ts +33 -0
  9. package/dist/esm/chat-ui/chat-messages.js +44 -0
  10. package/dist/esm/chat-ui/chat-messages.js.map +1 -0
  11. package/dist/esm/chat-ui/chat.d.ts +53 -0
  12. package/dist/esm/chat-ui/chat.js +57 -0
  13. package/dist/esm/chat-ui/chat.js.map +1 -0
  14. package/dist/esm/chat-ui/create-chat-hook.d.ts +48 -0
  15. package/dist/esm/chat-ui/create-chat-hook.js +57 -0
  16. package/dist/esm/chat-ui/create-chat-hook.js.map +1 -0
  17. package/dist/esm/chat-ui/create-ui-contexts.d.ts +29 -0
  18. package/dist/esm/chat-ui/create-ui-contexts.js +36 -0
  19. package/dist/esm/chat-ui/create-ui-contexts.js.map +1 -0
  20. package/dist/esm/chat-ui/create-ui.d.ts +153 -0
  21. package/dist/esm/chat-ui/create-ui.js +294 -0
  22. package/dist/esm/chat-ui/create-ui.js.map +1 -0
  23. package/dist/esm/chat-ui/markdown-plugins.d.ts +15 -0
  24. package/dist/esm/chat-ui/markdown-plugins.js +28 -0
  25. package/dist/esm/chat-ui/markdown-plugins.js.map +1 -0
  26. package/dist/esm/chat-ui/text-part.d.ts +57 -0
  27. package/dist/esm/chat-ui/text-part.js +46 -0
  28. package/dist/esm/chat-ui/text-part.js.map +1 -0
  29. package/dist/esm/chat-ui/thinking-part.d.ts +41 -0
  30. package/dist/esm/chat-ui/thinking-part.js +73 -0
  31. package/dist/esm/chat-ui/thinking-part.js.map +1 -0
  32. package/dist/esm/chat-ui/tool-approval.d.ts +52 -0
  33. package/dist/esm/chat-ui/tool-approval.js +83 -0
  34. package/dist/esm/chat-ui/tool-approval.js.map +1 -0
  35. package/dist/esm/chat-ui/tool-result-content.d.ts +18 -0
  36. package/dist/esm/chat-ui/tool-result-content.js +19 -0
  37. package/dist/esm/chat-ui/tool-result-content.js.map +1 -0
  38. package/dist/esm/create-chat-hook.d.ts +35 -0
  39. package/dist/esm/create-chat-hook.js +37 -0
  40. package/dist/esm/create-chat-hook.js.map +1 -0
  41. package/dist/esm/index.d.ts +3 -0
  42. package/dist/esm/index.js +3 -1
  43. package/dist/esm/ui.d.ts +10 -0
  44. package/dist/esm/ui.js +11 -0
  45. package/dist/esm/use-web-mcp-tools.d.ts +27 -0
  46. package/dist/esm/use-web-mcp-tools.js +36 -0
  47. package/dist/esm/use-web-mcp-tools.js.map +1 -0
  48. package/package.json +18 -4
  49. package/src/chat-ui/chat-input.tsx +177 -0
  50. package/src/chat-ui/chat-message.tsx +271 -0
  51. package/src/chat-ui/chat-messages.tsx +89 -0
  52. package/src/chat-ui/chat.tsx +98 -0
  53. package/src/chat-ui/create-chat-hook.tsx +103 -0
  54. package/src/chat-ui/create-ui-contexts.tsx +57 -0
  55. package/src/chat-ui/create-ui.tsx +761 -0
  56. package/src/chat-ui/markdown-plugins.ts +48 -0
  57. package/src/chat-ui/text-part.tsx +97 -0
  58. package/src/chat-ui/thinking-part.tsx +83 -0
  59. package/src/chat-ui/tool-approval.tsx +132 -0
  60. package/src/chat-ui/tool-result-content.ts +32 -0
  61. package/src/create-chat-hook.ts +67 -0
  62. package/src/index.ts +3 -0
  63. package/src/ui.ts +43 -0
  64. 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()