Getting Started
Components
- Agent Status
- Agent Template Card
- API Key Card
- Audio Player
- Bar Visualizer
- Channel Status Card
- Chat Composer
- Code Editor
- Condition Builder
- Confirm Dialog
- Conversation
- Conversation Bar
- Dataset Card
- Document Status Badge
- Extraction Strategy Select
- File Icon
- File Upload
- Indexing Progress
- Live Waveform
- Matrix
- Message
- Mic Selector
- Model Diff View
- Model Selector
- Model Type Filter
- Node Catalog
- Option Editor
- Orb
- Output Variables View
- Package Card
- Prompt Editor
- Provider Status Card
- Radio Card
- Resource Card
- Resource Sidebar
- Response
- Run Node List
- Scrub Bar
- Segment Card
- Shimmering Text
- Speech Input
- Token Trend Chart
- Tool Call Card
- Transcript Viewer
- Usage Stat Card
- Variable Binding Editor
- Variable Selector
- Voice Button
- Voice Picker
- Waveform
- Workflow Node Card
- Workspace Selector
Start a conversation
This is a simulated conversation
"use client"
import { useEffect, useState } from "react"
import { Card } from "@/components/ui/card"
import {
Conversation,
ConversationContent,
ConversationEmptyState,
ConversationScrollButton,
} from "@/components/ui/conversation"
import { Message, MessageContent } from "@/components/ui/message"
import { Response } from "@/components/ui/response"
const allMessages = [
{
id: "1",
role: "user" as const,
parts: [
{
type: "text",
text: "Can ZGI review this voice-agent brief?",
},
],
},
{
id: "2",
role: "assistant" as const,
parts: [
{
type: "text",
tokens: [
"Yes.",
" Share",
" the",
" brief",
" or",
" paste",
" the",
" workflow",
" goal,",
" and",
" I",
" will",
" identify",
" the",
" key",
" states.",
],
text: "Yes. Share the brief or paste the workflow goal, and I will identify the key states.",
},
],
},
{
id: "3",
role: "user" as const,
parts: [
{
type: "text",
text: "The agent should capture customer intent, confirm details, and route follow-ups.",
},
],
},
{
id: "4",
role: "assistant" as const,
parts: [
{
type: "text",
tokens: [
"Got",
" it.",
" The",
" core",
" states",
" are",
" intake,",
" confirmation,",
" routing,",
" and",
" handoff.",
" I",
" would",
" add",
" a",
" review",
" state",
" before",
" any",
" irreversible",
" action.",
],
text: "Got it. The core states are intake, confirmation, routing, and handoff. I would add a review state before any irreversible action.",
},
],
},
{
id: "5",
role: "user" as const,
parts: [
{
type: "text",
text: "Can you turn that into implementation tasks?",
},
],
},
{
id: "6",
role: "assistant" as const,
parts: [
{
type: "text",
tokens: [
"Yes.",
" I",
" would",
" split",
" it",
" into",
" capture,",
" validation,",
" transcript",
" review,",
" routing,",
" and",
" analytics",
" events.",
],
text: "Yes. I would split it into capture, validation, transcript review, routing, and analytics events.",
},
],
},
]
function ZgiMark({ className }: { className?: string }) {
return (
<svg
viewBox="0 0 64 64"
fill="none"
xmlns="http://www.w3.org/2000/svg"
aria-hidden="true"
className={className}
>
<rect width="64" height="64" rx="18" fill="#0052FF" />
<path
d="M20 17h24v6.5l-15.5 21H44V51H19v-6l15.2-21.5H20V17z"
fill="white"
/>
</svg>
)
}
const ConversationDemo = () => {
const [messages, setMessages] = useState<typeof allMessages>([])
const [streamingMessageIndex, setStreamingMessageIndex] = useState<
number | null
>(null)
const [streamingContent, setStreamingContent] = useState("")
useEffect(() => {
const timeouts: NodeJS.Timeout[] = []
const intervals: NodeJS.Timeout[] = []
let currentMessageIndex = 0
const addNextMessage = () => {
if (currentMessageIndex >= allMessages.length) return
const message = allMessages[currentMessageIndex]
const part = message.parts[0]
if (message.role === "assistant" && "tokens" in part && part.tokens) {
setStreamingMessageIndex(currentMessageIndex)
setStreamingContent("")
let currentContent = ""
let tokenIndex = 0
const streamInterval = setInterval(() => {
if (tokenIndex < part.tokens.length) {
currentContent += part.tokens[tokenIndex]
setStreamingContent(currentContent)
tokenIndex++
} else {
clearInterval(streamInterval)
setMessages((prev) => [...prev, message])
setStreamingMessageIndex(null)
setStreamingContent("")
currentMessageIndex++
// Add next message after a delay
timeouts.push(setTimeout(addNextMessage, 500))
}
}, 100)
intervals.push(streamInterval)
} else {
setMessages((prev) => [...prev, message])
currentMessageIndex++
timeouts.push(setTimeout(addNextMessage, 800))
}
}
// Start after 1 second
timeouts.push(setTimeout(addNextMessage, 1000))
return () => {
timeouts.forEach((timeout) => clearTimeout(timeout))
intervals.forEach((interval) => clearInterval(interval))
}
}, [])
return (
<Card className="relative mx-auto my-0 size-full h-[400px] py-0">
<div className="flex h-full flex-col">
<Conversation>
<ConversationContent>
{messages.length === 0 && streamingMessageIndex === null ? (
<ConversationEmptyState
icon={<ZgiMark className="size-12" />}
title="Start a conversation"
description="This is a simulated conversation"
/>
) : (
<>
{messages.map((message) => (
<Message from={message.role} key={message.id}>
<MessageContent>
{message.parts.map((part, i) => {
switch (part.type) {
case "text":
return (
<Response key={`${message.id}-${i}`}>
{part.text}
</Response>
)
default:
return null
}
})}
</MessageContent>
{message.role === "assistant" && (
<div className="ring-border bg-background flex size-8 items-center justify-center overflow-hidden rounded-full ring-1">
<ZgiMark className="size-7" />
</div>
)}
</Message>
))}
{streamingMessageIndex !== null && (
<Message
from={allMessages[streamingMessageIndex].role}
key={`streaming-${streamingMessageIndex}`}
>
<MessageContent>
<Response>{streamingContent || "\u200B"}</Response>
</MessageContent>
{allMessages[streamingMessageIndex].role ===
"assistant" && (
<div className="ring-border bg-background flex size-8 items-center justify-center overflow-hidden rounded-full ring-1">
<ZgiMark className="size-7" />
</div>
)}
</Message>
)}
</>
)}
</ConversationContent>
<ConversationScrollButton />
</Conversation>
</div>
</Card>
)
}
export ConversationDemo
Installation
pnpm dlx shadcn@latest add https://ui.zgi.ai/r/conversation.json
Usage
import {
Conversation,
ConversationContent,
ConversationEmptyState,
ConversationScrollButton,
} from "@/components/ui/conversation"Basic Conversation
<Conversation>
<ConversationContent>
{messages.map((message) => (
<div key={message.id}>{message.content}</div>
))}
</ConversationContent>
<ConversationScrollButton />
</Conversation>With Empty State
<Conversation>
<ConversationContent>
{messages.length === 0 ? (
<ConversationEmptyState
title="No messages yet"
description="Start a conversation to see messages here"
/>
) : (
messages.map((message) => <div key={message.id}>{message.content}</div>)
)}
</ConversationContent>
<ConversationScrollButton />
</Conversation>API Reference
Conversation
The main container component that manages scrolling behavior and sticky-to-bottom functionality.
Props
Extends all props from StickToBottom component from use-stick-to-bottom.
| Prop | Type | Description |
|---|---|---|
| className | string | Optional CSS classes |
| initial | "smooth" | Initial scroll behavior (default: "smooth") |
| resize | "smooth" | Resize scroll behavior (default: "smooth") |
| ...props | StickToBottom | All standard StickToBottom component props |
ConversationContent
Container for conversation messages.
Props
| Prop | Type | Description |
|---|---|---|
| className | string | Optional CSS classes |
| ...props | StickToBottom.Content | All StickToBottom.Content props |
ConversationEmptyState
Displays when there are no messages in the conversation.
Props
| Prop | Type | Description |
|---|---|---|
| title | string | Title text (default: "No messages yet") |
| description | string | Description text |
| icon | ReactNode | Optional icon to display |
| className | string | Optional CSS classes |
| children | ReactNode | Custom content (overrides default rendering) |
| ...props | HTMLDivElement | All standard div element props |
ConversationScrollButton
A scroll-to-bottom button that appears when the user scrolls up.
Props
| Prop | Type | Description |
|---|---|---|
| className | string | Optional CSS classes |
| ...props | ButtonProps | All standard Button props |
Notes
- Built on top of
use-stick-to-bottomfor smooth scrolling behavior - Automatically scrolls to bottom when new messages are added
- Scroll button only appears when user has scrolled away from the bottom
- Supports smooth scrolling animations
- Works with any message component structure
- This component is inspired by Vercel's AI SDK Conversation component with modifications for ZGI UI
Deploy and Scale Agents with ZGI
ZGI delivers the infrastructure and developer experience you need to ship reliable audio & agent applications at scale.
Talk to an expert