Add comprehensive CSS styles for UI components and dark mode support

This commit is contained in:
rUv
2025-06-07 13:28:02 +00:00
parent 90f03bac7d
commit 6fe0d42f90
22 changed files with 5992 additions and 187 deletions

252
ui/app.js Normal file
View File

@@ -0,0 +1,252 @@
// WiFi DensePose Application - Main Entry Point
import { TabManager } from './components/TabManager.js';
import { DashboardTab } from './components/DashboardTab.js';
import { HardwareTab } from './components/HardwareTab.js';
import { LiveDemoTab } from './components/LiveDemoTab.js';
import { apiService } from './services/api.service.js';
import { wsService } from './services/websocket.service.js';
import { healthService } from './services/health.service.js';
class WiFiDensePoseApp {
constructor() {
this.components = {};
this.isInitialized = false;
}
// Initialize application
async init() {
try {
console.log('Initializing WiFi DensePose UI...');
// Set up error handling
this.setupErrorHandling();
// Initialize services
await this.initializeServices();
// Initialize UI components
this.initializeComponents();
// Set up global event listeners
this.setupEventListeners();
this.isInitialized = true;
console.log('WiFi DensePose UI initialized successfully');
} catch (error) {
console.error('Failed to initialize application:', error);
this.showGlobalError('Failed to initialize application. Please refresh the page.');
}
}
// Initialize services
async initializeServices() {
// Add request interceptor for error handling
apiService.addResponseInterceptor(async (response, url) => {
if (!response.ok && response.status === 401) {
console.warn('Authentication required for:', url);
// Handle authentication if needed
}
return response;
});
// Check API availability
try {
const health = await healthService.checkLiveness();
console.log('API is available:', health);
} catch (error) {
console.error('API is not available:', error);
throw new Error('API is not available. Please ensure the backend is running.');
}
}
// Initialize UI components
initializeComponents() {
const container = document.querySelector('.container');
if (!container) {
throw new Error('Main container not found');
}
// Initialize tab manager
this.components.tabManager = new TabManager(container);
this.components.tabManager.init();
// Initialize tab components
this.initializeTabComponents();
// Set up tab change handling
this.components.tabManager.onTabChange((newTab, oldTab) => {
this.handleTabChange(newTab, oldTab);
});
}
// Initialize individual tab components
initializeTabComponents() {
// Dashboard tab
const dashboardContainer = document.getElementById('dashboard');
if (dashboardContainer) {
this.components.dashboard = new DashboardTab(dashboardContainer);
this.components.dashboard.init().catch(error => {
console.error('Failed to initialize dashboard:', error);
});
}
// Hardware tab
const hardwareContainer = document.getElementById('hardware');
if (hardwareContainer) {
this.components.hardware = new HardwareTab(hardwareContainer);
this.components.hardware.init();
}
// Live demo tab
const demoContainer = document.getElementById('demo');
if (demoContainer) {
this.components.demo = new LiveDemoTab(demoContainer);
this.components.demo.init();
}
// Architecture tab - static content, no component needed
// Performance tab - static content, no component needed
// Applications tab - static content, no component needed
}
// Handle tab changes
handleTabChange(newTab, oldTab) {
console.log(`Tab changed from ${oldTab} to ${newTab}`);
// Stop demo if leaving demo tab
if (oldTab === 'demo' && this.components.demo) {
this.components.demo.stopDemo();
}
// Update components based on active tab
switch (newTab) {
case 'dashboard':
// Dashboard auto-updates when visible
break;
case 'hardware':
// Hardware visualization is always active
break;
case 'demo':
// Demo starts manually
break;
}
}
// Set up global event listeners
setupEventListeners() {
// Handle window resize
window.addEventListener('resize', () => {
this.handleResize();
});
// Handle visibility change
document.addEventListener('visibilitychange', () => {
this.handleVisibilityChange();
});
// Handle before unload
window.addEventListener('beforeunload', () => {
this.cleanup();
});
}
// Handle window resize
handleResize() {
// Update canvas sizes if needed
const canvases = document.querySelectorAll('canvas');
canvases.forEach(canvas => {
const rect = canvas.parentElement.getBoundingClientRect();
if (canvas.width !== rect.width || canvas.height !== rect.height) {
canvas.width = rect.width;
canvas.height = rect.height;
}
});
}
// Handle visibility change
handleVisibilityChange() {
if (document.hidden) {
// Pause updates when page is hidden
console.log('Page hidden, pausing updates');
healthService.stopHealthMonitoring();
} else {
// Resume updates when page is visible
console.log('Page visible, resuming updates');
healthService.startHealthMonitoring();
}
}
// Set up error handling
setupErrorHandling() {
window.addEventListener('error', (event) => {
console.error('Global error:', event.error);
this.showGlobalError('An unexpected error occurred');
});
window.addEventListener('unhandledrejection', (event) => {
console.error('Unhandled promise rejection:', event.reason);
this.showGlobalError('An unexpected error occurred');
});
}
// Show global error message
showGlobalError(message) {
// Create error toast if it doesn't exist
let errorToast = document.getElementById('globalErrorToast');
if (!errorToast) {
errorToast = document.createElement('div');
errorToast.id = 'globalErrorToast';
errorToast.className = 'error-toast';
document.body.appendChild(errorToast);
}
errorToast.textContent = message;
errorToast.classList.add('show');
setTimeout(() => {
errorToast.classList.remove('show');
}, 5000);
}
// Clean up resources
cleanup() {
console.log('Cleaning up application resources...');
// Dispose all components
Object.values(this.components).forEach(component => {
if (component && typeof component.dispose === 'function') {
component.dispose();
}
});
// Disconnect all WebSocket connections
wsService.disconnectAll();
// Stop health monitoring
healthService.dispose();
}
// Public API
getComponent(name) {
return this.components[name];
}
isReady() {
return this.isInitialized;
}
}
// Initialize app when DOM is ready
document.addEventListener('DOMContentLoaded', () => {
window.wifiDensePoseApp = new WiFiDensePoseApp();
window.wifiDensePoseApp.init();
});
// Export for testing
export { WiFiDensePoseApp };

View File

