From 45b9dcdc6e6496242931a9d5bf68cd73401d7294 Mon Sep 17 00:00:00 2001 From: Beingpax Date: Thu, 7 Aug 2025 00:03:27 +0545 Subject: [PATCH] Add feedback for prompt change and power mode change --- MiniRecorder_Morphing_Requirements.md | 140 ------------------ VoiceInk/Notifications/AppNotifications.swift | 2 + .../PowerMode/PowerModeSessionManager.swift | 4 + VoiceInk/Services/AIEnhancementService.swift | 1 + .../Views/Recorder/MiniRecorderView.swift | 14 +- .../Views/Recorder/MiniWindowManager.swift | 26 ++++ 6 files changed, 45 insertions(+), 142 deletions(-) delete mode 100644 MiniRecorder_Morphing_Requirements.md diff --git a/MiniRecorder_Morphing_Requirements.md b/MiniRecorder_Morphing_Requirements.md deleted file mode 100644 index 2fd2928..0000000 --- a/MiniRecorder_Morphing_Requirements.md +++ /dev/null @@ -1,140 +0,0 @@ -# MiniRecorder Morphing Window Requirements - -## 🎯 Core Requirements - -### **Visual Behavior** -1. **Visualizer Always Centered**: The audio visualizer must remain in the exact same position throughout all animations -2. **Fixed Window Position**: The MiniRecorderView window position should never change during morphing -3. **Horizontal-Only Expansion**: Only width should change, height remains constant at 34px -4. **Hover-Triggered**: Expansion should occur on hover, collapse on hover exit - -### **Layout States** - -#### **Compact State (Default)** -- **Width**: ~70px (just enough for visualizer + minimal padding) -- **Content**: Audio visualizer/status display only -- **Buttons**: Hidden/not rendered -- **Centering**: Visualizer perfectly centered in compact window - -#### **Expanded State (On Hover)** -- **Width**: ~160px (current full width) -- **Content**: RecorderPromptButton + Visualizer + RecorderPowerModeButton -- **Buttons**: Fully visible and functional -- **Centering**: Visualizer remains in same absolute screen position - -## 🔧 Technical Constraints - -### **Window Positioning** -- Window's center point must remain constant -- When expanding from 70px → 160px, window should grow equally left and right (45px each side) -- `NSRect` calculations must account for center-anchored growth - -### **Animation Requirements** -- Smooth spring animation (~0.3-0.4s duration) -- Buttons should appear/disappear gracefully (fade in/out or slide from edges) -- No jarring movements or position jumps -- Reversible animation (expand ↔ collapse) - -### **SwiftUI Layout Considerations** -- HStack with conditional button rendering -- Visualizer maintains `frame(maxWidth: .infinity)` behavior in both states -- Proper spacing and padding calculations for both states - -## 💡 Recommended Implementation Strategy - -### **Approach: Center-Anchored Window Growth with Sliding Buttons** - -#### **Window Management (MiniRecorderPanel)** -``` -Compact Window Rect: -- Width: 70px -- Height: 34px -- X: screenCenter - 35px -- Y: current Y position - -Expanded Window Rect: -- Width: 160px -- Height: 34px -- X: screenCenter - 80px // Grows left by 45px -- Y: same Y position // Grows right by 45px -``` - -#### **SwiftUI Layout (MiniRecorderView)** -``` -HStack(spacing: 0) { - // Left button - slides in from left edge - if isExpanded { - RecorderPromptButton() - .transition(.move(edge: .leading).combined(with: .opacity)) - } - - // Visualizer - always centered, never moves - statusView - .frame(width: visualizerWidth) // Fixed width - - // Right button - slides in from right edge - if isExpanded { - RecorderPowerModeButton() - .transition(.move(edge: .trailing).combined(with: .opacity)) - } -} -``` - -#### **State Management** -- `@State private var isExpanded = false` -- `@State private var isHovering = false` -- Hover detection with debouncing for smooth UX -- Window resize triggered by state changes - -## 🎨 Animation Sequence - -### **Expansion (Compact → Expanded)** -1. **Trigger**: Mouse enters window bounds -2. **Window**: Animate width 70px → 160px (center-anchored) -3. **Buttons**: Slide in from edges with fade-in -4. **Duration**: ~0.3s with spring easing -5. **Result**: Visualizer appears unmoved, buttons visible - -### **Collapse (Expanded → Compact)** -1. **Trigger**: Mouse leaves window bounds (with delay) -2. **Buttons**: Slide out to edges with fade-out -3. **Window**: Animate width 160px → 70px (center-anchored) -4. **Duration**: ~0.3s with spring easing -5. **Result**: Back to visualizer-only, same position - -## 🚫 Critical Don'ts - -- **Never move the visualizer's absolute screen position** -- **Never change the window's center point** -- **Never animate height or vertical position** -- **Never show jarring button pop-ins (use smooth transitions)** -- **Never let buttons overlap the visualizer during animation** - -## 📐 Calculations - -### **Visualizer Dimensions** -- AudioVisualizer: 12 bars × 3px + 11 × 2px spacing = 58px width -- With padding: ~70px total compact width - -### **Button Dimensions** -- Each button: ~24px width + padding -- Total button space: ~90px (45px per side) -- Total expanded width: 70px + 90px = 160px - -### **Center-Anchored Growth** -``` -Compact X position: screenCenterX - 35px -Expanded X position: screenCenterX - 80px -Growth: 45px left + 45px right = 90px total -``` - -## 🎯 Success Criteria - -✅ **Visualizer never visually moves during any animation** -✅ **Window position anchor point remains constant** -✅ **Smooth hover-based expansion/collapse** -✅ **Buttons appear/disappear gracefully** -✅ **No layout jumps or glitches** -✅ **Maintains current functionality in expanded state** - -This approach ensures the visualizer appears completely stationary while the window "grows around it" to reveal the buttons, creating a seamless morphing effect. \ No newline at end of file diff --git a/VoiceInk/Notifications/AppNotifications.swift b/VoiceInk/Notifications/AppNotifications.swift index ed4cc24..cebd67b 100644 --- a/VoiceInk/Notifications/AppNotifications.swift +++ b/VoiceInk/Notifications/AppNotifications.swift @@ -9,4 +9,6 @@ extension Notification.Name { static let aiProviderKeyChanged = Notification.Name("aiProviderKeyChanged") static let licenseStatusChanged = Notification.Name("licenseStatusChanged") static let navigateToDestination = Notification.Name("navigateToDestination") + static let promptSelectionChanged = Notification.Name("promptSelectionChanged") + static let powerModeConfigurationApplied = Notification.Name("powerModeConfigurationApplied") } diff --git a/VoiceInk/PowerMode/PowerModeSessionManager.swift b/VoiceInk/PowerMode/PowerModeSessionManager.swift index 90aedc5..d326538 100644 --- a/VoiceInk/PowerMode/PowerModeSessionManager.swift +++ b/VoiceInk/PowerMode/PowerModeSessionManager.swift @@ -130,6 +130,10 @@ class PowerModeSessionManager { whisperState.currentTranscriptionModel?.name != modelName { await handleModelChange(to: selectedModel) } + + await MainActor.run { + NotificationCenter.default.post(name: .powerModeConfigurationApplied, object: nil) + } } private func restoreState(_ state: ApplicationState) async { diff --git a/VoiceInk/Services/AIEnhancementService.swift b/VoiceInk/Services/AIEnhancementService.swift index b9b601a..90269f1 100644 --- a/VoiceInk/Services/AIEnhancementService.swift +++ b/VoiceInk/Services/AIEnhancementService.swift @@ -46,6 +46,7 @@ class AIEnhancementService: ObservableObject { didSet { UserDefaults.standard.set(selectedPromptId?.uuidString, forKey: "selectedPromptId") NotificationCenter.default.post(name: .AppSettingsDidChange, object: nil) + NotificationCenter.default.post(name: .promptSelectionChanged, object: nil) } } diff --git a/VoiceInk/Views/Recorder/MiniRecorderView.swift b/VoiceInk/Views/Recorder/MiniRecorderView.swift index 6e72f64..17a8d24 100644 --- a/VoiceInk/Views/Recorder/MiniRecorderView.swift +++ b/VoiceInk/Views/Recorder/MiniRecorderView.swift @@ -50,28 +50,38 @@ struct MiniRecorderView: View { Group { if windowManager.isExpanded { RecorderPromptButton(showPopover: $showEnhancementPromptPopover) - .padding(.leading, 3) .transition(.scale(scale: 0.5).combined(with: .opacity)) } } .frame(width: windowManager.isExpanded ? nil : 0) + .frame(maxWidth: windowManager.isExpanded ? .infinity : 0) .clipped() + .opacity(windowManager.isExpanded ? 1 : 0) .animation(.easeInOut(duration: 0.25), value: windowManager.isExpanded) + if windowManager.isExpanded { + Spacer() + } + // Fixed visualizer zone statusView .frame(maxWidth: .infinity) + if windowManager.isExpanded { + Spacer() + } + // Right button zone Group { if windowManager.isExpanded { RecorderPowerModeButton(showPopover: $showPowerModePopover) - .padding(.trailing, 3) .transition(.scale(scale: 0.5).combined(with: .opacity)) } } .frame(width: windowManager.isExpanded ? nil : 0) + .frame(maxWidth: windowManager.isExpanded ? .infinity : 0) .clipped() + .opacity(windowManager.isExpanded ? 1 : 0) .animation(.easeInOut(duration: 0.25), value: windowManager.isExpanded) } .padding(.vertical, 8) diff --git a/VoiceInk/Views/Recorder/MiniWindowManager.swift b/VoiceInk/Views/Recorder/MiniWindowManager.swift index 3736a21..fc18e8a 100644 --- a/VoiceInk/Views/Recorder/MiniWindowManager.swift +++ b/VoiceInk/Views/Recorder/MiniWindowManager.swift @@ -26,11 +26,37 @@ class MiniWindowManager: ObservableObject { name: NSNotification.Name("HideMiniRecorder"), object: nil ) + + NotificationCenter.default.addObserver( + self, + selector: #selector(handleFeedbackNotification), + name: .promptSelectionChanged, + object: nil + ) + + NotificationCenter.default.addObserver( + self, + selector: #selector(handleFeedbackNotification), + name: .powerModeConfigurationApplied, + object: nil + ) } @objc private func handleHideNotification() { hide() } + + @objc private func handleFeedbackNotification() { + guard isVisible, !isExpanded else { return } + + expand() + + DispatchQueue.main.asyncAfter(deadline: .now() + 2.5) { + if self.isExpanded { + self.collapse() + } + } + } func show() { if isVisible { return }