Skip to content

Commit 2b0157f

Browse files
committed
Refactor ChatViewModel: Extract feature logic into specialized Managers
- Extracted MelodyManager, NavigationManager, DashboardManager, EventRulesManager, and FaceManager - Reduced ChatViewModel size from 937 to 614 lines (-34.5%) - Improved testability and modularity of the codebase - Fixed compiler warnings in ChatSessionController - Updated Project Structure in README.md with the new manager classes
1 parent 8503184 commit 2b0157f

9 files changed

Lines changed: 1133 additions & 408 deletions

File tree

README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1525,8 +1525,13 @@ app/src/
15251525
│ ├── manager/ # Application managers
15261526
│ │ ├── ApiKeyManager.kt # API key management
15271527
│ │ ├── AudioPlayer.kt # Audio playback engine
1528+
│ │ ├── DashboardManager.kt # Dashboard state management
15281529
│ │ ├── DrawingGameManager.kt # Drawing game state & logic
1530+
│ │ ├── EventRulesManager.kt # Event rules & persistence
1531+
│ │ ├── FaceManager.kt # Face recognition & settings
1532+
│ │ ├── MelodyManager.kt # Melody playback coordination
15291533
│ │ ├── MemoryGameManager.kt # Memory game state & logic
1534+
│ │ ├── NavigationManager.kt # Navigation overlay state
15301535
│ │ ├── PermissionManager.kt # Android permission handling
15311536
│ │ ├── QuizGameManager.kt # Quiz game state & logic
15321537
│ │ ├── RealtimeAudioInputManager.kt # Audio input for Realtime API