@@ -0,0 +1,309 @@
// Dashboard Tab Component
import { healthService } from '../services/health.service.js';
import { poseService } from '../services/pose.service.js';
export class DashboardTab {
constructor(containerElement) {
this.container = containerElement;
this.statsElements = {};
this.healthSubscription = null;
this.statsInterval = null;
}
// Initialize component
async init() {
this.cacheElements();
await this.loadInitialData();
this.startMonitoring();
}
// Cache DOM elements
cacheElements() {
// System stats
const statsContainer = this.container.querySelector('.system-stats');
if (statsContainer) {
this.statsElements = {
bodyRegions: statsContainer.querySelector('[data-stat="body-regions"] .stat-value'),
samplingRate: statsContainer.querySelector('[data-stat="sampling-rate"] .stat-value'),
accuracy: statsContainer.querySelector('[data-stat="accuracy"] .stat-value'),
hardwareCost: statsContainer.querySelector('[data-stat="hardware-cost"] .stat-value')
};
}
// Status indicators
this.statusElements = {
apiStatus: this.container.querySelector('.api-status'),
streamStatus: this.container.querySelector('.stream-status'),
hardwareStatus: this.container.querySelector('.hardware-status')
};
}
// Load initial data
async loadInitialData() {
try {
// Get API info
const info = await healthService.getApiInfo();
this.updateApiInfo(info);
// Get current stats
const stats = await poseService.getStats(1);
this.updateStats(stats);
} catch (error) {
console.error('Failed to load dashboard data:', error);
this.showError('Failed to load dashboard data');
}
}
// Start monitoring
startMonitoring() {
// Subscribe to health updates
this.healthSubscription = healthService.subscribeToHealth(health => {
this.updateHealthStatus(health);
});
// Start periodic stats updates
this.statsInterval = setInterval(() => {
this.updateLiveStats();
}, 5000);
// Start health monitoring
healthService.startHealthMonitoring(30000);
}
// Update API info display
updateApiInfo(info) {
// Update version
const versionElement = this.container.querySelector('.api-version');
if (versionElement && info.version) {
versionElement.textContent = `v${info.version}`;
}
// Update environment
const envElement = this.container.querySelector('.api-environment');
if (envElement && info.environment) {
envElement.textContent = info.environment;
envElement.className = `api-environment env-${info.environment}`;
}
// Update features status
if (info.features) {
this.updateFeatures(info.features);
}
}
// Update features display
updateFeatures(features) {
const featuresContainer = this.container.querySelector('.features-status');
if (!featuresContainer) return;
featuresContainer.innerHTML = '';
Object.entries(features).forEach(([feature, enabled]) => {
const featureElement = document.createElement('div');
featureElement.className = `feature-item ${enabled ? 'enabled' : 'disabled'}`;
featureElement.innerHTML = `
<span class="feature-name">${this.formatFeatureName(feature)}</span>
<span class="feature-status">${enabled ? '✓' : '✗'}</span>
`;
featuresContainer.appendChild(featureElement);
});
}
// Update health status
updateHealthStatus(health) {
if (!health) return;
// Update overall status
const overallStatus = this.container.querySelector('.overall-health');
if (overallStatus) {
overallStatus.className = `overall-health status-${health.status}`;
overallStatus.textContent = health.status.toUpperCase();
}
// Update component statuses
if (health.components) {
Object.entries(health.components).forEach(([component, status]) => {
this.updateComponentStatus(component, status);
});
}
// Update metrics
if (health.metrics) {
this.updateSystemMetrics(health.metrics);
}
}
// Update component status
updateComponentStatus(component, status) {
const element = this.container.querySelector(`[data-component="${component}"]`);
if (element) {
element.className = `component-status status-${status.status}`;
element.querySelector('.status-text').textContent = status.status;
if (status.message) {
element.querySelector('.status-message').textContent = status.message;
}
}
}
// Update system metrics
updateSystemMetrics(metrics) {
// CPU usage
const cpuElement = this.container.querySelector('.cpu-usage');
if (cpuElement && metrics.cpu_percent !== undefined) {
cpuElement.textContent = `${metrics.cpu_percent.toFixed(1)}%`;
this.updateProgressBar('cpu', metrics.cpu_percent);
}
// Memory usage
const memoryElement = this.container.querySelector('.memory-usage');
if (memoryElement && metrics.memory_percent !== undefined) {
memoryElement.textContent = `${metrics.memory_percent.toFixed(1)}%`;
this.updateProgressBar('memory', metrics.memory_percent);
}
// Disk usage
const diskElement = this.container.querySelector('.disk-usage');
if (diskElement && metrics.disk_percent !== undefined) {
diskElement.textContent = `${metrics.disk_percent.toFixed(1)}%`;
this.updateProgressBar('disk', metrics.disk_percent);
}
}
// Update progress bar
updateProgressBar(type, percent) {
const progressBar = this.container.querySelector(`.progress-bar[data-type="${type}"]`);
if (progressBar) {
const fill = progressBar.querySelector('.progress-fill');
if (fill) {
fill.style.width = `${percent}%`;
fill.className = `progress-fill ${this.getProgressClass(percent)}`;
}
}
}
// Get progress class based on percentage
getProgressClass(percent) {
if (percent >= 90) return 'critical';
if (percent >= 75) return 'warning';
return 'normal';
}
// Update live statistics
async updateLiveStats() {
try {
// Get current pose data
const currentPose = await poseService.getCurrentPose();
this.updatePoseStats(currentPose);
// Get zones summary
const zonesSummary = await poseService.getZonesSummary();
this.updateZonesDisplay(zonesSummary);
} catch (error) {
console.error('Failed to update live stats:', error);
}
}
// Update pose statistics
updatePoseStats(poseData) {
if (!poseData) return;
// Update person count
const personCount = this.container.querySelector('.person-count');
if (personCount) {
personCount.textContent = poseData.total_persons || 0;
}
// Update average confidence
const avgConfidence = this.container.querySelector('.avg-confidence');
if (avgConfidence && poseData.persons) {
const confidences = poseData.persons.map(p => p.confidence);
const avg = confidences.length > 0
? (confidences.reduce((a, b) => a + b, 0) / confidences.length * 100).toFixed(1)
: 0;
avgConfidence.textContent = `${avg}%`;
}
}
// Update zones display
updateZonesDisplay(zonesSummary) {
const zonesContainer = this.container.querySelector('.zones-summary');
if (!zonesContainer || !zonesSummary) return;
zonesContainer.innerHTML = '';
Object.entries(zonesSummary.zones).forEach(([zoneId, data]) => {
const zoneElement = document.createElement('div');
zoneElement.className = 'zone-item';
zoneElement.innerHTML = `
<span class="zone-name">${data.name || zoneId}</span>
<span class="zone-count">${data.person_count}</span>
`;
zonesContainer.appendChild(zoneElement);
});
}
// Update statistics
updateStats(stats) {
if (!stats) return;
// Update detection count
const detectionCount = this.container.querySelector('.detection-count');
if (detectionCount && stats.total_detections !== undefined) {
detectionCount.textContent = this.formatNumber(stats.total_detections);
}
// Update accuracy if available
if (this.statsElements.accuracy && stats.average_confidence !== undefined) {
this.statsElements.accuracy.textContent = `${(stats.average_confidence * 100).toFixed(1)}%`;
}
}
// Format feature name
formatFeatureName(name) {
return name.replace(/_/g, ' ')
.split(' ')
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
.join(' ');
}
// Format large numbers
formatNumber(num) {
if (num >= 1000000) {
return `${(num / 1000000).toFixed(1)}M`;
}
if (num >= 1000) {
return `${(num / 1000).toFixed(1)}K`;
}
return num.toString();
}
// Show error message
showError(message) {
const errorContainer = this.container.querySelector('.error-container');
if (errorContainer) {
errorContainer.textContent = message;
errorContainer.style.display = 'block';
setTimeout(() => {
errorContainer.style.display = 'none';
}, 5000);
}
}
// Clean up
dispose() {
if (this.healthSubscription) {
this.healthSubscription();
}
if (this.statsInterval) {
clearInterval(this.statsInterval);
}
healthService.stopHealthMonitoring();
}
}

View File

@@ -0,0 +1,165 @@
// Hardware Tab Component
export class HardwareTab {
constructor(containerElement) {
this.container = containerElement;
this.antennas = [];
this.csiUpdateInterval = null;
this.isActive = false;
}
// Initialize component
init() {
this.setupAntennas();
this.startCSISimulation();
}
// Set up antenna interactions
setupAntennas() {
this.antennas = Array.from(this.container.querySelectorAll('.antenna'));
this.antennas.forEach(antenna => {
antenna.addEventListener('click', () => {
antenna.classList.toggle('active');
this.updateCSIDisplay();
});
});
}
// Start CSI simulation
startCSISimulation() {
// Initial update
this.updateCSIDisplay();
// Set up periodic updates
this.csiUpdateInterval = setInterval(() => {
if (this.hasActiveAntennas()) {
this.updateCSIDisplay();
}
}, 1000);
}
// Check if any antennas are active
hasActiveAntennas() {
return this.antennas.some(antenna => antenna.classList.contains('active'));
}
// Update CSI display
updateCSIDisplay() {
const activeAntennas = this.antennas.filter(a => a.classList.contains('active'));
const isActive = activeAntennas.length > 0;
// Get display elements
const amplitudeFill = this.container.querySelector('.csi-fill.amplitude');
const phaseFill = this.container.querySelector('.csi-fill.phase');
const amplitudeValue = this.container.querySelector('.csi-row:first-child .csi-value');
const phaseValue = this.container.querySelector('.csi-row:last-child .csi-value');
if (!isActive) {
// Set to zero when no antennas active
if (amplitudeFill) amplitudeFill.style.width = '0%';
if (phaseFill) phaseFill.style.width = '0%';
if (amplitudeValue) amplitudeValue.textContent = '0.00';
if (phaseValue) phaseValue.textContent = '0.0π';
return;
}
// Generate realistic CSI values based on active antennas
const txCount = activeAntennas.filter(a => a.classList.contains('tx')).length;
const rxCount = activeAntennas.filter(a => a.classList.contains('rx')).length;
// Amplitude increases with more active antennas
const baseAmplitude = 0.3 + (txCount * 0.1) + (rxCount * 0.05);
const amplitude = Math.min(0.95, baseAmplitude + (Math.random() * 0.1 - 0.05));
// Phase varies more with multiple antennas
const phaseVariation = 0.5 + (activeAntennas.length * 0.1);
const phase = 0.5 + Math.random() * phaseVariation;
// Update display
if (amplitudeFill) {
amplitudeFill.style.width = `${amplitude * 100}%`;
amplitudeFill.style.transition = 'width 0.5s ease';
}
if (phaseFill) {
phaseFill.style.width = `${phase * 50}%`;
phaseFill.style.transition = 'width 0.5s ease';
}
if (amplitudeValue) {
amplitudeValue.textContent = amplitude.toFixed(2);
}
if (phaseValue) {
phaseValue.textContent = `${phase.toFixed(1)}π`;
}
// Update antenna array visualization
this.updateAntennaArray(activeAntennas);
}
// Update antenna array visualization
updateAntennaArray(activeAntennas) {
const arrayStatus = this.container.querySelector('.array-status');
if (!arrayStatus) return;
const txActive = activeAntennas.filter(a => a.classList.contains('tx')).length;
const rxActive = activeAntennas.filter(a => a.classList.contains('rx')).length;
arrayStatus.innerHTML = `
<div class="array-info">
<span class="info-label">Active TX:</span>
<span class="info-value">${txActive}/3</span>
</div>
<div class="array-info">
<span class="info-label">Active RX:</span>
<span class="info-value">${rxActive}/6</span>
</div>
<div class="array-info">
<span class="info-label">Signal Quality:</span>
<span class="info-value">${this.calculateSignalQuality(txActive, rxActive)}%</span>
</div>
`;
}
// Calculate signal quality based on active antennas
calculateSignalQuality(txCount, rxCount) {
if (txCount === 0 || rxCount === 0) return 0;
const txRatio = txCount / 3;
const rxRatio = rxCount / 6;
const quality = (txRatio * 0.4 + rxRatio * 0.6) * 100;
return Math.round(quality);
}
// Toggle all antennas
toggleAllAntennas(active) {
this.antennas.forEach(antenna => {
antenna.classList.toggle('active', active);
});
this.updateCSIDisplay();
}
// Reset antenna configuration
resetAntennas() {
// Set default configuration (all active)
this.antennas.forEach(antenna => {
antenna.classList.add('active');
});
this.updateCSIDisplay();
}
// Clean up
dispose() {
if (this.csiUpdateInterval) {
clearInterval(this.csiUpdateInterval);
this.csiUpdateInterval = null;
}
this.antennas.forEach(antenna => {
antenna.removeEventListener('click', this.toggleAntenna);
});
}
}

