| layout | title | parent | nav_order |
|---|---|---|---|
default |
Chapter 2: UI Architecture & Components |
Chatbox Tutorial |
2 |
Welcome to Chapter 2: UI Architecture & Components. In this part of Chatbox Tutorial: Building Modern AI Chat Interfaces, you will build an intuitive mental model first, then move into concrete implementation details and practical production tradeoffs.
This chapter explores the user interface architecture and component design patterns used in modern AI chat applications like Chatbox.
// Main application structure
const ChatApplication = {
layout: {
sidebar: "Navigation and conversation list",
main: {
header: "Conversation title and controls",
messages: "Message display area",
input: "Message composition area"
}
},
components: {
MessageBubble: "Individual message display",
TypingIndicator: "Shows when AI is typing",
MessageInput: "Text input with send button",
ConversationList: "List of chat conversations",
SettingsPanel: "Application settings"
}
}// Application state structure
interface AppState {
conversations: Conversation[]
currentConversation: string | null
messages: Map<string, Message[]>
ui: {
sidebarOpen: boolean
theme: 'light' | 'dark' | 'auto'
fontSize: 'small' | 'medium' | 'large'
}
user: {
preferences: UserPreferences
settings: AppSettings
}
}
class StateManager {
private state: AppState
private listeners: StateChangeListener[] = []
constructor(initialState: AppState) {
this.state = { ...initialState }
}
getState(): AppState {
return { ...this.state }
}
updateState(updater: (state: AppState) => AppState) {
const newState = updater(this.state)
this.state = newState
this.notifyListeners(newState)
}
subscribe(listener: StateChangeListener) {
this.listeners.push(listener)
return () => {
this.listeners = this.listeners.filter(l => l !== listener)
}
}
private notifyListeners(newState: AppState) {
this.listeners.forEach(listener => listener(newState))
}
}// Message bubble component
interface MessageBubbleProps {
message: Message
isUser: boolean
showAvatar: boolean
onEdit?: (messageId: string) => void
onDelete?: (messageId: string) => void
}
const MessageBubble: React.FC<MessageBubbleProps> = ({
message,
isUser,
showAvatar,
onEdit,
onDelete
}) => {
const [isEditing, setIsEditing] = useState(false)
const [editText, setEditText] = useState(message.content)
const handleEdit = () => {
if (isEditing) {
onEdit?.(message.id)
}
setIsEditing(!isEditing)
}
return (
<div className={`message-bubble ${isUser ? 'user' : 'ai'}`}>
{showAvatar && (
<Avatar src={isUser ? userAvatar : aiAvatar} size="small" />
)}
<div className="message-content">
{isEditing ? (
<MessageEditor
value={editText}
onChange={setEditText}
onSave={handleEdit}
onCancel={() => setIsEditing(false)}
/>
) : (
<MessageContent content={message.content} />
)}
</div>
<MessageActions
onEdit={handleEdit}
onDelete={() => onDelete?.(message.id)}
canEdit={isUser && message.canEdit}
/>
</div>
)
}// Advanced message input
interface MessageInputProps {
onSend: (content: string) => void
placeholder?: string
disabled?: boolean
maxLength?: number
showFormatting?: boolean
}
const MessageInput: React.FC<MessageInputProps> = ({
onSend,
placeholder = "Type your message...",
disabled = false,
maxLength = 4000,
showFormatting = true
}) => {
const [message, setMessage] = useState('')
const [isTyping, setIsTyping] = useState(false)
const textareaRef = useRef<HTMLTextAreaElement>(null)
const handleSend = () => {
if (message.trim() && !disabled) {
onSend(message.trim())
setMessage('')
textareaRef.current?.focus()
}
}
const handleKeyPress = (e: KeyboardEvent) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault()
handleSend()
}
}
const adjustTextareaHeight = () => {
const textarea = textareaRef.current
if (textarea) {
textarea.style.height = 'auto'
textarea.style.height = Math.min(textarea.scrollHeight, 200) + 'px'
}
}
useEffect(() => {
adjustTextareaHeight()
}, [message])
return (
<div className="message-input-container">
{showFormatting && (
<FormattingToolbar
onFormat={(format) => applyFormatting(format)}
/>
)}
<div className="input-wrapper">
<textarea
ref={textareaRef}
value={message}
onChange={(e) => setMessage(e.target.value)}
onKeyPress={handleKeyPress}
placeholder={placeholder}
disabled={disabled}
maxLength={maxLength}
rows={1}
className="message-textarea"
/>
<div className="input-actions">
<CharacterCount current={message.length} max={maxLength} />
<SendButton
onClick={handleSend}
disabled={!message.trim() || disabled}
/>
</div>
</div>
</div>
)
}// Conversation management
interface ConversationListProps {
conversations: Conversation[]
currentConversationId: string | null
onSelectConversation: (id: string) => void
onCreateConversation: () => void
onDeleteConversation: (id: string) => void
}
const ConversationList: React.FC<ConversationListProps> = ({
conversations,
currentConversationId,
onSelectConversation,
onCreateConversation,
onDeleteConversation
}) => {
const [searchTerm, setSearchTerm] = useState('')
const filteredConversations = conversations.filter(conv =>
conv.title.toLowerCase().includes(searchTerm.toLowerCase())
)
return (
<div className="conversation-list">
<div className="list-header">
<h3>Conversations</h3>
<Button
icon="plus"
onClick={onCreateConversation}
size="small"
/>
</div>
<SearchInput
value={searchTerm}
onChange={setSearchTerm}
placeholder="Search conversations..."
/>
<div className="conversations">
{filteredConversations.map(conversation => (
<ConversationItem
key={conversation.id}
conversation={conversation}
isActive={conversation.id === currentConversationId}
onClick={() => onSelectConversation(conversation.id)}
onDelete={() => onDeleteConversation(conversation.id)}
/>
))}
</div>
</div>
)
}// Efficiently render large message lists
class VirtualizedMessageList extends React.Component {
constructor(props) {
super(props)
this.state = {
startIndex: 0,
endIndex: 50,
itemHeight: 80
}
this.containerRef = React.createRef()
}
componentDidMount() {
this.updateVisibleRange()
window.addEventListener('scroll', this.handleScroll)
}
componentWillUnmount() {
window.removeEventListener('scroll', this.handleScroll)
}
handleScroll = () => {
this.updateVisibleRange()
}
updateVisibleRange = () => {
const container = this.containerRef.current
if (!container) return
const scrollTop = container.scrollTop
const containerHeight = container.clientHeight
const startIndex = Math.floor(scrollTop / this.state.itemHeight)
const endIndex = Math.min(
startIndex + Math.ceil(containerHeight / this.state.itemHeight) + 10,
this.props.messages.length
)
this.setState({ startIndex, endIndex })
}
render() {
const { messages, renderMessage } = this.props
const { startIndex, endIndex, itemHeight } = this.state
const visibleMessages = messages.slice(startIndex, endIndex)
const totalHeight = messages.length * itemHeight
const offsetY = startIndex * itemHeight
return (
<div
ref={this.containerRef}
className="virtualized-list"
style={{ height: '100%', overflow: 'auto' }}
>
<div style={{ height: totalHeight, position: 'relative' }}>
<div
style={{
transform: `translateY(${offsetY}px)`,
position: 'absolute',
top: 0,
left: 0,
right: 0
}}
>
{visibleMessages.map((message, index) => (
<div key={message.id} style={{ height: itemHeight }}>
{renderMessage(message)}
</div>
))}
</div>
</div>
</div>
)
}
}// Real-time message updates
class RealTimeMessageHandler {
constructor(socket, messageStore) {
this.socket = socket
this.messageStore = messageStore
this.setupSocketListeners()
}
setupSocketListeners() {
this.socket.on('message', (message) => {
this.handleIncomingMessage(message)
})
this.socket.on('typing', (userId) => {
this.handleTypingIndicator(userId)
})
this.socket.on('message_update', (updatedMessage) => {
this.handleMessageUpdate(updatedMessage)
})
this.socket.on('message_delete', (messageId) => {
this.handleMessageDeletion(messageId)
})
}
handleIncomingMessage(message) {
// Add message to store
this.messageStore.addMessage(message)
// Scroll to bottom if user is at bottom
if (this.isUserAtBottom()) {
this.scrollToBottom()
}
// Show notification if window not focused
if (!document.hasFocus()) {
this.showNotification(message)
}
}
handleTypingIndicator(userId) {
this.messageStore.setTyping(userId, true)
// Clear typing indicator after 3 seconds
setTimeout(() => {
this.messageStore.setTyping(userId, false)
}, 3000)
}
isUserAtBottom() {
const container = document.querySelector('.messages-container')
if (!container) return false
const { scrollTop, scrollHeight, clientHeight } = container
return scrollTop + clientHeight >= scrollHeight - 100
}
scrollToBottom() {
const container = document.querySelector('.messages-container')
if (container) {
container.scrollTop = container.scrollHeight
}
}
}// Comprehensive theming system
interface Theme {
name: string
colors: {
primary: string
secondary: string
background: string
surface: string
text: {
primary: string
secondary: string
disabled: string
}
border: string
error: string
success: string
warning: string
}
typography: {
fontFamily: string
fontSize: {
xs: string
sm: string
md: string
lg: string
xl: string
}
fontWeight: {
light: number
regular: number
medium: number
bold: number
}
}
spacing: {
xs: string
sm: string
md: string
lg: string
xl: string
}
borderRadius: {
sm: string
md: string
lg: string
xl: string
}
shadows: {
sm: string
md: string
lg: string
xl: string
}
}
class ThemeManager {
private themes: Map<string, Theme> = new Map()
private currentTheme: string = 'light'
registerTheme(name: string, theme: Theme) {
this.themes.set(name, theme)
}
setTheme(name: string) {
if (this.themes.has(name)) {
this.currentTheme = name
this.applyTheme(this.themes.get(name)!)
this.persistThemeChoice(name)
}
}
getCurrentTheme(): Theme {
return this.themes.get(this.currentTheme)!
}
private applyTheme(theme: Theme) {
const root = document.documentElement
// Apply CSS custom properties
Object.entries(theme.colors).forEach(([key, value]) => {
if (typeof value === 'object') {
Object.entries(value).forEach(([subKey, subValue]) => {
root.style.setProperty(`--color-${key}-${subKey}`, subValue)
})
} else {
root.style.setProperty(`--color-${key}`, value)
}
})
// Apply typography
Object.entries(theme.typography.fontSize).forEach(([key, value]) => {
root.style.setProperty(`--font-size-${key}`, value)
})
// Apply spacing
Object.entries(theme.spacing).forEach(([key, value]) => {
root.style.setProperty(`--spacing-${key}`, value)
})
}
private persistThemeChoice(themeName: string) {
localStorage.setItem('preferred-theme', themeName)
}
initializeFromStorage() {
const savedTheme = localStorage.getItem('preferred-theme')
if (savedTheme && this.themes.has(savedTheme)) {
this.setTheme(savedTheme)
}
}
}// Responsive design system
const breakpoints = {
mobile: 0,
tablet: 768,
desktop: 1024,
wide: 1440
}
const mediaQueries = {
mobile: `@media (max-width: ${breakpoints.tablet - 1}px)`,
tablet: `@media (min-width: ${breakpoints.tablet}px) and (max-width: ${breakpoints.desktop - 1}px)`,
desktop: `@media (min-width: ${breakpoints.desktop}px)`,
wide: `@media (min-width: ${breakpoints.wide}px)`
}
// Responsive component patterns
const ResponsiveContainer: React.FC = ({ children }) => {
const [screenSize, setScreenSize] = useState<'mobile' | 'tablet' | 'desktop' | 'wide'>('desktop')
useEffect(() => {
const updateScreenSize = () => {
const width = window.innerWidth
if (width < breakpoints.tablet) {
setScreenSize('mobile')
} else if (width < breakpoints.desktop) {
setScreenSize('tablet')
} else if (width < breakpoints.wide) {
setScreenSize('desktop')
} else {
setScreenSize('wide')
}
}
updateScreenSize()
window.addEventListener('resize', updateScreenSize)
return () => window.removeEventListener('resize', updateScreenSize)
}, [])
return (
<div className={`container ${screenSize}`}>
{children}
</div>
)
}// Accessibility features
const AccessibleChat: React.FC = () => {
const [announcements, setAnnouncements] = useState<string[]>([])
const announce = (message: string) => {
setAnnouncements(prev => [...prev, message])
// Clear announcements after screen readers process them
setTimeout(() => {
setAnnouncements(prev => prev.slice(1))
}, 1000)
}
const handleSendMessage = (content: string) => {
// Announce message sending
announce('Message sent')
// Actual send logic
sendMessage(content)
// Announce response
announce('AI is responding')
}
return (
<div role="application" aria-label="AI Chat Application">
{/* Screen reader announcements */}
<div
aria-live="polite"
aria-atomic="true"
className="sr-only"
>
{announcements.map((announcement, index) => (
<div key={index}>{announcement}</div>
))}
</div>
{/* Main chat interface with proper ARIA labels */}
<div className="chat-container">
<header>
<h1 id="chat-title">AI Chat Assistant</h1>
</header>
<main
role="log"
aria-label="Chat messages"
aria-labelledby="chat-title"
className="messages-area"
>
{/* Messages with proper roles */}
</main>
<footer>
<div className="message-input">
<label htmlFor="message-input" className="sr-only">
Type your message
</label>
<textarea
id="message-input"
aria-describedby="input-help"
placeholder="Type your message..."
/>
<button
type="button"
aria-label="Send message"
onClick={() => handleSendMessage(message)}
>
Send
</button>
</div>
<div id="input-help" className="sr-only">
Press Enter to send, Shift+Enter for new line
</div>
</footer>
</div>
</div>
)
}- ✅ Designed comprehensive UI architecture
- ✅ Built core chat components
- ✅ Implemented virtual scrolling for performance
- ✅ Added real-time update capabilities
- ✅ Created flexible theming system
- ✅ Ensured responsive design
- ✅ Added accessibility features
Key Takeaways:
- Component hierarchy provides clear structure
- State management enables complex interactions
- Virtual scrolling handles large message lists efficiently
- Real-time updates create engaging user experience
- Theming system enables customization
- Responsive design works across all devices
- Accessibility ensures inclusive user experience
Most teams struggle here because the hard part is not writing more code, but deciding clear boundaries for message, className, container so behavior stays predictable as complexity grows.
In practical terms, this chapter helps you avoid three common failures:
- coupling core logic too tightly to one implementation path
- missing the handoff boundaries between setup, execution, and validation
- shipping changes without clear rollback or observability strategy
After working through this chapter, you should be able to reason about Chapter 2: UI Architecture & Components as an operating subsystem inside Chatbox Tutorial: Building Modern AI Chat Interfaces, with explicit contracts for inputs, state transitions, and outputs.
Use the implementation notes around input, state, messages as your checklist when adapting these patterns to your own repository.
Under the hood, Chapter 2: UI Architecture & Components usually follows a repeatable control path:
- Context bootstrap: initialize runtime config and prerequisites for
message. - Input normalization: shape incoming data so
classNamereceives stable contracts. - Core execution: run the main logic branch and propagate intermediate state through
container. - Policy and safety checks: enforce limits, auth scopes, and failure boundaries.
- Output composition: return canonical result payloads for downstream consumers.
- Operational telemetry: emit logs/metrics needed for debugging and performance tuning.
When debugging, walk this sequence in order and confirm each stage has explicit success/failure conditions.
Use the following upstream sources to verify implementation details while reading this chapter:
- View Repo
Why it matters: authoritative reference on
View Repo(github.com).
Suggested trace strategy:
- search upstream code for
messageandclassNameto map concrete implementation paths - compare docs claims against actual runtime/config code before reusing patterns in production