Add comprehensive CSS styles for UI components and dark mode support
This commit is contained in:
309
ui/components/DashboardTab.js
Normal file
309
ui/components/DashboardTab.js
Normal 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();
|
||||
}
|
||||
}
|
||||
165
ui/components/HardwareTab.js
Normal file
165
ui/components/HardwareTab.js
Normal 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);
|
||||
});
|
||||
}
|
||||
}
|
||||
391
ui/components/LiveDemoTab.js
Normal file
391
ui/components/LiveDemoTab.js
Normal 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
138
ui/components/TabManager.js
Normal 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 = [];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user