Parsed Output:
"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
pnpm dlx shadcn@latest add @simple-ai/chat-input
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, 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
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
"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
"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) |