app/src/main/java/ch/fhnw/pepper_realtime/controller/ChatSessionController.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -315,7 +315,7 @@ class ChatSessionController @Inject constructor(
315315
keyManager.openAiApiKey,
316316
keyManager.xaiApiKey
317317
)
318-
if (headerName != null && headerValue != null) {
318+
if (headerName.isNotEmpty() && headerValue.isNotEmpty()) {
319319
headers[headerName] = headerValue
320320
}
321321
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
package ch.fhnw.pepper_realtime.manager
2+
3+
import ch.fhnw.pepper_realtime.data.PerceptionData
4+
import ch.fhnw.pepper_realtime.ui.DashboardState
5+
import kotlinx.coroutines.flow.MutableStateFlow
6+
import kotlinx.coroutines.flow.StateFlow
7+
import kotlinx.coroutines.flow.asStateFlow
8+
import kotlinx.coroutines.flow.update
9+
import javax.inject.Inject
10+
import javax.inject.Singleton
11+
12+
/**
13+
* Manager for perception dashboard overlay state.
14+
* Extracted from ChatViewModel for better separation of concerns.
15+
*/
16+
@Singleton
17+
class DashboardManager @Inject constructor() {
18+
19+
private val _state = MutableStateFlow(DashboardState())
20+
val state: StateFlow<DashboardState> = _state.asStateFlow()
21+
22+
// Callback for refreshing face list when dashboard opens
23+
private var onDashboardOpened: (() -> Unit)? = null
24+
25+
/**
26+
* Set callback to be invoked when dashboard opens.
27+
* Used to trigger face list refresh.
28+
*/
29+
fun setOnDashboardOpenedCallback(callback: () -> Unit) {
30+
onDashboardOpened = callback
31+
}
32+
33+
fun showDashboard() {
34+
_state.update { it.copy(isVisible = true, isMonitoring = true) }
35+
onDashboardOpened?.invoke()
36+
}
37+
38+
fun hideDashboard() {
39+
_state.update { it.copy(isVisible = false, isMonitoring = false) }
40+
}
41+
42+
fun toggleDashboard() {
43+
val willBeVisible = !_state.value.isVisible
44+
_state.update { it.copy(isVisible = willBeVisible, isMonitoring = willBeVisible) }
45+
if (willBeVisible) {
46+
onDashboardOpened?.invoke()
47+
}
48+
}
49+
50+
fun updateDashboardHumans(humans: List<PerceptionData.HumanInfo>, timestamp: String) {
51+
_state.update { it.copy(humans = humans, lastUpdate = timestamp) }
52+
}
53+
54+
fun resetDashboard() {
55+
_state.value = DashboardState()
56+
}
57+
}
Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
package ch.fhnw.pepper_realtime.manager
2+
3+
import android.util.Log
4+
import ch.fhnw.pepper_realtime.data.EventRule
5+
import ch.fhnw.pepper_realtime.data.MatchedRule
6+
import ch.fhnw.pepper_realtime.data.RulePersistence
7+
import ch.fhnw.pepper_realtime.data.RuleActionType
8+
import ch.fhnw.pepper_realtime.service.EventRuleEngine
9+
import ch.fhnw.pepper_realtime.ui.EventRulesState
10+
import kotlinx.coroutines.flow.MutableStateFlow
11+
import kotlinx.coroutines.flow.StateFlow
12+
import kotlinx.coroutines.flow.asStateFlow
13+
import kotlinx.coroutines.flow.update
14+
import javax.inject.Inject
15+
import javax.inject.Singleton
16+
17+
/**
18+
* Manager for event rules state and operations.
19+
* Extracted from ChatViewModel for better separation of concerns.
20+
*/
21+
@Singleton
22+
class EventRulesManager @Inject constructor(
23+
private val eventRuleEngine: EventRuleEngine,
24+
private val rulePersistence: RulePersistence
25+
) {
26+
27+
companion object {
28+
private const val TAG = "EventRulesManager"
29+
}
30+
31+
// Expose eventRuleEngine for external access (e.g., for evaluate() calls from PerceptionService)
32+
val engine: EventRuleEngine get() = eventRuleEngine
33+
34+
private val _state = MutableStateFlow(EventRulesState())
35+
val state: StateFlow<EventRulesState> = _state.asStateFlow()
36+
37+
/**
38+
* Callback interface for rule actions that affect the chat.
39+
*/
40+
interface RuleActionHandler {
41+
fun onAddEventMessage(matchedRule: MatchedRule)
42+
fun onSendToRealtimeAPI(text: String, requestResponse: Boolean, allowInterrupt: Boolean)
43+
}
44+
45+
private var ruleActionHandler: RuleActionHandler? = null
46+
47+
/**
48+
* Set the handler for rule actions.
49+
* Should be called during ViewModel initialization.
50+
*/
51+
fun setRuleActionHandler(handler: RuleActionHandler) {
52+
ruleActionHandler = handler
53+
}
54+
55+
/**
56+
* Initialize the event rule engine with persisted rules.
57+
*/
58+
fun initialize() {
59+
val rules = rulePersistence.loadRules()
60+
eventRuleEngine.loadRules(rules)
61+
_state.update { it.copy(rules = rules) }
62+
63+
// Set up listener for matched rules
64+
eventRuleEngine.setListener(object : EventRuleEngine.RuleMatchListener {
65+
override fun onRuleMatched(matchedRule: MatchedRule) {
66+
handleRuleAction(matchedRule)
67+
}
68+
})
69+
70+
Log.i(TAG, "Event rules initialized with ${rules.size} rules")
71+
}
72+
73+
/**
74+
* Set the robot state provider for rule condition checks.
75+
*/
76+
fun setRobotStateProvider(provider: EventRuleEngine.RobotStateProvider) {
77+
eventRuleEngine.setRobotStateProvider(provider)
78+
}
79+
80+
/**
81+
* Handle a matched rule by executing the appropriate action.
82+
*/
83+
private fun handleRuleAction(matchedRule: MatchedRule) {
84+
Log.i(TAG, "Handling rule action: ${matchedRule.rule.name} (${matchedRule.rule.actionType})")
85+
86+
// Add to recent triggered rules for UI feedback
87+
_state.update { state ->
88+
val newRecent = (listOf(matchedRule) + state.recentTriggeredRules).take(10)
89+
state.copy(recentTriggeredRules = newRecent)
90+
}
91+
92+
// Notify handler to add event message to chat
93+
ruleActionHandler?.onAddEventMessage(matchedRule)
94+
95+
// Execute the action via callback
96+
when (matchedRule.rule.actionType) {
97+
RuleActionType.INTERRUPT_AND_RESPOND -> {
98+
ruleActionHandler?.onSendToRealtimeAPI(
99+
text = matchedRule.resolvedTemplate,
100+
requestResponse = true,
101+
allowInterrupt = true
102+
)
103+
}
104+
RuleActionType.APPEND_AND_RESPOND -> {
105+
ruleActionHandler?.onSendToRealtimeAPI(
106+
text = matchedRule.resolvedTemplate,
107+
requestResponse = true,
108+
allowInterrupt = false
109+
)
110+
}
111+
RuleActionType.SILENT_UPDATE -> {
112+
ruleActionHandler?.onSendToRealtimeAPI(
113+
text = matchedRule.resolvedTemplate,
114+
requestResponse = false,
115+
allowInterrupt = false
116+
)
117+
}
118+
}
119+
}
120+
121+
// ==================== UI State Methods ====================
122+
123+
fun showEventRules() {
124+
_state.update { it.copy(isVisible = true) }
125+
}
126+
127+
fun hideEventRules() {
128+
_state.update { it.copy(isVisible = false) }
129+
}
130+
131+
fun toggleEventRules() {
132+
_state.update { it.copy(isVisible = !it.isVisible) }
133+
}
134+
135+
// ==================== CRUD Operations ====================
136+
137+
fun addEventRule(rule: EventRule) {
138+
eventRuleEngine.addRule(rule)
139+
saveEventRules()
140+
_state.update { it.copy(rules = eventRuleEngine.getRules()) }
141+
}
142+
143+
fun updateEventRule(rule: EventRule) {
144+
eventRuleEngine.updateRule(rule)
145+
saveEventRules()
146+
_state.update { it.copy(rules = eventRuleEngine.getRules()) }
147+
}
148+
149+
fun deleteEventRule(ruleId: String) {
150+
eventRuleEngine.removeRule(ruleId)
151+
saveEventRules()
152+
_state.update { it.copy(rules = eventRuleEngine.getRules()) }
153+
}
154+
155+
fun toggleEventRuleEnabled(ruleId: String) {
156+
val rules = eventRuleEngine.getRules()
157+
val rule = rules.find { it.id == ruleId } ?: return
158+
val updatedRule = rule.copy(enabled = !rule.enabled)
159+
updateEventRule(updatedRule)
160+
}
161+
162+
private fun saveEventRules() {
163+
rulePersistence.saveRules(eventRuleEngine.getRules())
164+
}
165+
166+
fun resetEventRulesToDefaults() {
167+
rulePersistence.resetToDefaults()
168+
val rules = rulePersistence.loadRules()
169+
eventRuleEngine.loadRules(rules)
170+
eventRuleEngine.resetCooldowns()
171+
_state.update { it.copy(rules = rules) }
172+
}
173+
174+
// ==================== Import/Export ====================
175+
176+
fun exportEventRules(): String {
177+
return rulePersistence.exportToJson()
178+
}
179+
180+
fun importEventRules(json: String, merge: Boolean = false): Int {
181+
val count = rulePersistence.importFromJson(json, merge)
182+
if (count >= 0) {
183+
val rules = rulePersistence.loadRules()
184+
eventRuleEngine.loadRules(rules)
185+
_state.update { it.copy(rules = rules) }
186+
}
187+
return count
188+
}
189+
190+
fun setEditingRule(rule: EventRule?) {
191+
_state.update { it.copy(editingRule = rule) }
192+
}
193+
}

0 commit comments

Comments
 (0)