View File

@@ -0,0 +1,391 @@
// Live Demo Tab Component
import { poseService } from '../services/pose.service.js';
import { streamService } from '../services/stream.service.js';
export class LiveDemoTab {
constructor(containerElement) {
this.container = containerElement;
this.isRunning = false;
this.streamConnection = null;
this.poseSubscription = null;
this.signalCanvas = null;
this.poseCanvas = null;
this.signalCtx = null;
this.poseCtx = null;
this.animationFrame = null;
this.signalTime = 0;
this.poseData = null;
}
// Initialize component
init() {
this.setupCanvases();
this.setupControls();
this.initializeDisplays();
}
// Set up canvases
setupCanvases() {
this.signalCanvas = this.container.querySelector('#signalCanvas');
this.poseCanvas = this.container.querySelector('#poseCanvas');
if (this.signalCanvas) {
this.signalCtx = this.signalCanvas.getContext('2d');
}
if (this.poseCanvas) {
this.poseCtx = this.poseCanvas.getContext('2d');
}
}
// Set up control buttons
setupControls() {
const startButton = this.container.querySelector('#startDemo');
const stopButton = this.container.querySelector('#stopDemo');
if (startButton) {
startButton.addEventListener('click', () => this.startDemo());
}
if (stopButton) {
stopButton.addEventListener('click', () => this.stopDemo());
}
}
// Initialize displays
initializeDisplays() {
// Initialize signal canvas
if (this.signalCtx) {
this.signalCtx.fillStyle = 'rgba(0, 0, 0, 0.2)';
this.signalCtx.fillRect(0, 0, this.signalCanvas.width, this.signalCanvas.height);
}
// Initialize pose canvas
if (this.poseCtx) {
this.poseCtx.fillStyle = 'rgba(0, 0, 0, 0.2)';
this.poseCtx.fillRect(0, 0, this.poseCanvas.width, this.poseCanvas.height);
}
}
// Start demo
async startDemo() {
if (this.isRunning) return;
try {
// Update UI
this.isRunning = true;
this.updateControls();
this.updateStatus('Starting...', 'info');
// Check stream status
const streamStatus = await streamService.getStatus();
if (!streamStatus.is_active) {
// Try to start streaming
await streamService.start();
}
// Start pose stream
this.streamConnection = poseService.startPoseStream({
minConfidence: 0.5,
maxFps: 30
});
// Subscribe to pose updates
this.poseSubscription = poseService.subscribeToPoseUpdates(update => {
this.handlePoseUpdate(update);
});
// Start animations
this.startAnimations();
// Update status
this.updateStatus('Running', 'success');
} catch (error) {
console.error('Failed to start demo:', error);
this.updateStatus('Failed to start', 'error');
this.stopDemo();
}
}
// Stop demo
stopDemo() {
if (!this.isRunning) return;
// Update UI
this.isRunning = false;
this.updateControls();
this.updateStatus('Stopped', 'info');
// Stop pose stream
if (this.poseSubscription) {
this.poseSubscription();
this.poseSubscription = null;
}
poseService.stopPoseStream();
this.streamConnection = null;
// Stop animations
if (this.animationFrame) {
cancelAnimationFrame(this.animationFrame);
this.animationFrame = null;
}
}
// Update controls
updateControls() {
const startButton = this.container.querySelector('#startDemo');
const stopButton = this.container.querySelector('#stopDemo');
if (startButton) {
startButton.disabled = this.isRunning;
}
if (stopButton) {
stopButton.disabled = !this.isRunning;
}
}
// Update status display
updateStatus(text, type) {
const statusElement = this.container.querySelector('#demoStatus');
if (statusElement) {
statusElement.textContent = text;
statusElement.className = `status status--${type}`;
}
}
// Handle pose updates
handlePoseUpdate(update) {
switch (update.type) {
case 'connected':
console.log('Pose stream connected');
break;
case 'pose_update':
this.poseData = update.data;
this.updateMetrics(update.data);
break;
case 'error':
console.error('Pose stream error:', update.error);
this.updateStatus('Stream error', 'error');
break;
case 'disconnected':
console.log('Pose stream disconnected');
if (this.isRunning) {
this.updateStatus('Disconnected', 'warning');
}
break;
}
}
// Update metrics display
updateMetrics(poseData) {
if (!poseData) return;
// Update signal strength (simulated based on detection confidence)
const signalStrength = this.container.querySelector('#signalStrength');
if (signalStrength) {
const strength = poseData.persons?.length > 0
? -45 - Math.random() * 10
: -55 - Math.random() * 10;
signalStrength.textContent = `${strength.toFixed(0)} dBm`;
}
// Update latency
const latency = this.container.querySelector('#latency');
if (latency && poseData.processing_time) {
latency.textContent = `${poseData.processing_time.toFixed(0)} ms`;
}
// Update person count
const personCount = this.container.querySelector('#personCount');
if (personCount) {
personCount.textContent = poseData.persons?.length || 0;
}
// Update confidence
const confidence = this.container.querySelector('#confidence');
if (confidence && poseData.persons?.length > 0) {
const avgConfidence = poseData.persons.reduce((sum, p) => sum + p.confidence, 0)
/ poseData.persons.length * 100;
confidence.textContent = `${avgConfidence.toFixed(1)}%`;
}
// Update keypoints
const keypoints = this.container.querySelector('#keypoints');
if (keypoints && poseData.persons?.length > 0) {
const totalKeypoints = poseData.persons[0].keypoints?.length || 0;
const detectedKeypoints = poseData.persons[0].keypoints?.filter(kp => kp.confidence > 0.5).length || 0;
keypoints.textContent = `${detectedKeypoints}/${totalKeypoints}`;
}
}
// Start animations
startAnimations() {
const animate = () => {
if (!this.isRunning) return;
// Update signal visualization
this.updateSignalVisualization();
// Update pose visualization
this.updatePoseVisualization();
this.animationFrame = requestAnimationFrame(animate);
};
animate();
}
// Update signal visualization
updateSignalVisualization() {
if (!this.signalCtx) return;
const ctx = this.signalCtx;
const width = this.signalCanvas.width;
const height = this.signalCanvas.height;
// Clear canvas with fade effect
ctx.fillStyle = 'rgba(0, 0, 0, 0.1)';
ctx.fillRect(0, 0, width, height);
// Draw amplitude signal
ctx.beginPath();
ctx.strokeStyle = '#1FB8CD';
ctx.lineWidth = 2;
for (let x = 0; x < width; x++) {
const hasData = this.poseData?.persons?.length > 0;
const amplitude = hasData ? 30 : 10;
const frequency = hasData ? 0.05 : 0.02;
const y = height / 2 +
Math.sin(x * frequency + this.signalTime) * amplitude +
Math.sin(x * 0.02 + this.signalTime * 1.5) * 15;
if (x === 0) {
ctx.moveTo(x, y);
} else {
ctx.lineTo(x, y);
}
}
ctx.stroke();
// Draw phase signal
ctx.beginPath();
ctx.strokeStyle = '#FFC185';
ctx.lineWidth = 2;
for (let x = 0; x < width; x++) {
const hasData = this.poseData?.persons?.length > 0;
const amplitude = hasData ? 25 : 15;
const y = height / 2 +
Math.cos(x * 0.03 + this.signalTime * 0.8) * amplitude +
Math.cos(x * 0.01 + this.signalTime * 0.5) * 20;
if (x === 0) {
ctx.moveTo(x, y);
} else {
ctx.lineTo(x, y);
}
}
ctx.stroke();
this.signalTime += 0.05;
}
// Update pose visualization
updatePoseVisualization() {
if (!this.poseCtx || !this.poseData) return;
const ctx = this.poseCtx;
const width = this.poseCanvas.width;
const height = this.poseCanvas.height;
// Clear canvas
ctx.clearRect(0, 0, width, height);
ctx.fillStyle = 'rgba(0, 0, 0, 0.2)';
ctx.fillRect(0, 0, width, height);
// Draw each detected person
if (this.poseData.persons) {
this.poseData.persons.forEach((person, index) => {
this.drawPerson(ctx, person, index);
});
}
}
// Draw a person's pose
drawPerson(ctx, person, index) {
if (!person.keypoints) return;
// Define COCO keypoint connections
const connections = [
[0, 1], [0, 2], [1, 3], [2, 4], // Head
[5, 6], [5, 7], [7, 9], [6, 8], [8, 10], // Arms
[5, 11], [6, 12], [11, 12], // Body
[11, 13], [13, 15], [12, 14], [14, 16] // Legs
];
// Scale keypoints to canvas
const scale = Math.min(this.poseCanvas.width, this.poseCanvas.height) / 2;
const offsetX = this.poseCanvas.width / 2;
const offsetY = this.poseCanvas.height / 2;
// Draw skeleton connections
ctx.strokeStyle = `hsl(${index * 60}, 70%, 50%)`;
ctx.lineWidth = 3;
connections.forEach(([i, j]) => {
const kp1 = person.keypoints[i];
const kp2 = person.keypoints[j];
if (kp1 && kp2 && kp1.confidence > 0.3 && kp2.confidence > 0.3) {
ctx.beginPath();
ctx.moveTo(kp1.x * scale + offsetX, kp1.y * scale + offsetY);
ctx.lineTo(kp2.x * scale + offsetX, kp2.y * scale + offsetY);
ctx.stroke();
}
});
// Draw keypoints
ctx.fillStyle = `hsl(${index * 60}, 70%, 60%)`;
person.keypoints.forEach(kp => {
if (kp.confidence > 0.3) {
ctx.beginPath();
ctx.arc(
kp.x * scale + offsetX,
kp.y * scale + offsetY,
5,
0,
Math.PI * 2
);
ctx.fill();
}
});
// Draw confidence label
ctx.fillStyle = 'white';
ctx.font = '12px monospace';
ctx.fillText(
`Person ${index + 1}: ${(person.confidence * 100).toFixed(1)}%`,
10,
20 + index * 20
);
}
// Clean up
dispose() {
this.stopDemo();
}
}

