@tanstack/ai-react 0.22.4 → 0.23.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/README.md +11 -15
- package/dist/esm/chat-ui/chat-input.d.ts +57 -0
- package/dist/esm/chat-ui/chat-input.js +124 -0
- package/dist/esm/chat-ui/chat-input.js.map +1 -0
- package/dist/esm/chat-ui/chat-message.d.ts +91 -0
- package/dist/esm/chat-ui/chat-message.js +157 -0
- package/dist/esm/chat-ui/chat-message.js.map +1 -0
- package/dist/esm/chat-ui/chat-messages.d.ts +33 -0
- package/dist/esm/chat-ui/chat-messages.js +44 -0
- package/dist/esm/chat-ui/chat-messages.js.map +1 -0
- package/dist/esm/chat-ui/chat.d.ts +53 -0
- package/dist/esm/chat-ui/chat.js +57 -0
- package/dist/esm/chat-ui/chat.js.map +1 -0
- package/dist/esm/chat-ui/create-chat-hook.d.ts +48 -0
- package/dist/esm/chat-ui/create-chat-hook.js +57 -0
- package/dist/esm/chat-ui/create-chat-hook.js.map +1 -0
- package/dist/esm/chat-ui/create-ui-contexts.d.ts +29 -0
- package/dist/esm/chat-ui/create-ui-contexts.js +36 -0
- package/dist/esm/chat-ui/create-ui-contexts.js.map +1 -0
- package/dist/esm/chat-ui/create-ui.d.ts +153 -0
- package/dist/esm/chat-ui/create-ui.js +294 -0
- package/dist/esm/chat-ui/create-ui.js.map +1 -0
- package/dist/esm/chat-ui/markdown-plugins.d.ts +15 -0
- package/dist/esm/chat-ui/markdown-plugins.js +28 -0
- package/dist/esm/chat-ui/markdown-plugins.js.map +1 -0
- package/dist/esm/chat-ui/text-part.d.ts +57 -0
- package/dist/esm/chat-ui/text-part.js +46 -0
- package/dist/esm/chat-ui/text-part.js.map +1 -0
- package/dist/esm/chat-ui/thinking-part.d.ts +41 -0
- package/dist/esm/chat-ui/thinking-part.js +73 -0
- package/dist/esm/chat-ui/thinking-part.js.map +1 -0
- package/dist/esm/chat-ui/tool-approval.d.ts +52 -0
- package/dist/esm/chat-ui/tool-approval.js +83 -0
- package/dist/esm/chat-ui/tool-approval.js.map +1 -0
- package/dist/esm/chat-ui/tool-result-content.d.ts +18 -0
- package/dist/esm/chat-ui/tool-result-content.js +19 -0
- package/dist/esm/chat-ui/tool-result-content.js.map +1 -0
- package/dist/esm/create-chat-hook.d.ts +35 -0
- package/dist/esm/create-chat-hook.js +37 -0
- package/dist/esm/create-chat-hook.js.map +1 -0
- package/dist/esm/index.d.ts +1 -0
- package/dist/esm/index.js +2 -1
- package/dist/esm/ui.d.ts +10 -0
- package/dist/esm/ui.js +11 -0
- package/package.json +18 -4
- package/src/chat-ui/chat-input.tsx +177 -0
- package/src/chat-ui/chat-message.tsx +271 -0
- package/src/chat-ui/chat-messages.tsx +89 -0
- package/src/chat-ui/chat.tsx +98 -0
- package/src/chat-ui/create-chat-hook.tsx +103 -0
- package/src/chat-ui/create-ui-contexts.tsx +57 -0
- package/src/chat-ui/create-ui.tsx +761 -0
- package/src/chat-ui/markdown-plugins.ts +48 -0
- package/src/chat-ui/text-part.tsx +97 -0
- package/src/chat-ui/thinking-part.tsx +83 -0
- package/src/chat-ui/tool-approval.tsx +132 -0
- package/src/chat-ui/tool-result-content.ts +32 -0
- package/src/create-chat-hook.ts +67 -0
- package/src/index.ts +1 -0
- package/src/ui.ts +43 -0
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import remarkGfm from 'remark-gfm'
|
|
2
|
+
import rehypeRaw from 'rehype-raw'
|
|
3
|
+
import rehypeSanitize from 'rehype-sanitize'
|
|
4
|
+
import rehypeHighlight from 'rehype-highlight'
|
|
5
|
+
import type { Options as ReactMarkdownOptions } from 'react-markdown'
|
|
6
|
+
|
|
7
|
+
export type PluggableList = NonNullable<ReactMarkdownOptions['remarkPlugins']>
|
|
8
|
+
|
|
9
|
+
export const DEFAULT_REMARK_PLUGINS: PluggableList = [remarkGfm]
|
|
10
|
+
export const DEFAULT_REHYPE_PLUGINS_BEFORE_USER: PluggableList = [
|
|
11
|
+
rehypeRaw,
|
|
12
|
+
rehypeHighlight,
|
|
13
|
+
]
|
|
14
|
+
export const DEFAULT_REHYPE_PLUGINS_AFTER_USER: PluggableList = [rehypeSanitize]
|
|
15
|
+
|
|
16
|
+
export interface ResolveMarkdownPluginsOptions {
|
|
17
|
+
remarkPlugins?: PluggableList
|
|
18
|
+
rehypePlugins?: PluggableList
|
|
19
|
+
disableDefaultPlugins?: boolean
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface ResolvedMarkdownPlugins {
|
|
23
|
+
remarkPlugins: PluggableList
|
|
24
|
+
rehypePlugins: PluggableList
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function resolveMarkdownPlugins(
|
|
28
|
+
options: ResolveMarkdownPluginsOptions,
|
|
29
|
+
): ResolvedMarkdownPlugins {
|
|
30
|
+
const userRemark = options.remarkPlugins ?? []
|
|
31
|
+
const userRehype = options.rehypePlugins ?? []
|
|
32
|
+
|
|
33
|
+
if (options.disableDefaultPlugins) {
|
|
34
|
+
return {
|
|
35
|
+
remarkPlugins: [...userRemark],
|
|
36
|
+
rehypePlugins: [...userRehype],
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
remarkPlugins: [...DEFAULT_REMARK_PLUGINS, ...userRemark],
|
|
42
|
+
rehypePlugins: [
|
|
43
|
+
...DEFAULT_REHYPE_PLUGINS_BEFORE_USER,
|
|
44
|
+
...userRehype,
|
|
45
|
+
...DEFAULT_REHYPE_PLUGINS_AFTER_USER,
|
|
46
|
+
],
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import ReactMarkdown from 'react-markdown'
|
|
2
|
+
import { resolveMarkdownPlugins } from './markdown-plugins'
|
|
3
|
+
import type { Components } from 'react-markdown'
|
|
4
|
+
import type { PluggableList } from './markdown-plugins'
|
|
5
|
+
|
|
6
|
+
export interface TextPartProps {
|
|
7
|
+
/** The text content to render */
|
|
8
|
+
content: string
|
|
9
|
+
/** The role of the message (user, assistant, or system) - optional for standalone use */
|
|
10
|
+
role?: 'user' | 'assistant' | 'system'
|
|
11
|
+
/** Base className applied to all text parts */
|
|
12
|
+
className?: string
|
|
13
|
+
/** Additional className for user messages */
|
|
14
|
+
userClassName?: string
|
|
15
|
+
/** Additional className for assistant messages (also used for system messages) */
|
|
16
|
+
assistantClassName?: string
|
|
17
|
+
/**
|
|
18
|
+
* Additional remark plugins, appended after the defaults
|
|
19
|
+
* (or replacing them when `disableDefaultPlugins` is true).
|
|
20
|
+
*/
|
|
21
|
+
remarkPlugins?: PluggableList
|
|
22
|
+
/**
|
|
23
|
+
* Additional rehype plugins. Inserted between the built-in
|
|
24
|
+
* `rehypeRaw`/`rehypeHighlight` and the trailing `rehypeSanitize`
|
|
25
|
+
* so sanitization always runs last. When `disableDefaultPlugins`
|
|
26
|
+
* is true, replaces the entire chain.
|
|
27
|
+
*/
|
|
28
|
+
rehypePlugins?: PluggableList
|
|
29
|
+
/** react-markdown `components` overrides (e.g. custom `a`, `code`). */
|
|
30
|
+
components?: Components
|
|
31
|
+
/**
|
|
32
|
+
* Drop the built-in plugin defaults entirely. The consumer becomes
|
|
33
|
+
* responsible for syntax highlighting, GFM, raw HTML handling, and
|
|
34
|
+
* sanitization. Use with care — disabling defaults removes the
|
|
35
|
+
* built-in XSS sanitizer.
|
|
36
|
+
*/
|
|
37
|
+
disableDefaultPlugins?: boolean
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* TextPart component - renders markdown text with syntax highlighting.
|
|
42
|
+
*
|
|
43
|
+
* @example Standalone usage
|
|
44
|
+
* ```tsx
|
|
45
|
+
* <TextPart
|
|
46
|
+
* content="Hello **world**!"
|
|
47
|
+
* role="user"
|
|
48
|
+
* className="p-4 rounded"
|
|
49
|
+
* userClassName="bg-blue-500"
|
|
50
|
+
* assistantClassName="bg-gray-500"
|
|
51
|
+
* />
|
|
52
|
+
* ```
|
|
53
|
+
*
|
|
54
|
+
* @example Add a markdown plugin (e.g. CJK bold/emphasis support)
|
|
55
|
+
* ```tsx
|
|
56
|
+
* import remarkCjkFriendly from 'remark-cjk-friendly'
|
|
57
|
+
*
|
|
58
|
+
* <TextPart content={content} remarkPlugins={[remarkCjkFriendly]} />
|
|
59
|
+
* ```
|
|
60
|
+
*/
|
|
61
|
+
export function TextPart({
|
|
62
|
+
content,
|
|
63
|
+
role,
|
|
64
|
+
className = '',
|
|
65
|
+
userClassName = '',
|
|
66
|
+
assistantClassName = '',
|
|
67
|
+
remarkPlugins,
|
|
68
|
+
rehypePlugins,
|
|
69
|
+
components,
|
|
70
|
+
disableDefaultPlugins,
|
|
71
|
+
}: TextPartProps) {
|
|
72
|
+
const roleClassName =
|
|
73
|
+
role === 'user'
|
|
74
|
+
? userClassName
|
|
75
|
+
: role === 'assistant'
|
|
76
|
+
? assistantClassName
|
|
77
|
+
: ''
|
|
78
|
+
const combinedClassName = [className, roleClassName].filter(Boolean).join(' ')
|
|
79
|
+
|
|
80
|
+
const resolved = resolveMarkdownPlugins({
|
|
81
|
+
remarkPlugins,
|
|
82
|
+
rehypePlugins,
|
|
83
|
+
disableDefaultPlugins,
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
return (
|
|
87
|
+
<div className={combinedClassName || undefined}>
|
|
88
|
+
<ReactMarkdown
|
|
89
|
+
remarkPlugins={resolved.remarkPlugins}
|
|
90
|
+
rehypePlugins={resolved.rehypePlugins}
|
|
91
|
+
components={components}
|
|
92
|
+
>
|
|
93
|
+
{content}
|
|
94
|
+
</ReactMarkdown>
|
|
95
|
+
</div>
|
|
96
|
+
)
|
|
97
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react'
|
|
2
|
+
|
|
3
|
+
export interface ThinkingPartProps {
|
|
4
|
+
/** The thinking content to render */
|
|
5
|
+
content: string
|
|
6
|
+
/** Base className applied to thinking parts */
|
|
7
|
+
className?: string
|
|
8
|
+
/** Whether thinking is complete (has text content after) */
|
|
9
|
+
isComplete?: boolean
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* ThinkingPart component - renders thinking/reasoning content
|
|
14
|
+
*
|
|
15
|
+
* This component displays the model's internal reasoning process,
|
|
16
|
+
* typically shown in a collapsed or expandable format to distinguish
|
|
17
|
+
* it from the final response. It automatically collapses when thinking
|
|
18
|
+
* is complete.
|
|
19
|
+
*
|
|
20
|
+
* @example Standalone usage
|
|
21
|
+
* ```tsx
|
|
22
|
+
* <ThinkingPart
|
|
23
|
+
* content="Let me think about this step by step..."
|
|
24
|
+
* className="p-4 rounded bg-gray-100"
|
|
25
|
+
* />
|
|
26
|
+
* ```
|
|
27
|
+
*
|
|
28
|
+
* @example Usage in partRenderers
|
|
29
|
+
* ```tsx
|
|
30
|
+
* <ChatMessage
|
|
31
|
+
* message={message}
|
|
32
|
+
* partRenderers={{
|
|
33
|
+
* thinking: ({ content, isComplete }) => (
|
|
34
|
+
* <ThinkingPart
|
|
35
|
+
* content={content}
|
|
36
|
+
* isComplete={isComplete}
|
|
37
|
+
* className="px-5 py-3 rounded-2xl bg-gray-800/50"
|
|
38
|
+
* />
|
|
39
|
+
* )
|
|
40
|
+
* }}
|
|
41
|
+
* />
|
|
42
|
+
* ```
|
|
43
|
+
*/
|
|
44
|
+
export function ThinkingPart({
|
|
45
|
+
content,
|
|
46
|
+
className = '',
|
|
47
|
+
isComplete = false,
|
|
48
|
+
}: ThinkingPartProps) {
|
|
49
|
+
const [isCollapsed, setIsCollapsed] = useState(false)
|
|
50
|
+
|
|
51
|
+
// Auto-collapse when thinking completes
|
|
52
|
+
useEffect(() => {
|
|
53
|
+
if (isComplete) {
|
|
54
|
+
setIsCollapsed(true)
|
|
55
|
+
}
|
|
56
|
+
}, [isComplete])
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<div
|
|
60
|
+
className={className || undefined}
|
|
61
|
+
data-part-type="thinking"
|
|
62
|
+
data-part-content
|
|
63
|
+
>
|
|
64
|
+
<button
|
|
65
|
+
onClick={() => setIsCollapsed(!isCollapsed)}
|
|
66
|
+
className="flex items-center gap-2 text-sm text-gray-400 hover:text-gray-300 transition-colors mb-2"
|
|
67
|
+
aria-expanded={!isCollapsed}
|
|
68
|
+
aria-label={isCollapsed ? 'Expand thinking' : 'Collapse thinking'}
|
|
69
|
+
>
|
|
70
|
+
<span className="text-xs">{isCollapsed ? '▶' : '▼'}</span>
|
|
71
|
+
<span className="italic">💭 Thinking...</span>
|
|
72
|
+
{isComplete && (
|
|
73
|
+
<span className="text-xs text-gray-500">(complete)</span>
|
|
74
|
+
)}
|
|
75
|
+
</button>
|
|
76
|
+
{!isCollapsed && (
|
|
77
|
+
<div className="text-gray-300 whitespace-pre-wrap font-mono text-sm">
|
|
78
|
+
{content}
|
|
79
|
+
</div>
|
|
80
|
+
)}
|
|
81
|
+
</div>
|
|
82
|
+
)
|
|
83
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { useChatContext } from './chat'
|
|
2
|
+
import type { ReactNode } from 'react'
|
|
3
|
+
|
|
4
|
+
export interface ToolApprovalProps {
|
|
5
|
+
/** Tool call ID */
|
|
6
|
+
toolCallId: string
|
|
7
|
+
/** Tool name */
|
|
8
|
+
toolName: string
|
|
9
|
+
/** Parsed tool arguments/input */
|
|
10
|
+
input: any
|
|
11
|
+
/** Approval metadata */
|
|
12
|
+
approval: {
|
|
13
|
+
id: string
|
|
14
|
+
needsApproval: boolean
|
|
15
|
+
approved?: boolean
|
|
16
|
+
}
|
|
17
|
+
/** CSS class name */
|
|
18
|
+
className?: string
|
|
19
|
+
/** Custom render prop */
|
|
20
|
+
children?: (props: ToolApprovalRenderProps) => ReactNode
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface ToolApprovalRenderProps {
|
|
24
|
+
/** Tool name */
|
|
25
|
+
toolName: string
|
|
26
|
+
/** Parsed input */
|
|
27
|
+
input: any
|
|
28
|
+
/** Approve the tool call */
|
|
29
|
+
onApprove: () => void
|
|
30
|
+
/** Deny the tool call */
|
|
31
|
+
onDeny: () => void
|
|
32
|
+
/** Whether user has responded */
|
|
33
|
+
hasResponded: boolean
|
|
34
|
+
/** User's decision (if responded) */
|
|
35
|
+
approved?: boolean
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* @deprecated Use `createChatUI()` interrupt components with `chat.interrupts`.
|
|
40
|
+
* Deprecated in 0.9.0. Removed in 1.0.0.
|
|
41
|
+
*
|
|
42
|
+
* Tool approval component - renders approve/deny buttons for tools that need approval
|
|
43
|
+
*
|
|
44
|
+
* @example
|
|
45
|
+
* ```tsx
|
|
46
|
+
* {part.approval && (
|
|
47
|
+
* <ToolApproval
|
|
48
|
+
* toolCallId={part.id}
|
|
49
|
+
* toolName={part.name}
|
|
50
|
+
* input={JSON.parse(part.arguments)}
|
|
51
|
+
* approval={part.approval}
|
|
52
|
+
* />
|
|
53
|
+
* )}
|
|
54
|
+
* ```
|
|
55
|
+
*/
|
|
56
|
+
export function ToolApproval({
|
|
57
|
+
toolCallId: _,
|
|
58
|
+
toolName,
|
|
59
|
+
input,
|
|
60
|
+
approval,
|
|
61
|
+
className,
|
|
62
|
+
children,
|
|
63
|
+
}: ToolApprovalProps) {
|
|
64
|
+
const { addToolApprovalResponse } = useChatContext()
|
|
65
|
+
|
|
66
|
+
const handleApprove = () => {
|
|
67
|
+
void addToolApprovalResponse({
|
|
68
|
+
id: approval.id,
|
|
69
|
+
approved: true,
|
|
70
|
+
})
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const handleDeny = () => {
|
|
74
|
+
void addToolApprovalResponse({
|
|
75
|
+
id: approval.id,
|
|
76
|
+
approved: false,
|
|
77
|
+
})
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const hasResponded = approval.approved !== undefined
|
|
81
|
+
|
|
82
|
+
const renderProps: ToolApprovalRenderProps = {
|
|
83
|
+
toolName,
|
|
84
|
+
input,
|
|
85
|
+
onApprove: handleApprove,
|
|
86
|
+
onDeny: handleDeny,
|
|
87
|
+
hasResponded,
|
|
88
|
+
approved: approval.approved,
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Render prop pattern
|
|
92
|
+
if (children) {
|
|
93
|
+
return <>{children(renderProps)}</>
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Already responded - show decision
|
|
97
|
+
if (hasResponded) {
|
|
98
|
+
return (
|
|
99
|
+
<div
|
|
100
|
+
className={className}
|
|
101
|
+
data-tool-approval
|
|
102
|
+
data-approval-status={approval.approved ? 'approved' : 'denied'}
|
|
103
|
+
>
|
|
104
|
+
{approval.approved ? '✓ Approved' : '✗ Denied'}
|
|
105
|
+
</div>
|
|
106
|
+
)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Default approval UI
|
|
110
|
+
return (
|
|
111
|
+
<div
|
|
112
|
+
className={className}
|
|
113
|
+
data-tool-approval
|
|
114
|
+
data-approval-status="pending"
|
|
115
|
+
>
|
|
116
|
+
<div data-approval-header>
|
|
117
|
+
<strong>{toolName}</strong> requires approval
|
|
118
|
+
</div>
|
|
119
|
+
<div data-approval-input>
|
|
120
|
+
<pre>{JSON.stringify(input, null, 2)}</pre>
|
|
121
|
+
</div>
|
|
122
|
+
<div data-approval-actions>
|
|
123
|
+
<button onClick={handleApprove} data-approval-approve>
|
|
124
|
+
Approve
|
|
125
|
+
</button>
|
|
126
|
+
<button onClick={handleDeny} data-approval-deny>
|
|
127
|
+
Deny
|
|
128
|
+
</button>
|
|
129
|
+
</div>
|
|
130
|
+
</div>
|
|
131
|
+
)
|
|
132
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { UIMessage } from '../types'
|
|
2
|
+
|
|
3
|
+
type ToolResultPart = Extract<
|
|
4
|
+
UIMessage['parts'][number],
|
|
5
|
+
{ type: 'tool-result' }
|
|
6
|
+
>
|
|
7
|
+
|
|
8
|
+
/** `string | Array<ContentPart>` — a tool result's raw content. */
|
|
9
|
+
type ToolResultContent = ToolResultPart['content']
|
|
10
|
+
|
|
11
|
+
type ContentPartItem = Exclude<ToolResultContent, string>[number]
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Reduce a tool-result part's `content` to a plain string for rendering.
|
|
15
|
+
*
|
|
16
|
+
* Tool results carry `string | Array<ContentPart>` (multimodal results are
|
|
17
|
+
* normalized to an array of content parts upstream). The `ChatMessage`
|
|
18
|
+
* renderers operate on plain strings, so array content is flattened to the
|
|
19
|
+
* concatenation of its text parts. Non-text parts (image, audio, video,
|
|
20
|
+
* document) have no string form here and are skipped — matching the
|
|
21
|
+
* text-extraction behavior used elsewhere for `string | Array<ContentPart>`.
|
|
22
|
+
*/
|
|
23
|
+
export function toolResultContentToString(content: ToolResultContent): string {
|
|
24
|
+
if (typeof content === 'string') return content
|
|
25
|
+
return content
|
|
26
|
+
.filter(
|
|
27
|
+
(part): part is Extract<ContentPartItem, { type: 'text' }> =>
|
|
28
|
+
part.type === 'text',
|
|
29
|
+
)
|
|
30
|
+
.map((part) => part.content)
|
|
31
|
+
.join('')
|
|
32
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { useChat as useUnboundChat } from './use-chat'
|
|
2
|
+
import type {
|
|
3
|
+
AnyClientTool,
|
|
4
|
+
InterruptDefinition,
|
|
5
|
+
SchemaInput,
|
|
6
|
+
} from '@tanstack/ai/client'
|
|
7
|
+
import type { InferredClientContext } from '@tanstack/ai-client'
|
|
8
|
+
import type { UseChatOptions } from './types'
|
|
9
|
+
|
|
10
|
+
type ChatHookOverrides<
|
|
11
|
+
TTools extends ReadonlyArray<AnyClientTool>,
|
|
12
|
+
TSchema extends SchemaInput | undefined,
|
|
13
|
+
TContext,
|
|
14
|
+
TInterrupts extends ReadonlyArray<InterruptDefinition<any, any, any, any>>,
|
|
15
|
+
> = {
|
|
16
|
+
threadId?: string
|
|
17
|
+
live?: boolean
|
|
18
|
+
forwardedProps?: Record<string, any>
|
|
19
|
+
body?: Record<string, any>
|
|
20
|
+
initialMessages?: UseChatOptions<
|
|
21
|
+
TTools,
|
|
22
|
+
TSchema,
|
|
23
|
+
TContext,
|
|
24
|
+
TInterrupts
|
|
25
|
+
>['initialMessages']
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Bind chat options once at module scope. The returned `useChat` hook
|
|
30
|
+
* creates a chat instance from those options.
|
|
31
|
+
*
|
|
32
|
+
* Pass per-call overrides for instance keys such as `threadId`,
|
|
33
|
+
* `initialMessages`, `live`, and `forwardedProps`. Do not change `tools`,
|
|
34
|
+
* `interrupts`, or `outputSchema` here. Those stay on the factory options.
|
|
35
|
+
*
|
|
36
|
+
* Rename the hook at the call site if you already import `useChat` from
|
|
37
|
+
* `@tanstack/ai-react`: `const { useChat: useAppChat } = createChatHook(chatOptions)`.
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* ```tsx
|
|
41
|
+
* const { useChat } = createChatHook(chatOptions)
|
|
42
|
+
*
|
|
43
|
+
* function Support() {
|
|
44
|
+
* const chat = useChat({ threadId: 'support-1' })
|
|
45
|
+
* return <UI.Chat chat={chat} />
|
|
46
|
+
* }
|
|
47
|
+
* ```
|
|
48
|
+
*/
|
|
49
|
+
export function createChatHook<
|
|
50
|
+
const TTools extends ReadonlyArray<AnyClientTool> = any,
|
|
51
|
+
TSchema extends SchemaInput | undefined = undefined,
|
|
52
|
+
TContext = InferredClientContext<TTools>,
|
|
53
|
+
const TInterrupts extends ReadonlyArray<
|
|
54
|
+
InterruptDefinition<any, any, any, any>
|
|
55
|
+
> = readonly [],
|
|
56
|
+
>(options: UseChatOptions<TTools, TSchema, TContext, TInterrupts>) {
|
|
57
|
+
function useChat(
|
|
58
|
+
overrides?: ChatHookOverrides<TTools, TSchema, TContext, TInterrupts>,
|
|
59
|
+
) {
|
|
60
|
+
if (!overrides) {
|
|
61
|
+
return useUnboundChat(options)
|
|
62
|
+
}
|
|
63
|
+
return useUnboundChat({ ...options, ...overrides })
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return { useChat }
|
|
67
|
+
}
|
package/src/index.ts
CHANGED
package/src/ui.ts
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// Barrel entry for the `@tanstack/ai-react/ui` subpath. The JSX
|
|
2
|
+
// implementation lives under `./chat-ui`; this `.ts` re-export exists so
|
|
3
|
+
// kiira's dist->src resolution (which maps `dist/esm/ui.d.ts` to `src/ui.ts`,
|
|
4
|
+
// never a directory index) can type-check docs snippets that import this
|
|
5
|
+
// subpath.
|
|
6
|
+
export {
|
|
7
|
+
createChatUI,
|
|
8
|
+
type ChatUIComponents,
|
|
9
|
+
type ChatUIFactoryConfig,
|
|
10
|
+
type ChatUIHost,
|
|
11
|
+
type ChatUIQueueItem,
|
|
12
|
+
type InputProps,
|
|
13
|
+
type InterruptProps,
|
|
14
|
+
type LayoutProps,
|
|
15
|
+
type MessageProps,
|
|
16
|
+
type PartProps,
|
|
17
|
+
type QueueProps,
|
|
18
|
+
type ToolProps,
|
|
19
|
+
} from './chat-ui/create-ui'
|
|
20
|
+
export { createChatHook } from './chat-ui/create-chat-hook'
|
|
21
|
+
export {
|
|
22
|
+
createChatHookContexts,
|
|
23
|
+
type ChatUIContexts,
|
|
24
|
+
} from './chat-ui/create-ui-contexts'
|
|
25
|
+
export { Chat, useChatContext, type ChatProps } from './chat-ui/chat'
|
|
26
|
+
export { ChatMessages, type ChatMessagesProps } from './chat-ui/chat-messages'
|
|
27
|
+
export {
|
|
28
|
+
ChatMessage,
|
|
29
|
+
type ChatMessageProps,
|
|
30
|
+
type ToolCallRenderProps,
|
|
31
|
+
} from './chat-ui/chat-message'
|
|
32
|
+
export {
|
|
33
|
+
ChatInput,
|
|
34
|
+
type ChatInputProps,
|
|
35
|
+
type ChatInputRenderProps,
|
|
36
|
+
} from './chat-ui/chat-input'
|
|
37
|
+
export {
|
|
38
|
+
ToolApproval,
|
|
39
|
+
type ToolApprovalProps,
|
|
40
|
+
type ToolApprovalRenderProps,
|
|
41
|
+
} from './chat-ui/tool-approval'
|
|
42
|
+
export { TextPart, type TextPartProps } from './chat-ui/text-part'
|
|
43
|
+
export { ThinkingPart, type ThinkingPartProps } from './chat-ui/thinking-part'
|