Message Parts
Working with rich content parts for multimedia and interactive chat experiences
Message parts are the building blocks for creating rich, interactive chat experiences beyond simple text. They allow you to embed text, files, sources, events, artifacts, and custom content types directly into chat messages as structured components.
Parts System Overview
Chat-UI supports two fundamental types of parts that make up chat messages:
1. Text Parts
Text parts contain markdown content that gets rendered as formatted text. They use the text
type and are the primary way to display textual content.
2. Data Parts
Data parts contain structured data for rich interactive components like weather widgets, file attachments, sources, and more.
Both built-in and custom parts are both using the data-
prefix. We're using here the convention from Vercel AI SDK 5 to use the data-
prefix to detect data parts in messages.
Message Structure with Parts
interface Message {
id: string
role: 'user' | 'assistant' | 'system'
parts: MessagePart[]
}
// Two types of parts
type MessagePart = TextPart | DataPart
// Text parts for markdown content
interface TextPart {
type: 'text'
text: string
}
// Data parts for rich components
interface DataPart {
id?: string // if provided, only the last part with same id is kept
type: string // should use 'data-' prefix for data parts
data?: any
}
How Chat-UI Renders Parts
Parts are automatically rendered when using the ChatMessage.Content
component. Each part type has a corresponding component that checks if the current part matches its type:
<ChatMessage message={message}>
<ChatMessage.Content>
{/* Built-in part components */}
<ChatMessage.Part.Markdown />
<ChatMessage.Part.File />
<ChatMessage.Part.Event />
<ChatMessage.Part.Artifact />
<ChatMessage.Part.Source />
<ChatMessage.Part.Suggestion />
{/* Custom part components */}
<WeatherPart />
<WikiPart />
</ChatMessage.Content>
</ChatMessage>
The rendering system:
- Iterates through each part in
message.parts
- Provides each part to all child components via
ChatPartProvider
- Each component uses
usePart(partType)
to check if it should render - Only the matching component renders, others return
null
Built-in Parts
Chat-UI provides several built-in part types for common use cases:
Text Parts (text
)
Display markdown content with syntax highlighting, links, and formatting.
const textPart = {
type: 'text',
text: `
# Heading
This is **bold** and *italic* text.
\`\`\`javascript
console.log('Hello, world!')
\`\`\`
`
}
File Parts (data-file
)
Display file attachments with download links and metadata.
const filePart = {
type: 'data-file',
data: {
name: 'quarterly-report.pdf',
type: 'application/pdf',
url: '/files/quarterly-report.pdf',
size: 2048576 // bytes
}
}
Source Parts (data-sources
)
Display citations and source references with document grouping.
const sourcesPart = {
type: 'data-sources',
data: {
nodes: [
{
id: 'source1',
url: '/documents/research-paper.pdf',
metadata: {
title: 'Machine Learning in Healthcare',
author: 'Dr. Jane Smith',
page_number: 15,
section: 'Methodology'
}
}
]
}
}
Event Parts (data-event
)
Display process events, function calls, and system activities with status updates.
const eventPart = {
id: 'search_event', // Same ID will update previous event
type: 'data-event',
data: {
title: 'Calling tool `search_database`',
status: 'success',
data: {
query: 'machine learning papers',
result: 'Found 8 relevant papers'
}
}
}
Artifact Parts (data-artifact
)
Create interactive code and document artifacts that users can edit.
const artifactPart = {
type: 'data-artifact',
data: {
type: 'code',
data: {
title: 'Data Analysis Script',
file_name: 'analyze_data.py',
language: 'python',
code: `
import pandas as pd
import matplotlib.pyplot as plt
def analyze_sales_data(file_path):
df = pd.read_csv(file_path)
monthly_sales = df.groupby('month')['sales'].sum()
return monthly_sales
`
}
}
}
Suggestion Parts (data-suggested_questions
)
Provide interactive follow-up questions to guide conversation.
const suggestionPart = {
type: 'data-suggested_questions',
data: [
'Can you explain the methodology in more detail?',
'What are the potential limitations?',
'How does this compare to traditional methods?'
]
}
Creating Custom Parts
Create domain-specific parts for specialized content by implementing a custom render component:
1. Define the Part Type and Data Interface
const WeatherPartType = 'data-weather'
type WeatherData = {
location: string
temperature: number
condition: string
humidity: number
windSpeed: number
}
2. Create the Component
import { usePart } from '@llamaindex/chat-ui'
export function WeatherPart() {
// usePart returns data only if current part matches the type
const weatherData = usePart<WeatherData>(WeatherPartType)
if (!weatherData) return null
return (
<div className="weather-widget">
<h3>{weatherData.location}</h3>
<div className="temperature">{weatherData.temperature}°C</div>
<div className="condition">{weatherData.condition}</div>
<div className="details">
<span>Humidity: {weatherData.humidity}%</span>
<span>Wind: {weatherData.windSpeed} km/h</span>
</div>
</div>
)
}
3. Add to Message Rendering
<ChatMessage message={message}>
<ChatMessage.Content>
<ChatMessage.Part.Markdown />
<ChatMessage.Part.File />
{/* Add your custom component */}
<WeatherPart />
</ChatMessage.Content>
</ChatMessage>
Adding Parts from Backend via SSE Protocol
Parts are streamed using the Server-Sent Events (SSE) protocol, which provides real-time communication between the server and client. Read more about SSE protocol in Vercel AI SDK 5 documentation.
Here's how the streaming implementation works in the backend:
Response Headers
The server must set specific headers for SSE streaming:
return new Response(stream, {
headers: {
'Content-Type': 'text/event-stream',
'Connection': 'keep-alive',
},
})
Stream Format
Each chunk sent to the client must follow the SSE format with a data:
prefix:
const DATA_PREFIX = 'data: '
function writeStream(chunk: TextChunk | DataChunk) {
controller.enqueue(
encoder.encode(`${DATA_PREFIX}${JSON.stringify(chunk)}\n\n`)
)
}
Chunk Types
The streaming protocol supports two types of chunks:
Text Chunks (for streaming text content)
interface TextChunk {
type: 'text-start' | 'text-delta' | 'text-end'
id: string
delta?: string // only for text-delta
}
// Example sequence:
// data: {"type":"text-start","id":"msg-123"}
// data: {"type":"text-delta","id":"msg-123","delta":"Hello "}
// data: {"type":"text-delta","id":"msg-123","delta":"world!"}
// data: {"type":"text-end","id":"msg-123"}
Data Chunks (for rich components)
interface DataChunk {
id?: string // optional - same ID replaces previous parts
type: `data-${string}` // requires 'data-' prefix
data: Record<string, any>
}
// Example:
// data: {"type":"data-weather","data":{"location":"SF","temp":22}}
Implementation Example
const fakeChatStream = (parts: (string | MessagePart)[]): ReadableStream => {
return new ReadableStream({
async start(controller) {
const encoder = new TextEncoder()
function writeStream(chunk: TextChunk | DataChunk) {
controller.enqueue(
encoder.encode(`${DATA_PREFIX}${JSON.stringify(chunk)}\n\n`)
)
}
async function writeText(content: string) {
const messageId = crypto.randomUUID()
// Start text stream
writeStream({ id: messageId, type: 'text-start' })
// Stream tokens
for (const token of content.split(' ')) {
writeStream({
id: messageId,
type: 'text-delta',
delta: token + ' '
})
await new Promise(resolve => setTimeout(resolve, 30))
}
// End text stream
writeStream({ id: messageId, type: 'text-end' })
}
async function writeData(data: MessagePart) {
writeStream({
id: data.id,
type: `data-${data.type}`,
data: data.data
})
}
// Stream all parts
for (const item of parts) {
if (typeof item === 'string') {
await writeText(item)
} else {
await writeData(item)
}
}
controller.close()
},
})
}
Important ID Behavior for Data Parts
When data parts have the same id
, only the last data part with that ID will exist in message.parts
. This is useful for:
- Single data display: Show only the final result (e.g., hide loading, show final weather data)
- Progressive updates: Update the same component as new data arrives (e.g., streaming events)
If you want multiple parts of the same type, don't provide an ID or use different IDs.
Example:
- When calling a tool, send an event with tool call information:
part1 = {
id: 'demo_sample_event_id',
type: 'data-event',
data: {
title: 'Calling tool `get_weather` with input `San Francisco, CA`',
status: 'pending',
},
}
- When the tool call is completed, send an event with the tool call result. The previous event with the same id will be replaced by the new one.
part2 = {
id: 'demo_sample_event_id',
type: 'data-event',
data: {
title: 'Calling tool `get_weather` with input `San Francisco, CA`',
status: 'pending',
},
}
When checking message.parts
, you will only see the last event with the final result.
Important Notes
- SSE Format: Each message must be prefixed with
data:
and end with\n\n
- JSON Encoding: All chunks are JSON-encoded objects
- Text Streaming: Text content requires start/delta/end sequence for proper rendering
- Data Parts: Must use
data-
prefix in the type field - ID Behavior: Same IDs in data parts will replace previous parts with that ID
Complete Message Example
const message = {
id: 'msg-123',
role: 'assistant',
parts: [
{
type: 'text',
text: 'I\'ve analyzed your data and here are the results:'
},
{
type: 'data-artifact',
data: {
type: 'code',
data: {
title: 'Sales Analysis',
file_name: 'analysis.py',
language: 'python',
code: 'import pandas as pd\n# Analysis code...'
}
}
},
{
type: 'data-sources',
data: {
nodes: [
{
id: '1',
url: '/data/sales.csv',
metadata: { title: 'Sales Data Q4 2024' }
}
]
}
},
{
type: 'data-suggested_questions',
data: [
'Can you explain the quarterly trends?',
'What about the seasonal patterns?',
'How can we improve performance?'
]
}
]
}
Utility Functions
usePart Hook
Extract part data by type within part components:
import { usePart } from '@llamaindex/chat-ui'
function CustomPartComponent() {
// Returns data only if current part matches type, null otherwise
const weatherData = usePart<WeatherData>('data-weather')
const textContent = usePart<string>('text')
// Component logic...
}
getParts Function
Extract all parts of a specific type from a message:
import { getParts } from '@llamaindex/chat-ui'
// Get all text content from a message
const allTextParts = getParts<string>(message, 'text')
// Get all weather data parts
const allWeatherData = getParts<WeatherData>(message, 'data-weather')
This function is useful for:
- Aggregating data from multiple parts
- Building summaries or indexes
- Processing historical data
Best Practices
- Use the
data-
prefix for all custom part types - Provide IDs only when you want parts to replace each other
- Keep data structures simple and serializable
- Handle null cases in custom components when data doesn't match
- Mix text and data parts to create rich, contextual experiences
- Stream progressively to improve perceived performance
Next Steps
- Artifacts - Learn about interactive code and document artifacts
- Widgets - Explore widget implementation details
- Examples - See complete implementation examples
- Customization - Style and customize part appearance