Summarize the main product goals.
AI interfaces / BLOCK
AI Chat
A responsive assistant chat with streaming replies, file attachments, and conversation controls.
AI Chat
A responsive assistant chat with streaming and file attachments.
Overview
Bring your own model handler and connect it to a ready-made chat workspace. The block handles the responsive conversation UI, streamed text, attachment previews, cancellation, and local chat history.
Features
- Provider-agnostic onSend handler accepts a string or an async stream.
- Streams plain text and code without rendering unfinished Markdown fences as raw text.
- Lets users stop generation, retry errors, and continue typing while a response streams.
- Supports image, document, audio, video, and custom attachment options.
- Includes local chat history with pin, archive, restore, and delete actions.
- Provides a collapsible desktop sidebar and a closed-by-default mobile drawer.
- Includes optional voice input and configurable accent color, attachment count, and response size.
Installation
Choose a package manager to add the complete editable block to components/ai-chat-block. The CLI installs its runtime dependencies and keeps the helper files inside that folder.
$ npx @istmx/ui add ai-chat-blockCopies the install command for your selected package manager.
React and Tailwind CSS should already be configured in your app. The command installs Motion, Tabler Icons, clsx, and tailwind-merge when they are missing.
Dependencies
Files included
The installer creates one self-contained folder. Each file has one clear role, so you can customize the block without tracking down portfolio-specific imports.
components/ai-chat-block/├── index.ts├── ai-chat-block.tsx├── ai-chat-input.tsx├── chat-composer.tsx├── chat-sidebar.tsx├── message-list.tsx├── chat-message.tsx├── message-content.tsx├── chat-code-block.tsx├── code-highlighter.tsx├── attachment.tsx├── default-attachments.tsx├── send-button.tsx├── voice-button.tsx├── chat-logo.tsx├── chat-user-avatar.tsx├── types.ts├── utils.tsindex.tsPublic component and type exports.
ai-chat-block.tsxMain block, local conversation state, model request lifecycle, streaming, retry, and stop behavior.
ai-chat-input.tsxMessage field, attachment selection and previews, voice input, and send controls.
chat-composer.tsxConnects the main block to the message input and forwards composer events.
chat-sidebar.tsxConversation list, new chat, pin, archive, restore, delete, and account area.
message-list.tsxScrollable conversation log and empty state.
chat-message.tsxUser and assistant message layout, thinking disclosure, files, and errors.
message-content.tsxRenders paragraphs, headings, lists, inline formatting, and live code fences.
chat-code-block.tsxFinished code presentation and copy action.
code-highlighter.tsxSmall dependency-free tokenizer used by the code block.
attachment.tsxAttachment picker dialog, file preview tiles, and enlarged previews.
default-attachments.tsxDefault image, document, audio, and video options.
send-button.tsxAnimated send, sending, stop, and sent states.
voice-button.tsxOptional browser voice-input control.
chat-logo.tsxInline SVG mark used by the assistant and sidebar.
chat-user-avatar.tsxFallback user avatar; pass userAvatar to show your own profile image.
types.tsPublic request, response, thread, message, and attachment types.
utils.tsLocal Tailwind class merging helper; no shared lib/utils file is required.
Connect your chat handler
Import the component and pass an onSend function. It receives the message, selected File objects, and an AbortSignal. Keep provider credentials and model calls in your server endpoint.
import { AiChatBlock } from '@/components/ai-chat-block'import type { AiChatBlockProps } from '@/components/ai-chat-block' const onSend: AiChatBlockProps['onSend'] = async ({ message, attachments, signal,}) => { const body = new FormData() body.set('message', message) attachments.forEach(({ file }) => body.append('files', file)) const response = await fetch('/api/chat', { method: 'POST', body, signal, }) if (!response.ok) throw new Error('The chat request failed.') const result = (await response.json()) as { answer: string } return result.answer} export function SupportChat() { return ( <div className="mx-auto w-full max-w-5xl"> <AiChatBlock title="Support assistant" assistantName="ISTMX AI" userName="You" accentColor="#0369a1" maxResponseCharacters={40_000} onSend={onSend} /> </div> )}Return a live response
Return an async iterable of text chunks to stream a response. The block renders text as it arrives and passes the same abort signal through so Stop can cancel your provider request.
import type { AiChatBlockProps } from '@/components/ai-chat-block' async function* decodeTextStream( body: ReadableStream<Uint8Array>,): AsyncGenerator<string> { const reader = body.getReader() const decoder = new TextDecoder() try { while (true) { const { done, value } = await reader.read() if (done) break const text = decoder.decode(value, { stream: true }) if (text) yield text } const remaining = decoder.decode() if (remaining) yield remaining } finally { reader.releaseLock() }} const onSend: AiChatBlockProps['onSend'] = async ({ message, attachments, signal }) => { const body = new FormData() body.set('message', message) attachments.forEach(({ file }) => body.append('files', file)) const response = await fetch('/api/chat/stream', { method: 'POST', body, signal }) if (!response.ok) throw new Error('The chat request failed.') return response.body ? decodeTextStream(response.body) : response.text()}Adapt your SDK’s chunk shape to strings or ChatStreamChunk objects before yielding. For a short visible activity status, yield a thinking-delta chunk; do not forward private model reasoning.
Configure attachments
Use the default image, document, audio, and video options, or pass your own list to control accepted file types and selection behavior.
import { AiChatBlock, DEFAULT_ATTACHMENT_OPTIONS,} from '@/components/ai-chat-block'import type { AiChatBlockProps, AttachmentOption } from '@/components/ai-chat-block' const attachmentOptions: AttachmentOption[] = DEFAULT_ATTACHMENT_OPTIONS.filter((option) => option.id !== 'video') export function SupportChat({ onSend }: { onSend: AiChatBlockProps['onSend'] }) { return ( <AiChatBlock attachmentOptions={attachmentOptions} maxAttachments={5} maxFileSizeBytes={10 * 1024 * 1024} onSend={onSend} /> )}How it works
The onSend callback receives the message, selected attachments, and an AbortSignal. It can return a complete string or an async iterable of text chunks. Yield thinking-delta or reasoning-delta chunks only when you have a short, user-facing activity update to show.
Composition
AiChatBlock├── ChatSidebar│ ├── Pinned conversations│ ├── Recent conversations│ └── Account profile├── MessageList│ └── ChatMessage│ ├── ThinkingDisclosure│ ├── MessageContent│ └── AttachmentPreviews└── ChatComposer ├── AttachmentDialog ├── VoiceButton └── SendButton / StopAPI reference
onSend(request: ChatRequest) => Promise<ChatResponse>Required. Connect your model or API and return a string or async text stream.
initialMessages?readonly ChatTurn[]Messages for the first local conversation.
initialThreads?readonly ChatThread[]Optional local chat history to show in the sidebar.
title?stringHeading displayed in the chat header.
description?stringShort context shown beneath the chat heading.
assistantName? / userName?stringNames displayed alongside each message and in the account area.
assistantAvatar? / userAvatar?ReactNodeOptional custom avatar content for assistant and user messages.
attachmentOptions?readonly AttachmentOption[]File types and selection behavior offered by the plus menu.
maxAttachments?numberMaximum files per message. Defaults to 10.
maxFileSizeBytes?numberOptional per-file size limit.
maxResponseCharacters?numberMaximum streamed response characters. Defaults to 60,000.
accentColor?stringCSS color used for focus and active accents.
allowVoiceInput?booleanShows the optional voice input control.
placeholder? / emptyMessage?stringComposer placeholder and empty conversation guidance.
onListeningChange?(listening: boolean) => voidNotifies the host when voice input starts or stops.
className?stringAdditional classes for sizing and layout.
The listed props reflect the current block interface.
Accessibility and limitations
Uses native controls and labels, supports keyboard operation, exposes streaming status, respects reduced motion, and keeps attachment and chat-history actions available without hover on touch screens.
Conversation history is held in component state and is cleared when the page reloads. Connect a server-side store to persist conversations. Model credentials and provider calls belong in your application handler, not in the browser-facing component.
