diff --git a/.changeset/create-chat-hook.md b/.changeset/create-chat-hook.md new file mode 100644 index 0000000000..aea3db151e --- /dev/null +++ b/.changeset/create-chat-hook.md @@ -0,0 +1,12 @@ +--- +'@tanstack/ai-react': minor +'@tanstack/ai-solid': minor +'@tanstack/ai-vue': minor +'@tanstack/ai-svelte': minor +--- + +Chat UI now lives on the framework packages at `@tanstack/ai-react/ui`, `@tanstack/ai-solid/ui`, `@tanstack/ai-vue/ui`, and `@tanstack/ai-svelte/ui`. + +Call `useChat(options)` from the framework package. Pass that instance and a typed `components` map to ``. `useChatContext()` reads the same instance from inside `Chat`. + +`createChatHook(options)` on the main package still returns a bound `useChat`. It does not take `chatComponents`. diff --git a/.changeset/deprecate-ai-ui-packages.md b/.changeset/deprecate-ai-ui-packages.md new file mode 100644 index 0000000000..164cc1c77d --- /dev/null +++ b/.changeset/deprecate-ai-ui-packages.md @@ -0,0 +1,11 @@ +--- +'@tanstack/ai-react-ui': minor +'@tanstack/ai-solid-ui': minor +'@tanstack/ai-vue-ui': minor +--- + +Deprecated. Import chat UI from `@tanstack/ai-react/ui`, `@tanstack/ai-solid/ui`, or `@tanstack/ai-vue/ui` instead. + +These packages re-export the new `/ui` subpath until 1.0.0. `npm install` prints a deprecation warning. + +`@tanstack/ai-svelte-ui` was never published. Svelte chat UI lives only on `@tanstack/ai-svelte/ui`. diff --git a/.changeset/typed-headless-chat-ui.md b/.changeset/typed-headless-chat-ui.md index db5dfc9eec..14c84dab83 100644 --- a/.changeset/typed-headless-chat-ui.md +++ b/.changeset/typed-headless-chat-ui.md @@ -1,9 +1,9 @@ --- '@tanstack/ai-client': minor -'@tanstack/ai-react-ui': minor -'@tanstack/ai-solid-ui': minor -'@tanstack/ai-vue-ui': minor -'@tanstack/ai-svelte-ui': minor +'@tanstack/ai-react': minor +'@tanstack/ai-solid': minor +'@tanstack/ai-vue': minor +'@tanstack/ai-svelte': minor --- Add typed headless `createChatUI()` adapters. Chat options control the types of message parts, tools, structured output, and interrupts. `defineComponents` requires a component for every tool name and every registered interrupt id. `InterruptProps` pins a tool approval or a registered generic interrupt. Old Chat orchestration stays importable and deprecated until 1.0. diff --git a/README.md b/README.md index c9bbb8c10d..d524f36907 100644 --- a/README.md +++ b/README.md @@ -226,15 +226,15 @@ on. ## Framework Packages -| Package | What it provides | -| -------------------------------------------------------------------------- | ------------------------------------------------------------------------ | -| [`@tanstack/ai-client`](https://tanstack.com/ai/latest/docs/api/ai-client) | Headless chat, realtime, and generation clients | -| [`@tanstack/ai-react`](https://tanstack.com/ai/latest/docs/api/ai-react) | React hooks including `useChat`, `useRealtimeChat`, and generation hooks | -| [`@tanstack/ai-solid`](https://tanstack.com/ai/latest/docs/api/ai-solid) | Solid hooks for chat and generations | -| [`@tanstack/ai-vue`](https://tanstack.com/ai/latest/docs/api/ai-vue) | Vue composables for chat and generations | -| [`@tanstack/ai-svelte`](https://tanstack.com/ai/latest/docs/api/ai-svelte) | Svelte 5 factories for chat and generations | -| [`@tanstack/ai-preact`](https://tanstack.com/ai/latest/docs/api/ai-preact) | Preact hooks for chat | -| `@tanstack/ai-react-ui`, `@tanstack/ai-solid-ui`, `@tanstack/ai-vue-ui` | Headless UI components for chat interfaces | +| Package | What it provides | +| ------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | +| [`@tanstack/ai-client`](https://tanstack.com/ai/latest/docs/api/ai-client) | Headless chat, realtime, and generation clients | +| [`@tanstack/ai-react`](https://tanstack.com/ai/latest/docs/api/ai-react) | React hooks including `useChat`, `useRealtimeChat`, and generation hooks | +| [`@tanstack/ai-solid`](https://tanstack.com/ai/latest/docs/api/ai-solid) | Solid hooks for chat and generations | +| [`@tanstack/ai-vue`](https://tanstack.com/ai/latest/docs/api/ai-vue) | Vue composables for chat and generations | +| [`@tanstack/ai-svelte`](https://tanstack.com/ai/latest/docs/api/ai-svelte) | Svelte 5 factories for chat and generations | +| [`@tanstack/ai-preact`](https://tanstack.com/ai/latest/docs/api/ai-preact) | Preact hooks for chat | +| `@tanstack/ai-react/ui`, `@tanstack/ai-solid/ui`, `@tanstack/ai-vue/ui`, `@tanstack/ai-svelte/ui` | Headless UI components for chat interfaces. `@tanstack/ai-react-ui`, `@tanstack/ai-solid-ui`, and `@tanstack/ai-vue-ui` are deprecated. | ## Advanced Docs diff --git a/codemods/ag-ui-compliance/transform.ts b/codemods/ag-ui-compliance/transform.ts index 37eb154620..d4924f7a90 100644 --- a/codemods/ag-ui-compliance/transform.ts +++ b/codemods/ag-ui-compliance/transform.ts @@ -36,11 +36,11 @@ import type { const FRAMEWORK_USE_CHAT_PACKAGES = new Set([ '@tanstack/ai-react', - '@tanstack/ai-react-ui', + '@tanstack/ai-react/ui', '@tanstack/ai-vue', - '@tanstack/ai-vue-ui', + '@tanstack/ai-vue/ui', '@tanstack/ai-solid', - '@tanstack/ai-solid-ui', + '@tanstack/ai-solid/ui', '@tanstack/ai-preact', ]) diff --git a/docs/api/ai-react.md b/docs/api/ai-react.md index ff4fafe768..5ee34a4001 100644 --- a/docs/api/ai-react.md +++ b/docs/api/ai-react.md @@ -14,7 +14,7 @@ keywords: React hooks for TanStack AI, providing convenient React bindings for the headless client. -For a typed headless chat UI, see [React Chat UI](../ui/react) and [Migrate to createChatUI](../migration/create-ui). +For a typed headless chat UI, see [React Chat UI](../ui/react) and [Chat UI packages](../migration/create-ui). For React Native, the documented support surface is narrow: `useChat` with chat connection adapters. React DOM-specific UI packages and TanStack AI devtools UI are not part of the React Native support surface. @@ -28,6 +28,27 @@ For a complete native journey, see npm install @tanstack/ai-react ``` +## `createChatHook(options)` + +Bind `chatOptions` once at module scope. Call `useChat()` in the screen to create the instance. Per-call overrides may set `threadId`, `initialMessages`, `live`, and `forwardedProps`. They must not change `tools`, `interrupts`, or `outputSchema`. + +```tsx +import { createChatHook, fetchServerSentEvents } from "@tanstack/ai-react"; + +const chatOptions = { + connection: fetchServerSentEvents("/api/chat"), +}; + +const { useChat } = createChatHook(chatOptions); + +function ChatScreen() { + const chat = useChat({ threadId: "support-1" }); + return null; +} +``` + +`useChat(chatOptions)` from this package still works when you want to pass the full object at the call site. Rename the bound hook if both are in one file: `const { useChat: useSupportChat } = createChatHook(chatOptions)`. + ## `useChat(options?)` Main hook for managing chat state in React with full type safety. diff --git a/docs/api/ai-solid.md b/docs/api/ai-solid.md index 3fd64dfb6a..3bada592f7 100644 --- a/docs/api/ai-solid.md +++ b/docs/api/ai-solid.md @@ -15,7 +15,7 @@ keywords: SolidJS primitives for TanStack AI, providing convenient SolidJS bindings for the headless client. -For a typed headless chat UI, see [Solid Chat UI](../ui/solid) and [Migrate to createChatUI](../migration/create-ui). +For a typed headless chat UI, see [Solid Chat UI](../ui/solid) and [Chat UI packages](../migration/create-ui). ## Installation @@ -23,6 +23,27 @@ For a typed headless chat UI, see [Solid Chat UI](../ui/solid) and [Migrate to c npm install @tanstack/ai-solid ``` +## `createChatHook(options)` + +Bind `chatOptions` once at module scope. Call `useChat()` in the screen to create the instance. Per-call overrides may set `threadId`, `initialMessages`, `live`, and `forwardedProps`. They must not change `tools`, `interrupts`, or `outputSchema`. + +```tsx +import { createChatHook, fetchServerSentEvents } from "@tanstack/ai-solid"; + +const chatOptions = { + connection: fetchServerSentEvents("/api/chat"), +}; + +const { useChat } = createChatHook(chatOptions); + +function ChatScreen() { + const chat = useChat({ threadId: "support-1" }); + return null; +} +``` + +`useChat(chatOptions)` from this package still works when you want to pass the full object at the call site. Rename the bound primitive if both are in one file: `const { useChat: useSupportChat } = createChatHook(chatOptions)`. + ## `useChat(options?)` Main primitive for managing chat state in SolidJS with full type safety. diff --git a/docs/api/ai-svelte.md b/docs/api/ai-svelte.md index a59e25da52..ba99f6de4e 100644 --- a/docs/api/ai-svelte.md +++ b/docs/api/ai-svelte.md @@ -15,7 +15,7 @@ keywords: Svelte 5 bindings for TanStack AI, providing reactive factory functions for the headless client using Svelte runes. -For a typed headless chat UI, see [Svelte Chat UI](../ui/svelte) and [Migrate to createChatUI](../migration/create-ui). +For a typed headless chat UI, see [Svelte Chat UI](../ui/svelte) and [Chat UI packages](../migration/create-ui). ## Installation @@ -23,6 +23,23 @@ For a typed headless chat UI, see [Svelte Chat UI](../ui/svelte) and [Migrate to npm install @tanstack/ai-svelte ``` +## `createChatHook(options)` + +Bind `chatOptions` once at module scope. Call `createChat()` to create the instance. Per-call overrides may set `threadId`, `initialMessages`, `live`, and `forwardedProps`. They must not change `tools`, `interrupts`, or `outputSchema`. + +```ts +import { createChatHook, fetchServerSentEvents } from "@tanstack/ai-svelte"; + +const chatOptions = { + connection: fetchServerSentEvents("/api/chat"), +}; + +const { createChat } = createChatHook(chatOptions); +const chat = createChat({ threadId: "support-1" }); +``` + +`createChat(chatOptions)` from this package still works when you want to pass the full object at the call site. Rename the bound factory if both are in one file: `const { createChat: createSupportChat } = createChatHook(chatOptions)`. + ## `createChat(options)` Factory function for managing chat state in Svelte 5 with full type safety. diff --git a/docs/api/ai-vue.md b/docs/api/ai-vue.md index cd46cf22e6..ffc8336a80 100644 --- a/docs/api/ai-vue.md +++ b/docs/api/ai-vue.md @@ -15,7 +15,7 @@ keywords: Vue composables for TanStack AI, providing convenient Vue 3 bindings for the headless client. -For a typed headless chat UI, see [Vue Chat UI](../ui/vue) and [Migrate to createChatUI](../migration/create-ui). +For a typed headless chat UI, see [Vue Chat UI](../ui/vue) and [Chat UI packages](../migration/create-ui). ## Installation @@ -23,6 +23,24 @@ For a typed headless chat UI, see [Vue Chat UI](../ui/vue) and [Migrate to creat npm install @tanstack/ai-vue ``` +## `createChatHook(options)` + +Bind `chatOptions` once at module scope. Call `useChat()` in the screen to create the instance. Per-call overrides may set `threadId`, `initialMessages`, `live`, and `forwardedProps`. They must not change `tools`, `interrupts`, or `outputSchema`. + +```ts +import { createChatHook, fetchServerSentEvents } from "@tanstack/ai-vue"; + +const chatOptions = { + connection: fetchServerSentEvents("/api/chat"), +}; + +const { useChat } = createChatHook(chatOptions); + +const chat = useChat({ threadId: "support-1" }); +``` + +`useChat(chatOptions)` from this package still works when you want to pass the full object at the call site. Rename the bound composable if both are in one file: `const { useChat: useSupportChat } = createChatHook(chatOptions)`. + ## `useChat(options?)` Main composable for managing chat state in Vue with full type safety. diff --git a/docs/config.json b/docs/config.json index 0b79db5749..a838e2d832 100644 --- a/docs/config.json +++ b/docs/config.json @@ -219,31 +219,31 @@ "label": "React", "to": "ui/react", "addedAt": "2026-08-26", - "updatedAt": "2026-08-27" + "updatedAt": "2026-08-30" }, { "label": "Solid", "to": "ui/solid", "addedAt": "2026-08-26", - "updatedAt": "2026-08-27" + "updatedAt": "2026-08-30" }, { "label": "Vue", "to": "ui/vue", "addedAt": "2026-08-26", - "updatedAt": "2026-08-27" + "updatedAt": "2026-08-30" }, { "label": "Svelte", "to": "ui/svelte", "addedAt": "2026-08-26", - "updatedAt": "2026-08-27" + "updatedAt": "2026-08-30" }, { "label": "Custom Adapters", "to": "ui/custom-adapters", "addedAt": "2026-08-26", - "updatedAt": "2026-08-26" + "updatedAt": "2026-08-30" } ] }, @@ -886,10 +886,10 @@ "updatedAt": "2026-07-22" }, { - "label": "createChatUI", + "label": "Chat UI packages", "to": "migration/create-ui", "addedAt": "2026-08-26", - "updatedAt": "2026-08-27" + "updatedAt": "2026-08-30" }, { "label": "From Vercel AI SDK", diff --git a/docs/migration/create-ui.md b/docs/migration/create-ui.md index 6a6836231f..8d4baa4fc3 100644 --- a/docs/migration/create-ui.md +++ b/docs/migration/create-ui.md @@ -1,44 +1,35 @@ --- -title: Migrate to createChatUI +title: Chat UI packages id: migrate-create-ui order: 5 -description: "Move chat-state ownership out of the old Chat component and onto createChatUI with a typed component map." +description: "Move from @tanstack/ai-*-ui to the framework /ui subpath. Call useChat and render Chat with a component map." keywords: - tanstack ai - - createChatUI + - Chat - migration - deprecation --- -The old `Chat` component owned chat state and lost configured types. `createChatUI` keeps types from your `chatOptions` and leaves `useChat` in your app. +Change your import from `@tanstack/ai-react-ui` to `@tanstack/ai-react/ui`. Call `useChat(chatOptions)` and render ``. -This is a semantic migration. There is no codemod. +The same move applies to Solid, Vue, and Svelte. + +> **Deprecated.** `@tanstack/ai-react-ui`, `@tanstack/ai-solid-ui`, and `@tanstack/ai-vue-ui` re-export the new `/ui` subpath until each package's `1.0.0`. `npm install` prints a warning. Import from `/ui` in new code. Svelte never published a `*-ui` package. Use `@tanstack/ai-svelte/ui`. ## What changes -1. You call `useChat` or `createChat` yourself. -2. You supply every visible component. -3. Tool inputs stay optional while they stream. -4. Tool approvals come from `chat.interrupts`. -5. Unknown runtime keys can use a fallback or render nothing. -6. `createChatUI()` must run at module scope so identity stays stable. +1. Chat UI lives on the framework package: `@tanstack/ai-react/ui`, `@tanstack/ai-solid/ui`, `@tanstack/ai-vue/ui`, `@tanstack/ai-svelte/ui`. +2. You call `useChat` or `createChat` yourself. +3. You pass a `components` map into `Chat`. +4. You supply every visible component. There is no default markup. ## Why -The old APIs drop configured types, keep unused properties, use a deprecated approval path, cover only part of the message protocol, and own chat state. Two orchestration models duplicate fixes. - -## Minimum versions - -- `@tanstack/ai-react-ui` 0.9.0 -- `@tanstack/ai-solid-ui` 0.8.0 -- `@tanstack/ai-vue-ui` 0.3.0 -- `@tanstack/ai-svelte-ui` 0.2.0 - -Old orchestration exports stay importable until each package's `1.0.0`. `TextPart` and `ThinkingPart` stay supported. +A separate `*-ui` package split chat UI from the framework package. The old `Chat` component also owned chat state and dropped configured types. `Chat` from `/ui` keeps types from your `chatOptions` and leaves state on `useChat`. ## Before -```tsx +```tsx ignore import { fetchServerSentEvents } from '@tanstack/ai-react' import { Chat, ChatMessages, ChatInput } from '@tanstack/ai-react-ui' @@ -58,15 +49,13 @@ export function OldChat() { ```tsx import { fetchServerSentEvents, useChat } from '@tanstack/ai-react' -import { createChatUI } from '@tanstack/ai-react-ui' +import { Chat, type ChatUIComponents } from '@tanstack/ai-react/ui' const chatOptions = { connection: fetchServerSentEvents('/api/chat'), } -const UI = createChatUI(chatOptions) - -const components = UI.defineComponents({ +const components = { layout: ({ renderMessages, renderInput }) => (
{renderMessages()} @@ -74,9 +63,7 @@ const components = UI.defineComponents({
), message: ({ renderParts }) =>
{renderParts()}
, - input: () => { - const chat = UI.useChat() - return ( + input: ({ chat }) => (
{ event.preventDefault() @@ -90,32 +77,21 @@ const components = UI.defineComponents({ >
- ) - }, + ), parts: { fallback: () => null }, -}) +} satisfies ChatUIComponents export function NewChat() { const chat = useChat(chatOptions) - return + return } ``` ## Steps 1. Move `connection`, `tools`, and `interrupts` into a module-level `chatOptions` object. -2. Call `createChatUI(chatOptions)` next to that object. +2. Build a `components` map typed with `ChatUIComponents`. 3. Call `useChat(chatOptions)` in the screen component. -4. Define `layout`, `message`, `parts`, `tools`, and `interrupts` in `defineComponents`. -5. Replace `` with ``. - -## Gotchas - -- A shared `chatOptions` variable does not need `as const`. -- A mapped tool can read `interrupt` and render the approval itself. That approval stays off the list. A component on `interrupts.tools` uses the list. -- Generic interrupts live under `interrupts.generic`: a registered id such as `choosePlan`, plus `fallback`. Unbound interrupts use `fallback`. -- TypeScript requires a `tools` component for every tool name and an `interrupts.generic` component for every interrupt id. `generic.fallback` is optional. -- Matched `tool-result` parts are hidden in automatic traversal. Unmatched results stay visible. -- Nested providers use the nearest chat instance. +4. Render ``. -See the [React UI guide](../ui/react) for a full map. +Old orchestration exports stay importable from the deprecated `*-ui` packages until `1.0.0`. diff --git a/docs/ui/custom-adapters.md b/docs/ui/custom-adapters.md index d51a4e8f60..10c2c61470 100644 --- a/docs/ui/custom-adapters.md +++ b/docs/ui/custom-adapters.md @@ -5,7 +5,7 @@ order: 5 description: "Build a framework adapter on @tanstack/ai-client/ui. The core is types and selectors only." keywords: - tanstack ai - - createChatUI + - Chat - custom adapter - headless ui --- @@ -20,7 +20,7 @@ The subpath gives you: Your adapter owns: -1. Native components and context +1. Native components 2. Native reactivity 3. Render callbacks, slots, or snippets 4. Development warnings for missing mapped keys @@ -29,6 +29,6 @@ Do not add default markup. Do not add a new store. The app owns `useChat` or `cr Call `selectChatUI({ messages, interrupts, inlineToolNames })`. Automatic traversal skips a `tool-result` only when `matched` is true. Keep unmatched results. -Warn once per missing runtime key in development. Each build tool detects development mode differently, so the adapter prints the warning. +Warn once per missing runtime key in development. -See the [React](./react), [Solid](./solid), [Vue](./vue), and [Svelte](./svelte) adapters for the public names to match: `Chat`, `Provider`, `Messages`, `Message`, `Part`, `Interrupts`, `Interrupt`, and `defineComponents`. +See the [React](./react), [Solid](./solid), [Vue](./vue), and [Svelte](./svelte) adapters. The public names to match are `Chat` and `ChatUIComponents`. diff --git a/docs/ui/react.md b/docs/ui/react.md index 2a05727143..0bad7e7096 100644 --- a/docs/ui/react.md +++ b/docs/ui/react.md @@ -2,21 +2,21 @@ title: React Chat UI id: typed-headless-ui-react order: 1 -description: "Build a typed, headless React chat UI with createChatUI. Your chat options control the types of tools, parts, and interrupts." +description: "Render a typed chat tree with Chat and a component map. Your chat options control the types of tools, parts, and interrupts." keywords: - tanstack ai - - createChatUI + - Chat - react - headless ui - useChat - ToolProps --- -Install `@tanstack/ai-react-ui`, then call `createChatUI(chatOptions)` once at module scope. Your app owns `useChat`. The UI only renders. Call `UI.useChat()` inside a mapped component when it needs live chat. That call is the same value you passed into `UI.Chat`. +Install `@tanstack/ai-react`. Import `Chat` from `@tanstack/ai-react/ui`. Call `useChat(chatOptions)` in the screen. Pass a module-level `components` object into `Chat`. You supply every visible component. There is no default markup, style, or copy. -`defineComponents` needs a `tools` entry for every tool name in `chatOptions`. It also needs an `interrupts.generic` entry for every interrupt id. `generic.fallback` is optional. +The map needs a `tools` entry for every tool name in `chatOptions`. It also needs an `interrupts.generic` entry for every interrupt id. `generic.fallback` is optional. ## Server @@ -42,7 +42,7 @@ export async function POST(request: Request) { ```tsx import { fetchServerSentEvents, useChat } from '@tanstack/ai-react' -import { createChatUI } from '@tanstack/ai-react-ui' +import { Chat, type ChatUIComponents } from '@tanstack/ai-react/ui' import { defineInterrupt, toolDefinition } from '@tanstack/ai' import { z } from 'zod' @@ -74,18 +74,9 @@ const chatOptions = { outputSchema: z.object({ answer: z.string() }), } -const UI = createChatUI(chatOptions) - -const components = UI.defineComponents({ - layout: function Layout({ - renderMessages, - renderInterrupts, - renderInput, - }) { - const chat = UI.useChat() +const components = { + layout: ({ chat, renderMessages, renderInterrupts, renderInput }) => { if (chat.error) return

{chat.error.message}

- if (chat.isLoading && chat.messages.length === 0) return

Loading

- if (chat.messages.length === 0) return

Empty

return (
{renderMessages()} @@ -94,47 +85,30 @@ const components = UI.defineComponents({
) }, - message: function Message({ message, renderParts }) { - return
{renderParts()}
- }, - input: function Input() { - const chat = UI.useChat() - return ( -
{ - event.preventDefault() - const form = event.currentTarget - const field = form.elements.namedItem('message') - if (!(field instanceof HTMLInputElement)) return - void chat.sendMessage(field.value) - field.value = '' - }} - > - - -
- ) - }, + message: ({ renderParts }) =>
{renderParts()}
, + input: ({ chat }) => ( +
{ + event.preventDefault() + const form = event.currentTarget + const field = form.elements.namedItem('message') + if (!(field instanceof HTMLInputElement)) return + void chat.sendMessage(field.value) + field.value = '' + }} + > + + +
+ ), parts: { - text: ({ part }) =>

{part.content}

, - structuredOutput: ({ part }) =>
{part.raw}
, - toolResult: ({ part }) => {String(part.content)}, - fallback: ({ part }) => {part.type}, + text: ({ part }) => (part.type === 'text' ?

{part.content}

: null), + structuredOutput: ({ part }) => + part.type === 'structured-output' ?
{part.raw}
: null, + fallback: () => null, }, tools: { - getWeather: ({ part, result }) => { - if (part.state === 'awaiting-input') return

Waiting

- if (part.state === 'input-streaming') return

Streaming input

- if (part.state === 'input-complete') return

{part.input?.city}

- if (part.state === 'approval-requested') return

Need approval

- if (part.state === 'approval-responded') return

Responded

- if (part.state === 'error') return

Error

- return ( -

- {part.input?.city}: {String(part.output?.temperature ?? result?.content)} -

- ) - }, + getWeather: ({ part }) =>

{part.input?.city}

, purchaseItem: ({ part, interrupt }) => (
{part.input?.item} @@ -156,390 +130,37 @@ const components = UI.defineComponents({ fallback: ({ interrupt }) =>

{interrupt.reason}

, }, }, -}) +} satisfies ChatUIComponents export function ChatScreen() { const chat = useChat(chatOptions) - return + return } ``` ## Type a component in its own file -A tool map grows fast. Move a tool into its own file and type the props with `ToolProps`. - -`ToolProps` takes your `chatOptions` type and the tool name. Then `part.input` and `part.output` stay exact. - -Part components work the same way. `PartProps` already has a text part. You do not check `part.type`. Use `'structuredOutput'`, `'thinking'`, `'toolResult'`, and the other keys from the `parts` map. `fallback` still sees every part type. +Use `ToolProps`, `PartProps`, `InterruptProps`, `LayoutProps`, or `InputProps` with `typeof chatOptions`. ```tsx -import { fetchServerSentEvents } from '@tanstack/ai-react' -import { createChatUI, type PartProps, type ToolProps } from '@tanstack/ai-react-ui' -import { toolDefinition } from '@tanstack/ai' -import { z } from 'zod' - -const getWeather = toolDefinition({ - name: 'getWeather', - description: 'Look up weather', - inputSchema: z.object({ city: z.string() }), - outputSchema: z.object({ temperature: z.number() }), -}).client() - -const chatOptions = { - connection: fetchServerSentEvents('/api/chat'), - tools: [getWeather], -} +import type { ToolProps } from '@tanstack/ai-react/ui' +import type { chatOptions } from './options' export function WeatherTool({ part, - result, }: ToolProps) { - if (part.state === 'awaiting-input') return

Waiting

- if (part.state === 'input-streaming') return

Streaming input

- if (part.state === 'error') return

Error

- return ( -

- {part.input?.city}: {String(part.output?.temperature ?? result?.content)} -

- ) -} - -export function TextPart({ part }: PartProps) { - return

{part.content}

-} - -const UI = createChatUI(chatOptions) - -export const components = UI.defineComponents({ - layout: ({ renderMessages }) => renderMessages(), - message: ({ renderParts }) =>
{renderParts()}
, - parts: { text: TextPart, fallback: () => null }, - tools: { getWeather: WeatherTool }, -}) -``` - -1. Put `chatOptions` in a shared module. -2. Import `ToolProps` or `PartProps` from `@tanstack/ai-react-ui`. -3. Type the component with `ToolProps` or `PartProps`. -4. Pass that component into `tools.getWeather` or `parts.text`. - -For an interrupt, use `InterruptProps`. Pass a tool name or a registered interrupt id as the second type argument. Then you do not check `interrupt.kind`. - -- A tool approval: `InterruptProps`. Then `interrupt.toolName` is `'purchaseItem'`. -- A registered generic interrupt: `InterruptProps`. Then `interrupt.payload` and `interrupt.resolveInterrupt` match the definition. - -```tsx -import { fetchServerSentEvents } from '@tanstack/ai-react' -import { createChatUI, type InterruptProps } from '@tanstack/ai-react-ui' -import { defineInterrupt } from '@tanstack/ai' -import { z } from 'zod' - -const choosePlan = defineInterrupt({ - id: 'choosePlan', - payloadSchema: z.object({ title: z.string() }), - responseSchema: z.string(), -}) - -const chatOptions = { - connection: fetchServerSentEvents('/api/chat'), - interrupts: [choosePlan], -} - -export function ChoosePlan({ - interrupt, -}: InterruptProps) { - return ( - - ) -} - -const UI = createChatUI(chatOptions) - -export const components = UI.defineComponents({ - layout: ({ renderInterrupts }) => renderInterrupts(), - message: ({ renderParts }) =>
{renderParts()}
, - parts: { fallback: () => null }, - interrupts: { - generic: { - choosePlan: ChoosePlan, - }, - }, -}) -``` - -Other prop types from the same package: - -- `LayoutProps` -- `MessageProps` -- `InputProps` -- `PartProps` with a part key such as `'text'` -- `InterruptProps` for tool approvals, registered generic interrupts, and `generic.fallback`. Pass a tool name or interrupt id as the second type argument. - -## Read chat from `UI.useChat()` - -Mapped components do not receive `chat` as a prop. Call `UI.useChat()` inside a component when it needs live chat. That call opts the component into chat re-renders. Nested children can call it too. - -```tsx -import { fetchServerSentEvents, useChat } from '@tanstack/ai-react' -import { createChatUI } from '@tanstack/ai-react-ui' - -const chatOptions = { - connection: fetchServerSentEvents('/api/chat'), -} - -const UI = createChatUI(chatOptions) - -function StatusLine() { - const chat = UI.useChat() - if (chat.error) return

{chat.error.message}

- if (chat.isLoading) return

Loading

- return

{chat.messages.length} messages

-} - -const components = UI.defineComponents({ - layout: ({ renderMessages, renderInput }) => ( -
- - {renderMessages()} - {renderInput()} -
- ), - message: ({ renderParts }) =>
{renderParts()}
, - parts: { fallback: () => null }, -}) - -export function ChatScreen() { - const chat = useChat(chatOptions) - return -} -``` - -Call `UI.useChat()` only inside `UI.Chat` or `UI.Provider`. A call outside that tree throws. - -`useChat(chatOptions)` from `@tanstack/ai-react` still owns the state. `UI.useChat()` only reads the instance that you passed into the provider. - -## Tool approvals: inline or list - -A tool with `needsApproval: true` can render its approval in two places. - -### Inline, next to the tool - -Read `interrupt` on the tool. Render the approval in that same component. Do not register `interrupts.tools` for that name. A mapped tool keeps its approval off the list. - -`interrupt` is already the approval for that tool name. You do not check `interrupt.kind`. - -```tsx -import { fetchServerSentEvents, useChat } from '@tanstack/ai-react' -import { createChatUI, type ToolProps } from '@tanstack/ai-react-ui' -import { toolDefinition } from '@tanstack/ai' -import { z } from 'zod' - -const purchaseItem = toolDefinition({ - name: 'purchaseItem', - description: 'Buy an item', - needsApproval: true, - inputSchema: z.object({ item: z.string() }), - outputSchema: z.object({ ok: z.boolean() }), -}).client() - -const chatOptions = { - connection: fetchServerSentEvents('/api/chat'), - tools: [purchaseItem], -} - -const UI = createChatUI(chatOptions) - -function PurchaseItem({ - part, - interrupt, -}: ToolProps) { - return ( -
- {part.input?.item} - {interrupt?.status === 'pending' ? ( - - ) : null} -
- ) -} - -const components = UI.defineComponents({ - layout: ({ renderMessages, renderInterrupts }) => ( -
- {renderMessages()} - {renderInterrupts()} -
- ), - message: ({ renderParts }) =>
{renderParts()}
, - parts: { fallback: () => null }, - tools: { - purchaseItem: PurchaseItem, - }, -}) - -export function InlineApprovalChat() { - const chat = useChat(chatOptions) - return + return

{part.input?.city}

} ``` -To split the approval into its own file, type it with `InterruptProps`. Render that component from the tool. - -### List, in `renderInterrupts()` - -Register the approval under `interrupts.tools`. That component appears in the interrupt list. Do not also render `interrupt` on the tool unless you want it in both places. - -```tsx -import { fetchServerSentEvents, useChat } from '@tanstack/ai-react' -import { createChatUI } from '@tanstack/ai-react-ui' -import { toolDefinition } from '@tanstack/ai' -import { z } from 'zod' - -const purchaseItem = toolDefinition({ - name: 'purchaseItem', - description: 'Buy an item', - needsApproval: true, - inputSchema: z.object({ item: z.string() }), - outputSchema: z.object({ ok: z.boolean() }), -}).client() - -const chatOptions = { - connection: fetchServerSentEvents('/api/chat'), - tools: [purchaseItem], -} - -const UI = createChatUI(chatOptions) - -const components = UI.defineComponents({ - layout: ({ renderMessages, renderInterrupts }) => ( -
- {renderMessages()} - {renderInterrupts()} -
- ), - message: ({ renderParts }) =>
{renderParts()}
, - parts: { fallback: () => null }, - tools: { - purchaseItem: ({ part }) =>
{part.input?.item}
, - }, - interrupts: { - tools: { - purchaseItem: ({ interrupt }) => ( - - ), - }, - }, -}) - -export function ListApprovalChat() { - const chat = useChat(chatOptions) - return -} -``` +`layout` and `input` receive `chat` as a prop. Nested children can call `useChatContext()` inside `Chat`. -## Generic interrupts +## Tool approvals -Generic interrupts always render in the list (`renderInterrupts()` / ``). They never render inside a tool. +A mapped tool that is not listed under `interrupts.tools` keeps its approval off the list. Read `interrupt` on the tool. -Map them under `interrupts.generic`: - -- A registered id such as `choosePlan`: the component for that definition -- `fallback`: every other list interrupt, including an unknown generic id and an unbound interrupt this chat does not own - -```tsx -import { fetchServerSentEvents, useChat } from '@tanstack/ai-react' -import { createChatUI } from '@tanstack/ai-react-ui' -import { defineInterrupt } from '@tanstack/ai' -import { z } from 'zod' - -const choosePlan = defineInterrupt({ - id: 'choosePlan', - payloadSchema: z.object({ title: z.string() }), - responseSchema: z.string(), -}) - -const chatOptions = { - connection: fetchServerSentEvents('/api/chat'), - interrupts: [choosePlan], -} - -const UI = createChatUI(chatOptions) - -const components = UI.defineComponents({ - layout: ({ renderInterrupts }) => renderInterrupts(), - message: ({ renderParts }) =>
{renderParts()}
, - parts: { fallback: () => null }, - interrupts: { - generic: { - choosePlan: ({ interrupt }) => ( - - ), - fallback: ({ interrupt }) => - interrupt.kind === 'unbound' ? ( -

Paused elsewhere: {interrupt.reason}

- ) : ( -

{interrupt.reason}

- ), - }, - }, -}) - -export function GenericInterruptChat() { - const chat = useChat(chatOptions) - return -} -``` - -You can mix this map with `interrupts.tools` in the same `defineComponents` call. - -## Manual traversal - -```tsx -import { fetchServerSentEvents, useChat } from '@tanstack/ai-react' -import { createChatUI } from '@tanstack/ai-react-ui' - -const chatOptions = { - connection: fetchServerSentEvents('/api/chat'), -} - -const UI = createChatUI(chatOptions) - -const components = UI.defineComponents({ - layout: ({ renderMessages }) => renderMessages(), - message: ({ renderParts }) =>
{renderParts()}
, - parts: { fallback: () => null }, -}) - -export function ManualChat() { - const chat = useChat(chatOptions) - return ( - - - {(messages) => - messages.map((message) => ( - - {(parts) => - parts.map((part, index) => ( - {part.key} - )) - } - - )) - } - - - ) -} -``` +If you register `interrupts.tools.purchaseItem`, that component renders in `renderInterrupts()` instead. -Unknown runtime tool names warn once in development and render nothing. Add a `parts.fallback` for unknown part types. +Generic interrupts always render in the list. -See also [Solid](./solid), [Vue](./vue), [Svelte](./svelte), and [custom adapters](./custom-adapters). +Unknown runtime tool names warn once in development and render nothing. Add `parts.fallback` for unknown part types. diff --git a/docs/ui/solid.md b/docs/ui/solid.md index b5fcaeb36e..1c52e96a92 100644 --- a/docs/ui/solid.md +++ b/docs/ui/solid.md @@ -2,18 +2,18 @@ title: Solid Chat UI id: typed-headless-ui-solid order: 2 -description: "Build a typed, headless Solid chat UI with createChatUI. Accessors stay tracked. Your app owns useChat." +description: "Render a typed chat tree with Chat and a component map. Keep accessors tracked." keywords: - tanstack ai - - createChatUI + - Chat - solid - headless ui - ToolProps --- -Install `@tanstack/ai-solid-ui`, then call `createChatUI(chatOptions)` once at module scope. Do not destructure reactive props. +Install `@tanstack/ai-solid`. Import `Chat` from `@tanstack/ai-solid/ui`. Call `useChat(chatOptions)` in the screen. Pass a module-level `components` object into `Chat`. Do not destructure reactive props. -`defineComponents` needs a `tools` entry for every tool name in `chatOptions`. It also needs an `interrupts.generic` entry for every interrupt id. `generic.fallback` is optional. +> **Deprecated.** Do not install `@tanstack/ai-solid-ui`. That package re-exports this subpath until 1.0.0. See [Chat UI packages](../migration/create-ui). The server route matches the [React page](./react). Use `gpt-5.6` on the OpenAI text adapter. @@ -21,7 +21,7 @@ The server route matches the [React page](./react). Use `gpt-5.6` on the OpenAI ```tsx import { fetchServerSentEvents, useChat } from '@tanstack/ai-solid' -import { createChatUI } from '@tanstack/ai-solid-ui' +import { Chat, type ChatUIComponents } from '@tanstack/ai-solid/ui' import { toolDefinition } from '@tanstack/ai' import { z } from 'zod' @@ -37,9 +37,7 @@ const chatOptions = { tools: [getWeather], } -const UI = createChatUI(chatOptions) - -const components = UI.defineComponents({ +const components = { layout: (props) => ( <> {props.renderMessages()} @@ -54,100 +52,12 @@ const components = UI.defineComponents({ tools: { getWeather: (props) => {props.part.input?.city}, }, -}) +} satisfies ChatUIComponents -export function ChatScreen() { +export function Support() { const chat = useChat(chatOptions) - return -} -``` - -## Type a component in its own file - -Use `ToolProps` the same way as React. Keep the `props` object so Solid can track it. - -```tsx -import { fetchServerSentEvents } from '@tanstack/ai-solid' -import { createChatUI, type ToolProps } from '@tanstack/ai-solid-ui' -import { toolDefinition } from '@tanstack/ai' -import { z } from 'zod' - -const getWeather = toolDefinition({ - name: 'getWeather', - description: 'Look up weather', - inputSchema: z.object({ city: z.string() }), - outputSchema: z.object({ temperature: z.number() }), -}).client() - -const chatOptions = { - connection: fetchServerSentEvents('/api/chat'), - tools: [getWeather], -} - -export function WeatherTool( - props: ToolProps, -) { - return {props.part.input?.city} + return } - -const UI = createChatUI(chatOptions) - -export const components = UI.defineComponents({ - layout: (props) => props.renderMessages(), - message: (props) =>
{props.renderParts()}
, - parts: { fallback: () => null }, - tools: { getWeather: WeatherTool }, -}) ``` -Part components use `PartProps`. Then `part` is already a text part. - -Interrupt components use `InterruptProps`. Then `interrupt.payload` matches the definition. - -Mapped components do not receive `chat` as a prop. Call `UI.useChat()` when a component needs live chat. That call opts the component into chat updates. Nested children can call it too. - -## Read chat from `UI.useChat()` - -```tsx -import { fetchServerSentEvents, useChat } from '@tanstack/ai-solid' -import { createChatUI } from '@tanstack/ai-solid-ui' - -const chatOptions = { - connection: fetchServerSentEvents('/api/chat'), -} - -const UI = createChatUI(chatOptions) - -function StatusLine() { - const chat = UI.useChat() - return

{chat.messages.length} messages

-} - -const components = UI.defineComponents({ - layout: (props) => ( - <> - - {props.renderMessages()} - - ), - message: (props) =>
{props.renderParts()}
, - parts: { fallback: () => null }, -}) - -export function ChatScreen() { - const chat = useChat(chatOptions) - return -} -``` - -Call `UI.useChat()` only inside `UI.Chat` or `UI.Provider`. - -## Interrupts - -Tool approvals sit in the tool when you read `props.interrupt`. Put a component on `interrupts.tools` to send that approval to the list instead. Generic interrupts always sit in the list under `interrupts.generic`: `{ choosePlan, fallback }`. An unbound interrupt uses `fallback`. Branch on `interrupt.kind === 'unbound'` if the copy must differ. - -The full map is on the [React page](./react). - -Manual list: `{(messages) => {messages().length}}`. - -Pass `props.chat`, `props.part`, and `props.renderParts()` without destructure. +Use `ToolProps` the same way as React. Keep the `props` object so Solid can track it. `layout` and `input` receive `chat` as a prop. Nested children can call `useChatContext()` inside `Chat`. diff --git a/docs/ui/svelte.md b/docs/ui/svelte.md index 1fea54dca2..c1f13a4c9d 100644 --- a/docs/ui/svelte.md +++ b/docs/ui/svelte.md @@ -2,18 +2,16 @@ title: Svelte Chat UI id: typed-headless-ui-svelte order: 4 -description: "Build a typed, headless Svelte 5 chat UI with createChatUI, snippets, and static components." +description: "Render a typed chat tree with Chat, snippets, and a component map." keywords: - tanstack ai - - createChatUI + - Chat - svelte - headless ui - ToolProps --- -Install `@tanstack/ai-svelte-ui`. Call `createChatUI(chatOptions)` once. Pass `{ui}`, `{chat}`, and `{components}` into `UIChat`. - -`defineComponents` needs a `tools` entry for every tool name in `chatOptions`. It also needs an `interrupts.generic` entry for every interrupt id. `generic.fallback` is optional. +Install `@tanstack/ai-svelte`. Import `Chat` from `@tanstack/ai-svelte/ui`. Call `createChat(chatOptions)` in the page. Pass `{chat}` and `components` into `Chat`. The server route matches the [React page](./react). Use `gpt-5.6` on the OpenAI text adapter. @@ -21,81 +19,31 @@ The server route matches the [React page](./react). Use `gpt-5.6` on the OpenAI ```svelte - - -``` - -`Layout.svelte` receives snippets `messages`, `interrupts`, and `input`. `Message.svelte` receives snippet `parts`. A tool with an approval receives prop `interrupt`. - -## Type a component in its own file - -Type the `$props()` of a tool file with `ToolProps`. Share the same `chatOptions` module that you pass to `createChatUI`. - -```svelte - - -{part.input?.city} -``` - -Part components use `PartProps`. Then `part` is already a text part. - -Interrupt components use `InterruptProps`. Then `interrupt.payload` matches the definition. - -Mapped components do not receive `chat` as a prop. Call `ui.useChat()` when a component needs live chat. That call opts the component into chat updates. Nested children can call it too. - -## Read chat from `ui.useChat()` - -Import the same `ui` descriptor in a child file. Call `ui.useChat()` only under `UIChat` or `UIProvider`. - -```svelte - -

{chat.messages.length} messages

+ ``` -`createChat(chatOptions)` owns the state. `ui.useChat()` reads the instance you passed into `UIChat`. A call outside that tree throws. - -## Interrupts - -Tool approvals sit in the tool when you read the `interrupt` prop. Put a component on `interrupts.tools` to send that approval to the list instead. Generic interrupts always sit in the list under `interrupts.generic`: `{ choosePlan, fallback }`. An unbound interrupt uses `fallback`. Branch on `interrupt.kind === 'unbound'` if the copy must differ. +Layout receives a `chat` prop and `messages`, `interrupts`, and `input` snippets. Message receives a `parts` snippet. -The full map is on the [React page](./react). +Type a tool file with `ToolProps`. Share the same `chatOptions` module that you pass to `createChat`. diff --git a/docs/ui/vue.md b/docs/ui/vue.md index 0624e31bb6..5e80a2c8e1 100644 --- a/docs/ui/vue.md +++ b/docs/ui/vue.md @@ -2,18 +2,18 @@ title: Vue Chat UI id: typed-headless-ui-vue order: 3 -description: "Build a typed, headless Vue chat UI with createChatUI and static primitives. Slots replace render callbacks." +description: "Render a typed chat tree with Chat and a component map. Slots replace render callbacks." keywords: - tanstack ai - - createChatUI + - Chat - vue - headless ui - ToolProps --- -Install `@tanstack/ai-vue-ui`. Call `createChatUI(chatOptions)` once. Pass the descriptor as `ui` into `UIChat`, `UIProvider`, and the other static primitives. +Install `@tanstack/ai-vue`. Import `Chat` from `@tanstack/ai-vue/ui`. Call `useChat(chatOptions)` in the screen. Pass a `components` object into `Chat`. Layout uses `messages`, `interrupts`, and `input` slots. Message uses a `parts` slot. -`defineComponents` needs a `tools` entry for every tool name in `chatOptions`. It also needs an `interrupts.generic` entry for every interrupt id. `generic.fallback` is optional. +> **Deprecated.** Do not install `@tanstack/ai-vue-ui`. That package re-exports this subpath until 1.0.0. See [Chat UI packages](../migration/create-ui). The server route matches the [React page](./react). Use `gpt-5.6` on the OpenAI text adapter. @@ -22,7 +22,7 @@ The server route matches the [React page](./react). Use `gpt-5.6` on the OpenAI ```ts import { defineComponent, h } from 'vue' import { fetchServerSentEvents, useChat } from '@tanstack/ai-vue' -import { createChatUI, UIChat } from '@tanstack/ai-vue-ui' +import { Chat } from '@tanstack/ai-vue/ui' import { toolDefinition } from '@tanstack/ai' import { z } from 'zod' @@ -38,13 +38,13 @@ const chatOptions = { tools: [getWeather], } -const ui = createChatUI(chatOptions) - -const components = ui.defineComponents({ +const components = { layout: defineComponent((_, { slots }) => () => - h('div', [slots.messages?.(), slots.interrupts?.(), slots.input?.()]), + h('main', [slots.messages?.(), slots.input?.()]), + ), + message: defineComponent((_, { slots }) => () => + h('article', slots.parts?.()), ), - message: defineComponent((_, { slots }) => () => h('article', slots.parts?.())), parts: { fallback: defineComponent({ props: ['part'], @@ -61,110 +61,14 @@ const components = ui.defineComponents({ }, }), }, -}) - -export default defineComponent({ - setup() { - const chat = useChat(chatOptions) - return () => h(UIChat, { ui, chat, components }) - }, -}) -``` - -Layout uses slots `messages`, `interrupts`, and `input`. Message uses slot `parts`. Manual lists use the default slot on `UIMessages` with `{ messages }`. - -## Type a component in its own file - -Use `ToolProps` on the component props. Share the same `chatOptions` object that you pass to `createChatUI`. - -```ts -import { defineComponent, h } from 'vue' -import { fetchServerSentEvents } from '@tanstack/ai-vue' -import { createChatUI, type ToolProps } from '@tanstack/ai-vue-ui' -import { toolDefinition } from '@tanstack/ai' -import { z } from 'zod' - -const getWeather = toolDefinition({ - name: 'getWeather', - description: 'Look up weather', - inputSchema: z.object({ city: z.string() }), - outputSchema: z.object({ temperature: z.number() }), -}).client() - -const chatOptions = { - connection: fetchServerSentEvents('/api/chat'), - tools: [getWeather], -} - -export const WeatherTool = defineComponent( - (props: ToolProps) => { - return () => h('strong', props.part.input?.city) - }, -) - -const ui = createChatUI(chatOptions) - -export const components = ui.defineComponents({ - layout: defineComponent((_, { slots }) => () => - h('div', [slots.messages?.(), slots.interrupts?.(), slots.input?.()]), - ), - message: defineComponent((_, { slots }) => () => h('article', slots.parts?.())), - parts: { fallback: defineComponent(() => () => null) }, - tools: { getWeather: WeatherTool }, -}) -``` - -Part components use `PartProps`. Then `part` is already a text part. - -Interrupt components use `InterruptProps`. Then `interrupt.payload` matches the definition. - -Mapped components do not receive `chat` as a prop. Call `ui.useChat()` when a component needs live chat. That call opts the component into chat updates. Nested children can call it too. - -## Read chat from `ui.useChat()` - -Call `ui.useChat()` inside a child of `UIChat` or `UIProvider`. - -```ts -import { defineComponent, h } from 'vue' -import { fetchServerSentEvents, useChat } from '@tanstack/ai-vue' -import { createChatUI, UIChat } from '@tanstack/ai-vue-ui' - -const chatOptions = { - connection: fetchServerSentEvents('/api/chat'), } -const ui = createChatUI(chatOptions) - -const StatusLine = defineComponent({ - setup() { - const chat = ui.useChat() - return () => { - const messages = Array.isArray(chat.messages) ? chat.messages : [] - return h('p', String(messages.length) + ' messages') - } - }, -}) - -const components = ui.defineComponents({ - layout: defineComponent((_, { slots }) => () => - h('main', [h(StatusLine), slots.messages?.()]), - ), - message: defineComponent((_, { slots }) => () => h('article', slots.parts?.())), - parts: { fallback: defineComponent(() => () => null) }, -}) - export default defineComponent({ setup() { const chat = useChat(chatOptions) - return () => h(UIChat, { ui, chat, components }) + return () => h(Chat, { chat, components }) }, }) ``` -`useChat(chatOptions)` from `@tanstack/ai-vue` owns the state. `ui.useChat()` reads the instance you passed into `UIChat`. - -## Interrupts - -Tool approvals sit in the tool when you read the `interrupt` prop. Put a component on `interrupts.tools` to send that approval to the list instead. Generic interrupts always sit in the list under `interrupts.generic`: `{ choosePlan, fallback }`. An unbound interrupt uses `fallback`. Branch on `interrupt.kind === 'unbound'` if the copy must differ. - -The full map is on the [React page](./react). +Use `ToolProps` on a tool component's props. `input` receives `chat` as a prop. diff --git a/examples/README.md b/examples/README.md index d23370be39..2e03103fdd 100644 --- a/examples/README.md +++ b/examples/README.md @@ -164,7 +164,7 @@ A React SPA that connects to **Go, Rust, PHP, Zig, Bash, and Python chat servers **Tech Stack:** - React + Vite (SPA) -- `@tanstack/ai-react` + `@tanstack/ai-react-ui` +- `@tanstack/ai-react` + `@tanstack/ai-react/ui` - Go chat server (`net/http`, `:8001`) - Rust chat server (Axum, `:8002`) - PHP chat server (built-in server + curl, `:8003`) diff --git a/examples/ag-ui/README.md b/examples/ag-ui/README.md index 770997aaf1..38f4bd949e 100644 --- a/examples/ag-ui/README.md +++ b/examples/ag-ui/README.md @@ -8,7 +8,7 @@ This example shows that any backend can serve `@tanstack/ai-react` clients as lo | Layer | Stack | | ------------- | ------------------------------------------------------------------------ | -| Client | React, Vite, `@tanstack/ai-react`, `@tanstack/ai-react-ui` | +| Client | React, Vite, `@tanstack/ai-react`, `@tanstack/ai-react/ui` | | Go server | `net/http`, hand-rolled AG-UI SSE, OpenAI/Anthropic streaming on `:8001` | | Rust server | Axum, hand-rolled AG-UI SSE, OpenAI/Anthropic streaming on `:8002` | | PHP server | Built-in PHP server + curl, hand-rolled AG-UI SSE on `:8003` | diff --git a/examples/ag-ui/package.json b/examples/ag-ui/package.json index 5be67b3556..3e8c09fcd0 100644 --- a/examples/ag-ui/package.json +++ b/examples/ag-ui/package.json @@ -21,7 +21,6 @@ "dependencies": { "@tanstack/ai-client": "workspace:*", "@tanstack/ai-react": "workspace:*", - "@tanstack/ai-react-ui": "workspace:*", "react": "^19.2.3", "react-dom": "^19.2.3" }, diff --git a/examples/ag-ui/src/App.tsx b/examples/ag-ui/src/App.tsx index 84b0f5180e..95bfee6534 100644 --- a/examples/ag-ui/src/App.tsx +++ b/examples/ag-ui/src/App.tsx @@ -1,43 +1,38 @@ -import { useEffect, useMemo, useState } from 'react' +import { createContext, useContext, useEffect, useMemo, useState } from 'react' import { fetchServerSentEvents, useChat } from '@tanstack/ai-react' import type { ConnectionAdapter } from '@tanstack/ai-react' -import { ChatMessage, createChatUI } from '@tanstack/ai-react-ui' +import { Chat } from '@tanstack/ai-react/ui' +import type { ChatUIComponents } from '@tanstack/ai-react/ui' -const UI = createChatUI({}) +const LabelsContext = createContext({ placeholder: '', emptyLabel: '' }) -function AgUiChat({ - connection, - placeholder, - emptyLabel, -}: { - connection: ConnectionAdapter - placeholder: string - emptyLabel: string -}) { - const chat = useChat({ connection }) - const [draft, setDraft] = useState('') - const components = UI.defineComponents({ - layout: ({ renderMessages, renderInput }) => { - const current = UI.useChat() - return ( -
- {current.error ? ( -

{current.error.message}

- ) : current.messages.length === 0 ? ( -
- {emptyLabel} -
- ) : ( -
- {renderMessages()} -
- )} -
{renderInput()}
-
- ) - }, - message: ({ message }) => , - input: () => ( +const chatComponents = { + layout: ({ chat, renderMessages, renderInput }) => { + const { emptyLabel } = useContext(LabelsContext) + return ( +
+ {chat.error ? ( +

{chat.error.message}

+ ) : chat.messages.length === 0 ? ( +
+ {emptyLabel} +
+ ) : ( +
+ {renderMessages()} +
+ )} +
{renderInput()}
+
+ ) + }, + message: ({ message, renderParts }) => ( +
{renderParts()}
+ ), + input: function Input({ chat }) { + const { placeholder } = useContext(LabelsContext) + const [draft, setDraft] = useState('') + return (
{ @@ -61,10 +56,29 @@ function AgUiChat({ Send
- ), - parts: { fallback: () => null }, - }) - return + ) + }, + parts: { + text: ({ part }) => (part.type === 'text' ?

{part.content}

: null), + fallback: () => null, + }, +} satisfies ChatUIComponents<{}> + +function AgUiChat({ + connection, + placeholder, + emptyLabel, +}: { + connection: ConnectionAdapter + placeholder: string + emptyLabel: string +}) { + const chat = useChat({ connection }) + return ( + + + + ) } type Backend = 'go' | 'rust' | 'php' | 'zig' | 'bash' | 'python' diff --git a/examples/ts-react-chat/package.json b/examples/ts-react-chat/package.json index f685519340..18faaadfcf 100644 --- a/examples/ts-react-chat/package.json +++ b/examples/ts-react-chat/package.json @@ -42,7 +42,6 @@ "@tanstack/ai-openrouter": "workspace:*", "@tanstack/ai-persistence": "workspace:*", "@tanstack/ai-react": "workspace:*", - "@tanstack/ai-react-ui": "workspace:*", "@tanstack/ai-sandbox": "workspace:*", "@tanstack/ai-sandbox-daytona": "workspace:*", "@tanstack/ai-sandbox-docker": "workspace:*", diff --git a/examples/ts-react-chat/src/routes/index.tsx b/examples/ts-react-chat/src/routes/index.tsx index c40d1a01f9..9fdca10439 100644 --- a/examples/ts-react-chat/src/routes/index.tsx +++ b/examples/ts-react-chat/src/routes/index.tsx @@ -36,7 +36,7 @@ import { startOpenRouterPkceLogin, } from '@tanstack/ai-openrouter/pkce' import { clientTools } from '@tanstack/ai-client' -import { ThinkingPart } from '@tanstack/ai-react-ui' +import { ThinkingPart } from '@tanstack/ai-react/ui' import type { BoundInterrupts } from '@tanstack/ai-client' import type { UIMessage } from '@tanstack/ai-react' import type { ContentPart } from '@tanstack/ai' diff --git a/examples/ts-react-chat/src/routes/mcp-demo.tsx b/examples/ts-react-chat/src/routes/mcp-demo.tsx index 2bc99b5c6e..d3d196b558 100644 --- a/examples/ts-react-chat/src/routes/mcp-demo.tsx +++ b/examples/ts-react-chat/src/routes/mcp-demo.tsx @@ -7,7 +7,7 @@ import rehypeSanitize from 'rehype-sanitize' import rehypeHighlight from 'rehype-highlight' import remarkGfm from 'remark-gfm' import { fetchServerSentEvents, useChat } from '@tanstack/ai-react' -import { ThinkingPart } from '@tanstack/ai-react-ui' +import { ThinkingPart } from '@tanstack/ai-react/ui' import type { UIMessage } from '@tanstack/ai-react' import { MCP_PROVIDERS, type McpProvider } from '@/lib/mcp-providers' diff --git a/examples/ts-react-chat/src/routes/threads.tsx b/examples/ts-react-chat/src/routes/threads.tsx index 8889cd67fe..766d83b71c 100644 --- a/examples/ts-react-chat/src/routes/threads.tsx +++ b/examples/ts-react-chat/src/routes/threads.tsx @@ -2,7 +2,7 @@ import { useEffect, useRef, useState } from 'react' import { createFileRoute } from '@tanstack/react-router' import { fetchServerSentEvents, useChat } from '@tanstack/ai-react' import { clientTools } from '@tanstack/ai-client' -import { ThinkingPart } from '@tanstack/ai-react-ui' +import { ThinkingPart } from '@tanstack/ai-react/ui' import ReactMarkdown from 'react-markdown' import rehypeRaw from 'rehype-raw' import rehypeSanitize from 'rehype-sanitize' diff --git a/examples/ts-react-ui-chatbot/README.md b/examples/ts-react-ui-chatbot/README.md index b7d7e829c7..da7794b899 100644 --- a/examples/ts-react-ui-chatbot/README.md +++ b/examples/ts-react-ui-chatbot/README.md @@ -1,6 +1,6 @@ # Typed headless chat + shadcn AI components -One-route TanStack Start app. It uses `createChatUI()` from `@tanstack/ai-react-ui` and the chatbot pieces from [shadcn.io/ai/chatbot](https://www.shadcn.io/ai/chatbot): Conversation, Message, Prompt Input, Model Selector, Reasoning, Sources, plus Tool for approvals. +One-route TanStack Start app. It uses `useChat` from `@tanstack/ai-react` and `Chat` from `@tanstack/ai-react/ui` with the chatbot pieces from [shadcn.io/ai/chatbot](https://www.shadcn.io/ai/chatbot): Conversation, Message, Prompt Input, Model Selector, Reasoning, Sources, plus Tool for approvals. ```bash pnpm install diff --git a/examples/ts-react-ui-chatbot/package.json b/examples/ts-react-ui-chatbot/package.json index 9508019edc..a33bc233fd 100644 --- a/examples/ts-react-ui-chatbot/package.json +++ b/examples/ts-react-ui-chatbot/package.json @@ -15,7 +15,6 @@ "@tanstack/ai-client": "workspace:*", "@tanstack/ai-openai": "workspace:*", "@tanstack/ai-react": "workspace:*", - "@tanstack/ai-react-ui": "workspace:*", "@tanstack/react-devtools": "^0.9.10", "@tanstack/react-router": "^1.158.4", "@tanstack/react-router-devtools": "^1.158.4", diff --git a/examples/ts-react-ui-chatbot/src/chat/options.ts b/examples/ts-react-ui-chatbot/src/chat/options.ts index 73870c0f50..7a5f8ffcde 100644 --- a/examples/ts-react-ui-chatbot/src/chat/options.ts +++ b/examples/ts-react-ui-chatbot/src/chat/options.ts @@ -1,5 +1,4 @@ import { fetchServerSentEvents } from '@tanstack/ai-react' -import { createChatUI } from '@tanstack/ai-react-ui' import { byok } from './byok' import { chatInterrupts } from './interrupts' import { itinerarySchema } from './schema' @@ -13,5 +12,3 @@ export const chatOptions = { byok, byokProvider: () => 'openai' as const, } - -export const UI = createChatUI(chatOptions) diff --git a/examples/ts-react-ui-chatbot/src/components/chat/book-stay-tool.tsx b/examples/ts-react-ui-chatbot/src/components/chat/book-stay-tool.tsx index ace01c9535..096e413244 100644 --- a/examples/ts-react-ui-chatbot/src/components/chat/book-stay-tool.tsx +++ b/examples/ts-react-ui-chatbot/src/components/chat/book-stay-tool.tsx @@ -1,4 +1,4 @@ -import type { InterruptProps, ToolProps } from '@tanstack/ai-react-ui' +import type { InterruptProps, ToolProps } from '@tanstack/ai-react/ui' import { Button } from '@/components/ui/button' import { Tool, diff --git a/examples/ts-react-ui-chatbot/src/components/chat/choose-budget.tsx b/examples/ts-react-ui-chatbot/src/components/chat/choose-budget.tsx index 4cdeabbff9..0cc24b8cd2 100644 --- a/examples/ts-react-ui-chatbot/src/components/chat/choose-budget.tsx +++ b/examples/ts-react-ui-chatbot/src/components/chat/choose-budget.tsx @@ -1,4 +1,4 @@ -import type { InterruptProps } from '@tanstack/ai-react-ui' +import type { InterruptProps } from '@tanstack/ai-react/ui' import { Button } from '@/components/ui/button' import type { chatOptions } from '@/chat/options' diff --git a/examples/ts-react-ui-chatbot/src/components/chat/confirm-payment-tool.tsx b/examples/ts-react-ui-chatbot/src/components/chat/confirm-payment-tool.tsx index 240c035860..4d1a6176de 100644 --- a/examples/ts-react-ui-chatbot/src/components/chat/confirm-payment-tool.tsx +++ b/examples/ts-react-ui-chatbot/src/components/chat/confirm-payment-tool.tsx @@ -1,4 +1,4 @@ -import type { InterruptProps, ToolProps } from '@tanstack/ai-react-ui' +import type { InterruptProps, ToolProps } from '@tanstack/ai-react/ui' import { Button } from '@/components/ui/button' import { Tool, diff --git a/examples/ts-react-ui-chatbot/src/components/chat/fallback-interrupt.tsx b/examples/ts-react-ui-chatbot/src/components/chat/fallback-interrupt.tsx index 7548b2f81a..ce8b986256 100644 --- a/examples/ts-react-ui-chatbot/src/components/chat/fallback-interrupt.tsx +++ b/examples/ts-react-ui-chatbot/src/components/chat/fallback-interrupt.tsx @@ -1,4 +1,4 @@ -import type { InterruptProps } from '@tanstack/ai-react-ui' +import type { InterruptProps } from '@tanstack/ai-react/ui' import type { chatOptions } from '@/chat/options' export function FallbackInterrupt({ diff --git a/examples/ts-react-ui-chatbot/src/components/chat/fallback-part.tsx b/examples/ts-react-ui-chatbot/src/components/chat/fallback-part.tsx index 1932e75ef9..2f46e96062 100644 --- a/examples/ts-react-ui-chatbot/src/components/chat/fallback-part.tsx +++ b/examples/ts-react-ui-chatbot/src/components/chat/fallback-part.tsx @@ -1,4 +1,4 @@ -import type { PartProps } from '@tanstack/ai-react-ui' +import type { PartProps } from '@tanstack/ai-react/ui' import type { chatOptions } from '@/chat/options' export function FallbackPart({ part }: PartProps) { diff --git a/examples/ts-react-ui-chatbot/src/components/chat/input.tsx b/examples/ts-react-ui-chatbot/src/components/chat/input.tsx index 19201c9b61..e8745fb0b5 100644 --- a/examples/ts-react-ui-chatbot/src/components/chat/input.tsx +++ b/examples/ts-react-ui-chatbot/src/components/chat/input.tsx @@ -13,7 +13,8 @@ import { XIcon, } from 'lucide-react' import { byok } from '@/chat/byok' -import { UI } from '@/chat/options' +import type { InputProps } from '@tanstack/ai-react/ui' +import type { chatOptions } from '@/chat/options' import { PromptInput, PromptInputButton, @@ -68,8 +69,7 @@ async function fileToDraft(file: File): Promise { } } -export function ChatPromptInput() { - const chat = UI.useChat() +export function ChatPromptInput({ chat }: InputProps) { const [model, setModel] = useState(selectedModel) const [text, setText] = useState('') const [drafts, setDrafts] = useState>([]) diff --git a/examples/ts-react-ui-chatbot/src/components/chat/layout.tsx b/examples/ts-react-ui-chatbot/src/components/chat/layout.tsx index 1857bd5bc0..fd15f798b8 100644 --- a/examples/ts-react-ui-chatbot/src/components/chat/layout.tsx +++ b/examples/ts-react-ui-chatbot/src/components/chat/layout.tsx @@ -1,5 +1,5 @@ import { CompassIcon } from 'lucide-react' -import type { LayoutProps } from '@tanstack/ai-react-ui' +import type { LayoutProps } from '@tanstack/ai-react/ui' import type { ContentPart } from '@tanstack/ai/client' import { Conversation, @@ -8,7 +8,6 @@ import { ConversationScrollButton, } from '@/components/ai/conversation' import { Suggestion, Suggestions } from '@/components/ai/suggestions' -import { UI } from '@/chat/options' import type { chatOptions } from '@/chat/options' const LISBON_PHOTO = @@ -35,11 +34,11 @@ const PROMPTS: Array<{ label: string; content?: Array }> = [ ] export function ChatLayout({ + chat, renderMessages, renderInterrupts, renderInput, }: LayoutProps) { - const chat = UI.useChat() if (chat.error) { return

{chat.error.message}

} diff --git a/examples/ts-react-ui-chatbot/src/components/chat/lookup-place-tool.tsx b/examples/ts-react-ui-chatbot/src/components/chat/lookup-place-tool.tsx index f5c18a0412..1c2ef8d4ad 100644 --- a/examples/ts-react-ui-chatbot/src/components/chat/lookup-place-tool.tsx +++ b/examples/ts-react-ui-chatbot/src/components/chat/lookup-place-tool.tsx @@ -1,4 +1,4 @@ -import type { ToolProps } from '@tanstack/ai-react-ui' +import type { ToolProps } from '@tanstack/ai-react/ui' import { Source, Sources, diff --git a/examples/ts-react-ui-chatbot/src/components/chat/media-parts.tsx b/examples/ts-react-ui-chatbot/src/components/chat/media-parts.tsx index 98f433dcd5..d10a57f218 100644 --- a/examples/ts-react-ui-chatbot/src/components/chat/media-parts.tsx +++ b/examples/ts-react-ui-chatbot/src/components/chat/media-parts.tsx @@ -1,4 +1,4 @@ -import type { PartProps } from '@tanstack/ai-react-ui' +import type { PartProps } from '@tanstack/ai-react/ui' import type { chatOptions } from '@/chat/options' function sourceHref(source: { diff --git a/examples/ts-react-ui-chatbot/src/components/chat/message.tsx b/examples/ts-react-ui-chatbot/src/components/chat/message.tsx index c65bf3c3c1..d32ed35068 100644 --- a/examples/ts-react-ui-chatbot/src/components/chat/message.tsx +++ b/examples/ts-react-ui-chatbot/src/components/chat/message.tsx @@ -1,4 +1,4 @@ -import type { MessageProps } from '@tanstack/ai-react-ui' +import type { MessageProps } from '@tanstack/ai-react/ui' import { Message, MessageAvatar, MessageContent } from '@/components/ai/message' import type { chatOptions } from '@/chat/options' diff --git a/examples/ts-react-ui-chatbot/src/components/chat/structured-output.tsx b/examples/ts-react-ui-chatbot/src/components/chat/structured-output.tsx index 4962289cce..fff90c4e68 100644 --- a/examples/ts-react-ui-chatbot/src/components/chat/structured-output.tsx +++ b/examples/ts-react-ui-chatbot/src/components/chat/structured-output.tsx @@ -1,4 +1,4 @@ -import type { PartProps } from '@tanstack/ai-react-ui' +import type { PartProps } from '@tanstack/ai-react/ui' import type { chatOptions } from '@/chat/options' export function StructuredOutputPart({ diff --git a/examples/ts-react-ui-chatbot/src/components/chat/text-part.tsx b/examples/ts-react-ui-chatbot/src/components/chat/text-part.tsx index ed8b1c5f1c..277c212168 100644 --- a/examples/ts-react-ui-chatbot/src/components/chat/text-part.tsx +++ b/examples/ts-react-ui-chatbot/src/components/chat/text-part.tsx @@ -1,4 +1,4 @@ -import type { PartProps } from '@tanstack/ai-react-ui' +import type { PartProps } from '@tanstack/ai-react/ui' import { MessageResponse } from '@/components/ai/message' import type { chatOptions } from '@/chat/options' diff --git a/examples/ts-react-ui-chatbot/src/components/chat/thinking-part.tsx b/examples/ts-react-ui-chatbot/src/components/chat/thinking-part.tsx index 6fe0d4b86a..39ca921106 100644 --- a/examples/ts-react-ui-chatbot/src/components/chat/thinking-part.tsx +++ b/examples/ts-react-ui-chatbot/src/components/chat/thinking-part.tsx @@ -1,4 +1,4 @@ -import type { PartProps } from '@tanstack/ai-react-ui' +import type { PartProps } from '@tanstack/ai-react/ui' import { Reasoning, ReasoningContent, diff --git a/examples/ts-react-ui-chatbot/src/components/chat/ui-components.tsx b/examples/ts-react-ui-chatbot/src/components/chat/ui-components.tsx index f09b9e1fec..bf12ab861d 100644 --- a/examples/ts-react-ui-chatbot/src/components/chat/ui-components.tsx +++ b/examples/ts-react-ui-chatbot/src/components/chat/ui-components.tsx @@ -1,4 +1,5 @@ -import { UI } from '@/chat/options' +import type { ChatUIComponents } from '@tanstack/ai-react/ui' +import { chatOptions } from '@/chat/options' import { BookStayApproval, BookStayTool } from './book-stay-tool' import { ConfirmPaymentTool } from './confirm-payment-tool' import { ChatPromptInput } from './input' @@ -20,7 +21,7 @@ import { StructuredOutputPart } from './structured-output' import { TextPart } from './text-part' import { ThinkingPart } from './thinking-part' -export const components = UI.defineComponents({ +export const chatComponents = { layout: ChatLayout, message: ChatMessage, input: ChatPromptInput, @@ -50,4 +51,4 @@ export const components = UI.defineComponents({ fallback: FallbackInterrupt, }, }, -}) +} satisfies ChatUIComponents diff --git a/examples/ts-react-ui-chatbot/src/routes/index.tsx b/examples/ts-react-ui-chatbot/src/routes/index.tsx index 4006447d35..03457c1aa3 100644 --- a/examples/ts-react-ui-chatbot/src/routes/index.tsx +++ b/examples/ts-react-ui-chatbot/src/routes/index.tsx @@ -1,9 +1,9 @@ import { createFileRoute } from '@tanstack/react-router' import { useChat } from '@tanstack/ai-react' -import { UI } from '@/chat/options' +import { Chat } from '@tanstack/ai-react/ui' import { chatOptions } from '@/chat/options' import { selectedModel } from '@/chat/model' -import { components } from '@/components/chat/ui-components' +import { chatComponents } from '@/components/chat/ui-components' export const Route = createFileRoute('/')({ component: TripDesk, @@ -12,8 +12,8 @@ export const Route = createFileRoute('/')({ function TripDesk() { const chat = useChat({ ...chatOptions, - get forwardedProps() { - return { model: selectedModel } + forwardedProps: { + model: selectedModel, }, }) @@ -32,7 +32,7 @@ function TripDesk() { src="/brand/logos/tanstack-landscape-white.svg" /> - +
) } diff --git a/examples/ts-solid-chat/package.json b/examples/ts-solid-chat/package.json index c00979bd29..bccf27dce9 100644 --- a/examples/ts-solid-chat/package.json +++ b/examples/ts-solid-chat/package.json @@ -19,7 +19,6 @@ "@tanstack/ai-ollama": "workspace:*", "@tanstack/ai-openai": "workspace:*", "@tanstack/ai-solid": "workspace:*", - "@tanstack/ai-solid-ui": "workspace:*", "@tanstack/nitro-v2-vite-plugin": "^1.155.0", "@tanstack/router-plugin": "^1.158.4", "@tanstack/solid-ai-devtools": "workspace:*", diff --git a/examples/ts-solid-chat/src/routes/index.tsx b/examples/ts-solid-chat/src/routes/index.tsx index fba21a969d..9023b4d27d 100644 --- a/examples/ts-solid-chat/src/routes/index.tsx +++ b/examples/ts-solid-chat/src/routes/index.tsx @@ -3,7 +3,7 @@ import Send from 'lucide-solid/icons/send' import Square from 'lucide-solid/icons/square' import { fetchServerSentEvents, useChat } from '@tanstack/ai-solid' import { createChatClientOptions } from '@tanstack/ai-client' -import { TextPart, ThinkingPart } from '@tanstack/ai-solid-ui' +import { TextPart, ThinkingPart } from '@tanstack/ai-solid/ui' import { For, createSignal } from 'solid-js' import type { InferChatMessages, UIMessage } from '@tanstack/ai-client' diff --git a/examples/ts-solid-chat/src/routes/solid-ui.tsx b/examples/ts-solid-chat/src/routes/solid-ui.tsx index 017a61e989..c95a0c50ed 100644 --- a/examples/ts-solid-chat/src/routes/solid-ui.tsx +++ b/examples/ts-solid-chat/src/routes/solid-ui.tsx @@ -1,6 +1,7 @@ import { createFileRoute } from '@tanstack/solid-router' import { fetchServerSentEvents, useChat } from '@tanstack/ai-solid' -import { createChatUI } from '@tanstack/ai-solid-ui' +import { Chat } from '@tanstack/ai-solid/ui' +import type { ChatUIComponents } from '@tanstack/ai-solid/ui' import { createSignal } from 'solid-js' import { clientTools } from '@/lib/guitar-tools' @@ -9,26 +10,23 @@ const chatOptions = { tools: clientTools, } -const UI = createChatUI(chatOptions) - -function SolidUIPage() { - const chat = useChat(chatOptions) - const [draft, setDraft] = createSignal('') - const components = UI.defineComponents({ - layout: (props) => ( -
-
- {props.renderMessages()} -
- {props.renderInput()} +const chatComponents = { + layout: (props) => ( +
+
+ {props.renderMessages()}
- ), - message: (props) => ( -
- {props.renderParts()} -
- ), - input: () => ( + {props.renderInput()} +
+ ), + message: (props) => ( +
+ {props.renderParts()} +
+ ), + input: function Input(props) { + const [draft, setDraft] = createSignal('') + return (
{ @@ -36,7 +34,7 @@ function SolidUIPage() { const text = draft().trim() if (!text) return setDraft('') - void chat.sendMessage(text) + void props.chat.sendMessage(text) }} > setDraft(event.currentTarget.value)} />
+ ) + }, + parts: { + fallback: (props) => + props.part.type === 'text' ?

{props.part.content}

: null, + }, + tools: { + recommendGuitar: (props) =>

{props.part.input?.id}

, + getPersonalGuitarPreference: (props) => ( +

{props.part.output?.preference}

), - parts: { - fallback: (props) => - props.part.type === 'text' ?

{props.part.content}

: null, - }, - tools: { - recommendGuitar: (props) =>

{props.part.input?.id}

, - getPersonalGuitarPreference: (props) => ( -

{props.part.output?.preference}

- ), - addToWishList: (props) => ( -

- {props.part.input?.guitarId} - {props.interrupt?.status === 'pending' ? ( - - ) : null} -

- ), - addToCart: (props) => ( -

- {props.part.input?.guitarId} - {props.interrupt?.status === 'pending' ? ( - - ) : null} -

- ), - }, - }) + addToWishList: (props) => ( +

+ {props.part.input?.guitarId} + {props.interrupt?.status === 'pending' ? ( + + ) : null} +

+ ), + addToCart: (props) => ( +

+ {props.part.input?.guitarId} + {props.interrupt?.status === 'pending' ? ( + + ) : null} +

+ ), + }, +} satisfies ChatUIComponents - return +function SolidUIPage() { + const chat = useChat(chatOptions) + return } export const Route = createFileRoute('/solid-ui')({ diff --git a/examples/ts-solid-chat/vite.config.ts b/examples/ts-solid-chat/vite.config.ts index b7c77c0506..c248359822 100644 --- a/examples/ts-solid-chat/vite.config.ts +++ b/examples/ts-solid-chat/vite.config.ts @@ -10,7 +10,7 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url)) const config = defineConfig({ ssr: { - noExternal: ['@tanstack/ai-solid-ui'], + noExternal: ['@tanstack/ai-solid/ui'], }, resolve: { tsconfigPaths: true, diff --git a/examples/ts-svelte-chat/package.json b/examples/ts-svelte-chat/package.json index 5ce76cb7c7..80a3e08237 100644 --- a/examples/ts-svelte-chat/package.json +++ b/examples/ts-svelte-chat/package.json @@ -20,7 +20,6 @@ "@tanstack/ai-ollama": "workspace:*", "@tanstack/ai-openai": "workspace:*", "@tanstack/ai-svelte": "workspace:*", - "@tanstack/ai-svelte-ui": "workspace:*", "highlight.js": "^11.11.1", "lucide-svelte": "^0.468.0", "marked": "^15.0.6", diff --git a/examples/ts-svelte-chat/src/routes/svelte-ui/+page.svelte b/examples/ts-svelte-chat/src/routes/svelte-ui/+page.svelte index 137b8d005c..bfdb10f3c5 100644 --- a/examples/ts-svelte-chat/src/routes/svelte-ui/+page.svelte +++ b/examples/ts-svelte-chat/src/routes/svelte-ui/+page.svelte @@ -1,10 +1,10 @@ - TanStack AI - Svelte createChatUI + TanStack AI - Svelte Chat UI - + diff --git a/examples/ts-vue-chat/package.json b/examples/ts-vue-chat/package.json index 26964075ea..fdc51a7b56 100644 --- a/examples/ts-vue-chat/package.json +++ b/examples/ts-vue-chat/package.json @@ -18,7 +18,6 @@ "@tanstack/ai-ollama": "workspace:*", "@tanstack/ai-openai": "workspace:*", "@tanstack/ai-vue": "workspace:*", - "@tanstack/ai-vue-ui": "workspace:*", "marked": "^15.0.6", "vue": "^3.5.25", "vue-router": "^4.5.0", diff --git a/examples/ts-vue-chat/src/views/VueUIView.vue b/examples/ts-vue-chat/src/views/VueUIView.vue index face539c0d..e09c06aaf2 100644 --- a/examples/ts-vue-chat/src/views/VueUIView.vue +++ b/examples/ts-vue-chat/src/views/VueUIView.vue @@ -2,7 +2,7 @@ import { computed, defineComponent, h, ref } from 'vue' import { fetchServerSentEvents, useChat } from '@tanstack/ai-vue' import { clientTools } from '@tanstack/ai-client' -import { createChatUI, UIChat } from '@tanstack/ai-vue-ui' +import { Chat } from '@tanstack/ai-vue/ui' import type { ModelOption } from '@/lib/model-selection' @@ -83,11 +83,9 @@ const chatOptions = { }, } -const ui = createChatUI(chatOptions) -const chat = useChat(chatOptions) const draft = ref('') -const components = ui.defineComponents({ +const chatComponents = { layout: defineComponent( (_, { slots }) => () => @@ -98,18 +96,18 @@ const components = ui.defineComponents({ ), message: defineComponent({ props: ['message'], - setup(props) { + setup(props, { slots }) { return () => - h('article', { 'data-role': props.message.role }, [ - ...(props.message.parts ?? []).map( - (part: { type: string; content?: string }) => - part.type === 'text' ? h('p', part.content) : null, - ), - ]) + h( + 'article', + { 'data-role': props.message.role }, + slots.parts?.() ?? slots.default?.(), + ) }, }), input: defineComponent({ - setup() { + props: ['chat'], + setup(props) { return () => h('div', { class: 'border-t border-orange-500/20 bg-gray-800 p-4' }, [ h( @@ -120,7 +118,7 @@ const components = ui.defineComponents({ const text = draft.value.trim() if (!text) return draft.value = '' - void chat.sendMessage(text) + void props.chat.sendMessage(text) }, }, [ @@ -193,7 +191,9 @@ const components = ui.defineComponents({ }, }), }, -}) +} + +const chat = useChat(chatOptions) diff --git a/knip.json b/knip.json index e6abef78b5..66b0d7c6a4 100644 --- a/knip.json +++ b/knip.json @@ -58,17 +58,11 @@ "ignoreDependencies": ["cloudflare"] }, "packages/ai-react": { - "ignoreDependencies": ["@mcp-ui/client"] + "ignoreDependencies": ["@mcp-ui/client", "react-dom"] }, "packages/ai-preact": { "ignoreDependencies": ["@mcp-ui/client"] }, - "packages/ai-react-ui": { - "ignoreDependencies": ["react-dom"] - }, - "packages/ai-vue-ui": { - "ignore": ["src/use-chat-context.ts"] - }, "packages/ai-bedrock": {}, "packages/ai-grok-build": { "ignoreDependencies": ["@tanstack/ai-acp"] diff --git a/packages/ai-client/README.md b/packages/ai-client/README.md index c9bbb8c10d..d524f36907 100644 --- a/packages/ai-client/README.md +++ b/packages/ai-client/README.md @@ -226,15 +226,15 @@ on. ## Framework Packages -| Package | What it provides | -| -------------------------------------------------------------------------- | ------------------------------------------------------------------------ | -| [`@tanstack/ai-client`](https://tanstack.com/ai/latest/docs/api/ai-client) | Headless chat, realtime, and generation clients | -| [`@tanstack/ai-react`](https://tanstack.com/ai/latest/docs/api/ai-react) | React hooks including `useChat`, `useRealtimeChat`, and generation hooks | -| [`@tanstack/ai-solid`](https://tanstack.com/ai/latest/docs/api/ai-solid) | Solid hooks for chat and generations | -| [`@tanstack/ai-vue`](https://tanstack.com/ai/latest/docs/api/ai-vue) | Vue composables for chat and generations | -| [`@tanstack/ai-svelte`](https://tanstack.com/ai/latest/docs/api/ai-svelte) | Svelte 5 factories for chat and generations | -| [`@tanstack/ai-preact`](https://tanstack.com/ai/latest/docs/api/ai-preact) | Preact hooks for chat | -| `@tanstack/ai-react-ui`, `@tanstack/ai-solid-ui`, `@tanstack/ai-vue-ui` | Headless UI components for chat interfaces | +| Package | What it provides | +| ------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | +| [`@tanstack/ai-client`](https://tanstack.com/ai/latest/docs/api/ai-client) | Headless chat, realtime, and generation clients | +| [`@tanstack/ai-react`](https://tanstack.com/ai/latest/docs/api/ai-react) | React hooks including `useChat`, `useRealtimeChat`, and generation hooks | +| [`@tanstack/ai-solid`](https://tanstack.com/ai/latest/docs/api/ai-solid) | Solid hooks for chat and generations | +| [`@tanstack/ai-vue`](https://tanstack.com/ai/latest/docs/api/ai-vue) | Vue composables for chat and generations | +| [`@tanstack/ai-svelte`](https://tanstack.com/ai/latest/docs/api/ai-svelte) | Svelte 5 factories for chat and generations | +| [`@tanstack/ai-preact`](https://tanstack.com/ai/latest/docs/api/ai-preact) | Preact hooks for chat | +| `@tanstack/ai-react/ui`, `@tanstack/ai-solid/ui`, `@tanstack/ai-vue/ui`, `@tanstack/ai-svelte/ui` | Headless UI components for chat interfaces. `@tanstack/ai-react-ui`, `@tanstack/ai-solid-ui`, and `@tanstack/ai-vue-ui` are deprecated. | ## Advanced Docs diff --git a/packages/ai-client/src/ui.ts b/packages/ai-client/src/ui.ts index d745111870..b7f59e1148 100644 --- a/packages/ai-client/src/ui.ts +++ b/packages/ai-client/src/ui.ts @@ -24,6 +24,7 @@ export type { ChatUISelectedGenericPart, ChatUISelectedMessage, ChatUISelectedPart, + ChatUISelectedPartOf, ChatUISelectedResultPart, ChatUISelectedToolPart, ChatUISelection, diff --git a/packages/ai-client/src/ui/types.ts b/packages/ai-client/src/ui/types.ts index 32aad63230..85f3b58e69 100644 --- a/packages/ai-client/src/ui/types.ts +++ b/packages/ai-client/src/ui/types.ts @@ -206,6 +206,18 @@ export type ChatUISelectedPart = | ChatUISelectedResultPart | ChatUISelectedGenericPart +export type ChatUISelectedPartOf< + TOptions, + TKey extends ChatUIPartKey = ChatUIPartKey, +> = TKey extends 'toolCall' + ? ChatUIToolPart + : TKey extends 'toolResult' + ? ChatUISelectedResultPart + : { + key: TKey + part: ChatUIPartOf + } + export type ChatUISelectedMessage = { message: UIMessage parts: Array diff --git a/packages/ai-client/tests/ui-types.test-d.ts b/packages/ai-client/tests/ui-types.test-d.ts index 87aec41609..0d570e6ee4 100644 --- a/packages/ai-client/tests/ui-types.test-d.ts +++ b/packages/ai-client/tests/ui-types.test-d.ts @@ -3,6 +3,7 @@ import type { ChatUIData, ChatUIInterrupt, ChatUIInterruptOf, + ChatUISelectedPartOf, ChatUIToolPart, RegisteredUIInterrupt, } from '../src/ui' @@ -35,3 +36,7 @@ expectTypeOf().toEqualTypeOf<'purchaseItem'>() type AnyInterrupt = ChatUIInterruptOf expectTypeOf().toEqualTypeOf() + +type TextSelected = ChatUISelectedPartOf +expectTypeOf().toEqualTypeOf<'text'>() +expectTypeOf().toEqualTypeOf() diff --git a/packages/ai-devtools/README.md b/packages/ai-devtools/README.md index c9bbb8c10d..d524f36907 100644 --- a/packages/ai-devtools/README.md +++ b/packages/ai-devtools/README.md @@ -226,15 +226,15 @@ on. ## Framework Packages -| Package | What it provides | -| -------------------------------------------------------------------------- | ------------------------------------------------------------------------ | -| [`@tanstack/ai-client`](https://tanstack.com/ai/latest/docs/api/ai-client) | Headless chat, realtime, and generation clients | -| [`@tanstack/ai-react`](https://tanstack.com/ai/latest/docs/api/ai-react) | React hooks including `useChat`, `useRealtimeChat`, and generation hooks | -| [`@tanstack/ai-solid`](https://tanstack.com/ai/latest/docs/api/ai-solid) | Solid hooks for chat and generations | -| [`@tanstack/ai-vue`](https://tanstack.com/ai/latest/docs/api/ai-vue) | Vue composables for chat and generations | -| [`@tanstack/ai-svelte`](https://tanstack.com/ai/latest/docs/api/ai-svelte) | Svelte 5 factories for chat and generations | -| [`@tanstack/ai-preact`](https://tanstack.com/ai/latest/docs/api/ai-preact) | Preact hooks for chat | -| `@tanstack/ai-react-ui`, `@tanstack/ai-solid-ui`, `@tanstack/ai-vue-ui` | Headless UI components for chat interfaces | +| Package | What it provides | +| ------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | +| [`@tanstack/ai-client`](https://tanstack.com/ai/latest/docs/api/ai-client) | Headless chat, realtime, and generation clients | +| [`@tanstack/ai-react`](https://tanstack.com/ai/latest/docs/api/ai-react) | React hooks including `useChat`, `useRealtimeChat`, and generation hooks | +| [`@tanstack/ai-solid`](https://tanstack.com/ai/latest/docs/api/ai-solid) | Solid hooks for chat and generations | +| [`@tanstack/ai-vue`](https://tanstack.com/ai/latest/docs/api/ai-vue) | Vue composables for chat and generations | +| [`@tanstack/ai-svelte`](https://tanstack.com/ai/latest/docs/api/ai-svelte) | Svelte 5 factories for chat and generations | +| [`@tanstack/ai-preact`](https://tanstack.com/ai/latest/docs/api/ai-preact) | Preact hooks for chat | +| `@tanstack/ai-react/ui`, `@tanstack/ai-solid/ui`, `@tanstack/ai-vue/ui`, `@tanstack/ai-svelte/ui` | Headless UI components for chat interfaces. `@tanstack/ai-react-ui`, `@tanstack/ai-solid-ui`, and `@tanstack/ai-vue-ui` are deprecated. | ## Advanced Docs diff --git a/packages/ai-gemini/README.md b/packages/ai-gemini/README.md index c9bbb8c10d..d524f36907 100644 --- a/packages/ai-gemini/README.md +++ b/packages/ai-gemini/README.md @@ -226,15 +226,15 @@ on. ## Framework Packages -| Package | What it provides | -| -------------------------------------------------------------------------- | ------------------------------------------------------------------------ | -| [`@tanstack/ai-client`](https://tanstack.com/ai/latest/docs/api/ai-client) | Headless chat, realtime, and generation clients | -| [`@tanstack/ai-react`](https://tanstack.com/ai/latest/docs/api/ai-react) | React hooks including `useChat`, `useRealtimeChat`, and generation hooks | -| [`@tanstack/ai-solid`](https://tanstack.com/ai/latest/docs/api/ai-solid) | Solid hooks for chat and generations | -| [`@tanstack/ai-vue`](https://tanstack.com/ai/latest/docs/api/ai-vue) | Vue composables for chat and generations | -| [`@tanstack/ai-svelte`](https://tanstack.com/ai/latest/docs/api/ai-svelte) | Svelte 5 factories for chat and generations | -| [`@tanstack/ai-preact`](https://tanstack.com/ai/latest/docs/api/ai-preact) | Preact hooks for chat | -| `@tanstack/ai-react-ui`, `@tanstack/ai-solid-ui`, `@tanstack/ai-vue-ui` | Headless UI components for chat interfaces | +| Package | What it provides | +| ------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | +| [`@tanstack/ai-client`](https://tanstack.com/ai/latest/docs/api/ai-client) | Headless chat, realtime, and generation clients | +| [`@tanstack/ai-react`](https://tanstack.com/ai/latest/docs/api/ai-react) | React hooks including `useChat`, `useRealtimeChat`, and generation hooks | +| [`@tanstack/ai-solid`](https://tanstack.com/ai/latest/docs/api/ai-solid) | Solid hooks for chat and generations | +| [`@tanstack/ai-vue`](https://tanstack.com/ai/latest/docs/api/ai-vue) | Vue composables for chat and generations | +| [`@tanstack/ai-svelte`](https://tanstack.com/ai/latest/docs/api/ai-svelte) | Svelte 5 factories for chat and generations | +| [`@tanstack/ai-preact`](https://tanstack.com/ai/latest/docs/api/ai-preact) | Preact hooks for chat | +| `@tanstack/ai-react/ui`, `@tanstack/ai-solid/ui`, `@tanstack/ai-vue/ui`, `@tanstack/ai-svelte/ui` | Headless UI components for chat interfaces. `@tanstack/ai-react-ui`, `@tanstack/ai-solid-ui`, and `@tanstack/ai-vue-ui` are deprecated. | ## Advanced Docs diff --git a/packages/ai-ollama/README.md b/packages/ai-ollama/README.md index c9bbb8c10d..d524f36907 100644 --- a/packages/ai-ollama/README.md +++ b/packages/ai-ollama/README.md @@ -226,15 +226,15 @@ on. ## Framework Packages -| Package | What it provides | -| -------------------------------------------------------------------------- | ------------------------------------------------------------------------ | -| [`@tanstack/ai-client`](https://tanstack.com/ai/latest/docs/api/ai-client) | Headless chat, realtime, and generation clients | -| [`@tanstack/ai-react`](https://tanstack.com/ai/latest/docs/api/ai-react) | React hooks including `useChat`, `useRealtimeChat`, and generation hooks | -| [`@tanstack/ai-solid`](https://tanstack.com/ai/latest/docs/api/ai-solid) | Solid hooks for chat and generations | -| [`@tanstack/ai-vue`](https://tanstack.com/ai/latest/docs/api/ai-vue) | Vue composables for chat and generations | -| [`@tanstack/ai-svelte`](https://tanstack.com/ai/latest/docs/api/ai-svelte) | Svelte 5 factories for chat and generations | -| [`@tanstack/ai-preact`](https://tanstack.com/ai/latest/docs/api/ai-preact) | Preact hooks for chat | -| `@tanstack/ai-react-ui`, `@tanstack/ai-solid-ui`, `@tanstack/ai-vue-ui` | Headless UI components for chat interfaces | +| Package | What it provides | +| ------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | +| [`@tanstack/ai-client`](https://tanstack.com/ai/latest/docs/api/ai-client) | Headless chat, realtime, and generation clients | +| [`@tanstack/ai-react`](https://tanstack.com/ai/latest/docs/api/ai-react) | React hooks including `useChat`, `useRealtimeChat`, and generation hooks | +| [`@tanstack/ai-solid`](https://tanstack.com/ai/latest/docs/api/ai-solid) | Solid hooks for chat and generations | +| [`@tanstack/ai-vue`](https://tanstack.com/ai/latest/docs/api/ai-vue) | Vue composables for chat and generations | +| [`@tanstack/ai-svelte`](https://tanstack.com/ai/latest/docs/api/ai-svelte) | Svelte 5 factories for chat and generations | +| [`@tanstack/ai-preact`](https://tanstack.com/ai/latest/docs/api/ai-preact) | Preact hooks for chat | +| `@tanstack/ai-react/ui`, `@tanstack/ai-solid/ui`, `@tanstack/ai-vue/ui`, `@tanstack/ai-svelte/ui` | Headless UI components for chat interfaces. `@tanstack/ai-react-ui`, `@tanstack/ai-solid-ui`, and `@tanstack/ai-vue-ui` are deprecated. | ## Advanced Docs diff --git a/packages/ai-openai/README.md b/packages/ai-openai/README.md index c9bbb8c10d..d524f36907 100644 --- a/packages/ai-openai/README.md +++ b/packages/ai-openai/README.md @@ -226,15 +226,15 @@ on. ## Framework Packages -| Package | What it provides | -| -------------------------------------------------------------------------- | ------------------------------------------------------------------------ | -| [`@tanstack/ai-client`](https://tanstack.com/ai/latest/docs/api/ai-client) | Headless chat, realtime, and generation clients | -| [`@tanstack/ai-react`](https://tanstack.com/ai/latest/docs/api/ai-react) | React hooks including `useChat`, `useRealtimeChat`, and generation hooks | -| [`@tanstack/ai-solid`](https://tanstack.com/ai/latest/docs/api/ai-solid) | Solid hooks for chat and generations | -| [`@tanstack/ai-vue`](https://tanstack.com/ai/latest/docs/api/ai-vue) | Vue composables for chat and generations | -| [`@tanstack/ai-svelte`](https://tanstack.com/ai/latest/docs/api/ai-svelte) | Svelte 5 factories for chat and generations | -| [`@tanstack/ai-preact`](https://tanstack.com/ai/latest/docs/api/ai-preact) | Preact hooks for chat | -| `@tanstack/ai-react-ui`, `@tanstack/ai-solid-ui`, `@tanstack/ai-vue-ui` | Headless UI components for chat interfaces | +| Package | What it provides | +| ------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | +| [`@tanstack/ai-client`](https://tanstack.com/ai/latest/docs/api/ai-client) | Headless chat, realtime, and generation clients | +| [`@tanstack/ai-react`](https://tanstack.com/ai/latest/docs/api/ai-react) | React hooks including `useChat`, `useRealtimeChat`, and generation hooks | +| [`@tanstack/ai-solid`](https://tanstack.com/ai/latest/docs/api/ai-solid) | Solid hooks for chat and generations | +| [`@tanstack/ai-vue`](https://tanstack.com/ai/latest/docs/api/ai-vue) | Vue composables for chat and generations | +| [`@tanstack/ai-svelte`](https://tanstack.com/ai/latest/docs/api/ai-svelte) | Svelte 5 factories for chat and generations | +| [`@tanstack/ai-preact`](https://tanstack.com/ai/latest/docs/api/ai-preact) | Preact hooks for chat | +| `@tanstack/ai-react/ui`, `@tanstack/ai-solid/ui`, `@tanstack/ai-vue/ui`, `@tanstack/ai-svelte/ui` | Headless UI components for chat interfaces. `@tanstack/ai-react-ui`, `@tanstack/ai-solid-ui`, and `@tanstack/ai-vue-ui` are deprecated. | ## Advanced Docs diff --git a/packages/ai-openrouter/README.md b/packages/ai-openrouter/README.md index c9bbb8c10d..d524f36907 100644 --- a/packages/ai-openrouter/README.md +++ b/packages/ai-openrouter/README.md @@ -226,15 +226,15 @@ on. ## Framework Packages -| Package | What it provides | -| -------------------------------------------------------------------------- | ------------------------------------------------------------------------ | -| [`@tanstack/ai-client`](https://tanstack.com/ai/latest/docs/api/ai-client) | Headless chat, realtime, and generation clients | -| [`@tanstack/ai-react`](https://tanstack.com/ai/latest/docs/api/ai-react) | React hooks including `useChat`, `useRealtimeChat`, and generation hooks | -| [`@tanstack/ai-solid`](https://tanstack.com/ai/latest/docs/api/ai-solid) | Solid hooks for chat and generations | -| [`@tanstack/ai-vue`](https://tanstack.com/ai/latest/docs/api/ai-vue) | Vue composables for chat and generations | -| [`@tanstack/ai-svelte`](https://tanstack.com/ai/latest/docs/api/ai-svelte) | Svelte 5 factories for chat and generations | -| [`@tanstack/ai-preact`](https://tanstack.com/ai/latest/docs/api/ai-preact) | Preact hooks for chat | -| `@tanstack/ai-react-ui`, `@tanstack/ai-solid-ui`, `@tanstack/ai-vue-ui` | Headless UI components for chat interfaces | +| Package | What it provides | +| ------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | +| [`@tanstack/ai-client`](https://tanstack.com/ai/latest/docs/api/ai-client) | Headless chat, realtime, and generation clients | +| [`@tanstack/ai-react`](https://tanstack.com/ai/latest/docs/api/ai-react) | React hooks including `useChat`, `useRealtimeChat`, and generation hooks | +| [`@tanstack/ai-solid`](https://tanstack.com/ai/latest/docs/api/ai-solid) | Solid hooks for chat and generations | +| [`@tanstack/ai-vue`](https://tanstack.com/ai/latest/docs/api/ai-vue) | Vue composables for chat and generations | +| [`@tanstack/ai-svelte`](https://tanstack.com/ai/latest/docs/api/ai-svelte) | Svelte 5 factories for chat and generations | +| [`@tanstack/ai-preact`](https://tanstack.com/ai/latest/docs/api/ai-preact) | Preact hooks for chat | +| `@tanstack/ai-react/ui`, `@tanstack/ai-solid/ui`, `@tanstack/ai-vue/ui`, `@tanstack/ai-svelte/ui` | Headless UI components for chat interfaces. `@tanstack/ai-react-ui`, `@tanstack/ai-solid-ui`, and `@tanstack/ai-vue-ui` are deprecated. | ## Advanced Docs diff --git a/packages/ai-preact/README.md b/packages/ai-preact/README.md index c9bbb8c10d..d524f36907 100644 --- a/packages/ai-preact/README.md +++ b/packages/ai-preact/README.md @@ -226,15 +226,15 @@ on. ## Framework Packages -| Package | What it provides | -| -------------------------------------------------------------------------- | ------------------------------------------------------------------------ | -| [`@tanstack/ai-client`](https://tanstack.com/ai/latest/docs/api/ai-client) | Headless chat, realtime, and generation clients | -| [`@tanstack/ai-react`](https://tanstack.com/ai/latest/docs/api/ai-react) | React hooks including `useChat`, `useRealtimeChat`, and generation hooks | -| [`@tanstack/ai-solid`](https://tanstack.com/ai/latest/docs/api/ai-solid) | Solid hooks for chat and generations | -| [`@tanstack/ai-vue`](https://tanstack.com/ai/latest/docs/api/ai-vue) | Vue composables for chat and generations | -| [`@tanstack/ai-svelte`](https://tanstack.com/ai/latest/docs/api/ai-svelte) | Svelte 5 factories for chat and generations | -| [`@tanstack/ai-preact`](https://tanstack.com/ai/latest/docs/api/ai-preact) | Preact hooks for chat | -| `@tanstack/ai-react-ui`, `@tanstack/ai-solid-ui`, `@tanstack/ai-vue-ui` | Headless UI components for chat interfaces | +| Package | What it provides | +| ------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | +| [`@tanstack/ai-client`](https://tanstack.com/ai/latest/docs/api/ai-client) | Headless chat, realtime, and generation clients | +| [`@tanstack/ai-react`](https://tanstack.com/ai/latest/docs/api/ai-react) | React hooks including `useChat`, `useRealtimeChat`, and generation hooks | +| [`@tanstack/ai-solid`](https://tanstack.com/ai/latest/docs/api/ai-solid) | Solid hooks for chat and generations | +| [`@tanstack/ai-vue`](https://tanstack.com/ai/latest/docs/api/ai-vue) | Vue composables for chat and generations | +| [`@tanstack/ai-svelte`](https://tanstack.com/ai/latest/docs/api/ai-svelte) | Svelte 5 factories for chat and generations | +| [`@tanstack/ai-preact`](https://tanstack.com/ai/latest/docs/api/ai-preact) | Preact hooks for chat | +| `@tanstack/ai-react/ui`, `@tanstack/ai-solid/ui`, `@tanstack/ai-vue/ui`, `@tanstack/ai-svelte/ui` | Headless UI components for chat interfaces. `@tanstack/ai-react-ui`, `@tanstack/ai-solid-ui`, and `@tanstack/ai-vue-ui` are deprecated. | ## Advanced Docs diff --git a/packages/ai-react-ui/CHANGELOG.md b/packages/ai-react-ui/CHANGELOG.md deleted file mode 100644 index 591480e967..0000000000 --- a/packages/ai-react-ui/CHANGELOG.md +++ /dev/null @@ -1,442 +0,0 @@ -# @tanstack/ai-react-ui - -## 0.8.22 - -### Patch Changes - -- Updated dependencies [[`62c19ed`](https://github.com/TanStack/ai/commit/62c19edce7a814d868491ca920003899ec4c486b), [`4e9c5d2`](https://github.com/TanStack/ai/commit/4e9c5d2887dc9a3d64fce6ed424aa68f451e20f6), [`62c19ed`](https://github.com/TanStack/ai/commit/62c19edce7a814d868491ca920003899ec4c486b)]: - - @tanstack/ai-client@0.29.0 - - @tanstack/ai-react@0.22.2 - -## 0.8.21 - -### Patch Changes - -- Updated dependencies [[`ad3840c`](https://github.com/TanStack/ai/commit/ad3840c44bd09860b29cd22f24a0cdce3adaf9dc), [`365efb8`](https://github.com/TanStack/ai/commit/365efb870e5a3fe084d0969f71781ffc772e39a0), [`a955710`](https://github.com/TanStack/ai/commit/a9557101048286c225145bad058ca81ff275b330), [`a74817a`](https://github.com/TanStack/ai/commit/a74817a38e8deb4c7ff86d81067db12befbadf5d)]: - - @tanstack/ai-client@0.28.0 - - @tanstack/ai-react@0.22.1 - -## 0.8.20 - -### Patch Changes - -- Updated dependencies [[`b7ebcb0`](https://github.com/TanStack/ai/commit/b7ebcb0bbe63e425facb5e38f138bd0cd36637dd)]: - - @tanstack/ai-client@0.27.0 - - @tanstack/ai-react@0.22.0 - -## 0.8.19 - -### Patch Changes - -- Updated dependencies [[`1c0415b`](https://github.com/TanStack/ai/commit/1c0415bec4bbefcd3abf784d0209af05aca5db46)]: - - @tanstack/ai-client@0.26.0 - - @tanstack/ai-react@0.21.3 - -## 0.8.18 - -### Patch Changes - -- Updated dependencies [[`5f68cbc`](https://github.com/TanStack/ai/commit/5f68cbccf3621b48dae73cedcb1e59cb4cbe72b4), [`32e62ab`](https://github.com/TanStack/ai/commit/32e62ab8b7dc6a8a13ca3851c8925ab806e08f29)]: - - @tanstack/ai-client@0.25.0 - - @tanstack/ai-react@0.21.0 - -## 0.8.17 - -### Patch Changes - -- Updated dependencies [[`f6b9d89`](https://github.com/TanStack/ai/commit/f6b9d89e7365f0cb30a41cf3a46b7dd237fbc73c), [`ecd12a4`](https://github.com/TanStack/ai/commit/ecd12a408987bc75649c21aada6948282a2a66dd)]: - - @tanstack/ai-client@0.24.0 - - @tanstack/ai-react@0.20.0 - -## 0.8.16 - -### Patch Changes - -- Updated dependencies [[`7499171`](https://github.com/TanStack/ai/commit/74991716aea4d90a5d0363676a1e3349689a48e8)]: - - @tanstack/ai-client@0.23.0 - - @tanstack/ai-react@0.19.0 - -## 0.8.15 - -### Patch Changes - -- Updated dependencies [[`35946e3`](https://github.com/TanStack/ai/commit/35946e3c39fb123c133ebe662f8e2cf0139f2b8c)]: - - @tanstack/ai-client@0.22.0 - - @tanstack/ai-react@0.18.0 - -## 0.8.14 - -### Patch Changes - -- Updated dependencies [[`2665085`](https://github.com/TanStack/ai/commit/2665085970ab4d792778bb2b635ef27fbdcb6be1), [`f830d9e`](https://github.com/TanStack/ai/commit/f830d9e7a41e3554c424c3e41ba847dfd1577589), [`f830d9e`](https://github.com/TanStack/ai/commit/f830d9e7a41e3554c424c3e41ba847dfd1577589)]: - - @tanstack/ai-client@0.21.0 - - @tanstack/ai-react@0.17.0 - -## 0.8.13 - -### Patch Changes - -- Updated dependencies [[`5deda27`](https://github.com/TanStack/ai/commit/5deda27085c8785894a28feb5bb3655dbd8f7e0a)]: - - @tanstack/ai-client@0.20.0 - - @tanstack/ai-react@0.16.4 - -## 0.8.12 - -### Patch Changes - -- Updated dependencies [[`33acdd4`](https://github.com/TanStack/ai/commit/33acdd4df4aef13d594700d9b52087252091bd40), [`c22c663`](https://github.com/TanStack/ai/commit/c22c6632fdca761033cb9c4273bf61fc8ce86662), [`c1a8732`](https://github.com/TanStack/ai/commit/c1a87327b4a3463d37158f32ca90184b5fd092bb)]: - - @tanstack/ai-client@0.19.0 - - @tanstack/ai-react@0.16.0 - -## 0.8.11 - -### Patch Changes - -- [#844](https://github.com/TanStack/ai/pull/844) [`a6cceba`](https://github.com/TanStack/ai/commit/a6cceba4812e7e986183ee856112fcf5f8fa12ff) - Republish all packages with their compiled `dist/` output. - - Releases `0.33.0`–`0.36.0` were published without a `dist/` directory: the - release workflow relied on an Nx-cached `build` whose outputs were not - materialized to disk before `changeset publish` packed the tarballs, and - `files: ["dist"]` silently includes nothing when `dist/` is absent. The - published packages therefore contained only `src/`, so every export - (`./dist/esm/*.js`) resolved to a missing file and the packages were - uninstallable. - - The publish step now runs a fresh, cache-bypassing build of all packages - immediately before publishing, guaranteeing compiled artifacts are present in - every tarball. - -- Updated dependencies [[`a6cceba`](https://github.com/TanStack/ai/commit/a6cceba4812e7e986183ee856112fcf5f8fa12ff)]: - - @tanstack/ai-client@0.18.6 - - @tanstack/ai-react@0.15.15 - -## 0.8.10 - -### Patch Changes - -- [#783](https://github.com/TanStack/ai/pull/783) [`0278a90`](https://github.com/TanStack/ai/commit/0278a900e80ab7203124a74c841222b20b1c3bd3) - Fix `ChatMessage` rendering of multimodal tool results. Tool-result content is `string | Array`, but the renderer previously typed the message part as `any` and passed the raw content straight to React — an array of content-part objects would throw React's "Objects are not valid as a React child". The part is now typed as `UIMessage['parts'][number]`, and array content is flattened to the concatenation of its text parts (non-text parts are skipped) before rendering, both for the built-in renderer and the `toolResultRenderer` prop. - -- Updated dependencies [[`540cbf1`](https://github.com/TanStack/ai/commit/540cbf18a2f7d6c07b44f7f4da0ac3873c0d2581)]: - - @tanstack/ai-client@0.18.3 - - @tanstack/ai-react@0.15.12 - -## 0.8.9 - -### Patch Changes - -- Updated dependencies [[`8fa6cc5`](https://github.com/TanStack/ai/commit/8fa6cc56c5f36e22885c98a511dcceb2bfc0da1f)]: - - @tanstack/ai-client@0.18.0 - - @tanstack/ai-react@0.15.9 - -## 0.8.8 - -### Patch Changes - -- [#769](https://github.com/TanStack/ai/pull/769) [`1d1bb52`](https://github.com/TanStack/ai/commit/1d1bb5219a38d9718cc926148e93fc27d5d2305b) - Add repository metadata (`homepage`, `bugs`, `funding`), fix `repository.directory` to point at each package, and include an MIT `LICENSE` file in every published package. - -- Updated dependencies [[`1d1bb52`](https://github.com/TanStack/ai/commit/1d1bb5219a38d9718cc926148e93fc27d5d2305b)]: - - @tanstack/ai-client@0.17.1 - - @tanstack/ai-react@0.15.6 - -## 0.8.7 - -### Patch Changes - -- Updated dependencies [[`7d44569`](https://github.com/TanStack/ai/commit/7d445693ea079d7a85498a4465179ddd5f548cb0)]: - - @tanstack/ai-client@0.17.0 - - @tanstack/ai-react@0.15.5 - -## 0.8.6 - -### Patch Changes - -- Updated dependencies [[`755e995`](https://github.com/TanStack/ai/commit/755e9953a31e879c4b88df0e7672ce1224886c97)]: - - @tanstack/ai-client@0.16.0 - - @tanstack/ai-react@0.15.0 - -## 0.8.5 - -### Patch Changes - -- Updated dependencies [[`c1ae8b9`](https://github.com/TanStack/ai/commit/c1ae8b94c83d70508975568eb4fc9b45f1af540b), [`8036b50`](https://github.com/TanStack/ai/commit/8036b5054330a180023c6e3225b8d2735a43a919)]: - - @tanstack/ai-client@0.15.0 - - @tanstack/ai-react@0.14.0 - -## 0.8.4 - -### Patch Changes - -- Updated dependencies [[`d5645cf`](https://github.com/TanStack/ai/commit/d5645cfd4d1b9cfc877f7d4d714517e166a99ce3)]: - - @tanstack/ai-client@0.14.0 - - @tanstack/ai-react@0.13.0 - -## 0.8.3 - -### Patch Changes - -- Updated dependencies [[`5634f18`](https://github.com/TanStack/ai/commit/5634f186a4946ca3e1942fbfcbf1291ec9bd9855)]: - - @tanstack/ai-client@0.13.0 - - @tanstack/ai-react@0.12.1 - -## 0.8.2 - -### Patch Changes - -- Updated dependencies [[`ad23da9`](https://github.com/TanStack/ai/commit/ad23da92c279759b3778672dcee3d1616a02994b)]: - - @tanstack/ai-client@0.12.0 - - @tanstack/ai-react@0.12.0 - -## 0.8.1 - -### Patch Changes - -- Refresh package README content and npm metadata for better discoverability. ([#626](https://github.com/TanStack/ai/pull/626)) - -- Updated dependencies [[`ebeb22e`](https://github.com/TanStack/ai/commit/ebeb22ec68f456b09e0181ac6f5d1ac25a0affd2)]: - - @tanstack/ai-client@0.11.6 - - @tanstack/ai-react@0.11.6 - -## 0.8.0 - -### Minor Changes - -- `TextPart` now accepts `remarkPlugins`, `rehypePlugins`, and (React/Solid) ([#599](https://github.com/TanStack/ai/pull/599)) - `components` props, plus a `disableDefaultPlugins` escape hatch. User plugins - merge with the secure defaults — `rehype-sanitize` continues to run last - unless defaults are disabled. - - This fixes [#164](https://github.com/TanStack/ai/issues/164): bold and - emphasis in Japanese, Chinese, and Korean text rendered incorrectly because - of a CommonMark spec defect. Consumers can now drop in - [`remark-cjk-friendly`](https://www.npmjs.com/package/remark-cjk-friendly) - with a single prop: - - ```tsx - import remarkCjkFriendly from 'remark-cjk-friendly' - ; - ``` - - Also fixes a latent bug in `@tanstack/ai-react-ui` where `remark-gfm` was - passed inside the rehype plugin array, silently disabling GFM features - (tables, strikethrough, task lists) in the React `TextPart`. - - `@tanstack/ai-vue-ui` omits the `components` prop because its underlying - renderer (`@crazydos/vue-markdown`) does not expose component overrides; - use that library's slot API for custom rendering. - -### Patch Changes - -- Adopt `@tanstack/eslint-config@0.4.0` and clean up the local override layer. ([#607](https://github.com/TanStack/ai/pull/607)) - - Bump `@tanstack/eslint-config` from `0.3.3` to `0.4.0`. - - Drop dead `pnpm/enforce-catalog` and `pnpm/json-enforce-catalog` disables (upstream removed `eslint-plugin-pnpm` in `0.3.1`). - - Drop the `no-case-declarations: off` override — no current source actually violates it. - - Drop the `no-shadow: off` override — upstream sets it to `warn`, so it surfaces in editors without blocking CI. - - Remove ~25 unnecessary type assertions across the publishable packages that the upgraded `typescript-eslint` now catches via `no-unnecessary-type-assertion`. One deliberately defensive cast in `ag-ui-wire.ts` is preserved with an inline opt-out and a reason comment. - - No public-API or runtime-behavior changes. - -- Updated dependencies [[`a03d12b`](https://github.com/TanStack/ai/commit/a03d12b13ade93f3e262c6ffa996696ce27472ef)]: - - @tanstack/ai-client@0.11.4 - - @tanstack/ai-react@0.11.4 - -## 0.7.2 - -### Patch Changes - -- Tighten TypeScript safety: enable `noImplicitOverride`, ([#579](https://github.com/TanStack/ai/pull/579)) - `noFallthroughCasesInSwitch`, and `useDefineForClassFields` in the - root `tsconfig.json`; add a typed-ESLint block scoped to - `packages/*/src/**` that turns on `no-floating-promises`, - `no-misused-promises`, `await-thenable`, - `switch-exhaustiveness-check`, `consistent-type-exports`, - `prefer-readonly`, and `no-non-null-assertion` (errors), plus - `no-explicit-any` (warning). `@ts-ignore` and `@ts-nocheck` are - disallowed in library source via `@typescript-eslint/ban-ts-comment`, - and `as unknown as ` double-casts are blocked by a - `no-restricted-syntax` rule (escape hatches available with an inline - reason). Two flags from the original five-flag set — - `noPropertyAccessFromIndexSignature` and `exactOptionalPropertyTypes` - — were tried and rolled back: they produced ~500 lines of bracket- - access and conditional-spread churn without catching any real bugs, - and `exactOptionalPropertyTypes` would have forced consumers using - it themselves to deal with our internals' style preferences. - - User-visible API surface is unchanged; this is a hardening pass to - keep streaming/agent-loop correctness and discriminated-union - exhaustiveness honest going forward. See issue #564. - -- Updated dependencies []: - - @tanstack/ai-client@0.11.3 - - @tanstack/ai-react@0.11.3 - -## 0.7.1 - -### Patch Changes - -- Updated dependencies [[`2e0e2eb`](https://github.com/TanStack/ai/commit/2e0e2eb72684aac82e570d57767656e218289b49), [`b05adb1`](https://github.com/TanStack/ai/commit/b05adb1b9ca756edc926c6b86ee6fae37dd966c0)]: - - @tanstack/ai-client@0.11.0 - - @tanstack/ai-react@0.11.0 - -## 0.7.0 - -### Minor Changes - -- **Breaking:** AG-UI client-to-server compliance. ([#511](https://github.com/TanStack/ai/pull/511)) - - `@tanstack/ai-client` now POSTs an AG-UI `RunAgentInput` request body and `@tanstack/ai` server endpoints must use the new `chatParamsFromRequestBody` + `mergeAgentTools` helpers. Upgrade both packages together. - - Highlights: - - **Wire format**: `{threadId, runId, state, messages, tools, context, forwardedProps}` (per AG-UI 0.0.52 `RunAgentInputSchema`) instead of `{messages, data}`. - - **New server helpers** exported from `@tanstack/ai`: `chatParamsFromRequestBody`, `mergeAgentTools`. - - **`chat()` accepts `threadId`, `runId`, `parentRunId`** as optional fields for AG-UI run correlation. - - **`ChatClient` accepts `threadId`** option; auto-generates and persists per session if omitted; fresh `runId` per send. - - **Client tools auto-advertised** to the server via `RunAgentInput.tools`. - - **Foreign AG-UI clients** can hit a TanStack server: `developer` collapses to `system`, `reasoning`/`activity` drop. - - See `docs/migration/ag-ui-compliance.md` for full migration steps. - -### Patch Changes - -- Updated dependencies [[`a9d1916`](https://github.com/TanStack/ai/commit/a9d19165a5028515cf1d091d611c8ac4b5b86099)]: - - @tanstack/ai-client@0.10.0 - - @tanstack/ai-react@0.10.0 - -## 0.6.4 - -### Patch Changes - -- Updated dependencies [[`98979f7`](https://github.com/TanStack/ai/commit/98979f7e72f4b5bfb816fb14b60a12871f8c4bec)]: - - @tanstack/ai-react@0.9.0 - - @tanstack/ai-client@0.9.2 - -## 0.6.3 - -### Patch Changes - -- Updated dependencies [[`b2d3cc1`](https://github.com/TanStack/ai/commit/b2d3cc131a31c54bd1e5841f958fbe333514e508), [`13cceae`](https://github.com/TanStack/ai/commit/13cceaedf64e398ca15b8dbbbfe215329ea26794)]: - - @tanstack/ai-client@0.9.0 - - @tanstack/ai-react@0.8.1 - -## 0.6.2 - -### Patch Changes - -- Updated dependencies [[`54523f5`](https://github.com/TanStack/ai/commit/54523f5e9a9b4d4ea6c49e4551936bc2cc25593a), [`008f015`](https://github.com/TanStack/ai/commit/008f0154f852e7e6734d3e3d35cad47780b52b7a), [`af9eb7b`](https://github.com/TanStack/ai/commit/af9eb7bbb875b23b7e99b2e6b743636daad402d1)]: - - @tanstack/ai-client@0.8.0 - - @tanstack/ai-react@0.8.0 - -## 0.6.1 - -### Patch Changes - -- Updated dependencies [[`86be1c8`](https://github.com/TanStack/ai/commit/86be1c8262bb3176ea786aa0af115b38c3e3f51a)]: - - @tanstack/ai-client@0.7.0 - - @tanstack/ai-react@0.7.0 - -## 0.6.0 - -### Patch Changes - -- Updated dependencies [[`6dfffca`](https://github.com/TanStack/ai/commit/6dfffca99aeac1ada59eb288f8eb09e564d3db1e), [`6dfffca`](https://github.com/TanStack/ai/commit/6dfffca99aeac1ada59eb288f8eb09e564d3db1e)]: - - @tanstack/ai-client@0.6.0 - - @tanstack/ai-react@0.6.4 - -## 0.5.0 - -### Patch Changes - -- Updated dependencies [[`5aa6acc`](https://github.com/TanStack/ai/commit/5aa6acc1a4faea5346f750322e80984abf2d7059), [`1f800aa`](https://github.com/TanStack/ai/commit/1f800aacf57081f37a075bc8d08ff397cb33cbe9)]: - - @tanstack/ai-client@0.5.0 - - @tanstack/ai-react@0.6.0 - -## 0.5.1 - -### Patch Changes - -- Updated dependencies [[`5d98472`](https://github.com/TanStack/ai/commit/5d984722e1f84725e3cfda834fbda3d0341ecedd)]: - - @tanstack/ai-client@0.4.4 - - @tanstack/ai-react@0.5.1 - -## 0.5.0 - -### Patch Changes - -- Updated dependencies [[`0158d14`](https://github.com/TanStack/ai/commit/0158d14df00639ff5325680ae91b7791c189e60f)]: - - @tanstack/ai-react@0.5.0 - - @tanstack/ai-client@0.4.1 - -## 0.4.0 - -### Patch Changes - -- Updated dependencies [[`99ccee5`](https://github.com/TanStack/ai/commit/99ccee5c72df12adc13bede98142c6da84d13cc4), [`230bab6`](https://github.com/TanStack/ai/commit/230bab6417c8ff2c25586a12126c85e27dd7bc15)]: - - @tanstack/ai-client@0.4.0 - - @tanstack/ai-react@0.4.0 - -## 0.3.0 - -### Patch Changes - -- Updated dependencies [[`e52135f`](https://github.com/TanStack/ai/commit/e52135f6ec3285227679411636e208ae84a408d7)]: - - @tanstack/ai-client@0.3.0 - - @tanstack/ai-react@0.3.0 - -## 0.2.1 - -### Patch Changes - -- fix up readmes ([#188](https://github.com/TanStack/ai/pull/188)) - -- Updated dependencies [[`181e0ac`](https://github.com/TanStack/ai/commit/181e0acdfb44b27db6cf871b36593c0f867cadf9)]: - - @tanstack/ai-react@0.2.1 - -## 0.2.0 - -### Patch Changes - -- Updated dependencies [[`c5df33c`](https://github.com/TanStack/ai/commit/c5df33c2d3e72c3332048ffe7c64a553e5ea86fb)]: - - @tanstack/ai-react@0.2.0 - -## 0.1.0 - -### Minor Changes - -- Split up adapters for better tree shaking into separate functionalities ([#137](https://github.com/TanStack/ai/pull/137)) - -### Patch Changes - -- Updated dependencies [[`8d77614`](https://github.com/TanStack/ai/commit/8d776146f94ffd1579e1ab01b26dcb94d1bb3092)]: - - @tanstack/ai-client@0.1.0 - - @tanstack/ai-react@0.1.0 - -## 0.0.3 - -### Patch Changes - -- Updated dependencies []: - - @tanstack/ai-client@0.0.3 - - @tanstack/ai-react@0.0.3 - -## 0.0.2 - -### Patch Changes - -- Updated dependencies [[`a7bd563`](https://github.com/TanStack/ai/commit/a7bd5639eb2fbf1b4169eb307f77149f4a85a915)]: - - @tanstack/ai-client@0.0.2 - - @tanstack/ai-react@0.0.2 - -## 0.0.1 - -### Patch Changes - -- initial release of AI ([#76](https://github.com/TanStack/ai/pull/76)) - -## 0.0.1 - -### Patch Changes - -- Initial release of TanStack AI ([#72](https://github.com/TanStack/ai/pull/72)) - -- Updated dependencies [[`a9b54c2`](https://github.com/TanStack/ai/commit/a9b54c21282d16036a427761e0784b159a6f2d99)]: - - @tanstack/ai-client@0.0.1 - - @tanstack/ai-react@0.0.1 diff --git a/packages/ai-react-ui/README.md b/packages/ai-react-ui/README.md index c9bbb8c10d..9d1e775ab0 100644 --- a/packages/ai-react-ui/README.md +++ b/packages/ai-react-ui/README.md @@ -1,328 +1,9 @@ -
- - - - TanStack AI - -
+# @tanstack/ai-react-ui (deprecated) -
+This package is deprecated since 0.9.0. It will be removed in 1.0.0. - +Import chat UI from [`@tanstack/ai-react/ui`](https://tanstack.com/ai/latest/docs/ui/react) instead. -
+This package re-exports that subpath so existing installs keep working during the move. - - -# TanStack AI - -Type-safe, provider-agnostic TypeScript SDK for building streaming chat, -tool-calling agents, structured outputs, realtime voice, media generation, and -framework-native AI apps. - -TanStack AI is built from composable activities and provider adapters. Use one -provider or switch between many. Import only chat, or add image, audio, video, -speech, transcription, summarization, realtime, Code Mode, devtools, and -framework bindings as your app needs them. - -## Read the docs -> - -## Start Here - -- [Overview](https://tanstack.com/ai/latest/docs/getting-started/overview) - - what TanStack AI is and how the packages fit together. -- [Quick Start: React](https://tanstack.com/ai/latest/docs/getting-started/quick-start) - - add streaming chat to a React app. -- [Quick Start: Vue](https://tanstack.com/ai/latest/docs/getting-started/quick-start-vue) - - build with Vue composables. -- [Quick Start: Svelte](https://tanstack.com/ai/latest/docs/getting-started/quick-start-svelte) - - build with Svelte 5 runes. -- [Quick Start: Server Only](https://tanstack.com/ai/latest/docs/getting-started/quick-start-server) - - use TanStack AI from a server endpoint, script, or backend service. -- [TanStack AI vs Vercel AI SDK](https://tanstack.com/ai/latest/docs/comparison/vercel-ai-sdk) - - compare architecture, feature coverage, and tradeoffs. - -## What You Can Build - -- Streaming chat experiences with typed messages, tool calls, reasoning parts, - and configurable connection adapters. -- Type-safe tools that can run on the server or client from one shared - `toolDefinition()` contract. -- Structured output flows backed by JSON Schema, Zod, ArkType, Valibot, or - plain JSON Schema. -- Multimodal prompts and responses that include text, images, audio, video, and - documents. -- Image, audio, video, speech, transcription, and summarization workflows using - a shared generation client pattern. -- Realtime voice chat with provider adapters for realtime sessions and token - minting. -- Code Mode agents that let an LLM write and execute TypeScript in an isolated - sandbox to orchestrate tools with loops, branches, and parallel calls. -- Devtools and observability pipelines for inspecting messages, tool calls, - stream chunks, errors, usage, and OpenTelemetry traces. -- Framework-native clients for React, Solid, Vue, Svelte, and Preact, plus a - headless client for custom runtimes. - -## Install - -Install the core package and the provider/framework packages your app uses: - -```bash -pnpm add @tanstack/ai @tanstack/ai-openai -``` - -For a React chat UI: - -```bash -pnpm add @tanstack/ai @tanstack/ai-client @tanstack/ai-react @tanstack/ai-openai -``` - -OpenRouter is also a good starting point if you want access to many providers -through one API key: - -```bash -pnpm add @tanstack/ai @tanstack/ai-openrouter -``` - -## Streaming Chat - -```typescript -import { chat, toServerSentEventsResponse } from '@tanstack/ai' -import { openaiText } from '@tanstack/ai-openai' - -export async function POST(request: Request) { - const body = await request.json() - - const stream = chat({ - adapter: openaiText('gpt-5.2'), - messages: body.messages, - }) - - return toServerSentEventsResponse(stream) -} -``` - -Learn more in the -[Chat & Streaming docs](https://tanstack.com/ai/latest/docs/chat/streaming) and -[Connection Adapters docs](https://tanstack.com/ai/latest/docs/chat/connection-adapters). - -## Type-Safe Tools - -Define a tool once, then attach a server or client implementation with the same -input and output types: - -```typescript -import { toolDefinition } from '@tanstack/ai' -import { z } from 'zod' - -const getProducts = toolDefinition({ - name: 'getProducts', - description: 'Search the product catalog', - inputSchema: z.object({ query: z.string() }), - outputSchema: z.array( - z.object({ - id: z.string(), - name: z.string(), - }), - ), -}).server(async ({ query }) => { - return db.products.search(query) -}) -``` - -Learn more in the -[Tools docs](https://tanstack.com/ai/latest/docs/tools/tools), -[Tool Approval Flow docs](https://tanstack.com/ai/latest/docs/tools/tool-approval), -and -[Lazy Tool Discovery docs](https://tanstack.com/ai/latest/docs/tools/lazy-tool-discovery). - -## Structured Outputs - -Use `outputSchema` when you need typed objects instead of freeform text: - -```typescript -import { chat } from '@tanstack/ai' -import { openaiText } from '@tanstack/ai-openai' -import { z } from 'zod' - -const Person = z.object({ - name: z.string(), - age: z.number(), -}) - -const person = await chat({ - adapter: openaiText('gpt-5.2'), - messages: [{ role: 'user', content: 'Ada Lovelace, 36' }], - outputSchema: Person, -}) -``` - -Learn more in the -[Structured Outputs docs](https://tanstack.com/ai/latest/docs/structured-outputs/overview). - -## Media, Realtime, and Code Mode - -- [Generations](https://tanstack.com/ai/latest/docs/media/generations) - one - pattern for image generation, text-to-speech, transcription, summarization, - audio generation, and video generation. -- [Realtime Voice Chat](https://tanstack.com/ai/latest/docs/media/realtime-chat) - - build low-latency realtime voice experiences. -- [Code Mode](https://tanstack.com/ai/latest/docs/code-mode/code-mode) - let - models write and execute TypeScript inside a secure isolate. -- [Code Mode with Snippets](https://tanstack.com/ai/latest/docs/code-mode/code-mode-with-snippets) - - give Code Mode reusable runtime capabilities. - -## Providers - -Official adapters include: - -| Package | Use it for | -| ------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------ | -| [`@tanstack/ai-openrouter`](https://tanstack.com/ai/latest/docs/adapters/openrouter) | 300+ models through one OpenRouter API, with per-request cost tracking | -| [`@tanstack/ai-openai`](https://tanstack.com/ai/latest/docs/adapters/openai) | OpenAI chat, image, video, speech, transcription, realtime, and provider tools | -| [`@tanstack/ai-anthropic`](https://tanstack.com/ai/latest/docs/adapters/anthropic) | Anthropic Claude chat, thinking, tools, structured outputs, and Vertex Claude | -| [`@tanstack/ai-gemini`](https://tanstack.com/ai/latest/docs/adapters/gemini) | Google Gemini chat, image, speech, and audio generation | -| [`@tanstack/ai-vertex`](https://tanstack.com/ai/latest/docs/adapters/vertex) | Gemini on Vertex AI with regional endpoints and Google Cloud credentials | -| [`@tanstack/ai-ollama`](https://tanstack.com/ai/latest/docs/adapters/ollama) | Local Ollama models | -| [`@tanstack/ai-grok`](https://tanstack.com/ai/latest/docs/adapters/grok) | xAI Grok chat, images, and realtime | -| [`@tanstack/ai-groq`](https://tanstack.com/ai/latest/docs/adapters/groq) | Groq low-latency inference | -| [`@tanstack/ai-elevenlabs`](https://tanstack.com/ai/latest/docs/adapters/elevenlabs) | ElevenLabs realtime voice, speech, transcription, music, and sound effects | -| [`@tanstack/ai-byteplus`](https://tanstack.com/ai/latest/docs/adapters/byteplus) | BytePlus Seed chat, Seedance video, Seedream image, and Seed Speech TTS/ASR | -| [`@tanstack/ai-fal`](https://tanstack.com/ai/latest/docs/adapters/fal) | fal.ai image, video, audio, speech, and transcription models | - -The adapter system is tree-shakeable by activity. Import `openaiText` for chat, -`openaiImage` for images, `falVideo` for video, `geminiSpeech` for TTS, and so -on. - -## Framework Packages - -| Package | What it provides | -| -------------------------------------------------------------------------- | ------------------------------------------------------------------------ | -| [`@tanstack/ai-client`](https://tanstack.com/ai/latest/docs/api/ai-client) | Headless chat, realtime, and generation clients | -| [`@tanstack/ai-react`](https://tanstack.com/ai/latest/docs/api/ai-react) | React hooks including `useChat`, `useRealtimeChat`, and generation hooks | -| [`@tanstack/ai-solid`](https://tanstack.com/ai/latest/docs/api/ai-solid) | Solid hooks for chat and generations | -| [`@tanstack/ai-vue`](https://tanstack.com/ai/latest/docs/api/ai-vue) | Vue composables for chat and generations | -| [`@tanstack/ai-svelte`](https://tanstack.com/ai/latest/docs/api/ai-svelte) | Svelte 5 factories for chat and generations | -| [`@tanstack/ai-preact`](https://tanstack.com/ai/latest/docs/api/ai-preact) | Preact hooks for chat | -| `@tanstack/ai-react-ui`, `@tanstack/ai-solid-ui`, `@tanstack/ai-vue-ui` | Headless UI components for chat interfaces | - -## Advanced Docs - -- [Middleware](https://tanstack.com/ai/latest/docs/advanced/middleware) - hook - into chat configuration, chunks, tool calls, usage, errors, and structured - outputs. -- [OpenTelemetry](https://tanstack.com/ai/latest/docs/advanced/otel) - emit - vendor-neutral GenAI traces and metrics. -- [Observability](https://tanstack.com/ai/latest/docs/advanced/observability) - - subscribe to typed TanStack AI events. -- [Per-Model Type Safety](https://tanstack.com/ai/latest/docs/advanced/per-model-type-safety) - - narrow model options and content modalities to the selected model. -- [Runtime Adapter Switching](https://tanstack.com/ai/latest/docs/advanced/runtime-adapter-switching) - - switch providers at runtime. -- [Tree-Shaking](https://tanstack.com/ai/latest/docs/advanced/tree-shaking) - - ship only the activities and adapters you use. -- [Agent Skills](https://tanstack.com/ai/latest/docs/getting-started/agent-skills) - - install TanStack AI skills into Claude Code, Cursor, GitHub Copilot, Codex, - and other coding agents with TanStack Intent. - -## Get Involved - -- Read the [docs](https://tanstack.com/ai). -- Participate in [GitHub discussions](https://github.com/TanStack/ai/discussions). -- Chat with the community on [Discord](https://discord.com/invite/WrRKjPJ). -- See [CONTRIBUTING.md](https://github.com/TanStack/ai/blob/main/CONTRIBUTING.md) - for setup instructions. -- [Become a sponsor](https://github.com/sponsors/tannerlinsley/). - -## Partners - - - - - - -
- - - - - CodeRabbit - - - - - - - - Cloudflare - - -
- -
- AI and you? -

- We're looking for TanStack AI partners to join our mission. Partner with us - to push the boundaries of TanStack AI and build amazing things together. -

- LET'S CHAT -
- -## Explore the TanStack Ecosystem - -- TanStack Config - - tooling for JS/TS packages -- TanStack DB - reactive - sync client store -- TanStack Devtools - - unified devtools panel -- TanStack Form - - type-safe form state -- TanStack Pacer - - debouncing, throttling, batching -- TanStack Query - - async state and caching -- TanStack Ranger - - range and slider primitives -- TanStack Router - - type-safe routing, caching, and URL state -- TanStack Start - - full-stack SSR and streaming -- TanStack Store - - reactive data store -- TanStack Table - - headless datagrids -- TanStack Virtual - - virtualized rendering - -...and more at TanStack.com. +See the [migration guide](https://tanstack.com/ai/latest/docs/migration/create-ui). diff --git a/packages/ai-react-ui/package.json b/packages/ai-react-ui/package.json index a49b3d1525..a7207617a4 100644 --- a/packages/ai-react-ui/package.json +++ b/packages/ai-react-ui/package.json @@ -1,7 +1,7 @@ { "name": "@tanstack/ai-react-ui", "version": "0.8.22", - "description": "Headless React components for building TanStack AI chat interfaces with streamed message parts.", + "description": "Deprecated. Import chat UI from @tanstack/ai-react/ui instead.", "author": "Tanner Linsley", "license": "MIT", "homepage": "https://tanstack.com/ai", @@ -17,6 +17,7 @@ "type": "github", "url": "https://github.com/sponsors/tannerlinsley" }, + "deprecated": "Use @tanstack/ai-react/ui instead. See https://tanstack.com/ai/latest/docs/migration/create-ui", "module": "./dist/esm/index.js", "types": "./dist/esm/index.d.ts", "type": "module", @@ -27,7 +28,8 @@ } }, "files": [ - "dist" + "dist", + "src" ], "scripts": { "build": "vite build", @@ -40,38 +42,18 @@ }, "keywords": [ "ai", - "ai-sdk", - "typescript", "tanstack", "react", "chat", "ui", - "headless", - "components", - "streaming", - "generative-ai" + "deprecated" ], "dependencies": { - "react-markdown": "^10.1.0", - "rehype-highlight": "^7.0.2", - "rehype-raw": "^7.0.0", - "rehype-sanitize": "^6.0.0", - "remark-gfm": "^4.0.1" - }, - "peerDependencies": { - "@tanstack/ai-client": "workspace:^", - "@tanstack/ai-react": "workspace:^", - "@types/react": ">=18.0.0", - "react": "^18.0.0 || ^19.0.0", - "react-dom": "^18.0.0 || ^19.0.0" + "@tanstack/ai-react": "workspace:^" }, "devDependencies": { - "@tanstack/ai-client": "workspace:*", - "@tanstack/ai-react": "workspace:*", "@types/react": "^19.2.7", - "@vitest/coverage-v8": "4.1.10", "react": "^19.2.3", - "react-dom": "^19.2.3", "vite": "^8.2.1" } } diff --git a/packages/ai-react-ui/src/create-ui.tsx b/packages/ai-react-ui/src/create-ui.tsx deleted file mode 100644 index eacbbb76eb..0000000000 --- a/packages/ai-react-ui/src/create-ui.tsx +++ /dev/null @@ -1,468 +0,0 @@ -import { createContext, memo, useCallback, useContext, useMemo } from 'react' -import type { ComponentType, ReactNode } from 'react' -import { - automaticPartsForMessage, - collectInlineToolNames, - resolveInterruptComponent, - selectChatUI, - selectMessageUI, -} from '@tanstack/ai-client/ui' -import type { - ChatUIData, - ChatUIHasNamedInterrupts, - ChatUIHasNamedTools, - ChatUIInterrupt, - ChatUIInterruptName, - ChatUIInterruptOf, - ChatUIInterruptsOf, - ChatUIMessages, - ChatUINamedInterruptId, - ChatUIPartKey, - ChatUIPartOf, - ChatUISchemaOf, - ChatUISelectedPart, - ChatUIToolApproval, - ChatUIToolName, - ChatUIToolsOf, -} from '@tanstack/ai-client/ui' -import type { - MessagePart, - ToolCallPart, - ToolResultPart, - UIMessage, -} from '@tanstack/ai-client' -import type { UseChatReturn } from '@tanstack/ai-react' - -export type ChatUIHost = UseChatReturn< - ChatUIToolsOf, - ChatUISchemaOf, - ChatUIInterruptsOf -> - -export type LayoutProps = { - renderMessages: () => ReactNode - renderInterrupts: () => ReactNode - renderInput: () => ReactNode - readonly __ui?: TOptions -} - -export type MessageProps = { - message: UIMessage, ChatUIData> - renderParts: () => ReactNode -} - -export type InputProps = { - readonly __ui?: TOptions -} - -export type PartProps = { - part: ChatUIPartOf -} - -export type ToolProps< - TOptions, - TName extends ChatUIToolName = ChatUIToolName, -> = { - part: Extract>, { name: TName }> - result?: ToolResultPart - interrupt?: ChatUIToolApproval -} - -export type InterruptProps< - TOptions, - TName extends ChatUIInterruptName = never, -> = { - interrupt: ChatUIInterruptOf - readonly __ui?: TOptions -} - -type GenericInterruptComponents = - ChatUIHasNamedInterrupts extends true - ? { - [K in ChatUINamedInterruptId]: ComponentType< - InterruptProps> - > - } & { - fallback?: ComponentType> - } - : { - fallback?: ComponentType> - } - -type ToolApprovalMap = { - [K in ChatUIToolName]?: ComponentType< - InterruptProps> - > -} - -export type ChatUIComponents = { - layout: ComponentType> - message: ComponentType> - input?: ComponentType> - parts: { - [K in ChatUIPartKey]?: ComponentType> - } & { - fallback?: ComponentType> - } -} & (ChatUIHasNamedTools extends true - ? { - tools: { - [K in ChatUIToolName]: ComponentType> - } - } - : { - tools?: { - [K in ChatUIToolName]?: ComponentType> - } - }) & - (ChatUIHasNamedInterrupts extends true - ? { - interrupts: { - tools?: ToolApprovalMap - generic: GenericInterruptComponents - } - } - : { - interrupts?: { - tools?: ToolApprovalMap - generic?: GenericInterruptComponents - } - }) - -type ComponentsValue = { - components: ChatUIComponents - warn: (key: string, message: string) => void - inlineToolNames: ReadonlyArray -} - -function createWarnOnce() { - const seen = new Set() - return (key: string, message: string) => { - if (process.env.NODE_ENV === 'production') return - if (seen.has(key)) return - seen.add(key) - console.warn(message) - } -} - -function readMessages(chat: ChatUIHost) { - return chat.messages as ChatUIMessages -} - -function readInterrupts(chat: ChatUIHost) { - return chat.interrupts ?? [] -} - -function selectedPartPropsEqual( - prev: { selected: ChatUISelectedPart }, - next: { selected: ChatUISelectedPart }, -) { - if (prev.selected.key !== next.selected.key) return false - if (prev.selected.part !== next.selected.part) return false - if (prev.selected.key === 'toolCall' && next.selected.key === 'toolCall') { - return ( - prev.selected.result === next.selected.result && - prev.selected.interrupt === next.selected.interrupt - ) - } - if ( - prev.selected.key === 'toolResult' && - next.selected.key === 'toolResult' - ) { - return prev.selected.matched === next.selected.matched - } - return true -} - -export function createChatUI(options: TOptions) { - void options - const warn = createWarnOnce() - const ChatContext = createContext | null>(null) - const ComponentsContext = createContext | null>( - null, - ) - - function useChat() { - const chat = useContext(ChatContext) - if (!chat) { - throw new Error( - 'Chat UI components must be wrapped in UI.Provider or UI.Chat.', - ) - } - return chat - } - - function useComponents() { - const value = useContext(ComponentsContext) - if (!value) { - throw new Error( - 'Chat UI components must be wrapped in UI.Provider or UI.Chat.', - ) - } - return value - } - - function defineComponents( - components: ChatUIComponents, - ): ChatUIComponents { - return components - } - - function inlineNames(components: ChatUIComponents) { - return collectInlineToolNames( - components.interrupts?.tools as Record | undefined, - Object.keys(components.tools ?? {}), - ) - } - - function Provider({ - chat, - components, - children, - }: { - chat: ChatUIHost - components: ChatUIComponents - children?: ReactNode - }) { - const componentsValue = useMemo( - () => ({ - components, - warn, - inlineToolNames: inlineNames(components), - }), - [components], - ) - return ( - - {children} - - ) - } - - function Chat({ - chat, - components, - }: { - chat: ChatUIHost - components: ChatUIComponents - }) { - const Layout = components.layout - const renderMessages = useCallback(() => , []) - const renderInterrupts = useCallback(() => , []) - const renderInput = useCallback(() => { - const Input = components.input - return Input ? : null - }, [components.input]) - return ( - - - - ) - } - - function Messages({ - children, - }: { - children?: (messages: ChatUIMessages) => ReactNode - } = {}) { - const chat = useChat() - const { inlineToolNames } = useComponents() - const messages = readMessages(chat) - const interrupts = readInterrupts(chat) - if (children) return <>{children(messages)} - return ( - <> - {messages.map((message) => ( - - ))} - - ) - } - - const MessageView = memo(function MessageView({ - message, - interrupts, - inlineToolNames, - children, - }: { - message: ChatUIMessages[number] - interrupts: ReadonlyArray - inlineToolNames: ReadonlyArray - children?: (parts: Array) => ReactNode - }) { - const { components } = useComponents() - const selected = selectMessageUI(message, { - interrupts, - inlineToolNames, - }) - if (children) return <>{children(selected.parts)} - const MessageComponent = components.message - return ( - ( - - )} - /> - ) - }) - - function Message({ - message, - children, - }: { - message: ChatUIMessages[number] - children?: (parts: Array) => ReactNode - }) { - const chat = useChat() - const { inlineToolNames } = useComponents() - return ( - - ) - } - - const AutomaticParts = memo(function AutomaticParts({ - message, - interrupts, - inlineToolNames, - }: { - message: ChatUIMessages[number] - interrupts: ReadonlyArray - inlineToolNames: ReadonlyArray - }) { - const selected = selectMessageUI(message, { - interrupts, - inlineToolNames, - }) - return ( - <> - {automaticPartsForMessage(selected).map((part, index) => ( - - ))} - - ) - }) - - const SelectedPartView = memo(function SelectedPartView({ - selected, - }: { - selected: ChatUISelectedPart - }) { - const { components, warn: warnMissing } = useComponents() - if (selected.key === 'toolCall') { - const name = selected.part.name - const Tool = components.tools?.[name as ChatUIToolName] as - | ComponentType> - | undefined - if (!Tool) { - warnMissing( - `tool:${name}`, - `[tanstack-ai-ui] Missing tools.${name} component`, - ) - return null - } - return ( - ['part']} - result={selected.result} - interrupt={selected.interrupt as ToolProps['interrupt']} - /> - ) - } - - const PartComponent = (components.parts[selected.key] ?? - components.parts.fallback) as - | ComponentType> - | undefined - if (!PartComponent) { - warnMissing( - `part:${selected.key}`, - `[tanstack-ai-ui] Missing parts.${selected.key} component`, - ) - return null - } - return ['part']} /> - }, selectedPartPropsEqual) - - function Part({ part }: { part: MessagePart }) { - const chat = useChat() - const selected = selectMessageUI( - { id: 'part', role: 'assistant', parts: [part] }, - { interrupts: readInterrupts(chat), inlineToolNames: [] }, - ).parts[0] - if (!selected) return null - return - } - - function Interrupts({ - children, - }: { - children?: (interrupts: ReadonlyArray) => ReactNode - } = {}) { - const chat = useChat() - const { inlineToolNames } = useComponents() - const selected = selectChatUI({ - messages: readMessages(chat), - interrupts: readInterrupts(chat), - inlineToolNames, - }) - if (children) return <>{children(selected.interrupts)} - return ( - <> - {selected.interrupts.map((interrupt) => ( - - ))} - - ) - } - - const Interrupt = memo(function Interrupt({ - interrupt, - }: { - interrupt: ChatUIInterrupt - }) { - const { components, warn: warnMissing } = useComponents() - const Component = resolveInterruptComponent( - interrupt, - components.interrupts, - ) as ComponentType> | undefined - if (!Component) { - warnMissing( - `interrupt:${interrupt.id}`, - `[tanstack-ai-ui] Missing interrupt component for ${interrupt.kind}`, - ) - return null - } - return - }) - - return { - Chat, - Provider, - Messages, - Message, - Part, - Interrupts, - Interrupt, - defineComponents, - useChat, - } -} diff --git a/packages/ai-react-ui/src/index.ts b/packages/ai-react-ui/src/index.ts index 7d5633f8bf..033d0f45d8 100644 --- a/packages/ai-react-ui/src/index.ts +++ b/packages/ai-react-ui/src/index.ts @@ -1,73 +1,6 @@ /** - * @tanstack/ai-react-ui - * - * Headless React components for building AI chat interfaces. - * - * Features: - * - Parts-based message rendering (text, tool calls, tool results) - * - Native tool approval workflows - * - Client-side tool execution support - * - Streaming support - * - Fully customizable with render props - * - Compound component pattern - * - * @example - * ```tsx - * import { Chat, ChatMessages, ChatInput, ChatMessage } from '@tanstack/ai-react-ui' - * - * - * - * {(message) => } - * - * - * - * ``` + * @deprecated Since 0.9.0. Import from `@tanstack/ai-react/ui` instead. + * See https://tanstack.com/ai/latest/docs/migration/create-ui + * Removed in 1.0.0. */ - -export { - createChatUI, - type ChatUIComponents, - type ChatUIHost, - type InputProps, - type InterruptProps, - type LayoutProps, - type MessageProps, - type PartProps, - type ToolProps, -} from './create-ui' - -// Main components -export { Chat, useChatContext, type ChatProps } from './chat' -export { ChatMessages, type ChatMessagesProps } from './chat-messages' -export { - ChatMessage, - type ChatMessageProps, - type ToolCallRenderProps, -} from './chat-message' -export { - ChatInput, - type ChatInputProps, - type ChatInputRenderProps, -} from './chat-input' -export { - ToolApproval, - type ToolApprovalProps, - type ToolApprovalRenderProps, -} from './tool-approval' -export { TextPart, type TextPartProps } from './text-part' -export { ThinkingPart, type ThinkingPartProps } from './thinking-part' - -// Re-export hooks from @tanstack/ai-react for convenience -export { useChat } from '@tanstack/ai-react' - -// Re-export types from @tanstack/ai-react -export type { - UIMessage, - MessagePart, - ToolCallPart, - ToolResultPart, - TextPart as TextPartType, - ConnectionAdapter, -} from '@tanstack/ai-client' - -export type { UseChatOptions, UseChatReturn } from '@tanstack/ai-react' +export * from '@tanstack/ai-react/ui' diff --git a/packages/ai-react-ui/tests/coexistence.test.ts b/packages/ai-react-ui/tests/coexistence.test.ts deleted file mode 100644 index 4c9c6cb332..0000000000 --- a/packages/ai-react-ui/tests/coexistence.test.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { describe, expect, it } from 'vitest' -import { - Chat, - ChatInput, - ChatMessage, - ChatMessages, - TextPart, - ThinkingPart, - createChatUI, -} from '../src' - -describe('public coexistence', () => { - it('exports old and new APIs before 1.0', () => { - expect(Chat).toBeDefined() - expect(ChatInput).toBeDefined() - expect(ChatMessage).toBeDefined() - expect(ChatMessages).toBeDefined() - expect(TextPart).toBeDefined() - expect(ThinkingPart).toBeDefined() - expect(createChatUI).toBeDefined() - }) -}) diff --git a/packages/ai-react-ui/tests/create-ui.test.tsx b/packages/ai-react-ui/tests/create-ui.test.tsx deleted file mode 100644 index 845cad4e40..0000000000 --- a/packages/ai-react-ui/tests/create-ui.test.tsx +++ /dev/null @@ -1,337 +0,0 @@ -import { renderToStaticMarkup } from 'react-dom/server' -import { describe, expect, it, vi } from 'vitest' -import { createChatUI } from '../src/create-ui' -import type { ChatUIHost } from '../src/create-ui' -import { - chatOptions, - createChatResult, - genericInterrupt, - messageWithToolResults, - orphanResultMessage, - purchaseApprovalInterrupt, - purchaseApprovalMessage, - unboundInterrupt, - unknownToolMessage, -} from '../../ai-client/tests/ui-fixtures' -import type { ToolCallState } from '@tanstack/ai-client' - -function host( - init?: Parameters[0], -): ChatUIHost { - return createChatResult(init ?? {}) as unknown as ChatUIHost< - typeof chatOptions - > -} - -describe('createChatUI', () => { - it('renders automatic and manual trees', () => { - const UI = createChatUI(chatOptions) - const components = UI.defineComponents({ - layout: ({ renderMessages, renderInterrupts }) => ( - <> - {renderMessages()} - {renderInterrupts()} - - ), - message: ({ renderParts }) =>
{renderParts()}
, - parts: { fallback: ({ part }) => {part.type} }, - tools: { - getWeather: ({ part }) => {part.input?.city}, - purchaseItem: () => null, - }, - interrupts: { - generic: { - choosePlan: () => null, - fallback: ({ interrupt }) => {interrupt.reason}, - }, - }, - }) - - const chat = host({ messages: [messageWithToolResults] }) - const automatic = renderToStaticMarkup( - , - ) - expect(automatic).toContain('Paris') - - const manual = renderToStaticMarkup( - - - {(messages) => {messages.length}} - - , - ) - expect(manual).toContain('1') - }) - - it('warns once for a missing runtime key', () => { - const warn = vi.spyOn(console, 'warn').mockImplementation(() => undefined) - const UI = createChatUI(chatOptions) - const chat = host({ messages: [unknownToolMessage] }) - const components = UI.defineComponents({ - layout: ({ renderMessages }) => renderMessages(), - message: ({ renderParts }) => renderParts(), - parts: { fallback: () => null }, - tools: { - getWeather: () => null, - purchaseItem: () => null, - }, - interrupts: { - generic: { choosePlan: () => null, fallback: () => null }, - }, - }) - renderToStaticMarkup() - renderToStaticMarkup() - expect(warn).toHaveBeenCalledTimes(1) - warn.mockRestore() - }) - - it('keeps unmatched tool results and suppresses matched ones', () => { - const UI = createChatUI(chatOptions) - const components = UI.defineComponents({ - layout: ({ renderMessages }) => renderMessages(), - message: ({ renderParts }) => renderParts(), - parts: { - toolResult: ({ part }) => - part.type === 'tool-result' ? {String(part.content)} : null, - fallback: () => null, - }, - tools: { - getWeather: () => weather, - purchaseItem: () => null, - }, - interrupts: { - generic: { choosePlan: () => null, fallback: () => null }, - }, - }) - - const matched = renderToStaticMarkup( - , - ) - expect(matched).toContain('weather') - expect(matched).not.toContain('') - - const unmatched = renderToStaticMarkup( - , - ) - expect(unmatched).toContain('standalone') - }) - - it('puts list approvals in Interrupts when interrupts.tools has the tool', () => { - const UI = createChatUI(chatOptions) - const list = UI.defineComponents({ - layout: ({ renderMessages, renderInterrupts }) => ( - <> - {renderMessages()} - {renderInterrupts()} - - ), - message: ({ renderParts }) => renderParts(), - parts: { fallback: () => null }, - tools: { - getWeather: () => null, - purchaseItem: () =>
tool
, - }, - interrupts: { - tools: { - purchaseItem: () => list-approval, - }, - generic: { choosePlan: () => null, fallback: () => null }, - }, - }) - - const listMarkup = renderToStaticMarkup( - , - ) - expect(listMarkup).toContain('list-approval') - expect(listMarkup).toContain('tool') - }) - - it('lets a tool render its approval from interrupt without a registered interrupt component', () => { - const warn = vi.spyOn(console, 'warn').mockImplementation(() => undefined) - const UI = createChatUI(chatOptions) - const components = UI.defineComponents({ - layout: ({ renderMessages, renderInterrupts }) => ( - <> - {renderMessages()} - {renderInterrupts()} - - ), - message: ({ renderParts }) => renderParts(), - parts: { fallback: () => null }, - tools: { - getWeather: () => null, - purchaseItem: ({ interrupt }) => - interrupt ? {interrupt.toolName} : tool, - }, - interrupts: { - generic: { choosePlan: () => null, fallback: () => list }, - }, - }) - const markup = renderToStaticMarkup( - , - ) - expect(markup).toContain('purchaseItem') - expect(markup).not.toContain('list') - expect( - warn.mock.calls.filter((call) => - String(call[0]).includes('[tanstack-ai-ui]'), - ), - ).toHaveLength(0) - warn.mockRestore() - }) - - it('renders registered generic interrupts and sends the rest to fallback', () => { - const UI = createChatUI(chatOptions) - const components = UI.defineComponents({ - layout: ({ renderInterrupts }) => renderInterrupts(), - message: ({ renderParts }) => renderParts(), - parts: { fallback: () => null }, - tools: { - getWeather: () => null, - purchaseItem: () => null, - }, - interrupts: { - generic: { - choosePlan: () => plan, - fallback: () => fallback, - }, - }, - }) - - const markup = renderToStaticMarkup( - , - ) - expect(markup).toContain('plan') - expect(markup).toContain('fallback') - }) - - it('omits input when no input component exists', () => { - const UI = createChatUI(chatOptions) - const components = UI.defineComponents({ - layout: ({ renderInput }) =>
{renderInput()}
, - message: ({ renderParts }) => renderParts(), - parts: { fallback: () => null }, - tools: { - getWeather: () => null, - purchaseItem: () => null, - }, - interrupts: { - generic: { choosePlan: () => null }, - }, - }) - const markup = renderToStaticMarkup( - , - ) - expect(markup).toBe('
') - }) - - it('reads chat from nested provider context and throws outside a provider', () => { - const UI = createChatUI(chatOptions) - const components = UI.defineComponents({ - layout: () => { - const chat = UI.useChat() - return

{chat.messages.length}

- }, - message: ({ renderParts }) => renderParts(), - parts: { fallback: () => null }, - tools: { - getWeather: () => null, - purchaseItem: () => null, - }, - interrupts: { - generic: { choosePlan: () => null }, - }, - }) - - const inner = host({ messages: [messageWithToolResults] }) - const outer = host({ messages: [] }) - const markup = renderToStaticMarkup( - - - , - ) - expect(markup).toContain('

1

') - - function Broken() { - UI.useChat() - return null - } - expect(() => renderToStaticMarkup()).toThrow( - /UI.Provider or UI.Chat/, - ) - }) - - it('renders a tool component for every ToolCallState', () => { - const states: Array = [ - 'awaiting-input', - 'input-streaming', - 'input-complete', - 'approval-requested', - 'approval-responded', - 'complete', - 'error', - ] - const UI = createChatUI(chatOptions) - const components = UI.defineComponents({ - layout: ({ renderMessages }) => renderMessages(), - message: ({ renderParts }) => renderParts(), - parts: { fallback: () => null }, - tools: { - getWeather: ({ part }) => {part.state}, - purchaseItem: () => null, - }, - interrupts: { - generic: { choosePlan: () => null, fallback: () => null }, - }, - }) - - for (const state of states) { - const markup = renderToStaticMarkup( - , - ) - expect(markup).toContain(`${state}`) - } - }) -}) diff --git a/packages/ai-react-ui/tsconfig.json b/packages/ai-react-ui/tsconfig.json index d3e610bff5..aae0726181 100644 --- a/packages/ai-react-ui/tsconfig.json +++ b/packages/ai-react-ui/tsconfig.json @@ -6,6 +6,6 @@ "lib": ["ES2022", "DOM"], "rootDir": "../.." }, - "include": ["src", "tests"], + "include": ["src"], "exclude": ["node_modules", "dist"] } diff --git a/packages/ai-react-ui/vite.config.ts b/packages/ai-react-ui/vite.config.ts index 782c82ec49..190315c4bf 100644 --- a/packages/ai-react-ui/vite.config.ts +++ b/packages/ai-react-ui/vite.config.ts @@ -9,20 +9,8 @@ const config = defineConfig({ watch: false, globals: true, environment: 'node', - include: ['tests/**/*.test.ts', 'tests/**/*.test.tsx'], - coverage: { - provider: 'v8', - reporter: ['text', 'json', 'html', 'lcov'], - exclude: [ - 'node_modules/', - 'dist/', - 'tests/', - '**/*.test.ts', - '**/*.config.ts', - '**/types.ts', - ], - include: ['src/**/*.ts'], - }, + include: ['tests/**/*.test.ts'], + passWithNoTests: true, }, }) diff --git a/packages/ai-react/README.md b/packages/ai-react/README.md index c9bbb8c10d..d524f36907 100644 --- a/packages/ai-react/README.md +++ b/packages/ai-react/README.md @@ -226,15 +226,15 @@ on. ## Framework Packages -| Package | What it provides | -| -------------------------------------------------------------------------- | ------------------------------------------------------------------------ | -| [`@tanstack/ai-client`](https://tanstack.com/ai/latest/docs/api/ai-client) | Headless chat, realtime, and generation clients | -| [`@tanstack/ai-react`](https://tanstack.com/ai/latest/docs/api/ai-react) | React hooks including `useChat`, `useRealtimeChat`, and generation hooks | -| [`@tanstack/ai-solid`](https://tanstack.com/ai/latest/docs/api/ai-solid) | Solid hooks for chat and generations | -| [`@tanstack/ai-vue`](https://tanstack.com/ai/latest/docs/api/ai-vue) | Vue composables for chat and generations | -| [`@tanstack/ai-svelte`](https://tanstack.com/ai/latest/docs/api/ai-svelte) | Svelte 5 factories for chat and generations | -| [`@tanstack/ai-preact`](https://tanstack.com/ai/latest/docs/api/ai-preact) | Preact hooks for chat | -| `@tanstack/ai-react-ui`, `@tanstack/ai-solid-ui`, `@tanstack/ai-vue-ui` | Headless UI components for chat interfaces | +| Package | What it provides | +| ------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | +| [`@tanstack/ai-client`](https://tanstack.com/ai/latest/docs/api/ai-client) | Headless chat, realtime, and generation clients | +| [`@tanstack/ai-react`](https://tanstack.com/ai/latest/docs/api/ai-react) | React hooks including `useChat`, `useRealtimeChat`, and generation hooks | +| [`@tanstack/ai-solid`](https://tanstack.com/ai/latest/docs/api/ai-solid) | Solid hooks for chat and generations | +| [`@tanstack/ai-vue`](https://tanstack.com/ai/latest/docs/api/ai-vue) | Vue composables for chat and generations | +| [`@tanstack/ai-svelte`](https://tanstack.com/ai/latest/docs/api/ai-svelte) | Svelte 5 factories for chat and generations | +| [`@tanstack/ai-preact`](https://tanstack.com/ai/latest/docs/api/ai-preact) | Preact hooks for chat | +| `@tanstack/ai-react/ui`, `@tanstack/ai-solid/ui`, `@tanstack/ai-vue/ui`, `@tanstack/ai-svelte/ui` | Headless UI components for chat interfaces. `@tanstack/ai-react-ui`, `@tanstack/ai-solid-ui`, and `@tanstack/ai-vue-ui` are deprecated. | ## Advanced Docs diff --git a/packages/ai-react/package.json b/packages/ai-react/package.json index abe8955b9b..2f7d2282c3 100644 --- a/packages/ai-react/package.json +++ b/packages/ai-react/package.json @@ -28,6 +28,10 @@ "./mcp-apps": { "types": "./dist/esm/mcp-apps.d.ts", "import": "./dist/esm/mcp-apps.js" + }, + "./ui": { + "types": "./dist/esm/ui.d.ts", + "import": "./dist/esm/ui.js" } }, "files": [ @@ -59,17 +63,26 @@ "media-generation" ], "dependencies": { - "@tanstack/ai-client": "workspace:^" + "@tanstack/ai-client": "workspace:^", + "react-markdown": "^10.1.0", + "rehype-highlight": "^7.0.2", + "rehype-raw": "^7.0.0", + "rehype-sanitize": "^6.0.0", + "remark-gfm": "^4.0.1" }, "peerDependencies": { "@mcp-ui/client": "^7", "@tanstack/ai": "workspace:^", "@types/react": ">=18.0.0", - "react": ">=18.0.0" + "react": ">=18.0.0", + "react-dom": ">=18.0.0" }, "peerDependenciesMeta": { "@mcp-ui/client": { "optional": true + }, + "react-dom": { + "optional": true } }, "devDependencies": { @@ -81,6 +94,7 @@ "@vitest/coverage-v8": "4.1.10", "jsdom": "^27.4.0", "react": "^19.2.3", + "react-dom": "^19.2.3", "vite": "^8.2.1" } } diff --git a/packages/ai-react-ui/src/chat-input.tsx b/packages/ai-react/src/chat-ui/chat-input.tsx similarity index 100% rename from packages/ai-react-ui/src/chat-input.tsx rename to packages/ai-react/src/chat-ui/chat-input.tsx diff --git a/packages/ai-react-ui/src/chat-message.tsx b/packages/ai-react/src/chat-ui/chat-message.tsx similarity index 99% rename from packages/ai-react-ui/src/chat-message.tsx rename to packages/ai-react/src/chat-ui/chat-message.tsx index 8df82253cf..41dd183161 100644 --- a/packages/ai-react-ui/src/chat-message.tsx +++ b/packages/ai-react/src/chat-ui/chat-message.tsx @@ -1,7 +1,7 @@ import { ThinkingPart } from './thinking-part' import { toolResultContentToString } from './tool-result-content' import type { ReactNode } from 'react' -import type { UIMessage } from '@tanstack/ai-react' +import type { UIMessage } from '../types' export interface ToolCallRenderProps { id: string diff --git a/packages/ai-react-ui/src/chat-messages.tsx b/packages/ai-react/src/chat-ui/chat-messages.tsx similarity index 97% rename from packages/ai-react-ui/src/chat-messages.tsx rename to packages/ai-react/src/chat-ui/chat-messages.tsx index e05526442e..f87ea034da 100644 --- a/packages/ai-react-ui/src/chat-messages.tsx +++ b/packages/ai-react/src/chat-ui/chat-messages.tsx @@ -2,7 +2,7 @@ import { useEffect, useRef } from 'react' import { useChatContext } from './chat' import { ChatMessage } from './chat-message' import type { ReactNode } from 'react' -import type { UIMessage } from '@tanstack/ai-react' +import type { UIMessage } from '../types' /** @deprecated Use `createChatUI()` Messages instead. Deprecated in 0.9.0. Removed in 1.0.0. */ export interface ChatMessagesProps { diff --git a/packages/ai-react-ui/src/chat.tsx b/packages/ai-react/src/chat-ui/chat.tsx similarity index 87% rename from packages/ai-react-ui/src/chat.tsx rename to packages/ai-react/src/chat-ui/chat.tsx index 7b78b872af..463fa2d20f 100644 --- a/packages/ai-react-ui/src/chat.tsx +++ b/packages/ai-react/src/chat-ui/chat.tsx @@ -1,11 +1,8 @@ import { createContext, useContext } from 'react' -import { useChat } from '@tanstack/ai-react' +import type { ConnectionAdapter } from '@tanstack/ai-client' +import { useChat } from '../use-chat' import type { ReactNode } from 'react' -import type { - ConnectionAdapter, - UIMessage, - UseChatReturn, -} from '@tanstack/ai-react' +import type { UIMessage, UseChatReturn } from '../types' /** * Chat context - provides chat state to all child components @@ -16,7 +13,7 @@ const ChatContext = createContext(null) * Hook to access chat context * @throws Error if used outside of Chat component */ -/** @deprecated Use `createChatUI().useChat()` instead. Deprecated in 0.9.0. Removed in 1.0.0. */ +/** @deprecated Use `createChatUI().useChatContext()` instead. Deprecated in 0.9.0. Removed in 1.0.0. */ export function useChatContext() { const context = useContext(ChatContext) if (!context) { @@ -54,9 +51,9 @@ export interface ChatProps { } /** - * @deprecated Use `createChatUI()` from `@tanstack/ai-react-ui` instead. + * @deprecated Since 0.9.0. Use `Chat` from `@tanstack/ai-react/ui` with `useChat` from `@tanstack/ai-react`. * See https://tanstack.com/ai/latest/docs/migration/create-ui - * Deprecated in 0.9.0. Removed in 1.0.0. + * Removed in 1.0.0. * * Root Chat component - provides context for all chat subcomponents * diff --git a/packages/ai-react/src/chat-ui/create-ui.tsx b/packages/ai-react/src/chat-ui/create-ui.tsx new file mode 100644 index 0000000000..07379a8990 --- /dev/null +++ b/packages/ai-react/src/chat-ui/create-ui.tsx @@ -0,0 +1,304 @@ +import { createContext, memo, useContext } from 'react' +import type { ComponentType, ReactNode } from 'react' +import { + automaticPartsForMessage, + collectInlineToolNames, + resolveInterruptComponent, + selectChatUI, + selectMessageUI, +} from '@tanstack/ai-client/ui' +import type { + ChatUIData, + ChatUIHasNamedInterrupts, + ChatUIHasNamedTools, + ChatUIInterruptName, + ChatUIInterruptOf, + ChatUIInterruptsOf, + ChatUIMessages, + ChatUINamedInterruptId, + ChatUIPartKey, + ChatUIPartOf, + ChatUISchemaOf, + ChatUISelectedPart, + ChatUIToolApproval, + ChatUIToolName, + ChatUIToolsOf, +} from '@tanstack/ai-client/ui' +import type { + ToolCallPart, + ToolResultPart, + UIMessage, +} from '@tanstack/ai-client' +import type { UseChatReturn } from '../types' + +export type ChatUIHost = UseChatReturn< + ChatUIToolsOf, + ChatUISchemaOf, + ChatUIInterruptsOf +> + +export type LayoutProps = { + chat: ChatUIHost + renderMessages: () => ReactNode + renderInterrupts: () => ReactNode + renderInput: () => ReactNode +} + +export type MessageProps = { + chat: ChatUIHost + message: UIMessage, ChatUIData> + renderParts: () => ReactNode +} + +export type InputProps = { + chat: ChatUIHost +} + +export type PartProps = { + part: ChatUIPartOf +} + +export type ToolProps< + TOptions, + TName extends ChatUIToolName = ChatUIToolName, +> = { + part: Extract>, { name: TName }> + result?: ToolResultPart + interrupt?: ChatUIToolApproval +} + +export type InterruptProps< + TOptions, + TName extends ChatUIInterruptName = never, +> = { + interrupt: ChatUIInterruptOf +} + +type GenericInterruptComponents = + ChatUIHasNamedInterrupts extends true + ? { + [K in ChatUINamedInterruptId]: ComponentType< + InterruptProps> + > + } & { + fallback?: ComponentType> + } + : { + fallback?: ComponentType> + } + +type ToolApprovalMap = { + [K in ChatUIToolName]?: ComponentType< + InterruptProps> + > +} + +export type ChatUIComponents = { + layout: ComponentType> + message: ComponentType> + input?: ComponentType> + parts: { + [K in ChatUIPartKey]?: ComponentType> + } & { + fallback?: ComponentType> + } +} & (ChatUIHasNamedTools extends true + ? { + tools: { + [K in ChatUIToolName]: ComponentType> + } + } + : { + tools?: { + [K in ChatUIToolName]?: ComponentType> + } + }) & + (ChatUIHasNamedInterrupts extends true + ? { + interrupts: { + tools?: ToolApprovalMap + generic: GenericInterruptComponents + } + } + : { + interrupts?: { + tools?: ToolApprovalMap + generic?: GenericInterruptComponents + } + }) + +type ChatTree = { + chat: ChatUIHost + components: ChatUIComponents + warn: (key: string, message: string) => void + inlineToolNames: Array +} + +const ChatTreeContext = createContext(null) + +const warnedKeys = new Set() +function warnMissing(key: string, message: string) { + if (warnedKeys.has(key)) return + warnedKeys.add(key) + console.warn(message) +} + +function useChatTree() { + const tree = useContext(ChatTreeContext) + if (!tree) { + throw new Error('`useChatContext` must be used within `Chat`.') + } + return tree +} + +export function useChatContext(): ChatUIHost { + return useChatTree().chat +} + +function selectedPartPropsEqual( + prev: { selected: ChatUISelectedPart }, + next: { selected: ChatUISelectedPart }, +) { + if (prev.selected.key !== next.selected.key) return false + if (prev.selected.part !== next.selected.part) return false + if (prev.selected.key === 'toolCall' && next.selected.key === 'toolCall') { + return ( + prev.selected.result === next.selected.result && + prev.selected.interrupt === next.selected.interrupt + ) + } + if ( + prev.selected.key === 'toolResult' && + next.selected.key === 'toolResult' + ) { + return prev.selected.matched === next.selected.matched + } + return true +} + +const SelectedPartInner = memo(function SelectedPartInner({ + selected, +}: { + selected: ChatUISelectedPart +}) { + const { components, warn } = useChatTree() + if (selected.key === 'toolCall') { + const name = selected.part.name + const Tool = components.tools?.[name as never] as + | ComponentType> + | undefined + if (!Tool) { + warn(`tool:${name}`, `[tanstack-ai-ui] Missing tools.${name} component`) + return null + } + return ( + ['interrupt']} + /> + ) + } + + const PartComponent = (components.parts[selected.key] ?? + components.parts.fallback) as ComponentType> | undefined + if (!PartComponent) { + warn( + `part:${selected.key}`, + `[tanstack-ai-ui] Missing parts.${selected.key} component`, + ) + return null + } + return ['part']} /> +}, selectedPartPropsEqual) + +function AutomaticParts({ message }: { message: ChatUIMessages[number] }) { + const { chat, inlineToolNames } = useChatTree() + const selected = selectMessageUI(message, { + interrupts: chat.interrupts ?? [], + inlineToolNames, + }) + return ( + <> + {automaticPartsForMessage(selected).map((part, index) => ( + + ))} + + ) +} + +function Messages() { + const { chat, components } = useChatTree() + const MessageComponent = components.message + return ( + <> + {(chat.messages as ChatUIMessages).map((message) => ( + } + /> + ))} + + ) +} + +function Interrupts() { + const { chat, components, warn, inlineToolNames } = useChatTree() + const selected = selectChatUI({ + messages: chat.messages, + interrupts: chat.interrupts ?? [], + inlineToolNames, + }) + return ( + <> + {selected.interrupts.map((interrupt) => { + const Component = resolveInterruptComponent( + interrupt, + components.interrupts, + ) as ComponentType> | undefined + if (!Component) { + warn( + `interrupt:${interrupt.id}`, + `[tanstack-ai-ui] Missing interrupt component for ${interrupt.kind}`, + ) + return null + } + return + })} + + ) +} + +export function Chat({ + chat, + components, +}: { + chat: ChatUIHost + components: ChatUIComponents +}) { + const tools = (components as ChatUIComponents).tools + const inlineToolNames = collectInlineToolNames( + components.interrupts?.tools as Record | undefined, + Object.keys(tools ?? {}), + ) + const Layout = components.layout + const Input = components.input + const tree: ChatTree = { + chat: chat as ChatUIHost, + components: components as ChatUIComponents, + warn: warnMissing, + inlineToolNames, + } + return ( + + } + renderInterrupts={() => } + renderInput={() => (Input ? : null)} + /> + + ) +} diff --git a/packages/ai-react-ui/src/markdown-plugins.ts b/packages/ai-react/src/chat-ui/markdown-plugins.ts similarity index 100% rename from packages/ai-react-ui/src/markdown-plugins.ts rename to packages/ai-react/src/chat-ui/markdown-plugins.ts diff --git a/packages/ai-react-ui/src/text-part.tsx b/packages/ai-react/src/chat-ui/text-part.tsx similarity index 100% rename from packages/ai-react-ui/src/text-part.tsx rename to packages/ai-react/src/chat-ui/text-part.tsx diff --git a/packages/ai-react-ui/src/thinking-part.tsx b/packages/ai-react/src/chat-ui/thinking-part.tsx similarity index 100% rename from packages/ai-react-ui/src/thinking-part.tsx rename to packages/ai-react/src/chat-ui/thinking-part.tsx diff --git a/packages/ai-react-ui/src/tool-approval.tsx b/packages/ai-react/src/chat-ui/tool-approval.tsx similarity index 100% rename from packages/ai-react-ui/src/tool-approval.tsx rename to packages/ai-react/src/chat-ui/tool-approval.tsx diff --git a/packages/ai-react-ui/src/tool-result-content.ts b/packages/ai-react/src/chat-ui/tool-result-content.ts similarity index 95% rename from packages/ai-react-ui/src/tool-result-content.ts rename to packages/ai-react/src/chat-ui/tool-result-content.ts index a6d5659f3a..8825b51ad9 100644 --- a/packages/ai-react-ui/src/tool-result-content.ts +++ b/packages/ai-react/src/chat-ui/tool-result-content.ts @@ -1,4 +1,4 @@ -import type { UIMessage } from '@tanstack/ai-react' +import type { UIMessage } from '../types' type ToolResultPart = Extract< UIMessage['parts'][number], diff --git a/packages/ai-react/src/create-chat-hook.ts b/packages/ai-react/src/create-chat-hook.ts new file mode 100644 index 0000000000..f143af1ef1 --- /dev/null +++ b/packages/ai-react/src/create-chat-hook.ts @@ -0,0 +1,67 @@ +import { useChat as useUnboundChat } from './use-chat' +import type { + AnyClientTool, + InterruptDefinition, + SchemaInput, +} from '@tanstack/ai/client' +import type { InferredClientContext } from '@tanstack/ai-client' +import type { UseChatOptions } from './types' + +type ChatHookOverrides< + TTools extends ReadonlyArray, + TSchema extends SchemaInput | undefined, + TContext, + TInterrupts extends ReadonlyArray>, +> = { + threadId?: string + live?: boolean + forwardedProps?: Record + body?: Record + initialMessages?: UseChatOptions< + TTools, + TSchema, + TContext, + TInterrupts + >['initialMessages'] +} + +/** + * Bind chat options once at module scope. The returned `useChat` hook + * creates a chat instance from those options. + * + * Pass per-call overrides for instance keys such as `threadId`, + * `initialMessages`, `live`, and `forwardedProps`. Do not change `tools`, + * `interrupts`, or `outputSchema` here. Those stay on the factory options. + * + * Rename the hook at the call site if you already import `useChat` from + * `@tanstack/ai-react`: `const { useChat: useAppChat } = createChatHook(chatOptions)`. + * + * @example + * ```tsx + * const { useChat } = createChatHook(chatOptions) + * + * function Support() { + * const chat = useChat({ threadId: 'support-1' }) + * return + * } + * ``` + */ +export function createChatHook< + const TTools extends ReadonlyArray = any, + TSchema extends SchemaInput | undefined = undefined, + TContext = InferredClientContext, + const TInterrupts extends ReadonlyArray< + InterruptDefinition + > = readonly [], +>(options: UseChatOptions) { + function useChat( + overrides?: ChatHookOverrides, + ) { + if (!overrides) { + return useUnboundChat(options) + } + return useUnboundChat({ ...options, ...overrides }) + } + + return { useChat } +} diff --git a/packages/ai-react/src/index.ts b/packages/ai-react/src/index.ts index 0ea29a70f3..1ac2e347ab 100644 --- a/packages/ai-react/src/index.ts +++ b/packages/ai-react/src/index.ts @@ -1,4 +1,5 @@ export { useChat } from './use-chat' +export { createChatHook } from './create-chat-hook' export { useByok } from './use-byok' export { useRealtimeChat } from './use-realtime-chat' export { useMcpAppBridge } from './use-mcp-app-bridge' diff --git a/packages/ai-react/src/ui.ts b/packages/ai-react/src/ui.ts new file mode 100644 index 0000000000..454f39bdaa --- /dev/null +++ b/packages/ai-react/src/ui.ts @@ -0,0 +1,40 @@ +// Barrel entry for the `@tanstack/ai-react/ui` subpath. The JSX +// implementation lives under `./chat-ui`; this `.ts` re-export exists so +// kiira's dist->src resolution (which maps `dist/esm/ui.d.ts` to `src/ui.ts`, +// never a directory index) can type-check docs snippets that import this +// subpath. +export { + Chat, + useChatContext, + type ChatUIComponents, + type ChatUIHost, + type InputProps, + type InterruptProps, + type LayoutProps, + type MessageProps, + type PartProps, + type ToolProps, +} from './chat-ui/create-ui' +export { + Chat as DeprecatedChat, + useChatContext as useDeprecatedChatContext, + type ChatProps, +} from './chat-ui/chat' +export { ChatMessages, type ChatMessagesProps } from './chat-ui/chat-messages' +export { + ChatMessage, + type ChatMessageProps, + type ToolCallRenderProps, +} from './chat-ui/chat-message' +export { + ChatInput, + type ChatInputProps, + type ChatInputRenderProps, +} from './chat-ui/chat-input' +export { + ToolApproval, + type ToolApprovalProps, + type ToolApprovalRenderProps, +} from './chat-ui/tool-approval' +export { TextPart, type TextPartProps } from './chat-ui/text-part' +export { ThinkingPart, type ThinkingPartProps } from './chat-ui/thinking-part' diff --git a/packages/ai-react/tests/chat-ui/coexistence.test.ts b/packages/ai-react/tests/chat-ui/coexistence.test.ts new file mode 100644 index 0000000000..2db3c28849 --- /dev/null +++ b/packages/ai-react/tests/chat-ui/coexistence.test.ts @@ -0,0 +1,11 @@ +import { describe, expect, it } from 'vitest' +import { Chat, useChatContext } from '../../src/ui' +import { createChatHook } from '../../src/create-chat-hook' + +describe('public coexistence', () => { + it('exports Chat UI and the options-only createChatHook', () => { + expect(Chat).toBeDefined() + expect(useChatContext).toBeDefined() + expect(createChatHook).toBeDefined() + }) +}) diff --git a/packages/ai-react-ui/tests/create-ui-types.test.tsx b/packages/ai-react/tests/chat-ui/create-ui-types.test.tsx similarity index 81% rename from packages/ai-react-ui/tests/create-ui-types.test.tsx rename to packages/ai-react/tests/chat-ui/create-ui-types.test.tsx index 9dfe8e6644..9c951999fb 100644 --- a/packages/ai-react-ui/tests/create-ui-types.test.tsx +++ b/packages/ai-react/tests/chat-ui/create-ui-types.test.tsx @@ -1,12 +1,12 @@ import { expectTypeOf, it } from 'vitest' -import { createChatUI } from '../src/create-ui' import type { + ChatUIComponents, ChatUIHost, InterruptProps, PartProps, ToolProps, -} from '../src/create-ui' -import { chatOptions } from '../../ai-client/tests/ui-fixtures' +} from '../../src/chat-ui/create-ui' +import { chatOptions } from '../../../ai-client/tests/ui-fixtures' it('types tool and interrupt component props from chatOptions', () => { type TextPartProps = PartProps @@ -47,13 +47,9 @@ it('types tool and interrupt component props from chatOptions', () => { { title: string } | undefined >() - const UI = createChatUI(chatOptions) - - UI.defineComponents({ - layout: ({ renderMessages }) => { - expectTypeOf(UI.useChat()).toEqualTypeOf>() - expectTypeOf(UI.useChat().sendMessage).toBeFunction() - expectTypeOf(UI.useChat().queue).toBeArray() + const components = { + layout: ({ chat, renderMessages }) => { + expectTypeOf(chat).toEqualTypeOf>() return renderMessages() }, message: ({ renderParts }) => renderParts(), @@ -99,13 +95,13 @@ it('types tool and interrupt component props from chatOptions', () => { fallback: () => null, }, }, - }) + } satisfies ChatUIComponents + void components - UI.defineComponents({ + const missingTool = { layout: () => null, message: () => null, parts: { fallback: () => null }, - // @ts-expect-error Every configured tool needs a component. tools: { getWeather: () => null, }, @@ -114,9 +110,11 @@ it('types tool and interrupt component props from chatOptions', () => { choosePlan: () => null, }, }, - }) + } + // @ts-expect-error Every configured tool needs a component. + missingTool satisfies ChatUIComponents - UI.defineComponents({ + const missingInterrupt = { layout: () => null, message: () => null, parts: { fallback: () => null }, @@ -125,17 +123,11 @@ it('types tool and interrupt component props from chatOptions', () => { purchaseItem: () => null, }, interrupts: { - // @ts-expect-error Every registered interrupt id needs a component. generic: { fallback: () => null, }, }, - }) - - const Untyped = createChatUI({}) - Untyped.defineComponents({ - layout: () => null, - message: () => null, - parts: { fallback: () => null }, - }) + } + // @ts-expect-error Every registered interrupt id needs a component. + missingInterrupt satisfies ChatUIComponents }) diff --git a/packages/ai-react/tests/chat-ui/create-ui.test.tsx b/packages/ai-react/tests/chat-ui/create-ui.test.tsx new file mode 100644 index 0000000000..a66ccddb09 --- /dev/null +++ b/packages/ai-react/tests/chat-ui/create-ui.test.tsx @@ -0,0 +1,248 @@ +import { renderToStaticMarkup } from 'react-dom/server' +import { describe, expect, it, vi } from 'vitest' +import { Chat, useChatContext } from '../../src/chat-ui/create-ui' +import type { ChatUIComponents, ChatUIHost } from '../../src/chat-ui/create-ui' +import { + chatOptions, + createChatResult, + genericInterrupt, + messageWithToolResults, + orphanResultMessage, + purchaseApprovalInterrupt, + purchaseApprovalMessage, + unboundInterrupt, + unknownToolMessage, +} from '../../../ai-client/tests/ui-fixtures' +import type { ToolCallState } from '@tanstack/ai-client' + +function host( + init?: Parameters[0], +): ChatUIHost { + return createChatResult(init ?? {}) as unknown as ChatUIHost< + typeof chatOptions + > +} + +const baseComponents: ChatUIComponents = { + layout: ({ renderMessages, renderInterrupts, renderInput }) => ( + <> + {renderMessages()} + {renderInterrupts()} + {renderInput()} + + ), + message: ({ renderParts }) =>
{renderParts()}
, + parts: { fallback: ({ part }) => {part.type} }, + tools: { + getWeather: ({ part }) => {part.input?.city}, + purchaseItem: () => null, + }, + interrupts: { + generic: { + choosePlan: () => null, + fallback: ({ interrupt }) => {interrupt.reason}, + }, + }, +} + +function render( + chat: ChatUIHost, + patch?: Partial> & { + parts?: ChatUIComponents['parts'] + tools?: ChatUIComponents['tools'] + interrupts?: ChatUIComponents['interrupts'] + }, +) { + const components: ChatUIComponents = { + ...baseComponents, + ...patch, + parts: { ...baseComponents.parts, ...patch?.parts }, + tools: { ...baseComponents.tools, ...patch?.tools }, + interrupts: { + tools: { + ...baseComponents.interrupts.tools, + ...patch?.interrupts?.tools, + }, + generic: { + ...baseComponents.interrupts.generic, + ...patch?.interrupts?.generic, + }, + }, + } + return renderToStaticMarkup() +} + +describe('Chat', () => { + it('renders mapped tools from chat.messages', () => { + expect(render(host({ messages: [messageWithToolResults] }))).toContain( + 'Paris', + ) + }) + + it('warns once for a missing runtime key', () => { + const warn = vi.spyOn(console, 'warn').mockImplementation(() => undefined) + const chat = host({ messages: [unknownToolMessage] }) + render(chat) + render(chat) + expect(warn).toHaveBeenCalledTimes(1) + warn.mockRestore() + }) + + it('keeps unmatched tool results and suppresses matched ones', () => { + const parts = { + toolResult: ({ part }: { part: { type: string; content?: unknown } }) => + part.type === 'tool-result' ? {String(part.content)} : null, + fallback: () => null, + } + const matched = render(host({ messages: [messageWithToolResults] }), { + parts, + tools: { + getWeather: () => weather, + purchaseItem: () => null, + }, + }) + expect(matched).toContain('weather') + expect(matched).not.toContain('') + + const unmatched = render(host({ messages: [orphanResultMessage] }), { + parts, + tools: { + getWeather: () => weather, + purchaseItem: () => null, + }, + }) + expect(unmatched).toContain('standalone') + }) + + it('puts list approvals in the interrupt list when interrupts.tools has the tool', () => { + const markup = render( + host({ + messages: [purchaseApprovalMessage], + interrupts: [purchaseApprovalInterrupt], + }), + { + tools: { + getWeather: () => null, + purchaseItem: () =>
tool
, + }, + interrupts: { + tools: { + purchaseItem: () => list-approval, + }, + generic: { choosePlan: () => null, fallback: () => null }, + }, + }, + ) + expect(markup).toContain('list-approval') + expect(markup).toContain('tool') + }) + + it('lets a tool render its approval from interrupt without a registered interrupt component', () => { + const warn = vi.spyOn(console, 'warn').mockImplementation(() => undefined) + const markup = render( + host({ + messages: [purchaseApprovalMessage], + interrupts: [purchaseApprovalInterrupt], + }), + { + tools: { + getWeather: () => null, + purchaseItem: ({ interrupt }) => + interrupt ? {interrupt.toolName} : tool, + }, + interrupts: { + generic: { choosePlan: () => null, fallback: () => list }, + }, + }, + ) + expect(markup).toContain('purchaseItem') + expect(markup).not.toContain('list') + expect( + warn.mock.calls.filter((call) => + String(call[0]).includes('[tanstack-ai-ui]'), + ), + ).toHaveLength(0) + warn.mockRestore() + }) + + it('renders registered generic interrupts and sends the rest to fallback', () => { + const markup = render( + host({ + interrupts: [genericInterrupt, unboundInterrupt], + }), + { + layout: ({ renderInterrupts }) => renderInterrupts(), + interrupts: { + generic: { + choosePlan: () => plan, + fallback: () => fallback, + }, + }, + }, + ) + expect(markup).toContain('plan') + expect(markup).toContain('fallback') + }) + + it('omits input when no input component exists', () => { + const markup = render(host({}), { + layout: ({ renderInput }) =>
{renderInput()}
, + }) + expect(markup).toBe('
') + }) + + it('passes chat into the input component', () => { + const markup = render(host({}), { + input: ({ chat }) => {chat.messages.length}, + }) + expect(markup).toContain('0') + }) + + it('throws useChatContext outside Chat', () => { + function Broken() { + useChatContext() + return null + } + expect(() => renderToStaticMarkup()).toThrow(/useChatContext/) + }) + + it('renders a tool component for every ToolCallState', () => { + const states: Array = [ + 'awaiting-input', + 'input-streaming', + 'input-complete', + 'approval-requested', + 'approval-responded', + 'complete', + 'error', + ] + for (const state of states) { + const markup = render( + host({ + messages: [ + { + id: state, + role: 'assistant', + parts: [ + { + type: 'tool-call', + id: `call-${state}`, + name: 'getWeather', + arguments: '{}', + state, + }, + ], + }, + ], + }), + { + tools: { + getWeather: ({ part }) => {part.state}, + purchaseItem: () => null, + }, + }, + ) + expect(markup).toContain(`${state}`) + } + }) +}) diff --git a/packages/ai-react-ui/tests/markdown-plugins.test.ts b/packages/ai-react/tests/chat-ui/markdown-plugins.test.ts similarity index 98% rename from packages/ai-react-ui/tests/markdown-plugins.test.ts rename to packages/ai-react/tests/chat-ui/markdown-plugins.test.ts index 6e1011bddc..c783a39d68 100644 --- a/packages/ai-react-ui/tests/markdown-plugins.test.ts +++ b/packages/ai-react/tests/chat-ui/markdown-plugins.test.ts @@ -8,7 +8,7 @@ import { DEFAULT_REHYPE_PLUGINS_BEFORE_USER, DEFAULT_REMARK_PLUGINS, resolveMarkdownPlugins, -} from '../src/markdown-plugins' +} from '../../src/chat-ui/markdown-plugins' const userRemark = () => () => {} const userRehype = () => () => {} diff --git a/packages/ai-react-ui/tests/react-dom-server.d.ts b/packages/ai-react/tests/chat-ui/react-dom-server.d.ts similarity index 100% rename from packages/ai-react-ui/tests/react-dom-server.d.ts rename to packages/ai-react/tests/chat-ui/react-dom-server.d.ts diff --git a/packages/ai-react-ui/tests/tool-result-content.test.ts b/packages/ai-react/tests/chat-ui/tool-result-content.test.ts similarity index 94% rename from packages/ai-react-ui/tests/tool-result-content.test.ts rename to packages/ai-react/tests/chat-ui/tool-result-content.test.ts index 4f51c69e82..3a142833e0 100644 --- a/packages/ai-react-ui/tests/tool-result-content.test.ts +++ b/packages/ai-react/tests/chat-ui/tool-result-content.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest' -import { toolResultContentToString } from '../src/tool-result-content' +import { toolResultContentToString } from '../../src/chat-ui/tool-result-content' describe('toolResultContentToString', () => { it('returns string content unchanged', () => { diff --git a/packages/ai-react/tests/create-chat-hook-types.test.ts b/packages/ai-react/tests/create-chat-hook-types.test.ts new file mode 100644 index 0000000000..def6d30c2d --- /dev/null +++ b/packages/ai-react/tests/create-chat-hook-types.test.ts @@ -0,0 +1,38 @@ +import { expectTypeOf, it } from 'vitest' +import { toolDefinition } from '@tanstack/ai' +import { createChatHook } from '../src/create-chat-hook' +import { useChat as useUnboundChat } from '../src/use-chat' + +it('bound useChat infers tool names from factory options', () => { + const check = () => { + const getWeather = toolDefinition({ + name: 'getWeather', + description: 'Look up weather', + }).client(() => ({ ok: true })) + const purchaseItem = toolDefinition({ + name: 'purchaseItem', + description: 'Buy an item', + }).client(() => ({ ok: true })) + + const chatOptions = { + connection: { connect: async function* () {} }, + tools: [getWeather, purchaseItem], + } + + const { useChat } = createChatHook(chatOptions) + const fromHook = useChat() + const fromUseChat = useUnboundChat(chatOptions) + + expectTypeOf(fromHook).toEqualTypeOf(fromUseChat) + + const message = fromHook.messages[0] + if (message?.role === 'assistant') { + for (const part of message.parts) { + if (part.type === 'tool-call') { + expectTypeOf(part.name).toEqualTypeOf<'getWeather' | 'purchaseItem'>() + } + } + } + } + void check +}) diff --git a/packages/ai-react/tests/create-chat-hook.test.ts b/packages/ai-react/tests/create-chat-hook.test.ts new file mode 100644 index 0000000000..6a8462d268 --- /dev/null +++ b/packages/ai-react/tests/create-chat-hook.test.ts @@ -0,0 +1,54 @@ +import { renderHook } from '@testing-library/react' +import { describe, expect, it } from 'vitest' +import { createChatHook } from '../src/create-chat-hook' +import { createMockConnectionAdapter } from './test-utils' +import type { UIMessage } from '../src/types' + +describe('createChatHook', () => { + it('creates a chat from factory options', () => { + const initialMessages: Array = [ + { + id: 'msg-1', + role: 'user', + parts: [{ type: 'text', content: 'Hello' }], + createdAt: new Date(), + }, + ] + const { useChat } = createChatHook({ + connection: createMockConnectionAdapter(), + initialMessages, + }) + + const { result } = renderHook(() => useChat()) + + expect(result.current.messages).toEqual(initialMessages) + }) + + it('merges per-call instance overrides', () => { + const { useChat } = createChatHook({ + connection: createMockConnectionAdapter(), + initialMessages: [ + { + id: 'factory', + role: 'user', + parts: [{ type: 'text', content: 'factory' }], + createdAt: new Date(), + }, + ], + }) + + const overrideMessages: Array = [ + { + id: 'override', + role: 'user', + parts: [{ type: 'text', content: 'override' }], + createdAt: new Date(), + }, + ] + const { result } = renderHook(() => + useChat({ initialMessages: overrideMessages, threadId: 'thread-1' }), + ) + + expect(result.current.messages).toEqual(overrideMessages) + }) +}) diff --git a/packages/ai-react/vite.config.ts b/packages/ai-react/vite.config.ts index a697ee62f8..0ccd51800e 100644 --- a/packages/ai-react/vite.config.ts +++ b/packages/ai-react/vite.config.ts @@ -29,7 +29,7 @@ const config = defineConfig({ export default mergeConfig( config, tanstackViteConfig({ - entry: ['./src/index.ts', './src/mcp-apps.ts'], + entry: ['./src/index.ts', './src/mcp-apps.ts', './src/ui.ts'], srcDir: './src', cjs: false, }), diff --git a/packages/ai-solid-ui/CHANGELOG.md b/packages/ai-solid-ui/CHANGELOG.md deleted file mode 100644 index 93dc65a432..0000000000 --- a/packages/ai-solid-ui/CHANGELOG.md +++ /dev/null @@ -1,403 +0,0 @@ -# @tanstack/ai-solid-ui - -## 0.7.21 - -### Patch Changes - -- Updated dependencies [[`62c19ed`](https://github.com/TanStack/ai/commit/62c19edce7a814d868491ca920003899ec4c486b), [`4e9c5d2`](https://github.com/TanStack/ai/commit/4e9c5d2887dc9a3d64fce6ed424aa68f451e20f6), [`62c19ed`](https://github.com/TanStack/ai/commit/62c19edce7a814d868491ca920003899ec4c486b)]: - - @tanstack/ai-client@0.29.0 - - @tanstack/ai-solid@0.19.2 - -## 0.7.20 - -### Patch Changes - -- Updated dependencies [[`ad3840c`](https://github.com/TanStack/ai/commit/ad3840c44bd09860b29cd22f24a0cdce3adaf9dc), [`365efb8`](https://github.com/TanStack/ai/commit/365efb870e5a3fe084d0969f71781ffc772e39a0), [`a955710`](https://github.com/TanStack/ai/commit/a9557101048286c225145bad058ca81ff275b330), [`a74817a`](https://github.com/TanStack/ai/commit/a74817a38e8deb4c7ff86d81067db12befbadf5d)]: - - @tanstack/ai-client@0.28.0 - - @tanstack/ai-solid@0.19.1 - -## 0.7.19 - -### Patch Changes - -- Updated dependencies [[`b7ebcb0`](https://github.com/TanStack/ai/commit/b7ebcb0bbe63e425facb5e38f138bd0cd36637dd)]: - - @tanstack/ai-client@0.27.0 - - @tanstack/ai-solid@0.19.0 - -## 0.7.18 - -### Patch Changes - -- Updated dependencies [[`1c0415b`](https://github.com/TanStack/ai/commit/1c0415bec4bbefcd3abf784d0209af05aca5db46)]: - - @tanstack/ai-client@0.26.0 - - @tanstack/ai-solid@0.18.3 - -## 0.7.17 - -### Patch Changes - -- Updated dependencies [[`5f68cbc`](https://github.com/TanStack/ai/commit/5f68cbccf3621b48dae73cedcb1e59cb4cbe72b4), [`32e62ab`](https://github.com/TanStack/ai/commit/32e62ab8b7dc6a8a13ca3851c8925ab806e08f29)]: - - @tanstack/ai-client@0.25.0 - - @tanstack/ai-solid@0.18.0 - -## 0.7.16 - -### Patch Changes - -- Updated dependencies [[`f6b9d89`](https://github.com/TanStack/ai/commit/f6b9d89e7365f0cb30a41cf3a46b7dd237fbc73c), [`ecd12a4`](https://github.com/TanStack/ai/commit/ecd12a408987bc75649c21aada6948282a2a66dd)]: - - @tanstack/ai-client@0.24.0 - - @tanstack/ai-solid@0.17.0 - -## 0.7.15 - -### Patch Changes - -- Updated dependencies [[`7499171`](https://github.com/TanStack/ai/commit/74991716aea4d90a5d0363676a1e3349689a48e8)]: - - @tanstack/ai-client@0.23.0 - - @tanstack/ai-solid@0.16.0 - -## 0.7.14 - -### Patch Changes - -- Updated dependencies [[`35946e3`](https://github.com/TanStack/ai/commit/35946e3c39fb123c133ebe662f8e2cf0139f2b8c)]: - - @tanstack/ai-client@0.22.0 - - @tanstack/ai-solid@0.15.0 - -## 0.7.13 - -### Patch Changes - -- Updated dependencies [[`2665085`](https://github.com/TanStack/ai/commit/2665085970ab4d792778bb2b635ef27fbdcb6be1), [`f830d9e`](https://github.com/TanStack/ai/commit/f830d9e7a41e3554c424c3e41ba847dfd1577589), [`f830d9e`](https://github.com/TanStack/ai/commit/f830d9e7a41e3554c424c3e41ba847dfd1577589)]: - - @tanstack/ai-client@0.21.0 - - @tanstack/ai-solid@0.14.4 - -## 0.7.12 - -### Patch Changes - -- Updated dependencies [[`5deda27`](https://github.com/TanStack/ai/commit/5deda27085c8785894a28feb5bb3655dbd8f7e0a)]: - - @tanstack/ai-client@0.20.0 - - @tanstack/ai-solid@0.14.3 - -## 0.7.11 - -### Patch Changes - -- Updated dependencies [[`33acdd4`](https://github.com/TanStack/ai/commit/33acdd4df4aef13d594700d9b52087252091bd40), [`c22c663`](https://github.com/TanStack/ai/commit/c22c6632fdca761033cb9c4273bf61fc8ce86662), [`c1a8732`](https://github.com/TanStack/ai/commit/c1a87327b4a3463d37158f32ca90184b5fd092bb)]: - - @tanstack/ai-client@0.19.0 - - @tanstack/ai-solid@0.14.0 - -## 0.7.10 - -### Patch Changes - -- [#844](https://github.com/TanStack/ai/pull/844) [`a6cceba`](https://github.com/TanStack/ai/commit/a6cceba4812e7e986183ee856112fcf5f8fa12ff) - Republish all packages with their compiled `dist/` output. - - Releases `0.33.0`–`0.36.0` were published without a `dist/` directory: the - release workflow relied on an Nx-cached `build` whose outputs were not - materialized to disk before `changeset publish` packed the tarballs, and - `files: ["dist"]` silently includes nothing when `dist/` is absent. The - published packages therefore contained only `src/`, so every export - (`./dist/esm/*.js`) resolved to a missing file and the packages were - uninstallable. - - The publish step now runs a fresh, cache-bypassing build of all packages - immediately before publishing, guaranteeing compiled artifacts are present in - every tarball. - -- Updated dependencies [[`a6cceba`](https://github.com/TanStack/ai/commit/a6cceba4812e7e986183ee856112fcf5f8fa12ff)]: - - @tanstack/ai-client@0.18.6 - - @tanstack/ai-solid@0.13.15 - -## 0.7.9 - -### Patch Changes - -- Updated dependencies [[`8fa6cc5`](https://github.com/TanStack/ai/commit/8fa6cc56c5f36e22885c98a511dcceb2bfc0da1f)]: - - @tanstack/ai-client@0.18.0 - - @tanstack/ai-solid@0.13.9 - -## 0.7.8 - -### Patch Changes - -- [#769](https://github.com/TanStack/ai/pull/769) [`1d1bb52`](https://github.com/TanStack/ai/commit/1d1bb5219a38d9718cc926148e93fc27d5d2305b) - Add repository metadata (`homepage`, `bugs`, `funding`), fix `repository.directory` to point at each package, and include an MIT `LICENSE` file in every published package. - -- Updated dependencies [[`1d1bb52`](https://github.com/TanStack/ai/commit/1d1bb5219a38d9718cc926148e93fc27d5d2305b)]: - - @tanstack/ai-client@0.17.1 - - @tanstack/ai-solid@0.13.6 - -## 0.7.7 - -### Patch Changes - -- Updated dependencies [[`7d44569`](https://github.com/TanStack/ai/commit/7d445693ea079d7a85498a4465179ddd5f548cb0)]: - - @tanstack/ai-client@0.17.0 - - @tanstack/ai-solid@0.13.5 - -## 0.7.6 - -### Patch Changes - -- Updated dependencies [[`755e995`](https://github.com/TanStack/ai/commit/755e9953a31e879c4b88df0e7672ce1224886c97)]: - - @tanstack/ai-client@0.16.0 - - @tanstack/ai-solid@0.13.0 - -## 0.7.5 - -### Patch Changes - -- Updated dependencies [[`c1ae8b9`](https://github.com/TanStack/ai/commit/c1ae8b94c83d70508975568eb4fc9b45f1af540b), [`8036b50`](https://github.com/TanStack/ai/commit/8036b5054330a180023c6e3225b8d2735a43a919)]: - - @tanstack/ai-client@0.15.0 - - @tanstack/ai-solid@0.12.0 - -## 0.7.4 - -### Patch Changes - -- Updated dependencies [[`d5645cf`](https://github.com/TanStack/ai/commit/d5645cfd4d1b9cfc877f7d4d714517e166a99ce3)]: - - @tanstack/ai-client@0.14.0 - - @tanstack/ai-solid@0.11.0 - -## 0.7.3 - -### Patch Changes - -- Updated dependencies [[`5634f18`](https://github.com/TanStack/ai/commit/5634f186a4946ca3e1942fbfcbf1291ec9bd9855)]: - - @tanstack/ai-client@0.13.0 - - @tanstack/ai-solid@0.10.10 - -## 0.7.2 - -### Patch Changes - -- Updated dependencies [[`ad23da9`](https://github.com/TanStack/ai/commit/ad23da92c279759b3778672dcee3d1616a02994b)]: - - @tanstack/ai-client@0.12.0 - - @tanstack/ai-solid@0.10.9 - -## 0.7.1 - -### Patch Changes - -- Refresh package README content and npm metadata for better discoverability. ([#626](https://github.com/TanStack/ai/pull/626)) - -- Updated dependencies [[`ebeb22e`](https://github.com/TanStack/ai/commit/ebeb22ec68f456b09e0181ac6f5d1ac25a0affd2)]: - - @tanstack/ai-client@0.11.6 - - @tanstack/ai-solid@0.10.6 - -## 0.7.0 - -### Minor Changes - -- `TextPart` now accepts `remarkPlugins`, `rehypePlugins`, and (React/Solid) ([#599](https://github.com/TanStack/ai/pull/599)) - `components` props, plus a `disableDefaultPlugins` escape hatch. User plugins - merge with the secure defaults — `rehype-sanitize` continues to run last - unless defaults are disabled. - - This fixes [#164](https://github.com/TanStack/ai/issues/164): bold and - emphasis in Japanese, Chinese, and Korean text rendered incorrectly because - of a CommonMark spec defect. Consumers can now drop in - [`remark-cjk-friendly`](https://www.npmjs.com/package/remark-cjk-friendly) - with a single prop: - - ```tsx - import remarkCjkFriendly from 'remark-cjk-friendly' - ; - ``` - - Also fixes a latent bug in `@tanstack/ai-react-ui` where `remark-gfm` was - passed inside the rehype plugin array, silently disabling GFM features - (tables, strikethrough, task lists) in the React `TextPart`. - - `@tanstack/ai-vue-ui` omits the `components` prop because its underlying - renderer (`@crazydos/vue-markdown`) does not expose component overrides; - use that library's slot API for custom rendering. - -### Patch Changes - -- Updated dependencies [[`a03d12b`](https://github.com/TanStack/ai/commit/a03d12b13ade93f3e262c6ffa996696ce27472ef)]: - - @tanstack/ai-client@0.11.4 - - @tanstack/ai-solid@0.10.4 - -## 0.6.7 - -### Patch Changes - -- Tighten TypeScript safety: enable `noImplicitOverride`, ([#579](https://github.com/TanStack/ai/pull/579)) - `noFallthroughCasesInSwitch`, and `useDefineForClassFields` in the - root `tsconfig.json`; add a typed-ESLint block scoped to - `packages/*/src/**` that turns on `no-floating-promises`, - `no-misused-promises`, `await-thenable`, - `switch-exhaustiveness-check`, `consistent-type-exports`, - `prefer-readonly`, and `no-non-null-assertion` (errors), plus - `no-explicit-any` (warning). `@ts-ignore` and `@ts-nocheck` are - disallowed in library source via `@typescript-eslint/ban-ts-comment`, - and `as unknown as ` double-casts are blocked by a - `no-restricted-syntax` rule (escape hatches available with an inline - reason). Two flags from the original five-flag set — - `noPropertyAccessFromIndexSignature` and `exactOptionalPropertyTypes` - — were tried and rolled back: they produced ~500 lines of bracket- - access and conditional-spread churn without catching any real bugs, - and `exactOptionalPropertyTypes` would have forced consumers using - it themselves to deal with our internals' style preferences. - - User-visible API surface is unchanged; this is a hardening pass to - keep streaming/agent-loop correctness and discriminated-union - exhaustiveness honest going forward. See issue #564. - -- Updated dependencies []: - - @tanstack/ai-client@0.11.3 - - @tanstack/ai-solid@0.10.3 - -## 0.6.6 - -### Patch Changes - -- Updated dependencies [[`2e0e2eb`](https://github.com/TanStack/ai/commit/2e0e2eb72684aac82e570d57767656e218289b49)]: - - @tanstack/ai-client@0.11.0 - - @tanstack/ai-solid@0.10.0 - -## 0.6.5 - -### Patch Changes - -- Updated dependencies [[`a9d1916`](https://github.com/TanStack/ai/commit/a9d19165a5028515cf1d091d611c8ac4b5b86099)]: - - @tanstack/ai-client@0.10.0 - - @tanstack/ai-solid@0.9.0 - -## 0.6.4 - -### Patch Changes - -- Updated dependencies [[`98979f7`](https://github.com/TanStack/ai/commit/98979f7e72f4b5bfb816fb14b60a12871f8c4bec)]: - - @tanstack/ai-solid@0.8.0 - - @tanstack/ai-client@0.9.2 - -## 0.6.3 - -### Patch Changes - -- Updated dependencies [[`b2d3cc1`](https://github.com/TanStack/ai/commit/b2d3cc131a31c54bd1e5841f958fbe333514e508), [`13cceae`](https://github.com/TanStack/ai/commit/13cceaedf64e398ca15b8dbbbfe215329ea26794)]: - - @tanstack/ai-client@0.9.0 - - @tanstack/ai-solid@0.7.1 - -## 0.6.2 - -### Patch Changes - -- Updated dependencies [[`54523f5`](https://github.com/TanStack/ai/commit/54523f5e9a9b4d4ea6c49e4551936bc2cc25593a), [`008f015`](https://github.com/TanStack/ai/commit/008f0154f852e7e6734d3e3d35cad47780b52b7a), [`af9eb7b`](https://github.com/TanStack/ai/commit/af9eb7bbb875b23b7e99b2e6b743636daad402d1)]: - - @tanstack/ai-client@0.8.0 - - @tanstack/ai-solid@0.7.0 - -## 0.6.1 - -### Patch Changes - -- Updated dependencies [[`86be1c8`](https://github.com/TanStack/ai/commit/86be1c8262bb3176ea786aa0af115b38c3e3f51a)]: - - @tanstack/ai-client@0.7.0 - - @tanstack/ai-solid@0.6.5 - -## 0.6.0 - -### Patch Changes - -- Updated dependencies [[`6dfffca`](https://github.com/TanStack/ai/commit/6dfffca99aeac1ada59eb288f8eb09e564d3db1e), [`6dfffca`](https://github.com/TanStack/ai/commit/6dfffca99aeac1ada59eb288f8eb09e564d3db1e)]: - - @tanstack/ai-client@0.6.0 - - @tanstack/ai-solid@0.6.4 - -## 0.5.0 - -### Patch Changes - -- Updated dependencies [[`5aa6acc`](https://github.com/TanStack/ai/commit/5aa6acc1a4faea5346f750322e80984abf2d7059), [`1f800aa`](https://github.com/TanStack/ai/commit/1f800aacf57081f37a075bc8d08ff397cb33cbe9)]: - - @tanstack/ai-client@0.5.0 - - @tanstack/ai-solid@0.6.0 - -## 0.5.1 - -### Patch Changes - -- Updated dependencies [[`5d98472`](https://github.com/TanStack/ai/commit/5d984722e1f84725e3cfda834fbda3d0341ecedd)]: - - @tanstack/ai-client@0.4.4 - - @tanstack/ai-solid@0.5.1 - -## 0.5.0 - -### Patch Changes - -- Updated dependencies [[`0158d14`](https://github.com/TanStack/ai/commit/0158d14df00639ff5325680ae91b7791c189e60f)]: - - @tanstack/ai-solid@0.5.0 - - @tanstack/ai-client@0.4.1 - -## 0.4.0 - -### Patch Changes - -- Updated dependencies [[`99ccee5`](https://github.com/TanStack/ai/commit/99ccee5c72df12adc13bede98142c6da84d13cc4), [`230bab6`](https://github.com/TanStack/ai/commit/230bab6417c8ff2c25586a12126c85e27dd7bc15)]: - - @tanstack/ai-client@0.4.0 - - @tanstack/ai-solid@0.4.0 - -## 0.3.0 - -### Patch Changes - -- Updated dependencies [[`e52135f`](https://github.com/TanStack/ai/commit/e52135f6ec3285227679411636e208ae84a408d7)]: - - @tanstack/ai-client@0.3.0 - - @tanstack/ai-solid@0.3.0 - -## 0.2.1 - -### Patch Changes - -- Updated dependencies [[`181e0ac`](https://github.com/TanStack/ai/commit/181e0acdfb44b27db6cf871b36593c0f867cadf9), [`181e0ac`](https://github.com/TanStack/ai/commit/181e0acdfb44b27db6cf871b36593c0f867cadf9)]: - - @tanstack/ai-solid@0.2.1 - - @tanstack/ai-client@0.2.1 - -## 0.2.0 - -### Patch Changes - -- Updated dependencies [[`c5df33c`](https://github.com/TanStack/ai/commit/c5df33c2d3e72c3332048ffe7c64a553e5ea86fb)]: - - @tanstack/ai-client@0.2.0 - - @tanstack/ai-solid@0.2.0 - -## 0.1.0 - -### Patch Changes - -- Updated dependencies [[`8d77614`](https://github.com/TanStack/ai/commit/8d776146f94ffd1579e1ab01b26dcb94d1bb3092)]: - - @tanstack/ai-client@0.1.0 - - @tanstack/ai-solid@0.1.0 - -## 0.0.3 - -### Patch Changes - -- Updated dependencies []: - - @tanstack/ai-client@0.0.3 - - @tanstack/ai-solid@0.0.3 - -## 0.0.2 - -### Patch Changes - -- Updated dependencies [[`a7bd563`](https://github.com/TanStack/ai/commit/a7bd5639eb2fbf1b4169eb307f77149f4a85a915)]: - - @tanstack/ai-client@0.0.2 - - @tanstack/ai-solid@0.0.2 - -## 0.0.1 - -### Patch Changes - -- initial release of AI ([#76](https://github.com/TanStack/ai/pull/76)) - -## 0.0.1 - -### Patch Changes - -- Initial release of TanStack AI ([#72](https://github.com/TanStack/ai/pull/72)) - -- Updated dependencies [[`a9b54c2`](https://github.com/TanStack/ai/commit/a9b54c21282d16036a427761e0784b159a6f2d99)]: - - @tanstack/ai-client@0.0.1 - - @tanstack/ai-solid@0.0.1 diff --git a/packages/ai-solid-ui/README.md b/packages/ai-solid-ui/README.md index c9bbb8c10d..ae45778b89 100644 --- a/packages/ai-solid-ui/README.md +++ b/packages/ai-solid-ui/README.md @@ -1,328 +1,9 @@ -
- - - - TanStack AI - -
+# @tanstack/ai-solid-ui (deprecated) -
+This package is deprecated since 0.8.0. It will be removed in 1.0.0. - +Import chat UI from [`@tanstack/ai-solid/ui`](https://tanstack.com/ai/latest/docs/ui/solid) instead. -
+This package re-exports that subpath so existing installs keep working during the move. - - -# TanStack AI - -Type-safe, provider-agnostic TypeScript SDK for building streaming chat, -tool-calling agents, structured outputs, realtime voice, media generation, and -framework-native AI apps. - -TanStack AI is built from composable activities and provider adapters. Use one -provider or switch between many. Import only chat, or add image, audio, video, -speech, transcription, summarization, realtime, Code Mode, devtools, and -framework bindings as your app needs them. - -## Read the docs -> - -## Start Here - -- [Overview](https://tanstack.com/ai/latest/docs/getting-started/overview) - - what TanStack AI is and how the packages fit together. -- [Quick Start: React](https://tanstack.com/ai/latest/docs/getting-started/quick-start) - - add streaming chat to a React app. -- [Quick Start: Vue](https://tanstack.com/ai/latest/docs/getting-started/quick-start-vue) - - build with Vue composables. -- [Quick Start: Svelte](https://tanstack.com/ai/latest/docs/getting-started/quick-start-svelte) - - build with Svelte 5 runes. -- [Quick Start: Server Only](https://tanstack.com/ai/latest/docs/getting-started/quick-start-server) - - use TanStack AI from a server endpoint, script, or backend service. -- [TanStack AI vs Vercel AI SDK](https://tanstack.com/ai/latest/docs/comparison/vercel-ai-sdk) - - compare architecture, feature coverage, and tradeoffs. - -## What You Can Build - -- Streaming chat experiences with typed messages, tool calls, reasoning parts, - and configurable connection adapters. -- Type-safe tools that can run on the server or client from one shared - `toolDefinition()` contract. -- Structured output flows backed by JSON Schema, Zod, ArkType, Valibot, or - plain JSON Schema. -- Multimodal prompts and responses that include text, images, audio, video, and - documents. -- Image, audio, video, speech, transcription, and summarization workflows using - a shared generation client pattern. -- Realtime voice chat with provider adapters for realtime sessions and token - minting. -- Code Mode agents that let an LLM write and execute TypeScript in an isolated - sandbox to orchestrate tools with loops, branches, and parallel calls. -- Devtools and observability pipelines for inspecting messages, tool calls, - stream chunks, errors, usage, and OpenTelemetry traces. -- Framework-native clients for React, Solid, Vue, Svelte, and Preact, plus a - headless client for custom runtimes. - -## Install - -Install the core package and the provider/framework packages your app uses: - -```bash -pnpm add @tanstack/ai @tanstack/ai-openai -``` - -For a React chat UI: - -```bash -pnpm add @tanstack/ai @tanstack/ai-client @tanstack/ai-react @tanstack/ai-openai -``` - -OpenRouter is also a good starting point if you want access to many providers -through one API key: - -```bash -pnpm add @tanstack/ai @tanstack/ai-openrouter -``` - -## Streaming Chat - -```typescript -import { chat, toServerSentEventsResponse } from '@tanstack/ai' -import { openaiText } from '@tanstack/ai-openai' - -export async function POST(request: Request) { - const body = await request.json() - - const stream = chat({ - adapter: openaiText('gpt-5.2'), - messages: body.messages, - }) - - return toServerSentEventsResponse(stream) -} -``` - -Learn more in the -[Chat & Streaming docs](https://tanstack.com/ai/latest/docs/chat/streaming) and -[Connection Adapters docs](https://tanstack.com/ai/latest/docs/chat/connection-adapters). - -## Type-Safe Tools - -Define a tool once, then attach a server or client implementation with the same -input and output types: - -```typescript -import { toolDefinition } from '@tanstack/ai' -import { z } from 'zod' - -const getProducts = toolDefinition({ - name: 'getProducts', - description: 'Search the product catalog', - inputSchema: z.object({ query: z.string() }), - outputSchema: z.array( - z.object({ - id: z.string(), - name: z.string(), - }), - ), -}).server(async ({ query }) => { - return db.products.search(query) -}) -``` - -Learn more in the -[Tools docs](https://tanstack.com/ai/latest/docs/tools/tools), -[Tool Approval Flow docs](https://tanstack.com/ai/latest/docs/tools/tool-approval), -and -[Lazy Tool Discovery docs](https://tanstack.com/ai/latest/docs/tools/lazy-tool-discovery). - -## Structured Outputs - -Use `outputSchema` when you need typed objects instead of freeform text: - -```typescript -import { chat } from '@tanstack/ai' -import { openaiText } from '@tanstack/ai-openai' -import { z } from 'zod' - -const Person = z.object({ - name: z.string(), - age: z.number(), -}) - -const person = await chat({ - adapter: openaiText('gpt-5.2'), - messages: [{ role: 'user', content: 'Ada Lovelace, 36' }], - outputSchema: Person, -}) -``` - -Learn more in the -[Structured Outputs docs](https://tanstack.com/ai/latest/docs/structured-outputs/overview). - -## Media, Realtime, and Code Mode - -- [Generations](https://tanstack.com/ai/latest/docs/media/generations) - one - pattern for image generation, text-to-speech, transcription, summarization, - audio generation, and video generation. -- [Realtime Voice Chat](https://tanstack.com/ai/latest/docs/media/realtime-chat) - - build low-latency realtime voice experiences. -- [Code Mode](https://tanstack.com/ai/latest/docs/code-mode/code-mode) - let - models write and execute TypeScript inside a secure isolate. -- [Code Mode with Snippets](https://tanstack.com/ai/latest/docs/code-mode/code-mode-with-snippets) - - give Code Mode reusable runtime capabilities. - -## Providers - -Official adapters include: - -| Package | Use it for | -| ------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------ | -| [`@tanstack/ai-openrouter`](https://tanstack.com/ai/latest/docs/adapters/openrouter) | 300+ models through one OpenRouter API, with per-request cost tracking | -| [`@tanstack/ai-openai`](https://tanstack.com/ai/latest/docs/adapters/openai) | OpenAI chat, image, video, speech, transcription, realtime, and provider tools | -| [`@tanstack/ai-anthropic`](https://tanstack.com/ai/latest/docs/adapters/anthropic) | Anthropic Claude chat, thinking, tools, structured outputs, and Vertex Claude | -| [`@tanstack/ai-gemini`](https://tanstack.com/ai/latest/docs/adapters/gemini) | Google Gemini chat, image, speech, and audio generation | -| [`@tanstack/ai-vertex`](https://tanstack.com/ai/latest/docs/adapters/vertex) | Gemini on Vertex AI with regional endpoints and Google Cloud credentials | -| [`@tanstack/ai-ollama`](https://tanstack.com/ai/latest/docs/adapters/ollama) | Local Ollama models | -| [`@tanstack/ai-grok`](https://tanstack.com/ai/latest/docs/adapters/grok) | xAI Grok chat, images, and realtime | -| [`@tanstack/ai-groq`](https://tanstack.com/ai/latest/docs/adapters/groq) | Groq low-latency inference | -| [`@tanstack/ai-elevenlabs`](https://tanstack.com/ai/latest/docs/adapters/elevenlabs) | ElevenLabs realtime voice, speech, transcription, music, and sound effects | -| [`@tanstack/ai-byteplus`](https://tanstack.com/ai/latest/docs/adapters/byteplus) | BytePlus Seed chat, Seedance video, Seedream image, and Seed Speech TTS/ASR | -| [`@tanstack/ai-fal`](https://tanstack.com/ai/latest/docs/adapters/fal) | fal.ai image, video, audio, speech, and transcription models | - -The adapter system is tree-shakeable by activity. Import `openaiText` for chat, -`openaiImage` for images, `falVideo` for video, `geminiSpeech` for TTS, and so -on. - -## Framework Packages - -| Package | What it provides | -| -------------------------------------------------------------------------- | ------------------------------------------------------------------------ | -| [`@tanstack/ai-client`](https://tanstack.com/ai/latest/docs/api/ai-client) | Headless chat, realtime, and generation clients | -| [`@tanstack/ai-react`](https://tanstack.com/ai/latest/docs/api/ai-react) | React hooks including `useChat`, `useRealtimeChat`, and generation hooks | -| [`@tanstack/ai-solid`](https://tanstack.com/ai/latest/docs/api/ai-solid) | Solid hooks for chat and generations | -| [`@tanstack/ai-vue`](https://tanstack.com/ai/latest/docs/api/ai-vue) | Vue composables for chat and generations | -| [`@tanstack/ai-svelte`](https://tanstack.com/ai/latest/docs/api/ai-svelte) | Svelte 5 factories for chat and generations | -| [`@tanstack/ai-preact`](https://tanstack.com/ai/latest/docs/api/ai-preact) | Preact hooks for chat | -| `@tanstack/ai-react-ui`, `@tanstack/ai-solid-ui`, `@tanstack/ai-vue-ui` | Headless UI components for chat interfaces | - -## Advanced Docs - -- [Middleware](https://tanstack.com/ai/latest/docs/advanced/middleware) - hook - into chat configuration, chunks, tool calls, usage, errors, and structured - outputs. -- [OpenTelemetry](https://tanstack.com/ai/latest/docs/advanced/otel) - emit - vendor-neutral GenAI traces and metrics. -- [Observability](https://tanstack.com/ai/latest/docs/advanced/observability) - - subscribe to typed TanStack AI events. -- [Per-Model Type Safety](https://tanstack.com/ai/latest/docs/advanced/per-model-type-safety) - - narrow model options and content modalities to the selected model. -- [Runtime Adapter Switching](https://tanstack.com/ai/latest/docs/advanced/runtime-adapter-switching) - - switch providers at runtime. -- [Tree-Shaking](https://tanstack.com/ai/latest/docs/advanced/tree-shaking) - - ship only the activities and adapters you use. -- [Agent Skills](https://tanstack.com/ai/latest/docs/getting-started/agent-skills) - - install TanStack AI skills into Claude Code, Cursor, GitHub Copilot, Codex, - and other coding agents with TanStack Intent. - -## Get Involved - -- Read the [docs](https://tanstack.com/ai). -- Participate in [GitHub discussions](https://github.com/TanStack/ai/discussions). -- Chat with the community on [Discord](https://discord.com/invite/WrRKjPJ). -- See [CONTRIBUTING.md](https://github.com/TanStack/ai/blob/main/CONTRIBUTING.md) - for setup instructions. -- [Become a sponsor](https://github.com/sponsors/tannerlinsley/). - -## Partners - - - - - - -
- - - - - CodeRabbit - - - - - - - - Cloudflare - - -
- -
- AI and you? -

- We're looking for TanStack AI partners to join our mission. Partner with us - to push the boundaries of TanStack AI and build amazing things together. -

- LET'S CHAT -
- -## Explore the TanStack Ecosystem - -- TanStack Config - - tooling for JS/TS packages -- TanStack DB - reactive - sync client store -- TanStack Devtools - - unified devtools panel -- TanStack Form - - type-safe form state -- TanStack Pacer - - debouncing, throttling, batching -- TanStack Query - - async state and caching -- TanStack Ranger - - range and slider primitives -- TanStack Router - - type-safe routing, caching, and URL state -- TanStack Start - - full-stack SSR and streaming -- TanStack Store - - reactive data store -- TanStack Table - - headless datagrids -- TanStack Virtual - - virtualized rendering - -...and more at TanStack.com. +See the [migration guide](https://tanstack.com/ai/latest/docs/migration/create-ui). diff --git a/packages/ai-solid-ui/package.json b/packages/ai-solid-ui/package.json index 81d69c7582..b714c31a81 100644 --- a/packages/ai-solid-ui/package.json +++ b/packages/ai-solid-ui/package.json @@ -1,7 +1,7 @@ { "name": "@tanstack/ai-solid-ui", "version": "0.7.21", - "description": "Headless Solid components for building TanStack AI chat interfaces with streamed message parts.", + "description": "Deprecated. Import chat UI from @tanstack/ai-solid/ui instead.", "author": "Tanner Linsley", "license": "MIT", "homepage": "https://tanstack.com/ai", @@ -17,6 +17,7 @@ "type": "github", "url": "https://github.com/sponsors/tannerlinsley" }, + "deprecated": "Use @tanstack/ai-solid/ui instead. See https://tanstack.com/ai/latest/docs/migration/create-ui", "module": "./src/index.ts", "types": "./src/index.ts", "type": "module", @@ -42,37 +43,17 @@ }, "keywords": [ "ai", - "ai-sdk", - "typescript", "tanstack", "solid", - "solidjs", "chat", "ui", - "headless", - "components", - "streaming", - "generative-ai" + "deprecated" ], "dependencies": { - "rehype-highlight": "^7.0.2", - "rehype-raw": "^7.0.0", - "rehype-sanitize": "^6.0.0", - "remark-gfm": "^4.0.1", - "solid-markdown": "^2.1.0" - }, - "peerDependencies": { - "@tanstack/ai-client": "workspace:^", - "@tanstack/ai-solid": "workspace:^", - "solid-js": ">=1.9.7" + "@tanstack/ai-solid": "workspace:^" }, "devDependencies": { - "@tanstack/ai-client": "workspace:*", - "@tanstack/ai-solid": "workspace:*", - "@vitest/coverage-v8": "4.1.10", - "happy-dom": "^20.11.2", "solid-js": "^1.9.10", - "vite": "^8.2.1", - "vite-plugin-solid": "^2.11.14" + "vite": "^8.2.1" } } diff --git a/packages/ai-solid-ui/src/create-ui.tsx b/packages/ai-solid-ui/src/create-ui.tsx deleted file mode 100644 index 9bec0dec8a..0000000000 --- a/packages/ai-solid-ui/src/create-ui.tsx +++ /dev/null @@ -1,432 +0,0 @@ -import { For, createContext, useContext } from 'solid-js' -import type { Accessor, Component, JSX } from 'solid-js' -import { - automaticPartsForMessage, - collectInlineToolNames, - resolveInterruptComponent, - selectChatUI, - selectMessageUI, -} from '@tanstack/ai-client/ui' -import type { - ChatUIData, - ChatUIHasNamedInterrupts, - ChatUIHasNamedTools, - ChatUIInterrupt, - ChatUIInterruptName, - ChatUIInterruptOf, - ChatUIInterruptsOf, - ChatUINamedInterruptId, - ChatUIPartKey, - ChatUIPartOf, - ChatUISchemaOf, - ChatUISelectedPart, - ChatUIToolApproval, - ChatUIToolName, - ChatUIToolsOf, -} from '@tanstack/ai-client/ui' -import type { - MessagePart, - ToolCallPart, - ToolResultPart, - UIMessage, -} from '@tanstack/ai-client' -import type { UseChatReturn } from '@tanstack/ai-solid' - -export type ChatUIHost = UseChatReturn< - ChatUIToolsOf, - ChatUISchemaOf, - ChatUIInterruptsOf -> - -export type LayoutProps = { - renderMessages: () => JSX.Element - renderInterrupts: () => JSX.Element - renderInput: () => JSX.Element - readonly __ui?: TOptions -} - -export type MessageProps = { - message: UIMessage, ChatUIData> - renderParts: () => JSX.Element -} - -export type InputProps = { - readonly __ui?: TOptions -} - -export type PartProps = { - part: ChatUIPartOf -} - -export type ToolProps< - TOptions, - TName extends ChatUIToolName = ChatUIToolName, -> = { - part: Extract>, { name: TName }> - result?: ToolResultPart - interrupt?: ChatUIToolApproval -} - -export type InterruptProps< - TOptions, - TName extends ChatUIInterruptName = never, -> = { - interrupt: ChatUIInterruptOf - readonly __ui?: TOptions -} - -type GenericInterruptComponents = - ChatUIHasNamedInterrupts extends true - ? { - [K in ChatUINamedInterruptId]: Component< - InterruptProps> - > - } & { - fallback?: Component> - } - : { - fallback?: Component> - } - -type ToolApprovalMap = { - [K in ChatUIToolName]?: Component< - InterruptProps> - > -} - -export type ChatUIComponents = { - layout: Component> - message: Component> - input?: Component> - parts: { - [K in ChatUIPartKey]?: Component> - } & { - fallback?: Component> - } -} & (ChatUIHasNamedTools extends true - ? { - tools: { - [K in ChatUIToolName]: Component> - } - } - : { - tools?: { - [K in ChatUIToolName]?: Component> - } - }) & - (ChatUIHasNamedInterrupts extends true - ? { - interrupts: { - tools?: ToolApprovalMap - generic: GenericInterruptComponents - } - } - : { - interrupts?: { - tools?: ToolApprovalMap - generic?: GenericInterruptComponents - } - }) - -type ComponentsValue = { - components: ChatUIComponents - warn: (key: string, message: string) => void - inlineToolNames: ReadonlyArray -} - -function createWarnOnce() { - const seen = new Set() - return (key: string, message: string) => { - if (process.env.NODE_ENV === 'production') return - if (seen.has(key)) return - seen.add(key) - console.warn(message) - } -} - -function readMessages( - chat: ChatUIHost, -): ReadonlyArray { - return chat.messages() -} - -function readInterrupts( - chat: ChatUIHost, -): ReadonlyArray { - return chat.interrupts() -} - -function messagesAccessor( - chat: ChatUIHost, -): Accessor> { - return chat.messages -} - -export function createChatUI(options: TOptions) { - void options - const warn = createWarnOnce() - const ChatContext = createContext | null>(null) - const ComponentsContext = createContext | null>( - null, - ) - - function useChat() { - const chat = useContext(ChatContext) - if (!chat) { - throw new Error( - 'Chat UI components must be wrapped in UI.Provider or UI.Chat.', - ) - } - return chat - } - - function useComponents() { - const value = useContext(ComponentsContext) - if (!value) { - throw new Error( - 'Chat UI components must be wrapped in UI.Provider or UI.Chat.', - ) - } - return value - } - - function defineComponents(components: ChatUIComponents) { - return components - } - - function inlineNames(components: ChatUIComponents) { - return collectInlineToolNames( - components.interrupts?.tools as Record | undefined, - Object.keys(components.tools ?? {}), - ) - } - - function Provider(props: { - chat: ChatUIHost - components: ChatUIComponents - children?: JSX.Element - }) { - const componentsValue: ComponentsValue = { - get components() { - return props.components - }, - warn, - get inlineToolNames() { - return inlineNames(props.components) - }, - } - return ( - - - {props.children} - - - ) - } - - function Chat(props: { - chat: ChatUIHost - components: ChatUIComponents - }) { - const Layout = props.components.layout - return ( - - } - renderInterrupts={() => } - renderInput={() => { - const Input = props.components.input - return Input ? : null - }} - /> - - ) - } - - function Messages(props: { - children?: ( - messages: Accessor>>, - ) => JSX.Element - }) { - const chat = useChat() - const comps = useComponents() - if (props.children) return <>{props.children(messagesAccessor(chat))} - return ( - - {(message) => ( - - )} - - ) - } - - function MessageView(props: { - message: UIMessage - interrupts: ReadonlyArray - inlineToolNames: ReadonlyArray - children?: (parts: Array) => JSX.Element - }) { - const comps = useComponents() - const selected = selectMessageUI(props.message, { - interrupts: props.interrupts, - inlineToolNames: props.inlineToolNames, - }) - if (props.children) return <>{props.children(selected.parts)} - const MessageComponent = comps.components.message - return ( - ['message']} - renderParts={() => ( - - )} - /> - ) - } - - function Message(props: { - message: UIMessage - children?: (parts: Array) => JSX.Element - }) { - const chat = useChat() - const comps = useComponents() - return ( - - ) - } - - function AutomaticParts(props: { - message: UIMessage - interrupts: ReadonlyArray - inlineToolNames: ReadonlyArray - }) { - const selected = selectMessageUI(props.message, { - interrupts: props.interrupts, - inlineToolNames: props.inlineToolNames, - }) - return ( - <> - {automaticPartsForMessage(selected).map((part) => ( - - ))} - - ) - } - - function SelectedPartView(props: { selected: ChatUISelectedPart }) { - const comps = useComponents() - if (props.selected.key === 'toolCall') { - const name = props.selected.part.name - const Tool = comps.components.tools?.[ - name as ChatUIToolName - ] as Component> | undefined - if (!Tool) { - comps.warn( - `tool:${name}`, - `[tanstack-ai-ui] Missing tools.${name} component`, - ) - return null - } - return ( - ['part']} - result={props.selected.result} - interrupt={ - (props.selected.key === 'toolCall' - ? props.selected.interrupt - : undefined) as ToolProps['interrupt'] - } - /> - ) - } - const PartComponent = (comps.components.parts[props.selected.key] ?? - comps.components.parts.fallback) as - | Component> - | undefined - if (!PartComponent) { - comps.warn( - `part:${props.selected.key}`, - `[tanstack-ai-ui] Missing parts.${props.selected.key} component`, - ) - return null - } - return ( - ['part']} - /> - ) - } - - function Part(props: { part: MessagePart }) { - const chat = useChat() - const selected = selectMessageUI( - { id: 'part', role: 'assistant', parts: [props.part] }, - { interrupts: readInterrupts(chat), inlineToolNames: [] }, - ).parts[0] - if (!selected) return null - return - } - - function Interrupts(props: { - children?: ( - interrupts: Accessor>, - ) => JSX.Element - }) { - const chat = useChat() - const comps = useComponents() - const selected = () => - selectChatUI({ - messages: readMessages(chat), - interrupts: readInterrupts(chat), - inlineToolNames: comps.inlineToolNames, - }) - if (props.children) { - return <>{props.children(() => selected().interrupts)} - } - return ( - - {(interrupt) => } - - ) - } - - function Interrupt(props: { interrupt: ChatUIInterrupt }) { - const comps = useComponents() - const Component = resolveInterruptComponent( - props.interrupt, - comps.components.interrupts, - ) as Component> | undefined - if (!Component) { - comps.warn( - `interrupt:${props.interrupt.id}`, - `[tanstack-ai-ui] Missing interrupt component for ${props.interrupt.kind}`, - ) - return null - } - return - } - - return { - Chat, - Provider, - Messages, - Message, - Part, - Interrupts, - Interrupt, - defineComponents, - useChat, - } -} diff --git a/packages/ai-solid-ui/src/index.ts b/packages/ai-solid-ui/src/index.ts index 0d7c2236b5..4bf2d51f1d 100644 --- a/packages/ai-solid-ui/src/index.ts +++ b/packages/ai-solid-ui/src/index.ts @@ -1,73 +1,6 @@ /** - * @tanstack/ai-solid-ui - * - * Headless Solid.js components for building AI chat interfaces. - * - * Features: - * - Parts-based message rendering (text, tool calls, tool results) - * - Native tool approval workflows - * - Client-side tool execution support - * - Streaming support - * - Fully customizable with render props - * - Compound component pattern - * - * @example - * ```tsx - * import { Chat, ChatMessages, ChatInput, ChatMessage } from '@tanstack/ai-solid-ui' - * - * - * - * {(message) => } - * - * - * - * ``` + * @deprecated Since 0.8.0. Import from `@tanstack/ai-solid/ui` instead. + * See https://tanstack.com/ai/latest/docs/migration/create-ui + * Removed in 1.0.0. */ - -export { - createChatUI, - type ChatUIComponents, - type ChatUIHost, - type InputProps, - type InterruptProps, - type LayoutProps, - type MessageProps, - type PartProps, - type ToolProps, -} from './create-ui' - -// Main components -export { Chat, useChatContext, type ChatProps } from './chat' -export { ChatMessages, type ChatMessagesProps } from './chat-messages' -export { - ChatMessage, - type ChatMessageProps, - type ToolCallRenderProps, -} from './chat-message' -export { - ChatInput, - type ChatInputProps, - type ChatInputRenderProps, -} from './chat-input' -export { - ToolApproval, - type ToolApprovalProps, - type ToolApprovalRenderProps, -} from './tool-approval' -export { TextPart, type TextPartProps } from './text-part' -export { ThinkingPart, type ThinkingPartProps } from './thinking-part' - -// Re-export hooks from @tanstack/ai-solid for convenience -export { useChat } from '@tanstack/ai-solid' - -// Re-export types from @tanstack/ai-solid -export type { - UIMessage, - MessagePart, - ToolCallPart, - ToolResultPart, - TextPart as TextPartType, - ConnectionAdapter, -} from '@tanstack/ai-client' - -export type { UseChatOptions, UseChatReturn } from '@tanstack/ai-solid' +export * from '@tanstack/ai-solid/ui' diff --git a/packages/ai-solid-ui/tests/coexistence.test.ts b/packages/ai-solid-ui/tests/coexistence.test.ts deleted file mode 100644 index 9a5b49983a..0000000000 --- a/packages/ai-solid-ui/tests/coexistence.test.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { describe, expect, it } from 'vitest' -import { createChatUI } from '../src/create-ui' -import { readFileSync } from 'node:fs' -import { dirname, join } from 'node:path' -import { fileURLToPath } from 'node:url' - -const src = dirname(fileURLToPath(import.meta.url)) -const index = readFileSync(join(src, '../src/index.ts'), 'utf8') - -describe('public coexistence', () => { - it('exports old and new APIs before 1.0', () => { - expect(createChatUI).toBeDefined() - expect(index).toContain('export { Chat') - expect(index).toContain('ChatInput') - expect(index).toContain('ChatMessage') - expect(index).toContain('ChatMessages') - expect(index).toContain('TextPart') - expect(index).toContain('ThinkingPart') - expect(index).toContain('createChatUI') - }) -}) diff --git a/packages/ai-solid-ui/tests/create-ui-types.test.tsx b/packages/ai-solid-ui/tests/create-ui-types.test.tsx deleted file mode 100644 index 94da17e71b..0000000000 --- a/packages/ai-solid-ui/tests/create-ui-types.test.tsx +++ /dev/null @@ -1,103 +0,0 @@ -import { expectTypeOf, it } from 'vitest' -import { createChatUI } from '../src/create-ui' -import type { ChatUIHost, InterruptProps, ToolProps } from '../src/create-ui' -import { chatOptions } from '../../ai-client/tests/ui-fixtures' - -it('requires every tool name and interrupt id', () => { - type PurchaseInterrupt = InterruptProps - expectTypeOf< - PurchaseInterrupt['interrupt']['toolName'] - >().toEqualTypeOf<'purchaseItem'>() - type ChoosePlan = InterruptProps - expectTypeOf().toEqualTypeOf< - { title: string } | undefined - >() - type PurchaseTool = ToolProps - expectTypeOf< - NonNullable['originalArgs'] - >().toEqualTypeOf<{ - item: string - }>() - - const UI = createChatUI(chatOptions) - - UI.defineComponents({ - layout: (props) => { - expectTypeOf(UI.useChat()).toEqualTypeOf>() - expectTypeOf(UI.useChat().sendMessage).toBeFunction() - return props.renderMessages() - }, - message: (props) => props.renderParts(), - parts: { - fallback: () => null, - }, - tools: { - getWeather: (props) => { - expectTypeOf(props.part.input).toEqualTypeOf< - { city: string } | undefined - >() - expectTypeOf(props.part.output).toEqualTypeOf< - { temperature: number } | undefined - >() - return null - }, - purchaseItem: (props) => { - expectTypeOf(props.interrupt?.toolName).toEqualTypeOf< - 'purchaseItem' | undefined - >() - return null - }, - // @ts-expect-error This tool is not in chatOptions. - unknownTool: () => null, - }, - interrupts: { - generic: { - choosePlan: (props) => { - props.interrupt.resolveInterrupt('approved') - // @ts-expect-error The response schema accepts a string. - props.interrupt.resolveInterrupt(42) - return null - }, - fallback: () => null, - }, - }, - }) - - UI.defineComponents({ - layout: () => null, - message: () => null, - parts: { fallback: () => null }, - // @ts-expect-error Every configured tool needs a component. - tools: { - getWeather: () => null, - }, - interrupts: { - generic: { - choosePlan: () => null, - }, - }, - }) - - UI.defineComponents({ - layout: () => null, - message: () => null, - parts: { fallback: () => null }, - tools: { - getWeather: () => null, - purchaseItem: () => null, - }, - interrupts: { - // @ts-expect-error Every registered interrupt id needs a component. - generic: { - fallback: () => null, - }, - }, - }) - - const Untyped = createChatUI({}) - Untyped.defineComponents({ - layout: () => null, - message: () => null, - parts: { fallback: () => null }, - }) -}) diff --git a/packages/ai-solid-ui/tests/create-ui.test.tsx b/packages/ai-solid-ui/tests/create-ui.test.tsx deleted file mode 100644 index 9501cc07df..0000000000 --- a/packages/ai-solid-ui/tests/create-ui.test.tsx +++ /dev/null @@ -1,74 +0,0 @@ -import { render } from 'solid-js/web' -import { describe, expect, it, vi } from 'vitest' -import { createChatUI } from '../src/create-ui' -import type { ChatUIHost } from '../src/create-ui' -import { - chatOptions, - createSolidChatResult, - messageWithToolResults, - unknownToolMessage, -} from '../../ai-client/tests/ui-fixtures' - -function host( - ...args: Parameters -): ChatUIHost { - return createSolidChatResult(...args) as unknown as ChatUIHost< - typeof chatOptions - > -} - -function renderHtml(node: () => unknown) { - const container = document.createElement('div') - render(node as () => import('solid-js').JSX.Element, container) - return container.innerHTML -} - -describe('Solid createChatUI', () => { - it('renders automatic and manual traversal', () => { - const UI = createChatUI(chatOptions) - const chat = host([messageWithToolResults]) - const components = UI.defineComponents({ - layout: (props) => <>{props.renderMessages()}, - message: (props) =>
{props.renderParts()}
, - parts: { fallback: (props) => {props.part.type} }, - tools: { - getWeather: (props) => {props.part.input?.city}, - purchaseItem: () => null, - }, - interrupts: { generic: { choosePlan: () => null, fallback: () => null } }, - }) - - expect( - renderHtml(() => ), - ).toContain('Paris') - expect( - renderHtml(() => ( - - - {(messages) => {messages().length}} - - - )), - ).toContain('1') - }) - - it('warns once for a missing runtime key', () => { - const warn = vi.spyOn(console, 'warn').mockImplementation(() => undefined) - const UI = createChatUI(chatOptions) - const chat = host([unknownToolMessage]) - const components = UI.defineComponents({ - layout: (props) => props.renderMessages(), - message: (props) => props.renderParts(), - parts: { fallback: () => null }, - tools: { - getWeather: () => null, - purchaseItem: () => null, - }, - interrupts: { generic: { choosePlan: () => null, fallback: () => null } }, - }) - renderHtml(() => ) - renderHtml(() => ) - expect(warn).toHaveBeenCalledTimes(1) - warn.mockRestore() - }) -}) diff --git a/packages/ai-solid-ui/tsconfig.json b/packages/ai-solid-ui/tsconfig.json index 3cd8594f74..d2a10ad437 100644 --- a/packages/ai-solid-ui/tsconfig.json +++ b/packages/ai-solid-ui/tsconfig.json @@ -7,6 +7,6 @@ "lib": ["ES2022", "DOM"], "rootDir": "../.." }, - "include": ["src", "tests"], + "include": ["src"], "exclude": ["node_modules", "dist"] } diff --git a/packages/ai-solid-ui/vite.config.ts b/packages/ai-solid-ui/vite.config.ts index 31293a7dbd..190315c4bf 100644 --- a/packages/ai-solid-ui/vite.config.ts +++ b/packages/ai-solid-ui/vite.config.ts @@ -1,37 +1,16 @@ import { defineConfig, mergeConfig } from 'vitest/config' import { tanstackViteConfig } from '@tanstack/vite-config' -import solid from 'vite-plugin-solid' import packageJson from './package.json' const config = defineConfig({ - // Solid components compile via vite-plugin-solid's babel transform. Under - // vite 7 esbuild handled the JSX implicitly (jsxImportSource: solid-js); - // vite 8's Rolldown parser rejects raw JSX ("JSX syntax is disabled"), so - // the transform must be wired in explicitly. - plugins: [solid()], test: { name: packageJson.name, dir: './', watch: false, globals: true, - // Solid UI components pull in solid-js/web (references `document`) once - // vite-plugin-solid transforms them into the test graph, so tests run in - // a DOM environment rather than 'node'. - environment: 'happy-dom', - include: ['tests/**/*.test.ts', 'tests/**/*.test.tsx'], - coverage: { - provider: 'v8', - reporter: ['text', 'json', 'html', 'lcov'], - exclude: [ - 'node_modules/', - 'dist/', - 'tests/', - '**/*.test.ts', - '**/*.config.ts', - '**/types.ts', - ], - include: ['src/**/*.ts'], - }, + environment: 'node', + include: ['tests/**/*.test.ts'], + passWithNoTests: true, }, }) diff --git a/packages/ai-solid/package.json b/packages/ai-solid/package.json index 59099d3a8e..1ff580977d 100644 --- a/packages/ai-solid/package.json +++ b/packages/ai-solid/package.json @@ -24,6 +24,11 @@ ".": { "types": "./dist/index.d.ts", "import": "./dist/index.js" + }, + "./ui": { + "solid": "./src/ui.ts", + "types": "./src/ui.ts", + "import": "./src/ui.ts" } }, "files": [ @@ -53,7 +58,12 @@ "media-generation" ], "dependencies": { - "@tanstack/ai-client": "workspace:^" + "@tanstack/ai-client": "workspace:^", + "rehype-highlight": "^7.0.2", + "rehype-raw": "^7.0.0", + "rehype-sanitize": "^6.0.0", + "remark-gfm": "^4.0.1", + "solid-markdown": "^2.1.0" }, "peerDependencies": { "@tanstack/ai": "workspace:^", @@ -69,6 +79,8 @@ "solid-js": "^1.9.10", "tsdown": "^0.17.0-beta.6", "typescript": "5.9.3", + "vite": "^8.2.1", + "vite-plugin-solid": "^2.11.14", "vitest": "^4.1.10" } } diff --git a/packages/ai-solid-ui/src/chat-input.tsx b/packages/ai-solid/src/chat-ui/chat-input.tsx similarity index 100% rename from packages/ai-solid-ui/src/chat-input.tsx rename to packages/ai-solid/src/chat-ui/chat-input.tsx diff --git a/packages/ai-solid-ui/src/chat-message.tsx b/packages/ai-solid/src/chat-ui/chat-message.tsx similarity index 99% rename from packages/ai-solid-ui/src/chat-message.tsx rename to packages/ai-solid/src/chat-ui/chat-message.tsx index c874e1ef01..f971adbd32 100644 --- a/packages/ai-solid-ui/src/chat-message.tsx +++ b/packages/ai-solid/src/chat-ui/chat-message.tsx @@ -1,7 +1,7 @@ import { For, Show } from 'solid-js' import { ThinkingPart } from './thinking-part' import type { JSX } from 'solid-js' -import type { UIMessage } from '@tanstack/ai-solid' +import type { UIMessage } from '../types' export interface ToolCallRenderProps { id: string diff --git a/packages/ai-solid-ui/src/chat-messages.tsx b/packages/ai-solid/src/chat-ui/chat-messages.tsx similarity index 98% rename from packages/ai-solid-ui/src/chat-messages.tsx rename to packages/ai-solid/src/chat-ui/chat-messages.tsx index e2cf3a3868..2938360339 100644 --- a/packages/ai-solid-ui/src/chat-messages.tsx +++ b/packages/ai-solid/src/chat-ui/chat-messages.tsx @@ -2,7 +2,7 @@ import { For, Show, createEffect } from 'solid-js' import { useChatContext } from './chat' import { ChatMessage } from './chat-message' import type { JSX } from 'solid-js' -import type { UIMessage } from '@tanstack/ai-solid' +import type { UIMessage } from '../types' /** @deprecated Use `createChatUI()` Messages instead. Deprecated in 0.8.0. Removed in 1.0.0. */ export interface ChatMessagesProps { diff --git a/packages/ai-solid-ui/src/chat.tsx b/packages/ai-solid/src/chat-ui/chat.tsx similarity index 91% rename from packages/ai-solid-ui/src/chat.tsx rename to packages/ai-solid/src/chat-ui/chat.tsx index ea0dba7903..e72b00e45b 100644 --- a/packages/ai-solid-ui/src/chat.tsx +++ b/packages/ai-solid/src/chat-ui/chat.tsx @@ -1,11 +1,8 @@ import { createContext, useContext } from 'solid-js' -import { useChat } from '@tanstack/ai-solid' +import type { ConnectionAdapter } from '@tanstack/ai-client' +import { useChat } from '../use-chat' import type { JSX } from 'solid-js' -import type { - ConnectionAdapter, - UIMessage, - UseChatReturn, -} from '@tanstack/ai-solid' +import type { UIMessage, UseChatReturn } from '../types' /** * Chat context - provides chat state to all child components @@ -59,9 +56,9 @@ export interface ChatProps { } /** - * @deprecated Use `createChatUI()` from `@tanstack/ai-solid-ui` instead. + * @deprecated Since 0.8.0. Use `Chat` from `@tanstack/ai-solid/ui` with `useChat` from `@tanstack/ai-solid`. * See https://tanstack.com/ai/latest/docs/migration/create-ui - * Deprecated in 0.8.0. Removed in 1.0.0. + * Removed in 1.0.0. * * Root Chat component - provides context for all chat subcomponents * diff --git a/packages/ai-solid/src/chat-ui/create-ui.tsx b/packages/ai-solid/src/chat-ui/create-ui.tsx new file mode 100644 index 0000000000..03e668daf1 --- /dev/null +++ b/packages/ai-solid/src/chat-ui/create-ui.tsx @@ -0,0 +1,291 @@ +import { For, createContext, createMemo, useContext } from 'solid-js' +import type { Component, JSX } from 'solid-js' +import { + automaticPartsForMessage, + collectInlineToolNames, + resolveInterruptComponent, + selectChatUI, + selectMessageUI, +} from '@tanstack/ai-client/ui' +import type { + ChatUIData, + ChatUIHasNamedInterrupts, + ChatUIHasNamedTools, + ChatUIInterruptName, + ChatUIInterruptOf, + ChatUIInterruptsOf, + ChatUINamedInterruptId, + ChatUIPartKey, + ChatUIPartOf, + ChatUISchemaOf, + ChatUISelectedPart, + ChatUIToolApproval, + ChatUIToolName, + ChatUIToolsOf, +} from '@tanstack/ai-client/ui' +import type { + ToolCallPart, + ToolResultPart, + UIMessage, +} from '@tanstack/ai-client' +import type { UseChatReturn } from '../types' + +export type ChatUIHost = UseChatReturn< + ChatUIToolsOf, + ChatUISchemaOf, + ChatUIInterruptsOf +> + +export type LayoutProps = { + chat: ChatUIHost + renderMessages: () => JSX.Element + renderInterrupts: () => JSX.Element + renderInput: () => JSX.Element +} + +export type MessageProps = { + chat: ChatUIHost + message: UIMessage, ChatUIData> + renderParts: () => JSX.Element +} + +export type InputProps = { + chat: ChatUIHost +} + +export type PartProps = { + part: ChatUIPartOf +} + +export type ToolProps< + TOptions, + TName extends ChatUIToolName = ChatUIToolName, +> = { + part: Extract>, { name: TName }> + result?: ToolResultPart + interrupt?: ChatUIToolApproval +} + +export type InterruptProps< + TOptions, + TName extends ChatUIInterruptName = never, +> = { + interrupt: ChatUIInterruptOf +} + +type GenericInterruptComponents = + ChatUIHasNamedInterrupts extends true + ? { + [K in ChatUINamedInterruptId]: Component< + InterruptProps> + > + } & { + fallback?: Component> + } + : { + fallback?: Component> + } + +type ToolApprovalMap = { + [K in ChatUIToolName]?: Component< + InterruptProps> + > +} + +export type ChatUIComponents = { + layout: Component> + message: Component> + input?: Component> + parts: { + [K in ChatUIPartKey]?: Component> + } & { + fallback?: Component> + } +} & (ChatUIHasNamedTools extends true + ? { + tools: { + [K in ChatUIToolName]: Component> + } + } + : { + tools?: { + [K in ChatUIToolName]?: Component> + } + }) & + (ChatUIHasNamedInterrupts extends true + ? { + interrupts: { + tools?: ToolApprovalMap + generic: GenericInterruptComponents + } + } + : { + interrupts?: { + tools?: ToolApprovalMap + generic?: GenericInterruptComponents + } + }) + +type ChatTree = { + chat: ChatUIHost + components: ChatUIComponents + warn: (key: string, message: string) => void + inlineToolNames: Array +} + +const ChatTreeContext = createContext() + +const warnedKeys = new Set() +function warnMissing(key: string, message: string) { + if (warnedKeys.has(key)) return + warnedKeys.add(key) + console.warn(message) +} + +function useChatTree() { + const tree = useContext(ChatTreeContext) + if (!tree) { + throw new Error('`useChatContext` must be used within `Chat`.') + } + return tree +} + +export function useChatContext(): ChatUIHost { + return useChatTree().chat +} + +function SelectedPart(props: { selected: ChatUISelectedPart }) { + const tree = useChatTree() + const selected = () => props.selected + const node = createMemo(() => { + const current = selected() + if (current.key === 'toolCall') { + const name = current.part.name + const Tool = tree.components.tools?.[name as never] as + | Component> + | undefined + if (!Tool) { + tree.warn( + `tool:${name}`, + `[tanstack-ai-ui] Missing tools.${name} component`, + ) + return null + } + return ( + ['interrupt']} + /> + ) + } + const PartComponent = (tree.components.parts[current.key] ?? + tree.components.parts.fallback) as Component> | undefined + if (!PartComponent) { + tree.warn( + `part:${current.key}`, + `[tanstack-ai-ui] Missing parts.${current.key} component`, + ) + return null + } + return ['part']} /> + }) + return <>{node()} +} + +function AutomaticParts(props: { message: UIMessage }) { + const tree = useChatTree() + const parts = createMemo(() => { + const selected = selectMessageUI(props.message, { + interrupts: tree.chat.interrupts(), + inlineToolNames: tree.inlineToolNames, + }) + return automaticPartsForMessage(selected) + }) + return {(part) => } +} + +function Messages() { + const tree = useChatTree() + const MessageComponent = tree.components.message + return ( + + {(message) => ( + } + /> + )} + + ) +} + +function Interrupts() { + const tree = useChatTree() + const selected = createMemo(() => + selectChatUI({ + messages: tree.chat.messages(), + interrupts: tree.chat.interrupts(), + inlineToolNames: tree.inlineToolNames, + }), + ) + return ( + + {(interrupt) => { + const Component = resolveInterruptComponent( + interrupt, + tree.components.interrupts, + ) as Component> | undefined + if (!Component) { + tree.warn( + `interrupt:${interrupt.id}`, + `[tanstack-ai-ui] Missing interrupt component for ${interrupt.kind}`, + ) + return null + } + return + }} + + ) +} + +export function Chat(props: { + chat: ChatUIHost + components: ChatUIComponents +}) { + const tree: ChatTree = { + get chat() { + return props.chat as ChatUIHost + }, + get components() { + return props.components as ChatUIComponents + }, + warn: warnMissing, + get inlineToolNames() { + const tools = (props.components as ChatUIComponents).tools + return collectInlineToolNames( + props.components.interrupts?.tools as + | Record + | undefined, + Object.keys(tools ?? {}), + ) + }, + } + return ( + + {(() => { + const Layout = props.components.layout + const Input = props.components.input + return ( + } + renderInterrupts={() => } + renderInput={() => (Input ? : null)} + /> + ) + })()} + + ) +} diff --git a/packages/ai-solid-ui/src/markdown-plugins.ts b/packages/ai-solid/src/chat-ui/markdown-plugins.ts similarity index 100% rename from packages/ai-solid-ui/src/markdown-plugins.ts rename to packages/ai-solid/src/chat-ui/markdown-plugins.ts diff --git a/packages/ai-solid-ui/src/text-part.tsx b/packages/ai-solid/src/chat-ui/text-part.tsx similarity index 100% rename from packages/ai-solid-ui/src/text-part.tsx rename to packages/ai-solid/src/chat-ui/text-part.tsx diff --git a/packages/ai-solid-ui/src/thinking-part.tsx b/packages/ai-solid/src/chat-ui/thinking-part.tsx similarity index 100% rename from packages/ai-solid-ui/src/thinking-part.tsx rename to packages/ai-solid/src/chat-ui/thinking-part.tsx diff --git a/packages/ai-solid-ui/src/tool-approval.tsx b/packages/ai-solid/src/chat-ui/tool-approval.tsx similarity index 100% rename from packages/ai-solid-ui/src/tool-approval.tsx rename to packages/ai-solid/src/chat-ui/tool-approval.tsx diff --git a/packages/ai-solid/src/create-chat-hook.ts b/packages/ai-solid/src/create-chat-hook.ts new file mode 100644 index 0000000000..c815e8b0d9 --- /dev/null +++ b/packages/ai-solid/src/create-chat-hook.ts @@ -0,0 +1,67 @@ +import { useChat as useUnboundChat } from './use-chat' +import type { + AnyClientTool, + InterruptDefinition, + SchemaInput, +} from '@tanstack/ai' +import type { InferredClientContext } from '@tanstack/ai-client' +import type { UseChatOptions } from './types' + +type ChatHookOverrides< + TTools extends ReadonlyArray, + TSchema extends SchemaInput | undefined, + TContext, + TInterrupts extends ReadonlyArray>, +> = { + threadId?: string + live?: boolean + forwardedProps?: Record + body?: Record + initialMessages?: UseChatOptions< + TTools, + TSchema, + TContext, + TInterrupts + >['initialMessages'] +} + +/** + * Bind chat options once at module scope. The returned `useChat` primitive + * creates a chat instance from those options. + * + * Pass per-call overrides for instance keys such as `threadId`, + * `initialMessages`, `live`, and `forwardedProps`. Do not change `tools`, + * `interrupts`, or `outputSchema` here. Those stay on the factory options. + * + * Rename the primitive at the call site if you already import `useChat` from + * `@tanstack/ai-solid`: `const { useChat: useAppChat } = createChatHook(chatOptions)`. + * + * @example + * ```tsx + * const { useChat } = createChatHook(chatOptions) + * + * function Support() { + * const chat = useChat({ threadId: 'support-1' }) + * return + * } + * ``` + */ +export function createChatHook< + const TTools extends ReadonlyArray = any, + TSchema extends SchemaInput | undefined = undefined, + TContext = InferredClientContext, + const TInterrupts extends ReadonlyArray< + InterruptDefinition + > = readonly [], +>(options: UseChatOptions) { + function useChat( + overrides?: ChatHookOverrides, + ) { + if (!overrides) { + return useUnboundChat(options) + } + return useUnboundChat({ ...options, ...overrides }) + } + + return { useChat } +} diff --git a/packages/ai-solid/src/index.ts b/packages/ai-solid/src/index.ts index acf935e460..e0cdb8df9a 100644 --- a/packages/ai-solid/src/index.ts +++ b/packages/ai-solid/src/index.ts @@ -1,4 +1,5 @@ export { useChat } from './use-chat' +export { createChatHook } from './create-chat-hook' export { useByok } from './use-byok' export type { DeepPartial, diff --git a/packages/ai-solid/src/ui.ts b/packages/ai-solid/src/ui.ts new file mode 100644 index 0000000000..47288a6d37 --- /dev/null +++ b/packages/ai-solid/src/ui.ts @@ -0,0 +1,36 @@ +// Barrel entry for the `@tanstack/ai-solid/ui` subpath. +export { + Chat, + useChatContext, + type ChatUIComponents, + type ChatUIHost, + type InputProps, + type InterruptProps, + type LayoutProps, + type MessageProps, + type PartProps, + type ToolProps, +} from './chat-ui/create-ui' +export { + Chat as DeprecatedChat, + useChatContext as useDeprecatedChatContext, + type ChatProps, +} from './chat-ui/chat' +export { ChatMessages, type ChatMessagesProps } from './chat-ui/chat-messages' +export { + ChatMessage, + type ChatMessageProps, + type ToolCallRenderProps, +} from './chat-ui/chat-message' +export { + ChatInput, + type ChatInputProps, + type ChatInputRenderProps, +} from './chat-ui/chat-input' +export { + ToolApproval, + type ToolApprovalProps, + type ToolApprovalRenderProps, +} from './chat-ui/tool-approval' +export { TextPart, type TextPartProps } from './chat-ui/text-part' +export { ThinkingPart, type ThinkingPartProps } from './chat-ui/thinking-part' diff --git a/packages/ai-solid/tests/chat-ui/coexistence.test.ts b/packages/ai-solid/tests/chat-ui/coexistence.test.ts new file mode 100644 index 0000000000..2db3c28849 --- /dev/null +++ b/packages/ai-solid/tests/chat-ui/coexistence.test.ts @@ -0,0 +1,11 @@ +import { describe, expect, it } from 'vitest' +import { Chat, useChatContext } from '../../src/ui' +import { createChatHook } from '../../src/create-chat-hook' + +describe('public coexistence', () => { + it('exports Chat UI and the options-only createChatHook', () => { + expect(Chat).toBeDefined() + expect(useChatContext).toBeDefined() + expect(createChatHook).toBeDefined() + }) +}) diff --git a/packages/ai-solid/tests/chat-ui/create-ui-types.test.tsx b/packages/ai-solid/tests/chat-ui/create-ui-types.test.tsx new file mode 100644 index 0000000000..58208c0e1f --- /dev/null +++ b/packages/ai-solid/tests/chat-ui/create-ui-types.test.tsx @@ -0,0 +1,34 @@ +import { expectTypeOf, it } from 'vitest' +import type { + ChatUIComponents, + ChatUIHost, + ToolProps, +} from '../../src/chat-ui/create-ui' +import { chatOptions } from '../../../ai-client/tests/ui-fixtures' + +it('types tool props from chatOptions', () => { + type WeatherToolProps = ToolProps + expectTypeOf().toEqualTypeOf< + { city: string } | undefined + >() + + const components = { + layout: (props: { chat: ChatUIHost }) => { + expectTypeOf(props.chat.sendMessage).toBeFunction() + return null + }, + message: () => null, + parts: { fallback: () => null }, + tools: { + getWeather: () => null, + purchaseItem: () => null, + }, + interrupts: { + generic: { + choosePlan: () => null, + fallback: () => null, + }, + }, + } satisfies ChatUIComponents + void components +}) diff --git a/packages/ai-solid/tests/chat-ui/create-ui.test.tsx b/packages/ai-solid/tests/chat-ui/create-ui.test.tsx new file mode 100644 index 0000000000..bd74103390 --- /dev/null +++ b/packages/ai-solid/tests/chat-ui/create-ui.test.tsx @@ -0,0 +1,53 @@ +import { render } from 'solid-js/web' +import { describe, expect, it, vi } from 'vitest' +import { Chat } from '../../src/chat-ui/create-ui' +import type { ChatUIComponents, ChatUIHost } from '../../src/chat-ui/create-ui' +import { + chatOptions, + createSolidChatResult, + messageWithToolResults, + unknownToolMessage, +} from '../../../ai-client/tests/ui-fixtures' + +function host( + ...args: Parameters +): ChatUIHost { + return createSolidChatResult(...args) as unknown as ChatUIHost< + typeof chatOptions + > +} + +function renderHtml(node: () => unknown) { + const container = document.createElement('div') + render(node as () => import('solid-js').JSX.Element, container) + return container.innerHTML +} + +const baseComponents: ChatUIComponents = { + layout: (props) => <>{props.renderMessages()}, + message: (props) =>
{props.renderParts()}
, + parts: { fallback: (props) => {props.part.type} }, + tools: { + getWeather: (props) => {props.part.input?.city}, + purchaseItem: () => null, + }, + interrupts: { generic: { choosePlan: () => null, fallback: () => null } }, +} + +describe('Solid Chat', () => { + it('renders mapped tools from chat.messages', () => { + const chat = host([messageWithToolResults]) + expect( + renderHtml(() => ), + ).toContain('Paris') + }) + + it('warns once for a missing runtime key', () => { + const warn = vi.spyOn(console, 'warn').mockImplementation(() => undefined) + const chat = host([unknownToolMessage]) + renderHtml(() => ) + renderHtml(() => ) + expect(warn).toHaveBeenCalledTimes(1) + warn.mockRestore() + }) +}) diff --git a/packages/ai-solid-ui/tests/markdown-plugins.test.ts b/packages/ai-solid/tests/chat-ui/markdown-plugins.test.ts similarity index 98% rename from packages/ai-solid-ui/tests/markdown-plugins.test.ts rename to packages/ai-solid/tests/chat-ui/markdown-plugins.test.ts index 890d5f6974..8b060a0e67 100644 --- a/packages/ai-solid-ui/tests/markdown-plugins.test.ts +++ b/packages/ai-solid/tests/chat-ui/markdown-plugins.test.ts @@ -8,7 +8,7 @@ import { DEFAULT_REHYPE_PLUGINS_BEFORE_USER, DEFAULT_REMARK_PLUGINS, resolveMarkdownPlugins, -} from '../src/markdown-plugins' +} from '../../src/chat-ui/markdown-plugins' const userRemark = () => () => {} const userRehype = () => () => {} diff --git a/packages/ai-solid/tests/create-chat-hook-types.test.ts b/packages/ai-solid/tests/create-chat-hook-types.test.ts new file mode 100644 index 0000000000..174fb1dd94 --- /dev/null +++ b/packages/ai-solid/tests/create-chat-hook-types.test.ts @@ -0,0 +1,29 @@ +import { expectTypeOf, it } from 'vitest' +import { toolDefinition } from '@tanstack/ai' +import { createChatHook } from '../src/create-chat-hook' +import { useChat as useUnboundChat } from '../src/use-chat' + +it('bound useChat infers tool names from factory options', () => { + const check = () => { + const getWeather = toolDefinition({ + name: 'getWeather', + description: 'Look up weather', + }).client(() => ({ ok: true })) + const purchaseItem = toolDefinition({ + name: 'purchaseItem', + description: 'Buy an item', + }).client(() => ({ ok: true })) + + const chatOptions = { + connection: { connect: async function* () {} }, + tools: [getWeather, purchaseItem], + } + + const { useChat } = createChatHook(chatOptions) + const fromHook = useChat() + const fromUseChat = useUnboundChat(chatOptions) + + expectTypeOf(fromHook).toEqualTypeOf(fromUseChat) + } + void check +}) diff --git a/packages/ai-solid/tests/create-chat-hook.test.ts b/packages/ai-solid/tests/create-chat-hook.test.ts new file mode 100644 index 0000000000..f8e7bc5374 --- /dev/null +++ b/packages/ai-solid/tests/create-chat-hook.test.ts @@ -0,0 +1,54 @@ +import { renderHook } from '@solidjs/testing-library' +import { describe, expect, it } from 'vitest' +import { createChatHook } from '../src/create-chat-hook' +import { createMockConnectionAdapter } from './test-utils' +import type { UIMessage } from '../src/types' + +describe('createChatHook', () => { + it('creates a chat from factory options', () => { + const initialMessages: Array = [ + { + id: 'msg-1', + role: 'user', + parts: [{ type: 'text', content: 'Hello' }], + createdAt: new Date(), + }, + ] + const { useChat } = createChatHook({ + connection: createMockConnectionAdapter(), + initialMessages, + }) + + const chat = renderHook(() => useChat()).result + + expect(chat.messages()).toEqual(initialMessages) + }) + + it('merges per-call instance overrides', () => { + const { useChat } = createChatHook({ + connection: createMockConnectionAdapter(), + initialMessages: [ + { + id: 'factory', + role: 'user', + parts: [{ type: 'text', content: 'factory' }], + createdAt: new Date(), + }, + ], + }) + + const overrideMessages: Array = [ + { + id: 'override', + role: 'user', + parts: [{ type: 'text', content: 'override' }], + createdAt: new Date(), + }, + ] + const chat = renderHook(() => + useChat({ initialMessages: overrideMessages, threadId: 'thread-1' }), + ).result + + expect(chat.messages()).toEqual(overrideMessages) + }) +}) diff --git a/packages/ai-solid/tsconfig.json b/packages/ai-solid/tsconfig.json index 67ca59b7b1..a8bbc88c69 100644 --- a/packages/ai-solid/tsconfig.json +++ b/packages/ai-solid/tsconfig.json @@ -4,7 +4,8 @@ "outDir": "dist", "jsx": "preserve", "jsxImportSource": "solid-js", - "lib": ["ES2022", "DOM"] + "lib": ["ES2022", "DOM"], + "rootDir": "../.." }, "include": ["src", "tests"], "exclude": ["node_modules", "dist"] diff --git a/packages/ai-solid/vitest.config.ts b/packages/ai-solid/vitest.config.ts index 607c52f248..1b7841f0ab 100644 --- a/packages/ai-solid/vitest.config.ts +++ b/packages/ai-solid/vitest.config.ts @@ -1,10 +1,12 @@ import { defineConfig } from 'vitest/config' import { resolve } from 'path' import { fileURLToPath } from 'url' +import solid from 'vite-plugin-solid' const __dirname = fileURLToPath(new URL('.', import.meta.url)) export default defineConfig({ + plugins: [solid()], test: { globals: true, environment: 'jsdom', diff --git a/packages/ai-svelte-ui/LICENSE b/packages/ai-svelte-ui/LICENSE deleted file mode 100644 index 308cb68dc0..0000000000 --- a/packages/ai-svelte-ui/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2025 Tanner Linsley - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/packages/ai-svelte-ui/README.md b/packages/ai-svelte-ui/README.md deleted file mode 100644 index 4c4ae78c21..0000000000 --- a/packages/ai-svelte-ui/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# @tanstack/ai-svelte-ui - -Headless Svelte 5 chat UI. Call `createChatUI(chatOptions)` and supply every visible component. - -See https://tanstack.com/ai/latest/docs/ui/svelte diff --git a/packages/ai-svelte-ui/package.json b/packages/ai-svelte-ui/package.json deleted file mode 100644 index 4dc0dcfe7d..0000000000 --- a/packages/ai-svelte-ui/package.json +++ /dev/null @@ -1,70 +0,0 @@ -{ - "name": "@tanstack/ai-svelte-ui", - "version": "0.1.0", - "description": "Headless Svelte 5 components for building TanStack AI chat interfaces with typed createChatUI adapters.", - "author": "Tanner Linsley", - "license": "MIT", - "homepage": "https://tanstack.com/ai", - "repository": { - "type": "git", - "url": "git+https://github.com/TanStack/ai.git", - "directory": "packages/ai-svelte-ui" - }, - "bugs": { - "url": "https://github.com/TanStack/ai/issues" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" - }, - "type": "module", - "svelte": "./dist/index.js", - "module": "./dist/index.js", - "types": "./dist/index.d.ts", - "exports": { - ".": { - "types": "./dist/index.d.ts", - "svelte": "./dist/index.js", - "import": "./dist/index.js" - } - }, - "files": [ - "dist", - "src" - ], - "scripts": { - "clean": "rm -rf ./dist", - "test:oxlint": "oxlint src --type-aware", - "test:lib": "vitest run", - "test:lib:dev": "pnpm test:lib --watch", - "test:types": "svelte-check", - "test:build": "publint --strict", - "build": "svelte-package -i src -o dist" - }, - "keywords": [ - "ai", - "tanstack", - "svelte", - "svelte5", - "chat", - "ui", - "headless" - ], - "dependencies": { - "@tanstack/ai-client": "workspace:^" - }, - "peerDependencies": { - "@tanstack/ai-svelte": "workspace:^", - "svelte": "^5.0.0" - }, - "devDependencies": { - "@sveltejs/package": "^2.3.10", - "@sveltejs/vite-plugin-svelte": "^7.3.0", - "@types/node": "^24.10.1", - "@vitest/coverage-v8": "4.1.10", - "svelte": "^5.56.9", - "svelte-check": "^4.2.0", - "typescript": "5.9.3", - "vite": "^8.2.1" - } -} diff --git a/packages/ai-svelte-ui/src/index.ts b/packages/ai-svelte-ui/src/index.ts deleted file mode 100644 index 57dd9560f9..0000000000 --- a/packages/ai-svelte-ui/src/index.ts +++ /dev/null @@ -1,20 +0,0 @@ -export { createChatUI, getUIContext } from './create-ui' -export type { - ChatUIComponents, - ChatUIHost, - InputProps, - InterruptProps, - LayoutProps, - MessageProps, - PartProps, - ToolProps, - UIDescriptor, -} from './create-ui' - -export { default as UIChat } from './ui-chat.svelte' -export { default as UIProvider } from './ui-provider.svelte' -export { default as UIMessages } from './ui-messages.svelte' -export { default as UIMessage } from './ui-message.svelte' -export { default as UIPart } from './ui-part.svelte' -export { default as UIInterrupts } from './ui-interrupts.svelte' -export { default as UIInterrupt } from './ui-interrupt.svelte' diff --git a/packages/ai-svelte-ui/svelte.config.js b/packages/ai-svelte-ui/svelte.config.js deleted file mode 100644 index bfa43e59d7..0000000000 --- a/packages/ai-svelte-ui/svelte.config.js +++ /dev/null @@ -1,11 +0,0 @@ -import { vitePreprocess } from '@sveltejs/vite-plugin-svelte' - -/** @type {import('@sveltejs/package').Config} */ -const config = { - preprocess: vitePreprocess(), - compilerOptions: { - runes: true, - }, -} - -export default config diff --git a/packages/ai-svelte-ui/tests/create-ui-types.test.ts b/packages/ai-svelte-ui/tests/create-ui-types.test.ts deleted file mode 100644 index 8c7e3d9e5c..0000000000 --- a/packages/ai-svelte-ui/tests/create-ui-types.test.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { describe, expectTypeOf, it } from 'vitest' -import { createChatUI } from '../src/create-ui' -import type { ChatUIHost, InterruptProps, ToolProps } from '../src/create-ui' -import { chatOptions } from '../../ai-client/tests/ui-fixtures' - -describe('Svelte createChatUI types', () => { - it('infers defineComponents from a bare options variable', () => { - type PurchaseInterrupt = InterruptProps - expectTypeOf< - PurchaseInterrupt['interrupt']['toolName'] - >().toEqualTypeOf<'purchaseItem'>() - type ChoosePlan = InterruptProps - expectTypeOf().toEqualTypeOf< - { title: string } | undefined - >() - type PurchaseTool = ToolProps - expectTypeOf< - NonNullable['originalArgs'] - >().toEqualTypeOf<{ item: string }>() - - const ui = createChatUI(chatOptions) - expectTypeOf(ui.useChat).returns.toEqualTypeOf< - ChatUIHost - >() - ui.defineComponents({ - layout: {}, - message: {}, - parts: { fallback: {} }, - tools: { - getWeather: {}, - purchaseItem: {}, - // @ts-expect-error This tool is not in chatOptions. - unknownTool: {}, - }, - interrupts: { - generic: { - choosePlan: {}, - }, - }, - }) - - ui.defineComponents({ - layout: {}, - message: {}, - parts: { fallback: {} }, - // @ts-expect-error Every configured tool needs a component. - tools: { - getWeather: {}, - }, - interrupts: { - generic: { - choosePlan: {}, - }, - }, - }) - - ui.defineComponents({ - layout: {}, - message: {}, - parts: { fallback: {} }, - tools: { - getWeather: {}, - purchaseItem: {}, - }, - interrupts: { - // @ts-expect-error Every registered interrupt id needs a component. - generic: { - fallback: {}, - }, - }, - }) - - const untyped = createChatUI({}) - untyped.defineComponents({ - layout: {}, - message: {}, - parts: { fallback: {} }, - }) - expectTypeOf(ui.defineComponents).toBeFunction() - }) -}) diff --git a/packages/ai-svelte-ui/tests/fixtures/automatic.svelte b/packages/ai-svelte-ui/tests/fixtures/automatic.svelte deleted file mode 100644 index bbf61eedd8..0000000000 --- a/packages/ai-svelte-ui/tests/fixtures/automatic.svelte +++ /dev/null @@ -1,6 +0,0 @@ - - - diff --git a/packages/ai-svelte-ui/tests/fixtures/manual.svelte b/packages/ai-svelte-ui/tests/fixtures/manual.svelte deleted file mode 100644 index 1d01724890..0000000000 --- a/packages/ai-svelte-ui/tests/fixtures/manual.svelte +++ /dev/null @@ -1,12 +0,0 @@ - - - - - {#snippet children(messages)} - {messages.length} - {/snippet} - - diff --git a/packages/ai-svelte-ui/tsconfig.json b/packages/ai-svelte-ui/tsconfig.json deleted file mode 100644 index ad7559ac3d..0000000000 --- a/packages/ai-svelte-ui/tsconfig.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "extends": "../../tsconfig.base.json", - "compilerOptions": { - "outDir": "dist", - "lib": ["ES2022", "DOM"], - "rootDir": "../.." - }, - "include": ["src", "tests", "**/*.svelte"], - "exclude": ["node_modules", "dist"] -} diff --git a/packages/ai-svelte-ui/vite.config.ts b/packages/ai-svelte-ui/vite.config.ts deleted file mode 100644 index 6e8a02a816..0000000000 --- a/packages/ai-svelte-ui/vite.config.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { defineConfig, mergeConfig } from 'vitest/config' -import { svelte } from '@sveltejs/vite-plugin-svelte' -import { tanstackViteConfig } from '@tanstack/vite-config' -import packageJson from './package.json' - -const config = defineConfig({ - plugins: [ - svelte({ - compilerOptions: { - runes: true, - }, - }), - ], - test: { - name: packageJson.name, - dir: './', - watch: false, - globals: true, - environment: 'node', - include: ['tests/**/*.test.ts'], - coverage: { - provider: 'v8', - reporter: ['text', 'json', 'html', 'lcov'], - exclude: ['node_modules/', 'dist/', 'tests/'], - include: ['src/**/*.ts', 'src/**/*.svelte'], - }, - }, -}) - -export default mergeConfig( - config, - tanstackViteConfig({ - entry: ['./src/index.ts'], - srcDir: './src', - cjs: false, - }), -) diff --git a/packages/ai-svelte/package.json b/packages/ai-svelte/package.json index 1c9f6a4b88..ea592f4ce0 100644 --- a/packages/ai-svelte/package.json +++ b/packages/ai-svelte/package.json @@ -26,6 +26,11 @@ "types": "./dist/index.d.ts", "svelte": "./dist/index.js", "import": "./dist/index.js" + }, + "./ui": { + "types": "./dist/ui.d.ts", + "svelte": "./dist/ui.js", + "import": "./dist/ui.js" } }, "files": [ diff --git a/packages/ai-svelte-ui/src/create-ui.ts b/packages/ai-svelte/src/chat-ui/create-ui.ts similarity index 94% rename from packages/ai-svelte-ui/src/create-ui.ts rename to packages/ai-svelte/src/chat-ui/create-ui.ts index 2a84563098..13c9b52883 100644 --- a/packages/ai-svelte-ui/src/create-ui.ts +++ b/packages/ai-svelte/src/chat-ui/create-ui.ts @@ -28,7 +28,7 @@ import type { ToolResultPart, UIMessage, } from '@tanstack/ai-client' -import type { CreateChatReturn } from '@tanstack/ai-svelte' +import type { CreateChatReturn } from '../types' export type ChatUIHost = CreateChatReturn< ChatUIToolsOf, @@ -81,10 +81,8 @@ export type ChatUIComponents = { export type UIDescriptor = { key: symbol warn: (key: string, message: string) => void - defineComponents: ( - components: ChatUIComponents, - ) => ChatUIComponents - useChat: () => ChatUIHost + components: ChatUIComponents + useChatContext: () => ChatUIHost } export type UIContextValue = { @@ -113,18 +111,15 @@ function createWarnOnce() { } } -export function createChatUI( - options: TOptions, +export function createDescriptor( + components: ChatUIComponents, ): UIDescriptor { - void options const ui: UIRuntime = { key: Symbol('tanstack-ai-ui-chat'), componentsKey: Symbol('tanstack-ai-ui-components'), warn: createWarnOnce(), - defineComponents(components) { - return components - }, - useChat() { + components, + useChatContext() { return getChatContext(ui) }, } diff --git a/packages/ai-svelte-ui/src/selected-part.svelte b/packages/ai-svelte/src/chat-ui/selected-part.svelte similarity index 100% rename from packages/ai-svelte-ui/src/selected-part.svelte rename to packages/ai-svelte/src/chat-ui/selected-part.svelte diff --git a/packages/ai-svelte-ui/src/ui-chat.svelte b/packages/ai-svelte/src/chat-ui/ui-chat.svelte similarity index 70% rename from packages/ai-svelte-ui/src/ui-chat.svelte rename to packages/ai-svelte/src/chat-ui/ui-chat.svelte index 7a99feae29..471ed65cdf 100644 --- a/packages/ai-svelte-ui/src/ui-chat.svelte +++ b/packages/ai-svelte/src/chat-ui/ui-chat.svelte @@ -2,26 +2,23 @@ import UIProvider from './ui-provider.svelte' import UIMessages from './ui-messages.svelte' import UIInterrupts from './ui-interrupts.svelte' - import type { ChatUIComponents, UIDescriptor } from './create-ui' + import { createDescriptor, type ChatUIComponents } from './create-ui' let { - ui, chat, components, }: { - ui: UIDescriptor - // ponytail: Svelte components are not generic, so ChatUIHost cannot - // accept a typed createChat() host with tool-approval interrupts. chat: any components: ChatUIComponents } = $props() + const ui = createDescriptor(components) const Layout = $derived(components.layout as any) const Input = $derived(components.input as any) - + {#snippet messages()} {/snippet} @@ -30,7 +27,7 @@ {/snippet} {#snippet input()} {#if Input} - + {/if} {/snippet} diff --git a/packages/ai-svelte-ui/src/ui-interrupt.svelte b/packages/ai-svelte/src/chat-ui/ui-interrupt.svelte similarity index 100% rename from packages/ai-svelte-ui/src/ui-interrupt.svelte rename to packages/ai-svelte/src/chat-ui/ui-interrupt.svelte diff --git a/packages/ai-svelte-ui/src/ui-interrupts.svelte b/packages/ai-svelte/src/chat-ui/ui-interrupts.svelte similarity index 100% rename from packages/ai-svelte-ui/src/ui-interrupts.svelte rename to packages/ai-svelte/src/chat-ui/ui-interrupts.svelte diff --git a/packages/ai-svelte-ui/src/ui-message.svelte b/packages/ai-svelte/src/chat-ui/ui-message.svelte similarity index 100% rename from packages/ai-svelte-ui/src/ui-message.svelte rename to packages/ai-svelte/src/chat-ui/ui-message.svelte diff --git a/packages/ai-svelte-ui/src/ui-messages.svelte b/packages/ai-svelte/src/chat-ui/ui-messages.svelte similarity index 100% rename from packages/ai-svelte-ui/src/ui-messages.svelte rename to packages/ai-svelte/src/chat-ui/ui-messages.svelte diff --git a/packages/ai-svelte-ui/src/ui-part.svelte b/packages/ai-svelte/src/chat-ui/ui-part.svelte similarity index 100% rename from packages/ai-svelte-ui/src/ui-part.svelte rename to packages/ai-svelte/src/chat-ui/ui-part.svelte diff --git a/packages/ai-svelte-ui/src/ui-provider.svelte b/packages/ai-svelte/src/chat-ui/ui-provider.svelte similarity index 83% rename from packages/ai-svelte-ui/src/ui-provider.svelte rename to packages/ai-svelte/src/chat-ui/ui-provider.svelte index 671f0c863e..3044868494 100644 --- a/packages/ai-svelte-ui/src/ui-provider.svelte +++ b/packages/ai-svelte/src/chat-ui/ui-provider.svelte @@ -15,14 +15,14 @@ // ponytail: Svelte components are not generic, so ChatUIHost cannot // accept a typed createChat() host with tool-approval interrupts. chat: any - components: ChatUIComponents + components?: ChatUIComponents children?: import('svelte').Snippet } = $props() setUIContext({ ui, chat, - components: components as never, + components: (components ?? ui.components) as never, }) diff --git a/packages/ai-svelte/src/create-chat-hook.ts b/packages/ai-svelte/src/create-chat-hook.ts new file mode 100644 index 0000000000..1573516530 --- /dev/null +++ b/packages/ai-svelte/src/create-chat-hook.ts @@ -0,0 +1,67 @@ +import { createChat as createUnboundChat } from './create-chat.svelte' +import type { + AnyClientTool, + InterruptDefinition, + SchemaInput, +} from '@tanstack/ai' +import type { InferredClientContext } from '@tanstack/ai-client' +import type { CreateChatOptions } from './types' + +type ChatHookOverrides< + TTools extends ReadonlyArray, + TSchema extends SchemaInput | undefined, + TContext, + TInterrupts extends ReadonlyArray>, +> = { + threadId?: string + live?: boolean + forwardedProps?: Record + body?: Record + initialMessages?: CreateChatOptions< + TTools, + TSchema, + TContext, + TInterrupts + >['initialMessages'] +} + +/** + * Bind chat options once at module scope. The returned `createChat` + * function creates a chat instance from those options. + * + * Pass per-call overrides for instance keys such as `threadId`, + * `initialMessages`, `live`, and `forwardedProps`. Do not change `tools`, + * `interrupts`, or `outputSchema` here. Those stay on the factory options. + * + * Rename the function at the call site if you already import `createChat` from + * `@tanstack/ai-svelte`: `const { createChat: createAppChat } = createChatHook(chatOptions)`. + * + * @example + * ```svelte + * + * ``` + */ +export function createChatHook< + const TTools extends ReadonlyArray = any, + TSchema extends SchemaInput | undefined = undefined, + TContext = InferredClientContext, + const TInterrupts extends ReadonlyArray< + InterruptDefinition + > = readonly [], +>(options: CreateChatOptions) { + function createChat( + overrides?: ChatHookOverrides, + ) { + if (!overrides) { + return createUnboundChat(options) + } + return createUnboundChat({ ...options, ...overrides }) + } + + return { createChat } +} diff --git a/packages/ai-svelte/src/index.ts b/packages/ai-svelte/src/index.ts index eef9ea27d0..cfff999d40 100644 --- a/packages/ai-svelte/src/index.ts +++ b/packages/ai-svelte/src/index.ts @@ -1,4 +1,5 @@ export { createChat } from './create-chat.svelte' +export { createChatHook } from './create-chat-hook' export { createByok } from './create-byok.svelte' export type { CreateChatOptions, diff --git a/packages/ai-svelte/src/ui.ts b/packages/ai-svelte/src/ui.ts new file mode 100644 index 0000000000..286999c98c --- /dev/null +++ b/packages/ai-svelte/src/ui.ts @@ -0,0 +1,20 @@ +// Barrel entry for the `@tanstack/ai-svelte/ui` subpath. +export { getUIContext } from './chat-ui/create-ui' +export type { + ChatUIComponents, + ChatUIHost, + InputProps, + InterruptProps, + LayoutProps, + MessageProps, + PartProps, + ToolProps, + UIDescriptor, +} from './chat-ui/create-ui' +export { default as Chat } from './chat-ui/ui-chat.svelte' +export { default as UIProvider } from './chat-ui/ui-provider.svelte' +export { default as UIMessages } from './chat-ui/ui-messages.svelte' +export { default as UIMessage } from './chat-ui/ui-message.svelte' +export { default as UIPart } from './chat-ui/ui-part.svelte' +export { default as UIInterrupts } from './chat-ui/ui-interrupts.svelte' +export { default as UIInterrupt } from './chat-ui/ui-interrupt.svelte' diff --git a/packages/ai-svelte/tests/chat-ui/create-ui-types.test.ts b/packages/ai-svelte/tests/chat-ui/create-ui-types.test.ts new file mode 100644 index 0000000000..263b5e9b77 --- /dev/null +++ b/packages/ai-svelte/tests/chat-ui/create-ui-types.test.ts @@ -0,0 +1,29 @@ +import { describe, expectTypeOf, it } from 'vitest' +import type { ChatUIComponents, ToolProps } from '../../src/chat-ui/create-ui' +import { chatOptions } from '../../../ai-client/tests/ui-fixtures' + +describe('Svelte Chat UI types', () => { + it('types tool props from chatOptions', () => { + type WeatherToolProps = ToolProps + expectTypeOf().toEqualTypeOf< + { city: string } | undefined + >() + + const components = { + layout: {}, + message: {}, + parts: { fallback: {} }, + tools: { + getWeather: {}, + purchaseItem: {}, + }, + interrupts: { + generic: { + choosePlan: {}, + fallback: {}, + }, + }, + } satisfies ChatUIComponents + void components + }) +}) diff --git a/packages/ai-svelte-ui/tests/create-ui.test.ts b/packages/ai-svelte/tests/chat-ui/create-ui.test.ts similarity index 51% rename from packages/ai-svelte-ui/tests/create-ui.test.ts rename to packages/ai-svelte/tests/chat-ui/create-ui.test.ts index 1eb6813a58..23724d16f4 100644 --- a/packages/ai-svelte-ui/tests/create-ui.test.ts +++ b/packages/ai-svelte/tests/chat-ui/create-ui.test.ts @@ -1,11 +1,10 @@ +/** @vitest-environment node */ import { render } from 'svelte/server' import { describe, expect, it } from 'vitest' import Automatic from './fixtures/automatic.svelte' -import Manual from './fixtures/manual.svelte' -describe('Svelte createChatUI', () => { - it('renders automatic and snippet traversal', () => { +describe('Svelte Chat', () => { + it('renders mapped tools from chat.messages', () => { expect(render(Automatic).body).toContain('Paris') - expect(render(Manual).body).toContain('1') }) }) diff --git a/packages/ai-svelte/tests/chat-ui/fixtures/automatic.svelte b/packages/ai-svelte/tests/chat-ui/fixtures/automatic.svelte new file mode 100644 index 0000000000..91e89ecb1c --- /dev/null +++ b/packages/ai-svelte/tests/chat-ui/fixtures/automatic.svelte @@ -0,0 +1,6 @@ + + + diff --git a/packages/ai-svelte-ui/tests/fixtures/empty.svelte b/packages/ai-svelte/tests/chat-ui/fixtures/empty.svelte similarity index 100% rename from packages/ai-svelte-ui/tests/fixtures/empty.svelte rename to packages/ai-svelte/tests/chat-ui/fixtures/empty.svelte diff --git a/packages/ai-svelte-ui/tests/fixtures/fallback.svelte b/packages/ai-svelte/tests/chat-ui/fixtures/fallback.svelte similarity index 100% rename from packages/ai-svelte-ui/tests/fixtures/fallback.svelte rename to packages/ai-svelte/tests/chat-ui/fixtures/fallback.svelte diff --git a/packages/ai-svelte-ui/tests/fixtures/fixture-data.ts b/packages/ai-svelte/tests/chat-ui/fixtures/fixture-data.ts similarity index 75% rename from packages/ai-svelte-ui/tests/fixtures/fixture-data.ts rename to packages/ai-svelte/tests/chat-ui/fixtures/fixture-data.ts index fae2c5074f..0e94b50129 100644 --- a/packages/ai-svelte-ui/tests/fixtures/fixture-data.ts +++ b/packages/ai-svelte/tests/chat-ui/fixtures/fixture-data.ts @@ -1,19 +1,15 @@ -import { createChatUI } from '../../src/create-ui' import { chatOptions, createSvelteChatResult, messageWithToolResults, -} from '../../../ai-client/tests/ui-fixtures' +} from '../../../../ai-client/tests/ui-fixtures' import Layout from './layout.svelte' import Message from './message.svelte' import Weather from './weather.svelte' import Fallback from './fallback.svelte' import Empty from './empty.svelte' -export const ui = createChatUI(chatOptions) -export const chat: ReturnType = - createSvelteChatResult([messageWithToolResults]) -export const components = ui.defineComponents({ +export const components = { layout: Layout, message: Message, parts: { fallback: Fallback }, @@ -22,4 +18,7 @@ export const components = ui.defineComponents({ purchaseItem: Empty, }, interrupts: { generic: { choosePlan: Empty, fallback: Empty } }, -}) +} +export const chat: ReturnType = + createSvelteChatResult([messageWithToolResults]) +void chatOptions diff --git a/packages/ai-svelte-ui/tests/fixtures/layout.svelte b/packages/ai-svelte/tests/chat-ui/fixtures/layout.svelte similarity index 58% rename from packages/ai-svelte-ui/tests/fixtures/layout.svelte rename to packages/ai-svelte/tests/chat-ui/fixtures/layout.svelte index c12834c129..7ca62b3710 100644 --- a/packages/ai-svelte-ui/tests/fixtures/layout.svelte +++ b/packages/ai-svelte/tests/chat-ui/fixtures/layout.svelte @@ -2,7 +2,10 @@ let { messages, }: { + chat?: unknown messages?: import('svelte').Snippet + interrupts?: import('svelte').Snippet + input?: import('svelte').Snippet } = $props() diff --git a/packages/ai-svelte-ui/tests/fixtures/message.svelte b/packages/ai-svelte/tests/chat-ui/fixtures/message.svelte similarity index 100% rename from packages/ai-svelte-ui/tests/fixtures/message.svelte rename to packages/ai-svelte/tests/chat-ui/fixtures/message.svelte diff --git a/packages/ai-svelte-ui/tests/fixtures/weather.svelte b/packages/ai-svelte/tests/chat-ui/fixtures/weather.svelte similarity index 100% rename from packages/ai-svelte-ui/tests/fixtures/weather.svelte rename to packages/ai-svelte/tests/chat-ui/fixtures/weather.svelte diff --git a/packages/ai-svelte/tests/create-chat-hook-types.test.ts b/packages/ai-svelte/tests/create-chat-hook-types.test.ts new file mode 100644 index 0000000000..9f655a3986 --- /dev/null +++ b/packages/ai-svelte/tests/create-chat-hook-types.test.ts @@ -0,0 +1,29 @@ +import { expectTypeOf, it } from 'vitest' +import { toolDefinition } from '@tanstack/ai' +import { createChatHook } from '../src/create-chat-hook' +import { createChat as createUnboundChat } from '../src/create-chat.svelte' + +it('bound createChat infers tool names from factory options', () => { + const check = () => { + const getWeather = toolDefinition({ + name: 'getWeather', + description: 'Look up weather', + }).client(() => ({ ok: true })) + const purchaseItem = toolDefinition({ + name: 'purchaseItem', + description: 'Buy an item', + }).client(() => ({ ok: true })) + + const chatOptions = { + connection: { connect: async function* () {} }, + tools: [getWeather, purchaseItem], + } + + const { createChat } = createChatHook(chatOptions) + const fromHook = createChat() + const fromCreateChat = createUnboundChat(chatOptions) + + expectTypeOf(fromHook).toEqualTypeOf(fromCreateChat) + } + void check +}) diff --git a/packages/ai-svelte/tests/create-chat-hook.test.ts b/packages/ai-svelte/tests/create-chat-hook.test.ts new file mode 100644 index 0000000000..d9905496c7 --- /dev/null +++ b/packages/ai-svelte/tests/create-chat-hook.test.ts @@ -0,0 +1,53 @@ +import { describe, expect, it } from 'vitest' +import { createChatHook } from '../src/create-chat-hook' +import { createMockConnectionAdapter } from './test-utils' + +describe('createChatHook', () => { + it('creates a chat from factory options', () => { + const initialMessages = [ + { + id: 'msg-1', + role: 'user' as const, + parts: [{ type: 'text' as const, content: 'Hello' }], + createdAt: new Date(), + }, + ] + const { createChat } = createChatHook({ + connection: createMockConnectionAdapter(), + initialMessages, + }) + + const chat = createChat() + + expect(chat.messages).toEqual(initialMessages) + }) + + it('merges per-call instance overrides', () => { + const { createChat } = createChatHook({ + connection: createMockConnectionAdapter(), + initialMessages: [ + { + id: 'factory', + role: 'user' as const, + parts: [{ type: 'text' as const, content: 'factory' }], + createdAt: new Date(), + }, + ], + }) + + const overrideMessages = [ + { + id: 'override', + role: 'user' as const, + parts: [{ type: 'text' as const, content: 'override' }], + createdAt: new Date(), + }, + ] + const chat = createChat({ + initialMessages: overrideMessages, + threadId: 'thread-1', + }) + + expect(chat.messages).toEqual(overrideMessages) + }) +}) diff --git a/packages/ai-svelte/tsconfig.json b/packages/ai-svelte/tsconfig.json index c408026ae3..ad7559ac3d 100644 --- a/packages/ai-svelte/tsconfig.json +++ b/packages/ai-svelte/tsconfig.json @@ -2,7 +2,8 @@ "extends": "../../tsconfig.base.json", "compilerOptions": { "outDir": "dist", - "lib": ["ES2022", "DOM"] + "lib": ["ES2022", "DOM"], + "rootDir": "../.." }, "include": ["src", "tests", "**/*.svelte"], "exclude": ["node_modules", "dist"] diff --git a/packages/ai-vue-ui/CHANGELOG.md b/packages/ai-vue-ui/CHANGELOG.md deleted file mode 100644 index ffde61e35c..0000000000 --- a/packages/ai-vue-ui/CHANGELOG.md +++ /dev/null @@ -1,677 +0,0 @@ -# @tanstack/ai-vue-ui - -## 0.2.40 - -### Patch Changes - -- Updated dependencies [[`b7ebcb0`](https://github.com/TanStack/ai/commit/b7ebcb0bbe63e425facb5e38f138bd0cd36637dd)]: - - @tanstack/ai-vue@0.19.0 - -## 0.2.39 - -### Patch Changes - -- Updated dependencies [[`32e62ab`](https://github.com/TanStack/ai/commit/32e62ab8b7dc6a8a13ca3851c8925ab806e08f29)]: - - @tanstack/ai-vue@0.18.0 - -## 0.2.38 - -### Patch Changes - -- Updated dependencies [[`f6b9d89`](https://github.com/TanStack/ai/commit/f6b9d89e7365f0cb30a41cf3a46b7dd237fbc73c), [`ecd12a4`](https://github.com/TanStack/ai/commit/ecd12a408987bc75649c21aada6948282a2a66dd)]: - - @tanstack/ai-vue@0.17.0 - -## 0.2.37 - -### Patch Changes - -- [#1071](https://github.com/TanStack/ai/pull/1071) [`ea9c077`](https://github.com/TanStack/ai/commit/ea9c07724bd6992480238a699fbb18835eab743e) - fix: publish internal dependency ranges as `^x.y.z` instead of exact pins - - Internal dependencies on other TanStack AI packages used `workspace:*` in - `dependencies` and `peerDependencies`. pnpm rewrites that to an **exact** version - at publish time, so a released package asked for e.g. `@tanstack/ai-utils@0.4.0` - rather than `^0.4.0`. - - Two consequences for consumers: - - **Duplicate copies.** An exact pin cannot dedupe. Installing a newer - `@tanstack/ai` alongside a package pinned to the previous patch produced two - copies in the tree, which breaks `instanceof` checks and module-level state, - and inflates bundles. - - **Unsatisfiable peers.** An exactly pinned `peerDependency` conflicts the - moment the internal package ships its next patch, forcing consumers into - overrides or `--legacy-peer-deps`. - - These fields now use `workspace:^`, which publishes as `^x.y.z`. Every package - here is still `0.x`, so `^0.43.1` resolves to `0.43.x` only — patches dedupe - cleanly and no breaking minor is ever pulled in. - - `devDependencies` deliberately keep `workspace:*`: they are never published, and - `*` correctly means "always build against the local copy". - -- Updated dependencies [[`ea9c077`](https://github.com/TanStack/ai/commit/ea9c07724bd6992480238a699fbb18835eab743e)]: - - @tanstack/ai-vue@0.16.2 - -## 0.2.36 - -### Patch Changes - -- Updated dependencies []: - - @tanstack/ai-vue@0.16.1 - -## 0.2.35 - -### Patch Changes - -- Updated dependencies [[`7499171`](https://github.com/TanStack/ai/commit/74991716aea4d90a5d0363676a1e3349689a48e8)]: - - @tanstack/ai-vue@0.16.0 - -## 0.2.34 - -### Patch Changes - -- Updated dependencies []: - - @tanstack/ai-vue@0.15.1 - -## 0.2.33 - -### Patch Changes - -- Updated dependencies [[`35946e3`](https://github.com/TanStack/ai/commit/35946e3c39fb123c133ebe662f8e2cf0139f2b8c)]: - - @tanstack/ai-vue@0.15.0 - -## 0.2.32 - -### Patch Changes - -- Updated dependencies [[`f830d9e`](https://github.com/TanStack/ai/commit/f830d9e7a41e3554c424c3e41ba847dfd1577589)]: - - @tanstack/ai-vue@0.14.4 - -## 0.2.31 - -### Patch Changes - -- Updated dependencies []: - - @tanstack/ai-vue@0.14.3 - -## 0.2.30 - -### Patch Changes - -- Updated dependencies []: - - @tanstack/ai-vue@0.14.2 - -## 0.2.29 - -### Patch Changes - -- Updated dependencies []: - - @tanstack/ai-vue@0.14.1 - -## 0.2.28 - -### Patch Changes - -- Updated dependencies [[`33acdd4`](https://github.com/TanStack/ai/commit/33acdd4df4aef13d594700d9b52087252091bd40), [`c22c663`](https://github.com/TanStack/ai/commit/c22c6632fdca761033cb9c4273bf61fc8ce86662)]: - - @tanstack/ai-vue@0.14.0 - -## 0.2.27 - -### Patch Changes - -- [#844](https://github.com/TanStack/ai/pull/844) [`a6cceba`](https://github.com/TanStack/ai/commit/a6cceba4812e7e986183ee856112fcf5f8fa12ff) - Republish all packages with their compiled `dist/` output. - - Releases `0.33.0`–`0.36.0` were published without a `dist/` directory: the - release workflow relied on an Nx-cached `build` whose outputs were not - materialized to disk before `changeset publish` packed the tarballs, and - `files: ["dist"]` silently includes nothing when `dist/` is absent. The - published packages therefore contained only `src/`, so every export - (`./dist/esm/*.js`) resolved to a missing file and the packages were - uninstallable. - - The publish step now runs a fresh, cache-bypassing build of all packages - immediately before publishing, guaranteeing compiled artifacts are present in - every tarball. - -- Updated dependencies [[`a6cceba`](https://github.com/TanStack/ai/commit/a6cceba4812e7e986183ee856112fcf5f8fa12ff)]: - - @tanstack/ai-vue@0.13.15 - -## 0.2.26 - -### Patch Changes - -- Updated dependencies []: - - @tanstack/ai-vue@0.13.14 - -## 0.2.25 - -### Patch Changes - -- Updated dependencies []: - - @tanstack/ai-vue@0.13.13 - -## 0.2.24 - -### Patch Changes - -- Updated dependencies []: - - @tanstack/ai-vue@0.13.12 - -## 0.2.23 - -### Patch Changes - -- Updated dependencies []: - - @tanstack/ai-vue@0.13.11 - -## 0.2.22 - -### Patch Changes - -- Updated dependencies []: - - @tanstack/ai-vue@0.13.10 - -## 0.2.21 - -### Patch Changes - -- Updated dependencies []: - - @tanstack/ai-vue@0.13.9 - -## 0.2.20 - -### Patch Changes - -- Updated dependencies []: - - @tanstack/ai-vue@0.13.8 - -## 0.2.19 - -### Patch Changes - -- Updated dependencies []: - - @tanstack/ai-vue@0.13.7 - -## 0.2.18 - -### Patch Changes - -- [#769](https://github.com/TanStack/ai/pull/769) [`1d1bb52`](https://github.com/TanStack/ai/commit/1d1bb5219a38d9718cc926148e93fc27d5d2305b) - Add repository metadata (`homepage`, `bugs`, `funding`), fix `repository.directory` to point at each package, and include an MIT `LICENSE` file in every published package. - -- Updated dependencies [[`1d1bb52`](https://github.com/TanStack/ai/commit/1d1bb5219a38d9718cc926148e93fc27d5d2305b)]: - - @tanstack/ai-vue@0.13.6 - -## 0.2.17 - -### Patch Changes - -- Updated dependencies []: - - @tanstack/ai-vue@0.13.5 - -## 0.2.16 - -### Patch Changes - -- Updated dependencies []: - - @tanstack/ai-vue@0.13.4 - -## 0.2.15 - -### Patch Changes - -- Updated dependencies [[`d5cb4b9`](https://github.com/TanStack/ai/commit/d5cb4b9445c5b97b06a7fc224dd2c3a92f0e802a)]: - - @tanstack/ai-vue@0.13.3 - -## 0.2.14 - -### Patch Changes - -- Updated dependencies []: - - @tanstack/ai-vue@0.13.2 - -## 0.2.13 - -### Patch Changes - -- Updated dependencies []: - - @tanstack/ai-vue@0.13.1 - -## 0.2.12 - -### Patch Changes - -- Updated dependencies [[`755e995`](https://github.com/TanStack/ai/commit/755e9953a31e879c4b88df0e7672ce1224886c97)]: - - @tanstack/ai-vue@0.13.0 - -## 0.2.11 - -### Patch Changes - -- Updated dependencies []: - - @tanstack/ai-vue@0.12.2 - -## 0.2.10 - -### Patch Changes - -- Updated dependencies []: - - @tanstack/ai-vue@0.12.1 - -## 0.2.9 - -### Patch Changes - -- Updated dependencies [[`8036b50`](https://github.com/TanStack/ai/commit/8036b5054330a180023c6e3225b8d2735a43a919)]: - - @tanstack/ai-vue@0.12.0 - -## 0.2.8 - -### Patch Changes - -- Updated dependencies []: - - @tanstack/ai-vue@0.11.1 - -## 0.2.7 - -### Patch Changes - -- Updated dependencies [[`d5645cf`](https://github.com/TanStack/ai/commit/d5645cfd4d1b9cfc877f7d4d714517e166a99ce3)]: - - @tanstack/ai-vue@0.11.0 - -## 0.2.6 - -### Patch Changes - -- Updated dependencies [[`5634f18`](https://github.com/TanStack/ai/commit/5634f186a4946ca3e1942fbfcbf1291ec9bd9855)]: - - @tanstack/ai-vue@0.10.11 - -## 0.2.5 - -### Patch Changes - -- Updated dependencies [[`ad23da9`](https://github.com/TanStack/ai/commit/ad23da92c279759b3778672dcee3d1616a02994b)]: - - @tanstack/ai-vue@0.10.10 - -## 0.2.4 - -### Patch Changes - -- Updated dependencies []: - - @tanstack/ai-vue@0.10.9 - -## 0.2.3 - -### Patch Changes - -- Updated dependencies []: - - @tanstack/ai-vue@0.10.8 - -## 0.2.2 - -### Patch Changes - -- Refresh package README content and npm metadata for better discoverability. ([#626](https://github.com/TanStack/ai/pull/626)) - -- Updated dependencies [[`ebeb22e`](https://github.com/TanStack/ai/commit/ebeb22ec68f456b09e0181ac6f5d1ac25a0affd2)]: - - @tanstack/ai-vue@0.10.7 - -## 0.2.1 - -### Patch Changes - -- Updated dependencies []: - - @tanstack/ai-vue@0.10.6 - -## 0.2.0 - -### Minor Changes - -- `TextPart` now accepts `remarkPlugins`, `rehypePlugins`, and (React/Solid) ([#599](https://github.com/TanStack/ai/pull/599)) - `components` props, plus a `disableDefaultPlugins` escape hatch. User plugins - merge with the secure defaults — `rehype-sanitize` continues to run last - unless defaults are disabled. - - This fixes [#164](https://github.com/TanStack/ai/issues/164): bold and - emphasis in Japanese, Chinese, and Korean text rendered incorrectly because - of a CommonMark spec defect. Consumers can now drop in - [`remark-cjk-friendly`](https://www.npmjs.com/package/remark-cjk-friendly) - with a single prop: - - ```tsx - import remarkCjkFriendly from 'remark-cjk-friendly' - ; - ``` - - Also fixes a latent bug in `@tanstack/ai-react-ui` where `remark-gfm` was - passed inside the rehype plugin array, silently disabling GFM features - (tables, strikethrough, task lists) in the React `TextPart`. - - `@tanstack/ai-vue-ui` omits the `components` prop because its underlying - renderer (`@crazydos/vue-markdown`) does not expose component overrides; - use that library's slot API for custom rendering. - -### Patch Changes - -- Updated dependencies [[`a03d12b`](https://github.com/TanStack/ai/commit/a03d12b13ade93f3e262c6ffa996696ce27472ef)]: - - @tanstack/ai-vue@0.10.5 - -## 0.1.40 - -### Patch Changes - -- Tighten TypeScript safety: enable `noImplicitOverride`, ([#579](https://github.com/TanStack/ai/pull/579)) - `noFallthroughCasesInSwitch`, and `useDefineForClassFields` in the - root `tsconfig.json`; add a typed-ESLint block scoped to - `packages/*/src/**` that turns on `no-floating-promises`, - `no-misused-promises`, `await-thenable`, - `switch-exhaustiveness-check`, `consistent-type-exports`, - `prefer-readonly`, and `no-non-null-assertion` (errors), plus - `no-explicit-any` (warning). `@ts-ignore` and `@ts-nocheck` are - disallowed in library source via `@typescript-eslint/ban-ts-comment`, - and `as unknown as ` double-casts are blocked by a - `no-restricted-syntax` rule (escape hatches available with an inline - reason). Two flags from the original five-flag set — - `noPropertyAccessFromIndexSignature` and `exactOptionalPropertyTypes` - — were tried and rolled back: they produced ~500 lines of bracket- - access and conditional-spread churn without catching any real bugs, - and `exactOptionalPropertyTypes` would have forced consumers using - it themselves to deal with our internals' style preferences. - - User-visible API surface is unchanged; this is a hardening pass to - keep streaming/agent-loop correctness and discriminated-union - exhaustiveness honest going forward. See issue #564. - -- Updated dependencies []: - - @tanstack/ai-vue@0.10.4 - -## 0.1.39 - -### Patch Changes - -- Updated dependencies []: - - @tanstack/ai-vue@0.10.3 - -## 0.1.38 - -### Patch Changes - -- Updated dependencies []: - - @tanstack/ai-vue@0.10.2 - -## 0.1.37 - -### Patch Changes - -- Updated dependencies [[`88fe80c`](https://github.com/TanStack/ai/commit/88fe80c404a218bf3e3a1ed5853a14f61248ed14)]: - - @tanstack/ai-vue@0.10.1 - -## 0.1.36 - -### Patch Changes - -- Updated dependencies [[`2e0e2eb`](https://github.com/TanStack/ai/commit/2e0e2eb72684aac82e570d57767656e218289b49)]: - - @tanstack/ai-vue@0.10.0 - -## 0.1.35 - -### Patch Changes - -- Updated dependencies [[`a9d1916`](https://github.com/TanStack/ai/commit/a9d19165a5028515cf1d091d611c8ac4b5b86099)]: - - @tanstack/ai-vue@0.9.0 - -## 0.1.34 - -### Patch Changes - -- Updated dependencies [[`98979f7`](https://github.com/TanStack/ai/commit/98979f7e72f4b5bfb816fb14b60a12871f8c4bec)]: - - @tanstack/ai-vue@0.8.0 - -## 0.1.33 - -### Patch Changes - -- Updated dependencies []: - - @tanstack/ai-vue@0.7.2 - -## 0.1.32 - -### Patch Changes - -- Updated dependencies []: - - @tanstack/ai-vue@0.7.1 - -## 0.1.31 - -### Patch Changes - -- Updated dependencies [[`54523f5`](https://github.com/TanStack/ai/commit/54523f5e9a9b4d4ea6c49e4551936bc2cc25593a), [`af9eb7b`](https://github.com/TanStack/ai/commit/af9eb7bbb875b23b7e99b2e6b743636daad402d1)]: - - @tanstack/ai-vue@0.7.0 - -## 0.1.30 - -### Patch Changes - -- Updated dependencies []: - - @tanstack/ai-vue@0.6.19 - -## 0.1.29 - -### Patch Changes - -- Updated dependencies []: - - @tanstack/ai-vue@0.6.18 - -## 0.1.28 - -### Patch Changes - -- Updated dependencies []: - - @tanstack/ai-vue@0.6.17 - -## 0.1.27 - -### Patch Changes - -- Updated dependencies []: - - @tanstack/ai-vue@0.6.16 - -## 0.1.26 - -### Patch Changes - -- Updated dependencies []: - - @tanstack/ai-vue@0.6.15 - -## 0.1.25 - -### Patch Changes - -- Updated dependencies []: - - @tanstack/ai-vue@0.6.14 - -## 0.1.24 - -### Patch Changes - -- Updated dependencies []: - - @tanstack/ai-vue@0.6.13 - -## 0.1.23 - -### Patch Changes - -- Updated dependencies []: - - @tanstack/ai-vue@0.6.12 - -## 0.1.22 - -### Patch Changes - -- Updated dependencies []: - - @tanstack/ai-vue@0.6.11 - -## 0.1.21 - -### Patch Changes - -- Updated dependencies []: - - @tanstack/ai-vue@0.6.10 - -## 0.1.20 - -### Patch Changes - -- Updated dependencies []: - - @tanstack/ai-vue@0.6.9 - -## 0.1.19 - -### Patch Changes - -- Updated dependencies []: - - @tanstack/ai-vue@0.6.8 - -## 0.1.18 - -### Patch Changes - -- Updated dependencies [[`64b9cba`](https://github.com/TanStack/ai/commit/64b9cba2ebf89162b809ba575c49ef12c0e87ee7), [`dc53e1b`](https://github.com/TanStack/ai/commit/dc53e1b89fddf6fc744e4788731e8ca64ec3d250)]: - - @tanstack/ai-vue@0.6.7 - -## 0.1.17 - -### Patch Changes - -- Updated dependencies []: - - @tanstack/ai-vue@0.6.6 - -## 0.1.16 - -### Patch Changes - -- Updated dependencies []: - - @tanstack/ai-vue@0.6.5 - -## 0.1.15 - -### Patch Changes - -- Updated dependencies [[`6dfffca`](https://github.com/TanStack/ai/commit/6dfffca99aeac1ada59eb288f8eb09e564d3db1e)]: - - @tanstack/ai-vue@0.6.4 - -## 0.1.14 - -### Patch Changes - -- Updated dependencies []: - - @tanstack/ai-vue@0.6.3 - -## 0.1.13 - -### Patch Changes - -- Updated dependencies []: - - @tanstack/ai-vue@0.6.2 - -## 0.1.12 - -### Patch Changes - -- Updated dependencies []: - - @tanstack/ai-vue@0.6.1 - -## 0.1.11 - -### Patch Changes - -- Updated dependencies [[`5aa6acc`](https://github.com/TanStack/ai/commit/5aa6acc1a4faea5346f750322e80984abf2d7059)]: - - @tanstack/ai-vue@0.6.0 - -## 0.1.10 - -### Patch Changes - -- Updated dependencies []: - - @tanstack/ai-vue@0.5.4 - -## 0.1.9 - -### Patch Changes - -- Updated dependencies []: - - @tanstack/ai-vue@0.5.3 - -## 0.1.8 - -### Patch Changes - -- Updated dependencies []: - - @tanstack/ai-vue@0.5.2 - -## 0.1.7 - -### Patch Changes - -- Updated dependencies []: - - @tanstack/ai-vue@0.5.1 - -## 0.1.6 - -### Patch Changes - -- Updated dependencies [[`0158d14`](https://github.com/TanStack/ai/commit/0158d14df00639ff5325680ae91b7791c189e60f)]: - - @tanstack/ai-vue@0.5.0 - -## 0.1.5 - -### Patch Changes - -- Updated dependencies [[`99ccee5`](https://github.com/TanStack/ai/commit/99ccee5c72df12adc13bede98142c6da84d13cc4)]: - - @tanstack/ai-vue@0.4.0 - -## 0.1.4 - -### Patch Changes - -- Updated dependencies []: - - @tanstack/ai-vue@0.3.0 - -## 0.1.3 - -### Patch Changes - -- Updated dependencies []: - - @tanstack/ai-vue@0.2.2 - -## 0.1.2 - -### Patch Changes - -- Updated dependencies []: - - @tanstack/ai-vue@0.2.1 - -## 0.1.1 - -### Patch Changes - -- Updated dependencies [[`c5df33c`](https://github.com/TanStack/ai/commit/c5df33c2d3e72c3332048ffe7c64a553e5ea86fb)]: - - @tanstack/ai-vue@0.2.0 - -## 0.1.0 - -### Minor Changes - -- Split up adapters for better tree shaking into separate functionalities ([#137](https://github.com/TanStack/ai/pull/137)) - -### Patch Changes - -- Updated dependencies [[`8d77614`](https://github.com/TanStack/ai/commit/8d776146f94ffd1579e1ab01b26dcb94d1bb3092)]: - - @tanstack/ai-client@0.1.0 - - @tanstack/ai-vue@0.1.0 - -## 0.0.1 - -### Patch Changes - -- Updated dependencies []: - - @tanstack/ai-client@0.0.3 - - @tanstack/ai-vue@0.0.2 diff --git a/packages/ai-vue-ui/README.md b/packages/ai-vue-ui/README.md index c9bbb8c10d..2c5724edd2 100644 --- a/packages/ai-vue-ui/README.md +++ b/packages/ai-vue-ui/README.md @@ -1,328 +1,9 @@ -
- - - - TanStack AI - -
+# @tanstack/ai-vue-ui (deprecated) -
+This package is deprecated since 0.3.0. It will be removed in 1.0.0. - +Import chat UI from [`@tanstack/ai-vue/ui`](https://tanstack.com/ai/latest/docs/ui/vue) instead. -
+This package re-exports that subpath so existing installs keep working during the move. - - -# TanStack AI - -Type-safe, provider-agnostic TypeScript SDK for building streaming chat, -tool-calling agents, structured outputs, realtime voice, media generation, and -framework-native AI apps. - -TanStack AI is built from composable activities and provider adapters. Use one -provider or switch between many. Import only chat, or add image, audio, video, -speech, transcription, summarization, realtime, Code Mode, devtools, and -framework bindings as your app needs them. - -## Read the docs -> - -## Start Here - -- [Overview](https://tanstack.com/ai/latest/docs/getting-started/overview) - - what TanStack AI is and how the packages fit together. -- [Quick Start: React](https://tanstack.com/ai/latest/docs/getting-started/quick-start) - - add streaming chat to a React app. -- [Quick Start: Vue](https://tanstack.com/ai/latest/docs/getting-started/quick-start-vue) - - build with Vue composables. -- [Quick Start: Svelte](https://tanstack.com/ai/latest/docs/getting-started/quick-start-svelte) - - build with Svelte 5 runes. -- [Quick Start: Server Only](https://tanstack.com/ai/latest/docs/getting-started/quick-start-server) - - use TanStack AI from a server endpoint, script, or backend service. -- [TanStack AI vs Vercel AI SDK](https://tanstack.com/ai/latest/docs/comparison/vercel-ai-sdk) - - compare architecture, feature coverage, and tradeoffs. - -## What You Can Build - -- Streaming chat experiences with typed messages, tool calls, reasoning parts, - and configurable connection adapters. -- Type-safe tools that can run on the server or client from one shared - `toolDefinition()` contract. -- Structured output flows backed by JSON Schema, Zod, ArkType, Valibot, or - plain JSON Schema. -- Multimodal prompts and responses that include text, images, audio, video, and - documents. -- Image, audio, video, speech, transcription, and summarization workflows using - a shared generation client pattern. -- Realtime voice chat with provider adapters for realtime sessions and token - minting. -- Code Mode agents that let an LLM write and execute TypeScript in an isolated - sandbox to orchestrate tools with loops, branches, and parallel calls. -- Devtools and observability pipelines for inspecting messages, tool calls, - stream chunks, errors, usage, and OpenTelemetry traces. -- Framework-native clients for React, Solid, Vue, Svelte, and Preact, plus a - headless client for custom runtimes. - -## Install - -Install the core package and the provider/framework packages your app uses: - -```bash -pnpm add @tanstack/ai @tanstack/ai-openai -``` - -For a React chat UI: - -```bash -pnpm add @tanstack/ai @tanstack/ai-client @tanstack/ai-react @tanstack/ai-openai -``` - -OpenRouter is also a good starting point if you want access to many providers -through one API key: - -```bash -pnpm add @tanstack/ai @tanstack/ai-openrouter -``` - -## Streaming Chat - -```typescript -import { chat, toServerSentEventsResponse } from '@tanstack/ai' -import { openaiText } from '@tanstack/ai-openai' - -export async function POST(request: Request) { - const body = await request.json() - - const stream = chat({ - adapter: openaiText('gpt-5.2'), - messages: body.messages, - }) - - return toServerSentEventsResponse(stream) -} -``` - -Learn more in the -[Chat & Streaming docs](https://tanstack.com/ai/latest/docs/chat/streaming) and -[Connection Adapters docs](https://tanstack.com/ai/latest/docs/chat/connection-adapters). - -## Type-Safe Tools - -Define a tool once, then attach a server or client implementation with the same -input and output types: - -```typescript -import { toolDefinition } from '@tanstack/ai' -import { z } from 'zod' - -const getProducts = toolDefinition({ - name: 'getProducts', - description: 'Search the product catalog', - inputSchema: z.object({ query: z.string() }), - outputSchema: z.array( - z.object({ - id: z.string(), - name: z.string(), - }), - ), -}).server(async ({ query }) => { - return db.products.search(query) -}) -``` - -Learn more in the -[Tools docs](https://tanstack.com/ai/latest/docs/tools/tools), -[Tool Approval Flow docs](https://tanstack.com/ai/latest/docs/tools/tool-approval), -and -[Lazy Tool Discovery docs](https://tanstack.com/ai/latest/docs/tools/lazy-tool-discovery). - -## Structured Outputs - -Use `outputSchema` when you need typed objects instead of freeform text: - -```typescript -import { chat } from '@tanstack/ai' -import { openaiText } from '@tanstack/ai-openai' -import { z } from 'zod' - -const Person = z.object({ - name: z.string(), - age: z.number(), -}) - -const person = await chat({ - adapter: openaiText('gpt-5.2'), - messages: [{ role: 'user', content: 'Ada Lovelace, 36' }], - outputSchema: Person, -}) -``` - -Learn more in the -[Structured Outputs docs](https://tanstack.com/ai/latest/docs/structured-outputs/overview). - -## Media, Realtime, and Code Mode - -- [Generations](https://tanstack.com/ai/latest/docs/media/generations) - one - pattern for image generation, text-to-speech, transcription, summarization, - audio generation, and video generation. -- [Realtime Voice Chat](https://tanstack.com/ai/latest/docs/media/realtime-chat) - - build low-latency realtime voice experiences. -- [Code Mode](https://tanstack.com/ai/latest/docs/code-mode/code-mode) - let - models write and execute TypeScript inside a secure isolate. -- [Code Mode with Snippets](https://tanstack.com/ai/latest/docs/code-mode/code-mode-with-snippets) - - give Code Mode reusable runtime capabilities. - -## Providers - -Official adapters include: - -| Package | Use it for | -| ------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------ | -| [`@tanstack/ai-openrouter`](https://tanstack.com/ai/latest/docs/adapters/openrouter) | 300+ models through one OpenRouter API, with per-request cost tracking | -| [`@tanstack/ai-openai`](https://tanstack.com/ai/latest/docs/adapters/openai) | OpenAI chat, image, video, speech, transcription, realtime, and provider tools | -| [`@tanstack/ai-anthropic`](https://tanstack.com/ai/latest/docs/adapters/anthropic) | Anthropic Claude chat, thinking, tools, structured outputs, and Vertex Claude | -| [`@tanstack/ai-gemini`](https://tanstack.com/ai/latest/docs/adapters/gemini) | Google Gemini chat, image, speech, and audio generation | -| [`@tanstack/ai-vertex`](https://tanstack.com/ai/latest/docs/adapters/vertex) | Gemini on Vertex AI with regional endpoints and Google Cloud credentials | -| [`@tanstack/ai-ollama`](https://tanstack.com/ai/latest/docs/adapters/ollama) | Local Ollama models | -| [`@tanstack/ai-grok`](https://tanstack.com/ai/latest/docs/adapters/grok) | xAI Grok chat, images, and realtime | -| [`@tanstack/ai-groq`](https://tanstack.com/ai/latest/docs/adapters/groq) | Groq low-latency inference | -| [`@tanstack/ai-elevenlabs`](https://tanstack.com/ai/latest/docs/adapters/elevenlabs) | ElevenLabs realtime voice, speech, transcription, music, and sound effects | -| [`@tanstack/ai-byteplus`](https://tanstack.com/ai/latest/docs/adapters/byteplus) | BytePlus Seed chat, Seedance video, Seedream image, and Seed Speech TTS/ASR | -| [`@tanstack/ai-fal`](https://tanstack.com/ai/latest/docs/adapters/fal) | fal.ai image, video, audio, speech, and transcription models | - -The adapter system is tree-shakeable by activity. Import `openaiText` for chat, -`openaiImage` for images, `falVideo` for video, `geminiSpeech` for TTS, and so -on. - -## Framework Packages - -| Package | What it provides | -| -------------------------------------------------------------------------- | ------------------------------------------------------------------------ | -| [`@tanstack/ai-client`](https://tanstack.com/ai/latest/docs/api/ai-client) | Headless chat, realtime, and generation clients | -| [`@tanstack/ai-react`](https://tanstack.com/ai/latest/docs/api/ai-react) | React hooks including `useChat`, `useRealtimeChat`, and generation hooks | -| [`@tanstack/ai-solid`](https://tanstack.com/ai/latest/docs/api/ai-solid) | Solid hooks for chat and generations | -| [`@tanstack/ai-vue`](https://tanstack.com/ai/latest/docs/api/ai-vue) | Vue composables for chat and generations | -| [`@tanstack/ai-svelte`](https://tanstack.com/ai/latest/docs/api/ai-svelte) | Svelte 5 factories for chat and generations | -| [`@tanstack/ai-preact`](https://tanstack.com/ai/latest/docs/api/ai-preact) | Preact hooks for chat | -| `@tanstack/ai-react-ui`, `@tanstack/ai-solid-ui`, `@tanstack/ai-vue-ui` | Headless UI components for chat interfaces | - -## Advanced Docs - -- [Middleware](https://tanstack.com/ai/latest/docs/advanced/middleware) - hook - into chat configuration, chunks, tool calls, usage, errors, and structured - outputs. -- [OpenTelemetry](https://tanstack.com/ai/latest/docs/advanced/otel) - emit - vendor-neutral GenAI traces and metrics. -- [Observability](https://tanstack.com/ai/latest/docs/advanced/observability) - - subscribe to typed TanStack AI events. -- [Per-Model Type Safety](https://tanstack.com/ai/latest/docs/advanced/per-model-type-safety) - - narrow model options and content modalities to the selected model. -- [Runtime Adapter Switching](https://tanstack.com/ai/latest/docs/advanced/runtime-adapter-switching) - - switch providers at runtime. -- [Tree-Shaking](https://tanstack.com/ai/latest/docs/advanced/tree-shaking) - - ship only the activities and adapters you use. -- [Agent Skills](https://tanstack.com/ai/latest/docs/getting-started/agent-skills) - - install TanStack AI skills into Claude Code, Cursor, GitHub Copilot, Codex, - and other coding agents with TanStack Intent. - -## Get Involved - -- Read the [docs](https://tanstack.com/ai). -- Participate in [GitHub discussions](https://github.com/TanStack/ai/discussions). -- Chat with the community on [Discord](https://discord.com/invite/WrRKjPJ). -- See [CONTRIBUTING.md](https://github.com/TanStack/ai/blob/main/CONTRIBUTING.md) - for setup instructions. -- [Become a sponsor](https://github.com/sponsors/tannerlinsley/). - -## Partners - - - - - - -
- - - - - CodeRabbit - - - - - - - - Cloudflare - - -
- -
- AI and you? -

- We're looking for TanStack AI partners to join our mission. Partner with us - to push the boundaries of TanStack AI and build amazing things together. -

- LET'S CHAT -
- -## Explore the TanStack Ecosystem - -- TanStack Config - - tooling for JS/TS packages -- TanStack DB - reactive - sync client store -- TanStack Devtools - - unified devtools panel -- TanStack Form - - type-safe form state -- TanStack Pacer - - debouncing, throttling, batching -- TanStack Query - - async state and caching -- TanStack Ranger - - range and slider primitives -- TanStack Router - - type-safe routing, caching, and URL state -- TanStack Start - - full-stack SSR and streaming -- TanStack Store - - reactive data store -- TanStack Table - - headless datagrids -- TanStack Virtual - - virtualized rendering - -...and more at TanStack.com. +See the [migration guide](https://tanstack.com/ai/latest/docs/migration/create-ui). diff --git a/packages/ai-vue-ui/package.json b/packages/ai-vue-ui/package.json index 8b0d6e5064..abdec28850 100644 --- a/packages/ai-vue-ui/package.json +++ b/packages/ai-vue-ui/package.json @@ -1,7 +1,7 @@ { "name": "@tanstack/ai-vue-ui", "version": "0.2.40", - "description": "Headless Vue components for building TanStack AI chat interfaces with streamed message parts.", + "description": "Deprecated. Import chat UI from @tanstack/ai-vue/ui instead.", "author": "Tanner Linsley", "license": "MIT", "homepage": "https://tanstack.com/ai", @@ -17,57 +17,39 @@ "type": "github", "url": "https://github.com/sponsors/tannerlinsley" }, + "deprecated": "Use @tanstack/ai-vue/ui instead. See https://tanstack.com/ai/latest/docs/migration/create-ui", "module": "./src/index.ts", "types": "./src/index.ts", "type": "module", "exports": { ".": { - "types": "./dist/esm/index.d.ts", - "import": "./dist/esm/index.js" + "types": "./src/index.ts", + "import": "./src/index.ts" } }, "files": [ - "src", - "dist" + "src" ], "scripts": { - "build": "vite build", + "build": "node -e \"process.exit(0)\"", "clean": "premove ./build ./dist", - "test:build": "publint --strict", "test:oxlint": "oxlint src --type-aware", "test:lib": "vitest --passWithNoTests", "test:lib:dev": "pnpm test:lib --watch", - "test:types": "vue-tsc --noEmit" + "test:types": "tsc" }, "keywords": [ "ai", - "ai-sdk", - "typescript", "tanstack", "vue", "chat", "ui", - "headless", - "components", - "streaming", - "generative-ai" + "deprecated" ], "dependencies": { - "@crazydos/vue-markdown": "^1.1.4", - "@tanstack/ai-client": "workspace:^", - "@tanstack/ai-vue": "workspace:^", - "rehype-highlight": "^7.0.2", - "rehype-raw": "^7.0.0", - "remark-gfm": "^4.0.1" - }, - "peerDependencies": { - "vue": ">=3.5.0" + "@tanstack/ai-vue": "workspace:^" }, "devDependencies": { - "@vitejs/plugin-vue": "^6.0.8", - "@vitest/coverage-v8": "4.1.10", - "vite": "^8.2.1", - "vue": "^3.5.25", - "vue-tsc": "^2.2.10" + "vue": "^3.5.25" } } diff --git a/packages/ai-vue-ui/src/create-ui.ts b/packages/ai-vue-ui/src/create-ui.ts deleted file mode 100644 index accf0271e4..0000000000 --- a/packages/ai-vue-ui/src/create-ui.ts +++ /dev/null @@ -1,477 +0,0 @@ -import { defineComponent, h, inject, provide } from 'vue' -import type { Component, InjectionKey, PropType, VNode } from 'vue' -import { - automaticPartsForMessage, - collectInlineToolNames, - resolveInterruptComponent, - selectChatUI, - selectMessageUI, -} from '@tanstack/ai-client/ui' -import type { - ChatUIData, - ChatUIHasNamedInterrupts, - ChatUIHasNamedTools, - ChatUIInterrupt, - ChatUIInterruptName, - ChatUIInterruptOf, - ChatUIInterruptsOf, - ChatUINamedInterruptId, - ChatUIPartKey, - ChatUIPartOf, - ChatUISchemaOf, - ChatUIToolApproval, - ChatUIToolName, - ChatUIToolsOf, -} from '@tanstack/ai-client/ui' -import type { - MessagePart, - ToolCallPart, - ToolResultPart, - UIMessage as UIMessageModel, -} from '@tanstack/ai-client' -import type { UseChatReturn } from '@tanstack/ai-vue' - -export type ChatUIHost = UseChatReturn< - ChatUIToolsOf, - ChatUISchemaOf, - ChatUIInterruptsOf -> - -export type LayoutProps = { - readonly __ui?: TOptions -} - -export type MessageProps = { - message: UIMessageModel, ChatUIData> -} - -export type InputProps = { - readonly __ui?: TOptions -} - -export type PartProps = { - part: ChatUIPartOf -} - -export type ToolProps< - TOptions, - TName extends ChatUIToolName = ChatUIToolName, -> = { - part: Extract>, { name: TName }> - result?: ToolResultPart - interrupt?: ChatUIToolApproval -} - -export type InterruptProps< - TOptions, - TName extends ChatUIInterruptName = never, -> = { - interrupt: ChatUIInterruptOf - readonly __ui?: TOptions -} - -type GenericInterruptComponents = - ChatUIHasNamedInterrupts extends true - ? { - [K in ChatUINamedInterruptId]: Component< - InterruptProps> - > - } & { - fallback?: Component> - } - : { - fallback?: Component> - } - -type ToolApprovalMap = { - [K in ChatUIToolName]?: Component< - InterruptProps> - > -} - -export type ChatUIComponents = { - layout: Component - message: Component - input?: Component - parts: { - [K in ChatUIPartKey]?: Component> - } & { - fallback?: Component> - } -} & (ChatUIHasNamedTools extends true - ? { - tools: { - [K in ChatUIToolName]: Component> - } - } - : { - tools?: { - [K in ChatUIToolName]?: Component> - } - }) & - (ChatUIHasNamedInterrupts extends true - ? { - interrupts: { - tools?: ToolApprovalMap - generic: GenericInterruptComponents - } - } - : { - interrupts?: { - tools?: ToolApprovalMap - generic?: GenericInterruptComponents - } - }) - -export type UIDescriptor = { - key: InjectionKey> - warn: (key: string, message: string) => void - defineComponents: ( - components: ChatUIComponents, - ) => ChatUIComponents - useChat: () => ChatUIHost -} - -type VueChatUIComponents = { - layout: Component - message: Component - input?: Component - parts: { - [K in ChatUIPartKey]?: Component - } & { - fallback?: Component - } - tools?: Record - interrupts?: { - tools?: Record - generic?: Record - } -} - -type ComponentsValue = { - components: VueChatUIComponents - warn: (key: string, message: string) => void - inlineToolNames: ReadonlyArray -} - -type UIRuntime = UIDescriptor & { - componentsKey: InjectionKey -} - -function createWarnOnce() { - const seen = new Set() - return (key: string, message: string) => { - if (process.env.NODE_ENV === 'production') return - if (seen.has(key)) return - seen.add(key) - console.warn(message) - } -} - -function unwrapRef(value: unknown): unknown { - if ( - value && - typeof value === 'object' && - 'value' in value && - !Array.isArray(value) - ) { - return (value as { value: unknown }).value - } - return value -} - -function readMessages( - chat: ChatUIHost, -): ReadonlyArray { - const value = unwrapRef(chat.messages) - return Array.isArray(value) ? value : [] -} - -function readInterrupts( - chat: ChatUIHost, -): ReadonlyArray { - const value = unwrapRef(chat.interrupts) - return Array.isArray(value) ? value : [] -} - -function useChatContext(ui: UIDescriptor) { - const chat = inject(ui.key) - if (!chat) { - throw new Error( - 'Chat UI components must be wrapped in UIProvider or UIChat.', - ) - } - return chat -} - -function useComponentsContext(ui: UIDescriptor) { - const value = inject((ui as UIRuntime).componentsKey) - if (!value) { - throw new Error( - 'Chat UI components must be wrapped in UIProvider or UIChat.', - ) - } - return value -} - -function inlineNames(components: ChatUIComponents) { - return collectInlineToolNames( - components.interrupts?.tools as Record | undefined, - Object.keys(components.tools ?? {}), - ) -} - -function renderSelectedPart( - selected: ReturnType[number], - comps: ComponentsValue, -): VNode | null { - if (selected.key === 'toolCall') { - const name = selected.part.name - const Tool = comps.components.tools?.[name] as Component | undefined - if (!Tool) { - comps.warn( - `tool:${name}`, - `[tanstack-ai-ui] Missing tools.${name} component`, - ) - return null - } - return h(Tool, { - part: selected.part, - result: selected.result, - interrupt: selected.interrupt, - }) - } - const PartComponent = (comps.components.parts[selected.key] ?? - comps.components.parts.fallback) as Component | undefined - if (!PartComponent) { - comps.warn( - `part:${selected.key}`, - `[tanstack-ai-ui] Missing parts.${selected.key} component`, - ) - return null - } - return h(PartComponent, { part: selected.part }) -} - -export function createChatUI( - options: TOptions, -): UIDescriptor { - void options - const ui: UIRuntime = { - key: Symbol('tanstack-ai-ui-chat') as InjectionKey>, - componentsKey: Symbol( - 'tanstack-ai-ui-components', - ) as InjectionKey, - warn: createWarnOnce(), - defineComponents(components) { - return components - }, - useChat() { - return useChatContext(ui) - }, - } - return ui -} - -export const UIProvider = defineComponent({ - name: 'UIProvider', - props: { - ui: { type: Object as PropType>, required: true }, - chat: { type: Object, required: true }, - components: { - type: Object as PropType, - required: true, - }, - }, - setup(props, { slots }) { - provide(props.ui.key, props.chat as ChatUIHost) - provide((props.ui as UIRuntime).componentsKey, { - components: props.components, - warn: props.ui.warn, - inlineToolNames: inlineNames(props.components as ChatUIComponents), - }) - return () => slots.default?.() - }, -}) - -export const UIMessages = defineComponent({ - name: 'UIMessages', - props: { - ui: { type: Object as PropType>, required: true }, - }, - setup(props, { slots }) { - return () => { - const chat = useChatContext(props.ui) - const messages = readMessages(chat) - const interrupts = readInterrupts(chat) - if (slots.default) return slots.default({ messages }) - return messages.map((message) => - h(UIMessage, { - key: message.id, - ui: props.ui, - message, - interrupts, - }), - ) - } - }, -}) - -const UISelectedPart = defineComponent({ - name: 'UISelectedPart', - props: { - ui: { type: Object as PropType>, required: true }, - selected: { - type: Object as PropType< - ReturnType[number] - >, - required: true, - }, - }, - setup(props) { - return () => - renderSelectedPart(props.selected, useComponentsContext(props.ui)) - }, -}) - -export const UIMessage = defineComponent({ - name: 'UIMessage', - props: { - ui: { type: Object as PropType>, required: true }, - message: { type: Object as PropType, required: true }, - interrupts: { - type: Array as PropType>, - default: undefined, - }, - }, - setup(props, { slots }) { - return () => { - const comps = useComponentsContext(props.ui) - const interrupts = - props.interrupts ?? readInterrupts(useChatContext(props.ui)) - const selected = selectMessageUI(props.message, { - interrupts, - inlineToolNames: comps.inlineToolNames, - }) - if (slots.default) return slots.default({ parts: selected.parts }) - return h( - comps.components.message, - { message: props.message }, - { - parts: () => - automaticPartsForMessage(selected).map((part, index) => - h(UISelectedPart, { - key: `${props.message.id}-${index}`, - ui: props.ui, - selected: part, - }), - ), - }, - ) - } - }, -}) - -export const UIPart = defineComponent({ - name: 'UIPart', - props: { - ui: { type: Object as PropType>, required: true }, - part: { type: Object as PropType, required: true }, - }, - setup(props) { - return () => { - const selected = selectMessageUI( - { id: 'part', role: 'assistant', parts: [props.part] }, - { - interrupts: readInterrupts(useChatContext(props.ui)), - inlineToolNames: [], - }, - ).parts[0] - if (!selected) return null - return h(UISelectedPart, { - ui: props.ui, - selected, - }) - } - }, -}) - -export const UIInterrupts = defineComponent({ - name: 'UIInterrupts', - props: { - ui: { type: Object as PropType>, required: true }, - }, - setup(props, { slots }) { - return () => { - const chat = useChatContext(props.ui) - const comps = useComponentsContext(props.ui) - const selected = selectChatUI({ - messages: readMessages(chat), - interrupts: readInterrupts(chat), - inlineToolNames: comps.inlineToolNames, - }) - if (slots.default) - return slots.default({ interrupts: selected.interrupts }) - return selected.interrupts.map((interrupt) => - h(UIInterrupt, { key: interrupt.id, ui: props.ui, interrupt }), - ) - } - }, -}) - -export const UIInterrupt = defineComponent({ - name: 'UIInterrupt', - props: { - ui: { type: Object as PropType>, required: true }, - interrupt: { type: Object as PropType, required: true }, - }, - setup(props) { - return () => { - const comps = useComponentsContext(props.ui) - const Component = resolveInterruptComponent( - props.interrupt, - comps.components.interrupts, - ) as Component | undefined - if (!Component) { - comps.warn( - `interrupt:${props.interrupt.id}`, - `[tanstack-ai-ui] Missing interrupt component for ${props.interrupt.kind}`, - ) - return null - } - return h(Component, { interrupt: props.interrupt }) - } - }, -}) - -export const UIChat = defineComponent({ - name: 'UIChat', - props: { - ui: { type: Object as PropType>, required: true }, - chat: { type: Object, required: true }, - components: { - type: Object as PropType, - required: true, - }, - }, - setup(props) { - return () => - h( - UIProvider, - { ui: props.ui, chat: props.chat, components: props.components }, - { - default: () => - h( - props.components.layout, - {}, - { - messages: () => h(UIMessages, { ui: props.ui }), - interrupts: () => h(UIInterrupts, { ui: props.ui }), - input: () => - props.components.input ? h(props.components.input) : null, - }, - ), - }, - ) - }, -}) diff --git a/packages/ai-vue-ui/src/index.ts b/packages/ai-vue-ui/src/index.ts index 9dbb5db3ed..4f6f2a06d7 100644 --- a/packages/ai-vue-ui/src/index.ts +++ b/packages/ai-vue-ui/src/index.ts @@ -1,50 +1,6 @@ -import Chat from './chat.vue' -import ChatInput from './chat-input.vue' -import ChatMessage from './chat-message.vue' -import ChatMessages from './chat-messages.vue' -import ThinkingPart from './thinking-part.vue' -import TextPart from './text-part.vue' -import ToolApproval from './tool-approval.vue' - -export { - createChatUI, - UIChat, - UIProvider, - UIMessages, - UIMessage, - UIPart, - UIInterrupts, - UIInterrupt, - type ChatUIComponents, - type ChatUIHost, - type InputProps, - type InterruptProps, - type LayoutProps, - type MessageProps, - type PartProps, - type ToolProps, - type UIDescriptor, -} from './create-ui' - -export { - Chat, - ChatInput, - ChatMessage, - ChatMessages, - ThinkingPart, - TextPart, - ToolApproval, -} - -export type { - ChatProps, - ChatInputProps, - ChatInputRenderProps, - ChatMessageProps, - ChatMessagesProps, - ThinkingPartProps, - TextPartProps, - ToolApprovalProps, - ToolApprovalRenderProps, - ToolCallRenderProps, -} from './types' +/** + * @deprecated Since 0.3.0. Import from `@tanstack/ai-vue/ui` instead. + * See https://tanstack.com/ai/latest/docs/migration/create-ui + * Removed in 1.0.0. + */ +export * from '@tanstack/ai-vue/ui' diff --git a/packages/ai-vue-ui/src/vue-shim.d.ts b/packages/ai-vue-ui/src/vue-shim.d.ts new file mode 100644 index 0000000000..a69ae5c88d --- /dev/null +++ b/packages/ai-vue-ui/src/vue-shim.d.ts @@ -0,0 +1,9 @@ +declare module '*.vue' { + import type { DefineComponent } from 'vue' + const component: DefineComponent< + Record, + Record, + unknown + > + export default component +} diff --git a/packages/ai-vue-ui/tests/coexistence.test.ts b/packages/ai-vue-ui/tests/coexistence.test.ts deleted file mode 100644 index 4c9c6cb332..0000000000 --- a/packages/ai-vue-ui/tests/coexistence.test.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { describe, expect, it } from 'vitest' -import { - Chat, - ChatInput, - ChatMessage, - ChatMessages, - TextPart, - ThinkingPart, - createChatUI, -} from '../src' - -describe('public coexistence', () => { - it('exports old and new APIs before 1.0', () => { - expect(Chat).toBeDefined() - expect(ChatInput).toBeDefined() - expect(ChatMessage).toBeDefined() - expect(ChatMessages).toBeDefined() - expect(TextPart).toBeDefined() - expect(ThinkingPart).toBeDefined() - expect(createChatUI).toBeDefined() - }) -}) diff --git a/packages/ai-vue-ui/tests/create-ui-types.test.ts b/packages/ai-vue-ui/tests/create-ui-types.test.ts deleted file mode 100644 index 1ebc210ed2..0000000000 --- a/packages/ai-vue-ui/tests/create-ui-types.test.ts +++ /dev/null @@ -1,85 +0,0 @@ -import { expectTypeOf, it } from 'vitest' -import { createChatUI } from '../src/create-ui' -import type { ChatUIHost, InterruptProps, ToolProps } from '../src/create-ui' -import { chatOptions } from '../../ai-client/tests/ui-fixtures' - -it('requires every tool name and interrupt id', () => { - type PurchaseInterrupt = InterruptProps - expectTypeOf< - PurchaseInterrupt['interrupt']['toolName'] - >().toEqualTypeOf<'purchaseItem'>() - type ChoosePlan = InterruptProps - expectTypeOf().toEqualTypeOf< - { title: string } | undefined - >() - type PurchaseTool = ToolProps - expectTypeOf< - NonNullable['originalArgs'] - >().toEqualTypeOf<{ item: string }>() - - const ui = createChatUI(chatOptions) - expectTypeOf(ui.useChat).returns.toEqualTypeOf< - ChatUIHost - >() - - ui.defineComponents({ - layout: () => null, - message: () => null, - parts: { fallback: () => null }, - tools: { - getWeather: () => null, - purchaseItem: () => null, - // @ts-expect-error This tool is not in chatOptions. - unknownTool: () => null, - }, - interrupts: { - generic: { - choosePlan: (props: { - interrupt: { resolveInterrupt: (value: string) => void } - }) => { - expectTypeOf(props.interrupt.resolveInterrupt).toBeFunction() - return null - }, - fallback: () => null, - }, - }, - }) - - ui.defineComponents({ - layout: () => null, - message: () => null, - parts: { fallback: () => null }, - // @ts-expect-error Every configured tool needs a component. - tools: { - getWeather: () => null, - }, - interrupts: { - generic: { - choosePlan: () => null, - }, - }, - }) - - ui.defineComponents({ - layout: () => null, - message: () => null, - parts: { fallback: () => null }, - tools: { - getWeather: () => null, - purchaseItem: () => null, - }, - interrupts: { - // @ts-expect-error Every registered interrupt id needs a component. - generic: { - fallback: () => null, - }, - }, - }) - - const untyped = createChatUI({}) - untyped.defineComponents({ - layout: () => null, - message: () => null, - parts: { fallback: () => null }, - }) -}) diff --git a/packages/ai-vue-ui/tests/create-ui.test.ts b/packages/ai-vue-ui/tests/create-ui.test.ts deleted file mode 100644 index b2de4595fe..0000000000 --- a/packages/ai-vue-ui/tests/create-ui.test.ts +++ /dev/null @@ -1,120 +0,0 @@ -import { defineComponent, h } from 'vue' -import { describe, expect, it, vi } from 'vitest' -import { createChatUI, UIChat, UIMessages, UIProvider } from '../src' -import { renderVueText } from './test-renderer' -import { - chatOptions, - createVueChatResult, - messageWithToolResults, - unknownToolMessage, -} from '../../ai-client/tests/ui-fixtures' - -describe('Vue createChatUI', () => { - it('renders automatic and scoped-slot traversal', async () => { - const ui = createChatUI(chatOptions) - const chat = createVueChatResult([messageWithToolResults]) - const components = ui.defineComponents({ - layout: defineComponent( - (_, { slots }) => - () => - slots.messages?.(), - ), - message: defineComponent( - (_, { slots }) => - () => - h('article', slots.parts?.()), - ), - parts: { - fallback: defineComponent({ - props: ['part'], - setup(props) { - return () => h('span', props.part.type) - }, - }), - }, - tools: { - getWeather: defineComponent({ - props: ['part'], - setup(props) { - return () => h('strong', props.part.input?.city) - }, - }), - purchaseItem: defineComponent(() => () => null), - }, - interrupts: { - generic: { - choosePlan: defineComponent(() => () => null), - fallback: defineComponent(() => () => null), - }, - }, - }) - - const automatic = await renderVueText( - defineComponent(() => () => h(UIChat, { ui, chat, components })), - ) - expect(automatic).toContain('Paris') - - const manual = await renderVueText( - defineComponent( - () => () => - h( - UIProvider, - { ui, chat, components }, - { - default: () => - h( - UIMessages, - { ui }, - { - default: ({ messages }: { messages: Array }) => - h('span', String(messages.length)), - }, - ), - }, - ), - ), - ) - expect(manual).toContain('1') - }) - - it('warns once for a missing runtime key', async () => { - const warn = vi.spyOn(console, 'warn').mockImplementation(() => undefined) - const ui = createChatUI(chatOptions) - const chat = createVueChatResult([unknownToolMessage]) - const components = ui.defineComponents({ - layout: defineComponent( - (_, { slots }) => - () => - slots.messages?.(), - ), - message: defineComponent( - (_, { slots }) => - () => - slots.parts?.(), - ), - parts: { fallback: defineComponent(() => () => null) }, - tools: { - getWeather: defineComponent(() => () => null), - purchaseItem: defineComponent(() => () => null), - }, - interrupts: { - generic: { - choosePlan: defineComponent(() => () => null), - fallback: defineComponent(() => () => null), - }, - }, - }) - await renderVueText( - defineComponent(() => () => h(UIChat, { ui, chat, components })), - ) - await renderVueText( - defineComponent(() => () => h(UIChat, { ui, chat, components })), - ) - expect( - warn.mock.calls.filter((call) => - String(call[0]).includes('[tanstack-ai-ui]'), - ), - ).toHaveLength(1) - warn.mockRestore() - }) -}) diff --git a/packages/ai-vue-ui/tsconfig.json b/packages/ai-vue-ui/tsconfig.json index 7e7e60e47e..ff01a530fa 100644 --- a/packages/ai-vue-ui/tsconfig.json +++ b/packages/ai-vue-ui/tsconfig.json @@ -6,6 +6,6 @@ "jsx": "preserve", "lib": ["ES2022", "DOM"] }, - "include": ["src", "tests"], + "include": ["src"], "exclude": ["node_modules", "dist"] } diff --git a/packages/ai-vue-ui/vite.config.ts b/packages/ai-vue-ui/vite.config.ts index 58e0135d8a..1769fa910d 100644 --- a/packages/ai-vue-ui/vite.config.ts +++ b/packages/ai-vue-ui/vite.config.ts @@ -1,10 +1,7 @@ -import { defineConfig, mergeConfig } from 'vitest/config' -import { tanstackViteConfig } from '@tanstack/vite-config' -import vue from '@vitejs/plugin-vue' +import { defineConfig } from 'vitest/config' import packageJson from './package.json' -const config = defineConfig({ - plugins: [vue()], +export default defineConfig({ test: { name: packageJson.name, dir: './', @@ -12,27 +9,6 @@ const config = defineConfig({ globals: true, environment: 'node', include: ['tests/**/*.test.ts'], - coverage: { - provider: 'v8', - reporter: ['text', 'json', 'html', 'lcov'], - exclude: [ - 'node_modules/', - 'dist/', - 'tests/', - '**/*.test.ts', - '**/*.config.ts', - '**/types.ts', - ], - include: ['src/**/*.ts'], - }, + passWithNoTests: true, }, }) - -export default mergeConfig( - config, - tanstackViteConfig({ - entry: ['./src/index.ts'], - srcDir: './src', - cjs: false, - }), -) diff --git a/packages/ai-vue/README.md b/packages/ai-vue/README.md index c9bbb8c10d..d524f36907 100644 --- a/packages/ai-vue/README.md +++ b/packages/ai-vue/README.md @@ -226,15 +226,15 @@ on. ## Framework Packages -| Package | What it provides | -| -------------------------------------------------------------------------- | ------------------------------------------------------------------------ | -| [`@tanstack/ai-client`](https://tanstack.com/ai/latest/docs/api/ai-client) | Headless chat, realtime, and generation clients | -| [`@tanstack/ai-react`](https://tanstack.com/ai/latest/docs/api/ai-react) | React hooks including `useChat`, `useRealtimeChat`, and generation hooks | -| [`@tanstack/ai-solid`](https://tanstack.com/ai/latest/docs/api/ai-solid) | Solid hooks for chat and generations | -| [`@tanstack/ai-vue`](https://tanstack.com/ai/latest/docs/api/ai-vue) | Vue composables for chat and generations | -| [`@tanstack/ai-svelte`](https://tanstack.com/ai/latest/docs/api/ai-svelte) | Svelte 5 factories for chat and generations | -| [`@tanstack/ai-preact`](https://tanstack.com/ai/latest/docs/api/ai-preact) | Preact hooks for chat | -| `@tanstack/ai-react-ui`, `@tanstack/ai-solid-ui`, `@tanstack/ai-vue-ui` | Headless UI components for chat interfaces | +| Package | What it provides | +| ------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | +| [`@tanstack/ai-client`](https://tanstack.com/ai/latest/docs/api/ai-client) | Headless chat, realtime, and generation clients | +| [`@tanstack/ai-react`](https://tanstack.com/ai/latest/docs/api/ai-react) | React hooks including `useChat`, `useRealtimeChat`, and generation hooks | +| [`@tanstack/ai-solid`](https://tanstack.com/ai/latest/docs/api/ai-solid) | Solid hooks for chat and generations | +| [`@tanstack/ai-vue`](https://tanstack.com/ai/latest/docs/api/ai-vue) | Vue composables for chat and generations | +| [`@tanstack/ai-svelte`](https://tanstack.com/ai/latest/docs/api/ai-svelte) | Svelte 5 factories for chat and generations | +| [`@tanstack/ai-preact`](https://tanstack.com/ai/latest/docs/api/ai-preact) | Preact hooks for chat | +| `@tanstack/ai-react/ui`, `@tanstack/ai-solid/ui`, `@tanstack/ai-vue/ui`, `@tanstack/ai-svelte/ui` | Headless UI components for chat interfaces. `@tanstack/ai-react-ui`, `@tanstack/ai-solid-ui`, and `@tanstack/ai-vue-ui` are deprecated. | ## Advanced Docs diff --git a/packages/ai-vue/package.json b/packages/ai-vue/package.json index ad8231d710..d2d5f10956 100644 --- a/packages/ai-vue/package.json +++ b/packages/ai-vue/package.json @@ -24,6 +24,10 @@ ".": { "types": "./dist/index.d.ts", "import": "./dist/index.js" + }, + "./ui": { + "types": "./src/ui.ts", + "import": "./src/ui.ts" } }, "files": [ @@ -35,7 +39,7 @@ "test:oxlint": "oxlint src --type-aware", "test:lib": "vitest run", "test:lib:dev": "pnpm test:lib --watch", - "test:types": "tsc", + "test:types": "vue-tsc --noEmit", "build": "tsdown" }, "keywords": [ @@ -53,7 +57,11 @@ "media-generation" ], "dependencies": { - "@tanstack/ai-client": "workspace:^" + "@crazydos/vue-markdown": "^1.1.4", + "@tanstack/ai-client": "workspace:^", + "rehype-highlight": "^7.0.2", + "rehype-raw": "^7.0.0", + "remark-gfm": "^4.0.1" }, "peerDependencies": { "@tanstack/ai": "workspace:^", @@ -63,12 +71,15 @@ "@standard-schema/spec": "^1.1.0", "@tanstack/ai": "workspace:*", "@types/node": "^24.10.1", + "@vitejs/plugin-vue": "^6.0.8", "@vitest/coverage-v8": "4.1.10", "@vue/test-utils": "^2.4.6", "jsdom": "^27.4.0", "tsdown": "^0.17.0-beta.6", "typescript": "5.9.3", + "vite": "^8.2.1", "vitest": "^4.1.10", - "vue": "^3.5.25" + "vue": "^3.5.25", + "vue-tsc": "^2.2.10" } } diff --git a/packages/ai-vue-ui/src/chat-input.vue b/packages/ai-vue/src/chat-ui/chat-input.vue similarity index 100% rename from packages/ai-vue-ui/src/chat-input.vue rename to packages/ai-vue/src/chat-ui/chat-input.vue diff --git a/packages/ai-vue-ui/src/chat-message.vue b/packages/ai-vue/src/chat-ui/chat-message.vue similarity index 100% rename from packages/ai-vue-ui/src/chat-message.vue rename to packages/ai-vue/src/chat-ui/chat-message.vue diff --git a/packages/ai-vue-ui/src/chat-messages.vue b/packages/ai-vue/src/chat-ui/chat-messages.vue similarity index 100% rename from packages/ai-vue-ui/src/chat-messages.vue rename to packages/ai-vue/src/chat-ui/chat-messages.vue diff --git a/packages/ai-vue-ui/src/chat.vue b/packages/ai-vue/src/chat-ui/chat.vue similarity index 83% rename from packages/ai-vue-ui/src/chat.vue rename to packages/ai-vue/src/chat-ui/chat.vue index 3bfff3b232..26aba503b1 100644 --- a/packages/ai-vue-ui/src/chat.vue +++ b/packages/ai-vue/src/chat-ui/chat.vue @@ -1,9 +1,9 @@