Compare commits

..

5 Commits

Author SHA1 Message Date
github-actions[bot] 6891a72053 chore: update vendor submodules to latest main 2026-03-13 00:36:02 +00:00
ruv 6e03a47867 docs: update user guide with v0.4.1 firmware release and CSI troubleshooting
- Add v0.4.1 to firmware release table as recommended stable release
- Update flash command with correct partition offsets (8MB, OTA)
- Add "CSI not enabled" troubleshooting entry
- Add warning about pre-v0.4.1 firmware CSI bug

Co-Authored-By: claude-flow <ruv@ruv.net>
2026-03-12 13:49:20 -04:00
ruv 9d1140de2d docs: update README firmware release table with v0.4.1
Add v0.4.1-esp32 as the recommended stable release and update the
flash command to match the current partition layout.

Co-Authored-By: claude-flow <ruv@ruv.net>
2026-03-12 13:49:20 -04:00
ruv 952f27a1ce fix(firmware): enable CSI in sdkconfig and add build guard (ADR-057)
The committed sdkconfig had CONFIG_ESP_WIFI_CSI_ENABLED disabled, causing
all builds to crash at runtime with "CSI not enabled in menuconfig".
Root cause: sdkconfig.defaults.template existed but ESP-IDF only reads
sdkconfig.defaults (no .template suffix).

Fixes:
- Add sdkconfig.defaults with CONFIG_ESP_WIFI_CSI_ENABLED=y
- Add #error compile guard in csi_collector.c to prevent recurrence
- Fix NVS encryption default (requires eFuse, breaks clean builds)

Verified: Docker build + flash to ESP32-S3 + CSI callbacks confirmed.

Closes #241
Relates to #223, #238, #234, #210, #190

Co-Authored-By: claude-flow <ruv@ruv.net>
2026-03-12 13:49:20 -04:00
Reuven f7d043d727 docs: fix Docker commands to use CSI_SOURCE environment variable
The Docker image uses CSI_SOURCE env var to select the data source,
not command-line arguments appended after the image name.

Fixed:
- ESP32 mode examples now use -e CSI_SOURCE=esp32
- Training mode example now uses --entrypoint override
- Added CSI_SOURCE value table in Docker section

Fixes #226

