use bincode::{Decode, Encode};
use serde::{Deserialize, Serialize};
use std::collections::hash_map::DefaultHasher;
use std::collections::HashMap;
use std::hash::{Hash, Hasher};
use std::time::{SystemTime, UNIX_EPOCH};
use strsim::jaro_winkler;
pub type FilteredWidgetDescription = HashMap<String, serde_json::Value>;
#[derive(Debug, Clone, Encode, Decode, Serialize, Deserialize)]
pub struct Widget {
pub label: Option<String>,
pub minimum: Option<f64>,
pub maximum: Option<f64>,
pub is_generated: Option<bool>,
pub display_type: Option<String>,
pub current_value: Option<f64>,
pub event_id: Option<u64>,
pub values: Vec<f64>,
}
impl Widget {
pub fn simplified(label: Option<String>, event_id: Option<u64>, values: Vec<f64>) -> Self {
let current_value = if !values.is_empty() {
Some(values[0])
} else {
None
};
Self {
label,
event_id,
values: values.clone(),
minimum: None,
maximum: None,
is_generated: None,
display_type: None,
current_value,
}
}
pub fn get_values(&self) -> Vec<f64> {
let mut result = self.values.clone();
if let Some(current) = self.current_value {
if !result.contains(¤t) {
result.push(current);
}
}
result
}
}
#[derive(Debug, Clone, Encode, Decode, Serialize, Deserialize)]
pub struct WidgetValue {
pub widget_id: String,
pub label: Option<String>,
pub value: f64,
pub confidence: f64,
}
#[derive(Debug, Clone, Encode, Decode, Serialize, Deserialize)]
pub struct Preset {
pub name: String,
pub description: Option<String>,
pub widget_values: Vec<WidgetValue>,
pub created_by: Option<String>,
pub usage_count: u32,
pub last_used: u64,
}
#[derive(Debug, Clone, Encode, Decode, Serialize, Deserialize)]
pub struct WidgetFeatures {
pub label_tokens: Vec<String>,
pub min_value: f64,
pub max_value: f64,
pub range: f64,
pub is_generated: f64,
pub display_type_hash: u64,
pub value_patterns: Vec<f64>,
pub normalized_position: f64,
}
impl Default for WidgetFeatures {
fn default() -> Self {
Self {
label_tokens: Vec::new(),
min_value: 0.0,
max_value: 100.0,
range: 100.0,
is_generated: 0.0,
display_type_hash: 0,
value_patterns: Vec::new(),
normalized_position: 0.0,
}
}
}
#[derive(Debug, Clone, Encode, Decode, Serialize, Deserialize)]
pub struct ValueStats {
pub common_values: Vec<f64>,
pub frequency_map: HashMap<String, u32>,
pub mean: f64,
pub std_dev: f64,
pub percentiles: Vec<f64>,
}
#[derive(Debug, Clone, Encode, Decode, Serialize, Deserialize)]
pub struct WidgetRecord {
pub id: u64,
pub widget: Widget,
pub features: WidgetFeatures,
pub frequency: u32,
pub last_seen: u64,
pub value_stats: Option<ValueStats>,
}
impl From<FilteredWidgetDescription> for WidgetRecord {
fn from(filtered: FilteredWidgetDescription) -> Self {
fn extract_string(map: &HashMap<String, serde_json::Value>, key: &str) -> Option<String> {
map.get(key).and_then(|v| v.as_str().map(|s| s.to_string()))
}
fn extract_f64(map: &HashMap<String, serde_json::Value>, key: &str) -> Option<f64> {
map.get(key).and_then(|v| v.as_f64())
}
fn extract_bool(map: &HashMap<String, serde_json::Value>, key: &str) -> Option<bool> {
map.get(key).and_then(|v| v.as_bool())
}
fn extract_u64(map: &HashMap<String, serde_json::Value>, key: &str) -> Option<u64> {
map.get(key).and_then(|v| v.as_u64())
}
let current_value = extract_f64(&filtered, "current_value");
let event_id = extract_u64(&filtered, "concreteEventID");
let widget = Widget {
label: extract_string(&filtered, "label"),
minimum: extract_f64(&filtered, "minimum"),
maximum: extract_f64(&filtered, "maximum"),
current_value,
is_generated: extract_bool(&filtered, "isGenerated"),
display_type: extract_string(&filtered, "displayType"),
event_id,
values: if let Some(val) = current_value { vec![val] } else { Vec::new() },
};
let label_tokens = if let Some(ref label) = widget.label {
label
.to_lowercase()
.split_whitespace()
.map(|s| s.chars().filter(|c| c.is_alphanumeric()).collect())
.filter(|s: &String| !s.is_empty())
.collect()
} else {
Vec::new()
};
let min_value = widget.minimum.unwrap_or(0.0);
let max_value = widget.maximum.unwrap_or(1.0);
let range = max_value - min_value;
let display_type_hash = if let Some(ref display_type) = widget.display_type {
let mut hasher = std::collections::hash_map::DefaultHasher::new();
std::hash::Hash::hash(display_type, &mut hasher);
std::hash::Hasher::finish(&hasher)
} else {
0
};
let features = WidgetFeatures {
label_tokens,
min_value,
max_value,
range,
is_generated: if widget.is_generated.unwrap_or(false) {
1.0
} else {
0.0
},
display_type_hash,
value_patterns: if let Some(current) = widget.current_value {
vec![current]
} else {
Vec::new()
},
normalized_position: widget.current_value.unwrap_or(0.5)
};
let current_time = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_else(|_| std::time::Duration::from_secs(0))
.as_secs();
let id = extract_u64(&filtered, "concreteEventID").unwrap_or(0);
WidgetRecord {
id,
widget,
features,
frequency: 1,
last_seen: current_time,
value_stats: None,
}
}
}
#[derive(Debug, Clone, Encode, Decode, Serialize, Deserialize)]
pub struct Suggestion {
pub widget: Widget,
pub confidence: f64,
pub reason: String,
pub suggested_value: Option<f64>,
pub value_confidence: f64,
pub alternative_values: Vec<f64>,
}
pub struct WidgetSuggestionEngine {
pub records: Vec<WidgetRecord>,
pub presets: Vec<Preset>,
pub display_types: HashMap<String, u64>,
pub next_id: u64,
}
impl WidgetSuggestionEngine {
pub fn new() -> Self {
Self {
records: Vec::new(),
presets: Vec::new(),
display_types: HashMap::new(),
next_id: 1,
}
}
pub fn store_widget(&mut self, widget: Widget) {
let current_time = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs();
let features = self.extract_features(&widget);
if let Some(event_id) = widget.event_id {
for i in 0..self.records.len() {
if self.records[i].widget.event_id == Some(event_id) {
self.records[i].frequency += 1;
self.records[i].last_seen = current_time;
if widget.label.is_some() && self.records[i].widget.label.is_none() {
self.records[i].widget.label = widget.label.clone();
}
for &value in &widget.values {
if !self.records[i].widget.values.contains(&value) {
self.records[i].widget.values.push(value);
self.records[i].features.value_patterns.push(value);
}
}
return;
}
}
}
if let Some(label) = &widget.label {
for i in 0..self.records.len() {
if let Some(record_label) = &self.records[i].widget.label {
if record_label == label {
self.records[i].frequency += 1;
self.records[i].last_seen = current_time;
if widget.event_id.is_some() && self.records[i].widget.event_id.is_none() {
self.records[i].widget.event_id = widget.event_id;
}
for &value in &widget.values {
if !self.records[i].widget.values.contains(&value) {
self.records[i].widget.values.push(value);
self.records[i].features.value_patterns.push(value);
}
}
return;
}
}
}
}
let mut found_similar = false;
for i in 0..self.records.len() {
let similarity = self.calculate_similarity(&features, &self.records[i].features);
if similarity > 0.85 {
self.records[i].frequency += 1;
self.records[i].last_seen = current_time;
if widget.label.is_some() && self.records[i].widget.label.is_none() {
self.records[i].widget.label = widget.label.clone();
}
if widget.event_id.is_some() && self.records[i].widget.event_id.is_none() {
self.records[i].widget.event_id = widget.event_id;
}
for &value in &widget.values {
if !self.records[i].widget.values.contains(&value) {
self.records[i].widget.values.push(value);
self.records[i].features.value_patterns.push(value);
}
}
found_similar = true;
break;
}
}
if !found_similar {
let record = WidgetRecord {
id: self.next_id,
widget,
features,
frequency: 1,
last_seen: current_time,
value_stats: None,
};
self.records.push(record);
self.next_id += 1;
}
}
pub fn store_preset(&mut self, preset: Preset) {
if let Some(existing) = self.presets.iter_mut().find(|p| p.name == preset.name) {
existing.usage_count += 1;
existing.last_used = preset.last_used;
existing.widget_values = preset.widget_values;
existing.description = preset.description;
} else {
self.presets.push(preset);
}
}
pub fn get_suggestions(
&self,
partial_widget: &Widget,
max_suggestions: usize,
) -> Vec<Suggestion> {
if let Some(event_id) = partial_widget.event_id {
return self.get_suggestions_by_event_id(event_id, max_suggestions);
}
let features = self.extract_features_partial(partial_widget);
let mut suggestions = Vec::new();
if let Some(label) = &partial_widget.label {
for record in &self.records {
if let Some(record_label) = &record.widget.label {
if record_label == label {
let (suggested_value, value_confidence, alternative_values) =
self.suggest_values_from_vector(&record.widget);
let reason = format!(
"Exact label match for '{}' (frequency: {})",
label,
record.frequency
);
suggestions.push(Suggestion {
widget: record.widget.clone(),
confidence: 1.0, reason,
suggested_value,
value_confidence,
alternative_values,
});
}
}
}
}
if suggestions.len() < max_suggestions {
for record in &self.records {
if suggestions.iter().any(|s| s.widget.label == record.widget.label) {
continue;
}
let similarity = self.calculate_similarity(&features, &record.features);
if similarity > 0.3 {
let (suggested_value, value_confidence, alternative_values) =
self.suggest_values_from_vector(&record.widget);
let reason = format!(
"Similar to {} (similarity: {:.2}, frequency: {})",
record.widget.label.as_deref().unwrap_or("unnamed widget"),
similarity,
record.frequency
);
suggestions.push(Suggestion {
widget: record.widget.clone(),
confidence: similarity,
reason,
suggested_value,
value_confidence,
alternative_values,
});
}
}
}
suggestions.sort_by(|a, b| b.confidence.partial_cmp(&a.confidence).unwrap());
suggestions.truncate(max_suggestions);
suggestions
}
pub fn get_suggestions_by_event_id(
&self,
event_id: u64,
max_suggestions: usize,
) -> Vec<Suggestion> {
let matching_records: Vec<&WidgetRecord> = self.records.iter()
.filter(|r| r.widget.event_id == Some(event_id) || r.id == event_id)
.collect();
if matching_records.is_empty() {
if let Some(first_record) = self.records.first() {
return self.get_suggestions(&first_record.widget, max_suggestions);
} else {
return Vec::new();
}
}
let mut suggestions = Vec::new();
for &record in &matching_records {
let (suggested_value, value_confidence, alternative_values) =
self.suggest_values_from_vector(&record.widget);
let reason = format!(
"Exact match for event ID {} ({})",
event_id,
record.widget.label.as_deref().unwrap_or("unnamed widget")
);
suggestions.push(Suggestion {
widget: record.widget.clone(),
confidence: 1.0, reason,
suggested_value,
value_confidence,
alternative_values,
});
}
if suggestions.len() < max_suggestions {
if let Some(&template) = matching_records.first() {
let features = &template.features;
for record in &self.records {
if record.widget.event_id == Some(event_id) || record.id == event_id {
continue;
}
let similarity = self.calculate_similarity(features, &record.features);
if similarity > 0.5 { let (suggested_value, value_confidence, alternative_values) =
self.suggest_values_from_vector(&record.widget);
let reason = format!(
"Similar to event ID {} ({}) with similarity {:.2}",
event_id,
template.widget.label.as_deref().unwrap_or("unnamed widget"),
similarity
);
suggestions.push(Suggestion {
widget: record.widget.clone(),
confidence: similarity,
reason,
suggested_value,
value_confidence,
alternative_values,
});
}
}
}
}
suggestions.sort_by(|a, b| b.confidence.partial_cmp(&a.confidence).unwrap());
suggestions.truncate(max_suggestions);
suggestions
}
fn suggest_values_from_vector(&self, widget: &Widget) -> (Option<f64>, f64, Vec<f64>) {
let values = widget.get_values();
if values.is_empty() {
return (None, 0.3, vec![0.5, 0.3, 0.7]); }
let confidence = match values.len() {
0 => 0.3,
1..=2 => 0.5,
3..=5 => 0.7,
_ => 0.9,
};
let mut value_counts: HashMap<String, u32> = HashMap::new();
for &val in &values {
let key = format!("{:.4}", val);
*value_counts.entry(key).or_insert(0) += 1;
}
let mut most_common_value = values[0];
let mut max_count = 1;
for (val_str, count) in value_counts.iter() {
if *count > max_count {
if let Ok(val) = val_str.parse::<f64>() {
most_common_value = val;
max_count = *count;
}
}
}
let mut unique_values = values.clone();
unique_values.sort_by(|a, b| a.partial_cmp(b).unwrap());
unique_values.dedup();
(Some(most_common_value), confidence, unique_values)
}
pub fn get_preset_insights(&self, widget: &Widget) -> Option<String> {
for preset in &self.presets {
for widget_value in &preset.widget_values {
if let Some(label) = &widget.label {
if let Some(preset_label) = &widget_value.label {
if jaro_winkler(label, preset_label) > 0.8 {
return Some(format!(
"This widget is often set to {} in the '{}' preset",
widget_value.value, preset.name
));
}
}
}
}
}
None
}
pub fn get_stats(&self) -> HashMap<String, usize> {
let mut stats = HashMap::new();
stats.insert("total_widgets".to_string(), self.records.len());
stats.insert("total_presets".to_string(), self.presets.len());
stats.insert("display_types".to_string(), self.display_types.len());
stats
}
fn extract_features(&mut self, widget: &Widget) -> WidgetFeatures {
let label_tokens = if let Some(label) = &widget.label {
self.tokenize_label(label)
} else {
Vec::new()
};
let min_value = widget.minimum.unwrap_or(0.0);
let max_value = widget.maximum.unwrap_or(100.0);
let range = max_value - min_value;
let display_type_hash = if let Some(display_type) = &widget.display_type {
let mut hasher = DefaultHasher::new();
display_type.hash(&mut hasher);
let hash = hasher.finish();
self.display_types.insert(display_type.clone(), hash);
hash
} else {
0
};
let is_generated = if widget.is_generated.unwrap_or(false) {
1.0
} else {
0.0
};
let mut value_patterns = self.extract_value_patterns(&label_tokens, &widget.display_type);
if let Some(current) = widget.current_value {
value_patterns.push(current);
}
let normalized_position = widget.current_value.unwrap_or(0.5);
WidgetFeatures {
label_tokens,
min_value,
max_value,
range,
is_generated,
display_type_hash,
value_patterns,
normalized_position,
}
}
fn extract_features_partial(&self, widget: &Widget) -> WidgetFeatures {
let label_tokens = if let Some(label) = &widget.label {
self.tokenize_label(label)
} else {
Vec::new()
};
let min_value = widget.minimum.unwrap_or(0.0);
let max_value = widget.maximum.unwrap_or(100.0);
let range = max_value - min_value;
let display_type_hash = if let Some(display_type) = &widget.display_type {
let mut hasher = DefaultHasher::new();
display_type.hash(&mut hasher);
hasher.finish()
} else {
0
};
let is_generated = if widget.is_generated.unwrap_or(false) {
1.0
} else {
0.0
};
let mut value_patterns = self.extract_value_patterns(&label_tokens, &widget.display_type);
if let Some(current) = widget.current_value {
value_patterns.push(current);
}
let normalized_position = widget.current_value.unwrap_or(0.5);
WidgetFeatures {
label_tokens,
min_value,
max_value,
range,
is_generated,
display_type_hash,
value_patterns,
normalized_position,
}
}
fn tokenize_label(&self, label: &str) -> Vec<String> {
label
.to_lowercase()
.split_whitespace()
.filter(|word| !word.is_empty())
.map(|word| word.to_string())
.collect()
}
fn calculate_similarity(&self, features1: &WidgetFeatures, features2: &WidgetFeatures) -> f64 {
let label_similarity =
self.calculate_label_similarity(&features1.label_tokens, &features2.label_tokens);
let range_similarity = self.calculate_range_similarity(features1, features2);
let display_type_similarity = if features1.display_type_hash == features2.display_type_hash
&& features1.display_type_hash != 0
{
1.0
} else {
0.0
};
let generated_similarity = 1.0 - (features1.is_generated - features2.is_generated).abs();
let similarity = (label_similarity * 0.4)
+ (range_similarity * 0.3)
+ (display_type_similarity * 0.2)
+ (generated_similarity * 0.1);
similarity.clamp(0.0, 1.0)
}
fn calculate_label_similarity(&self, tokens1: &[String], tokens2: &[String]) -> f64 {
if tokens1.is_empty() || tokens2.is_empty() {
return if tokens1.is_empty() && tokens2.is_empty() {
1.0
} else {
0.0
};
}
let mut total_similarity = 0.0;
let mut matches = 0;
for token1 in tokens1 {
let mut best_match = 0.0;
for token2 in tokens2 {
let similarity = jaro_winkler(token1, token2);
if similarity > best_match {
best_match = similarity;
}
}
if best_match > 0.7 {
total_similarity += best_match;
matches += 1;
}
}
if matches > 0 {
total_similarity / matches as f64
} else {
0.0
}
}
fn calculate_range_similarity(
&self,
features1: &WidgetFeatures,
features2: &WidgetFeatures,
) -> f64 {
let min_diff = (features1.min_value - features2.min_value).abs();
let max_diff = (features1.max_value - features2.max_value).abs();
let range_diff = (features1.range - features2.range).abs();
let max_range = features1.range.max(features2.range);
if max_range == 0.0 {
return 1.0;
}
let normalized_diff = (min_diff + max_diff + range_diff) / (3.0 * max_range);
1.0 - normalized_diff.min(1.0)
}
fn extract_value_patterns(
&self,
label_tokens: &[String],
_display_type: &Option<String>,
) -> Vec<f64> {
let mut patterns = Vec::new();
for token in label_tokens {
match token.as_str() {
"volume" | "level" | "gain" => patterns.push(0.75),
"bass" | "low" => patterns.push(0.6),
"treble" | "high" => patterns.push(0.7),
"mid" | "middle" => patterns.push(0.5),
"pan" => patterns.push(0.5),
"reverb" | "delay" => patterns.push(0.3),
_ => {}
}
}
if patterns.is_empty() {
patterns.push(0.5); }
patterns
}
}
impl Default for WidgetSuggestionEngine {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod conversion_tests {
use super::*;
#[test]
fn test_filtered_widget_description_conversion() {
let mut filtered = FilteredWidgetDescription::new();
filtered.insert(
"concreteEventID".to_string(),
serde_json::Value::Number(serde_json::Number::from(42)),
);
filtered.insert(
"label".to_string(),
serde_json::Value::String("Master Volume".to_string()),
);
filtered.insert(
"minimum".to_string(),
serde_json::Value::Number(serde_json::Number::from_f64(0.0).unwrap()),
);
filtered.insert(
"maximum".to_string(),
serde_json::Value::Number(serde_json::Number::from_f64(127.0).unwrap()),
);
filtered.insert(
"current_value".to_string(),
serde_json::Value::Number(serde_json::Number::from_f64(0.75).unwrap()),
);
filtered.insert(
"displayType".to_string(),
serde_json::Value::String("slider".to_string()),
);
filtered.insert("isGenerated".to_string(), serde_json::Value::Bool(false));
let widget_record: WidgetRecord = filtered.into();
assert_eq!(widget_record.id, 42);
assert_eq!(
widget_record.widget.label,
Some("Master Volume".to_string())
);
assert_eq!(widget_record.widget.minimum, Some(0.0));
assert_eq!(widget_record.widget.maximum, Some(127.0));
assert_eq!(
widget_record.widget.display_type,
Some("slider".to_string())
);
assert_eq!(widget_record.widget.is_generated, Some(false));
assert_eq!(widget_record.frequency, 1);
assert_eq!(
widget_record.features.label_tokens,
vec!["master", "volume"]
);
assert_eq!(widget_record.features.min_value, 0.0);
assert_eq!(widget_record.features.max_value, 127.0);
assert_eq!(widget_record.features.range, 127.0);
assert_eq!(widget_record.features.is_generated, 0.0);
assert!(widget_record.features.display_type_hash != 0);
assert_eq!(widget_record.widget.current_value, Some(0.75));
assert!(widget_record.features.value_patterns.contains(&0.75));
}
#[test]
fn test_conversion_with_missing_fields() {
let mut filtered = FilteredWidgetDescription::new();
filtered.insert(
"concreteEventID".to_string(),
serde_json::Value::Number(serde_json::Number::from(100)),
);
filtered.insert(
"label".to_string(),
serde_json::Value::String("Test Control".to_string()),
);
let widget_record: WidgetRecord = filtered.into();
assert_eq!(widget_record.id, 100);
assert_eq!(widget_record.widget.label, Some("Test Control".to_string()));
assert_eq!(widget_record.widget.minimum, None);
assert_eq!(widget_record.widget.maximum, None);
assert_eq!(widget_record.widget.display_type, None);
assert_eq!(widget_record.widget.is_generated, None);
assert_eq!(widget_record.features.min_value, 0.0); assert_eq!(widget_record.features.max_value, 1.0); assert_eq!(widget_record.features.range, 1.0);
assert_eq!(widget_record.features.display_type_hash, 0);
}
#[test]
fn test_normalized_value_accumulation() {
let mut engine = WidgetSuggestionEngine::new();
let widget1 = Widget {
label: Some("Volume".to_string()),
minimum: Some(0.0),
maximum: Some(100.0),
current_value: Some(0.7), is_generated: Some(false),
display_type: Some("slider".to_string()),
event_id: None,
values: vec![0.7],
};
engine.store_widget(widget1.clone());
let mut widget2 = widget1.clone();
widget2.current_value = Some(0.8);
engine.store_widget(widget2);
let mut widget3 = widget1.clone();
widget3.current_value = Some(0.75);
engine.store_widget(widget3);
assert_eq!(engine.records.len(), 1); assert_eq!(engine.records[0].frequency, 3);
let patterns = &engine.records[0].features.value_patterns;
assert!(patterns.contains(&0.7));
assert!(patterns.contains(&0.8));
assert!(patterns.contains(&0.75));
assert!(patterns.len() >= 3);
}
}