# 🧠 Second Brain Frontend Rebuild - Jules + Stitch Prompt

## Mission Brief

You are **Jules**, working with **Stitch** to rebuild the Second Brain frontend application. The existing code has critical UX/UI issues that make it feel unprofessional. Your job is to transform it into a production-grade chat/RAG interface matching the quality of ChatGPT, Claude, and Perplexity.

---

## 🎯 Repository Information

**Current Codebase:** Uploaded ZIP file contains existing project  
**Target:** https://github.com/DFFM-maker/second-brain

**Project Structure:**
```
second-brain/
├── client/          ← FOCUS HERE (frontend)
└── server/          ← DON'T TOUCH (backend works fine)
```

---

## 📊 Current State Analysis

### Tech Stack (Already Configured)
- React 19.2.0 + Vite 7.2.4
- TypeScript 5.9.3 (strict mode)
- Tailwind CSS 3.4.19
- React Router DOM 7.13.0
- clsx + tailwind-merge

### What's Working
✅ Base layout with sidebar  
✅ Chat input positioned at bottom  
✅ API integration functional  
✅ Dark mode theme configured  
✅ Basic routing setup

### Critical Problems Found

**ChatPage.tsx (lines 1-160):**
- ❌ No animated typing indicator (just spinning icon)
- ❌ Zero timestamps on messages
- ❌ Plain text rendering (no markdown support)
- ❌ Poor User vs AI visual distinction
- ❌ Native `<select>` for models (ugly)
- ❌ No file upload capability

**Sidebar.tsx (lines 135-163):**
- ❌ "Recents" and "History" sections always empty
- ❌ No empty state designs (looks broken)
- ❌ Mobile menu exists but needs improvement

**Missing Entirely:**
- ❌ Error boundaries
- ❌ Loading skeletons
- ❌ Toast notifications
- ❌ Keyboard shortcuts
- ❌ Proper state management

---

## 🚀 Implementation Plan

### Phase 1: Dependencies (5 min)

Install required packages:

```bash
npm install lucide-react zustand axios react-hook-form zod sonner framer-motion react-markdown remark-gfm react-syntax-highlighter reactflow recharts @types/react-syntax-highlighter -D
```

### Phase 2: Folder Structure (5 min)

Create new directories:

```bash
mkdir -p src/hooks src/stores src/types
```

Expected structure:
```
src/
├── components/
│   ├── chat/
│   │   ├── ChatInput.tsx          ✅ EXISTS - needs file upload
│   │   ├── ChatMessage.tsx        ✅ EXISTS - needs markdown
│   │   ├── ContextSidebar.tsx     ✅ EXISTS
│   │   ├── TypingIndicator.tsx    🆕 CREATE
│   │   ├── ModelSelector.tsx      🆕 CREATE
│   │   ├── RAGToggle.tsx          🆕 CREATE
│   │   └── MessageMarkdown.tsx    🆕 CREATE
│   ├── layout/
│   │   ├── Header.tsx             ✅ EXISTS
│   │   ├── MainLayout.tsx         ✅ EXISTS
│   │   └── Sidebar.tsx            ✅ EXISTS - add empty states
│   └── ui/
│       ├── Button.tsx             ✅ EXISTS
│       ├── EmptyState.tsx         🆕 CREATE
│       ├── Skeleton.tsx           🆕 CREATE
│       ├── ErrorBoundary.tsx      🆕 CREATE
│       └── Toast.tsx              🆕 CREATE
├── hooks/
│   ├── useChat.ts                 🆕 CREATE
│   ├── useMediaQuery.ts           🆕 CREATE
│   └── useKeyboardShortcuts.ts    🆕 CREATE
├── stores/
│   ├── chatStore.ts               🆕 CREATE
│   └── uiStore.ts                 🆕 CREATE
└── types/
    ├── chat.ts                    🆕 CREATE
    ├── document.ts                🆕 CREATE
    └── api.ts                     🆕 CREATE
```

---

## 💻 Code Implementation

### STEP 1: Create Type Definitions

**File:** `src/types/chat.ts`

```typescript
export interface Message {
  id: string
  role: 'user' | 'assistant'
  content: string
  timestamp: Date
  sources?: string[]
}

export interface ChatState {
  messages: Message[]
  isTyping: boolean
  sendMessage: (params: SendMessageParams) => Promise<void>
  clearMessages: () => void
}

export interface SendMessageParams {
  content: string
  files?: File[]
  model: string
  useRag: boolean
}
```

