@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,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
+ }