@@ -10,38 +10,46 @@ package com.example.executorchllamademo.ui.screens
1010
1111import androidx.compose.foundation.background
1212import androidx.compose.foundation.clickable
13- import androidx.compose.foundation.interaction.MutableInteractionSource
13+ import androidx.compose.foundation.gestures.detectTapGestures
14+ import androidx.compose.foundation.layout.Box
1415import androidx.compose.foundation.layout.Column
1516import androidx.compose.foundation.layout.Row
1617import androidx.compose.foundation.layout.fillMaxSize
1718import androidx.compose.foundation.layout.fillMaxWidth
1819import androidx.compose.foundation.layout.imePadding
1920import androidx.compose.foundation.layout.padding
21+ import androidx.compose.foundation.layout.size
2022import androidx.compose.foundation.lazy.LazyColumn
21- import androidx.compose.foundation.lazy.itemsIndexed
23+ import androidx.compose.foundation.lazy.items
2224import androidx.compose.foundation.lazy.rememberLazyListState
2325import androidx.compose.material.icons.Icons
2426import androidx.compose.material.icons.filled.ArrowBack
2527import androidx.compose.material.icons.filled.Article
28+ import androidx.compose.material.icons.filled.KeyboardArrowDown
2629import androidx.compose.material.icons.filled.SwapHoriz
2730import androidx.compose.material3.AlertDialog
2831import androidx.compose.material3.ExperimentalMaterial3Api
2932import androidx.compose.material3.Icon
3033import androidx.compose.material3.IconButton
3134import androidx.compose.material3.RadioButton
3235import androidx.compose.material3.Scaffold
36+ import androidx.compose.material3.SmallFloatingActionButton
3337import androidx.compose.material3.Text
3438import androidx.compose.material3.TextButton
3539import androidx.compose.material3.TopAppBar
3640import androidx.compose.material3.TopAppBarDefaults
3741import androidx.compose.runtime.Composable
3842import androidx.compose.runtime.DisposableEffect
3943import androidx.compose.runtime.LaunchedEffect
44+ import androidx.compose.runtime.derivedStateOf
4045import androidx.compose.runtime.getValue
4146import androidx.compose.runtime.mutableStateOf
4247import androidx.compose.runtime.remember
48+ import androidx.compose.runtime.rememberCoroutineScope
4349import androidx.compose.runtime.setValue
50+ import androidx.compose.ui.Alignment
4451import androidx.compose.ui.Modifier
52+ import androidx.compose.ui.input.pointer.pointerInput
4553import androidx.compose.ui.platform.LocalFocusManager
4654import androidx.compose.ui.platform.LocalLifecycleOwner
4755import androidx.compose.ui.res.stringResource
@@ -53,6 +61,7 @@ import com.example.executorchllamademo.ui.components.MessageItem
5361import com.example.executorchllamademo.ui.theme.LocalAppColors
5462import com.example.executorchllamademo.ui.viewmodel.ChatViewModel
5563import kotlinx.coroutines.delay
64+ import kotlinx.coroutines.launch
5665import androidx.lifecycle.Lifecycle
5766import androidx.lifecycle.LifecycleEventObserver
5867
@@ -72,14 +81,40 @@ fun ChatScreen(
7281 val listState = rememberLazyListState()
7382 val appColors = LocalAppColors .current
7483 val focusManager = LocalFocusManager .current
84+ val coroutineScope = rememberCoroutineScope()
7585
76- // Auto-scroll to bottom when new messages are added or content changes during generation
77- LaunchedEffect (viewModel.messages.size, viewModel.scrollTrigger) {
86+ // Detect whether the user is near the bottom of the list
87+ val isNearBottom by remember {
88+ derivedStateOf {
89+ val layoutInfo = listState.layoutInfo
90+ val lastVisible = layoutInfo.visibleItemsInfo.lastOrNull()?.index ? : 0
91+ lastVisible >= layoutInfo.totalItemsCount - 2
92+ }
93+ }
94+
95+ // Scroll to bottom when a new message is added (user sent or new response placeholder)
96+ LaunchedEffect (viewModel.messages.size) {
7897 if (viewModel.messages.isNotEmpty()) {
7998 listState.animateScrollToItem(viewModel.messages.size - 1 )
8099 }
81100 }
82101
102+ // During generation: poll and scroll only if user is near bottom (throttled)
103+ LaunchedEffect (viewModel.isGenerating) {
104+ if (viewModel.isGenerating) {
105+ while (viewModel.isGenerating) {
106+ delay(300 )
107+ if (isNearBottom && viewModel.messages.isNotEmpty()) {
108+ listState.scrollToItem(viewModel.messages.size - 1 )
109+ }
110+ }
111+ // Final animated scroll when generation completes
112+ if (isNearBottom && viewModel.messages.isNotEmpty()) {
113+ listState.animateScrollToItem(viewModel.messages.size - 1 )
114+ }
115+ }
116+ }
117+
83118 // Periodically update memory usage
84119 LaunchedEffect (Unit ) {
85120 while (true ) {
@@ -165,27 +200,52 @@ fun ChatScreen(
165200 .fillMaxSize()
166201 .padding(paddingValues)
167202 .imePadding()
168- .clickable(
169- indication = null ,
170- interactionSource = remember { MutableInteractionSource () }
171- ) {
172- focusManager.clearFocus()
203+ .pointerInput(Unit ) {
204+ detectTapGestures(onTap = { focusManager.clearFocus() })
173205 }
174206 ) {
175- // Messages list
176- LazyColumn (
177- state = listState,
207+ // Messages list with scroll-to-bottom FAB overlay
208+ Box (
178209 modifier = Modifier
179210 .weight(1f )
180211 .fillMaxWidth()
181- .background(appColors.chatBackground)
182- .padding(horizontal = 8 .dp)
183212 ) {
184- itemsIndexed(
185- items = viewModel.messages,
186- key = { index, message -> " ${index} _${message.timestamp} _${message.promptID} " }
187- ) { _, message ->
188- MessageItem (message = message)
213+ LazyColumn (
214+ state = listState,
215+ modifier = Modifier
216+ .fillMaxSize()
217+ .background(appColors.chatBackground)
218+ .padding(horizontal = 8 .dp)
219+ ) {
220+ items(
221+ items = viewModel.messages,
222+ key = { message -> message.id }
223+ ) { message ->
224+ MessageItem (message = message)
225+ }
226+ }
227+
228+ // Scroll-to-bottom FAB: shown when user scrolls up
229+ if (! isNearBottom && viewModel.messages.isNotEmpty()) {
230+ SmallFloatingActionButton (
231+ onClick = {
232+ coroutineScope.launch {
233+ listState.animateScrollToItem(viewModel.messages.size - 1 )
234+ }
235+ },
236+ modifier = Modifier
237+ .align(Alignment .BottomEnd )
238+ .padding(12 .dp)
239+ .size(36 .dp),
240+ containerColor = appColors.navBar
241+ ) {
242+ Icon (
243+ imageVector = Icons .Filled .KeyboardArrowDown ,
244+ contentDescription = " Scroll to bottom" ,
245+ tint = appColors.textOnNavBar,
246+ modifier = Modifier .size(20 .dp)
247+ )
248+ }
189249 }
190250 }
191251
0 commit comments