---

### STEP 2: Create Zustand Store

**File:** `src/stores/chatStore.ts`

```typescript
import { create } from 'zustand'
import { Message, ChatState, SendMessageParams } from '../types/chat'

export const useChatStore = create<ChatState>((set, get) => ({
  messages: [{
    id: '1',
    role: 'assistant',
    content: "Welcome. I've indexed your workspace. What would you like to synthesize today?",
    timestamp: new Date(),
    sources: []
  }],
  isTyping: false,

  sendMessage: async ({ content, files, model, useRag }: SendMessageParams) => {
    // Add user message
    const userMessage: Message = {
      id: Date.now().toString(),
      role: 'user',
      content,
      timestamp: new Date(),
    }
    
    set(state => ({ messages: [...state.messages, userMessage] }))
    set({ isTyping: true })

    try {
      const res = await fetch('/api/chat', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ message: content, model, useRag }),
      })

      if (!res.ok) throw new Error('Chat request failed')

      const data = await res.json()

      const aiMessage: Message = {
        id: (Date.now() + 1).toString(),
        role: 'assistant',
        content: data.answer,
        timestamp: new Date(),
        sources: data.sources,
      }

      set(state => ({ messages: [...state.messages, aiMessage] }))
    } catch (err) {
      console.error('Chat error:', err)
      
      const errorMessage: Message = {
        id: (Date.now() + 1).toString(),
        role: 'assistant',
        content: 'Error connecting to brain. Please try again.',
        timestamp: new Date(),
      }
      
      set(state => ({ messages: [...state.messages, errorMessage] }))
    } finally {
      set({ isTyping: false })
    }
  },

  clearMessages: () => set({
    messages: [{
      id: '1',
      role: 'assistant',
      content: "Welcome. I've indexed your workspace. What would you like to synthesize today?",
      timestamp: new Date(),
      sources: []
    }]
  }),
}))
```

**File:** `src/stores/uiStore.ts`

```typescript
import { create } from 'zustand'
import { persist } from 'zustand/middleware'

interface UIState {
  sidebarOpen: boolean
  toggleSidebar: () => void
  closeSidebar: () => void
  openSidebar: () => void
}

export const useUIStore = create<UIState>()(
  persist(
    (set) => ({
      sidebarOpen: false,
      toggleSidebar: () => set(state => ({ sidebarOpen: !state.sidebarOpen })),
      closeSidebar: () => set({ sidebarOpen: false }),
      openSidebar: () => set({ sidebarOpen: true }),
    }),
    { name: 'ui-storage' }
  )
)
```

---

### STEP 3: Create TypingIndicator Component

**File:** `src/components/chat/TypingIndicator.tsx`

```typescript
export function TypingIndicator() {
  return (
    <div className="flex items-start gap-3 px-6 py-4 max-w-4xl">
      <div className="w-8 h-8 rounded-full bg-zinc-800 flex items-center justify-center flex-shrink-0">
        <span className="material-symbols-outlined text-zinc-400 text-sm">smart_toy</span>
      </div>
      <div className="flex flex-col gap-2 mt-1">
        <span className="text-xs font-semibold text-zinc-500 uppercase tracking-wider">
          Assistant
        </span>
        <div className="flex gap-1.5 items-center">
          <div 
            className="w-2 h-2 bg-zinc-600 rounded-full animate-bounce" 
            style={{ animationDelay: '0ms', animationDuration: '1s' }} 
          />
          <div 
            className="w-2 h-2 bg-zinc-600 rounded-full animate-bounce" 
            style={{ animationDelay: '150ms', animationDuration: '1s' }} 
          />
          <div 
            className="w-2 h-2 bg-zinc-600 rounded-full animate-bounce" 
            style={{ animationDelay: '300ms', animationDuration: '1s' }} 
          />
        </div>
      </div>
    </div>
  )
}
```

---

### STEP 4: Create ModelSelector Component

**File:** `src/components/chat/ModelSelector.tsx`

