@tanstack/ai-solid 0.19.4 → 0.20.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,39 @@
1
+ import { UseChatOptions, UseChatReturn } from "./types.js";
2
+ import { InferredClientContext } from "@tanstack/ai-client";
3
+ import { AnyClientTool, InterruptDefinition, SchemaInput } from "@tanstack/ai";
4
+
5
+ //#region src/create-chat-hook.d.ts
6
+ type ChatHookOverrides<TTools extends ReadonlyArray<AnyClientTool>, TSchema extends SchemaInput | undefined, TContext, TInterrupts extends ReadonlyArray<InterruptDefinition<any, any, any, any>>> = {
7
+ threadId?: string;
8
+ live?: boolean;
9
+ forwardedProps?: Record<string, any>;
10
+ body?: Record<string, any>;
11
+ initialMessages?: UseChatOptions<TTools, TSchema, TContext, TInterrupts>['initialMessages'];
12
+ };
13
+ /**
14
+ * Bind chat options once at module scope. The returned `useChat` primitive
15
+ * creates a chat instance from those options.
16
+ *
17
+ * Pass per-call overrides for instance keys such as `threadId`,
18
+ * `initialMessages`, `live`, and `forwardedProps`. Do not change `tools`,
19
+ * `interrupts`, or `outputSchema` here. Those stay on the factory options.
20
+ *
21
+ * Rename the primitive at the call site if you already import `useChat` from
22
+ * `@tanstack/ai-solid`: `const { useChat: useAppChat } = createChatHook(chatOptions)`.
23
+ *
24
+ * @example
25
+ * ```tsx
26
+ * const { useChat } = createChatHook(chatOptions)
27
+ *
28
+ * function Support() {
29
+ * const chat = useChat({ threadId: 'support-1' })
30
+ * return <UI.Chat chat={chat} components={components} />
31
+ * }
32
+ * ```
33
+ */
34
+ declare function createChatHook<const TTools extends ReadonlyArray<AnyClientTool> = any, TSchema extends SchemaInput | undefined = undefined, TContext = InferredClientContext<TTools>, const TInterrupts extends ReadonlyArray<InterruptDefinition<any, any, any, any>> = readonly []>(options: UseChatOptions<TTools, TSchema, TContext, TInterrupts>): {
35
+ useChat: (overrides?: ChatHookOverrides<TTools, TSchema, TContext, TInterrupts>) => UseChatReturn<TTools, TSchema, TInterrupts>;
36
+ };
37
+ //#endregion
38
+ export { createChatHook };
39
+ //# sourceMappingURL=create-chat-hook.d.ts.map
@@ -0,0 +1,38 @@
1
+ import { useChat } from "./use-chat.js";
2
+
3
+ //#region src/create-chat-hook.ts
4
+ /**
5
+ * Bind chat options once at module scope. The returned `useChat` primitive
6
+ * creates a chat instance from those options.
7
+ *
8
+ * Pass per-call overrides for instance keys such as `threadId`,
9
+ * `initialMessages`, `live`, and `forwardedProps`. Do not change `tools`,
10
+ * `interrupts`, or `outputSchema` here. Those stay on the factory options.
11
+ *
12
+ * Rename the primitive at the call site if you already import `useChat` from
13
+ * `@tanstack/ai-solid`: `const { useChat: useAppChat } = createChatHook(chatOptions)`.
14
+ *
15
+ * @example
16
+ * ```tsx
17
+ * const { useChat } = createChatHook(chatOptions)
18
+ *
19
+ * function Support() {
20
+ * const chat = useChat({ threadId: 'support-1' })
21
+ * return <UI.Chat chat={chat} components={components} />
22
+ * }
23
+ * ```
24
+ */
25
+ function createChatHook(options) {
26
+ function useChat$1(overrides) {
27
+ if (!overrides) return useChat(options);
28
+ return useChat({
29
+ ...options,
30
+ ...overrides
31
+ });
32
+ }
33
+ return { useChat: useChat$1 };
34
+ }
35
+
36
+ //#endregion
37
+ export { createChatHook };
38
+ //# sourceMappingURL=create-chat-hook.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"create-chat-hook.js","names":["useChat","useUnboundChat"],"sources":["../src/create-chat-hook.ts"],"sourcesContent":["import { useChat as useUnboundChat } from './use-chat'\nimport type {\n AnyClientTool,\n InterruptDefinition,\n SchemaInput,\n} from '@tanstack/ai'\nimport type { InferredClientContext } from '@tanstack/ai-client'\nimport type { UseChatOptions } from './types'\n\ntype ChatHookOverrides<\n TTools extends ReadonlyArray<AnyClientTool>,\n TSchema extends SchemaInput | undefined,\n TContext,\n TInterrupts extends ReadonlyArray<InterruptDefinition<any, any, any, any>>,\n> = {\n threadId?: string\n live?: boolean\n forwardedProps?: Record<string, any>\n body?: Record<string, any>\n initialMessages?: UseChatOptions<\n TTools,\n TSchema,\n TContext,\n TInterrupts\n >['initialMessages']\n}\n\n/**\n * Bind chat options once at module scope. The returned `useChat` primitive\n * creates a chat instance from those options.\n *\n * Pass per-call overrides for instance keys such as `threadId`,\n * `initialMessages`, `live`, and `forwardedProps`. Do not change `tools`,\n * `interrupts`, or `outputSchema` here. Those stay on the factory options.\n *\n * Rename the primitive at the call site if you already import `useChat` from\n * `@tanstack/ai-solid`: `const { useChat: useAppChat } = createChatHook(chatOptions)`.\n *\n * @example\n * ```tsx\n * const { useChat } = createChatHook(chatOptions)\n *\n * function Support() {\n * const chat = useChat({ threadId: 'support-1' })\n * return <UI.Chat chat={chat} components={components} />\n * }\n * ```\n */\nexport function createChatHook<\n const TTools extends ReadonlyArray<AnyClientTool> = any,\n TSchema extends SchemaInput | undefined = undefined,\n TContext = InferredClientContext<TTools>,\n const TInterrupts extends ReadonlyArray<\n InterruptDefinition<any, any, any, any>\n > = readonly [],\n>(options: UseChatOptions<TTools, TSchema, TContext, TInterrupts>) {\n function useChat(\n overrides?: ChatHookOverrides<TTools, TSchema, TContext, TInterrupts>,\n ) {\n if (!overrides) {\n return useUnboundChat(options)\n }\n return useUnboundChat({ ...options, ...overrides })\n }\n\n return { useChat }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;AAgDA,SAAgB,eAOd,SAAiE;CACjE,SAASA,UACP,WACA;AACA,MAAI,CAAC,UACH,QAAOC,QAAe,QAAQ;AAEhC,SAAOA,QAAe;GAAE,GAAG;GAAS,GAAG;GAAW,CAAC;;AAGrD,QAAO,EAAE,oBAAS"}
package/dist/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { ChatRequestBody, DeepPartial, QueueConfig, QueueOption, QueueStrategy, QueuedMessage, SendMessageOptions, UIMessage, UseChatOptions, UseChatReturn, WhenBusy } from "./types.js";
2
2
  import { useChat } from "./use-chat.js";
