ChatBox
Slideover chat widget for AI conversations and messaging
ChatBox
The ChatBox widget provides a slideover chat interface for AI conversations, customer support, or team messaging. It slides in from the side of the screen and maintains conversation context across sessions.
Type Signature
interface ChatBoxProps {
/** Chat messages */
messages: Array<{
id: string
role: 'user' | 'assistant' | 'system'
content: string
timestamp?: string
avatar?: string
}>
/** Message send handler */
onSend?: (message: string) => void
/** Input placeholder */
placeholder?: string
/** Enable file uploads */
allowUploads?: boolean
/** Show timestamps */
showTimestamps?: boolean
/** Streaming response */
streaming?: boolean
/** Loading state */
loading?: boolean
/** Chat title */
title?: string
/** Position (left or right) */
position?: 'left' | 'right'
}Usage
<ChatBox
title="AI Assistant"
messages={[
{
id: '1',
role: 'assistant',
content: 'Hello! How can I help you today?',
timestamp: '2024-01-15T10:00:00Z',
},
{
id: '2',
role: 'user',
content: 'I need help with my API key',
timestamp: '2024-01-15T10:01:00Z',
},
{
id: '3',
role: 'assistant',
content: 'I can help with that. What would you like to know about API keys?',
timestamp: '2024-01-15T10:01:30Z',
},
]}
onSend={(message) => {
sendMessage(message)
}}
placeholder="Type a message..."
showTimestamps={true}
/>Props
messages
Array of chat messages to display. Each message has a role (user, assistant, system), content, and optional timestamp.
<ChatBox
messages={[
{
id: '1',
role: 'system',
content: 'This conversation is encrypted end-to-end',
},
{
id: '2',
role: 'assistant',
content: 'Welcome! How can I assist you?',
timestamp: '2024-01-15T10:00:00Z',
},
{
id: '3',
role: 'user',
content: 'I have a question about billing',
timestamp: '2024-01-15T10:01:00Z',
avatar: 'https://...',
},
]}
/>onSend
Callback when user sends a message.
<ChatBox
messages={messages}
onSend={(message) => {
// Add user message
setMessages([...messages, {
id: Date.now().toString(),
role: 'user',
content: message,
timestamp: new Date().toISOString(),
}])
// Send to AI/backend
sendToAI(message)
}}
/>placeholder
Placeholder text in the message input. Defaults to "Type a message...".
<ChatBox placeholder="Ask me anything..." />allowUploads
Enable file attachment uploads.
<ChatBox
allowUploads={true}
onFileUpload={(file) => {
uploadFile(file)
}}
/>showTimestamps
Display message timestamps. Defaults to true.
<ChatBox
showTimestamps={true}
messages={messages}
/>streaming
Enable streaming responses for AI messages.
<ChatBox
streaming={true}
messages={messages}
onStream={(chunk) => {
// Update streaming message
updateStreamingMessage(chunk)
}}
/>title
Chat widget title displayed in header.
<ChatBox title="Customer Support" messages={messages} />position
Widget position on screen. Defaults to 'right'.
// Slide from right (default)
<ChatBox position="right" messages={messages} />
// Slide from left
<ChatBox position="left" messages={messages} />Widget Context Usage
ChatBox is typically used as a slideover widget:
Slideover Context
Slide in from the side:
function App() {
const [chatOpen, setChatOpen] = useState(false)
const [messages, setMessages] = useState([])
return (
<>
<Shell>
<DashboardView>
<Button onClick={() => setChatOpen(true)}>
Open Chat
</Button>
</DashboardView>
</Shell>
<Slideover
open={chatOpen}
onClose={() => setChatOpen(false)}
position="right"
>
<ChatBox
title="AI Assistant"
messages={messages}
onSend={async (message) => {
// Add user message
const userMsg = {
id: Date.now().toString(),
role: 'user',
content: message,
timestamp: new Date().toISOString(),
}
setMessages([...messages, userMsg])
// Get AI response
const response = await getAIResponse(message)
setMessages((prev) => [
...prev,
{
id: (Date.now() + 1).toString(),
role: 'assistant',
content: response,
timestamp: new Date().toISOString(),
},
])
}}
/>
</Slideover>
</>
)
}Popup Context
Small popup chat window:
<Popup
trigger={
<Button size="icon">
<MessageSquare />
</Button>
}
position="bottom-right"
>
<ChatBox
title="Quick Chat"
messages={messages}
placeholder="Quick question?"
/>
</Popup>Inline Context
Embedded in page:
<div className="grid grid-cols-3 gap-4">
<div className="col-span-2">
<MainContent />
</div>
<div className="col-span-1">
<ChatBox
title="Help"
messages={messages}
/>
</div>
</div>Common Patterns
AI Assistant Chat
function AIAssistantChat() {
const [messages, setMessages] = useState([
{
id: '1',
role: 'assistant',
content: 'Hello! I\'m your AI assistant. How can I help you today?',
},
])
const handleSend = async (message: string) => {
// Add user message
const userMsg = {
id: Date.now().toString(),
role: 'user',
content: message,
timestamp: new Date().toISOString(),
}
setMessages((prev) => [...prev, userMsg])
// Get AI response
const response = await fetch('/api/chat', {
method: 'POST',
body: JSON.stringify({ message }),
}).then((r) => r.json())
// Add assistant response
setMessages((prev) => [
...prev,
{
id: (Date.now() + 1).toString(),
role: 'assistant',
content: response.message,
timestamp: new Date().toISOString(),
},
])
}
return (
<ChatBox
title="AI Assistant"
messages={messages}
onSend={handleSend}
placeholder="Ask me anything..."
/>
)
}Customer Support Chat
function SupportChat() {
const [messages, setMessages] = useState([])
const [agent, setAgent] = useState(null)
useEffect(() => {
// Connect to support chat
const connectSupport = async () => {
const connection = await initiateChat()
setAgent(connection.agent)
connection.onMessage((msg) => {
setMessages((prev) => [...prev, msg])
})
}
connectSupport()
}, [])
return (
<ChatBox
title={agent ? `Chat with ${agent.name}` : 'Connecting...'}
messages={messages}
onSend={(message) => {
sendToAgent(message)
}}
placeholder="Type your message..."
showTimestamps={true}
/>
)
}Team Messaging
function TeamChat({ channelId }) {
const [messages, setMessages] = useState([])
useEffect(() => {
// Subscribe to channel messages
const unsubscribe = subscribeToChannel(channelId, (message) => {
setMessages((prev) => [...prev, message])
})
return () => unsubscribe()
}, [channelId])
return (
<ChatBox
title="#general"
messages={messages}
onSend={(message) => {
sendToChannel(channelId, message)
}}
showTimestamps={true}
/>
)
}Code Assistant Chat
function CodeAssistantChat() {
const [messages, setMessages] = useState([])
return (
<ChatBox
title="Code Assistant"
messages={messages}
onSend={async (message) => {
// Send to code AI
const response = await getCodeCompletion(message)
setMessages((prev) => [
...prev,
{
id: Date.now().toString(),
role: 'user',
content: message,
},
{
id: (Date.now() + 1).toString(),
role: 'assistant',
content: response.code,
metadata: {
language: response.language,
explanation: response.explanation,
},
},
])
}}
renderMessage={(message) => {
if (message.metadata?.language) {
return (
<div>
<CodeBlock
language={message.metadata.language}
code={message.content}
/>
<p className="mt-2 text-sm">{message.metadata.explanation}</p>
</div>
)
}
return <p>{message.content}</p>
}}
/>
)
}Streaming Responses
Stream AI responses in real-time:
function StreamingChat() {
const [messages, setMessages] = useState([])
const [streaming, setStreaming] = useState(false)
const handleSend = async (message: string) => {
// Add user message
setMessages((prev) => [
...prev,
{ id: Date.now().toString(), role: 'user', content: message },
])
// Start streaming response
setStreaming(true)
const streamId = (Date.now() + 1).toString()
setMessages((prev) => [
...prev,
{ id: streamId, role: 'assistant', content: '' },
])
const response = await fetch('/api/chat/stream', {
method: 'POST',
body: JSON.stringify({ message }),
})
const reader = response.body.getReader()
const decoder = new TextDecoder()
while (true) {
const { done, value } = await reader.read()
if (done) break
const chunk = decoder.decode(value)
setMessages((prev) =>
prev.map((m) =>
m.id === streamId ? { ...m, content: m.content + chunk } : m
)
)
}
setStreaming(false)
}
return (
<ChatBox
messages={messages}
onSend={handleSend}
streaming={streaming}
/>
)
}Message Formatting
Support rich message formatting:
function FormattedChat() {
const renderMessage = (message) => {
// Parse markdown
if (message.content.includes('```')) {
return <MarkdownRenderer content={message.content} />
}
// Parse links
const withLinks = message.content.replace(
/(https?:\/\/[^\s]+)/g,
'<a href="$1" target="_blank">$1</a>'
)
return <div dangerouslySetInnerHTML={{ __html: withLinks }} />
}
return (
<ChatBox
messages={messages}
renderMessage={renderMessage}
/>
)
}File Uploads
Support file attachments:
function ChatWithUploads() {
const [messages, setMessages] = useState([])
const handleFileUpload = async (file: File) => {
// Upload file
const url = await uploadFile(file)
// Add file message
setMessages((prev) => [
...prev,
{
id: Date.now().toString(),
role: 'user',
content: `Uploaded: ${file.name}`,
metadata: {
type: 'file',
url,
filename: file.name,
size: file.size,
},
},
])
}
return (
<ChatBox
messages={messages}
allowUploads={true}
onFileUpload={handleFileUpload}
renderMessage={(message) => {
if (message.metadata?.type === 'file') {
return (
<div className="flex items-center gap-2">
<FileIcon />
<a href={message.metadata.url} target="_blank">
{message.metadata.filename}
</a>
<span className="text-sm text-muted-foreground">
({formatBytes(message.metadata.size)})
</span>
</div>
)
}
return <p>{message.content}</p>
}}
/>
)
}Typing Indicators
Show when someone is typing:
function ChatWithTyping() {
const [messages, setMessages] = useState([])
const [isTyping, setIsTyping] = useState(false)
return (
<ChatBox
messages={messages}
footer={
isTyping ? (
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<div className="flex gap-1">
<span className="animate-bounce">•</span>
<span className="animate-bounce delay-100">•</span>
<span className="animate-bounce delay-200">•</span>
</div>
<span>Assistant is typing...</span>
</div>
) : null
}
/>
)
}Message Reactions
Add emoji reactions to messages:
function ChatWithReactions() {
const [messages, setMessages] = useState([])
const addReaction = (messageId: string, emoji: string) => {
setMessages((prev) =>
prev.map((m) => {
if (m.id === messageId) {
const reactions = m.reactions || {}
reactions[emoji] = (reactions[emoji] || 0) + 1
return { ...m, reactions }
}
return m
})
)
}
return (
<ChatBox
messages={messages}
renderMessage={(message) => (
<div>
<p>{message.content}</p>
{message.reactions && (
<div className="flex gap-2 mt-2">
{Object.entries(message.reactions).map(([emoji, count]) => (
<button
key={emoji}
className="text-xs px-2 py-1 rounded bg-muted"
onClick={() => addReaction(message.id, emoji)}
>
{emoji} {count}
</button>
))}
</div>
)}
</div>
)}
/>
)
}Persistence
Save chat history:
function PersistentChat() {
const [messages, setMessages] = useState(() => {
// Load from localStorage
const saved = localStorage.getItem('chat-messages')
return saved ? JSON.parse(saved) : []
})
useEffect(() => {
// Save to localStorage
localStorage.setItem('chat-messages', JSON.stringify(messages))
}, [messages])
return (
<ChatBox
messages={messages}
onSend={(message) => {
setMessages((prev) => [
...prev,
{
id: Date.now().toString(),
role: 'user',
content: message,
timestamp: new Date().toISOString(),
},
])
}}
/>
)
}Accessibility
ChatBox implements accessibility best practices:
- ARIA labels - Descriptive labels for screen readers
- Keyboard navigation - Full keyboard support
- Focus management - Proper focus handling
- Live regions - New messages announced to screen readers
<aside role="complementary" aria-label="Chat">
<div role="log" aria-live="polite" aria-atomic="false">
<div role="article" aria-label="Message from Assistant">
<p>Hello! How can I help?</p>
<time datetime="2024-01-15T10:00:00Z">10:00 AM</time>
</div>
</div>
<form role="search" aria-label="Send message">
<input type="text" aria-label="Message" />
<button type="submit">Send</button>
</form>
</aside>Best Practices
Limit Message History
Keep message list performant:
// Good - show recent messages
<ChatBox messages={messages.slice(-50)} />
// Bad - showing all messages can slow down
<ChatBox messages={allMessages} /> // 1000+ messagesShow Loading States
Indicate when waiting for response:
<ChatBox
messages={messages}
loading={isWaitingForResponse}
placeholder={
isWaitingForResponse ? 'Waiting for response...' : 'Type a message...'
}
/>Handle Errors Gracefully
Show error messages clearly:
const handleSend = async (message: string) => {
try {
await sendMessage(message)
} catch (error) {
setMessages((prev) => [
...prev,
{
id: Date.now().toString(),
role: 'system',
content: 'Failed to send message. Please try again.',
},
])
}
}Auto-Scroll to Bottom
Keep latest messages visible:
const messagesEndRef = useRef(null)
useEffect(() => {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' })
}, [messages])Related Components
- DetailPanel - Detail side panel component
- ActivityPanel - Activity feed component
- SearchBox - Modal search widget
- CommandPalette - Keyboard commands widget