@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,52 @@
|
|
|
1
|
+
import { ReactNode } from 'react';
|
|
2
|
+
export interface ToolApprovalProps {
|
|
3
|
+
/** Tool call ID */
|
|
4
|
+
toolCallId: string;
|
|
5
|
+
/** Tool name */
|
|
6
|
+
toolName: string;
|
|
7
|
+
/** Parsed tool arguments/input */
|
|
8
|
+
input: any;
|
|
9
|
+
/** Approval metadata */
|
|
10
|
+
approval: {
|
|
11
|
+
id: string;
|
|
12
|
+
needsApproval: boolean;
|
|
13
|
+
approved?: boolean;
|
|
14
|
+
};
|
|
15
|
+
/** CSS class name */
|
|
16
|
+
className?: string;
|
|
17
|
+
/** Custom render prop */
|
|
18
|
+
children?: (props: ToolApprovalRenderProps) => ReactNode;
|
|
19
|
+
}
|
|
20
|
+
export interface ToolApprovalRenderProps {
|
|
21
|
+
/** Tool name */
|
|
22
|
+
toolName: string;
|
|
23
|
+
/** Parsed input */
|
|
24
|
+
input: any;
|
|
25
|
+
/** Approve the tool call */
|
|
26
|
+
onApprove: () => void;
|
|
27
|
+
/** Deny the tool call */
|
|
28
|
+
onDeny: () => void;
|
|
29
|
+
/** Whether user has responded */
|
|
30
|
+
hasResponded: boolean;
|
|
31
|
+
/** User's decision (if responded) */
|
|
32
|
+
approved?: boolean;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* @deprecated Use `createChatUI()` interrupt components with `chat.interrupts`.
|
|
36
|
+
* Deprecated in 0.9.0. Removed in 1.0.0.
|
|
37
|
+
*
|
|
38
|
+
* Tool approval component - renders approve/deny buttons for tools that need approval
|
|
39
|
+
*
|
|
40
|
+
* @example
|
|
41
|
+
* ```tsx
|
|
42
|
+
* {part.approval && (
|
|
43
|
+
* <ToolApproval
|
|
44
|
+
* toolCallId={part.id}
|
|
45
|
+
* toolName={part.name}
|
|
46
|
+
* input={JSON.parse(part.arguments)}
|
|
47
|
+
* approval={part.approval}
|
|
48
|
+
* />
|
|
49
|
+
* )}
|
|
50
|
+
* ```
|
|
51
|
+
*/
|
|
52
|
+
export declare function ToolApproval({ toolCallId: _, toolName, input, approval, className, children, }: ToolApprovalProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { useChatContext } from "./chat.js";
|
|
2
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
//#region src/chat-ui/tool-approval.tsx
|
|
4
|
+
/**
|
|
5
|
+
* @deprecated Use `createChatUI()` interrupt components with `chat.interrupts`.
|
|
6
|
+
* Deprecated in 0.9.0. Removed in 1.0.0.
|
|
7
|
+
*
|
|
8
|
+
* Tool approval component - renders approve/deny buttons for tools that need approval
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```tsx
|
|
12
|
+
* {part.approval && (
|
|
13
|
+
* <ToolApproval
|
|
14
|
+
* toolCallId={part.id}
|
|
15
|
+
* toolName={part.name}
|
|
16
|
+
* input={JSON.parse(part.arguments)}
|
|
17
|
+
* approval={part.approval}
|
|
18
|
+
* />
|
|
19
|
+
* )}
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
function ToolApproval({ toolCallId: _, toolName, input, approval, className, children }) {
|
|
23
|
+
const { addToolApprovalResponse } = useChatContext();
|
|
24
|
+
const handleApprove = () => {
|
|
25
|
+
addToolApprovalResponse({
|
|
26
|
+
id: approval.id,
|
|
27
|
+
approved: true
|
|
28
|
+
});
|
|
29
|
+
};
|
|
30
|
+
const handleDeny = () => {
|
|
31
|
+
addToolApprovalResponse({
|
|
32
|
+
id: approval.id,
|
|
33
|
+
approved: false
|
|
34
|
+
});
|
|
35
|
+
};
|
|
36
|
+
const hasResponded = approval.approved !== void 0;
|
|
37
|
+
const renderProps = {
|
|
38
|
+
toolName,
|
|
39
|
+
input,
|
|
40
|
+
onApprove: handleApprove,
|
|
41
|
+
onDeny: handleDeny,
|
|
42
|
+
hasResponded,
|
|
43
|
+
approved: approval.approved
|
|
44
|
+
};
|
|
45
|
+
if (children) return /* @__PURE__ */ jsx(Fragment, { children: children(renderProps) });
|
|
46
|
+
if (hasResponded) return /* @__PURE__ */ jsx("div", {
|
|
47
|
+
className,
|
|
48
|
+
"data-tool-approval": true,
|
|
49
|
+
"data-approval-status": approval.approved ? "approved" : "denied",
|
|
50
|
+
children: approval.approved ? "✓ Approved" : "✗ Denied"
|
|
51
|
+
});
|
|
52
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
53
|
+
className,
|
|
54
|
+
"data-tool-approval": true,
|
|
55
|
+
"data-approval-status": "pending",
|
|
56
|
+
children: [
|
|
57
|
+
/* @__PURE__ */ jsxs("div", {
|
|
58
|
+
"data-approval-header": true,
|
|
59
|
+
children: [/* @__PURE__ */ jsx("strong", { children: toolName }), " requires approval"]
|
|
60
|
+
}),
|
|
61
|
+
/* @__PURE__ */ jsx("div", {
|
|
62
|
+
"data-approval-input": true,
|
|
63
|
+
children: /* @__PURE__ */ jsx("pre", { children: JSON.stringify(input, null, 2) })
|
|
64
|
+
}),
|
|
65
|
+
/* @__PURE__ */ jsxs("div", {
|
|
66
|
+
"data-approval-actions": true,
|
|
67
|
+
children: [/* @__PURE__ */ jsx("button", {
|
|
68
|
+
onClick: handleApprove,
|
|
69
|
+
"data-approval-approve": true,
|
|
70
|
+
children: "Approve"
|
|
71
|
+
}), /* @__PURE__ */ jsx("button", {
|
|
72
|
+
onClick: handleDeny,
|
|
73
|
+
"data-approval-deny": true,
|
|
74
|
+
children: "Deny"
|
|
75
|
+
})]
|
|
76
|
+
})
|
|
77
|
+
]
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
//#endregion
|
|
81
|
+
export { ToolApproval };
|
|
82
|
+
|
|
83
|
+
//# sourceMappingURL=tool-approval.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tool-approval.js","names":[],"sources":["../../../src/chat-ui/tool-approval.tsx"],"sourcesContent":["import { useChatContext } from './chat'\nimport type { ReactNode } from 'react'\n\nexport interface ToolApprovalProps {\n /** Tool call ID */\n toolCallId: string\n /** Tool name */\n toolName: string\n /** Parsed tool arguments/input */\n input: any\n /** Approval metadata */\n approval: {\n id: string\n needsApproval: boolean\n approved?: boolean\n }\n /** CSS class name */\n className?: string\n /** Custom render prop */\n children?: (props: ToolApprovalRenderProps) => ReactNode\n}\n\nexport interface ToolApprovalRenderProps {\n /** Tool name */\n toolName: string\n /** Parsed input */\n input: any\n /** Approve the tool call */\n onApprove: () => void\n /** Deny the tool call */\n onDeny: () => void\n /** Whether user has responded */\n hasResponded: boolean\n /** User's decision (if responded) */\n approved?: boolean\n}\n\n/**\n * @deprecated Use `createChatUI()` interrupt components with `chat.interrupts`.\n * Deprecated in 0.9.0. Removed in 1.0.0.\n *\n * Tool approval component - renders approve/deny buttons for tools that need approval\n *\n * @example\n * ```tsx\n * {part.approval && (\n * <ToolApproval\n * toolCallId={part.id}\n * toolName={part.name}\n * input={JSON.parse(part.arguments)}\n * approval={part.approval}\n * />\n * )}\n * ```\n */\nexport function ToolApproval({\n toolCallId: _,\n toolName,\n input,\n approval,\n className,\n children,\n}: ToolApprovalProps) {\n const { addToolApprovalResponse } = useChatContext()\n\n const handleApprove = () => {\n void addToolApprovalResponse({\n id: approval.id,\n approved: true,\n })\n }\n\n const handleDeny = () => {\n void addToolApprovalResponse({\n id: approval.id,\n approved: false,\n })\n }\n\n const hasResponded = approval.approved !== undefined\n\n const renderProps: ToolApprovalRenderProps = {\n toolName,\n input,\n onApprove: handleApprove,\n onDeny: handleDeny,\n hasResponded,\n approved: approval.approved,\n }\n\n // Render prop pattern\n if (children) {\n return <>{children(renderProps)}</>\n }\n\n // Already responded - show decision\n if (hasResponded) {\n return (\n <div\n className={className}\n data-tool-approval\n data-approval-status={approval.approved ? 'approved' : 'denied'}\n >\n {approval.approved ? '✓ Approved' : '✗ Denied'}\n </div>\n )\n }\n\n // Default approval UI\n return (\n <div\n className={className}\n data-tool-approval\n data-approval-status=\"pending\"\n >\n <div data-approval-header>\n <strong>{toolName}</strong> requires approval\n </div>\n <div data-approval-input>\n <pre>{JSON.stringify(input, null, 2)}</pre>\n </div>\n <div data-approval-actions>\n <button onClick={handleApprove} data-approval-approve>\n Approve\n </button>\n <button onClick={handleDeny} data-approval-deny>\n Deny\n </button>\n </div>\n </div>\n )\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;AAuDA,SAAgB,aAAa,EAC3B,YAAY,GACZ,UACA,OACA,UACA,WACA,YACoB;CACpB,MAAM,EAAE,4BAA4B,eAAe;CAEnD,MAAM,sBAAsB;EAC1B,wBAA6B;GAC3B,IAAI,SAAS;GACb,UAAU;EACZ,CAAC;CACH;CAEA,MAAM,mBAAmB;EACvB,wBAA6B;GAC3B,IAAI,SAAS;GACb,UAAU;EACZ,CAAC;CACH;CAEA,MAAM,eAAe,SAAS,aAAa,KAAA;CAE3C,MAAM,cAAuC;EAC3C;EACA;EACA,WAAW;EACX,QAAQ;EACR;EACA,UAAU,SAAS;CACrB;CAGA,IAAI,UACF,OAAO,oBAAA,UAAA,EAAA,UAAG,SAAS,WAAW,EAAI,CAAA;CAIpC,IAAI,cACF,OACE,oBAAC,OAAD;EACa;EACX,sBAAA;EACA,wBAAsB,SAAS,WAAW,aAAa;EAEtD,UAAA,SAAS,WAAW,eAAe;CACjC,CAAA;CAKT,OACE,qBAAC,OAAD;EACa;EACX,sBAAA;EACA,wBAAqB;EAHvB,UAAA;GAKE,qBAAC,OAAD;IAAK,wBAAA;IAAL,UAAA,CACE,oBAAC,UAAD,EAAA,UAAS,SAAiB,CAAA,GAAC,oBACxB;;GACL,oBAAC,OAAD;IAAK,uBAAA;IACH,UAAA,oBAAC,OAAD,EAAA,UAAM,KAAK,UAAU,OAAO,MAAM,CAAC,EAAO,CAAA;GACvC,CAAA;GACL,qBAAC,OAAD;IAAK,yBAAA;IAAL,UAAA,CACE,oBAAC,UAAD;KAAQ,SAAS;KAAe,yBAAA;KAAsB,UAAA;IAE9C,CAAA,GACR,oBAAC,UAAD;KAAQ,SAAS;KAAY,sBAAA;KAAmB,UAAA;IAExC,CAAA,CACL;;EACF;;AAET"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { UIMessage } from '../types.js';
|
|
2
|
+
type ToolResultPart = Extract<UIMessage['parts'][number], {
|
|
3
|
+
type: 'tool-result';
|
|
4
|
+
}>;
|
|
5
|
+
/** `string | Array<ContentPart>` — a tool result's raw content. */
|
|
6
|
+
type ToolResultContent = ToolResultPart['content'];
|
|
7
|
+
/**
|
|
8
|
+
* Reduce a tool-result part's `content` to a plain string for rendering.
|
|
9
|
+
*
|
|
10
|
+
* Tool results carry `string | Array<ContentPart>` (multimodal results are
|
|
11
|
+
* normalized to an array of content parts upstream). The `ChatMessage`
|
|
12
|
+
* renderers operate on plain strings, so array content is flattened to the
|
|
13
|
+
* concatenation of its text parts. Non-text parts (image, audio, video,
|
|
14
|
+
* document) have no string form here and are skipped — matching the
|
|
15
|
+
* text-extraction behavior used elsewhere for `string | Array<ContentPart>`.
|
|
16
|
+
*/
|
|
17
|
+
export declare function toolResultContentToString(content: ToolResultContent): string;
|
|
18
|
+
export {};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
//#region src/chat-ui/tool-result-content.ts
|
|
2
|
+
/**
|
|
3
|
+
* Reduce a tool-result part's `content` to a plain string for rendering.
|
|
4
|
+
*
|
|
5
|
+
* Tool results carry `string | Array<ContentPart>` (multimodal results are
|
|
6
|
+
* normalized to an array of content parts upstream). The `ChatMessage`
|
|
7
|
+
* renderers operate on plain strings, so array content is flattened to the
|
|
8
|
+
* concatenation of its text parts. Non-text parts (image, audio, video,
|
|
9
|
+
* document) have no string form here and are skipped — matching the
|
|
10
|
+
* text-extraction behavior used elsewhere for `string | Array<ContentPart>`.
|
|
11
|
+
*/
|
|
12
|
+
function toolResultContentToString(content) {
|
|
13
|
+
if (typeof content === "string") return content;
|
|
14
|
+
return content.filter((part) => part.type === "text").map((part) => part.content).join("");
|
|
15
|
+
}
|
|
16
|
+
//#endregion
|
|
17
|
+
export { toolResultContentToString };
|
|
18
|
+
|
|
19
|
+
//# sourceMappingURL=tool-result-content.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tool-result-content.js","names":[],"sources":["../../../src/chat-ui/tool-result-content.ts"],"sourcesContent":["import type { UIMessage } from '../types'\n\ntype ToolResultPart = Extract<\n UIMessage['parts'][number],\n { type: 'tool-result' }\n>\n\n/** `string | Array<ContentPart>` — a tool result's raw content. */\ntype ToolResultContent = ToolResultPart['content']\n\ntype ContentPartItem = Exclude<ToolResultContent, string>[number]\n\n/**\n * Reduce a tool-result part's `content` to a plain string for rendering.\n *\n * Tool results carry `string | Array<ContentPart>` (multimodal results are\n * normalized to an array of content parts upstream). The `ChatMessage`\n * renderers operate on plain strings, so array content is flattened to the\n * concatenation of its text parts. Non-text parts (image, audio, video,\n * document) have no string form here and are skipped — matching the\n * text-extraction behavior used elsewhere for `string | Array<ContentPart>`.\n */\nexport function toolResultContentToString(content: ToolResultContent): string {\n if (typeof content === 'string') return content\n return content\n .filter(\n (part): part is Extract<ContentPartItem, { type: 'text' }> =>\n part.type === 'text',\n )\n .map((part) => part.content)\n .join('')\n}\n"],"mappings":";;;;;;;;;;;AAsBA,SAAgB,0BAA0B,SAAoC;CAC5E,IAAI,OAAO,YAAY,UAAU,OAAO;CACxC,OAAO,QACJ,QACE,SACC,KAAK,SAAS,MAClB,CAAC,CACA,KAAK,SAAS,KAAK,OAAO,CAAC,CAC3B,KAAK,EAAE;AACZ"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { AnyClientTool, InterruptDefinition, SchemaInput } from '@tanstack/ai/client';
|
|
2
|
+
import { InferredClientContext } from '@tanstack/ai-client';
|
|
3
|
+
import { UseChatOptions } from './types.js';
|
|
4
|
+
type ChatHookOverrides<TTools extends ReadonlyArray<AnyClientTool>, TSchema extends SchemaInput | undefined, TContext, TInterrupts extends ReadonlyArray<InterruptDefinition<any, any, any, any>>> = {
|
|
5
|
+
threadId?: string;
|
|
6
|
+
live?: boolean;
|
|
7
|
+
forwardedProps?: Record<string, any>;
|
|
8
|
+
body?: Record<string, any>;
|
|
9
|
+
initialMessages?: UseChatOptions<TTools, TSchema, TContext, TInterrupts>['initialMessages'];
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* Bind chat options once at module scope. The returned `useChat` hook
|
|
13
|
+
* creates a chat instance from those options.
|
|
14
|
+
*
|
|
15
|
+
* Pass per-call overrides for instance keys such as `threadId`,
|
|
16
|
+
* `initialMessages`, `live`, and `forwardedProps`. Do not change `tools`,
|
|
17
|
+
* `interrupts`, or `outputSchema` here. Those stay on the factory options.
|
|
18
|
+
*
|
|
19
|
+
* Rename the hook at the call site if you already import `useChat` from
|
|
20
|
+
* `@tanstack/ai-react`: `const { useChat: useAppChat } = createChatHook(chatOptions)`.
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* ```tsx
|
|
24
|
+
* const { useChat } = createChatHook(chatOptions)
|
|
25
|
+
*
|
|
26
|
+
* function Support() {
|
|
27
|
+
* const chat = useChat({ threadId: 'support-1' })
|
|
28
|
+
* return <UI.Chat chat={chat} />
|
|
29
|
+
* }
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
32
|
+
export 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>): {
|
|
33
|
+
useChat: (overrides?: ChatHookOverrides<TTools, TSchema, TContext, TInterrupts>) => import('./types.js').UseChatReturn<TTools, TSchema, TInterrupts>;
|
|
34
|
+
};
|
|
35
|
+
export {};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { useChat } from "./use-chat.js";
|
|
2
|
+
//#region src/create-chat-hook.ts
|
|
3
|
+
/**
|
|
4
|
+
* Bind chat options once at module scope. The returned `useChat` hook
|
|
5
|
+
* creates a chat instance from those options.
|
|
6
|
+
*
|
|
7
|
+
* Pass per-call overrides for instance keys such as `threadId`,
|
|
8
|
+
* `initialMessages`, `live`, and `forwardedProps`. Do not change `tools`,
|
|
9
|
+
* `interrupts`, or `outputSchema` here. Those stay on the factory options.
|
|
10
|
+
*
|
|
11
|
+
* Rename the hook at the call site if you already import `useChat` from
|
|
12
|
+
* `@tanstack/ai-react`: `const { useChat: useAppChat } = createChatHook(chatOptions)`.
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```tsx
|
|
16
|
+
* const { useChat } = createChatHook(chatOptions)
|
|
17
|
+
*
|
|
18
|
+
* function Support() {
|
|
19
|
+
* const chat = useChat({ threadId: 'support-1' })
|
|
20
|
+
* return <UI.Chat chat={chat} />
|
|
21
|
+
* }
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
function createChatHook(options) {
|
|
25
|
+
function useChat$1(overrides) {
|
|
26
|
+
if (!overrides) return useChat(options);
|
|
27
|
+
return useChat({
|
|
28
|
+
...options,
|
|
29
|
+
...overrides
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
return { useChat: useChat$1 };
|
|
33
|
+
}
|
|
34
|
+
//#endregion
|
|
35
|
+
export { createChatHook };
|
|
36
|
+
|
|
37
|
+
//# sourceMappingURL=create-chat-hook.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"create-chat-hook.js","names":[],"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/client'\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` hook\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 hook at the call site if you already import `useChat` from\n * `@tanstack/ai-react`: `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} />\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,SAAS,UACP,WACA;EACA,IAAI,CAAC,WACH,OAAO,QAAe,OAAO;EAE/B,OAAO,QAAe;GAAE,GAAG;GAAS,GAAG;EAAU,CAAC;CACpD;CAEA,OAAO,EAAE,SAAA,UAAQ;AACnB"}
|
package/dist/esm/index.d.ts
CHANGED
package/dist/esm/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 { useRealtimeChat } from "./use-realtime-chat.js";
|
|
4
5
|
import { useMcpAppBridge } from "./use-mcp-app-bridge.js";
|
|
@@ -11,4 +12,4 @@ import { useSummarize } from "./use-summarize.js";
|
|
|
11
12
|
import { useGenerateVideo } from "./use-generate-video.js";
|
|
12
13
|
import { useAudioRecorder } from "./use-audio-recorder.js";
|
|
13
14
|
import { StorageUnavailableError, createChatClientOptions, createMcpAppBridge, fetchHttpStream, fetchServerSentEvents, indexedDBPersistence, localStoragePersistence, rpcStream, sessionStoragePersistence, stream, webSocket, xhrHttpStream, xhrServerSentEvents } from "@tanstack/ai-client";
|
|
14
|
-
export { StorageUnavailableError, createChatClientOptions, createMcpAppBridge, fetchHttpStream, fetchServerSentEvents, indexedDBPersistence, localStoragePersistence, rpcStream, sessionStoragePersistence, stream, useAudioRecorder, useByok, useChat, useGenerateAudio, useGenerateImage, useGenerateSpeech, useGenerateVideo, useGeneration, useMcpAppBridge, useRealtimeChat, useSummarize, useTranscription, webSocket, xhrHttpStream, xhrServerSentEvents };
|
|
15
|
+
export { StorageUnavailableError, createChatClientOptions, createChatHook, createMcpAppBridge, fetchHttpStream, fetchServerSentEvents, indexedDBPersistence, localStoragePersistence, rpcStream, sessionStoragePersistence, stream, useAudioRecorder, useByok, useChat, useGenerateAudio, useGenerateImage, useGenerateSpeech, useGenerateVideo, useGeneration, useMcpAppBridge, useRealtimeChat, useSummarize, useTranscription, webSocket, xhrHttpStream, xhrServerSentEvents };
|
package/dist/esm/ui.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export { createChatUI, type ChatUIComponents, type ChatUIFactoryConfig, type ChatUIHost, type ChatUIQueueItem, type InputProps, type InterruptProps, type LayoutProps, type MessageProps, type PartProps, type QueueProps, type ToolProps, } from './chat-ui/create-ui.js';
|
|
2
|
+
export { createChatHook } from './chat-ui/create-chat-hook.js';
|
|
3
|
+
export { createChatHookContexts, type ChatUIContexts, } from './chat-ui/create-ui-contexts.js';
|
|
4
|
+
export { Chat, useChatContext, type ChatProps } from './chat-ui/chat.js';
|
|
5
|
+
export { ChatMessages, type ChatMessagesProps } from './chat-ui/chat-messages.js';
|
|
6
|
+
export { ChatMessage, type ChatMessageProps, type ToolCallRenderProps, } from './chat-ui/chat-message.js';
|
|
7
|
+
export { ChatInput, type ChatInputProps, type ChatInputRenderProps, } from './chat-ui/chat-input.js';
|
|
8
|
+
export { ToolApproval, type ToolApprovalProps, type ToolApprovalRenderProps, } from './chat-ui/tool-approval.js';
|
|
9
|
+
export { TextPart, type TextPartProps } from './chat-ui/text-part.js';
|
|
10
|
+
export { ThinkingPart, type ThinkingPartProps } from './chat-ui/thinking-part.js';
|
package/dist/esm/ui.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { createChatHookContexts } from "./chat-ui/create-ui-contexts.js";
|
|
2
|
+
import { createChatUI } from "./chat-ui/create-ui.js";
|
|
3
|
+
import { createChatHook } from "./chat-ui/create-chat-hook.js";
|
|
4
|
+
import { Chat, useChatContext } from "./chat-ui/chat.js";
|
|
5
|
+
import { ThinkingPart } from "./chat-ui/thinking-part.js";
|
|
6
|
+
import { ChatMessage } from "./chat-ui/chat-message.js";
|
|
7
|
+
import { ChatMessages } from "./chat-ui/chat-messages.js";
|
|
8
|
+
import { ChatInput } from "./chat-ui/chat-input.js";
|
|
9
|
+
import { ToolApproval } from "./chat-ui/tool-approval.js";
|
|
10
|
+
import { TextPart } from "./chat-ui/text-part.js";
|
|
11
|
+
export { Chat, ChatInput, ChatMessage, ChatMessages, TextPart, ThinkingPart, ToolApproval, createChatHook, createChatHookContexts, createChatUI, useChatContext };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tanstack/ai-react",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.23.0",
|
|
4
4
|
"description": "React hooks for TanStack AI streaming chat, realtime voice, structured outputs, and media generation.",
|
|
5
5
|
"author": "Tanner Linsley",
|
|
6
6
|
"license": "MIT",
|
|
@@ -28,6 +28,10 @@
|
|
|
28
28
|
"./mcp-apps": {
|
|
29
29
|
"types": "./dist/esm/mcp-apps.d.ts",
|
|
30
30
|
"import": "./dist/esm/mcp-apps.js"
|
|
31
|
+
},
|
|
32
|
+
"./ui": {
|
|
33
|
+
"types": "./dist/esm/ui.d.ts",
|
|
34
|
+
"import": "./dist/esm/ui.js"
|
|
31
35
|
}
|
|
32
36
|
},
|
|
33
37
|
"files": [
|
|
@@ -49,17 +53,26 @@
|
|
|
49
53
|
"media-generation"
|
|
50
54
|
],
|
|
51
55
|
"dependencies": {
|
|
52
|
-
"
|
|
56
|
+
"react-markdown": "^10.1.0",
|
|
57
|
+
"rehype-highlight": "^7.0.2",
|
|
58
|
+
"rehype-raw": "^7.0.0",
|
|
59
|
+
"rehype-sanitize": "^6.0.0",
|
|
60
|
+
"remark-gfm": "^4.0.1",
|
|
61
|
+
"@tanstack/ai-client": "^0.30.0"
|
|
53
62
|
},
|
|
54
63
|
"peerDependencies": {
|
|
55
64
|
"@mcp-ui/client": "^7",
|
|
56
65
|
"@types/react": ">=18.0.0",
|
|
57
66
|
"react": ">=18.0.0",
|
|
58
|
-
"
|
|
67
|
+
"react-dom": ">=18.0.0",
|
|
68
|
+
"@tanstack/ai": "^0.52.1"
|
|
59
69
|
},
|
|
60
70
|
"peerDependenciesMeta": {
|
|
61
71
|
"@mcp-ui/client": {
|
|
62
72
|
"optional": true
|
|
73
|
+
},
|
|
74
|
+
"react-dom": {
|
|
75
|
+
"optional": true
|
|
63
76
|
}
|
|
64
77
|
},
|
|
65
78
|
"devDependencies": {
|
|
@@ -70,8 +83,9 @@
|
|
|
70
83
|
"@vitest/coverage-v8": "4.1.10",
|
|
71
84
|
"jsdom": "^27.4.0",
|
|
72
85
|
"react": "^19.2.3",
|
|
86
|
+
"react-dom": "^19.2.3",
|
|
73
87
|
"vite": "^8.2.1",
|
|
74
|
-
"@tanstack/ai": "0.52.
|
|
88
|
+
"@tanstack/ai": "0.52.1"
|
|
75
89
|
},
|
|
76
90
|
"scripts": {
|
|
77
91
|
"clean": "premove ./build ./dist",
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { useRef, useState } from 'react'
|
|
2
|
+
import { useChatContext } from './chat'
|
|
3
|
+
import type { ReactNode, RefObject } from 'react'
|
|
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 to the input element */
|
|
17
|
+
inputRef: RefObject<HTMLInputElement | HTMLTextAreaElement | null>
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface ChatInputProps {
|
|
21
|
+
/** Render prop for full control */
|
|
22
|
+
children?: (props: ChatInputRenderProps) => ReactNode
|
|
23
|
+
/** CSS class name */
|
|
24
|
+
className?: string
|
|
25
|
+
/** Placeholder text */
|
|
26
|
+
placeholder?: string
|
|
27
|
+
/** Disable input */
|
|
28
|
+
disabled?: boolean
|
|
29
|
+
/** Submit on Enter (Shift+Enter for new line) */
|
|
30
|
+
submitOnEnter?: boolean
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* @deprecated Use `createChatUI()` and an application-owned input component.
|
|
35
|
+
* Deprecated in 0.9.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} onChange={(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({
|
|
63
|
+
children,
|
|
64
|
+
className,
|
|
65
|
+
placeholder = 'Type a message...',
|
|
66
|
+
disabled: disabledProp,
|
|
67
|
+
submitOnEnter = true,
|
|
68
|
+
}: ChatInputProps) {
|
|
69
|
+
const { sendMessage, isLoading } = useChatContext()
|
|
70
|
+
const [value, setValue] = useState('')
|
|
71
|
+
const inputRef = useRef<HTMLInputElement | HTMLTextAreaElement>(null)
|
|
72
|
+
|
|
73
|
+
const disabled = disabledProp || isLoading
|
|
74
|
+
|
|
75
|
+
const handleSubmit = () => {
|
|
76
|
+
if (!value.trim() || disabled) return
|
|
77
|
+
void sendMessage(value)
|
|
78
|
+
setValue('')
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const renderProps: ChatInputRenderProps = {
|
|
82
|
+
value,
|
|
83
|
+
onChange: setValue,
|
|
84
|
+
onSubmit: handleSubmit,
|
|
85
|
+
isLoading,
|
|
86
|
+
disabled,
|
|
87
|
+
inputRef,
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Render prop pattern
|
|
91
|
+
if (children) {
|
|
92
|
+
return <>{children(renderProps)}</>
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Default implementation
|
|
96
|
+
return (
|
|
97
|
+
<div
|
|
98
|
+
className={className}
|
|
99
|
+
data-chat-input
|
|
100
|
+
style={{
|
|
101
|
+
display: 'flex',
|
|
102
|
+
gap: '0.75rem',
|
|
103
|
+
alignItems: 'center',
|
|
104
|
+
width: '100%',
|
|
105
|
+
}}
|
|
106
|
+
>
|
|
107
|
+
<input
|
|
108
|
+
ref={inputRef as React.RefObject<HTMLInputElement>}
|
|
109
|
+
type="text"
|
|
110
|
+
value={value}
|
|
111
|
+
onChange={(e) => setValue(e.target.value)}
|
|
112
|
+
onKeyDown={(e) => {
|
|
113
|
+
if (submitOnEnter && e.key === 'Enter') {
|
|
114
|
+
e.preventDefault()
|
|
115
|
+
handleSubmit()
|
|
116
|
+
}
|
|
117
|
+
}}
|
|
118
|
+
placeholder={placeholder}
|
|
119
|
+
disabled={disabled}
|
|
120
|
+
data-chat-textarea
|
|
121
|
+
style={{
|
|
122
|
+
flex: 1,
|
|
123
|
+
padding: '0.75rem 1rem',
|
|
124
|
+
fontSize: '0.875rem',
|
|
125
|
+
border: '1px solid rgba(255, 255, 255, 0.1)',
|
|
126
|
+
borderRadius: '0.75rem',
|
|
127
|
+
backgroundColor: 'rgba(31, 41, 55, 0.5)',
|
|
128
|
+
color: 'white',
|
|
129
|
+
outline: 'none',
|
|
130
|
+
transition: 'all 0.2s',
|
|
131
|
+
}}
|
|
132
|
+
onFocus={(e) => {
|
|
133
|
+
e.target.style.borderColor = 'rgba(249, 115, 22, 0.4)'
|
|
134
|
+
e.target.style.boxShadow = '0 0 0 2px rgba(249, 115, 22, 0.2)'
|
|
135
|
+
}}
|
|
136
|
+
onBlur={(e) => {
|
|
137
|
+
e.target.style.borderColor = 'rgba(255, 255, 255, 0.1)'
|
|
138
|
+
e.target.style.boxShadow = 'none'
|
|
139
|
+
}}
|
|
140
|
+
/>
|
|
141
|
+
<button
|
|
142
|
+
onClick={handleSubmit}
|
|
143
|
+
disabled={disabled || !value.trim()}
|
|
144
|
+
data-chat-submit
|
|
145
|
+
style={{
|
|
146
|
+
padding: '0.75rem 1.5rem',
|
|
147
|
+
fontSize: '0.875rem',
|
|
148
|
+
fontWeight: 500,
|
|
149
|
+
color: 'white',
|
|
150
|
+
backgroundColor:
|
|
151
|
+
disabled || !value.trim()
|
|
152
|
+
? 'rgba(107, 114, 128, 0.5)'
|
|
153
|
+
: 'rgb(249, 115, 22)',
|
|
154
|
+
border: 'none',
|
|
155
|
+
borderRadius: '0.75rem',
|
|
156
|
+
cursor: disabled || !value.trim() ? 'not-allowed' : 'pointer',
|
|
157
|
+
transition: 'all 0.2s',
|
|
158
|
+
whiteSpace: 'nowrap',
|
|
159
|
+
}}
|
|
160
|
+
onMouseEnter={(e) => {
|
|
161
|
+
if (!disabled && value.trim()) {
|
|
162
|
+
;(e.target as HTMLButtonElement).style.backgroundColor =
|
|
163
|
+
'rgb(234, 88, 12)'
|
|
164
|
+
}
|
|
165
|
+
}}
|
|
166
|
+
onMouseLeave={(e) => {
|
|
167
|
+
if (!disabled && value.trim()) {
|
|
168
|
+
;(e.target as HTMLButtonElement).style.backgroundColor =
|
|
169
|
+
'rgb(249, 115, 22)'
|
|
170
|
+
}
|
|
171
|
+
}}
|
|
172
|
+
>
|
|
173
|
+
{isLoading ? 'Sending...' : 'Send'}
|
|
174
|
+
</button>
|
|
175
|
+
</div>
|
|
176
|
+
)
|
|
177
|
+
}
|