second-brain/CHATJS_BEST_PRACTICES.md

672 lines
17 KiB
Markdown

# 🚀 ChatJS Best Practices - Second Brain Integration Guide
## Overview
This document contains advanced patterns from [ChatJS](https://github.com/FranciscoMoretti/chatjs) that should be integrated into the Second Brain project to achieve production-grade quality.
---
## 1. 📜 Smart Scroll Management
**Problem**: Current implementation scrolls even when user is reading previous messages.
**ChatJS Solution**: `use-scroll-anchor.ts`
```typescript
// hooks/use-scroll-anchor.ts
import { useEffect, useRef, useState } from 'react'
export function useScrollAnchor() {
const messagesRef = useRef<HTMLDivElement>(null)
const scrollRef = useRef<HTMLDivElement>(null)
const isAtBottomRef = useRef(true)
const [showScrollButton, setShowScrollButton] = useState(false)
useEffect(() => {
const messagesEl = messagesRef.current
const scrollEl = scrollRef.current
if (!messagesEl || !scrollEl) return
const handleScroll = () => {
const { scrollTop, scrollHeight, clientHeight } = scrollEl
const distanceFromBottom = scrollHeight - scrollTop - clientHeight
// User is at bottom if within 50px
isAtBottomRef.current = distanceFromBottom < 50
setShowScrollButton(!isAtBottomRef.current)
}
scrollEl.addEventListener('scroll', handleScroll)
return () => scrollEl.removeEventListener('scroll', handleScroll)
}, [])
const scrollToBottom = () => {
const messagesEl = messagesRef.current
if (messagesEl) {
messagesEl.scrollIntoView({ behavior: 'smooth', block: 'end' })
}
}
// Auto-scroll ONLY if user is at bottom
const autoScroll = () => {
if (isAtBottomRef.current) {
scrollToBottom()
}
}
return {
messagesRef,
scrollRef,
showScrollButton,
scrollToBottom,
autoScroll,
}
}
```
**Usage in ChatPage**:
```typescript
export function ChatPage() {
const { messages, isTyping } = useChatStore()
const { messagesRef, scrollRef, showScrollButton, scrollToBottom, autoScroll } = useScrollAnchor()
useEffect(() => {
autoScroll() // Only scrolls if user is at bottom
}, [messages, isTyping])
return (
<div className="flex flex-col h-full">
<div ref={scrollRef} className="flex-1 overflow-y-auto">
<div className="py-8">
{messages.map(msg => <ChatMessage key={msg.id} message={msg} />)}
{isTyping && <TypingIndicator />}
<div ref={messagesRef} />
</div>
</div>
{/* Scroll to Bottom Button */}
{showScrollButton && (
<button
onClick={scrollToBottom}
className="absolute bottom-24 right-8 p-3 bg-primary rounded-full shadow-lg"
>
<ChevronDown className="w-5 h-5 text-white" />
</button>
)}
<ChatInput />
</div>
)
}
```
---
## 2. 🎬 Message Actions
**ChatJS has excellent message actions on hover.**
```typescript
// components/chat/MessageActions.tsx
import { Copy, RotateCw, Edit, Share, MoreHorizontal } from 'lucide-react'
import { toast } from 'sonner'
interface MessageActionsProps {
message: Message
onRegenerate?: () => void
onEdit?: () => void
}
export function MessageActions({ message, onRegenerate, onEdit }: MessageActionsProps) {
const copyToClipboard = () => {
navigator.clipboard.writeText(message.content)
toast.success('Copied to clipboard')
}
return (
<div className="flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
{/* Copy */}
<button
onClick={copyToClipboard}
className="p-1.5 hover:bg-zinc-800 rounded transition-colors"
title="Copy"
>
<Copy className="w-4 h-4 text-zinc-400" />
</button>
{/* Regenerate (AI messages only) */}
{message.role === 'assistant' && onRegenerate && (
<button
onClick={onRegenerate}
className="p-1.5 hover:bg-zinc-800 rounded transition-colors"
title="Regenerate"
>
<RotateCw className="w-4 h-4 text-zinc-400" />
</button>
)}
{/* Edit (User messages only) */}
{message.role === 'user' && onEdit && (
<button
onClick={onEdit}
className="p-1.5 hover:bg-zinc-800 rounded transition-colors"
title="Edit"
>
<Edit className="w-4 h-4 text-zinc-400" />
</button>
)}
{/* Share */}
<button
className="p-1.5 hover:bg-zinc-800 rounded transition-colors"
title="Share"
>
<Share className="w-4 h-4 text-zinc-400" />
</button>
</div>
)
}
```
**Update ChatMessage to include actions**:
```typescript
export function ChatMessage({ message }: ChatMessageProps) {
const { regenerateMessage } = useChatStore()
const isUser = message.role === 'user'
return (
<div className="group flex gap-3 max-w-4xl px-6 py-4 relative">
{/* Avatar */}
<div className={/* ... */}>
{/* ... */}
</div>
{/* Content */}
<div className="flex-1 flex flex-col gap-2">
{/* ... existing content ... */}
</div>
{/* Actions - appears on hover */}
<MessageActions
message={message}
onRegenerate={() => regenerateMessage(message.id)}
/>
</div>
)
}
```
---
## 3. 📎 Advanced File Upload
**ChatJS has excellent drag & drop with preview.**
```typescript
// components/chat/FileUploadZone.tsx
import { useState } from 'react'
import { Upload, X, File, Image } from 'lucide-react'
interface FileUploadZoneProps {
onFilesSelect: (files: File[]) => void
selectedFiles: File[]
onFileRemove: (index: number) => void
}
export function FileUploadZone({ onFilesSelect, selectedFiles, onFileRemove }: FileUploadZoneProps) {
const [isDragging, setIsDragging] = useState(false)
const handleDrop = (e: React.DragEvent) => {
e.preventDefault()
setIsDragging(false)
const files = Array.from(e.dataTransfer.files)
onFilesSelect(files)
}
const handleDragOver = (e: React.DragEvent) => {
e.preventDefault()
setIsDragging(true)
}
const handleDragLeave = () => {
setIsDragging(false)
}
return (
<div>
{/* Drag & Drop Zone */}
<div
onDrop={handleDrop}
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
className={`
border-2 border-dashed rounded-lg p-8 transition-all
${isDragging
? 'border-primary bg-primary/5'
: 'border-zinc-700 hover:border-zinc-600'
}
`}
>
<div className="flex flex-col items-center gap-2 text-center">
<Upload className={`w-8 h-8 ${isDragging ? 'text-primary' : 'text-zinc-500'}`} />
<p className="text-sm text-zinc-400">
{isDragging ? 'Drop files here' : 'Drag files here or click to browse'}
</p>
<p className="text-xs text-zinc-600">
Supports: Images, PDFs, Documents (max 10MB)
</p>
</div>
</div>
{/* File Previews */}
{selectedFiles.length > 0 && (
<div className="mt-4 space-y-2">
{selectedFiles.map((file, index) => (
<FilePreviewCard
key={index}
file={file}
onRemove={() => onFileRemove(index)}
/>
))}
</div>
)}
</div>
)
}
// File Preview Card
function FilePreviewCard({ file, onRemove }: { file: File; onRemove: () => void }) {
const isImage = file.type.startsWith('image/')
const [preview, setPreview] = useState<string | null>(null)
useEffect(() => {
if (isImage) {
const reader = new FileReader()
reader.onloadend = () => setPreview(reader.result as string)
reader.readAsDataURL(file)
}
}, [file, isImage])
return (
<div className="flex items-center gap-3 p-3 bg-zinc-900 border border-zinc-800 rounded-lg">
{/* Icon or Image Preview */}
{isImage && preview ? (
<img src={preview} alt={file.name} className="w-12 h-12 object-cover rounded" />
) : (
<div className="w-12 h-12 bg-zinc-800 rounded flex items-center justify-center">
<File className="w-6 h-6 text-zinc-500" />
</div>
)}
{/* File Info */}
<div className="flex-1 min-w-0">
<p className="text-sm font-medium text-zinc-200 truncate">{file.name}</p>
<p className="text-xs text-zinc-500">{formatFileSize(file.size)}</p>
</div>
{/* Remove Button */}
<button
onClick={onRemove}
className="p-1 hover:bg-zinc-800 rounded transition-colors"
>
<X className="w-4 h-4 text-zinc-500" />
</button>
</div>
)
}
function formatFileSize(bytes: number): string {
if (bytes < 1024) return bytes + ' B'
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB'
return (bytes / (1024 * 1024)).toFixed(1) + ' MB'
}
```
---
## 4. ⌨️ Advanced Keyboard Shortcuts
**ChatJS has a keyboard shortcuts modal.**
```typescript
// components/KeyboardShortcutsModal.tsx
import { useEffect, useState } from 'react'
export function KeyboardShortcutsModal() {
const [isOpen, setIsOpen] = useState(false)
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
// Cmd+/ or Ctrl+/ to toggle
if ((e.metaKey || e.ctrlKey) && e.key === '/') {
e.preventDefault()
setIsOpen(prev => !prev)
}
}
window.addEventListener('keydown', handleKeyDown)
return () => window.removeEventListener('keydown', handleKeyDown)
}, [])
if (!isOpen) return null
const shortcuts = [
{ keys: ['Cmd', 'N'], action: 'New chat' },
{ keys: ['Cmd', 'K'], action: 'Search conversations' },
{ keys: ['Cmd', 'L'], action: 'Go to library' },
{ keys: ['Cmd', 'M'], action: 'Go to neural map' },
{ keys: ['Cmd', '/'], action: 'Show keyboard shortcuts' },
{ keys: ['Escape'], action: 'Close modal' },
{ keys: ['Cmd', 'Enter'], action: 'Send message' },
]
return (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
<div className="bg-zinc-900 border border-zinc-800 rounded-xl p-6 max-w-md w-full">
<h2 className="text-xl font-bold text-zinc-100 mb-4">
Keyboard Shortcuts
</h2>
<div className="space-y-2">
{shortcuts.map((shortcut, i) => (
<div key={i} className="flex items-center justify-between py-2">
<span className="text-sm text-zinc-400">{shortcut.action}</span>
<div className="flex gap-1">
{shortcut.keys.map((key, j) => (
<kbd
key={j}
className="px-2 py-1 bg-zinc-800 border border-zinc-700 rounded text-xs font-mono text-zinc-300"
>
{key}
</kbd>
))}
</div>
</div>
))}
</div>
<button
onClick={() => setIsOpen(false)}
className="mt-6 w-full px-4 py-2 bg-primary text-white rounded-lg hover:bg-primary/90 transition-colors"
>
Close
</button>
</div>
</div>
)
}
```
---
## 5. 🔄 Regenerate Message
**Add to chatStore.ts**:
```typescript
interface ChatState {
messages: Message[]
isTyping: boolean
sendMessage: (params: SendMessageParams) => Promise<void>
regenerateMessage: (messageId: string) => Promise<void> // NEW
clearMessages: () => void
}
export const useChatStore = create<ChatState>((set, get) => ({
// ... existing code ...
regenerateMessage: async (messageId: string) => {
const { messages } = get()
const messageIndex = messages.findIndex(m => m.id === messageId)
if (messageIndex === -1) return
// Find the last user message before this AI message
const userMessage = messages
.slice(0, messageIndex)
.reverse()
.find(m => m.role === 'user')
if (!userMessage) return
// Remove the AI message and everything after it
set({ messages: messages.slice(0, messageIndex) })
// Regenerate from user message
set({ isTyping: true })
try {
const res = await fetch('/api/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
message: userMessage.content,
// Use same model/rag settings as original
}),
})
const data = await res.json()
const newMessage: Message = {
id: Date.now().toString(),
role: 'assistant',
content: data.answer,
timestamp: new Date(),
sources: data.sources,
}
set(state => ({ messages: [...state.messages, newMessage] }))
} catch (err) {
console.error('Regenerate error:', err)
} finally {
set({ isTyping: false })
}
},
}))
```
---
## 6. 📱 Mobile Improvements
**ChatJS has excellent mobile UX.**
### Mobile-Specific Improvements:
```typescript
// components/chat/MobileOptimizations.tsx
// 1. Virtual Keyboard Handling
useEffect(() => {
const handleResize = () => {
// Adjust viewport when keyboard opens
const vh = window.innerHeight * 0.01
document.documentElement.style.setProperty('--vh', `${vh}px`)
}
window.addEventListener('resize', handleResize)
handleResize()
return () => window.removeEventListener('resize', handleResize)
}, [])
// 2. Touch Gestures for Sidebar
const handleSwipe = (e: TouchEvent) => {
// Swipe from left edge to open sidebar
if (e.touches[0].clientX < 20) {
openSidebar()
}
}
// 3. Prevent Zoom on Double-Tap
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" />
// 4. Safe Area Insets (for iPhone notch)
<div className="pb-[env(safe-area-inset-bottom)]">
<ChatInput />
</div>
```
---
## 7. 🎨 Loading States
**ChatJS has skeleton loaders everywhere.**
```typescript
// components/ui/MessageSkeleton.tsx
export function MessageSkeleton() {
return (
<div className="flex gap-3 max-w-4xl px-6 py-4 animate-pulse">
<div className="w-8 h-8 rounded-full bg-zinc-800" />
<div className="flex-1 space-y-2">
<div className="h-3 bg-zinc-800 rounded w-1/4" />
<div className="h-4 bg-zinc-800 rounded w-3/4" />
<div className="h-4 bg-zinc-800 rounded w-2/3" />
</div>
</div>
)
}
// Usage
{isLoading ? (
<>
<MessageSkeleton />
<MessageSkeleton />
</>
) : (
messages.map(msg => <ChatMessage key={msg.id} message={msg} />)
)}
```
---
## 8. 🔔 Better Error Handling
**ChatJS shows user-friendly errors.**
```typescript
// lib/error-handler.ts
export function handleChatError(error: unknown) {
console.error('Chat error:', error)
if (error instanceof Error) {
// Network errors
if (error.message.includes('Failed to fetch')) {
toast.error('Connection lost. Check your internet.')
return
}
// API errors
if (error.message.includes('429')) {
toast.error('Rate limit exceeded. Please wait a moment.')
return
}
if (error.message.includes('401')) {
toast.error('Session expired. Please log in again.')
return
}
}
// Generic error
toast.error('Something went wrong. Please try again.')
}
// Usage in chatStore
catch (err) {
handleChatError(err)
}
```
---
## 9. 💾 Local Storage Persistence
**ChatJS saves draft messages.**
```typescript
// hooks/useLocalStorage.ts
export function useLocalStorage<T>(key: string, initialValue: T) {
const [storedValue, setStoredValue] = useState<T>(() => {
try {
const item = window.localStorage.getItem(key)
return item ? JSON.parse(item) : initialValue
} catch (error) {
console.error(error)
return initialValue
}
})
const setValue = (value: T | ((val: T) => T)) => {
try {
const valueToStore = value instanceof Function ? value(storedValue) : value
setStoredValue(valueToStore)
window.localStorage.setItem(key, JSON.stringify(valueToStore))
} catch (error) {
console.error(error)
}
}
return [storedValue, setValue] as const
}
// Usage in ChatInput
export function ChatInput() {
const [draftInput, setDraftInput] = useLocalStorage('chat-draft', '')
const handleSend = () => {
// ... send logic
setDraftInput('') // Clear draft
}
return (
<textarea
value={draftInput}
onChange={(e) => setDraftInput(e.target.value)}
/>
)
}
```
---
## 10. 🎯 Implementation Priority
**For Second Brain, implement in this order:**
1.**Smart Scroll** (useScrollAnchor) - Immediate UX improvement
2.**Message Actions** (Copy, Regenerate) - Professional feel
3.**Better File Upload** (Drag & drop with preview) - Core feature
4.**Keyboard Shortcuts Modal** - Power user feature
5.**Loading Skeletons** - Polish
6.**Error Handling** - Reliability
7.**Draft Persistence** - Nice to have
8.**Mobile Optimizations** - Once desktop is solid
---
## 📝 Summary
ChatJS demonstrates production-grade patterns that Second Brain should adopt:
- **Smart scrolling** prevents annoying auto-scroll
- **Message actions** make the chat feel professional
- **File previews** improve upload UX dramatically
- **Keyboard shortcuts** delight power users
- **Error handling** builds trust
- **Loading states** feel responsive
The key insight: **Small details compound into a delightful experience.**
---
## 🔗 Resources
- ChatJS Live Demo: https://chatjs.dev
- ChatJS Docs: https://chatjs.dev/docs
- ChatJS GitHub: https://github.com/FranciscoMoretti/chatjs
Use this as reference when polishing Second Brain! 🚀