@@ -13,8 +13,10 @@ import androidx.compose.foundation.layout.fillMaxWidth
1313import androidx.compose.foundation.layout.height
1414import androidx.compose.foundation.layout.padding
1515import androidx.compose.foundation.layout.requiredSize
16+ import androidx.compose.foundation.lazy.LazyColumn
1617import androidx.compose.foundation.lazy.LazyRow
1718import androidx.compose.foundation.lazy.items
19+ import androidx.compose.foundation.lazy.rememberLazyListState
1820import androidx.compose.foundation.rememberScrollState
1921import androidx.compose.foundation.verticalScroll
2022import androidx.compose.material.icons.Icons
@@ -32,6 +34,7 @@ import androidx.compose.material3.Text
3234import androidx.compose.material3.TextButton
3335import androidx.compose.runtime.Composable
3436import androidx.compose.runtime.DisposableEffect
37+ import androidx.compose.runtime.LaunchedEffect
3538import androidx.compose.runtime.collectAsState
3639import androidx.compose.runtime.getValue
3740import 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