Frontend Architecture¶
This document describes the React frontend architecture of AI Code UI.
Technology Stack¶
- React 18.2 - UI library
- Vite 7.3 - Build tool and dev server
- Tailwind CSS 3.4 - Utility-first CSS
- React Router 6.8 - Client-side routing
- CodeMirror 6 - Code editor
- xterm.js 5.5 - Terminal emulator
- i18next 25.8 - Internationalization framework
- react-i18next 16.5 - React bindings for i18next
Directory Structure¶
src/
├── main.jsx # Application entry point
├── App.jsx # Root component
├── index.css # Global styles
│
├── components/ # React components
│ ├── ChatInterface.jsx # Main chat UI
│ ├── Sidebar.jsx # Navigation sidebar
│ ├── Settings.jsx # Settings panel
│ ├── FileTree.jsx # File browser
│ ├── CodeEditor.jsx # Code editor
│ ├── Shell.jsx # Terminal
│ ├── GitPanel.jsx # Git operations
│ ├── MainContent.jsx # Content router
│ ├── ThinkingModeSelector.jsx # Extended thinking mode selector
│ └── ui/ # Reusable UI components
│
├── contexts/ # React contexts
│ ├── AuthContext.jsx
│ ├── WebSocketContext.jsx
│ ├── ThemeContext.jsx
│ └── TaskMasterContext.jsx
│
├── hooks/ # Custom hooks
│ ├── useLocalStorage.jsx
│ ├── useAudioRecorder.js
│ └── useVersionCheck.js
│
├── i18n/ # Internationalization
│ ├── config.js # i18next configuration
│ ├── languages.js # Supported languages
│ └── locales/ # Translation files
│ ├── en/ # English translations
│ │ ├── common.json
│ │ ├── chat.json
│ │ ├── settings.json
│ │ ├── sidebar.json
│ │ ├── auth.json
│ │ └── codeEditor.json
│ └── zh-CN/ # Simplified Chinese translations
│ ├── common.json
│ ├── chat.json
│ ├── settings.json
│ ├── sidebar.json
│ ├── auth.json
│ └── codeEditor.json
│
└── utils/ # Utility functions
├── api.js # REST API client
├── websocket.js # WebSocket hook
└── whisper.js # Audio transcription
Component Hierarchy¶
App.jsx
├── AuthProvider
│ └── ThemeProvider
│ └── WebSocketProvider
│ └── TaskMasterProvider
│ └── TasksSettingsProvider
│ └── Router
│ ├── /login → Login
│ ├── /register → Register
│ ├── /onboarding → Onboarding
│ └── / → MainLayout
│ ├── Sidebar
│ │ ├── ProjectList
│ │ ├── SessionList
│ │ └── QuickSettings
│ └── MainContent
│ ├── ChatInterface
│ ├── FileTree + CodeEditor
│ ├── Shell
│ ├── GitPanel
│ └── TaskList
Core Components¶
App.jsx (974 lines)¶
Root component responsibilities: - Provider composition - Routing setup - Project/session state management - Session protection system - Version checking
function App() {
const [selectedProject, setSelectedProject] = useState(null);
const [selectedSession, setSelectedSession] = useState(null);
const [isSessionActive, setIsSessionActive] = useState(false);
return (
<AuthProvider>
<ThemeProvider>
<WebSocketProvider>
<TaskMasterProvider>
<Router>
<Routes>
<Route path="/login" element={<Login />} />
<Route path="/" element={
<ProtectedRoute>
<MainLayout
selectedProject={selectedProject}
selectedSession={selectedSession}
// ...
/>
</ProtectedRoute>
} />
</Routes>
</Router>
</TaskMasterProvider>
</WebSocketProvider>
</ThemeProvider>
</AuthProvider>
);
}
ChatInterface.jsx (5,614 lines)¶
Main chat interface features: - Message rendering (text, code, tools) - Streaming response display - Tool approval UI - Token usage tracking - Message input with keyboard shortcuts - File attachments - Voice input (Whisper)
Key state:
const [messages, setMessages] = useState([]);
const [isStreaming, setIsStreaming] = useState(false);
const [pendingApprovals, setPendingApprovals] = useState([]);
const [tokenUsage, setTokenUsage] = useState(null);
Sidebar.jsx (~1,800 lines)¶
Navigation sidebar features: - Project list with provider grouping - Session list per project - Search/filter functionality - Project creation/import - Session management (create, delete, rename)
FileTree.jsx (~1,200 lines)¶
File browser features: - Recursive directory tree - File selection and preview - Context menu (create, rename, delete) - Drag and drop - Integration with CodeEditor
CodeEditor.jsx (~600 lines)¶
Code editor features: - Syntax highlighting (multiple languages) - Line numbers - Auto-save - File change detection - Multiple themes
Languages supported: - JavaScript/TypeScript - Python - HTML/CSS - JSON - Markdown - And more via CodeMirror
Shell.jsx (~400 lines)¶
Terminal emulator features: - Interactive PTY session - WebSocket communication - Terminal resizing - Copy/paste support - Custom styling
GitPanel.jsx (~1,740 lines)¶
Git operations: - Repository status - File staging/unstaging - Commit with message - Branch management - Push/pull - Diff viewing - Commit history
Settings.jsx (~2,370 lines)¶
Multi-tab settings: - General settings (theme, provider) - Git configuration - API keys management - Credentials storage - MCP servers - Account settings
State Management¶
Context-Based Architecture¶
The application uses React Context for global state:
AuthContext¶
const AuthContext = createContext();
export function AuthProvider({ children }) {
const [user, setUser] = useState(null);
const [isLoading, setIsLoading] = useState(true);
const login = async (username, password) => {
const response = await api.login(username, password);
setUser(response.user);
localStorage.setItem('token', response.token);
};
const logout = () => {
setUser(null);
localStorage.removeItem('token');
};
return (
<AuthContext.Provider value={{ user, login, logout, isLoading }}>
{children}
</AuthContext.Provider>
);
}
export const useAuth = () => useContext(AuthContext);
WebSocketContext¶
const WebSocketContext = createContext();
export function WebSocketProvider({ children }) {
const ws = useWebSocket('/ws');
return (
<WebSocketContext.Provider value={ws}>
{children}
</WebSocketContext.Provider>
);
}
export const useWS = () => useContext(WebSocketContext);
ThemeContext¶
const ThemeContext = createContext();
export function ThemeProvider({ children }) {
const [theme, setTheme] = useLocalStorage('theme', 'system');
useEffect(() => {
const root = document.documentElement;
if (theme === 'dark' ||
(theme === 'system' && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
root.classList.add('dark');
} else {
root.classList.remove('dark');
}
}, [theme]);
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
{children}
</ThemeContext.Provider>
);
}
TaskMasterContext¶
const TaskMasterContext = createContext();
export function TaskMasterProvider({ children }) {
const [projects, setProjects] = useState([]);
const [tasks, setTasks] = useState([]);
const [mcpStatus, setMcpStatus] = useState('disconnected');
// Fetch projects and tasks
// Handle MCP connection
// ...
return (
<TaskMasterContext.Provider value={{ projects, tasks, mcpStatus }}>
{children}
</TaskMasterContext.Provider>
);
}
Custom Hooks¶
useLocalStorage¶
function useLocalStorage(key, initialValue) {
const [storedValue, setStoredValue] = useState(() => {
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch (error) {
return initialValue;
}
});
const setValue = (value) => {
setStoredValue(value);
window.localStorage.setItem(key, JSON.stringify(value));
};
return [storedValue, setValue];
}
useWebSocket¶
function useWebSocket(url) {
const [ws, setWs] = useState(null);
const [isConnected, setIsConnected] = useState(false);
const [lastMessage, setLastMessage] = useState(null);
useEffect(() => {
const socket = new WebSocket(url);
socket.onopen = () => setIsConnected(true);
socket.onclose = () => {
setIsConnected(false);
// Implement reconnection
};
socket.onmessage = (event) => {
setLastMessage(JSON.parse(event.data));
};
setWs(socket);
return () => socket.close();
}, [url]);
const send = useCallback((message) => {
if (ws && isConnected) {
ws.send(JSON.stringify(message));
}
}, [ws, isConnected]);
return { isConnected, lastMessage, send };
}
useVersionCheck¶
function useVersionCheck() {
const [updateAvailable, setUpdateAvailable] = useState(false);
const [latestVersion, setLatestVersion] = useState(null);
useEffect(() => {
const checkVersion = async () => {
const response = await fetch('/api/version');
const { current, latest } = await response.json();
if (semver.gt(latest, current)) {
setUpdateAvailable(true);
setLatestVersion(latest);
}
};
checkVersion();
}, []);
return { updateAvailable, latestVersion };
}
API Communication¶
REST API Client¶
Located in src/utils/api.js:
const API_BASE = '/api';
async function fetchWithAuth(url, options = {}) {
const token = localStorage.getItem('token');
const response = await fetch(`${API_BASE}${url}`, {
...options,
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`,
...options.headers,
},
});
if (!response.ok) {
throw new Error(await response.text());
}
return response.json();
}
export const api = {
// Auth
login: (username, password) =>
fetchWithAuth('/auth/login', {
method: 'POST',
body: JSON.stringify({ username, password }),
}),
// Projects
getProjects: () => fetchWithAuth('/projects'),
createProject: (path) =>
fetchWithAuth('/projects', {
method: 'POST',
body: JSON.stringify({ path }),
}),
// Sessions
getSessions: (projectPath) =>
fetchWithAuth(`/projects/${encodeURIComponent(projectPath)}/sessions`),
// Files
getFiles: (path) => fetchWithAuth(`/files?path=${encodeURIComponent(path)}`),
getFileContent: (path) =>
fetchWithAuth(`/files/content?path=${encodeURIComponent(path)}`),
// Git
getGitStatus: (projectPath) =>
fetchWithAuth(`/git/status?projectPath=${encodeURIComponent(projectPath)}`),
// Settings
getSettings: () => fetchWithAuth('/settings'),
updateSettings: (settings) =>
fetchWithAuth('/settings', {
method: 'PUT',
body: JSON.stringify(settings),
}),
};
Styling¶
Tailwind CSS¶
Configuration in tailwind.config.js:
module.exports = {
darkMode: 'class',
content: ['./index.html', './src/**/*.{js,jsx}'],
theme: {
extend: {
colors: {
background: 'hsl(var(--background))',
foreground: 'hsl(var(--foreground))',
primary: {
DEFAULT: 'hsl(var(--primary))',
foreground: 'hsl(var(--primary-foreground))',
},
// ...
},
},
},
};
CSS Variables¶
Defined in src/index.css:
:root {
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;
--primary: 222.2 47.4% 11.2%;
--primary-foreground: 210 40% 98%;
/* ... */
}
.dark {
--background: 222.2 84% 4.9%;
--foreground: 210 40% 98%;
--primary: 210 40% 98%;
--primary-foreground: 222.2 47.4% 11.2%;
/* ... */
}
Component Styling Pattern¶
function Button({ variant = 'primary', size = 'md', children, ...props }) {
const baseClasses = 'inline-flex items-center justify-center rounded-md font-medium';
const variantClasses = {
primary: 'bg-primary text-primary-foreground hover:bg-primary/90',
secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
outline: 'border border-input bg-background hover:bg-accent',
};
const sizeClasses = {
sm: 'h-8 px-3 text-sm',
md: 'h-10 px-4',
lg: 'h-12 px-6 text-lg',
};
return (
<button
className={`${baseClasses} ${variantClasses[variant]} ${sizeClasses[size]}`}
{...props}
>
{children}
</button>
);
}
Build Configuration¶
Vite Config¶
// vite.config.js
export default defineConfig({
plugins: [react()],
server: {
port: 5173,
proxy: {
'/api': 'http://localhost:3001',
'/ws': {
target: 'ws://localhost:3001',
ws: true,
},
'/shell': {
target: 'ws://localhost:3001',
ws: true,
},
},
},
build: {
outDir: 'dist',
rollupOptions: {
output: {
manualChunks: {
'vendor-react': ['react', 'react-dom', 'react-router-dom'],
'vendor-codemirror': [
'@codemirror/view',
'@codemirror/state',
'@codemirror/commands',
],
'vendor-xterm': ['xterm', 'xterm-addon-fit'],
},
},
},
},
});
Code Splitting¶
The build produces these chunks:
- vendor-react.js - React ecosystem
- vendor-codemirror.js - Code editor
- vendor-xterm.js - Terminal
- index.js - Application code
Performance Optimization¶
React.memo¶
const MessageItem = React.memo(function MessageItem({ message, onAction }) {
return (
<div className="message">
{/* ... */}
</div>
);
}, (prevProps, nextProps) => {
return prevProps.message.id === nextProps.message.id;
});
useMemo and useCallback¶
function ChatInterface({ messages, onSend }) {
const sortedMessages = useMemo(
() => messages.sort((a, b) => a.timestamp - b.timestamp),
[messages]
);
const handleSend = useCallback((text) => {
onSend({ text, timestamp: Date.now() });
}, [onSend]);
return (/* ... */);
}
Virtualization¶
For long lists (messages, files), consider virtualization:
import { useVirtualizer } from '@tanstack/react-virtual';
function MessageList({ messages }) {
const parentRef = useRef();
const virtualizer = useVirtualizer({
count: messages.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 100,
});
return (
<div ref={parentRef} className="h-full overflow-auto">
<div style={{ height: virtualizer.getTotalSize() }}>
{virtualizer.getVirtualItems().map((virtualItem) => (
<MessageItem
key={virtualItem.key}
message={messages[virtualItem.index]}
style={{
position: 'absolute',
top: virtualItem.start,
height: virtualItem.size,
}}
/>
))}
</div>
</div>
);
}
Internationalization (i18n)¶
The application uses i18next for multi-language support.
Configuration¶
Located in src/i18n/config.js:
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import LanguageDetector from 'i18next-browser-languagedetector';
i18n
.use(LanguageDetector)
.use(initReactI18next)
.init({
resources: { en: {...}, 'zh-CN': {...} },
lng: getSavedLanguage(),
fallbackLng: 'en',
ns: ['common', 'settings', 'auth', 'sidebar', 'chat', 'codeEditor'],
defaultNS: 'common',
interpolation: { escapeValue: false },
detection: {
order: ['localStorage'],
lookupLocalStorage: 'userLanguage',
},
});
Supported Languages¶
Defined in src/i18n/languages.js:
| Code | Label | Native Name |
|---|---|---|
en |
English | English |
zh-CN |
Simplified Chinese | 简体中文 |
Translation Namespaces¶
| Namespace | Purpose |
|---|---|
common |
Shared UI elements, buttons, labels |
settings |
Settings panel strings |
auth |
Login, registration, authentication |
sidebar |
Sidebar navigation, projects, sessions |
chat |
Chat interface, messages, tools |
codeEditor |
Code editor UI strings |
Using Translations in Components¶
import { useTranslation } from 'react-i18next';
function MyComponent() {
const { t } = useTranslation('chat');
return (
<div>
<h1>{t('thinkingMode.selector.title')}</h1>
<p>{t('input.placeholder', { provider: 'Claude' })}</p>
</div>
);
}
Adding a New Language¶
- Create locale folder:
src/i18n/locales/{lang-code}/ - Copy all JSON files from
en/and translate - Import and add resources in
src/i18n/config.js - Add language entry in
src/i18n/languages.js
Extended Thinking Modes¶
The ThinkingModeSelector component allows users to select different thinking depth levels for Claude responses.
Available Modes¶
| Mode | Prefix | Description |
|---|---|---|
| Standard | (none) | Regular Claude response |
| Think | think |
Basic extended thinking |
| Think Hard | think hard |
More thorough evaluation |
| Think Harder | think harder |
Deep analysis with alternatives |
| Ultrathink | ultrathink |
Maximum thinking budget |
Usage¶
import ThinkingModeSelector from './ThinkingModeSelector';
<ThinkingModeSelector
selectedMode="think"
onModeChange={(mode) => setThinkingMode(mode)}
onClose={() => setShowSelector(false)}
/>
The selected mode's prefix is prepended to the user's message when sent to Claude.
Testing (Future)¶
Recommended testing setup:
Unit Tests (Vitest)¶
// src/utils/api.test.js
import { describe, it, expect, vi } from 'vitest';
import { api } from './api';
describe('api', () => {
it('should fetch projects', async () => {
global.fetch = vi.fn().mockResolvedValue({
ok: true,
json: () => Promise.resolve({ projects: [] }),
});
const result = await api.getProjects();
expect(result.projects).toEqual([]);
});
});
Component Tests (React Testing Library)¶
// src/components/Button.test.jsx
import { render, screen, fireEvent } from '@testing-library/react';
import { Button } from './Button';
describe('Button', () => {
it('should render children', () => {
render(<Button>Click me</Button>);
expect(screen.getByText('Click me')).toBeInTheDocument();
});
it('should call onClick', () => {
const onClick = vi.fn();
render(<Button onClick={onClick}>Click</Button>);
fireEvent.click(screen.getByText('Click'));
expect(onClick).toHaveBeenCalled();
});
});