```typescript
import { useState, useRef, useEffect } from 'react'
import { ChevronDown } from 'lucide-react'
import { cn } from '../../lib/utils'

const OLLAMA_MODELS = [
  'LLAMA3:8B',
  'MINIMAX-M2:1CLOUD',
  'MINICPM-V:LATEST',
  'GLM-4.6:CLOUD',
  'MISTRAL-7B-INSTRUCT-V0.3-Q5_K_M',
  'QWEN2.5-CODER:14B-INSTRUCT-Q4_K_M',
  'QWEN2.5-CODER:7B',
  'NOMIC-EMBED-TEXT:LATEST',
]

interface ModelSelectorProps {
  value: string
  onChange: (model: string) => void
}

export function ModelSelector({ value, onChange }: ModelSelectorProps) {
  const [isOpen, setIsOpen] = useState(false)
  const [models, setModels] = useState<string[]>(OLLAMA_MODELS)
  const dropdownRef = useRef<HTMLDivElement>(null)

  // Fetch models from API
  useEffect(() => {
    fetch('/api/models')
      .then(res => res.json())
      .then(data => {
        if (Array.isArray(data) && data.length > 0) {
          setModels(data)
        }
      })
      .catch(err => console.error('Failed to fetch models:', err))
  }, [])

  // Close on click outside
  useEffect(() => {
    const handleClick = (e: MouseEvent) => {
      if (dropdownRef.current && !dropdownRef.current.contains(e.target as Node)) {
        setIsOpen(false)
      }
    }
    document.addEventListener('mousedown', handleClick)
    return () => document.removeEventListener('mousedown', handleClick)
  }, [])

  return (
    <div className="relative" ref={dropdownRef}>
      <button
        onClick={() => setIsOpen(!isOpen)}
        className="flex items-center gap-2 px-3 py-2 bg-zinc-900 border border-zinc-700 rounded-lg hover:bg-zinc-800 transition-colors text-sm font-medium"
      >
        <span className="material-symbols-outlined text-primary !text-[18px]">psychology</span>
        <span className="text-zinc-200">{value}</span>
        <ChevronDown className={cn(
          "w-4 h-4 text-zinc-400 transition-transform",
          isOpen && "rotate-180"
        )} />
      </button>

      {isOpen && (
        <div className="absolute top-full left-0 mt-2 w-80 bg-zinc-900 border border-zinc-700 rounded-lg shadow-xl z-50 max-h-80 overflow-y-auto">
          {models.map((model) => (
            <button
              key={model}
              onClick={() => {
                onChange(model)
                setIsOpen(false)
              }}
              className={cn(
                "w-full px-4 py-2.5 text-left text-sm transition-colors hover:bg-zinc-800",
                value === model && "bg-primary/10 text-primary font-semibold"
              )}
            >
              {model}
            </button>
          ))}
        </div>
      )}
    </div>
  )
}
```

---

### STEP 5: Create RAGToggle Component

**File:** `src/components/chat/RAGToggle.tsx`

```typescript
import { cn } from '../../lib/utils'

interface RAGToggleProps {
  enabled: boolean
  onChange: (enabled: boolean) => void
}

export function RAGToggle({ enabled, onChange }: RAGToggleProps) {
  return (
    <button
      onClick={() => onChange(!enabled)}
      className={cn(
        "flex items-center gap-2 px-3 py-2 rounded-lg border transition-all text-xs font-bold uppercase tracking-wider",
        enabled
          ? "bg-primary/10 border-primary/30 text-primary"
          : "bg-zinc-900 border-zinc-700 text-zinc-400 hover:bg-zinc-800"
      )}
    >
      <span className="material-symbols-outlined !text-[18px]">
        {enabled ? 'database' : 'chat_bubble'}
      </span>
      <span>{enabled ? 'BRAIN ON' : 'CHAT ONLY'}</span>
    </button>
  )
}
```

---

### STEP 6: Update ChatMessage Component

**File:** `src/components/chat/ChatMessage.tsx` (REPLACE EXISTING)

