@tanstack/ai-solid 0.19.4 → 0.21.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,261 @@
1
+ import { For, Show } from 'solid-js'
2
+ import { ThinkingPart } from './thinking-part'
3
+ import type { JSX } from 'solid-js'
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
+ /** @deprecated Use `createChatUI()` Message instead. Deprecated in 0.8.0. Removed in 1.0.0. */
16
+ export interface ChatMessageProps {
17
+ /** The message to render */
18
+ message: UIMessage
19
+ /** Base CSS class name */
20
+ class?: string
21
+ /** Additional class for user messages */
22
+ userClass?: string
23
+ /** Additional class for assistant messages */
24
+ assistantClass?: string
25
+ /** Custom renderer for text parts */
26
+ textPartRenderer?: (props: { content: string }) => JSX.Element
27
+ /** Custom renderer for thinking parts */
28
+ thinkingPartRenderer?: (props: {
29
+ content: string
30
+ isComplete?: boolean
31
+ }) => JSX.Element
32
+ /** Named tool renderers - use the tool name as the key */
33
+ toolsRenderer?: Record<string, (props: ToolCallRenderProps) => JSX.Element>
34
+ /** Default tool renderer when tool name not found in toolsRenderer */
35
+ defaultToolRenderer?: (props: ToolCallRenderProps) => JSX.Element
36
+ /** Custom renderer for tool result parts */
37
+ toolResultRenderer?: (props: {
38
+ toolCallId: string
39
+ content: string
40
+ state: string
41
+ }) => JSX.Element
42
+ }
43
+
44
+ /**
45
+ * @deprecated Use `createChatUI()` Message instead. Deprecated in 0.8.0. Removed in 1.0.0.
46
+ *
47
+ * Message component - renders a single message with all its parts
48
+ *
49
+ * This component natively understands TanStack AI's parts-based message format:
50
+ * - thinking parts: rendered as collapsible thinking/reasoning sections (auto-collapses when complete)
51
+ * - text parts: rendered as content
52
+ * - tool-call parts: rendered with state, approvals, etc.
53
+ * - tool-result parts: rendered with results
54
+ *
55
+ * @example Basic usage
56
+ * ```tsx
57
+ * <Chat.Message message={message} />
58
+ * ```
59
+ *
60
+ * @example With role-based styling
61
+ * ```tsx
62
+ * <ChatMessage
63
+ * message={message}
64
+ * class="flex"
65
+ * userClass="justify-end"
66
+ * assistantClass="justify-start"
67
+ * />
68
+ * ```
69
+ *
70
+ * @example With custom thinking renderer
71
+ * ```tsx
72
+ * <ChatMessage
73
+ * message={message}
74
+ * thinkingPartRenderer={({ content, isComplete }) => (
75
+ * <details open={!isComplete}>
76
+ * <summary>Thinking...</summary>
77
+ * <pre>{content}</pre>
78
+ * </details>
79
+ * )}
80
+ * />
81
+ * ```
82
+ *
83
+ * @example With named tool renderers
84
+ * ```tsx
85
+ * <ChatMessage
86
+ * message={message}
87
+ * toolsRenderer={{
88
+ * recommendGuitar: ({ id, arguments: args }) => <GuitarCard {...JSON.parse(args)} />,
89
+ * weatherLookup: ({ id, arguments: args }) => <WeatherWidget {...JSON.parse(args)} />,
90
+ * }}
91
+ * defaultToolRenderer={() => null}
92
+ * />
93
+ * ```
94
+ */
95
+ export function ChatMessage(props: ChatMessageProps) {
96
+ // Combine classes based on role
97
+ const roleClass = () =>
98
+ props.message.role === 'user'
99
+ ? (props.userClass ?? '')
100
+ : (props.assistantClass ?? '')
101
+
102
+ const combinedClass = () =>
103
+ [props.class ?? '', roleClass()].filter(Boolean).join(' ')
104
+
105
+ return (
106
+ <div
107
+ class={combinedClass() || undefined}
108
+ data-message-id={props.message.id}
109
+ data-message-role={props.message.role}
110
+ data-message-created={props.message.createdAt?.toISOString()}
111
+ >
112
+ <For each={props.message.parts}>
113
+ {(part, index) => {
114
+ // Check if thinking is complete (if there's a text part after this thinking part)
115
+ const isThinkingComplete = () =>
116
+ part.type === 'thinking' &&
117
+ props.message.parts
118
+ .slice(index() + 1)
119
+ .some((p) => p.type === 'text')
120
+
121
+ return (
122
+ <MessagePart
123
+ part={part}
124
+ isThinkingComplete={isThinkingComplete()}
125
+ textPartRenderer={props.textPartRenderer}
126
+ thinkingPartRenderer={props.thinkingPartRenderer}
127
+ toolsRenderer={props.toolsRenderer}
128
+ defaultToolRenderer={props.defaultToolRenderer}
129
+ toolResultRenderer={props.toolResultRenderer}
130
+ />
131
+ )
132
+ }}
133
+ </For>
134
+ </div>
135
+ )
136
+ }
137
+
138
+ function MessagePart(props: {
139
+ part: any
140
+ isThinkingComplete?: boolean
141
+ textPartRenderer?: ChatMessageProps['textPartRenderer']
142
+ thinkingPartRenderer?: ChatMessageProps['thinkingPartRenderer']
143
+ toolsRenderer?: ChatMessageProps['toolsRenderer']
144
+ defaultToolRenderer?: ChatMessageProps['defaultToolRenderer']
145
+ toolResultRenderer?: ChatMessageProps['toolResultRenderer']
146
+ }) {
147
+ // Text part
148
+ if (props.part.type === 'text') {
149
+ if (props.textPartRenderer) {
150
+ return <>{props.textPartRenderer({ content: props.part.content })}</>
151
+ }
152
+ return (
153
+ <div data-part-type="text" data-part-content>
154
+ {props.part.content}
155
+ </div>
156
+ )
157
+ }
158
+
159
+ // Thinking part
160
+ if (props.part.type === 'thinking') {
161
+ if (props.thinkingPartRenderer) {
162
+ return (
163
+ <>
164
+ {props.thinkingPartRenderer({
165
+ content: props.part.content,
166
+ isComplete: props.isThinkingComplete,
167
+ })}
168
+ </>
169
+ )
170
+ }
171
+ return (
172
+ <ThinkingPart
173
+ content={props.part.content}
174
+ isComplete={props.isThinkingComplete}
175
+ />
176
+ )
177
+ }
178
+
179
+ // Tool call part
180
+ if (props.part.type === 'tool-call') {
181
+ const toolProps: ToolCallRenderProps = {
182
+ id: props.part.id,
183
+ name: props.part.name,
184
+ arguments: props.part.arguments,
185
+ state: props.part.state,
186
+ approval: props.part.approval,
187
+ output: props.part.output,
188
+ }
189
+
190
+ // Check if there's a specific renderer for this tool
191
+ if (props.toolsRenderer?.[props.part.name]) {
192
+ return <>{props.toolsRenderer[props.part.name]?.(toolProps)}</>
193
+ }
194
+
195
+ // Use default tool renderer if provided
196
+ if (props.defaultToolRenderer) {
197
+ return <>{props.defaultToolRenderer(toolProps)}</>
198
+ }
199
+
200
+ // Fallback to built-in default renderer
201
+ return (
202
+ <div
203
+ data-part-type="tool-call"
204
+ data-tool-name={props.part.name}
205
+ data-tool-state={props.part.state}
206
+ data-tool-id={props.part.id}
207
+ >
208
+ <div data-tool-header>
209
+ <strong>{props.part.name}</strong>
210
+ <span data-tool-state-badge>{props.part.state}</span>
211
+ </div>
212
+ <Show when={props.part.arguments}>
213
+ <div data-tool-arguments>
214
+ <pre>{props.part.arguments}</pre>
215
+ </div>
216
+ </Show>
217
+ <Show when={props.part.approval}>
218
+ <div data-tool-approval>
219
+ {props.part.approval.approved !== undefined
220
+ ? props.part.approval.approved
221
+ ? '✓ Approved'
222
+ : '✗ Denied'
223
+ : '⏳ Awaiting approval...'}
224
+ </div>
225
+ </Show>
226
+ <Show when={props.part.output}>
227
+ <div data-tool-output>
228
+ <pre>{JSON.stringify(props.part.output, null, 2)}</pre>
229
+ </div>
230
+ </Show>
231
+ </div>
232
+ )
233
+ }
234
+
235
+ // Tool result part
236
+ if (props.part.type === 'tool-result') {
237
+ if (props.toolResultRenderer) {
238
+ return (
239
+ <>
240
+ {props.toolResultRenderer({
241
+ toolCallId: props.part.toolCallId,
242
+ content: props.part.content,
243
+ state: props.part.state,
244
+ })}
245
+ </>
246
+ )
247
+ }
248
+
249
+ return (
250
+ <div
251
+ data-part-type="tool-result"
252
+ data-tool-call-id={props.part.toolCallId}
253
+ data-tool-result-state={props.part.state}
254
+ >
255
+ <div data-tool-result-content>{props.part.content}</div>
256
+ </div>
257
+ )
258
+ }
259
+
260
+ return null
261
+ }
@@ -0,0 +1,91 @@
1
+ import { For, Show, createEffect } from 'solid-js'
2
+ import { useChatContext } from './chat'
3
+ import { ChatMessage } from './chat-message'
4
+ import type { JSX } from 'solid-js'
5
+ import type { UIMessage } from '../types'
6
+
7
+ /** @deprecated Use `createChatUI()` Messages instead. Deprecated in 0.8.0. Removed in 1.0.0. */
8
+ export interface ChatMessagesProps {
9
+ /** Custom render function for each message */
10
+ children?: (message: UIMessage, index: number) => JSX.Element
11
+ /** CSS class name */
12
+ class?: string
13
+ /** Element to show when there are no messages */
14
+ emptyState?: JSX.Element
15
+ /** Element to show while loading the first message */
16
+ loadingState?: JSX.Element
17
+ /** Custom error renderer */
18
+ errorState?: (props: {
19
+ error: Error
20
+ reload: () => Promise<void>
21
+ }) => JSX.Element
22
+ /** Auto-scroll to bottom on new messages */
23
+ autoScroll?: boolean
24
+ }
25
+
26
+ /**
27
+ * @deprecated Use `createChatUI()` Messages instead. Deprecated in 0.8.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(props: ChatMessagesProps) {
39
+ const { messages, isLoading, error, reload } = useChatContext()
40
+ let containerRef: HTMLDivElement | undefined
41
+
42
+ // Auto-scroll to bottom on new messages
43
+ createEffect(() => {
44
+ // Track messages to trigger effect on change
45
+ messages()
46
+ if ((props.autoScroll ?? true) && containerRef) {
47
+ containerRef.scrollTop = containerRef.scrollHeight
48
+ }
49
+ })
50
+
51
+ return (
52
+ <Show
53
+ when={!error() || !props.errorState}
54
+ fallback={(() => {
55
+ const err = error()
56
+ return err ? <>{props.errorState?.({ error: err, reload })}</> : null
57
+ })()}
58
+ >
59
+ <Show
60
+ when={!isLoading() || messages().length > 0 || !props.loadingState}
61
+ fallback={<>{props.loadingState}</>}
62
+ >
63
+ <Show
64
+ when={messages().length > 0 || !props.emptyState}
65
+ fallback={<>{props.emptyState}</>}
66
+ >
67
+ <div
68
+ ref={(el) => {
69
+ containerRef = el
70
+ }}
71
+ class={props.class}
72
+ data-chat-messages
73
+ data-message-count={messages().length}
74
+ >
75
+ <For each={messages()}>
76
+ {(message, index) =>
77
+ props.children ? (
78
+ <div data-message-id={message.id}>
79
+ {props.children(message, index())}
80
+ </div>
81
+ ) : (
82
+ <ChatMessage message={message} />
83
+ )
84
+ }
85
+ </For>
86
+ </div>
87
+ </Show>
88
+ </Show>
89
+ </Show>
90
+ )
91
+ }
@@ -0,0 +1,95 @@
1
+ import { createContext, useContext } from 'solid-js'
2
+ import type { ConnectionAdapter } from '@tanstack/ai-client'
3
+ import { useChat } from '../use-chat'
4
+ import type { JSX } from 'solid-js'
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().useChat()` instead. Deprecated in 0.8.0. Removed in 1.0.0. */
17
+ export function useChatContext(): UseChatReturn {
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.8.0. Removed in 1.0.0. */
28
+ export interface ChatProps {
29
+ /** Child components (Chat.Messages, Chat.Input, etc.) */
30
+ children: JSX.Element
31
+ /** CSS class name for the root element */
32
+ class?: 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
+ /** Client-side tools with execute functions */
50
+ tools?: Array<any>
51
+ /** Custom tool components registry for rendering */
52
+ toolComponents?: Record<
53
+ string,
54
+ (props: { input: any; output?: any }) => JSX.Element
55
+ >
56
+ }
57
+
58
+ /**
59
+ * @deprecated Since 0.8.0. Use `createChatHook()` from `@tanstack/ai-solid/ui` instead.
60
+ * See https://tanstack.com/ai/latest/docs/ui/solid
61
+ * Removed in 1.0.0.
62
+ *
63
+ * Root Chat component - provides context for all chat subcomponents
64
+ *
65
+ * @example
66
+ * ```tsx
67
+ * <Chat connection={fetchServerSentEvents("/api/chat")}>
68
+ * <Chat.Messages />
69
+ * <Chat.Input />
70
+ * </Chat>
71
+ * ```
72
+ */
73
+ export function Chat(props: ChatProps) {
74
+ const chat = useChat({
75
+ connection: props.connection,
76
+ ...(props.initialMessages !== undefined && {
77
+ initialMessages: props.initialMessages,
78
+ }),
79
+ ...(props.id !== undefined && { id: props.id }),
80
+ ...(props.body !== undefined && { body: props.body }),
81
+ ...(props.onResponse !== undefined && { onResponse: props.onResponse }),
82
+ ...(props.onChunk !== undefined && { onChunk: props.onChunk }),
83
+ ...(props.onFinish !== undefined && { onFinish: props.onFinish }),
84
+ ...(props.onError !== undefined && { onError: props.onError }),
85
+ ...(props.tools !== undefined && { tools: props.tools }),
86
+ })
87
+
88
+ return (
89
+ <ChatContext.Provider value={chat}>
90
+ <div class={props.class} data-chat-root>
91
+ {props.children}
92
+ </div>
93
+ </ChatContext.Provider>
94
+ )
95
+ }
@@ -0,0 +1,77 @@
1
+ import type { Component } from 'solid-js'
2
+ import type { InferredClientContext } from '@tanstack/ai-client'
3
+ import type {
4
+ ChatUIInterruptsOf,
5
+ ChatUISchemaOf,
6
+ ChatUIToolsOf,
7
+ } from '@tanstack/ai-client/ui'
8
+ import { useChat as useUnboundChat } from '../use-chat'
9
+ import type { UseChatOptions } from '../types'
10
+ import { createChatUI } from './create-ui'
11
+ import type { ChatUIFactoryConfig, ChatUIHost, InputProps } from './create-ui'
12
+
13
+ type HeadlessOptions<TOptions> = UseChatOptions<
14
+ ChatUIToolsOf<TOptions>,
15
+ ChatUISchemaOf<TOptions>,
16
+ InferredClientContext<ChatUIToolsOf<TOptions>>,
17
+ ChatUIInterruptsOf<TOptions>
18
+ >
19
+
20
+ type ChatInstanceOverrides<TOptions> = {
21
+ threadId?: string
22
+ live?: boolean
23
+ forwardedProps?: Record<string, any>
24
+ body?: Record<string, any>
25
+ initialMessages?: HeadlessOptions<TOptions>['initialMessages']
26
+ }
27
+
28
+ /**
29
+ * Bind chat options and UI widgets once at module scope.
30
+ *
31
+ * This matches Form `createFormHook` and Table `createTableHook`. The
32
+ * returned `useAppChat` creates a chat instance. `chat.AppChat` renders it.
33
+ * Context `useChatContext` reads that instance. Part and interrupt
34
+ * widgets take `part` / `interrupt` as props.
35
+ *
36
+ * Pass instance overrides such as `threadId` into `useAppChat()`.
37
+ */
38
+ export function createChatHook<
39
+ const TOptions,
40
+ TInput extends Component<any> | undefined =
41
+ | Component<InputProps<NoInfer<TOptions>>>
42
+ | undefined,
43
+ >({
44
+ options,
45
+ ...chatComponents
46
+ }: {
47
+ options: TOptions
48
+ } & ChatUIFactoryConfig<NoInfer<TOptions>, TInput>) {
49
+ const ui = createChatUI(
50
+ options,
51
+ chatComponents as ChatUIFactoryConfig<NoInfer<TOptions>, TInput>,
52
+ )
53
+
54
+ function useAppChat(overrides?: ChatInstanceOverrides<TOptions>) {
55
+ const chat = (
56
+ overrides
57
+ ? useUnboundChat({
58
+ ...(options as HeadlessOptions<TOptions>),
59
+ ...overrides,
60
+ })
61
+ : useUnboundChat(options as HeadlessOptions<TOptions>)
62
+ ) as ChatUIHost<TOptions>
63
+
64
+ function AppChat() {
65
+ return <ui.Chat chat={chat} />
66
+ }
67
+
68
+ return Object.assign(chat, { AppChat }) as ChatUIHost<TOptions> & {
69
+ AppChat: Component
70
+ }
71
+ }
72
+
73
+ return {
74
+ useAppChat,
75
+ useChatContext: ui.useChatContext,
76
+ }
77
+ }
@@ -0,0 +1,52 @@
1
+ import { createContext, useContext } from 'solid-js'
2
+ import type { Context } from 'solid-js'
3
+ import type {
4
+ ChatUIInterrupt,
5
+ ChatUISelectedPart,
6
+ } from '@tanstack/ai-client/ui'
7
+ import type { UseChatReturn } from '../types'
8
+
9
+ export interface ChatUIContexts {
10
+ chatContext: Context<UseChatReturn<any, any, any> | undefined>
11
+ partContext: Context<ChatUISelectedPart | undefined>
12
+ interruptContext: Context<ChatUIInterrupt | undefined>
13
+ useChatContext: () => UseChatReturn<any, any, any>
14
+ }
15
+
16
+ /**
17
+ * Create a fresh set of chat UI contexts. This matches Form
18
+ * `createFormHookContexts` and Table `createTableHookContexts`.
19
+ *
20
+ * Most apps can skip this. `createChatHook` uses shared module contexts by
21
+ * default. Call this when a widget file cannot import the `createChatHook`
22
+ * result, or when nested chats need isolated providers.
23
+ *
24
+ * Prefer `useChatContext` from `createChatHook` when you can import that
25
+ * result. Part and interrupt widgets take `part` / `interrupt` as props.
26
+ */
27
+ export function createChatHookContexts(): ChatUIContexts {
28
+ const chatContext = createContext<UseChatReturn<any, any, any> | undefined>(
29
+ undefined,
30
+ )
31
+ const partContext = createContext<ChatUISelectedPart | undefined>(undefined)
32
+ const interruptContext = createContext<ChatUIInterrupt | undefined>(undefined)
33
+
34
+ function useChatContext() {
35
+ const chat = useContext(chatContext)
36
+ if (!chat) {
37
+ throw new Error(
38
+ '`useChatContext` must be used within `UI.Provider` or `UI.Chat`.',
39
+ )
40
+ }
41
+ return chat
42
+ }
43
+
44
+ return {
45
+ chatContext,
46
+ partContext,
47
+ interruptContext,
48
+ useChatContext,
49
+ }
50
+ }
51
+
52
+ export const defaultChatUIContexts = createChatHookContexts()