Frontend Development Guide
This document provides comprehensive technical documentation for frontend development in the WooAI Chatbot Pro WordPress plugin. It covers the build system, component architecture, state management patterns, and development workflows.
1. Tech Stack
The frontend is built with modern React ecosystem technologies optimized for WordPress integration:
| Technology | Version | Purpose |
|---|---|---|
| React | 18.3.1 | UI component library |
| TypeScript | 5.7.2 | Static type checking |
| Tailwind CSS | 3.4.15 | Utility-first styling |
| Radix UI | 1.x | Accessible primitives |
| class-variance-authority | 0.7.1 | Component variants |
| Recharts | 2.15.4 | Analytics visualizations |
| React Hook Form | 7.55.0 | Form state management |
| Framer Motion | 11.16.1 | Animations |
| Lucide React | 0.487.0 | Icon library |
| dnd-kit | 6.3.1 | Drag-and-drop functionality |
Key Architectural Decisions
- No Redux/Zustand: State is managed through React Context for simplicity and WordPress compatibility
- No React Router: Navigation handled via WordPress admin menu URLs with state synchronization
- Bundled React: React is bundled (not external) to ensure version consistency across WordPress environments
- CSS Isolation: Widget uses
wac-prefix to prevent style conflicts with WordPress themes
Why we bundle React instead of using wp.element: WordPress 6.4 ships React 18.2, but some hosts still run 6.1 with React 17. We had 3 bug reports from theme conflicts before we gave up and bundled it. Yes, it adds ~40KB. Worth it for the sanity.
Things that will bite you
- Hot reload doesn't work with WordPress - just accept it, run
npm run devand refresh manually - Don't use
window.fetch- use ourapiClientwrapper, it handles nonces and retries - Tailwind classes get purged - if your dynamic class isn't working, check
tailwind.config.jssafelist - React DevTools won't show component names in prod - this is intentional (smaller bundle)
2. Project Structure
assets/src/
├── admin/ # Admin panel (wp-admin)
│ ├── index.tsx # Entry point
│ ├── styles.css # Admin-specific styles
│ ├── types.ts # Admin type definitions
│ ├── components/ # Admin-specific components
│ │ ├── AdminLayout.tsx # Main layout wrapper
│ │ ├── AdminSidebar.tsx # Navigation sidebar
│ │ ├── PlaybookEditor/ # Complex editor components
│ │ │ ├── StepEditor.tsx
│ │ │ ├── StepList.tsx
│ │ │ └── StepTypes/ # Step type editors
│ │ └── ProductSelector/ # Product picker component
│ └── pages/ # Admin page components
│ ├── Dashboard.tsx
│ ├── AIProviders.tsx
│ ├── Analytics.tsx
│ ├── Appearance.tsx
│ ├── Languages.tsx
│ ├── Playbooks.tsx
│ ├── Promotions.tsx
│ ├── RAG.tsx
│ └── Topics.tsx
│
├── chat/ # Frontend chat widget
│ ├── index.tsx # Entry point
│ ├── styles.css # Widget styles
│ ├── components/ # Widget components
│ │ ├── ChatWidgetBottom.tsx # Main widget container
│ │ ├── ChatMessage.tsx # Message bubble
│ │ ├── ProductCarousel.tsx # Product display
│ │ ├── TypingIndicator.tsx # Loading state
│ │ ├── TopicsBar.tsx # Topic suggestions
│ │ ├── PlaybookStep.tsx # Playbook UI
│ │ └── PlaybookSteps/ # Step renderers
│ └── utils/
│ └── wacClassNames.ts # Widget class prefixer
│
├── components/ui/ # Shared shadcn/ui components
│ ├── button.tsx
│ ├── card.tsx
│ ├── dialog.tsx
│ ├── form.tsx
│ ├── input.tsx
│ ├── select.tsx
│ ├── tabs.tsx
│ └── ... (40+ components)
│
├── contexts/ # React Context providers
│ ├── ChatContext.tsx # Chat state management
│ ├── SettingsContext.tsx # Plugin settings
│ ├── AnalyticsContext.tsx # Analytics data
│ └── PlaybookContext.tsx # Playbook execution
│
├── services/ # API service layer
│ ├── chatService.ts # Chat API operations
│ ├── settingsService.ts # Settings API
│ ├── analyticsService.ts # Analytics API
│ ├── cartService.ts # WooCommerce cart
│ └── productsService.ts # Product search
│
├── types/ # TypeScript definitions
│ ├── api.ts # API request/response types
│ ├── models.ts # Domain models
│ ├── settings.ts # Settings interfaces
│ ├── playbook.ts # Playbook types
│ └── promotion.ts # Promotion types
│
├── utils/ # Shared utilities
│ ├── api/
│ │ ├── client.ts # HTTP client with retry
│ │ ├── errors.ts # Error handling
│ │ └── index.ts
│ ├── hooks/
│ │ ├── useDebounce.ts
│ │ ├── useAsync.ts
│ │ └── index.ts
│ ├── cn.ts # Class name merger
│ └── logger.ts # Logging utility
│
├── hooks/ # Custom React hooks
│ └── useTranslation.ts # i18n hook
│
└── analytics/ # Analytics tracking
└── index.ts # Event tracking
3. Build System
Webpack Configuration
The build uses Webpack 5 with dual entry points for admin and chat widget bundles:
// webpack.config.js (simplified)
module.exports = (env, argv) => ({
entry: {
admin: './assets/src/admin/index.tsx',
'chat-widget': './assets/src/chat/index.tsx',
},
output: {
path: path.resolve(__dirname, 'assets'),
filename: (pathData) => {
if (pathData.chunk.name === 'admin') {
return 'admin/js/admin.bundle.js';
}
if (pathData.chunk.name === 'chat-widget') {
return 'chat/js/chat-widget.bundle.js';
}
return '[name].bundle.js';
},
},
resolve: {
extensions: ['.ts', '.tsx', '.js', '.jsx', '.json'],
alias: {
'@': path.resolve(__dirname, 'assets/src'),
'@admin': path.resolve(__dirname, 'assets/src/admin'),
'@chat': path.resolve(__dirname, 'assets/src/chat'),
'@components': path.resolve(__dirname, 'assets/src/components'),
'@hooks': path.resolve(__dirname, 'assets/src/hooks'),
'@utils': path.resolve(__dirname, 'assets/src/utils'),
'@types': path.resolve(__dirname, 'assets/src/types'),
},
},
externals: {
jquery: 'jQuery',
'@wordpress/i18n': 'wp.i18n',
},
});
Output Structure
assets/
├── admin/
│ ├── js/
│ │ └── admin.bundle.js # Admin React app
│ └── css/
│ └── admin.bundle.css # Admin styles
├── chat/
│ ├── js/
│ │ └── chat-widget.bundle.js # Widget React app
│ └── css/
│ └── chat-widget.bundle.css # Widget styles
└── images/
└── ai-assistant-figma.png # Widget icon
Chunk Splitting Strategy
Vendor code is split into separate chunks for optimal caching:
optimization: {
splitChunks: {
cacheGroups: {
adminVendor: {
test: /[\\/]node_modules[\\/]/,
name: 'admin-vendor',
chunks: (chunk) => chunk.name === 'admin',
priority: 10,
},
chatVendor: {
test: /[\\/]node_modules[\\/]/,
name: 'chat-vendor',
chunks: (chunk) => chunk.name === 'chat-widget',
priority: 10,
},
},
},
},
npm Scripts
# Development with watch mode
npm run dev
# Production build (minified, no console logs)
npm run build
# Development build (no minification)
npm run build:dev
# Type checking only
npm run typecheck
# Linting with auto-fix
npm run lint
# Run tests
npm run test
# Clean build artifacts
npm run clean
Development vs Production Builds
| Feature | Development | Production |
|---|---|---|
| Source maps | eval-source-map |
source-map |
| Minification | Disabled | TerserPlugin |
| Console logs | Preserved | Removed |
| Type checking | transpileOnly |
Full type check |
| CSS extraction | style-loader |
MiniCssExtractPlugin |
4. Component Architecture
Admin Panel Hierarchy
AdminApp
├── SettingsProvider (context)
│ └── AnalyticsProvider (context)
│ └── AdminLayout
│ ├── AdminSidebar
│ └── [Page Component]
│ ├── Dashboard
│ ├── AIProviders
│ ├── Analytics
│ ├── Appearance
│ ├── Languages
│ ├── Playbooks
│ │ └── PlaybookEditor
│ │ ├── StepList
│ │ └── StepEditor
│ │ └── [StepType]Editor
│ ├── Promotions
│ ├── RAG
│ └── Topics
Widget Component Hierarchy
ChatWidget
└── ChatProvider (context)
└── ChatWidgetBottom
├── [icon state] → Floating button
├── [collapsed state] → Input bar only
├── [minimized state] → Compact window
└── [expanded state] → Full chat panel
├── Header
├── MessageList
│ ├── ChatMessage
│ │ └── ProductCarousel
│ ├── TypingIndicator
│ └── PlaybookStep
├── TopicsBar
└── InputArea
Component Pattern: shadcn/ui with CVA
Components use class-variance-authority for variant management:
// assets/src/components/ui/button.tsx
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "./utils";
const buttonVariants = cva(
// Base styles
"inline-flex items-center justify-center rounded-md text-sm font-medium transition-all",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive: "bg-destructive text-white hover:bg-destructive/90",
outline: "border bg-background hover:bg-accent",
secondary: "bg-secondary text-secondary-foreground",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-9 px-4 py-2",
sm: "h-8 rounded-md px-3",
lg: "h-10 rounded-md px-6",
icon: "size-9",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
);
function Button({
className,
variant,
size,
asChild = false,
...props
}: React.ComponentProps<"button"> & VariantProps<typeof buttonVariants> & {
asChild?: boolean;
}) {
const Comp = asChild ? Slot : "button";
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
{...props}
/>
);
}
Radix UI Composition Pattern
Radix primitives are wrapped with styled exports:
// assets/src/components/ui/dialog.tsx
import * as DialogPrimitive from "@radix-ui/react-dialog";
const Dialog = DialogPrimitive.Root;
const DialogTrigger = DialogPrimitive.Trigger;
const DialogContent = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<DialogPortal>
<DialogOverlay />
<DialogPrimitive.Content
ref={ref}
className={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%]",
"bg-background p-6 shadow-lg duration-200",
className
)}
{...props}
>
{children}
</DialogPrimitive.Content>
</DialogPortal>
));
5. State Management
Context Architecture
The plugin uses four primary React Contexts:
┌─────────────────────────────────────────────────────────────┐
│ React App Root │
├─────────────────────────────────────────────────────────────┤
│ ┌───────────────────────────────────────────────────────┐ │
│ │ SettingsProvider │ │
│ │ - Plugin settings (AI provider, appearance, etc.) │ │
│ │ - Optimistic updates with rollback │ │
│ ├───────────────────────────────────────────────────────┤ │
│ │ ┌─────────────────────────────────────────────────┐ │ │
│ │ │ AnalyticsProvider │ │ │
│ │ │ - KPI metrics, funnel data │ │ │
│ │ │ - Conversations list │ │ │
│ │ │ - CSV export functionality │ │ │
│ │ ├─────────────────────────────────────────────────┤ │ │
│ │ │ ┌───────────────────────────────────────────┐ │ │ │
│ │ │ │ ChatProvider │ │ │ │
│ │ │ │ - Messages array │ │ │ │
│ │ │ │ - Session management │ │ │ │
│ │ │ │ - Send/receive message handlers │ │ │ │
│ │ │ ├───────────────────────────────────────────┤ │ │ │
│ │ │ │ ┌─────────────────────────────────────┐ │ │ │ │
│ │ │ │ │ PlaybookProvider │ │ │ │ │
│ │ │ │ │ - Active playbook state │ │ │ │ │
│ │ │ │ │ - Step navigation │ │ │ │ │
│ │ │ │ │ - Collected variables │ │ │ │ │
│ │ │ │ └─────────────────────────────────────┘ │ │ │ │
│ │ │ └───────────────────────────────────────────┘ │ │ │
│ │ └─────────────────────────────────────────────────┘ │ │
│ └───────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
ChatContext Implementation
// assets/src/contexts/ChatContext.tsx
export interface ChatContextValue {
messages: ChatMessage[];
sessionId: string | null;
loading: boolean;
error: ApiError | null;
sendMessage: (message: string, context?: ChatContext) => Promise<void>;
clearHistory: () => Promise<void>;
loadHistory: () => Promise<void>;
getRelatedProducts: (query?: string, limit?: number) => Promise<Product[] | undefined>;
}
export const ChatProvider: React.FC<ChatProviderProps> = ({
children,
autoInit = true,
initialContext
}) => {
const [messages, setMessages] = useState<ChatMessage[]>([]);
const [sessionId, setSessionId] = useState<string | null>(null);
const [loading, setLoading] = useState<boolean>(false);
const [error, setError] = useState<ApiError | null>(null);
// Initialize session on mount
useEffect(() => {
if (autoInit) {
initializeSession();
}
}, [autoInit, initializeSession]);
const sendMessage = useCallback(async (message: string, context?: ChatContext) => {
if (!message.trim()) return;
setLoading(true);
setError(null);
// Add user message immediately (optimistic)
const userMessage: ChatMessage = {
id: `user-${Date.now()}`,
role: 'user',
content: message,
timestamp: Date.now(),
};
setMessages((prev) => [...prev, userMessage]);
try {
const response = await chatService.sendMessage(message, sessionId, context);
// Add assistant response
const assistantMessage: ChatMessage = {
id: `assistant-${Date.now()}`,
role: 'assistant',
content: response.message,
timestamp: Date.now(),
products: response.products,
suggestions: response.suggestions,
};
setMessages((prev) => [...prev, assistantMessage]);
} catch (err) {
// Remove optimistic user message on error
setMessages((prev) => prev.slice(0, -1));
setError(err as ApiError);
} finally {
setLoading(false);
}
}, [sessionId]);
// ... other methods
return (
<ChatContext.Provider value={{ messages, sessionId, loading, error, sendMessage, ... }}>
{children}
</ChatContext.Provider>
);
};
// Hook with safety check
export const useChat = (): ChatContextValue => {
const context = useContext(ChatContext);
if (context === undefined) {
throw new Error('useChat must be used within a ChatProvider');
}
return context;
};
SettingsContext with Optimistic Updates
// assets/src/contexts/SettingsContext.tsx
const updateSettings = useCallback(async (newSettings: Partial<Settings>) => {
if (!settings) throw new Error('Settings not loaded');
// Store for rollback
const previousSettings = settings;
// Optimistic update
setSettings((prev) => ({
...prev,
...newSettings,
aiProvider: { ...prev.aiProvider, ...(newSettings.aiProvider || {}) },
appearance: { ...prev.appearance, ...(newSettings.appearance || {}) },
}));
try {
const updatedSettings = await settingsService.updateSettings(newSettings);
setSettings(updatedSettings);
} catch (err) {
// Rollback on error
setSettings(previousSettings);
throw err;
}
}, [settings]);
PlaybookContext State Machine
// assets/src/contexts/PlaybookContext.tsx
export interface PlaybookState {
active: boolean;
playbookId: string | null;
stateId: number | null;
currentStepIndex: number;
totalSteps: number;
stepData: PlaybookStepData | null;
status: 'idle' | 'loading' | 'active' | 'paused' | 'completed';
collectedVariables: Record<string, unknown>;
}
// State transitions:
// idle -> loading (startPlaybook)
// loading -> active (API success)
// active -> loading (submitResponse)
// active -> paused (pausePlaybook)
// paused -> active (resumePlaybook)
// active -> completed (playbook finished)
// any -> idle (cancelPlaybook)
6. API Services
HTTP Client Architecture
The API client provides enterprise-grade features:
// assets/src/utils/api/client.ts
class ApiClient {
private baseUrl: string;
private nonce: string;
private requestInterceptors: RequestInterceptor[] = [];
private responseInterceptors: ResponseInterceptor[] = [];
private errorInterceptors: ErrorInterceptor[] = [];
private defaultRetryCount: number = 3;
private defaultRetryDelay: number = 1000;
// Automatic WordPress API URL detection
private detectApiUrl(): string {
const globals = window.wooAiChatbot ?? window.wooAIChatbot;
if (globals?.apiUrl) return globals.apiUrl;
return `${window.location.origin}/wp-json/woo-ai/v1/`;
}
// Exponential backoff retry logic
private async request<T>(endpoint: string, config: RequestConfig = {}): Promise<T> {
const { retry = true, retryCount = 0, maxRetries = 3 } = config;
try {
const response = await fetch(url, {
...config,
headers: {
'Content-Type': 'application/json',
'X-WP-Nonce': this.nonce,
...config.headers,
},
});
if (!response.ok) {
const error = new ApiError(message, response.status, code);
// Retry on retryable errors
if (retry && error.isRetryable() && retryCount < maxRetries) {
await this.sleep(this.calculateBackoff(retryCount + 1, 1000));
return this.request<T>(endpoint, { ...config, retryCount: retryCount + 1 });
}
throw error;
}
return response.json();
} catch (error) {
// Network error retry logic
if (retry && retryCount < maxRetries) {
await this.sleep(this.calculateBackoff(retryCount + 1, 1000));
return this.request<T>(endpoint, { ...config, retryCount: retryCount + 1 });
}
throw error;
}
}
// Convenience methods
get<T>(endpoint: string, params?: Record<string, unknown>): Promise<T>;
post<T>(endpoint: string, data?: unknown): Promise<T>;
put<T>(endpoint: string, data?: unknown): Promise<T>;
delete<T>(endpoint: string): Promise<T>;
}
Custom ApiError Class
// assets/src/utils/api/client.ts
export class ApiError extends Error {
constructor(
message: string,
public statusCode?: number,
public code?: string,
public data?: unknown
) {
super(message);
this.name = 'ApiError';
}
isNetworkError(): boolean {
return !this.statusCode || this.statusCode === 0;
}
isClientError(): boolean {
return !!this.statusCode && this.statusCode >= 400 && this.statusCode < 500;
}
isServerError(): boolean {
return !!this.statusCode && this.statusCode >= 500;
}
isRetryable(): boolean {
// Retry on: network errors, 408, 429, 5xx
if (this.isNetworkError()) return true;
if (this.statusCode === 408 || this.statusCode === 429) return true;
if (this.isServerError()) return true;
return false;
}
}
Service Pattern Example
// assets/src/services/chatService.ts
class ChatService {
private readonly baseEndpoint = '/woo-ai/v1/chat';
async sendMessage(
message: string,
sessionId?: string,
context?: ChatContext
): Promise<ChatResponse> {
validateChatRequest({ message, sessionId, context });
const response = await fetchAPI<WPChatMessageResponse>(
`${this.baseEndpoint}/message`,
{
method: 'POST',
body: JSON.stringify({
message,
session_id: sessionId,
context,
}),
}
);
return {
sessionId: response.session_id,
message: response.message?.content || '',
products: response.products || [],
suggestions: response.suggestions || [],
};
}
async getOrCreateSession(context?: ChatContext): Promise<string> {
// Check localStorage first
const storedSessionId = getSessionId();
if (storedSessionId) {
const session = await this.getSessionById(storedSessionId);
if (session) return storedSessionId;
}
// Create new session
const response = await fetchAPI<{ session_id: string }>(
`${this.baseEndpoint}/session`,
{ method: 'POST' }
);
saveSessionId(response.session_id);
return response.session_id;
}
}
export default new ChatService();
7. Styling
Tailwind Configuration
// tailwind.config.js
module.exports = {
darkMode: ['class'],
content: [
'./assets/src/**/*.{ts,tsx,js,jsx}',
'./includes/**/*.php',
'./templates/**/*.php',
],
// CRITICAL: Widget class prefix for style isolation
prefix: 'wac-',
// Safelist dynamically used classes
safelist: [
'wac-rounded', 'wac-rounded-md', 'wac-rounded-lg',
'wac-shadow', 'wac-shadow-md', 'wac-shadow-lg',
'hover:wac-shadow-lg', 'hover:wac-bg-gray-100',
// ... more dynamic classes
],
theme: {
extend: {
colors: {
// CSS custom property integration
border: 'hsl(var(--wac-border))',
primary: {
DEFAULT: 'hsl(var(--wac-primary))',
foreground: 'hsl(var(--wac-primary-foreground))',
},
// WordPress admin colors
'wp-admin-blue': '#2271b1',
'wp-admin-gray': '#f0f0f1',
// Chat-specific colors
'chat-user': '#0084ff',
'chat-bot': '#f0f0f0',
},
zIndex: {
'chat-widget': '9999',
'wp-admin-bar': '99999',
},
},
},
plugins: [
// RTL support
require('tailwindcss/plugin')(({ addVariant }) => {
addVariant('rtl', '[dir="rtl"] &');
addVariant('ltr', '[dir="ltr"] &');
}),
],
};
Widget Class Prefixing
The wac() function ensures all Tailwind classes are prefixed:
// assets/src/chat/utils/wacClassNames.ts
const WAC_PREFIX = 'wac-';
const prefixToken = (token: string): string => {
if (!token) return '';
const isImportant = token.startsWith('!');
const cleanToken = isImportant ? token.slice(1) : token;
const segments = cleanToken.split(':');
// Prefix only the utility class, not the variant
const prefixedSegments = segments.map((segment, index) => {
if (index === segments.length - 1) {
return segment.startsWith(WAC_PREFIX) ? segment : `${WAC_PREFIX}${segment}`;
}
return segment;
});
const prefixed = prefixedSegments.join(':');
return isImportant ? `!${prefixed}` : prefixed;
};
export const wac = (
...classLists: Array<string | false | null | undefined>
): string => {
return classLists
.filter(Boolean)
.map((item) => normalizeInput(item as string))
.join(' ');
};
// Usage:
// wac('px-4 py-2 hover:bg-gray-100')
// => 'wac-px-4 wac-py-2 hover:wac-bg-gray-100'
RTL Support (Hebrew)
// Component with RTL awareness
const { isRTL, direction } = useTranslation();
return (
<div style={{ direction: direction as 'ltr' | 'rtl' }}>
<input
style={{
paddingInlineStart: '44px', // Use logical properties
paddingInlineEnd: '112px',
}}
/>
<div style={isRTL ? { right: '8px' } : { left: '8px' }}>
{/* Positioned element */}
</div>
</div>
);
CSS Custom Properties for Theming
/* Widget CSS custom properties */
:root {
--waa-primary: #6366f1;
--waa-primary-light: #818cf8;
--waa-primary-dark: #4f46e5;
--waa-primary-50: #eef2ff;
--waa-gray-50: #f9fafb;
--waa-gray-100: #f3f4f6;
--waa-gray-200: #e5e7eb;
--waa-gray-300: #d1d5db;
--waa-gray-400: #9ca3af;
--waa-gray-500: #6b7280;
--waa-gray-600: #4b5563;
--waa-gray-700: #374151;
--waa-gray-900: #111827;
--waa-chat-user-bg: var(--waa-primary);
--waa-chat-bot-bg: var(--waa-gray-100);
--waa-chat-bot-text: var(--waa-gray-900);
}
8. TypeScript
Strict Mode Configuration
// tsconfig.json
{
"compilerOptions": {
"target": "ES2020",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"moduleResolution": "node",
"jsx": "react-jsx",
// Strict type checking
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"noImplicitAny": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"strictBindCallApply": true,
"strictPropertyInitialization": true,
"noImplicitThis": true,
"alwaysStrict": true,
"forceConsistentCasingInFileNames": true,
// Path aliases
"baseUrl": ".",
"paths": {
"@/*": ["assets/src/*"],
"@admin/*": ["assets/src/admin/*"],
"@chat/*": ["assets/src/chat/*"],
"@components/*": ["assets/src/components/*"],
"@hooks/*": ["assets/src/hooks/*"],
"@utils/*": ["assets/src/utils/*"],
"@types/*": ["assets/src/types/*"]
}
}
}
Type Definition Patterns
// assets/src/types/api.ts
// API Request/Response types
export interface ChatMessageRequest {
session_id: string;
message: string;
}
export interface ChatMessageResponse {
success: boolean;
session_id: string;
message: {
role: 'assistant';
content: string;
};
context: SessionContext;
}
// Generic API wrapper
export type APIResponse<T> = SuccessResponse<T> | ErrorResponse;
// Type guards
export function isErrorResponse(
response: SuccessResponse | ErrorResponse
): response is ErrorResponse {
return response.success === false;
}
WordPress Global Types
// assets/src/types/global.d.ts
declare global {
interface Window {
wooAiChatbot?: {
nonce: string;
apiUrl: string;
pluginUrl: string;
currentPage: string;
welcomeMessage?: string;
};
wooAIChatbot?: Window['wooAiChatbot'];
wooAIChatConfig?: {
appearance?: AppearanceConfig;
initialMessage?: string;
};
}
}
9. Development Workflow
Watch Mode Development
# Start development server with hot reload
npm run dev
# This runs:
# webpack --mode development --watch
The watch mode provides:
- Fast incremental builds (~500ms on save)
- Source maps for debugging
- No minification for readable output
- TypeScript transpile-only mode (skip type checking for speed)
Type Checking Separately
# Run type checking independently (CI/pre-commit)
npm run typecheck
# This runs:
# tsc --noEmit
Production Build
# Build for production
npm run build
# This includes:
# - Full type checking
# - Minification (Terser)
# - Console log removal
# - CSS optimization
# - Source maps
Debugging Tips
- React DevTools: Install the browser extension for component inspection
- Redux DevTools: Not used (Context-based), but you can add logging interceptors
- Network Tab: Filter by
/wp-json/woo-ai/to see API calls - Console Logging: Use
logger.debug()for conditional logging
// assets/src/utils/logger.ts
export const logger = {
debug: (...args: unknown[]) => {
if (process.env.NODE_ENV === 'development') {
console.log('[WAC]', ...args);
}
},
error: (...args: unknown[]) => console.error('[WAC]', ...args),
};
- WordPress Debug: Enable
WP_DEBUGto see PHP errors alongside JS
10. Adding New Features
Adding a New Admin Page
- Create the page component:
// assets/src/admin/pages/NewFeature.tsx
import React from 'react';
import { Card, CardHeader, CardTitle, CardContent } from '@components/ui/card';
import { useSettings } from '@/contexts/SettingsContext';
export const NewFeature: React.FC = () => {
const { settings, updateSettings, loading } = useSettings();
if (loading) {
return <div>Loading...</div>;
}
return (
<div className="p-6 space-y-6">
<h1 className="text-2xl font-semibold">New Feature</h1>
<Card>
<CardHeader>
<CardTitle>Feature Settings</CardTitle>
</CardHeader>
<CardContent>
{/* Feature content */}
</CardContent>
</Card>
</div>
);
};
- Add to admin types:
// assets/src/admin/types.ts
export type AdminPage =
| 'dashboard'
| 'ai-providers'
| 'new-feature' // Add here
// ...
- Register in index.tsx:
// assets/src/admin/index.tsx
import { NewFeature } from './pages/NewFeature';
const pageSlugMap: Partial<Record<AdminPage, string>> = {
// ...
'new-feature': 'woo-ai-chatbot-new-feature',
};
const renderPage = () => {
switch (currentPage) {
// ...
case 'new-feature':
return <NewFeature />;
}
};
- Register PHP menu item (in PHP):
// includes/admin/class-admin-menu.php
add_submenu_page(
'woo-ai-chatbot',
'New Feature',
'New Feature',
'manage_options',
'woo-ai-chatbot-new-feature',
[$this, 'render_react_admin']
);
Adding a New Widget Component
- Create the component:
// assets/src/chat/components/NewWidget.tsx
import React from 'react';
import { wac } from '../utils/wacClassNames';
import { useChat } from '@/contexts/ChatContext';
interface NewWidgetProps {
onAction: () => void;
}
export const NewWidget: React.FC<NewWidgetProps> = ({ onAction }) => {
const { sessionId } = useChat();
return (
<div className={wac('p-4 bg-white rounded-lg shadow-md')}>
<button
onClick={onAction}
className={wac('px-4 py-2 bg-primary text-white rounded hover:bg-primary/90')}
>
Action
</button>
</div>
);
};
- Export from index:
// assets/src/chat/components/index.ts
export { NewWidget } from './NewWidget';
- Use in ChatWidgetBottom:
import { NewWidget } from './NewWidget';
// In the component tree
{showNewWidget && (
<NewWidget onAction={handleWidgetAction} />
)}
Adding a New Context
- Create the context:
// assets/src/contexts/NewContext.tsx
import { createContext, useContext, useState, useCallback } from 'react';
interface NewContextValue {
data: DataType | null;
loading: boolean;
error: Error | null;
fetchData: () => Promise<void>;
updateData: (updates: Partial<DataType>) => Promise<void>;
}
const NewContext = createContext<NewContextValue | undefined>(undefined);
export const NewProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [data, setData] = useState<DataType | null>(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<Error | null>(null);
const fetchData = useCallback(async () => {
setLoading(true);
try {
const result = await apiClient.get<DataType>('/endpoint');
setData(result);
} catch (err) {
setError(err as Error);
} finally {
setLoading(false);
}
}, []);
const updateData = useCallback(async (updates: Partial<DataType>) => {
// Optimistic update pattern
const previous = data;
setData((prev) => ({ ...prev, ...updates }));
try {
const result = await apiClient.post<DataType>('/endpoint', updates);
setData(result);
} catch (err) {
setData(previous); // Rollback
throw err;
}
}, [data]);
return (
<NewContext.Provider value={{ data, loading, error, fetchData, updateData }}>
{children}
</NewContext.Provider>
);
};
export const useNewContext = (): NewContextValue => {
const context = useContext(NewContext);
if (context === undefined) {
throw new Error('useNewContext must be used within a NewProvider');
}
return context;
};
- Add to provider tree in the appropriate entry point.
Appendix: Common Patterns Reference
Form with React Hook Form
import { useForm } from 'react-hook-form';
import { Form, FormField, FormItem, FormLabel, FormControl } from '@components/ui/form';
const MyForm = () => {
const form = useForm<FormData>({
defaultValues: { name: '', email: '' },
});
const onSubmit = async (data: FormData) => {
await saveData(data);
};
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)}>
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Name</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
</FormItem>
)}
/>
<Button type="submit">Save</Button>
</form>
</Form>
);
};
Async Data Loading Pattern
const MyComponent = () => {
const [data, setData] = useState<Data | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
useEffect(() => {
const load = async () => {
try {
const result = await fetchData();
setData(result);
} catch (err) {
setError(err as Error);
} finally {
setLoading(false);
}
};
load();
}, []);
if (loading) return <Skeleton />;
if (error) return <Alert variant="destructive">{error.message}</Alert>;
if (!data) return null;
return <div>{/* Render data */}</div>;
};
Last updated: December 2024 Version: 0.2.1