@@ -10,17 +10,9 @@ import SwiftUI
1010
1111/// Voice-enabled Menu Toggle Button
1212///
13- /// Integrates menu toggle functionality with voice control, providing
14- /// dual interaction modes and real-time visual feedback.
15- ///
1613/// **Interaction Modes:**
17- /// - Tap → Toggle menu + activate voice control (test mode)
18- /// - Hover → Voice control only (hands-free)
19- ///
20- /// **Visual Feedback:**
21- /// - Button opacity changes based on menu and voice state
22- /// - Real-time STT transcription display
23- /// - Color-coded success/error messages
14+ /// - Tap → Toggle menu + activate voice control
15+ /// - Hover → Activate voice control (hands-free mode)
2416struct VoiceControlMenuButton : View {
2517
2618 // MARK: - Constants
@@ -34,24 +26,15 @@ struct VoiceControlMenuButton: View {
3426
3527 @Environment ( ImmersiveViewModel . self) private var immersiveViewModel
3628
37- // MARK: - State
38-
39- @State private var voiceControlVM : VoiceControlViewModel
40-
4129 // MARK: - Properties
4230
43- /// Action to perform when button is tapped (menu toggle)
31+ @ Bindable var viewModel : VoiceControlViewModel
4432 let action : ( ) -> Void
4533
4634 // MARK: - Initialization
4735
48- /// Initialize with shared VoiceControlViewModel
49- ///
50- /// - Parameters:
51- /// - viewModel: VoiceControlViewModel instance (shared with overlay)
52- /// - action: Menu toggle action
5336 init ( viewModel: VoiceControlViewModel , action: @escaping ( ) -> Void ) {
54- _voiceControlVM = State ( initialValue : viewModel)
37+ self . viewModel = viewModel
5538 self . action = action
5639 }
5740
@@ -61,22 +44,26 @@ struct VoiceControlMenuButton: View {
6144 VStack ( spacing: 12 ) {
6245 buttonImage
6346
64- // Real -time STT transcription
65- if let partialText = voiceControlVM . uiState. partialTranscription, !partialText. isEmpty {
47+ // ㅁReal -time STT transcription
48+ if let partialText = viewModel . uiState. partialTranscription, !partialText. isEmpty {
6649 transcriptionText ( partialText)
6750 }
6851
6952 // Command result feedback
70- if shouldShowFeedback, let feedback = voiceControlVM . uiState. feedbackMessage {
53+ if viewModel . uiState . shouldShowFeedback, let feedback = viewModel . uiState. feedbackMessage {
7154 feedbackText ( feedback)
7255 }
7356
7457 // Status instruction message
75- if let message = statusMessage {
58+ if let message = resolvedStatusMessage {
7659 statusText ( message)
7760 }
7861 }
7962 . onTapGesture ( perform: handleTap)
63+ . hoverEffect ( )
64+ . onContinuousHover { phase in
65+ handleHover ( phase)
66+ }
8067 }
8168
8269 // MARK: - Subviews
@@ -90,8 +77,7 @@ struct VoiceControlMenuButton: View {
9077 . frame ( width: Constants . buttonSize, height: Constants . buttonSize)
9178 . opacity ( buttonOpacity)
9279
93- // Loading indicator
94- if voiceControlVM. uiState. isProcessing {
80+ if viewModel. uiState. isProcessing {
9581 ProgressView ( )
9682 . progressViewStyle ( . circular)
9783 . scaleEffect ( Constants . progressScale)
@@ -100,8 +86,6 @@ struct VoiceControlMenuButton: View {
10086 }
10187 }
10288
103- // MARK: - Text Components
104-
10589 /// Real-time transcription text view
10690 @ViewBuilder
10791 private func transcriptionText( _ text: String ) -> some View {
@@ -144,19 +128,16 @@ struct VoiceControlMenuButton: View {
144128 . cornerRadius ( 8 )
145129 }
146130
147- // MARK: - Computed Properties - State
131+ // MARK: - Computed Properties
148132
149- /// Current voice control state
150133 private var voiceState : VoiceControlState {
151- voiceControlVM . uiState. state
134+ viewModel . uiState. state
152135 }
153136
154- /// Whether menu is currently open
155137 private var isMenuOpen : Bool {
156138 immersiveViewModel. isMenuActive
157139 }
158140
159- /// Button opacity (voice state > menu state priority)
160141 private var buttonOpacity : Double {
161142 switch voiceState {
162143 case . idle: isMenuOpen ? 1.0 : 0.25
@@ -166,86 +147,63 @@ struct VoiceControlMenuButton: View {
166147 }
167148 }
168149
169- // MARK: - Computed Properties - Messages
150+ // MARK: - UI Mapping (Abstract → SwiftUI)
170151
171- /// Status instruction message based on voice state
172- private var statusMessage : String ? {
173- switch voiceState {
174- case . idle:
175- nil
176- case . standby:
177- " 'Hippo'라고 말하세요 "
178- case . listening:
179- if voiceControlVM. uiState. isProcessing {
180- " 처리 중... "
181- } else if voiceControlVM. uiState. feedbackType == . success {
182- nil // Hide when showing success feedback
183- } else {
184- " 명령을 말씀해주세요 "
185- }
186- case . retry:
187- voiceControlVM. uiState. lastErrorMessage ?? " 다시 시도해주세요 "
152+ /// Resolve abstract message key to actual text
153+ private var resolvedStatusMessage : String ? {
154+ switch viewModel. uiState. statusMessageKey {
155+ case . none: return nil
156+ case . standbyGuide: return " 'Hippo'라고 말하세요 "
157+ case . listening: return " 명령을 말씀해주세요 "
158+ case . processing: return " 처리 중... "
159+ case . retry( let errorMessage) : return errorMessage ?? " 다시 시도해주세요 "
188160 }
189161 }
190162
191- /// Whether to show feedback message
192- private var shouldShowFeedback : Bool {
193- voiceState == . listening && !voiceControlVM. uiState. isProcessing
194- }
195-
196- /// Feedback text color based on feedback type
163+ /// Map color key to SwiftUI Color
197164 private var feedbackColor : Color {
198- switch voiceControlVM. uiState. feedbackType {
199- case . success: . green
200- case . error: . orange
201- case . info: . blue
202- }
165+ mapColorKey ( viewModel. uiState. feedbackColorKey)
203166 }
204167
205- /// Feedback background color based on feedback type
206- private var feedbackBackground : some ShapeStyle {
207- switch voiceControlVM. uiState. feedbackType {
208- case . success: AnyShapeStyle ( Color . green. opacity ( 0.15 ) )
209- case . error: AnyShapeStyle ( Color . orange. opacity ( 0.15 ) )
210- case . info: AnyShapeStyle ( Color . blue. opacity ( 0.15 ) )
211- }
168+ private var feedbackBackground : Color {
169+ mapColorKey ( viewModel. uiState. feedbackColorKey) . opacity ( 0.15 )
212170 }
213171
214- /// Status text color based on voice state
215172 private var statusColor : Color {
216173 switch voiceState {
217- case . idle: . secondary
218- case . standby: . blue
219- case . listening: . green
220- case . retry: . orange
174+ case . idle: return . secondary
175+ default : return mapColorKey ( viewModel. uiState. statusColorKey)
221176 }
222177 }
223178
224- /// Status background color based on voice state
225- private var statusBackground : some ShapeStyle {
179+ private var statusBackground : Color {
226180 switch voiceState {
227- case . idle: AnyShapeStyle ( Color . clear)
228- case . standby: AnyShapeStyle ( Color . blue. opacity ( 0.15 ) )
229- case . listening: AnyShapeStyle ( Color . green. opacity ( 0.15 ) )
230- case . retry: AnyShapeStyle ( Color . orange. opacity ( 0.15 ) )
181+ case . idle: return . clear
182+ default : return mapColorKey ( viewModel. uiState. statusColorKey) . opacity ( 0.15 )
183+ }
184+ }
185+
186+ private func mapColorKey( _ key: VoiceFeedbackColor ) -> Color {
187+ switch key {
188+ case . success: return . green
189+ case . error: return . orange
190+ case . info: return . blue
231191 }
232192 }
233193
234194 // MARK: - Actions
235195
236- /// Handle button tap - toggle menu and activate voice control
237196 private func handleTap( ) {
238197 action ( )
239- voiceControlVM . onWakeWordDetected ( )
198+ viewModel . onWakeWordDetected ( )
240199 }
241200
242- /// Handle hover phase changes (disabled - using tap only)
243201 private func handleHover( _ phase: HoverPhase ) {
244202 switch phase {
245203 case . active:
246- voiceControlVM . onHoverBegan ( )
204+ viewModel . onHoverBegan ( )
247205 case . ended:
248- voiceControlVM . onHoverEnded ( )
206+ viewModel . onHoverEnded ( )
249207 }
250208 }
251209}
@@ -256,9 +214,8 @@ struct VoiceControlMenuButton: View {
256214 PreviewContainer ( )
257215}
258216
259- // MARK: - Preview Helpers
260-
261- private struct PreviewContainer : View {
217+ #if DEBUG
218+ fileprivate struct PreviewContainer : View {
262219 @State private var immersiveVM = ImmersiveViewModel ( )
263220 @State private var voiceControlVM = VoiceControlViewModel (
264221 commandExecutor: MockVoiceControlManager ( )
@@ -273,9 +230,9 @@ private struct PreviewContainer: View {
273230 }
274231}
275232
276- /// Mock VoiceControlManager for previews and testing
277- internal struct MockVoiceControlManager : VoiceCommandExecutor {
233+ fileprivate struct MockVoiceControlManager : VoiceCommandExecutor {
278234 func execute( _ intent: VoiceCommandIntent ) async throws {
279235 print ( " Mock execute: \( intent) " )
280236 }
281237}
238+ #endif
0 commit comments