Frontend Service¶
Comprehensive documentation for the VoiceERA Frontend service.
Overview¶
The Frontend is the user-facing web interface for VoiceERA, built with Next.js 16+, React 18+, and TailwindCSS 4+.
Key Responsibilities: - User authentication & account management - Agent creation and management - Campaign management and monitoring - Real-time voice call interface - Call history and recordings - Analytics dashboard
Getting Started¶
Prerequisites¶
- Node.js 18+
- npm or yarn
- Modern web browser (Chrome, Firefox, Safari, Edge)
Installation¶
cd voicera_frontend
# Install dependencies
npm install
# or
yarn install
# Configure environment
cp .env.example .env.local
# Edit with your settings
Development¶
# Run dev server (with hot reload)
npm run dev
# Open in browser
# http://localhost:3000
Production Build¶
# Build for production
npm run build
# Start production server
npm run start
# Or via Docker
docker build -t voicera-frontend .
docker run -p 3000:3000 voicera-frontend
Project Structure¶
voicera_frontend/
├── app/
│ ├── layout.tsx # Root layout
│ ├── page.tsx # Home page
│ ├── (auth)/ # Auth routes
│ │ ├── login/
│ │ │ └── page.tsx
│ │ ├── signup/
│ │ │ └── page.tsx
│ │ └── forgot-password/
│ │ └── page.tsx
│ ├── (dashboard)/ # Protected routes
│ │ ├── layout.tsx
│ │ ├── page.tsx # Dashboard home
│ │ ├── agents/
│ │ │ ├── page.tsx # Agents list
│ │ │ ├── [id]/
│ │ │ │ └── page.tsx # Agent details
│ │ │ └── new/
│ │ │ └── page.tsx # Create agent
│ │ ├── campaigns/
│ │ │ ├── page.tsx
│ │ │ ├── [id]/
│ │ │ │ └── page.tsx
│ │ │ └── new/
│ │ │ └── page.tsx
│ │ ├── call-logs/
│ │ │ ├── page.tsx
│ │ │ └── [id]/
│ │ │ └── page.tsx
│ │ ├── analytics/
│ │ │ └── page.tsx
│ │ ├── settings/
│ │ │ └── page.tsx
│ │ └── profile/
│ │ └── page.tsx
│ └── api/ # API routes (if needed)
│ ├── auth/
│ └── proxy/
├── components/
│ ├── app-sidebar.tsx # Navigation sidebar
│ ├── assistants/ # Agent components
│ │ ├── agent-form.tsx
│ │ ├── agent-card.tsx
│ │ └── agent-list.tsx
│ ├── campaigns/ # Campaign components
│ │ ├── campaign-form.tsx
│ │ ├── campaign-card.tsx
│ │ └── campaign-list.tsx
│ ├── voice/ # Voice call components
│ │ ├── voice-interface.tsx
│ │ ├── audio-player.tsx
│ │ └── call-status.tsx
│ ├── analytics/ # Analytics components
│ │ ├── metrics-card.tsx
│ │ ├── chart-widget.tsx
│ │ └── analytics-dashboard.tsx
│ ├── ui/ # Reusable UI components
│ │ ├── button.tsx
│ │ ├── card.tsx
│ │ ├── modal.tsx
│ │ ├── dropdown.tsx
│ │ ├── input.tsx
│ │ └── ...
│ ├── header.tsx
│ ├── footer.tsx
│ └── layouts/
│ ├── authenticated-layout.tsx
│ └── public-layout.tsx
├── hooks/
│ ├── use-auth.ts # Auth hook
│ ├── use-voice.ts # Voice call hook
│ ├── use-api.ts # API interaction
│ ├── use-mobile.ts # Mobile detection
│ └── use-analytics.ts
├── lib/
│ ├── api.ts # API client
│ ├── api-config.ts # API configuration
│ ├── auth.ts # Auth utilities
│ ├── websocket.ts # WebSocket client
│ ├── utils.ts # Utility functions
│ └── constants.ts # Constants
├── styles/
│ └── globals.css
├── public/ # Static assets
│ └── images/
├── package.json
├── tsconfig.json
├── next.config.ts
├── tailwind.config.js
├── postcss.config.js
└── .env.example
Key Features¶
Authentication¶
// Custom hook for auth state
function useAuth() {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
// Check if user is logged in
const token = localStorage.getItem('token');
if (token) {
verifyToken(token).then(setUser);
}
setLoading(false);
}, []);
const login = async (email, password) => {
const response = await api.post('/auth/login', {
email,
password
});
localStorage.setItem('token', response.token);
setUser(response.user);
};
const logout = () => {
localStorage.removeItem('token');
setUser(null);
};
return { user, loading, login, logout };
}
Voice Call Interface¶
// Component for real-time voice calls
function VoiceInterface({ campaignId, agentId }) {
const [callActive, setCallActive] = useState(false);
const [transcript, setTranscript] = useState('');
const [isListening, setIsListening] = useState(false);
const mediaRecorder = useRef(null);
const ws = useRef(null);
const startCall = async () => {
// Establish WebSocket connection
ws.current = new WebSocket(
process.env.NEXT_PUBLIC_VOICE_SERVER_URL
);
ws.current.onopen = async () => {
// Send auth token
ws.current.send(JSON.stringify({
type: 'auth',
token: getToken(),
agent_id: agentId
}));
};
ws.current.onmessage = (event) => {
const message = JSON.parse(event.data);
if (message.type === 'ready') {
setCallActive(true);
startAudioCapture();
} else if (message.type === 'audio') {
playAudio(message.data);
}
};
};
const startAudioCapture = async () => {
const stream = await navigator.mediaDevices
.getUserMedia({ audio: true });
mediaRecorder.current = new MediaRecorder(stream);
mediaRecorder.current.ondataavailable = (event) => {
const audioData = event.data;
ws.current.send(JSON.stringify({
type: 'audio',
data: audioData
}));
};
mediaRecorder.current.start(100); // Send chunks every 100ms
setIsListening(true);
};
const endCall = () => {
mediaRecorder.current.stop();
ws.current.send(JSON.stringify({
type: 'control',
action: 'end'
}));
ws.current.close();
setCallActive(false);
setIsListening(false);
};
return (
<div className="voice-interface">
<div className="call-status">
{callActive ? 'Call in Progress' : 'Call Ended'}
</div>
<div className="transcript">
<p>{transcript}</p>
</div>
<div className="controls">
{!callActive ? (
<button onClick={startCall} className="btn-primary">
Start Call
</button>
) : (
<button onClick={endCall} className="btn-danger">
End Call
</button>
)}
</div>
<div className="audio-status">
{isListening && (
<span className="recording-indicator">
🎤 Recording...
</span>
)}
</div>
</div>
);
}
Analytics Dashboard¶
// Analytics component
function AnalyticsDashboard() {
const [metrics, setMetrics] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
const fetchMetrics = async () => {
const response = await api.get('/analytics/calls');
setMetrics(response.data);
setLoading(false);
};
fetchMetrics();
const interval = setInterval(fetchMetrics, 30000); // Refresh every 30s
return () => clearInterval(interval);
}, []);
if (loading) return <LoadingSpinner />;
return (
<div className="analytics-grid">
<MetricCard
title="Total Calls"
value={metrics.total_calls}
icon="📞"
/>
<MetricCard
title="Average Duration"
value={`${Math.round(metrics.avg_duration)}s`}
icon="⏱️"
/>
<MetricCard
title="Sentiment Score"
value={`${metrics.sentiment_positive}%`}
icon="😊"
/>
<ChartWidget
title="Calls Over Time"
data={metrics.calls_by_hour}
type="line"
/>
<ChartWidget
title="Sentiment Distribution"
data={metrics.sentiment_distribution}
type="pie"
/>
</div>
);
}
API Integration¶
API Client Configuration¶
// lib/api-config.ts
export const API_BASE_URL =
process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:8000';
export const API_TIMEOUT = 30000;
export const API_ENDPOINTS = {
AUTH: {
LOGIN: '/auth/login',
SIGNUP: '/auth/signup',
REFRESH: '/auth/refresh-token',
ME: '/auth/me'
},
AGENTS: {
LIST: '/agents',
CREATE: '/agents',
DETAIL: (id: string) => `/agents/${id}`,
UPDATE: (id: string) => `/agents/${id}`,
DELETE: (id: string) => `/agents/${id}`
},
CAMPAIGNS: {
LIST: '/campaigns',
CREATE: '/campaigns',
DETAIL: (id: string) => `/campaigns/${id}`,
LAUNCH: (id: string) => `/campaigns/${id}/launch`
},
CALL_LOGS: {
LIST: '/call-logs',
DETAIL: (id: string) => `/call-logs/${id}`
},
ANALYTICS: {
CALLS: '/analytics/calls',
SENTIMENT: '/analytics/sentiment'
}
};
API Client¶
// lib/api.ts
import axios from 'axios';
export const apiClient = axios.create({
baseURL: API_BASE_URL,
timeout: API_TIMEOUT
});
// Add token to headers
apiClient.interceptors.request.use((config) => {
const token = localStorage.getItem('token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
// Handle errors
apiClient.interceptors.response.use(
response => response,
error => {
if (error.response?.status === 401) {
// Redirect to login
window.location.href = '/login';
}
return Promise.reject(error);
}
);
export const api = {
get: (url: string) => apiClient.get(url),
post: (url: string, data: any) => apiClient.post(url, data),
put: (url: string, data: any) => apiClient.put(url, data),
delete: (url: string) => apiClient.delete(url)
};
Styling¶
TailwindCSS Configuration¶
// tailwind.config.js
module.exports = {
content: [
'./app/**/*.{js,ts,jsx,tsx}',
'./components/**/*.{js,ts,jsx,tsx}'
],
theme: {
extend: {
colors: {
primary: '#3B82F6',
secondary: '#10B981',
danger: '#EF4444'
},
spacing: {
'container': '1200px'
}
}
},
plugins: []
};
Example Component Styling¶
export function AgentCard({ agent }) {
return (
<div className="bg-white rounded-lg shadow-md p-6 hover:shadow-lg transition-shadow">
<h3 className="text-lg font-semibold text-gray-900">
{agent.name}
</h3>
<p className="text-sm text-gray-600 mt-2">
{agent.description}
</p>
<div className="flex gap-2 mt-4">
<button className="px-4 py-2 bg-primary text-white rounded hover:bg-blue-600">
Edit
</button>
<button className="px-4 py-2 bg-danger text-white rounded hover:bg-red-600">
Delete
</button>
</div>
</div>
);
}
Environment Variables¶
# API Configuration
NEXT_PUBLIC_API_BASE_URL=http://localhost:8000
NEXT_PUBLIC_API_TIMEOUT=30000
# Voice Server
NEXT_PUBLIC_VOICE_SERVER_URL=http://localhost:7860
NEXT_PUBLIC_WS_URL=ws://localhost:7860
# Authentication
NEXT_PUBLIC_AUTH_ENABLED=true
NEXT_PUBLIC_JWT_STORAGE_KEY=voicera_token
# Application
NEXT_PUBLIC_APP_NAME=VoiceERA
NEXT_PUBLIC_APP_VERSION=1.0.0
NEXT_PUBLIC_LOG_LEVEL=info
# Analytics
NEXT_PUBLIC_ANALYTICS_ENABLED=false
Next Steps¶
- Installation - Set up development
- Quick Start - Get running
- Backend API - API documentation