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
1 change: 1 addition & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -347,6 +347,7 @@ dependencies {
implementation(libs.compose.ui)
implementation(libs.compose.ui.graphics)
implementation(libs.compose.material3)
implementation(libs.compose.activity)
implementation(libs.compose.ui.tooling.preview)
implementation(libs.foundation)
debugImplementation(libs.compose.ui.tooling)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.LinearProgressIndicator
Expand Down Expand Up @@ -65,6 +66,8 @@ import com.nextcloud.client.assistant.repository.local.MockAssistantLocalReposit
import com.nextcloud.client.assistant.repository.remote.MockAssistantRemoteRepository
import com.nextcloud.client.assistant.task.TaskView
import com.nextcloud.client.assistant.taskTypes.TaskTypesRow
import com.nextcloud.client.assistant.translate.TranslationScreen
import com.nextcloud.client.assistant.translate.TranslationViewModel
import com.nextcloud.ui.composeActivity.ComposeActivity
import com.nextcloud.ui.composeActivity.ComposeViewModel
import com.nextcloud.ui.composeComponents.alertDialog.SimpleAlertDialog
Expand Down Expand Up @@ -95,6 +98,7 @@ fun AssistantScreen(
val messageId by viewModel.snackbarMessageId.collectAsState()
val screenOverlayState by viewModel.screenOverlayState.collectAsState()
val selectedTaskType by viewModel.selectedTaskType.collectAsState()
val isTranslationTask by viewModel.isTranslationTask.collectAsState()
val filteredTaskList by viewModel.filteredTaskList.collectAsState()
val screenState by viewModel.screenState.collectAsState()
val taskTypes by viewModel.taskTypes.collectAsState()
Expand Down Expand Up @@ -160,6 +164,7 @@ fun AssistantScreen(
}
})
}