```typescript
import ReactMarkdown from 'react-markdown'
import remarkGfm from 'remark-gfm'
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'
import { vscDarkPlus } from 'react-syntax-highlighter/dist/esm/styles/prism'
import { Message } from '../../types/chat'

interface ChatMessageProps {
  message: Message
}

export function ChatMessage({ message }: ChatMessageProps) {
  const isUser = message.role === 'user'

  return (
    <div className={`flex gap-3 max-w-4xl px-6 py-4 ${isUser ? 'ml-auto flex-row-reverse' : 'mr-auto'}`}>
      {/* Avatar */}
      <div className={`w-8 h-8 rounded-full flex items-center justify-center flex-shrink-0 ${
        isUser ? 'bg-primary' : 'bg-zinc-800'
      }`}>
        <span className="material-symbols-outlined text-white text-sm">
          {isUser ? 'person' : 'smart_toy'}
        </span>
      </div>

      {/* Content */}
      <div className="flex-1 flex flex-col gap-2">
        {/* Header with timestamp */}
        <div className={`flex items-center gap-2 ${isUser ? 'justify-end' : 'justify-start'}`}>
          <span className="text-xs font-semibold text-zinc-500 uppercase tracking-wider">
            {isUser ? 'You' : 'Assistant'}
          </span>
          <span className="text-xs text-zinc-600">
            {formatTimestamp(message.timestamp)}
          </span>
        </div>

        {/* Message bubble */}
        <div className={`rounded-2xl px-4 py-3 ${
          isUser
            ? 'bg-primary text-white rounded-tr-none'
            : 'bg-zinc-900 text-zinc-100 border border-zinc-800 rounded-tl-none'
        }`}>
          {isUser ? (
            <p className="whitespace-pre-wrap text-[15px] leading-relaxed">{message.content}</p>
          ) : (
            <ReactMarkdown
              remarkPlugins={[remarkGfm]}
              className="prose prose-invert prose-sm max-w-none"
              components={{
                code({ node, inline, className, children, ...props }) {
                  const match = /language-(\w+)/.exec(className || '')
                  return !inline && match ? (
                    <SyntaxHighlighter
                      style={vscDarkPlus}
                      language={match[1]}
                      PreTag="div"
                      className="rounded-lg my-2"
                      {...props}
                    >
                      {String(children).replace(/\n$/, '')}
                    </SyntaxHighlighter>
                  ) : (
                    <code className="bg-zinc-800 px-1.5 py-0.5 rounded text-sm font-mono" {...props}>
                      {children}
                    </code>
                  )
                },
                a({ node, children, ...props }) {
                  return (
                    <a className="text-primary hover:underline" {...props}>
                      {children}
                    </a>
                  )
                },
              }}
            >
              {message.content}
            </ReactMarkdown>
          )}

          {/* Sources */}
          {message.sources && message.sources.length > 0 && (
            <div className="mt-3 pt-3 border-t border-zinc-700">
              <p className="text-xs font-bold text-zinc-400 mb-2">SOURCES</p>
              <div className="flex flex-wrap gap-2">
                {message.sources.map((source, i) => (
                  <span
                    key={i}
                    className="flex items-center gap-1.5 bg-zinc-800 px-2 py-1 rounded text-xs text-zinc-400 border border-zinc-700"
                  >
                    <span className="material-symbols-outlined !text-xs">description</span>
                    {source}
                  </span>
                ))}
              </div>
            </div>
          )}
        </div>
      </div>
    </div>
  )
}

// Helper function
function formatTimestamp(date: Date): string {
  const now = new Date()
  const diff = now.getTime() - date.getTime()
  const minutes = Math.floor(diff / 60000)
  
  if (minutes < 1) return 'just now'
  if (minutes < 60) return `${minutes}m ago`
  if (minutes < 1440) return `${Math.floor(minutes / 60)}h ago`
  return date.toLocaleDateString()
}
```

---

### STEP 7: Improve ChatInput with File Upload

**File:** `src/components/chat/ChatInput.tsx` (REPLACE EXISTING)

