Skip to content

Latest commit

 

History

History
761 lines (643 loc) · 19.4 KB

File metadata and controls

761 lines (643 loc) · 19.4 KB
layout title parent nav_order
default
Chapter 2: UI Architecture & Components
Chatbox Tutorial
2

Chapter 2: UI Architecture & Components

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.

🎨 UI Architecture Overview

Component Hierarchy

// 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"
  }
}

State Management

// 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))
  }
}

🧩 Core Components

Message Components

// 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>
  )
}

Message Input Component

// 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 List Component

// 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>
  )
}

🎯 Advanced UI Patterns

Virtual Scrolling

// 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 Updates

// 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
    }
  }
}

🎨 Theming System

Theme Architecture

// 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

Breakpoint System

// 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

ARIA Support

// 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>
  )
}

📝 Chapter Summary

  • ✅ 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

What Problem Does This Solve?

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.

How it Works Under the Hood

Under the hood, Chapter 2: UI Architecture & Components usually follows a repeatable control path:

  1. Context bootstrap: initialize runtime config and prerequisites for message.
  2. Input normalization: shape incoming data so className receives stable contracts.
  3. Core execution: run the main logic branch and propagate intermediate state through container.
  4. Policy and safety checks: enforce limits, auth scopes, and failure boundaries.
  5. Output composition: return canonical result payloads for downstream consumers.
  6. 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.

Source Walkthrough

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 message and className to map concrete implementation paths
  • compare docs claims against actual runtime/config code before reusing patterns in production

Chapter Connections