AssistantPage.Content.id -> {
Scaffold(
modifier = Modifier.pullToRefresh(
Expand Down Expand Up @@ -190,7 +195,7 @@ fun AssistantScreen(
}
},
bottomBar = {
if (!taskTypes.isNullOrEmpty()) {
if (!taskTypes.isNullOrEmpty() && selectedTaskType?.isTranslate() != true) {
InputBar(
sessionId,
selectedTaskType,
Expand All @@ -200,6 +205,24 @@ fun AssistantScreen(
},
snackbarHost = {
SnackbarHost(snackbarHostState)
},
floatingActionButton = {
if (selectedTaskType?.isTranslate() == true && !isTranslationTask) {
FloatingActionButton(onClick = {
viewModel.updateTranslationTaskState(true)
viewModel.updateScreenState(AssistantScreenState.Translation(null))
}, content = {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.padding(16.dp)
) {
Icon(
painter = painterResource(R.drawable.ic_plus),
contentDescription = "translate button"
)
}
})
}
}
) { paddingValues ->
when (screenState) {
Expand Down Expand Up @@ -229,6 +252,23 @@ fun AssistantScreen(
)
}

is AssistantScreenState.Translation -> {
selectedTaskType?.let {
val task = (screenState as AssistantScreenState.Translation).task
val textToTranslate = task?.input?.input ?: selectedText ?: ""

val translationViewModel =
TranslationViewModel(remoteRepository = viewModel.getRemoteRepository())

translationViewModel.init(it, task, textToTranslate)

TranslationScreen(
viewModel = translationViewModel,
assistantViewModel = viewModel
)
}
}

else -> EmptyContent(
paddingValues,
iconId = R.drawable.spinner_inner,
Expand Down Expand Up @@ -371,6 +411,7 @@ private fun TaskContent(
items(taskList, key = { it.id }) { task ->
TaskView(
task,
viewModel,
capability,
showTaskActions = {
val newState = ScreenOverlayState.TaskActions(task)
Expand Down Expand Up @@ -468,7 +509,7 @@ private fun getMockConversationViewModel(): ConversationViewModel {
)
}

private fun getMockAssistantViewModel(giveEmptyTasks: Boolean): AssistantViewModel {
fun getMockAssistantViewModel(giveEmptyTasks: Boolean): AssistantViewModel {
val mockLocalRepository = MockAssistantLocalRepository()
val mockRemoteRepository = MockAssistantRemoteRepository(giveEmptyTasks)
return AssistantViewModel(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ class AssistantViewModel(
private const val POLLING_INTERVAL_MS = 15_000L
}

private val _inputBarText = MutableStateFlow<String>("")
private val _inputBarText = MutableStateFlow("")
val inputBarText: StateFlow<String> = _inputBarText

private val _screenState = MutableStateFlow<AssistantScreenState?>(null)
Expand All @@ -59,6 +59,11 @@ class AssistantViewModel(
private val _snackbarMessageId = MutableStateFlow<Int?>(null)
val snackbarMessageId: StateFlow<Int?> = _snackbarMessageId

private val _isTranslationTask = MutableStateFlow(false)
val isTranslationTask: StateFlow<Boolean> = _isTranslationTask

private val selectedTask = MutableStateFlow<Task?>(null)

private val _selectedTaskType = MutableStateFlow<TaskTypeData?>(null)
val selectedTaskType: StateFlow<TaskTypeData?> = _selectedTaskType

Expand Down Expand Up @@ -123,12 +128,13 @@ class AssistantViewModel(
// endregion

private suspend fun pollTaskList() {
val cachedTasks = localRepository.getCachedTasks(accountName)
val taskType = _selectedTaskType.value?.id ?: return

val cachedTasks = localRepository.getCachedTasks(accountName, taskType)
if (cachedTasks.isNotEmpty()) {
_filteredTaskList.value = cachedTasks.sortedByDescending { it.id }
}

val taskType = _selectedTaskType.value?.id ?: return
val result = remoteRepository.getTaskList(taskType)
if (result != null) {
taskList = result
Expand Down Expand Up @@ -163,18 +169,28 @@ class AssistantViewModel(
private fun observeScreenState() {
viewModelScope.launch {
combine(
selectedTask,
_selectedTaskType,
_chatMessages,
_filteredTaskList
) { selectedTask, chats, tasks ->
val isChat = selectedTask?.isChat() == true
) { selectedTask, selectedTaskType, chats, tasks ->
val isChat = selectedTaskType?.isChat() == true
val isTranslation =
selectedTaskType?.isTranslate() == true && selectedTask?.isTranslate() == true

when {
selectedTask == null -> AssistantScreenState.Loading
selectedTaskType == null -> AssistantScreenState.Loading
isTranslation -> AssistantScreenState.Translation(selectedTask)
isChat && chats.isEmpty() -> AssistantScreenState.emptyChatList()
isChat -> AssistantScreenState.ChatContent
!isChat && (tasks == null || tasks.isEmpty()) -> AssistantScreenState.emptyTaskList()
else -> AssistantScreenState.TaskContent
else -> {
if (!_isTranslationTask.value) {
AssistantScreenState.TaskContent
} else {
_screenState.value
}
}
}
}.collect { newState ->
_screenState.value = newState
Expand Down Expand Up @@ -240,17 +256,27 @@ class AssistantViewModel(

fun selectTaskType(task: TaskTypeData) {
Log_OC.d(TAG, "Task type changed: ${task.name}, session id: ${_sessionId.value}")

// clear task list immediately when task type change
if (_selectedTaskType.value != task) {
_filteredTaskList.update {
listOf()
}
}

updateTaskType(task)

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

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

Expand All @@ -268,12 +294,16 @@ class AssistantViewModel(
}

fun fetchTaskList() = viewModelScope.launch(Dispatchers.IO) {
val cached = localRepository.getCachedTasks(accountName)
val taskType = _selectedTaskType.value ?: return@launch

val cached = localRepository.getCachedTasks(accountName, taskType.name)
if (cached.isNotEmpty()) {
_filteredTaskList.value = cached.sortedByDescending { it.id }
_filteredTaskList.update {
cached.sortedByDescending { it.id }
}
}

_selectedTaskType.value?.id?.let { typeId ->
taskType.id?.let { typeId ->
remoteRepository.getTaskList(typeId)?.let { result ->
taskList = result
_filteredTaskList.value = result.sortedByDescending { it.id }
Expand All @@ -292,9 +322,11 @@ class AssistantViewModel(
}

updateSnackbarMessage(message)

val taskType = _selectedTaskType.value ?: return@launch
if (result.isSuccess) {
removeTaskFromList(id)
localRepository.deleteTask(id, accountName)
localRepository.deleteTask(id, accountName, taskType.name)
}
}
// endregion
Expand All @@ -305,6 +337,12 @@ class AssistantViewModel(
}
}

fun selectTask(task: Task?) {
selectedTask.update {
task
}
}

fun updateSnackbarMessage(value: Int?) {
_snackbarMessageId.update {
value
Expand All @@ -323,6 +361,25 @@ class AssistantViewModel(
}
}

fun updateScreenState(state: AssistantScreenState) {
_screenState.update {
state
}
}

fun updateTranslationTaskState(value: Boolean) {
_isTranslationTask.update {
value
}
}

fun onTranslationScreenDismissed() {
updateTranslationTaskState(false)
selectTask(null)
}

fun getRemoteRepository(): AssistantRemoteRepository = remoteRepository

private fun removeTaskFromList(id: Long) {
_filteredTaskList.update { currentList ->
currentList?.filter { it.id != id }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
package com.nextcloud.client.assistant.model

import com.owncloud.android.R
import com.owncloud.android.lib.resources.assistant.v2.model.Task

sealed class AssistantScreenState {
data object Loading : AssistantScreenState()
Expand All @@ -16,6 +17,8 @@ sealed class AssistantScreenState {

data object ChatContent : AssistantScreenState()

data class Translation(val task: Task?) : AssistantScreenState()

data class EmptyContent(val iconId: Int?, val titleId: Int?, val descriptionId: Int?) : AssistantScreenState()

companion object {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import com.owncloud.android.lib.resources.assistant.v2.model.Task

interface AssistantLocalRepository {
suspend fun cacheTasks(tasks: List<Task>, accountName: String)
suspend fun getCachedTasks(accountName: String): List<Task>
suspend fun getCachedTasks(accountName: String, type: String): List<Task>
suspend fun insertTask(task: Task, accountName: String)
suspend fun deleteTask(id: Long, accountName: String)
suspend fun deleteTask(id: Long, accountName: String, type: String)
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,17 +20,17 @@ class AssistantLocalRepositoryImpl(private val assistantDao: AssistantDao) : Ass
assistantDao.insertAssistantTasks(entities)
}

override suspend fun getCachedTasks(accountName: String): List<Task> {
val entities = assistantDao.getAssistantTasksByAccount(accountName)
override suspend fun getCachedTasks(accountName: String, type: String): List<Task> {
val entities = assistantDao.getAssistantTasksByAccount(accountName, type)
return entities.map { it.toTask() }
}

override suspend fun insertTask(task: Task, accountName: String) {
assistantDao.insertAssistantTask(task.toEntity(accountName))
}

override suspend fun deleteTask(id: Long, accountName: String) {
val cached = assistantDao.getAssistantTasksByAccount(accountName).firstOrNull { it.id == id } ?: return
override suspend fun deleteTask(id: Long, accountName: String, type: String) {
val cached = assistantDao.getAssistantTasksByAccount(accountName, type).firstOrNull { it.id == id } ?: return
assistantDao.deleteAssistantTask(cached)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,14 @@ class MockAssistantLocalRepository : AssistantLocalRepository {
}
}

override suspend fun getCachedTasks(accountName: String): List<Task> = mutex.withLock { tasks.toList() }
override suspend fun getCachedTasks(accountName: String, type: String): List<Task> =
mutex.withLock { tasks.toList() }

override suspend fun insertTask(task: Task, accountName: String) {
mutex.withLock { tasks.add(task) }
}

override suspend fun deleteTask(id: Long, accountName: String) {
override suspend fun deleteTask(id: Long, accountName: String, type: String) {
mutex.withLock { tasks.removeAll { it.id == id } }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import com.owncloud.android.lib.resources.assistant.chat.model.Session
import com.owncloud.android.lib.resources.assistant.chat.model.SessionTask
import com.owncloud.android.lib.resources.assistant.v2.model.Task
import com.owncloud.android.lib.resources.assistant.v2.model.TaskTypeData
import com.owncloud.android.lib.resources.assistant.v2.model.TranslationRequest

interface AssistantRemoteRepository {
suspend fun getTaskTypes(): List<TaskTypeData>?
Expand All @@ -36,4 +37,6 @@ interface AssistantRemoteRepository {
suspend fun generateSession(sessionId: String): SessionTask?

suspend fun checkGeneration(taskId: String, sessionId: String): ChatMessage?

suspend fun translate(input: TranslationRequest, taskType: TaskTypeData): RemoteOperationResult<Void>
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,13 @@ import com.owncloud.android.lib.resources.assistant.v1.GetTaskListRemoteOperatio
import com.owncloud.android.lib.resources.assistant.v1.GetTaskTypesRemoteOperationV1
import com.owncloud.android.lib.resources.assistant.v1.model.toV2
import com.owncloud.android.lib.resources.assistant.v2.CreateTaskRemoteOperationV2
import com.owncloud.android.lib.resources.assistant.v2.CreateTranslationTaskRemoteOperation
import com.owncloud.android.lib.resources.assistant.v2.DeleteTaskRemoteOperationV2
import com.owncloud.android.lib.resources.assistant.v2.GetTaskListRemoteOperationV2
import com.owncloud.android.lib.resources.assistant.v2.GetTaskTypesRemoteOperationV2
import com.owncloud.android.lib.resources.assistant.v2.model.Task
import com.owncloud.android.lib.resources.assistant.v2.model.TaskTypeData
import com.owncloud.android.lib.resources.assistant.v2.model.TranslationRequest
import com.owncloud.android.lib.resources.status.NextcloudVersion
import com.owncloud.android.lib.resources.status.OCCapability
import kotlinx.coroutines.Dispatchers
Expand Down Expand Up @@ -124,4 +126,9 @@ class AssistantRemoteRepositoryImpl(private val client: NextcloudClient, capabil
val result = CheckGenerationRemoteOperation(taskId, sessionId).execute(client)
if (result.isSuccess) result.resultData else null
}

override suspend fun translate(input: TranslationRequest, taskType: TaskTypeData): RemoteOperationResult<Void> =
withContext(Dispatchers.IO) {
CreateTranslationTaskRemoteOperation(input, taskType).execute(client)
}
}
Loading
Loading