# Officer.dev Frontend Architecture
## Executive Summary
Officer's frontend is a modern React 19 SPA (Single Page Application) built with:
- **React 19** with automatic compiler optimizations (no useCallback/useMemo needed)
- **React Router 7** for client-side navigation
- **React Query** for server state management
- **Tailwind CSS 4** with custom components for styling
- **Shadcn/ui** component library base
- **WebSockets** for real-time terminal and AI chat
- **Modular architecture** with shared component and hook libraries
This document covers the frontend architecture, component patterns, state management, and development practices.
---
## Table of Contents
1. [Architecture Overview](#architecture-overview)
2. [Technology Stack](#technology-stack)
3. [Directory Structure](#directory-structure)
4. [Routing & Navigation](#routing--navigation)
5. [Component Architecture](#component-architecture)
6. [State Management](#state-management)
7. [API Communication](#api-communication)
8. [Real-Time Features](#real-time-features)
9. [Styling & Theming](#styling--theming)
10. [Performance & Optimization](#performance--optimization)
11. [Development Workflow](#development-workflow)
---
## Architecture Overview
### High-Level Architecture
```
┌──────────────────────────────────────────────────────────┐
│ Browser Application │
├──────────────────────────────────────────────────────────┤
│ │
│ App.tsx (Router Setup) │
│ ↓ │
│ BrowserRouter │
│ ├─ Authentication Routes (public) │
│ │ └─ Landing, SignIn, SignUp, ForgotPassword │
│ │ │
│ └─ Dashboard Routes (protected) │
│ ├─ HomeScreen │
│ ├─ SettingsPages (Profile, System, Resources) │
│ ├─ Automation │
│ ├─ SessionListPage (Chat with Pi) │
│ ├─ PlansScreen │
│ ├─ FilesScreen │
│ ├─ CodeEditorScreen │
│ ├─ SkillsScreen │
│ ├─ TasksScreen │
│ ├─ ProcessesScreen │
│ ├─ TaskLogsScreen │
│ ├─ WorkspacesScreen │
│ ├─ ProjectListScreen │
│ ├─ ProjectScreen │
│ └─ TerminalScreen │
│ │
│ ↓ Shared State/Context │
│ │
│ ├─ AuthContext (useAuth) │
│ ├─ GlobalState (useGlobalState) │
│ ├─ ServerSettings (useServerSettings) │
│ └─ ReactQuery (useQuery, useMutation) │
│ │
│ ↓ WebSocket Connections │
│ │
│ ├─ Terminal WebSocket │
│ │ └─ Interactive shell in xterm.js │
│ │ │
│ └─ Pi Chat WebSocket │
│ └─ Real-time streaming chat with Claude │
│ │
└──────────────────────────────────────────────────────────┘
↓ HTTP/REST & WebSocket
┌────────────────┐
│ Officer API │
│ (port 5000) │
└────────────────┘
```
### Data Flow
```
User Interaction (Click, Type, etc.)
↓
Component Event Handler
↓
State Update (useState/Context/Query)
↓
API Call (HTTP or WebSocket)
↓
Server Processing
↓
Response
↓
Update Component State
↓
Re-render
↓
Updated UI
```
---
## Technology Stack
### Core Frontend Framework
- **React 19** - Latest with automatic compiler optimizations
- **React DOM 19** - DOM rendering
- **React Router 7** - Client-side routing (file-based patterns support)
- **TypeScript 5.9** - Strict mode enabled
### State Management
- **React Context** - Component tree data sharing
- **React Query (TanStack)** - Server state, caching, synchronization
- **Custom hooks** - Encapsulated business logic
- **Zustand** - Optional lightweight state (if used)
### UI & Styling
- **Tailwind CSS 4** - Utility-first CSS framework
- **Shadcn/ui** - Accessible component library base
- **Radix UI** - Headless components for accessibility
- **Lucide React** - Icon library
- **Tabler Icons** - Additional icons
- **Class Variance Authority (CVA)** - Component style variations
### Real-Time Communication
- **WebSocket API** - Native browser WebSocket
- **Json RPC** - Message protocol over WebSocket
### Code Editor & Terminal
- **Monaco Editor** (@monaco-editor/react) - VS Code-like editor
- **xterm.js** (@xterm/xterm) - Terminal emulator
- **@uiw/react-textarea-code-editor** - Simple code editing
### Data Visualization & Rich Content
- **Recharts** - Chart library
- **React Markdown** - Markdown rendering
- **Shiki** - Syntax highlighting
- **HTML2Canvas** - Screenshot capture
- **React Three Fiber** - 3D rendering (if used)
### Form & Validation
- **React Hook Form** - Form state management
- **Zod** - Schema validation
- **@hookform/resolvers** - Form validation resolvers
### UI Components & Utilities
- **Sonner** - Toast notifications
- **Vaul** - Drawer component
- **React Resizable Panels** - Resizable layout panels
- **React Virtual** - Virtual scrolling
- **Input OTP** - OTP input component
- **React Spinners** - Loading animations
- **React CountUp** - Animated numbers
### Authentication
- **@simplewebauthn/browser** - WebAuthn/passkey support
- **@react-oauth/google** - Google OAuth integration
- **JWT Decode** - Token decoding
### External Services
- **Googleapis** - Google API integration
- **Nodemailer** (backend) - Email sending
### Development & Build Tools
- **Bun** - Runtime and package manager
- **Vite** - Build tool and dev server (if used)
- **Playwright** - E2E testing
- **Testing Library** - Component testing utilities
- **Happy DOM** - DOM testing
---
## Directory Structure
### Frontend Layout
```
src/
├── apps/
│ └── officer-web/ # Main dashboard app
│ ├── App.tsx # Root component & routing
│ ├── frontend.tsx # Vite entry point (if applicable)
│ ├── index.html # HTML template
│ │
│ ├── Screens/ # Page-level components
│ │ ├── Authentication/ # Auth screens (public)
│ │ │ ├── LandingPage.tsx
│ │ │ ├── VerifyScreen.tsx
│ │ │ ├── SignInScreen.tsx
│ │ │ ├── SignUpScreen.tsx
│ │ │ ├── ForgotPassword.tsx
│ │ │ ├── ResetPassword.tsx
│ │ │ └── SignoutScreen.tsx
│ │ │
│ │ └── Dashboard/ # Dashboard screens (protected)
│ │ ├── HomeScreen.tsx
│ │ ├── SettingsPages/
│ │ │ ├── ProfileSettings.tsx
│ │ │ ├── SystemSettings.tsx
│ │ │ └── ResourceSettings.tsx
│ │ ├── Automation.tsx
│ │ ├── SessionListPage.tsx (Pi Chat)
│ │ ├── PlansScreen.tsx
│ │ ├── FilesScreen.tsx
│ │ ├── CodeEditorScreen.tsx
│ │ ├── SkillsScreen.tsx
│ │ ├── TasksScreen.tsx
│ │ ├── ProcessesScreen.tsx
│ │ ├── TaskLogsScreen.tsx
│ │ ├── WorkspacesScreen.tsx
│ │ ├── WorkspaceScreen.tsx
│ │ ├── ProjectListScreen.tsx
│ │ ├── ProjectScreen.tsx
│ │ └── TerminalScreen.tsx
│ │
│ ├── state/ # Component state
│ │ ├── useAuth.ts # Authentication state
│ │ ├── useInitialData.ts # Initial data loading
│ │ └── useServerSettings.ts # Server configuration
│ │
│ ├── lib/ # Client utilities
│ │ ├── api.ts # API client
│ │ └── websocket.ts # WebSocket utilities
│ │
│ ├── styles/ # Global styles
│ │ ├── global.css
│ │ └── tailwind.css
│ │
│ └── locales/ # Translations
│ ├── en.json
│ └── ...
│
└── workspaces/ # Shared libraries
├── components/ # Reusable React components
│ ├── ui/ # Basic UI components
│ │ ├── button.tsx
│ │ ├── input.tsx
│ │ ├── dialog.tsx
│ │ ├── dropdown-menu.tsx
│ │ ├── select.tsx
│ │ ├── tabs.tsx
│ │ ├── card.tsx
│ │ └── ...more
│ │
│ ├── Complex/ # Feature components
│ │ ├── DataTable/
│ │ ├── MarkdownEditor.tsx
│ │ ├── ColorPicker.tsx
│ │ ├── CommandBlock.tsx
│ │ └── ...
│ │
│ ├── Workspace/ # Workspace-specific
│ ├── Dialogs/ # Dialog components
│ ├── ErrorDialogs/ # Error handling UI
│ ├── Logos/ # Logo variants
│ └── package.json
│
├── hooks/ # Custom React hooks
│ ├── useAuth.ts # Auth state hook
│ ├── useClient.ts # API client hook
│ ├── useQuery.ts # Data fetching
│ ├── useMutation.ts # Data mutation
│ └── ...
│
├── helpers/ # Utility functions
│ ├── formatters.ts # Date, number formatting
│ ├── validators.ts # Input validation
│ ├── converters.ts # Type conversions
│ └── ...
│
├── state/ # Global state
│ ├── useGlobalState.ts # Global state hook
│ └── ...
│
├── types/ # Type definitions
│ └── index.ts # Export all types
│
├── config/ # Configuration
│ ├── constants.ts
│ └── env.ts
│
├── definitions/ # Enums and constants
│ ├── roles.ts
│ ├── statuses.ts
│ └── ...
│
├── widgets/ # Complex UI widgets
│ └── ...
│
└── i18n/ # Internationalization
└── ...
```
---
## Routing & Navigation
### React Router Setup: `App.tsx`
```typescript
import { BrowserRouter, Routes, Route, Navigate } from 'react-router';
import { useAuth } from 'hooks/useAuth';
import { useServerSettings } from 'state/useServerSettings';
export function App() {
const { isLoading, isAuthenticated } = useAuth();
const { onboardingComplete, isLoading: isServerSettingsLoading } = useServerSettings();
// Show nothing while loading auth state
if (isLoading || isServerSettingsLoading) return null;
return (
{!isAuthenticated && (
} />
} />
} />
} />
} />
)}
{isAuthenticated && onboardingComplete && (
{/* Home & Settings */}
} />
} />
} />
} />
{/* Features */}
} />
} />
} />
} />
} />
} />
} />
} />
} />
} />
{/* Workspaces & Projects */}
} />
} />
} />
} />
} />
{/* Development */}
} />
{/* Auth */}
} />
{/* Fallback */}
} />
)}
);
}
```
### Layout Components
**AuthenticationLayout** - Wrapper for public pages
```typescript
export const AuthenticationLayout = ({ children }: Props) => {
return (
{children}
);
};
```
**DashboardLayout** - Wrapper for protected pages
```typescript
export const DashboardLayout = ({ children }: Props) => {
return (
);
};
```
---
## Component Architecture
### Component Patterns
#### 1. Functional Components with Props
All components are functional components using React 19:
```typescript
type ButtonProps = {
children: React.ReactNode;
onClick: () => void;
variant?: 'primary' | 'secondary' | 'outline';
size?: 'sm' | 'md' | 'lg';
disabled?: boolean;
};
export const Button = ({
children,
onClick,
variant = 'primary',
size = 'md',
disabled = false,
}: ButtonProps) => {
return (
{children}
);
};
```
#### 2. Component Organization
Complex components follow a structured pattern:
```typescript
// Directory structure
ComponentName/
├── index.tsx # Exports the component
├── ComponentName.tsx # Main implementation
├── hooks/ # Local hooks
│ └── useComponentState.ts
├── types.ts # Component types
└── utils.ts # Helper functions
// index.tsx
export { ComponentName } from './ComponentName';
export type { ComponentNameProps } from './types';
// ComponentName.tsx
import { useComponentState } from './hooks/useComponentState';
import type { ComponentNameProps } from './types';
export const ComponentName = ({ prop1, prop2 }: ComponentNameProps) => {
const { state, actions } = useComponentState();
return (
{/* Render */}
);
};
```
#### 3. React 19 Patterns
**NO useCallback - React 19's compiler handles optimization:**
```typescript
// ❌ BAD - Unnecessary useCallback
export const Form = () => {
const handleSubmit = useCallback((data) => {
api.post('/data', data);
}, []);
return ;
};
// ✅ GOOD - Plain function
export const Form = () => {
const handleSubmit = (data) => {
api.post('/data', data);
};
return ;
};
```
**NO useMemo - Compiler handles memoization:**
```typescript
// ❌ BAD - Unnecessary useMemo
export const List = ({ items, filter }) => {
const filtered = useMemo(
() => items.filter(i => i.type === filter),
[items, filter]
);
return {filtered.map(i => {i.name} )} ;
};
// ✅ GOOD - Direct calculation
export const List = ({ items, filter }) => {
const filtered = items.filter(i => i.type === filter);
return {filtered.map(i => {i.name} )} ;
};
```
**Avoid stale closures - access object properties directly:**
```typescript
// ❌ BAD - Destructuring creates stale references
export const Editor = ({ state }) => {
const { content, save } = state;
useEffect(() => {
const timer = setTimeout(() => {
save(content); // 'content' is stale!
}, 1000);
return () => clearTimeout(timer);
}, [content, save]);
};
// ✅ GOOD - Direct property access
export const Editor = ({ state }) => {
useEffect(() => {
const timer = setTimeout(() => {
state.save(state.content); // Always fresh
}, 1000);
return () => clearTimeout(timer);
}, [state]);
};
```
#### 4. Keyboard Event Handling
Always prevent default for game/editor controls:
```typescript
export const CodeEditor = () => {
const handleKeyDown = (ev: KeyboardEvent) => {
// Prevent Space from scrolling page
if (ev.code === 'Space') {
ev.preventDefault();
// Handle space key
}
// Prevent Escape from closing dialogs
if (ev.key === 'Escape') {
ev.preventDefault();
// Handle escape
}
};
return {/* ... */}
;
};
```
#### 5. Button Focus Management
Buttons retain focus after clicking, interfering with keyboard shortcuts:
```typescript
export const Toolbar = () => {
return (
<>
{
handleSave();
e.currentTarget.blur(); // Remove focus to prevent Space re-triggering
}}
>
Save
>
);
};
```
---
## State Management
### Authentication State: `useAuth`
```typescript
// Location: workspaces/hooks/useAuth.ts
export const useAuth = () => {
const [isLoading, setIsLoading] = useState(true);
const [isAuthenticated, setIsAuthenticated] = useState(false);
const [user, setUser] = useState(null);
const [token, setToken] = useState(
() => localStorage.getItem('token')
);
// Load auth state on mount
useEffect(() => {
const load = async () => {
if (!token) {
setIsAuthenticated(false);
setIsLoading(false);
return;
}
try {
// Verify token validity
const response = await fetch('/api/auth/verify', {
headers: { Authorization: `Bearer ${token}` },
});
if (response.ok) {
const user = await response.json();
setUser(user);
setIsAuthenticated(true);
} else {
setIsAuthenticated(false);
localStorage.removeItem('token');
}
} catch {
setIsAuthenticated(false);
} finally {
setIsLoading(false);
}
};
load();
}, [token]);
const login = async (email: string, password: string) => {
const response = await fetch('/api/auth/signin', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password }),
});
if (response.ok) {
const { token, user } = await response.json();
localStorage.setItem('token', token);
setToken(token);
setUser(user);
setIsAuthenticated(true);
return { success: true };
}
return { success: false, error: await response.text() };
};
const logout = async () => {
try {
await fetch('/api/auth/signout', {
method: 'POST',
headers: { Authorization: `Bearer ${token}` },
});
} catch {
// Ignore errors during logout
} finally {
localStorage.removeItem('token');
setToken(null);
setUser(null);
setIsAuthenticated(false);
}
};
return {
isLoading,
isAuthenticated,
user,
token,
login,
logout,
};
};
// Usage in components
export const Dashboard = () => {
const { user, logout } = useAuth();
return (
Welcome, {user?.email}
Logout
);
};
```
### Server Settings State: `useServerSettings`
```typescript
export const useServerSettings = () => {
const [isLoading, setIsLoading] = useState(true);
const [onboardingComplete, setOnboardingComplete] = useState(false);
const [plugins, setPlugins] = useState([]);
const [settings, setSettings] = useState(null);
useEffect(() => {
const load = async () => {
try {
const response = await fetch('/api/server-settings');
if (response.ok) {
const data = await response.json();
setOnboardingComplete(data.onboardingComplete);
setPlugins(data.plugins);
setSettings(data.settings);
}
} catch {
console.error('Failed to load server settings');
} finally {
setIsLoading(false);
}
};
load();
}, []);
return {
isLoading,
onboardingComplete,
plugins,
settings,
};
};
```
### Global State: `useGlobalState`
For component-tree-wide state, use Context:
```typescript
type GlobalContextType = {
theme: 'light' | 'dark';
setTheme: (theme: 'light' | 'dark') => void;
sidebarOpen: boolean;
setSidebarOpen: (open: boolean) => void;
};
const GlobalContext = createContext(null);
export const GlobalProvider = ({ children }: { children: React.ReactNode }) => {
const [theme, setTheme] = useState<'light' | 'dark'>(() => {
const saved = localStorage.getItem('theme');
return (saved as 'light' | 'dark') || 'dark';
});
const [sidebarOpen, setSidebarOpen] = useState(true);
useEffect(() => {
localStorage.setItem('theme', theme);
document.documentElement.classList.toggle('dark', theme === 'dark');
}, [theme]);
return (
{children}
);
};
export const useGlobalState = () => {
const context = useContext(GlobalContext);
if (!context) {
throw new Error('useGlobalState must be used within GlobalProvider');
}
return context;
};
```
### React Query for Server State
```typescript
// useQueryPlan.ts
export const useQueryPlan = (id: number) => {
return useQuery({
queryKey: ['plans', id],
queryFn: async () => {
const response = await fetch(`/api/plans/${id}`, {
headers: { Authorization: `Bearer ${localStorage.getItem('token')}` },
});
if (!response.ok) throw new Error('Failed to load plan');
return response.json();
},
});
};
// useMutationCreatePlan.ts
export const useMutationCreatePlan = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (data: CreatePlanInput) => {
const response = await fetch('/api/plans', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${localStorage.getItem('token')}`,
},
body: JSON.stringify(data),
});
if (!response.ok) throw new Error('Failed to create plan');
return response.json();
},
onSuccess: () => {
// Invalidate related queries
queryClient.invalidateQueries({ queryKey: ['plans'] });
},
});
};
// In component
export const CreatePlanForm = () => {
const { mutate, isPending } = useMutationCreatePlan();
const handleSubmit = (data: CreatePlanInput) => {
mutate(data);
};
return (
);
};
```
---
## API Communication
### Client Initialization: `src/apps/officer-web/lib/api.ts`
```typescript
// Centralized API client
const API_BASE = import.meta.env.VITE_API_URL || '/api';
export const apiClient = {
async fetch(
endpoint: string,
options: RequestInit = {},
): Promise {
const token = localStorage.getItem('token');
return fetch(`${API_BASE}${endpoint}`, {
...options,
headers: {
'Content-Type': 'application/json',
...(token && { Authorization: `Bearer ${token}` }),
...options.headers,
},
});
},
async get(endpoint: string): Promise {
const response = await this.fetch(endpoint);
if (!response.ok) throw new Error(`GET ${endpoint} failed`);
return response.json();
},
async post(endpoint: string, body?: any): Promise {
const response = await this.fetch(endpoint, {
method: 'POST',
body: body ? JSON.stringify(body) : undefined,
});
if (!response.ok) throw new Error(`POST ${endpoint} failed`);
return response.json();
},
async put(endpoint: string, body?: any): Promise {
const response = await this.fetch(endpoint, {
method: 'PUT',
body: body ? JSON.stringify(body) : undefined,
});
if (!response.ok) throw new Error(`PUT ${endpoint} failed`);
return response.json();
},
async delete(endpoint: string): Promise {
const response = await this.fetch(endpoint, { method: 'DELETE' });
if (!response.ok) throw new Error(`DELETE ${endpoint} failed`);
return response.json();
},
};
// Usage
const user = await apiClient.get('/users/me');
await apiClient.post('/plans', { title: 'New Plan' });
```
---
## Real-Time Features
### Terminal WebSocket: `TerminalScreen.tsx`
```typescript
import { XTerm } from '@xterm/xterm';
import { FitAddon } from '@xterm/addon-fit';
import '@xterm/xterm/css/xterm.css';
export const TerminalScreen = () => {
const terminalRef = useRef(null);
const xtermRef = useRef(null);
const wsRef = useRef(null);
useEffect(() => {
const term = new XTerm({
cols: 120,
rows: 40,
theme: { background: '#0f172a', foreground: '#e2e8f0' },
});
const fitAddon = new FitAddon();
term.loadAddon(fitAddon);
// Mount terminal
if (terminalRef.current) {
term.open(terminalRef.current);
fitAddon.fit();
}
xtermRef.current = term;
// Connect WebSocket
const token = localStorage.getItem('token');
const cwd = '/home/user'; // Or from settings
const ws = new WebSocket(
`ws://localhost:5000/api/terminal/ws?token=${token}&cwd=${encodeURIComponent(cwd)}&cols=120&rows=40`
);
ws.onopen = () => {
console.log('Terminal connected');
};
ws.onmessage = (event) => {
const message = JSON.parse(event.data);
if (message.type === 'output') {
term.write(message.data); // Display output
} else if (message.type === 'error') {
console.error('Terminal error:', message.error);
}
};
ws.onerror = () => {
term.write('\r\nConnection error\r\n');
};
ws.onclose = () => {
term.write('\r\nDisconnected\r\n');
};
// Send input from terminal
term.onData((data) => {
ws.send(JSON.stringify({ type: 'input', data }));
});
wsRef.current = ws;
// Handle resize
const handleResize = () => {
fitAddon.fit();
const { cols, rows } = term;
ws.send(JSON.stringify({ type: 'resize', cols, rows }));
};
window.addEventListener('resize', handleResize);
// Cleanup
return () => {
window.removeEventListener('resize', handleResize);
ws.close();
term.dispose();
};
}, []);
return
;
};
```
### Pi Chat WebSocket: `SessionListPage.tsx`
```typescript
export const SessionListPage = ({ sessionId, isNew }: Props) => {
const [messages, setMessages] = useState([]);
const [input, setInput] = useState('');
const [isLoading, setIsLoading] = useState(false);
const wsRef = useRef(null);
// Connect to Pi chat WebSocket
useEffect(() => {
const token = localStorage.getItem('token');
const url = `ws://localhost:5000/api/pi/chat/ws?token=${token}&sessionId=${sessionId}`;
const ws = new WebSocket(url);
ws.onopen = () => {
console.log('Pi chat connected');
};
ws.onmessage = (event) => {
const message = JSON.parse(event.data);
switch (message.type) {
case 'message_update':
// Stream response text
if (message.event.type === 'text_delta') {
setMessages(prev => {
const last = prev[prev.length - 1];
if (last && last.role === 'assistant') {
return [
...prev.slice(0, -1),
{ ...last, content: last.content + message.event.delta },
];
}
return prev;
});
}
break;
case 'tool_execution_start':
setMessages(prev => [
...prev,
{ role: 'tool', toolName: message.toolName, content: 'Executing...' },
]);
break;
case 'agent_end':
setIsLoading(false);
break;
}
};
ws.onerror = (error) => {
console.error('Pi chat error:', error);
setIsLoading(false);
};
wsRef.current = ws;
return () => ws.close();
}, [sessionId]);
// Send message
const handleSendMessage = () => {
if (!input.trim() || !wsRef.current) return;
const userMessage = input;
setInput('');
setIsLoading(true);
// Add user message to UI
setMessages(prev => [
...prev,
{ role: 'user', content: userMessage },
]);
// Send to Pi
wsRef.current.send(JSON.stringify({
type: 'prompt',
text: userMessage,
}));
};
return (
{messages.map((msg, i) => (
))}
{isLoading &&
Loading...
}
);
};
```
---
## Styling & Theming
### Tailwind CSS Integration
Officer uses Tailwind CSS 4 with a custom configuration:
```javascript
// tailwind.config.js
export default {
content: [
'./src/**/*.{ts,tsx}',
],
theme: {
extend: {
colors: {
// Custom color palette if needed
},
spacing: {
// Custom spacing
},
animation: {
// Custom animations
},
},
},
plugins: [
require('tailwindcss-animate'),
],
};
```
### Shadcn/ui Components
Reusable UI components from shadcn/ui:
```typescript
// Button component wrapper
import { Button as ShadcnButton } from '@/components/ui/button';
export const Button = (props) => (
);
// Usage
Click me
```
### Custom Theming
Support for light/dark themes:
```typescript
// Global theme management
export const useTheme = () => {
const { theme, setTheme } = useGlobalState();
useEffect(() => {
document.documentElement.classList.toggle('dark', theme === 'dark');
localStorage.setItem('theme', theme);
}, [theme]);
return { theme, setTheme };
};
// In component
export const ThemeToggle = () => {
const { theme, setTheme } = useTheme();
return (
setTheme(theme === 'dark' ? 'light' : 'dark')}>
{theme === 'dark' ? '☀️' : '🌙'}
);
};
```
---
## Performance & Optimization
### Image Optimization
Use Next Image component or lazy loading:
```typescript
import { lazy, Suspense } from 'react';
// Lazy load heavy components
const CodeEditor = lazy(() => import('./CodeEditor'));
export const FeaturePage = () => {
return (
Loading...}>
);
};
```
### Virtual Scrolling for Large Lists
Use React Virtual for efficient rendering:
```typescript
import { useVirtualizer } from '@tanstack/react-virtual';
export const VirtualList = ({ items }: { items: Item[] }) => {
const parentRef = useRef(null);
const virtualizer = useVirtualizer({
count: items.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 50,
});
return (
{virtualizer.getVirtualItems().map(virtualItem => (
{items[virtualItem.index]?.name}
))}
);
};
```
### Code Splitting with React Router
Routes are automatically code-split:
```typescript
// Lazy load screen components
const HomeScreen = lazy(() => import('./HomeScreen'));
const SettingsScreen = lazy(() => import('./SettingsScreen'));
}>
} />
} />
```
### React Query Caching
Automatic caching and background updates:
```typescript
// Data is cached per queryKey
const { data: plans } = useQuery({
queryKey: ['plans'], // Cached
queryFn: fetchPlans,
staleTime: 1000 * 60 * 5, // 5 minutes
gcTime: 1000 * 60 * 10, // 10 minutes (was cacheTime)
});
// Background refetch when window regains focus
useQuery({
queryKey: ['plans'],
queryFn: fetchPlans,
refetchOnWindowFocus: true,
});
```
---
## Development Workflow
### Development Server
```bash
# Start Bun dev server with hot reload
bun dev
# Runs on http://localhost:5000/
# Frontend and backend both reload on file changes
```
### Environment Variables
```bash
# .env (client-side, public)
VITE_API_URL=http://localhost:5000/api
```
### TypeScript Type Checking
```bash
# Run TypeScript compiler
tsc --noEmit
# Included in build process
bun run build
```
### Code Formatting
```bash
# Format all files
bun format
# Format specific files
bun format:check
# Prettier config in .prettierrc
{
"semi": true,
"singleQuote": true,
"trailingComma": "all",
"printWidth": 120
}
```
### Component Development
When creating new features:
1. **Create component structure**
```
src/apps/officer-web/Screens/MyFeature/
├── index.tsx # Exports
├── MyFeatureScreen.tsx # Component
├── hooks/
│ └── useMyFeature.ts
├── types.ts
└── utils.ts
```
2. **Define types**
```typescript
// types.ts
export type MyFeatureProps = {
onSubmit: (data: FormData) => void;
disabled?: boolean;
};
```
3. **Implement component**
```typescript
// MyFeatureScreen.tsx
import { useMyFeature } from './hooks/useMyFeature';
import type { MyFeatureProps } from './types';
export const MyFeatureScreen = ({ onSubmit, disabled }: MyFeatureProps) => {
const { state, actions } = useMyFeature();
return (/* JSX */);
};
```
4. **Export**
```typescript
// index.tsx
export { MyFeatureScreen } from './MyFeatureScreen';
export type { MyFeatureProps } from './types';
```
---
## Integration Points
### API Endpoints Used
- **Auth**: `/api/auth/*` (signin, signup, verify, etc.)
- **Plans**: `/api/plans` (GET, POST, PUT, DELETE)
- **Skills**: `/api/skills` (GET, POST)
- **Tasks**: `/api/tasks` (CRUD operations)
- **Files**: `/api/file-browser` (navigation, upload)
- **Settings**: `/api/user` (preferences)
- **Terminal WS**: `/api/terminal/ws` (real-time shell)
- **Pi Chat WS**: `/api/pi/chat/ws` (real-time AI assistant)
### State Flow from Server to UI
```
Database
↓
API Response
↓
React Query Cache
↓
Component State
↓
Rendered UI
```
---
## Debugging
### Browser DevTools
```javascript
// Log component props
console.log('Props:', props);
// Log state updates
console.log('State changed:', newState);
// React DevTools browser extension
// Inspect component tree and props
```
### React Query Devtools
```typescript
// Installed: @tanstack/react-query-devtools
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
export function App() {
return (
<>
{/* App content */}
>
);
}
```
---
## Common Patterns
### Loading States
```typescript
export const DataComponent = () => {
const { data, isLoading, error } = useQuery({
queryKey: ['data'],
queryFn: fetchData,
});
if (isLoading) return ;
if (error) return ;
if (!data) return ;
return ;
};
```
### Form Handling with React Hook Form
```typescript
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
const schema = z.object({
email: z.string().email('Invalid email'),
password: z.string().min(8, 'Min 8 chars'),
});
type FormData = z.infer;
export const LoginForm = () => {
const { register, handleSubmit, formState: { errors } } = useForm({
resolver: zodResolver(schema),
});
const onSubmit = async (data: FormData) => {
await apiClient.post('/auth/signin', data);
};
return (
);
};
```
### Toast Notifications
```typescript
import { toast } from 'sonner';
// Success
toast.success('Operation completed!');
// Error
toast.error('Something went wrong', {
description: 'Please try again later',
});
// Custom
toast.custom((t) => (
Custom notification
));
```
---
## Related Documentation
- **Backend Architecture**: See `OFFICERDEV_BACKEND.md`
- **CONVENTIONS.md**: Detailed code patterns and style
- **CLAUDE.md**: Project overview
- **apps/officer-web/CLAUDE.md**: Frontend-specific patterns
---
## Summary
Officer's frontend is a modern React 19 SPA featuring:
- Type-safe component architecture
- Real-time WebSocket communication
- Server state management with React Query
- Beautiful UI with Tailwind CSS and shadcn/ui
- Strong React patterns without useCallback/useMemo
- Comprehensive authentication and authorization
- Responsive design for all devices
- AI-powered development assistance via Pi
- Interactive terminal emulation
The architecture prioritizes developer experience, type safety, and performance while providing a rich, interactive user experience for life management and AI-assisted development.