diff --git a/chat-example/ChatContainer.tsx b/chat-example/ChatContainer.tsx
new file mode 100644
index 0000000..7952376
--- /dev/null
+++ b/chat-example/ChatContainer.tsx
@@ -0,0 +1,90 @@
+'use client'
+
+import { useEffect, useState } from 'react'
+
+import { ChatInterface } from './ChatInterface'
+
+type ChatError = 'SESSION_CREATION_FAILED' | 'UNEXPECTED_ERROR'
+
+/**
+ * Error component to display when session creation fails.
+ */
+const ErrorDisplay: React.FC<{ error: ChatError }> = ({ error }) => {
+ const errorMessages = {
+ SESSION_CREATION_FAILED: 'Failed to create chat session. Please try again.',
+ UNEXPECTED_ERROR: 'An unexpected error occurred. Please try again later.',
+ }
+
+ return (
+
+ {errorMessages[error]}
+
+ )
+}
+
+/**
+ * Container component that handles session creation and renders the chat interface.
+ * This is a client component that creates a new chat session before rendering the chat UI.
+ */
+export const ChatContainer: React.FC = () => {
+ const [sessionId, setSessionId] = useState(null)
+ const [error, setError] = useState(null)
+ const [isLoading, setIsLoading] = useState(true)
+
+ useEffect(() => {
+ const createSession = async () => {
+ try {
+ setIsLoading(true)
+ setError(null)
+
+ const response = await fetch('/api/chat/session', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ })
+
+ if (!response.ok) {
+ throw new Error('Failed to create session')
+ }
+
+ const data = await response.json()
+
+ if (!data?.sessionId) {
+ throw new Error('SESSION_CREATION_FAILED')
+ }
+
+ setSessionId(data.sessionId)
+ } catch (err) {
+ console.error('Error creating chat session:', err)
+ setError(
+ err instanceof Error && err.message === 'SESSION_CREATION_FAILED'
+ ? 'SESSION_CREATION_FAILED'
+ : 'UNEXPECTED_ERROR',
+ )
+ } finally {
+ setIsLoading(false)
+ }
+ }
+
+ void createSession()
+ }, [])
+
+ if (isLoading) {
+ return (
+
+ )
+ }
+
+ if (error) {
+ return
+ }
+
+ if (!sessionId) {
+ return null
+ }
+
+ return
+}
diff --git a/chat-example/ChatInterface.tsx b/chat-example/ChatInterface.tsx
new file mode 100644
index 0000000..c34d94e
--- /dev/null
+++ b/chat-example/ChatInterface.tsx
@@ -0,0 +1,140 @@
+'use client'
+
+import { useCallback, useMemo } from 'react'
+
+import { Header, Button, Icon } from '@your-org/components'
+import classNames from 'classnames'
+
+import { useChat } from '../hooks/useChat'
+import { useChatScroll } from '../hooks/useChatScroll'
+import { StreamingState } from '../types'
+
+type ChatInterfaceProps = {
+ /**
+ * The ID of the conversation to use.
+ */
+ sessionId: string
+}
+
+/**
+ * Renders a chat interface with streaming responses.
+ * This is a client component that manages the chat state for a given conversation.
+ */
+export const ChatInterface: React.FC = ({ sessionId }) => {
+ const { handleSendMessage, messages, streamingState, error, handleRetry } =
+ useChat({ sessionId })
+
+ const hasMessages = useMemo(() => messages.length > 0, [messages])
+
+ const { scrollContainerRef, chatEndRef, isAtBottom, scrollToBottom } =
+ useChatScroll(hasMessages)
+
+ const handleScrollToBottom = useCallback(() => {
+ scrollToBottom()
+ }, [scrollToBottom])
+
+ const handleSubmit = useCallback(
+ (message: string) => {
+ void (async () => {
+ scrollToBottom()
+ await handleSendMessage(message)
+ })()
+ },
+ [handleSendMessage, scrollToBottom],
+ )
+
+ return (
+ <>
+
+
+ {!hasMessages && (
+
+
+ Chat Interface
+
+ Start a conversation with our AI assistant
+
+
+
+ )}
+
+ {hasMessages && (
+
+
+ {new Date().toLocaleString()}
+
+
+ {messages.map((message) =>
+ message.type === 'user' ? (
+
+ {message.content}
+
+ ) : (
+
+ {message.content}
+ {error && message.id === messages.at(-1)?.id && (
+
+ )}
+
+ ),
+ )}
+
+
+
+ )}
+
+
+
+
+ {!isAtBottom && hasMessages && (
+
+
+
+ )}
+
+
+
+ {
+ if (e.key === 'Enter') {
+ handleSubmit(e.currentTarget.value)
+ e.currentTarget.value = ''
+ }
+ }}
+ placeholder="Type your message..."
+ type="text"
+ />
+
+
+
+
+ >
+ )
+}
diff --git a/chat-example/useChat.tsx b/chat-example/useChat.tsx
new file mode 100644
index 0000000..ccd605d
--- /dev/null
+++ b/chat-example/useChat.tsx
@@ -0,0 +1,254 @@
+import { useCallback, useEffect, useReducer, useRef } from 'react'
+
+// Types
+export enum StreamingState {
+ IDLE = 'idle',
+ STREAMING = 'streaming',
+ RETRYING = 'retrying',
+}
+
+type Message = {
+ id: string
+ type: 'user' | 'bot'
+ content: string
+ timestamp: Date
+ isComplete?: boolean
+}
+
+type ChatState = {
+ messages: Message[]
+ streamingState: StreamingState
+ error: boolean
+}
+
+// Action types
+type ChatAction =
+ | { type: 'ADD_USER_MESSAGE'; payload: { content: string } }
+ | { type: 'ADD_BOT_MESSAGE'; payload: { id: string } }
+ | { type: 'APPEND_TO_BOT_MESSAGE'; payload: { content: string } }
+ | { type: 'COMPLETE_BOT_MESSAGE' }
+ | { type: 'SET_STREAMING_STATE'; payload: StreamingState }
+ | { type: 'SET_ERROR'; payload: boolean }
+ | { type: 'REMOVE_LAST_BOT_MESSAGE' }
+
+// API endpoint
+const CHAT_API_URL = '/api/chat'
+
+// Initial state
+const initialState: ChatState = {
+ messages: [],
+ streamingState: StreamingState.IDLE,
+ error: false,
+}
+
+// Reducer
+function chatReducer(state: ChatState, action: ChatAction): ChatState {
+ switch (action.type) {
+ case 'ADD_USER_MESSAGE':
+ return {
+ ...state,
+ messages: [
+ ...state.messages,
+ {
+ id: crypto.randomUUID(),
+ type: 'user',
+ content: action.payload.content,
+ timestamp: new Date(),
+ },
+ ],
+ }
+
+ case 'ADD_BOT_MESSAGE':
+ return {
+ ...state,
+ messages: [
+ ...state.messages,
+ {
+ id: action.payload.id,
+ type: 'bot',
+ content: '',
+ timestamp: new Date(),
+ isComplete: false,
+ },
+ ],
+ }
+
+ case 'APPEND_TO_BOT_MESSAGE':
+ return {
+ ...state,
+ messages: state.messages.map((msg, index) =>
+ index === state.messages.length - 1 && msg.type === 'bot'
+ ? { ...msg, content: msg.content + action.payload.content }
+ : msg,
+ ),
+ }
+
+ case 'COMPLETE_BOT_MESSAGE':
+ return {
+ ...state,
+ messages: state.messages.map((msg, index) =>
+ index === state.messages.length - 1 && msg.type === 'bot'
+ ? { ...msg, isComplete: true }
+ : msg,
+ ),
+ }
+
+ case 'SET_STREAMING_STATE':
+ return {
+ ...state,
+ streamingState: action.payload,
+ }
+
+ case 'SET_ERROR':
+ return {
+ ...state,
+ error: action.payload,
+ }
+
+ case 'REMOVE_LAST_BOT_MESSAGE':
+ return {
+ ...state,
+ messages: state.messages.slice(0, -1),
+ }
+
+ default:
+ return state
+ }
+}
+
+type UseChatProps = {
+ sessionId: string
+}
+
+/**
+ * A React hook that manages chat message interactions with a streaming API endpoint.
+ * Provides functionality to send messages, receive streaming responses, and manage chat state.
+ */
+export const useChat = ({ sessionId }: UseChatProps) => {
+ const [state, dispatch] = useReducer(chatReducer, initialState)
+ const isMounted = useRef(true)
+
+ // Cleanup on unmount
+ useEffect(() => {
+ isMounted.current = true
+ return () => {
+ isMounted.current = false
+ }
+ }, [])
+
+ // Safe dispatch to avoid updating unmounted component
+ const safeDispatch = useCallback(
+ (action: ChatAction) => {
+ if (isMounted.current) {
+ dispatch(action)
+ }
+ },
+ [dispatch],
+ )
+
+ const handleError = useCallback(() => {
+ safeDispatch({ type: 'SET_ERROR', payload: true })
+ safeDispatch({ type: 'SET_STREAMING_STATE', payload: StreamingState.IDLE })
+ }, [safeDispatch])
+
+ const processStream = useCallback(
+ async (reader: ReadableStreamDefaultReader) => {
+ try {
+ while (true) {
+ const { done, value } = await reader.read()
+ if (done) break
+
+ // Convert the chunk to text
+ const chunk = new TextDecoder().decode(value)
+ const data = JSON.parse(chunk)
+
+ // Handle the chunk
+ if (data.type === 'completion') {
+ safeDispatch({ type: 'COMPLETE_BOT_MESSAGE' })
+ continue
+ }
+
+ if (data.content) {
+ safeDispatch({
+ type: 'APPEND_TO_BOT_MESSAGE',
+ payload: { content: data.content },
+ })
+ }
+ }
+ } catch (error) {
+ handleError()
+ }
+ },
+ [safeDispatch, handleError],
+ )
+
+ const handleSendMessage = useCallback(
+ async (content: string) => {
+ if (!isMounted.current) return
+
+ // Reset error state
+ safeDispatch({ type: 'SET_ERROR', payload: false })
+
+ // Set streaming state
+ safeDispatch({
+ type: 'SET_STREAMING_STATE',
+ payload: StreamingState.STREAMING,
+ })
+
+ // Add bot message placeholder
+ const botMessageId = crypto.randomUUID()
+ safeDispatch({ type: 'ADD_BOT_MESSAGE', payload: { id: botMessageId } })
+
+ try {
+ const response = await fetch(CHAT_API_URL, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({
+ message: content,
+ sessionId,
+ }),
+ })
+
+ if (!response.ok) {
+ throw new Error('HTTP error')
+ }
+
+ const reader = response.body?.getReader()
+ if (!reader) {
+ throw new Error('No reader available')
+ }
+
+ await processStream(reader)
+ } catch (error) {
+ handleError()
+ } finally {
+ safeDispatch({
+ type: 'SET_STREAMING_STATE',
+ payload: StreamingState.IDLE,
+ })
+ }
+ },
+ [safeDispatch, sessionId, processStream, handleError],
+ )
+
+ const handleRetry = useCallback(async () => {
+ safeDispatch({ type: 'REMOVE_LAST_BOT_MESSAGE' })
+ const lastUserMessage = state.messages
+ .filter((message) => message.type === 'user')
+ .at(-1)?.content
+
+ if (lastUserMessage) {
+ await handleSendMessage(lastUserMessage)
+ }
+ }, [safeDispatch, handleSendMessage, state.messages])
+
+ return {
+ handleSendMessage,
+ messages: state.messages,
+ streamingState: state.streamingState,
+ error: state.error,
+ handleRetry,
+ }
+}