Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 52 additions & 22 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 @@ -89,11 +91,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 @@ -130,12 +134,8 @@ fun AssistantScreen(
}
}

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

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

DisposableEffect(Unit) {
Expand All @@ -154,8 +154,9 @@ fun AssistantScreen(
scope.launch {
pagerState.scrollToPage(AssistantPage.Content.id)
}
}, openChat = { newSessionId ->
viewModel.initSessionId(newSessionId)
}, openChat = { conversation ->
chatViewModel.updateSessionTitle(conversation.timestamp)
chatViewModel.selectConversation(conversation.id)
taskTypes.getChat()?.let { chatTaskType ->
viewModel.selectTaskType(chatTaskType)
}
Expand All @@ -174,32 +175,54 @@ 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
)

Spacer(modifier = Modifier.height(8.dp))
}
})
}
}
},
bottomBar = {
if (!taskTypes.isNullOrEmpty() && selectedTaskType?.isTranslate() != true) {
InputBar(
sessionId,
selectedTaskType,
viewModel
viewModel,
chatViewModel
)
}
},
Expand Down Expand Up @@ -247,7 +270,7 @@ fun AssistantScreen(

AssistantScreenState.ChatContent -> {
ChatContent(
viewModel = viewModel,
chatViewModel = chatViewModel,
modifier = Modifier.padding(paddingValues)
)
}
Expand Down Expand Up @@ -291,7 +314,12 @@ 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()

Expand Down Expand Up @@ -339,9 +367,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 Down Expand Up @@ -474,6 +502,7 @@ private fun AssistantScreenPreview() {
composeViewModel = ComposeViewModel(),
conversationViewModel = getMockConversationViewModel(),
viewModel = getMockAssistantViewModel(false),
chatViewModel = ChatViewModel(MockAssistantRemoteRepository()),
activity = ComposeActivity(),
capability = OCCapability().apply {
versionMayor = 30
Expand All @@ -493,6 +522,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