138
ui/components/TabManager.js Normal file
View File

@@ -0,0 +1,138 @@
// Tab Manager Component
export class TabManager {
constructor(containerElement) {
this.container = containerElement;
this.tabs = [];
this.activeTab = null;
this.tabChangeCallbacks = [];
}
// Initialize tabs
init() {
// Find all tabs and contents
this.tabs = Array.from(this.container.querySelectorAll('.nav-tab'));
this.tabContents = Array.from(this.container.querySelectorAll('.tab-content'));
// Set up event listeners
this.tabs.forEach(tab => {
tab.addEventListener('click', () => this.switchTab(tab));
});
// Activate first tab if none active
const activeTab = this.tabs.find(tab => tab.classList.contains('active'));
if (activeTab) {
this.activeTab = activeTab.getAttribute('data-tab');
} else if (this.tabs.length > 0) {
this.switchTab(this.tabs[0]);
}
}
// Switch to a tab
switchTab(tabElement) {
const tabId = tabElement.getAttribute('data-tab');
if (tabId === this.activeTab) {
return;
}
// Update tab states
this.tabs.forEach(tab => {
tab.classList.toggle('active', tab === tabElement);
});
// Update content visibility
this.tabContents.forEach(content => {
content.classList.toggle('active', content.id === tabId);
});
// Update active tab
const previousTab = this.activeTab;
this.activeTab = tabId;
// Notify callbacks
this.notifyTabChange(tabId, previousTab);
}
// Switch to tab by ID
switchToTab(tabId) {
const tab = this.tabs.find(t => t.getAttribute('data-tab') === tabId);
if (tab) {
this.switchTab(tab);
}
}
// Register tab change callback
onTabChange(callback) {
this.tabChangeCallbacks.push(callback);
// Return unsubscribe function
return () => {
const index = this.tabChangeCallbacks.indexOf(callback);
if (index > -1) {
this.tabChangeCallbacks.splice(index, 1);
}
};
}
// Notify tab change callbacks
notifyTabChange(newTab, previousTab) {
this.tabChangeCallbacks.forEach(callback => {
try {
callback(newTab, previousTab);
} catch (error) {
console.error('Error in tab change callback:', error);
}
});
}
// Get active tab
getActiveTab() {
return this.activeTab;
}
// Enable/disable tab
setTabEnabled(tabId, enabled) {
const tab = this.tabs.find(t => t.getAttribute('data-tab') === tabId);
if (tab) {
tab.disabled = !enabled;
tab.classList.toggle('disabled', !enabled);
}
}
// Show/hide tab
setTabVisible(tabId, visible) {
const tab = this.tabs.find(t => t.getAttribute('data-tab') === tabId);
if (tab) {
tab.style.display = visible ? '' : 'none';
}
}
// Add badge to tab
setTabBadge(tabId, badge) {
const tab = this.tabs.find(t => t.getAttribute('data-tab') === tabId);
if (!tab) return;
// Remove existing badge
const existingBadge = tab.querySelector('.tab-badge');
if (existingBadge) {
existingBadge.remove();
}
// Add new badge if provided
if (badge) {
const badgeElement = document.createElement('span');
badgeElement.className = 'tab-badge';
badgeElement.textContent = badge;
tab.appendChild(badgeElement);
}
}
// Clean up
dispose() {
this.tabs.forEach(tab => {
tab.removeEventListener('click', this.switchTab);
});
this.tabChangeCallbacks = [];
}
}

118
ui/config/api.config.js Normal file
View File

@@ -0,0 +1,118 @@
// API Configuration for WiFi-DensePose UI
export const API_CONFIG = {
BASE_URL: window.location.origin,
API_VERSION: '/api/v1',
WS_PREFIX: 'ws://',
WSS_PREFIX: 'wss://',
// API Endpoints
ENDPOINTS: {
// Root & Info
ROOT: '/',
INFO: '/api/v1/info',
STATUS: '/api/v1/status',
METRICS: '/api/v1/metrics',
// Health
HEALTH: {
SYSTEM: '/health/health',
READY: '/health/ready',
LIVE: '/health/live',
METRICS: '/health/metrics',
VERSION: '/health/version'
},
// Pose
POSE: {
CURRENT: '/api/v1/pose/current',
ANALYZE: '/api/v1/pose/analyze',
ZONE_OCCUPANCY: '/api/v1/pose/zones/{zone_id}/occupancy',
ZONES_SUMMARY: '/api/v1/pose/zones/summary',
HISTORICAL: '/api/v1/pose/historical',
ACTIVITIES: '/api/v1/pose/activities',
CALIBRATE: '/api/v1/pose/calibrate',
CALIBRATION_STATUS: '/api/v1/pose/calibration/status',
STATS: '/api/v1/pose/stats'
},
// Streaming
STREAM: {
STATUS: '/api/v1/stream/status',
START: '/api/v1/stream/start',
STOP: '/api/v1/stream/stop',
CLIENTS: '/api/v1/stream/clients',
DISCONNECT_CLIENT: '/api/v1/stream/clients/{client_id}',
BROADCAST: '/api/v1/stream/broadcast',
METRICS: '/api/v1/stream/metrics',
// WebSocket endpoints
WS_POSE: '/api/v1/stream/pose',
WS_EVENTS: '/api/v1/stream/events'
},
// Development (only in dev mode)
DEV: {
CONFIG: '/api/v1/dev/config',
RESET: '/api/v1/dev/reset'
}
},
// Default request options
DEFAULT_HEADERS: {
'Content-Type': 'application/json',
'Accept': 'application/json'
},
// Rate limiting
RATE_LIMITS: {
REQUESTS_PER_MINUTE: 60,
BURST_LIMIT: 10
},
// WebSocket configuration
WS_CONFIG: {
RECONNECT_DELAY: 5000,
MAX_RECONNECT_ATTEMPTS: 5,
PING_INTERVAL: 30000,
MESSAGE_TIMEOUT: 10000
}
};
// Helper function to build API URLs
export function buildApiUrl(endpoint, params = {}) {
let url = `${API_CONFIG.BASE_URL}${endpoint}`;
// Replace path parameters
Object.keys(params).forEach(key => {
if (url.includes(`{${key}}`)) {
url = url.replace(`{${key}}`, params[key]);
delete params[key];
}
});
// Add query parameters
const queryParams = new URLSearchParams(params);
if (queryParams.toString()) {
url += `?${queryParams.toString()}`;
}
return url;
}
// Helper function to build WebSocket URLs
export function buildWsUrl(endpoint, params = {}) {
const protocol = window.location.protocol === 'https:'
? API_CONFIG.WSS_PREFIX
: API_CONFIG.WS_PREFIX;
const host = window.location.host;
let url = `${protocol}${host}${endpoint}`;
// Add query parameters
const queryParams = new URLSearchParams(params);
if (queryParams.toString()) {
url += `?${queryParams.toString()}`;
}
return url;
}

489
ui/index.html Normal file
View File

