Skip to content

Commit c553f70

Browse files
Add files via upload
1 parent 3197c56 commit c553f70

3 files changed

Lines changed: 257 additions & 127 deletions

File tree

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package com.google.ai.sample.feature.multimodal
2+
3+
/**
4+
* Data class representing a chat message in the conversation
5+
*/
6+
data class ChatMessage(
7+
val text: String,
8+
val isUser: Boolean,
9+
val timestamp: Long = System.currentTimeMillis()
10+
)

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

Lines changed: 151 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,10 @@ import androidx.compose.foundation.layout.fillMaxWidth
1313
import androidx.compose.foundation.layout.height
1414
import androidx.compose.foundation.layout.padding
1515
import androidx.compose.foundation.layout.requiredSize
16+
import androidx.compose.foundation.lazy.LazyColumn
1617
import androidx.compose.foundation.lazy.LazyRow
1718
import androidx.compose.foundation.lazy.items
19+
import androidx.compose.foundation.lazy.rememberLazyListState
1820
import androidx.compose.foundation.rememberScrollState
1921
import androidx.compose.foundation.verticalScroll
2022
import androidx.compose.material.icons.Icons
@@ -32,6 +34,7 @@ import androidx.compose.material3.Text
3234
import androidx.compose.material3.TextButton
3335
import androidx.compose.runtime.Composable
3436
import androidx.compose.runtime.DisposableEffect
37+
import androidx.compose.runtime.LaunchedEffect
3538
import androidx.compose.runtime.collectAsState
3639
import androidx.compose.runtime.getValue
3740
import androidx.compose.runtime.mutableStateListOf
@@ -71,6 +74,7 @@ internal fun PhotoReasoningRoute(
7174
val photoReasoningUiState by viewModel.uiState.collectAsState()
7275
val commandExecutionStatus by viewModel.commandExecutionStatus.collectAsState()
7376
val detectedCommands by viewModel.detectedCommands.collectAsState()
77+
val chatHistory by viewModel.chatHistory.collectAsState()
7478

7579
val coroutineScope = rememberCoroutineScope()
7680
val imageRequestBuilder = ImageRequest.Builder(LocalContext.current)
@@ -108,6 +112,7 @@ internal fun PhotoReasoningRoute(
108112
uiState = photoReasoningUiState,
109113
commandExecutionStatus = commandExecutionStatus,
110114
detectedCommands = detectedCommands,
115+
chatHistory = chatHistory,
111116
systemMessage = systemMessage,
112117
onSystemMessageChanged = { newMessage ->
113118
// Update the local state
@@ -161,6 +166,7 @@ fun PhotoReasoningScreen(
161166
uiState: PhotoReasoningUiState = PhotoReasoningUiState.Initial,
162167
commandExecutionStatus: String = "",
163168
detectedCommands: List<Command> = emptyList(),
169+
chatHistory: List<ChatMessage> = emptyList(),
164170
systemMessage: String = "",
165171
onSystemMessageChanged: (String) -> Unit = {},
166172
onReasonClicked: (String, List<Uri>) -> Unit = { _, _ -> },
@@ -169,6 +175,14 @@ fun PhotoReasoningScreen(
169175
) {
170176
var userQuestion by rememberSaveable { mutableStateOf("") }
171177
val imageUris = rememberSaveable(saver = UriSaver()) { mutableStateListOf() }
178+
val chatListState = rememberLazyListState()
179+
180+
// Auto-scroll to bottom when chat history changes
181+
LaunchedEffect(chatHistory.size) {
182+
if (chatHistory.isNotEmpty()) {
183+
chatListState.animateScrollToItem(chatHistory.size - 1)
184+
}
185+
}
172186

173187
val pickMedia = rememberLauncherForActivityResult(
174188
ActivityResultContracts.PickVisualMedia()
@@ -181,7 +195,6 @@ fun PhotoReasoningScreen(
181195
Column(
182196
modifier = Modifier
183197
.padding(all = 16.dp)
184-
.verticalScroll(rememberScrollState())
185198
) {
186199
// System Message Card (3 lines display height, but no text limit)
187200
Card(
@@ -196,7 +209,7 @@ fun PhotoReasoningScreen(
196209
modifier = Modifier.padding(16.dp)
197210
) {
198211
Text(
199-
text = "Systemnachricht:",
212+
text = "System Message:",
200213
style = MaterialTheme.typography.titleMedium,
201214
color = MaterialTheme.colorScheme.onPrimaryContainer
202215
)
@@ -210,7 +223,7 @@ fun PhotoReasoningScreen(
210223
.fillMaxWidth()
211224
.height(80.dp), // Fixed height for approximately 3 lines
212225
textStyle = MaterialTheme.typography.bodyMedium,
213-
placeholder = { Text("Geben Sie hier eine Systemnachricht ein...") },
226+
placeholder = { Text("Enter a system message here...") },
214227
// Allow scrolling within the field for longer text
215228
maxLines = 3,
216229
// No character limit
@@ -233,83 +246,50 @@ fun PhotoReasoningScreen(
233246
modifier = Modifier.padding(16.dp)
234247
) {
235248
Text(
236-
text = "Accessibility Service ist nicht aktiviert",
249+
text = "Accessibility Service is not enabled",
237250
color = MaterialTheme.colorScheme.error,
238251
style = MaterialTheme.typography.titleMedium
239252
)
240253
Spacer(modifier = Modifier.height(8.dp))
241254
Text(
242-
text = "Die Klick-Funktionalität benötigt den Accessibility Service. Bitte aktivieren Sie ihn in den Einstellungen.",
255+
text = "The click functionality requires the Accessibility Service. Please enable it in the settings.",
243256
color = MaterialTheme.colorScheme.error
244257
)
245258
Spacer(modifier = Modifier.height(8.dp))
246259
TextButton(
247260
onClick = onEnableAccessibilityService
248261
) {
249-
Text("Accessibility Service aktivieren")
262+
Text("Enable Accessibility Service")
250263
}
251264
}
252265
}
253266
}
254267

255-
// Input Card
256-
Card(
257-
modifier = Modifier.fillMaxWidth()
258-
) {
259-
Row(
260-
modifier = Modifier.padding(top = 16.dp)
268+
// Chat History
269+
if (chatHistory.isNotEmpty()) {
270+
Card(
271+
modifier = Modifier
272+
.fillMaxWidth()
273+
.weight(1f)
274+
.padding(bottom = 16.dp)
261275
) {
262-
IconButton(
263-
onClick = {
264-
pickMedia.launch(
265-
PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly)
266-
)
267-
},
276+
LazyColumn(
277+
state = chatListState,
268278
modifier = Modifier
269-
.padding(all = 4.dp)
270-
.align(Alignment.CenterVertically)
271-
) {
272-
Icon(
273-
Icons.Rounded.Add,
274-
contentDescription = stringResource(R.string.add_image),
275-
)
276-
}
277-
OutlinedTextField(
278-
value = userQuestion,
279-
label = { Text(stringResource(R.string.reason_label)) },
280-
placeholder = { Text(stringResource(R.string.reason_hint)) },
281-
onValueChange = { userQuestion = it },
282-
modifier = Modifier
283-
.fillMaxWidth(0.8f)
284-
)
285-
TextButton(
286-
onClick = {
287-
if (userQuestion.isNotBlank()) {
288-
onReasonClicked(userQuestion, imageUris.toList())
289-
}
290-
},
291-
modifier = Modifier
292-
.padding(all = 4.dp)
293-
.align(Alignment.CenterVertically)
279+
.fillMaxWidth()
280+
.padding(8.dp)
294281
) {
295-
Text(stringResource(R.string.action_go))
296-
}
297-
}
298-
LazyRow(
299-
modifier = Modifier.padding(all = 8.dp)
300-
) {
301-
items(imageUris) { imageUri ->
302-
AsyncImage(
303-
model = imageUri,
304-
contentDescription = null,
305-
modifier = Modifier
306-
.padding(4.dp)
307-
.requiredSize(72.dp)
308-
)
282+
items(chatHistory) { message ->
283+
ChatMessageItem(message)
284+
Divider(
285+
modifier = Modifier.padding(vertical = 8.dp),
286+
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.1f)
287+
)
288+
}
309289
}
310290
}
311291
}
312-
292+
313293
// Command Execution Status
314294
if (commandExecutionStatus.isNotEmpty()) {
315295
Card(
@@ -324,7 +304,7 @@ fun PhotoReasoningScreen(
324304
modifier = Modifier.padding(16.dp)
325305
) {
326306
Text(
327-
text = "Befehlsstatus:",
307+
text = "Command Status:",
328308
style = MaterialTheme.typography.titleMedium
329309
)
330310
Spacer(modifier = Modifier.height(4.dp))
@@ -350,16 +330,16 @@ fun PhotoReasoningScreen(
350330
modifier = Modifier.padding(16.dp)
351331
) {
352332
Text(
353-
text = "Erkannte Befehle:",
333+
text = "Detected Commands:",
354334
style = MaterialTheme.typography.titleMedium
355335
)
356336
Spacer(modifier = Modifier.height(4.dp))
357337

358338
detectedCommands.forEachIndexed { index, command ->
359339
val commandText = when (command) {
360-
is Command.ClickButton -> "Klick auf Button: \"${command.buttonText}\""
361-
is Command.TapCoordinates -> "Tippen auf Koordinaten: (${command.x}, ${command.y})"
362-
is Command.TakeScreenshot -> "Screenshot aufnehmen"
340+
is Command.ClickButton -> "Click button: \"${command.buttonText}\""
341+
is Command.TapCoordinates -> "Tap coordinates: (${command.x}, ${command.y})"
342+
is Command.TakeScreenshot -> "Take screenshot"
363343
}
364344

365345
Text(
@@ -378,6 +358,65 @@ fun PhotoReasoningScreen(
378358
}
379359
}
380360

361+
// Input Card
362+
Card(
363+
modifier = Modifier.fillMaxWidth()
364+
) {
365+
Row(
366+
modifier = Modifier.padding(top = 16.dp)
367+
) {
368+
IconButton(
369+
onClick = {
370+
pickMedia.launch(
371+
PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly)
372+
)
373+
},
374+
modifier = Modifier
375+
.padding(all = 4.dp)
376+
.align(Alignment.CenterVertically)
377+
) {
378+
Icon(
379+
Icons.Rounded.Add,
380+
contentDescription = stringResource(R.string.add_image),
381+
)
382+
}
383+
OutlinedTextField(
384+
value = userQuestion,
385+
label = { Text(stringResource(R.string.reason_label)) },
386+
placeholder = { Text(stringResource(R.string.reason_hint)) },
387+
onValueChange = { userQuestion = it },
388+
modifier = Modifier
389+
.fillMaxWidth(0.8f)
390+
)
391+
TextButton(
392+
onClick = {
393+
if (userQuestion.isNotBlank()) {
394+
onReasonClicked(userQuestion, imageUris.toList())
395+
userQuestion = "" // Clear input after sending
396+
}
397+
},
398+
modifier = Modifier
399+
.padding(all = 4.dp)
400+
.align(Alignment.CenterVertically)
401+
) {
402+
Text(stringResource(R.string.action_go))
403+
}
404+
}
405+
LazyRow(
406+
modifier = Modifier.padding(all = 8.dp)
407+
) {
408+
items(imageUris) { imageUri ->
409+
AsyncImage(
410+
model = imageUri,
411+
contentDescription = null,
412+
modifier = Modifier
413+
.padding(4.dp)
414+
.requiredSize(72.dp)
415+
)
416+
}
417+
}
418+
}
419+
381420
when (uiState) {
382421
PhotoReasoningUiState.Initial -> {
383422
// Nothing is shown
@@ -395,39 +434,7 @@ fun PhotoReasoningScreen(
395434
}
396435

397436
is PhotoReasoningUiState.Success -> {
398-
Card(
399-
modifier = Modifier
400-
.padding(vertical = 16.dp)
401-
.fillMaxWidth(),
402-
shape = MaterialTheme.shapes.large,
403-
colors = CardDefaults.cardColors(
404-
containerColor = MaterialTheme.colorScheme.onSecondaryContainer
405-
)
406-
) {
407-
Row(
408-
modifier = Modifier
409-
.padding(all = 16.dp)
410-
.fillMaxWidth()
411-
) {
412-
Icon(
413-
Icons.Outlined.Person,
414-
contentDescription = "Person Icon",
415-
tint = MaterialTheme.colorScheme.onSecondary,
416-
modifier = Modifier
417-
.requiredSize(36.dp)
418-
.drawBehind {
419-
drawCircle(color = Color.White)
420-
}
421-
)
422-
Text(
423-
text = uiState.outputText,
424-
color = MaterialTheme.colorScheme.onSecondary,
425-
modifier = Modifier
426-
.padding(start = 16.dp)
427-
.fillMaxWidth()
428-
)
429-
}
430-
}
437+
// Success state is now handled by the chat history
431438
}
432439

433440
is PhotoReasoningUiState.Error -> {
@@ -450,3 +457,51 @@ fun PhotoReasoningScreen(
450457
}
451458
}
452459
}
460+
461+
@Composable
462+
fun ChatMessageItem(message: ChatMessage) {
463+
val backgroundColor = if (message.isUser) {
464+
MaterialTheme.colorScheme.primaryContainer
465+
} else {
466+
MaterialTheme.colorScheme.secondaryContainer
467+
}
468+
469+
val textColor = if (message.isUser) {
470+
MaterialTheme.colorScheme.onPrimaryContainer
471+
} else {
472+
MaterialTheme.colorScheme.onSecondaryContainer
473+
}
474+
475+
Card(
476+
modifier = Modifier
477+
.fillMaxWidth()
478+
.padding(vertical = 4.dp),
479+
colors = CardDefaults.cardColors(
480+
containerColor = backgroundColor
481+
)
482+
) {
483+
Row(
484+
modifier = Modifier
485+
.padding(all = 12.dp)
486+
.fillMaxWidth()
487+
) {
488+
if (!message.isUser) {
489+
Icon(
490+
Icons.Outlined.Person,
491+
contentDescription = "AI Icon",
492+
tint = textColor,
493+
modifier = Modifier
494+
.requiredSize(24.dp)
495+
.padding(end = 8.dp)
496+
)
497+
}
498+
499+
Text(
500+
text = message.text,
501+
color = textColor,
502+
style = MaterialTheme.typography.bodyMedium,
503+
modifier = Modifier.weight(1f)
504+
)
505+
}
506+
}
507+
}

0 commit comments

Comments
 (0)