3
+ import { createChatHook } from "./create-chat-hook.js";
3
4
  import { useByok } from "./use-byok.js";
4
5
  import { UseGenerationOptions, UseGenerationReturn, useGeneration } from "./use-generation.js";
5
6
  import { UseGenerateImageOptions, UseGenerateImageReturn, useGenerateImage } from "./use-generate-image.js";
@@ -10,4 +11,4 @@ import { UseSummarizeOptions, UseSummarizeReturn, useSummarize } from "./use-sum
10
11
  import { UseGenerateVideoOptions, UseGenerateVideoReturn, useGenerateVideo } from "./use-generate-video.js";
11
12
  import { UseAudioRecorderOptions, UseAudioRecorderReturn, useAudioRecorder } from "./use-audio-recorder.js";
12
13
  import { AudioGenerateInput, ChatClientPersistence, ChatPersistedState, ChatPersistenceOption, ChatStorageAdapter, ConnectConnectionAdapter, ConnectionAdapter, FetchConnectionOptions, GenerationClientState, ImageGenerateInput, IndexedDBPersistenceOptions, InferChatMessages, RunAgentInputContext, SpeechGenerateInput, StorageUnavailableError, SubscribeConnectionAdapter, SummarizeGenerateInput, TranscriptionGenerateInput, VideoGenerateInput, VideoGenerateResult, VideoStatusInfo, WebSocketConnectionOptions, WebStoragePersistenceOptions, XhrConnectionOptions, createChatClientOptions, fetchHttpStream, fetchServerSentEvents, indexedDBPersistence, localStoragePersistence, rpcStream, sessionStoragePersistence, stream, webSocket, xhrHttpStream, xhrServerSentEvents } from "@tanstack/ai-client";