Co-Authored-By: claude-flow <ruv@ruv.net>
2026-03-10 12:16:06 -04:00
24 changed files with 171 additions and 4030 deletions
+5 -3
View File
@@ -1043,14 +1043,16 @@ Download a pre-built binary — no build toolchain needed:
| Release | What's included | Tag |
|---------|-----------------|-----|
| [v0.2.0](https://github.com/ruvnet/RuView/releases/tag/v0.2.0-esp32) | Stable — raw CSI streaming, multi-node TDM, channel hopping | `v0.2.0-esp32` |
| [v0.4.1](https://github.com/ruvnet/RuView/releases/tag/v0.4.1-esp32) | **Stable** — CSI build fix, compile guard, AMOLED display, edge intelligence ([ADR-057](docs/adr/ADR-057-firmware-csi-build-guard.md)) | `v0.4.1-esp32` |
| [v0.3.0-alpha](https://github.com/ruvnet/RuView/releases/tag/v0.3.0-alpha-esp32) | Alpha — adds on-device edge intelligence and WASM modules ([ADR-039](docs/adr/ADR-039-esp32-edge-intelligence.md), [ADR-040](docs/adr/ADR-040-wasm-programmable-sensing.md)) | `v0.3.0-alpha-esp32` |
| [v0.2.0](https://github.com/ruvnet/RuView/releases/tag/v0.2.0-esp32) | Raw CSI streaming, multi-node TDM, channel hopping | `v0.2.0-esp32` |
```bash
# 1. Flash the firmware to your ESP32-S3
python -m esptool --chip esp32s3 --port COM7 --baud 460800 \
write_flash --flash_mode dio --flash_size 8MB \
0x0 bootloader.bin 0x8000 partition-table.bin 0x10000 esp32-csi-node.bin
write_flash --flash-mode dio --flash-size 8MB --flash-freq 80m \
0x0 bootloader.bin 0x8000 partition-table.bin \
0xf000 ota_data_initial.bin 0x20000 esp32-csi-node.bin
# 2. Set WiFi credentials and server address (stored in flash, survives reboots)
python firmware/esp32-csi-node/provision.py --port COM7 \
@@ -1,240 +0,0 @@
# ADR-057: Desktop App Training & RuVector Integration
| Field | Value |
|-------|-------|
| Status | Proposed |
| Date | 2026-03-10 |
| Authors | RuView Team |
| Reviewers | - |
| Related | ADR-016, ADR-017, ADR-024, ADR-027 |
## Context
The RuView desktop application currently provides device discovery, firmware flashing, OTA updates, and real-time sensing visualization. However, users cannot train models or configure RuVector signal processing modules directly from the desktop app.
The following crates exist in the workspace but are not exposed in the desktop UI:
### Training Crate (`wifi-densepose-train`)
- Dataset management (MM-Fi, Wi-Pose formats)
- Model architectures (CSI encoder, pose decoder)
- Training loops with metrics tracking
- Checkpoint save/load
- ruview_metrics integration
### RuVector Crates (5 modules)
1. **ruvector-mincut** - Graph-based person segmentation, DynamicPersonMatcher
2. **ruvector-attn-mincut** - Attention-weighted antenna selection
3. **ruvector-temporal-tensor** - Temporal CSI compression, breathing detection
4. **ruvector-solver** - Sparse interpolation, triangulation
5. **ruvector-attention** - Spatial attention, BVP extraction
## Decision
Add a new **"Training"** page to the desktop application with tabbed navigation:
### Tab Structure
```
┌─────────────────────────────────────────────────────────────┐
│ Training & Models │
├──────────┬──────────┬──────────┬──────────┬────────────────┤
│ Datasets │ Models │ Training │ RuVector │ Metrics │
└──────────┴──────────┴──────────┴──────────┴────────────────┘
```
### Tab 1: Datasets
- **Download** standard datasets (MM-Fi, Wi-Pose)
- **Import** custom CSI recordings
- **Preview** dataset samples (CSI heatmaps, labels)
- **Split** into train/val/test sets
- **Statistics** - sample counts, class distribution
### Tab 2: Models
- **Browse** available architectures:
- CSI Encoder (CNN, Transformer)
- Pose Decoder (LSTM, GRU)
- AETHER embedding network (ADR-024)
- MERIDIAN domain adaptor (ADR-027)
- **Load** checkpoints from disk
- **View** model summary (params, layers, memory)
- **Export** to ONNX/TorchScript
### Tab 3: Training
- **Configure** training:
- Learning rate, batch size, epochs
- Optimizer (Adam, SGD, AdamW)
- Loss function selection
- Data augmentation toggles
- **GPU Detection** - CUDA/Metal availability
- **Start/Stop** training jobs
- **Progress** - live loss curves, ETA
- **Checkpointing** - auto-save best model
### Tab 4: RuVector
- **Module Configuration**:
- MinCut graph parameters
- Attention weights
- Temporal compression ratio
- Solver interpolation settings
- **Live Testing** - apply to real-time CSI stream
- **Comparison** - A/B test configurations
- **Export** - save optimal config
### Tab 5: Metrics
- **Loss Curves** - training/validation over epochs
- **Evaluation** - PCK, mAP, IoU scores
- **Confusion Matrix** - per-joint accuracy
- **Export** - CSV, JSON, TensorBoard format
## Architecture
### Backend (Rust/Tauri)
```
wifi-densepose-desktop/
├── src/
│ ├── commands/
│ │ ├── training.rs # NEW: Training job management
│ │ ├── datasets.rs # NEW: Dataset download/import
│ │ ├── models.rs # NEW: Model loading/export
│ │ ├── ruvector.rs # NEW: RuVector config
│ │ └── metrics.rs # NEW: Metrics retrieval
│ └── domain/
│ ├── training.rs # Training state machine
│ └── ruvector.rs # RuVector config types
```
### Frontend (React/TypeScript)
```
ui/src/pages/
├── Training/
│ ├── index.tsx # Tab container
│ ├── DatasetsTab.tsx # Dataset management
│ ├── ModelsTab.tsx # Model browser
│ ├── TrainingTab.tsx # Training control
│ ├── RuVectorTab.tsx # Signal processing config
│ └── MetricsTab.tsx # Visualization
```
### Tauri Commands
| Command | Description |
|---------|-------------|
| `list_datasets` | Get available datasets |
| `download_dataset` | Download standard dataset |
| `import_dataset` | Import custom recordings |
| `list_models` | Get model architectures |
| `load_checkpoint` | Load model weights |
| `export_model` | Export to ONNX |
| `detect_gpu` | Check CUDA/Metal |
| `start_training` | Begin training job |
| `stop_training` | Cancel training |
| `training_progress` | Get current status |
| `get_ruvector_config` | Load RuVector settings |
| `set_ruvector_config` | Update settings |
| `test_ruvector_live` | Apply to live CSI |
| `get_metrics` | Retrieve training metrics |
### Event System
Training progress updates via Tauri events:
```rust
#[derive(Serialize, Clone)]
pub struct TrainingProgress {
pub epoch: u32,
pub total_epochs: u32,
pub batch: u32,
pub total_batches: u32,
pub train_loss: f32,
pub val_loss: Option<f32>,
pub learning_rate: f32,
pub eta_secs: u64,
pub gpu_memory_mb: Option<u64>,
}
// Emit every batch
app.emit("training:progress", progress)?;
// Emit on completion
app.emit("training:complete", result)?;
```
## Implementation Plan
### Phase 1: Foundation (Week 1-2)
1. Create `Training` page skeleton with tabs
2. Implement `detect_gpu` command
3. Add dataset listing/download commands
4. Design TypeScript types for all entities
### Phase 2: Dataset Management (Week 3)
1. MM-Fi dataset downloader
2. Wi-Pose dataset downloader
3. Custom dataset import (CSV/NPZ)
4. Dataset preview component
### Phase 3: Model Management (Week 4)
1. Model architecture browser
2. Checkpoint loading
3. Model summary display
4. ONNX export
### Phase 4: Training Loop (Week 5-6)
1. Training configuration UI
2. Background training thread
3. Progress event emission
4. Checkpoint auto-save
5. Training history persistence
### Phase 5: RuVector Integration (Week 7)
1. RuVector config UI
2. Live CSI testing
3. A/B comparison mode
4. Config export/import
### Phase 6: Metrics & Polish (Week 8)
1. Loss curve visualization (Chart.js/Recharts)
2. Evaluation metrics display
3. Export functionality
4. Error handling & edge cases
## Risks & Mitigations
| Risk | Probability | Impact | Mitigation |
|------|-------------|--------|------------|
| No GPU available | Medium | High | CPU fallback with warning |
| Large dataset downloads | High | Medium | Resume support, progress UI |
| Training crashes | Medium | High | Checkpoint recovery, error reporting |
| Memory exhaustion | Low | High | Batch size auto-tuning |
| UI blocking | Medium | High | All training in background thread |
## Success Criteria
1. User can download MM-Fi dataset from UI
2. User can start training with GPU detection
3. Live progress updates without UI freeze
4. Training can be paused/resumed
5. RuVector config changes apply to live CSI
6. Metrics display updates in real-time
7. Models can be exported to ONNX
## Alternatives Considered
### 1. Separate Training App
- **Rejected**: Fragments user experience, duplicates code
### 2. Web-based Training Dashboard
- **Rejected**: Requires server, no offline support
### 3. CLI-only Training
- **Rejected**: Poor UX for non-technical users
## References
- ADR-016: RuVector Training Pipeline Integration
- ADR-017: RuVector Signal + MAT Integration
- ADR-024: AETHER Contrastive CSI Embedding
- ADR-027: MERIDIAN Domain Generalization
- Tauri v2 Events: https://v2.tauri.app/develop/calling-rust/#events
@@ -0,0 +1,82 @@
# ADR-057: Firmware CSI Build Guard and sdkconfig.defaults
| Field | Value |
|-------------|---------------------------------------------|
| **Status** | Accepted |
| **Date** | 2026-03-12 |
| **Authors** | ruv |
| **Issues** | #223, #238, #234, #210, #190 |
## Context
Multiple GitHub issues (#223, #238, #234, #210, #190) report firmware problems
that fall into two categories:
1. **CSI not enabled at runtime** — The committed `sdkconfig` had
`# CONFIG_ESP_WIFI_CSI_ENABLED is not set` (line 1135), meaning users who
built from source or used pre-built binaries got the runtime error:
`E (6700) wifi:CSI not enabled in menuconfig!`
Root cause: `sdkconfig.defaults.template` existed with the correct setting
(`CONFIG_ESP_WIFI_CSI_ENABLED=y`) but ESP-IDF only reads
`sdkconfig.defaults` — not `.template` suffixed files. No `sdkconfig.defaults`
file was committed.
2. **Unsupported ESP32 variants** — Users attempting to use original ESP32
(D0WD) and ESP32-C3 boards. The firmware targets ESP32-S3 only
(`CONFIG_IDF_TARGET="esp32s3"`, Xtensa architecture) and this was not
surfaced clearly enough in documentation or build errors.
## Decision
### Fix 1: Commit `sdkconfig.defaults` (not just template)
Copy `sdkconfig.defaults.template``sdkconfig.defaults` so that ESP-IDF
applies the correct defaults (including `CONFIG_ESP_WIFI_CSI_ENABLED=y`)
automatically when `sdkconfig` is regenerated.
### Fix 2: `#error` compile-time guard in `csi_collector.c`
Add a preprocessor guard:
```c
#ifndef CONFIG_ESP_WIFI_CSI_ENABLED
#error "CONFIG_ESP_WIFI_CSI_ENABLED must be set in sdkconfig."
#endif
```
This turns a confusing runtime crash into a clear compile-time error with
instructions on how to fix it.
### Fix 3: Fix committed `sdkconfig`
Change line 1135 from `# CONFIG_ESP_WIFI_CSI_ENABLED is not set` to
`CONFIG_ESP_WIFI_CSI_ENABLED=y`.
## Consequences
- **Positive**: New builds will always have CSI enabled. Users building from
source will get a clear compile error if CSI is somehow disabled.
- **Positive**: Pre-built release binaries will include CSI support.
- **Neutral**: Original ESP32 and ESP32-C3 remain unsupported. This is by
design — only ESP32-S3 has the CSI API surface we depend on. Future ADRs
may address multi-target support if demand warrants it.
- **Negative**: None identified.
## Hardware Support Matrix
| Variant | CSI Support | Firmware Target | Status |
|--------------|-------------|-----------------|---------------|
| ESP32-S3 | Yes | Yes | Supported |
| ESP32 (orig) | Partial | No | Unsupported |
| ESP32-C3 | Yes (IDF 5.1+) | No | Unsupported |
| ESP32-C6 | Yes | No | Unsupported |
## Notes
- ESP32-C3 and C6 use RISC-V architecture; a separate build target
(`idf.py set-target esp32c3`) would be needed.
- Original ESP32 has limited CSI (no STBC HT-LTF2, fewer subcarriers).
- Users on unsupported hardware can still write custom firmware using the
ADR-018 binary frame format (magic `0xC5110001`) for interop with the
Rust aggregator.
+33 -11
View File
@@ -78,6 +78,17 @@ docker pull ruvnet/wifi-densepose:latest
Multi-architecture image (amd64 + arm64). Works on Intel/AMD and Apple Silicon Macs. Contains the Rust sensing server, Three.js UI, and all signal processing.
**Data source selection:** Use the `CSI_SOURCE` environment variable to select the sensing mode:
| Value | Description |
|-------|-------------|
| `auto` | (default) Probe for ESP32 on UDP 5005, fall back to simulation |
| `esp32` | Receive real CSI frames from ESP32 devices over UDP |
| `simulated` | Generate synthetic CSI frames (no hardware required) |
| `wifi` | Host Wi-Fi RSSI (not available inside containers) |
Example: `docker run -e CSI_SOURCE=esp32 -p 3000:3000 -p 5005:5005/udp ruvnet/wifi-densepose:latest`
### From Source (Rust)
```bash
@@ -267,8 +278,8 @@ Real Channel State Information at 20 Hz with 56-192 subcarriers. Required for po
# From source
./target/release/sensing-server --source esp32 --udp-port 5005 --http-port 3000 --ws-port 3001
# Docker
docker run -p 3000:3000 -p 3001:3001 -p 5005:5005/udp ruvnet/wifi-densepose:latest --source esp32
# Docker (use CSI_SOURCE environment variable)
docker run -p 3000:3000 -p 3001:3001 -p 5005:5005/udp -e CSI_SOURCE=esp32 ruvnet/wifi-densepose:latest
```
The ESP32 nodes stream binary CSI frames over UDP to port 5005. See [Hardware Setup](#esp32-s3-mesh) for flashing instructions.
@@ -679,9 +690,11 @@ Download the dataset files and place them in a `data/` directory.
./target/release/sensing-server --train --dataset data/ --dataset-type mmfi --epochs 100 --save-rvf model.rvf
# Via Docker (mount your data directory)
# Note: Training mode requires overriding the default entrypoint
docker run --rm \
-v $(pwd)/data:/data \
-v $(pwd)/output:/output \
--entrypoint /app/sensing-server \
ruvnet/wifi-densepose:latest \
--train --dataset /data --epochs 100 --export-rvf /output/model.rvf
```
@@ -797,14 +810,18 @@ Pre-built binaries are available at [Releases](https://github.com/ruvnet/RuView/
| Release | What It Includes | Tag |
|---------|-----------------|-----|
| [v0.2.0](https://github.com/ruvnet/RuView/releases/tag/v0.2.0-esp32) | Stable — raw CSI streaming, TDM, channel hopping, QUIC mesh | `v0.2.0-esp32` |
| [v0.4.1](https://github.com/ruvnet/RuView/releases/tag/v0.4.1-esp32) | **Stable** — CSI build fix, compile guard, AMOLED display, edge intelligence ([ADR-057](../docs/adr/ADR-057-firmware-csi-build-guard.md)) | `v0.4.1-esp32` |
| [v0.3.0-alpha](https://github.com/ruvnet/RuView/releases/tag/v0.3.0-alpha-esp32) | Alpha — adds on-device edge intelligence (ADR-039) | `v0.3.0-alpha-esp32` |
| [v0.2.0](https://github.com/ruvnet/RuView/releases/tag/v0.2.0-esp32) | Raw CSI streaming, TDM, channel hopping, QUIC mesh | `v0.2.0-esp32` |
> **Important:** Firmware versions prior to v0.4.1 had CSI **disabled** in the build config, causing a runtime error (`E wifi:CSI not enabled in menuconfig!`). Always use v0.4.1 or later.
```bash
# Flash an ESP32-S3 (requires esptool: pip install esptool)
python -m esptool --chip esp32s3 --port COM7 --baud 460800 \
write-flash --flash-mode dio --flash-size 4MB \
0x0 bootloader.bin 0x8000 partition-table.bin 0x10000 esp32-csi-node.bin
write-flash --flash-mode dio --flash-size 8MB --flash-freq 80m \
0x0 bootloader.bin 0x8000 partition-table.bin \
0xf000 ota_data_initial.bin 0x20000 esp32-csi-node.bin
```
**Provisioning:**
@@ -885,8 +902,8 @@ Binary size: 777 KB (24% free in the 1 MB app partition).
# From source
./target/release/sensing-server --source esp32 --udp-port 5005 --http-port 3000 --ws-port 3001
# Docker
docker run -p 3000:3000 -p 3001:3001 -p 5005:5005/udp ruvnet/wifi-densepose:latest --source esp32
# Docker (use CSI_SOURCE environment variable)
docker run -p 3000:3000 -p 3001:3001 -p 5005:5005/udp -e CSI_SOURCE=esp32 ruvnet/wifi-densepose:latest
```
See [ADR-018](../docs/adr/ADR-018-esp32-dev-implementation.md), [ADR-029](../docs/adr/ADR-029-ruvsense-multistatic-sensing-mode.md), and [Tutorial #34](https://github.com/ruvnet/RuView/issues/34).
@@ -953,12 +970,17 @@ Add the WebSocket port mapping:
docker run -p 3000:3000 -p 3001:3001 ruvnet/wifi-densepose:latest
```
### ESP32: "CSI not enabled in menuconfig"
Firmware versions prior to v0.4.1 had `CONFIG_ESP_WIFI_CSI_ENABLED` disabled in the build config. Upgrade to [v0.4.1](https://github.com/ruvnet/RuView/releases/tag/v0.4.1-esp32) or later. If building from source, ensure `sdkconfig.defaults` exists (not just `sdkconfig.defaults.template`). See [ADR-057](../docs/adr/ADR-057-firmware-csi-build-guard.md).
### ESP32: No data arriving
1. Verify the ESP32 is connected to the same WiFi network
2. Check the target IP matches the sensing server machine: `python firmware/esp32-csi-node/provision.py --port COM7 --target-ip <YOUR_IP>`
3. Verify UDP port 5005 is not blocked by firewall
4. Test with: `nc -lu 5005` (Linux) or similar UDP listener
1. Verify firmware is v0.4.1+ (older versions had CSI disabled — see above)
2. Verify the ESP32 is connected to the same WiFi network
3. Check the target IP matches the sensing server machine: `python firmware/esp32-csi-node/provision.py --port COM7 --target-ip <YOUR_IP>`
4. Verify UDP port 5005 is not blocked by firewall
5. Test with: `nc -lu 5005` (Linux) or similar UDP listener
### Build: Rust compilation errors
@@ -21,6 +21,16 @@
#include "esp_timer.h"
#include "sdkconfig.h"
/* ADR-057: Build-time guard — fail early if CSI is not enabled in sdkconfig.
* Without this, the firmware compiles but crashes at runtime with:
* "E (xxxx) wifi:CSI not enabled in menuconfig!"
* which is confusing for users flashing pre-built binaries. */
#ifndef CONFIG_ESP_WIFI_CSI_ENABLED
#error "CONFIG_ESP_WIFI_CSI_ENABLED must be set in sdkconfig. " \
"Run: idf.py menuconfig -> Component config -> Wi-Fi -> Enable WiFi CSI, " \
"or copy sdkconfig.defaults.template to sdkconfig.defaults before building."
#endif
static const char *TAG = "csi_collector";
static uint32_t s_sequence = 0;
@@ -0,0 +1,33 @@
# ESP32-S3 CSI Node — Default SDK Configuration
# This file is applied automatically by idf.py when no sdkconfig exists.
# Target: ESP32-S3
CONFIG_IDF_TARGET="esp32s3"
# Use custom partition table (8MB flash with OTA — ADR-045)
CONFIG_PARTITION_TABLE_CUSTOM=y
CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions_display.csv"
# Flash configuration: 8MB (Quad SPI)
CONFIG_ESPTOOLPY_FLASHSIZE_8MB=y
CONFIG_ESPTOOLPY_FLASHSIZE="8MB"
# Compiler optimization: optimize for size to reduce binary
CONFIG_COMPILER_OPTIMIZATION_SIZE=y
# Enable CSI (Channel State Information) in WiFi driver
CONFIG_ESP_WIFI_CSI_ENABLED=y
# NVS encryption disabled by default (requires eFuse provisioning).
# Enable only after burning HMAC key to eFuse block.
# CONFIG_NVS_ENCRYPTION is not set
# Disable unused features to reduce binary size
CONFIG_BOOTLOADER_LOG_LEVEL_WARN=y
CONFIG_LOG_DEFAULT_LEVEL_INFO=y
# LWIP: enable extended socket options for UDP multicast
CONFIG_LWIP_SO_RCVBUF=y
# FreeRTOS: increase task stack for CSI processing
CONFIG_ESP_MAIN_TASK_STACK_SIZE=8192
@@ -18,8 +18,9 @@ CONFIG_COMPILER_OPTIMIZATION_SIZE=y
# Enable CSI (Channel State Information) in WiFi driver
CONFIG_ESP_WIFI_CSI_ENABLED=y
# Enable NVS encryption for secure credential storage
CONFIG_NVS_ENCRYPTION=y
# NVS encryption disabled by default (requires eFuse provisioning).
# Enable only after burning HMAC key to eFuse block.
# CONFIG_NVS_ENCRYPTION is not set
# Disable unused features to reduce binary size
CONFIG_BOOTLOADER_LOG_LEVEL_WARN=y
@@ -4,5 +4,4 @@ pub mod ota;
pub mod provision;
pub mod server;
pub mod settings;
pub mod training;
pub mod wasm;
@@ -1,482 +0,0 @@
//! Training commands for the desktop application.
//!
//! Provides Tauri commands for:
//! - GPU detection
//! - Dataset management
//! - Model/checkpoint operations
//! - Training job control
//! - RuVector configuration
//! - Metrics retrieval
use crate::domain::training::{
CheckpointInfo, DatasetFormat, DatasetInfo, EpochMetrics, EvaluationMetrics,
GpuBackend, GpuInfo, JointAccuracy, LiveTestMetrics,
ModelInfo, ModelType, RuVectorConfig, TrainingConfig, TrainingJob,
TrainingProgress, TrainingStatus,
};
use crate::state::AppState;
use tauri::State;
// ============================================================================
// Standard Datasets (built-in)
// ============================================================================
fn get_standard_datasets() -> Vec<DatasetInfo> {
vec![
DatasetInfo {
id: "mmfi".into(),
name: "MM-Fi Dataset".into(),
description: "Multi-modal WiFi sensing dataset with 40 subjects, 27 activities".into(),
format: DatasetFormat::MmFi,
size_mb: 2400.0,
samples: 320000,
downloaded: false,
path: None,
url: Some("https://ntu-aiot-lab.github.io/mm-fi".into()),
},
DatasetInfo {
id: "wipose".into(),
name: "Wi-Pose Dataset".into(),
description: "WiFi-based pose estimation with 3D skeleton annotations".into(),
format: DatasetFormat::WiPose,
size_mb: 1800.0,
samples: 150000,
downloaded: false,
path: None,
url: Some("https://github.com/Wi-Pose".into()),
},
DatasetInfo {
id: "wiar".into(),
name: "WiAR Dataset".into(),
description: "WiFi activity recognition with CSI data".into(),
format: DatasetFormat::Wiar,
size_mb: 500.0,
samples: 45000,
downloaded: false,
path: None,
url: Some("https://github.com/WiAR".into()),
},
]
}
// ============================================================================
// Standard Model Architectures
// ============================================================================
fn get_standard_models() -> Vec<ModelInfo> {
vec![
ModelInfo {
id: "csi-encoder-cnn".into(),
name: "CSI Encoder (CNN)".into(),
model_type: ModelType::Encoder,
description: "Convolutional encoder for CSI amplitude/phase features".into(),
params_m: 2.3,
memory_mb: 128,
paper: None,
},
ModelInfo {
id: "csi-encoder-transformer".into(),
name: "CSI Encoder (Transformer)".into(),
model_type: ModelType::Encoder,
description: "Self-attention based CSI feature extraction".into(),
params_m: 8.5,
memory_mb: 384,
paper: Some("WiFi-ViT 2024".into()),
},
ModelInfo {
id: "pose-decoder-lstm".into(),
name: "Pose Decoder (LSTM)".into(),
model_type: ModelType::Decoder,
description: "Recurrent decoder for temporal pose estimation".into(),
params_m: 1.8,
memory_mb: 96,
paper: None,
},
ModelInfo {
id: "pose-decoder-gru".into(),
name: "Pose Decoder (GRU)".into(),
model_type: ModelType::Decoder,
description: "Gated recurrent unit pose decoder (faster)".into(),
params_m: 1.2,
memory_mb: 64,
paper: None,
},
ModelInfo {
id: "aether-embedding".into(),
name: "AETHER Embedding".into(),
model_type: ModelType::Embedding,
description: "Contrastive CSI embedding for person re-identification (ADR-024)".into(),
params_m: 4.2,
memory_mb: 192,
paper: Some("AETHER 2025".into()),
},
ModelInfo {
id: "meridian-adaptor".into(),
name: "MERIDIAN Adaptor".into(),
model_type: ModelType::Adaptor,
description: "Cross-environment domain generalization module (ADR-027)".into(),
params_m: 3.1,
memory_mb: 144,
paper: Some("MERIDIAN 2025".into()),
},
]
}
// ============================================================================
// GPU Detection Commands
// ============================================================================
/// Detect available GPU(s) and return information.
#[tauri::command]
pub async fn detect_gpu(state: State<'_, AppState>) -> Result<GpuInfo, String> {
// Check for cached GPU info
if let Ok(training) = state.training.lock() {
if let Some(ref info) = training.gpu_info {
return Ok(info.clone());
}
}
// Detect GPU
let info = detect_gpu_internal();
// Cache the result
if let Ok(mut training) = state.training.lock() {
training.gpu_info = Some(info.clone());
}
Ok(info)
}
fn detect_gpu_internal() -> GpuInfo {
// Check for Metal on macOS
#[cfg(target_os = "macos")]
{
// Check if system has Apple Silicon or discrete GPU
let has_metal = std::process::Command::new("system_profiler")
.args(["SPDisplaysDataType", "-json"])
.output()
.map(|o| {
let output = String::from_utf8_lossy(&o.stdout);
output.contains("Metal") || output.contains("Apple M")
})
.unwrap_or(false);
if has_metal {
// Try to get GPU name
let name = std::process::Command::new("system_profiler")
.args(["SPDisplaysDataType"])
.output()
.ok()
.and_then(|o| {
let output = String::from_utf8_lossy(&o.stdout);
// Parse chipset name
for line in output.lines() {
if line.contains("Chipset Model:") {
return line.split(':').nth(1).map(|s| s.trim().to_string());
}
}
None
});
return GpuInfo {
available: true,
backend: GpuBackend::Metal,
name,
memory_mb: None, // Metal doesn't easily expose this
cuda_version: None,
metal_supported: true,
};
}
}
// Check for CUDA on Linux/Windows
#[cfg(any(target_os = "linux", target_os = "windows"))]
{
// Try nvidia-smi for CUDA detection
if let Ok(output) = std::process::Command::new("nvidia-smi")
.args(["--query-gpu=name,memory.total", "--format=csv,noheader,nounits"])
.output()
{
if output.status.success() {
let stdout = String::from_utf8_lossy(&output.stdout);
let parts: Vec<&str> = stdout.trim().split(',').collect();
let name = parts.first().map(|s| s.trim().to_string());
let memory_mb = parts.get(1)
.and_then(|s| s.trim().parse::<u64>().ok());
// Get CUDA version
let cuda_version = std::process::Command::new("nvidia-smi")
.output()
.ok()
.and_then(|o| {
let output = String::from_utf8_lossy(&o.stdout);
for line in output.lines() {
if line.contains("CUDA Version:") {
return line.split("CUDA Version:")
.nth(1)
.map(|s| s.split_whitespace().next().unwrap_or("").to_string());
}
}
None
});
return GpuInfo {
available: true,
backend: GpuBackend::Cuda,
name,
memory_mb,
cuda_version,
metal_supported: false,
};
}
}
}
// Fall back to CPU
GpuInfo {
available: false,
backend: GpuBackend::Cpu,
name: None,
memory_mb: None,
cuda_version: None,
metal_supported: false,
}
}
// ============================================================================
// Dataset Commands
// ============================================================================
/// List available datasets (both standard and downloaded).
#[tauri::command]
pub async fn list_datasets(state: State<'_, AppState>) -> Result<Vec<String>, String> {
let training = state.training.lock().map_err(|e| e.to_string())?;
// Return IDs of downloaded datasets
Ok(training.datasets.iter()
.filter(|d| d.downloaded)
.map(|d| d.id.clone())
.collect())
}
/// Get full dataset information.
#[tauri::command]
pub async fn get_datasets(state: State<'_, AppState>) -> Result<Vec<DatasetInfo>, String> {
let mut training = state.training.lock().map_err(|e| e.to_string())?;
// Initialize with standard datasets if empty
if training.datasets.is_empty() {
training.datasets = get_standard_datasets();
}
Ok(training.datasets.clone())
}
/// Download a dataset (placeholder - actual download would need async HTTP).
#[tauri::command]
pub async fn download_dataset(
dataset_id: String,
state: State<'_, AppState>,
) -> Result<DatasetInfo, String> {
let mut training = state.training.lock().map_err(|e| e.to_string())?;
// Find the dataset
let dataset = training.datasets.iter_mut()
.find(|d| d.id == dataset_id)
.ok_or_else(|| format!("Dataset not found: {}", dataset_id))?;
// Simulate download completion
dataset.downloaded = true;
dataset.path = Some(format!("~/.ruview/datasets/{}", dataset_id));
Ok(dataset.clone())
}
// ============================================================================
// Model/Checkpoint Commands
// ============================================================================
/// List available model architectures.
#[tauri::command]
pub async fn list_models() -> Result<Vec<ModelInfo>, String> {
Ok(get_standard_models())
}
/// List saved checkpoints.
#[tauri::command]
pub async fn list_checkpoints(state: State<'_, AppState>) -> Result<Vec<CheckpointInfo>, String> {
let training = state.training.lock().map_err(|e| e.to_string())?;
Ok(training.checkpoints.clone())
}
/// Export a model checkpoint to ONNX or TorchScript.
#[tauri::command]
pub async fn export_model(
checkpoint_id: String,
format: String,
state: State<'_, AppState>,
) -> Result<String, String> {
let training = state.training.lock().map_err(|e| e.to_string())?;
let checkpoint = training.checkpoints.iter()
.find(|c| c.id == checkpoint_id)
.ok_or_else(|| format!("Checkpoint not found: {}", checkpoint_id))?;
let output_path = match format.as_str() {
"onnx" => format!("{}.onnx", checkpoint.path.trim_end_matches(".pt")),
"torchscript" => format!("{}.ts", checkpoint.path.trim_end_matches(".pt")),
_ => return Err(format!("Unsupported format: {}", format)),
};
// In a real implementation, this would call the actual export logic
Ok(output_path)
}
// ============================================================================
// Training Job Commands
// ============================================================================
/// Start a training job.
#[tauri::command]
pub async fn start_training(
config: TrainingConfig,
state: State<'_, AppState>,
) -> Result<String, String> {
let mut training = state.training.lock().map_err(|e| e.to_string())?;
// Create a new job
let job_id = uuid::Uuid::new_v4().to_string();
let job = TrainingJob {
id: job_id.clone(),
config,
status: TrainingStatus::Running,
started_at: Some(chrono::Utc::now().to_rfc3339()),
progress: TrainingProgress::default(),
loss_history: Vec::new(),
};
training.current_job = Some(job);
// In a real implementation, this would spawn a background training thread
// and emit progress events via Tauri's event system
Ok(job_id)
}
/// Stop the current training job.
#[tauri::command]
pub async fn stop_training(state: State<'_, AppState>) -> Result<(), String> {
let mut training = state.training.lock().map_err(|e| e.to_string())?;
if let Some(ref mut job) = training.current_job {
job.status = TrainingStatus::Paused;
}
Ok(())
}
/// Get current training progress.
#[tauri::command]
pub async fn training_progress(state: State<'_, AppState>) -> Result<Option<TrainingProgress>, String> {
let training = state.training.lock().map_err(|e| e.to_string())?;
Ok(training.current_job.as_ref().map(|j| j.progress.clone()))
}
// ============================================================================
// RuVector Configuration Commands
// ============================================================================
/// Get current RuVector configuration.
#[tauri::command]
pub async fn get_ruvector_config(state: State<'_, AppState>) -> Result<RuVectorConfig, String> {
let training = state.training.lock().map_err(|e| e.to_string())?;
Ok(training.ruvector_config.clone())
}
/// Set RuVector configuration.
#[tauri::command]
pub async fn set_ruvector_config(
config: RuVectorConfig,
state: State<'_, AppState>,
) -> Result<(), String> {
let mut training = state.training.lock().map_err(|e| e.to_string())?;
training.ruvector_config = config;
Ok(())
}
/// Test RuVector modules on live CSI data.
#[tauri::command]
pub async fn test_ruvector_live(
_state: State<'_, AppState>,
) -> Result<LiveTestMetrics, String> {
// In a real implementation, this would process live CSI data
// through the RuVector pipeline and return metrics
Ok(LiveTestMetrics {
fps: 30.0,
latency_ms: 15.0,
persons_detected: 1,
})
}
// ============================================================================
// Metrics Commands
// ============================================================================
/// Get training history (loss/accuracy per epoch).
#[tauri::command]
pub async fn get_training_history(state: State<'_, AppState>) -> Result<Vec<EpochMetrics>, String> {
let training = state.training.lock().map_err(|e| e.to_string())?;
Ok(training.training_history.clone())
}
/// Get evaluation metrics.
#[tauri::command]
pub async fn get_evaluation_metrics(state: State<'_, AppState>) -> Result<Option<EvaluationMetrics>, String> {
let training = state.training.lock().map_err(|e| e.to_string())?;
Ok(training.evaluation_metrics.clone())
}
/// Get per-joint accuracy metrics.
#[tauri::command]
pub async fn get_joint_accuracies(state: State<'_, AppState>) -> Result<Vec<JointAccuracy>, String> {
let training = state.training.lock().map_err(|e| e.to_string())?;
Ok(training.joint_accuracies.clone())
}
// ============================================================================
// Tests
// ============================================================================
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_standard_datasets() {
let datasets = get_standard_datasets();
assert_eq!(datasets.len(), 3);
assert!(datasets.iter().any(|d| d.id == "mmfi"));
}
#[test]
fn test_standard_models() {
let models = get_standard_models();
assert_eq!(models.len(), 6);
assert!(models.iter().any(|m| m.id == "csi-encoder-cnn"));
}
#[test]
fn test_detect_gpu_internal() {
let info = detect_gpu_internal();
// Just verify it returns valid data
assert!(matches!(info.backend, GpuBackend::Cpu | GpuBackend::Cuda | GpuBackend::Metal));
}
#[test]
fn test_ruvector_config_default() {
let config = RuVectorConfig::default();
assert!(config.mincut_enabled);
assert_eq!(config.attention_heads, 4);
}
}
@@ -1,4 +1,3 @@
pub mod config;
pub mod firmware;
pub mod node;
pub mod training;
@@ -1,312 +0,0 @@
//! Training domain types for the desktop application.
use serde::{Deserialize, Serialize};
/// GPU backend type.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
#[serde(rename_all = "lowercase")]
pub enum GpuBackend {
Cuda,
Metal,
#[default]
Cpu,
}
/// GPU information.
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct GpuInfo {
pub available: bool,
pub backend: GpuBackend,
pub name: Option<String>,
pub memory_mb: Option<u64>,
pub cuda_version: Option<String>,
pub metal_supported: bool,
}
/// Dataset format type.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
#[serde(rename_all = "lowercase")]
pub enum DatasetFormat {
#[default]
MmFi,
WiPose,
Wiar,
Custom,
}
/// Dataset information.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DatasetInfo {
pub id: String,
pub name: String,
pub description: String,
pub format: DatasetFormat,
pub size_mb: f64,
pub samples: u64,
pub downloaded: bool,
pub path: Option<String>,
pub url: Option<String>,
}
/// Model architecture type.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
#[serde(rename_all = "lowercase")]
pub enum ModelType {
#[default]
Encoder,
Decoder,
Embedding,
Adaptor,
}
/// Model architecture information.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ModelInfo {
pub id: String,
pub name: String,
pub model_type: ModelType,
pub description: String,
pub params_m: f64,
pub memory_mb: u64,
pub paper: Option<String>,
}
/// Checkpoint information.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CheckpointInfo {
pub id: String,
pub model_id: String,
pub name: String,
pub epoch: u32,
pub val_loss: f64,
pub created_at: String,
pub path: String,
pub size_mb: f64,
}
/// Training configuration.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TrainingConfig {
pub dataset_id: String,
pub model_id: String,
pub epochs: u32,
pub batch_size: u32,
pub learning_rate: f64,
pub optimizer: OptimizerType,
pub weight_decay: f64,
pub use_augmentation: bool,
pub checkpoint_every: u32,
}
impl Default for TrainingConfig {
fn default() -> Self {
Self {
dataset_id: "mmfi".into(),
model_id: "csi-encoder-cnn".into(),
epochs: 100,
batch_size: 32,
learning_rate: 0.001,
optimizer: OptimizerType::Adam,
weight_decay: 0.0001,
use_augmentation: true,
checkpoint_every: 10,
}
}
}
/// Optimizer type.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
#[serde(rename_all = "lowercase")]
pub enum OptimizerType {
#[default]
Adam,
AdamW,
Sgd,
}
/// Training job status.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
#[serde(rename_all = "lowercase")]
pub enum TrainingStatus {
#[default]
Pending,
Running,
Paused,
Completed,
Failed,
}
/// Training progress.
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct TrainingProgress {
pub epoch: u32,
pub total_epochs: u32,
pub batch: u32,
pub total_batches: u32,
pub train_loss: f64,
pub val_loss: Option<f64>,
pub learning_rate: f64,
pub eta_secs: u64,
pub gpu_memory_mb: Option<u64>,
}
/// Training job.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TrainingJob {
pub id: String,
pub config: TrainingConfig,
pub status: TrainingStatus,
pub started_at: Option<String>,
pub progress: TrainingProgress,
pub loss_history: Vec<EpochMetrics>,
}
/// Metrics for a single epoch.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EpochMetrics {
pub epoch: u32,
pub train_loss: f64,
pub val_loss: f64,
pub train_acc: f64,
pub val_acc: f64,
pub learning_rate: f64,
pub timestamp: String,
}
/// Evaluation metrics.
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct EvaluationMetrics {
pub pck_05: f64,
pub pck_10: f64,
pub pck_20: f64,
pub map_50: f64,
pub map_75: f64,
pub iou: f64,
}
/// Per-joint accuracy.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JointAccuracy {
pub joint: String,
pub accuracy: f64,
}
/// RuVector interpolation mode.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
#[serde(rename_all = "lowercase")]
pub enum InterpolationMode {
Linear,
Cubic,
#[default]
Sparse,
}
/// RuVector module configuration.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RuVectorConfig {
// MinCut parameters
pub mincut_enabled: bool,
pub mincut_threshold: f64,
pub mincut_max_persons: u32,
// Attention parameters
pub attention_enabled: bool,
pub attention_heads: u32,
pub attention_dropout: f64,
// Temporal parameters
pub temporal_enabled: bool,
pub temporal_window_ms: u32,
pub temporal_compression_ratio: u32,
// Solver parameters
pub solver_enabled: bool,
pub solver_interpolation: InterpolationMode,
pub solver_subcarrier_count: u32,
// BVP parameters
pub bvp_enabled: bool,
pub bvp_filter_hz: (f64, f64),
}
impl Default for RuVectorConfig {
fn default() -> Self {
Self {
mincut_enabled: true,
mincut_threshold: 0.5,
mincut_max_persons: 5,
attention_enabled: true,
attention_heads: 4,
attention_dropout: 0.1,
temporal_enabled: true,
temporal_window_ms: 500,
temporal_compression_ratio: 4,
solver_enabled: true,
solver_interpolation: InterpolationMode::Sparse,
solver_subcarrier_count: 56,
bvp_enabled: false,
bvp_filter_hz: (0.7, 4.0),
}
}
}
/// Live test metrics from RuVector processing.
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct LiveTestMetrics {
pub fps: f64,
pub latency_ms: f64,
pub persons_detected: u32,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_gpu_info_default() {
let info = GpuInfo::default();
assert!(!info.available);
assert_eq!(info.backend, GpuBackend::Cpu);
}
#[test]
fn test_training_config_default() {
let config = TrainingConfig::default();
assert_eq!(config.epochs, 100);
assert_eq!(config.batch_size, 32);
assert_eq!(config.optimizer, OptimizerType::Adam);
}
#[test]
fn test_ruvector_config_default() {
let config = RuVectorConfig::default();
assert!(config.mincut_enabled);
assert_eq!(config.mincut_threshold, 0.5);
assert_eq!(config.attention_heads, 4);
}
#[test]
fn test_serialization() {
let config = TrainingConfig::default();
let json = serde_json::to_string(&config).unwrap();
let parsed: TrainingConfig = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.epochs, config.epochs);
}
#[test]
fn test_dataset_info() {
let dataset = DatasetInfo {
id: "mmfi".into(),
name: "MM-Fi Dataset".into(),
description: "Multi-modal WiFi sensing".into(),
format: DatasetFormat::MmFi,
size_mb: 2400.0,
samples: 320000,
downloaded: false,
path: None,
url: Some("https://example.com/mmfi.zip".into()),
};
assert_eq!(dataset.id, "mmfi");
assert!(!dataset.downloaded);
}
}
@@ -2,7 +2,7 @@ pub mod commands;
pub mod domain;
pub mod state;
use commands::{discovery, flash, ota, provision, server, settings, training, wasm};
use commands::{discovery, flash, ota, provision, server, settings, wasm};
pub fn run() {
tauri::Builder::default()
@@ -46,23 +46,6 @@ pub fn run() {
// Settings
settings::get_settings,
settings::save_settings,
// Training
training::detect_gpu,
training::list_datasets,
training::get_datasets,
training::download_dataset,
training::list_models,
training::list_checkpoints,
training::export_model,
training::start_training,
training::stop_training,
training::training_progress,
training::get_ruvector_config,
training::set_ruvector_config,
training::test_ruvector_live,
training::get_training_history,
training::get_evaluation_metrics,
training::get_joint_accuracies,
])
.run(tauri::generate_context!())
.expect("error while running tauri application");
@@ -3,10 +3,6 @@ use std::sync::Mutex;
use std::time::Instant;
use crate::domain::node::DiscoveredNode;
use crate::domain::training::{
CheckpointInfo, DatasetInfo, EpochMetrics, EvaluationMetrics,
GpuInfo, JointAccuracy, RuVectorConfig, TrainingJob,
};
/// Sub-state for discovered nodes.
#[derive(Default)]
@@ -91,33 +87,6 @@ impl Default for SettingsState {
}
}
/// Sub-state for training operations.
pub struct TrainingState {
pub gpu_info: Option<GpuInfo>,
pub datasets: Vec<DatasetInfo>,
pub checkpoints: Vec<CheckpointInfo>,
pub current_job: Option<TrainingJob>,
pub ruvector_config: RuVectorConfig,
pub training_history: Vec<EpochMetrics>,
pub evaluation_metrics: Option<EvaluationMetrics>,
pub joint_accuracies: Vec<JointAccuracy>,
}
impl Default for TrainingState {
fn default() -> Self {
Self {
gpu_info: None,
datasets: Vec::new(),
checkpoints: Vec::new(),
current_job: None,
ruvector_config: RuVectorConfig::default(),
training_history: Vec::new(),
evaluation_metrics: None,
joint_accuracies: Vec::new(),
}
}
}
/// Top-level application state managed by Tauri.
pub struct AppState {
pub discovery: Mutex<DiscoveryState>,
@@ -125,7 +94,6 @@ pub struct AppState {
pub flash: Mutex<FlashState>,
pub ota: Mutex<OtaState>,
pub settings: Mutex<SettingsState>,
pub training: Mutex<TrainingState>,
}
impl Default for AppState {
@@ -136,7 +104,6 @@ impl Default for AppState {
flash: Mutex::new(FlashState::default()),
ota: Mutex::new(OtaState::default()),
settings: Mutex::new(SettingsState::default()),
training: Mutex::new(TrainingState::default()),
}
}
}
@@ -168,9 +135,6 @@ impl AppState {
if let Ok(mut settings) = self.settings.lock() {
*settings = SettingsState::default();
}
if let Ok(mut training) = self.training.lock() {
*training = TrainingState::default();
}
}
}
@@ -1,7 +1,7 @@
{
"$schema": "https://raw.githubusercontent.com/tauri-apps/tauri/dev/crates/tauri-config-schema/schema.json",
"productName": "RuView Desktop",
"version": "0.5.0",
"version": "0.4.4",
"identifier": "net.ruv.ruview",
"build": {
"frontendDist": "ui/dist",
@@ -1,7 +1,7 @@
{
"name": "ruview-desktop-ui",
"private": true,
"version": "0.5.0",
"version": "0.4.4",
"type": "module",
"scripts": {
"dev": "vite",
@@ -8,7 +8,6 @@ import { OtaUpdate } from "./pages/OtaUpdate";
import { EdgeModules } from "./pages/EdgeModules";
import { Sensing } from "./pages/Sensing";
import { MeshView } from "./pages/MeshView";
import Training from "./pages/Training";
import { Settings } from "./pages/Settings";
type Page =
@@ -20,7 +19,6 @@ type Page =
| "wasm"
| "sensing"
| "mesh"
| "training"
| "settings";
interface NavItem {
@@ -38,7 +36,6 @@ const NAV_ITEMS: NavItem[] = [
{ id: "wasm", label: "Edge Modules", icon: "\u2B21" },
{ id: "sensing", label: "Sensing", icon: "\u2248" },
{ id: "mesh", label: "Mesh View", icon: "\u2B2F" },
{ id: "training", label: "Training", icon: "\u2B50" },
{ id: "settings", label: "Settings", icon: "\u2699" },
];
@@ -102,7 +99,6 @@ const App: React.FC = () => {
case "wasm": return <EdgeModules />;
case "sensing": return <Sensing />;
case "mesh": return <MeshView />;
case "training": return <Training />;
case "settings": return <Settings />;
}
};
@@ -1,369 +0,0 @@
import React, { useState, useEffect } from "react";
import { invoke } from "@tauri-apps/api/core";
interface Dataset {
id: string;
name: string;
description: string;
size_mb: number;
samples: number;
downloaded: boolean;
path: string | null;
}
const STANDARD_DATASETS: Omit<Dataset, "downloaded" | "path">[] = [
{
id: "mmfi",
name: "MM-Fi Dataset",
description: "Multi-modal WiFi sensing dataset with 40 subjects, 27 activities",
size_mb: 2400,
samples: 320000,
},
{
id: "wipose",
name: "Wi-Pose Dataset",
description: "WiFi-based pose estimation with 3D skeleton annotations",
size_mb: 1800,
samples: 150000,
},
{
id: "wiar",
name: "WiAR Dataset",
description: "WiFi activity recognition with CSI data",
size_mb: 500,
samples: 45000,
},
];
const DatasetsTab: React.FC = () => {
const [datasets, setDatasets] = useState<Dataset[]>([]);
const [downloading, setDownloading] = useState<string | null>(null);
const [downloadProgress, setDownloadProgress] = useState<number>(0);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
loadDatasets();
}, []);
const loadDatasets = async () => {
try {
const downloaded = await invoke<string[]>("list_datasets");
const ds = STANDARD_DATASETS.map((d) => ({
...d,
downloaded: downloaded.includes(d.id),
path: downloaded.includes(d.id) ? `~/.ruview/datasets/${d.id}` : null,
}));
setDatasets(ds);
} catch (err) {
// If command not implemented yet, show placeholders
setDatasets(
STANDARD_DATASETS.map((d) => ({
...d,
downloaded: false,
path: null,
}))
);
}
};
const handleDownload = async (datasetId: string) => {
setDownloading(datasetId);
setDownloadProgress(0);
setError(null);
try {
// Simulate download progress for now
for (let i = 0; i <= 100; i += 10) {
setDownloadProgress(i);
await new Promise((r) => setTimeout(r, 500));
}
// TODO: Call actual download command
// await invoke("download_dataset", { datasetId });
setDatasets((prev) =>
prev.map((d) =>
d.id === datasetId
? { ...d, downloaded: true, path: `~/.ruview/datasets/${d.id}` }
: d
)
);
} catch (err) {
setError(`Download failed: ${err}`);
} finally {
setDownloading(null);
}
};
return (
<div>
{/* Stats Row */}
<div
style={{
display: "grid",
gridTemplateColumns: "repeat(3, 1fr)",
gap: "var(--space-4)",
marginBottom: "var(--space-5)",
}}
>
<StatCard
label="Available Datasets"
value={datasets.length}
/>
<StatCard
label="Downloaded"
value={datasets.filter((d) => d.downloaded).length}
color="var(--status-online)"
/>
<StatCard
label="Total Samples"
value={`${(datasets.reduce((acc, d) => acc + (d.downloaded ? d.samples : 0), 0) / 1000).toFixed(0)}K`}
/>
</div>
{error && (
<div
style={{
background: "rgba(248, 81, 73, 0.1)",
border: "1px solid rgba(248, 81, 73, 0.3)",
borderRadius: 6,
padding: "var(--space-3)",
marginBottom: "var(--space-4)",
fontSize: 13,
color: "var(--status-error)",
}}
>
{error}
</div>
)}
{/* Dataset Cards */}
<div
style={{
display: "grid",
gridTemplateColumns: "repeat(auto-fill, minmax(340px, 1fr))",
gap: "var(--space-4)",
}}
>
{datasets.map((dataset) => (
<div
key={dataset.id}
className="card"
style={{
padding: "var(--space-4)",
opacity: dataset.downloaded ? 1 : 0.85,
}}
>
<div
style={{
display: "flex",
justifyContent: "space-between",
alignItems: "start",
marginBottom: "var(--space-3)",
}}
>
<div>
<h3 style={{ margin: 0, fontSize: 15, fontWeight: 600 }}>
{dataset.name}
</h3>
<p
style={{
fontSize: 12,
color: "var(--text-muted)",
marginTop: 4,
lineHeight: 1.4,
}}
>
{dataset.description}
</p>
</div>
{dataset.downloaded && (
<span
style={{
background: "rgba(63, 185, 80, 0.15)",
color: "var(--status-online)",
padding: "2px 8px",
borderRadius: 4,
fontSize: 10,
fontWeight: 600,
}}
>
DOWNLOADED
</span>
)}
</div>
<div
style={{
display: "flex",
gap: "var(--space-4)",
fontSize: 12,
color: "var(--text-secondary)",
marginBottom: "var(--space-3)",
}}
>
<span>📦 {(dataset.size_mb / 1024).toFixed(1)} GB</span>
<span>📊 {(dataset.samples / 1000).toFixed(0)}K samples</span>
</div>
{downloading === dataset.id ? (
<div>
<div
style={{
height: 4,
background: "var(--border)",
borderRadius: 2,
overflow: "hidden",
}}
>
<div
style={{
width: `${downloadProgress}%`,
height: "100%",
background: "var(--accent)",
transition: "width 0.3s",
}}
/>
</div>
<div
style={{
fontSize: 11,
color: "var(--text-muted)",
marginTop: 4,
textAlign: "center",
}}
>
Downloading... {downloadProgress}%
</div>
</div>
) : (
<div style={{ display: "flex", gap: "var(--space-2)" }}>
{dataset.downloaded ? (
<>
<button
style={{
flex: 1,
padding: "8px 12px",
background: "rgba(56, 139, 253, 0.1)",
border: "1px solid rgba(56, 139, 253, 0.3)",
borderRadius: 6,
color: "var(--accent)",
fontSize: 12,
fontWeight: 600,
cursor: "pointer",
}}
>
Preview
</button>
<button
style={{
padding: "8px 12px",
background: "transparent",
border: "1px solid var(--border)",
borderRadius: 6,
color: "var(--text-secondary)",
fontSize: 12,
fontWeight: 600,
cursor: "pointer",
}}
>
Delete
</button>
</>
) : (
<button
onClick={() => handleDownload(dataset.id)}
className="btn-gradient"
style={{ flex: 1, fontSize: 12 }}
>
Download Dataset
</button>
)}
</div>
)}
</div>
))}
</div>
{/* Import Custom Dataset */}
<div
className="card"
style={{
marginTop: "var(--space-5)",
padding: "var(--space-4)",
border: "2px dashed var(--border)",
textAlign: "center",
}}
>
<div style={{ fontSize: 32, marginBottom: "var(--space-2)" }}>📁</div>
<h4 style={{ margin: 0, fontSize: 14, fontWeight: 600 }}>
Import Custom Dataset
</h4>
<p
style={{
fontSize: 12,
color: "var(--text-muted)",
marginTop: 4,
marginBottom: "var(--space-3)",
}}
>
Import CSI recordings in CSV, NPZ, or HDF5 format
</p>
<button
style={{
padding: "8px 20px",
background: "transparent",
border: "1px solid var(--border)",
borderRadius: 6,
color: "var(--text-secondary)",
fontSize: 12,
fontWeight: 600,
cursor: "pointer",
}}
>
Browse Files
</button>
</div>
</div>
);
};
function StatCard({
label,
value,
color,
}: {
label: string;
value: number | string;
color?: string;
}) {
return (
<div className="card-glow" style={{ padding: "var(--space-4)" }}>
<div
style={{
fontSize: 10,
textTransform: "uppercase",
letterSpacing: "0.06em",
color: "var(--text-muted)",
marginBottom: "var(--space-2)",
fontWeight: 600,
}}
>
{label}
</div>
<div
style={{
fontFamily: "var(--font-mono)",
fontSize: 28,
fontWeight: 600,
color: color || "var(--text-primary)",
letterSpacing: "-0.02em",
}}
>
{value}
</div>
</div>
);
}
export default DatasetsTab;
@@ -1,609 +0,0 @@
import React, { useState, useEffect } from "react";
import { invoke } from "@tauri-apps/api/core";
interface TrainingMetrics {
epoch: number;
train_loss: number;
val_loss: number;
train_acc: number;
val_acc: number;
learning_rate: number;
timestamp: string;
}
interface EvaluationMetrics {
pck_05: number;
pck_10: number;
pck_20: number;
map_50: number;
map_75: number;
iou: number;
}
interface JointAccuracy {
joint: string;
accuracy: number;
}
const JOINT_NAMES = [
"nose",
"left_eye",
"right_eye",
"left_ear",
"right_ear",
"left_shoulder",
"right_shoulder",
"left_elbow",
"right_elbow",
"left_wrist",
"right_wrist",
"left_hip",
"right_hip",
"left_knee",
"right_knee",
"left_ankle",
"right_ankle",
];
const MetricsTab: React.FC = () => {
const [trainingHistory, setTrainingHistory] = useState<TrainingMetrics[]>([]);
const [evaluation, setEvaluation] = useState<EvaluationMetrics | null>(null);
const [jointAccuracies, setJointAccuracies] = useState<JointAccuracy[]>([]);
const [selectedMetric, setSelectedMetric] = useState<"loss" | "accuracy">("loss");
const [exporting, setExporting] = useState(false);
useEffect(() => {
loadMetrics();
}, []);
const loadMetrics = async () => {
try {
const metrics = await invoke<TrainingMetrics[]>("get_training_history");
setTrainingHistory(metrics);
const evalMetrics = await invoke<EvaluationMetrics>("get_evaluation_metrics");
setEvaluation(evalMetrics);
const joints = await invoke<JointAccuracy[]>("get_joint_accuracies");
setJointAccuracies(joints);
} catch (err) {
// Generate mock data for demonstration
const mockHistory: TrainingMetrics[] = [];
for (let i = 1; i <= 50; i++) {
mockHistory.push({
epoch: i,
train_loss: 0.5 * Math.exp(-i / 20) + 0.02 + Math.random() * 0.01,
val_loss: 0.55 * Math.exp(-i / 18) + 0.025 + Math.random() * 0.015,
train_acc: 1 - 0.5 * Math.exp(-i / 15) - Math.random() * 0.02,
val_acc: 1 - 0.55 * Math.exp(-i / 15) - Math.random() * 0.025,
learning_rate: 0.001 * Math.pow(0.95, Math.floor(i / 10)),
timestamp: new Date(Date.now() - (50 - i) * 60000).toISOString(),
});
}
setTrainingHistory(mockHistory);
setEvaluation({
pck_05: 0.72,
pck_10: 0.89,
pck_20: 0.96,
map_50: 0.84,
map_75: 0.71,
iou: 0.78,
});
setJointAccuracies(
JOINT_NAMES.map((joint) => ({
joint,
accuracy: 0.7 + Math.random() * 0.25,
}))
);
}
};
const exportMetrics = async (format: "csv" | "json" | "tensorboard") => {
setExporting(true);
try {
if (format === "json") {
const data = {
training: trainingHistory,
evaluation,
joints: jointAccuracies,
};
const blob = new Blob([JSON.stringify(data, null, 2)], { type: "application/json" });
downloadBlob(blob, "metrics.json");
} else if (format === "csv") {
const headers = "epoch,train_loss,val_loss,train_acc,val_acc,learning_rate\n";
const rows = trainingHistory
.map(
(m) =>
`${m.epoch},${m.train_loss.toFixed(6)},${m.val_loss.toFixed(6)},${m.train_acc.toFixed(4)},${m.val_acc.toFixed(4)},${m.learning_rate.toExponential(2)}`
)
.join("\n");
const blob = new Blob([headers + rows], { type: "text/csv" });
downloadBlob(blob, "training_history.csv");
} else {
// TensorBoard format would require server-side handling
alert("TensorBoard export requires running the backend server");
}
} finally {
setExporting(false);
}
};
const downloadBlob = (blob: Blob, filename: string) => {
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = filename;
a.click();
URL.revokeObjectURL(url);
};
const maxLoss = Math.max(
...trainingHistory.map((m) => Math.max(m.train_loss, m.val_loss)),
0.1
);
return (
<div>
{/* Summary Stats */}
<div
style={{
display: "grid",
gridTemplateColumns: "repeat(4, 1fr)",
gap: "var(--space-4)",
marginBottom: "var(--space-5)",
}}
>
<StatCard
label="Epochs Trained"
value={trainingHistory.length}
/>
<StatCard
label="Best Val Loss"
value={
trainingHistory.length > 0
? Math.min(...trainingHistory.map((m) => m.val_loss)).toFixed(4)
: "—"
}
color="var(--status-online)"
/>
<StatCard
label="Best Val Acc"
value={
trainingHistory.length > 0
? `${(Math.max(...trainingHistory.map((m) => m.val_acc)) * 100).toFixed(1)}%`
: "—"
}
color="var(--accent)"
/>
<StatCard
label="PCK@0.1"
value={evaluation ? `${(evaluation.pck_10 * 100).toFixed(1)}%` : "—"}
/>
</div>
<div style={{ display: "grid", gridTemplateColumns: "2fr 1fr", gap: "var(--space-5)" }}>
{/* Loss/Accuracy Charts */}
<div className="card" style={{ padding: "var(--space-4)" }}>
<div
style={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
marginBottom: "var(--space-4)",
}}
>
<h3 style={{ margin: 0, fontSize: 14, fontWeight: 600 }}>Training Curves</h3>
<div style={{ display: "flex", gap: "var(--space-2)" }}>
<button
onClick={() => setSelectedMetric("loss")}
style={{
padding: "6px 12px",
background: selectedMetric === "loss" ? "var(--accent)" : "transparent",
border: `1px solid ${selectedMetric === "loss" ? "var(--accent)" : "var(--border)"}`,
borderRadius: 4,
color: selectedMetric === "loss" ? "white" : "var(--text-secondary)",
fontSize: 11,
fontWeight: 600,
cursor: "pointer",
}}
>
Loss
</button>
<button
onClick={() => setSelectedMetric("accuracy")}
style={{
padding: "6px 12px",
background: selectedMetric === "accuracy" ? "var(--accent)" : "transparent",
border: `1px solid ${selectedMetric === "accuracy" ? "var(--accent)" : "var(--border)"}`,
borderRadius: 4,
color: selectedMetric === "accuracy" ? "white" : "var(--text-secondary)",
fontSize: 11,
fontWeight: 600,
cursor: "pointer",
}}
>
Accuracy
</button>
</div>
</div>
{/* Chart Area */}
<div
style={{
height: 250,
position: "relative",
background: "var(--bg-secondary)",
borderRadius: 8,
padding: "var(--space-3)",
}}
>
{trainingHistory.length === 0 ? (
<div
style={{
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
height: "100%",
color: "var(--text-muted)",
}}
>
<span style={{ fontSize: 32 }}>📊</span>
<p style={{ fontSize: 13, marginTop: "var(--space-2)" }}>
No training data yet
</p>
</div>
) : (
<svg width="100%" height="100%" viewBox="0 0 500 200" preserveAspectRatio="none">
{/* Grid lines */}
{[0, 0.25, 0.5, 0.75, 1].map((y) => (
<line
key={y}
x1="0"
y1={y * 180}
x2="500"
y2={y * 180}
stroke="var(--border)"
strokeWidth="0.5"
strokeDasharray="4"
/>
))}
{/* Train line */}
<polyline
fill="none"
stroke="var(--accent)"
strokeWidth="2"
points={trainingHistory
.map((m, i) => {
const x = (i / (trainingHistory.length - 1)) * 500;
const value = selectedMetric === "loss" ? m.train_loss : m.train_acc;
const y =
selectedMetric === "loss"
? (value / maxLoss) * 180
: (1 - value) * 180;
return `${x},${y}`;
})
.join(" ")}
/>
{/* Val line */}
<polyline
fill="none"
stroke="var(--status-online)"
strokeWidth="2"
points={trainingHistory
.map((m, i) => {
const x = (i / (trainingHistory.length - 1)) * 500;
const value = selectedMetric === "loss" ? m.val_loss : m.val_acc;
const y =
selectedMetric === "loss"
? (value / maxLoss) * 180
: (1 - value) * 180;
return `${x},${y}`;
})
.join(" ")}
/>
</svg>
)}
{/* Legend */}
<div
style={{
position: "absolute",
top: "var(--space-2)",
right: "var(--space-2)",
display: "flex",
gap: "var(--space-3)",
fontSize: 11,
}}
>
<span style={{ display: "flex", alignItems: "center", gap: 4 }}>
<span
style={{
width: 12,
height: 3,
background: "var(--accent)",
borderRadius: 2,
}}
/>
Train
</span>
<span style={{ display: "flex", alignItems: "center", gap: 4 }}>
<span
style={{
width: 12,
height: 3,
background: "var(--status-online)",
borderRadius: 2,
}}
/>
Validation
</span>
</div>
</div>
</div>
{/* Evaluation Metrics */}
<div className="card" style={{ padding: "var(--space-4)" }}>
<h3 style={{ margin: 0, fontSize: 14, fontWeight: 600, marginBottom: "var(--space-4)" }}>
Evaluation Metrics
</h3>
{!evaluation ? (
<div
style={{
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
height: 200,
color: "var(--text-muted)",
}}
>
<span style={{ fontSize: 32 }}>📏</span>
<p style={{ fontSize: 13, marginTop: "var(--space-2)" }}>
Run evaluation to see metrics
</p>
</div>
) : (
<div style={{ display: "flex", flexDirection: "column", gap: "var(--space-3)" }}>
<MetricBar label="PCK@0.05" value={evaluation.pck_05} color="#f59e0b" />
<MetricBar label="PCK@0.10" value={evaluation.pck_10} color="var(--accent)" />
<MetricBar label="PCK@0.20" value={evaluation.pck_20} color="var(--status-online)" />
<div style={{ height: 1, background: "var(--border)", margin: "var(--space-2) 0" }} />
<MetricBar label="mAP@0.50" value={evaluation.map_50} color="#a855f7" />
<MetricBar label="mAP@0.75" value={evaluation.map_75} color="#ec4899" />
<MetricBar label="IoU" value={evaluation.iou} color="#06b6d4" />
</div>
)}
</div>
</div>
{/* Joint-wise Accuracy */}
<div className="card" style={{ marginTop: "var(--space-5)", padding: "var(--space-4)" }}>
<h3 style={{ margin: 0, fontSize: 14, fontWeight: 600, marginBottom: "var(--space-4)" }}>
Per-Joint Accuracy
</h3>
{jointAccuracies.length === 0 ? (
<div
style={{
textAlign: "center",
padding: "var(--space-5)",
color: "var(--text-muted)",
}}
>
No joint accuracy data available
</div>
) : (
<div
style={{
display: "grid",
gridTemplateColumns: "repeat(auto-fill, minmax(140px, 1fr))",
gap: "var(--space-3)",
}}
>
{jointAccuracies.map((ja) => (
<div
key={ja.joint}
style={{
padding: "var(--space-3)",
background: "var(--bg-secondary)",
borderRadius: 6,
textAlign: "center",
}}
>
<div
style={{
fontSize: 11,
color: "var(--text-muted)",
marginBottom: 4,
textTransform: "capitalize",
}}
>
{ja.joint.replace("_", " ")}
</div>
<div
style={{
fontFamily: "var(--font-mono)",
fontSize: 18,
fontWeight: 600,
color:
ja.accuracy > 0.9
? "var(--status-online)"
: ja.accuracy > 0.8
? "var(--accent)"
: ja.accuracy > 0.7
? "#f59e0b"
: "var(--status-error)",
}}
>
{(ja.accuracy * 100).toFixed(1)}%
</div>
</div>
))}
</div>
)}
</div>
{/* Export Section */}
<div
className="card"
style={{
marginTop: "var(--space-5)",
padding: "var(--space-4)",
display: "flex",
justifyContent: "space-between",
alignItems: "center",
}}
>
<div>
<h3 style={{ margin: 0, fontSize: 14, fontWeight: 600 }}>Export Metrics</h3>
<p style={{ fontSize: 12, color: "var(--text-muted)", marginTop: 4 }}>
Download training history and evaluation results
</p>
</div>
<div style={{ display: "flex", gap: "var(--space-2)" }}>
<button
onClick={() => exportMetrics("csv")}
disabled={exporting || trainingHistory.length === 0}
style={{
padding: "8px 16px",
background: "rgba(56, 139, 253, 0.1)",
border: "1px solid rgba(56, 139, 253, 0.3)",
borderRadius: 6,
color: "var(--accent)",
fontSize: 12,
fontWeight: 600,
cursor: trainingHistory.length === 0 ? "not-allowed" : "pointer",
opacity: trainingHistory.length === 0 ? 0.5 : 1,
}}
>
CSV
</button>
<button
onClick={() => exportMetrics("json")}
disabled={exporting || trainingHistory.length === 0}
style={{
padding: "8px 16px",
background: "rgba(56, 139, 253, 0.1)",
border: "1px solid rgba(56, 139, 253, 0.3)",
borderRadius: 6,
color: "var(--accent)",
fontSize: 12,
fontWeight: 600,
cursor: trainingHistory.length === 0 ? "not-allowed" : "pointer",
opacity: trainingHistory.length === 0 ? 0.5 : 1,
}}
>
JSON
</button>
<button
onClick={() => exportMetrics("tensorboard")}
disabled={exporting || trainingHistory.length === 0}
style={{
padding: "8px 16px",
background: "transparent",
border: "1px solid var(--border)",
borderRadius: 6,
color: "var(--text-secondary)",
fontSize: 12,
fontWeight: 600,
cursor: trainingHistory.length === 0 ? "not-allowed" : "pointer",
opacity: trainingHistory.length === 0 ? 0.5 : 1,
}}
>
TensorBoard
</button>
</div>
</div>
</div>
);
};
function StatCard({
label,
value,
color,
}: {
label: string;
value: number | string;
color?: string;
}) {
return (
<div className="card-glow" style={{ padding: "var(--space-4)" }}>
<div
style={{
fontSize: 10,
textTransform: "uppercase",
letterSpacing: "0.06em",
color: "var(--text-muted)",
marginBottom: "var(--space-2)",
fontWeight: 600,
}}
>
{label}
</div>
<div
style={{
fontFamily: "var(--font-mono)",
fontSize: 28,
fontWeight: 600,
color: color || "var(--text-primary)",
letterSpacing: "-0.02em",
}}
>
{value}
</div>
</div>
);
}
function MetricBar({
label,
value,
color,
}: {
label: string;
value: number;
color: string;
}) {
return (
<div>
<div
style={{
display: "flex",
justifyContent: "space-between",
fontSize: 12,
marginBottom: 4,
}}
>
<span>{label}</span>
<span style={{ fontFamily: "var(--font-mono)", fontWeight: 600 }}>
{(value * 100).toFixed(1)}%
</span>
</div>
<div
style={{
height: 6,
background: "var(--bg-secondary)",
borderRadius: 3,
overflow: "hidden",
}}
>
<div
style={{
width: `${value * 100}%`,
height: "100%",
background: color,
borderRadius: 3,
transition: "width 0.5s",
}}
/>
</div>
</div>
);
}
export default MetricsTab;
@@ -1,405 +0,0 @@
import React, { useState, useEffect } from "react";
import { invoke } from "@tauri-apps/api/core";
interface ModelArchitecture {
id: string;
name: string;
type: "encoder" | "decoder" | "embedding" | "adaptor";
description: string;
params_m: number;
memory_mb: number;
paper?: string;
}
interface Checkpoint {
id: string;
model_id: string;
name: string;
epoch: number;
val_loss: number;
created_at: string;
path: string;
size_mb: number;
}
const MODEL_ARCHITECTURES: ModelArchitecture[] = [
{
id: "csi-encoder-cnn",
name: "CSI Encoder (CNN)",
type: "encoder",
description: "Convolutional encoder for CSI amplitude/phase features",
params_m: 2.3,
memory_mb: 128,
},
{
id: "csi-encoder-transformer",
name: "CSI Encoder (Transformer)",
type: "encoder",
description: "Self-attention based CSI feature extraction",
params_m: 8.5,
memory_mb: 384,
paper: "WiFi-ViT 2024",
},
{
id: "pose-decoder-lstm",
name: "Pose Decoder (LSTM)",
type: "decoder",
description: "Recurrent decoder for temporal pose estimation",
params_m: 1.8,
memory_mb: 96,
},
{
id: "pose-decoder-gru",
name: "Pose Decoder (GRU)",
type: "decoder",
description: "Gated recurrent unit pose decoder (faster)",
params_m: 1.2,
memory_mb: 64,
},
{
id: "aether-embedding",
name: "AETHER Embedding",
type: "embedding",
description: "Contrastive CSI embedding for person re-identification (ADR-024)",
params_m: 4.2,
memory_mb: 192,
paper: "AETHER 2025",
},
{
id: "meridian-adaptor",
name: "MERIDIAN Adaptor",
type: "adaptor",
description: "Cross-environment domain generalization module (ADR-027)",
params_m: 3.1,
memory_mb: 144,
paper: "MERIDIAN 2025",
},
];
const ModelsTab: React.FC = () => {
const [checkpoints, setCheckpoints] = useState<Checkpoint[]>([]);
const [selectedModel, setSelectedModel] = useState<string | null>(null);
const [exporting, setExporting] = useState<string | null>(null);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
loadCheckpoints();
}, []);
const loadCheckpoints = async () => {
try {
const loaded = await invoke<Checkpoint[]>("list_checkpoints");
setCheckpoints(loaded);
} catch (err) {
// Mock data if command not implemented
setCheckpoints([
{
id: "ckpt-001",
model_id: "csi-encoder-cnn",
name: "CSI-CNN v1.2",
epoch: 50,
val_loss: 0.0234,
created_at: "2026-03-08T14:30:00Z",
path: "~/.ruview/models/csi-cnn-v1.2.pt",
size_mb: 12.4,
},
{
id: "ckpt-002",
model_id: "pose-decoder-gru",
name: "Pose-GRU v2.0",
epoch: 100,
val_loss: 0.0189,
created_at: "2026-03-09T09:15:00Z",
path: "~/.ruview/models/pose-gru-v2.pt",
size_mb: 8.2,
},
]);
}
};
const handleExport = async (checkpointId: string, format: "onnx" | "torchscript") => {
setExporting(checkpointId);
setError(null);
try {
await invoke("export_model", { checkpointId, format });
// Success notification would go here
} catch (err) {
setError(`Export failed: ${err}`);
} finally {
setExporting(null);
}
};
const getTypeColor = (type: ModelArchitecture["type"]) => {
switch (type) {
case "encoder":
return "var(--accent)";
case "decoder":
return "var(--status-online)";
case "embedding":
return "#a855f7";
case "adaptor":
return "#f59e0b";
}
};
return (
<div>
{/* Stats Row */}
<div
style={{
display: "grid",
gridTemplateColumns: "repeat(4, 1fr)",
gap: "var(--space-4)",
marginBottom: "var(--space-5)",
}}
>
<StatCard label="Architectures" value={MODEL_ARCHITECTURES.length} />
<StatCard
label="Checkpoints"
value={checkpoints.length}
color="var(--status-online)"
/>
<StatCard
label="Total Params"
value={`${MODEL_ARCHITECTURES.reduce((acc, m) => acc + m.params_m, 0).toFixed(1)}M`}
/>
<StatCard
label="Storage Used"
value={`${checkpoints.reduce((acc, c) => acc + c.size_mb, 0).toFixed(1)} MB`}
/>
</div>
{error && (
<div
style={{
background: "rgba(248, 81, 73, 0.1)",
border: "1px solid rgba(248, 81, 73, 0.3)",
borderRadius: 6,
padding: "var(--space-3)",
marginBottom: "var(--space-4)",
fontSize: 13,
color: "var(--status-error)",
}}
>
{error}
</div>
)}
{/* Model Architectures */}
<h3 style={{ fontSize: 14, fontWeight: 600, marginBottom: "var(--space-3)" }}>
Available Architectures
</h3>
<div
style={{
display: "grid",
gridTemplateColumns: "repeat(auto-fill, minmax(300px, 1fr))",
gap: "var(--space-3)",
marginBottom: "var(--space-5)",
}}
>
{MODEL_ARCHITECTURES.map((model) => (
<div
key={model.id}
className="card"
style={{
padding: "var(--space-3)",
cursor: "pointer",
border:
selectedModel === model.id
? "1px solid var(--accent)"
: "1px solid transparent",
}}
onClick={() => setSelectedModel(model.id)}
>
<div
style={{
display: "flex",
justifyContent: "space-between",
alignItems: "start",
marginBottom: "var(--space-2)",
}}
>
<div>
<h4 style={{ margin: 0, fontSize: 14, fontWeight: 600 }}>
{model.name}
</h4>
<span
style={{
display: "inline-block",
marginTop: 4,
padding: "1px 6px",
borderRadius: 3,
fontSize: 10,
fontWeight: 600,
textTransform: "uppercase",
background: `${getTypeColor(model.type)}20`,
color: getTypeColor(model.type),
}}
>
{model.type}
</span>
</div>
{model.paper && (
<span
style={{
fontSize: 10,
color: "var(--text-muted)",
fontStyle: "italic",
}}
>
{model.paper}
</span>
)}
</div>
<p
style={{
fontSize: 11,
color: "var(--text-muted)",
margin: "var(--space-2) 0",
lineHeight: 1.4,
}}
>
{model.description}
</p>
<div
style={{
display: "flex",
gap: "var(--space-3)",
fontSize: 11,
color: "var(--text-secondary)",
}}
>
<span>🧮 {model.params_m}M params</span>
<span>💾 {model.memory_mb} MB</span>
</div>
</div>
))}
</div>
{/* Checkpoints */}
<h3 style={{ fontSize: 14, fontWeight: 600, marginBottom: "var(--space-3)" }}>
Saved Checkpoints
</h3>
{checkpoints.length === 0 ? (
<div
className="card"
style={{
padding: "var(--space-5)",
textAlign: "center",
color: "var(--text-muted)",
}}
>
<div style={{ fontSize: 32, marginBottom: "var(--space-2)" }}>📦</div>
<p style={{ fontSize: 13 }}>No checkpoints saved yet</p>
<p style={{ fontSize: 12 }}>Train a model to create checkpoints</p>
</div>
) : (
<div style={{ display: "flex", flexDirection: "column", gap: "var(--space-2)" }}>
{checkpoints.map((ckpt) => (
<div
key={ckpt.id}
className="card"
style={{
padding: "var(--space-3)",
display: "flex",
justifyContent: "space-between",
alignItems: "center",
}}
>
<div>
<div style={{ fontWeight: 600, fontSize: 13 }}>{ckpt.name}</div>
<div
style={{
fontSize: 11,
color: "var(--text-muted)",
marginTop: 2,
}}
>
Epoch {ckpt.epoch} Val Loss: {ckpt.val_loss.toFixed(4)} {" "}
{ckpt.size_mb.toFixed(1)} MB
</div>
</div>
<div style={{ display: "flex", gap: "var(--space-2)" }}>
<button
onClick={() => handleExport(ckpt.id, "onnx")}
disabled={exporting === ckpt.id}
style={{
padding: "6px 12px",
background: "rgba(56, 139, 253, 0.1)",
border: "1px solid rgba(56, 139, 253, 0.3)",
borderRadius: 4,
color: "var(--accent)",
fontSize: 11,
fontWeight: 600,
cursor: exporting === ckpt.id ? "wait" : "pointer",
opacity: exporting === ckpt.id ? 0.6 : 1,
}}
>
{exporting === ckpt.id ? "Exporting..." : "ONNX"}
</button>
<button
onClick={() => handleExport(ckpt.id, "torchscript")}
disabled={exporting === ckpt.id}
style={{
padding: "6px 12px",
background: "transparent",
border: "1px solid var(--border)",
borderRadius: 4,
color: "var(--text-secondary)",
fontSize: 11,
fontWeight: 600,
cursor: exporting === ckpt.id ? "wait" : "pointer",
opacity: exporting === ckpt.id ? 0.6 : 1,
}}
>
TorchScript
</button>
</div>
</div>
))}
</div>
)}
</div>
);
};
function StatCard({
label,
value,
color,
}: {
label: string;
value: number | string;
color?: string;
}) {
return (
<div className="card-glow" style={{ padding: "var(--space-4)" }}>
<div
style={{
fontSize: 10,
textTransform: "uppercase",
letterSpacing: "0.06em",
color: "var(--text-muted)",
marginBottom: "var(--space-2)",
fontWeight: 600,
}}
>
{label}
</div>
<div
style={{
fontFamily: "var(--font-mono)",
fontSize: 28,
fontWeight: 600,
color: color || "var(--text-primary)",
letterSpacing: "-0.02em",
}}
>
{value}
</div>
</div>
);
}
export default ModelsTab;
@@ -1,767 +0,0 @@
import React, { useState, useEffect } from "react";
import { invoke } from "@tauri-apps/api/core";
interface RuVectorConfig {
// MinCut Parameters
mincut_enabled: boolean;
mincut_threshold: number;
mincut_max_persons: number;
// Attention Parameters
attention_enabled: boolean;
attention_heads: number;
attention_dropout: number;
// Temporal Parameters
temporal_enabled: boolean;
temporal_window_ms: number;
temporal_compression_ratio: number;
// Solver Parameters
solver_enabled: boolean;
solver_interpolation: "linear" | "cubic" | "sparse";
solver_subcarrier_count: number;
// BVP Parameters
bvp_enabled: boolean;
bvp_filter_hz: [number, number];
}
const DEFAULT_CONFIG: RuVectorConfig = {
mincut_enabled: true,
mincut_threshold: 0.5,
mincut_max_persons: 5,
attention_enabled: true,
attention_heads: 4,
attention_dropout: 0.1,
temporal_enabled: true,
temporal_window_ms: 500,
temporal_compression_ratio: 4,
solver_enabled: true,
solver_interpolation: "sparse",
solver_subcarrier_count: 56,
bvp_enabled: false,
bvp_filter_hz: [0.7, 4.0],
};
const MODULES = [
{
id: "mincut",
name: "MinCut Segmentation",
crate: "ruvector-mincut",
description: "Graph-based person segmentation using DynamicPersonMatcher",
icon: "✂️",
},
{
id: "attention",
name: "Spatial Attention",
crate: "ruvector-attention",
description: "Attention-weighted antenna selection and BVP extraction",
icon: "🎯",
},
{
id: "temporal",
name: "Temporal Tensor",
crate: "ruvector-temporal-tensor",
description: "Temporal CSI compression and breathing detection",
icon: "⏱️",
},
{
id: "solver",
name: "Sparse Solver",
crate: "ruvector-solver",
description: "Sparse interpolation (114→56 subcarriers) and triangulation",
icon: "🧮",
},
{
id: "attn-mincut",
name: "Attention MinCut",
crate: "ruvector-attn-mincut",
description: "Combined attention-weighted graph segmentation",
icon: "🔀",
},
];
const RuVectorTab: React.FC = () => {
const [config, setConfig] = useState<RuVectorConfig>(DEFAULT_CONFIG);
const [testingLive, setTestingLive] = useState(false);
const [liveMetrics, setLiveMetrics] = useState<{
fps: number;
latency_ms: number;
persons_detected: number;
} | null>(null);
const [saved, setSaved] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
loadConfig();
}, []);
const loadConfig = async () => {
try {
const loaded = await invoke<RuVectorConfig>("get_ruvector_config");
setConfig(loaded);
} catch (err) {
// Use defaults if not implemented
}
};
const saveConfig = async () => {
setError(null);
try {
await invoke("set_ruvector_config", { config });
setSaved(true);
} catch (err) {
setError(`Failed to save: ${err}`);
}
};
const handleChange = <K extends keyof RuVectorConfig>(
key: K,
value: RuVectorConfig[K]
) => {
setConfig((prev) => ({ ...prev, [key]: value }));
setSaved(false);
};
const startLiveTest = async () => {
setTestingLive(true);
setError(null);
try {
// Simulate live testing metrics
const interval = setInterval(() => {
setLiveMetrics({
fps: 25 + Math.random() * 10,
latency_ms: 15 + Math.random() * 10,
persons_detected: Math.floor(Math.random() * 3) + 1,
});
}, 500);
// Stop after 10 seconds for demo
setTimeout(() => {
clearInterval(interval);
setTestingLive(false);
setLiveMetrics(null);
}, 10000);
} catch (err) {
setError(`Live test failed: ${err}`);
setTestingLive(false);
}
};
const exportConfig = () => {
const blob = new Blob([JSON.stringify(config, null, 2)], {
type: "application/json",
});
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = "ruvector-config.json";
a.click();
URL.revokeObjectURL(url);
};
return (
<div>
{/* Module Cards */}
<div
style={{
display: "grid",
gridTemplateColumns: "repeat(auto-fill, minmax(200px, 1fr))",
gap: "var(--space-3)",
marginBottom: "var(--space-5)",
}}
>
{MODULES.map((mod) => {
const isEnabled =
config[`${mod.id.replace("-", "_")}_enabled` as keyof RuVectorConfig] ?? true;
return (
<div
key={mod.id}
className="card"
style={{
padding: "var(--space-3)",
opacity: isEnabled ? 1 : 0.5,
transition: "opacity 0.2s",
}}
>
<div
style={{
display: "flex",
justifyContent: "space-between",
alignItems: "start",
}}
>
<span style={{ fontSize: 24 }}>{mod.icon}</span>
<span
style={{
fontSize: 9,
padding: "2px 6px",
borderRadius: 3,
background: isEnabled
? "rgba(63, 185, 80, 0.15)"
: "rgba(139, 148, 158, 0.15)",
color: isEnabled ? "var(--status-online)" : "var(--text-muted)",
fontWeight: 600,
}}
>
{isEnabled ? "ON" : "OFF"}
</span>
</div>
<h4 style={{ margin: "var(--space-2) 0 4px", fontSize: 13, fontWeight: 600 }}>
{mod.name}
</h4>
<p
style={{
fontSize: 11,
color: "var(--text-muted)",
margin: 0,
lineHeight: 1.4,
}}
>
{mod.description}
</p>
<div
style={{
marginTop: "var(--space-2)",
fontFamily: "var(--font-mono)",
fontSize: 10,
color: "var(--text-secondary)",
}}
>
{mod.crate}
</div>
</div>
);
})}
</div>
{error && (
<div
style={{
background: "rgba(248, 81, 73, 0.1)",
border: "1px solid rgba(248, 81, 73, 0.3)",
borderRadius: 6,
padding: "var(--space-3)",
marginBottom: "var(--space-4)",
fontSize: 13,
color: "var(--status-error)",
}}
>
{error}
</div>
)}
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: "var(--space-5)" }}>
{/* Configuration Panel */}
<div className="card" style={{ padding: "var(--space-4)" }}>
<h3 style={{ margin: 0, fontSize: 14, fontWeight: 600, marginBottom: "var(--space-4)" }}>
Module Configuration
</h3>
{/* MinCut Section */}
<ConfigSection title="MinCut Segmentation">
<ToggleRow
label="Enable MinCut"
checked={config.mincut_enabled}
onChange={(v) => handleChange("mincut_enabled", v)}
/>
<SliderRow
label="Threshold"
value={config.mincut_threshold}
min={0.1}
max={1.0}
step={0.05}
onChange={(v) => handleChange("mincut_threshold", v)}
disabled={!config.mincut_enabled}
/>
<NumberRow
label="Max Persons"
value={config.mincut_max_persons}
min={1}
max={10}
onChange={(v) => handleChange("mincut_max_persons", v)}
disabled={!config.mincut_enabled}
/>
</ConfigSection>
{/* Attention Section */}
<ConfigSection title="Spatial Attention">
<ToggleRow
label="Enable Attention"
checked={config.attention_enabled}
onChange={(v) => handleChange("attention_enabled", v)}
/>
<NumberRow
label="Attention Heads"
value={config.attention_heads}
min={1}
max={16}
onChange={(v) => handleChange("attention_heads", v)}
disabled={!config.attention_enabled}
/>
<SliderRow
label="Dropout"
value={config.attention_dropout}
min={0}
max={0.5}
step={0.05}
onChange={(v) => handleChange("attention_dropout", v)}
disabled={!config.attention_enabled}
/>
</ConfigSection>
{/* Temporal Section */}
<ConfigSection title="Temporal Processing">
<ToggleRow
label="Enable Temporal"
checked={config.temporal_enabled}
onChange={(v) => handleChange("temporal_enabled", v)}
/>
<NumberRow
label="Window (ms)"
value={config.temporal_window_ms}
min={100}
max={2000}
step={100}
onChange={(v) => handleChange("temporal_window_ms", v)}
disabled={!config.temporal_enabled}
/>
<NumberRow
label="Compression Ratio"
value={config.temporal_compression_ratio}
min={1}
max={16}
onChange={(v) => handleChange("temporal_compression_ratio", v)}
disabled={!config.temporal_enabled}
/>
</ConfigSection>
{/* Solver Section */}
<ConfigSection title="Sparse Solver">
<ToggleRow
label="Enable Solver"
checked={config.solver_enabled}
onChange={(v) => handleChange("solver_enabled", v)}
/>
<div style={{ marginBottom: "var(--space-2)" }}>
<label style={labelStyle}>Interpolation</label>
<select
value={config.solver_interpolation}
onChange={(e) =>
handleChange(
"solver_interpolation",
e.target.value as RuVectorConfig["solver_interpolation"]
)
}
disabled={!config.solver_enabled}
style={{
...inputStyle,
opacity: config.solver_enabled ? 1 : 0.5,
}}
>
<option value="linear">Linear</option>
<option value="cubic">Cubic</option>
<option value="sparse">Sparse (L1)</option>
</select>
</div>
<NumberRow
label="Subcarrier Count"
value={config.solver_subcarrier_count}
min={28}
max={114}
onChange={(v) => handleChange("solver_subcarrier_count", v)}
disabled={!config.solver_enabled}
/>
</ConfigSection>
{/* Action Buttons */}
<div
style={{
display: "flex",
gap: "var(--space-2)",
marginTop: "var(--space-4)",
}}
>
<button
onClick={saveConfig}
className="btn-gradient"
style={{
flex: 1,
padding: "10px",
fontSize: 12,
opacity: saved ? 0.6 : 1,
}}
disabled={saved}
>
{saved ? "Saved" : "Save Configuration"}
</button>
<button
onClick={exportConfig}
style={{
padding: "10px 16px",
background: "transparent",
border: "1px solid var(--border)",
borderRadius: 6,
color: "var(--text-secondary)",
fontSize: 12,
fontWeight: 600,
cursor: "pointer",
}}
>
Export
</button>
</div>
</div>
{/* Live Testing Panel */}
<div className="card" style={{ padding: "var(--space-4)" }}>
<h3 style={{ margin: 0, fontSize: 14, fontWeight: 600, marginBottom: "var(--space-4)" }}>
Live Testing
</h3>
<div
style={{
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
minHeight: 200,
background: "var(--bg-secondary)",
borderRadius: 8,
marginBottom: "var(--space-4)",
}}
>
{testingLive ? (
<>
<div
style={{
fontSize: 48,
animation: "pulse 1s infinite",
}}
>
📡
</div>
<p style={{ fontSize: 13, color: "var(--text-secondary)", marginTop: "var(--space-2)" }}>
Processing live CSI stream...
</p>
</>
) : (
<>
<div style={{ fontSize: 48, opacity: 0.5 }}>📡</div>
<p style={{ fontSize: 13, color: "var(--text-muted)", marginTop: "var(--space-2)" }}>
Start live test to apply config to real CSI data
</p>
</>
)}
</div>
{liveMetrics && (
<div
style={{
display: "grid",
gridTemplateColumns: "repeat(3, 1fr)",
gap: "var(--space-3)",
marginBottom: "var(--space-4)",
}}
>
<MetricCard label="FPS" value={liveMetrics.fps.toFixed(1)} />
<MetricCard label="Latency" value={`${liveMetrics.latency_ms.toFixed(0)}ms`} />
<MetricCard label="Persons" value={liveMetrics.persons_detected.toString()} />
</div>
)}
<button
onClick={testingLive ? () => setTestingLive(false) : startLiveTest}
style={{
width: "100%",
padding: "12px",
background: testingLive
? "rgba(248, 81, 73, 0.1)"
: "rgba(56, 139, 253, 0.1)",
border: `1px solid ${testingLive ? "rgba(248, 81, 73, 0.3)" : "rgba(56, 139, 253, 0.3)"}`,
borderRadius: 6,
color: testingLive ? "var(--status-error)" : "var(--accent)",
fontSize: 13,
fontWeight: 600,
cursor: "pointer",
}}
>
{testingLive ? "Stop Test" : "Start Live Test"}
</button>
{/* Presets */}
<div style={{ marginTop: "var(--space-5)" }}>
<h4 style={{ fontSize: 12, fontWeight: 600, marginBottom: "var(--space-3)" }}>
Quick Presets
</h4>
<div style={{ display: "flex", flexDirection: "column", gap: "var(--space-2)" }}>
<PresetButton
label="Low Latency"
description="Minimal processing for real-time"
onClick={() => {
setConfig({
...DEFAULT_CONFIG,
attention_heads: 2,
temporal_compression_ratio: 8,
solver_subcarrier_count: 28,
});
setSaved(false);
}}
/>
<PresetButton
label="High Accuracy"
description="Maximum quality, higher latency"
onClick={() => {
setConfig({
...DEFAULT_CONFIG,
attention_heads: 8,
temporal_compression_ratio: 2,
solver_subcarrier_count: 114,
solver_interpolation: "cubic",
});
setSaved(false);
}}
/>
<PresetButton
label="Balanced"
description="Default recommended settings"
onClick={() => {
setConfig(DEFAULT_CONFIG);
setSaved(false);
}}
/>
</div>
</div>
</div>
</div>
<style>{`
@keyframes pulse {
0%, 100% { transform: scale(1); }
50% { transform: scale(1.1); }
}
`}</style>
</div>
);
};
// Helper Components
function ConfigSection({ title, children }: { title: string; children: React.ReactNode }) {
return (
<div style={{ marginBottom: "var(--space-4)" }}>
<h4
style={{
fontSize: 11,
fontWeight: 600,
color: "var(--text-muted)",
textTransform: "uppercase",
letterSpacing: "0.04em",
marginBottom: "var(--space-2)",
}}
>
{title}
</h4>
{children}
</div>
);
}
function ToggleRow({
label,
checked,
onChange,
}: {
label: string;
checked: boolean;
onChange: (v: boolean) => void;
}) {
return (
<div
style={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
marginBottom: "var(--space-2)",
}}
>
<span style={{ fontSize: 12 }}>{label}</span>
<button
onClick={() => onChange(!checked)}
style={{
width: 40,
height: 22,
borderRadius: 11,
border: "none",
background: checked ? "var(--accent)" : "var(--border)",
position: "relative",
cursor: "pointer",
transition: "background 0.2s",
}}
>
<span
style={{
position: "absolute",
top: 2,
left: checked ? 20 : 2,
width: 18,
height: 18,
borderRadius: "50%",
background: "white",
transition: "left 0.2s",
}}
/>
</button>
</div>
);
}
function SliderRow({
label,
value,
min,
max,
step,
onChange,
disabled,
}: {
label: string;
value: number;
min: number;
max: number;
step: number;
onChange: (v: number) => void;
disabled?: boolean;
}) {
return (
<div style={{ marginBottom: "var(--space-2)", opacity: disabled ? 0.5 : 1 }}>
<div style={{ display: "flex", justifyContent: "space-between", marginBottom: 4 }}>
<span style={{ fontSize: 12 }}>{label}</span>
<span style={{ fontSize: 11, fontFamily: "var(--font-mono)", color: "var(--text-muted)" }}>
{value.toFixed(2)}
</span>
</div>
<input
type="range"
value={value}
min={min}
max={max}
step={step}
onChange={(e) => onChange(parseFloat(e.target.value))}
disabled={disabled}
style={{ width: "100%", cursor: disabled ? "not-allowed" : "pointer" }}
/>
</div>
);
}
function NumberRow({
label,
value,
min,
max,
step = 1,
onChange,
disabled,
}: {
label: string;
value: number;
min: number;
max: number;
step?: number;
onChange: (v: number) => void;
disabled?: boolean;
}) {
return (
<div
style={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
marginBottom: "var(--space-2)",
opacity: disabled ? 0.5 : 1,
}}
>
<span style={{ fontSize: 12 }}>{label}</span>
<input
type="number"
value={value}
min={min}
max={max}
step={step}
onChange={(e) => onChange(parseInt(e.target.value) || min)}
disabled={disabled}
style={{
width: 70,
padding: "4px 8px",
background: "var(--bg-secondary)",
border: "1px solid var(--border)",
borderRadius: 4,
color: "var(--text-primary)",
fontSize: 12,
textAlign: "right",
cursor: disabled ? "not-allowed" : "text",
}}
/>
</div>
);
}
function MetricCard({ label, value }: { label: string; value: string }) {
return (
<div className="card" style={{ padding: "var(--space-3)", textAlign: "center" }}>
<div style={{ fontSize: 10, color: "var(--text-muted)", marginBottom: 2 }}>{label}</div>
<div style={{ fontFamily: "var(--font-mono)", fontSize: 18, fontWeight: 600 }}>{value}</div>
</div>
);
}
function PresetButton({
label,
description,
onClick,
}: {
label: string;
description: string;
onClick: () => void;
}) {
return (
<button
onClick={onClick}
style={{
display: "flex",
flexDirection: "column",
alignItems: "start",
padding: "var(--space-3)",
background: "var(--bg-secondary)",
border: "1px solid var(--border)",
borderRadius: 6,
cursor: "pointer",
textAlign: "left",
}}
>
<span style={{ fontSize: 12, fontWeight: 600, color: "var(--text-primary)" }}>{label}</span>
<span style={{ fontSize: 11, color: "var(--text-muted)" }}>{description}</span>
</button>
);
}
const labelStyle: React.CSSProperties = {
display: "block",
fontSize: 11,
fontWeight: 600,
color: "var(--text-muted)",
marginBottom: 4,
};
const inputStyle: React.CSSProperties = {
width: "100%",
padding: "8px 12px",
background: "var(--bg-secondary)",
border: "1px solid var(--border)",
borderRadius: 6,
color: "var(--text-primary)",
fontSize: 13,
};
export default RuVectorTab;
@@ -1,601 +0,0 @@
import React, { useState, useEffect } from "react";
import { invoke } from "@tauri-apps/api/core";
import { listen, UnlistenFn } from "@tauri-apps/api/event";
interface TrainingConfig {
dataset_id: string;
model_id: string;
epochs: number;
batch_size: number;
learning_rate: number;
optimizer: "adam" | "sgd" | "adamw";
weight_decay: number;
use_augmentation: boolean;
checkpoint_every: number;
}
interface TrainingProgress {
epoch: number;
total_epochs: number;
batch: number;
total_batches: number;
train_loss: number;
val_loss: number | null;
learning_rate: number;
eta_secs: number;
gpu_memory_mb: number | null;
}
interface TrainingJob {
id: string;
status: "running" | "paused" | "completed" | "failed";
started_at: string;
progress: TrainingProgress;
}
const DEFAULT_CONFIG: TrainingConfig = {
dataset_id: "mmfi",
model_id: "csi-encoder-cnn",
epochs: 100,
batch_size: 32,
learning_rate: 0.001,
optimizer: "adam",
weight_decay: 0.0001,
use_augmentation: true,
checkpoint_every: 10,
};
interface TrainingTabProps {
gpuAvailable: boolean;
}
const TrainingTab: React.FC<TrainingTabProps> = ({ gpuAvailable }) => {
const [config, setConfig] = useState<TrainingConfig>(DEFAULT_CONFIG);
const [currentJob, setCurrentJob] = useState<TrainingJob | null>(null);
const [lossHistory, setLossHistory] = useState<{ epoch: number; train: number; val: number }[]>(
[]
);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
let unlisten: UnlistenFn | undefined;
const setupListener = async () => {
try {
unlisten = await listen<TrainingProgress>("training:progress", (event) => {
const progress = event.payload;
setCurrentJob((prev) =>
prev
? { ...prev, progress }
: {
id: "job-1",
status: "running",
started_at: new Date().toISOString(),
progress,
}
);
if (progress.val_loss !== null && progress.batch === progress.total_batches) {
setLossHistory((prev) => [
...prev,
{
epoch: progress.epoch,
train: progress.train_loss,
val: progress.val_loss!,
},
]);
}
});
} catch (err) {
console.error("Failed to setup training listener:", err);
}
};
setupListener();
return () => {
if (unlisten) unlisten();
};
}, []);
const handleStartTraining = async () => {
setError(null);
try {
await invoke("start_training", { config });
setCurrentJob({
id: `job-${Date.now()}`,
status: "running",
started_at: new Date().toISOString(),
progress: {
epoch: 0,
total_epochs: config.epochs,
batch: 0,
total_batches: 0,
train_loss: 0,
val_loss: null,
learning_rate: config.learning_rate,
eta_secs: 0,
gpu_memory_mb: null,
},
});
} catch (err) {
setError(`Failed to start training: ${err}`);
}
};
const handleStopTraining = async () => {
try {
await invoke("stop_training");
setCurrentJob((prev) => (prev ? { ...prev, status: "paused" } : null));
} catch (err) {
setError(`Failed to stop training: ${err}`);
}
};
const formatEta = (seconds: number) => {
if (seconds < 60) return `${seconds}s`;
if (seconds < 3600) return `${Math.floor(seconds / 60)}m ${seconds % 60}s`;
const hours = Math.floor(seconds / 3600);
const mins = Math.floor((seconds % 3600) / 60);
return `${hours}h ${mins}m`;
};
const progress = currentJob?.progress;
const epochProgress = progress ? (progress.epoch / progress.total_epochs) * 100 : 0;
const batchProgress = progress && progress.total_batches > 0
? (progress.batch / progress.total_batches) * 100
: 0;
return (
<div>
{/* GPU Warning */}
{!gpuAvailable && (
<div
style={{
background: "rgba(245, 158, 11, 0.1)",
border: "1px solid rgba(245, 158, 11, 0.3)",
borderRadius: 6,
padding: "var(--space-3)",
marginBottom: "var(--space-4)",
display: "flex",
alignItems: "center",
gap: "var(--space-3)",
}}
>
<span style={{ fontSize: 18 }}></span>
<div>
<div style={{ fontWeight: 600, fontSize: 13, color: "#f59e0b" }}>
GPU Not Available
</div>
<div style={{ fontSize: 12, color: "var(--text-muted)" }}>
Training will use CPU, which is significantly slower. Consider using a
machine with CUDA or Metal support.
</div>
</div>
</div>
)}
{error && (
<div
style={{
background: "rgba(248, 81, 73, 0.1)",
border: "1px solid rgba(248, 81, 73, 0.3)",
borderRadius: 6,
padding: "var(--space-3)",
marginBottom: "var(--space-4)",
fontSize: 13,
color: "var(--status-error)",
}}
>
{error}
</div>
)}
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: "var(--space-5)" }}>
{/* Configuration Panel */}
<div className="card" style={{ padding: "var(--space-4)" }}>
<h3 style={{ margin: 0, fontSize: 14, fontWeight: 600, marginBottom: "var(--space-4)" }}>
Training Configuration
</h3>
<div style={{ display: "flex", flexDirection: "column", gap: "var(--space-3)" }}>
<div>
<label style={labelStyle}>Dataset</label>
<select
value={config.dataset_id}
onChange={(e) => setConfig({ ...config, dataset_id: e.target.value })}
style={inputStyle}
>
<option value="mmfi">MM-Fi Dataset</option>
<option value="wipose">Wi-Pose Dataset</option>
<option value="wiar">WiAR Dataset</option>
</select>
</div>
<div>
<label style={labelStyle}>Model Architecture</label>
<select
value={config.model_id}
onChange={(e) => setConfig({ ...config, model_id: e.target.value })}
style={inputStyle}
>
<option value="csi-encoder-cnn">CSI Encoder (CNN)</option>
<option value="csi-encoder-transformer">CSI Encoder (Transformer)</option>
<option value="pose-decoder-lstm">Pose Decoder (LSTM)</option>
<option value="pose-decoder-gru">Pose Decoder (GRU)</option>
</select>
</div>
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: "var(--space-3)" }}>
<div>
<label style={labelStyle}>Epochs</label>
<input
type="number"
value={config.epochs}
onChange={(e) => setConfig({ ...config, epochs: parseInt(e.target.value) || 1 })}
min={1}
max={1000}
style={inputStyle}
/>
</div>
<div>
<label style={labelStyle}>Batch Size</label>
<input
type="number"
value={config.batch_size}
onChange={(e) =>
setConfig({ ...config, batch_size: parseInt(e.target.value) || 1 })
}
min={1}
max={512}
style={inputStyle}
/>
</div>
</div>
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: "var(--space-3)" }}>
<div>
<label style={labelStyle}>Learning Rate</label>
<input
type="number"
value={config.learning_rate}
onChange={(e) =>
setConfig({ ...config, learning_rate: parseFloat(e.target.value) || 0.001 })
}
step={0.0001}
min={0.00001}
max={1}
style={inputStyle}
/>
</div>
<div>
<label style={labelStyle}>Optimizer</label>
<select
value={config.optimizer}
onChange={(e) =>
setConfig({ ...config, optimizer: e.target.value as TrainingConfig["optimizer"] })
}
style={inputStyle}
>
<option value="adam">Adam</option>
<option value="adamw">AdamW</option>
<option value="sgd">SGD</option>
</select>
</div>
</div>
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: "var(--space-3)" }}>
<div>
<label style={labelStyle}>Weight Decay</label>
<input
type="number"
value={config.weight_decay}
onChange={(e) =>
setConfig({ ...config, weight_decay: parseFloat(e.target.value) || 0 })
}
step={0.0001}
min={0}
max={1}
style={inputStyle}
/>
</div>
<div>
<label style={labelStyle}>Checkpoint Every</label>
<input
type="number"
value={config.checkpoint_every}
onChange={(e) =>
setConfig({ ...config, checkpoint_every: parseInt(e.target.value) || 1 })
}
min={1}
max={100}
style={inputStyle}
/>
</div>
</div>
<div style={{ display: "flex", alignItems: "center", gap: "var(--space-2)" }}>
<input
type="checkbox"
id="augmentation"
checked={config.use_augmentation}
onChange={(e) => setConfig({ ...config, use_augmentation: e.target.checked })}
style={{ width: 16, height: 16 }}
/>
<label htmlFor="augmentation" style={{ fontSize: 13, cursor: "pointer" }}>
Enable Data Augmentation
</label>
</div>
<div style={{ marginTop: "var(--space-3)" }}>
{currentJob?.status === "running" ? (
<button
onClick={handleStopTraining}
style={{
width: "100%",
padding: "12px",
background: "rgba(248, 81, 73, 0.1)",
border: "1px solid rgba(248, 81, 73, 0.3)",
borderRadius: 6,
color: "var(--status-error)",
fontSize: 13,
fontWeight: 600,
cursor: "pointer",
}}
>
Stop Training
</button>
) : (
<button
onClick={handleStartTraining}
className="btn-gradient"
style={{ width: "100%", padding: "12px", fontSize: 13 }}
>
Start Training
</button>
)}
</div>
</div>
</div>
{/* Progress Panel */}
<div className="card" style={{ padding: "var(--space-4)" }}>
<h3 style={{ margin: 0, fontSize: 14, fontWeight: 600, marginBottom: "var(--space-4)" }}>
Training Progress
</h3>
{!currentJob ? (
<div
style={{
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
height: 300,
color: "var(--text-muted)",
}}
>
<div style={{ fontSize: 48, marginBottom: "var(--space-3)" }}>🎯</div>
<p style={{ fontSize: 13 }}>No training job running</p>
<p style={{ fontSize: 12 }}>Configure and start training to begin</p>
</div>
) : (
<div style={{ display: "flex", flexDirection: "column", gap: "var(--space-4)" }}>
{/* Status */}
<div
style={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
}}
>
<div style={{ display: "flex", alignItems: "center", gap: "var(--space-2)" }}>
<span
style={{
width: 8,
height: 8,
borderRadius: "50%",
background:
currentJob.status === "running"
? "var(--status-online)"
: currentJob.status === "paused"
? "#f59e0b"
: "var(--status-error)",
animation: currentJob.status === "running" ? "pulse 1.5s infinite" : "none",
}}
/>
<span style={{ fontSize: 13, fontWeight: 600, textTransform: "capitalize" }}>
{currentJob.status}
</span>
</div>
<span style={{ fontSize: 12, color: "var(--text-muted)" }}>
ETA: {formatEta(progress?.eta_secs ?? 0)}
</span>
</div>
{/* Epoch Progress */}
<div>
<div
style={{
display: "flex",
justifyContent: "space-between",
fontSize: 12,
marginBottom: 4,
}}
>
<span>Epoch</span>
<span>
{progress?.epoch ?? 0} / {progress?.total_epochs ?? config.epochs}
</span>
</div>
<div
style={{
height: 6,
background: "var(--border)",
borderRadius: 3,
overflow: "hidden",
}}
>
<div
style={{
width: `${epochProgress}%`,
height: "100%",
background: "var(--accent)",
transition: "width 0.3s",
}}
/>
</div>
</div>
{/* Batch Progress */}
<div>
<div
style={{
display: "flex",
justifyContent: "space-between",
fontSize: 12,
marginBottom: 4,
}}
>
<span>Batch</span>
<span>
{progress?.batch ?? 0} / {progress?.total_batches ?? 0}
</span>
</div>
<div
style={{
height: 4,
background: "var(--border)",
borderRadius: 2,
overflow: "hidden",
}}
>
<div
style={{
width: `${batchProgress}%`,
height: "100%",
background: "rgba(56, 139, 253, 0.5)",
transition: "width 0.1s",
}}
/>
</div>
</div>
{/* Stats Grid */}
<div
style={{
display: "grid",
gridTemplateColumns: "repeat(2, 1fr)",
gap: "var(--space-3)",
}}
>
<div className="card" style={{ padding: "var(--space-3)" }}>
<div style={{ fontSize: 10, color: "var(--text-muted)", marginBottom: 4 }}>
Train Loss
</div>
<div style={{ fontFamily: "var(--font-mono)", fontSize: 20, fontWeight: 600 }}>
{progress?.train_loss.toFixed(4) ?? "—"}
</div>
</div>
<div className="card" style={{ padding: "var(--space-3)" }}>
<div style={{ fontSize: 10, color: "var(--text-muted)", marginBottom: 4 }}>
Val Loss
</div>
<div
style={{
fontFamily: "var(--font-mono)",
fontSize: 20,
fontWeight: 600,
color: "var(--status-online)",
}}
>
{progress?.val_loss?.toFixed(4) ?? "—"}
</div>
</div>
<div className="card" style={{ padding: "var(--space-3)" }}>
<div style={{ fontSize: 10, color: "var(--text-muted)", marginBottom: 4 }}>
Learning Rate
</div>
<div style={{ fontFamily: "var(--font-mono)", fontSize: 14, fontWeight: 600 }}>
{progress?.learning_rate.toExponential(2) ?? "—"}
</div>
</div>
<div className="card" style={{ padding: "var(--space-3)" }}>
<div style={{ fontSize: 10, color: "var(--text-muted)", marginBottom: 4 }}>
GPU Memory
</div>
<div style={{ fontFamily: "var(--font-mono)", fontSize: 14, fontWeight: 600 }}>
{progress?.gpu_memory_mb ? `${progress.gpu_memory_mb} MB` : "N/A"}
</div>
</div>
</div>
{/* Mini Loss Chart */}
{lossHistory.length > 0 && (
<div>
<div style={{ fontSize: 12, fontWeight: 600, marginBottom: "var(--space-2)" }}>
Loss History
</div>
<div
style={{
height: 80,
display: "flex",
alignItems: "flex-end",
gap: 2,
padding: "var(--space-2)",
background: "var(--bg-secondary)",
borderRadius: 4,
}}
>
{lossHistory.slice(-20).map((h, i) => (
<div
key={i}
style={{
flex: 1,
height: `${Math.max(5, Math.min(100, (1 - h.train) * 100))}%`,
background: "var(--accent)",
borderRadius: 2,
opacity: 0.6 + (i / 20) * 0.4,
}}
title={`Epoch ${h.epoch}: Train=${h.train.toFixed(4)}, Val=${h.val.toFixed(4)}`}
/>
))}
</div>
</div>
)}
</div>
)}
</div>
</div>
<style>{`
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
`}</style>
</div>
);
};
const labelStyle: React.CSSProperties = {
display: "block",
fontSize: 11,
fontWeight: 600,
color: "var(--text-muted)",
marginBottom: 4,
textTransform: "uppercase",
letterSpacing: "0.04em",
};
const inputStyle: React.CSSProperties = {
width: "100%",
padding: "8px 12px",
background: "var(--bg-secondary)",
border: "1px solid var(--border)",
borderRadius: 6,
color: "var(--text-primary)",
fontSize: 13,
};
export default TrainingTab;
@@ -1,165 +0,0 @@
import React, { useState, useEffect } from "react";
import { invoke } from "@tauri-apps/api/core";
import DatasetsTab from "./DatasetsTab";
import ModelsTab from "./ModelsTab";
import TrainingTab from "./TrainingTab";
import RuVectorTab from "./RuVectorTab";
import MetricsTab from "./MetricsTab";
type TrainingTabType = "datasets" | "models" | "training" | "ruvector" | "metrics";
interface GpuInfo {
available: boolean;
name: string | null;
memory_mb: number | null;
cuda_version: string | null;
metal_supported: boolean;
}
const Training: React.FC = () => {
const [activeTab, setActiveTab] = useState<TrainingTabType>("datasets");
const [gpuInfo, setGpuInfo] = useState<GpuInfo | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
detectGpu();
}, []);
const detectGpu = async () => {
try {
const info = await invoke<GpuInfo>("detect_gpu");
setGpuInfo(info);
} catch (err) {
console.error("GPU detection failed:", err);
setGpuInfo({
available: false,
name: null,
memory_mb: null,
cuda_version: null,
metal_supported: false,
});
} finally {
setLoading(false);
}
};
const tabs: { id: TrainingTabType; label: string; icon: string }[] = [
{ id: "datasets", label: "Datasets", icon: "📊" },
{ id: "models", label: "Models", icon: "🧠" },
{ id: "training", label: "Training", icon: "⚡" },
{ id: "ruvector", label: "RuVector", icon: "📡" },
{ id: "metrics", label: "Metrics", icon: "📈" },
];
return (
<div style={{ padding: "var(--space-5)", maxWidth: 1400 }}>
{/* Header */}
<div
style={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
marginBottom: "var(--space-5)",
}}
>
<div>
<h1 className="heading-lg" style={{ margin: 0 }}>
Training & Models
</h1>
<p
style={{
fontSize: 13,
color: "var(--text-secondary)",
marginTop: 4,
}}
>
Train pose estimation models and configure RuVector signal processing
</p>
</div>
{/* GPU Status */}
<div
style={{
display: "flex",
alignItems: "center",
gap: "var(--space-3)",
padding: "var(--space-3) var(--space-4)",
background: gpuInfo?.available
? "rgba(63, 185, 80, 0.1)"
: "rgba(139, 148, 158, 0.1)",
border: `1px solid ${gpuInfo?.available ? "rgba(63, 185, 80, 0.3)" : "rgba(139, 148, 158, 0.3)"}`,
borderRadius: 8,
}}
>
<span style={{ fontSize: 18 }}>{gpuInfo?.available ? "🎮" : "💻"}</span>
<div>
<div style={{ fontSize: 12, fontWeight: 600, color: "var(--text-primary)" }}>
{loading
? "Detecting GPU..."
: gpuInfo?.available
? gpuInfo.name || "GPU Available"
: "CPU Mode"}
</div>
<div style={{ fontSize: 11, color: "var(--text-muted)" }}>
{gpuInfo?.cuda_version
? `CUDA ${gpuInfo.cuda_version}`
: gpuInfo?.metal_supported
? "Metal Supported"
: "No GPU acceleration"}
{gpuInfo?.memory_mb && `${Math.round(gpuInfo.memory_mb / 1024)}GB`}
</div>
</div>
</div>
</div>
{/* Tabs */}
<div
style={{
display: "flex",
gap: "var(--space-1)",
borderBottom: "1px solid var(--border)",
marginBottom: "var(--space-5)",
}}
>
{tabs.map((tab) => (
<button
key={tab.id}
onClick={() => setActiveTab(tab.id)}
style={{
display: "flex",
alignItems: "center",
gap: 6,
padding: "12px 20px",
border: "none",
background: "transparent",
color: activeTab === tab.id ? "var(--accent)" : "var(--text-secondary)",
fontSize: 13,
fontWeight: 600,
cursor: "pointer",
borderBottom:
activeTab === tab.id
? "2px solid var(--accent)"
: "2px solid transparent",
marginBottom: -1,
transition: "color 0.15s, border-color 0.15s",
}}
>
<span>{tab.icon}</span>
{tab.label}
</button>
))}
</div>
{/* Tab Content */}
<div>
{activeTab === "datasets" && <DatasetsTab />}
{activeTab === "models" && <ModelsTab />}
{activeTab === "training" && <TrainingTab gpuAvailable={gpuInfo?.available ?? false} />}
{activeTab === "ruvector" && <RuVectorTab />}
{activeTab === "metrics" && <MetricsTab />}
</div>
</div>
);
};
export default Training;
@@ -1,2 +1,2 @@
// Application version - single source of truth
export const APP_VERSION = "0.5.0";
export const APP_VERSION = "0.4.4";