diff --git a/CHATJS_BEST_PRACTICES.md b/CHATJS_BEST_PRACTICES.md new file mode 100644 index 0000000..c6d99b2 --- /dev/null +++ b/CHATJS_BEST_PRACTICES.md @@ -0,0 +1,671 @@ +# 🚀 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 ( +