Skip to content

Commit ae750d6

Browse files
committed
updates: fixes
1 parent 68821f3 commit ae750d6

File tree

3 files changed

+188
-67
lines changed

3 files changed

+188
-67
lines changed

plugins/notification/src/ext.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@ impl<R: tauri::Runtime, T: tauri::Manager<R>> NotificationPluginExt<R> for T {
114114

115115
#[tracing::instrument(skip(self))]
116116
fn start_detect_notification(&self) -> Result<(), Error> {
117+
// Get initial settings to configure meeting detector
117118
let auto_record_enabled = self.get_auto_record_enabled().unwrap_or(false);
118119
let auto_record_threshold = self.get_auto_record_threshold().unwrap_or(0.7);
119120

@@ -133,7 +134,13 @@ impl<R: tauri::Runtime, T: tauri::Manager<R>> NotificationPluginExt<R> for T {
133134
state_guard.meeting_detector.clone()
134135
};
135136

137+
// Capture app handle to access current settings inside callback
138+
let app_handle = self.app_handle().clone();
139+
136140
let cb = hypr_detect::new_callback(move |bundle_id| {
141+
// Fetch current auto-record settings each time (no stale state)
142+
let auto_record_enabled = app_handle.get_auto_record_enabled().unwrap_or(false);
143+
137144
// Process mic detection signal
138145
let signal = crate::meeting_detection::MeetingSignal::MicrophoneActive;
139146

plugins/notification/src/meeting_detection.rs

Lines changed: 167 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
1-
use chrono::{Duration, Utc};
1+
use chrono::{DateTime, Duration, Utc};
22
use hypr_db_user::Event;
3-
use std::collections::HashMap;
43
use std::sync::{Arc, Mutex};
54
use tauri_plugin_listener::ListenerPluginExt;
65
use tokio::sync::mpsc;
@@ -31,10 +30,98 @@ pub enum MeetingType {
3130
Unknown,
3231
}
3332

33+
/// Timestamped signal entry for the circular buffer
34+
#[derive(Debug, Clone)]
35+
struct TimestampedSignal {
36+
signal: MeetingSignal,
37+
timestamp: DateTime<Utc>,
38+
}
39+
40+
/// Circular buffer for storing recent meeting signals
41+
#[derive(Debug, Clone)]
42+
struct SignalBuffer {
43+
buffer: Vec<Option<TimestampedSignal>>,
44+
head: usize,
45+
size: usize,
46+
capacity: usize,
47+
}
48+
49+
impl SignalBuffer {
50+
fn new(capacity: usize) -> Self {
51+
Self {
52+
buffer: vec![None; capacity],
53+
head: 0,
54+
size: 0,
55+
capacity,
56+
}
57+
}
58+
59+
fn push(&mut self, signal: MeetingSignal) {
60+
let entry = TimestampedSignal {
61+
signal,
62+
timestamp: Utc::now(),
63+
};
64+
65+
self.buffer[self.head] = Some(entry);
66+
self.head = (self.head + 1) % self.capacity;
67+
if self.size < self.capacity {
68+
self.size += 1;
69+
}
70+
}
71+
72+
fn get_recent_signals(&self, duration: Duration) -> Vec<MeetingSignal> {
73+
let cutoff = Utc::now() - duration;
74+
let mut signals = Vec::new();
75+
76+
// Iterate through the buffer starting from the oldest entry
77+
let start_idx = if self.size == self.capacity {
78+
self.head
79+
} else {
80+
0
81+
};
82+
83+
for i in 0..self.size {
84+
let idx = (start_idx + i) % self.capacity;
85+
if let Some(ref entry) = self.buffer[idx] {
86+
if entry.timestamp >= cutoff {
87+
signals.push(entry.signal.clone());
88+
}
89+
}
90+
}
91+
92+
signals
93+
}
94+
95+
fn cleanup_old_signals(&mut self, max_age: Duration) {
96+
let cutoff = Utc::now() - max_age;
97+
let mut new_size = 0;
98+
99+
// Count valid signals (more recent than cutoff)
100+
for i in 0..self.size {
101+
let idx = if self.size == self.capacity {
102+
(self.head + i) % self.capacity
103+
} else {
104+
i
105+
};
106+
107+
if let Some(ref entry) = self.buffer[idx] {
108+
if entry.timestamp >= cutoff {
109+
new_size += 1;
110+
} else {
111+
// Clear old entries
112+
self.buffer[idx] = None;
113+
}
114+
}
115+
}
116+
117+
self.size = new_size;
118+
}
119+
}
120+
34121
/// Intelligent meeting detector that combines multiple signals
35122
#[derive(Clone)]
36123
pub struct MeetingDetector {
37-
signals: Arc<Mutex<HashMap<String, Vec<MeetingSignal>>>>,
124+
signal_buffer: Arc<Mutex<SignalBuffer>>,
38125
// Note: hypr_detect::Detector doesn't implement Clone, so we'll manage it differently
39126
signal_tx: mpsc::UnboundedSender<MeetingSignal>,
40127
signal_rx: Arc<Mutex<mpsc::UnboundedReceiver<MeetingSignal>>>,
@@ -43,18 +130,21 @@ pub struct MeetingDetector {
43130
auto_record_threshold: Arc<Mutex<f64>>,
44131
// App handle for triggering recording
45132
app_handle: Arc<Mutex<Option<tauri::AppHandle<tauri::Wry>>>>,
133+
// Event data for temporal calculations
134+
current_events: Arc<Mutex<Vec<Event>>>,
46135
}
47136

48137
impl Default for MeetingDetector {
49138
fn default() -> Self {
50139
let (signal_tx, signal_rx) = mpsc::unbounded_channel();
51140
Self {
52-
signals: Arc::new(Mutex::new(HashMap::new())),
141+
signal_buffer: Arc::new(Mutex::new(SignalBuffer::new(1000))), // Buffer for 1000 signals
53142
signal_tx,
54143
signal_rx: Arc::new(Mutex::new(signal_rx)),
55144
auto_record_enabled: Arc::new(Mutex::new(false)),
56145
auto_record_threshold: Arc::new(Mutex::new(0.7)),
57146
app_handle: Arc::new(Mutex::new(None)),
147+
current_events: Arc::new(Mutex::new(Vec::new())),
58148
}
59149
}
60150
}
@@ -86,6 +176,13 @@ impl MeetingDetector {
86176
}
87177
}
88178

179+
/// Update the current events for temporal calculations
180+
pub fn update_events(&self, events: Vec<Event>) {
181+
if let Ok(mut current_events) = self.current_events.lock() {
182+
*current_events = events;
183+
}
184+
}
185+
89186
/// Set auto-recording configuration
90187
///
91188
/// # Arguments
@@ -150,21 +247,16 @@ impl MeetingDetector {
150247

151248
/// Store signal for correlation analysis
152249
fn store_signal(&self, signal: MeetingSignal) {
153-
let mut signals = self.signals.lock().expect("Failed to acquire signals lock");
154-
let now = chrono::Utc::now();
155-
let time_key = now.timestamp().to_string();
156-
157-
// Store recent signals (last 10 minutes) for correlation
158-
let cutoff = now - Duration::minutes(10);
159-
signals.retain(|k, _| {
160-
k.parse::<i64>()
161-
.map_or(false, |ts| ts >= cutoff.timestamp())
162-
});
250+
let mut buffer = self
251+
.signal_buffer
252+
.lock()
253+
.expect("Failed to acquire signal buffer lock");
163254

164-
signals
165-
.entry(time_key)
166-
.or_insert_with(Vec::new)
167-
.push(signal);
255+
// Add the signal to the circular buffer
256+
buffer.push(signal);
257+
258+
// Cleanup old signals (older than 10 minutes)
259+
buffer.cleanup_old_signals(Duration::minutes(10));
168260
}
169261

170262
/// Calculate enhanced confidence score with signal correlation
@@ -179,48 +271,63 @@ impl MeetingDetector {
179271
let mut total_signals = vec![current_signal.clone()];
180272

181273
// Check for signal correlation patterns
182-
for signals_group in recent_signals.values() {
183-
for signal in signals_group {
184-
total_signals.push(signal.clone());
185-
186-
// Correlation bonuses for complementary signals
187-
match (current_signal, signal) {
188-
// Mic + Calendar = Strong meeting indication
189-
(MeetingSignal::MicrophoneActive, MeetingSignal::CalendarEvent(_))
190-
| (MeetingSignal::CalendarEvent(_), MeetingSignal::MicrophoneActive) => {
191-
correlation_bonus += 0.2;
192-
}
193-
// Browser + Mic = Active meeting
194-
(MeetingSignal::MicrophoneActive, MeetingSignal::BrowserMeeting(_))
195-
| (MeetingSignal::BrowserMeeting(_), MeetingSignal::MicrophoneActive) => {
196-
correlation_bonus += 0.25;
197-
}
198-
// App + Mic = Active meeting
199-
(MeetingSignal::MicrophoneActive, MeetingSignal::AppLaunched(_))
200-
| (MeetingSignal::AppLaunched(_), MeetingSignal::MicrophoneActive) => {
201-
correlation_bonus += 0.2;
202-
}
203-
// Calendar + Browser/App = Scheduled meeting starting
204-
(MeetingSignal::CalendarEvent(_), MeetingSignal::BrowserMeeting(_))
205-
| (MeetingSignal::BrowserMeeting(_), MeetingSignal::CalendarEvent(_))
206-
| (MeetingSignal::CalendarEvent(_), MeetingSignal::AppLaunched(_))
207-
| (MeetingSignal::AppLaunched(_), MeetingSignal::CalendarEvent(_)) => {
208-
correlation_bonus += 0.15;
209-
}
210-
// Multiple mic signals = sustained activity
211-
(MeetingSignal::MicrophoneActive, MeetingSignal::MicrophoneActive) => {
212-
correlation_bonus += 0.1;
213-
}
214-
_ => {}
274+
for signal in &recent_signals {
275+
total_signals.push(signal.clone());
276+
277+
// Correlation bonuses for complementary signals
278+
match (current_signal, signal) {
279+
// Mic + Calendar = Strong meeting indication
280+
(MeetingSignal::MicrophoneActive, MeetingSignal::CalendarEvent(_))
281+
| (MeetingSignal::CalendarEvent(_), MeetingSignal::MicrophoneActive) => {
282+
correlation_bonus += 0.2;
283+
}
284+
// Browser + Mic = Active meeting
285+
(MeetingSignal::MicrophoneActive, MeetingSignal::BrowserMeeting(_))
286+
| (MeetingSignal::BrowserMeeting(_), MeetingSignal::MicrophoneActive) => {
287+
correlation_bonus += 0.25;
288+
}
289+
// App + Mic = Active meeting
290+
(MeetingSignal::MicrophoneActive, MeetingSignal::AppLaunched(_))
291+
| (MeetingSignal::AppLaunched(_), MeetingSignal::MicrophoneActive) => {
292+
correlation_bonus += 0.2;
293+
}
294+
// Calendar + Browser/App = Scheduled meeting starting
295+
(MeetingSignal::CalendarEvent(_), MeetingSignal::BrowserMeeting(_))
296+
| (MeetingSignal::BrowserMeeting(_), MeetingSignal::CalendarEvent(_))
297+
| (MeetingSignal::CalendarEvent(_), MeetingSignal::AppLaunched(_))
298+
| (MeetingSignal::AppLaunched(_), MeetingSignal::CalendarEvent(_)) => {
299+
correlation_bonus += 0.15;
215300
}
301+
// Multiple mic signals = sustained activity
302+
(MeetingSignal::MicrophoneActive, MeetingSignal::MicrophoneActive) => {
303+
correlation_bonus += 0.1;
304+
}
305+
_ => {}
216306
}
217307
}
218308

219309
// Apply temporal proximity bonus for calendar events
220310
let temporal_bonus = match current_signal {
221-
MeetingSignal::CalendarEvent(_) => {
222-
// This would need calendar integration to calculate actual time proximity
223-
0.1 // Placeholder bonus
311+
MeetingSignal::CalendarEvent(event_id) => {
312+
// Calculate actual time proximity to event start
313+
if let Ok(events) = self.current_events.lock() {
314+
if let Some(event) = events.iter().find(|e| &e.id == event_id) {
315+
let now = Utc::now();
316+
let time_diff = (now - event.start_date).num_minutes().abs();
317+
318+
match time_diff {
319+
0..=2 => 0.25, // Very close to start time - highest bonus
320+
3..=5 => 0.2, // Close to start time
321+
6..=10 => 0.15, // Nearby start time
322+
11..=15 => 0.1, // Within reasonable window
323+
_ => 0.05, // Default small bonus for calendar events
324+
}
325+
} else {
326+
0.05 // Small bonus if event not found but still calendar signal
327+
}
328+
} else {
329+
0.05 // Small bonus if can't access events
330+
}
224331
}
225332
_ => 0.0,
226333
};
@@ -264,17 +371,12 @@ impl MeetingDetector {
264371
}
265372

266373
/// Get recent signals within the specified duration
267-
fn get_recent_signals(&self, duration: Duration) -> HashMap<String, Vec<MeetingSignal>> {
268-
let signals = self.signals.lock().expect("Failed to acquire signals lock");
269-
let cutoff = chrono::Utc::now() - duration;
270-
signals
271-
.iter()
272-
.filter(|(k, _)| {
273-
k.parse::<i64>()
274-
.map_or(false, |ts| ts >= cutoff.timestamp())
275-
})
276-
.map(|(k, v)| (k.clone(), v.clone()))
277-
.collect()
374+
fn get_recent_signals(&self, duration: Duration) -> Vec<MeetingSignal> {
375+
let buffer = self
376+
.signal_buffer
377+
.lock()
378+
.expect("Failed to acquire signal buffer lock");
379+
buffer.get_recent_signals(duration)
278380
}
279381

280382
/// Determine meeting type based on signal composition

plugins/notification/src/worker.rs

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,20 +76,32 @@ pub async fn perform_event_notification(_job: Job, ctx: Data<WorkerState>) -> Re
7676
}
7777

7878
// Enhanced auto-start logic with meeting detection integration
79+
let event_limit = ctx.config.event_limit.try_into().unwrap_or_else(|e| {
80+
tracing::warn!(
81+
"invalid_event_limit: value={}, error={:?}, using_default=10",
82+
ctx.config.event_limit,
83+
e
84+
);
85+
10 // Safe default fallback
86+
});
87+
7988
let all_events = ctx
8089
.db
8190
.list_events(Some(ListEventFilter {
8291
common: ListEventFilterCommon {
8392
user_id: ctx.user_id.clone(),
84-
limit: Some(ctx.config.event_limit.try_into().unwrap()),
93+
limit: Some(event_limit),
8594
},
8695
specific: ListEventFilterSpecific::DateRange {
8796
start: Utc::now() - Duration::minutes(15),
8897
end: Utc::now() + Duration::minutes(5),
8998
},
9099
}))
91100
.await
92-
.unwrap();
101+
.map_err(|e| crate::Error::Db(e).as_worker_error())?;
102+
103+
// Update meeting detector with current events for temporal calculations
104+
ctx.meeting_detector.update_events(all_events.clone());
93105

94106
// Use shared meeting detector to calculate scores for upcoming events
95107
let meeting_scores = ctx

0 commit comments

Comments
 (0)