13
- export { type AudioGenerateInput, type ChatClientPersistence, type ChatPersistedState, type ChatPersistenceOption, type ChatRequestBody, type ChatStorageAdapter, type ConnectConnectionAdapter, type ConnectionAdapter, type DeepPartial, type FetchConnectionOptions, type GenerationClientState, type ImageGenerateInput, type IndexedDBPersistenceOptions, type InferChatMessages, type QueueConfig, type QueueOption, type QueueStrategy, type QueuedMessage, type RunAgentInputContext, type SendMessageOptions, type SpeechGenerateInput, StorageUnavailableError, type SubscribeConnectionAdapter, type SummarizeGenerateInput, type TranscriptionGenerateInput, type UIMessage, type UseAudioRecorderOptions, type UseAudioRecorderReturn, type UseChatOptions, type UseChatReturn, type UseGenerateAudioOptions, type UseGenerateAudioReturn, type UseGenerateImageOptions, type UseGenerateImageReturn, type UseGenerateSpeechOptions, type UseGenerateSpeechReturn, type UseGenerateVideoOptions, type UseGenerateVideoReturn, type UseGenerationOptions, type UseGenerationReturn, type UseSummarizeOptions, type UseSummarizeReturn, type UseTranscriptionOptions, type UseTranscriptionReturn, type VideoGenerateInput, type VideoGenerateResult, type VideoStatusInfo, type WebSocketConnectionOptions, type WebStoragePersistenceOptions, type WhenBusy, type XhrConnectionOptions, createChatClientOptions, fetchHttpStream, fetchServerSentEvents, indexedDBPersistence, localStoragePersistence, rpcStream, sessionStoragePersistence, stream, useAudioRecorder, useByok, useChat, useGenerateAudio, useGenerateImage, useGenerateSpeech, useGenerateVideo, useGeneration, useSummarize, useTranscription, webSocket, xhrHttpStream, xhrServerSentEvents };
14
+ export { type AudioGenerateInput, type ChatClientPersistence, type ChatPersistedState, type ChatPersistenceOption, type ChatRequestBody, type ChatStorageAdapter, type ConnectConnectionAdapter, type ConnectionAdapter, type DeepPartial, type FetchConnectionOptions, type GenerationClientState, type ImageGenerateInput, type IndexedDBPersistenceOptions, type InferChatMessages, type QueueConfig, type QueueOption, type QueueStrategy, type QueuedMessage, type RunAgentInputContext, type SendMessageOptions, type SpeechGenerateInput, StorageUnavailableError, type SubscribeConnectionAdapter, type SummarizeGenerateInput, type TranscriptionGenerateInput, type UIMessage, type UseAudioRecorderOptions, type UseAudioRecorderReturn, type UseChatOptions, type UseChatReturn, type UseGenerateAudioOptions, type UseGenerateAudioReturn, type UseGenerateImageOptions, type UseGenerateImageReturn, type UseGenerateSpeechOptions, type UseGenerateSpeechReturn, type UseGenerateVideoOptions, type UseGenerateVideoReturn, type UseGenerationOptions, type UseGenerationReturn, type UseSummarizeOptions, type UseSummarizeReturn, type UseTranscriptionOptions, type UseTranscriptionReturn, type VideoGenerateInput, type VideoGenerateResult, type VideoStatusInfo, type WebSocketConnectionOptions, type WebStoragePersistenceOptions, type WhenBusy, type XhrConnectionOptions, createChatClientOptions, createChatHook, fetchHttpStream, fetchServerSentEvents, indexedDBPersistence, localStoragePersistence, rpcStream, sessionStoragePersistence, stream, useAudioRecorder, useByok, useChat, useGenerateAudio, useGenerateImage, useGenerateSpeech, useGenerateVideo, useGeneration, useSummarize, useTranscription, webSocket, xhrHttpStream, xhrServerSentEvents };
package/dist/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { useChat } from "./use-chat.js";
2
+ import { createChatHook } from "./create-chat-hook.js";
2
3
  import { useByok } from "./use-byok.js";
3
4
  import { useGeneration } from "./use-generation.js";
4
5
  import { useGenerateImage } from "./use-generate-image.js";
@@ -10,4 +11,4 @@ import { useGenerateVideo } from "./use-generate-video.js";
10
11
  import { useAudioRecorder } from "./use-audio-recorder.js";
11
12
  import { StorageUnavailableError, createChatClientOptions, fetchHttpStream, fetchServerSentEvents, indexedDBPersistence, localStoragePersistence, rpcStream, sessionStoragePersistence, stream, webSocket, xhrHttpStream, xhrServerSentEvents } from "@tanstack/ai-client";
12
13
 
