mdxui

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

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+ messages

Show 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])

On this page