# 🚀 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(null) const scrollRef = useRef(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 (
{messages.map(msg => )} {isTyping && }
{/* Scroll to Bottom Button */} {showScrollButton && ( )}
) } ``` --- ## 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 (
{/* Copy */} {/* Regenerate (AI messages only) */} {message.role === 'assistant' && onRegenerate && ( )} {/* Edit (User messages only) */} {message.role === 'user' && onEdit && ( )} {/* Share */}
) } ``` **Update ChatMessage to include actions**: ```typescript export function ChatMessage({ message }: ChatMessageProps) { const { regenerateMessage } = useChatStore() const isUser = message.role === 'user' return (
{/* Avatar */}
{/* ... */}
{/* Content */}
{/* ... existing content ... */}
{/* Actions - appears on hover */} regenerateMessage(message.id)} />
) } ``` --- ## 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 (
{/* Drag & Drop Zone */}

{isDragging ? 'Drop files here' : 'Drag files here or click to browse'}

Supports: Images, PDFs, Documents (max 10MB)

{/* File Previews */} {selectedFiles.length > 0 && (
{selectedFiles.map((file, index) => ( onFileRemove(index)} /> ))}
)}
) } // File Preview Card function FilePreviewCard({ file, onRemove }: { file: File; onRemove: () => void }) { const isImage = file.type.startsWith('image/') const [preview, setPreview] = useState(null) useEffect(() => { if (isImage) { const reader = new FileReader() reader.onloadend = () => setPreview(reader.result as string) reader.readAsDataURL(file) } }, [file, isImage]) return (
{/* Icon or Image Preview */} {isImage && preview ? ( {file.name} ) : (
)} {/* File Info */}

{file.name}

{formatFileSize(file.size)}

{/* Remove Button */}
) } 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 (

Keyboard Shortcuts

{shortcuts.map((shortcut, i) => (
{shortcut.action}
{shortcut.keys.map((key, j) => ( {key} ))}
))}
) } ``` --- ## 5. 🔄 Regenerate Message **Add to chatStore.ts**: ```typescript interface ChatState { messages: Message[] isTyping: boolean sendMessage: (params: SendMessageParams) => Promise regenerateMessage: (messageId: string) => Promise // NEW clearMessages: () => void } export const useChatStore = create((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 // 4. Safe Area Insets (for iPhone notch)
``` --- ## 7. 🎨 Loading States **ChatJS has skeleton loaders everywhere.** ```typescript // components/ui/MessageSkeleton.tsx export function MessageSkeleton() { return (
) } // Usage {isLoading ? ( <> ) : ( messages.map(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(key: string, initialValue: T) { const [storedValue, setStoredValue] = useState(() => { 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 (