AI
Chat
A full-featured AI chat window — markdown rendering with syntax-highlighted code blocks, token-by-token streaming, message grouping, per-message actions, attachments, voice input, and a generative-UI extension point for rendering arbitrary custom content in place of a message's text.
ChatPreview
Loading demo
Loading demo
Loading demo
Loading demo
Loading demo
Loading demo
Usage
tsx
import { useState } from "react";
import { Chat } from "@vesture/react";
import type { ChatMessage } from "@vesture/react";
function Example() {
const [messages, setMessages] = useState<ChatMessage[]>([]);
const [isGenerating, setIsGenerating] = useState(false);
const handleSend = (content: string) => {
setMessages((prev) => [...prev, { id: crypto.randomUUID(), role: "user", content }]);
setIsGenerating(true);
// Your own fetch/SSE handling — Chat only needs `content` to keep
// growing on a message with streaming: true while tokens arrive.
const id = crypto.randomUUID();
setMessages((prev) => [...prev, { id, role: "assistant", content: "", streaming: true }]);
streamReplyFromMyBackend((token) => {
setMessages((prev) =>
prev.map((m) => (m.id === id ? { ...m, content: m.content + token } : m))
);
}).then(() => {
setMessages((prev) => prev.map((m) => (m.id === id ? { ...m, streaming: false } : m)));
setIsGenerating(false);
});
};
return <Chat messages={messages} onSendMessage={handleSend} isGenerating={isGenerating} />;
}Behavior
- Fully controlled: messages is the complete array and onSendMessage fires with the composed text — Chat never fetches, stores, or owns a connection of any kind.
- Streaming: the consumer appends tokens into a message's content from their own fetch/SSE handling while streaming: true. Chat throttles its own markdown re-parse to roughly once per 80ms during an active stream instead of reparsing on every token, then parses once more immediately when streaming ends so nothing is left stale.
- content is markdown, parsed with marked and sanitized through a DOMPurify allowlist scoped to markdown's actual output shape. Fenced code blocks get syntax highlighting via highlight.js, remapped onto the active theme's color tokens rather than shipping a separate hardcoded highlight.js theme.
- Consecutive messages from the same sender (same role + sender.name, within a 5-minute window when timestamps are present) group visually — the avatar and name render once per group, and later messages in the run get a hover-revealed timestamp instead of repeating that chrome.
- Auto-scrolls to new content only when you're already scrolled near the bottom; otherwise a floating "New messages" button appears instead of yanking your scroll position out from under you.
- Per-message action menu — Copy, Edit, Delete, Regenerate, Retry. Edit only appears when onEditMessage is supplied (and only for user messages), Regenerate only when onRegenerateMessage is supplied (assistant messages only), Retry only for a message with status: "error". The whole menu is suppressed while a message is still streaming.
- Attachments: the paperclip button embeds the same FileUpload dropzone/queue FileUpload itself uses — onFilesAdded hands you the raw File[], the identical controlled-queue contract.
- Voice input: a mic button using the browser's native SpeechRecognition — no server-side transcription, and the button simply doesn't render in browsers without it (Firefox has no implementation). Runs continuously with live interim results until you click it again to stop, and surfaces a visible, human-readable error (denied permission, no microphone, a network blip) instead of silently reverting.
- Keyboard shortcuts: Arrow-Up on an empty, focused input recalls your own last sent message as a fresh compose prefill — distinct from the Edit action above, since the original message in history is never touched. A configurable focusShortcut (default "/") jumps focus to the input from anywhere on the page, and is automatically skipped while any other editable element already has focus (including while you're mid-sentence in Chat's own input).
- renderToolCall is a pure generative-UI extension point — Chat ships no built-in tool-call types of its own. Set toolCall on a message and provide renderToolCall to render arbitrary custom content (a chart, a card, anything) in place of, or alongside, that message's normal markdown bubble.
- For a multi-conversation app, see ChatWithThreads (a separate component, its own docs page) — it wraps Chat with a thread-list sidebar. Chat itself stays single-conversation and has no notion of threads.
Props
ChatMessage
| Prop | Type | Default | Description |
|---|---|---|---|
| id / role | string / "user" | "assistant" | "system" | — | Required. |
| content | string | — | Markdown source. |
| status | "sending" | "sent" | "error" | — | Drives the message's status chrome and, for "error", the Retry action. |
| timestamp | Date | — | Shown in the meta row; also used for the 5-minute message-grouping window. |
| sender | { name: string; avatarUrl?: string } | — | Shown once per message group. |
| streaming | boolean | — | Set true while you're appending tokens to content — suppresses the action menu until it's unset. |
| attachments | { name: string; url: string; type: string }[] | — | Rendered inline — images shown directly, other types as a file chip. |
| toolCall | { type: string; data: unknown } | — | Opt-in generative-UI payload — rendered by ChatProps.renderToolCall when both are present. |
ChatProps
| Prop | Type | Default | Description |
|---|---|---|---|
| messages | ChatMessage[] | — | Required. |
| onSendMessage | (content: string) => void | — | Fires with the composed text from the input. |
| isGenerating | boolean | — | Shows a typing indicator when true and no streaming message has content yet. |
| placeholder / disabled | string / boolean | — | Passed to the input. |
| emptyState | ReactNode | — | Composes the existing EmptyState component when messages is empty. |
| onEditMessage / onDeleteMessage / onRegenerateMessage / onRetryMessage | (id, ...) => void | — | Gate the matching action-menu item's visibility (see behavior above). |
| suggestions | string[] | — | Quick-action chips above the input; clicking one calls onSendMessage directly. |
| onFilesAdded | (files: File[]) => void | — | Fires with newly picked/dropped files from the attachment panel — same contract as FileUpload. |
| renderToolCall | (toolCall: { type: string; data: unknown }) => ReactNode | — | Generative-UI extension point — see behavior above. |
| focusShortcut | string | null | "/" | Global focus shortcut — a single key or a "mod+k"-style combo ("mod" = Cmd on macOS, Ctrl elsewhere). Pass null to disable. |