Files
chat-ui/docs/chat-ui/customization.mdx
T
2025-08-13 11:23:02 +08:00

723 lines
18 KiB
Plaintext

---
title: Customization
description: Style and customize the appearance and behavior of chat components
---
LlamaIndex Chat UI is built with customization in mind. You can style components, override default behaviors, and create custom themes to match your application's design system.
## Styling System
The library uses Tailwind CSS for styling and provides several customization approaches:
### CSS Variables
The library exposes CSS variables for easy theming:
```css
/* Custom theme variables */
:root {
--chat-primary: #3b82f6;
--chat-secondary: #6b7280;
--chat-background: #ffffff;
--chat-surface: #f9fafb;
--chat-border: #e5e7eb;
--chat-text: #1f2937;
--chat-text-secondary: #6b7280;
--chat-success: #10b981;
--chat-warning: #f59e0b;
--chat-error: #ef4444;
}
/* Dark theme */
[data-theme="dark"] {
--chat-primary: #60a5fa;
--chat-secondary: #9ca3af;
--chat-background: #111827;
--chat-surface: #1f2937;
--chat-border: #374151;
--chat-text: #f9fafb;
--chat-text-secondary: #d1d5db;
}
```
### Component-Level Styling
Override component styles using className props:
```tsx
import { ChatSection, ChatMessages, ChatInput } from '@llamaindex/chat-ui'
function CustomStyledChat() {
return (
<ChatSection
handler={handler}
autoOpenCanvas={true}
className="bg-gradient-to-b from-blue-50 to-white"
>
<ChatMessages className="bg-white/80 backdrop-blur rounded-lg shadow-lg">
<ChatMessages.List className="space-y-6 p-6">
{/* Custom message rendering */}
</ChatMessages.List>
</ChatMessages>
<ChatInput className="bg-white border-2 border-blue-200 rounded-xl p-4">
<ChatInput.Form className="flex items-end gap-3">
<ChatInput.Field
className="flex-1 border-none bg-gray-50 rounded-lg px-4 py-2"
placeholder="Ask me anything..."
/>
<ChatInput.Submit className="bg-blue-600 hover:bg-blue-700 text-white rounded-lg px-4 py-2">
Send
</ChatInput.Submit>
</ChatInput.Form>
</ChatInput>
</ChatSection>
)
}
```
## Message Customization
### Custom Message Layout
```tsx
import { ChatMessage, useChatMessage } from '@llamaindex/chat-ui'
function CustomMessageLayout() {
const { message, isLast } = useChatMessage()
return (
<ChatMessage
message={message}
isLast={isLast}
className={`
${message.role === 'user' ? 'ml-8' : 'mr-8'}
transition-all duration-200 hover:shadow-md
`}
>
<div className={`
flex gap-3 p-4 rounded-2xl
${message.role === 'user'
? 'bg-blue-600 text-white ml-auto'
: 'bg-gray-100 text-gray-900'
}
`}>
<ChatMessage.Avatar>
<CustomAvatar role={message.role} />
</ChatMessage.Avatar>
<div className="flex-1">
<ChatMessage.Content>
<ChatMessage.Part.Markdown />
<ChatMessage.Part.Image />
<ChatMessage.Part.Source />
</ChatMessage.Content>
<ChatMessage.Actions>
<CustomMessageActions />
</ChatMessage.Actions>
</div>
</div>
</ChatMessage>
)
}
function CustomAvatar({ role }: { role: string }) {
const avatarClass = role === 'user'
? 'bg-blue-500 text-white'
: 'bg-gray-300 text-gray-700'
return (
<div className={`
h-10 w-10 rounded-full flex items-center justify-center
text-sm font-semibold ${avatarClass}
`}>
{role === 'user' ? '👤' : '🤖'}
</div>
)
}
```
### Message Role Styling
```tsx
function RoleBasedMessage() {
const { message } = useChatMessage()
const roleStyles = {
user: {
container: 'justify-end',
bubble: 'bg-blue-600 text-white rounded-l-2xl rounded-tr-2xl',
maxWidth: 'max-w-[80%]'
},
assistant: {
container: 'justify-start',
bubble: 'bg-white border shadow-sm rounded-r-2xl rounded-tl-2xl',
maxWidth: 'max-w-[85%]'
},
system: {
container: 'justify-center',
bubble: 'bg-yellow-50 border border-yellow-200 rounded-lg text-yellow-800',
maxWidth: 'max-w-[70%]'
}
}
const styles = roleStyles[message.role] || roleStyles.assistant
return (
<div className={`flex ${styles.container} mb-4`}>
<div className={`${styles.bubble} ${styles.maxWidth} p-4`}>
<ChatMessage.Content>
<ChatMessage.Part.Markdown />
</ChatMessage.Content>
</div>
</div>
)
}
```
## Input Customization
### Custom Input Design
```tsx
import { ChatInput, useChatInput } from '@llamaindex/chat-ui'
function CustomChatInput() {
const { input, setInput, handleSubmit, isLoading } = useChatInput()
return (
<div className="relative border-t bg-white p-4">
<div className="mx-auto max-w-4xl">
<ChatInput>
<ChatInput.Form className="relative flex items-end gap-3">
{/* Custom input field with enhanced styling */}
<div className="relative flex-1">
<ChatInput.Field
className="
w-full resize-none rounded-2xl border border-gray-300
bg-white px-4 py-3 pr-12 text-sm
focus:border-blue-500 focus:outline-none focus:ring-2
focus:ring-blue-500/20 disabled:opacity-50
max-h-32 min-h-[44px]
"
placeholder="Type your message..."
disabled={isLoading}
/>
{/* Character counter */}
<div className="absolute bottom-1 right-12 text-xs text-gray-400">
{input.length}/2000
</div>
</div>
{/* Upload button */}
<ChatInput.Upload>
<button
type="button"
className="
flex h-11 w-11 items-center justify-center rounded-xl
border border-gray-300 bg-white hover:bg-gray-50
transition-colors disabled:opacity-50
"
disabled={isLoading}
>
<PaperclipIcon className="h-5 w-5 text-gray-600" />
</button>
</ChatInput.Upload>
{/* Submit button */}
<ChatInput.Submit>
<button
type="submit"
disabled={isLoading || !input.trim()}
className="
flex h-11 w-11 items-center justify-center rounded-xl
bg-blue-600 text-white hover:bg-blue-700
transition-colors disabled:opacity-50 disabled:cursor-not-allowed
"
>
{isLoading ? (
<LoadingSpinner className="h-5 w-5" />
) : (
<SendIcon className="h-5 w-5" />
)}
</button>
</ChatInput.Submit>
</ChatInput.Form>
</ChatInput>
</div>
</div>
)
}
```
### Input with Suggestions
```tsx
function InputWithSuggestions() {
const [suggestions, setSuggestions] = useState([])
const { input, setInput } = useChatInput()
const commonSuggestions = [
"How can I help you today?",
"Explain this concept",
"Write some code for",
"Summarize this document"
]
const filteredSuggestions = commonSuggestions.filter(s =>
s.toLowerCase().includes(input.toLowerCase()) && input.length > 0
)
return (
<div className="relative">
<ChatInput>
<ChatInput.Form>
<ChatInput.Field
onFocus={() => setSuggestions(filteredSuggestions)}
onBlur={() => setTimeout(() => setSuggestions([]), 150)}
/>
<ChatInput.Submit />
</ChatInput.Form>
</ChatInput>
{suggestions.length > 0 && (
<div className="absolute bottom-full left-0 right-0 mb-2">
<div className="bg-white border rounded-lg shadow-lg max-h-32 overflow-y-auto">
{suggestions.map((suggestion, index) => (
<button
key={index}
className="w-full px-3 py-2 text-left hover:bg-gray-50 text-sm"
onClick={() => {
setInput(suggestion)
setSuggestions([])
}}
>
{suggestion}
</button>
))}
</div>
</div>
)}
</div>
)
}
```
## Canvas Customization
### Custom Canvas Layout
```tsx
import { ChatCanvas, useChatCanvas } from '@llamaindex/chat-ui'
function CustomCanvas() {
const { currentArtifact, isVisible, hideCanvas } = useChatCanvas()
if (!isVisible || !currentArtifact) return null
return (
<div className="w-1/2 bg-gray-50 border-l flex flex-col">
{/* Custom header */}
<div className="flex items-center justify-between p-4 border-b bg-white">
<div>
<h2 className="font-semibold text-gray-900">
{currentArtifact.title}
</h2>
<p className="text-sm text-gray-500">
{currentArtifact.type === 'code' ? 'Code Editor' : 'Document'}
</p>
</div>
<div className="flex items-center gap-2">
<CustomCanvasActions />
<button
onClick={hideCanvas}
className="p-1 hover:bg-gray-100 rounded"
>
<XIcon className="h-5 w-5" />
</button>
</div>
</div>
{/* Custom content area */}
<div className="flex-1 overflow-hidden">
<ChatCanvas className="h-full">
{/* Canvas content renders here */}
</ChatCanvas>
</div>
{/* Custom footer */}
<div className="border-t bg-white p-3">
<div className="flex items-center justify-between text-sm text-gray-500">
<span>
{currentArtifact.type === 'code'
? `${currentArtifact.language} • ${currentArtifact.file_name}`
: 'Markdown Document'
}
</span>
<span>
Last modified: {new Date().toLocaleTimeString()}
</span>
</div>
</div>
</div>
)
}
```
## Widget Styling
### Custom Markdown Styling
```tsx
import { Markdown } from '@llamaindex/chat-ui/widgets'
function CustomMarkdown({ children }: { children: string }) {
return (
<div className="prose prose-sm max-w-none">
<style jsx>{`
.prose {
--tw-prose-body: #374151;
--tw-prose-headings: #111827;
--tw-prose-links: #3b82f6;
--tw-prose-bold: #111827;
--tw-prose-code: #dc2626;
--tw-prose-pre-bg: #f3f4f6;
--tw-prose-pre-code: #374151;
}
.prose code {
background: #f3f4f6;
padding: 0.125rem 0.25rem;
border-radius: 0.25rem;
font-size: 0.875em;
}
.prose blockquote {
border-left: 4px solid #3b82f6;
background: #eff6ff;
padding: 1rem;
margin: 1rem 0;
}
`}</style>
<Markdown>{children}</Markdown>
</div>
)
}
```
### Custom Code Block Styling
```tsx
import { CodeBlock } from '@llamaindex/chat-ui/widgets'
function StyledCodeBlock({ code, language, filename }: {
code: string
language: string
filename?: string
}) {
return (
<div className="my-4 overflow-hidden rounded-lg border border-gray-200">
{filename && (
<div className="flex items-center justify-between bg-gray-50 px-4 py-2">
<span className="text-sm font-medium text-gray-700">
{filename}
</span>
<span className="text-xs text-gray-500 uppercase">
{language}
</span>
</div>
)}
<div className="relative">
<CodeBlock
code={code}
language={language}
className="bg-gray-900 text-gray-100"
showLineNumbers={true}
/>
{/* Custom copy button */}
<button
className="absolute top-2 right-2 p-2 bg-gray-800 hover:bg-gray-700 rounded"
onClick={() => navigator.clipboard.writeText(code)}
>
<CopyIcon className="h-4 w-4 text-gray-300" />
</button>
</div>
</div>
)
}
```
## Theme System
### Theme Provider
```tsx
import { createContext, useContext, useState } from 'react'
interface Theme {
mode: 'light' | 'dark'
primaryColor: string
borderRadius: 'none' | 'sm' | 'md' | 'lg'
fontSize: 'sm' | 'base' | 'lg'
}
const ThemeContext = createContext<{
theme: Theme
updateTheme: (updates: Partial<Theme>) => void
}>({
theme: {
mode: 'light',
primaryColor: 'blue',
borderRadius: 'md',
fontSize: 'base'
},
updateTheme: () => {}
})
export function ThemeProvider({ children }: { children: React.ReactNode }) {
const [theme, setTheme] = useState<Theme>({
mode: 'light',
primaryColor: 'blue',
borderRadius: 'md',
fontSize: 'base'
})
const updateTheme = (updates: Partial<Theme>) => {
setTheme(prev => ({ ...prev, ...updates }))
}
return (
<ThemeContext.Provider value={{ theme, updateTheme }}>
<div
className={`theme-${theme.mode} theme-${theme.primaryColor}`}
data-theme={theme.mode}
data-radius={theme.borderRadius}
data-font-size={theme.fontSize}
>
{children}
</div>
</ThemeContext.Provider>
)
}
export const useTheme = () => useContext(ThemeContext)
```
### Themed Components
```tsx
function ThemedChatSection() {
const { theme } = useTheme()
const handler = useChat({
transport: new DefaultChatTransport({
api: '/api/chat',
}),
})
const themeClasses = {
light: 'bg-white text-gray-900',
dark: 'bg-gray-900 text-white'
}
const radiusClasses = {
none: 'rounded-none',
sm: 'rounded-sm',
md: 'rounded-md',
lg: 'rounded-lg'
}
const fontSizeClasses = {
sm: 'text-sm',
base: 'text-base',
lg: 'text-lg'
}
return (
<ChatSection
handler={handler}
className={`
${themeClasses[theme.mode]}
${fontSizeClasses[theme.fontSize]}
transition-all duration-200
`}
>
<ChatMessages
className={`
${radiusClasses[theme.borderRadius]}
border border-current/10
`}
/>
<ChatInput
className={`
${radiusClasses[theme.borderRadius]}
border border-current/20
`}
/>
</ChatSection>
)
}
```
## Animation and Transitions
### Message Animations
```tsx
import { motion, AnimatePresence } from 'framer-motion'
function AnimatedMessages() {
const { messages } = useChatUI()
return (
<AnimatePresence>
{messages.map((message, index) => (
<motion.div
key={message.id}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
transition={{ duration: 0.3, delay: index * 0.1 }}
>
<ChatMessage message={message}>
{/* Message content */}
</ChatMessage>
</motion.div>
))}
</AnimatePresence>
)
}
```
### Typing Indicator
```tsx
function TypingIndicator() {
const { isLoading } = useChatUI()
if (!isLoading) return null
return (
<motion.div
initial={{ opacity: 0, scale: 0.8 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.8 }}
className="flex items-center gap-2 p-4"
>
<div className="flex gap-1">
{[0, 1, 2].map((i) => (
<motion.div
key={i}
className="h-2 w-2 bg-gray-400 rounded-full"
animate={{
scale: [1, 1.2, 1],
opacity: [0.5, 1, 0.5]
}}
transition={{
duration: 1.5,
repeat: Infinity,
delay: i * 0.2
}}
/>
))}
</div>
<span className="text-sm text-gray-500">AI is typing...</span>
</motion.div>
)
}
```
## Responsive Design
### Mobile-First Layout
```tsx
function ResponsiveChatLayout() {
const [isMobile, setIsMobile] = useState(false)
useEffect(() => {
const checkMobile = () => setIsMobile(window.innerWidth < 768)
checkMobile()
window.addEventListener('resize', checkMobile)
return () => window.removeEventListener('resize', checkMobile)
}, [])
return (
<ChatSection
handler={handler}
className={isMobile ? 'flex-col h-full' : 'flex-row h-full'}
>
<div className={isMobile ? 'flex-1' : 'flex-1 flex flex-col'}>
<ChatMessages className={isMobile ? 'flex-1' : 'flex-1'} />
<ChatInput className={isMobile ? 'sticky bottom-0' : ''} />
</div>
{!isMobile && <ChatCanvas className="w-1/2" />}
</ChatSection>
)
}
```
## Accessibility Customization
### Enhanced Accessibility
```tsx
function AccessibleChat() {
const { messages, isLoading } = useChatUI()
return (
<div
role="log"
aria-live="polite"
aria-label="Chat conversation"
className="relative"
>
<ChatMessages>
<ChatMessages.List
role="log"
aria-busy={isLoading}
>
{messages.map((message, index) => (
<div
key={message.id}
role="article"
aria-label={`Message from ${message.role}`}
tabIndex={0}
className="focus:outline-none focus:ring-2 focus:ring-blue-500"
>
<ChatMessage message={message} />
</div>
))}
</ChatMessages.List>
</ChatMessages>
<ChatInput>
<ChatInput.Form>
<ChatInput.Field
aria-label="Type your message"
aria-describedby="chat-input-help"
/>
<div id="chat-input-help" className="sr-only">
Press Enter to send, Shift+Enter for new line
</div>
<ChatInput.Submit aria-label="Send message" />
</ChatInput.Form>
</ChatInput>
</div>
)
}
```
## Next Steps
- [Examples](./examples.mdx) - See complete customization examples
- [Core Components](./core-components.mdx) - Understand component structure for customization
- [Widgets](./widgets.mdx) - Customize widget appearance and behavior
- [Hooks](./hooks.mdx) - Use hooks for dynamic customization