```typescript
import { useState, useRef, KeyboardEvent } from 'react'
import { Send, Paperclip, X } from 'lucide-react'

interface ChatInputProps {
  onSend: (content: string, files?: File[]) => void
  disabled?: boolean
}

export function ChatInput({ onSend, disabled }: ChatInputProps) {
  const [input, setInput] = useState('')
  const [files, setFiles] = useState<File[]>([])
  const fileInputRef = useRef<HTMLInputElement>(null)
  const textareaRef = useRef<HTMLTextAreaElement>(null)

  const handleSend = () => {
    if (!input.trim() && files.length === 0) return
    onSend(input, files)
    setInput('')
    setFiles([])
  }

  const handleKeyDown = (e: KeyboardEvent<HTMLTextAreaElement>) => {
    if (e.key === 'Enter' && !e.shiftKey) {
      e.preventDefault()
      handleSend()
    }
  }

  const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    if (e.target.files) {
      setFiles(prev => [...prev, ...Array.from(e.target.files!)])
    }
  }

  return (
    <div className="max-w-3xl mx-auto w-full space-y-2">
      {/* File Preview */}
      {files.length > 0 && (
        <div className="flex flex-wrap gap-2">
          {files.map((file, idx) => (
            <div
              key={idx}
              className="flex items-center gap-2 px-3 py-2 bg-zinc-900 border border-zinc-700 rounded-lg"
            >
              <Paperclip className="w-4 h-4 text-zinc-400" />
              <span className="text-sm text-zinc-300">{file.name}</span>
              <button
                onClick={() => setFiles(files.filter((_, i) => i !== idx))}
                className="text-zinc-500 hover:text-zinc-300 transition-colors"
              >
                <X className="w-4 h-4" />
              </button>
            </div>
          ))}
        </div>
      )}

      {/* Input Container */}
      <div className="backdrop-blur-xl bg-zinc-900/50 border border-zinc-700 rounded-2xl p-3 shadow-2xl">
        {/* Textarea */}
        <div className="relative">
          <textarea
            ref={textareaRef}
            value={input}
            onChange={(e) => setInput(e.target.value)}
            onKeyDown={handleKeyDown}
            placeholder="Ask your brain anything..."
            disabled={disabled}
            className="w-full bg-transparent border-none focus:ring-0 text-zinc-100 placeholder-zinc-500 resize-none text-[15px] max-h-48 pr-10"
            rows={1}
            style={{
              minHeight: '52px',
              overflowY: input.split('\n').length > 3 ? 'auto' : 'hidden'
            }}
          />

          {/* File Upload Button */}
          <button
            onClick={() => fileInputRef.current?.click()}
            disabled={disabled}
            className="absolute right-2 top-1/2 -translate-y-1/2 text-zinc-500 hover:text-zinc-300 disabled:opacity-50 transition-colors"
          >
            <Paperclip className="w-5 h-5" />
          </button>
          <input
            ref={fileInputRef}
            type="file"
            multiple
            onChange={handleFileChange}
            className="hidden"
          />
        </div>

        {/* Bottom Bar */}
        <div className="flex items-center justify-between mt-2 pt-2 border-t border-zinc-800">
          <span className="text-xs text-zinc-600">
            {input.length} characters • Shift+Enter for new line
          </span>
          
          <button
            onClick={handleSend}
            disabled={(!input.trim() && files.length === 0) || disabled}
            className="bg-primary hover:bg-primary/90 disabled:opacity-50 disabled:cursor-not-allowed text-white p-2.5 rounded-xl transition-all shadow-lg shadow-primary/20"
          >
            <Send className="w-5 h-5" />
          </button>
        </div>
      </div>
    </div>
  )
}
```

---

### STEP 8: Refactor ChatPage

**File:** `src/pages/ChatPage.tsx` (REPLACE EXISTING)

```typescript
import { useState, useRef, useEffect } from 'react'
import { useChatStore } from '../stores/chatStore'
import { ChatMessage } from '../components/chat/ChatMessage'
import { ChatInput } from '../components/chat/ChatInput'
import { ModelSelector } from '../components/chat/ModelSelector'
import { RAGToggle } from '../components/chat/RAGToggle'
import { TypingIndicator } from '../components/chat/TypingIndicator'

export function ChatPage() {
  const { messages, isTyping, sendMessage } = useChatStore()
  const [selectedModel, setSelectedModel] = useState('llama3:8b')
  const [useRag, setUseRag] = useState(true)
  const bottomRef = useRef<HTMLDivElement>(null)

  // Auto-scroll to bottom on new messages
  useEffect(() => {
    bottomRef.current?.scrollIntoView({ behavior: 'smooth' })
  }, [messages, isTyping])

  const handleSend = async (content: string, files?: File[]) => {
    await sendMessage({
      content,
      files,
      model: selectedModel,
      useRag
    })
  }

  return (
    <div className="flex flex-col h-full relative">
      {/* Top Bar */}
      <div className="border-b border-zinc-800 px-6 py-4 flex items-center justify-between flex-shrink-0">
        <div className="flex items-center gap-3">
          <ModelSelector value={selectedModel} onChange={setSelectedModel} />
          <RAGToggle enabled={useRag} onChange={setUseRag} />
        </div>
      </div>

      {/* Messages Area - Scrollable */}
      <div className="flex-1 overflow-y-auto custom-scrollbar">
        <div className="py-8">
          {messages.map((msg) => (
            <ChatMessage key={msg.id} message={msg} />
          ))}
          
          {isTyping && <TypingIndicator />}
          
          <div ref={bottomRef} />
        </div>
      </div>

      {/* Input Area - FIXED BOTTOM */}
      <div className="border-t border-zinc-800 p-4 bg-gradient-to-t from-zinc-950 to-transparent flex-shrink-0">
        <ChatInput onSend={handleSend} disabled={isTyping} />
      </div>
    </div>
  )
}
```

