@fauzitech/ai-ui
v0.1.0
Published
Lightweight, headless-friendly React components for AI chat interfaces — streaming markdown, code blocks with copy, tool-call cards, citations, and a typing indicator. Zero heavy dependencies.
Downloads
162
Maintainers
Readme
@fauzitech/ai-ui
Lightweight React components for building AI chat interfaces — streaming markdown, code blocks with copy, tool-call cards, citations, and a typing indicator. No heavy dependencies, no react-markdown + remark + rehype stack weighing down your bundle.
Why
Most AI chat UIs reach for react-markdown and a chain of remark/rehype plugins. That works, but it pulls in a lot of weight and — critically — those parsers assume complete markdown. When you render LLM output token-by-token, you constantly hand them incomplete markdown: an unclosed code fence, a dangling **, a half-written link. They either throw or flicker.
ai-ui ships a tiny streaming-safe markdown parser built for exactly this. Feed it the accumulated text on every token and it never throws — unclosed constructs render gracefully (an open code fence stays an open code block; a dangling ** stays literal text). On top of that sit the components you actually need for a chat UI.
- Streaming-first — render partial markdown on every token without flicker or crashes
- Light — zero runtime dependencies beyond React; ESM + CJS
- Unstyled or styled — components emit clean class names; ship your own CSS or drop in the optional stylesheet
- Dark mode — the optional stylesheet respects
prefers-color-scheme - TypeScript — full types included
Install
npm install @fauzitech/ai-uireact and react-dom (>=18) are peer dependencies.
Quick start
import { Markdown, Message, TypingIndicator } from '@fauzitech/ai-ui';
import '@fauzitech/ai-ui/styles.css'; // optional baseline theme
function Chat({ messages, streaming }) {
return (
<div>
{messages.map((m) => (
<Message key={m.id} role={m.role} content={m.content} />
))}
{streaming && <Message role="assistant" content="" pending />}
</div>
);
}Streaming markdown
The whole point. Pass the accumulated text on every update — partial markdown renders cleanly.
import { Markdown } from '@fauzitech/ai-ui';
function StreamedAnswer({ text }: { text: string }) {
// `text` grows token-by-token. Markdown handles incomplete input safely.
return <Markdown>{text}</Markdown>;
}// Example with the Vercel AI SDK
import { useChat } from 'ai/react';
import { Message } from '@fauzitech/ai-ui';
export function ChatBox() {
const { messages } = useChat();
return messages.map((m) => (
<Message key={m.id} role={m.role} content={m.content} />
));
}Components
<Markdown>
Renders markdown text to React elements. Streaming-safe.
<Markdown className="my-prose">{text}</Markdown>Supported: headings, paragraphs, fenced code blocks, inline code, bold, italic, links, ordered/unordered lists, blockquotes, horizontal rules. Code fences render via <CodeBlock> with a copy button.
| Prop | Type | Description |
| --- | --- | --- |
| children | string | Markdown text (accumulated, can be partial) |
| className | string? | Extra class on the wrapper |
<Message>
A single chat turn.
<Message role="assistant" content="Hello **world**" avatar={<img src="/bot.png" />} />
<Message role="assistant" content="" pending /> {/* shows typing indicator */}| Prop | Type | Description |
| --- | --- | --- |
| role | 'user' \| 'assistant' \| 'system' | Turn author |
| content | string | Markdown content |
| pending | boolean? | Show typing indicator instead of content |
| avatar | ReactNode? | Optional avatar slot |
| className | string? | Extra class |
<CodeBlock>
Code block with language label and copy-to-clipboard button.
<CodeBlock code={"const x = 1;"} lang="js" />| Prop | Type | Description |
| --- | --- | --- |
| code | string | Code text |
| lang | string? | Language label |
| showCopy | boolean? | Show copy button (default true) |
<ToolCall>
Collapsible card for an agent tool/function call — name, status, arguments, and result.
<ToolCall
name="search_database"
status="success"
args={{ query: 'active users' }}
result={{ rows: 42 }}
/>| Prop | Type | Description |
| --- | --- | --- |
| name | string | Tool name |
| status | 'pending' \| 'running' \| 'success' \| 'error' | Call status (default success) |
| args | unknown? | Arguments (stringified as JSON) |
| result | unknown? | Result (stringified as JSON) |
| defaultOpen | boolean? | Start expanded (default false) |
<Citations>
A list of source citations with index badges.
<Citations citations={[
{ title: 'Next.js Docs', url: 'https://nextjs.org/docs', snippet: 'App Router...' },
]} /><TypingIndicator>
Three-dot bouncing indicator.
<TypingIndicator />Theming
Components emit aiui-* class names and never inline styles. Two options:
- Bring your own CSS — target the class names directly.
- Use the baseline — import
@fauzitech/ai-ui/styles.cssand override CSS custom properties:
.aiui-markdown {
--aiui-accent: #ec4899;
--aiui-radius: 12px;
}The baseline respects prefers-color-scheme: dark out of the box.
Advanced: the parser
The streaming-safe parser is exported directly if you want to build your own renderer:
import { parseMarkdown, parseInline, type Block, type Inline } from '@fauzitech/ai-ui';
const blocks = parseMarkdown('# Hi\n\nSome **bold** text');
// → [{ type: 'heading', level: 1, children: [...] }, { type: 'paragraph', ... }]It's also available as a standalone subpath with no React dependency:
import { parseMarkdown } from '@fauzitech/ai-ui/markdown';Open code fences report closed: false so you can render a "still streaming" state.
License
MIT © Muhammad Fauzi Azhar
