Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/typescript/ai-preact-ui/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# @tanstack/ai-preact-ui
306 changes: 306 additions & 0 deletions packages/typescript/ai-preact-ui/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,306 @@
<div align="center">
<img src="./media/header_ai.png" >
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if the media file exists
echo "=== Checking for media file ==="
fd -t f 'header_ai.png' packages/typescript/ai-preact-ui/ 2>&1 || echo "File not found"

# Also check directory structure
echo -e "\n=== Directory structure ==="
ls -la packages/typescript/ai-preact-ui/media/ 2>&1 || echo "Media directory not found"

# Verify the README.md content at line 2
echo -e "\n=== README.md content around line 2 ==="
sed -n '1,5p' packages/typescript/ai-preact-ui/README.md 2>&1

Repository: TanStack/ai

Length of output: 342


Image file is missing and alt text is not provided.

The image file ./media/header_ai.png does not exist in the repository, which breaks the documentation rendering. Additionally, the image tag is missing alt text for accessibility.

Proposed fix
  1. Add the missing header image file to packages/typescript/ai-preact-ui/media/header_ai.png
  2. Update the image tag to include alt text:
-  <img src="./media/header_ai.png" >
+  <img src="./media/header_ai.png" alt="TanStack AI Preact UI" >
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<img src="./media/header_ai.png" >
<img src="./media/header_ai.png" alt="TanStack AI Preact UI" >
🧰 Tools
🪛 markdownlint-cli2 (0.18.1)

2-2: Images should have alternate text (alt text)

(MD045, no-alt-text)

🤖 Prompt for AI Agents
In `@packages/typescript/ai-preact-ui/README.md` at line 2, The README references
a missing image and lacks alt text; add the image file at
packages/typescript/ai-preact-ui/media/header_ai.png and update the README's
<img src="./media/header_ai.png"> tag to include descriptive alt text (e.g.,
alt="AI Preact UI header") so the file exists and the image tag includes
accessibility text.

</div>

<br />

<div align="center">
<a href="https://npmjs.com/package/@tanstack/ai" target="\_parent">
<img alt="" src="https://img.shields.io/npm/dm/@tanstack/ai.svg" />
</a>
<a href="https://github.com/TanStack/ai" target="\_parent">
<img alt="" src="https://img.shields.io/github/stars/TanStack/ai.svg?style=social&label=Star" alt="GitHub stars" />
</a>
<a href="https://bundlephobia.com/result?p=@tanstack/ai@latest" target="\_parent">
<img alt="" src="https://badgen.net/bundlephobia/minzip/@tanstack/ai@latest" />
</a>
Comment on lines +8 to +16
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Badge links reference the wrong package.

The npm downloads and bundlephobia badges reference @tanstack/ai instead of @tanstack/ai-preact-ui. This will display metrics for the wrong package.

Proposed fix
-<a href="https://npmjs.com/package/@tanstack/ai" target="\_parent">
-  <img alt="" src="https://img.shields.io/npm/dm/@tanstack/ai.svg" />
+<a href="https://npmjs.com/package/@tanstack/ai-preact-ui" target="\_parent">
+  <img alt="" src="https://img.shields.io/npm/dm/@tanstack/ai-preact-ui.svg" />
 </a>
...
-<a href="https://bundlephobia.com/result?p=@tanstack/ai@latest" target="\_parent">
-  <img alt="" src="https://badgen.net/bundlephobia/minzip/@tanstack/ai@latest" />
+<a href="https://bundlephobia.com/result?p=@tanstack/ai-preact-ui@latest" target="\_parent">
+  <img alt="" src="https://badgen.net/bundlephobia/minzip/@tanstack/ai-preact-ui@latest" />
 </a>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<a href="https://npmjs.com/package/@tanstack/ai" target="\_parent">
<img alt="" src="https://img.shields.io/npm/dm/@tanstack/ai.svg" />
</a>
<a href="https://github.com/TanStack/ai" target="\_parent">
<img alt="" src="https://img.shields.io/github/stars/TanStack/ai.svg?style=social&label=Star" alt="GitHub stars" />
</a>
<a href="https://bundlephobia.com/result?p=@tanstack/ai@latest" target="\_parent">
<img alt="" src="https://badgen.net/bundlephobia/minzip/@tanstack/ai@latest" />
</a>
<a href="https://npmjs.com/package/@tanstack/ai-preact-ui" target="\_parent">
<img alt="" src="https://img.shields.io/npm/dm/@tanstack/ai-preact-ui.svg" />
</a>
<a href="https://github.com/TanStack/ai" target="\_parent">
<img alt="" src="https://img.shields.io/github/stars/TanStack/ai.svg?style=social&label=Star" alt="GitHub stars" />
</a>
<a href="https://bundlephobia.com/result?p=@tanstack/ai-preact-ui@latest" target="\_parent">
<img alt="" src="https://badgen.net/bundlephobia/minzip/@tanstack/ai-preact-ui@latest" />
</a>
🧰 Tools
🪛 markdownlint-cli2 (0.18.1)

