Skip to content

Commit df16b7f

Browse files
Merge pull request #16624 from nextcloud/fix/chat-task-flow
fix(assistant): chat task flow
2 parents b981fff + d95a894 commit df16b7f

11 files changed

Lines changed: 609 additions & 242 deletions

File tree

app/src/main/java/com/nextcloud/client/assistant/AssistantScreen.kt

Lines changed: 61 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ package com.nextcloud.client.assistant
99

1010
import android.app.Activity
1111
import androidx.compose.foundation.Image
12+
import androidx.compose.foundation.background
1213
import androidx.compose.foundation.layout.Arrangement
1314
import androidx.compose.foundation.layout.Column
1415
import androidx.compose.foundation.layout.PaddingValues
@@ -55,6 +56,7 @@ import androidx.compose.ui.tooling.preview.Preview
5556
import androidx.compose.ui.unit.dp
5657
import androidx.compose.ui.unit.sp
5758
import com.nextcloud.client.assistant.chat.ChatContent
59+
import com.nextcloud.client.assistant.chat.ChatViewModel
5860
import com.nextcloud.client.assistant.conversation.ConversationScreen
5961
import com.nextcloud.client.assistant.conversation.ConversationViewModel
6062
import com.nextcloud.client.assistant.conversation.repository.MockConversationRemoteRepository
@@ -92,11 +94,13 @@ private const val PULL_TO_REFRESH_DELAY = 1500L
9294
fun AssistantScreen(
9395
composeViewModel: ComposeViewModel,
9496
viewModel: AssistantViewModel,
97+
chatViewModel: ChatViewModel,
9598
conversationViewModel: ConversationViewModel,
9699
capability: OCCapability,
97100
activity: Activity
98101
) {
99102
val selectedText by composeViewModel.selectedText.collectAsState()
103+
val sessionTitle by chatViewModel.sessionTitle.collectAsState()
100104
val sessionId by viewModel.sessionId.collectAsState()
101105
val messageId by viewModel.snackbarMessageId.collectAsState()
102106
val screenOverlayState by viewModel.screenOverlayState.collectAsState()
@@ -140,12 +144,8 @@ fun AssistantScreen(
140144
}
141145
}
142146

