gestura_core_foundation/
interaction.rs

1//! Unified agent interaction model for gesture/tap/tilt + haptics
2//!
3//! This module defines the data model and injection points for multi-modal
4//! interaction with agents. It enables gesture, tap, tilt events and haptic
5//! responsiveness to feed agent context and influence tool selection.
6
7use serde::{Deserialize, Serialize};
8
9/// Types of gestures that can trigger agent interactions
10#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
11pub enum GestureType {
12    /// Single tap gesture
13    Tap,
14    /// Double tap gesture
15    DoubleTap,
16    /// Long press/hold gesture
17    Hold { duration_ms: u64 },
18    /// Slide gesture with direction
19    Slide { direction: SlideDirection },
20    /// Tilt gesture with angle (degrees from vertical)
21    Tilt { angle: f32 },
22    /// Rotation gesture
23    Rotate { degrees: f32 },
24    /// Shake gesture
25    Shake { intensity: f32 },
26}
27
28/// Direction for slide gestures
29#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
30pub enum SlideDirection {
31    Up,
32    Down,
33    Left,
34    Right,
35}
36
37/// Unified interaction event that can trigger agent actions
38#[derive(Debug, Clone, Serialize, Deserialize)]
39pub struct InteractionEvent {
40    /// Type of interaction
41    pub interaction_type: InteractionType,
42    /// Confidence score (0.0 - 1.0)
43    pub confidence: f32,
44    /// Timestamp in milliseconds since epoch
45    pub timestamp_ms: u64,
46    /// Source device identifier (e.g., "ring", "keyboard", "touch")
47    pub source: String,
48    /// Optional metadata for the interaction
49    pub metadata: Option<serde_json::Value>,
50}
51
52/// Types of interactions that can trigger agent actions
53#[derive(Debug, Clone, Serialize, Deserialize)]
54pub enum InteractionType {
55    /// Gesture from ring or touch device
56    Gesture(GestureType),
57    /// Voice command
58    Voice {
59        text: String,
60        language: Option<String>,
61    },
62    /// Hotkey press
63    Hotkey { key: String, modifiers: Vec<String> },
64    /// Button press on ring
65    Button {
66        button_id: u8,
67        press_type: ButtonPressType,
68    },
69}
70
71/// Button press types
72#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
73pub enum ButtonPressType {
74    Single,
75    Double,
76    Long,
77}
78
79/// Haptic feedback pattern for agent responses
80#[derive(Debug, Clone, Serialize, Deserialize)]
81pub struct HapticFeedback {
82    /// Pattern type
83    pub pattern: HapticPattern,
84    /// Intensity (0.0 - 1.0)
85    pub intensity: f32,
86    /// Duration in milliseconds
87    pub duration_ms: u32,
88    /// Repeat count (0 = single, >0 = repeat n times)
89    pub repeat_count: u8,
90    /// Delay between repeats in milliseconds
91    pub repeat_delay_ms: u32,
92}
93
94/// Predefined haptic patterns
95#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
96pub enum HapticPattern {
97    /// Quick click feedback
98    Click,
99    /// Gentle pulse
100    Pulse,
101    /// Ramping intensity
102    Ramp,
103    /// Heartbeat pattern
104    Heartbeat,
105    /// Notification alert
106    Notification,
107    /// Error/warning alert
108    Alert,
109    /// Success confirmation
110    Success,
111    /// Processing/thinking indicator
112    Processing,
113    /// Custom pattern ID
114    Custom(u8),
115}
116
117/// Extended agent context with interaction data
118#[derive(Debug, Clone, Default, Serialize, Deserialize)]
119pub struct InteractionContext {
120    /// Agent identifier
121    pub agent_id: String,
122    /// Current interaction that triggered this context
123    pub current_interaction: Option<InteractionEvent>,
124    /// Recent interaction history (for context)
125    pub recent_interactions: Vec<InteractionEvent>,
126    /// Suggested haptic feedback for response
127    pub suggested_haptic: Option<HapticFeedback>,
128    /// Tool selection hints based on interaction
129    pub tool_hints: Vec<ToolHint>,
130    /// Whether voice response is expected
131    pub expects_voice_response: bool,
132    /// Session identifier for continuity
133    pub session_id: Option<String>,
134}
135
136/// Hint for tool selection based on interaction context
137#[derive(Debug, Clone, Serialize, Deserialize)]
138pub struct ToolHint {
139    /// Tool name or category
140    pub tool: String,
141    /// Priority weight (higher = more likely to be selected)
142    pub priority: f32,
143    /// Reason for the hint
144    pub reason: String,
145}
146
147impl InteractionEvent {
148    /// Create a new gesture interaction event
149    pub fn gesture(gesture: GestureType, source: &str, confidence: f32) -> Self {
150        Self {
151            interaction_type: InteractionType::Gesture(gesture),
152            confidence,
153            timestamp_ms: chrono::Utc::now().timestamp_millis() as u64,
154            source: source.to_string(),
155            metadata: None,
156        }
157    }
158
159    /// Create a new voice interaction event
160    pub fn voice(text: &str, source: &str, confidence: f32) -> Self {
161        Self {
162            interaction_type: InteractionType::Voice {
163                text: text.to_string(),
164                language: None,
165            },
166            confidence,
167            timestamp_ms: chrono::Utc::now().timestamp_millis() as u64,
168            source: source.to_string(),
169            metadata: None,
170        }
171    }
172
173    /// Create a hotkey interaction event
174    pub fn hotkey(key: &str, modifiers: Vec<String>, source: &str) -> Self {
175        Self {
176            interaction_type: InteractionType::Hotkey {
177                key: key.to_string(),
178                modifiers,
179            },
180            confidence: 1.0,
181            timestamp_ms: chrono::Utc::now().timestamp_millis() as u64,
182            source: source.to_string(),
183            metadata: None,
184        }
185    }
186}
187
188impl HapticFeedback {
189    /// Quick click feedback
190    pub fn click() -> Self {
191        Self {
192            pattern: HapticPattern::Click,
193            intensity: 0.7,
194            duration_ms: 50,
195            repeat_count: 0,
196            repeat_delay_ms: 0,
197        }
198    }
199
200    /// Notification feedback
201    pub fn notification() -> Self {
202        Self {
203            pattern: HapticPattern::Notification,
204            intensity: 0.8,
205            duration_ms: 200,
206            repeat_count: 0,
207            repeat_delay_ms: 0,
208        }
209    }
210
211    /// Success confirmation feedback
212    pub fn success() -> Self {
213        Self {
214            pattern: HapticPattern::Success,
215            intensity: 0.6,
216            duration_ms: 150,
217            repeat_count: 1,
218            repeat_delay_ms: 100,
219        }
220    }
221
222    /// Alert/error feedback
223    pub fn alert() -> Self {
224        Self {
225            pattern: HapticPattern::Alert,
226            intensity: 1.0,
227            duration_ms: 300,
228            repeat_count: 2,
229            repeat_delay_ms: 150,
230        }
231    }
232
233    /// Processing/thinking indicator
234    pub fn processing() -> Self {
235        Self {
236            pattern: HapticPattern::Processing,
237            intensity: 0.4,
238            duration_ms: 100,
239            repeat_count: 3,
240            repeat_delay_ms: 200,
241        }
242    }
243}
244
245impl InteractionContext {
246    /// Create a new interaction context for an agent
247    pub fn new(agent_id: &str) -> Self {
248        Self {
249            agent_id: agent_id.to_string(),
250            ..Default::default()
251        }
252    }
253
254    /// Set the current interaction and derive tool hints
255    pub fn with_interaction(mut self, event: InteractionEvent) -> Self {
256        self.tool_hints = derive_tool_hints(&event);
257        self.suggested_haptic = suggest_haptic_for_interaction(&event);
258        self.expects_voice_response =
259            matches!(event.interaction_type, InteractionType::Voice { .. });
260        self.current_interaction = Some(event);
261        self
262    }
263
264    /// Add to recent interaction history
265    pub fn push_history(&mut self, event: InteractionEvent) {
266        const MAX_HISTORY: usize = 10;
267        self.recent_interactions.push(event);
268        if self.recent_interactions.len() > MAX_HISTORY {
269            self.recent_interactions.remove(0);
270        }
271    }
272}
273
274/// Derive tool hints based on interaction type
275fn derive_tool_hints(event: &InteractionEvent) -> Vec<ToolHint> {
276    let mut hints = Vec::new();
277
278    match &event.interaction_type {
279        InteractionType::Gesture(gesture) => match gesture {
280            GestureType::DoubleTap => {
281                hints.push(ToolHint {
282                    tool: "quick_action".to_string(),
283                    priority: 0.9,
284                    reason: "Double tap suggests quick action intent".to_string(),
285                });
286            }
287            GestureType::Hold { duration_ms } if *duration_ms > 1000 => {
288                hints.push(ToolHint {
289                    tool: "context_menu".to_string(),
290                    priority: 0.8,
291                    reason: "Long hold suggests context menu or detailed action".to_string(),
292                });
293            }
294            GestureType::Slide { direction } => {
295                let tool = match direction {
296                    SlideDirection::Up => "scroll_up",
297                    SlideDirection::Down => "scroll_down",
298                    SlideDirection::Left => "navigate_back",
299                    SlideDirection::Right => "navigate_forward",
300                };
301                hints.push(ToolHint {
302                    tool: tool.to_string(),
303                    priority: 0.7,
304                    reason: format!("Slide {:?} gesture", direction),
305                });
306            }
307            GestureType::Shake { intensity } if *intensity > 0.5 => {
308                hints.push(ToolHint {
309                    tool: "cancel".to_string(),
310                    priority: 0.85,
311                    reason: "Shake gesture suggests cancel/undo intent".to_string(),
312                });
313            }
314            _ => {}
315        },
316        InteractionType::Voice { text, .. } => {
317            // Voice interactions prioritize voice-friendly tools
318            hints.push(ToolHint {
319                tool: "voice_response".to_string(),
320                priority: 0.9,
321                reason: "Voice input expects voice output".to_string(),
322            });
323            if text.to_lowercase().contains("show") || text.to_lowercase().contains("display") {
324                hints.push(ToolHint {
325                    tool: "visual_display".to_string(),
326                    priority: 0.7,
327                    reason: "Voice command requests visual output".to_string(),
328                });
329            }
330        }
331        InteractionType::Hotkey { key, modifiers } => {
332            if modifiers.contains(&"Ctrl".to_string()) || modifiers.contains(&"Cmd".to_string()) {
333                hints.push(ToolHint {
334                    tool: "keyboard_shortcut".to_string(),
335                    priority: 0.8,
336                    reason: format!("Hotkey {} with modifiers", key),
337                });
338            }
339        }
340        InteractionType::Button { press_type, .. } => match press_type {
341            ButtonPressType::Long => {
342                hints.push(ToolHint {
343                    tool: "voice_input".to_string(),
344                    priority: 0.9,
345                    reason: "Long button press activates voice input".to_string(),
346                });
347            }
348            ButtonPressType::Double => {
349                hints.push(ToolHint {
350                    tool: "quick_action".to_string(),
351                    priority: 0.8,
352                    reason: "Double button press for quick action".to_string(),
353                });
354            }
355            _ => {}
356        },
357    }
358
359    hints
360}
361
362/// Suggest haptic feedback based on interaction type
363fn suggest_haptic_for_interaction(event: &InteractionEvent) -> Option<HapticFeedback> {
364    match &event.interaction_type {
365        InteractionType::Gesture(GestureType::Tap) => Some(HapticFeedback::click()),
366        InteractionType::Gesture(GestureType::DoubleTap) => Some(HapticFeedback {
367            pattern: HapticPattern::Click,
368            intensity: 0.8,
369            duration_ms: 40,
370            repeat_count: 1,
371            repeat_delay_ms: 50,
372        }),
373        InteractionType::Gesture(GestureType::Hold { .. }) => Some(HapticFeedback::notification()),
374        InteractionType::Voice { .. } => Some(HapticFeedback::processing()),
375        InteractionType::Button {
376            press_type: ButtonPressType::Long,
377            ..
378        } => Some(HapticFeedback::notification()),
379        _ => None,
380    }
381}
382
383#[cfg(test)]
384mod tests {
385    use super::*;
386
387    #[test]
388    fn test_gesture_event_creation() {
389        let event = InteractionEvent::gesture(GestureType::Tap, "ring", 0.95);
390        assert!(matches!(
391            event.interaction_type,
392            InteractionType::Gesture(GestureType::Tap)
393        ));
394        assert_eq!(event.source, "ring");
395        assert_eq!(event.confidence, 0.95);
396    }
397
398    #[test]
399    fn test_voice_event_creation() {
400        let event = InteractionEvent::voice("hello world", "microphone", 0.9);
401        if let InteractionType::Voice { text, .. } = &event.interaction_type {
402            assert_eq!(text, "hello world");
403        } else {
404            panic!("Expected Voice interaction type");
405        }
406    }
407
408    #[test]
409    fn test_interaction_context_with_hints() {
410        let event = InteractionEvent::gesture(GestureType::DoubleTap, "ring", 0.9);
411        let ctx = InteractionContext::new("test-agent").with_interaction(event);
412
413        assert!(!ctx.tool_hints.is_empty());
414        assert!(ctx.tool_hints.iter().any(|h| h.tool == "quick_action"));
415    }
416
417    #[test]
418    fn test_haptic_feedback_presets() {
419        let click = HapticFeedback::click();
420        assert_eq!(click.pattern, HapticPattern::Click);
421        assert_eq!(click.repeat_count, 0);
422
423        let alert = HapticFeedback::alert();
424        assert_eq!(alert.pattern, HapticPattern::Alert);
425        assert_eq!(alert.repeat_count, 2);
426    }
427}