---

### STEP 9: Add Empty States to Sidebar

**File:** `src/components/layout/Sidebar.tsx` (UPDATE EXISTING)

Find the "Recents" section (around line 135) and replace with:

```typescript
{/* Recents */}
<div className="flex flex-col gap-1 px-2 mb-6">
  <p className="px-3 text-[11px] font-bold text-zinc-500 uppercase tracking-widest mb-2">
    Recents
  </p>
  {recents.length === 0 ? (
    <div className="px-3 py-8 text-center">
      <span className="material-symbols-outlined text-zinc-700 !text-4xl mb-2 block">description</span>
      <p className="text-xs text-zinc-600 mb-1">No recent documents</p>
      <p className="text-xs text-zinc-700">Upload files to get started</p>
    </div>
  ) : (
    recents.map((item) => (
      <div
        key={item.name}
        className="flex items-center gap-3 px-3 py-2 rounded-lg cursor-pointer transition-all hover:bg-zinc-900 text-zinc-500 hover:text-white"
      >
        <span className="material-symbols-outlined !text-[20px]">{item.icon}</span>
        <p className="text-sm font-medium truncate flex-1">{item.name}</p>
      </div>
    ))
  )}
</div>
```

Find the "History" section (around line 155) and replace with:

```typescript
{/* History */}
<div className="flex flex-col gap-1 px-2 flex-1 overflow-y-auto">
  <p className="px-3 text-[11px] font-bold text-zinc-500 uppercase tracking-widest mb-2">
    History
  </p>
  {chats.length === 0 ? (
    <div className="px-3 py-8 text-center">
      <span className="material-symbols-outlined text-zinc-700 !text-4xl mb-2 block">chat_bubble</span>
      <p className="text-xs text-zinc-600 mb-1">No chat history</p>
      <p className="text-xs text-zinc-700">Start chatting to see history</p>
    </div>
  ) : (
    chats.map((chat) => (
      <div
        key={chat.id}
        className="flex items-center gap-3 px-3 py-1.5 text-zinc-500 hover:text-white cursor-pointer text-sm transition-colors group"
      >
        <span className="material-symbols-outlined !text-[18px] group-hover:text-primary transition-colors">
          {chat.icon}
        </span>
        <span className="truncate">{chat.name}</span>
      </div>
    ))
  )}
</div>
```

---

### STEP 10: Create UI Helper Components

**File:** `src/components/ui/EmptyState.tsx`

```typescript
import { LucideIcon } from 'lucide-react'

interface EmptyStateProps {
  icon: LucideIcon
  title: string
  description?: string
  action?: React.ReactNode
}

export function EmptyState({ icon: Icon, title, description, action }: EmptyStateProps) {
  return (
    <div className="flex flex-col items-center justify-center h-full py-12 text-center px-4">
      <Icon className="w-16 h-16 text-zinc-700 mb-4" />
      <h3 className="text-lg font-semibold text-zinc-300 mb-2">{title}</h3>
      {description && <p className="text-sm text-zinc-500 mb-6 max-w-md">{description}</p>}
      {action}
    </div>
  )
}
```

**File:** `src/components/ui/Skeleton.tsx`

```typescript
import { cn } from '../../lib/utils'

interface SkeletonProps {
  className?: string
}

export function Skeleton({ className }: SkeletonProps) {
  return (
    <div className={cn("animate-pulse bg-zinc-800 rounded", className)} />
  )
}
```

**File:** `src/components/ui/ErrorBoundary.tsx`

```typescript
import { Component, ErrorInfo, ReactNode } from 'react'
import { AlertTriangle } from 'lucide-react'

interface Props {
  children: ReactNode
}

interface State {
  hasError: boolean
  error?: Error
}

export class ErrorBoundary extends Component<Props, State> {
  state: State = { hasError: false }

  static getDerivedStateFromError(error: Error): State {
    return { hasError: true, error }
  }

  componentDidCatch(error: Error, errorInfo: ErrorInfo) {
    console.error('ErrorBoundary caught:', error, errorInfo)
  }

  render() {
    if (this.state.hasError) {
      return (
        <div className="min-h-screen flex items-center justify-center bg-zinc-950 p-4">
          <div className="max-w-md w-full text-center">
            <AlertTriangle className="w-16 h-16 text-red-500 mx-auto mb-4" />
            <h1 className="text-2xl font-bold text-zinc-100 mb-2">Something went wrong</h1>
            <p className="text-zinc-400 mb-6">
              {this.state.error?.message || 'An unexpected error occurred'}
            </p>
            <button
              onClick={() => window.location.reload()}
              className="px-6 py-3 bg-primary text-white rounded-lg hover:bg-primary/90 transition-colors"
            >
              Reload Page
            </button>
          </div>
        </div>
      )
    }

    return this.props.children
  }
}
```

