Skip to content
Merged
85 changes: 61 additions & 24 deletions app/src/main/java/com/nextcloud/client/assistant/AssistantScreen.kt
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ package com.nextcloud.client.assistant

import android.app.Activity
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
Expand Down Expand Up @@ -55,6 +56,7 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.nextcloud.client.assistant.chat.ChatContent
import com.nextcloud.client.assistant.chat.ChatViewModel
import com.nextcloud.client.assistant.conversation.ConversationScreen
import com.nextcloud.client.assistant.conversation.ConversationViewModel
import com.nextcloud.client.assistant.conversation.repository.MockConversationRemoteRepository
Expand Down Expand Up @@ -92,11 +94,13 @@ private const val PULL_TO_REFRESH_DELAY = 1500L
fun AssistantScreen(
composeViewModel: ComposeViewModel,
viewModel: AssistantViewModel,
chatViewModel: ChatViewModel,
conversationViewModel: ConversationViewModel,
capability: OCCapability,
activity: Activity
) {
val selectedText by composeViewModel.selectedText.collectAsState()
val sessionTitle by chatViewModel.sessionTitle.collectAsState()
val sessionId by viewModel.sessionId.collectAsState()
val messageId by viewModel.snackbarMessageId.collectAsState()
val screenOverlayState by viewModel.screenOverlayState.collectAsState()
Expand Down Expand Up @@ -140,12 +144,8 @@ fun AssistantScreen(
}
}