13
- export { StorageUnavailableError, createChatClientOptions, fetchHttpStream, fetchServerSentEvents, indexedDBPersistence, localStoragePersistence, rpcStream, sessionStoragePersistence, stream, useAudioRecorder, useByok, useChat, useGenerateAudio, useGenerateImage, useGenerateSpeech, useGenerateVideo, useGeneration, useSummarize, useTranscription, webSocket, xhrHttpStream, xhrServerSentEvents };
14
+ export { StorageUnavailableError, createChatClientOptions, createChatHook, fetchHttpStream, fetchServerSentEvents, indexedDBPersistence, localStoragePersistence, rpcStream, sessionStoragePersistence, stream, useAudioRecorder, useByok, useChat, useGenerateAudio, useGenerateImage, useGenerateSpeech, useGenerateVideo, useGeneration, useSummarize, useTranscription, webSocket, xhrHttpStream, xhrServerSentEvents };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tanstack/ai-solid",
3
- "version": "0.19.4",
3
+ "version": "0.20.0",
4
4
  "description": "Solid hooks for TanStack AI streaming chat, structured outputs, and media generation.",
5
5
  "author": "Tanner Linsley",
6
6
  "license": "MIT",
@@ -24,6 +24,11 @@
24
24
  ".": {
25
25
  "types": "./dist/index.d.ts",
26
26
  "import": "./dist/index.js"
27
+ },
28
+ "./ui": {
29
+ "solid": "./src/ui.ts",
30
+ "types": "./src/ui.ts",
31
+ "import": "./src/ui.ts"
27
32
  }
28
33
  },
29
34
  "files": [
@@ -45,11 +50,16 @@
45
50
  "media-generation"
46
51
  ],
47
52
  "dependencies": {
48
- "@tanstack/ai-client": "^0.29.2"
53
+ "rehype-highlight": "^7.0.2",
54
+ "rehype-raw": "^7.0.0",
55
+ "rehype-sanitize": "^6.0.0",
56
+ "remark-gfm": "^4.0.1",
57
+ "solid-markdown": "^2.1.0",
58
+ "@tanstack/ai-client": "^0.30.0"
49
59
  },
50
60
  "peerDependencies": {
51
61
  "solid-js": ">=1.9.10",
52
- "@tanstack/ai": "^0.52.0"
62
+ "@tanstack/ai": "^0.52.1"
53
63
  },
54
64
  "devDependencies": {
55
65
  "@solidjs/testing-library": "^0.8.10",
@@ -60,8 +70,10 @@
60
70
  "solid-js": "^1.9.10",
61
71
  "tsdown": "^0.17.0-beta.6",
62
72
  "typescript": "5.9.3",
73
+ "vite": "^8.2.1",
74
+ "vite-plugin-solid": "^2.11.14",
63
75
  "vitest": "^4.1.10",
64
- "@tanstack/ai": "0.52.0"
76
+ "@tanstack/ai": "0.52.1"
65
77
  },
