Skip to content

Commit 345df7d

Browse files
Add files via upload
1 parent fd49c83 commit 345df7d

1 file changed

Lines changed: 49 additions & 91 deletions

File tree

app/src/main/kotlin/com/google/ai/sample/feature/multimodal/PhotoReasoningViewModel.kt

Lines changed: 49 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,6 @@ import kotlinx.coroutines.flow.MutableStateFlow
3131
import kotlinx.coroutines.Job
3232
import kotlinx.coroutines.flow.StateFlow
3333
import kotlinx.coroutines.flow.asStateFlow
34-
// Removed duplicate StateFlow import
35-
// Removed duplicate asStateFlow import
3634
// import kotlinx.coroutines.isActive // Removed as we will use job.isActive
3735
import kotlinx.coroutines.launch
3836
import kotlinx.coroutines.withContext
@@ -50,17 +48,9 @@ class PhotoReasoningViewModel(
5048
MutableStateFlow(PhotoReasoningUiState.Initial)
5149
val uiState: StateFlow<PhotoReasoningUiState> =
5250
_uiState.asStateFlow()
53-
54-
private val _isInitialized = MutableStateFlow(false)
55-
val isInitialized: StateFlow<Boolean> = _isInitialized.asStateFlow()
56-
57-
private val _showStopNotificationFlow = MutableStateFlow(false)
58-
val showStopNotificationFlow: StateFlow<Boolean> = _showStopNotificationFlow.asStateFlow()
5951

6052
// Keep track of the latest screenshot URI
6153
private var latestScreenshotUri: Uri? = null
62-
private var lastProcessedScreenshotUri: Uri? = null
63-
private var lastProcessedScreenshotTime: Long = 0L
6454

6555
// Keep track of the current selected images
6656
private var currentSelectedImages: List<Bitmap> = emptyList()
@@ -106,27 +96,15 @@ class PhotoReasoningViewModel(
10696

10797
fun reason(
10898
userInput: String,
109-
selectedImages: List<Bitmap>,
110-
screenInfoForPrompt: String? = null,
111-
imageUrisForChat: List<String>? = null
99+
selectedImages: List<Bitmap>
112100
) {
113-
Log.d(TAG, "reason() called. User input: '$userInput', Image count: ${selectedImages.size}, ScreenInfo: ${screenInfoForPrompt != null}, ImageUris: ${imageUrisForChat != null}")
114101
_uiState.value = PhotoReasoningUiState.Loading
115-
Log.d(TAG, "Setting _showStopNotificationFlow to true")
116-
_showStopNotificationFlow.value = true
117-
Log.d(TAG, "_showStopNotificationFlow value is now: ${_showStopNotificationFlow.value}")
118-
stopExecutionFlag.set(false)
119-
120-
val combinedPromptTextBuilder = StringBuilder(userInput)
121-
if (screenInfoForPrompt != null && screenInfoForPrompt.isNotBlank()) { // Added isNotBlank check
122-
combinedPromptTextBuilder.append("\n\nScreen Context:\n$screenInfoForPrompt")
123-
}
124-
val aiPromptText = combinedPromptTextBuilder.toString()
102+
stopExecutionFlag.set(false) // Reset flag at the beginning of a new reason call
125103

126-
val prompt = "FOLLOW THE INSTRUCTIONS STRICTLY: $aiPromptText"
104+
val prompt = "FOLLOW THE INSTRUCTIONS STRICTLY: $userInput"
127105

128106
// Store the current user input and selected images
129-
currentUserInput = userInput // This should ideally store aiPromptText or handle context separately if needed for retry. For now, task is specific to prompt to AI and chat.
107+
currentUserInput = userInput
130108
currentSelectedImages = selectedImages
131109

132110
// Clear previous commands
@@ -135,9 +113,8 @@ class PhotoReasoningViewModel(
135113

136114
// Add user message to chat history
137115
val userMessage = PhotoReasoningMessage(
138-
text = aiPromptText, // Use the combined text
116+
text = userInput,
139117
participant = PhotoParticipant.USER,
140-
imageUris = imageUrisForChat ?: emptyList(), // Use the new parameter here
141118
isPending = false
142119
)
143120
_chatState.addMessage(userMessage)
@@ -198,7 +175,6 @@ class PhotoReasoningViewModel(
198175
}
199176

200177
fun onStopClicked() {
201-
_showStopNotificationFlow.value = false // Hide notification immediately on stop
202178
stopExecutionFlag.set(true)
203179
currentReasoningJob?.cancel()
204180
commandProcessingJob?.cancel()
@@ -252,12 +228,11 @@ class PhotoReasoningViewModel(
252228
if (currentReasoningJob?.isActive != true || stopExecutionFlag.get()) {
253229
if (stopExecutionFlag.get()) {
254230
// User initiated stop, onStopClicked will handle UI and message
255-
return // _showStopNotificationFlow is already false from onStopClicked
231+
return
256232
} else {
257233
// Cancellation not by user stop button
258234
_uiState.value = PhotoReasoningUiState.Error("Operation cancelled unexpectedly before sending.")
259235
updateAiMessage("Operation cancelled unexpectedly.")
260-
_showStopNotificationFlow.value = false
261236
return
262237
}
263238
}
@@ -269,12 +244,11 @@ class PhotoReasoningViewModel(
269244
if (currentReasoningJob?.isActive != true || stopExecutionFlag.get()) {
270245
if (stopExecutionFlag.get()) {
271246
// User initiated stop, onStopClicked will handle UI and message
272-
return // _showStopNotificationFlow is already false from onStopClicked
247+
return
273248
} else {
274249
// Cancellation not by user stop button
275250
_uiState.value = PhotoReasoningUiState.Error("Operation cancelled unexpectedly after sending.")
276251
updateAiMessage("Operation cancelled unexpectedly.")
277-
_showStopNotificationFlow.value = false
278252
return
279253
}
280254
}
@@ -289,12 +263,10 @@ class PhotoReasoningViewModel(
289263
if (stopExecutionFlag.get()) {
290264
// User initiated stop, onStopClicked will handle UI and message
291265
shouldProceed = false // Signal to skip further processing
292-
// _showStopNotificationFlow handled by onStopClicked or subsequent checks if shouldProceed is false
293266
} else {
294267
// Cancellation not by user stop button
295268
_uiState.value = PhotoReasoningUiState.Error("Operation cancelled unexpectedly during response processing.")
296269
updateAiMessage("Operation cancelled unexpectedly.")
297-
_showStopNotificationFlow.value = false
298270
shouldProceed = false // Signal to skip further processing
299271
}
300272
}
@@ -305,17 +277,16 @@ class PhotoReasoningViewModel(
305277
if (currentReasoningJob?.isActive != true || stopExecutionFlag.get()) { // Re-check for cancellation
306278
if (stopExecutionFlag.get()) {
307279
// User initiated stop, onStopClicked will handle UI and message
308-
// _showStopNotificationFlow handled by onStopClicked
280+
// No action needed here, shouldProceed will prevent further execution if already false
281+
// or the outer checks in this function will catch it.
309282
} else {
310283
_uiState.value = PhotoReasoningUiState.Error("Operation cancelled unexpectedly before UI update.")
311284
updateAiMessage("Operation cancelled unexpectedly.")
312-
_showStopNotificationFlow.value = false
313285
}
314286
// No return@withContext, logic will naturally skip due to outer 'if (shouldProceed)' and this check
315287
// or if shouldProceed was set to false earlier.
316288
} else {
317289
_uiState.value = PhotoReasoningUiState.Success(outputContent)
318-
_showStopNotificationFlow.value = false // Operation successful
319290

320291
// Update the AI message in chat history
321292
updateAiMessage(outputContent)
@@ -328,12 +299,6 @@ class PhotoReasoningViewModel(
328299
}
329300
}
330301
}
331-
} else {
332-
// If shouldProceed is false, and it wasn't due to stopExecutionFlag, ensure notification is cancelled.
333-
// If stopExecutionFlag was true, onStopClicked already handled it.
334-
if (!stopExecutionFlag.get()) {
335-
_showStopNotificationFlow.value = false
336-
}
337302
}
338303

339304

@@ -344,7 +309,6 @@ class PhotoReasoningViewModel(
344309
// Ensure we are still active before saving
345310
if (currentReasoningJob?.isActive == true && !stopExecutionFlag.get()) {
346311
saveChatHistory(MainActivity.getInstance()?.applicationContext)
347-
// _showStopNotificationFlow already set to false if successful
348312
}
349313
}
350314
}
@@ -353,15 +317,13 @@ class PhotoReasoningViewModel(
353317
// If user already stopped, just log the error and return.
354318
// Do not update UI or send chat messages as onStopClicked handles this.
355319
Log.w(TAG, "Exception caught after stop flag was set: ${e.message}", e)
356-
// _showStopNotificationFlow is already false from onStopClicked
357320
return
358321
}
359322
// If the stop flag is not set, but the job is inactive (cancelled by other means)
360323
if (currentReasoningJob?.isActive != true) {
361324
_uiState.value = PhotoReasoningUiState.Error("Operation cancelled and then an error occurred.") // Or a more fitting message
362325
updateAiMessage("Operation cancelled, error during cleanup: ${e.message}")
363326
Log.e(TAG, "Error generating content after job was cancelled: ${e.message}", e)
364-
_showStopNotificationFlow.value = false
365327
return
366328
}
367329

@@ -373,46 +335,30 @@ class PhotoReasoningViewModel(
373335
// The check currentReasoningJob?.isActive != true is still relevant if the job gets cancelled
374336
// by other means after the top checks in this catch block.
375337
// stopExecutionFlag.get() is less likely to be true here due to the top check, but kept for defense.
376-
if (currentReasoningJob?.isActive != true || stopExecutionFlag.get()){
377-
if (!stopExecutionFlag.get()) _showStopNotificationFlow.value = false
378-
return
379-
}
380-
handleQuotaExceededError(e, inputContent, retryCount) // This might retry or be terminal
381-
// If handleQuotaExceededError doesn't lead to a retry (i.e., it's terminal), we need to set flow to false.
382-
// This logic is tricky as handleQuotaExceededError has its own returns.
383-
// For now, assume if it returns here, it might retry. If it's terminal, it sets UI state and should set flow.
384-
// This will be refined by inspecting handleQuotaExceededError.
385-
// For now, if it's truly terminal and doesn't retry, the general error path below will catch it.
386-
// Let's assume retries handle the flow, and terminal errors are handled below or within the handler.
387-
return // if handleQuotaExceededError is not terminal and retries, it will reset the flow value at reason() start
338+
if (currentReasoningJob?.isActive != true || stopExecutionFlag.get()) return
339+
handleQuotaExceededError(e, inputContent, retryCount)
340+
return
388341
}
389342

390343
// Check for other 503 errors
391344
if (is503Error(e) && apiKeyManager != null) {
392-
if (currentReasoningJob?.isActive != true || stopExecutionFlag.get()){
393-
if (!stopExecutionFlag.get()) _showStopNotificationFlow.value = false
394-
return
395-
}
396-
handle503Error(e, inputContent, retryCount) // Similar to above, assumes retries handle flow
345+
if (currentReasoningJob?.isActive != true || stopExecutionFlag.get()) return
346+
handle503Error(e, inputContent, retryCount)
397347
return
398348
}
399349

400-
// If we get here, it's not a 503 error or quota exceeded error that led to a retry path
350+
// If we get here, it's not a 503 error or quota exceeded error
401351
// The stopExecutionFlag.get() check here is mostly redundant due to the top check,
402352
// but currentReasoningJob?.isActive is still a valid check.
403353
if (currentReasoningJob?.isActive == true && !stopExecutionFlag.get()) {
404354
withContext(Dispatchers.Main) {
405355
if (currentReasoningJob?.isActive != true || stopExecutionFlag.get()) {
406-
if (!stopExecutionFlag.get()) {
407-
_showStopNotificationFlow.value = false
408-
}
409356
// If cancelled (possibly between the outer check and here, or due to job inactivity)
410357
// Potentially log or update UI to reflect this specific state if desired.
411358
// For now, this means the error e won't be set as the primary UI state.
412359
} else {
413360
_uiState.value = PhotoReasoningUiState.Error(e.localizedMessage ?: "Unknown error")
414361
_commandExecutionStatus.value = "Error during generation: ${e.localizedMessage}"
415-
_showStopNotificationFlow.value = false // Terminal error
416362

417363
// Update chat with error message
418364
_chatState.replaceLastPendingMessage()
@@ -428,13 +374,17 @@ class PhotoReasoningViewModel(
428374
saveChatHistory(MainActivity.getInstance()?.applicationContext)
429375
}
430376
}
431-
} else { // This 'else' covers cases where job became inactive or stop flag was set concurrently
432-
if (!stopExecutionFlag.get()) { // If not stopped by user, then it's a cancellation
377+
} else {
378+
// This branch could be reached if:
379+
// 1. stopExecutionFlag was set by another thread between the top check and here.
380+
// 2. currentReasoningJob became inactive for reasons other than the stop flag.
381+
// If stopExecutionFlag is true, onStopClicked handles the message.
382+
// If only job inactive, a general "cancelled during error processing" might be okay,
383+
// but the specific check for job inactivity at the top of the catch block should handle most cases.
384+
if (!stopExecutionFlag.get()) {
433385
_uiState.value = PhotoReasoningUiState.Error("Operation error processing or cancelled.")
434386
updateAiMessage("Operation error processing or cancelled.")
435-
_showStopNotificationFlow.value = false
436387
}
437-
// If stopExecutionFlag.get() is true, onStopClicked handles the notification flow.
438388
}
439389
}
440390
}
@@ -713,9 +663,7 @@ class PhotoReasoningViewModel(
713663
_systemMessage.value = message
714664

715665
// Also load chat history
716-
loadChatHistory(context) // This line calls rebuildChatHistory internally
717-
718-
_isInitialized.value = true // Add this line
666+
loadChatHistory(context)
719667
}
720668

721669
/**
@@ -943,14 +891,6 @@ class PhotoReasoningViewModel(
943891
context: Context,
944892
screenInfo: String? = null
945893
) {
946-
val currentTime = System.currentTimeMillis()
947-
if (screenshotUri == lastProcessedScreenshotUri && (currentTime - lastProcessedScreenshotTime) < 2000) { // 2-second debounce window
948-
Log.w(TAG, "addScreenshotToConversation: Debouncing duplicate/rapid call for URI $screenshotUri")
949-
return // Exit the function early if it's a duplicate call within the window
950-
}
951-
lastProcessedScreenshotUri = screenshotUri
952-
lastProcessedScreenshotTime = currentTime
953-
954894
PhotoReasoningApplication.applicationScope.launch(Dispatchers.Main) {
955895
try {
956896
Log.d(TAG, "Adding screenshot to conversation: $screenshotUri")
@@ -972,6 +912,25 @@ class PhotoReasoningViewModel(
972912
// Show toast
973913
Toast.makeText(context, "Processing screenshot...", Toast.LENGTH_SHORT).show()
974914

915+
// Create message text with screen information if available
916+
val messageText = if (screenInfo != null) {
917+
"Screenshot captured\n\n$screenInfo"
918+
} else {
919+
"Screenshot captured"
920+
}
921+
922+
// Add screenshot message to chat history
923+
val screenshotMessage = PhotoReasoningMessage(
924+
text = messageText,
925+
participant = PhotoParticipant.USER,
926+
imageUris = listOf(screenshotUri.toString())
927+
)
928+
_chatState.addMessage(screenshotMessage)
929+
_chatMessagesFlow.value = chatMessages
930+
931+
// Save chat history after adding screenshot
932+
saveChatHistory(context)
933+
975934
// Process the screenshot
976935
val imageRequest = imageRequestBuilder!!
977936
.data(screenshotUri)
@@ -998,15 +957,14 @@ class PhotoReasoningViewModel(
998957
Toast.makeText(context, "Screenshot added, sending to AI...", Toast.LENGTH_SHORT).show()
999958

1000959
// Create prompt with screen information if available
1001-
val genericAnalysisPrompt = "Analyze the provided screenshot and its context."
960+
val prompt = if (screenInfo != null) {
961+
"Analyze this screenshot. Here is the available screen information: $screenInfo"
962+
} else {
963+
"Analyze this screenshot"
964+
}
1002965

1003966
// Re-send the query with only the latest screenshot
1004-
reason(
1005-
userInput = genericAnalysisPrompt,
1006-
selectedImages = listOf(bitmap),
1007-
screenInfoForPrompt = screenInfo,
1008-
imageUrisForChat = listOf(screenshotUri.toString()) // Add this argument
1009-
)
967+
reason(prompt, listOf(bitmap))
1010968

1011969
// Show a toast to indicate the screenshot was added
1012970
Toast.makeText(context, "Screenshot added to conversation", Toast.LENGTH_SHORT).show()

0 commit comments

Comments
 (0)