@@ -0,0 +1,489 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>WiFi DensePose: Human Tracking Through Walls</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div class="container">
<!-- Header -->
<header class="header">
<h1>WiFi DensePose</h1>
<p class="subtitle">Human Tracking Through Walls Using WiFi Signals</p>
<div class="header-info">
<span class="api-version"></span>
<span class="api-environment"></span>
<span class="overall-health"></span>
</div>
</header>
<!-- Navigation -->
<nav class="nav-tabs">
<button class="nav-tab active" data-tab="dashboard">Dashboard</button>
<button class="nav-tab" data-tab="hardware">Hardware</button>
<button class="nav-tab" data-tab="demo">Live Demo</button>
<button class="nav-tab" data-tab="architecture">Architecture</button>
<button class="nav-tab" data-tab="performance">Performance</button>
<button class="nav-tab" data-tab="applications">Applications</button>
</nav>
<!-- Dashboard Tab -->
<section id="dashboard" class="tab-content active">
<div class="hero-section">
<h2>Revolutionary WiFi-Based Human Pose Detection</h2>
<p class="hero-description">
AI can track your full-body movement through walls using just WiFi signals.
Researchers at Carnegie Mellon have trained a neural network to turn basic WiFi
signals into detailed wireframe models of human bodies.
</p>
<!-- Error container -->
<div class="error-container" style="display: none;"></div>
<!-- Live Status Panel -->
<div class="live-status-panel">
<h3>System Status</h3>
<div class="status-grid">
<div class="component-status" data-component="api">
<span class="component-name">API Server</span>
<span class="status-text">-</span>
<span class="status-message"></span>
</div>
<div class="component-status" data-component="hardware">
<span class="component-name">Hardware</span>
<span class="status-text">-</span>
<span class="status-message"></span>
</div>
<div class="component-status" data-component="inference">
<span class="component-name">Inference</span>
<span class="status-text">-</span>
<span class="status-message"></span>
</div>
<div class="component-status" data-component="streaming">
<span class="component-name">Streaming</span>
<span class="status-text">-</span>
<span class="status-message"></span>
</div>
</div>
</div>
<!-- System Metrics -->
<div class="system-metrics-panel">
<h3>System Metrics</h3>
<div class="metrics-grid">
<div class="metric-item">
<span class="metric-label">CPU Usage</span>
<div class="progress-bar" data-type="cpu">
<div class="progress-fill normal" style="width: 0%"></div>
</div>
<span class="cpu-usage">0%</span>
</div>
<div class="metric-item">
<span class="metric-label">Memory Usage</span>
<div class="progress-bar" data-type="memory">
<div class="progress-fill normal" style="width: 0%"></div>
</div>
<span class="memory-usage">0%</span>
</div>
<div class="metric-item">
<span class="metric-label">Disk Usage</span>
<div class="progress-bar" data-type="disk">
<div class="progress-fill normal" style="width: 0%"></div>
</div>
<span class="disk-usage">0%</span>
</div>
</div>
</div>
<!-- Features Status -->
<div class="features-panel">
<h3>Features</h3>
<div class="features-status"></div>
</div>
<!-- Live Statistics -->
<div class="live-stats-panel">
<h3>Live Statistics</h3>
<div class="stats-grid">
<div class="stat-item">
<span class="stat-label">Active Persons</span>
<span class="person-count">0</span>
</div>
<div class="stat-item">
<span class="stat-label">Avg Confidence</span>
<span class="avg-confidence">0%</span>
</div>
<div class="stat-item">
<span class="stat-label">Total Detections</span>
<span class="detection-count">0</span>
</div>
</div>
<div class="zones-panel">
<h4>Zone Occupancy</h4>
<div class="zones-summary"></div>
</div>
</div>
<div class="key-benefits">
<div class="benefit-card">
<div class="benefit-icon">🏠</div>
<h3>Through Walls</h3>
<p>Works through solid barriers with no line of sight required</p>
</div>
<div class="benefit-card">
<div class="benefit-icon">🔒</div>
<h3>Privacy-Preserving</h3>
<p>No cameras or visual recording - just WiFi signal analysis</p>
</div>
<div class="benefit-card">
<div class="benefit-icon"></div>
<h3>Real-Time</h3>
<p>Maps 24 body regions in real-time at 100Hz sampling rate</p>
</div>
<div class="benefit-card">
<div class="benefit-icon">💰</div>
<h3>Low Cost</h3>
<p>Built using $30 commercial WiFi hardware</p>
</div>
</div>
<div class="system-stats">
<div class="stat" data-stat="body-regions">
<span class="stat-value">24</span>
<span class="stat-label">Body Regions</span>
</div>
<div class="stat" data-stat="sampling-rate">
<span class="stat-value">100Hz</span>
<span class="stat-label">Sampling Rate</span>
</div>
<div class="stat" data-stat="accuracy">
<span class="stat-value">87.2%</span>
<span class="stat-label">Accuracy (AP@50)</span>
</div>
<div class="stat" data-stat="hardware-cost">
<span class="stat-value">$30</span>
<span class="stat-label">Hardware Cost</span>
</div>
</div>
</div>
</section>
<!-- Hardware Tab -->
<section id="hardware" class="tab-content">
<h2>Hardware Configuration</h2>
<div class="hardware-grid">
<div class="antenna-section">
<h3>3×3 Antenna Array</h3>
<p class="help-text">Click antennas to toggle their state</p>
<div class="antenna-array">
<div class="antenna-grid">
<div class="antenna tx active" data-type="TX1"></div>
<div class="antenna tx active" data-type="TX2"></div>
<div class="antenna tx active" data-type="TX3"></div>
<div class="antenna rx active" data-type="RX1"></div>
<div class="antenna rx active" data-type="RX2"></div>
<div class="antenna rx active" data-type="RX3"></div>
<div class="antenna rx active" data-type="RX4"></div>
<div class="antenna rx active" data-type="RX5"></div>
<div class="antenna rx active" data-type="RX6"></div>
</div>
<div class="antenna-legend">
<div class="legend-item">
<div class="legend-color tx"></div>
<span>Transmitters (3)</span>
</div>
<div class="legend-item">
<div class="legend-color rx"></div>
<span>Receivers (6)</span>
</div>
</div>
<div class="array-status"></div>
</div>
</div>
<div class="config-section">
<h3>WiFi Configuration</h3>
<div class="config-grid">
<div class="config-item">
<label>Frequency</label>
<div class="config-value">2.4GHz ± 20MHz</div>
</div>
<div class="config-item">
<label>Subcarriers</label>
<div class="config-value">30</div>
</div>
<div class="config-item">
<label>Sampling Rate</label>
<div class="config-value">100 Hz</div>
</div>
<div class="config-item">
<label>Total Cost</label>
<div class="config-value">$30</div>
</div>
</div>
<div class="csi-data">
<h4>Real-time CSI Data</h4>
<div class="csi-display">
<div class="csi-row">
<span>Amplitude:</span>
<div class="csi-bar">
<div class="csi-fill amplitude" style="width: 75%"></div>
</div>
<span class="csi-value">0.75</span>
</div>
<div class="csi-row">
<span>Phase:</span>
<div class="csi-bar">
<div class="csi-fill phase" style="width: 60%"></div>
</div>
<span class="csi-value">1.2π</span>
</div>
</div>
</div>
</div>
</div>
</section>
<!-- Demo Tab -->
<section id="demo" class="tab-content">
<h2>Live Demonstration</h2>
<div class="demo-controls">
<button id="startDemo" class="btn btn--primary">Start Stream</button>
<button id="stopDemo" class="btn btn--secondary" disabled>Stop Stream</button>
<div class="demo-status">
<span class="status status--info" id="demoStatus">Ready</span>
</div>
</div>
<div class="demo-grid">
<div class="signal-panel">
<h3>WiFi Signal Analysis</h3>
<div class="signal-display">
<canvas id="signalCanvas" width="400" height="200"></canvas>
</div>
<div class="signal-metrics">
<div class="metric">
<span>Signal Strength:</span>
<span id="signalStrength">-45 dBm</span>
</div>
<div class="metric">
<span>Processing Latency:</span>
<span id="latency">12 ms</span>
</div>
</div>
</div>
<div class="pose-panel">
<h3>Human Pose Detection</h3>
<div class="pose-display">
<canvas id="poseCanvas" width="400" height="300"></canvas>
</div>
<div class="detection-info">
<div class="info-item">
<span>Persons Detected:</span>
<span id="personCount">0</span>
</div>
<div class="info-item">
<span>Confidence:</span>
<span id="confidence">0.0%</span>
</div>
<div class="info-item">
<span>Keypoints:</span>
<span id="keypoints">0/0</span>
</div>
</div>
</div>
</div>
</section>
<!-- Architecture Tab -->
<section id="architecture" class="tab-content">
<h2>System Architecture</h2>
<div class="architecture-flow">
<img src="https://pplx-res.cloudinary.com/image/upload/v1748813853/gpt4o_images/m7zztcttnue7vaxclvuw.png"
alt="WiFi DensePose Architecture" class="architecture-image">
<div class="flow-steps">
<div class="step-card" data-step="1">
<div class="step-number">1</div>
<h3>CSI Input</h3>
<p>Channel State Information collected from WiFi antenna array</p>
</div>
<div class="step-card" data-step="2">
<div class="step-number">2</div>
<h3>Phase Sanitization</h3>
<p>Remove hardware-specific noise and normalize signal phase</p>
</div>
<div class="step-card" data-step="3">
<div class="step-number">3</div>
<h3>Modality Translation</h3>
<p>Convert WiFi signals to visual representation using CNN</p>
</div>
<div class="step-card" data-step="4">
<div class="step-number">4</div>
<h3>DensePose-RCNN</h3>
<p>Extract human pose keypoints and body part segmentation</p>
</div>
<div class="step-card" data-step="5">
<div class="step-number">5</div>
<h3>Wireframe Output</h3>
<p>Generate final human pose wireframe visualization</p>
</div>
</div>
</div>
</section>
<!-- Performance Tab -->
<section id="performance" class="tab-content">
<h2>Performance Analysis</h2>
<div class="performance-chart">
<img src="https://pplx-res.cloudinary.com/image/upload/v1748813924/pplx_code_interpreter/af6ef268_nsauu6.jpg"
alt="Performance Comparison Chart" class="chart-image">
</div>
<div class="performance-grid">
<div class="performance-card">
<h3>WiFi-based (Same Layout)</h3>
<div class="metric-list">
<div class="metric-item">
<span>Average Precision:</span>
<span class="metric-value">43.5%</span>
</div>
<div class="metric-item">
<span>AP@50:</span>
<span class="metric-value success">87.2%</span>
</div>
<div class="metric-item">
<span>AP@75:</span>
<span class="metric-value">44.6%</span>
</div>
</div>
</div>
<div class="performance-card">
<h3>Image-based (Reference)</h3>
<div class="metric-list">
<div class="metric-item">
<span>Average Precision:</span>
<span class="metric-value success">84.7%</span>
</div>
<div class="metric-item">
<span>AP@50:</span>
<span class="metric-value success">94.4%</span>
</div>
<div class="metric-item">
<span>AP@75:</span>
<span class="metric-value success">77.1%</span>
</div>
</div>
</div>
<div class="limitations-section">
<h3>Advantages & Limitations</h3>
<div class="pros-cons">
<div class="pros">
<h4>Advantages</h4>
<ul>
<li>Through-wall detection</li>
<li>Privacy preserving</li>
<li>Lighting independent</li>
<li>Low cost hardware</li>
<li>Uses existing WiFi</li>
</ul>
</div>
<div class="cons">
<h4>Limitations</h4>
<ul>
<li>Performance drops in different layouts</li>
<li>Requires WiFi-compatible devices</li>
<li>Training requires synchronized data</li>
</ul>
</div>
</div>
</div>
</div>
</section>
<!-- Applications Tab -->
<section id="applications" class="tab-content">
<h2>Real-World Applications</h2>
<div class="applications-grid">
<div class="app-card">
<div class="app-icon">👴</div>
<h3>Elderly Care Monitoring</h3>
<p>Monitor elderly individuals for falls or emergencies without invading privacy. Track movement patterns and detect anomalies in daily routines.</p>
<div class="app-features">
<span class="feature-tag">Fall Detection</span>
<span class="feature-tag">Activity Monitoring</span>
<span class="feature-tag">Emergency Alert</span>
</div>
</div>
<div class="app-card">
<div class="app-icon">🏠</div>
<h3>Home Security Systems</h3>
<p>Detect intruders and monitor home security without visible cameras. Track multiple persons and identify suspicious movement patterns.</p>
<div class="app-features">
<span class="feature-tag">Intrusion Detection</span>
<span class="feature-tag">Multi-person Tracking</span>
<span class="feature-tag">Invisible Monitoring</span>
</div>
</div>
<div class="app-card">
<div class="app-icon">🏥</div>
<h3>Healthcare Patient Monitoring</h3>
<p>Monitor patients in hospitals and care facilities. Track vital signs through movement analysis and detect health emergencies.</p>
<div class="app-features">
<span class="feature-tag">Vital Sign Analysis</span>
<span class="feature-tag">Movement Tracking</span>
<span class="feature-tag">Health Alerts</span>
</div>
</div>
<div class="app-card">
<div class="app-icon">🏢</div>
<h3>Smart Building Occupancy</h3>
<p>Optimize building energy consumption by tracking occupancy patterns. Control lighting, HVAC, and security systems automatically.</p>
<div class="app-features">
<span class="feature-tag">Energy Optimization</span>
<span class="feature-tag">Occupancy Tracking</span>
<span class="feature-tag">Smart Controls</span>
</div>
</div>
<div class="app-card">
<div class="app-icon">🥽</div>
<h3>AR/VR Applications</h3>
<p>Enable full-body tracking for virtual and augmented reality applications without wearing additional sensors or cameras.</p>
<div class="app-features">
<span class="feature-tag">Full Body Tracking</span>
<span class="feature-tag">Sensor-free</span>
<span class="feature-tag">Immersive Experience</span>
</div>
</div>
</div>
<div class="implementation-note">
<h3>Implementation Considerations</h3>
<p>While WiFi DensePose offers revolutionary capabilities, successful implementation requires careful consideration of environment setup, data privacy regulations, and system calibration for optimal performance.</p>
</div>
</section>
</div>
<!-- Error Toast -->
<div id="globalErrorToast" class="error-toast"></div>
<!-- Load application scripts as modules -->
<script type="module" src="app.js"></script>
</body>
</html>

