@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.
- package/dist/create-chat-hook.d.ts +39 -0
- package/dist/create-chat-hook.js +38 -0
- package/dist/create-chat-hook.js.map +1 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.js +3 -1
- package/dist/types.d.ts +4 -4
- package/dist/use-chat.d.ts +2 -2
- package/dist/use-web-mcp-tools.d.ts +26 -0
- package/dist/use-web-mcp-tools.js +31 -0
- package/dist/use-web-mcp-tools.js.map +1 -0
- package/package.json +16 -4
- package/src/chat-ui/chat-input.tsx +168 -0
- package/src/chat-ui/chat-message.tsx +261 -0
- package/src/chat-ui/chat-messages.tsx +91 -0
- package/src/chat-ui/chat.tsx +95 -0
- package/src/chat-ui/create-chat-hook.tsx +77 -0
- package/src/chat-ui/create-ui-contexts.tsx +52 -0
- package/src/chat-ui/create-ui.tsx +718 -0
- package/src/chat-ui/markdown-plugins.ts +48 -0
- package/src/chat-ui/text-part.tsx +71 -0
- package/src/chat-ui/thinking-part.tsx +79 -0
- package/src/chat-ui/tool-approval.tsx +121 -0
- package/src/create-chat-hook.ts +67 -0
- package/src/index.ts +3 -0
- package/src/ui.ts +39 -0
- package/src/use-web-mcp-tools.ts +55 -0
|
@@ -0,0 +1,718 @@
|
|
|
1
|
+
import { For, createContext, useContext } from 'solid-js'
|
|
2
|
+
import type { Accessor, Component, Context, JSX } from 'solid-js'
|
|
3
|
+
import {
|
|
4
|
+
automaticPartsForMessage,
|
|
5
|
+
collectInlineToolNames,
|
|
6
|
+
resolveInterruptComponent,
|
|
7
|
+
selectChatUI,
|
|
8
|
+
selectMessageUI,
|
|
9
|
+
} from '@tanstack/ai-client/ui'
|
|
10
|
+
import type {
|
|
11
|
+
ChatUIData,
|
|
12
|
+
ChatUIHasNamedInterrupts,
|
|
13
|
+
ChatUIHasNamedTools,
|
|
14
|
+
ChatUIInterrupt,
|
|
15
|
+
ChatUIInterruptName,
|
|
16
|
+
ChatUIInterruptOf,
|
|
17
|
+
ChatUIInterruptsOf,
|
|
18
|
+
ChatUINamedInterruptId,
|
|
19
|
+
ChatUIPartKey,
|
|
20
|
+
ChatUIPartOf,
|
|
21
|
+
ChatUISchemaOf,
|
|
22
|
+
ChatUISelectedPart,
|
|
23
|
+
ChatUISelectedPartOf,
|
|
24
|
+
ChatUIToolApproval,
|
|
25
|
+
ChatUIToolName,
|
|
26
|
+
ChatUIToolsOf,
|
|
27
|
+
} from '@tanstack/ai-client/ui'
|
|
28
|
+
import type {
|
|
29
|
+
MessagePart,
|
|
30
|
+
QueuedMessage,
|
|
31
|
+
ToolCallPart,
|
|
32
|
+
ToolResultPart,
|
|
33
|
+
UIMessage,
|
|
34
|
+
} from '@tanstack/ai-client'
|
|
35
|
+
import type { UseChatReturn } from '../types'
|
|
36
|
+
import { defaultChatUIContexts } from './create-ui-contexts'
|
|
37
|
+
import type { ChatUIContexts } from './create-ui-contexts'
|
|
38
|
+
|
|
39
|
+
export type ChatUIHost<TOptions = unknown> = UseChatReturn<
|
|
40
|
+
ChatUIToolsOf<TOptions>,
|
|
41
|
+
ChatUISchemaOf<TOptions>,
|
|
42
|
+
ChatUIInterruptsOf<TOptions>
|
|
43
|
+
>
|
|
44
|
+
|
|
45
|
+
export type ChatUIQueueItem = QueuedMessage & {
|
|
46
|
+
cancelQueued: () => void
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export type LayoutProps<
|
|
50
|
+
TOptions,
|
|
51
|
+
TInput extends Component<any> | undefined = Component<InputProps<TOptions>>,
|
|
52
|
+
> = {
|
|
53
|
+
Messages: Component
|
|
54
|
+
Interrupts: Component
|
|
55
|
+
Queue: Component
|
|
56
|
+
readonly __ui?: TOptions
|
|
57
|
+
} & (TInput extends Component<any> ? { Input: Component } : {})
|
|
58
|
+
|
|
59
|
+
export type MessageProps<TOptions> = {
|
|
60
|
+
message: UIMessage<ChatUIToolsOf<TOptions>, ChatUIData<TOptions>>
|
|
61
|
+
Parts: Component
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export type InputProps<TOptions> = {
|
|
65
|
+
readonly __ui?: TOptions
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export type QueueProps<TOptions> = {
|
|
69
|
+
item: ChatUIQueueItem
|
|
70
|
+
readonly __ui?: TOptions
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export type PartProps<TOptions, TKey extends ChatUIPartKey = ChatUIPartKey> = {
|
|
74
|
+
part: ChatUIPartOf<TOptions, TKey>
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export type ToolProps<
|
|
78
|
+
TOptions,
|
|
79
|
+
TName extends ChatUIToolName<TOptions> = ChatUIToolName<TOptions>,
|
|
80
|
+
> = {
|
|
81
|
+
part: Extract<ToolCallPart<ChatUIToolsOf<TOptions>>, { name: TName }>
|
|
82
|
+
result?: ToolResultPart
|
|
83
|
+
interrupt?: ChatUIToolApproval<TOptions, TName>
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export type InterruptProps<
|
|
87
|
+
TOptions,
|
|
88
|
+
TName extends ChatUIInterruptName<TOptions> = never,
|
|
89
|
+
> = {
|
|
90
|
+
interrupt: ChatUIInterruptOf<TOptions, TName>
|
|
91
|
+
readonly __ui?: TOptions
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
type GenericInterruptComponents<TOptions> =
|
|
95
|
+
ChatUIHasNamedInterrupts<TOptions> extends true
|
|
96
|
+
? {
|
|
97
|
+
[K in ChatUINamedInterruptId<TOptions>]: Component<
|
|
98
|
+
InterruptProps<TOptions, K & ChatUIInterruptName<TOptions>>
|
|
99
|
+
>
|
|
100
|
+
} & {
|
|
101
|
+
fallback?: Component<InterruptProps<TOptions>>
|
|
102
|
+
}
|
|
103
|
+
: {
|
|
104
|
+
fallback?: Component<InterruptProps<TOptions>>
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
type ToolApprovalMap<TOptions> = {
|
|
108
|
+
[K in ChatUIToolName<TOptions>]?: Component<
|
|
109
|
+
InterruptProps<TOptions, K & ChatUIInterruptName<TOptions>>
|
|
110
|
+
>
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** The chrome around the message list: `layout`, `message`, and `input`. */
|
|
114
|
+
export type ChatUIChromeComponents<
|
|
115
|
+
TOptions,
|
|
116
|
+
TInput extends Component<InputProps<TOptions>> | undefined = Component<
|
|
117
|
+
InputProps<TOptions>
|
|
118
|
+
>,
|
|
119
|
+
> = {
|
|
120
|
+
layout: Component<LayoutProps<TOptions, TInput>>
|
|
121
|
+
message: Component<MessageProps<TOptions>>
|
|
122
|
+
input?: TInput
|
|
123
|
+
queue?: Component<QueueProps<TOptions>>
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export type ChatUIPartsComponents<TOptions> = {
|
|
127
|
+
[K in ChatUIPartKey]?: Component<PartProps<TOptions, K>>
|
|
128
|
+
} & {
|
|
129
|
+
fallback?: Component<PartProps<TOptions>>
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export type ChatUIInterruptsComponents<TOptions> = {
|
|
133
|
+
tools?: ToolApprovalMap<TOptions>
|
|
134
|
+
generic: GenericInterruptComponents<TOptions>
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export type ChatUIComponents<
|
|
138
|
+
TOptions,
|
|
139
|
+
TInput extends Component<InputProps<TOptions>> | undefined = Component<
|
|
140
|
+
InputProps<TOptions>
|
|
141
|
+
>,
|
|
142
|
+
> = {
|
|
143
|
+
components: ChatUIChromeComponents<TOptions, TInput>
|
|
144
|
+
partsComponents: ChatUIPartsComponents<TOptions>
|
|
145
|
+
} & (ChatUIHasNamedTools<TOptions> extends true
|
|
146
|
+
? {
|
|
147
|
+
toolsComponents: {
|
|
148
|
+
[K in ChatUIToolName<TOptions>]: Component<ToolProps<TOptions, K>>
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
: {
|
|
152
|
+
toolsComponents?: {
|
|
153
|
+
[K in ChatUIToolName<TOptions>]?: Component<ToolProps<TOptions, K>>
|
|
154
|
+
}
|
|
155
|
+
}) &
|
|
156
|
+
(ChatUIHasNamedInterrupts<TOptions> extends true
|
|
157
|
+
? { interruptsComponents: ChatUIInterruptsComponents<TOptions> }
|
|
158
|
+
: {
|
|
159
|
+
interruptsComponents?: {
|
|
160
|
+
tools?: ToolApprovalMap<TOptions>
|
|
161
|
+
generic?: GenericInterruptComponents<TOptions>
|
|
162
|
+
}
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
/** Scoped contexts, for widgets in other files or nested chat trees. */
|
|
166
|
+
export type ChatUIContextConfig = {
|
|
167
|
+
chatContext?: ChatUIContexts['chatContext']
|
|
168
|
+
partContext?: ChatUIContexts['partContext']
|
|
169
|
+
interruptContext?: ChatUIContexts['interruptContext']
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export type ChatUIFactoryConfig<
|
|
173
|
+
TOptions,
|
|
174
|
+
TInput extends Component<InputProps<TOptions>> | undefined = Component<
|
|
175
|
+
InputProps<TOptions>
|
|
176
|
+
>,
|
|
177
|
+
> = ChatUIComponents<TOptions, TInput> & {
|
|
178
|
+
context?: ChatUIContextConfig
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
type BoundWidget = Component<Record<string, never>>
|
|
182
|
+
|
|
183
|
+
type PartMixins<TOptions> = {
|
|
184
|
+
[K in ChatUIPartKey]?: BoundWidget
|
|
185
|
+
} & {
|
|
186
|
+
[K in ChatUIToolName<TOptions>]: BoundWidget
|
|
187
|
+
} & {
|
|
188
|
+
Render: BoundWidget
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
type InterruptMixins<TOptions> = {
|
|
192
|
+
[K in ChatUINamedInterruptId<TOptions>]: BoundWidget
|
|
193
|
+
} & {
|
|
194
|
+
[K in ChatUIToolName<TOptions>]?: BoundWidget
|
|
195
|
+
} & {
|
|
196
|
+
fallback?: BoundWidget
|
|
197
|
+
Render: BoundWidget
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// This module ships as source, so it is type-checked against the *consumer's*
|
|
201
|
+
// tsconfig, which need not include `@types/node`. Declare `process` locally
|
|
202
|
+
// (same shape as the `src/env.d.ts` the devtools packages use) rather than
|
|
203
|
+
// reading it off `globalThis`: the literal `process.env.NODE_ENV` is the token
|
|
204
|
+
// bundlers substitute, so this keeps the branch constant-folded in production.
|
|
205
|
+
declare const process: {
|
|
206
|
+
env: {
|
|
207
|
+
NODE_ENV?: string
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function createWarnOnce() {
|
|
212
|
+
const seen = new Set<string>()
|
|
213
|
+
return (key: string, message: string) => {
|
|
214
|
+
if (process.env.NODE_ENV === 'production') return
|
|
215
|
+
if (seen.has(key)) return
|
|
216
|
+
seen.add(key)
|
|
217
|
+
console.warn(message)
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function readMessages<TOptions>(
|
|
222
|
+
chat: ChatUIHost<TOptions>,
|
|
223
|
+
): ReadonlyArray<any> {
|
|
224
|
+
return chat.messages()
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function readInterrupts<TOptions>(
|
|
228
|
+
chat: ChatUIHost<TOptions>,
|
|
229
|
+
): ReadonlyArray<any> {
|
|
230
|
+
return chat.interrupts()
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function messagesAccessor<TOptions>(
|
|
234
|
+
chat: ChatUIHost<TOptions>,
|
|
235
|
+
): Accessor<ReadonlyArray<any>> {
|
|
236
|
+
return chat.messages
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function isSelectedPart(
|
|
240
|
+
value: MessagePart | ChatUISelectedPart,
|
|
241
|
+
): value is ChatUISelectedPart {
|
|
242
|
+
return 'key' in value && 'part' in value
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function bindMap(
|
|
246
|
+
map: Record<string, Component<any> | undefined> | undefined,
|
|
247
|
+
bind: (component: Component<any>) => BoundWidget,
|
|
248
|
+
) {
|
|
249
|
+
const out: Record<string, BoundWidget> = {}
|
|
250
|
+
for (const [key, component] of Object.entries(map ?? {})) {
|
|
251
|
+
if (component) out[key] = bind(component)
|
|
252
|
+
}
|
|
253
|
+
return out
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Bind chat options and UI widgets once at module scope. This matches Form
|
|
258
|
+
* `createFormHook` and Table `createTableHook`.
|
|
259
|
+
*/
|
|
260
|
+
export function createChatUI<
|
|
261
|
+
const TOptions,
|
|
262
|
+
TInput extends Component<any> | undefined =
|
|
263
|
+
| Component<InputProps<NoInfer<TOptions>>>
|
|
264
|
+
| undefined,
|
|
265
|
+
>(options: TOptions, config: ChatUIFactoryConfig<NoInfer<TOptions>, TInput>) {
|
|
266
|
+
void options
|
|
267
|
+
const {
|
|
268
|
+
context: contextOption,
|
|
269
|
+
components,
|
|
270
|
+
partsComponents: parts,
|
|
271
|
+
toolsComponents: tools,
|
|
272
|
+
interruptsComponents: interrupts,
|
|
273
|
+
} = config as ChatUIFactoryConfig<TOptions, TInput> & {
|
|
274
|
+
toolsComponents?: Record<string, Component<any> | undefined>
|
|
275
|
+
interruptsComponents?: {
|
|
276
|
+
tools?: Record<string, Component<any> | undefined>
|
|
277
|
+
generic?: Record<string, Component<any> | undefined>
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
const {
|
|
281
|
+
layout: Layout,
|
|
282
|
+
message: MessageComponent,
|
|
283
|
+
input: InputComponent,
|
|
284
|
+
queue: QueueItemComponent,
|
|
285
|
+
} = components
|
|
286
|
+
const {
|
|
287
|
+
chatContext: chatContextOption,
|
|
288
|
+
partContext: partContextOption,
|
|
289
|
+
interruptContext: interruptContextOption,
|
|
290
|
+
} = contextOption ?? {}
|
|
291
|
+
const warn = createWarnOnce()
|
|
292
|
+
const ChatContext = (chatContextOption ??
|
|
293
|
+
defaultChatUIContexts.chatContext) as Context<
|
|
294
|
+
ChatUIHost<TOptions> | undefined
|
|
295
|
+
>
|
|
296
|
+
const PartContext = (partContextOption ??
|
|
297
|
+
defaultChatUIContexts.partContext) as Context<
|
|
298
|
+
ChatUISelectedPart | undefined
|
|
299
|
+
>
|
|
300
|
+
const InterruptContext = (interruptContextOption ??
|
|
301
|
+
defaultChatUIContexts.interruptContext) as Context<
|
|
302
|
+
ChatUIInterrupt | undefined
|
|
303
|
+
>
|
|
304
|
+
const inlineToolNames = collectInlineToolNames(
|
|
305
|
+
interrupts?.tools as Record<string, unknown> | undefined,
|
|
306
|
+
Object.keys(tools ?? {}),
|
|
307
|
+
)
|
|
308
|
+
|
|
309
|
+
function useChatContext() {
|
|
310
|
+
const chat = useContext(ChatContext)
|
|
311
|
+
if (!chat) {
|
|
312
|
+
throw new Error(
|
|
313
|
+
'`useChatContext` must be used within `UI.Provider` or `UI.Chat`.',
|
|
314
|
+
)
|
|
315
|
+
}
|
|
316
|
+
return chat
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function usePartContext<TKey extends ChatUIPartKey = ChatUIPartKey>() {
|
|
320
|
+
const selected = useContext(PartContext)
|
|
321
|
+
if (!selected) {
|
|
322
|
+
throw new Error(
|
|
323
|
+
'`usePartContext` must be used within `UI.Part` or an automatic part.',
|
|
324
|
+
)
|
|
325
|
+
}
|
|
326
|
+
return selected as ChatUISelectedPartOf<TOptions, TKey>
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function useInterruptContext<
|
|
330
|
+
TName extends ChatUIInterruptName<TOptions> = ChatUIInterruptName<TOptions>,
|
|
331
|
+
>() {
|
|
332
|
+
const interrupt = useContext(InterruptContext)
|
|
333
|
+
if (!interrupt) {
|
|
334
|
+
throw new Error(
|
|
335
|
+
'`useInterruptContext` must be used within `UI.Interrupt`.',
|
|
336
|
+
)
|
|
337
|
+
}
|
|
338
|
+
return interrupt as ChatUIInterruptOf<TOptions, TName>
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function bindPart(Component: Component<PartProps<TOptions>>) {
|
|
342
|
+
return function BoundPart() {
|
|
343
|
+
const selected = usePartContext()
|
|
344
|
+
return <Component part={selected.part as PartProps<TOptions>['part']} />
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function bindTool(Component: Component<ToolProps<TOptions>>) {
|
|
349
|
+
return function BoundTool() {
|
|
350
|
+
const selected = usePartContext()
|
|
351
|
+
if (selected.key !== 'toolCall') return null
|
|
352
|
+
return (
|
|
353
|
+
<Component
|
|
354
|
+
part={selected.part as ToolProps<TOptions>['part']}
|
|
355
|
+
result={selected.result}
|
|
356
|
+
interrupt={selected.interrupt as ToolProps<TOptions>['interrupt']}
|
|
357
|
+
/>
|
|
358
|
+
)
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function bindInterrupt(Component: Component<InterruptProps<TOptions>>) {
|
|
363
|
+
return function BoundInterrupt() {
|
|
364
|
+
const interrupt = useInterruptContext()
|
|
365
|
+
return (
|
|
366
|
+
<Component
|
|
367
|
+
interrupt={interrupt as InterruptProps<TOptions>['interrupt']}
|
|
368
|
+
/>
|
|
369
|
+
)
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
const partMixins = bindMap(
|
|
374
|
+
parts as Record<string, Component<any> | undefined>,
|
|
375
|
+
bindPart,
|
|
376
|
+
)
|
|
377
|
+
const toolMixins = bindMap(
|
|
378
|
+
tools as Record<string, Component<any> | undefined>,
|
|
379
|
+
bindTool,
|
|
380
|
+
)
|
|
381
|
+
const interruptMixins = {
|
|
382
|
+
...bindMap(
|
|
383
|
+
interrupts?.generic as
|
|
384
|
+
| Record<string, Component<any> | undefined>
|
|
385
|
+
| undefined,
|
|
386
|
+
bindInterrupt,
|
|
387
|
+
),
|
|
388
|
+
...bindMap(
|
|
389
|
+
interrupts?.tools as
|
|
390
|
+
| Record<string, Component<any> | undefined>
|
|
391
|
+
| undefined,
|
|
392
|
+
bindInterrupt,
|
|
393
|
+
),
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function mixPart(selected: ChatUISelectedPart) {
|
|
397
|
+
return Object.assign({}, selected, partMixins, toolMixins, {
|
|
398
|
+
Render: BoundRender,
|
|
399
|
+
}) as ChatUISelectedPart & PartMixins<TOptions>
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function mixInterrupt(interrupt: ChatUIInterrupt) {
|
|
403
|
+
return Object.assign({}, interrupt, interruptMixins, {
|
|
404
|
+
Render: BoundInterruptRender,
|
|
405
|
+
}) as ChatUIInterrupt & InterruptMixins<TOptions>
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function Provider(props: {
|
|
409
|
+
chat: ChatUIHost<TOptions>
|
|
410
|
+
children?: JSX.Element
|
|
411
|
+
}) {
|
|
412
|
+
return (
|
|
413
|
+
<ChatContext.Provider value={props.chat}>
|
|
414
|
+
{props.children}
|
|
415
|
+
</ChatContext.Provider>
|
|
416
|
+
)
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// Backstop for when the conditional `Input` type cannot be inferred (see the
|
|
420
|
+
// `input` note in docs/ui/solid.md). The type hides `Input` when no `input`
|
|
421
|
+
// is registered, but inference degrades on some config shapes, so always
|
|
422
|
+
// supply a component: warn once rather than crash on an undefined element.
|
|
423
|
+
function MissingInput() {
|
|
424
|
+
warn(
|
|
425
|
+
'input',
|
|
426
|
+
'[tanstack-ai-ui] Rendered <Input /> but no `input` component is registered.',
|
|
427
|
+
)
|
|
428
|
+
return null
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// Declared once per factory, so these props are stable for the kit's life.
|
|
432
|
+
const LayoutSlots = {
|
|
433
|
+
Messages: Messages as Component,
|
|
434
|
+
Interrupts: Interrupts as Component,
|
|
435
|
+
Queue: Queue as Component,
|
|
436
|
+
Input: (InputComponent ?? MissingInput) as Component,
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
function Chat(props: { chat: ChatUIHost<TOptions> }) {
|
|
440
|
+
return (
|
|
441
|
+
<Provider chat={props.chat}>
|
|
442
|
+
<Layout {...(LayoutSlots as any)} />
|
|
443
|
+
</Provider>
|
|
444
|
+
)
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
function Queue() {
|
|
448
|
+
const chat = useChatContext()
|
|
449
|
+
if (!QueueItemComponent) return null
|
|
450
|
+
return (
|
|
451
|
+
<For each={chat.queue()}>
|
|
452
|
+
{(item) => (
|
|
453
|
+
<QueueItemComponent
|
|
454
|
+
item={{
|
|
455
|
+
...item,
|
|
456
|
+
cancelQueued: () => chat.cancelQueued(item.id),
|
|
457
|
+
}}
|
|
458
|
+
/>
|
|
459
|
+
)}
|
|
460
|
+
</For>
|
|
461
|
+
)
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
function Messages(props: {
|
|
465
|
+
children?: (
|
|
466
|
+
messages: Accessor<ReadonlyArray<UIMessage<any, any>>>,
|
|
467
|
+
) => JSX.Element
|
|
468
|
+
}) {
|
|
469
|
+
const chat = useChatContext()
|
|
470
|
+
if (props.children) return <>{props.children(messagesAccessor(chat))}</>
|
|
471
|
+
return (
|
|
472
|
+
<For each={readMessages(chat)}>
|
|
473
|
+
{(message) => (
|
|
474
|
+
<MessageView
|
|
475
|
+
inlineToolNames={inlineToolNames}
|
|
476
|
+
interrupts={readInterrupts(chat)}
|
|
477
|
+
message={message}
|
|
478
|
+
/>
|
|
479
|
+
)}
|
|
480
|
+
</For>
|
|
481
|
+
)
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// Scoped to one message. `Parts` reads it instead of closing over the
|
|
485
|
+
// message, so the kit hands out one stable component, matching React.
|
|
486
|
+
type MessageRenderValue = {
|
|
487
|
+
message: UIMessage<any, any>
|
|
488
|
+
interrupts: ReadonlyArray<any>
|
|
489
|
+
inlineToolNames: ReadonlyArray<string>
|
|
490
|
+
}
|
|
491
|
+
const MessageRenderContext = createContext<MessageRenderValue>()
|
|
492
|
+
|
|
493
|
+
function Parts() {
|
|
494
|
+
const scope = useContext(MessageRenderContext)
|
|
495
|
+
if (!scope) {
|
|
496
|
+
throw new Error('`Parts` must be rendered by a `message` component.')
|
|
497
|
+
}
|
|
498
|
+
return (
|
|
499
|
+
<AutomaticParts
|
|
500
|
+
inlineToolNames={scope.inlineToolNames}
|
|
501
|
+
interrupts={scope.interrupts}
|
|
502
|
+
message={scope.message}
|
|
503
|
+
/>
|
|
504
|
+
)
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
function MessageView(props: {
|
|
508
|
+
message: UIMessage<any, any>
|
|
509
|
+
interrupts: ReadonlyArray<any>
|
|
510
|
+
inlineToolNames: ReadonlyArray<string>
|
|
511
|
+
children?: (parts: Array<ChatUISelectedPart>) => JSX.Element
|
|
512
|
+
}) {
|
|
513
|
+
const selected = selectMessageUI(props.message, {
|
|
514
|
+
interrupts: props.interrupts,
|
|
515
|
+
inlineToolNames: props.inlineToolNames,
|
|
516
|
+
})
|
|
517
|
+
if (props.children) return <>{props.children(selected.parts)}</>
|
|
518
|
+
return (
|
|
519
|
+
<MessageRenderContext.Provider
|
|
520
|
+
value={{
|
|
521
|
+
get message() {
|
|
522
|
+
return props.message
|
|
523
|
+
},
|
|
524
|
+
get interrupts() {
|
|
525
|
+
return props.interrupts
|
|
526
|
+
},
|
|
527
|
+
get inlineToolNames() {
|
|
528
|
+
return props.inlineToolNames
|
|
529
|
+
},
|
|
530
|
+
}}
|
|
531
|
+
>
|
|
532
|
+
<MessageComponent
|
|
533
|
+
message={props.message as MessageProps<TOptions>['message']}
|
|
534
|
+
Parts={Parts}
|
|
535
|
+
/>
|
|
536
|
+
</MessageRenderContext.Provider>
|
|
537
|
+
)
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
function Message(props: {
|
|
541
|
+
message: UIMessage<any, any>
|
|
542
|
+
children?: (parts: Array<ChatUISelectedPart>) => JSX.Element
|
|
543
|
+
}) {
|
|
544
|
+
const chat = useChatContext()
|
|
545
|
+
return (
|
|
546
|
+
<MessageView
|
|
547
|
+
children={props.children}
|
|
548
|
+
inlineToolNames={inlineToolNames}
|
|
549
|
+
interrupts={readInterrupts(chat)}
|
|
550
|
+
message={props.message}
|
|
551
|
+
/>
|
|
552
|
+
)
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
function AutomaticParts(props: {
|
|
556
|
+
message: UIMessage<any, any>
|
|
557
|
+
interrupts: ReadonlyArray<any>
|
|
558
|
+
inlineToolNames: ReadonlyArray<string>
|
|
559
|
+
}) {
|
|
560
|
+
const selected = selectMessageUI(props.message, {
|
|
561
|
+
interrupts: props.interrupts,
|
|
562
|
+
inlineToolNames: props.inlineToolNames,
|
|
563
|
+
})
|
|
564
|
+
return (
|
|
565
|
+
<>
|
|
566
|
+
{automaticPartsForMessage(selected).map((part) => (
|
|
567
|
+
<SelectedPartView selected={part} />
|
|
568
|
+
))}
|
|
569
|
+
</>
|
|
570
|
+
)
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
function SelectedPartInner(props: { selected: ChatUISelectedPart }) {
|
|
574
|
+
if (props.selected.key === 'toolCall') {
|
|
575
|
+
const name = props.selected.part.name
|
|
576
|
+
const Tool = tools?.[name as ChatUIToolName<TOptions>] as
|
|
577
|
+
| Component<ToolProps<TOptions>>
|
|
578
|
+
| undefined
|
|
579
|
+
if (!Tool) {
|
|
580
|
+
warn(`tool:${name}`, `[tanstack-ai-ui] Missing tools.${name} component`)
|
|
581
|
+
return null
|
|
582
|
+
}
|
|
583
|
+
return (
|
|
584
|
+
<Tool
|
|
585
|
+
part={props.selected.part as ToolProps<TOptions>['part']}
|
|
586
|
+
result={props.selected.result}
|
|
587
|
+
interrupt={
|
|
588
|
+
(props.selected.key === 'toolCall'
|
|
589
|
+
? props.selected.interrupt
|
|
590
|
+
: undefined) as ToolProps<TOptions>['interrupt']
|
|
591
|
+
}
|
|
592
|
+
/>
|
|
593
|
+
)
|
|
594
|
+
}
|
|
595
|
+
const PartComponent = (parts[props.selected.key] ?? parts.fallback) as
|
|
596
|
+
| Component<PartProps<TOptions>>
|
|
597
|
+
| undefined
|
|
598
|
+
if (!PartComponent) {
|
|
599
|
+
warn(
|
|
600
|
+
`part:${props.selected.key}`,
|
|
601
|
+
`[tanstack-ai-ui] Missing parts.${props.selected.key} component`,
|
|
602
|
+
)
|
|
603
|
+
return null
|
|
604
|
+
}
|
|
605
|
+
return (
|
|
606
|
+
<PartComponent
|
|
607
|
+
part={props.selected.part as PartProps<TOptions>['part']}
|
|
608
|
+
/>
|
|
609
|
+
)
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
function SelectedPartView(props: { selected: ChatUISelectedPart }) {
|
|
613
|
+
return (
|
|
614
|
+
<PartContext.Provider value={props.selected}>
|
|
615
|
+
<SelectedPartInner selected={props.selected} />
|
|
616
|
+
</PartContext.Provider>
|
|
617
|
+
)
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
function BoundRender() {
|
|
621
|
+
const selected = usePartContext()
|
|
622
|
+
return <SelectedPartInner selected={selected} />
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
function Part(props: {
|
|
626
|
+
part: MessagePart | ChatUISelectedPart
|
|
627
|
+
children?: (mixed: ChatUISelectedPart & PartMixins<TOptions>) => JSX.Element
|
|
628
|
+
}) {
|
|
629
|
+
const chat = useChatContext()
|
|
630
|
+
const selected = isSelectedPart(props.part)
|
|
631
|
+
? props.part
|
|
632
|
+
: selectMessageUI(
|
|
633
|
+
{ id: 'part', role: 'assistant', parts: [props.part] },
|
|
634
|
+
{ interrupts: readInterrupts(chat), inlineToolNames: [] },
|
|
635
|
+
).parts[0]
|
|
636
|
+
if (!selected) return null
|
|
637
|
+
return (
|
|
638
|
+
<PartContext.Provider value={selected}>
|
|
639
|
+
{props.children ? (
|
|
640
|
+
props.children(mixPart(selected))
|
|
641
|
+
) : (
|
|
642
|
+
<SelectedPartInner selected={selected} />
|
|
643
|
+
)}
|
|
644
|
+
</PartContext.Provider>
|
|
645
|
+
)
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
function Interrupts(props: {
|
|
649
|
+
children?: (
|
|
650
|
+
interrupts: Accessor<ReadonlyArray<ChatUIInterrupt>>,
|
|
651
|
+
) => JSX.Element
|
|
652
|
+
}) {
|
|
653
|
+
const chat = useChatContext()
|
|
654
|
+
const selected = () =>
|
|
655
|
+
selectChatUI({
|
|
656
|
+
messages: readMessages(chat),
|
|
657
|
+
interrupts: readInterrupts(chat),
|
|
658
|
+
inlineToolNames,
|
|
659
|
+
})
|
|
660
|
+
if (props.children) {
|
|
661
|
+
return <>{props.children(() => selected().interrupts)}</>
|
|
662
|
+
}
|
|
663
|
+
return (
|
|
664
|
+
<For each={selected().interrupts}>
|
|
665
|
+
{(interrupt) => <Interrupt interrupt={interrupt} />}
|
|
666
|
+
</For>
|
|
667
|
+
)
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
function InterruptInner(props: { interrupt: ChatUIInterrupt }) {
|
|
671
|
+
const Component = resolveInterruptComponent(props.interrupt, interrupts) as
|
|
672
|
+
| Component<InterruptProps<TOptions>>
|
|
673
|
+
| undefined
|
|
674
|
+
if (!Component) {
|
|
675
|
+
warn(
|
|
676
|
+
`interrupt:${props.interrupt.id}`,
|
|
677
|
+
`[tanstack-ai-ui] Missing interrupt component for ${props.interrupt.kind}`,
|
|
678
|
+
)
|
|
679
|
+
return null
|
|
680
|
+
}
|
|
681
|
+
return <Component interrupt={props.interrupt} />
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
function BoundInterruptRender() {
|
|
685
|
+
const interrupt = useInterruptContext()
|
|
686
|
+
return <InterruptInner interrupt={interrupt} />
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
function Interrupt(props: {
|
|
690
|
+
interrupt: ChatUIInterrupt
|
|
691
|
+
children?: (
|
|
692
|
+
mixed: ChatUIInterrupt & InterruptMixins<TOptions>,
|
|
693
|
+
) => JSX.Element
|
|
694
|
+
}) {
|
|
695
|
+
return (
|
|
696
|
+
<InterruptContext.Provider value={props.interrupt}>
|
|
697
|
+
{props.children ? (
|
|
698
|
+
props.children(mixInterrupt(props.interrupt))
|
|
699
|
+
) : (
|
|
700
|
+
<InterruptInner interrupt={props.interrupt} />
|
|
701
|
+
)}
|
|
702
|
+
</InterruptContext.Provider>
|
|
703
|
+
)
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
return {
|
|
707
|
+
Chat,
|
|
708
|
+
Provider,
|
|
709
|
+
Messages,
|
|
710
|
+
Message,
|
|
711
|
+
Part,
|
|
712
|
+
Interrupts,
|
|
713
|
+
Interrupt,
|
|
714
|
+
Queue,
|
|
715
|
+
useChatContext,
|
|
716
|
+
Input: InputComponent,
|
|
717
|
+
}
|
|
718
|
+
}
|