66
78
  "scripts": {
67
79
  "clean": "premove ./build ./dist",
@@ -0,0 +1,168 @@
1
+ import { createSignal } from 'solid-js'
2
+ import { useChatContext } from './chat'
3
+ import type { JSX } from 'solid-js'
4
+
5
+ export interface ChatInputRenderProps {
6
+ /** Current input value */
7
+ value: string
8
+ /** Set input value */
9
+ onChange: (value: string) => void
10
+ /** Submit the message */
11
+ onSubmit: () => void
12
+ /** Is the chat currently loading */
13
+ isLoading: boolean
14
+ /** Is input disabled */
15
+ disabled: boolean
16
+ /** Ref callback for the input element */
17
+ ref: (el: HTMLInputElement | HTMLTextAreaElement) => void
18
+ }
19
+
20
+ /** @deprecated Use `createChatUI()` and an application-owned input component. Deprecated in 0.8.0. Removed in 1.0.0. */
21
+ export interface ChatInputProps {
22
+ /** Render prop for full control */
23
+ children?: (props: ChatInputRenderProps) => JSX.Element
24
+ /** CSS class name */
25
+ class?: string
26
+ /** Placeholder text */
27
+ placeholder?: string
28
+ /** Disable input */
29
+ disabled?: boolean
30
+ /** Submit on Enter (Shift+Enter for new line) */
31
+ submitOnEnter?: boolean
32
+ }
33
+
34
+ /**
35
+ * @deprecated Use `createChatUI()` and an application-owned input component. Deprecated in 0.8.0. Removed in 1.0.0.
36
+ *
37
+ * Chat input component - handles message input and submission
38
+ *
39
+ * Features:
40
+ * - Auto-growing textarea
41
+ * - Submit on Enter (Shift+Enter for new line)
42
+ * - Loading state management
43
+ * - Full render prop support for custom UIs
44
+ *
45
+ * @example
46
+ * ```tsx
47
+ * <Chat.Input placeholder="Type your message..." />
48
+ * ```
49
+ *
50
+ * @example Custom UI with render prop
51
+ * ```tsx
52
+ * <Chat.Input>
53
+ * {({ value, onChange, onSubmit, isLoading }) => (
54
+ * <div>
55
+ * <textarea value={value} onInput={(e) => onChange(e.target.value)} />
56
+ * <button onClick={onSubmit} disabled={isLoading}>Send</button>
57
+ * </div>
58
+ * )}
59
+ * </Chat.Input>
60
+ * ```
61
+ */
62
+ export function ChatInput(props: ChatInputProps) {
63
+ const { sendMessage, isLoading } = useChatContext()
64
+ const [value, setValue] = createSignal('')
65
+
66
+ const disabled = () => props.disabled || isLoading()
67
+
68
+ const handleSubmit = () => {
69
+ if (!value().trim() || disabled()) return
70
+ void sendMessage(value())
71
+ setValue('')
72
+ }
73
+
74
+ const renderProps = (): ChatInputRenderProps => ({
75
+ value: value(),
76
+ onChange: setValue,
77
+ onSubmit: handleSubmit,
78
+ isLoading: isLoading(),
79
+ disabled: disabled(),
80
+
81
+ ref: () => {},
82
+ })
83
+
84
+ // Render prop pattern
85
+ if (props.children) {
86
+ return <>{props.children(renderProps())}</>
87
+ }
88
+
89
+ // Default implementation
90
+ return (
91
+ <div
92
+ class={props.class}
93
+ data-chat-input
94
+ style={{
95
+ display: 'flex',
96
+ gap: '0.75rem',
97
+ 'align-items': 'center',
98
+ width: '100%',
99
+ }}
100
+ >
101
+ <input
102
+ type="text"
103
+ value={value()}
104
+ onInput={(e) => setValue(e.currentTarget.value)}
105
+ onKeyDown={(e) => {
106
+ if ((props.submitOnEnter ?? true) && e.key === 'Enter') {
107
+ e.preventDefault()
108
+ handleSubmit()
109
+ }
110
+ }}
111
+ placeholder={props.placeholder ?? 'Type a message...'}
112
+ disabled={disabled()}
113
+ data-chat-textarea
114
+ style={{
115
+ flex: 1,
116
+ padding: '0.75rem 1rem',
117
+ 'font-size': '0.875rem',
118
+ border: '1px solid rgba(255, 255, 255, 0.1)',
119
+ 'border-radius': '0.75rem',
120
+ 'background-color': 'rgba(31, 41, 55, 0.5)',
121
+ color: 'white',
122
+ outline: 'none',
123
+ transition: 'all 0.2s',
124
+ }}
125
+ onFocus={(e) => {
126
+ e.currentTarget.style.borderColor = 'rgba(249, 115, 22, 0.4)'
127
+ e.currentTarget.style.boxShadow = '0 0 0 2px rgba(249, 115, 22, 0.2)'
128
+ }}
129
+ onBlur={(e) => {
130
+ e.currentTarget.style.borderColor = 'rgba(255, 255, 255, 0.1)'
131
+ e.currentTarget.style.boxShadow = 'none'
132
+ }}
133
+ />
134
+ <button
135
+ onClick={handleSubmit}
136
+ disabled={disabled() || !value().trim()}
137
+ data-chat-submit
138
+ style={{
139
+ padding: '0.75rem 1.5rem',
140
+ 'font-size': '0.875rem',
141
+ 'font-weight': 500,
142
+ color: 'white',
143
+ 'background-color':
144
+ disabled() || !value().trim()
145
+ ? 'rgba(107, 114, 128, 0.5)'
146
+ : 'rgb(249, 115, 22)',
147
+ border: 'none',
148
+ 'border-radius': '0.75rem',
149
+ cursor: disabled() || !value().trim() ? 'not-allowed' : 'pointer',
150
+ transition: 'all 0.2s',
151
+ 'white-space': 'nowrap',
152
+ }}
153
+ onMouseEnter={(e) => {
154
+ if (!disabled() && value().trim()) {
155
+ e.currentTarget.style.backgroundColor = 'rgb(234, 88, 12)'
156
+ }
157
+ }}
158
+ onMouseLeave={(e) => {
159
+ if (!disabled() && value().trim()) {
160
+ e.currentTarget.style.backgroundColor = 'rgb(249, 115, 22)'
161
+ }
162
+ }}
163
+ >
164
+ {isLoading() ? 'Sending...' : 'Send'}
165
+ </button>
166
+ </div>
167
+ )
168
+ }
@@ -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
+ }