**File:** `src/components/ui/Toast.tsx`

```typescript
import { Toaster } from 'sonner'

export function ToastProvider() {
  return (
    <Toaster
      position="bottom-right"
      theme="dark"
      toastOptions={{
        className: 'bg-zinc-900 border border-zinc-700 text-zinc-100',
      }}
    />
  )
}
```

---

### STEP 11: Add Toast Provider to App

**File:** `src/main.tsx` (UPDATE)

```typescript
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App.tsx'
import { ToastProvider } from './components/ui/Toast'
import './index.css'

ReactDOM.createRoot(document.getElementById('root')!).render(
  <React.StrictMode>
    <App />
    <ToastProvider />
  </React.StrictMode>,
)
```

---

### STEP 12: Add Error Boundary to App

**File:** `src/App.tsx` (UPDATE)

```typescript
import { BrowserRouter, Routes, Route } from "react-router-dom"
import { MainLayout } from "./components/layout/MainLayout"
import { ChatPage } from "./pages/ChatPage"
import { LibraryPage } from "./pages/LibraryPage"
import { APIPage } from "./pages/APIPage"
import { NeuralMapPage } from "./pages/NeuralMapPage"
import { ErrorBoundary } from "./components/ui/ErrorBoundary"

function App() {
  return (
    <ErrorBoundary>
      <BrowserRouter>
        <Routes>
          <Route path="/" element={<MainLayout />}>
            <Route index element={<ChatPage />} />
            <Route path="library" element={<LibraryPage />} />
            <Route path="api-management" element={<APIPage />} />
            <Route path="neural-map" element={<NeuralMapPage />} />
          </Route>
        </Routes>
      </BrowserRouter>
    </ErrorBoundary>
  )
}

export default App
```

---

### STEP 13: Create Keyboard Shortcuts Hook

**File:** `src/hooks/useKeyboardShortcuts.ts`

```typescript
import { useEffect } from 'react'
import { useNavigate } from 'react-router-dom'
import { useChatStore } from '../stores/chatStore'

export function useKeyboardShortcuts() {
  const navigate = useNavigate()
  const { clearMessages } = useChatStore()

  useEffect(() => {
    const handleKeyDown = (e: KeyboardEvent) => {
      const isMod = e.metaKey || e.ctrlKey

      // Cmd/Ctrl + N: New Chat
      if (isMod && e.key === 'n') {
        e.preventDefault()
        clearMessages()
        navigate('/')
      }

      // Cmd/Ctrl + L: Library
      if (isMod && e.key === 'l') {
        e.preventDefault()
        navigate('/library')
      }

      // Cmd/Ctrl + M: Neural Map
      if (isMod && e.key === 'm') {
        e.preventDefault()
        navigate('/neural-map')
      }
    }

    window.addEventListener('keydown', handleKeyDown)
    return () => window.removeEventListener('keydown', handleKeyDown)
  }, [navigate, clearMessages])
}
```

Then add to `App.tsx`:

```typescript
import { useKeyboardShortcuts } from './hooks/useKeyboardShortcuts'

function App() {
  useKeyboardShortcuts() // Add this line
  
  return (
    // ... rest
  )
}
```

---

### STEP 14: Create Media Query Hook

**File:** `src/hooks/useMediaQuery.ts`

```typescript
import { useState, useEffect } from 'react'

export function useMediaQuery(query: string): boolean {
  const [matches, setMatches] = useState(false)

  useEffect(() => {
    const media = window.matchMedia(query)
    if (media.matches !== matches) {
      setMatches(media.matches)
    }

    const listener = () => setMatches(media.matches)
    media.addEventListener('change', listener)
    return () => media.removeEventListener('change', listener)
  }, [matches, query])

  return matches
}
```

---

### STEP 15: Improve Mobile Menu in Header

