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-preact-ui" 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" />
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

Fix duplicate alt attribute.

The <img> tag contains two alt attributes (alt="" and alt="GitHub stars"), which is invalid HTML. Browsers typically use the first occurrence and ignore the second.

Proposed fix
-	  <img alt="" src="https://img.shields.io/github/stars/TanStack/ai.svg?style=social&label=Star" alt="GitHub stars" />
+	  <img alt="GitHub stars" src="https://img.shields.io/github/stars/TanStack/ai.svg?style=social&label=Star" />
📝 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 alt="" src="https://img.shields.io/github/stars/TanStack/ai.svg?style=social&label=Star" alt="GitHub stars" />
<img alt="GitHub stars" src="https://img.shields.io/github/stars/TanStack/ai.svg?style=social&label=Star" />
🧰 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` at line 12, The img tag has two
alt attributes causing invalid HTML; remove the duplicate empty alt and keep a
single meaningful alt (e.g., alt="GitHub stars") on the <img> element so it only
has one alt attribute and conveys the intended accessible text.

</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>
</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