12-12: Hard tabs
Column: 1

(MD010, no-hard-tabs)

🤖 Prompt for AI Agents
In `@packages/typescript/ai-preact-ui/README.md` around lines 8 - 16, The badges
in README.md point to `@tanstack/ai` instead of this package; update the npm and
bundlephobia badge links and image sources to reference `@tanstack/ai-preact-ui`
(replace occurrences of "@tanstack/ai" with "@tanstack/ai-preact-ui" in the
first and third <a> blocks), leaving the GitHub stars badge as-is if it should
still target the repo; ensure href and the src badge URLs both use the corrected
package name so badges show the right metrics.

</div>

<div align="center">
<a href="#badge">
<img alt="semantic-release" src="https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg">
</a>
<a href="#badge">
<img src="https://img.shields.io/github/v/release/tanstack/ai" alt="Release"/>
</a>
<a href="https://twitter.com/tan_stack">
<img src="https://img.shields.io/twitter/follow/tan_stack.svg?style=social" alt="Follow @TanStack"/>
</a>
</div>

<div align="center">

### [Become a Sponsor!](https://github.com/sponsors/tannerlinsley/)
</div>

# TanStack AI Preact UI

Headless Preact components for building AI chat interfaces with TanStack AI.

## Features

- 🎨 **Headless components** - Full control over styling and markup
- 🔄 **Parts-based rendering** - Native support for text, tool calls, thinking, and tool results
- ⚡ **Streaming support** - Real-time message updates
- 🔧 **Tool approval workflows** - Built-in support for tool approval UI
- 📦 **Compound components** - Composable API with `<Chat>`, `<ChatMessages>`, `<ChatInput>`, etc.
- 🎯 **Type-safe** - Full TypeScript support
- 🎭 **Render props** - Customize any part of the UI
- 🪝 **Preact hooks** - Built on `@tanstack/ai-preact`

## Installation

```bash
npm install @tanstack/ai-preact-ui @tanstack/ai-preact
# or
pnpm add @tanstack/ai-preact-ui @tanstack/ai-preact
# or
yarn add @tanstack/ai-preact-ui @tanstack/ai-preact
```

## Quick Start

```tsx
import {
Chat,
ChatMessages,
ChatInput,
ChatMessage,
} from '@tanstack/ai-preact-ui'
import { fetchServerSentEvents } from '@tanstack/ai-client'

function App() {
return (
<Chat connection={fetchServerSentEvents('/api/chat')}>
<ChatMessages>
{(message) => <ChatMessage message={message} />}
</ChatMessages>
<ChatInput placeholder="Type your message..." />
</Chat>
)
}
```

## Components

### `<Chat>`

Root component that provides chat context to all child components.

```tsx
<Chat
connection={fetchServerSentEvents('/api/chat')}
initialMessages={[]}
onFinish={(message) => console.log('Message complete:', message)}
>
{/* child components */}
</Chat>
```

### `<ChatMessages>`

Container for rendering all messages in the conversation.

```tsx
<ChatMessages
autoScroll={true}
emptyState={<p>No messages yet. Start a conversation!</p>}
loadingState={<p>Loading...</p>}
>
{(message) => <ChatMessage message={message} />}
</ChatMessages>
```

### `<ChatMessage>`

Renders a single message with all its parts (text, thinking, tool calls, tool results).

```tsx
<ChatMessage
message={message}
className="mb-4"
userClassName="text-blue-500"
assistantClassName="text-gray-700"
/>
```

#### Custom Part Renderers

```tsx
<ChatMessage
message={message}
textPartRenderer={({ content }) => (
<TextPart content={content} className="prose dark:prose-invert" />
)}
thinkingPartRenderer={({ content, isComplete }) => (
<ThinkingPart
content={content}
isComplete={isComplete}
className="bg-gray-100 dark:bg-gray-800"
/>
)}
toolsRenderer={{
weatherLookup: ({ arguments: args, output }) => (
<WeatherCard data={JSON.parse(args)} result={output} />
),
}}
/>
```

### `<ChatInput>`

Input component for sending messages.