139
ui/services/api.service.js Normal file
View File

@@ -0,0 +1,139 @@
// API Service for WiFi-DensePose UI
import { API_CONFIG, buildApiUrl } from '../config/api.config.js';
export class ApiService {
constructor() {
this.authToken = null;
this.requestInterceptors = [];
this.responseInterceptors = [];
}
// Set authentication token
setAuthToken(token) {
this.authToken = token;
}
// Add request interceptor
addRequestInterceptor(interceptor) {
this.requestInterceptors.push(interceptor);
}
// Add response interceptor
addResponseInterceptor(interceptor) {
this.responseInterceptors.push(interceptor);
}
// Build headers for requests
getHeaders(customHeaders = {}) {
const headers = {
...API_CONFIG.DEFAULT_HEADERS,
...customHeaders
};
if (this.authToken) {
headers['Authorization'] = `Bearer ${this.authToken}`;
}
return headers;
}
// Process request through interceptors
async processRequest(url, options) {
let processedUrl = url;
let processedOptions = options;
for (const interceptor of this.requestInterceptors) {
const result = await interceptor(processedUrl, processedOptions);
processedUrl = result.url || processedUrl;
processedOptions = result.options || processedOptions;
}
return { url: processedUrl, options: processedOptions };
}
// Process response through interceptors
async processResponse(response, url) {
let processedResponse = response;
for (const interceptor of this.responseInterceptors) {
processedResponse = await interceptor(processedResponse, url);
}
return processedResponse;
}
// Generic request method
async request(url, options = {}) {
try {
// Process request through interceptors
const processed = await this.processRequest(url, options);
// Make the request
const response = await fetch(processed.url, {
...processed.options,
headers: this.getHeaders(processed.options.headers)
});
// Process response through interceptors
const processedResponse = await this.processResponse(response, url);
// Handle errors
if (!processedResponse.ok) {
const error = await processedResponse.json().catch(() => ({
message: `HTTP ${processedResponse.status}: ${processedResponse.statusText}`
}));
throw new Error(error.message || error.detail || 'Request failed');
}
// Parse JSON response
const data = await processedResponse.json().catch(() => null);
return data;
} catch (error) {
console.error('API Request Error:', error);
throw error;
}
}
// GET request
async get(endpoint, params = {}, options = {}) {
const url = buildApiUrl(endpoint, params);
return this.request(url, {
method: 'GET',
...options
});
}
// POST request
async post(endpoint, data = {}, options = {}) {
const url = buildApiUrl(endpoint);
return this.request(url, {
method: 'POST',
body: JSON.stringify(data),
...options
});
}
// PUT request
async put(endpoint, data = {}, options = {}) {
const url = buildApiUrl(endpoint);
return this.request(url, {
method: 'PUT',
body: JSON.stringify(data),
...options
});
}
// DELETE request
async delete(endpoint, options = {}) {
const url = buildApiUrl(endpoint);
return this.request(url, {
method: 'DELETE',
...options
});
}
}
// Create singleton instance
export const apiService = new ApiService();

View File

