mirror of
https://github.com/run-llama/chat-ui.git
synced 2026-07-01 21:24:01 -04:00
723 lines
18 KiB
Plaintext
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 |