Skip to content

Commit fe51ad8

Browse files
Stop button (#30)
* Update CommandParser.kt * I've updated the default model in `GenerativeAiViewModelFactory.kt` from Gemini 2.0 Flash Lite to Gemini 2.5 Flash Preview. This involved updating the initial value of `currentModelName` in both `GenerativeViewModelFactory` and the `GenerativeAiViewModelFactory` companion object. The order of model options in the selection UI remains unchanged. * Update SystemMessageEntryPreferences.kt * Update SystemMessagePreferences.kt * Update PhotoReasoningScreen.kt * Update SystemMessageEntryPreferences.kt * Fix: Correct problematic string in SystemMessageEntryPreferences The guide string for the "Chromium-based Browser" default entry contained a sequence `\"-\"` which caused a Kotlin compiler error (unresolved reference, receiver type mismatch for minus operator) during release builds. This commit changes the problematic sequence `tap the \"-\" (multiple times)` to `tap the 'minus' symbol (multiple times)` to avoid the parsing issue and allow the build to complete successfully. The meaning of the guide text remains the same for you. * Update SystemMessageEntryPreferences.kt * feat: Implement stop button functionality in PhotoReasoningScreen This commit introduces a "Stop" button that appears in the PhotoReasoningScreen banner when I am busy processing your request or when the application is executing commands I generated. Key changes: - Modified PhotoReasoningScreen.kt to conditionally display a "Stop" button. When the "Stop" button is visible, the regular input elements (text field, "Add Image" button, "New" button, "Send" button) are hidden. - Implemented logic in PhotoReasoningViewModel.kt for the "Stop" button. Clicking "Stop" cancels my ongoing reasoning tasks (coroutines) and interrupts in-progress command execution loops. It updates the UI state to reflect that the operation was stopped. - Added a new PhotoReasoningUiState.Stopped state to differentiate user-initiated stops from other states like Loading, Success, or Error. - Added ViewModel unit tests to verify the cancellation logic, state changes, and chat message updates upon stopping. - Added screen UI tests to verify the conditional visibility of the "Stop" button and the regular input banner, as well as the invocation of the stop action. The "Stop" button allows you to interrupt lengthy operations or command sequences, providing better control over the application's behavior. * fix: Correct compilation errors in PhotoReasoningViewModel This commit addresses compilation errors that arose from the implementation of the stop button functionality. - Corrected unresolved `isActive` references by explicitly using `coroutineContext.isActive` within coroutines and suspend functions. This ensures checks are made against the correct coroutine context. - Reviewed `return` statements within `sendMessageWithRetry` and related functions to ensure they correctly exit from the intended scope. These changes resolve the build failures and ensure the stop functionality operates as intended with proper coroutine cancellation checks. * fix: Further attempt to correct compilation errors in PhotoReasoningViewModel This commit addresses compilation errors by refining how coroutine cancellation is checked. - Updated `isActive` checks to explicitly use the `isActive` property of the `Job` instances (`currentReasoningJob?.isActive == true` and `commandProcessingJob?.isActive == true`). This avoids reliance on implicit `coroutineContext` which may have caused resolution issues. These changes aim to resolve build failures. * fix: Correct non-local return in sendMessageWithRetry on stop-button branch Addresses a compilation error where a 'return' statement inside a `response.text?.let { ... }` block in the `sendMessageWithRetry` function was attempting a non-local return. Changed the problematic 'return' to 'return@let' to ensure it only returns from the lambda block, resolving the "'return' is not allowed here" error. This fix is based on the code structure observed on the stop-button branch. * Here's the plan: Fix incorrect return statements in PhotoReasoningViewModel I'll refactor the `sendMessageWithRetry` function to avoid using `return` statements that attempt to return from the outer function from within `let` and `withContext` blocks. I'll introduce a boolean flag `shouldProceed` to control the execution flow and ensure that cancellation requests are handled correctly without triggering compilation errors. * Fix incorrect return statements in PhotoReasoningViewModel This commit addresses compilation errors related to 'return' statements being used in disallowed contexts. Changes include: - Ensured `return@launch` is used in the `reason()` method's `launch` block, specifically within the nested `content` builder, to correctly exit the coroutine lambda at lines corresponding to the original error locations (approx. 136, 138, 141). - Verified that the `sendMessageWithRetry` method uses a boolean flag (`shouldProceed`) to manage control flow and avoid incorrect `return@let` and `return@withContext` statements that were previously causing compilation issues. * Delete app/local.properties * Delete app/src/test/kotlin/com/google/ai/sample directory * Delete app/src/androidTest/kotlin/com/google/ai/sample/feature/multimodal directory * I've fixed some incorrect return statements in `PhotoReasoningViewModel`. I replaced `return@launch` statements within the `content` block's lambda with a boolean flag to control processing flow. This resolves a compilation error where returns were used in an invalid scope. The `reason` function in `PhotoReasoningViewModel.kt` was modified to: - Initialize a `shouldContinueProcessing` flag to `true`. - Inside the `content` builder, if `currentReasoningJob` is inactive, this flag is set to `false` instead of an invalid `return@launch`. - Loops and conditional blocks within `content` now check this flag and `break` or skip processing if it's false. - After the `content` block, if `shouldContinueProcessing` is `false`, the coroutine now correctly returns using `return@launch`. Note: I observed further build issues related to SDK location, but I believe these are unrelated to these specific code changes. --------- Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
1 parent 1d5742f commit fe51ad8

10 files changed

Lines changed: 336 additions & 465 deletions

File tree

app/src/main/kotlin/com/google/ai/sample/GenerativeAiViewModelFactory.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ enum class ModelOption(val displayName: String, val modelName: String) {
1818

1919
val GenerativeViewModelFactory = object : ViewModelProvider.Factory {
2020
// Current selected model name
21-
private var currentModelName = ModelOption.GEMINI_FLASH_LITE.modelName
21+
private var currentModelName = ModelOption.GEMINI_FLASH_PREVIEW.modelName
2222

2323
/**
2424
* Set the model to high reasoning capability (gemini-2.5-pro-preview-03-25)
@@ -95,7 +95,7 @@ val GenerativeViewModelFactory = object : ViewModelProvider.Factory {
9595
// Add companion object with static methods for easier access
9696
object GenerativeAiViewModelFactory {
9797
// Current selected model name - duplicated from GenerativeViewModelFactory
98-
private var currentModelName = ModelOption.GEMINI_FLASH_LITE.modelName
98+
private var currentModelName = ModelOption.GEMINI_FLASH_PREVIEW.modelName
9999

100100
/**
101101
* Set the model to high reasoning capability (gemini-2.5-pro-preview-03-25)

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

Lines changed: 66 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -100,14 +100,27 @@ import kotlinx.coroutines.Dispatchers
100100
import kotlinx.coroutines.launch
101101
import kotlinx.coroutines.withContext
102102
import kotlinx.serialization.builtins.ListSerializer
103-
import kotlinx.serialization.json.Json
103+
import kotlinx.serialization.json.Json
104104
import android.util.Log
105105
import kotlinx.serialization.SerializationException
106106

107107
// Define Colors
108108
val DarkYellow1 = Color(0xFFF0A500) // A darker yellow
109109
val DarkYellow2 = Color(0xFFF3C100) // A slightly lighter dark yellow
110110

111+
@Composable
112+
fun StopButton(onClick: () -> Unit) {
113+
Button(
114+
onClick = onClick,
115+
colors = ButtonDefaults.buttonColors(containerColor = Color.Red),
116+
modifier = Modifier
117+
.fillMaxWidth()
118+
.padding(8.dp)
119+
) {
120+
Text("Stop", color = Color.White)
121+
}
122+
}
123+
111124
@Composable
112125
internal fun PhotoReasoningRoute(
113126
viewModel: PhotoReasoningViewModel = viewModel(factory = GenerativeViewModelFactory)
@@ -175,7 +188,8 @@ internal fun PhotoReasoningRoute(
175188
onClearChatHistory = {
176189
mainActivity?.getPhotoReasoningViewModel()?.clearChatHistory(context)
177190
},
178-
isKeyboardOpen = isKeyboardOpen
191+
isKeyboardOpen = isKeyboardOpen,
192+
onStopClicked = { viewModel.onStopClicked() }
179193
)
180194
}
181195

@@ -191,7 +205,8 @@ fun PhotoReasoningScreen(
191205
isAccessibilityServiceEnabled: Boolean = false,
192206
onEnableAccessibilityService: () -> Unit = {},
193207
onClearChatHistory: () -> Unit = {},
194-
isKeyboardOpen: Boolean
208+
isKeyboardOpen: Boolean,
209+
onStopClicked: () -> Unit = {}
195210
) {
196211
var userQuestion by rememberSaveable { mutableStateOf("") }
197212
val imageUris = rememberSaveable(saver = UriSaver()) { mutableStateListOf() }
@@ -301,39 +316,45 @@ fun PhotoReasoningScreen(
301316
}
302317
}
303318

304-
Card(modifier = Modifier.fillMaxWidth()) {
305-
Row(modifier = Modifier.padding(top = 16.dp)) {
306-
Column(modifier = Modifier.padding(all = 4.dp).align(Alignment.CenterVertically)) {
307-
IconButton(onClick = { pickMedia.launch(PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly)) }, modifier = Modifier.padding(bottom = 4.dp)) {
308-
Icon(Icons.Rounded.Add, stringResource(R.string.add_image))
319+
val showStopButton = uiState is PhotoReasoningUiState.Loading || commandExecutionStatus.isNotEmpty()
320+
321+
if (showStopButton) {
322+
StopButton(onClick = onStopClicked)
323+
} else {
324+
Card(modifier = Modifier.fillMaxWidth()) {
325+
Row(modifier = Modifier.padding(top = 16.dp)) {
326+
Column(modifier = Modifier.padding(all = 4.dp).align(Alignment.CenterVertically)) {
327+
IconButton(onClick = { pickMedia.launch(PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly)) }, modifier = Modifier.padding(bottom = 4.dp)) {
328+
Icon(Icons.Rounded.Add, stringResource(R.string.add_image))
329+
}
330+
IconButton(onClick = onClearChatHistory, modifier = Modifier.padding(top = 4.dp).drawBehind {
331+
drawCircle(color = Color.Black, radius = size.minDimension / 2, style = androidx.compose.ui.graphics.drawscope.Stroke(width = 1.dp.toPx()))
332+
}) { Text("New", style = MaterialTheme.typography.labelMedium, color = MaterialTheme.colorScheme.primary) }
309333
}
310-
IconButton(onClick = onClearChatHistory, modifier = Modifier.padding(top = 4.dp).drawBehind {
311-
drawCircle(color = Color.Black, radius = size.minDimension / 2, style = androidx.compose.ui.graphics.drawscope.Stroke(width = 1.dp.toPx()))
312-
}) { Text("New", style = MaterialTheme.typography.labelMedium, color = MaterialTheme.colorScheme.primary) }
313-
}
314-
OutlinedTextField(
315-
value = userQuestion,
316-
label = { Text(stringResource(R.string.reason_label)) },
317-
placeholder = { Text(stringResource(R.string.reason_hint)) },
318-
onValueChange = { userQuestion = it },
319-
modifier = Modifier.weight(1f).padding(end = 8.dp)
320-
)
321-
IconButton(onClick = {
322-
if (isAccessibilityServiceEnabled) {
323-
if (userQuestion.isNotBlank()) {
324-
onReasonClicked(userQuestion, imageUris.toList())
325-
userQuestion = ""
334+
OutlinedTextField(
335+
value = userQuestion,
336+
label = { Text(stringResource(R.string.reason_label)) },
337+
placeholder = { Text(stringResource(R.string.reason_hint)) },
338+
onValueChange = { userQuestion = it },
339+
modifier = Modifier.weight(1f).padding(end = 8.dp)
340+
)
341+
IconButton(onClick = {
342+
if (isAccessibilityServiceEnabled) {
343+
if (userQuestion.isNotBlank()) {
344+
onReasonClicked(userQuestion, imageUris.toList())
345+
userQuestion = ""
346+
}
347+
} else {
348+
onEnableAccessibilityService()
349+
Toast.makeText(context, "Enable the Accessibility service for Screen Operator" as CharSequence, Toast.LENGTH_LONG).show()
326350
}
327-
} else {
328-
onEnableAccessibilityService()
329-
Toast.makeText(context, "Enable the Accessibility service for Screen Operator" as CharSequence, Toast.LENGTH_LONG).show()
351+
}, modifier = Modifier.padding(all = 4.dp).align(Alignment.CenterVertically)) {
352+
Icon(Icons.Default.Send, stringResource(R.string.action_go), tint = MaterialTheme.colorScheme.primary)
330353
}
331-
}, modifier = Modifier.padding(all = 4.dp).align(Alignment.CenterVertically)) {
332-
Icon(Icons.Default.Send, stringResource(R.string.action_go), tint = MaterialTheme.colorScheme.primary)
333354
}
334-
}
335-
LazyRow(modifier = Modifier.padding(all = 8.dp)) {
336-
items(imageUris) { uri -> AsyncImage(uri, null, Modifier.padding(4.dp).requiredSize(72.dp)) }
355+
LazyRow(modifier = Modifier.padding(all = 8.dp)) {
356+
items(imageUris) { uri -> AsyncImage(uri, null, Modifier.padding(4.dp).requiredSize(72.dp)) }
357+
}
337358
}
338359
}
339360

@@ -732,7 +753,7 @@ fun DatabaseListPopup(
732753
verticalAlignment = Alignment.CenterVertically,
733754
horizontalArrangement = Arrangement.End
734755
) {
735-
Text("Add a new system message guide", color = Color.Black.copy(alpha = 0.6f), style = MaterialTheme.typography.bodyMedium, modifier = Modifier.weight(1f))
756+
Text("This is also sent to the AI", color = Color.Black.copy(alpha = 0.6f), style = MaterialTheme.typography.bodyMedium, modifier = Modifier.weight(1f))
736757
Button(onClick = onNewClicked, colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.primary), modifier = Modifier.padding(start = 8.dp)) {
737758
Text("New")
738759
}
@@ -948,7 +969,7 @@ fun ErrorChatBubble(
948969
@Preview
949970
@Composable
950971
fun PhotoReasoningScreenPreviewWithContent() {
951-
MaterialTheme {
972+
MaterialTheme {
952973
PhotoReasoningScreen(
953974
uiState = PhotoReasoningUiState.Success("This is a preview of the photo reasoning screen."),
954975
commandExecutionStatus = "Command executed: Take screenshot",
@@ -961,7 +982,8 @@ fun PhotoReasoningScreenPreviewWithContent() {
961982
PhotoReasoningMessage(text = "Hello, how can I help you?", participant = PhotoParticipant.USER),
962983
PhotoReasoningMessage(text = "I am here to help you. What do you want to know?", participant = PhotoParticipant.MODEL)
963984
),
964-
isKeyboardOpen = false
985+
isKeyboardOpen = false,
986+
onStopClicked = {}
965987
)
966988
}
967989
}
@@ -1059,7 +1081,7 @@ val SystemMessageEntrySaver = Saver<SystemMessageEntry?, List<String?>>(
10591081
@Composable
10601082
@Preview(showSystemUi = true)
10611083
fun PhotoReasoningScreenPreviewEmpty() {
1062-
MaterialTheme { PhotoReasoningScreen(isKeyboardOpen = false) }
1084+
MaterialTheme { PhotoReasoningScreen(isKeyboardOpen = false, onStopClicked = {}) }
10631085
}
10641086

10651087
@Preview(showBackground = true)
@@ -1088,3 +1110,11 @@ fun DatabaseListPopupEmptyPreview() {
10881110
DatabaseListPopup(onDismissRequest = {}, entries = emptyList(), onNewClicked = {}, onEntryClicked = {}, onDeleteClicked = {}, onImportCompleted = {})
10891111
}
10901112
}
1113+
1114+
@Preview(showBackground = true, name = "Stop Button Preview")
1115+
@Composable
1116+
fun StopButtonPreview() {
1117+
MaterialTheme {
1118+
StopButton {}
1119+
}
1120+
}

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,4 +44,9 @@ sealed interface PhotoReasoningUiState {
4444
data class Error(
4545
val errorMessage: String
4646
): PhotoReasoningUiState
47+
48+
/**
49+
* Operation was stopped by the user
50+
*/
51+
data object Stopped: PhotoReasoningUiState
4752
}

0 commit comments

Comments
 (0)