A2UI-native dynamic canvas component for React — interoperable with LangChain, 3dchat, Microsoft Agent Framework, and any A2UI-speaking client
npm install @atlas.agents/a2ui-canvasA2UI-native dynamic canvas component for React. Renders streaming, agent-driven visuals from any A2UI-speaking source — LangChain agents, Microsoft Agent Framework, or direct JSON.
- A2UI Protocol: Native support for the Agent-to-UI message protocol (createSurface, updateDataModel, updateComponents, etc.)
- 9 Renderers: Chart, Timeline, KnowledgeGraph, Map, Media, Document, Code, Artifact, Custom
- Streaming: JSONL, SSE, WebSocket, and direct message ingestion with batched updates
- Lazy Loading: Heavy renderers (D3, Cesium, shiki) loaded on demand with SVG fallbacks
- Schema-Only Export: Zero-React-dependency ./schema entry point for server-side / non-JS consumers
- Multi-Entry Build: Tree-shake individual renderers via ./renderers/chart, ./renderers/map, etc.
- Theme System: Built-in light/dark themes with full customization
- Type Safe: Full TypeScript support with discriminated union component types
- Backward Compatible: Legacy CanvasProvider, useCanvas, useCanvasContent hooks still work
``bash`
npm install @atlas.agents/a2ui-canvas
Install only the renderers you need:
`bashCharts (D3)
npm install d3
All renderers include built-in SVG/HTML fallbacks when their optional deps are not installed.
Quick Start
$3
Feed A2UI JSON messages from your AI agent and the canvas renders automatically:
`tsx
import { useA2UISurface, RendererResolver } from '@atlas.agents/a2ui-canvas';function AgentCanvas() {
const { activeSurface, getComponents, resolveData, store } = useA2UISurface({
sseUrl: '/api/agent/stream',
});
if (!activeSurface) return
Waiting for agent...; return (
{getComponents(activeSurface.id).map((component) => (
key={component.id}
component={component}
data={resolveData(activeSurface.id, '/')}
theme="dark"
/>
))}
);
}
`$3
Process A2UI messages directly without a stream:
`tsx
import { useA2UISurface } from '@atlas.agents/a2ui-canvas';const messages = [
{ createSurface: { surfaceId: 's1' } },
{
updateDataModel: {
surfaceId: 's1',
path: '/chart',
value: { labels: ['Jan', 'Feb', 'Mar'], values: [10, 25, 18] },
},
},
{
updateComponents: {
surfaceId: 's1',
components: [
{
id: 'c1',
component: 'Chart',
chartType: 'bar',
data: '/chart',
},
],
},
},
];
function App() {
const { activeSurface, getComponents } = useA2UISurface({ messages });
// ...render components
}
`$3
`tsx
// WebSocket
const result = useA2UISurface({ wsUrl: 'wss://agent.example.com/stream' });// Fetch (JSONL body)
const result = useA2UISurface({
streamUrl: '/api/agent/generate',
streamInit: { method: 'POST', body: JSON.stringify({ prompt: '...' }) },
});
`$3
The original
CanvasProvider / useCanvas API still works:`tsx
import { CanvasProvider, useCanvas, themes } from '@atlas.agents/a2ui-canvas';function App() {
return (
);
}
function MyApp() {
const { content, setContent } = useCanvas();
// ...
}
`A2UI Protocol
The library speaks the A2UI (Agent-to-UI) protocol. Agents send JSON messages to create surfaces, populate data models, and attach components:
$3
| Message | Description |
|---------|-------------|
|
createSurface | Create a new UI surface (canvas) |
| destroySurface | Remove a surface |
| updateDataModel | Set data at a JSON Pointer path |
| updateComponents | Set or replace components on a surface |
| removeComponents | Remove components by ID |
| appendData | Append items to an array (streaming convenience) |
| patchDataModel | JSON merge-patch the data model |$3
| Component | Description | Optional Dep |
|-----------|-------------|-------------|
|
Chart | Bar, line, scatter, pie, area, donut charts | d3 |
| Timeline | Events with alternating/grouped layouts | — |
| KnowledgeGraph | Force-directed node-edge graph | react-force-graph-2d |
| Map | Globe with markers | @cesium/engine + resium |
| Media | Image, video, audio | — |
| Document | Markdown/HTML documents | react-markdown |
| Code | Syntax-highlighted code blocks | shiki |
| Artifact | Sandboxed iframe (HTML/CSS/JS) | — |
| Custom | Registry-key based escape hatch | — |$3
`jsonl
{"createSurface":{"surfaceId":"s1","title":"Sales Dashboard"}}
{"updateDataModel":{"surfaceId":"s1","path":"/sales","value":{"labels":["Q1","Q2","Q3","Q4"],"values":[120,340,250,410]}}}
{"updateComponents":{"surfaceId":"s1","components":[{"id":"chart1","component":"Chart","chartType":"bar","data":"/sales","title":"Quarterly Sales"}]}}
`$3
`jsonl
{"createSurface":{"surfaceId":"s1"}}
{"updateDataModel":{"surfaceId":"s1","path":"/events","value":[{"date":"2024-01-15","title":"Project Started","description":"Kickoff meeting"},{"date":"2024-06-01","title":"v1.0 Release","description":"First public release"}]}}
{"updateComponents":{"surfaceId":"s1","components":[{"id":"t1","component":"Timeline","events":"/events","layout":"alternating"}]}}
`Schema-Only Import
For server-side validation or non-React consumers (Python, C#, etc.), import types and validation without React:
`ts
import {
validateMessage,
validateComponent,
isA2UIMessage,
DYNAMIC_CANVAS_CATALOG,
type A2UIMessage,
type A2UIComponent,
} from '@atlas.agents/a2ui-canvas/schema';const msg = { createSurface: { surfaceId: 's1' } };
const result = validateMessage(msg);
// { valid: true, messageType: 'createSurface' }
`API Reference
$3
####
useA2UISurface(options?)Primary hook — connects to a stream and manages surfaces.
`ts
interface UseA2UISurfaceOptions {
sseUrl?: string; // SSE endpoint
wsUrl?: string; // WebSocket endpoint
streamUrl?: string; // Fetch stream URL
streamInit?: RequestInit; // Fetch init options
messages?: A2UIMessage[]; // Direct messages
batchWindow?: number; // Batch window in ms (default: 50)
onError?: (error: Error) => void;
store?: StoreApi; // Shared store
}
`Returns:
surfaces, activeSurface, setActiveSurface, processMessage, processMessages, resolveData, getComponents, store####
useStreamingContent(options)Subscribe to a single surface's streaming updates.
`ts
const { surface, components, dataModel, resolveData, updatedAt } = useStreamingContent({
store, // from useA2UISurface
surfaceId, // surface to watch
});
`$3
Tree-shake by importing only what you need:
`ts
import ChartRenderer from '@atlas.agents/a2ui-canvas/renderers/chart';
import TimelineRenderer from '@atlas.agents/a2ui-canvas/renderers/timeline';
import KnowledgeGraphRenderer from '@atlas.agents/a2ui-canvas/renderers/knowledge-graph';
import MapRenderer from '@atlas.agents/a2ui-canvas/renderers/map';
import MediaRenderer from '@atlas.agents/a2ui-canvas/renderers/media';
import DocumentRenderer from '@atlas.agents/a2ui-canvas/renderers/document';
import CodeRenderer from '@atlas.agents/a2ui-canvas/renderers/code';
import ArtifactRenderer from '@atlas.agents/a2ui-canvas/renderers/artifact';
`$3
Auto-detect content type from raw text and convert to A2UI messages:
`ts
import { ContentAnalyzer } from '@atlas.agents/a2ui-canvas';// Detect content type
ContentAnalyzer.detectType('
`js\nconsole.log("hi")\n`'); // 'code'
ContentAnalyzer.detectType('# Hello World'); // 'document'
ContentAnalyzer.detectType('2024-01-01 Project Start'); // 'timeline'// Convert to A2UI messages
const messages = ContentAnalyzer.toA2UIMessages('
`python\nprint("hello")\n`');
// Returns: [createSurface, updateComponents with Code component]
`Theming
`tsx
import { themes } from '@atlas.agents/a2ui-canvas';// Built-in themes
themes.light
themes.dark
themes.default
// Custom theme
const myTheme = {
colors: {
background: '#0a0a0a',
surface: '#1a1a2e',
primary: '#0ea5e9',
secondary: '#6366f1',
text: '#e2e8f0',
muted: '#64748b',
border: '#334155',
highlight: '#1e293b',
},
// ...spacing, typography, borderRadius, shadows
};
`Custom Renderers
Register custom renderers by key:
`tsx
import { ComponentRegistry } from '@atlas.agents/a2ui-canvas';const registry = new ComponentRegistry();
registry.register('MyWidget', () => import('./MyWidget'));
// Then in A2UI messages:
{
updateComponents: {
surfaceId: 's1',
components: [{ id: 'w1', component: 'Custom', rendererKey: 'MyWidget', props: { foo: 'bar' } }]
}
}
`Architecture
`
AI Agent → A2UI JSON → StreamProcessor → SurfaceManager → RendererResolver → React Renderers
(JSONL/SSE/WS) (zustand store) (Suspense + ErrorBoundary)
↓
Chart | Timeline | Graph | Map | Media | Doc | Code | Artifact | Custom
`- StreamProcessor: Parses JSONL, SSE, WebSocket, or direct JSON arrays
- SurfaceManager: Zustand store managing surfaces, data models, and components
- RendererResolver: Suspense + ErrorBoundary wrapper for lazy-loaded renderers
- ComponentRegistry: Maps component type strings to React renderer factories
Bundle Size
| Entry | Gzip |
|-------|------|
| Schema only (
./schema) | ~2 KB |
| Core + all renderers (.) | ~8 KB |
| Individual renderers | 0.4–2.5 KB each |Heavy dependencies (D3, Cesium, shiki) are loaded on demand when a component requires them.
Development
`bash
npm install
npm run dev # Start Vite dev server
npm run build # Build library (ES + CJS)
npm run type-check # TypeScript check
npm test # Run tests (124 tests)
npm run test:watch # Watch mode
``MIT