**File:** `src/components/layout/Header.tsx` (UPDATE if exists, or skip if basic)

Add hamburger button for mobile:

```typescript
import { Menu } from 'lucide-react'
import { useUIStore } from '../../stores/uiStore'

export function Header() {
  const { toggleSidebar } = useUIStore()

  return (
    <header className="border-b border-zinc-800 px-4 py-3 flex items-center gap-4 flex-shrink-0">
      {/* Mobile hamburger */}
      <button
        onClick={toggleSidebar}
        className="md:hidden text-zinc-400 hover:text-zinc-100 transition-colors"
      >
        <Menu className="w-6 h-6" />
      </button>

      {/* Search */}
      <div className="flex-1 max-w-2xl mx-auto">
        <input
          type="text"
          placeholder="Search notes..."
          className="w-full bg-zinc-900 border border-zinc-700 rounded-lg px-4 py-2 text-sm text-zinc-100 placeholder-zinc-500 focus:outline-none focus:ring-2 focus:ring-primary/50 transition-all"
        />
      </div>
    </header>
  )
}
```

---

## ✅ Testing Checklist

After implementation, verify:

### Chat Interface
- [ ] Typing indicator shows 3 animated dots when AI is responding
- [ ] Timestamps appear on all messages in relative format ("2m ago")
- [ ] Markdown renders correctly (bold, italic, lists, code blocks)
- [ ] Code blocks have syntax highlighting
- [ ] User messages: blue background, right-aligned
- [ ] AI messages: zinc background, left-aligned, show sources
- [ ] Input stays at bottom, always visible
- [ ] File upload shows preview chips with remove button
- [ ] Auto-scroll works smoothly on new messages

### Model Selection
- [ ] Dropdown opens/closes correctly
- [ ] Selected model is highlighted
- [ ] Clicking outside closes dropdown
- [ ] Models load from API if available

### RAG Toggle
- [ ] Visual difference clear between ON/OFF states
- [ ] Sources show when RAG is ON
- [ ] No sources when RAG is OFF

### Sidebar
- [ ] Empty states show professional designs (not blank)
- [ ] Hamburger menu works on mobile (<768px)
- [ ] Sidebar overlays with backdrop on mobile
- [ ] Sidebar always visible on desktop (≥768px)
- [ ] Recents populate when documents exist
- [ ] History populates when chats exist

### Responsive
- [ ] Test on 320px width (mobile)
- [ ] Test on 768px width (tablet)
- [ ] Test on 1024px+ width (desktop)
- [ ] No horizontal scroll at any breakpoint
- [ ] All interactive elements are tappable on mobile

### Functionality
- [ ] Keyboard shortcuts work (Cmd+N, Cmd+L, Cmd+M)
- [ ] Error boundary catches errors
- [ ] Toast notifications appear
- [ ] No console errors
- [ ] TypeScript compiles without errors

---

## 🎯 Success Criteria

The rebuild is complete when:

1. **Chat feels professional** - Like ChatGPT/Claude, not a prototype
2. **Typing indicator is animated** - 3 bouncing dots, not just spinning icon
3. **Timestamps are visible** - Every message shows time
4. **Markdown works perfectly** - Code highlighting, proper formatting
5. **Empty states look good** - No blank sections anywhere
6. **Mobile is fully functional** - Hamburger menu, responsive layout
7. **No errors** - Console clean, TypeScript compiles
8. **Smooth UX** - Fast, responsive, professional

---

## 🚨 Critical Notes

- **DO NOT TOUCH** anything in `/server` directory
- **PRESERVE** existing API endpoints (they work fine)
- **REUSE** existing Button, Card, Input components where possible
- **USE** Material Symbols icons (already loaded) + Lucide for new icons
- **DARK MODE ONLY** - No light mode needed
- **KEEP** existing color palette from tailwind.config.js

---

## 📝 Jules Instructions

1. Start by installing dependencies
2. Create folder structure (hooks, stores, types)
3. Build components in this order:
   - Types → Stores → TypingIndicator → ModelSelector → RAGToggle
   - Update ChatMessage → Update ChatInput → Refactor ChatPage
   - Add empty states to Sidebar
   - Create helper components (EmptyState, Skeleton, ErrorBoundary)
4. Test everything thoroughly
5. Confirm all checklist items pass

Use **Stitch** for code generation. Work incrementally, test as you go. Focus on quality over speed.

Good luck! 🚀