143-
LaunchedEffect(sessionId) {
147+
LaunchedEffect(Unit) {
144148
viewModel.startPolling(sessionId)
145-
146-
sessionId?.let {
147-
viewModel.fetchChatMessages(it)
148-
}
149149
}
150150

151151
DisposableEffect(Unit) {
@@ -164,8 +164,10 @@ fun AssistantScreen(
164164
scope.launch {
165165
pagerState.scrollToPage(AssistantPage.Content.id)
166166
}
167-
}, openChat = { newSessionId ->
168-
viewModel.initSessionId(newSessionId)
167+
}, openChat = { conversation ->
168+
viewModel.updateInputBarText("")
169+
chatViewModel.updateSessionTitle(conversation.timestamp)
170+
chatViewModel.selectConversation(conversation.id)
169171
taskTypes.getChat()?.let { chatTaskType ->
170172
viewModel.selectTaskType(chatTaskType)
171173
}
@@ -184,32 +186,52 @@ fun AssistantScreen(
184186
scope.launch {
185187
delay(PULL_TO_REFRESH_DELAY)
186188

187-
val newSessionId = sessionId
188-
if (newSessionId != null) {
189-
viewModel.fetchChatMessages(newSessionId)
189+
val currentSessionId = sessionId
190+
if (currentSessionId != null) {
191+
chatViewModel.selectConversation(currentSessionId)
190192
} else {
191193
viewModel.fetchTaskList()
192194
}
193195
}
194196
}
195197
),
196198
topBar = {
197-
taskTypes?.let {
198-
TaskTypesRow(selectedTaskType, data = it, selectTaskType = { task ->
199-
viewModel.selectTaskType(task)
200-
}, navigateToConversationList = {
201-
scope.launch {
202-
pagerState.scrollToPage(AssistantPage.Conversation.id)
199+
Column(modifier = Modifier.fillMaxWidth(), horizontalAlignment = Alignment.Start) {
200+
taskTypes?.let {
201+
TaskTypesRow(selectedTaskType, data = it, selectTaskType = { task ->
202+
viewModel.selectTaskType(task)
203+
}, navigateToConversationList = {
204+
scope.launch {
205+
pagerState.scrollToPage(AssistantPage.Conversation.id)
206+
}
207+
})
208+
}
209+
210+
if (selectedTaskType?.isChat() == true && sessionTitle != null) {
211+
Column(
212+
modifier = Modifier
213+
.fillMaxWidth()
214+
.background(MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f))
215+
.padding(horizontal = 16.dp, vertical = 12.dp),
216+
verticalArrangement = Arrangement.Center
217+
) {
218+
Text(
219+
text = sessionTitle!!,
220+
style = MaterialTheme.typography.titleMedium,
221+
color = MaterialTheme.colorScheme.onSurface,
222+
maxLines = 1
223+
)
203224
}
204-
})
225+
}
205226
}
206227
},
207228
bottomBar = {
208229
if (!taskTypes.isNullOrEmpty() && selectedTaskType?.isTranslate() != true) {
209230
InputBar(
210231
sessionId,
211232
selectedTaskType,
212-
viewModel
233+
viewModel,
234+
chatViewModel
213235
)
214236
}
215237
},
@@ -257,7 +279,7 @@ fun AssistantScreen(
257279

258280
AssistantScreenState.ChatContent -> {
259281
ChatContent(
260-
viewModel = viewModel,
282+
chatViewModel = chatViewModel,
261283
modifier = Modifier.padding(paddingValues)
262284
)
263285
}
@@ -301,9 +323,15 @@ fun AssistantScreen(
301323

302324
@Suppress("LongMethod")
303325
@Composable
304-
private fun InputBar(sessionId: Long?, selectedTaskType: TaskTypeData?, viewModel: AssistantViewModel) {
326+
private fun InputBar(
327+
sessionId: Long?,
328+
selectedTaskType: TaskTypeData?,
329+
viewModel: AssistantViewModel,
330+
chatViewModel: ChatViewModel
331+
) {
305332
val scope = rememberCoroutineScope()
306333
val text by viewModel.inputBarText.collectAsState()
334+
val chatUIState by chatViewModel.uiState.collectAsState()
307335

308336
Surface(
309337
tonalElevation = 3.dp,
@@ -349,9 +377,9 @@ private fun InputBar(sessionId: Long?, selectedTaskType: TaskTypeData?, viewMode
349377
val taskType = selectedTaskType ?: return@IconButton
350378
if (taskType.isChat()) {
351379
if (sessionId != null) {
352-
viewModel.sendChatMessage(content = text, sessionId)
380+
chatViewModel.sendMessage(content = text, sessionId = sessionId)
353381
} else {
354-
viewModel.createConversation(text)
382+
chatViewModel.startNewConversation(content = text)
355383
}
356384
} else {
357385
viewModel.createTask(input = text, taskType = taskType)
@@ -361,12 +389,19 @@ private fun InputBar(sessionId: Long?, selectedTaskType: TaskTypeData?, viewMode
361389
delay(CHAT_INPUT_DELAY)
362390
viewModel.updateInputBarText("")
363391
}
364-
}
392+
},
393+
enabled = chatUIState.canSend()
365394
) {
366395
Icon(
367396
painter = painterResource(id = R.drawable.ic_send),
368397
contentDescription = stringResource(R.string.assistant_screen_send_message),
369-
tint = MaterialTheme.colorScheme.primary
398+
tint = if (chatUIState.canSend()) {
399+
MaterialTheme.colorScheme.primary
400+
} else {
401+
colorResource(
402+
R.color.disabled_text
403+
)
404+
}
370405
)
371406
}
372407
}
@@ -498,6 +533,7 @@ private fun AssistantScreenPreview() {
498533
composeViewModel = ComposeViewModel(),
499534
conversationViewModel = getMockConversationViewModel(),
500535
viewModel = getMockAssistantViewModel(false),
536+
chatViewModel = ChatViewModel(MockAssistantRemoteRepository()),
501537
activity = ComposeActivity(),
502538
capability = OCCapability().apply {
503539
versionMayor = 30
@@ -517,6 +553,7 @@ private fun AssistantEmptyScreenPreview() {
517553
composeViewModel = ComposeViewModel(),
518554
conversationViewModel = getMockConversationViewModel(),
519555
viewModel = getMockAssistantViewModel(true),
556+
chatViewModel = ChatViewModel(MockAssistantRemoteRepository()),
520557
activity = ComposeActivity(),
521558
capability = OCCapability().apply {
522559
versionMayor = 30

app/src/main/java/com/nextcloud/client/assistant/AssistantViewModel.kt

Lines changed: 3 additions & 102 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,8 @@ import com.nextcloud.client.assistant.model.ScreenOverlayState
1414
import com.nextcloud.client.assistant.repository.local.AssistantLocalRepository
1515
import com.nextcloud.client.assistant.repository.remote.AssistantRemoteRepository
1616
import com.nextcloud.utils.TimeConstants.MILLIS_PER_SECOND
17-
import com.nextcloud.utils.extensions.isHuman
1817
import com.owncloud.android.R
1918
import com.owncloud.android.lib.common.utils.Log_OC
20-
import com.owncloud.android.lib.resources.assistant.chat.model.ChatMessage
21-
import com.owncloud.android.lib.resources.assistant.chat.model.ChatMessageRequest
2219
import com.owncloud.android.lib.resources.assistant.v2.model.Task
2320
import com.owncloud.android.lib.resources.assistant.v2.model.TaskTypeData
2421
import kotlinx.coroutines.Dispatchers
@@ -75,14 +72,7 @@ class AssistantViewModel(
7572
private val _filteredTaskList = MutableStateFlow<List<Task>?>(null)
7673
val filteredTaskList: StateFlow<List<Task>?> = _filteredTaskList
7774

78-
private val _chatMessages = MutableStateFlow<List<ChatMessage>>(listOf())
79-
val chatMessages: StateFlow<List<ChatMessage>> = _chatMessages
80-
81-
private val _isAssistantAnswering = MutableStateFlow(false)
82-
val isAssistantAnswering: StateFlow<Boolean> = _isAssistantAnswering
83-
8475
private var pollingJob: Job? = null
85-
private var currentChatTaskId: String? = null
8676

8777
init {
8878
observeScreenState()
@@ -97,20 +87,8 @@ class AssistantViewModel(
9787
try {
9888
while (isActive) {
9989
delay(POLLING_INTERVAL_MS)
100-
10190
val taskType = _selectedTaskType.value ?: continue
102-
103-
if (taskType.isChat() && sessionId != null) {
104-
Log_OC.d(TAG, "Polling chat messages, sessionId: $sessionId")
105-
106-
if (currentChatTaskId == null) {
107-
remoteRepository.generateSession(sessionId.toString())?.let {
108-
currentChatTaskId = it.taskId.toString()
109-
}
110-
}
111-
112-
fetchNewChatMessage(sessionId)
113-
} else if (!taskType.isChat()) {
91+
if (!taskType.isChat()) {
11492
Log_OC.d(TAG, "Polling task list")
11593
pollTaskList()
11694
}
@@ -143,37 +121,13 @@ class AssistantViewModel(
143121
}
144122
}
145123

146-
fun fetchNewChatMessage(sessionId: Long) = viewModelScope.launch(Dispatchers.IO) {
147-
val taskId = currentChatTaskId ?: return@launch
148-
val newMessage = remoteRepository.checkGeneration(taskId, sessionId.toString()) ?: return@launch
149-
150-
_chatMessages.update { current ->
151-
val messageExists = current.any {
152-
it.id == newMessage.id ||
153-
(it.timestamp == newMessage.timestamp && it.content == newMessage.content)
154-
}
155-
156-
if (messageExists) {
157-
current
158-
} else {
159-
if (!newMessage.isHuman()) {
160-
_isAssistantAnswering.update {
161-
false
162-
}
163-
}
164-
current + newMessage
165-
}
166-
}
167-
}
168-
169124
private fun observeScreenState() {
170125
viewModelScope.launch {
171126
combine(
172127
selectedTask,
173128
_selectedTaskType,
174-
_chatMessages,
175129
_filteredTaskList
176-
) { selectedTask, selectedTaskType, chats, tasks ->
130+
) { selectedTask, selectedTaskType, tasks ->
177131
val isChat = selectedTaskType?.isChat() == true
178132
val isTranslation =
179133
selectedTaskType?.isTranslate() == true && selectedTask?.isTranslate() == true
@@ -183,11 +137,9 @@ class AssistantViewModel(
183137

184138
isTranslation -> AssistantScreenState.Translation(selectedTask)
185139

186-
isChat && chats.isEmpty() -> AssistantScreenState.emptyChatList()
187-
188140
isChat -> AssistantScreenState.ChatContent
189141

190-
!isChat && (tasks == null || tasks.isEmpty()) -> AssistantScreenState.emptyTaskList()
142+
!isChat && tasks.isNullOrEmpty() -> AssistantScreenState.emptyTaskList()
191143

192144
else -> {
193145
if (!_isTranslationTask.value) {
@@ -203,48 +155,6 @@ class AssistantViewModel(
203155
}
204156
}
205157

206-
// region chat
207-
fun sendChatMessage(content: String, sessionId: Long) = viewModelScope.launch(Dispatchers.IO) {
208-
val request = ChatMessageRequest(
209-
sessionId = sessionId.toString(),
210-
role = "human",
211-
content = content,
212-
timestamp = System.currentTimeMillis() / MILLIS_PER_SECOND,
213-
firstHumanMessage = _chatMessages.value.isEmpty()
214-
)
215-
216-
remoteRepository.sendChatMessage(request)?.let { newMessage ->
217-
_chatMessages.update { messages ->
218-
messages + newMessage
219-
}
220-
_isAssistantAnswering.update {
221-
true
222-
}
223-
} ?: updateSnackbarMessage(R.string.assistant_screen_chat_create_error)
224-
}
225-
226-
fun fetchChatMessages(sessionId: Long) = viewModelScope.launch(Dispatchers.IO) {
227-
remoteRepository.fetchChatMessages(sessionId)?.let { messageList ->
228-
_chatMessages.update {
229-
messageList
230-
}
231-
} ?: updateSnackbarMessage(R.string.assistant_screen_chat_fetch_error)
232-
}
233-
234-
fun createConversation(title: String) = viewModelScope.launch(Dispatchers.IO) {
235-
remoteRepository.createConversation(title)?.let { result ->
236-
initSessionId(result.session.id)
237-
sendChatMessage(title, result.session.id)
238-
}
239-
}
240-
241-
fun initSessionId(value: Long) {
242-
Log_OC.d(TAG, "session id updated: $value")
243-
currentChatTaskId = null
244-
_sessionId.update { value }
245-
}
246-
// endregion
247-
248158
// region task
249159
fun createTask(input: String, taskType: TaskTypeData) = viewModelScope.launch(Dispatchers.IO) {
250160
val result = remoteRepository.createTask(input, taskType)
@@ -273,15 +183,6 @@ class AssistantViewModel(
273183

274184
if (!task.isChat()) {
275185
fetchTaskList()
276-
return
277-
}
278-
279-
// only task chat type needs to be handled differently
280-
val sessionId = _sessionId.value ?: return
281-
if (_chatMessages.value.isEmpty()) {
282-
fetchChatMessages(sessionId)
283-
} else {
284-
fetchNewChatMessage(sessionId)
285186
}
286187
}
287188

0 commit comments

Comments
 (0)