```tsx
<ChatInput placeholder="Type a message..." submitOnEnter={true} />
```

#### Custom Input UI

```tsx
<ChatInput>
{({ value, onChange, onSubmit, isLoading }) => (
<div className="flex gap-2">
<input
value={value}
onInput={(e) => onChange(e.target.value)}
className="flex-1 px-4 py-2 border rounded"
/>
<button
onClick={onSubmit}
disabled={isLoading}
className="px-6 py-2 bg-blue-500 text-white rounded"
>
{isLoading ? 'Sending...' : 'Send'}
</button>
</div>
)}
</ChatInput>
```

### `<TextPart>`

Renders text content with markdown support and syntax highlighting.

````tsx
<TextPart
content="Hello **world**! Here's some code:\n```js\nconsole.log('hi')\n```"
role="assistant"
className="prose dark:prose-invert"
/>
````

### `<ThinkingPart>`

Renders model thinking/reasoning content with collapsible UI.

```tsx
<ThinkingPart
content="Let me think about this step by step..."
isComplete={false}
className="bg-gray-100 p-4 rounded"
/>
```

### `<ToolApproval>`

Renders approve/deny buttons for tools that require approval.

```tsx
<ToolApproval
toolCallId={part.id}
toolName={part.name}
input={JSON.parse(part.arguments)}
approval={part.approval}
/>
```

## Hooks

### `useChatContext()`

Access the chat context from any child component.

```tsx
import { useChatContext } from '@tanstack/ai-preact-ui'

function CustomComponent() {
const { messages, isLoading, sendMessage } = useChatContext()

return (
<div>
<p>Messages: {messages.length}</p>
<p>Loading: {isLoading ? 'Yes' : 'No'}</p>
</div>
)
}
```

## Styling

All components are headless and use data attributes for styling:

```css
/* Message styling */
[data-message-role='user'] {
background: #e3f2fd;
text-align: right;
}

[data-message-role='assistant'] {
background: #f5f5f5;
}

/* Part type styling */
[data-part-type='text'] {
padding: 1rem;
}

[data-part-type='thinking'] {
opacity: 0.8;
font-style: italic;
}

[data-part-type='tool-call'] {
border-left: 3px solid #2196f3;
padding-left: 1rem;
}
```

## Advanced Usage

### Custom Tool Renderers

```tsx
const toolRenderers = {
searchWeb: ({ arguments: args, output }) => (
<SearchResults query={JSON.parse(args).query} results={output} />
),
generateImage: ({ arguments: args, output }) => (
<ImagePreview prompt={JSON.parse(args).prompt} url={output?.url} />
),
}

<ChatMessage message={message} toolsRenderer={toolRenderers} />
```

### Error Handling

```tsx
<ChatMessages
errorState={({ error, reload }) => (
<div className="error">
<p>Error: {error.message}</p>
<button onClick={reload}>Retry</button>
</div>
)}
/>
```

## Documentation

For full documentation, visit [https://tanstack.com/ai](https://tanstack.com/ai)

## License

MIT
56 changes: 56 additions & 0 deletions packages/typescript/ai-preact-ui/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
{
"name": "@tanstack/ai-preact-ui",
"version": "0.0.0",
"description": "Headless Preact components for building AI chat interfaces",
"module": "./dist/esm/index.js",
"types": "./dist/esm/index.d.ts",
"type": "module",
"repository": {
"type": "git",
"url": "git+https://github.com/TanStack/ai.git",
"directory": "packages/typescript/ai-preact-ui"
},
"exports": {
".": {
"types": "./dist/esm/index.d.ts",
"import": "./dist/esm/index.js"
}
},
"scripts": {
"build": "vite build",
"clean": "premove ./build ./dist",
"test:build": "publint --strict",
"test:eslint": "eslint ./src",
"test:lib": "vitest --passWithNoTests",
"test:lib:dev": "pnpm test:lib --watch",
"test:types": "tsc"
},
"keywords": [
"tanstack",
"ai",
"preact",
"chat",
"ui",
"headless",
"components"
],
"dependencies": {
"@tanstack/ai-preact": "workspace:*",
"preact-md": "^0.2.1",
"rehype-highlight": "^7.0.2",
"rehype-raw": "^7.0.0",
"remark-gfm": "^4.0.1"
},
"peerDependencies": {
"@tanstack/ai-preact": "workspace:*",
"preact": ">=10.11.0"
},
"devDependencies": {
"@tanstack/ai-client": "workspace:*",
"@vitest/coverage-v8": "4.0.14",
"vite": "^7.2.7"
},
"files": [
"dist"
]
}
Loading
Loading