@@ -0,0 +1,138 @@
// Health Service for WiFi-DensePose UI
import { API_CONFIG } from '../config/api.config.js';
import { apiService } from './api.service.js';
export class HealthService {
constructor() {
this.healthCheckInterval = null;
this.healthSubscribers = [];
this.lastHealthStatus = null;
}
// Get system health
async getSystemHealth() {
const health = await apiService.get(API_CONFIG.ENDPOINTS.HEALTH.SYSTEM);
this.lastHealthStatus = health;
this.notifySubscribers(health);
return health;
}
// Check readiness
async checkReadiness() {
return apiService.get(API_CONFIG.ENDPOINTS.HEALTH.READY);
}
// Check liveness
async checkLiveness() {
return apiService.get(API_CONFIG.ENDPOINTS.HEALTH.LIVE);
}
// Get system metrics
async getSystemMetrics() {
return apiService.get(API_CONFIG.ENDPOINTS.HEALTH.METRICS);
}
// Get version info
async getVersion() {
return apiService.get(API_CONFIG.ENDPOINTS.HEALTH.VERSION);
}
// Get API info
async getApiInfo() {
return apiService.get(API_CONFIG.ENDPOINTS.INFO);
}
// Get API status
async getApiStatus() {
return apiService.get(API_CONFIG.ENDPOINTS.STATUS);
}
// Start periodic health checks
startHealthMonitoring(intervalMs = 30000) {
if (this.healthCheckInterval) {
console.warn('Health monitoring already active');
return;
}
// Initial check
this.getSystemHealth().catch(error => {
console.error('Initial health check failed:', error);
});
// Set up periodic checks
this.healthCheckInterval = setInterval(() => {
this.getSystemHealth().catch(error => {
console.error('Health check failed:', error);
this.notifySubscribers({
status: 'error',
error: error.message,
timestamp: new Date().toISOString()
});
});
}, intervalMs);
}
// Stop health monitoring
stopHealthMonitoring() {
if (this.healthCheckInterval) {
clearInterval(this.healthCheckInterval);
this.healthCheckInterval = null;
}
}
// Subscribe to health updates
subscribeToHealth(callback) {
this.healthSubscribers.push(callback);
// Send last known status if available
if (this.lastHealthStatus) {
callback(this.lastHealthStatus);
}
// Return unsubscribe function
return () => {
const index = this.healthSubscribers.indexOf(callback);
if (index > -1) {
this.healthSubscribers.splice(index, 1);
}
};
}
// Notify subscribers
notifySubscribers(health) {
this.healthSubscribers.forEach(callback => {
try {
callback(health);
} catch (error) {
console.error('Error in health subscriber:', error);
}
});
}
// Check if system is healthy
isSystemHealthy() {
if (!this.lastHealthStatus) {
return null;
}
return this.lastHealthStatus.status === 'healthy';
}
// Get component status
getComponentStatus(componentName) {
if (!this.lastHealthStatus?.components) {
return null;
}
return this.lastHealthStatus.components[componentName];
}
// Clean up
dispose() {
this.stopHealthMonitoring();
this.healthSubscribers = [];
this.lastHealthStatus = null;
}
}
// Create singleton instance
export const healthService = new HealthService();

303
ui/services/pose.service.js Normal file
View File

@@ -0,0 +1,303 @@
// Pose Service for WiFi-DensePose UI
import { API_CONFIG } from '../config/api.config.js';
import { apiService } from './api.service.js';
import { wsService } from './websocket.service.js';
export class PoseService {
constructor() {
this.streamConnection = null;
this.eventConnection = null;
this.poseSubscribers = [];
this.eventSubscribers = [];
}
// Get current pose estimation
async getCurrentPose(options = {}) {
const params = {
zone_ids: options.zoneIds?.join(','),
confidence_threshold: options.confidenceThreshold,
max_persons: options.maxPersons,
include_keypoints: options.includeKeypoints,
include_segmentation: options.includeSegmentation
};
// Remove undefined values
Object.keys(params).forEach(key =>
params[key] === undefined && delete params[key]
);
return apiService.get(API_CONFIG.ENDPOINTS.POSE.CURRENT, params);
}
// Analyze pose (requires auth)
async analyzePose(request) {
return apiService.post(API_CONFIG.ENDPOINTS.POSE.ANALYZE, request);
}
// Get zone occupancy
async getZoneOccupancy(zoneId) {
const endpoint = API_CONFIG.ENDPOINTS.POSE.ZONE_OCCUPANCY.replace('{zone_id}', zoneId);
return apiService.get(endpoint);
}
// Get zones summary
async getZonesSummary() {
return apiService.get(API_CONFIG.ENDPOINTS.POSE.ZONES_SUMMARY);
}
// Get historical data (requires auth)
async getHistoricalData(request) {
return apiService.post(API_CONFIG.ENDPOINTS.POSE.HISTORICAL, request);
}
// Get recent activities
async getActivities(options = {}) {
const params = {
zone_id: options.zoneId,
limit: options.limit || 50
};
// Remove undefined values
Object.keys(params).forEach(key =>
params[key] === undefined && delete params[key]
);
return apiService.get(API_CONFIG.ENDPOINTS.POSE.ACTIVITIES, params);
}
// Calibrate system (requires auth)
async calibrate() {
return apiService.post(API_CONFIG.ENDPOINTS.POSE.CALIBRATE);
}
// Get calibration status (requires auth)
async getCalibrationStatus() {
return apiService.get(API_CONFIG.ENDPOINTS.POSE.CALIBRATION_STATUS);
}
// Get pose statistics
async getStats(hours = 24) {
return apiService.get(API_CONFIG.ENDPOINTS.POSE.STATS, { hours });
}
// Start pose stream
startPoseStream(options = {}) {
if (this.streamConnection) {
console.warn('Pose stream already active');
return this.streamConnection;
}
const params = {
zone_ids: options.zoneIds?.join(','),
min_confidence: options.minConfidence || 0.5,
max_fps: options.maxFps || 30,
token: options.token || apiService.authToken
};
// Remove undefined values
Object.keys(params).forEach(key =>
params[key] === undefined && delete params[key]
);
this.streamConnection = wsService.connect(
API_CONFIG.ENDPOINTS.STREAM.WS_POSE,
params,
{
onOpen: () => {
console.log('Pose stream connected');
this.notifyPoseSubscribers({ type: 'connected' });
},
onMessage: (data) => {
this.handlePoseMessage(data);
},
onError: (error) => {
console.error('Pose stream error:', error);
this.notifyPoseSubscribers({ type: 'error', error });
},
onClose: () => {
console.log('Pose stream disconnected');
this.streamConnection = null;
this.notifyPoseSubscribers({ type: 'disconnected' });
}
}
);
return this.streamConnection;
}
// Stop pose stream
stopPoseStream() {
if (this.streamConnection) {
wsService.disconnect(this.streamConnection);
this.streamConnection = null;
}
}
// Subscribe to pose updates
subscribeToPoseUpdates(callback) {
this.poseSubscribers.push(callback);
// Return unsubscribe function
return () => {
const index = this.poseSubscribers.indexOf(callback);
if (index > -1) {
this.poseSubscribers.splice(index, 1);
}
};
}
// Handle pose stream messages
handlePoseMessage(data) {
const { type, payload } = data;
switch (type) {
case 'pose_data':
this.notifyPoseSubscribers({
type: 'pose_update',
data: payload
});
break;
case 'historical_data':
this.notifyPoseSubscribers({
type: 'historical_update',
data: payload
});
break;
case 'zone_statistics':
this.notifyPoseSubscribers({
type: 'zone_stats',
data: payload
});
break;
case 'system_event':
this.notifyPoseSubscribers({
type: 'system_event',
data: payload
});
break;
default:
console.log('Unknown pose message type:', type);
}
}
// Notify pose subscribers
notifyPoseSubscribers(update) {
this.poseSubscribers.forEach(callback => {
try {
callback(update);
} catch (error) {
console.error('Error in pose subscriber:', error);
}
});
}
// Start event stream
startEventStream(options = {}) {
if (this.eventConnection) {
console.warn('Event stream already active');
return this.eventConnection;
}
const params = {
event_types: options.eventTypes?.join(','),
zone_ids: options.zoneIds?.join(','),
token: options.token || apiService.authToken
};
// Remove undefined values
Object.keys(params).forEach(key =>
params[key] === undefined && delete params[key]
);
this.eventConnection = wsService.connect(
API_CONFIG.ENDPOINTS.STREAM.WS_EVENTS,
params,
{
onOpen: () => {
console.log('Event stream connected');
this.notifyEventSubscribers({ type: 'connected' });
},
onMessage: (data) => {
this.handleEventMessage(data);
},
onError: (error) => {
console.error('Event stream error:', error);
this.notifyEventSubscribers({ type: 'error', error });
},
onClose: () => {
console.log('Event stream disconnected');
this.eventConnection = null;
this.notifyEventSubscribers({ type: 'disconnected' });
}
}
);
return this.eventConnection;
}
// Stop event stream
stopEventStream() {
if (this.eventConnection) {
wsService.disconnect(this.eventConnection);
this.eventConnection = null;
}
}
// Subscribe to events
subscribeToEvents(callback) {
this.eventSubscribers.push(callback);
// Return unsubscribe function
return () => {
const index = this.eventSubscribers.indexOf(callback);
if (index > -1) {
this.eventSubscribers.splice(index, 1);
}
};
}
// Handle event stream messages
handleEventMessage(data) {
this.notifyEventSubscribers({
type: 'event',
data
});
}
// Notify event subscribers
notifyEventSubscribers(update) {
this.eventSubscribers.forEach(callback => {
try {
callback(update);
} catch (error) {
console.error('Error in event subscriber:', error);
}
});
}
// Update stream configuration
updateStreamConfig(connectionId, config) {
wsService.sendCommand(connectionId, 'update_config', config);
}
// Get stream status
requestStreamStatus(connectionId) {
wsService.sendCommand(connectionId, 'get_status');
}
// Clean up
dispose() {
this.stopPoseStream();
this.stopEventStream();
this.poseSubscribers = [];
this.eventSubscribers = [];
}
}
// Create singleton instance
export const poseService = new PoseService();

