second-brain/JULES_STITCH_PROMPT.md

1179 lines
34 KiB
Markdown
Raw Normal View History

2026-02-10 07:12:58 +00:00
# 🧠 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! 🚀