LaunchedEffect(sessionId) {
LaunchedEffect(Unit) {
viewModel.startPolling(sessionId)

sessionId?.let {
viewModel.fetchChatMessages(it)
}
}

DisposableEffect(Unit) {
Expand All @@ -164,8 +164,10 @@ fun AssistantScreen(
scope.launch {
pagerState.scrollToPage(AssistantPage.Content.id)
}
}, openChat = { newSessionId ->
viewModel.initSessionId(newSessionId)
}, openChat = { conversation ->
viewModel.updateInputBarText("")
chatViewModel.updateSessionTitle(conversation.timestamp)
chatViewModel.selectConversation(conversation.id)
taskTypes.getChat()?.let { chatTaskType ->
viewModel.selectTaskType(chatTaskType)
}
Expand All @@ -184,32 +186,52 @@ fun AssistantScreen(
scope.launch {
delay(PULL_TO_REFRESH_DELAY)

val newSessionId = sessionId
if (newSessionId != null) {
viewModel.fetchChatMessages(newSessionId)
val currentSessionId = sessionId
if (currentSessionId != null) {
chatViewModel.selectConversation(currentSessionId)
} else {
viewModel.fetchTaskList()
}
}
}
),
topBar = {
taskTypes?.let {
TaskTypesRow(selectedTaskType, data = it, selectTaskType = { task ->
viewModel.selectTaskType(task)
}, navigateToConversationList = {
scope.launch {
pagerState.scrollToPage(AssistantPage.Conversation.id)
Column(modifier = Modifier.fillMaxWidth(), horizontalAlignment = Alignment.Start) {
taskTypes?.let {
TaskTypesRow(selectedTaskType, data = it, selectTaskType = { task ->
viewModel.selectTaskType(task)
}, navigateToConversationList = {
scope.launch {
pagerState.scrollToPage(AssistantPage.Conversation.id)
}
})
}

if (selectedTaskType?.isChat() == true && sessionTitle != null) {
Column(
modifier = Modifier
.fillMaxWidth()
.background(MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f))
.padding(horizontal = 16.dp, vertical = 12.dp),
verticalArrangement = Arrangement.Center
) {
Text(
text = sessionTitle!!,
style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.onSurface,
maxLines = 1
)
}
})
}
}
},
bottomBar = {
if (!taskTypes.isNullOrEmpty() && selectedTaskType?.isTranslate() != true) {
InputBar(
sessionId,
selectedTaskType,
viewModel
viewModel,
chatViewModel
)
}
},
Expand Down Expand Up @@ -257,7 +279,7 @@ fun AssistantScreen(

AssistantScreenState.ChatContent -> {
ChatContent(
viewModel = viewModel,
chatViewModel = chatViewModel,
modifier = Modifier.padding(paddingValues)
)
}
Expand Down Expand Up @@ -301,9 +323,15 @@ fun AssistantScreen(

@Suppress("LongMethod")
@Composable
private fun InputBar(sessionId: Long?, selectedTaskType: TaskTypeData?, viewModel: AssistantViewModel) {
private fun InputBar(
sessionId: Long?,
selectedTaskType: TaskTypeData?,
viewModel: AssistantViewModel,
chatViewModel: ChatViewModel
) {
val scope = rememberCoroutineScope()
val text by viewModel.inputBarText.collectAsState()
val chatUIState by chatViewModel.uiState.collectAsState()

Surface(
tonalElevation = 3.dp,
Expand Down Expand Up @@ -349,9 +377,9 @@ private fun InputBar(sessionId: Long?, selectedTaskType: TaskTypeData?, viewMode
val taskType = selectedTaskType ?: return@IconButton
if (taskType.isChat()) {
if (sessionId != null) {
viewModel.sendChatMessage(content = text, sessionId)
chatViewModel.sendMessage(content = text, sessionId = sessionId)
} else {
viewModel.createConversation(text)
chatViewModel.startNewConversation(content = text)
}
} else {
viewModel.createTask(input = text, taskType = taskType)
Expand All @@ -361,12 +389,19 @@ private fun InputBar(sessionId: Long?, selectedTaskType: TaskTypeData?, viewMode
delay(CHAT_INPUT_DELAY)
viewModel.updateInputBarText("")
}
}
},
enabled = chatUIState.canSend()
) {
Icon(
painter = painterResource(id = R.drawable.ic_send),
contentDescription = stringResource(R.string.assistant_screen_send_message),
tint = MaterialTheme.colorScheme.primary
tint = if (chatUIState.canSend()) {
MaterialTheme.colorScheme.primary
} else {
colorResource(
R.color.disabled_text
)
}
)
}
}
Expand Down Expand Up @@ -498,6 +533,7 @@ private fun AssistantScreenPreview() {
composeViewModel = ComposeViewModel(),
conversationViewModel = getMockConversationViewModel(),
viewModel = getMockAssistantViewModel(false),
chatViewModel = ChatViewModel(MockAssistantRemoteRepository()),
activity = ComposeActivity(),
capability = OCCapability().apply {
versionMayor = 30
Expand All @@ -517,6 +553,7 @@ private fun AssistantEmptyScreenPreview() {
composeViewModel = ComposeViewModel(),
conversationViewModel = getMockConversationViewModel(),
viewModel = getMockAssistantViewModel(true),
chatViewModel = ChatViewModel(MockAssistantRemoteRepository()),
activity = ComposeActivity(),
capability = OCCapability().apply {
versionMayor = 30
Expand Down
105 changes: 3 additions & 102 deletions app/src/main/java/com/nextcloud/client/assistant/AssistantViewModel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,8 @@ import com.nextcloud.client.assistant.model.ScreenOverlayState
import com.nextcloud.client.assistant.repository.local.AssistantLocalRepository
import com.nextcloud.client.assistant.repository.remote.AssistantRemoteRepository
import com.nextcloud.utils.TimeConstants.MILLIS_PER_SECOND
import com.nextcloud.utils.extensions.isHuman
import com.owncloud.android.R
import com.owncloud.android.lib.common.utils.Log_OC
import com.owncloud.android.lib.resources.assistant.chat.model.ChatMessage
import com.owncloud.android.lib.resources.assistant.chat.model.ChatMessageRequest
import com.owncloud.android.lib.resources.assistant.v2.model.Task
import com.owncloud.android.lib.resources.assistant.v2.model.TaskTypeData
import kotlinx.coroutines.Dispatchers
Expand Down Expand Up @@ -75,14 +72,7 @@ class AssistantViewModel(
private val _filteredTaskList = MutableStateFlow<List<Task>?>(null)
val filteredTaskList: StateFlow<List<Task>?> = _filteredTaskList

private val _chatMessages = MutableStateFlow<List<ChatMessage>>(listOf())
val chatMessages: StateFlow<List<ChatMessage>> = _chatMessages

private val _isAssistantAnswering = MutableStateFlow(false)
val isAssistantAnswering: StateFlow<Boolean> = _isAssistantAnswering

private var pollingJob: Job? = null
private var currentChatTaskId: String? = null

init {
observeScreenState()
Expand All @@ -97,20 +87,8 @@ class AssistantViewModel(
try {
while (isActive) {
delay(POLLING_INTERVAL_MS)

val taskType = _selectedTaskType.value ?: continue

if (taskType.isChat() && sessionId != null) {
Log_OC.d(TAG, "Polling chat messages, sessionId: $sessionId")

if (currentChatTaskId == null) {
remoteRepository.generateSession(sessionId.toString())?.let {
currentChatTaskId = it.taskId.toString()
}
}

fetchNewChatMessage(sessionId)
} else if (!taskType.isChat()) {
if (!taskType.isChat()) {
Log_OC.d(TAG, "Polling task list")
pollTaskList()
}
Expand Down Expand Up @@ -143,37 +121,13 @@ class AssistantViewModel(
}
}

fun fetchNewChatMessage(sessionId: Long) = viewModelScope.launch(Dispatchers.IO) {
val taskId = currentChatTaskId ?: return@launch
val newMessage = remoteRepository.checkGeneration(taskId, sessionId.toString()) ?: return@launch

_chatMessages.update { current ->
val messageExists = current.any {
it.id == newMessage.id ||
(it.timestamp == newMessage.timestamp && it.content == newMessage.content)
}

if (messageExists) {
current
} else {
if (!newMessage.isHuman()) {
_isAssistantAnswering.update {
false
}
}
current + newMessage
}
}
}

private fun observeScreenState() {
viewModelScope.launch {
combine(
selectedTask,
_selectedTaskType,
_chatMessages,
_filteredTaskList
) { selectedTask, selectedTaskType, chats, tasks ->
) { selectedTask, selectedTaskType, tasks ->
val isChat = selectedTaskType?.isChat() == true
val isTranslation =
selectedTaskType?.isTranslate() == true && selectedTask?.isTranslate() == true
Expand All @@ -183,11 +137,9 @@ class AssistantViewModel(

isTranslation -> AssistantScreenState.Translation(selectedTask)

isChat && chats.isEmpty() -> AssistantScreenState.emptyChatList()

isChat -> AssistantScreenState.ChatContent

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

else -> {
if (!_isTranslationTask.value) {
Expand All @@ -203,48 +155,6 @@ class AssistantViewModel(
}
}

// region chat
fun sendChatMessage(content: String, sessionId: Long) = viewModelScope.launch(Dispatchers.IO) {
val request = ChatMessageRequest(
sessionId = sessionId.toString(),
role = "human",
content = content,
timestamp = System.currentTimeMillis() / MILLIS_PER_SECOND,
firstHumanMessage = _chatMessages.value.isEmpty()
)

remoteRepository.sendChatMessage(request)?.let { newMessage ->
_chatMessages.update { messages ->
messages + newMessage
}
_isAssistantAnswering.update {
true
}
} ?: updateSnackbarMessage(R.string.assistant_screen_chat_create_error)
}

fun fetchChatMessages(sessionId: Long) = viewModelScope.launch(Dispatchers.IO) {
remoteRepository.fetchChatMessages(sessionId)?.let { messageList ->
_chatMessages.update {
messageList
}
} ?: updateSnackbarMessage(R.string.assistant_screen_chat_fetch_error)
}

fun createConversation(title: String) = viewModelScope.launch(Dispatchers.IO) {
remoteRepository.createConversation(title)?.let { result ->
initSessionId(result.session.id)
sendChatMessage(title, result.session.id)
}
}

fun initSessionId(value: Long) {
Log_OC.d(TAG, "session id updated: $value")
currentChatTaskId = null
_sessionId.update { value }
}
// endregion

// region task
fun createTask(input: String, taskType: TaskTypeData) = viewModelScope.launch(Dispatchers.IO) {
val result = remoteRepository.createTask(input, taskType)
Expand Down Expand Up @@ -273,15 +183,6 @@ class AssistantViewModel(

if (!task.isChat()) {
fetchTaskList()
return
}

// only task chat type needs to be handled differently
val sessionId = _sessionId.value ?: return
if (_chatMessages.value.isEmpty()) {
fetchChatMessages(sessionId)
} else {
fetchNewChatMessage(sessionId)
}
}

Expand Down
Loading
Loading