View File

@@ -0,0 +1,59 @@
// Stream Service for WiFi-DensePose UI
import { API_CONFIG } from '../config/api.config.js';
import { apiService } from './api.service.js';
export class StreamService {
// Get streaming status
async getStatus() {
return apiService.get(API_CONFIG.ENDPOINTS.STREAM.STATUS);
}
// Start streaming (requires auth)
async start() {
return apiService.post(API_CONFIG.ENDPOINTS.STREAM.START);
}
// Stop streaming (requires auth)
async stop() {
return apiService.post(API_CONFIG.ENDPOINTS.STREAM.STOP);
}
// Get connected clients (requires auth)
async getClients() {
return apiService.get(API_CONFIG.ENDPOINTS.STREAM.CLIENTS);
}
// Disconnect a client (requires auth)
async disconnectClient(clientId) {
const endpoint = API_CONFIG.ENDPOINTS.STREAM.DISCONNECT_CLIENT.replace('{client_id}', clientId);
return apiService.delete(endpoint);
}
// Broadcast message (requires auth)
async broadcast(message, options = {}) {
const params = {
stream_type: options.streamType,
zone_ids: options.zoneIds?.join(',')
};
// Remove undefined values
Object.keys(params).forEach(key =>
params[key] === undefined && delete params[key]
);
return apiService.post(
API_CONFIG.ENDPOINTS.STREAM.BROADCAST,
message,
{ params }
);
}
// Get streaming metrics
async getMetrics() {
return apiService.get(API_CONFIG.ENDPOINTS.STREAM.METRICS);
}
}
// Create singleton instance
export const streamService = new StreamService();

View File

@@ -0,0 +1,305 @@
// WebSocket Service for WiFi-DensePose UI
import { API_CONFIG, buildWsUrl } from '../config/api.config.js';
export class WebSocketService {
constructor() {
this.connections = new Map();
this.messageHandlers = new Map();
this.reconnectAttempts = new Map();
}
// Connect to WebSocket endpoint
connect(endpoint, params = {}, handlers = {}) {
const url = buildWsUrl(endpoint, params);
// Check if already connected
if (this.connections.has(url)) {
console.warn(`Already connected to ${url}`);
return this.connections.get(url);
}
// Create WebSocket connection
const ws = new WebSocket(url);
const connectionId = this.generateId();
// Store connection
this.connections.set(url, {
id: connectionId,
ws,
url,
handlers,
status: 'connecting',
lastPing: null,
reconnectTimer: null
});
// Set up event handlers
this.setupEventHandlers(url, ws, handlers);
// Start ping interval
this.startPingInterval(url);
return connectionId;
}
// Set up WebSocket event handlers
setupEventHandlers(url, ws, handlers) {
const connection = this.connections.get(url);
ws.onopen = (event) => {
console.log(`WebSocket connected: ${url}`);
connection.status = 'connected';
this.reconnectAttempts.set(url, 0);
if (handlers.onOpen) {
handlers.onOpen(event);
}
};
ws.onmessage = (event) => {
try {
const data = JSON.parse(event.data);
// Handle different message types
this.handleMessage(url, data);
if (handlers.onMessage) {
handlers.onMessage(data);
}
} catch (error) {
console.error('Failed to parse WebSocket message:', error);
}
};
ws.onerror = (event) => {
console.error(`WebSocket error: ${url}`, event);
connection.status = 'error';
if (handlers.onError) {
handlers.onError(event);
}
};
ws.onclose = (event) => {
console.log(`WebSocket closed: ${url}`);
connection.status = 'closed';
// Clear ping interval
this.clearPingInterval(url);
if (handlers.onClose) {
handlers.onClose(event);
}
// Attempt reconnection if not intentionally closed
if (!event.wasClean && this.shouldReconnect(url)) {
this.scheduleReconnect(url);
} else {
this.connections.delete(url);
}
};
}
// Handle incoming messages
handleMessage(url, data) {
const { type, payload } = data;
// Handle system messages
switch (type) {
case 'pong':
this.handlePong(url);
break;
case 'connection_established':
console.log('Connection established:', payload);
break;
case 'error':
console.error('WebSocket error message:', payload);
break;
}
// Call registered message handlers
const handlers = this.messageHandlers.get(url) || [];
handlers.forEach(handler => handler(data));
}
// Send message through WebSocket
send(connectionId, message) {
const connection = this.findConnectionById(connectionId);
if (!connection) {
throw new Error(`Connection ${connectionId} not found`);
}
if (connection.status !== 'connected') {
throw new Error(`Connection ${connectionId} is not connected`);
}
const data = typeof message === 'string'
? message
: JSON.stringify(message);
connection.ws.send(data);
}
// Send command message
sendCommand(connectionId, command, payload = {}) {
this.send(connectionId, {
type: command,
payload,
timestamp: new Date().toISOString()
});
}
// Register message handler
onMessage(connectionId, handler) {
const connection = this.findConnectionById(connectionId);
if (!connection) {
throw new Error(`Connection ${connectionId} not found`);
}
if (!this.messageHandlers.has(connection.url)) {
this.messageHandlers.set(connection.url, []);
}
this.messageHandlers.get(connection.url).push(handler);
// Return unsubscribe function
return () => {
const handlers = this.messageHandlers.get(connection.url);
const index = handlers.indexOf(handler);
if (index > -1) {
handlers.splice(index, 1);
}
};
}
// Disconnect WebSocket
disconnect(connectionId) {
const connection = this.findConnectionById(connectionId);
if (!connection) {
return;
}
// Clear reconnection timer
if (connection.reconnectTimer) {
clearTimeout(connection.reconnectTimer);
}
// Clear ping interval
this.clearPingInterval(connection.url);
// Close WebSocket
if (connection.ws.readyState === WebSocket.OPEN) {
connection.ws.close(1000, 'Client disconnect');
}
// Clean up
this.connections.delete(connection.url);
this.messageHandlers.delete(connection.url);
this.reconnectAttempts.delete(connection.url);
}
// Disconnect all WebSockets
disconnectAll() {
const connectionIds = Array.from(this.connections.values()).map(c => c.id);
connectionIds.forEach(id => this.disconnect(id));
}
// Ping/Pong handling
startPingInterval(url) {
const connection = this.connections.get(url);
if (!connection) return;
connection.pingInterval = setInterval(() => {
if (connection.status === 'connected') {
this.sendPing(url);
}
}, API_CONFIG.WS_CONFIG.PING_INTERVAL);
}
clearPingInterval(url) {
const connection = this.connections.get(url);
if (connection && connection.pingInterval) {
clearInterval(connection.pingInterval);
}
}
sendPing(url) {
const connection = this.connections.get(url);
if (connection && connection.status === 'connected') {
connection.lastPing = Date.now();
connection.ws.send(JSON.stringify({ type: 'ping' }));
}
}
handlePong(url) {
const connection = this.connections.get(url);
if (connection) {
const latency = Date.now() - connection.lastPing;
console.log(`Pong received. Latency: ${latency}ms`);
}
}
// Reconnection logic
shouldReconnect(url) {
const attempts = this.reconnectAttempts.get(url) || 0;
return attempts < API_CONFIG.WS_CONFIG.MAX_RECONNECT_ATTEMPTS;
}
scheduleReconnect(url) {
const connection = this.connections.get(url);
if (!connection) return;
const attempts = this.reconnectAttempts.get(url) || 0;
const delay = API_CONFIG.WS_CONFIG.RECONNECT_DELAY * Math.pow(2, attempts);
console.log(`Scheduling reconnect in ${delay}ms (attempt ${attempts + 1})`);
connection.reconnectTimer = setTimeout(() => {
this.reconnectAttempts.set(url, attempts + 1);
// Get original parameters
const params = new URL(url).searchParams;
const paramsObj = Object.fromEntries(params);
const endpoint = url.replace(/^wss?:\/\/[^\/]+/, '').split('?')[0];
// Attempt reconnection
this.connect(endpoint, paramsObj, connection.handlers);
}, delay);
}
// Utility methods
findConnectionById(connectionId) {
for (const connection of this.connections.values()) {
if (connection.id === connectionId) {
return connection;
}
}
return null;
}
generateId() {
return `ws_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
}
getConnectionStatus(connectionId) {
const connection = this.findConnectionById(connectionId);
return connection ? connection.status : 'disconnected';
}
getActiveConnections() {
return Array.from(this.connections.values()).map(conn => ({
id: conn.id,
url: conn.url,
status: conn.status
}));
}
}
// Create singleton instance
export const wsService = new WebSocketService();

1307
ui/style.css Normal file

File diff suppressed because it is too large Load Diff