# /docs/agents

---
title: Agents
description: Get started with AI agents in your simple-ai project. A guide to create multi-agent chat applications.
---

# AI Agents Guide

Build multi-agent chat applications with pre-built AI agents. This guide walks you through creating your first project and adding agent capabilities.

> This system is built on top of the **Vercel AI SDK Agents**. For deep dives into agent concepts, check out their [official documentation](https://ai-sdk.dev/docs/agents/overview).


## 1. Add Multi-Agent Chat

The foundation of the agent system is the `chat-04` block. This component handles the UI, agent routing, and real-time streaming.

Install it using the CLI:

```bash
npx shadcn@latest add @simple-ai/chat-04
```

This adds the multi-agent chat interface where users can interact with different agents in a single conversation.

## 2. Install Agents

Now that you have the chat interface, you can install specific agents from the registry. Each agent adds new capabilities to your application.

### Weather Agent
Adds weather forecasting capabilities.

```bash
npx shadcn@latest add @simple-ai/weather-agent
```

## 3. Import the agents

Import the agents into your agent and tool registry files at `lib/ai/agents/agents-registry.ts` and `lib/ai/tools/tools-registry.ts`.

## How It Works

Once installed, agents are automatically available in your chat.
1.  **Install** the agent using the CLI.
2.  **Import** and add it to your agent and tool configuration (usually in `lib/ai/agents/agents-registry.ts` and `lib/ai/tools/tools-registry.ts`).
3.  **Chat** with the agent by mentioning it (e.g., "@weather-agent") or just asking a question if it's the default.

For more agents and examples, visit the [Agents Gallery](/agents).


# /docs/blocks

---
title: Blocks
description: Ready-to-use AI chat interface blocks powered by Vercel AI SDK. Copy and paste into your React apps.
---

## Prerequisites

Our blocks examples are built using the [Vercel AI SDK](https://sdk.vercel.ai/docs), which provides powerful utilities for building AI-powered user interfaces. The examples use:

- `useChat` hook for client-side chat state management
- `streamText` for server-side streaming responses
- OpenAI's GPT-4 model through the `@ai-sdk/openai` provider

### Client-Side Usage

```tsx
import { useChat } from 'ai/react'

export function Chat() {
  const { messages, input, handleInputChange, handleSubmit } = useChat({
    api: '/api/chat'
  })
  
  return (
    // Your chat UI components
  )
}
```

### Server-Side Implementation

```tsx
import { openai } from "@ai-sdk/openai";
import { streamText } from "ai";

export async function POST(req: Request) {
  const { messages } = await req.json();
  
  const result = streamText({
    model: openai("gpt-4-mini"),
    messages,
  });

  return result.toDataStreamResponse();
}
```

## Environment Setup

To use the OpenAI provider in our examples, you'll need an OpenAI API key. Create a `.env` file in your project root and add:

```bash
OPENAI_API_KEY=your_api_key_here
```

For more information about:
- useChat, see the [Vercel AI SDK Chat Documentation](https://sdk.vercel.ai/docs/api-reference/use-chat)
- streamText, see the [Vercel AI SDK Text Streaming Documentation](https://sdk.vercel.ai/docs/ai-sdk-core/generating-text)
- OpenAI provider setup, see the [OpenAI Provider Documentation](https://sdk.vercel.ai/providers/ai-sdk-providers/openai)




# /docs

---
title: Introduction
description: Get started building beautiful, accessible, and performant chat interfaces with simple-ai components.
---

## Why simple-ai?

* **Chat-First Design** - Components specifically designed for chat and conversational interfaces
* **Modern UX Patterns** - Implements patterns seen in leading AI chat applications
* **Copy and Paste** - Use components directly in your app and customize them to your needs
* **TypeScript** - Written in TypeScript for better developer experience

## Vercel AI SDK Compatibility

The components in simple-ai are designed to work seamlessly with [Vercel AI SDK](https://sdk.vercel.ai/). If you're building AI-powered applications with Vercel's SDK, our components provide the perfect UI layer for your chat interfaces, handling streaming responses, loading states, and complex chat interactions out of the box.

## Philosophy

simple-ai follows the same philosophy as [shadcn/ui](https://ui.shadcn.com): giving you ownership and control over the code. We believe that:

1. You should have complete control over your chat interface components
2. Components should be easy to customize and extend
4. Chat interfaces should be performant, even with large message histories

## Credits

This project wouldn't be possible without [shadcn/ui](https://ui.shadcn.com). It serves as the main inspiration for both the philosophy and implementation approach of this collection. We also draw inspiration from the [Vercel AI SDK](https://sdk.vercel.ai/) for our chat-specific components and interactions.

## FAQ

<Accordion type="multiple">

<AccordionItem value="faq-1">
	<AccordionTrigger>
		Why copy/paste and not packaged as a dependency?
	</AccordionTrigger>
	<AccordionContent>
The idea behind this is to give you ownership and control over the code, allowing you to decide how the components are built and styled.

Start with some sensible defaults, then customize the components to your needs.

This approach allows you to maintain full control over your chat interface implementation while starting with battle-tested components.
</AccordionContent>
</AccordionItem>

<AccordionItem value="faq-2">
  <AccordionTrigger>
    Which frameworks are supported?
  </AccordionTrigger>
  <AccordionContent>
    The components are primarily built for Next.js applications, but you can use them with any React-based framework that supports modern React features.
  </AccordionContent>
</AccordionItem>

<AccordionItem value="faq-3">
	<AccordionTrigger>
	Can I use this in my project?
	</AccordionTrigger>
	<AccordionContent>
Yes. Free to use for personal and commercial projects. No attribution required.

We'd love to see what you build with these components!
    </AccordionContent>
</AccordionItem>

</Accordion>


# /docs/installation

---
title: Installation
description: How to install dependencies and structure your app.
---

## Prerequisites

simple-ai is built on top of [shadcn/ui](https://ui.shadcn.com), so you'll need to set it up first:

1. Follow the [shadcn/ui installation guide](https://ui.shadcn.com/docs/installation) to set up shadcn/ui in your project
2. Install the base components you need from shadcn/ui using their CLI
3. Get familiar with how shadcn/ui works - our components follow the same philosophy

## Installing Components

Once you have shadcn/ui set up, you can start adding simple-ai components to your project. Each component page in our documentation includes a command you can run to add it to your project:

```bash
npx shadcn@latest add @simple-ai/chat-message
```

This will:
1. Copy the component to your project
2. Add all required npm dependencies
3. Add any required shadcn/ui components that the component depends on


# /docs/workflows

---
title: Workflows
description: Build powerful AI agent workflows with React Flow components integrated with Vercel AI SDK.
---

# AI Agent Workflow Builder

Build sophisticated AI agent workflows using an interactive visual interface powered by React Flow and deeply integrated with the Vercel AI SDK. Create linear, branching, and conditional workflows that execute seamlessly with real-time streaming and state management.

## Installation

Copy the complete workflow builder into your project using the shadcn CLI:

```bash
npx shadcn@latest add @simple-ai/workflow-01
```

## Live Examples

Before diving into the details, check out the [AI Workflows page](/ai-workflows) where you can:

- **See the workflow builder in action** with real, functional workflows
- **Switch between pre-built templates** (Code Agent, Wikipedia Agent, Support Agent)
- **Copy templates directly** into your project with the shadcn CLI

This is the best way to understand what's possible with the workflow builder!

## Key Features

- 🎨 **Visual Workflow Design**: Drag-and-drop interface for building complex agent flows
- 🤖 **Agent Nodes**: Configure AI agents with custom models, system prompts, tools, and structured outputs
- 🔀 **Conditional Routing**: Create dynamic, branching workflows using if-else nodes. Conditions are written in the powerful and standard **Common Expression Language (CEL)**, providing a secure and flexible way to evaluate node outputs.
- 💬 **Chat Integration**: Seamlessly integrates with the `useChat` hook and UI message streams
- ⚡ **Real-time Streaming**: Stream agent responses and workflow status updates live to your chat
- ✅ **Workflow Validation**: Real-time validation with helpful error messages
- 💾 **Easy Persistence**: Workflows are simple JSON (nodes + edges) - save to your database and run anywhere

## The Tech Stack

- [React Flow](https://reactflow.dev) - Visual workflow canvas and node management
- [Vercel AI SDK](https://ai-sdk.dev/docs/introduction) - AI capabilities with `streamText` and UI message streams
- [shadcn/ui](https://ui.shadcn.com) - Beautiful, accessible UI components

## How It Works

### Workflow Structure

Every workflow consists of **nodes** (the steps) and **edges** (the connections):

```typescript
{
  nodes: FlowNode[],  // Array of workflow nodes
  edges: FlowEdge[]   // Array of connections between nodes
}
```

### Node Types

1. **Start Node**: Entry point for every workflow - receives the user message

2. **Agent Node**: AI agents that process inputs using `streamText` from Vercel AI SDK
   - Configure model, system prompt, tools, and max steps
   - Choose between text or structured JSON output
   - Control conversation history inclusion

3. **If-Else Node**: Conditional routing based on previous node outputs using CEL expressions
4. **Wait Node**: Pauses the workflow for a specified duration (seconds, minutes, or hours) before continuing to the next node
5. **End Node**: Terminal point for workflow execution paths
6. **Note Node**: Documentation and comments (not executed)

### Execution Flow

Workflows execute **linearly** from start to end:

1. User sends a message via the chat interface
2. Frontend sends the message along with current nodes and edges using the `useChat` hook
3. Backend executes the workflow using UI message data streams:
   - Starts from the start node
   - Processes each node sequentially
   - Agent nodes use `streamText` and stream responses back to the UI
   - If-else nodes evaluate conditions and route to the next node
   - Continues until reaching an end node
4. Real-time status updates stream to the UI showing node execution states

### Persistence and Execution

Workflows are just data - save them to your database and use them in two ways:

**Option 1: Load into UI**
```typescript
// Load workflow into the visual editor
const workflow = await db.workflows.findById(id);
store.initializeWorkflow({
  nodes: workflow.nodes,
  edges: workflow.edges
});
```

**Option 2: Run in Background**
```typescript
// Execute workflow directly without loading into UI
const workflow = await db.workflows.findById(id);
const result = await executeWorkflow({
  nodes: workflow.nodes,
  edges: workflow.edges,
  messages: userMessages,
  writer: streamWriter
});
```

This means you can build workflows visually, save them, and then execute them programmatically in the background - perfect for automation, scheduled tasks, or API endpoints.

## Extending the Workflow

The new architecture makes it incredibly easy to add new, custom nodes to the workflow builder. Each node's logic is cleanly separated into three distinct parts, making them easy to manage and for AI tools to generate.

### Understanding the Node Architecture

Every node is defined by a folder within `src/registry/blocks/workflow-01/lib/workflow/nodes/`. This folder contains three key files that separate its concerns:

- **`*.shared.ts`**: Defines the node's data structure using a `zod` schema, its type definitions, and its validation logic. This code runs on both the client and server.

- **`*.client.tsx`**: Contains the React components for the node. This includes the visual representation on the React Flow canvas and the configuration panel that appears when a node is selected.

- **`*.server.ts`**: Holds the server-side execution logic. This is where the node's primary function is implemented, determining what happens when the workflow executes this step.

### Adding a New Node

Here's how you can add a new custom node, such as a simple `console-log` node:

**1. Create the Node Directory**

First, create a new folder for your node:

```bash
mkdir src/lib/workflow/nodes/console-log
```

**2. Define Shared Logic (`console-log.shared.ts`)**

Create the shared file to define the node's data schema and validation rules. For a simple log node, this might just include a message template.

**3. Create the Client Component (`console-log.client.tsx`)**

This file defines the node's appearance on the canvas and its settings panel. You'll export a React component for the node itself and another for its editor panel.

**4. Implement Server-side Execution (`console-log.server.ts`)**

This is where the node's backend logic lives. The `execute` function receives the current workflow context and returns the result and the ID of the next node to execute.

```typescript  
function executeConsoleLogNode(context) {
  const { node, executionMemory, previousNodeId } = context;
  const previousResult = executionMemory[previousNodeId];

  console.log(`[Workflow Log] From node ${node.id}:`, previousResult?.text);

  // ... find next node and return
}
```

**5. Create the Node Definition (`index.ts`)**

Combine the shared, client, and server parts into a single `NodeDefinition` object in the node's `index.ts` file.

**6. Register the Node**

Finally, import your new node definition and add it to the `nodeRegistry` object in `src/lib/workflow/nodes/index.ts`.

```typescript                  
import { consoleLogNodeDefinition } from "./console-log";
// ... other imports

const nodeDefinitions = {
  // ... other nodes
  "console-log": consoleLogNodeDefinition,
} as const;
```

Your `console-log` node will now appear in the "Add Nodes" panel and be fully integrated into the workflow system.

## Full Application Example

A full application example using this workflow builder is **coming soon**. In the meantime, explore the [live examples](/ai-workflows) to see the workflow builder in action.


# /docs/components/chat-input

---
title: Chat Input
description: A chat input component with mention support and automatic height adjustment.
component: true
---

```tsx
"use client";

import { FileIcon } from "lucide-react";
import { useEffect, useState } from "react";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { Badge } from "@/components/ui/badge";
import { highlightCode } from "@/lib/highlight-code";
import {
	ChatInput,
	ChatInputEditor,
	ChatInputGroupAddon,
	ChatInputMention,
	ChatInputMentionButton,
	ChatInputSubmitButton,
	createMentionConfig,
	useChatInput,
} from "@/components/ui/chat-input";

type MemberItem = {
	id: string;
	name: string;
	image?: string;
	type: string;
};

type FileItem = {
	id: string;
	name: string;
};

const members: MemberItem[] = [
	{ id: "1", name: "Alice", image: "/avatar-1.png", type: "agent" },
	{ id: "2", name: "Bob", type: "user" },
	{ id: "3", name: "Charlie", image: "/avatar-2.png", type: "bot" },
	{ id: "4", name: "Dave", type: "user" },
];

const files: FileItem[] = [
	{ id: "f1", name: "report.pdf" },
	{ id: "f2", name: "image.png" },
	{ id: "f3", name: "notes.txt" },
];

export function ChatInputWithMentions() {
	const [highlightedOutput, setHighlightedOutput] = useState<string>("");

	const { value, onChange, parsed, handleSubmit, mentionConfigs } =
		useChatInput({
			mentions: {
				member: createMentionConfig<MemberItem>({
					type: "member",
					trigger: "@",
					items: members,
				}),
				file: createMentionConfig<FileItem>({
					type: "file",
					trigger: "/",
					items: files,
				}),
			},
			onSubmit: (parsedValue) => {
				console.log("Submitted parsed:", parsedValue);
				console.log("Members mentioned:", parsedValue.member);
				console.log("Files mentioned:", parsedValue.file);
			},
		});

	useEffect(() => {
		highlightCode(JSON.stringify(parsed, null, 2), "json").then(
			setHighlightedOutput,
		);
	}, [parsed]);

	return (
		<div className="w-full h-full flex justify-center items-center">
			<div className="w-full max-w-md space-y-4">
				<ChatInput
					onSubmit={handleSubmit}
					value={value}
					onChange={onChange}
				>
					<ChatInputMention
						type={mentionConfigs.member.type}
						trigger={mentionConfigs.member.trigger}
						items={mentionConfigs.member.items}
					>
						{(item) => (
							<>
								<Avatar className="h-6 w-6">
									<AvatarImage
										src={item.image ?? "/placeholder.jpg"}
										alt={item.name}
									/>
									<AvatarFallback>
										{item.name[0].toUpperCase()}
									</AvatarFallback>
								</Avatar>

								<span
									className="text-sm font-medium truncate max-w-[120px]"
									title={item.name}
								>
									{item.name}
								</span>
								<Badge variant="outline" className="ml-auto">
									{item.type}
								</Badge>
							</>
						)}
					</ChatInputMention>
					<ChatInputMention
						type={mentionConfigs.file.type}
						trigger={mentionConfigs.file.trigger}
						items={mentionConfigs.file.items}
					>
						{(item) => (
							<>
								<FileIcon className="h-4 w-4 text-muted-foreground" />
								<span
									className="text-sm font-medium truncate max-w-[200px]"
									title={item.name}
								>
									{item.name}
								</span>
							</>
						)}
					</ChatInputMention>
					<ChatInputEditor placeholder="Type @ for agents, / for files..." />
					<ChatInputGroupAddon align="block-end">
						<ChatInputMentionButton />
						<ChatInputSubmitButton className="ml-auto" />
					</ChatInputGroupAddon>
				</ChatInput>

				{/* Debug output */}
				<div className="space-y-2">
					<div>
						<h4 className="font-semibold mb-2 text-sm">
							Parsed Output:
						</h4>
						<div className="bg-code rounded-lg border border-border p-0 text-sm max-h-32 overflow-y-auto">
							<div
								dangerouslySetInnerHTML={{
									__html: highlightedOutput,
								}}
							/>
						</div>
					</div>
				</div>
			</div>
		</div>
	);
}

```

## Installation

<CodeTabs>

<TabsList>
  <TabsTrigger value="cli">CLI</TabsTrigger>
  <TabsTrigger value="manual">Manual</TabsTrigger>
</TabsList>
<TabsContent value="cli">

```bash
npx shadcn@latest add @simple-ai/chat-input
```

</TabsContent>

<TabsContent value="manual">

<Steps>

<Step>Install required dependencies</Step>

```bash
npm install @tiptap/core @tiptap/react @tiptap/starter-kit @tiptap/extension-mention @tiptap/extension-placeholder @tiptap/suggestion tippy.js
```

<Step>Copy and paste the following code into your project.</Step>

`chat-input.tsx`

<ComponentSource name="chat-input" />

<Step>Update the import paths to match your project setup.</Step>

</Steps>

</TabsContent>

</CodeTabs>


## About

A powerful chat input component built on TipTap editor that features automatic height adjustment, smart keyboard handling, and optional mention support. Press Enter to submit or Shift+Enter for new lines. The component can be used as a simple text input or extended with mentions for @users, /files, and custom triggers.

This component is built on top of [TipTap](https://tiptap.dev/), a headless editor framework for building rich text editors. It provides a rich text editor with mention support, automatic height adjustment, and proper keyboard handling.

## Usage

```tsx
import {
  ChatInput,
  ChatInputEditor,
  ChatInputMention,
  ChatInputSubmitButton,
  createMentionConfig,
  useChatInput,
} from "@/components/ui/chat-input"

type Member = { id: string; name: string; avatar?: string };
const members: Member[] = [
  { id: "1", name: "Alice", avatar: "/alice.jpg" },
  { id: "2", name: "Bob" },
];

export function ChatWithMentions() {
  const { value, onChange, parsed, handleSubmit, mentionConfigs } =
    useChatInput({
      mentions: {
        member: createMentionConfig<Member>({
          type: "member",
          trigger: "@",
          items: members,
        }),
      },
      onSubmit: (parsed) => {
        console.log("Content:", parsed.content);
        console.log("Mentioned members:", parsed.member);
      },
    });

  return (
    <ChatInput onSubmit={handleSubmit} value={value} onChange={onChange}>
      <ChatInputMention
        type={mentionConfigs.member.type}
        trigger={mentionConfigs.member.trigger}
        items={mentionConfigs.member.items}
      >
        {(item) => <span>{item.name}</span>}
      </ChatInputMention>
      <ChatInputEditor placeholder="Type @ to mention..." />
      <ChatInputGroupAddon align="block-end">
        <ChatInputSubmitButton className="ml-auto" />
      </ChatInputGroupAddon>
    </ChatInput>
  );
}
```

## Examples

### Without mentions

```tsx
"use client";

import { useState } from "react";
import { toast } from "sonner";
import {
	ChatInput,
	ChatInputEditor,
	ChatInputGroupAddon,
	ChatInputSubmitButton,
	useChatInput,
} from "@/components/ui/chat-input";

export function ChatInputDemo() {
	const [isLoading, setIsLoading] = useState(false);

	const { value, onChange, handleSubmit } = useChatInput({
		onSubmit: (parsed) => {
			setIsLoading(true);
			toast(parsed.content);
			setTimeout(() => setIsLoading(false), 1000);
		},
	});

	return (
		<div className="w-full h-full flex justify-center items-center">
			<div className="w-full max-w-md">
				<ChatInput
					onSubmit={handleSubmit}
					value={value}
					onChange={onChange}
					isStreaming={isLoading}
					onStop={() => setIsLoading(false)}
				>
					<ChatInputEditor placeholder="Type a message..." />
					<ChatInputGroupAddon align="block-end">
						<ChatInputSubmitButton className="ml-auto" />
					</ChatInputGroupAddon>
				</ChatInput>
			</div>
		</div>
	);
}

```

### With Custom Layout

```tsx
"use client";

import { PlusIcon } from "lucide-react";
import { Separator } from "@/components/ui/separator";
import {
	ChatInput,
	ChatInputEditor,
	ChatInputGroupAddon,
	ChatInputGroupButton,
	ChatInputGroupText,
	ChatInputSubmitButton,
	useChatInput,
} from "@/components/ui/chat-input";

export function ChatInputWithAddons() {
	const { value, onChange, handleSubmit } = useChatInput({
		onSubmit: (parsed) => {
			console.log("Submitted:", parsed.content);
		},
	});

	return (
		<div className="w-full h-full flex justify-center items-center">
			<div className="w-full max-w-md">
				<ChatInput
					onSubmit={handleSubmit}
					value={value}
					onChange={onChange}
				>
					<ChatInputEditor placeholder="Type a message..." />
					<ChatInputGroupAddon align="block-end">
						<ChatInputGroupButton
							variant="outline"
							className="rounded-full"
							size="icon-sm"
						>
							<PlusIcon />
						</ChatInputGroupButton>
						<ChatInputGroupText className="ml-auto">
							52% used
						</ChatInputGroupText>
						<Separator orientation="vertical" className="h-6!" />
						<ChatInputSubmitButton />
					</ChatInputGroupAddon>
				</ChatInput>
			</div>
		</div>
	);
}

```

### Custom Mention Styling

## Reference

### ChatInput

Root container component that manages the input state and context.

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| onSubmit | `() => void` | - | Callback when message is submitted |
| isStreaming | `boolean` | `false` | Whether AI is currently streaming a response |
| onStop | `() => void` | - | Callback to stop streaming (shows stop button) |
| disabled | `boolean` | `false` | Disables the input |
| value | `JSONContent` | - | Controlled TipTap JSON value |
| onChange | `(value: JSONContent) => void` | - | Value change handler |

### ChatInputEditor

The TipTap editor instance for text input.

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| placeholder | `string` | `"Type a message..."` | Placeholder text |
| disabled | `boolean` | - | Disables editing |
| onEnter | `() => void` | - | Custom Enter key handler |
| value | `JSONContent` | - | Optional controlled value |
| onChange | `(value: JSONContent) => void` | - | Optional change handler |

### ChatInputMention

Registers a mention type (render prop pattern).

| Prop | Type | Description |
|------|------|-------------|
| type | `string` | Unique mention type identifier |
| trigger | `string` | Trigger character (e.g., `"@"` or `"/"`) |
| items | `T[]` | Array of mention items |
| children | `(item: T, isSelected: boolean) => ReactNode` | Render function for dropdown items |
| editorMentionClass | `string` | Custom CSS class for mentions in editor |

### ChatInputSubmitButton

Submit button that adapts to loading/streaming states.

| Prop | Type | Description |
|------|------|-------------|
| isStreaming | `boolean` | Override streaming state |
| onStop | `() => void` | Override stop handler |
| disabled | `boolean` | Disable the button |

### useChatInput

Manages chat input state with optional mentions support.

#### Parameters

| Prop | Type | Description |
|------|------|-------------|
| mentions | `Record<string, MentionConfig<any>>` | Mention configurations |
| initialValue | `JSONContent` | Initial value |
| onSubmit | `(parsed: ParsedFromObject<Mentions>) => void` | Callback when message is submitted |

#### Returns

| Prop | Type | Description |
|------|------|-------------|
| value | `JSONContent` | Current value |
| onChange | `(value: JSONContent) => void` | Value change handler |
| parsed | `ParsedFromObject<Mentions>` | Parsed output with content string and mention arrays |
| clear | `() => void` | Function to clear the input |
| handleSubmit | `() => void` | Submit handler |
| mentionConfigs | `Record<string, MentionConfig<any>>` | Mention configs (when using mentions) |


# /docs/components/chat-message-area

---
title: Chat Message Area
description: A component that adds auto scrolling functionality to a list of messages.
component: true
---

```tsx
"use client";

import type { UIMessage } from "@ai-sdk/react";
import {
	ChatMessage,
	ChatMessageAuthor,
	ChatMessageAvatar,
	ChatMessageAvatarFallback,
	ChatMessageAvatarImage,
	ChatMessageContainer,
	ChatMessageContent,
	ChatMessageHeader,
	ChatMessageMarkdown,
	ChatMessageTimestamp,
} from "@/components/ui/chat-message";
import {
	ChatMessageArea,
	ChatMessageAreaContent,
	ChatMessageAreaScrollButton,
} from "@/components/ui/chat-message-area";

const messages: UIMessage<{
	member: {
		image: string;
		name: string;
	};
}>[] = [
	{
		id: "1",
		parts: [
			{
				type: "text",
				text: "Can you tell me a story? Maybe something about a magical forest?",
			},
		],
		role: "user",
		metadata: {
			member: {
				image: "/avatar-1.png",
				name: "You",
			},
		},
	},
	{
		id: "2",
		parts: [
			{
				type: "text",
				text: "Of course! I'd love to tell you a story about the Whispering Woods. Would you like to hear it?",
			},
		],
		role: "assistant",
		metadata: {
			member: {
				image: "/avatar-2.png",
				name: "Assistant",
			},
		},
	},
	{
		id: "3",
		parts: [
			{
				type: "text",
				text: "Yes, please! I'm excited to hear it!",
			},
		],
		role: "user",
		metadata: {
			member: {
				image: "/avatar-1.png",
				name: "You",
			},
		},
	},
	{
		id: "4",
		parts: [
			{
				type: "text",
				text: "Deep in the heart of the Whispering Woods, there lived a young fox named Luna with fur as silver as moonlight. Unlike other foxes, Luna had the magical ability to speak with the ancient trees that surrounded her home.",
			},
		],
		role: "assistant",
		metadata: {
			member: {
				image: "/avatar-2.png",
				name: "Assistant",
			},
		},
	},
	{
		id: "5",
		parts: [
			{
				type: "text",
				text: "One day, Luna discovered that the oldest tree in the forest had fallen silent. This was very unusual, as this particular oak tree loved telling stories about the forest's history. Concerned, Luna decided to investigate.",
			},
		],
		role: "assistant",
		metadata: {
			member: {
				image: "/avatar-2.png",
				name: "Assistant",
			},
		},
	},
	{
		id: "6",
		parts: [
			{
				type: "text",
				text: "Oh no! What happened to the old oak tree?",
			},
		],
		role: "user",
		metadata: {
			member: {
				image: "/avatar-1.png",
				name: "You",
			},
		},
	},
	{
		id: "7",
		parts: [
			{
				type: "text",
				text: "As Luna approached the ancient oak, she noticed something glowing at its roots - a tiny crystal that pulsed with a soft blue light. The tree had been protecting this crystal for centuries, and now it was losing its power.",
			},
		],
		role: "assistant",
		metadata: {
			member: {
				image: "/avatar-2.png",
				name: "Assistant",
			},
		},
	},
	{
		id: "8",
		parts: [
			{
				type: "text",
				text: "Luna knew she had to help. She gathered dewdrops from spider webs at dawn, collected starlight in flower petals at night, and asked the wind to share its oldest songs. With these magical ingredients, she restored the crystal's power.",
			},
		],
		role: "assistant",
		metadata: {
			member: {
				image: "/avatar-2.png",
				name: "Assistant",
			},
		},
	},
	{
		id: "9",
		parts: [
			{
				type: "text",
				text: "Did it work? Did the old oak tree start speaking again?",
			},
		],
		role: "user",
		metadata: {
			member: {
				image: "/avatar-1.png",
				name: "You",
			},
		},
	},
	{
		id: "10",
		parts: [
			{
				type: "text",
				text: "Yes! The moment the crystal began glowing brightly again, the old oak's leaves rustled with joy, and its deep, wise voice returned. It thanked Luna for her help and shared even more wonderful stories about the forest's ancient magic.",
			},
		],
		role: "assistant",
		metadata: {
			member: {
				image: "/avatar-2.png",
				name: "Assistant",
			},
		},
	},
	{
		id: "11",
		parts: [
			{
				type: "text",
				text: "From that day forward, Luna became known as the Guardian of the Whispering Woods, and she made sure to visit the old oak tree every day to hear its wonderful tales.",
			},
		],
		role: "assistant",
		metadata: {
			member: {
				image: "/avatar-2.png",
				name: "Assistant",
			},
		},
	},
	{
		id: "12",
		parts: [
			{
				type: "text",
				text: "That was such a beautiful story! I loved how Luna helped save the old oak tree's voice.",
			},
		],
		role: "user",
		metadata: {
			member: {
				image: "/avatar-1.png",
				name: "You",
			},
		},
	},
	{
		id: "13",
		parts: [
			{
				type: "text",
				text: "I'm glad you enjoyed it! The story teaches us that even the smallest acts of kindness can help preserve the magic in our world.",
			},
		],
		role: "assistant",
		metadata: {
			member: {
				image: "/avatar-2.png",
				name: "Assistant",
			},
		},
	},
];

export function ChatMessageAreaDemo() {
	return (
		<ChatMessageArea>
			<ChatMessageAreaContent>
				{messages.map((message) => (
					<ChatMessage key={message.id}>
						<ChatMessageAvatar>
							<ChatMessageAvatarImage
								src={message.metadata?.member.image}
							/>
							<ChatMessageAvatarFallback>
								{message.metadata?.member.name
									.charAt(0)
									.toUpperCase()}
							</ChatMessageAvatarFallback>
						</ChatMessageAvatar>

						<ChatMessageContainer>
							<ChatMessageHeader>
								<ChatMessageAuthor>
									{message.metadata?.member.name}
								</ChatMessageAuthor>
								<ChatMessageTimestamp createdAt={new Date()} />
							</ChatMessageHeader>

							<ChatMessageContent>
								{message.parts
									.filter((part) => part.type === "text")
									.map((part) => (
										<ChatMessageMarkdown
											key={part.type}
											content={part.text}
										/>
									))}
							</ChatMessageContent>
						</ChatMessageContainer>
					</ChatMessage>
				))}
			</ChatMessageAreaContent>
			<ChatMessageAreaScrollButton />
		</ChatMessageArea>
	);
}

```

The Chat Message Area component provides a responsive and intelligent scrollable container specifically designed for streaming interfaces. It features a sophisticated auto-scrolling system that adapts to user interaction:

- **Smart Auto-scroll**: Automatically scrolls to new messages when they appear, but only if the user is already at the bottom
- **Interaction Awareness**: Auto-scroll automatically disables when users scroll up to read previous messages
- **Re-engagement**: Auto-scroll re-enables when users manually scroll back to the bottom
- **Mobile Optimized**: On touch devices, auto-scroll pauses while the user's finger is on the screen
- **Convenient Navigation**: Includes a scroll-to-bottom button that appears when not at the bottom

## About

This component is built on top of [use-stick-to-bottom](https://www.npmjs.com/package/use-stick-to-bottom), a React hook that provides smooth auto-scrolling functionality for chat interfaces and streaming content.

## Installation

<CodeTabs>

<TabsList>
  <TabsTrigger value="cli">CLI</TabsTrigger>
  <TabsTrigger value="manual">Manual</TabsTrigger>
</TabsList>
<TabsContent value="cli">

```bash
npx shadcn@latest add @simple-ai/chat-message-area
```

</TabsContent>

<TabsContent value="manual">

<Steps>

<Step>Install required dependencies</Step>

```bash
npm install use-stick-to-bottom
```

<Step>Copy and paste the following code into your project.</Step>

`chat-message-area.tsx`

<ComponentSource name="chat-message-area" />

<Step>Update the import paths to match your project setup.</Step>

</Steps>

</TabsContent>

</CodeTabs>

## Usage

```tsx
import {
  ChatMessageArea,
  ChatMessageAreaContent,
  ChatMessageAreaScrollButton,
} from "@/components/ui/chat-message-area"
```

```tsx
<ChatMessageArea>
  <ChatMessageAreaContent className="px-4 py-8 space-y-4">
    {/* Your messages go here */}
  </ChatMessageAreaContent>
  <ChatMessageAreaScrollButton />
</ChatMessageArea>
```

## Examples

### Alignment

You can align the scroll button to the left, right, or center using the `alignment` prop on `ChatMessageAreaScrollButton`.

```tsx
"use client";

import type { UIMessage } from "@ai-sdk/react";
import {
	ChatMessage,
	ChatMessageAuthor,
	ChatMessageAvatar,
	ChatMessageAvatarFallback,
	ChatMessageAvatarImage,
	ChatMessageContainer,
	ChatMessageContent,
	ChatMessageHeader,
	ChatMessageMarkdown,
	ChatMessageTimestamp,
} from "@/components/ui/chat-message";
import {
	ChatMessageArea,
	ChatMessageAreaContent,
	ChatMessageAreaScrollButton,
} from "@/components/ui/chat-message-area";

const messages: UIMessage<{
	member: {
		image: string;
		name: string;
	};
}>[] = [
	{
		id: "1",
		parts: [
			{
				type: "text",
				text: "Can you tell me a story? Maybe something about a magical forest?",
			},
		],
		role: "user",
		metadata: {
			member: {
				image: "/avatar-1.png",
				name: "You",
			},
		},
	},
	{
		id: "2",
		parts: [
			{
				type: "text",
				text: "Of course! I'd love to tell you a story about the Whispering Woods. Would you like to hear it?",
			},
		],
		role: "assistant",
		metadata: {
			member: {
				image: "/avatar-2.png",
				name: "Assistant",
			},
		},
	},
	{
		id: "3",
		parts: [
			{
				type: "text",
				text: "Yes, please! I'm excited to hear it!",
			},
		],
		role: "user",
		metadata: {
			member: {
				image: "/avatar-1.png",
				name: "You",
			},
		},
	},
	{
		id: "4",
		parts: [
			{
				type: "text",
				text: "Deep in the heart of the Whispering Woods, there lived a young fox named Luna with fur as silver as moonlight. Unlike other foxes, Luna had the magical ability to speak with the ancient trees that surrounded her home.",
			},
		],
		role: "assistant",
		metadata: {
			member: {
				image: "/avatar-2.png",
				name: "Assistant",
			},
		},
	},
	{
		id: "5",
		parts: [
			{
				type: "text",
				text: "One day, Luna discovered that the oldest tree in the forest had fallen silent. This was very unusual, as this particular oak tree loved telling stories about the forest's history. Concerned, Luna decided to investigate.",
			},
		],
		role: "assistant",
		metadata: {
			member: {
				image: "/avatar-2.png",
				name: "Assistant",
			},
		},
	},
	{
		id: "6",
		parts: [
			{
				type: "text",
				text: "Oh no! What happened to the old oak tree?",
			},
		],
		role: "user",
		metadata: {
			member: {
				image: "/avatar-1.png",
				name: "You",
			},
		},
	},
	{
		id: "7",
		parts: [
			{
				type: "text",
				text: "As Luna approached the ancient oak, she noticed something glowing at its roots - a tiny crystal that pulsed with a soft blue light. The tree had been protecting this crystal for centuries, and now it was losing its power.",
			},
		],
		role: "assistant",
		metadata: {
			member: {
				image: "/avatar-2.png",
				name: "Assistant",
			},
		},
	},
	{
		id: "8",
		parts: [
			{
				type: "text",
				text: "Luna knew she had to help. She gathered dewdrops from spider webs at dawn, collected starlight in flower petals at night, and asked the wind to share its oldest songs. With these magical ingredients, she restored the crystal's power.",
			},
		],
		role: "assistant",
		metadata: {
			member: {
				image: "/avatar-2.png",
				name: "Assistant",
			},
		},
	},
	{
		id: "9",
		parts: [
			{
				type: "text",
				text: "Did it work? Did the old oak tree start speaking again?",
			},
		],
		role: "user",
		metadata: {
			member: {
				image: "/avatar-1.png",
				name: "You",
			},
		},
	},
	{
		id: "10",
		parts: [
			{
				type: "text",
				text: "Yes! The moment the crystal began glowing brightly again, the old oak's leaves rustled with joy, and its deep, wise voice returned. It thanked Luna for her help and shared even more wonderful stories about the forest's ancient magic.",
			},
		],
		role: "assistant",
		metadata: {
			member: {
				image: "/avatar-2.png",
				name: "Assistant",
			},
		},
	},
	{
		id: "11",
		parts: [
			{
				type: "text",
				text: "From that day forward, Luna became known as the Guardian of the Whispering Woods, and she made sure to visit the old oak tree every day to hear its wonderful tales.",
			},
		],
		role: "assistant",
		metadata: {
			member: {
				image: "/avatar-2.png",
				name: "Assistant",
			},
		},
	},
	{
		id: "12",
		parts: [
			{
				type: "text",
				text: "That was such a beautiful story! I loved how Luna helped save the old oak tree's voice.",
			},
		],
		role: "user",
		metadata: {
			member: {
				image: "/avatar-1.png",
				name: "You",
			},
		},
	},
	{
		id: "13",
		parts: [
			{
				type: "text",
				text: "I'm glad you enjoyed it! The story teaches us that even the smallest acts of kindness can help preserve the magic in our world.",
			},
		],
		role: "assistant",
		metadata: {
			member: {
				image: "/avatar-2.png",
				name: "Assistant",
			},
		},
	},
];

export function ChatMessageAreaDemoAlignment() {
	return (
		<ChatMessageArea>
			<ChatMessageAreaContent>
				{messages.map((message) => (
					<ChatMessage key={message.id}>
						<ChatMessageAvatar>
							<ChatMessageAvatarImage
								src={message.metadata?.member.image}
							/>
							<ChatMessageAvatarFallback>
								{message.metadata?.member.name
									.charAt(0)
									.toUpperCase()}
							</ChatMessageAvatarFallback>
						</ChatMessageAvatar>

						<ChatMessageContainer>
							<ChatMessageHeader>
								<ChatMessageAuthor>
									{message.metadata?.member.name}
								</ChatMessageAuthor>
								<ChatMessageTimestamp createdAt={new Date()} />
							</ChatMessageHeader>

							<ChatMessageContent>
								{message.parts
									.filter((part) => part.type === "text")
									.map((part) => (
										<ChatMessageMarkdown
											key={part.type}
											content={part.text}
										/>
									))}
							</ChatMessageContent>
						</ChatMessageContainer>
					</ChatMessage>
				))}
			</ChatMessageAreaContent>
			<ChatMessageAreaScrollButton alignment="center" />
		</ChatMessageArea>
	);
}

```

### Streaming

Try out the auto-scrolling behavior in this live demo. Scroll up to pause auto-scrolling, scroll to bottom to re-enable it, or use the scroll button for quick navigation. On mobile, touch the screen to temporarily pause auto-scroll.

```tsx
"use client";

import type { UIMessage } from "@ai-sdk/react";
import { useEffect, useState } from "react";
import { Button } from "@/components/ui/button";
import {
	ChatMessage,
	ChatMessageAuthor,
	ChatMessageAvatar,
	ChatMessageAvatarFallback,
	ChatMessageAvatarImage,
	ChatMessageContainer,
	ChatMessageContent,
	ChatMessageHeader,
	ChatMessageMarkdown,
	ChatMessageTimestamp,
} from "@/components/ui/chat-message";
import {
	ChatMessageArea,
	ChatMessageAreaContent,
	ChatMessageAreaScrollButton,
} from "@/components/ui/chat-message-area";

const userMessage: UIMessage<{
	member: {
		image: string;
		name: string;
	};
}> = {
	id: "1",
	parts: [
		{
			type: "text",
			text: "Can you tell me a magical story?",
		},
	],
	role: "user",
	metadata: {
		member: {
			image: "/avatar-1.png",
			name: "You",
		},
	},
};

const STORY = `# The Tale of the Enchanted Forest

Deep in a mystical realm, there lay an ancient forest that held secrets beyond imagination. The trees here weren't mere plants; they were the keepers of ancient wisdom, their leaves whispering tales of ages past.

## The Guardian's Call

In this magical place lived a young spirit named Aria, chosen by the forest itself to be its guardian. Her hair flowed like silver moonlight, and her eyes held the depth of ancient pools.

### The First Challenge

* One morning, Aria discovered that something was amiss
* The usually vibrant crystal flowers had begun to fade
* The ancient whispers of the trees grew fainter
* A shadow crept through the undergrowth

## The Journey Begins

Aria knew she had to act quickly. She gathered:
1. Dewdrops from spider webs at dawn
2. Starlight captured in moonflower petals
3. Songs of the morning birds
4. Tears of the evening mist

> "In the heart of every forest lies a truth waiting to be discovered" - Ancient Forest Proverb

### The Discovery

As Aria ventured deeper into the forest's heart, she found an ancient clearing. Here, the very air seemed to pulse with magic. Crystal formations emerged from the earth like frozen rainbows, each one containing memories of the forest's past.

## The Ancient Guardians

In her quest, Aria encountered the spirits of previous guardians:

* The Flame Keeper, whose torch lit the darkest paths
* The Wind Whisperer, who knew the language of storms
* The Earth Mother, who could heal the land with a touch
* The Star Walker, who mapped the celestial dance

Each spirit shared their wisdom:

1. "Change is the only constant in nature"
2. "Listen to the silence between heartbeats"
3. "Growth comes from embracing the unknown"
4. "Every ending is a new beginning"

### The Hidden Valley

Beyond the ancient clearing, Aria discovered a hidden valley where:

* Rainbow falls cascaded into pools of liquid starlight
* Flowers sang melodies of forgotten lullabies
* Butterflies painted the air with trails of golden dust
* Ancient runes danced on stone walls, telling stories of old

## The Dark Challenge

As night fell, shadows gathered and formed into creatures of doubt:

1. The Mist Wraith, who clouded clear thoughts
2. The Echo Thief, who stole confident voices
3. The Dream Weaver, who tangled hopes in fear
4. The Time Shifter, who made moments feel eternal

> "Courage is not the absence of fear, but the wisdom to dance with it" - Whispers of the Wise Trees

### The Battle Within

Aria faced her greatest challenge not in the physical realm, but within herself. She learned that:

* True power comes from acceptance
* Change can be beautiful and terrifying
* Magic flows through all living things
* Every creature has its own song to sing

## The Resolution

Through her connection with the forest, Aria learned that the fading magic was not a sign of decay, but of transformation. The forest wasn't dying; it was evolving, preparing for a new age of wonders.

### The New Beginning

* The crystal flowers bloomed again, brighter than ever
* New forms of magic emerged from the ancient earth
* The whispers of the trees grew stronger, carrying new songs
* And Aria, forever changed, became one with the forest's heart

## The Legacy

As the forest renewed itself:

1. Ancient spells transformed into new forms of magic
2. Lost pathways revealed themselves to worthy wanderers
3. The boundary between dreams and reality grew thin
4. Nature's wisdom found new ways to express itself

### The Eternal Dance

The forest continues its eternal dance:

* Seasons shift in kaleidoscope patterns
* Magic pulses in harmony with the earth's heartbeat
* New guardians arise when they're needed most
* Stories weave themselves into the fabric of reality

---

*And so, the tale of the Enchanted Forest continues, ever-changing, ever-growing, in the endless cycle of magic and wonder. Each day brings new mysteries, and each night holds its own enchantments, as the forest and its guardian dance their eternal dance of transformation and renewal...*`;

export function ChatMessageAreaDemo() {
	const [streamContent, setStreamContent] = useState("");
	const [isStreaming, setIsStreaming] = useState(false);

	useEffect(() => {
		if (!isStreaming) {
			return;
		}

		let currentIndex = 0;
		const words = STORY.split(" ");

		const streamInterval = setInterval(() => {
			if (currentIndex >= words.length) {
				clearInterval(streamInterval);
				setIsStreaming(false);
				return;
			}

			const nextChunk = words.slice(0, currentIndex + 3).join(" ");
			setStreamContent(nextChunk);
			currentIndex += 3;
		}, 70);

		return () => clearInterval(streamInterval);
	}, [isStreaming]);

	const handleStart = () => {
		setStreamContent("");
		setIsStreaming(true);
	};

	return (
		<div className="space-y-4 w-full h-full">
			<Button
				onClick={handleStart}
				disabled={isStreaming}
				className="w-full"
			>
				{streamContent ? "Restart Story" : "Start Story"}{" "}
				{isStreaming && "(Streaming...)"}
			</Button>
			<div className="border rounded-md h-[320px] overflow-y-auto">
				<ChatMessageArea>
					<ChatMessageAreaContent>
						<ChatMessage key={userMessage.id}>
							<ChatMessageAvatar>
								<ChatMessageAvatarImage
									src={userMessage.metadata?.member.image}
								/>
								<ChatMessageAvatarFallback>
									{userMessage.metadata?.member.name
										.charAt(0)
										.toUpperCase()}
								</ChatMessageAvatarFallback>
							</ChatMessageAvatar>

							<ChatMessageContainer>
								<ChatMessageHeader>
									<ChatMessageAuthor>
										{userMessage.metadata?.member.name}
									</ChatMessageAuthor>
									<ChatMessageTimestamp
										createdAt={new Date()}
									/>
								</ChatMessageHeader>

								<ChatMessageContent>
									{userMessage.parts
										.filter((part) => part.type === "text")
										.map((part) => (
											<ChatMessageMarkdown
												key={part.type}
												content={part.text}
											/>
										))}
								</ChatMessageContent>
							</ChatMessageContainer>
						</ChatMessage>

						{streamContent && (
							<ChatMessage key="2" id="2">
								<ChatMessageAvatar>
									<ChatMessageAvatarImage src="/avatar-2.png" />
									<ChatMessageAvatarFallback>
										A
									</ChatMessageAvatarFallback>
								</ChatMessageAvatar>

								<ChatMessageContainer>
									<ChatMessageHeader>
										<ChatMessageAuthor>
											Assistant
										</ChatMessageAuthor>
										<ChatMessageTimestamp
											createdAt={new Date()}
										/>
									</ChatMessageHeader>

									<ChatMessageContent>
										<ChatMessageMarkdown
											content={streamContent}
										/>
									</ChatMessageContent>
								</ChatMessageContainer>
							</ChatMessage>
						)}
					</ChatMessageAreaContent>
					<ChatMessageAreaScrollButton />
				</ChatMessageArea>
			</div>
		</div>
	);
}

```

# /docs/components/chat-message

---
title: Chat Message
description: A fully composable component for displaying chat messages with rich features like timestamps, actions, and threading.
featured: true
component: true
---

```tsx
"use client";

import type { UIMessage } from "@ai-sdk/react";
import { Copy, ThumbsUp } from "lucide-react";
import {
	ChatMessage,
	ChatMessageAction,
	ChatMessageActions,
	ChatMessageAuthor,
	ChatMessageAvatar,
	ChatMessageAvatarFallback,
	ChatMessageAvatarImage,
	ChatMessageContainer,
	ChatMessageContent,
	ChatMessageHeader,
	ChatMessageMarkdown,
	ChatMessageThread,
	ChatMessageThreadAction,
	ChatMessageThreadReplyCount,
	ChatMessageThreadTimestamp,
	ChatMessageTimestamp,
} from "@/components/ui/chat-message";

const messages: UIMessage<{
	member: {
		image: string;
		name: string;
	};
	threadData?: {
		messageCount: number;
		lastReply: Date;
		member: {
			image: string;
			name: string;
		};
	};
}>[] = [
	{
		id: "1",
		parts: [
			{
				type: "text",
				text: "Can you implement the new user authentication feature?",
			},
		],
		role: "user",
		metadata: {
			member: {
				image: "/avatar-1.png",
				name: "Pedro",
			},
		},
	},
	{
		id: "2",
		parts: [
			{
				type: "text",
				text: "Sure, I'll work on implementing the user authentication feature. I'll respond on the thread with updates as I progress.",
			},
		],
		role: "assistant",
		metadata: {
			member: {
				image: "/avatar-2.png",
				name: "Project Assistant",
			},
			threadData: {
				messageCount: 3,
				lastReply: new Date(),
				member: {
					image: "/avatar-1.png",
					name: "Pedro",
				},
			},
		},
	},
	{
		id: "3",
		parts: [
			{
				type: "text",
				text: "What's the progress on the other task?",
			},
		],
		role: "user",
		metadata: {
			member: {
				image: "/avatar-1.png",
				name: "Pedro",
			},
		},
	},
];

export function ChatMessageDemo() {
	return (
		<div className="w-full max-h-[400px] overflow-y-auto">
			{messages.map((message) => (
				<ChatMessage key={message.id}>
					<ChatMessageActions>
						<ChatMessageAction label="Copy">
							<Copy className="size-4" />
						</ChatMessageAction>
						<ChatMessageAction label="Like">
							<ThumbsUp className="size-4" />
						</ChatMessageAction>
					</ChatMessageActions>

					<ChatMessageAvatar>
						<ChatMessageAvatarImage
							src={message.metadata?.member.image}
						/>
						<ChatMessageAvatarFallback>
							{message.metadata?.member.name
								.charAt(0)
								.toUpperCase()}
						</ChatMessageAvatarFallback>
					</ChatMessageAvatar>

					<ChatMessageContainer>
						<ChatMessageHeader>
							<ChatMessageAuthor>
								{message.metadata?.member.name}
							</ChatMessageAuthor>
							<ChatMessageTimestamp createdAt={new Date()} />
						</ChatMessageHeader>

						<ChatMessageContent>
							{message.parts
								.filter((part) => part.type === "text")
								.map((part) => (
									<ChatMessageMarkdown
										key={part.type}
										content={part.text}
									/>
								))}
						</ChatMessageContent>

						{message.metadata?.threadData && (
							<ChatMessageThread>
								<ChatMessageAvatar>
									<ChatMessageAvatarImage
										src={
											message.metadata?.threadData.member
												.image
										}
									/>
									<ChatMessageAvatarFallback>
										{message.metadata?.threadData.member.name
											.charAt(0)
											.toUpperCase()}
									</ChatMessageAvatarFallback>
								</ChatMessageAvatar>
								<ChatMessageThreadReplyCount>
									{message.metadata.threadData.messageCount}{" "}
									replies
								</ChatMessageThreadReplyCount>
								<ChatMessageThreadTimestamp
									date={message.metadata.threadData.lastReply}
								/>
								<ChatMessageThreadAction />
							</ChatMessageThread>
						)}
					</ChatMessageContainer>
				</ChatMessage>
			))}
		</div>
	);
}

```

## Installation

<CodeTabs>

<TabsList>
  <TabsTrigger value="cli">CLI</TabsTrigger>
  <TabsTrigger value="manual">Manual</TabsTrigger>
</TabsList>
<TabsContent value="cli">

```bash
npx shadcn@latest add @simple-ai/chat-message
```

</TabsContent>

<TabsContent value="manual">

<Steps>

<Step>Copy and paste the following code into your project.</Step>

`chat-message.tsx`

<ComponentSource name="chat-message" />

<Step>Update the import paths to match your project setup.</Step>
<Step>Install the Markdown Content component for rendering markdown in messages.</Step>

<Link href="/docs/components/markdown-content">Markdown Content</Link>

</Steps>

</TabsContent>

</CodeTabs>

## Examples

### Simple Message

A minimal chat message without any actions or threading.

```tsx
"use client";

import type { UIMessage } from "@ai-sdk/react";
import {
	ChatMessage,
	ChatMessageAuthor,
	ChatMessageAvatar,
	ChatMessageAvatarAssistantIcon,
	ChatMessageAvatarUserIcon,
	ChatMessageContainer,
	ChatMessageContent,
	ChatMessageHeader,
	ChatMessageMarkdown,
	ChatMessageTimestamp,
} from "@/components/ui/chat-message";

const messages: UIMessage[] = [
	{
		id: "1",
		parts: [
			{
				type: "text",
				text: "Hey, how are you doing today?",
			},
		],
		role: "user",
	},
	{
		id: "2",
		parts: [
			{
				type: "text",
				text: "I'm doing great! Just working on some projects. How can I help you?",
			},
		],
		role: "assistant",
	},
	{
		id: "3",
		parts: [
			{
				type: "text",
				text: "I need help with my React application.",
			},
		],
		role: "user",
	},
];

export function ChatMessageDemoSimple() {
	return (
		<div className="w-full max-h-[400px] overflow-y-auto">
			{messages.map((message) => (
				<ChatMessage key={message.id}>
					<ChatMessageAvatar>
						{message.role === "user" ? (
							<ChatMessageAvatarUserIcon />
						) : (
							<ChatMessageAvatarAssistantIcon />
						)}
					</ChatMessageAvatar>

					<ChatMessageContainer>
						<ChatMessageHeader>
							<ChatMessageAuthor>
								{message.role === "user" ? "You" : "Assistant"}
							</ChatMessageAuthor>
							<ChatMessageTimestamp createdAt={new Date()} />
						</ChatMessageHeader>
						<ChatMessageContent>
							{message.parts
								.filter((part) => part.type === "text")
								.map((part) => (
									<ChatMessageMarkdown
										key={part.type}
										content={part.text}
									/>
								))}
						</ChatMessageContent>
					</ChatMessageContainer>
				</ChatMessage>
			))}
		</div>
	);
}

```

### With Avatar Images

Chat messages using image avatars instead of icons.

```tsx
"use client";

import type { UIMessage } from "@ai-sdk/react";
import {
	ChatMessage,
	ChatMessageAuthor,
	ChatMessageAvatar,
	ChatMessageAvatarImage,
	ChatMessageContainer,
	ChatMessageContent,
	ChatMessageHeader,
	ChatMessageMarkdown,
	ChatMessageTimestamp,
} from "@/components/ui/chat-message";

const messages: UIMessage<{
	avatarImage: string;
}>[] = [
	{
		id: "1",
		parts: [
			{
				type: "text",
				text: "Hi there! This is a user message with an avatar image.",
			},
		],
		role: "user",
		metadata: {
			avatarImage: "/avatar-1.png",
		},
	},
	{
		id: "2",
		parts: [
			{
				type: "text",
				text: "And this is an assistant message with its own avatar image.",
			},
		],
		role: "assistant",
		metadata: {
			avatarImage: "/avatar-2.png",
		},
	},
	{
		id: "3",
		parts: [
			{
				type: "text",
				text: "Great! Avatar images are displaying properly.",
			},
		],
		role: "user",
		metadata: {
			avatarImage: "/avatar-1.png",
		},
	},
];

export function ChatMessageDemoAvatarImage() {
	return (
		<div className="w-full max-h-[400px] overflow-y-auto">
			{messages.map((message) => (
				<ChatMessage key={message.id}>
					<ChatMessageAvatar>
						<ChatMessageAvatarImage
							src={message.metadata?.avatarImage}
							alt="Avatar"
						/>
					</ChatMessageAvatar>

					<ChatMessageContainer>
						<ChatMessageHeader>
							<ChatMessageAuthor>
								{message.role === "user" ? "You" : "Assistant"}
							</ChatMessageAuthor>
							<ChatMessageTimestamp createdAt={new Date()} />
						</ChatMessageHeader>
						<ChatMessageContent>
							{message.parts
								.filter((part) => part.type === "text")
								.map((part) => (
									<ChatMessageMarkdown
										key={part.type}
										content={part.text}
									/>
								))}
						</ChatMessageContent>
					</ChatMessageContainer>
				</ChatMessage>
			))}
		</div>
	);
}

```



# /docs/components/chat-suggestions

---
title: Chat Suggestions
description: A composable component for displaying chat prompt suggestions as clickable buttons.
component: true
---

```tsx
"use client";

import { useState } from "react";
import {
	ChatSuggestion,
	ChatSuggestions,
	ChatSuggestionsContent,
	ChatSuggestionsDescription,
	ChatSuggestionsHeader,
	ChatSuggestionsTitle,
} from "@/components/ui/chat-suggestions";

export function ChatSuggestionsDemo() {
	const [selectedSuggestion, setSelectedSuggestion] = useState<string>("");

	const suggestions = [
		"Tell me about your features",
		"How do I get started?",
		"Show me an example",
		"What are the pricing options?",
	];

	return (
		<div className="flex flex-col gap-4 p-4">
			<ChatSuggestions>
				<ChatSuggestionsHeader>
					<ChatSuggestionsTitle>
						Try these prompts:
					</ChatSuggestionsTitle>
					<ChatSuggestionsDescription>
						Click a suggestion to get started
					</ChatSuggestionsDescription>
				</ChatSuggestionsHeader>
				<ChatSuggestionsContent>
					{suggestions.map((suggestion) => (
						<ChatSuggestion
							key={suggestion}
							onClick={() => setSelectedSuggestion(suggestion)}
						>
							{suggestion}
						</ChatSuggestion>
					))}
				</ChatSuggestionsContent>
			</ChatSuggestions>

			{selectedSuggestion && (
				<div className="p-3 bg-muted rounded-md text-sm">
					<span className="font-medium">Selected:</span>{" "}
					{selectedSuggestion}
				</div>
			)}
		</div>
	);
}

```

## Installation

<CodeTabs>

<TabsList>
  <TabsTrigger value="cli">CLI</TabsTrigger>
  <TabsTrigger value="manual">Manual</TabsTrigger>
</TabsList>
<TabsContent value="cli">

```bash
npx shadcn@latest add @simple-ai/chat-suggestions
```

</TabsContent>

<TabsContent value="manual">

<Steps>

<Step>Copy and paste the following code into your project.</Step>

`chat-suggestions.tsx`

<ComponentSource name="chat-suggestions" />

<Step>Update the import paths to match your project setup.</Step>

</Steps>

</TabsContent>

</CodeTabs>

## About

The Chat Suggestions component provides a composable way to display prompt suggestions to users in your chat interface. Following the shadcn/ui design philosophy, it's built with small, reusable pieces that can be composed together to create flexible layouts.

Perfect for onboarding new users, suggesting common queries, or providing quick actions in empty chat states.

## Usage

```tsx
import {
  ChatSuggestion,
  ChatSuggestions,
  ChatSuggestionsContent,
  ChatSuggestionsDescription,
  ChatSuggestionsHeader,
  ChatSuggestionsTitle,
} from "@/components/ui/chat-suggestions"
```

### Basic Usage

```tsx
<ChatSuggestions>
  <ChatSuggestionsTitle>Try these prompts:</ChatSuggestionsTitle>
  <ChatSuggestionsContent>
    <ChatSuggestion onClick={() => console.log("What can you help me with?")}>
      What can you help me with?
    </ChatSuggestion>
    <ChatSuggestion onClick={() => console.log("Show me examples")}>
      Show me examples
    </ChatSuggestion>
  </ChatSuggestionsContent>
</ChatSuggestions>
```

### With Header and Description

```tsx
<ChatSuggestions>
  <ChatSuggestionsHeader>
    <ChatSuggestionsTitle>Quick start</ChatSuggestionsTitle>
    <ChatSuggestionsDescription>
      Click a suggestion to get started
    </ChatSuggestionsDescription>
  </ChatSuggestionsHeader>
  <ChatSuggestionsContent>
    <ChatSuggestion>Tell me about your features</ChatSuggestion>
    <ChatSuggestion>How do I get started?</ChatSuggestion>
  </ChatSuggestionsContent>
</ChatSuggestions>
```

## Examples

### Simple Suggestions

A minimal chat suggestions layout with just a title and suggestions.

```tsx
"use client";

import {
	ChatSuggestion,
	ChatSuggestions,
	ChatSuggestionsContent,
	ChatSuggestionsHeader,
	ChatSuggestionsTitle,
} from "@/components/ui/chat-suggestions";

export function ChatSuggestionsDemoSimple() {
	const suggestions = [
		"What can you help me with?",
		"Show me examples",
		"Explain the basics",
	];

	return (
		<div className="p-4">
			<ChatSuggestions>
				<ChatSuggestionsHeader>
					<ChatSuggestionsTitle>Quick start:</ChatSuggestionsTitle>
				</ChatSuggestionsHeader>
				<ChatSuggestionsContent>
					{suggestions.map((suggestion) => (
						<ChatSuggestion key={suggestion}>
							{suggestion}
						</ChatSuggestion>
					))}
				</ChatSuggestionsContent>
			</ChatSuggestions>
		</div>
	);
}

```



# /docs/components

---
title: Components
description: Here you can find all the components available in the library. We are working on adding more components.
---

<ComponentsList type="components" />


# /docs/components/jsx-renderer

---
title: JSX Renderer
description: A component that renders JSX strings with access to Tailwind CSS, shadcn components, and Lucide icons.
links:
  api: https://github.com/TroyAlford/react-jsx-parser
---

```tsx
"use client";

import { Calendar, Clock, Plane } from "lucide-react";
import { useEffect, useState } from "react";
import { toast } from "sonner";
import { Button } from "@/components/ui/button";
import { Slider } from "@/components/ui/slider";
import { highlightCode } from "@/lib/highlight-code";
import { JsxRenderer } from "@/components/ui/jsx-renderer";

const fullJsx = `<div className="max-w-xl rounded-lg border bg-card p-6">
  <div className="flex items-center gap-2 mb-6">
	<Plane className="size-5 text-primary" />
	<h2 className="text-xl font-semibold">Flight Reservation</h2>
  </div>
  <div className="space-y-4 mb-6">
	<div className="flex items-center justify-between p-3 bg-muted rounded-md">
	  <div className="flex items-center gap-2">
		<Calendar className="size-4 text-muted-foreground" />
		<span className="text-sm font-medium">Departure</span>
	  </div>
	  <span className="text-sm">June 15, 2024</span>
	</div>
	<div className="flex items-center justify-between p-3 bg-muted rounded-md">
	  <div className="flex items-center gap-2">
		<Clock className="size-4 text-muted-foreground" />
		<span className="text-sm font-medium">Time</span>
	  </div>
	  <span className="text-sm">10:30 AM</span>
	</div>
	<div className="flex items-center justify-between p-3 bg-muted rounded-md">
	  <div>
		<div className="text-sm font-medium mb-1">San Francisco (SFO)</div>
		<div className="text-xs text-muted-foreground">to New York (JFK)</div>
	  </div>
	  <div className="text-right">
		<div className="text-sm font-semibold">$349</div>
		<div className="text-xs text-muted-foreground">Economy</div>
	  </div>
	</div>
  </div>
  <div className="flex gap-2">
	<Button className="flex-1" onClick={() => onClickHandler("Select Flight")}>Select Flight</Button>
	<Button variant="outline" className="flex-1" onClick={() => onClickHandler("View Details")}>View Details</Button>
  </div>
</div>`;

export function JsxRendererDemo() {
	const [percentage, setPercentage] = useState([100]);
	const [highlightedCode, setHighlightedCode] = useState<string>("");
	const partialJsx = fullJsx.slice(
		0,
		Math.floor((fullJsx.length * percentage[0]) / 100),
	);

	useEffect(() => {
		highlightCode(partialJsx, "tsx").then(setHighlightedCode);
	}, [partialJsx]);

	const onClickHandler = (value: string) => {
		toast.success(`${value} clicked`);
	};

	return (
		<div className="max-h-[450px] space-y-4 w-full overflow-y-auto py-4">
			<Slider
				value={percentage}
				onValueChange={setPercentage}
				max={100}
				step={1}
				className="w-full"
			/>

			<div className="border rounded-lg">
				<JsxRenderer
					blacklistedAttrs={[]}
					jsx={partialJsx}
					components={{ Plane, Calendar, Clock, Button }}
					className="w-full"
					bindings={{
						onClickHandler: onClickHandler,
					}}
				/>
			</div>
			<div className="space-y-2">
				<div className="relative">
					<div
						className="bg-code text-sm rounded-lg overflow-x-auto overflow-y-auto"
						dangerouslySetInnerHTML={{ __html: highlightedCode }}
					/>
				</div>
			</div>
		</div>
	);
}

```

## Installation

<CodeTabs>

<TabsList>
  <TabsTrigger value="cli">CLI</TabsTrigger>
  <TabsTrigger value="manual">Manual</TabsTrigger>
</TabsList>
<TabsContent value="cli">

```bash
npx shadcn@latest add jsx-renderer
```

</TabsContent>

<TabsContent value="manual">

<Steps>

<Step>Install the required dependencies.</Step>

```bash
npm install react-jsx-parser react-error-boundary
```

<Step>Copy and paste the following code into your project.</Step>

`jsx-renderer.tsx`

<ComponentSource name="jsx-renderer" />

<Step>Update the import paths to match your project setup.</Step>

</Steps>

</TabsContent>

</CodeTabs>

## About

The JSX Renderer component allows you to render JSX strings dynamically with built-in error handling, state management, and component injection. It's particularly useful for AI-generated UI components or when you need to render JSX from external sources.

### Use Cases

- **AI-Generated UI**: Render user interfaces generated by AI models
- **Code Preview**: Display rendered JSX in documentation or playgrounds

### States

The JSX Renderer supports different states that affect its appearance and behavior:

- **interactive**: Default state with full interactivity
- **disabled**: Reduces opacity and disables pointer events
- **read-only**: Disables pointer events but maintains visibility
- **streaming**: Shows a pulsing animation with reduced opacity
- **error**: Adds a red border for error indication

## Usage

```tsx
import { JsxRenderer } from "@/components/ui/jsx-renderer"
import { Button } from "@/components/ui/button"
import { Card } from "@/components/ui/card"

export function MyComponent() {
  const jsxString = `
    <Card className="p-4">
      <h2 className="text-lg font-semibold">Hello World</h2>
      <Button>Click me</Button>
    </Card>
  `

  return (
    <JsxRenderer
      jsx={jsxString}
      components={{
        Button,
        Card,
      }}
    />
  )
}
```

### Component Requirements

When using the JSX Renderer, you need to provide all components that are referenced in your JSX string. For example:

```tsx
const components = {
  Button,
  Card,
  Input,
  // ... any other components you want to use
}

const jsxString = `
  <Card className="p-4">
    <h2 className="text-lg font-semibold">Hello World</h2>
    <Button>Click me</Button>
  </Card>
`

<JsxRenderer jsx={jsxString} components={components} />
```

## Examples

### Streaming State

```tsx
"use client";

import { Cloud, CloudRain, Sun, Sunset } from "lucide-react";
import { useEffect, useState } from "react";
import { JsxRenderer } from "@/components/ui/jsx-renderer";

const fullJsx = `<div className="max-w-xl rounded-xl bg-gradient-to-br from-blue-400 to-blue-500 p-6 text-white shadow-lg">
  <div className="flex justify-between items-center mb-4">
	<h2 className="text-2xl font-medium">Mexico City</h2>
	<Sun className="text-yellow-300 text-4xl" />
  </div>
  <div className="flex justify-between items-end mb-4">
	<div className="text-6xl font-light">
	  <span>47°</span>
	</div>
	<div className="text-right">
	  <p className="text-sm">Winter storm warning</p>
	</div>
  </div>
  <div className="grid grid-cols-6 gap-8">
	<div className="text-center flex flex-col items-center">
	  <p className="text-sm mb-1">4PM</p>
	  <Sun className="text-yellow-300 mb-1" />
	  <p className="text-sm font-semibold">46°</p>
	</div>
	<div className="text-center flex flex-col items-center">
	  <p className="text-sm mb-1">5PM</p>
	  <Sun className="text-yellow-300 mb-1" />
	  <p className="text-sm font-semibold">44°</p>
	</div>
	<div className="text-center flex flex-col items-center">
	  <p className="text-sm mb-1">6PM</p>
	  <CloudRain className="text-yellow-300 mb-1" />
	  <p className="text-sm font-semibold">41°</p>
	</div>
	<div className="text-center flex flex-col items-center">
	  <p className="text-sm mb-1">7PM</p>
	  <Sunset className="text-yellow-500 mb-1" />
	  <p className="text-sm font-semibold">41°</p>
	</div>
	<div className="text-center flex flex-col items-center">
	  <p className="text-sm mb-1">8PM</p>
	  <Cloud className="text-gray-300 mb-1" />
	  <p className="text-sm font-semibold">37°</p>
	</div>
	<div className="text-center flex flex-col items-center">
	  <p className="text-sm mb-1">9PM</p>
	  <Cloud className="text-gray-300 mb-1" />
	  <p className="text-sm font-semibold">35°</p>
	</div>
  </div>
</div>`;

const jsxLines = fullJsx.split("\n");

export function JsxRendererStreamingDemo() {
	const [currentLines, setCurrentLines] = useState(jsxLines.slice(0, 1)); // Start with header and current temp

	useEffect(() => {
		const interval = setInterval(() => {
			if (currentLines.length >= jsxLines.length) {
				setCurrentLines(jsxLines.slice(0, 1));
				return;
			}
			setCurrentLines((prev) => [...prev, jsxLines[prev.length]]);
		}, 100);

		return () => clearInterval(interval);
	}, [currentLines.length]);

	const currentJsx = currentLines.join("\n");

	return (
		<div className="w-full">
			<JsxRenderer
				jsx={currentJsx}
				components={{ Cloud, Sun, CloudRain, Sunset }}
				state="streaming"
				className="w-full h-[260px]"
			/>
		</div>
	);
}

```

### Disabled State

```tsx
"use client";

import { Calendar, ChefHat, Clock } from "lucide-react";
import { JsxRenderer } from "@/components/ui/jsx-renderer";

export function JsxRendererDisabledDemo() {
	const jsx = `<div className="max-w-xl rounded-lg border bg-card p-6">
  <div className="flex items-center gap-2 mb-6">
    <ChefHat className="size-5 text-primary" />
    <h2 className="text-xl font-semibold">Restaurant Reservation</h2>
  </div>
  <div className="space-y-4 mb-6">
    <div className="flex items-center justify-between p-3 bg-muted rounded-md">
      <div className="flex items-center gap-2">
        <Calendar className="size-4 text-muted-foreground" />
        <span className="text-sm font-medium">Date</span>
      </div>
      <span className="text-sm">October 20, 2024</span>
    </div>
    <div className="flex items-center justify-between p-3 bg-muted rounded-md">
      <div className="flex items-center gap-2">
        <Clock className="size-4 text-muted-foreground" />
        <span className="text-sm font-medium">Time</span>
      </div>
      <span className="text-sm">7:00 PM</span>
    </div>
    <div className="flex items-center justify-between p-3 bg-muted rounded-md">
      <div>
        <div className="text-sm font-medium mb-1">The Golden Spoon</div>
        <div className="text-xs text-muted-foreground">Italian Cuisine • Downtown</div>
      </div>
      <div className="text-right">
        <div className="text-sm font-semibold">4 Guests</div>
        <div className="text-xs text-muted-foreground">Window Seat</div>
      </div>
    </div>
  </div>
  <div className="flex gap-2">
    <button className="flex-1 bg-primary text-primary-foreground hover:bg-primary/90 px-4 py-2 rounded-md font-medium">
      Confirm Reservation
    </button>
    <button className="flex-1 border border-input bg-background hover:bg-accent hover:text-accent-foreground px-4 py-2 rounded-md font-medium">
      Modify Details
    </button>
  </div>
</div>`;

	return (
		<div className="w-full">
			<JsxRenderer
				jsx={jsx}
				components={{ Calendar, Clock, ChefHat }}
				state="disabled"
				className="w-full"
			/>
		</div>
	);
}

```

### Error Handling

The component includes an error boundary to catch rendering errors:

```tsx
<JsxRenderer
  jsx={jsxString}
  onError={(error, errorInfo) => console.error(error)}
  fallback={CustomFallback}
/>
```

## API Reference

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `jsx` | `string` | - | The JSX string to render |
| `fixIncompleteJsx` | `boolean` | `true` | Whether to automatically complete unclosed JSX tags |
| `components` | `Record<string, React.ComponentType>` | - | Map of component names to their React components |
| `state` | `"disabled" \| "read-only" \| "interactive" \| "streaming" \| "error"` | `"interactive"` | The state of the renderer affecting styling and behavior |
| `fallback` | `React.ComponentType` | `ErrorFallback` | Custom fallback component for error boundary |
| `onError` | `(error: Error, errorInfo: React.ErrorInfo) => void` | - | Callback for error handling |




# /docs/components/markdown-content

---
title: Markdown Content
description: A component that renders markdown content.
component: true
---

```tsx
"use client";

import { useState } from "react";
import { MarkdownContent } from "@/components/ui/markdown-content";

export function MarkdownContentDemo() {
	const [content] = useState(
		`# Markdown Example

This is a sample paragraph that demonstrates how markdown content can be rendered in your application. It shows various markdown elements working together.

## Features List

- Clean and simple syntax
- Support for headings and paragraphs
- Bullet point lists
- Easy to read and write
- Customizable styling

## Text Formatting

You can make text **bold**, *italic*, or ***both***. You can also use ~~strikethrough~~ text.

### Links and Images

Here's a [link to GitHub](https://github.com) and below is a sample image:

![Markdown Logo](https://markdown-here.com/img/icon256.png)

### Code Examples

Inline code: \`const greeting = "Hello World!"\`

#### Code Block	

\`\`\`javascript
// Code block
function sayHello() {
  console.log("Hello!");
}
\`\`\`

### Blockquotes

> This is a blockquote
> It can span multiple lines
>> And can be nested

### Tables

| Header 1 | Header 2 | Header 3 |
|----------|----------|----------|
| Row 1    | Data     | Data     |
| Row 2    | Data     | Data     |

### Task Lists

- [x] Completed task
- [ ] Pending task
- [ ] Another task

### Horizontal Rule

---

### Ordered List

1. First item
2. Second item
3. Third item
   1. Sub-item 1
   2. Sub-item 2
`,
	);
	return (
		<div className="w-full max-h-[400px] overflow-y-auto">
			<MarkdownContent content={content} />
		</div>
	);
}

```

This component provides a simple way to render Markdown content with optimized performance through memoization. The implementation uses memoized blocks which prevents unnecessary re-renders and improves rendering efficiency, especially when dealing with streaming content.

The memoization approach is inspired by the [Vercel AI SDK's markdown chatbot example](https://sdk.vercel.ai/cookbook/next/markdown-chatbot-with-memoization). As they explain, memoization caches parsed Markdown blocks and reuses them to avoid redundant parsing and rendering operations. This is particularly beneficial when content is being streamed or updated frequently, as it ensures that only changed blocks are re-rendered rather than the entire content.

## Installation

<CodeTabs>

<TabsList>
  <TabsTrigger value="cli">CLI</TabsTrigger>
  <TabsTrigger value="manual">Manual</TabsTrigger>
</TabsList>
<TabsContent value="cli">

```bash
npx shadcn@latest add @simple-ai/markdown-content
```

</TabsContent>

<TabsContent value="manual">

<Steps>

<Step>Install the following dependencies:</Step>

```bash
npm install react-markdown marked
```

<Step>Copy and paste the following code into your project.</Step>

`markdown-content.tsx`

<ComponentSource name="markdown-content" />

<Step>Update the import paths to match your project setup.</Step>

</Steps>

</TabsContent>

</CodeTabs>

## Usage

```tsx
import { MarkdownContent } from "@/components/ui/markdown-content"
```

```tsx
<MarkdownContent id="my-id" content={content} />
```

## Examples

### Streaming

This component supports streaming content with automatic batching

```tsx
"use client";

import { useEffect, useState } from "react";
import { Button } from "@/components/ui/button";
import { MarkdownContent } from "../ui/markdown-content";

export function MarkdownStreamingDemo() {
	const [content, setContent] = useState("");
	const [isStreaming, setIsStreaming] = useState(false);
	const fullContent = `# The Lost Key
## A Tale of Mystery

* Sarah woke up to find her favorite golden key missing from its usual spot
* She remembered using it last night to lock her diary
* The search began:
  * Under the bed - nothing but dust
  * In her coat pockets - empty
  * On her desk - just scattered papers

> "Sometimes what we're looking for is right where we least expect it"

* As she made her bed, something shiny caught her eye
* The key had slipped between the pages of her book
* With a smile, she realized she'd been using it as a bookmark

*The End*`;

	useEffect(() => {
		// Typically you'd use a streaming API to get the content, this is just a demo
		if (!isStreaming) {
			return;
		}

		let currentIndex = 0;
		const words = fullContent.split(" ");

		const streamInterval = setInterval(() => {
			if (currentIndex >= words.length) {
				clearInterval(streamInterval);
				setIsStreaming(false);
				return;
			}

			const nextChunk = words.slice(0, currentIndex + 3).join(" ");
			setContent(nextChunk);
			currentIndex += 3;
		}, 70);

		return () => clearInterval(streamInterval);
	}, [isStreaming]);

	const handleStart = () => {
		setContent("");
		setIsStreaming(true);
	};

	return (
		<div className="space-y-4 w-full max-h-[400px] overflow-y-auto">
			<div className="flex gap-2">
				<Button onClick={handleStart} disabled={isStreaming}>
					{content ? "Restart" : "Start"} Streaming
				</Button>
			</div>
			<div className="p-4 w-full min-h-[200px] border rounded-md overflow-y-auto">
				<MarkdownContent content={content} />
			</div>
		</div>
	);
}

```

# /docs/components/model-selector

---
title: Model Selector
description: A dropdown component for selecting AI models with provider icons.
---

```tsx
"use client";

import { useState } from "react";
import { type Model, ModelSelector } from "@/components/ui/model-selector";

export function ModelSelectorDemo() {
	const [model, setModel] = useState<Model>("deepseek-chat");

	return (
		<div className="w-full max-w-sm">
			<ModelSelector value={model} onChange={setModel} />
		</div>
	);
}

```

## Installation

```bash
npx shadcn@latest add model-selector
```

## Usage

The Model Selector provides a dropdown interface for choosing between different AI models from various providers like OpenAI, Groq, and DeepSeek.

```tsx
import { ModelSelector, type Model } from "@/components/ui/model-selector"
import { useState } from "react"

export function MyComponent() {
  const [selectedModel, setSelectedModel] = useState<Model>("gpt-4o")

  return (
    <ModelSelector
      value={selectedModel}
      onChange={setSelectedModel}
    />
  )
}
```

## API Reference

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `value` | `Model` | - | The currently selected model |
| `onChange` | `(value: Model) => void` | - | Callback function called when model changes |
| `disabledModels` | `Model[]` | - | Array of models to disable in the selector |

## Available Models

The component includes support for the following AI models:

- **OpenAI**: `gpt-4o`, `gpt-4o-mini`
- **Groq**: `llama-3.3-70b-versatile`, `llama-3.1-8b-instant`, `deepseek-r1-distill-llama-70b`
- **DeepSeek**: `deepseek-chat`

## Disabling Models

You can disable specific models by passing the `disabledModels` prop:

```tsx
<ModelSelector
  value={selectedModel}
  onChange={setSelectedModel}
  disabledModels={["gpt-4o", "llama-3.3-70b-versatile"]}
/>
```

## Integration with AI SDK

The Model Selector is designed to work seamlessly with Vercel AI SDK:

```tsx
import { generateText } from "ai"
import { openai } from "@ai-sdk/openai"
import { groq } from "@ai-sdk/groq"
import { deepseek } from "@ai-sdk/deepseek"

const modelMap = {
  "gpt-4o": openai("gpt-4o"),
  "gpt-4o-mini": openai("gpt-4o-mini"),
  "llama-3.3-70b-versatile": groq("llama-3.3-70b-versatile"),
  "llama-3.1-8b-instant": groq("llama-3.1-8b-instant"),
  "deepseek-chat": deepseek("deepseek-chat"),
  "deepseek-r1-distill-llama-70b": groq("deepseek-r1-distill-llama-70b"),
}

export function ChatComponent() {
  const [selectedModel, setSelectedModel] = useState<Model>("gpt-4o")

  const handleSendMessage = async (message: string) => {
    const model = modelMap[selectedModel]
    const result = await generateText({
      model,
      prompt: message,
    })
    // Handle response...
  }

  return (
    <div>
      <ModelSelector
        value={selectedModel}
        onChange={setSelectedModel}
      />
      {/* Chat interface */}
    </div>
  )
}
```

## Customization

The component uses shadcn/ui's Select component internally, so you can pass any additional props that the Select component accepts:

```tsx
<ModelSelector
  value={selectedModel}
  onChange={setSelectedModel}
  className="w-64"
  placeholder="Choose a model"
/>
```

# /docs/components/tool-invocation

---
title: Tool Invocation
description: A component for displaying tool invocations with different states and collapsible content.
component: true
---

```tsx
"use client";

import type { UIMessage } from "@ai-sdk/react";
import type { InferUITools } from "ai";
import { tool } from "ai";
import z from "zod";
import {
	ChatMessage,
	ChatMessageAuthor,
	ChatMessageAvatar,
	ChatMessageAvatarFallback,
	ChatMessageAvatarImage,
	ChatMessageContainer,
	ChatMessageContent,
	ChatMessageHeader,
	ChatMessageMarkdown,
	ChatMessageTimestamp,
} from "@/components/ui/chat-message";
import {
	ChatMessageArea,
	ChatMessageAreaContent,
	ChatMessageAreaScrollButton,
} from "@/components/ui/chat-message-area";
import {
	ToolInvocation,
	ToolInvocationContentCollapsible,
	ToolInvocationHeader,
	ToolInvocationName,
	ToolInvocationRawData,
} from "@/components/ui/tool-invocation";

const searchDatabaseTool = tool({
	name: "search-database",
	description: "Search the database for information",
	inputSchema: z.object({
		query: z.string(),
	}),
	outputSchema: z.string(),
	execute: () => {
		return "Result of searching the database";
	},
});

const toolSet = {
	"search-database": searchDatabaseTool,
};

const messages: Array<
	UIMessage<
		{
			member: {
				image: string;
				name: string;
			};
		},
		never,
		InferUITools<typeof toolSet>
	>
> = [
	{
		id: "1",
		parts: [
			{
				type: "text",
				text: "Can you search for information about magical forests?",
			},
		],
		role: "user",
		metadata: {
			member: {
				image: "/avatar-1.png",
				name: "You",
			},
		},
	},
	{
		id: "2",
		parts: [
			{
				type: "tool-search-database",
				toolCallId: "search-tool-1",
				state: "output-available",
				input: {
					query: "magical forest stories",
				},
				output: "Found several stories about magical forests, including 'The Whispering Woods' - a tale about a magical forest where trees can talk and animals sing beautiful songs. The story follows Luna, a young fox with silver fur who can speak with ancient trees.",
			},
			{
				type: "text",
				text: "I found some great information! There's a wonderful story called 'The Whispering Woods' about a magical forest where trees can talk and animals sing. The main character is Luna, a young fox with silver fur who has the special ability to communicate with ancient trees.",
			},
		],
		role: "assistant",
		metadata: {
			member: {
				image: "/avatar-2.png",
				name: "Assistant",
			},
		},
	},
];

export function ChatMessageAreaDemo() {
	return (
		<ChatMessageArea>
			<ChatMessageAreaContent>
				{messages.map((message) => (
					<ChatMessage key={message.id}>
						<ChatMessageAvatar>
							<ChatMessageAvatarImage
								src={message.metadata?.member.image}
							/>
							<ChatMessageAvatarFallback>
								{message.metadata?.member.name
									.charAt(0)
									.toUpperCase()}
							</ChatMessageAvatarFallback>
						</ChatMessageAvatar>

						<ChatMessageContainer>
							<ChatMessageHeader>
								<ChatMessageAuthor>
									{message.metadata?.member.name}
								</ChatMessageAuthor>
								<ChatMessageTimestamp createdAt={new Date()} />
							</ChatMessageHeader>

							<ChatMessageContent>
								{message.parts.map((part) => {
									if (part.type === "text") {
										return (
											<ChatMessageMarkdown
												key={`${message.id}-text-${part.text.slice(0, 20)}`}
												content={part.text}
											/>
										);
									}
									if (part.type === "tool-search-database") {
										const hasInput =
											part.input != null &&
											part.input !== undefined;
										const hasOutput =
											part.output != null &&
											part.output !== undefined;

										const toolName = part.type.slice(5);
										return (
											<ToolInvocation
												key={part.toolCallId}
												className="w-full"
											>
												<ToolInvocationHeader>
													<ToolInvocationName
														name={toolName}
														type={part.state}
														isError={
															part.state ===
															"output-error"
														}
													/>
												</ToolInvocationHeader>
												{(hasInput ||
													hasOutput ||
													part.errorText) && (
													<ToolInvocationContentCollapsible>
														{hasInput && (
															<ToolInvocationRawData
																data={
																	part.input
																}
																title="Arguments"
															/>
														)}
														{part.errorText && (
															<ToolInvocationRawData
																data={{
																	error: part.errorText,
																}}
																title="Error"
															/>
														)}
														{hasOutput && (
															<ToolInvocationRawData
																data={
																	part.output
																}
																title="Result"
															/>
														)}
													</ToolInvocationContentCollapsible>
												)}
											</ToolInvocation>
										);
									}

									return null;
								})}
							</ChatMessageContent>
						</ChatMessageContainer>
					</ChatMessage>
				))}
			</ChatMessageAreaContent>
			<ChatMessageAreaScrollButton />
		</ChatMessageArea>
	);
}

```

## Installation

<CodeTabs>

<TabsList>
  <TabsTrigger value="cli">CLI</TabsTrigger>
  <TabsTrigger value="manual">Manual</TabsTrigger>
</TabsList>
<TabsContent value="cli">

```bash
npx shadcn@latest add @simple-ai/tool-invocation
```

</TabsContent>

<TabsContent value="manual">

<Steps>

<Step>Install required dependencies</Step>

```bash
npm install lucide-react
```

<Step>Copy and paste the following code into your project.</Step>

`tool-invocation.tsx`

<ComponentSource name="tool-invocation" />

`id-to-readable-text.ts`

<ComponentSource name="id-to-readable-text" />

<Step>Update the import paths to match your project setup.</Step>

</Steps>

</TabsContent>

</CodeTabs>

## About

The Tool Invocation component displays tool calls with visual indicators for different states (streaming, available, error). It includes collapsible sections for viewing input arguments and output results.

## Examples

### Complete Tool Invocation

A tool invocation with completed results showing both input arguments and output.

```tsx
"use client";

import type { UIMessage } from "@ai-sdk/react";
import type { InferUITools } from "ai";
import { tool } from "ai";
import z from "zod";
import {
	ChatMessage,
	ChatMessageAuthor,
	ChatMessageAvatar,
	ChatMessageAvatarFallback,
	ChatMessageAvatarImage,
	ChatMessageContainer,
	ChatMessageContent,
	ChatMessageHeader,
	ChatMessageMarkdown,
	ChatMessageTimestamp,
} from "@/components/ui/chat-message";
import {
	ChatMessageArea,
	ChatMessageAreaContent,
	ChatMessageAreaScrollButton,
} from "@/components/ui/chat-message-area";
import {
	ToolInvocation,
	ToolInvocationContentCollapsible,
	ToolInvocationHeader,
	ToolInvocationName,
	ToolInvocationRawData,
} from "@/components/ui/tool-invocation";

const searchDatabaseTool = tool({
	name: "search-database",
	description: "Search the database for information",
	inputSchema: z.object({
		query: z.string(),
	}),
	outputSchema: z.string(),
	execute: () => {
		return "Result of searching the database";
	},
});

const toolSet = {
	"search-database": searchDatabaseTool,
};

const messages: Array<
	UIMessage<
		{
			member: {
				image: string;
				name: string;
			};
		},
		never,
		InferUITools<typeof toolSet>
	>
> = [
	{
		id: "1",
		parts: [
			{
				type: "text",
				text: "Can you search for information about magical forests?",
			},
		],
		role: "user",
		metadata: {
			member: {
				image: "/avatar-1.png",
				name: "You",
			},
		},
	},
	{
		id: "2",
		parts: [
			{
				type: "tool-search-database",
				toolCallId: "search-tool-1",
				state: "output-available",
				input: {
					query: "magical forest stories",
				},
				output: "Found several stories about magical forests, including 'The Whispering Woods' - a tale about a magical forest where trees can talk and animals sing beautiful songs. The story follows Luna, a young fox with silver fur who can speak with ancient trees.",
			},
			{
				type: "text",
				text: "I found some great information! There's a wonderful story called 'The Whispering Woods' about a magical forest where trees can talk and animals sing. The main character is Luna, a young fox with silver fur who has the special ability to communicate with ancient trees.",
			},
		],
		role: "assistant",
		metadata: {
			member: {
				image: "/avatar-2.png",
				name: "Assistant",
			},
		},
	},
];

export function ChatMessageAreaDemo() {
	return (
		<ChatMessageArea>
			<ChatMessageAreaContent>
				{messages.map((message) => (
					<ChatMessage key={message.id}>
						<ChatMessageAvatar>
							<ChatMessageAvatarImage
								src={message.metadata?.member.image}
							/>
							<ChatMessageAvatarFallback>
								{message.metadata?.member.name
									.charAt(0)
									.toUpperCase()}
							</ChatMessageAvatarFallback>
						</ChatMessageAvatar>

						<ChatMessageContainer>
							<ChatMessageHeader>
								<ChatMessageAuthor>
									{message.metadata?.member.name}
								</ChatMessageAuthor>
								<ChatMessageTimestamp createdAt={new Date()} />
							</ChatMessageHeader>

							<ChatMessageContent>
								{message.parts.map((part) => {
									if (part.type === "text") {
										return (
											<ChatMessageMarkdown
												key={`${message.id}-text-${part.text.slice(0, 20)}`}
												content={part.text}
											/>
										);
									}
									if (part.type === "tool-search-database") {
										const hasInput =
											part.input != null &&
											part.input !== undefined;
										const hasOutput =
											part.output != null &&
											part.output !== undefined;

										const toolName = part.type.slice(5);
										return (
											<ToolInvocation
												key={part.toolCallId}
												className="w-full"
											>
												<ToolInvocationHeader>
													<ToolInvocationName
														name={toolName}
														type={part.state}
														isError={
															part.state ===
															"output-error"
														}
													/>
												</ToolInvocationHeader>
												{(hasInput ||
													hasOutput ||
													part.errorText) && (
													<ToolInvocationContentCollapsible>
														{hasInput && (
															<ToolInvocationRawData
																data={
																	part.input
																}
																title="Arguments"
															/>
														)}
														{part.errorText && (
															<ToolInvocationRawData
																data={{
																	error: part.errorText,
																}}
																title="Error"
															/>
														)}
														{hasOutput && (
															<ToolInvocationRawData
																data={
																	part.output
																}
																title="Result"
															/>
														)}
													</ToolInvocationContentCollapsible>
												)}
											</ToolInvocation>
										);
									}

									return null;
								})}
							</ChatMessageContent>
						</ChatMessageContainer>
					</ChatMessage>
				))}
			</ChatMessageAreaContent>
			<ChatMessageAreaScrollButton />
		</ChatMessageArea>
	);
}

```

### Loading State

A tool invocation showing the loading state while the tool is being executed.

```tsx
"use client";

import type { UIMessage } from "@ai-sdk/react";
import type { InferUITools } from "ai";
import { tool } from "ai";
import z from "zod";
import {
	ChatMessage,
	ChatMessageAuthor,
	ChatMessageAvatar,
	ChatMessageAvatarFallback,
	ChatMessageAvatarImage,
	ChatMessageContainer,
	ChatMessageContent,
	ChatMessageHeader,
	ChatMessageMarkdown,
	ChatMessageTimestamp,
} from "@/components/ui/chat-message";
import {
	ChatMessageArea,
	ChatMessageAreaContent,
	ChatMessageAreaScrollButton,
} from "@/components/ui/chat-message-area";
import {
	ToolInvocation,
	ToolInvocationContentCollapsible,
	ToolInvocationHeader,
	ToolInvocationName,
	ToolInvocationRawData,
} from "@/components/ui/tool-invocation";

const searchDatabaseTool = tool({
	name: "search-database",
	description: "Search the database for information",
	inputSchema: z.object({
		query: z.string(),
	}),
	outputSchema: z.string(),
	execute: () => {
		return "Result of searching the database";
	},
});

const toolSet = {
	"search-database": searchDatabaseTool,
};

const messages: Array<
	UIMessage<
		{
			member: {
				image: string;
				name: string;
			};
		},
		never,
		InferUITools<typeof toolSet>
	>
> = [
	{
		id: "1",
		parts: [
			{
				type: "text",
				text: "Can you search for information about magical forests?",
			},
		],
		role: "user",
		metadata: {
			member: {
				image: "/avatar-1.png",
				name: "You",
			},
		},
	},
	{
		id: "2",
		parts: [
			{
				type: "tool-search-database",
				toolCallId: "search-tool-1",
				state: "input-available",
				input: {
					query: "magical forest stories",
				},
			},
		],
		role: "assistant",
		metadata: {
			member: {
				image: "/avatar-2.png",
				name: "Assistant",
			},
		},
	},
];

export function ToolInvocationDemoLoading() {
	return (
		<ChatMessageArea>
			<ChatMessageAreaContent>
				{messages.map((message) => (
					<ChatMessage key={message.id}>
						<ChatMessageAvatar>
							<ChatMessageAvatarImage
								src={message.metadata?.member.image}
							/>
							<ChatMessageAvatarFallback>
								{message.metadata?.member.name
									.charAt(0)
									.toUpperCase()}
							</ChatMessageAvatarFallback>
						</ChatMessageAvatar>

						<ChatMessageContainer>
							<ChatMessageHeader>
								<ChatMessageAuthor>
									{message.metadata?.member.name}
								</ChatMessageAuthor>
								<ChatMessageTimestamp createdAt={new Date()} />
							</ChatMessageHeader>

							<ChatMessageContent>
								{message.parts.map((part) => {
									if (part.type === "text") {
										return (
											<ChatMessageMarkdown
												key={`${message.id}-text-${part.text.slice(0, 20)}`}
												content={part.text}
											/>
										);
									}
									if (part.type === "tool-search-database") {
										const hasInput =
											part.input != null &&
											part.input !== undefined;
										const hasOutput =
											part.output != null &&
											part.output !== undefined;

										const toolName = part.type.slice(5);
										return (
											<ToolInvocation
												key={part.toolCallId}
												className="w-full"
											>
												<ToolInvocationHeader>
													<ToolInvocationName
														name={toolName}
														type={part.state}
														isError={
															part.state ===
															"output-error"
														}
													/>
												</ToolInvocationHeader>
												{(hasInput ||
													hasOutput ||
													part.errorText) && (
													<ToolInvocationContentCollapsible>
														{hasInput && (
															<ToolInvocationRawData
																data={
																	part.input
																}
																title="Arguments"
															/>
														)}
														{part.errorText && (
															<ToolInvocationRawData
																data={{
																	error: part.errorText,
																}}
																title="Error"
															/>
														)}
														{hasOutput && (
															<ToolInvocationRawData
																data={
																	part.output
																}
																title="Result"
															/>
														)}
													</ToolInvocationContentCollapsible>
												)}
											</ToolInvocation>
										);
									}

									return null;
								})}
							</ChatMessageContent>
						</ChatMessageContainer>
					</ChatMessage>
				))}
			</ChatMessageAreaContent>
			<ChatMessageAreaScrollButton />
		</ChatMessageArea>
	);
}

```

### Error State

A tool invocation showing an error state when the tool execution fails.

```tsx
"use client";

import type { UIMessage } from "@ai-sdk/react";
import type { InferUITools } from "ai";
import { tool } from "ai";
import z from "zod";
import {
	ChatMessage,
	ChatMessageAuthor,
	ChatMessageAvatar,
	ChatMessageAvatarFallback,
	ChatMessageAvatarImage,
	ChatMessageContainer,
	ChatMessageContent,
	ChatMessageHeader,
	ChatMessageMarkdown,
	ChatMessageTimestamp,
} from "@/components/ui/chat-message";
import {
	ChatMessageArea,
	ChatMessageAreaContent,
	ChatMessageAreaScrollButton,
} from "@/components/ui/chat-message-area";
import {
	ToolInvocation,
	ToolInvocationContentCollapsible,
	ToolInvocationHeader,
	ToolInvocationName,
	ToolInvocationRawData,
} from "@/components/ui/tool-invocation";

const searchDatabaseTool = tool({
	name: "search-database",
	description: "Search the database for information",
	inputSchema: z.object({
		query: z.string(),
	}),
	outputSchema: z.string(),
	execute: () => {
		return "Result of searching the database";
	},
});

const toolSet = {
	"search-database": searchDatabaseTool,
};

const messages: Array<
	UIMessage<
		{
			member: {
				image: string;
				name: string;
			};
		},
		never,
		InferUITools<typeof toolSet>
	>
> = [
	{
		id: "1",
		parts: [
			{
				type: "text",
				text: "Can you search for information about magical forests?",
			},
		],
		role: "user",
		metadata: {
			member: {
				image: "/avatar-1.png",
				name: "You",
			},
		},
	},
	{
		id: "2",
		parts: [
			{
				type: "tool-search-database",
				toolCallId: "search-tool-1",
				state: "output-error",
				input: {
					query: "magical forest stories",
				},
				output: undefined,
				errorText:
					"Database connection timeout. Please try again later.",
			},
			{
				type: "text",
				text: "I encountered an error while searching. The database connection timed out.",
			},
		],
		role: "assistant",
		metadata: {
			member: {
				image: "/avatar-2.png",
				name: "Assistant",
			},
		},
	},
];

export function ToolInvocationDemoError() {
	return (
		<ChatMessageArea>
			<ChatMessageAreaContent>
				{messages.map((message) => (
					<ChatMessage key={message.id}>
						<ChatMessageAvatar>
							<ChatMessageAvatarImage
								src={message.metadata?.member.image}
							/>
							<ChatMessageAvatarFallback>
								{message.metadata?.member.name
									.charAt(0)
									.toUpperCase()}
							</ChatMessageAvatarFallback>
						</ChatMessageAvatar>

						<ChatMessageContainer>
							<ChatMessageHeader>
								<ChatMessageAuthor>
									{message.metadata?.member.name}
								</ChatMessageAuthor>
								<ChatMessageTimestamp createdAt={new Date()} />
							</ChatMessageHeader>

							<ChatMessageContent>
								{message.parts.map((part) => {
									if (part.type === "text") {
										return (
											<ChatMessageMarkdown
												key={`${message.id}-text-${part.text.slice(0, 20)}`}
												content={part.text}
											/>
										);
									}
									if (part.type === "tool-search-database") {
										const hasInput =
											part.input != null &&
											part.input !== undefined;
										const hasOutput =
											part.output != null &&
											part.output !== undefined;

										const toolName = part.type.slice(5);
										return (
											<ToolInvocation
												key={part.toolCallId}
												className="w-full"
											>
												<ToolInvocationHeader>
													<ToolInvocationName
														name={toolName}
														type={part.state}
														isError={
															part.state ===
															"output-error"
														}
													/>
												</ToolInvocationHeader>
												{(hasInput ||
													hasOutput ||
													part.errorText) && (
													<ToolInvocationContentCollapsible>
														{hasInput && (
															<ToolInvocationRawData
																data={
																	part.input
																}
																title="Arguments"
															/>
														)}
														{part.errorText && (
															<ToolInvocationRawData
																data={{
																	error: part.errorText,
																}}
																title="Error"
															/>
														)}
														{hasOutput && (
															<ToolInvocationRawData
																data={
																	part.output
																}
																title="Result"
															/>
														)}
													</ToolInvocationContentCollapsible>
												)}
											</ToolInvocation>
										);
									}

									return null;
								})}
							</ChatMessageContent>
						</ChatMessageContainer>
					</ChatMessage>
				))}
			</ChatMessageAreaContent>
			<ChatMessageAreaScrollButton />
		</ChatMessageArea>
	);
}

```


# /docs/workflows/editable-handle

---
title: Editable Handle
description: A React Flow component that allows you to dynamically add, edit, or remove input/output handles on your nodes - perfect for building nodes with flexible connection points.
featured: true
component: true
---

```tsx
"use client";

import "@xyflow/react/dist/style.css";

import {
	addEdge,
	applyEdgeChanges,
	applyNodeChanges,
	Background,
	type Connection,
	type EdgeChange,
	type Node,
	type NodeChange,
	type NodeProps,
	type NodeTypes,
	Position,
	ReactFlow,
	ReactFlowProvider,
	useUpdateNodeInternals,
} from "@xyflow/react";
import { nanoid } from "nanoid";
import { useCallback, useState } from "react";
import { Button } from "@/components/ui/button";
import {
	EditableHandle,
	EditableHandleDialog,
} from "@/components/ui/flow/editable-handle";

const DynamicHandlesNode = ({ id }: NodeProps<Node>) => {
	const updateNodeInternals = useUpdateNodeInternals();
	const [handles, setHandles] = useState<
		{ id: string; name: string; description?: string }[]
	>([
		{
			id: "1",
			name: "input1",
			description: "Input 1 description",
		},
	]);

	const handleCreate = useCallback(
		(name: string, description?: string) => {
			console.log("New handle", name, description);
			const newHandle = {
				id: `handle-${nanoid()}`,
				name,
				description,
			};
			setHandles((prev) => [...prev, newHandle]);
			updateNodeInternals(id);
			return true;
		},
		[id, updateNodeInternals],
	);

	const handleUpdate = useCallback(
		(handleId: string, newName: string, newDescription?: string) => {
			setHandles((prev) =>
				prev.map((handle) =>
					handle.id === handleId
						? {
								...handle,
								name: newName,
								description: newDescription,
							}
						: handle,
				),
			);
			return true;
		},
		[],
	);

	const handleDelete = useCallback(
		(handleId: string) => {
			setHandles((prev) =>
				prev.filter((handle) => handle.id !== handleId),
			);
			updateNodeInternals(id);
		},
		[id, updateNodeInternals],
	);

	return (
		<div className="py-4 border rounded-lg bg-background w-[300px]">
			<h2 className="text-lg font-semibold mb-4 px-4">
				Node with dynamic Handles
			</h2>

			<EditableHandleDialog
				variant="create"
				label=""
				onSave={handleCreate}
				onCancel={() => {}}
				align="start"
			>
				<Button variant="outline" size="sm" className="h-8 ml-4">
					Create New Handle
				</Button>
			</EditableHandleDialog>

			<div className="mt-4 space-y-2">
				{handles.map((handle) => (
					<div key={handle.id} className="flex items-center gap-2">
						<EditableHandle
							nodeId={id}
							handleId={handle.id}
							name={handle.name}
							description={handle.description}
							type="target"
							position={Position.Left}
							wrapperClassName="w-full"
							onUpdateTool={handleUpdate}
							onDelete={handleDelete}
						/>
					</div>
				))}
			</div>
		</div>
	);
};

const nodeTypes: NodeTypes = {
	"editable-handle-node": DynamicHandlesNode,
};

const initialNodes = [
	{
		id: "node-1",
		type: "editable-handle-node",
		position: { x: 0, y: 0 },
		data: {},
	},
];

export function EditableHandleDemo() {
	const [nodes, setNodes] = useState<Node[]>(initialNodes);
	const [edges, setEdges] = useState([]);

	const defaultViewport = { x: 100, y: 150, zoom: 1.1 };

	const onNodesChange = useCallback(
		(changes: NodeChange<Node>[]) =>
			setNodes((nds) => applyNodeChanges(changes, nds)),
		[],
	);
	const onEdgesChange = useCallback(
		(changes: EdgeChange<never>[]) =>
			setEdges((eds) => applyEdgeChanges(changes, eds)),
		[],
	);
	const onConnect = useCallback(
		(connection: Connection) => setEdges((eds) => addEdge(connection, eds)),
		[],
	);

	return (
		<div className="w-full h-full">
			<ReactFlowProvider>
				<ReactFlow
					nodes={nodes}
					edges={edges}
					onNodesChange={onNodesChange}
					onEdgesChange={onEdgesChange}
					onConnect={onConnect}
					nodeTypes={nodeTypes}
					defaultViewport={defaultViewport}
				>
					<Background />
				</ReactFlow>
			</ReactFlowProvider>
		</div>
	);
}

```

## Overview

The Editable Handle component makes it easy to create nodes with dynamic connection points (handles). Instead of having fixed inputs and outputs, your nodes can:

- Let users add new input/output handles on the fly
- Edit handle labels and descriptions
- Remove handles when they're no longer needed

This is particularly useful when building nodes that need to adapt to different use cases or when you want to give users more flexibility in how nodes connect to each other.

## Installation

<Tabs defaultValue="cli">

<TabsList>
  <TabsTrigger value="cli">CLI</TabsTrigger>
  <TabsTrigger value="manual">Manual</TabsTrigger>
</TabsList>

<TabsContent value="cli">

```bash
npx shadcn@latest add @simple-ai/editable-handle
```

</TabsContent>

<TabsContent value="manual">

<Steps>

<Step>Install the required shadcn/ui components:</Step>

The Editable Handle component requires these shadcn/ui components:
- [Button](https://ui.shadcn.com/docs/components/button)
- [Input](https://ui.shadcn.com/docs/components/input)
- [Popover](https://ui.shadcn.com/docs/components/popover)
- [Textarea](https://ui.shadcn.com/docs/components/textarea)

<Step>Copy and paste the following code into your project.</Step>

<ComponentSource name="editable-handle" />

<Step>Update the import paths to match your project setup.</Step>

</Steps>

</TabsContent>

</Tabs>

## Usage

The Editable Handle system has two main parts:

1. `EditableHandleDialog`: Used to create new handles with a user-friendly popup
2. `EditableHandle`: The handle component itself that users can edit or delete

### Adding New Handles

Use `EditableHandleDialog` to let users create new handles. It provides a simple interface for entering a handle's label and optional description.

```tsx
import { EditableHandleDialog } from "@/components/ui/flow/editable-handle"

function MyNode() {
  const handleCreate = (name: string, description?: string) => {
    // Add the new handle to your state
    return true // Return true if handle was created successfully
  }

  return (
    <EditableHandleDialog
      variant="create"
      onSave={handleCreate}
      align="start"
    >
      <Button variant="outline" size="sm">
        Add New Input
      </Button>
    </EditableHandleDialog>
  )
}
```

### Managing Handles

Use `EditableHandle` to display handles that users can edit or remove.

```tsx
import { EditableHandle } from "@/components/ui/flow/editable-handle"
import { Position } from "@xyflow/react"

function MyNode({ id }: NodeProps) {
  const handleUpdate = (handleId: string, newName: string, newDescription?: string) => {
    // Update your handle state
    return true // Return true if update was successful
  }

  const handleDelete = (handleId: string) => {
    // Remove the handle from your state
  }

  return (
    <EditableHandle
      nodeId={id}
      handleId="handle-1"
      name="Input 1"
      description="Optional description"
      type="target"
      position={Position.Left}
      onUpdateTool={handleUpdate}
      onDelete={handleDelete}
      showDescription
    />
  )
}
```

# /docs/workflows/generate-text-node

---
title: Generate Text Node
description: A React Flow node component that represents Vercel AI SDK's text generation capabilities, featuring system instructions, prompts, and optional tool outputs
featured: true
component: true
---

```tsx
"use client";

import "@xyflow/react/dist/style.css";

import {
	addEdge,
	applyEdgeChanges,
	applyNodeChanges,
	Background,
	type Connection,
	type EdgeChange,
	type Node,
	type NodeChange,
	type NodeProps,
	type NodeTypes,
	ReactFlow,
	ReactFlowProvider,
} from "@xyflow/react";
import { nanoid } from "nanoid";
import { useCallback, useState } from "react";
import type { Model } from "@/components/ui/model-selector";
import { GenerateTextNode } from "../ui/flow/generate-text-node";

const GenerateTextNodeController = ({
	id,
	data,
	...props
}: NodeProps<Node>) => {
	const [model, setModel] = useState<Model>("deepseek-chat");
	const [toolHandles, setToolHandles] = useState({
		tools: [
			{
				id: "name",
				name: "name",
			},
		],
	});

	const handleCreateTool = useCallback(() => {
		setToolHandles({
			...toolHandles,
			tools: [...toolHandles.tools, { id: nanoid(), name: "name" }],
		});
		return true;
	}, [toolHandles]);

	const handleRemoveTool = useCallback(() => {
		setToolHandles({
			...toolHandles,
			tools: toolHandles.tools.filter((tool) => tool.id !== "name"),
		});
		return true;
	}, [toolHandles]);

	const handleUpdateTool = useCallback(
		(toolId: string, newName: string, newDescription?: string) => {
			setToolHandles({
				...toolHandles,
				tools: toolHandles.tools.map((tool) =>
					tool.id === toolId
						? {
								...tool,
								name: newName,
								description: newDescription,
							}
						: tool,
				),
			});
			return true;
		},
		[toolHandles],
	);

	return (
		<GenerateTextNode
			id={id}
			data={{
				status: "idle",
				config: { model },
				dynamicHandles: toolHandles,
			}}
			{...props}
			type="generate-text"
			onModelChange={(model) => setModel(model)}
			onCreateTool={handleCreateTool}
			onRemoveTool={handleRemoveTool}
			onUpdateTool={handleUpdateTool}
			onDeleteNode={() => {}}
		/>
	);
};

const nodeTypes: NodeTypes = {
	"generate-text": GenerateTextNodeController,
};

const initialNodes = [
	{
		id: "node-1",
		type: "generate-text",
		position: { x: 0, y: -130 },
		data: {},
	},
];

export function ResizableNodeDemo() {
	const [nodes, setNodes] = useState<Node[]>(initialNodes);
	const [edges, setEdges] = useState([]);

	// Add default viewport configuration
	const defaultViewport = { x: 100, y: 200, zoom: 1 };

	const onNodesChange = useCallback(
		(changes: NodeChange<Node>[]) =>
			setNodes((nds) => applyNodeChanges(changes, nds)),
		[],
	);
	const onEdgesChange = useCallback(
		(changes: EdgeChange<never>[]) =>
			setEdges((eds) => applyEdgeChanges(changes, eds)),
		[],
	);
	const onConnect = useCallback(
		(connection: Connection) => setEdges((eds) => addEdge(connection, eds)),
		[],
	);
	return (
		<div className="w-full h-full">
			<ReactFlowProvider>
				<ReactFlow
					nodes={nodes}
					edges={edges}
					onNodesChange={onNodesChange}
					onEdgesChange={onEdgesChange}
					onConnect={onConnect}
					nodeTypes={nodeTypes}
					defaultViewport={defaultViewport}
					/* fitView */
				>
					<Background />
				</ReactFlow>
			</ReactFlowProvider>
		</div>
	);
}

```

## Overview

The Generate Text Node is a React Flow component that provides a visual interface representing the `generateText` function from Vercel AI SDK. When executing your flow, this node will map to the SDK's text generation functionality. It allows you to:

- Set system-level instructions that guide the AI's behavior
- Input specific prompts for each generation
- Receive generated text as output
- Optionally define tool outputs that the AI can use to route information to different parts of your workflow

## Components Used

This node is built using several React Flow Components:
- [Base Node](https://reactflow.dev/components/nodes/base-node) - For the core node structure and styling
- [Node Header](https://reactflow.dev/components/nodes/node-header) - For the node's header section
- [Labeled Handle](https://reactflow.dev/components/handles/labeled-handle) - For the input and output connection points

## Installation

<Tabs defaultValue="cli">

<TabsList>
  <TabsTrigger value="cli">CLI</TabsTrigger>
  <TabsTrigger value="manual">Manual</TabsTrigger>
</TabsList>

<TabsContent value="cli">

```bash
npx shadcn@latest add @simple-ai/generate-text-node
```

</TabsContent>

<TabsContent value="manual">

<Steps>

<Step>Install the required shadcn/ui components:</Step>

```bash
npx shadcn@latest add button
npx shadcn@latest add separator
```

<Step>Copy and paste the following code into your project.</Step>

<ComponentSource name="generate-text-node" />

<Step>Update the import paths to match your project setup.</Step>

</Steps>

</TabsContent>

</Tabs>

## Usage

The Generate Text Node requires a controller component to manage its state and handle tool-related operations. Here's how to implement it in your React Flow application:

```tsx
// Controller component to manage the Generate Text Node
const GenerateTextNodeController = ({
  id,
  data,
  ...props
}: NodeProps<Node>) => {
  const [model, setModel] = useState<Model>("deepseek-chat");
  const [toolHandles, setToolHandles] = useState({
    tools: [{ id: "name", name: "name" }],
  });

  // Handle tool creation
  const handleCreateTool = useCallback(() => {
    setToolHandles({
      ...toolHandles,
      tools: [...toolHandles.tools, { id: nanoid(), name: "name" }],
    });
    return true;
  }, [toolHandles]);

  // Handle tool removal
  const handleRemoveTool = useCallback((toolId: string) => {
    setToolHandles({
      ...toolHandles,
      tools: toolHandles.tools.filter((tool) => tool.id !== toolId),
    });
  }, [toolHandles]);

  return (
    <GenerateTextNode
      id={id}
      data={{
        status: "idle",
        config: { model },
        dynamicHandles: toolHandles,
      }}
      {...props}
      onModelChange={(model) => setModel(model)}
      onCreateTool={handleCreateTool}
      onRemoveTool={handleRemoveTool}
      onUpdateTool={handleUpdateTool}
    />
  );
};

// Register the node type
const nodeTypes = {
  "generate-text": GenerateTextNodeController,
};
```

The node provides the following connection points:

- **Inputs**:
  - `system`: For system-level instructions that guide the AI's behavior
  - `prompt`: For the specific text generation prompt
- **Outputs**:
  - `result`: The main output containing the generated text
  - Dynamic tool outputs: Optional outputs that can be added/removed as needed

The node's state includes:
- Model selection for text generation
- Processing status indication (idle, processing, error, success)
- Dynamic tool management (add, remove, update)


# /docs/workflows/index

---
title: Workflows
description: Build powerful AI agent workflows with our React Flow components powered by Vercel AI SDK.
---

<ComponentsList type="workflows"/>


# /docs/workflows/prompt-crafter-node

---
title: Prompt Crafter Node
description: A React Flow node component for building dynamic prompts by combining multiple inputs using a template-based approach
featured: true
component: true
---

```tsx
"use client";

import "@xyflow/react/dist/style.css";

import {
	addEdge,
	applyEdgeChanges,
	applyNodeChanges,
	Background,
	type Connection,
	type EdgeChange,
	type Node,
	type NodeChange,
	type NodeProps,
	type NodeTypes,
	ReactFlow,
	ReactFlowProvider,
} from "@xyflow/react";
import { nanoid } from "nanoid";
import { useCallback, useState } from "react";
import { PromptCrafterNode } from "@/components/ui/flow/prompt-crafter-node";

const PromptCrafterNodeController = ({
	id,
	data,
	...props
}: NodeProps<Node>) => {
	const [template, setTemplate] = useState("Hello {{name}}");
	const [dynamicHandles, setDynamicHandles] = useState({
		"template-tags": [
			{
				id: "name",
				name: "name",
			},
		],
	});

	const handleCreateInput = useCallback(
		(name: string) => {
			setDynamicHandles({
				...dynamicHandles,
				"template-tags": [
					...dynamicHandles["template-tags"],
					{ id: nanoid(), name },
				],
			});
			return true;
		},
		[dynamicHandles],
	);

	const handleRemoveInput = useCallback(() => {
		setDynamicHandles({
			...dynamicHandles,
			"template-tags": dynamicHandles["template-tags"].filter(
				(input) => input.id !== "name",
			),
		});
		return true;
	}, [dynamicHandles]);

	const handleUpdateInputName = useCallback(
		(handleId: string, newLabel: string) => {
			setDynamicHandles({
				...dynamicHandles,
				"template-tags": dynamicHandles["template-tags"].map((input) =>
					input.id === handleId
						? { ...input, name: newLabel }
						: input,
				),
			});
			return true;
		},
		[dynamicHandles],
	);

	return (
		<PromptCrafterNode
			id={id}
			data={{
				status: "success",
				config: {
					template,
				},
				dynamicHandles,
			}}
			{...props}
			type="prompt-crafter"
			onPromptTextChange={setTemplate}
			onCreateInput={handleCreateInput}
			onRemoveInput={handleRemoveInput}
			onUpdateInputName={handleUpdateInputName}
			onDeleteNode={() => {}}
		/>
	);
};

const nodeTypes: NodeTypes = {
	"prompt-crafter-node": PromptCrafterNodeController,
};

const initialNodes = [
	{
		id: "node-1",
		type: "prompt-crafter-node",
		position: { x: 0, y: -50 },
		data: {},
	},
];

export function ResizableNodeDemo() {
	const [nodes, setNodes] = useState<Node[]>(initialNodes);
	const [edges, setEdges] = useState([]);

	// Add default viewport configuration
	const defaultViewport = { x: 100, y: 100, zoom: 0.9 };

	const onNodesChange = useCallback(
		(changes: NodeChange<Node>[]) =>
			setNodes((nds) => applyNodeChanges(changes, nds)),
		[],
	);
	const onEdgesChange = useCallback(
		(changes: EdgeChange<never>[]) =>
			setEdges((eds) => applyEdgeChanges(changes, eds)),
		[],
	);
	const onConnect = useCallback(
		(connection: Connection) => setEdges((eds) => addEdge(connection, eds)),
		[],
	);
	return (
		<div className="w-full h-full">
			<ReactFlowProvider>
				<ReactFlow
					nodes={nodes}
					edges={edges}
					onNodesChange={onNodesChange}
					onEdgesChange={onEdgesChange}
					onConnect={onConnect}
					nodeTypes={nodeTypes}
					defaultViewport={defaultViewport}
					/* fitView */
				>
					<Background />
				</ReactFlow>
			</ReactFlowProvider>
		</div>
	);
}

```

## Overview

The Prompt Crafter Node is a React Flow component that allows you to build dynamic prompts by combining multiple inputs using a template-based approach. It provides:

- A template editor where you can write your prompt with placeholders
- Dynamic input handles that can receive text from other nodes
- Template syntax highlighting that validates input references
- A single output that combines all inputs according to your template

## Components Used

This node is built using several React Flow Components:
- [Base Node](https://reactflow.dev/components/nodes/base-node) - For the core node structure and styling
- [Node Header](https://reactflow.dev/components/nodes/node-header) - For the node's header section
- [Labeled Handle](https://reactflow.dev/components/handles/labeled-handle) - For the input and output connection points

## Installation

<Tabs defaultValue="cli">

<TabsList>
  <TabsTrigger value="cli">CLI</TabsTrigger>
  <TabsTrigger value="manual">Manual</TabsTrigger>
</TabsList>

<TabsContent value="cli">

```bash
npx shadcn@latest add @simple-ai/prompt-crafter-node
```

</TabsContent>

<TabsContent value="manual">

<Steps>

<Step>Install the required shadcn/ui components:</Step>

```bash
npx shadcn@latest add button
npx shadcn@latest add separator
npx shadcn@latest add command
npx shadcn@latest add popover
```

<Step>Install CodeMirror dependencies:</Step>

```bash
npm install @uiw/react-codemirror @uiw/codemirror-themes @codemirror/language @codemirror/view @lezer/highlight
```

<Step>Copy and paste the following code into your project.</Step>

<ComponentSource name="prompt-crafter-node" />

<Step>Update the import paths to match your project setup.</Step>

</Steps>

</TabsContent>

</Tabs>

## Usage

The Prompt Crafter Node requires a controller component to manage its state and handle input-related operations. Here's how to implement it in your React Flow application:

```tsx
// Controller component to manage the Prompt Crafter Node
const PromptCrafterNodeController = ({
  id,
  data,
  ...props
}: NodeProps<Node>) => {
  const [template, setTemplate] = useState("Hello {{name}}");
  const [dynamicHandles, setDynamicHandles] = useState({
    "template-tags": [{ id: "name", name: "name" }],
  });

  // Handle input creation
  const handleCreateInput = useCallback((name: string) => {
    setDynamicHandles({
      ...dynamicHandles,
      "template-tags": [
        ...dynamicHandles["template-tags"],
        { id: nanoid(), name },
      ],
    });
    return true;
  }, [dynamicHandles]);

  // Handle input removal
  const handleRemoveInput = useCallback((handleId: string) => {
    setDynamicHandles({
      ...dynamicHandles,
      "template-tags": dynamicHandles["template-tags"].filter(
        (input) => input.id !== handleId
      ),
    });
  }, [dynamicHandles]);

  return (
    <PromptCrafterNode
      id={id}
      data={{
        status: "idle",
        config: { template },
        dynamicHandles,
      }}
      {...props}
      onPromptTextChange={setTemplate}
      onCreateInput={handleCreateInput}
      onRemoveInput={handleRemoveInput}
      onUpdateInputName={handleUpdateInputName}
    />
  );
};

// Register the node type
const nodeTypes = {
  "prompt-crafter": PromptCrafterNodeController,
};
```

The node provides the following connection points:

- **Inputs**:
  - Dynamic inputs that can be added/removed as needed
  - Each input can be referenced in the template using `{{input-name}}`
- **Output**:
  - `result`: The final prompt with all inputs combined according to the template

The node's features include:
- Template editor with syntax highlighting
- Dynamic input management (add, remove, rename)
- Input validation in the template
- Visual feedback for valid/invalid input references
- Quick input insertion through a command palette



# /docs/workflows/resizable-node

---
title: Resizable Node
description: A wrapper React Flow node component that adds resizing functionality to other nodes
featured: true
component: true
---

```tsx
"use client";

import "@xyflow/react/dist/style.css";

import {
	addEdge,
	applyEdgeChanges,
	applyNodeChanges,
	Background,
	type Connection,
	type EdgeChange,
	type Node,
	type NodeChange,
	type NodeProps,
	type NodeTypes,
	ReactFlow,
	ReactFlowProvider,
} from "@xyflow/react";
import { useCallback, useState } from "react";
import { ResizableNode } from "@/components/ui/flow/resizable-node";

const TextResizableNode = (props: NodeProps<Node>) => {
	return (
		<ResizableNode
			selected={props.selected}
			className="flex flex-col items-center justify-center p-4"
		>
			<span>{props.data.value as string}</span>
		</ResizableNode>
	);
};

const nodeTypes: NodeTypes = {
	"text-resizable-node": TextResizableNode,
};

const initialNodes = [
	{
		id: "node-1",
		type: "text-resizable-node",
		position: { x: 0, y: 0 },
		data: { value: "Try to resize me" },
	},
];

export function ResizableNodeDemo() {
	const [nodes, setNodes] = useState<Node[]>(initialNodes);
	const [edges, setEdges] = useState([]);

	// Add default viewport configuration
	const defaultViewport = { x: 100, y: 150, zoom: 1 };

	const onNodesChange = useCallback(
		(changes: NodeChange<Node>[]) =>
			setNodes((nds) => applyNodeChanges(changes, nds)),
		[],
	);
	const onEdgesChange = useCallback(
		(changes: EdgeChange<never>[]) =>
			setEdges((eds) => applyEdgeChanges(changes, eds)),
		[],
	);
	const onConnect = useCallback(
		(connection: Connection) => setEdges((eds) => addEdge(connection, eds)),
		[],
	);

	return (
		<div className="w-full h-full">
			<ReactFlowProvider>
				<ReactFlow
					nodes={nodes}
					edges={edges}
					onNodesChange={onNodesChange}
					onEdgesChange={onEdgesChange}
					onConnect={onConnect}
					nodeTypes={nodeTypes}
					defaultViewport={defaultViewport}
					/* fitView */
				>
					<Background />
				</ReactFlow>
			</ReactFlowProvider>
		</div>
	);
}

```

## Overview

The Resizable Node is a simple wrapper component that adds resizing functionality to other nodes in your React Flow application. It provides:

- Resize handles that appear when the node is selected
- Built-in size constraints for consistent node sizes
- Integration with React Flow's base node system

## Components Used

This node is built using several React Flow Components:
- [Base Node](https://reactflow.dev/components/nodes/base-node) - For the core node structure and styling
- [Node Resizer](https://reactflow.dev/api-reference/components/node-resizer) - For the resizing functionality

## Installation

<Tabs defaultValue="cli">

<TabsList>
  <TabsTrigger value="cli">CLI</TabsTrigger>
  <TabsTrigger value="manual">Manual</TabsTrigger>
</TabsList>

<TabsContent value="cli">

```bash
npx shadcn@latest add @simple-ai/resizable-node
```

</TabsContent>

<TabsContent value="manual">

<Steps>

<Step>Copy and paste the following code into your project.</Step>

<ComponentSource name="resizable-node" />

<Step>Update the import paths to match your project setup.</Step>

</Steps>

</TabsContent>

</Tabs>

## Usage

The Resizable Node is designed to wrap other node content to make it resizable. Here's how to use it in your React Flow application:

```tsx
// Example of a custom node using ResizableNode
const TextResizableNode = (props: NodeProps<Node>) => {
  return (
    <ResizableNode
      selected={props.selected}
      className="flex flex-col items-center justify-center p-4"
    >
      <span>{props.data.value as string}</span>
    </ResizableNode>
  );
};

// Register the node type
const nodeTypes = {
  "text-resizable-node": TextResizableNode,
};
```

The node includes the following features:
- Size constraints (min: 250x200px, max: 800x800px)
- Resize handles visible on selection
- Hover effect with orange highlight
- Customizable through className prop


# /docs/workflows/status-edge

---
title: Status Edge
description: A React Flow edge component that provides visual feedback through color-coded states
featured: true
component: true
---

```tsx
"use client";

import "@xyflow/react/dist/style.css";

import { Background, ReactFlow, ReactFlowProvider } from "@xyflow/react";

import { StatusEdge } from "@/components/ui/flow/status-edge";

const defaultNodes = [
	{
		id: "1",
		position: { x: 200, y: 200 },
		data: { label: "Node" },
	},
	{
		id: "2",
		position: { x: 400, y: 400 },
		data: { label: "Node" },
	},
];

const defaultEdges = [
	{
		id: "e1-2",
		source: "1",
		target: "2",
		type: "status",
		data: {
			error: true,
		},
	},
];

const edgeTypes = {
	status: StatusEdge,
};

export function StatusEdgeDemo() {
	const defaultViewport = { x: -100, y: -100, zoom: 1.1 };

	return (
		<div className="w-full h-full">
			<ReactFlowProvider>
				<ReactFlow
					defaultNodes={defaultNodes}
					defaultEdges={defaultEdges}
					edgeTypes={edgeTypes}
					defaultViewport={defaultViewport}
				>
					<Background />
				</ReactFlow>
			</ReactFlowProvider>
		</div>
	);
}

```

## Overview

The Status Edge is a simple React Flow edge component that provides visual feedback through color coding. It provides:

- Color-coded states for different conditions:
  - Red for error state
  - Gray for default state
- Smooth transitions between states
- Built on React Flow's BaseEdge component

## Components Used

This edge is built using React Flow Components:
- [Base Edge](https://reactflow.dev/api-reference/components/base-edge) - For the core edge structure
- [getBezierPath](https://reactflow.dev/api-reference/utils/get-bezier-path) - For calculating the curved edge path

## Installation

<Tabs defaultValue="cli">

<TabsList>
  <TabsTrigger value="cli">CLI</TabsTrigger>
  <TabsTrigger value="manual">Manual</TabsTrigger>
</TabsList>

<TabsContent value="cli">

```bash
npx shadcn@latest add @simple-ai/status-edge
```

</TabsContent>

<TabsContent value="manual">

<Steps>

<Step>Copy and paste the following code into your project.</Step>

<ComponentSource name="status-edge" />

<Step>Update the import paths to match your project setup.</Step>

</Steps>

</TabsContent>

</Tabs>

## Usage

The Status Edge is designed to provide visual feedback for connections in your flow. Here's how to use it in your React Flow application:

```tsx
// Example usage of the Status Edge
const edges = [
  {
    id: "e1-2",
    source: "1",
    target: "2",
    type: "status",
    data: {
      error: true // Set to true to show error state
    }
  }
];

// Register the edge type
const edgeTypes = {
  status: StatusEdge,
};
```


# /docs/workflows/text-input-node

---
title: Text Input Node
description: A React Flow node component that provides a simple text input interface with a resizable textarea and single output
featured: true
component: true
---

```tsx
"use client";

import "@xyflow/react/dist/style.css";

import {
	addEdge,
	applyEdgeChanges,
	applyNodeChanges,
	Background,
	type Connection,
	type EdgeChange,
	type Node,
	type NodeChange,
	type NodeProps,
	type NodeTypes,
	ReactFlow,
	ReactFlowProvider,
} from "@xyflow/react";

import { useCallback, useState } from "react";
import { TextInputNode } from "@/components/ui/flow/text-input-node";

const TextInputNodeController = ({ id, data, ...props }: NodeProps<Node>) => {
	const [value, setValue] = useState("Hello World!");

	return (
		<TextInputNode
			id={id}
			data={{
				status: "idle",
				config: { value },
			}}
			{...props}
			type="text-input"
			onTextChange={(value) => setValue(value)}
			onDeleteNode={() => {}}
		/>
	);
};

const nodeTypes: NodeTypes = {
	"text-input": TextInputNodeController,
};

const initialNodes = [
	{
		id: "node-1",
		type: "text-input",
		position: { x: 0, y: -50 },
		data: {},
	},
];

export function ResizableNodeDemo() {
	const [nodes, setNodes] = useState<Node[]>(initialNodes);
	const [edges, setEdges] = useState([]);

	// Add default viewport configuration
	const defaultViewport = { x: 100, y: 120, zoom: 1.2 };

	const onNodesChange = useCallback(
		(changes: NodeChange<Node>[]) =>
			setNodes((nds) => applyNodeChanges(changes, nds)),
		[],
	);
	const onEdgesChange = useCallback(
		(changes: EdgeChange<never>[]) =>
			setEdges((eds) => applyEdgeChanges(changes, eds)),
		[],
	);
	const onConnect = useCallback(
		(connection: Connection) => setEdges((eds) => addEdge(connection, eds)),
		[],
	);
	return (
		<div className="w-full h-full">
			<ReactFlowProvider>
				<ReactFlow
					nodes={nodes}
					edges={edges}
					onNodesChange={onNodesChange}
					onEdgesChange={onEdgesChange}
					onConnect={onConnect}
					nodeTypes={nodeTypes}
					defaultViewport={defaultViewport}
					/* fitView */
				>
					<Background />
				</ReactFlow>
			</ReactFlowProvider>
		</div>
	);
}

```

## Overview

The Text Input Node is a simple React Flow component that allows you to input text that can be used by other nodes in your workflow. It provides:

- A resizable textarea for entering and editing text
- A single output handle that sends the entered text to connected nodes
- Visual status feedback for different states

## Components Used

This node is built using several React Flow Components:
- [Base Node](https://reactflow.dev/components/nodes/base-node) - For the core node structure and styling
- [Node Header](https://reactflow.dev/components/nodes/node-header) - For the node's header section
- [Labeled Handle](https://reactflow.dev/components/handles/labeled-handle) - For the output connection point

## Installation

<Tabs defaultValue="cli">

<TabsList>
  <TabsTrigger value="cli">CLI</TabsTrigger>
  <TabsTrigger value="manual">Manual</TabsTrigger>
</TabsList>

<TabsContent value="cli">

```bash
npx shadcn@latest add @simple-ai/text-input-node
```

</TabsContent>

<TabsContent value="manual">

<Steps>

<Step>Install the required shadcn/ui components:</Step>

```bash
npx shadcn@latest add textarea
npx shadcn@latest add separator
```

<Step>Copy and paste the following code into your project.</Step>

<ComponentSource name="text-input-node" />

<Step>Update the import paths to match your project setup.</Step>

</Steps>

</TabsContent>

</Tabs>

## Usage

The Text Input Node requires a controller component to manage its state. Here's how to implement it in your React Flow application:

```tsx
// Controller component to manage the Text Input Node
const TextInputNodeController = ({
  id,
  data,
  ...props
}: NodeProps<Node>) => {
  const [value, setValue] = useState("Hello World!");

  return (
    <TextInputNode
      id={id}
      data={{
        status: "idle",
        config: { value },
      }}
      {...props}
      onTextChange={(value) => setValue(value)}
      onDeleteNode={() => {}}
    />
  );
};

// Register the node type
const nodeTypes = {
  "text-input": TextInputNodeController,
};
```

The node provides the following connection point:

- **Output**:
  - `result`: The text content entered in the textarea

The node's features include:
- Resizable textarea for text input
- Status indication (idle, processing, error, success)
- Clean and intuitive interface
- Multi-line text support


# /docs/workflows/visualize-text-node

---
title: Visualize Text Node
description: A React Flow node component for displaying text content with Markdown support and a resizable interface
featured: true
component: true
---

```tsx
"use client";

import "@xyflow/react/dist/style.css";

import {
	addEdge,
	applyEdgeChanges,
	applyNodeChanges,
	Background,
	type Connection,
	type EdgeChange,
	type Node,
	type NodeChange,
	type NodeTypes,
	ReactFlow,
	ReactFlowProvider,
} from "@xyflow/react";
import { useCallback, useState } from "react";
import { VisualizeTextNode } from "@/components/ui/flow/visualize-text-node";

const nodeTypes: NodeTypes = {
	"visualize-text": VisualizeTextNode,
};

const initialNodes = [
	{
		id: "node-1",
		type: "visualize-text",
		position: { x: 0, y: -50 },
		data: {
			input: "### I support markdown\n\nVisualize text coming from other nodes\n\n- 1\n- 2\n- 3",
			status: "success",
		},
	},
];

export function VisualizeTextNodeDemo() {
	const [nodes, setNodes] = useState<Node[]>(initialNodes);
	const [edges, setEdges] = useState([]);

	// Add default viewport configuration
	const defaultViewport = { x: 100, y: 75, zoom: 1 };

	const onNodesChange = useCallback(
		(changes: NodeChange<Node>[]) =>
			setNodes((nds) => applyNodeChanges(changes, nds)),
		[],
	);
	const onEdgesChange = useCallback(
		(changes: EdgeChange<never>[]) =>
			setEdges((eds) => applyEdgeChanges(changes, eds)),
		[],
	);
	const onConnect = useCallback(
		(connection: Connection) => setEdges((eds) => addEdge(connection, eds)),
		[],
	);
	return (
		<div className="w-full h-full">
			<ReactFlowProvider>
				<ReactFlow
					nodes={nodes}
					edges={edges}
					onNodesChange={onNodesChange}
					onEdgesChange={onEdgesChange}
					onConnect={onConnect}
					nodeTypes={nodeTypes}
					defaultViewport={defaultViewport}
					/* fitView */
				>
					<Background />
				</ReactFlow>
			</ReactFlowProvider>
		</div>
	);
}

```

## Overview

The Visualize Text Node is a simple React Flow component that displays text content from other nodes with Markdown support. It provides:

- A resizable container for displaying text
- Markdown rendering for formatted text display
- A single input handle to receive text from other nodes
- Visual status feedback for different states

## Components Used

This node is built using several React Flow Components:
- [Base Node](https://reactflow.dev/components/nodes/base-node) - For the core node structure and styling
- [Node Header](https://reactflow.dev/components/nodes/node-header) - For the node's header section
- [Labeled Handle](https://reactflow.dev/components/handles/labeled-handle) - For the input connection point

It also uses our [Markdown Content](/docs/components/markdown-content) component for rendering markdown-formatted text.

## Installation

<Tabs defaultValue="cli">

<TabsList>
  <TabsTrigger value="cli">CLI</TabsTrigger>
  <TabsTrigger value="manual">Manual</TabsTrigger>
</TabsList>

<TabsContent value="cli">

```bash
npx shadcn@latest add @simple-ai/visualize-text-node
```

</TabsContent>

<TabsContent value="manual">

<Steps>

<Step>Install the required shadcn/ui components:</Step>

```bash
npx shadcn@latest add separator
```

<Step>Copy and paste the following code into your project.</Step>

<ComponentSource name="visualize-text-node" />

<Step>Update the import paths to match your project setup.</Step>

</Steps>

</TabsContent>

</Tabs>

## Usage

The Visualize Text Node is designed to display text content from other nodes. Here's how to implement it in your React Flow application:

```tsx
// Example usage of the Visualize Text Node
const nodes = [
  {
    id: "visualize-1",
    type: "visualize-text",
    position: { x: 0, y: 0 },
    data: {
      input: "### Markdown Example\n\nDisplaying text with:\n- Markdown formatting\n- From other nodes",
      status: "success"
    }
  }
];

// Register the node type
const nodeTypes = {
  "visualize-text": VisualizeTextNode,
};
```

The node provides the following connection point:

- **Input**:
  - `input`: Receives text content to be displayed

The node's features include:
- Markdown rendering support
- Resizable display area
- Text selection enabled
- Status indication (idle, processing, error, success)


