From ff3dd36dcdfb73b6299f2458a6b137a78c38bfce Mon Sep 17 00:00:00 2001 From: alperozturk Date: Mon, 10 Nov 2025 16:27:24 +0100 Subject: [PATCH 01/29] feat: chat ui Signed-off-by: alperozturk --- .../client/assistant/AssistantScreen.kt | 141 ++++++++++++------ .../assistant/component/AddTaskAlertDialog.kt | 66 -------- .../conversation/ConversationScreen.kt | 75 ++++++++++ .../assistant/model/ScreenOverlayState.kt | 24 --- .../client/assistant/model/ScreenState.kt | 8 +- .../assistant/taskTypes/TaskTypesRow.kt | 29 +++- .../ui/composeActivity/ComposeActivity.kt | 44 +++--- .../ui/composeActivity/ComposeDestination.kt | 2 +- .../ui/composeActivity/ComposeNavigation.kt | 18 +++ app/src/main/res/values/strings.xml | 6 +- 10 files changed, 253 insertions(+), 160 deletions(-) delete mode 100644 app/src/main/java/com/nextcloud/client/assistant/component/AddTaskAlertDialog.kt create mode 100644 app/src/main/java/com/nextcloud/client/assistant/conversation/ConversationScreen.kt create mode 100644 app/src/main/java/com/nextcloud/ui/composeActivity/ComposeNavigation.kt diff --git a/app/src/main/java/com/nextcloud/client/assistant/AssistantScreen.kt b/app/src/main/java/com/nextcloud/client/assistant/AssistantScreen.kt index a1931bf5a7a0..9bd33b024cd0 100644 --- a/app/src/main/java/com/nextcloud/client/assistant/AssistantScreen.kt +++ b/app/src/main/java/com/nextcloud/client/assistant/AssistantScreen.kt @@ -12,6 +12,7 @@ import androidx.compose.foundation.Image import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth @@ -20,17 +21,16 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Add import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.FabPosition -import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton import androidx.compose.material3.LinearProgressIndicator import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Scaffold import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.material3.pulltorefresh.pullToRefresh import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState @@ -39,8 +39,10 @@ import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.ColorFilter @@ -51,7 +53,6 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import com.nextcloud.client.assistant.component.AddTaskAlertDialog import com.nextcloud.client.assistant.extensions.getInputTitle import com.nextcloud.client.assistant.model.ScreenOverlayState import com.nextcloud.client.assistant.model.ScreenState @@ -69,12 +70,17 @@ import com.owncloud.android.lib.resources.status.OCCapability import kotlinx.coroutines.delay import kotlinx.coroutines.launch +private const val CHAT_INPUT_DELAY = 100L private const val PULL_TO_REFRESH_DELAY = 1500L @Suppress("LongMethod") @OptIn(ExperimentalMaterial3Api::class) @Composable -fun AssistantScreen(viewModel: AssistantViewModel, capability: OCCapability, activity: Activity) { +fun AssistantScreen( + viewModel: AssistantViewModel, + capability: OCCapability, + activity: Activity +) { val messageId by viewModel.snackbarMessageId.collectAsState() val screenOverlayState by viewModel.screenOverlayState.collectAsState() @@ -116,20 +122,20 @@ fun AssistantScreen(viewModel: AssistantViewModel, capability: OCCapability, act ), topBar = { taskTypes?.let { - TaskTypesRow(selectedTaskType, data = it) { task -> - viewModel.selectTaskType(task) - } + val data = listOf(TaskTypeData.conversationList) + it + TaskTypesRow(selectedTaskType, data = data, selectTaskType = { + viewModel.selectTaskType(it) + }) } }, - floatingActionButton = { + bottomBar = { if (!taskTypes.isNullOrEmpty()) { - AddTaskButton( + ChatInputBar( selectedTaskType, viewModel ) } }, - floatingActionButtonPosition = FabPosition.EndOverlay, snackbarHost = { SnackbarHost(snackbarHostState) } @@ -139,8 +145,9 @@ fun AssistantScreen(viewModel: AssistantViewModel, capability: OCCapability, act val state = (screenState as ScreenState.EmptyContent) EmptyContent( paddingValues, - state.iconId, - state.descriptionId + iconId = state.iconId, + descriptionId = state.descriptionId, + titleId = state.titleId ) } @@ -155,8 +162,9 @@ fun AssistantScreen(viewModel: AssistantViewModel, capability: OCCapability, act else -> EmptyContent( paddingValues, - R.drawable.spinner_inner, - R.string.assistant_screen_loading + iconId = R.drawable.spinner_inner, + titleId = null, + descriptionId = R.string.assistant_screen_loading ) } @@ -170,16 +178,67 @@ fun AssistantScreen(viewModel: AssistantViewModel, capability: OCCapability, act } @Composable -private fun AddTaskButton(selectedTaskType: TaskTypeData?, viewModel: AssistantViewModel) { - FloatingActionButton( - onClick = { - selectedTaskType?.let { - val newState = ScreenOverlayState.AddTask(it, "") - viewModel.updateTaskListScreenState(newState) +private fun ChatInputBar( + selectedTaskType: TaskTypeData?, + viewModel: AssistantViewModel +) { + val scope = rememberCoroutineScope() + var text by remember { mutableStateOf("") } + + Surface( + tonalElevation = 3.dp, + shadowElevation = 4.dp, + color = MaterialTheme.colorScheme.surface + ) { + Column(modifier = Modifier + .fillMaxWidth() + .padding(8.dp), + horizontalAlignment = Alignment.CenterHorizontally) { + Text( + text = stringResource(R.string.assistant_generation_warning), + fontSize = 11.sp, + textAlign = TextAlign.Center, + color = colorResource(R.color.text_color) + ) + + Spacer(modifier = Modifier.height(8.dp)) + + Row( + modifier = Modifier + .fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically + ) { + OutlinedTextField( + value = text, + onValueChange = { text = it }, + modifier = Modifier + .weight(1f) + .padding(end = 8.dp), + placeholder = { Text(selectedTaskType?.description ?: "") }, + singleLine = true + ) + + IconButton( + onClick = { + if (text.isNotBlank()) { + selectedTaskType?.let { + viewModel.createTask(input = text, taskType = it) + } + scope.launch { + delay(CHAT_INPUT_DELAY) + text = "" + } + } + } + ) { + Icon( + painter = painterResource(id = R.drawable.ic_send), + contentDescription = "Send message", + tint = MaterialTheme.colorScheme.primary + ) + } } } - ) { - Icon(Icons.Filled.Add, "Add Task Icon") } } @@ -187,22 +246,6 @@ private fun AddTaskButton(selectedTaskType: TaskTypeData?, viewModel: AssistantV @Composable private fun OverlayState(state: ScreenOverlayState?, activity: Activity, viewModel: AssistantViewModel) { when (state) { - is ScreenOverlayState.AddTask -> { - AddTaskAlertDialog( - title = state.taskType.name, - description = state.taskType.description, - defaultInput = state.input, - addTask = { input -> - state.taskType.let { taskType -> - viewModel.createTask(input = input, taskType = taskType) - } - }, - dismiss = { - viewModel.updateTaskListScreenState(null) - } - ) - } - is ScreenOverlayState.DeleteTask -> { SimpleAlertDialog( title = stringResource(id = R.string.assistant_screen_delete_task_alert_dialog_title), @@ -213,9 +256,7 @@ private fun OverlayState(state: ScreenOverlayState?, activity: Activity, viewMod } is ScreenOverlayState.TaskActions -> { - val actions = state.getActions(activity, onEditCompleted = { addTask -> - viewModel.updateTaskListScreenState(addTask) - }, onDeleteCompleted = { deleteTask -> + val actions = state.getActions(activity, onDeleteCompleted = { deleteTask -> viewModel.updateTaskListScreenState(deleteTask) }) @@ -260,7 +301,7 @@ private fun AssistantContent( } @Composable -private fun EmptyContent(paddingValues: PaddingValues, iconId: Int?, descriptionId: Int) { +private fun EmptyContent(paddingValues: PaddingValues, iconId: Int?, descriptionId: Int, titleId: Int?) { Column( modifier = Modifier .fillMaxSize() @@ -280,9 +321,19 @@ private fun EmptyContent(paddingValues: PaddingValues, iconId: Int?, description Spacer(modifier = Modifier.height(8.dp)) } + titleId?.let { + Text( + text = stringResource(titleId), + fontSize = MaterialTheme.typography.bodyMedium.fontSize, + textAlign = TextAlign.Center, + color = colorResource(R.color.text_color) + ) + Spacer(modifier = Modifier.height(8.dp)) + } + Text( text = stringResource(descriptionId), - fontSize = 18.sp, + fontSize = MaterialTheme.typography.bodyMedium.fontSize, textAlign = TextAlign.Center, color = colorResource(R.color.text_color) ) diff --git a/app/src/main/java/com/nextcloud/client/assistant/component/AddTaskAlertDialog.kt b/app/src/main/java/com/nextcloud/client/assistant/component/AddTaskAlertDialog.kt deleted file mode 100644 index e9ccbb4b3c6d..000000000000 --- a/app/src/main/java/com/nextcloud/client/assistant/component/AddTaskAlertDialog.kt +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Nextcloud - Android Client - * - * SPDX-FileCopyrightText: 2024 Alper Ozturk - * SPDX-FileCopyrightText: 2024 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only - */ -package com.nextcloud.client.assistant.component - -import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.material3.Text -import androidx.compose.material3.TextField -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.input.KeyboardType -import androidx.compose.ui.tooling.preview.Preview -import com.nextcloud.ui.composeComponents.alertDialog.SimpleAlertDialog -import com.owncloud.android.R - -@Composable -fun AddTaskAlertDialog( - title: String, - description: String?, - defaultInput: String = "", - addTask: (String) -> Unit, - dismiss: () -> Unit -) { - var input by remember { - mutableStateOf(defaultInput) - } - - SimpleAlertDialog( - title = title, - description = description ?: "", - dismiss = { dismiss() }, - onComplete = { - addTask(input) - }, - content = { - TextField( - placeholder = { - Text( - text = stringResource( - id = R.string.assistant_screen_create_task_alert_dialog_input_field_placeholder - ) - ) - }, - keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text), - value = input, - onValueChange = { - input = it - } - ) - } - ) -} - -@Composable -@Preview -private fun AddTaskAlertDialogPreview() { - AddTaskAlertDialog(title = "Title", description = "Description", addTask = { }, dismiss = {}) -} diff --git a/app/src/main/java/com/nextcloud/client/assistant/conversation/ConversationScreen.kt b/app/src/main/java/com/nextcloud/client/assistant/conversation/ConversationScreen.kt new file mode 100644 index 000000000000..9e1a223b6380 --- /dev/null +++ b/app/src/main/java/com/nextcloud/client/assistant/conversation/ConversationScreen.kt @@ -0,0 +1,75 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2025 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nextcloud.client.assistant.conversation + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import com.nextcloud.ui.composeActivity.ComposeDestination +import com.nextcloud.ui.composeActivity.ComposeNavigation +import com.owncloud.android.R + +@Composable +fun ConversationList() { + Column( + modifier = Modifier + .fillMaxSize() + .padding(16.dp) + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, + modifier = Modifier.fillMaxWidth() + ) { + Text( + text = stringResource(id = R.string.assistant_screen_loading), + style = MaterialTheme.typography.titleMedium + ) + IconButton(onClick = { + ComposeNavigation.navigate(ComposeDestination.AssistantScreen) + }) { + Icon( + painter = painterResource(id = R.drawable.ic_close), + contentDescription = "Close drawer" + ) + } + } + + Spacer(modifier = Modifier.height(12.dp)) + + // Mock conversation list for now + val mockConversations = listOf("General Chat", "Documentation Helper", "Bug Report Draft") + + mockConversations.forEach { conversation -> + Text( + text = conversation, + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 8.dp) + .clickable { }, + style = MaterialTheme.typography.bodyLarge + ) + } + } +} diff --git a/app/src/main/java/com/nextcloud/client/assistant/model/ScreenOverlayState.kt b/app/src/main/java/com/nextcloud/client/assistant/model/ScreenOverlayState.kt index 02ceb335b72d..76a100b60d96 100644 --- a/app/src/main/java/com/nextcloud/client/assistant/model/ScreenOverlayState.kt +++ b/app/src/main/java/com/nextcloud/client/assistant/model/ScreenOverlayState.kt @@ -8,20 +8,16 @@ package com.nextcloud.client.assistant.model import android.app.Activity -import com.nextcloud.client.assistant.extensions.getInput import com.nextcloud.client.assistant.extensions.getInputAndOutput import com.nextcloud.utils.extensions.showShareIntent import com.owncloud.android.R import com.owncloud.android.lib.resources.assistant.v2.model.Task -import com.owncloud.android.lib.resources.assistant.v2.model.TaskTypeData import com.owncloud.android.utils.ClipboardUtil sealed class ScreenOverlayState { data class DeleteTask(val id: Long) : ScreenOverlayState() - data class AddTask(val taskType: TaskTypeData, val input: String) : ScreenOverlayState() data class TaskActions(val task: Task) : ScreenOverlayState() { private fun getInputAndOutput(): String = task.getInputAndOutput() - private fun getInput(): String? = task.getInput() private fun getCopyToClipboardAction(activity: Activity): Triple Unit> = Triple( R.drawable.ic_content_copy, @@ -37,22 +33,6 @@ sealed class ScreenOverlayState { activity.showShareIntent(getInputAndOutput()) } - private fun getEditAction(activity: Activity, onComplete: (AddTask) -> Unit): Triple Unit> = - Triple( - R.drawable.ic_edit, - R.string.action_edit - ) { - val taskType = TaskTypeData( - task.type, - activity.getString(R.string.assistant_screen_add_task_alert_dialog_title), - null, - emptyMap(), - emptyMap() - ) - val newState = AddTask(taskType, getInput() ?: "") - onComplete(newState) - } - private fun getDeleteAction(onComplete: (DeleteTask) -> Unit): Triple Unit> = Triple( R.drawable.ic_delete, R.string.assistant_screen_task_more_actions_bottom_sheet_delete_action @@ -63,14 +43,10 @@ sealed class ScreenOverlayState { fun getActions( activity: Activity, - onEditCompleted: (AddTask) -> Unit, onDeleteCompleted: (DeleteTask) -> Unit ): List Unit>> = listOf( getShareAction(activity), getCopyToClipboardAction(activity), - getEditAction(activity, onComplete = { - onEditCompleted(it) - }), getDeleteAction(onComplete = { onDeleteCompleted(it) }) diff --git a/app/src/main/java/com/nextcloud/client/assistant/model/ScreenState.kt b/app/src/main/java/com/nextcloud/client/assistant/model/ScreenState.kt index 3c2ba4d83e82..b245b5470539 100644 --- a/app/src/main/java/com/nextcloud/client/assistant/model/ScreenState.kt +++ b/app/src/main/java/com/nextcloud/client/assistant/model/ScreenState.kt @@ -14,17 +14,19 @@ sealed class ScreenState { data object Content : ScreenState() - data class EmptyContent(val iconId: Int?, val descriptionId: Int) : ScreenState() + data class EmptyContent(val iconId: Int?, val titleId: Int?, val descriptionId: Int) : ScreenState() companion object { fun emptyTaskTypes(): ScreenState = EmptyContent( + titleId = null, descriptionId = R.string.assistant_screen_task_list_empty_warning, iconId = null ) fun emptyTaskList(): ScreenState = EmptyContent( - descriptionId = R.string.assistant_screen_create_a_new_task_from_bottom_right_text, - iconId = null + iconId = R.drawable.ic_assistant, + titleId = R.string.assistant_screen_chat_title, + descriptionId = R.string.assistant_screen_chat_description, ) } } diff --git a/app/src/main/java/com/nextcloud/client/assistant/taskTypes/TaskTypesRow.kt b/app/src/main/java/com/nextcloud/client/assistant/taskTypes/TaskTypesRow.kt index 3e0e9ad05d5f..bfe6ef7bb4cc 100644 --- a/app/src/main/java/com/nextcloud/client/assistant/taskTypes/TaskTypesRow.kt +++ b/app/src/main/java/com/nextcloud/client/assistant/taskTypes/TaskTypesRow.kt @@ -8,6 +8,8 @@ package com.nextcloud.client.assistant.taskTypes import android.annotation.SuppressLint +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.PrimaryScrollableTabRow import androidx.compose.material3.Tab import androidx.compose.material3.TabRowDefaults @@ -15,14 +17,21 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.res.colorResource +import androidx.compose.ui.res.painterResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import com.nextcloud.ui.composeActivity.ComposeDestination +import com.nextcloud.ui.composeActivity.ComposeNavigation import com.owncloud.android.R import com.owncloud.android.lib.resources.assistant.v2.model.TaskTypeData @SuppressLint("ResourceType") @Composable -fun TaskTypesRow(selectedTaskType: TaskTypeData?, data: List, selectTaskType: (TaskTypeData) -> Unit) { +fun TaskTypesRow( + selectedTaskType: TaskTypeData?, + data: List, + selectTaskType: (TaskTypeData) -> Unit +) { val selectedTabIndex = data.indexOfFirst { it.id == selectedTaskType?.id }.takeIf { it >= 0 } ?: 0 PrimaryScrollableTabRow( @@ -45,6 +54,20 @@ fun TaskTypesRow(selectedTaskType: TaskTypeData?, data: List, sele unselectedContentColor = colorResource(R.color.disabled_text), text = { Text(text = taskType.name) } ) + } else { + Tab( + selected = selectedTaskType?.id == taskType.id, + onClick = { ComposeNavigation.navigate(ComposeDestination.ConversationScreen) }, + selectedContentColor = colorResource(R.color.text_color), + unselectedContentColor = colorResource(R.color.disabled_text), + icon = { + Icon( + painter = painterResource(id = R.drawable.ic_menu), + contentDescription = "open conversation list button", + tint = MaterialTheme.colorScheme.primary + ) + }, + ) } } } @@ -61,5 +84,7 @@ private fun TaskTypesRowPreview() { TaskTypeData("4", "Summarize", "", emptyMap(), emptyMap()) ) - TaskTypesRow(selectedTaskType, taskTypes) { } + TaskTypesRow(selectedTaskType, taskTypes, { + + }) } diff --git a/app/src/main/java/com/nextcloud/ui/composeActivity/ComposeActivity.kt b/app/src/main/java/com/nextcloud/ui/composeActivity/ComposeActivity.kt index 61cd04ec7e17..11344de5437a 100644 --- a/app/src/main/java/com/nextcloud/ui/composeActivity/ComposeActivity.kt +++ b/app/src/main/java/com/nextcloud/ui/composeActivity/ComposeActivity.kt @@ -12,12 +12,14 @@ import android.view.MenuItem import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import com.nextcloud.client.assistant.AssistantScreen import com.nextcloud.client.assistant.AssistantViewModel +import com.nextcloud.client.assistant.conversation.ConversationList import com.nextcloud.client.assistant.repository.local.AssistantLocalRepositoryImpl import com.nextcloud.client.assistant.repository.remote.AssistantRemoteRepositoryImpl import com.nextcloud.client.database.NextcloudDatabase @@ -41,7 +43,7 @@ class ComposeActivity : DrawerActivity() { binding = ActivityComposeBinding.inflate(layoutInflater) setContentView(binding.root) - val destination = intent.getSerializableArgument(DESTINATION, ComposeDestination::class.java) + val destination = intent.getSerializableArgument(DESTINATION, ComposeDestination::class.java) ?: return val titleId = intent.getIntExtra(TITLE, R.string.empty) setupDrawer() @@ -69,31 +71,39 @@ class ComposeActivity : DrawerActivity() { } @Composable - private fun Content(destination: ComposeDestination?) { + private fun Content(destination: ComposeDestination) { + val currentScreen by ComposeNavigation.currentScreen.collectAsState() var nextcloudClient by remember { mutableStateOf(null) } LaunchedEffect(Unit) { + ComposeNavigation.navigate(destination) nextcloudClient = clientRepository.getNextcloudClient() } - if (destination == ComposeDestination.AssistantScreen) { - binding.bottomNavigation.menu.findItem(R.id.nav_assistant).run { - isChecked = true - } + when(currentScreen) { + ComposeDestination.AssistantScreen -> { + binding.bottomNavigation.menu.findItem(R.id.nav_assistant).run { + isChecked = true + } - val dao = NextcloudDatabase.instance().assistantDao() + val dao = NextcloudDatabase.instance().assistantDao() - nextcloudClient?.let { client -> - AssistantScreen( - viewModel = AssistantViewModel( - accountName = userAccountManager.user.accountName, - remoteRepository = AssistantRemoteRepositoryImpl(client, capabilities), - localRepository = AssistantLocalRepositoryImpl(dao) - ), - activity = this, - capability = capabilities - ) + nextcloudClient?.let { client -> + AssistantScreen( + viewModel = AssistantViewModel( + accountName = userAccountManager.user.accountName, + remoteRepository = AssistantRemoteRepositoryImpl(client, capabilities), + localRepository = AssistantLocalRepositoryImpl(dao) + ), + activity = this, + capability = capabilities + ) + } + } + ComposeDestination.ConversationScreen -> { + ConversationList() } + else -> Unit } } } diff --git a/app/src/main/java/com/nextcloud/ui/composeActivity/ComposeDestination.kt b/app/src/main/java/com/nextcloud/ui/composeActivity/ComposeDestination.kt index 10e80ad4ca7f..aaa557bf62d0 100644 --- a/app/src/main/java/com/nextcloud/ui/composeActivity/ComposeDestination.kt +++ b/app/src/main/java/com/nextcloud/ui/composeActivity/ComposeDestination.kt @@ -10,5 +10,5 @@ package com.nextcloud.ui.composeActivity import java.io.Serializable enum class ComposeDestination : Serializable { - AssistantScreen + AssistantScreen, ConversationScreen } diff --git a/app/src/main/java/com/nextcloud/ui/composeActivity/ComposeNavigation.kt b/app/src/main/java/com/nextcloud/ui/composeActivity/ComposeNavigation.kt new file mode 100644 index 000000000000..453bb439b8f2 --- /dev/null +++ b/app/src/main/java/com/nextcloud/ui/composeActivity/ComposeNavigation.kt @@ -0,0 +1,18 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2025 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nextcloud.ui.composeActivity + +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.update + +object ComposeNavigation { + private var root: MutableStateFlow = MutableStateFlow(null) + val currentScreen: StateFlow = root + fun navigate(value: ComposeDestination) = root.update { value } +} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 86d539bf531c..78de740cf697 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -61,7 +61,9 @@ Assistant Loading task list… The task output is not ready yet. - Create a new task from bottom right + + Hello there! What can I help you with today? + Try sending a message to spark a conversation. Delete task Are you sure you want to delete this task? Add new task @@ -78,7 +80,7 @@ Input Output - This content was generated by AI and can make mistakes. + Output shown here is generated by AI. Make sure to always double-check. Recommended files From 6fa7a316cace018a25cdd268d0305f8b1ab0d964 Mon Sep 17 00:00:00 2001 From: alperozturk Date: Tue, 11 Nov 2025 09:58:31 +0100 Subject: [PATCH 02/29] feat: chat ui api calls Signed-off-by: alperozturk --- .../client/assistant/AssistantScreen.kt | 12 +- .../client/assistant/AssistantViewModel.kt | 12 +- .../conversation/ConversationScreen.kt | 184 ++++++++++++++---- .../conversation/ConversationViewModel.kt | 94 +++++++++ .../model/ConversationScreenState.kt | 22 +++ .../ConversationRemoteRepository.kt | 16 ++ .../ConversationRemoteRepositoryImpl.kt | 42 ++++ ...ScreenState.kt => AssistantScreenState.kt} | 12 +- .../ui/composeActivity/ComposeActivity.kt | 10 +- app/src/main/res/values/strings.xml | 4 + 10 files changed, 345 insertions(+), 63 deletions(-) create mode 100644 app/src/main/java/com/nextcloud/client/assistant/conversation/ConversationViewModel.kt create mode 100644 app/src/main/java/com/nextcloud/client/assistant/conversation/model/ConversationScreenState.kt create mode 100644 app/src/main/java/com/nextcloud/client/assistant/conversation/repository/ConversationRemoteRepository.kt create mode 100644 app/src/main/java/com/nextcloud/client/assistant/conversation/repository/ConversationRemoteRepositoryImpl.kt rename app/src/main/java/com/nextcloud/client/assistant/model/{ScreenState.kt => AssistantScreenState.kt} (67%) diff --git a/app/src/main/java/com/nextcloud/client/assistant/AssistantScreen.kt b/app/src/main/java/com/nextcloud/client/assistant/AssistantScreen.kt index 9bd33b024cd0..3c9fd1f58f7f 100644 --- a/app/src/main/java/com/nextcloud/client/assistant/AssistantScreen.kt +++ b/app/src/main/java/com/nextcloud/client/assistant/AssistantScreen.kt @@ -55,7 +55,7 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.nextcloud.client.assistant.extensions.getInputTitle import com.nextcloud.client.assistant.model.ScreenOverlayState -import com.nextcloud.client.assistant.model.ScreenState +import com.nextcloud.client.assistant.model.AssistantScreenState import com.nextcloud.client.assistant.repository.local.MockAssistantLocalRepository import com.nextcloud.client.assistant.repository.remote.MockAssistantRemoteRepository import com.nextcloud.client.assistant.task.TaskView @@ -123,8 +123,8 @@ fun AssistantScreen( topBar = { taskTypes?.let { val data = listOf(TaskTypeData.conversationList) + it - TaskTypesRow(selectedTaskType, data = data, selectTaskType = { - viewModel.selectTaskType(it) + TaskTypesRow(selectedTaskType, data = data, selectTaskType = { task -> + viewModel.selectTaskType(task) }) } }, @@ -141,8 +141,8 @@ fun AssistantScreen( } ) { paddingValues -> when (screenState) { - is ScreenState.EmptyContent -> { - val state = (screenState as ScreenState.EmptyContent) + is AssistantScreenState.EmptyContent -> { + val state = (screenState as AssistantScreenState.EmptyContent) EmptyContent( paddingValues, iconId = state.iconId, @@ -151,7 +151,7 @@ fun AssistantScreen( ) } - ScreenState.Content -> { + AssistantScreenState.Content -> { AssistantContent( paddingValues, filteredTaskList ?: listOf(), diff --git a/app/src/main/java/com/nextcloud/client/assistant/AssistantViewModel.kt b/app/src/main/java/com/nextcloud/client/assistant/AssistantViewModel.kt index 6f64bb69b4f4..b866e06fb25d 100644 --- a/app/src/main/java/com/nextcloud/client/assistant/AssistantViewModel.kt +++ b/app/src/main/java/com/nextcloud/client/assistant/AssistantViewModel.kt @@ -10,7 +10,7 @@ package com.nextcloud.client.assistant import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.nextcloud.client.assistant.model.ScreenOverlayState -import com.nextcloud.client.assistant.model.ScreenState +import com.nextcloud.client.assistant.model.AssistantScreenState import com.nextcloud.client.assistant.repository.local.AssistantLocalRepository import com.nextcloud.client.assistant.repository.remote.AssistantRemoteRepository import com.owncloud.android.R @@ -37,8 +37,8 @@ class AssistantViewModel( private const val TASK_LIST_POLLING_INTERVAL_MS = 15_000L } - private val _screenState = MutableStateFlow(null) - val screenState: StateFlow = _screenState + private val _screenState = MutableStateFlow(null) + val screenState: StateFlow = _screenState private val _screenOverlayState = MutableStateFlow(null) val screenOverlayState: StateFlow = _screenOverlayState @@ -132,7 +132,7 @@ class AssistantViewModel( val taskTypesResult = remoteRepository.getTaskTypes() if (taskTypesResult == null || taskTypesResult.isEmpty()) { _screenState.update { - ScreenState.emptyTaskTypes() + AssistantScreenState.emptyTaskTypes() } return@launch } @@ -179,9 +179,9 @@ class AssistantViewModel( private fun updateTaskListScreenState() { _screenState.update { if (_filteredTaskList.value?.isEmpty() == true) { - ScreenState.emptyTaskList() + AssistantScreenState.emptyTaskList() } else { - ScreenState.Content + AssistantScreenState.Content } } } diff --git a/app/src/main/java/com/nextcloud/client/assistant/conversation/ConversationScreen.kt b/app/src/main/java/com/nextcloud/client/assistant/conversation/ConversationScreen.kt index 9e1a223b6380..37dc842c4974 100644 --- a/app/src/main/java/com/nextcloud/client/assistant/conversation/ConversationScreen.kt +++ b/app/src/main/java/com/nextcloud/client/assistant/conversation/ConversationScreen.kt @@ -7,69 +7,167 @@ package com.nextcloud.client.assistant.conversation -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Add +import androidx.compose.material.icons.filled.Delete +import androidx.compose.material3.* +import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp -import com.nextcloud.ui.composeActivity.ComposeDestination -import com.nextcloud.ui.composeActivity.ComposeNavigation -import com.owncloud.android.R +import com.nextcloud.client.assistant.conversation.model.ConversationScreenState +import com.owncloud.android.lib.resources.assistant.chat.model.Conversation +@OptIn(ExperimentalMaterial3Api::class) @Composable -fun ConversationList() { - Column( - modifier = Modifier +fun ConversationScreen( + viewModel: ConversationViewModel, + onConversationClick: (Conversation) -> Unit = {}, + onCreateConversationClick: () -> Unit = {} +) { + val context = LocalContext.current + val screenState by viewModel.screenState.collectAsState() + val errorMessageId by viewModel.errorMessageId.collectAsState() + val conversations by viewModel.conversations.collectAsState() + val snackbarHostState = remember { SnackbarHostState() } + + LaunchedEffect(errorMessageId) { + errorMessageId?.let { + snackbarHostState.showSnackbar(context.getString(it)) + } + } + + Scaffold( + snackbarHost = { + SnackbarHost(snackbarHostState) + } + ) { innerPadding -> + when (screenState) { + is ConversationScreenState.Loading -> { + Box( + modifier = Modifier + .fillMaxSize() + .padding(innerPadding), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator() + } + } + + is ConversationScreenState.EmptyContent -> { + Box( + modifier = Modifier + .fillMaxSize() + .padding(innerPadding), + contentAlignment = Alignment.Center + ) { + Text("No conversations found.") + } + } + + else -> { + ConversationList( + conversations = conversations, + onConversationClick = { onConversationClick(it) }, + onDeleteClick = { viewModel.deleteConversation(it.id.toString()) }, + onCreateConversationClick = onCreateConversationClick, + modifier = Modifier.padding(innerPadding) + ) + } + } + } +} + +@Composable +private fun ConversationList( + conversations: List, + onConversationClick: (Conversation) -> Unit, + onDeleteClick: (Conversation) -> Unit, + onCreateConversationClick: () -> Unit, + modifier: Modifier = Modifier +) { + LazyColumn( + modifier = modifier .fillMaxSize() - .padding(16.dp) + .padding(16.dp), + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + items(conversations) { conversation -> + ConversationListItem( + conversation = conversation, + onClick = { onConversationClick(conversation) }, + onDeleteClick = { onDeleteClick(conversation) } + ) + } + + item { + Spacer(modifier = Modifier.height(24.dp)) + CreateConversationButton(onClick = onCreateConversationClick) + } + } +} + +@Composable +private fun ConversationListItem( + conversation: Conversation, + onClick: () -> Unit, + onDeleteClick: () -> Unit +) { + ElevatedButton( + onClick = onClick, + modifier = Modifier.fillMaxWidth(), + contentPadding = PaddingValues(horizontal = 16.dp, vertical = 12.dp) ) { Row( + modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween, - modifier = Modifier.fillMaxWidth() + horizontalArrangement = Arrangement.SpaceBetween ) { Text( - text = stringResource(id = R.string.assistant_screen_loading), - style = MaterialTheme.typography.titleMedium + text = conversation.title, + style = MaterialTheme.typography.bodyLarge ) - IconButton(onClick = { - ComposeNavigation.navigate(ComposeDestination.AssistantScreen) - }) { + IconButton( + onClick = onDeleteClick + ) { Icon( - painter = painterResource(id = R.drawable.ic_close), - contentDescription = "Close drawer" + imageVector = Icons.Default.Delete, + contentDescription = "Delete conversation" ) } } + } +} - Spacer(modifier = Modifier.height(12.dp)) - - // Mock conversation list for now - val mockConversations = listOf("General Chat", "Documentation Helper", "Bug Report Draft") - - mockConversations.forEach { conversation -> +@Composable +private fun CreateConversationButton( + onClick: () -> Unit +) { + OutlinedButton( + onClick = onClick, + modifier = Modifier + .fillMaxWidth() + .padding(top = 8.dp), + contentPadding = PaddingValues(vertical = 12.dp, horizontal = 16.dp) + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center, + modifier = Modifier.fillMaxWidth() + ) { Text( - text = conversation, - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 8.dp) - .clickable { }, + text = "Create new conversation", style = MaterialTheme.typography.bodyLarge ) + Spacer(modifier = Modifier.width(8.dp)) + Icon( + imageVector = Icons.Default.Add, + contentDescription = "Add conversation" + ) } } } diff --git a/app/src/main/java/com/nextcloud/client/assistant/conversation/ConversationViewModel.kt b/app/src/main/java/com/nextcloud/client/assistant/conversation/ConversationViewModel.kt new file mode 100644 index 000000000000..28257e38c910 --- /dev/null +++ b/app/src/main/java/com/nextcloud/client/assistant/conversation/ConversationViewModel.kt @@ -0,0 +1,94 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2025 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nextcloud.client.assistant.conversation + +import androidx.lifecycle.ViewModel +import com.nextcloud.client.assistant.conversation.repository.ConversationRemoteRepository +import androidx.lifecycle.viewModelScope +import com.nextcloud.client.assistant.conversation.model.ConversationScreenState +import com.owncloud.android.R +import com.owncloud.android.lib.resources.assistant.chat.model.Conversation +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch + +class ConversationViewModel( + private val remoteRepository: ConversationRemoteRepository +) : ViewModel() { + private val _errorMessageId = MutableStateFlow(null) + val errorMessageId: StateFlow = _errorMessageId + + private val _screenState = MutableStateFlow(null) + val screenState: StateFlow = _screenState + + private val _conversations = MutableStateFlow>(listOf()) + val conversations: StateFlow> = _conversations.asStateFlow() + + fun fetchConversations() { + _screenState.update { + ConversationScreenState.Loading + } + + viewModelScope.launch(Dispatchers.IO) { + val conversations = remoteRepository.fetchConversationList() + if (conversations != null) { + if (conversations.isEmpty()) { + _screenState.update { + ConversationScreenState.emptyConversationList() + } + } else { + _screenState.update { + null + } + _conversations.update { + conversations + } + } + } else { + _errorMessageId.update { + R.string.assistant_screen_conversation_list_fetch_error + } + } + } + } + + fun createConversation(title: String?, timestamp: Long) { + viewModelScope.launch(Dispatchers.IO) { + val newConversation = remoteRepository.createConversation(title, timestamp) + if (newConversation != null) { + _conversations.update { + it + newConversation + } + } else { + _errorMessageId.update { + R.string.assistant_screen_conversation_create_error + } + } + } + } + + fun deleteConversation(sessionId: String) { + viewModelScope.launch(Dispatchers.IO) { + val success = remoteRepository.deleteConversation(sessionId) + if (success) { + // TODO: CHECK IF ITS WORKING... + val updatedList = _conversations.value.filterNot { it.id == sessionId.toLong() } + _conversations.update { + updatedList + } + } else { + _errorMessageId.update { + R.string.assistant_screen_conversation_delete_error + } + } + } + } +} diff --git a/app/src/main/java/com/nextcloud/client/assistant/conversation/model/ConversationScreenState.kt b/app/src/main/java/com/nextcloud/client/assistant/conversation/model/ConversationScreenState.kt new file mode 100644 index 000000000000..093ddb177db8 --- /dev/null +++ b/app/src/main/java/com/nextcloud/client/assistant/conversation/model/ConversationScreenState.kt @@ -0,0 +1,22 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2025 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nextcloud.client.assistant.conversation.model + +import com.owncloud.android.R + +sealed class ConversationScreenState { + data object Loading : ConversationScreenState() + + data class EmptyContent(val descriptionId: Int) : ConversationScreenState() + + companion object { + fun emptyConversationList(): ConversationScreenState = EmptyContent( + descriptionId = R.string.assistant_screen_no_conversation, + ) + } +} diff --git a/app/src/main/java/com/nextcloud/client/assistant/conversation/repository/ConversationRemoteRepository.kt b/app/src/main/java/com/nextcloud/client/assistant/conversation/repository/ConversationRemoteRepository.kt new file mode 100644 index 000000000000..81aaa829a9e2 --- /dev/null +++ b/app/src/main/java/com/nextcloud/client/assistant/conversation/repository/ConversationRemoteRepository.kt @@ -0,0 +1,16 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2025 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nextcloud.client.assistant.conversation.repository + +import com.owncloud.android.lib.resources.assistant.chat.model.Conversation + +interface ConversationRemoteRepository { + suspend fun fetchConversationList(): List? + suspend fun createConversation(title: String?, timestamp: Long): Conversation? + suspend fun deleteConversation(sessionId: String): Boolean +} diff --git a/app/src/main/java/com/nextcloud/client/assistant/conversation/repository/ConversationRemoteRepositoryImpl.kt b/app/src/main/java/com/nextcloud/client/assistant/conversation/repository/ConversationRemoteRepositoryImpl.kt new file mode 100644 index 000000000000..caf9160f85b4 --- /dev/null +++ b/app/src/main/java/com/nextcloud/client/assistant/conversation/repository/ConversationRemoteRepositoryImpl.kt @@ -0,0 +1,42 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2025 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nextcloud.client.assistant.conversation.repository + +import com.nextcloud.common.NextcloudClient +import com.owncloud.android.lib.resources.assistant.chat.CreateConversationRemoteOperation +import com.owncloud.android.lib.resources.assistant.chat.DeleteConversationRemoteOperation +import com.owncloud.android.lib.resources.assistant.chat.GetConversationListRemoteOperation +import com.owncloud.android.lib.resources.assistant.chat.model.Conversation + +class ConversationRemoteRepositoryImpl(private val client: NextcloudClient): ConversationRemoteRepository { + override suspend fun fetchConversationList(): List? { + val result = GetConversationListRemoteOperation().execute(client) + return if (result.isSuccess) { + result.resultData + } else { + null + } + } + + override suspend fun createConversation( + title: String?, + timestamp: Long + ): Conversation? { + val result = CreateConversationRemoteOperation(title, timestamp).execute(client) + return if (result.isSuccess) { + result.resultData + } else { + null + } + } + + override suspend fun deleteConversation(sessionId: String): Boolean { + val result = DeleteConversationRemoteOperation(sessionId).execute(client) + return result.isSuccess + } +} diff --git a/app/src/main/java/com/nextcloud/client/assistant/model/ScreenState.kt b/app/src/main/java/com/nextcloud/client/assistant/model/AssistantScreenState.kt similarity index 67% rename from app/src/main/java/com/nextcloud/client/assistant/model/ScreenState.kt rename to app/src/main/java/com/nextcloud/client/assistant/model/AssistantScreenState.kt index b245b5470539..ead9f98c59a8 100644 --- a/app/src/main/java/com/nextcloud/client/assistant/model/ScreenState.kt +++ b/app/src/main/java/com/nextcloud/client/assistant/model/AssistantScreenState.kt @@ -9,21 +9,21 @@ package com.nextcloud.client.assistant.model import com.owncloud.android.R -sealed class ScreenState { - data object Loading : ScreenState() +sealed class AssistantScreenState { + data object Loading : AssistantScreenState() - data object Content : ScreenState() + data object Content : AssistantScreenState() - data class EmptyContent(val iconId: Int?, val titleId: Int?, val descriptionId: Int) : ScreenState() + data class EmptyContent(val iconId: Int?, val titleId: Int?, val descriptionId: Int) : AssistantScreenState() companion object { - fun emptyTaskTypes(): ScreenState = EmptyContent( + fun emptyTaskTypes(): AssistantScreenState = EmptyContent( titleId = null, descriptionId = R.string.assistant_screen_task_list_empty_warning, iconId = null ) - fun emptyTaskList(): ScreenState = EmptyContent( + fun emptyTaskList(): AssistantScreenState = EmptyContent( iconId = R.drawable.ic_assistant, titleId = R.string.assistant_screen_chat_title, descriptionId = R.string.assistant_screen_chat_description, diff --git a/app/src/main/java/com/nextcloud/ui/composeActivity/ComposeActivity.kt b/app/src/main/java/com/nextcloud/ui/composeActivity/ComposeActivity.kt index 11344de5437a..7f3b56a84517 100644 --- a/app/src/main/java/com/nextcloud/ui/composeActivity/ComposeActivity.kt +++ b/app/src/main/java/com/nextcloud/ui/composeActivity/ComposeActivity.kt @@ -19,7 +19,9 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import com.nextcloud.client.assistant.AssistantScreen import com.nextcloud.client.assistant.AssistantViewModel -import com.nextcloud.client.assistant.conversation.ConversationList +import com.nextcloud.client.assistant.conversation.ConversationScreen +import com.nextcloud.client.assistant.conversation.ConversationViewModel +import com.nextcloud.client.assistant.conversation.repository.ConversationRemoteRepositoryImpl import com.nextcloud.client.assistant.repository.local.AssistantLocalRepositoryImpl import com.nextcloud.client.assistant.repository.remote.AssistantRemoteRepositoryImpl import com.nextcloud.client.database.NextcloudDatabase @@ -101,7 +103,11 @@ class ComposeActivity : DrawerActivity() { } } ComposeDestination.ConversationScreen -> { - ConversationList() + nextcloudClient?.let { client -> + ConversationScreen(viewModel = ConversationViewModel( + remoteRepository = ConversationRemoteRepositoryImpl(client) + )) + } } else -> Unit } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 78de740cf697..472fd8ef4848 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -61,6 +61,10 @@ Assistant Loading task list… The task output is not ready yet. + No conversations yet + Failed to fetch conversation list + Failed to create conversation + Failed to delete conversation Hello there! What can I help you with today? Try sending a message to spark a conversation. From 69e517e35ed6f09b6c8b3ec0c3a4ce1874c67f99 Mon Sep 17 00:00:00 2001 From: alperozturk Date: Tue, 11 Nov 2025 11:33:22 +0100 Subject: [PATCH 03/29] feat: chat ui api calls Signed-off-by: alperozturk --- .../conversation/ConversationScreen.kt | 165 ++++++++++++------ .../conversation/ConversationViewModel.kt | 8 +- .../ConversationRemoteRepository.kt | 3 +- .../ConversationRemoteRepositoryImpl.kt | 3 +- app/src/main/res/values/strings.xml | 1 + 5 files changed, 121 insertions(+), 59 deletions(-) diff --git a/app/src/main/java/com/nextcloud/client/assistant/conversation/ConversationScreen.kt b/app/src/main/java/com/nextcloud/client/assistant/conversation/ConversationScreen.kt index 37dc842c4974..69c565e7b3e3 100644 --- a/app/src/main/java/com/nextcloud/client/assistant/conversation/ConversationScreen.kt +++ b/app/src/main/java/com/nextcloud/client/assistant/conversation/ConversationScreen.kt @@ -8,33 +8,43 @@ package com.nextcloud.client.assistant.conversation import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Add -import androidx.compose.material.icons.filled.Delete +import androidx.compose.material.icons.filled.Close +import androidx.compose.material.icons.outlined.Delete import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.nextcloud.client.assistant.conversation.model.ConversationScreenState +import com.nextcloud.ui.composeActivity.ComposeDestination +import com.nextcloud.ui.composeActivity.ComposeNavigation import com.owncloud.android.lib.resources.assistant.chat.model.Conversation +import com.owncloud.android.R + +private val BUTTON_HEIGHT = 32.dp @OptIn(ExperimentalMaterial3Api::class) @Composable -fun ConversationScreen( - viewModel: ConversationViewModel, - onConversationClick: (Conversation) -> Unit = {}, - onCreateConversationClick: () -> Unit = {} -) { +fun ConversationScreen(viewModel: ConversationViewModel) { val context = LocalContext.current val screenState by viewModel.screenState.collectAsState() val errorMessageId by viewModel.errorMessageId.collectAsState() val conversations by viewModel.conversations.collectAsState() val snackbarHostState = remember { SnackbarHostState() } + LaunchedEffect(Unit) { + viewModel.fetchConversations() + } + LaunchedEffect(errorMessageId) { errorMessageId?.let { snackbarHostState.showSnackbar(context.getString(it)) @@ -42,8 +52,28 @@ fun ConversationScreen( } Scaffold( + topBar = { + Row(modifier = Modifier.fillMaxWidth()) { + Spacer(modifier = Modifier.weight(1f)) + IconButton( + onClick = { + ComposeNavigation.navigate(ComposeDestination.AssistantScreen) + } + ) { + Icon( + imageVector = Icons.Default.Close, + contentDescription = "close conversations list" + ) + } + } + }, snackbarHost = { SnackbarHost(snackbarHostState) + }, + bottomBar = { + CreateConversationButton(onClick = { + viewModel.createConversation(null) + }) } ) { innerPadding -> when (screenState) { @@ -72,9 +102,7 @@ fun ConversationScreen( else -> { ConversationList( conversations = conversations, - onConversationClick = { onConversationClick(it) }, onDeleteClick = { viewModel.deleteConversation(it.id.toString()) }, - onCreateConversationClick = onCreateConversationClick, modifier = Modifier.padding(innerPadding) ) } @@ -85,28 +113,24 @@ fun ConversationScreen( @Composable private fun ConversationList( conversations: List, - onConversationClick: (Conversation) -> Unit, onDeleteClick: (Conversation) -> Unit, - onCreateConversationClick: () -> Unit, modifier: Modifier = Modifier ) { LazyColumn( modifier = modifier .fillMaxSize() .padding(16.dp), - verticalArrangement = Arrangement.spacedBy(12.dp) + verticalArrangement = Arrangement.Bottom, ) { items(conversations) { conversation -> ConversationListItem( conversation = conversation, - onClick = { onConversationClick(conversation) }, + onClick = { + // TODO: + }, onDeleteClick = { onDeleteClick(conversation) } ) - } - - item { - Spacer(modifier = Modifier.height(24.dp)) - CreateConversationButton(onClick = onCreateConversationClick) + Spacer(modifier = Modifier.height(8.dp)) } } } @@ -117,27 +141,32 @@ private fun ConversationListItem( onClick: () -> Unit, onDeleteClick: () -> Unit ) { - ElevatedButton( - onClick = onClick, - modifier = Modifier.fillMaxWidth(), - contentPadding = PaddingValues(horizontal = 16.dp, vertical = 12.dp) - ) { - Row( - modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween + Box(modifier = Modifier.fillMaxWidth(), contentAlignment = Alignment.BottomCenter) { + FilledTonalButton( + onClick = onClick, + shape = RoundedCornerShape(8.dp), + contentPadding = PaddingValues(vertical = 12.dp, horizontal = 16.dp) ) { - Text( - text = conversation.title, - style = MaterialTheme.typography.bodyLarge - ) - IconButton( - onClick = onDeleteClick + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .height(BUTTON_HEIGHT) ) { - Icon( - imageVector = Icons.Default.Delete, - contentDescription = "Delete conversation" + Text( + text = conversation.titleRepresentation(), + style = MaterialTheme.typography.bodyLarge ) + + Spacer(modifier = Modifier.weight(1f)) + + IconButton( + onClick = onDeleteClick + ) { + Icon( + imageVector = Icons.Outlined.Delete, + contentDescription = "Delete conversation" + ) + } } } } @@ -147,27 +176,53 @@ private fun ConversationListItem( private fun CreateConversationButton( onClick: () -> Unit ) { - OutlinedButton( - onClick = onClick, - modifier = Modifier - .fillMaxWidth() - .padding(top = 8.dp), - contentPadding = PaddingValues(vertical = 12.dp, horizontal = 16.dp) - ) { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.Center, - modifier = Modifier.fillMaxWidth() + Box(modifier = Modifier.fillMaxWidth(), contentAlignment = Alignment.BottomCenter) { + OutlinedButton( + onClick = onClick, + shape = RoundedCornerShape(8.dp), + contentPadding = PaddingValues(vertical = 12.dp, horizontal = 16.dp) ) { - Text( - text = "Create new conversation", - style = MaterialTheme.typography.bodyLarge - ) - Spacer(modifier = Modifier.width(8.dp)) - Icon( - imageVector = Icons.Default.Add, - contentDescription = "Add conversation" - ) + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center, + modifier = Modifier + .fillMaxWidth(0.8f) + .height(BUTTON_HEIGHT) + ) { + Text( + text = stringResource(R.string.assistant_screen_conversation_create_text), + style = MaterialTheme.typography.bodyLarge + ) + Spacer(modifier = Modifier.width(8.dp)) + Icon( + imageVector = Icons.Default.Add, + contentDescription = "Add conversation" + ) + } } } } + +@Preview +@Composable +private fun ConversationListPreview() { + Column { + ConversationListItem(Conversation(1L, "User1", "Who is Al Pacino?", 1762847286L, "", null), { + + }, { + + }) + + Spacer(modifier = Modifier.height(8.dp)) + + ConversationListItem(Conversation(2L, "User1", "What is JetpackCompose?", 1761847286L, "", null), { + + }, { + + }) + + Spacer(modifier = Modifier.height(8.dp)) + + CreateConversationButton { } + } +} diff --git a/app/src/main/java/com/nextcloud/client/assistant/conversation/ConversationViewModel.kt b/app/src/main/java/com/nextcloud/client/assistant/conversation/ConversationViewModel.kt index 28257e38c910..c988905e4dd1 100644 --- a/app/src/main/java/com/nextcloud/client/assistant/conversation/ConversationViewModel.kt +++ b/app/src/main/java/com/nextcloud/client/assistant/conversation/ConversationViewModel.kt @@ -53,6 +53,9 @@ class ConversationViewModel( } } } else { + _screenState.update { + null + } _errorMessageId.update { R.string.assistant_screen_conversation_list_fetch_error } @@ -60,12 +63,13 @@ class ConversationViewModel( } } - fun createConversation(title: String?, timestamp: Long) { + fun createConversation(title: String?) { viewModelScope.launch(Dispatchers.IO) { + val timestamp = System.currentTimeMillis() / 1000 val newConversation = remoteRepository.createConversation(title, timestamp) if (newConversation != null) { _conversations.update { - it + newConversation + listOf(newConversation.session) + it } } else { _errorMessageId.update { diff --git a/app/src/main/java/com/nextcloud/client/assistant/conversation/repository/ConversationRemoteRepository.kt b/app/src/main/java/com/nextcloud/client/assistant/conversation/repository/ConversationRemoteRepository.kt index 81aaa829a9e2..a0d034d575c3 100644 --- a/app/src/main/java/com/nextcloud/client/assistant/conversation/repository/ConversationRemoteRepository.kt +++ b/app/src/main/java/com/nextcloud/client/assistant/conversation/repository/ConversationRemoteRepository.kt @@ -8,9 +8,10 @@ package com.nextcloud.client.assistant.conversation.repository import com.owncloud.android.lib.resources.assistant.chat.model.Conversation +import com.owncloud.android.lib.resources.assistant.chat.model.CreateConversation interface ConversationRemoteRepository { suspend fun fetchConversationList(): List? - suspend fun createConversation(title: String?, timestamp: Long): Conversation? + suspend fun createConversation(title: String?, timestamp: Long): CreateConversation? suspend fun deleteConversation(sessionId: String): Boolean } diff --git a/app/src/main/java/com/nextcloud/client/assistant/conversation/repository/ConversationRemoteRepositoryImpl.kt b/app/src/main/java/com/nextcloud/client/assistant/conversation/repository/ConversationRemoteRepositoryImpl.kt index caf9160f85b4..c719a589ad8c 100644 --- a/app/src/main/java/com/nextcloud/client/assistant/conversation/repository/ConversationRemoteRepositoryImpl.kt +++ b/app/src/main/java/com/nextcloud/client/assistant/conversation/repository/ConversationRemoteRepositoryImpl.kt @@ -12,6 +12,7 @@ import com.owncloud.android.lib.resources.assistant.chat.CreateConversationRemot import com.owncloud.android.lib.resources.assistant.chat.DeleteConversationRemoteOperation import com.owncloud.android.lib.resources.assistant.chat.GetConversationListRemoteOperation import com.owncloud.android.lib.resources.assistant.chat.model.Conversation +import com.owncloud.android.lib.resources.assistant.chat.model.CreateConversation class ConversationRemoteRepositoryImpl(private val client: NextcloudClient): ConversationRemoteRepository { override suspend fun fetchConversationList(): List? { @@ -26,7 +27,7 @@ class ConversationRemoteRepositoryImpl(private val client: NextcloudClient): Con override suspend fun createConversation( title: String?, timestamp: Long - ): Conversation? { + ): CreateConversation? { val result = CreateConversationRemoteOperation(title, timestamp).execute(client) return if (result.isSuccess) { result.resultData diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 472fd8ef4848..05af99ff3074 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -65,6 +65,7 @@ Failed to fetch conversation list Failed to create conversation Failed to delete conversation + New conversation Hello there! What can I help you with today? Try sending a message to spark a conversation. From cef991cace175edc2e3b5c23b9c47d391466790f Mon Sep 17 00:00:00 2001 From: alperozturk Date: Tue, 11 Nov 2025 12:36:40 +0100 Subject: [PATCH 04/29] implement chat content Signed-off-by: alperozturk --- .../client/assistant/AssistantScreen.kt | 37 +++++++--- .../client/assistant/AssistantViewModel.kt | 47 ++++++++++++- .../client/assistant/chat/ChatContent.kt | 68 +++++++++++++++++++ .../conversation/ConversationScreen.kt | 5 +- .../remote/AssistantRemoteRepository.kt | 6 ++ .../remote/AssistantRemoteRepositoryImpl.kt | 22 ++++++ .../ui/composeActivity/ComposeActivity.kt | 19 +++--- .../ui/composeActivity/ComposeDestination.kt | 15 +++- .../android/ui/activity/DrawerActivity.java | 8 +-- app/src/main/res/values/strings.xml | 2 + 10 files changed, 201 insertions(+), 28 deletions(-) create mode 100644 app/src/main/java/com/nextcloud/client/assistant/chat/ChatContent.kt diff --git a/app/src/main/java/com/nextcloud/client/assistant/AssistantScreen.kt b/app/src/main/java/com/nextcloud/client/assistant/AssistantScreen.kt index 3c9fd1f58f7f..eb0a42c4926c 100644 --- a/app/src/main/java/com/nextcloud/client/assistant/AssistantScreen.kt +++ b/app/src/main/java/com/nextcloud/client/assistant/AssistantScreen.kt @@ -53,9 +53,10 @@ import androidx.compose.ui.text.style.TextAlign 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.extensions.getInputTitle -import com.nextcloud.client.assistant.model.ScreenOverlayState import com.nextcloud.client.assistant.model.AssistantScreenState +import com.nextcloud.client.assistant.model.ScreenOverlayState import com.nextcloud.client.assistant.repository.local.MockAssistantLocalRepository import com.nextcloud.client.assistant.repository.remote.MockAssistantRemoteRepository import com.nextcloud.client.assistant.task.TaskView @@ -79,8 +80,10 @@ private const val PULL_TO_REFRESH_DELAY = 1500L fun AssistantScreen( viewModel: AssistantViewModel, capability: OCCapability, - activity: Activity + activity: Activity, + sessionId: Long? = null ) { + val chatMessages by viewModel.chatMessages.collectAsState() val messageId by viewModel.snackbarMessageId.collectAsState() val screenOverlayState by viewModel.screenOverlayState.collectAsState() @@ -101,6 +104,9 @@ fun AssistantScreen( LaunchedEffect(Unit) { viewModel.startTaskListPolling() + if (sessionId != null) { + viewModel.fetchChatMessages(sessionId) + } } DisposableEffect(Unit) { @@ -131,6 +137,7 @@ fun AssistantScreen( bottomBar = { if (!taskTypes.isNullOrEmpty()) { ChatInputBar( + sessionId, selectedTaskType, viewModel ) @@ -152,12 +159,19 @@ fun AssistantScreen( } AssistantScreenState.Content -> { - AssistantContent( - paddingValues, - filteredTaskList ?: listOf(), - viewModel, - capability - ) + if (sessionId == null) { + AssistantContent( + paddingValues, + filteredTaskList ?: listOf(), + viewModel, + capability + ) + } else { + ChatContent( + chatMessages = chatMessages, + modifier = Modifier.padding(paddingValues) + ) + } } else -> EmptyContent( @@ -179,6 +193,7 @@ fun AssistantScreen( @Composable private fun ChatInputBar( + sessionId: Long?, selectedTaskType: TaskTypeData?, viewModel: AssistantViewModel ) { @@ -222,7 +237,11 @@ private fun ChatInputBar( onClick = { if (text.isNotBlank()) { selectedTaskType?.let { - viewModel.createTask(input = text, taskType = it) + if (it.isChat) { + viewModel.sendChatMessage(content = text, sessionId) + } else { + viewModel.createTask(input = text, taskType = it) + } } scope.launch { delay(CHAT_INPUT_DELAY) diff --git a/app/src/main/java/com/nextcloud/client/assistant/AssistantViewModel.kt b/app/src/main/java/com/nextcloud/client/assistant/AssistantViewModel.kt index b866e06fb25d..d67f8af6642f 100644 --- a/app/src/main/java/com/nextcloud/client/assistant/AssistantViewModel.kt +++ b/app/src/main/java/com/nextcloud/client/assistant/AssistantViewModel.kt @@ -9,12 +9,14 @@ package com.nextcloud.client.assistant import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.nextcloud.client.assistant.model.ScreenOverlayState import com.nextcloud.client.assistant.model.AssistantScreenState +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.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 @@ -57,6 +59,9 @@ class AssistantViewModel( private val _filteredTaskList = MutableStateFlow?>(null) val filteredTaskList: StateFlow?> = _filteredTaskList + private val _chatMessages = MutableStateFlow?>(null) + val chatMessages: StateFlow?> = _chatMessages + private var taskPollingJob: Job? = null init { @@ -101,6 +106,46 @@ class AssistantViewModel( } } + fun sendChatMessage(content: String, sessionId: Long?) { + sessionId ?: return + val timestamp = System.currentTimeMillis().div(1000) + val firstHumanMessage = _chatMessages.value?.isEmpty() ?: false + val request = + ChatMessageRequest( + sessionId = sessionId.toString(), + role = "human", + content = content, + timestamp = timestamp, + firstHumanMessage = firstHumanMessage + ) + + viewModelScope.launch(Dispatchers.IO) { + val result = remoteRepository.sendChatMessage(request) + if (result != null) { + fetchChatMessages(sessionId) + } else { + _snackbarMessageId.update { + R.string.assistant_screen_chat_create_error + } + } + } + } + + fun fetchChatMessages(sessionId: Long) { + viewModelScope.launch(Dispatchers.IO) { + val result = remoteRepository.fetchChatMessages(sessionId) + if (result != null) { + _chatMessages.update { + result + } + } else { + _snackbarMessageId.update { + R.string.assistant_screen_chat_fetch_error + } + } + } + } + @Suppress("MagicNumber") fun createTask(input: String, taskType: TaskTypeData) { viewModelScope.launch(Dispatchers.IO) { diff --git a/app/src/main/java/com/nextcloud/client/assistant/chat/ChatContent.kt b/app/src/main/java/com/nextcloud/client/assistant/chat/ChatContent.kt new file mode 100644 index 000000000000..76cfbb409fad --- /dev/null +++ b/app/src/main/java/com/nextcloud/client/assistant/chat/ChatContent.kt @@ -0,0 +1,68 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2025 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nextcloud.client.assistant.chat + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.owncloud.android.lib.resources.assistant.chat.model.ChatMessage + +@Composable +fun ChatContent( + chatMessages: List?, + modifier: Modifier = Modifier +) { + LazyColumn( + modifier = modifier + .fillMaxSize() + .padding(horizontal = 12.dp, vertical = 8.dp), + verticalArrangement = Arrangement.Top, + horizontalAlignment = Alignment.CenterHorizontally, + reverseLayout = false + ) { + chatMessages?.let { messages -> + items(messages, key = { it.id }) { message -> + ChatMessageItem(message = message) + Spacer(modifier = Modifier.height(8.dp)) + } + } + } +} + +@Composable +private fun ChatMessageItem(message: ChatMessage) { + Row( + modifier = Modifier + .fillMaxWidth(), + horizontalArrangement = if (message.isHuman) Arrangement.End else Arrangement.Start + ) { + Box( + modifier = Modifier + .background( + color = if (message.isHuman) MaterialTheme.colorScheme.primary.copy(alpha = 0.1f) + else MaterialTheme.colorScheme.secondary.copy(alpha = 0.1f), + shape = MaterialTheme.shapes.medium + ) + .padding(horizontal = 12.dp, vertical = 8.dp) + ) { + Text( + text = message.content, + style = MaterialTheme.typography.bodyMedium, + color = if (message.isHuman) MaterialTheme.colorScheme.onSurface + else MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } +} diff --git a/app/src/main/java/com/nextcloud/client/assistant/conversation/ConversationScreen.kt b/app/src/main/java/com/nextcloud/client/assistant/conversation/ConversationScreen.kt index 69c565e7b3e3..508a4bf1d161 100644 --- a/app/src/main/java/com/nextcloud/client/assistant/conversation/ConversationScreen.kt +++ b/app/src/main/java/com/nextcloud/client/assistant/conversation/ConversationScreen.kt @@ -57,7 +57,7 @@ fun ConversationScreen(viewModel: ConversationViewModel) { Spacer(modifier = Modifier.weight(1f)) IconButton( onClick = { - ComposeNavigation.navigate(ComposeDestination.AssistantScreen) + ComposeNavigation.navigate(ComposeDestination.AssistantScreen(null)) } ) { Icon( @@ -126,7 +126,8 @@ private fun ConversationList( ConversationListItem( conversation = conversation, onClick = { - // TODO: + val destination = ComposeDestination.AssistantScreen(conversation.id) + ComposeNavigation.navigate(destination) }, onDeleteClick = { onDeleteClick(conversation) } ) diff --git a/app/src/main/java/com/nextcloud/client/assistant/repository/remote/AssistantRemoteRepository.kt b/app/src/main/java/com/nextcloud/client/assistant/repository/remote/AssistantRemoteRepository.kt index c46245bfd450..81fc40070d85 100644 --- a/app/src/main/java/com/nextcloud/client/assistant/repository/remote/AssistantRemoteRepository.kt +++ b/app/src/main/java/com/nextcloud/client/assistant/repository/remote/AssistantRemoteRepository.kt @@ -8,6 +8,8 @@ package com.nextcloud.client.assistant.repository.remote import com.owncloud.android.lib.common.operations.RemoteOperationResult +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 @@ -19,4 +21,8 @@ interface AssistantRemoteRepository { fun getTaskList(taskType: String): List? fun deleteTask(id: Long): RemoteOperationResult + + fun fetchChatMessages(id: Long): List? + + fun sendChatMessage(request: ChatMessageRequest): ChatMessage? } diff --git a/app/src/main/java/com/nextcloud/client/assistant/repository/remote/AssistantRemoteRepositoryImpl.kt b/app/src/main/java/com/nextcloud/client/assistant/repository/remote/AssistantRemoteRepositoryImpl.kt index 07545a3591ec..0524c8271c1d 100644 --- a/app/src/main/java/com/nextcloud/client/assistant/repository/remote/AssistantRemoteRepositoryImpl.kt +++ b/app/src/main/java/com/nextcloud/client/assistant/repository/remote/AssistantRemoteRepositoryImpl.kt @@ -9,6 +9,10 @@ package com.nextcloud.client.assistant.repository.remote import com.nextcloud.common.NextcloudClient import com.owncloud.android.lib.common.operations.RemoteOperationResult import com.owncloud.android.lib.common.operations.RemoteOperationResult.ResultCode +import com.owncloud.android.lib.resources.assistant.chat.CreateMessageRemoteOperation +import com.owncloud.android.lib.resources.assistant.chat.GetMessagesRemoteOperation +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.v1.CreateTaskRemoteOperationV1 import com.owncloud.android.lib.resources.assistant.v1.DeleteTaskRemoteOperationV1 import com.owncloud.android.lib.resources.assistant.v1.GetTaskListRemoteOperationV1 @@ -77,4 +81,22 @@ class AssistantRemoteRepositoryImpl(private val client: NextcloudClient, capabil } else { DeleteTaskRemoteOperationV1(id).execute(client) } + + override fun fetchChatMessages(id: Long): List? { + val result = GetMessagesRemoteOperation(id.toString()).execute(client) + return if (result.isSuccess) { + result.resultData + } else { + null + } + } + + override fun sendChatMessage(request: ChatMessageRequest): ChatMessage? { + val result = CreateMessageRemoteOperation(request).execute(client) + return if (result.isSuccess) { + result.resultData + } else { + null + } + } } diff --git a/app/src/main/java/com/nextcloud/ui/composeActivity/ComposeActivity.kt b/app/src/main/java/com/nextcloud/ui/composeActivity/ComposeActivity.kt index 7f3b56a84517..ee9de8ddc75e 100644 --- a/app/src/main/java/com/nextcloud/ui/composeActivity/ComposeActivity.kt +++ b/app/src/main/java/com/nextcloud/ui/composeActivity/ComposeActivity.kt @@ -26,7 +26,6 @@ import com.nextcloud.client.assistant.repository.local.AssistantLocalRepositoryI import com.nextcloud.client.assistant.repository.remote.AssistantRemoteRepositoryImpl import com.nextcloud.client.database.NextcloudDatabase import com.nextcloud.common.NextcloudClient -import com.nextcloud.utils.extensions.getSerializableArgument import com.owncloud.android.R import com.owncloud.android.databinding.ActivityComposeBinding import com.owncloud.android.ui.activity.DrawerActivity @@ -45,7 +44,7 @@ class ComposeActivity : DrawerActivity() { binding = ActivityComposeBinding.inflate(layoutInflater) setContentView(binding.root) - val destination = intent.getSerializableArgument(DESTINATION, ComposeDestination::class.java) ?: return + val destinationId = intent.getIntExtra(DESTINATION, -1) val titleId = intent.getIntExtra(TITLE, R.string.empty) setupDrawer() @@ -58,7 +57,7 @@ class ComposeActivity : DrawerActivity() { MaterialTheme( colorScheme = viewThemeUtils.getColorScheme(this), content = { - Content(destination) + Content(ComposeDestination.fromId(destinationId)) } ) } @@ -82,13 +81,14 @@ class ComposeActivity : DrawerActivity() { nextcloudClient = clientRepository.getNextcloudClient() } - when(currentScreen) { - ComposeDestination.AssistantScreen -> { - binding.bottomNavigation.menu.findItem(R.id.nav_assistant).run { - isChecked = true - } + binding.bottomNavigation.menu.findItem(R.id.nav_assistant).run { + isChecked = true + } + when(currentScreen) { + is ComposeDestination.AssistantScreen -> { val dao = NextcloudDatabase.instance().assistantDao() + val sessionId = (currentScreen as? ComposeDestination.AssistantScreen)?.sessionId nextcloudClient?.let { client -> AssistantScreen( @@ -98,7 +98,8 @@ class ComposeActivity : DrawerActivity() { localRepository = AssistantLocalRepositoryImpl(dao) ), activity = this, - capability = capabilities + capability = capabilities, + sessionId = sessionId ) } } diff --git a/app/src/main/java/com/nextcloud/ui/composeActivity/ComposeDestination.kt b/app/src/main/java/com/nextcloud/ui/composeActivity/ComposeDestination.kt index aaa557bf62d0..9932da8f25ea 100644 --- a/app/src/main/java/com/nextcloud/ui/composeActivity/ComposeDestination.kt +++ b/app/src/main/java/com/nextcloud/ui/composeActivity/ComposeDestination.kt @@ -7,8 +7,17 @@ */ package com.nextcloud.ui.composeActivity -import java.io.Serializable +sealed class ComposeDestination(val id: Int) { + data class AssistantScreen(val sessionId: Long?) : ComposeDestination(0) + data object ConversationScreen : ComposeDestination(1) -enum class ComposeDestination : Serializable { - AssistantScreen, ConversationScreen + companion object { + fun fromId(id: Int): ComposeDestination { + return when (id) { + 0 -> AssistantScreen(null) + 1 -> ConversationScreen + else -> throw IllegalArgumentException("Unknown destination: $id") + } + } + } } diff --git a/app/src/main/java/com/owncloud/android/ui/activity/DrawerActivity.java b/app/src/main/java/com/owncloud/android/ui/activity/DrawerActivity.java index c2e16c0630b7..5b72371a8922 100644 --- a/app/src/main/java/com/owncloud/android/ui/activity/DrawerActivity.java +++ b/app/src/main/java/com/owncloud/android/ui/activity/DrawerActivity.java @@ -282,7 +282,7 @@ private void handleBottomNavigationViewClicks() { setupToolbar(); handleSearchEvents(new SearchEvent("", SearchRemoteOperation.SearchType.FAVORITE_SEARCH), menuItemId); } else if (menuItemId == R.id.nav_assistant && !(this instanceof ComposeActivity)) { - startComposeActivity(ComposeDestination.AssistantScreen, R.string.assistant_screen_top_bar_title); + startComposeActivity(new ComposeDestination.AssistantScreen(null), R.string.assistant_screen_top_bar_title); } else if (menuItemId == R.id.nav_gallery) { setupToolbar(); startPhotoSearch(menuItem.getItemId()); @@ -470,7 +470,7 @@ private void showTopBanner(ConstraintLayout banner) { moreView.setOnClickListener(v -> LinkHelper.INSTANCE.openAppStore("Nextcloud", true, this)); assistantView.setOnClickListener(v -> { DrawerActivity.menuItemId = Menu.NONE; - startComposeActivity(ComposeDestination.AssistantScreen, R.string.assistant_screen_top_bar_title); + startComposeActivity(new ComposeDestination.AssistantScreen(null), R.string.assistant_screen_top_bar_title); }); if (getCapabilities() != null && getCapabilities().getAssistant().isTrue()) { assistantView.setVisibility(View.VISIBLE); @@ -632,7 +632,7 @@ private void onNavigationItemClicked(final MenuItem menuItem) { startRecentlyModifiedSearch(menuItem); } else if (itemId == R.id.nav_assistant) { resetOnlyPersonalAndOnDevice(); - startComposeActivity(ComposeDestination.AssistantScreen, R.string.assistant_screen_top_bar_title); + startComposeActivity(new ComposeDestination.AssistantScreen(null), R.string.assistant_screen_top_bar_title); } else if (itemId == R.id.nav_groupfolders) { resetOnlyPersonalAndOnDevice(); Intent intent = new Intent(getApplicationContext(), FileDisplayActivity.class); @@ -652,7 +652,7 @@ private void onNavigationItemClicked(final MenuItem menuItem) { private void startComposeActivity(ComposeDestination destination, int titleId) { Intent composeActivity = new Intent(getApplicationContext(), ComposeActivity.class); - composeActivity.putExtra(ComposeActivity.DESTINATION, destination); + composeActivity.putExtra(ComposeActivity.DESTINATION, destination.getId()); composeActivity.putExtra(ComposeActivity.TITLE, titleId); startActivity(composeActivity); } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 05af99ff3074..e182ff1827fe 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -66,6 +66,8 @@ Failed to create conversation Failed to delete conversation New conversation + Failed to send a message + Failed to fetch chat messages Hello there! What can I help you with today? Try sending a message to spark a conversation. From f898140c2ec60ff8a3456998395c63b7ef69a763 Mon Sep 17 00:00:00 2001 From: alperozturk Date: Tue, 11 Nov 2025 13:33:47 +0100 Subject: [PATCH 05/29] implement chat content Signed-off-by: alperozturk --- .../client/assistant/AssistantScreen.kt | 43 +++--- .../client/assistant/AssistantViewModel.kt | 67 ++++++--- .../client/assistant/chat/ChatContent.kt | 133 ++++++++++++++---- .../assistant/model/AssistantScreenState.kt | 4 +- .../remote/MockAssistantRemoteRepository.kt | 10 ++ 5 files changed, 184 insertions(+), 73 deletions(-) diff --git a/app/src/main/java/com/nextcloud/client/assistant/AssistantScreen.kt b/app/src/main/java/com/nextcloud/client/assistant/AssistantScreen.kt index eb0a42c4926c..7d8b56f5916a 100644 --- a/app/src/main/java/com/nextcloud/client/assistant/AssistantScreen.kt +++ b/app/src/main/java/com/nextcloud/client/assistant/AssistantScreen.kt @@ -83,7 +83,6 @@ fun AssistantScreen( activity: Activity, sessionId: Long? = null ) { - val chatMessages by viewModel.chatMessages.collectAsState() val messageId by viewModel.snackbarMessageId.collectAsState() val screenOverlayState by viewModel.screenOverlayState.collectAsState() @@ -103,7 +102,7 @@ fun AssistantScreen( } LaunchedEffect(Unit) { - viewModel.startTaskListPolling() + viewModel.startPolling(sessionId) if (sessionId != null) { viewModel.fetchChatMessages(sessionId) } @@ -111,7 +110,7 @@ fun AssistantScreen( DisposableEffect(Unit) { onDispose { - viewModel.stopTaskListPolling() + viewModel.stopPolling() } } @@ -158,20 +157,20 @@ fun AssistantScreen( ) } - AssistantScreenState.Content -> { - if (sessionId == null) { - AssistantContent( - paddingValues, - filteredTaskList ?: listOf(), - viewModel, - capability - ) - } else { - ChatContent( - chatMessages = chatMessages, - modifier = Modifier.padding(paddingValues) - ) - } + AssistantScreenState.TaskContent -> { + TaskContent( + paddingValues, + filteredTaskList ?: listOf(), + viewModel, + capability + ) + } + + AssistantScreenState.ChatContent -> { + ChatContent( + viewModel = viewModel, + modifier = Modifier.padding(paddingValues) + ) } else -> EmptyContent( @@ -269,20 +268,20 @@ private fun OverlayState(state: ScreenOverlayState?, activity: Activity, viewMod SimpleAlertDialog( title = stringResource(id = R.string.assistant_screen_delete_task_alert_dialog_title), description = stringResource(id = R.string.assistant_screen_delete_task_alert_dialog_description), - dismiss = { viewModel.updateTaskListScreenState(null) }, + dismiss = { viewModel.updateScreenState(null) }, onComplete = { viewModel.deleteTask(state.id) } ) } is ScreenOverlayState.TaskActions -> { val actions = state.getActions(activity, onDeleteCompleted = { deleteTask -> - viewModel.updateTaskListScreenState(deleteTask) + viewModel.updateScreenState(deleteTask) }) MoreActionsBottomSheet( title = state.task.getInputTitle(), actions = actions, - dismiss = { viewModel.updateTaskListScreenState(null) } + dismiss = { viewModel.updateScreenState(null) } ) } @@ -291,7 +290,7 @@ private fun OverlayState(state: ScreenOverlayState?, activity: Activity, viewMod } @Composable -private fun AssistantContent( +private fun TaskContent( paddingValues: PaddingValues, taskList: List, viewModel: AssistantViewModel, @@ -311,7 +310,7 @@ private fun AssistantContent( capability, showTaskActions = { val newState = ScreenOverlayState.TaskActions(task) - viewModel.updateTaskListScreenState(newState) + viewModel.updateScreenState(newState) } ) Spacer(modifier = Modifier.height(8.dp)) diff --git a/app/src/main/java/com/nextcloud/client/assistant/AssistantViewModel.kt b/app/src/main/java/com/nextcloud/client/assistant/AssistantViewModel.kt index d67f8af6642f..be17a61187c0 100644 --- a/app/src/main/java/com/nextcloud/client/assistant/AssistantViewModel.kt +++ b/app/src/main/java/com/nextcloud/client/assistant/AssistantViewModel.kt @@ -36,7 +36,7 @@ class AssistantViewModel( companion object { private const val TAG = "AssistantViewModel" - private const val TASK_LIST_POLLING_INTERVAL_MS = 15_000L + private const val POLLING_INTERVAL_MS = 15_000L } private val _screenState = MutableStateFlow(null) @@ -59,25 +59,29 @@ class AssistantViewModel( private val _filteredTaskList = MutableStateFlow?>(null) val filteredTaskList: StateFlow?> = _filteredTaskList - private val _chatMessages = MutableStateFlow?>(null) - val chatMessages: StateFlow?> = _chatMessages + private val _chatMessages = MutableStateFlow>(listOf()) + val chatMessages: StateFlow> = _chatMessages - private var taskPollingJob: Job? = null + private var pollingJob: Job? = null init { fetchTaskTypes() } // region task polling - fun startTaskListPolling() { - stopTaskListPolling() + fun startPolling(sessionId: Long?) { + stopPolling() - taskPollingJob = viewModelScope.launch(Dispatchers.IO) { + pollingJob = viewModelScope.launch(Dispatchers.IO) { try { while (isActive) { - Log_OC.d(TAG, "Polling task list...") - fetchTaskListSuspending() - delay(TASK_LIST_POLLING_INTERVAL_MS) + Log_OC.d(TAG, "Polling list...") + if (sessionId != null) { + pollChatMessages(sessionId) + } else { + pollTaskList() + } + delay(POLLING_INTERVAL_MS) } } finally { Log_OC.d(TAG, "Polling coroutine cancelled") @@ -85,13 +89,13 @@ class AssistantViewModel( } } - fun stopTaskListPolling() { - taskPollingJob?.cancel() - taskPollingJob = null + fun stopPolling() { + pollingJob?.cancel() + pollingJob = null } // endregion - private suspend fun fetchTaskListSuspending() { + private suspend fun pollTaskList() { val cachedTasks = localRepository.getCachedTasks(accountName) if (cachedTasks.isNotEmpty()) { _filteredTaskList.value = cachedTasks.sortedByDescending { it.id } @@ -106,10 +110,19 @@ class AssistantViewModel( } } + private fun pollChatMessages(sessionId: Long) { + val result = remoteRepository.fetchChatMessages(sessionId) + if (result != null) { + _chatMessages.update { + result + } + } + } + fun sendChatMessage(content: String, sessionId: Long?) { sessionId ?: return val timestamp = System.currentTimeMillis().div(1000) - val firstHumanMessage = _chatMessages.value?.isEmpty() ?: false + val firstHumanMessage = _chatMessages.value.isEmpty() val request = ChatMessageRequest( sessionId = sessionId.toString(), @@ -135,6 +148,10 @@ class AssistantViewModel( viewModelScope.launch(Dispatchers.IO) { val result = remoteRepository.fetchChatMessages(sessionId) if (result != null) { + _screenState.update { + AssistantScreenState.ChatContent + } + _chatMessages.update { result } @@ -198,7 +215,7 @@ class AssistantViewModel( _filteredTaskList.update { cachedTasks.sortedByDescending { it.id } } - updateTaskListScreenState() + updateScreenState() } val taskType = _selectedTaskType.value?.id ?: return@launch @@ -217,16 +234,22 @@ class AssistantViewModel( updateSnackbarMessage(R.string.assistant_screen_task_list_error_state_message) } - updateTaskListScreenState() + updateScreenState() } } - private fun updateTaskListScreenState() { + private fun updateScreenState() { + val isChat = _selectedTaskType.value?.isChat ?: false + _screenState.update { - if (_filteredTaskList.value?.isEmpty() == true) { - AssistantScreenState.emptyTaskList() + if (isChat) { + AssistantScreenState.ChatContent } else { - AssistantScreenState.Content + if (_filteredTaskList.value?.isEmpty() == true) { + AssistantScreenState.emptyTaskList() + } else { + AssistantScreenState.TaskContent + } } } } @@ -256,7 +279,7 @@ class AssistantViewModel( } } - fun updateTaskListScreenState(value: ScreenOverlayState?) { + fun updateScreenState(value: ScreenOverlayState?) { _screenOverlayState.update { value } diff --git a/app/src/main/java/com/nextcloud/client/assistant/chat/ChatContent.kt b/app/src/main/java/com/nextcloud/client/assistant/chat/ChatContent.kt index 76cfbb409fad..d8c6e389c93a 100644 --- a/app/src/main/java/com/nextcloud/client/assistant/chat/ChatContent.kt +++ b/app/src/main/java/com/nextcloud/client/assistant/chat/ChatContent.kt @@ -7,62 +7,139 @@ package com.nextcloud.client.assistant.chat +import androidx.compose.foundation.Image import androidx.compose.foundation.background -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.defaultMinSize +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items -import androidx.compose.material3.MaterialTheme +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.TextStyle import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.nextcloud.client.assistant.AssistantViewModel +import com.owncloud.android.R import com.owncloud.android.lib.resources.assistant.chat.model.ChatMessage @Composable fun ChatContent( - chatMessages: List?, + viewModel: AssistantViewModel, modifier: Modifier = Modifier ) { + val chatMessages by viewModel.chatMessages.collectAsState() + LazyColumn( modifier = modifier .fillMaxSize() .padding(horizontal = 12.dp, vertical = 8.dp), - verticalArrangement = Arrangement.Top, - horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Bottom, reverseLayout = false ) { - chatMessages?.let { messages -> - items(messages, key = { it.id }) { message -> - ChatMessageItem(message = message) - Spacer(modifier = Modifier.height(8.dp)) - } + items(chatMessages, key = { it.id }) { message -> + ChatMessageItem(message = message) + Spacer(modifier = Modifier.height(8.dp)) } } } @Composable private fun ChatMessageItem(message: ChatMessage) { - Row( - modifier = Modifier - .fillMaxWidth(), - horizontalArrangement = if (message.isHuman) Arrangement.End else Arrangement.Start - ) { - Box( - modifier = Modifier - .background( - color = if (message.isHuman) MaterialTheme.colorScheme.primary.copy(alpha = 0.1f) - else MaterialTheme.colorScheme.secondary.copy(alpha = 0.1f), - shape = MaterialTheme.shapes.medium + val modifier = if (message.isHuman()) { + Modifier + .padding(start = 16.dp, end = 8.dp) + .defaultMinSize(minHeight = 60.dp) + .clip(RoundedCornerShape(topEnd = 20.dp, topStart = 20.dp, bottomStart = 20.dp)) + .background( + brush = Brush.linearGradient( + colors = listOf( + Color(0xFF007EF4), + Color(0xFF2A75BC), + ) ) - .padding(horizontal = 12.dp, vertical = 8.dp) - ) { - Text( - text = message.content, - style = MaterialTheme.typography.bodyMedium, - color = if (message.isHuman) MaterialTheme.colorScheme.onSurface - else MaterialTheme.colorScheme.onSurfaceVariant ) + } else { + Modifier + .padding(start = 8.dp, end = 16.dp) + .defaultMinSize(minHeight = 60.dp) + .clip(RoundedCornerShape(topEnd = 20.dp, topStart = 20.dp, bottomEnd = 20.dp)) + .background( + brush = Brush.linearGradient( + colors = listOf( + Color(0xFF454545), + Color(0xFF2B2B2B), + ) + ) + ) + } + + val boxArrangement = if (message.isHuman()) Alignment.CenterEnd else Alignment.CenterStart + + Box(modifier = Modifier.padding(vertical = 12.dp).fillMaxWidth(), contentAlignment = boxArrangement) { + Row( + verticalAlignment = Alignment.Bottom, + ) { + if (!message.isHuman()) { + Box( + modifier = Modifier + .size(40.dp) + .clip(CircleShape) + .background(Color.White) + ) + { + Image( + painter = painterResource(id = R.drawable.ic_assistant), + contentDescription = null, + contentScale = ContentScale.Crop + ) + } + } + + + Box( + modifier = modifier + ) { + Column( + modifier = Modifier.padding(8.dp), + horizontalAlignment = Alignment.Start, + ) { + Text( + text = message.content, + style = TextStyle( + color = Color.White, + fontSize = 16.sp, + ) + ) + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = message.timestampRepresentation(), + style = TextStyle( + color = Color.White, + fontSize = 12.sp, + ) + ) + } + } } } } diff --git a/app/src/main/java/com/nextcloud/client/assistant/model/AssistantScreenState.kt b/app/src/main/java/com/nextcloud/client/assistant/model/AssistantScreenState.kt index ead9f98c59a8..43b046a2bde8 100644 --- a/app/src/main/java/com/nextcloud/client/assistant/model/AssistantScreenState.kt +++ b/app/src/main/java/com/nextcloud/client/assistant/model/AssistantScreenState.kt @@ -12,7 +12,9 @@ import com.owncloud.android.R sealed class AssistantScreenState { data object Loading : AssistantScreenState() - data object Content : AssistantScreenState() + data object TaskContent : AssistantScreenState() + + data object ChatContent : AssistantScreenState() data class EmptyContent(val iconId: Int?, val titleId: Int?, val descriptionId: Int) : AssistantScreenState() diff --git a/app/src/main/java/com/nextcloud/client/assistant/repository/remote/MockAssistantRemoteRepository.kt b/app/src/main/java/com/nextcloud/client/assistant/repository/remote/MockAssistantRemoteRepository.kt index b7acd88b41fd..28bd3dc50636 100644 --- a/app/src/main/java/com/nextcloud/client/assistant/repository/remote/MockAssistantRemoteRepository.kt +++ b/app/src/main/java/com/nextcloud/client/assistant/repository/remote/MockAssistantRemoteRepository.kt @@ -8,6 +8,8 @@ package com.nextcloud.client.assistant.repository.remote import com.nextcloud.utils.extensions.getRandomString import com.owncloud.android.lib.common.operations.RemoteOperationResult +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.Shape import com.owncloud.android.lib.resources.assistant.v2.model.Task import com.owncloud.android.lib.resources.assistant.v2.model.TaskInput @@ -64,4 +66,12 @@ class MockAssistantRemoteRepository(private val giveEmptyTasks: Boolean = false) override fun deleteTask(id: Long): RemoteOperationResult = RemoteOperationResult(RemoteOperationResult.ResultCode.OK) + + override fun fetchChatMessages(id: Long): List? { + return emptyList() + } + + override fun sendChatMessage(request: ChatMessageRequest): ChatMessage? { + return null + } } From 7bfa5421104c3d0c7c24a7fd5ddea24ac3cd87e7 Mon Sep 17 00:00:00 2001 From: alperozturk Date: Tue, 11 Nov 2025 15:00:51 +0100 Subject: [PATCH 06/29] implement fast chat Signed-off-by: alperozturk --- app/build.gradle.kts | 1 + .../client/assistant/AssistantScreen.kt | 46 +++-- .../client/assistant/AssistantViewModel.kt | 21 ++- .../client/assistant/chat/ChatContent.kt | 168 +++++++++++------- .../remote/AssistantRemoteRepository.kt | 3 + .../remote/AssistantRemoteRepositoryImpl.kt | 14 ++ .../remote/MockAssistantRemoteRepository.kt | 5 + .../ui/composeActivity/ComposeActivity.kt | 2 +- gradle/libs.versions.toml | 2 + 9 files changed, 176 insertions(+), 86 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 7c5764b7af6b..1d38331a1588 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -355,6 +355,7 @@ dependencies { implementation(libs.compose.ui.graphics) implementation(libs.compose.material3) implementation(libs.compose.ui.tooling.preview) + implementation(libs.foundation) debugImplementation(libs.compose.ui.tooling) // endregion diff --git a/app/src/main/java/com/nextcloud/client/assistant/AssistantScreen.kt b/app/src/main/java/com/nextcloud/client/assistant/AssistantScreen.kt index 7d8b56f5916a..205f5873d239 100644 --- a/app/src/main/java/com/nextcloud/client/assistant/AssistantScreen.kt +++ b/app/src/main/java/com/nextcloud/client/assistant/AssistantScreen.kt @@ -81,8 +81,9 @@ fun AssistantScreen( viewModel: AssistantViewModel, capability: OCCapability, activity: Activity, - sessionId: Long? = null + sessionIdArg: Long? = null ) { + val sessionId by viewModel.sessionId.collectAsState() val messageId by viewModel.snackbarMessageId.collectAsState() val screenOverlayState by viewModel.screenOverlayState.collectAsState() @@ -101,10 +102,17 @@ fun AssistantScreen( } } + // replace polling task for newly created conversation + LaunchedEffect(sessionId) { + val newSessionId = sessionId ?: return@LaunchedEffect + viewModel.startPolling(newSessionId) + viewModel.fetchChatMessages(newSessionId) + } + LaunchedEffect(Unit) { - viewModel.startPolling(sessionId) - if (sessionId != null) { - viewModel.fetchChatMessages(sessionId) + viewModel.startPolling(sessionIdArg) + if (sessionIdArg != null) { + viewModel.fetchChatMessages(sessionIdArg) } } @@ -136,7 +144,7 @@ fun AssistantScreen( bottomBar = { if (!taskTypes.isNullOrEmpty()) { ChatInputBar( - sessionId, + sessionIdArg, selectedTaskType, viewModel ) @@ -234,18 +242,24 @@ private fun ChatInputBar( IconButton( onClick = { - if (text.isNotBlank()) { - selectedTaskType?.let { - if (it.isChat) { - viewModel.sendChatMessage(content = text, sessionId) - } else { - viewModel.createTask(input = text, taskType = it) - } - } - scope.launch { - delay(CHAT_INPUT_DELAY) - text = "" + if (text.isBlank()) { + return@IconButton + } + + val taskType = selectedTaskType ?: return@IconButton + if (taskType.isChat) { + if (sessionId != null) { + viewModel.sendChatMessage(content = text, sessionId) + } else { + viewModel.createConversation(text) } + } else { + viewModel.createTask(input = text, taskType = taskType) + } + + scope.launch { + delay(CHAT_INPUT_DELAY) + text = "" } } ) { diff --git a/app/src/main/java/com/nextcloud/client/assistant/AssistantViewModel.kt b/app/src/main/java/com/nextcloud/client/assistant/AssistantViewModel.kt index be17a61187c0..d8336edf7460 100644 --- a/app/src/main/java/com/nextcloud/client/assistant/AssistantViewModel.kt +++ b/app/src/main/java/com/nextcloud/client/assistant/AssistantViewModel.kt @@ -45,6 +45,9 @@ class AssistantViewModel( private val _screenOverlayState = MutableStateFlow(null) val screenOverlayState: StateFlow = _screenOverlayState + private val _sessionId = MutableStateFlow(null) + val sessionId: StateFlow = _sessionId + private val _snackbarMessageId = MutableStateFlow(null) val snackbarMessageId: StateFlow = _snackbarMessageId @@ -119,8 +122,7 @@ class AssistantViewModel( } } - fun sendChatMessage(content: String, sessionId: Long?) { - sessionId ?: return + fun sendChatMessage(content: String, sessionId: Long) { val timestamp = System.currentTimeMillis().div(1000) val firstHumanMessage = _chatMessages.value.isEmpty() val request = @@ -144,6 +146,12 @@ class AssistantViewModel( } } + fun initSessionId(value: Long) { + _sessionId.update { + value + } + } + fun fetchChatMessages(sessionId: Long) { viewModelScope.launch(Dispatchers.IO) { val result = remoteRepository.fetchChatMessages(sessionId) @@ -163,6 +171,15 @@ class AssistantViewModel( } } + fun createConversation(title: String) { + viewModelScope.launch(Dispatchers.IO) { + val result = remoteRepository.createConversation(title) + if (result != null) { + sendChatMessage(content = title, sessionId = result.session.id) + } + } + } + @Suppress("MagicNumber") fun createTask(input: String, taskType: TaskTypeData) { viewModelScope.launch(Dispatchers.IO) { diff --git a/app/src/main/java/com/nextcloud/client/assistant/chat/ChatContent.kt b/app/src/main/java/com/nextcloud/client/assistant/chat/ChatContent.kt index d8c6e389c93a..18f7e47818a3 100644 --- a/app/src/main/java/com/nextcloud/client/assistant/chat/ChatContent.kt +++ b/app/src/main/java/com/nextcloud/client/assistant/chat/ChatContent.kt @@ -22,18 +22,20 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.colorResource import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.TextStyle import androidx.compose.ui.unit.dp @@ -42,104 +44,136 @@ import com.nextcloud.client.assistant.AssistantViewModel import com.owncloud.android.R import com.owncloud.android.lib.resources.assistant.chat.model.ChatMessage +private val MIN_CHAT_HEIGHT = 60.dp +private val CHAT_BUBBLE_CORNER_RADIUS = 8.dp +private val ASSISTANT_ICON_SIZE = 40.dp + @Composable fun ChatContent( viewModel: AssistantViewModel, modifier: Modifier = Modifier ) { val chatMessages by viewModel.chatMessages.collectAsState() + val listState = rememberLazyListState() + + LaunchedEffect(chatMessages) { + listState.animateScrollToItem(listState.layoutInfo.totalItemsCount) + } LazyColumn( modifier = modifier .fillMaxSize() .padding(horizontal = 12.dp, vertical = 8.dp), verticalArrangement = Arrangement.Bottom, - reverseLayout = false + reverseLayout = false, + state = listState, ) { items(chatMessages, key = { it.id }) { message -> - ChatMessageItem(message = message) + if (message.isHuman()) { + UserMessageItem(message) + } else { + AssistantMessageItem(message) + } Spacer(modifier = Modifier.height(8.dp)) } } } @Composable -private fun ChatMessageItem(message: ChatMessage) { - val modifier = if (message.isHuman()) { - Modifier - .padding(start = 16.dp, end = 8.dp) - .defaultMinSize(minHeight = 60.dp) - .clip(RoundedCornerShape(topEnd = 20.dp, topStart = 20.dp, bottomStart = 20.dp)) - .background( - brush = Brush.linearGradient( - colors = listOf( - Color(0xFF007EF4), - Color(0xFF2A75BC), - ) - ) - ) - } else { - Modifier - .padding(start = 8.dp, end = 16.dp) - .defaultMinSize(minHeight = 60.dp) - .clip(RoundedCornerShape(topEnd = 20.dp, topStart = 20.dp, bottomEnd = 20.dp)) - .background( - brush = Brush.linearGradient( - colors = listOf( - Color(0xFF454545), - Color(0xFF2B2B2B), - ) - ) - ) - } - - val boxArrangement = if (message.isHuman()) Alignment.CenterEnd else Alignment.CenterStart - - Box(modifier = Modifier.padding(vertical = 12.dp).fillMaxWidth(), contentAlignment = boxArrangement) { +private fun AssistantMessageItem(message: ChatMessage) { + Box( + modifier = Modifier + .padding(vertical = 12.dp) + .fillMaxWidth(), contentAlignment = Alignment.CenterStart + ) { Row( verticalAlignment = Alignment.Bottom, ) { - if (!message.isHuman()) { - Box( - modifier = Modifier - .size(40.dp) - .clip(CircleShape) - .background(Color.White) + Box( + modifier = Modifier + .size(ASSISTANT_ICON_SIZE) + .clip(CircleShape) + .background(Color.White), + contentAlignment = Alignment.Center + ) { + Image( + painter = painterResource(id = R.drawable.ic_assistant), + contentDescription = null, + contentScale = ContentScale.Crop, + alignment = Alignment.Center ) - { - Image( - painter = painterResource(id = R.drawable.ic_assistant), - contentDescription = null, - contentScale = ContentScale.Crop - ) - } } - Box( - modifier = modifier - ) { - Column( - modifier = Modifier.padding(8.dp), - horizontalAlignment = Alignment.Start, - ) { - Text( - text = message.content, - style = TextStyle( - color = Color.White, - fontSize = 16.sp, + modifier = Modifier + .padding(start = 8.dp, end = 16.dp) + .defaultMinSize(minHeight = MIN_CHAT_HEIGHT) + .clip( + RoundedCornerShape( + topEnd = CHAT_BUBBLE_CORNER_RADIUS, + topStart = CHAT_BUBBLE_CORNER_RADIUS, + bottomEnd = CHAT_BUBBLE_CORNER_RADIUS ) ) - Spacer(modifier = Modifier.height(8.dp)) - Text( - text = message.timestampRepresentation(), - style = TextStyle( - color = Color.White, - fontSize = 12.sp, + .background( + color = colorResource(R.color.task_container) + ) + ) { + MessageTextItem(message) + } + } + } +} + +@Composable +private fun UserMessageItem(message: ChatMessage) { + Box( + modifier = Modifier + .padding(vertical = 12.dp) + .fillMaxWidth(), contentAlignment = Alignment.CenterEnd + ) { + Row( + verticalAlignment = Alignment.Bottom, + ) { + Box( + modifier = Modifier + .padding(start = 16.dp, end = 8.dp) + .defaultMinSize(minHeight = MIN_CHAT_HEIGHT) + .clip( + RoundedCornerShape( + topEnd = CHAT_BUBBLE_CORNER_RADIUS, + topStart = CHAT_BUBBLE_CORNER_RADIUS, + bottomStart = CHAT_BUBBLE_CORNER_RADIUS ) ) - } + .background(color = colorResource(R.color.task_container)) + ) { + MessageTextItem(message) } } } } + +@Composable +private fun MessageTextItem(message: ChatMessage) { + Column( + modifier = Modifier.padding(8.dp), + horizontalAlignment = Alignment.Start, + ) { + Text( + text = message.content, + style = TextStyle( + color = colorResource(R.color.text_color), + fontSize = 16.sp, + ) + ) + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = message.timestampRepresentation(), + style = TextStyle( + color = colorResource(R.color.text_color), + fontSize = 12.sp, + ) + ) + } +} diff --git a/app/src/main/java/com/nextcloud/client/assistant/repository/remote/AssistantRemoteRepository.kt b/app/src/main/java/com/nextcloud/client/assistant/repository/remote/AssistantRemoteRepository.kt index 81fc40070d85..420b9b08168e 100644 --- a/app/src/main/java/com/nextcloud/client/assistant/repository/remote/AssistantRemoteRepository.kt +++ b/app/src/main/java/com/nextcloud/client/assistant/repository/remote/AssistantRemoteRepository.kt @@ -10,6 +10,7 @@ package com.nextcloud.client.assistant.repository.remote import com.owncloud.android.lib.common.operations.RemoteOperationResult 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.chat.model.CreateConversation import com.owncloud.android.lib.resources.assistant.v2.model.Task import com.owncloud.android.lib.resources.assistant.v2.model.TaskTypeData @@ -25,4 +26,6 @@ interface AssistantRemoteRepository { fun fetchChatMessages(id: Long): List? fun sendChatMessage(request: ChatMessageRequest): ChatMessage? + + fun createConversation(title: String): CreateConversation? } diff --git a/app/src/main/java/com/nextcloud/client/assistant/repository/remote/AssistantRemoteRepositoryImpl.kt b/app/src/main/java/com/nextcloud/client/assistant/repository/remote/AssistantRemoteRepositoryImpl.kt index 0524c8271c1d..26286df875f5 100644 --- a/app/src/main/java/com/nextcloud/client/assistant/repository/remote/AssistantRemoteRepositoryImpl.kt +++ b/app/src/main/java/com/nextcloud/client/assistant/repository/remote/AssistantRemoteRepositoryImpl.kt @@ -9,10 +9,12 @@ package com.nextcloud.client.assistant.repository.remote import com.nextcloud.common.NextcloudClient import com.owncloud.android.lib.common.operations.RemoteOperationResult import com.owncloud.android.lib.common.operations.RemoteOperationResult.ResultCode +import com.owncloud.android.lib.resources.assistant.chat.CreateConversationRemoteOperation import com.owncloud.android.lib.resources.assistant.chat.CreateMessageRemoteOperation import com.owncloud.android.lib.resources.assistant.chat.GetMessagesRemoteOperation 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.chat.model.CreateConversation import com.owncloud.android.lib.resources.assistant.v1.CreateTaskRemoteOperationV1 import com.owncloud.android.lib.resources.assistant.v1.DeleteTaskRemoteOperationV1 import com.owncloud.android.lib.resources.assistant.v1.GetTaskListRemoteOperationV1 @@ -99,4 +101,16 @@ class AssistantRemoteRepositoryImpl(private val client: NextcloudClient, capabil null } } + + @Suppress("MagicNumber") + override fun createConversation(title: String): CreateConversation? { + val timestamp = (System.currentTimeMillis() / 1000) + val result = + CreateConversationRemoteOperation(title, timestamp).execute(client) + return if (result.isSuccess) { + result.resultData + } else { + null + } + } } diff --git a/app/src/main/java/com/nextcloud/client/assistant/repository/remote/MockAssistantRemoteRepository.kt b/app/src/main/java/com/nextcloud/client/assistant/repository/remote/MockAssistantRemoteRepository.kt index 28bd3dc50636..fcd56e7d44a5 100644 --- a/app/src/main/java/com/nextcloud/client/assistant/repository/remote/MockAssistantRemoteRepository.kt +++ b/app/src/main/java/com/nextcloud/client/assistant/repository/remote/MockAssistantRemoteRepository.kt @@ -10,6 +10,7 @@ import com.nextcloud.utils.extensions.getRandomString import com.owncloud.android.lib.common.operations.RemoteOperationResult 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.chat.model.CreateConversation import com.owncloud.android.lib.resources.assistant.v2.model.Shape import com.owncloud.android.lib.resources.assistant.v2.model.Task import com.owncloud.android.lib.resources.assistant.v2.model.TaskInput @@ -74,4 +75,8 @@ class MockAssistantRemoteRepository(private val giveEmptyTasks: Boolean = false) override fun sendChatMessage(request: ChatMessageRequest): ChatMessage? { return null } + + override fun createConversation(title: String): CreateConversation? { + return null + } } diff --git a/app/src/main/java/com/nextcloud/ui/composeActivity/ComposeActivity.kt b/app/src/main/java/com/nextcloud/ui/composeActivity/ComposeActivity.kt index ee9de8ddc75e..b103c9d60718 100644 --- a/app/src/main/java/com/nextcloud/ui/composeActivity/ComposeActivity.kt +++ b/app/src/main/java/com/nextcloud/ui/composeActivity/ComposeActivity.kt @@ -99,7 +99,7 @@ class ComposeActivity : DrawerActivity() { ), activity = this, capability = capabilities, - sessionId = sessionId + sessionIdArg = sessionId ) } } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index dee97d166595..4cdc360006d4 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -84,6 +84,7 @@ spotless = "8.1.0" stateless4jVersion = "2.6.0" webkitVersion = "1.14.0" workRuntime = "2.10.4" +foundationVersion = "1.9.4" [libraries] # Crypto @@ -236,6 +237,7 @@ qrcodescanner = { module = "com.github.nextcloud-deps:qrcodescanner", version.re # Worker work-runtime = { module = "androidx.work:work-runtime", version.ref = "workRuntime" } work-runtime-ktx = { module = "androidx.work:work-runtime-ktx", version.ref = "workRuntime" } +foundation = { group = "androidx.compose.foundation", name = "foundation", version.ref = "foundationVersion" } [bundles] media3 = ["media3-ui", "media3-session", "media3-exoplayer", "media3-datasource"] From 7a96aa1b13b329fcfa8d27cc4f9c5d4acc0f81cf Mon Sep 17 00:00:00 2001 From: alperozturk Date: Tue, 11 Nov 2025 15:07:11 +0100 Subject: [PATCH 07/29] implement fast chat Signed-off-by: alperozturk --- .../com/nextcloud/client/assistant/AssistantViewModel.kt | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/com/nextcloud/client/assistant/AssistantViewModel.kt b/app/src/main/java/com/nextcloud/client/assistant/AssistantViewModel.kt index d8336edf7460..27092a3d1a9b 100644 --- a/app/src/main/java/com/nextcloud/client/assistant/AssistantViewModel.kt +++ b/app/src/main/java/com/nextcloud/client/assistant/AssistantViewModel.kt @@ -146,12 +146,6 @@ class AssistantViewModel( } } - fun initSessionId(value: Long) { - _sessionId.update { - value - } - } - fun fetchChatMessages(sessionId: Long) { viewModelScope.launch(Dispatchers.IO) { val result = remoteRepository.fetchChatMessages(sessionId) @@ -175,6 +169,9 @@ class AssistantViewModel( viewModelScope.launch(Dispatchers.IO) { val result = remoteRepository.createConversation(title) if (result != null) { + _sessionId.update { + result.session.id + } sendChatMessage(content = title, sessionId = result.session.id) } } From bf4393c6c10788db5f24252244ed5398c5af2540 Mon Sep 17 00:00:00 2001 From: alperozturk Date: Tue, 11 Nov 2025 15:51:49 +0100 Subject: [PATCH 08/29] implement pager for side conversation screen Signed-off-by: alperozturk --- .../client/assistant/AssistantScreen.kt | 184 +++++++++++------- .../client/assistant/AssistantViewModel.kt | 17 +- .../conversation/ConversationScreen.kt | 52 +++-- .../conversation/ConversationViewModel.kt | 3 +- .../MockConversationRemoteRepository.kt | 28 +++ .../assistant/model/AssistantScreenState.kt | 2 +- .../assistant/taskTypes/TaskTypesRow.kt | 9 +- .../ui/composeActivity/ComposeActivity.kt | 32 ++- .../ui/composeActivity/ComposeDestination.kt | 2 - app/src/main/res/values/strings.xml | 2 +- 10 files changed, 213 insertions(+), 118 deletions(-) create mode 100644 app/src/main/java/com/nextcloud/client/assistant/conversation/repository/MockConversationRemoteRepository.kt diff --git a/app/src/main/java/com/nextcloud/client/assistant/AssistantScreen.kt b/app/src/main/java/com/nextcloud/client/assistant/AssistantScreen.kt index 205f5873d239..3c38bca571fd 100644 --- a/app/src/main/java/com/nextcloud/client/assistant/AssistantScreen.kt +++ b/app/src/main/java/com/nextcloud/client/assistant/AssistantScreen.kt @@ -21,6 +21,8 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyColumn 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.Icon import androidx.compose.material3.IconButton @@ -54,6 +56,9 @@ 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.conversation.ConversationScreen +import com.nextcloud.client.assistant.conversation.ConversationViewModel +import com.nextcloud.client.assistant.conversation.repository.MockConversationRemoteRepository import com.nextcloud.client.assistant.extensions.getInputTitle import com.nextcloud.client.assistant.model.AssistantScreenState import com.nextcloud.client.assistant.model.ScreenOverlayState @@ -79,6 +84,7 @@ private const val PULL_TO_REFRESH_DELAY = 1500L @Composable fun AssistantScreen( viewModel: AssistantViewModel, + conversationViewModel: ConversationViewModel, capability: OCCapability, activity: Activity, sessionIdArg: Long? = null @@ -94,6 +100,7 @@ fun AssistantScreen( val scope = rememberCoroutineScope() val pullRefreshState = rememberPullToRefreshState() val snackbarHostState = remember { SnackbarHostState() } + val pagerState = rememberPagerState(initialPage = 1, pageCount = { 2 }) LaunchedEffect(messageId) { messageId?.let { @@ -122,79 +129,109 @@ fun AssistantScreen( } } - Scaffold( - modifier = Modifier.pullToRefresh( - false, - pullRefreshState, - onRefresh = { - scope.launch { - delay(PULL_TO_REFRESH_DELAY) - viewModel.fetchTaskList() - } - } - ), - topBar = { - taskTypes?.let { - val data = listOf(TaskTypeData.conversationList) + it - TaskTypesRow(selectedTaskType, data = data, selectTaskType = { task -> - viewModel.selectTaskType(task) + HorizontalPager( + state = pagerState, + ) { page -> + when (page) { + 0 -> { + ConversationScreen(viewModel = conversationViewModel, close = { + scope.launch { + pagerState.scrollToPage(1) + } + }, openChat = { newSessionId -> + viewModel.initSessionId(newSessionId) + scope.launch { + pagerState.scrollToPage(1) + } }) } - }, - bottomBar = { - if (!taskTypes.isNullOrEmpty()) { - ChatInputBar( - sessionIdArg, - selectedTaskType, - viewModel - ) - } - }, - snackbarHost = { - SnackbarHost(snackbarHostState) - } - ) { paddingValues -> - when (screenState) { - is AssistantScreenState.EmptyContent -> { - val state = (screenState as AssistantScreenState.EmptyContent) - EmptyContent( - paddingValues, - iconId = state.iconId, - descriptionId = state.descriptionId, - titleId = state.titleId - ) - } + 1 -> { + Scaffold( + modifier = Modifier.pullToRefresh( + false, + pullRefreshState, + onRefresh = { + scope.launch { + delay(PULL_TO_REFRESH_DELAY) + + if (sessionId != null || sessionIdArg != null) { + val id = sessionId ?: sessionIdArg + viewModel.fetchChatMessages(id!!) + } else { + viewModel.fetchTaskList() + } + } + } + ), + topBar = { + taskTypes?.let { + val data = listOf(TaskTypeData.conversationList) + it + TaskTypesRow(selectedTaskType, data = data, selectTaskType = { task -> + viewModel.selectTaskType(task) + }, navigateToConversationList = { + scope.launch { + pagerState.scrollToPage(0) + } + }) + } + }, + bottomBar = { + if (!taskTypes.isNullOrEmpty()) { + ChatInputBar( + sessionIdArg ?: sessionId, + selectedTaskType, + viewModel + ) + } + }, + snackbarHost = { + SnackbarHost(snackbarHostState) + } + ) { paddingValues -> + when (screenState) { + is AssistantScreenState.EmptyContent -> { + val state = (screenState as AssistantScreenState.EmptyContent) + EmptyContent( + paddingValues, + iconId = state.iconId, + descriptionId = state.descriptionId, + titleId = state.titleId + ) + } - AssistantScreenState.TaskContent -> { - TaskContent( - paddingValues, - filteredTaskList ?: listOf(), - viewModel, - capability - ) - } + AssistantScreenState.TaskContent -> { + TaskContent( + paddingValues, + filteredTaskList ?: listOf(), + viewModel, + capability + ) + } - AssistantScreenState.ChatContent -> { - ChatContent( - viewModel = viewModel, - modifier = Modifier.padding(paddingValues) - ) - } + AssistantScreenState.ChatContent -> { + ChatContent( + viewModel = viewModel, + modifier = Modifier.padding(paddingValues) + ) + } - else -> EmptyContent( - paddingValues, - iconId = R.drawable.spinner_inner, - titleId = null, - descriptionId = R.string.assistant_screen_loading - ) - } + else -> EmptyContent( + paddingValues, + iconId = R.drawable.spinner_inner, + titleId = null, + descriptionId = R.string.assistant_screen_loading + ) + } - LinearProgressIndicator( - progress = { pullRefreshState.distanceFraction }, - modifier = Modifier.fillMaxWidth() - ) + LinearProgressIndicator( + progress = { pullRefreshState.distanceFraction }, + modifier = Modifier.fillMaxWidth() + ) - OverlayState(screenOverlayState, activity, viewModel) + OverlayState(screenOverlayState, activity, viewModel) + } + } + } } } @@ -379,7 +416,8 @@ private fun AssistantScreenPreview() { MaterialTheme( content = { AssistantScreen( - viewModel = getMockViewModel(false), + conversationViewModel = getMockConversationViewModel(), + viewModel = getMockAssistantViewModel(false), activity = ComposeActivity(), capability = OCCapability().apply { versionMayor = 30 @@ -396,7 +434,8 @@ private fun AssistantEmptyScreenPreview() { MaterialTheme( content = { AssistantScreen( - viewModel = getMockViewModel(true), + conversationViewModel = getMockConversationViewModel(), + viewModel = getMockAssistantViewModel(true), activity = ComposeActivity(), capability = OCCapability().apply { versionMayor = 30 @@ -406,7 +445,14 @@ private fun AssistantEmptyScreenPreview() { ) } -private fun getMockViewModel(giveEmptyTasks: Boolean): AssistantViewModel { +private fun getMockConversationViewModel(): ConversationViewModel { + val mockRemoteRepository = MockConversationRemoteRepository() + return ConversationViewModel( + remoteRepository = mockRemoteRepository, + ) +} + +private fun getMockAssistantViewModel(giveEmptyTasks: Boolean): AssistantViewModel { val mockLocalRepository = MockAssistantLocalRepository() val mockRemoteRepository = MockAssistantRemoteRepository(giveEmptyTasks) return AssistantViewModel( diff --git a/app/src/main/java/com/nextcloud/client/assistant/AssistantViewModel.kt b/app/src/main/java/com/nextcloud/client/assistant/AssistantViewModel.kt index 27092a3d1a9b..810086bf58d8 100644 --- a/app/src/main/java/com/nextcloud/client/assistant/AssistantViewModel.kt +++ b/app/src/main/java/com/nextcloud/client/assistant/AssistantViewModel.kt @@ -78,7 +78,7 @@ class AssistantViewModel( pollingJob = viewModelScope.launch(Dispatchers.IO) { try { while (isActive) { - Log_OC.d(TAG, "Polling list...") + Log_OC.d(TAG, "Polling list, sessionId: $sessionId") if (sessionId != null) { pollChatMessages(sessionId) } else { @@ -87,7 +87,7 @@ class AssistantViewModel( delay(POLLING_INTERVAL_MS) } } finally { - Log_OC.d(TAG, "Polling coroutine cancelled") + Log_OC.d(TAG, "Polling coroutine cancelled, sessionId: $sessionId") } } } @@ -153,7 +153,6 @@ class AssistantViewModel( _screenState.update { AssistantScreenState.ChatContent } - _chatMessages.update { result } @@ -169,14 +168,18 @@ class AssistantViewModel( viewModelScope.launch(Dispatchers.IO) { val result = remoteRepository.createConversation(title) if (result != null) { - _sessionId.update { - result.session.id - } + initSessionId(result.session.id) sendChatMessage(content = title, sessionId = result.session.id) } } } + fun initSessionId(value: Long) { + _sessionId.update { + value + } + } + @Suppress("MagicNumber") fun createTask(input: String, taskType: TaskTypeData) { viewModelScope.launch(Dispatchers.IO) { @@ -260,7 +263,7 @@ class AssistantViewModel( AssistantScreenState.ChatContent } else { if (_filteredTaskList.value?.isEmpty() == true) { - AssistantScreenState.emptyTaskList() + AssistantScreenState.emptyChatList() } else { AssistantScreenState.TaskContent } diff --git a/app/src/main/java/com/nextcloud/client/assistant/conversation/ConversationScreen.kt b/app/src/main/java/com/nextcloud/client/assistant/conversation/ConversationScreen.kt index 508a4bf1d161..74ee965620df 100644 --- a/app/src/main/java/com/nextcloud/client/assistant/conversation/ConversationScreen.kt +++ b/app/src/main/java/com/nextcloud/client/assistant/conversation/ConversationScreen.kt @@ -7,8 +7,17 @@ package com.nextcloud.client.assistant.conversation -import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.shape.RoundedCornerShape @@ -16,8 +25,22 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.Close import androidx.compose.material.icons.outlined.Delete -import androidx.compose.material3.* -import androidx.compose.runtime.* +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.FilledTonalButton +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext @@ -25,16 +48,18 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.nextcloud.client.assistant.conversation.model.ConversationScreenState -import com.nextcloud.ui.composeActivity.ComposeDestination -import com.nextcloud.ui.composeActivity.ComposeNavigation -import com.owncloud.android.lib.resources.assistant.chat.model.Conversation import com.owncloud.android.R +import com.owncloud.android.lib.resources.assistant.chat.model.Conversation private val BUTTON_HEIGHT = 32.dp @OptIn(ExperimentalMaterial3Api::class) @Composable -fun ConversationScreen(viewModel: ConversationViewModel) { +fun ConversationScreen( + viewModel: ConversationViewModel, + close: () -> Unit, + openChat: (Long) -> Unit +) { val context = LocalContext.current val screenState by viewModel.screenState.collectAsState() val errorMessageId by viewModel.errorMessageId.collectAsState() @@ -57,7 +82,7 @@ fun ConversationScreen(viewModel: ConversationViewModel) { Spacer(modifier = Modifier.weight(1f)) IconButton( onClick = { - ComposeNavigation.navigate(ComposeDestination.AssistantScreen(null)) + close() } ) { Icon( @@ -95,7 +120,7 @@ fun ConversationScreen(viewModel: ConversationViewModel) { .padding(innerPadding), contentAlignment = Alignment.Center ) { - Text("No conversations found.") + Text(stringResource(R.string.assistant_screen_empty_conversation_text)) } } @@ -103,7 +128,8 @@ fun ConversationScreen(viewModel: ConversationViewModel) { ConversationList( conversations = conversations, onDeleteClick = { viewModel.deleteConversation(it.id.toString()) }, - modifier = Modifier.padding(innerPadding) + modifier = Modifier.padding(innerPadding), + openChat = openChat ) } } @@ -114,7 +140,8 @@ fun ConversationScreen(viewModel: ConversationViewModel) { private fun ConversationList( conversations: List, onDeleteClick: (Conversation) -> Unit, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, + openChat: (Long) -> Unit ) { LazyColumn( modifier = modifier @@ -126,8 +153,7 @@ private fun ConversationList( ConversationListItem( conversation = conversation, onClick = { - val destination = ComposeDestination.AssistantScreen(conversation.id) - ComposeNavigation.navigate(destination) + openChat(conversation.id) }, onDeleteClick = { onDeleteClick(conversation) } ) diff --git a/app/src/main/java/com/nextcloud/client/assistant/conversation/ConversationViewModel.kt b/app/src/main/java/com/nextcloud/client/assistant/conversation/ConversationViewModel.kt index c988905e4dd1..cf4691db4e66 100644 --- a/app/src/main/java/com/nextcloud/client/assistant/conversation/ConversationViewModel.kt +++ b/app/src/main/java/com/nextcloud/client/assistant/conversation/ConversationViewModel.kt @@ -8,9 +8,9 @@ package com.nextcloud.client.assistant.conversation import androidx.lifecycle.ViewModel -import com.nextcloud.client.assistant.conversation.repository.ConversationRemoteRepository import androidx.lifecycle.viewModelScope import com.nextcloud.client.assistant.conversation.model.ConversationScreenState +import com.nextcloud.client.assistant.conversation.repository.ConversationRemoteRepository import com.owncloud.android.R import com.owncloud.android.lib.resources.assistant.chat.model.Conversation import kotlinx.coroutines.Dispatchers @@ -83,7 +83,6 @@ class ConversationViewModel( viewModelScope.launch(Dispatchers.IO) { val success = remoteRepository.deleteConversation(sessionId) if (success) { - // TODO: CHECK IF ITS WORKING... val updatedList = _conversations.value.filterNot { it.id == sessionId.toLong() } _conversations.update { updatedList diff --git a/app/src/main/java/com/nextcloud/client/assistant/conversation/repository/MockConversationRemoteRepository.kt b/app/src/main/java/com/nextcloud/client/assistant/conversation/repository/MockConversationRemoteRepository.kt new file mode 100644 index 000000000000..27785524b672 --- /dev/null +++ b/app/src/main/java/com/nextcloud/client/assistant/conversation/repository/MockConversationRemoteRepository.kt @@ -0,0 +1,28 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2025 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nextcloud.client.assistant.conversation.repository + +import com.owncloud.android.lib.resources.assistant.chat.model.Conversation +import com.owncloud.android.lib.resources.assistant.chat.model.CreateConversation + +class MockConversationRemoteRepository: ConversationRemoteRepository { + override suspend fun fetchConversationList(): List? { + return null + } + + override suspend fun createConversation( + title: String?, + timestamp: Long + ): CreateConversation? { + return null + } + + override suspend fun deleteConversation(sessionId: String): Boolean { + return true + } +} diff --git a/app/src/main/java/com/nextcloud/client/assistant/model/AssistantScreenState.kt b/app/src/main/java/com/nextcloud/client/assistant/model/AssistantScreenState.kt index 43b046a2bde8..7dafcf46d3c5 100644 --- a/app/src/main/java/com/nextcloud/client/assistant/model/AssistantScreenState.kt +++ b/app/src/main/java/com/nextcloud/client/assistant/model/AssistantScreenState.kt @@ -25,7 +25,7 @@ sealed class AssistantScreenState { iconId = null ) - fun emptyTaskList(): AssistantScreenState = EmptyContent( + fun emptyChatList(): AssistantScreenState = EmptyContent( iconId = R.drawable.ic_assistant, titleId = R.string.assistant_screen_chat_title, descriptionId = R.string.assistant_screen_chat_description, diff --git a/app/src/main/java/com/nextcloud/client/assistant/taskTypes/TaskTypesRow.kt b/app/src/main/java/com/nextcloud/client/assistant/taskTypes/TaskTypesRow.kt index bfe6ef7bb4cc..789b335ced39 100644 --- a/app/src/main/java/com/nextcloud/client/assistant/taskTypes/TaskTypesRow.kt +++ b/app/src/main/java/com/nextcloud/client/assistant/taskTypes/TaskTypesRow.kt @@ -20,8 +20,6 @@ import androidx.compose.ui.res.colorResource import androidx.compose.ui.res.painterResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import com.nextcloud.ui.composeActivity.ComposeDestination -import com.nextcloud.ui.composeActivity.ComposeNavigation import com.owncloud.android.R import com.owncloud.android.lib.resources.assistant.v2.model.TaskTypeData @@ -30,7 +28,8 @@ import com.owncloud.android.lib.resources.assistant.v2.model.TaskTypeData fun TaskTypesRow( selectedTaskType: TaskTypeData?, data: List, - selectTaskType: (TaskTypeData) -> Unit + selectTaskType: (TaskTypeData) -> Unit, + navigateToConversationList: () -> Unit ) { val selectedTabIndex = data.indexOfFirst { it.id == selectedTaskType?.id }.takeIf { it >= 0 } ?: 0 @@ -57,7 +56,7 @@ fun TaskTypesRow( } else { Tab( selected = selectedTaskType?.id == taskType.id, - onClick = { ComposeNavigation.navigate(ComposeDestination.ConversationScreen) }, + onClick = { navigateToConversationList() }, selectedContentColor = colorResource(R.color.text_color), unselectedContentColor = colorResource(R.color.disabled_text), icon = { @@ -86,5 +85,7 @@ private fun TaskTypesRowPreview() { TaskTypesRow(selectedTaskType, taskTypes, { + }, { + }) } diff --git a/app/src/main/java/com/nextcloud/ui/composeActivity/ComposeActivity.kt b/app/src/main/java/com/nextcloud/ui/composeActivity/ComposeActivity.kt index b103c9d60718..c6348385b881 100644 --- a/app/src/main/java/com/nextcloud/ui/composeActivity/ComposeActivity.kt +++ b/app/src/main/java/com/nextcloud/ui/composeActivity/ComposeActivity.kt @@ -19,7 +19,6 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import com.nextcloud.client.assistant.AssistantScreen import com.nextcloud.client.assistant.AssistantViewModel -import com.nextcloud.client.assistant.conversation.ConversationScreen import com.nextcloud.client.assistant.conversation.ConversationViewModel import com.nextcloud.client.assistant.conversation.repository.ConversationRemoteRepositoryImpl import com.nextcloud.client.assistant.repository.local.AssistantLocalRepositoryImpl @@ -89,26 +88,21 @@ class ComposeActivity : DrawerActivity() { is ComposeDestination.AssistantScreen -> { val dao = NextcloudDatabase.instance().assistantDao() val sessionId = (currentScreen as? ComposeDestination.AssistantScreen)?.sessionId + val client = nextcloudClient ?: return - nextcloudClient?.let { client -> - AssistantScreen( - viewModel = AssistantViewModel( - accountName = userAccountManager.user.accountName, - remoteRepository = AssistantRemoteRepositoryImpl(client, capabilities), - localRepository = AssistantLocalRepositoryImpl(dao) - ), - activity = this, - capability = capabilities, - sessionIdArg = sessionId - ) - } - } - ComposeDestination.ConversationScreen -> { - nextcloudClient?.let { client -> - ConversationScreen(viewModel = ConversationViewModel( + AssistantScreen( + viewModel = AssistantViewModel( + accountName = userAccountManager.user.accountName, + remoteRepository = AssistantRemoteRepositoryImpl(client, capabilities), + localRepository = AssistantLocalRepositoryImpl(dao) + ), + conversationViewModel = ConversationViewModel( remoteRepository = ConversationRemoteRepositoryImpl(client) - )) - } + ), + activity = this, + capability = capabilities, + sessionIdArg = sessionId + ) } else -> Unit } diff --git a/app/src/main/java/com/nextcloud/ui/composeActivity/ComposeDestination.kt b/app/src/main/java/com/nextcloud/ui/composeActivity/ComposeDestination.kt index 9932da8f25ea..7fcb02b024fd 100644 --- a/app/src/main/java/com/nextcloud/ui/composeActivity/ComposeDestination.kt +++ b/app/src/main/java/com/nextcloud/ui/composeActivity/ComposeDestination.kt @@ -9,13 +9,11 @@ package com.nextcloud.ui.composeActivity sealed class ComposeDestination(val id: Int) { data class AssistantScreen(val sessionId: Long?) : ComposeDestination(0) - data object ConversationScreen : ComposeDestination(1) companion object { fun fromId(id: Int): ComposeDestination { return when (id) { 0 -> AssistantScreen(null) - 1 -> ConversationScreen else -> throw IllegalArgumentException("Unknown destination: $id") } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index e182ff1827fe..5a0ed752dd03 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -68,7 +68,7 @@ New conversation Failed to send a message Failed to fetch chat messages - + No conversations found Hello there! What can I help you with today? Try sending a message to spark a conversation. Delete task From c0d637d36e3beb42d2a4ebae6787192027cde533 Mon Sep 17 00:00:00 2001 From: alperozturk Date: Tue, 11 Nov 2025 16:47:21 +0100 Subject: [PATCH 09/29] consolidate screenState Signed-off-by: alperozturk --- .../client/assistant/AssistantScreen.kt | 28 ++++----- .../client/assistant/AssistantViewModel.kt | 58 +++++++++---------- .../conversation/ConversationScreen.kt | 4 +- .../conversation/ConversationViewModel.kt | 6 +- .../model/ConversationScreenState.kt | 2 +- .../assistant/model/AssistantScreenState.kt | 12 +++- .../taskDetail/TaskDetailBottomSheet.kt | 2 +- app/src/main/res/values/strings.xml | 39 ++++++------- 8 files changed, 78 insertions(+), 73 deletions(-) diff --git a/app/src/main/java/com/nextcloud/client/assistant/AssistantScreen.kt b/app/src/main/java/com/nextcloud/client/assistant/AssistantScreen.kt index 3c38bca571fd..b4858387b02b 100644 --- a/app/src/main/java/com/nextcloud/client/assistant/AssistantScreen.kt +++ b/app/src/main/java/com/nextcloud/client/assistant/AssistantScreen.kt @@ -219,7 +219,7 @@ fun AssistantScreen( paddingValues, iconId = R.drawable.spinner_inner, titleId = null, - descriptionId = R.string.assistant_screen_loading + descriptionId = R.string.common_loading ) } @@ -254,7 +254,7 @@ private fun ChatInputBar( .padding(8.dp), horizontalAlignment = Alignment.CenterHorizontally) { Text( - text = stringResource(R.string.assistant_generation_warning), + text = stringResource(R.string.assistant_output_generation_warning_text), fontSize = 11.sp, textAlign = TextAlign.Center, color = colorResource(R.color.text_color) @@ -319,20 +319,20 @@ private fun OverlayState(state: ScreenOverlayState?, activity: Activity, viewMod SimpleAlertDialog( title = stringResource(id = R.string.assistant_screen_delete_task_alert_dialog_title), description = stringResource(id = R.string.assistant_screen_delete_task_alert_dialog_description), - dismiss = { viewModel.updateScreenState(null) }, + dismiss = { viewModel.updateScreenOverlayState(null) }, onComplete = { viewModel.deleteTask(state.id) } ) } is ScreenOverlayState.TaskActions -> { val actions = state.getActions(activity, onDeleteCompleted = { deleteTask -> - viewModel.updateScreenState(deleteTask) + viewModel.updateScreenOverlayState(deleteTask) }) MoreActionsBottomSheet( title = state.task.getInputTitle(), actions = actions, - dismiss = { viewModel.updateScreenState(null) } + dismiss = { viewModel.updateScreenOverlayState(null) } ) } @@ -361,7 +361,7 @@ private fun TaskContent( capability, showTaskActions = { val newState = ScreenOverlayState.TaskActions(task) - viewModel.updateScreenState(newState) + viewModel.updateScreenOverlayState(newState) } ) Spacer(modifier = Modifier.height(8.dp)) @@ -370,7 +370,7 @@ private fun TaskContent( } @Composable -private fun EmptyContent(paddingValues: PaddingValues, iconId: Int?, descriptionId: Int, titleId: Int?) { +private fun EmptyContent(paddingValues: PaddingValues, iconId: Int?, descriptionId: Int?, titleId: Int?) { Column( modifier = Modifier .fillMaxSize() @@ -400,12 +400,14 @@ private fun EmptyContent(paddingValues: PaddingValues, iconId: Int?, description Spacer(modifier = Modifier.height(8.dp)) } - Text( - text = stringResource(descriptionId), - fontSize = MaterialTheme.typography.bodyMedium.fontSize, - textAlign = TextAlign.Center, - color = colorResource(R.color.text_color) - ) + descriptionId?.let { + Text( + text = stringResource(descriptionId), + fontSize = MaterialTheme.typography.bodyMedium.fontSize, + textAlign = TextAlign.Center, + color = colorResource(R.color.text_color) + ) + } } } diff --git a/app/src/main/java/com/nextcloud/client/assistant/AssistantViewModel.kt b/app/src/main/java/com/nextcloud/client/assistant/AssistantViewModel.kt index 810086bf58d8..c49ce3924bc4 100644 --- a/app/src/main/java/com/nextcloud/client/assistant/AssistantViewModel.kt +++ b/app/src/main/java/com/nextcloud/client/assistant/AssistantViewModel.kt @@ -24,6 +24,7 @@ import kotlinx.coroutines.Job import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.update import kotlinx.coroutines.isActive import kotlinx.coroutines.launch @@ -68,6 +69,7 @@ class AssistantViewModel( private var pollingJob: Job? = null init { + observeScreenState() fetchTaskTypes() } @@ -122,6 +124,28 @@ class AssistantViewModel( } } + private fun observeScreenState() { + viewModelScope.launch { + combine( + _selectedTaskType, + _chatMessages, + _filteredTaskList + ) { selectedTask, chats, tasks -> + val isChat = selectedTask?.isChat == true + + when { + selectedTask == null -> AssistantScreenState.Loading + isChat && chats.isEmpty() -> AssistantScreenState.emptyChatList() + isChat -> AssistantScreenState.ChatContent + !isChat && (tasks == null || tasks.isEmpty()) -> AssistantScreenState.emptyTaskList() + else -> AssistantScreenState.TaskContent + } + }.collect { newState -> + _screenState.value = newState + } + } + } + fun sendChatMessage(content: String, sessionId: Long) { val timestamp = System.currentTimeMillis().div(1000) val firstHumanMessage = _chatMessages.value.isEmpty() @@ -139,9 +163,7 @@ class AssistantViewModel( if (result != null) { fetchChatMessages(sessionId) } else { - _snackbarMessageId.update { - R.string.assistant_screen_chat_create_error - } + updateSnackbarMessage(R.string.assistant_screen_chat_create_error) } } } @@ -150,16 +172,11 @@ class AssistantViewModel( viewModelScope.launch(Dispatchers.IO) { val result = remoteRepository.fetchChatMessages(sessionId) if (result != null) { - _screenState.update { - AssistantScreenState.ChatContent - } _chatMessages.update { result } } else { - _snackbarMessageId.update { - R.string.assistant_screen_chat_fetch_error - } + updateSnackbarMessage(R.string.assistant_screen_chat_fetch_error) } } } @@ -209,7 +226,7 @@ class AssistantViewModel( private fun fetchTaskTypes() { viewModelScope.launch(Dispatchers.IO) { val taskTypesResult = remoteRepository.getTaskTypes() - if (taskTypesResult == null || taskTypesResult.isEmpty()) { + if (taskTypesResult.isNullOrEmpty()) { _screenState.update { AssistantScreenState.emptyTaskTypes() } @@ -232,7 +249,6 @@ class AssistantViewModel( _filteredTaskList.update { cachedTasks.sortedByDescending { it.id } } - updateScreenState() } val taskType = _selectedTaskType.value?.id ?: return@launch @@ -250,24 +266,6 @@ class AssistantViewModel( } else { updateSnackbarMessage(R.string.assistant_screen_task_list_error_state_message) } - - updateScreenState() - } - } - - private fun updateScreenState() { - val isChat = _selectedTaskType.value?.isChat ?: false - - _screenState.update { - if (isChat) { - AssistantScreenState.ChatContent - } else { - if (_filteredTaskList.value?.isEmpty() == true) { - AssistantScreenState.emptyChatList() - } else { - AssistantScreenState.TaskContent - } - } } } @@ -296,7 +294,7 @@ class AssistantViewModel( } } - fun updateScreenState(value: ScreenOverlayState?) { + fun updateScreenOverlayState(value: ScreenOverlayState?) { _screenOverlayState.update { value } diff --git a/app/src/main/java/com/nextcloud/client/assistant/conversation/ConversationScreen.kt b/app/src/main/java/com/nextcloud/client/assistant/conversation/ConversationScreen.kt index 74ee965620df..aaf622fcfb92 100644 --- a/app/src/main/java/com/nextcloud/client/assistant/conversation/ConversationScreen.kt +++ b/app/src/main/java/com/nextcloud/client/assistant/conversation/ConversationScreen.kt @@ -120,7 +120,7 @@ fun ConversationScreen( .padding(innerPadding), contentAlignment = Alignment.Center ) { - Text(stringResource(R.string.assistant_screen_empty_conversation_text)) + Text(stringResource(R.string.conversation_screen_empty_content_title)) } } @@ -217,7 +217,7 @@ private fun CreateConversationButton( .height(BUTTON_HEIGHT) ) { Text( - text = stringResource(R.string.assistant_screen_conversation_create_text), + text = stringResource(R.string.conversation_screen_create_button_title), style = MaterialTheme.typography.bodyLarge ) Spacer(modifier = Modifier.width(8.dp)) diff --git a/app/src/main/java/com/nextcloud/client/assistant/conversation/ConversationViewModel.kt b/app/src/main/java/com/nextcloud/client/assistant/conversation/ConversationViewModel.kt index cf4691db4e66..394a7cae8eff 100644 --- a/app/src/main/java/com/nextcloud/client/assistant/conversation/ConversationViewModel.kt +++ b/app/src/main/java/com/nextcloud/client/assistant/conversation/ConversationViewModel.kt @@ -57,7 +57,7 @@ class ConversationViewModel( null } _errorMessageId.update { - R.string.assistant_screen_conversation_list_fetch_error + R.string.conversation_screen_fetch_error_title } } } @@ -73,7 +73,7 @@ class ConversationViewModel( } } else { _errorMessageId.update { - R.string.assistant_screen_conversation_create_error + R.string.conversation_screen_create_error_title } } } @@ -89,7 +89,7 @@ class ConversationViewModel( } } else { _errorMessageId.update { - R.string.assistant_screen_conversation_delete_error + R.string.conversation_screen_delete_error_title } } } diff --git a/app/src/main/java/com/nextcloud/client/assistant/conversation/model/ConversationScreenState.kt b/app/src/main/java/com/nextcloud/client/assistant/conversation/model/ConversationScreenState.kt index 093ddb177db8..7d7fb8e31dbf 100644 --- a/app/src/main/java/com/nextcloud/client/assistant/conversation/model/ConversationScreenState.kt +++ b/app/src/main/java/com/nextcloud/client/assistant/conversation/model/ConversationScreenState.kt @@ -16,7 +16,7 @@ sealed class ConversationScreenState { companion object { fun emptyConversationList(): ConversationScreenState = EmptyContent( - descriptionId = R.string.assistant_screen_no_conversation, + descriptionId = R.string.conversation_screen_empty_conversation_list_title, ) } } diff --git a/app/src/main/java/com/nextcloud/client/assistant/model/AssistantScreenState.kt b/app/src/main/java/com/nextcloud/client/assistant/model/AssistantScreenState.kt index 7dafcf46d3c5..7239e6c4b802 100644 --- a/app/src/main/java/com/nextcloud/client/assistant/model/AssistantScreenState.kt +++ b/app/src/main/java/com/nextcloud/client/assistant/model/AssistantScreenState.kt @@ -16,7 +16,7 @@ sealed class AssistantScreenState { data object ChatContent : AssistantScreenState() - data class EmptyContent(val iconId: Int?, val titleId: Int?, val descriptionId: Int) : AssistantScreenState() + data class EmptyContent(val iconId: Int?, val titleId: Int?, val descriptionId: Int?) : AssistantScreenState() companion object { fun emptyTaskTypes(): AssistantScreenState = EmptyContent( @@ -27,8 +27,14 @@ sealed class AssistantScreenState { fun emptyChatList(): AssistantScreenState = EmptyContent( iconId = R.drawable.ic_assistant, - titleId = R.string.assistant_screen_chat_title, - descriptionId = R.string.assistant_screen_chat_description, + titleId = R.string.assistant_screen_empty_content_title, + descriptionId = R.string.assistant_screen_empty_content_description, + ) + + fun emptyTaskList(): AssistantScreenState = EmptyContent( + iconId = R.drawable.ic_assistant, + titleId = R.string.assistant_screen_empty_content_title, + descriptionId = null, ) } } diff --git a/app/src/main/java/com/nextcloud/client/assistant/taskDetail/TaskDetailBottomSheet.kt b/app/src/main/java/com/nextcloud/client/assistant/taskDetail/TaskDetailBottomSheet.kt index a52b1141e3f5..0455adabe73b 100644 --- a/app/src/main/java/com/nextcloud/client/assistant/taskDetail/TaskDetailBottomSheet.kt +++ b/app/src/main/java/com/nextcloud/client/assistant/taskDetail/TaskDetailBottomSheet.kt @@ -104,7 +104,7 @@ fun TaskDetailBottomSheet(task: Task, showTaskActions: () -> Unit, dismiss: () - Spacer(modifier = Modifier.width(4.dp)) Text( - text = stringResource(R.string.assistant_generation_warning), + text = stringResource(R.string.assistant_output_generation_warning_text), color = colorResource(R.color.text_color), fontSize = 12.sp ) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 5a0ed752dd03..7708fe390e04 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -54,40 +54,39 @@ %1$d / %2$d - %3$s AnonymousAccountType - Unable to fetch task types, please check your internet connection. - Unable to fetch task list, please check your internet connection. - Task list is empty. Check assistant app configuration. - + Assistant - Loading task list… The task output is not ready yet. - No conversations yet - Failed to fetch conversation list - Failed to create conversation - Failed to delete conversation - New conversation Failed to send a message Failed to fetch chat messages - No conversations found - Hello there! What can I help you with today? - Try sending a message to spark a conversation. + Unable to fetch task types, please check your internet connection. + Unable to fetch task list, please check your internet connection. + Task list is empty. Check assistant app configuration. Delete task Are you sure you want to delete this task? - Add new task - Delete Task - Task successfully created An error occurred while creating the task - Task successfully deleted An error occurred while deleting the task - Type some text - Input Output - Output shown here is generated by AI. Make sure to always double-check. + + + No conversations yet + Failed to fetch conversation list + No conversations found + Failed to create conversation + Failed to delete conversation + New conversation + + + Output shown here is generated by AI. Make sure to always double-check. + + Hello there! What can I help you with today? + Try sending a message to spark a conversation. + Recommended files From 9a0bf092083a325c7040b0139207da61a5fe968a Mon Sep 17 00:00:00 2001 From: alperozturk Date: Wed, 12 Nov 2025 09:22:46 +0100 Subject: [PATCH 10/29] fix icon alignment Signed-off-by: alperozturk --- .../client/assistant/AssistantScreen.kt | 3 +- .../taskDetail/TaskDetailBottomSheet.kt | 4 +- .../assistant/taskTypes/TaskTypesRow.kt | 58 ++++++++++--------- app/src/main/res/drawable/ic_menu_open.xml | 15 +++++ 4 files changed, 51 insertions(+), 29 deletions(-) create mode 100644 app/src/main/res/drawable/ic_menu_open.xml diff --git a/app/src/main/java/com/nextcloud/client/assistant/AssistantScreen.kt b/app/src/main/java/com/nextcloud/client/assistant/AssistantScreen.kt index b4858387b02b..321067ff378b 100644 --- a/app/src/main/java/com/nextcloud/client/assistant/AssistantScreen.kt +++ b/app/src/main/java/com/nextcloud/client/assistant/AssistantScreen.kt @@ -165,8 +165,7 @@ fun AssistantScreen( ), topBar = { taskTypes?.let { - val data = listOf(TaskTypeData.conversationList) + it - TaskTypesRow(selectedTaskType, data = data, selectTaskType = { task -> + TaskTypesRow(selectedTaskType, data = it, selectTaskType = { task -> viewModel.selectTaskType(task) }, navigateToConversationList = { scope.launch { diff --git a/app/src/main/java/com/nextcloud/client/assistant/taskDetail/TaskDetailBottomSheet.kt b/app/src/main/java/com/nextcloud/client/assistant/taskDetail/TaskDetailBottomSheet.kt index 0455adabe73b..afa382d3e375 100644 --- a/app/src/main/java/com/nextcloud/client/assistant/taskDetail/TaskDetailBottomSheet.kt +++ b/app/src/main/java/com/nextcloud/client/assistant/taskDetail/TaskDetailBottomSheet.kt @@ -39,6 +39,7 @@ import androidx.compose.ui.res.colorResource import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp @@ -106,7 +107,8 @@ fun TaskDetailBottomSheet(task: Task, showTaskActions: () -> Unit, dismiss: () - Text( text = stringResource(R.string.assistant_output_generation_warning_text), color = colorResource(R.color.text_color), - fontSize = 12.sp + fontSize = 11.sp, + textAlign = TextAlign.Center ) } } diff --git a/app/src/main/java/com/nextcloud/client/assistant/taskTypes/TaskTypesRow.kt b/app/src/main/java/com/nextcloud/client/assistant/taskTypes/TaskTypesRow.kt index 789b335ced39..90e1c9d84e05 100644 --- a/app/src/main/java/com/nextcloud/client/assistant/taskTypes/TaskTypesRow.kt +++ b/app/src/main/java/com/nextcloud/client/assistant/taskTypes/TaskTypesRow.kt @@ -8,8 +8,13 @@ package com.nextcloud.client.assistant.taskTypes import android.annotation.SuppressLint +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.width import androidx.compose.material3.Icon -import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.IconButton import androidx.compose.material3.PrimaryScrollableTabRow import androidx.compose.material3.Tab import androidx.compose.material3.TabRowDefaults @@ -33,19 +38,34 @@ fun TaskTypesRow( ) { val selectedTabIndex = data.indexOfFirst { it.id == selectedTaskType?.id }.takeIf { it >= 0 } ?: 0 - PrimaryScrollableTabRow( - selectedTabIndex = selectedTabIndex, - edgePadding = 0.dp, - containerColor = colorResource(R.color.actionbar_color), - indicator = { - TabRowDefaults.SecondaryIndicator( - Modifier.tabIndicatorOffset(selectedTabIndex), - color = colorResource(R.color.primary) + Row( + modifier = Modifier.background(color = colorResource(R.color.actionbar_color)), + horizontalArrangement = Arrangement.Center + ) { + Spacer(modifier = Modifier.width(11.dp)) + + IconButton( + onClick = { navigateToConversationList() } + ) { + Icon( + painter = painterResource(id = R.drawable.ic_menu_open), + contentDescription = "open conversation list button", + tint = colorResource(R.color.text_color) ) } - ) { - data.forEach { taskType -> - if (taskType.name.isNotEmpty()) { + + PrimaryScrollableTabRow( + selectedTabIndex = selectedTabIndex, + edgePadding = 0.dp, + containerColor = colorResource(R.color.actionbar_color), + indicator = { + TabRowDefaults.SecondaryIndicator( + Modifier.tabIndicatorOffset(selectedTabIndex), + color = colorResource(R.color.primary) + ) + } + ) { + data.forEach { taskType -> Tab( selected = selectedTaskType?.id == taskType.id, onClick = { selectTaskType(taskType) }, @@ -53,20 +73,6 @@ fun TaskTypesRow( unselectedContentColor = colorResource(R.color.disabled_text), text = { Text(text = taskType.name) } ) - } else { - Tab( - selected = selectedTaskType?.id == taskType.id, - onClick = { navigateToConversationList() }, - selectedContentColor = colorResource(R.color.text_color), - unselectedContentColor = colorResource(R.color.disabled_text), - icon = { - Icon( - painter = painterResource(id = R.drawable.ic_menu), - contentDescription = "open conversation list button", - tint = MaterialTheme.colorScheme.primary - ) - }, - ) } } } diff --git a/app/src/main/res/drawable/ic_menu_open.xml b/app/src/main/res/drawable/ic_menu_open.xml new file mode 100644 index 000000000000..9db746f5b785 --- /dev/null +++ b/app/src/main/res/drawable/ic_menu_open.xml @@ -0,0 +1,15 @@ + + + + From 47e2590bf586288d304134d053487deceb99f227 Mon Sep 17 00:00:00 2001 From: alperozturk Date: Wed, 12 Nov 2025 09:33:54 +0100 Subject: [PATCH 11/29] fix polling, chat choose Signed-off-by: alperozturk --- .../client/assistant/AssistantScreen.kt | 3 +++ .../client/assistant/AssistantViewModel.kt | 16 +++++++++++----- .../taskDetail/TaskDetailBottomSheet.kt | 4 +++- 3 files changed, 17 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/com/nextcloud/client/assistant/AssistantScreen.kt b/app/src/main/java/com/nextcloud/client/assistant/AssistantScreen.kt index 321067ff378b..70cc398dcd7f 100644 --- a/app/src/main/java/com/nextcloud/client/assistant/AssistantScreen.kt +++ b/app/src/main/java/com/nextcloud/client/assistant/AssistantScreen.kt @@ -139,6 +139,9 @@ fun AssistantScreen( pagerState.scrollToPage(1) } }, openChat = { newSessionId -> + taskTypes?.find { it.isChat }?.let { chatTaskType -> + viewModel.updateTaskType(chatTaskType) + } viewModel.initSessionId(newSessionId) scope.launch { pagerState.scrollToPage(1) diff --git a/app/src/main/java/com/nextcloud/client/assistant/AssistantViewModel.kt b/app/src/main/java/com/nextcloud/client/assistant/AssistantViewModel.kt index c49ce3924bc4..244696afcbb0 100644 --- a/app/src/main/java/com/nextcloud/client/assistant/AssistantViewModel.kt +++ b/app/src/main/java/com/nextcloud/client/assistant/AssistantViewModel.kt @@ -83,9 +83,12 @@ class AssistantViewModel( Log_OC.d(TAG, "Polling list, sessionId: $sessionId") if (sessionId != null) { pollChatMessages(sessionId) - } else { + } + + if (_selectedTaskType.value?.isChat == false) { pollTaskList() } + delay(POLLING_INTERVAL_MS) } } finally { @@ -216,10 +219,7 @@ class AssistantViewModel( } fun selectTaskType(task: TaskTypeData) { - _selectedTaskType.update { - task - } - + updateTaskType(task) fetchTaskList() } @@ -288,6 +288,12 @@ class AssistantViewModel( } } + fun updateTaskType(value: TaskTypeData) { + _selectedTaskType.update { + value + } + } + fun updateSnackbarMessage(value: Int?) { _snackbarMessageId.update { value diff --git a/app/src/main/java/com/nextcloud/client/assistant/taskDetail/TaskDetailBottomSheet.kt b/app/src/main/java/com/nextcloud/client/assistant/taskDetail/TaskDetailBottomSheet.kt index afa382d3e375..73614996f005 100644 --- a/app/src/main/java/com/nextcloud/client/assistant/taskDetail/TaskDetailBottomSheet.kt +++ b/app/src/main/java/com/nextcloud/client/assistant/taskDetail/TaskDetailBottomSheet.kt @@ -21,6 +21,7 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons @@ -105,10 +106,11 @@ fun TaskDetailBottomSheet(task: Task, showTaskActions: () -> Unit, dismiss: () - Spacer(modifier = Modifier.width(4.dp)) Text( + modifier = Modifier.widthIn(max = 200.dp), text = stringResource(R.string.assistant_output_generation_warning_text), color = colorResource(R.color.text_color), fontSize = 11.sp, - textAlign = TextAlign.Center + textAlign = TextAlign.Center, ) } } From 5d7c6f51fc13d2517db193c26cc54bc7cfae304d Mon Sep 17 00:00:00 2001 From: alperozturk Date: Wed, 12 Nov 2025 09:59:16 +0100 Subject: [PATCH 12/29] fix codacy Signed-off-by: alperozturk --- .../client/assistant/AssistantScreen.kt | 51 ++++++++----------- .../client/assistant/AssistantViewModel.kt | 36 +++++++++---- .../client/assistant/chat/ChatContent.kt | 25 ++++----- .../conversation/ConversationScreen.kt | 25 +++------ .../conversation/ConversationViewModel.kt | 4 +- .../model/ConversationScreenState.kt | 2 +- .../ConversationRemoteRepositoryImpl.kt | 9 ++-- .../MockConversationRemoteRepository.kt | 17 ++----- .../assistant/model/AssistantScreenState.kt | 4 +- .../remote/AssistantRemoteRepositoryImpl.kt | 2 +- .../remote/MockAssistantRemoteRepository.kt | 12 ++--- .../taskDetail/TaskDetailBottomSheet.kt | 2 +- .../assistant/taskTypes/TaskTypesRow.kt | 6 ++- .../ui/composeActivity/ComposeActivity.kt | 8 +-- .../ui/composeActivity/ComposeDestination.kt | 8 ++- 15 files changed, 94 insertions(+), 117 deletions(-) diff --git a/app/src/main/java/com/nextcloud/client/assistant/AssistantScreen.kt b/app/src/main/java/com/nextcloud/client/assistant/AssistantScreen.kt index 70cc398dcd7f..1476cd10d06f 100644 --- a/app/src/main/java/com/nextcloud/client/assistant/AssistantScreen.kt +++ b/app/src/main/java/com/nextcloud/client/assistant/AssistantScreen.kt @@ -86,8 +86,7 @@ fun AssistantScreen( viewModel: AssistantViewModel, conversationViewModel: ConversationViewModel, capability: OCCapability, - activity: Activity, - sessionIdArg: Long? = null + activity: Activity ) { val sessionId by viewModel.sessionId.collectAsState() val messageId by viewModel.snackbarMessageId.collectAsState() @@ -109,17 +108,11 @@ fun AssistantScreen( } } - // replace polling task for newly created conversation LaunchedEffect(sessionId) { - val newSessionId = sessionId ?: return@LaunchedEffect - viewModel.startPolling(newSessionId) - viewModel.fetchChatMessages(newSessionId) - } + viewModel.startPolling(sessionId) - LaunchedEffect(Unit) { - viewModel.startPolling(sessionIdArg) - if (sessionIdArg != null) { - viewModel.fetchChatMessages(sessionIdArg) + sessionId?.let { + viewModel.fetchChatMessages(it) } } @@ -130,7 +123,7 @@ fun AssistantScreen( } HorizontalPager( - state = pagerState, + state = pagerState ) { page -> when (page) { 0 -> { @@ -139,10 +132,10 @@ fun AssistantScreen( pagerState.scrollToPage(1) } }, openChat = { newSessionId -> + viewModel.initSessionId(newSessionId) taskTypes?.find { it.isChat }?.let { chatTaskType -> - viewModel.updateTaskType(chatTaskType) + viewModel.selectTaskType(chatTaskType) } - viewModel.initSessionId(newSessionId) scope.launch { pagerState.scrollToPage(1) } @@ -157,9 +150,9 @@ fun AssistantScreen( scope.launch { delay(PULL_TO_REFRESH_DELAY) - if (sessionId != null || sessionIdArg != null) { - val id = sessionId ?: sessionIdArg - viewModel.fetchChatMessages(id!!) + val newSessionId = sessionId + if (newSessionId != null) { + viewModel.fetchChatMessages(newSessionId) } else { viewModel.fetchTaskList() } @@ -180,7 +173,7 @@ fun AssistantScreen( bottomBar = { if (!taskTypes.isNullOrEmpty()) { ChatInputBar( - sessionIdArg ?: sessionId, + sessionId, selectedTaskType, viewModel ) @@ -237,12 +230,9 @@ fun AssistantScreen( } } +@Suppress("LongMethod") @Composable -private fun ChatInputBar( - sessionId: Long?, - selectedTaskType: TaskTypeData?, - viewModel: AssistantViewModel -) { +private fun ChatInputBar(sessionId: Long?, selectedTaskType: TaskTypeData?, viewModel: AssistantViewModel) { val scope = rememberCoroutineScope() var text by remember { mutableStateOf("") } @@ -251,10 +241,12 @@ private fun ChatInputBar( shadowElevation = 4.dp, color = MaterialTheme.colorScheme.surface ) { - Column(modifier = Modifier - .fillMaxWidth() - .padding(8.dp), - horizontalAlignment = Alignment.CenterHorizontally) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(8.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { Text( text = stringResource(R.string.assistant_output_generation_warning_text), fontSize = 11.sp, @@ -452,7 +444,7 @@ private fun AssistantEmptyScreenPreview() { private fun getMockConversationViewModel(): ConversationViewModel { val mockRemoteRepository = MockConversationRemoteRepository() return ConversationViewModel( - remoteRepository = mockRemoteRepository, + remoteRepository = mockRemoteRepository ) } @@ -462,6 +454,7 @@ private fun getMockAssistantViewModel(giveEmptyTasks: Boolean): AssistantViewMod return AssistantViewModel( accountName = "test:localhost", remoteRepository = mockRemoteRepository, - localRepository = mockLocalRepository + localRepository = mockLocalRepository, + sessionIdArg = null ) } diff --git a/app/src/main/java/com/nextcloud/client/assistant/AssistantViewModel.kt b/app/src/main/java/com/nextcloud/client/assistant/AssistantViewModel.kt index 244696afcbb0..4a439249415e 100644 --- a/app/src/main/java/com/nextcloud/client/assistant/AssistantViewModel.kt +++ b/app/src/main/java/com/nextcloud/client/assistant/AssistantViewModel.kt @@ -29,15 +29,18 @@ import kotlinx.coroutines.flow.update import kotlinx.coroutines.isActive import kotlinx.coroutines.launch +@Suppress("TooManyFunctions") class AssistantViewModel( private val accountName: String, private val remoteRepository: AssistantRemoteRepository, - private val localRepository: AssistantLocalRepository + private val localRepository: AssistantLocalRepository, + sessionIdArg: Long? ) : ViewModel() { companion object { private const val TAG = "AssistantViewModel" private const val POLLING_INTERVAL_MS = 15_000L + private const val ONE_SECOND_MS = 1000L } private val _screenState = MutableStateFlow(null) @@ -46,7 +49,7 @@ class AssistantViewModel( private val _screenOverlayState = MutableStateFlow(null) val screenOverlayState: StateFlow = _screenOverlayState - private val _sessionId = MutableStateFlow(null) + private val _sessionId = MutableStateFlow(sessionIdArg) val sessionId: StateFlow = _sessionId private val _snackbarMessageId = MutableStateFlow(null) @@ -80,19 +83,20 @@ class AssistantViewModel( pollingJob = viewModelScope.launch(Dispatchers.IO) { try { while (isActive) { - Log_OC.d(TAG, "Polling list, sessionId: $sessionId") - if (sessionId != null) { - pollChatMessages(sessionId) - } + val isChat = (_selectedTaskType.value?.isChat == true) - if (_selectedTaskType.value?.isChat == false) { + if (isChat && sessionId != null) { + Log_OC.d(TAG, "Polling chat messages, sessionId: $sessionId") + pollChatMessages(sessionId) + } else if (!isChat) { + Log_OC.d(TAG, "Polling task list") pollTaskList() } delay(POLLING_INTERVAL_MS) } } finally { - Log_OC.d(TAG, "Polling coroutine cancelled, sessionId: $sessionId") + Log_OC.d(TAG, "Polling cancelled, sessionId: $sessionId") } } } @@ -150,7 +154,7 @@ class AssistantViewModel( } fun sendChatMessage(content: String, sessionId: Long) { - val timestamp = System.currentTimeMillis().div(1000) + val timestamp = System.currentTimeMillis().div(ONE_SECOND_MS) val firstHumanMessage = _chatMessages.value.isEmpty() val request = ChatMessageRequest( @@ -195,6 +199,8 @@ class AssistantViewModel( } fun initSessionId(value: Long) { + Log_OC.d(TAG, "session id updated: $value") + _sessionId.update { value } @@ -219,8 +225,16 @@ class AssistantViewModel( } fun selectTaskType(task: TaskTypeData) { + Log_OC.d(TAG, "task type changed: ${task.name}, session id: ${_sessionId.value}") + updateTaskType(task) - fetchTaskList() + + if (task.isChat) { + val sessionId = _sessionId.value ?: return + fetchChatMessages(sessionId) + } else { + fetchTaskList() + } } private fun fetchTaskTypes() { @@ -288,7 +302,7 @@ class AssistantViewModel( } } - fun updateTaskType(value: TaskTypeData) { + private fun updateTaskType(value: TaskTypeData) { _selectedTaskType.update { value } diff --git a/app/src/main/java/com/nextcloud/client/assistant/chat/ChatContent.kt b/app/src/main/java/com/nextcloud/client/assistant/chat/ChatContent.kt index 18f7e47818a3..c334818c46df 100644 --- a/app/src/main/java/com/nextcloud/client/assistant/chat/ChatContent.kt +++ b/app/src/main/java/com/nextcloud/client/assistant/chat/ChatContent.kt @@ -5,6 +5,8 @@ * SPDX-License-Identifier: AGPL-3.0-or-later */ +@file:Suppress("TopLevelPropertyNaming") + package com.nextcloud.client.assistant.chat import androidx.compose.foundation.Image @@ -49,10 +51,7 @@ private val CHAT_BUBBLE_CORNER_RADIUS = 8.dp private val ASSISTANT_ICON_SIZE = 40.dp @Composable -fun ChatContent( - viewModel: AssistantViewModel, - modifier: Modifier = Modifier -) { +fun ChatContent(viewModel: AssistantViewModel, modifier: Modifier = Modifier) { val chatMessages by viewModel.chatMessages.collectAsState() val listState = rememberLazyListState() @@ -66,7 +65,7 @@ fun ChatContent( .padding(horizontal = 12.dp, vertical = 8.dp), verticalArrangement = Arrangement.Bottom, reverseLayout = false, - state = listState, + state = listState ) { items(chatMessages, key = { it.id }) { message -> if (message.isHuman()) { @@ -84,10 +83,11 @@ private fun AssistantMessageItem(message: ChatMessage) { Box( modifier = Modifier .padding(vertical = 12.dp) - .fillMaxWidth(), contentAlignment = Alignment.CenterStart + .fillMaxWidth(), + contentAlignment = Alignment.CenterStart ) { Row( - verticalAlignment = Alignment.Bottom, + verticalAlignment = Alignment.Bottom ) { Box( modifier = Modifier @@ -130,10 +130,11 @@ private fun UserMessageItem(message: ChatMessage) { Box( modifier = Modifier .padding(vertical = 12.dp) - .fillMaxWidth(), contentAlignment = Alignment.CenterEnd + .fillMaxWidth(), + contentAlignment = Alignment.CenterEnd ) { Row( - verticalAlignment = Alignment.Bottom, + verticalAlignment = Alignment.Bottom ) { Box( modifier = Modifier @@ -158,13 +159,13 @@ private fun UserMessageItem(message: ChatMessage) { private fun MessageTextItem(message: ChatMessage) { Column( modifier = Modifier.padding(8.dp), - horizontalAlignment = Alignment.Start, + horizontalAlignment = Alignment.Start ) { Text( text = message.content, style = TextStyle( color = colorResource(R.color.text_color), - fontSize = 16.sp, + fontSize = 16.sp ) ) Spacer(modifier = Modifier.height(8.dp)) @@ -172,7 +173,7 @@ private fun MessageTextItem(message: ChatMessage) { text = message.timestampRepresentation(), style = TextStyle( color = colorResource(R.color.text_color), - fontSize = 12.sp, + fontSize = 12.sp ) ) } diff --git a/app/src/main/java/com/nextcloud/client/assistant/conversation/ConversationScreen.kt b/app/src/main/java/com/nextcloud/client/assistant/conversation/ConversationScreen.kt index aaf622fcfb92..69c7e95c73a9 100644 --- a/app/src/main/java/com/nextcloud/client/assistant/conversation/ConversationScreen.kt +++ b/app/src/main/java/com/nextcloud/client/assistant/conversation/ConversationScreen.kt @@ -5,6 +5,8 @@ * SPDX-License-Identifier: AGPL-3.0-or-later */ +@file:Suppress("TopLevelPropertyNaming", "MagicNumber") + package com.nextcloud.client.assistant.conversation import androidx.compose.foundation.layout.Arrangement @@ -54,12 +56,9 @@ import com.owncloud.android.lib.resources.assistant.chat.model.Conversation private val BUTTON_HEIGHT = 32.dp @OptIn(ExperimentalMaterial3Api::class) +@Suppress("LongMethod") @Composable -fun ConversationScreen( - viewModel: ConversationViewModel, - close: () -> Unit, - openChat: (Long) -> Unit -) { +fun ConversationScreen(viewModel: ConversationViewModel, close: () -> Unit, openChat: (Long) -> Unit) { val context = LocalContext.current val screenState by viewModel.screenState.collectAsState() val errorMessageId by viewModel.errorMessageId.collectAsState() @@ -147,7 +146,7 @@ private fun ConversationList( modifier = modifier .fillMaxSize() .padding(16.dp), - verticalArrangement = Arrangement.Bottom, + verticalArrangement = Arrangement.Bottom ) { items(conversations) { conversation -> ConversationListItem( @@ -163,11 +162,7 @@ private fun ConversationList( } @Composable -private fun ConversationListItem( - conversation: Conversation, - onClick: () -> Unit, - onDeleteClick: () -> Unit -) { +private fun ConversationListItem(conversation: Conversation, onClick: () -> Unit, onDeleteClick: () -> Unit) { Box(modifier = Modifier.fillMaxWidth(), contentAlignment = Alignment.BottomCenter) { FilledTonalButton( onClick = onClick, @@ -200,9 +195,7 @@ private fun ConversationListItem( } @Composable -private fun CreateConversationButton( - onClick: () -> Unit -) { +private fun CreateConversationButton(onClick: () -> Unit) { Box(modifier = Modifier.fillMaxWidth(), contentAlignment = Alignment.BottomCenter) { OutlinedButton( onClick = onClick, @@ -235,17 +228,13 @@ private fun CreateConversationButton( private fun ConversationListPreview() { Column { ConversationListItem(Conversation(1L, "User1", "Who is Al Pacino?", 1762847286L, "", null), { - }, { - }) Spacer(modifier = Modifier.height(8.dp)) ConversationListItem(Conversation(2L, "User1", "What is JetpackCompose?", 1761847286L, "", null), { - }, { - }) Spacer(modifier = Modifier.height(8.dp)) diff --git a/app/src/main/java/com/nextcloud/client/assistant/conversation/ConversationViewModel.kt b/app/src/main/java/com/nextcloud/client/assistant/conversation/ConversationViewModel.kt index 394a7cae8eff..3a7001feba0a 100644 --- a/app/src/main/java/com/nextcloud/client/assistant/conversation/ConversationViewModel.kt +++ b/app/src/main/java/com/nextcloud/client/assistant/conversation/ConversationViewModel.kt @@ -20,9 +20,7 @@ import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -class ConversationViewModel( - private val remoteRepository: ConversationRemoteRepository -) : ViewModel() { +class ConversationViewModel(private val remoteRepository: ConversationRemoteRepository) : ViewModel() { private val _errorMessageId = MutableStateFlow(null) val errorMessageId: StateFlow = _errorMessageId diff --git a/app/src/main/java/com/nextcloud/client/assistant/conversation/model/ConversationScreenState.kt b/app/src/main/java/com/nextcloud/client/assistant/conversation/model/ConversationScreenState.kt index 7d7fb8e31dbf..259debcf938e 100644 --- a/app/src/main/java/com/nextcloud/client/assistant/conversation/model/ConversationScreenState.kt +++ b/app/src/main/java/com/nextcloud/client/assistant/conversation/model/ConversationScreenState.kt @@ -16,7 +16,7 @@ sealed class ConversationScreenState { companion object { fun emptyConversationList(): ConversationScreenState = EmptyContent( - descriptionId = R.string.conversation_screen_empty_conversation_list_title, + descriptionId = R.string.conversation_screen_empty_conversation_list_title ) } } diff --git a/app/src/main/java/com/nextcloud/client/assistant/conversation/repository/ConversationRemoteRepositoryImpl.kt b/app/src/main/java/com/nextcloud/client/assistant/conversation/repository/ConversationRemoteRepositoryImpl.kt index c719a589ad8c..8fa89a2bd05e 100644 --- a/app/src/main/java/com/nextcloud/client/assistant/conversation/repository/ConversationRemoteRepositoryImpl.kt +++ b/app/src/main/java/com/nextcloud/client/assistant/conversation/repository/ConversationRemoteRepositoryImpl.kt @@ -14,20 +14,17 @@ import com.owncloud.android.lib.resources.assistant.chat.GetConversationListRemo import com.owncloud.android.lib.resources.assistant.chat.model.Conversation import com.owncloud.android.lib.resources.assistant.chat.model.CreateConversation -class ConversationRemoteRepositoryImpl(private val client: NextcloudClient): ConversationRemoteRepository { +class ConversationRemoteRepositoryImpl(private val client: NextcloudClient) : ConversationRemoteRepository { override suspend fun fetchConversationList(): List? { val result = GetConversationListRemoteOperation().execute(client) return if (result.isSuccess) { - result.resultData + result.resultData } else { null } } - override suspend fun createConversation( - title: String?, - timestamp: Long - ): CreateConversation? { + override suspend fun createConversation(title: String?, timestamp: Long): CreateConversation? { val result = CreateConversationRemoteOperation(title, timestamp).execute(client) return if (result.isSuccess) { result.resultData diff --git a/app/src/main/java/com/nextcloud/client/assistant/conversation/repository/MockConversationRemoteRepository.kt b/app/src/main/java/com/nextcloud/client/assistant/conversation/repository/MockConversationRemoteRepository.kt index 27785524b672..42e13346ac8a 100644 --- a/app/src/main/java/com/nextcloud/client/assistant/conversation/repository/MockConversationRemoteRepository.kt +++ b/app/src/main/java/com/nextcloud/client/assistant/conversation/repository/MockConversationRemoteRepository.kt @@ -10,19 +10,10 @@ package com.nextcloud.client.assistant.conversation.repository import com.owncloud.android.lib.resources.assistant.chat.model.Conversation import com.owncloud.android.lib.resources.assistant.chat.model.CreateConversation -class MockConversationRemoteRepository: ConversationRemoteRepository { - override suspend fun fetchConversationList(): List? { - return null - } +class MockConversationRemoteRepository : ConversationRemoteRepository { + override suspend fun fetchConversationList(): List? = null - override suspend fun createConversation( - title: String?, - timestamp: Long - ): CreateConversation? { - return null - } + override suspend fun createConversation(title: String?, timestamp: Long): CreateConversation? = null - override suspend fun deleteConversation(sessionId: String): Boolean { - return true - } + override suspend fun deleteConversation(sessionId: String): Boolean = true } diff --git a/app/src/main/java/com/nextcloud/client/assistant/model/AssistantScreenState.kt b/app/src/main/java/com/nextcloud/client/assistant/model/AssistantScreenState.kt index 7239e6c4b802..80d2f1a29fb1 100644 --- a/app/src/main/java/com/nextcloud/client/assistant/model/AssistantScreenState.kt +++ b/app/src/main/java/com/nextcloud/client/assistant/model/AssistantScreenState.kt @@ -28,13 +28,13 @@ sealed class AssistantScreenState { fun emptyChatList(): AssistantScreenState = EmptyContent( iconId = R.drawable.ic_assistant, titleId = R.string.assistant_screen_empty_content_title, - descriptionId = R.string.assistant_screen_empty_content_description, + descriptionId = R.string.assistant_screen_empty_content_description ) fun emptyTaskList(): AssistantScreenState = EmptyContent( iconId = R.drawable.ic_assistant, titleId = R.string.assistant_screen_empty_content_title, - descriptionId = null, + descriptionId = null ) } } diff --git a/app/src/main/java/com/nextcloud/client/assistant/repository/remote/AssistantRemoteRepositoryImpl.kt b/app/src/main/java/com/nextcloud/client/assistant/repository/remote/AssistantRemoteRepositoryImpl.kt index 26286df875f5..aff64e1bfcb0 100644 --- a/app/src/main/java/com/nextcloud/client/assistant/repository/remote/AssistantRemoteRepositoryImpl.kt +++ b/app/src/main/java/com/nextcloud/client/assistant/repository/remote/AssistantRemoteRepositoryImpl.kt @@ -87,7 +87,7 @@ class AssistantRemoteRepositoryImpl(private val client: NextcloudClient, capabil override fun fetchChatMessages(id: Long): List? { val result = GetMessagesRemoteOperation(id.toString()).execute(client) return if (result.isSuccess) { - result.resultData + result.resultData } else { null } diff --git a/app/src/main/java/com/nextcloud/client/assistant/repository/remote/MockAssistantRemoteRepository.kt b/app/src/main/java/com/nextcloud/client/assistant/repository/remote/MockAssistantRemoteRepository.kt index fcd56e7d44a5..1a612d35ecec 100644 --- a/app/src/main/java/com/nextcloud/client/assistant/repository/remote/MockAssistantRemoteRepository.kt +++ b/app/src/main/java/com/nextcloud/client/assistant/repository/remote/MockAssistantRemoteRepository.kt @@ -68,15 +68,9 @@ class MockAssistantRemoteRepository(private val giveEmptyTasks: Boolean = false) override fun deleteTask(id: Long): RemoteOperationResult = RemoteOperationResult(RemoteOperationResult.ResultCode.OK) - override fun fetchChatMessages(id: Long): List? { - return emptyList() - } + override fun fetchChatMessages(id: Long): List? = emptyList() - override fun sendChatMessage(request: ChatMessageRequest): ChatMessage? { - return null - } + override fun sendChatMessage(request: ChatMessageRequest): ChatMessage? = null - override fun createConversation(title: String): CreateConversation? { - return null - } + override fun createConversation(title: String): CreateConversation? = null } diff --git a/app/src/main/java/com/nextcloud/client/assistant/taskDetail/TaskDetailBottomSheet.kt b/app/src/main/java/com/nextcloud/client/assistant/taskDetail/TaskDetailBottomSheet.kt index 73614996f005..905dffca1b8c 100644 --- a/app/src/main/java/com/nextcloud/client/assistant/taskDetail/TaskDetailBottomSheet.kt +++ b/app/src/main/java/com/nextcloud/client/assistant/taskDetail/TaskDetailBottomSheet.kt @@ -110,7 +110,7 @@ fun TaskDetailBottomSheet(task: Task, showTaskActions: () -> Unit, dismiss: () - text = stringResource(R.string.assistant_output_generation_warning_text), color = colorResource(R.color.text_color), fontSize = 11.sp, - textAlign = TextAlign.Center, + textAlign = TextAlign.Center ) } } diff --git a/app/src/main/java/com/nextcloud/client/assistant/taskTypes/TaskTypesRow.kt b/app/src/main/java/com/nextcloud/client/assistant/taskTypes/TaskTypesRow.kt index 90e1c9d84e05..a6bdac6033d3 100644 --- a/app/src/main/java/com/nextcloud/client/assistant/taskTypes/TaskTypesRow.kt +++ b/app/src/main/java/com/nextcloud/client/assistant/taskTypes/TaskTypesRow.kt @@ -36,6 +36,10 @@ fun TaskTypesRow( selectTaskType: (TaskTypeData) -> Unit, navigateToConversationList: () -> Unit ) { + if (data.isEmpty()) { + return + } + val selectedTabIndex = data.indexOfFirst { it.id == selectedTaskType?.id }.takeIf { it >= 0 } ?: 0 Row( @@ -90,8 +94,6 @@ private fun TaskTypesRowPreview() { ) TaskTypesRow(selectedTaskType, taskTypes, { - }, { - }) } diff --git a/app/src/main/java/com/nextcloud/ui/composeActivity/ComposeActivity.kt b/app/src/main/java/com/nextcloud/ui/composeActivity/ComposeActivity.kt index c6348385b881..27b295bac9c3 100644 --- a/app/src/main/java/com/nextcloud/ui/composeActivity/ComposeActivity.kt +++ b/app/src/main/java/com/nextcloud/ui/composeActivity/ComposeActivity.kt @@ -84,7 +84,7 @@ class ComposeActivity : DrawerActivity() { isChecked = true } - when(currentScreen) { + when (currentScreen) { is ComposeDestination.AssistantScreen -> { val dao = NextcloudDatabase.instance().assistantDao() val sessionId = (currentScreen as? ComposeDestination.AssistantScreen)?.sessionId @@ -94,14 +94,14 @@ class ComposeActivity : DrawerActivity() { viewModel = AssistantViewModel( accountName = userAccountManager.user.accountName, remoteRepository = AssistantRemoteRepositoryImpl(client, capabilities), - localRepository = AssistantLocalRepositoryImpl(dao) + localRepository = AssistantLocalRepositoryImpl(dao), + sessionIdArg = sessionId ), conversationViewModel = ConversationViewModel( remoteRepository = ConversationRemoteRepositoryImpl(client) ), activity = this, - capability = capabilities, - sessionIdArg = sessionId + capability = capabilities ) } else -> Unit diff --git a/app/src/main/java/com/nextcloud/ui/composeActivity/ComposeDestination.kt b/app/src/main/java/com/nextcloud/ui/composeActivity/ComposeDestination.kt index 7fcb02b024fd..050c8e5a2848 100644 --- a/app/src/main/java/com/nextcloud/ui/composeActivity/ComposeDestination.kt +++ b/app/src/main/java/com/nextcloud/ui/composeActivity/ComposeDestination.kt @@ -11,11 +11,9 @@ sealed class ComposeDestination(val id: Int) { data class AssistantScreen(val sessionId: Long?) : ComposeDestination(0) companion object { - fun fromId(id: Int): ComposeDestination { - return when (id) { - 0 -> AssistantScreen(null) - else -> throw IllegalArgumentException("Unknown destination: $id") - } + fun fromId(id: Int): ComposeDestination = when (id) { + 0 -> AssistantScreen(null) + else -> throw IllegalArgumentException("Unknown destination: $id") } } } From 835d668880eb9bb6473e6a0c1bf66998658521d4 Mon Sep 17 00:00:00 2001 From: alperozturk Date: Wed, 12 Nov 2025 10:05:14 +0100 Subject: [PATCH 13/29] fix codacy Signed-off-by: alperozturk --- .../java/com/nextcloud/client/assistant/AssistantViewModel.kt | 4 ++-- .../client/assistant/conversation/ConversationViewModel.kt | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/nextcloud/client/assistant/AssistantViewModel.kt b/app/src/main/java/com/nextcloud/client/assistant/AssistantViewModel.kt index 4a439249415e..2fa688bc5296 100644 --- a/app/src/main/java/com/nextcloud/client/assistant/AssistantViewModel.kt +++ b/app/src/main/java/com/nextcloud/client/assistant/AssistantViewModel.kt @@ -13,6 +13,7 @@ import com.nextcloud.client.assistant.model.AssistantScreenState 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.owncloud.android.R import com.owncloud.android.lib.common.utils.Log_OC import com.owncloud.android.lib.resources.assistant.chat.model.ChatMessage @@ -40,7 +41,6 @@ class AssistantViewModel( companion object { private const val TAG = "AssistantViewModel" private const val POLLING_INTERVAL_MS = 15_000L - private const val ONE_SECOND_MS = 1000L } private val _screenState = MutableStateFlow(null) @@ -154,7 +154,7 @@ class AssistantViewModel( } fun sendChatMessage(content: String, sessionId: Long) { - val timestamp = System.currentTimeMillis().div(ONE_SECOND_MS) + val timestamp = System.currentTimeMillis().div(MILLIS_PER_SECOND) val firstHumanMessage = _chatMessages.value.isEmpty() val request = ChatMessageRequest( diff --git a/app/src/main/java/com/nextcloud/client/assistant/conversation/ConversationViewModel.kt b/app/src/main/java/com/nextcloud/client/assistant/conversation/ConversationViewModel.kt index 3a7001feba0a..c2e290c8fde0 100644 --- a/app/src/main/java/com/nextcloud/client/assistant/conversation/ConversationViewModel.kt +++ b/app/src/main/java/com/nextcloud/client/assistant/conversation/ConversationViewModel.kt @@ -11,6 +11,7 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.nextcloud.client.assistant.conversation.model.ConversationScreenState import com.nextcloud.client.assistant.conversation.repository.ConversationRemoteRepository +import com.nextcloud.utils.TimeConstants.MILLIS_PER_SECOND import com.owncloud.android.R import com.owncloud.android.lib.resources.assistant.chat.model.Conversation import kotlinx.coroutines.Dispatchers @@ -63,7 +64,7 @@ class ConversationViewModel(private val remoteRepository: ConversationRemoteRepo fun createConversation(title: String?) { viewModelScope.launch(Dispatchers.IO) { - val timestamp = System.currentTimeMillis() / 1000 + val timestamp = System.currentTimeMillis().div(MILLIS_PER_SECOND) val newConversation = remoteRepository.createConversation(title, timestamp) if (newConversation != null) { _conversations.update { From 9e2428a981e6fac409cfa7541cd3aa814b1ff0c1 Mon Sep 17 00:00:00 2001 From: alperozturk Date: Wed, 12 Nov 2025 12:22:55 +0100 Subject: [PATCH 14/29] better polling Signed-off-by: alperozturk --- .../client/assistant/AssistantScreen.kt | 1 - .../client/assistant/AssistantViewModel.kt | 233 +++++++++--------- .../client/assistant/chat/ChatContent.kt | 110 ++++++++- .../remote/AssistantRemoteRepository.kt | 22 +- .../remote/AssistantRemoteRepositoryImpl.kt | 103 ++++---- .../remote/MockAssistantRemoteRepository.kt | 21 +- .../taskDetail/TaskDetailBottomSheet.kt | 2 +- app/src/main/res/values/strings.xml | 2 +- 8 files changed, 308 insertions(+), 186 deletions(-) diff --git a/app/src/main/java/com/nextcloud/client/assistant/AssistantScreen.kt b/app/src/main/java/com/nextcloud/client/assistant/AssistantScreen.kt index 1476cd10d06f..bd1f4cd20860 100644 --- a/app/src/main/java/com/nextcloud/client/assistant/AssistantScreen.kt +++ b/app/src/main/java/com/nextcloud/client/assistant/AssistantScreen.kt @@ -91,7 +91,6 @@ fun AssistantScreen( val sessionId by viewModel.sessionId.collectAsState() val messageId by viewModel.snackbarMessageId.collectAsState() val screenOverlayState by viewModel.screenOverlayState.collectAsState() - val selectedTaskType by viewModel.selectedTaskType.collectAsState() val filteredTaskList by viewModel.filteredTaskList.collectAsState() val screenState by viewModel.screenState.collectAsState() diff --git a/app/src/main/java/com/nextcloud/client/assistant/AssistantViewModel.kt b/app/src/main/java/com/nextcloud/client/assistant/AssistantViewModel.kt index 2fa688bc5296..387fe5c5f0c3 100644 --- a/app/src/main/java/com/nextcloud/client/assistant/AssistantViewModel.kt +++ b/app/src/main/java/com/nextcloud/client/assistant/AssistantViewModel.kt @@ -69,7 +69,11 @@ class AssistantViewModel( private val _chatMessages = MutableStateFlow>(listOf()) val chatMessages: StateFlow> = _chatMessages + private val _isAssistantAnswering = MutableStateFlow(false) + val isAssistantAnswering: StateFlow = _isAssistantAnswering + private var pollingJob: Job? = null + private var currentChatTaskId: String? = null init { observeScreenState() @@ -83,17 +87,24 @@ class AssistantViewModel( pollingJob = viewModelScope.launch(Dispatchers.IO) { try { while (isActive) { - val isChat = (_selectedTaskType.value?.isChat == true) + delay(POLLING_INTERVAL_MS) - if (isChat && sessionId != null) { + val taskType = _selectedTaskType.value ?: continue + + if (taskType.isChat && sessionId != null) { Log_OC.d(TAG, "Polling chat messages, sessionId: $sessionId") - pollChatMessages(sessionId) - } else if (!isChat) { + + if (currentChatTaskId == null) { + remoteRepository.generateSession(sessionId.toString())?.let { + currentChatTaskId = it.taskId.toString() + } + } + + fetchNewChatMessage(sessionId) + } else if (!taskType.isChat) { Log_OC.d(TAG, "Polling task list") pollTaskList() } - - delay(POLLING_INTERVAL_MS) } } finally { Log_OC.d(TAG, "Polling cancelled, sessionId: $sessionId") @@ -122,11 +133,25 @@ class AssistantViewModel( } } - private fun pollChatMessages(sessionId: Long) { - val result = remoteRepository.fetchChatMessages(sessionId) - if (result != null) { - _chatMessages.update { - result + 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 } } } @@ -153,154 +178,122 @@ class AssistantViewModel( } } - fun sendChatMessage(content: String, sessionId: Long) { - val timestamp = System.currentTimeMillis().div(MILLIS_PER_SECOND) - val firstHumanMessage = _chatMessages.value.isEmpty() - val request = - ChatMessageRequest( - sessionId = sessionId.toString(), - role = "human", - content = content, - timestamp = timestamp, - firstHumanMessage = firstHumanMessage - ) - - viewModelScope.launch(Dispatchers.IO) { - val result = remoteRepository.sendChatMessage(request) - if (result != null) { - fetchChatMessages(sessionId) - } else { - updateSnackbarMessage(R.string.assistant_screen_chat_create_error) + // 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) { - val result = remoteRepository.fetchChatMessages(sessionId) - if (result != null) { - _chatMessages.update { - result - } - } else { - updateSnackbarMessage(R.string.assistant_screen_chat_fetch_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) { - val result = remoteRepository.createConversation(title) - if (result != null) { - initSessionId(result.session.id) - sendChatMessage(content = title, sessionId = result.session.id) - } + 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") - - _sessionId.update { - value - } + currentChatTaskId = null + _sessionId.update { value } } + // endregion - @Suppress("MagicNumber") - fun createTask(input: String, taskType: TaskTypeData) { - viewModelScope.launch(Dispatchers.IO) { - val result = remoteRepository.createTask(input, taskType) - - val messageId = if (result.isSuccess) { - R.string.assistant_screen_task_create_success_message - } else { - R.string.assistant_screen_task_create_fail_message - } - - updateSnackbarMessage(messageId) - - delay(2000L) - fetchTaskList() + // region task + fun createTask(input: String, taskType: TaskTypeData) = viewModelScope.launch(Dispatchers.IO) { + val result = remoteRepository.createTask(input, taskType) + val message = if (result.isSuccess) { + R.string.assistant_screen_task_create_success_message + } else { + R.string.assistant_screen_task_create_fail_message } + + updateSnackbarMessage(message) + delay(MILLIS_PER_SECOND * 2L) + fetchTaskList() } fun selectTaskType(task: TaskTypeData) { - Log_OC.d(TAG, "task type changed: ${task.name}, session id: ${_sessionId.value}") - + Log_OC.d(TAG, "Task type changed: ${task.name}, session id: ${_sessionId.value}") updateTaskType(task) + val sessionId = _sessionId.value ?: return if (task.isChat) { - val sessionId = _sessionId.value ?: return - fetchChatMessages(sessionId) + if (_chatMessages.value.isEmpty()) { + fetchChatMessages(sessionId) + } else { + fetchNewChatMessage(sessionId) + } } else { fetchTaskList() } } - private fun fetchTaskTypes() { - viewModelScope.launch(Dispatchers.IO) { - val taskTypesResult = remoteRepository.getTaskTypes() - if (taskTypesResult.isNullOrEmpty()) { - _screenState.update { - AssistantScreenState.emptyTaskTypes() - } - return@launch - } - - _taskTypes.update { - taskTypesResult - } + private fun fetchTaskTypes() = viewModelScope.launch(Dispatchers.IO) { + val result = remoteRepository.getTaskTypes() + if (result.isNullOrEmpty()) { + _screenState.value = AssistantScreenState.emptyTaskTypes() + return@launch + } - selectTaskType(taskTypesResult.first()) + _taskTypes.update { + result } + selectTaskType(result.first()) } - fun fetchTaskList() { - viewModelScope.launch(Dispatchers.IO) { - // Try cached data first - val cachedTasks = localRepository.getCachedTasks(accountName) - if (cachedTasks.isNotEmpty()) { - _filteredTaskList.update { - cachedTasks.sortedByDescending { it.id } - } - } + fun fetchTaskList() = viewModelScope.launch(Dispatchers.IO) { + val cached = localRepository.getCachedTasks(accountName) + if (cached.isNotEmpty()) { + _filteredTaskList.value = cached.sortedByDescending { it.id } + } - val taskType = _selectedTaskType.value?.id ?: return@launch - val result = remoteRepository.getTaskList(taskType) - if (result != null) { + _selectedTaskType.value?.id?.let { typeId -> + remoteRepository.getTaskList(typeId)?.let { result -> taskList = result - _filteredTaskList.update { - taskList?.sortedByDescending { task -> - task.id - } - } - + _filteredTaskList.value = result.sortedByDescending { it.id } localRepository.cacheTasks(result, accountName) updateSnackbarMessage(null) - } else { - updateSnackbarMessage(R.string.assistant_screen_task_list_error_state_message) - } + } ?: updateSnackbarMessage(R.string.assistant_screen_task_list_error_state_message) } } - fun deleteTask(id: Long) { - viewModelScope.launch(Dispatchers.IO) { - val result = remoteRepository.deleteTask(id) - - val messageId = if (result.isSuccess) { - R.string.assistant_screen_task_delete_success_message - } else { - R.string.assistant_screen_task_delete_fail_message - } - - updateSnackbarMessage(messageId) + fun deleteTask(id: Long) = viewModelScope.launch(Dispatchers.IO) { + val result = remoteRepository.deleteTask(id) + val message = if (result.isSuccess) { + R.string.assistant_screen_task_delete_success_message + } else { + R.string.assistant_screen_task_delete_fail_message + } - if (result.isSuccess) { - removeTaskFromList(id) - localRepository.deleteTask(id, accountName) - } + updateSnackbarMessage(message) + if (result.isSuccess) { + removeTaskFromList(id) + localRepository.deleteTask(id, accountName) } } + // endregion private fun updateTaskType(value: TaskTypeData) { _selectedTaskType.update { diff --git a/app/src/main/java/com/nextcloud/client/assistant/chat/ChatContent.kt b/app/src/main/java/com/nextcloud/client/assistant/chat/ChatContent.kt index c334818c46df..76212e73553b 100644 --- a/app/src/main/java/com/nextcloud/client/assistant/chat/ChatContent.kt +++ b/app/src/main/java/com/nextcloud/client/assistant/chat/ChatContent.kt @@ -5,10 +5,16 @@ * SPDX-License-Identifier: AGPL-3.0-or-later */ -@file:Suppress("TopLevelPropertyNaming") +@file:Suppress("TopLevelPropertyNaming", "MagicNumber") package com.nextcloud.client.assistant.chat +import androidx.compose.animation.core.LinearEasing +import androidx.compose.animation.core.RepeatMode +import androidx.compose.animation.core.animateFloat +import androidx.compose.animation.core.infiniteRepeatable +import androidx.compose.animation.core.rememberInfiniteTransition +import androidx.compose.animation.core.tween import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement @@ -35,14 +41,17 @@ import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.scale import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.res.colorResource import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.TextStyle import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.nextcloud.client.assistant.AssistantViewModel +import com.nextcloud.utils.TimeConstants import com.owncloud.android.R import com.owncloud.android.lib.resources.assistant.chat.model.ChatMessage @@ -53,6 +62,7 @@ private val ASSISTANT_ICON_SIZE = 40.dp @Composable fun ChatContent(viewModel: AssistantViewModel, modifier: Modifier = Modifier) { val chatMessages by viewModel.chatMessages.collectAsState() + val isAssistantAnswering by viewModel.isAssistantAnswering.collectAsState() val listState = rememberLazyListState() LaunchedEffect(chatMessages) { @@ -75,6 +85,104 @@ fun ChatContent(viewModel: AssistantViewModel, modifier: Modifier = Modifier) { } Spacer(modifier = Modifier.height(8.dp)) } + + if (isAssistantAnswering) { + item { + AssistantTypingIndicator() + Spacer(modifier = Modifier.height(8.dp)) + } + } + } +} + +@Composable +private fun AssistantTypingIndicator() { + Box( + modifier = Modifier + .padding(vertical = 12.dp) + .fillMaxWidth(), + contentAlignment = Alignment.CenterStart + ) { + Row( + verticalAlignment = Alignment.Bottom + ) { + AnimatedAssistantIcon() + + Box( + modifier = Modifier + .padding(start = 8.dp, end = 16.dp) + .defaultMinSize(minHeight = MIN_CHAT_HEIGHT) + .clip( + RoundedCornerShape( + topEnd = CHAT_BUBBLE_CORNER_RADIUS, + topStart = CHAT_BUBBLE_CORNER_RADIUS, + bottomEnd = CHAT_BUBBLE_CORNER_RADIUS + ) + ) + .background(color = colorResource(R.color.task_container)) + ) { + TypingAnimation() + } + } + } +} + +@Composable +private fun AnimatedAssistantIcon() { + val infiniteTransition = rememberInfiniteTransition(label = "assistant_icon_animation") + val scale by infiniteTransition.animateFloat( + initialValue = 1f, + targetValue = 1.1f, + animationSpec = infiniteRepeatable( + animation = tween(TimeConstants.MILLIS_PER_SECOND, easing = LinearEasing), + repeatMode = RepeatMode.Reverse + ), + label = "scale_animation" + ) + + Box( + modifier = Modifier + .size(ASSISTANT_ICON_SIZE) + .clip(CircleShape) + .background(Color.White), + contentAlignment = Alignment.Center + ) { + Image( + painter = painterResource(id = R.drawable.ic_assistant), + contentDescription = null, + contentScale = ContentScale.Crop, + alignment = Alignment.Center, + modifier = Modifier + .scale(scale) + ) + } +} + +@Composable +private fun TypingAnimation() { + val infiniteTransition = rememberInfiniteTransition(label = "typing_animation") + + val alpha by infiniteTransition.animateFloat( + initialValue = 0.3f, + targetValue = 1f, + animationSpec = infiniteRepeatable( + animation = tween(600, easing = LinearEasing), + repeatMode = RepeatMode.Reverse + ), + label = "typing_alpha" + ) + + Column( + modifier = Modifier.padding(8.dp), + horizontalAlignment = Alignment.Start + ) { + Text( + text = stringResource(R.string.assistant_thinking), + style = TextStyle( + color = colorResource(R.color.text_color).copy(alpha = alpha), + fontSize = 16.sp + ) + ) } } diff --git a/app/src/main/java/com/nextcloud/client/assistant/repository/remote/AssistantRemoteRepository.kt b/app/src/main/java/com/nextcloud/client/assistant/repository/remote/AssistantRemoteRepository.kt index 420b9b08168e..3eb48968bb78 100644 --- a/app/src/main/java/com/nextcloud/client/assistant/repository/remote/AssistantRemoteRepository.kt +++ b/app/src/main/java/com/nextcloud/client/assistant/repository/remote/AssistantRemoteRepository.kt @@ -11,21 +11,29 @@ import com.owncloud.android.lib.common.operations.RemoteOperationResult 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.chat.model.CreateConversation +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 interface AssistantRemoteRepository { - fun getTaskTypes(): List? + suspend fun getTaskTypes(): List? - fun createTask(input: String, taskType: TaskTypeData): RemoteOperationResult + suspend fun createTask(input: String, taskType: TaskTypeData): RemoteOperationResult - fun getTaskList(taskType: String): List? + suspend fun getTaskList(taskType: String): List? - fun deleteTask(id: Long): RemoteOperationResult + suspend fun deleteTask(id: Long): RemoteOperationResult - fun fetchChatMessages(id: Long): List? + suspend fun fetchChatMessages(id: Long): List? - fun sendChatMessage(request: ChatMessageRequest): ChatMessage? + suspend fun sendChatMessage(request: ChatMessageRequest): ChatMessage? - fun createConversation(title: String): CreateConversation? + suspend fun createConversation(title: String): CreateConversation? + + suspend fun checkSession(sessionId: String): Session? + + suspend fun generateSession(sessionId: String): SessionTask? + + suspend fun checkGeneration(taskId: String, sessionId: String): ChatMessage? } diff --git a/app/src/main/java/com/nextcloud/client/assistant/repository/remote/AssistantRemoteRepositoryImpl.kt b/app/src/main/java/com/nextcloud/client/assistant/repository/remote/AssistantRemoteRepositoryImpl.kt index aff64e1bfcb0..3030ecbe779d 100644 --- a/app/src/main/java/com/nextcloud/client/assistant/repository/remote/AssistantRemoteRepositoryImpl.kt +++ b/app/src/main/java/com/nextcloud/client/assistant/repository/remote/AssistantRemoteRepositoryImpl.kt @@ -7,14 +7,20 @@ package com.nextcloud.client.assistant.repository.remote import com.nextcloud.common.NextcloudClient +import com.nextcloud.utils.TimeConstants import com.owncloud.android.lib.common.operations.RemoteOperationResult import com.owncloud.android.lib.common.operations.RemoteOperationResult.ResultCode +import com.owncloud.android.lib.resources.assistant.chat.CheckGenerationRemoteOperation +import com.owncloud.android.lib.resources.assistant.chat.CheckSessionRemoteOperation import com.owncloud.android.lib.resources.assistant.chat.CreateConversationRemoteOperation import com.owncloud.android.lib.resources.assistant.chat.CreateMessageRemoteOperation +import com.owncloud.android.lib.resources.assistant.chat.GenerateSessionRemoteOperation import com.owncloud.android.lib.resources.assistant.chat.GetMessagesRemoteOperation 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.chat.model.CreateConversation +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.v1.CreateTaskRemoteOperationV1 import com.owncloud.android.lib.resources.assistant.v1.DeleteTaskRemoteOperationV1 import com.owncloud.android.lib.resources.assistant.v1.GetTaskListRemoteOperationV1 @@ -28,89 +34,94 @@ 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.status.NextcloudVersion import com.owncloud.android.lib.resources.status.OCCapability +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext class AssistantRemoteRepositoryImpl(private val client: NextcloudClient, capability: OCCapability) : AssistantRemoteRepository { private val supportsV2 = capability.version.isNewerOrEqual(NextcloudVersion.nextcloud_30) - @Suppress("ReturnCount") - override fun getTaskTypes(): List? { + override suspend fun getTaskTypes(): List? = withContext(Dispatchers.IO) { if (supportsV2) { val result = GetTaskTypesRemoteOperationV2().execute(client) if (result.isSuccess) { - return result.resultData + return@withContext result.resultData } } else { val result = GetTaskTypesRemoteOperationV1().execute(client) if (result.isSuccess) { - return result.resultData.toV2() + return@withContext result.resultData.toV2() } } - - return null + return@withContext null } - override fun createTask(input: String, taskType: TaskTypeData): RemoteOperationResult = if (supportsV2) { - CreateTaskRemoteOperationV2(input, taskType).execute(client) - } else { - if (taskType.id.isNullOrEmpty()) { - RemoteOperationResult(ResultCode.CANCELLED) - } else { - CreateTaskRemoteOperationV1(input, taskType.id!!).execute(client) + override suspend fun createTask(input: String, taskType: TaskTypeData): RemoteOperationResult = + withContext(Dispatchers.IO) { + if (supportsV2) { + CreateTaskRemoteOperationV2(input, taskType).execute(client) + } else { + if (taskType.id.isNullOrEmpty()) { + RemoteOperationResult(ResultCode.CANCELLED) + } else { + CreateTaskRemoteOperationV1(input, taskType.id!!).execute(client) + } + } } - } - @Suppress("ReturnCount") - override fun getTaskList(taskType: String): List? { + override suspend fun getTaskList(taskType: String): List? = withContext(Dispatchers.IO) { if (supportsV2) { val result = GetTaskListRemoteOperationV2(taskType).execute(client) if (result.isSuccess) { - return result.resultData.tasks.filter { it.appId == "assistant" } + return@withContext result.resultData.tasks.filter { it.appId == "assistant" } } } else { val result = GetTaskListRemoteOperationV1("assistant").execute(client) if (result.isSuccess) { - return result.resultData.toV2().tasks.filter { it.type == taskType } + return@withContext result.resultData.toV2().tasks.filter { it.type == taskType } } } - - return null + return@withContext null } - override fun deleteTask(id: Long): RemoteOperationResult = if (supportsV2) { - DeleteTaskRemoteOperationV2(id).execute(client) - } else { - DeleteTaskRemoteOperationV1(id).execute(client) + override suspend fun deleteTask(id: Long): RemoteOperationResult = withContext(Dispatchers.IO) { + if (supportsV2) { + DeleteTaskRemoteOperationV2(id).execute(client) + } else { + DeleteTaskRemoteOperationV1(id).execute(client) + } } - override fun fetchChatMessages(id: Long): List? { + override suspend fun fetchChatMessages(id: Long): List? = withContext(Dispatchers.IO) { val result = GetMessagesRemoteOperation(id.toString()).execute(client) - return if (result.isSuccess) { - result.resultData - } else { - null - } + if (result.isSuccess) result.resultData else null } - override fun sendChatMessage(request: ChatMessageRequest): ChatMessage? { + override suspend fun sendChatMessage(request: ChatMessageRequest): ChatMessage? = withContext(Dispatchers.IO) { val result = CreateMessageRemoteOperation(request).execute(client) - return if (result.isSuccess) { - result.resultData - } else { - null - } + if (result.isSuccess) result.resultData else null } - @Suppress("MagicNumber") - override fun createConversation(title: String): CreateConversation? { - val timestamp = (System.currentTimeMillis() / 1000) - val result = - CreateConversationRemoteOperation(title, timestamp).execute(client) - return if (result.isSuccess) { - result.resultData - } else { - null - } + override suspend fun createConversation(title: String): CreateConversation? = withContext(Dispatchers.IO) { + val timestamp = System.currentTimeMillis().div(TimeConstants.MILLIS_PER_SECOND) + val result = CreateConversationRemoteOperation(title, timestamp).execute(client) + if (result.isSuccess) result.resultData else null + } + + override suspend fun checkSession(sessionId: String): Session? = withContext(Dispatchers.IO) { + val result = CheckSessionRemoteOperation(sessionId).execute(client) + if (result.isSuccess) result.resultData else null + } + + override suspend fun generateSession(sessionId: String): SessionTask? = withContext(Dispatchers.IO) { + val result = GenerateSessionRemoteOperation(sessionId).execute(client) + if (result.isSuccess) result.resultData else null } + + override suspend fun checkGeneration(taskId: String, sessionId: String): ChatMessage? = + withContext(Dispatchers.IO) { + val result = CheckGenerationRemoteOperation(taskId, sessionId).execute(client) + if (result.isSuccess) result.resultData else null + } } diff --git a/app/src/main/java/com/nextcloud/client/assistant/repository/remote/MockAssistantRemoteRepository.kt b/app/src/main/java/com/nextcloud/client/assistant/repository/remote/MockAssistantRemoteRepository.kt index 1a612d35ecec..930489812e85 100644 --- a/app/src/main/java/com/nextcloud/client/assistant/repository/remote/MockAssistantRemoteRepository.kt +++ b/app/src/main/java/com/nextcloud/client/assistant/repository/remote/MockAssistantRemoteRepository.kt @@ -11,6 +11,8 @@ import com.owncloud.android.lib.common.operations.RemoteOperationResult 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.chat.model.CreateConversation +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.Shape import com.owncloud.android.lib.resources.assistant.v2.model.Task import com.owncloud.android.lib.resources.assistant.v2.model.TaskInput @@ -19,7 +21,7 @@ import com.owncloud.android.lib.resources.assistant.v2.model.TaskTypeData @Suppress("MagicNumber") class MockAssistantRemoteRepository(private val giveEmptyTasks: Boolean = false) : AssistantRemoteRepository { - override fun getTaskTypes(): List = listOf( + override suspend fun getTaskTypes(): List = listOf( TaskTypeData( id = "core:text2text", name = "Free text to text prompt", @@ -41,10 +43,10 @@ class MockAssistantRemoteRepository(private val giveEmptyTasks: Boolean = false) ) ) - override fun createTask(input: String, taskType: TaskTypeData): RemoteOperationResult = + override suspend fun createTask(input: String, taskType: TaskTypeData): RemoteOperationResult = RemoteOperationResult(RemoteOperationResult.ResultCode.OK) - override fun getTaskList(taskType: String): List = if (giveEmptyTasks) { + override suspend fun getTaskList(taskType: String): List = if (giveEmptyTasks) { listOf() } else { listOf( @@ -65,12 +67,13 @@ class MockAssistantRemoteRepository(private val giveEmptyTasks: Boolean = false) ) } - override fun deleteTask(id: Long): RemoteOperationResult = + override suspend fun deleteTask(id: Long): RemoteOperationResult = RemoteOperationResult(RemoteOperationResult.ResultCode.OK) + override suspend fun fetchChatMessages(id: Long): List = emptyList() + override suspend fun sendChatMessage(request: ChatMessageRequest): ChatMessage? = null + override suspend fun createConversation(title: String): CreateConversation? = null + override suspend fun checkSession(sessionId: String): Session? = null + override suspend fun generateSession(sessionId: String): SessionTask? = null - override fun fetchChatMessages(id: Long): List? = emptyList() - - override fun sendChatMessage(request: ChatMessageRequest): ChatMessage? = null - - override fun createConversation(title: String): CreateConversation? = null + override suspend fun checkGeneration(taskId: String, sessionId: String): ChatMessage? = null } diff --git a/app/src/main/java/com/nextcloud/client/assistant/taskDetail/TaskDetailBottomSheet.kt b/app/src/main/java/com/nextcloud/client/assistant/taskDetail/TaskDetailBottomSheet.kt index 905dffca1b8c..7b9ae673f6f4 100644 --- a/app/src/main/java/com/nextcloud/client/assistant/taskDetail/TaskDetailBottomSheet.kt +++ b/app/src/main/java/com/nextcloud/client/assistant/taskDetail/TaskDetailBottomSheet.kt @@ -106,7 +106,7 @@ fun TaskDetailBottomSheet(task: Task, showTaskActions: () -> Unit, dismiss: () - Spacer(modifier = Modifier.width(4.dp)) Text( - modifier = Modifier.widthIn(max = 200.dp), + modifier = Modifier.widthIn(max = 300.dp), text = stringResource(R.string.assistant_output_generation_warning_text), color = colorResource(R.color.text_color), fontSize = 11.sp, diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 7708fe390e04..16890d084880 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -83,7 +83,7 @@ Output shown here is generated by AI. Make sure to always double-check. - + Thinking... Hello there! What can I help you with today? Try sending a message to spark a conversation. From 09651c2efd454304e17a8e7232280636b26057d5 Mon Sep 17 00:00:00 2001 From: alperozturk Date: Wed, 12 Nov 2025 13:39:41 +0100 Subject: [PATCH 15/29] update lib Signed-off-by: alperozturk --- .../client/assistant/AssistantScreen.kt | 4 ++-- .../client/assistant/AssistantViewModel.kt | 8 +++---- gradle/libs.versions.toml | 2 +- gradle/verification-metadata.xml | 24 +++++++++++++++++++ 4 files changed, 31 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/com/nextcloud/client/assistant/AssistantScreen.kt b/app/src/main/java/com/nextcloud/client/assistant/AssistantScreen.kt index bd1f4cd20860..dcc2924184ba 100644 --- a/app/src/main/java/com/nextcloud/client/assistant/AssistantScreen.kt +++ b/app/src/main/java/com/nextcloud/client/assistant/AssistantScreen.kt @@ -132,7 +132,7 @@ fun AssistantScreen( } }, openChat = { newSessionId -> viewModel.initSessionId(newSessionId) - taskTypes?.find { it.isChat }?.let { chatTaskType -> + taskTypes?.find { it.isChat() }?.let { chatTaskType -> viewModel.selectTaskType(chatTaskType) } scope.launch { @@ -277,7 +277,7 @@ private fun ChatInputBar(sessionId: Long?, selectedTaskType: TaskTypeData?, view } val taskType = selectedTaskType ?: return@IconButton - if (taskType.isChat) { + if (taskType.isChat()) { if (sessionId != null) { viewModel.sendChatMessage(content = text, sessionId) } else { diff --git a/app/src/main/java/com/nextcloud/client/assistant/AssistantViewModel.kt b/app/src/main/java/com/nextcloud/client/assistant/AssistantViewModel.kt index 387fe5c5f0c3..bfeefd23aa0b 100644 --- a/app/src/main/java/com/nextcloud/client/assistant/AssistantViewModel.kt +++ b/app/src/main/java/com/nextcloud/client/assistant/AssistantViewModel.kt @@ -91,7 +91,7 @@ class AssistantViewModel( val taskType = _selectedTaskType.value ?: continue - if (taskType.isChat && sessionId != null) { + if (taskType.isChat() && sessionId != null) { Log_OC.d(TAG, "Polling chat messages, sessionId: $sessionId") if (currentChatTaskId == null) { @@ -101,7 +101,7 @@ class AssistantViewModel( } fetchNewChatMessage(sessionId) - } else if (!taskType.isChat) { + } else if (!taskType.isChat()) { Log_OC.d(TAG, "Polling task list") pollTaskList() } @@ -163,7 +163,7 @@ class AssistantViewModel( _chatMessages, _filteredTaskList ) { selectedTask, chats, tasks -> - val isChat = selectedTask?.isChat == true + val isChat = selectedTask?.isChat() == true when { selectedTask == null -> AssistantScreenState.Loading @@ -239,7 +239,7 @@ class AssistantViewModel( updateTaskType(task) val sessionId = _sessionId.value ?: return - if (task.isChat) { + if (task.isChat()) { if (_chatMessages.value.isEmpty()) { fetchChatMessages(sessionId) } else { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 4cdc360006d4..67ed9ee36964 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -5,7 +5,7 @@ androidCommonLibraryVersion = "0.29.0" androidGifDrawableVersion = "1.2.29" androidImageCropperVersion = "4.6.0" -androidLibraryVersion = "8c77f600ac942f6eb4e3c4447143fc949d34acc6" +androidLibraryVersion = "d4a24181d1" androidPluginVersion = '8.13.1' androidsvgVersion = "1.4" androidxMediaVersion = "1.5.1" diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index ea2799bc44af..c32b2a949b32 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -17,6 +17,9 @@ + + + @@ -18210,6 +18213,14 @@ + + + + + + + + @@ -21835,6 +21846,11 @@ + + + + + @@ -21935,6 +21951,14 @@ + + + + + + + + From 4894bf47688c2b2b76f1d95f8cb6c5c77e5a436d Mon Sep 17 00:00:00 2001 From: alperozturk Date: Thu, 13 Nov 2025 11:31:05 +0100 Subject: [PATCH 16/29] fix: chat date display Signed-off-by: alperozturk --- .idea/inspectionProfiles/ktlint.xml | 6 +++ .../assistant/AssistantRepositoryTests.kt | 34 ++++++++----- .../client/assistant/AssistantViewModel.kt | 1 + .../client/assistant/chat/ChatContent.kt | 50 ++++++++++++++++++- .../utils/extensions/ChatMessageExtensions.kt | 31 ++++++++++++ gradle/libs.versions.toml | 2 +- gradle/verification-metadata.xml | 8 +++ 7 files changed, 118 insertions(+), 14 deletions(-) create mode 100644 app/src/main/java/com/nextcloud/utils/extensions/ChatMessageExtensions.kt diff --git a/.idea/inspectionProfiles/ktlint.xml b/.idea/inspectionProfiles/ktlint.xml index f82b736f29a6..4aab2f7215ef 100644 --- a/.idea/inspectionProfiles/ktlint.xml +++ b/.idea/inspectionProfiles/ktlint.xml @@ -6,15 +6,19 @@ + + + + + + + + From f79fd6e6d7641ebbaaddd3cee9e06591a68f3cb3 Mon Sep 17 00:00:00 2001 From: alperozturk Date: Thu, 13 Nov 2025 12:58:39 +0100 Subject: [PATCH 17/29] fix ui Signed-off-by: alperozturk --- .../conversation/ConversationScreen.kt | 132 +++++++++--------- app/src/main/res/values/strings.xml | 3 +- 2 files changed, 66 insertions(+), 69 deletions(-) diff --git a/app/src/main/java/com/nextcloud/client/assistant/conversation/ConversationScreen.kt b/app/src/main/java/com/nextcloud/client/assistant/conversation/ConversationScreen.kt index 69c7e95c73a9..3d9bd03cd230 100644 --- a/app/src/main/java/com/nextcloud/client/assistant/conversation/ConversationScreen.kt +++ b/app/src/main/java/com/nextcloud/client/assistant/conversation/ConversationScreen.kt @@ -9,47 +9,51 @@ package com.nextcloud.client.assistant.conversation +import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.Close -import androidx.compose.material.icons.outlined.Delete import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.FilledTonalButton +import androidx.compose.material3.FabPosition +import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.OutlinedButton import androidx.compose.material3.Scaffold import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableLongStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.colorResource import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import com.nextcloud.client.assistant.conversation.model.ConversationScreenState +import com.nextcloud.ui.composeComponents.bottomSheet.MoreActionsBottomSheet import com.owncloud.android.R import com.owncloud.android.lib.resources.assistant.chat.model.Conversation @@ -94,11 +98,14 @@ fun ConversationScreen(viewModel: ConversationViewModel, close: () -> Unit, open snackbarHost = { SnackbarHost(snackbarHostState) }, - bottomBar = { - CreateConversationButton(onClick = { + floatingActionButton = { + FloatingActionButton(onClick = { viewModel.createConversation(null) - }) - } + }) { + Icon(Icons.Filled.Add, "Floating action button.") + } + }, + floatingActionButtonPosition = FabPosition.EndOverlay ) { innerPadding -> when (screenState) { is ConversationScreenState.Loading -> { @@ -125,8 +132,8 @@ fun ConversationScreen(viewModel: ConversationViewModel, close: () -> Unit, open else -> { ConversationList( + viewModel = viewModel, conversations = conversations, - onDeleteClick = { viewModel.deleteConversation(it.id.toString()) }, modifier = Modifier.padding(innerPadding), openChat = openChat ) @@ -137,11 +144,13 @@ fun ConversationScreen(viewModel: ConversationViewModel, close: () -> Unit, open @Composable private fun ConversationList( + viewModel: ConversationViewModel, conversations: List, - onDeleteClick: (Conversation) -> Unit, modifier: Modifier = Modifier, openChat: (Long) -> Unit ) { + var selectedConversationId by remember { mutableLongStateOf(-1L) } + LazyColumn( modifier = modifier .fillMaxSize() @@ -154,71 +163,60 @@ private fun ConversationList( onClick = { openChat(conversation.id) }, - onDeleteClick = { onDeleteClick(conversation) } + onLongPressed = { + selectedConversationId = conversation.id + } ) - Spacer(modifier = Modifier.height(8.dp)) + Spacer(modifier = Modifier.height(4.dp)) } } -} -@Composable -private fun ConversationListItem(conversation: Conversation, onClick: () -> Unit, onDeleteClick: () -> Unit) { - Box(modifier = Modifier.fillMaxWidth(), contentAlignment = Alignment.BottomCenter) { - FilledTonalButton( - onClick = onClick, - shape = RoundedCornerShape(8.dp), - contentPadding = PaddingValues(vertical = 12.dp, horizontal = 16.dp) - ) { - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier - .height(BUTTON_HEIGHT) + if (selectedConversationId != -1L) { + val bottomSheetAction = listOf( + Triple( + R.drawable.ic_delete, + R.string.conversation_screen_delete_button_title ) { - Text( - text = conversation.titleRepresentation(), - style = MaterialTheme.typography.bodyLarge - ) - - Spacer(modifier = Modifier.weight(1f)) - - IconButton( - onClick = onDeleteClick - ) { - Icon( - imageVector = Icons.Outlined.Delete, - contentDescription = "Delete conversation" - ) - } + viewModel.deleteConversation(selectedConversationId.toString()) + selectedConversationId = -1L } - } + ) + + MoreActionsBottomSheet( + actions = bottomSheetAction, + dismiss = { selectedConversationId = -1L } + ) } } @Composable -private fun CreateConversationButton(onClick: () -> Unit) { - Box(modifier = Modifier.fillMaxWidth(), contentAlignment = Alignment.BottomCenter) { - OutlinedButton( - onClick = onClick, - shape = RoundedCornerShape(8.dp), - contentPadding = PaddingValues(vertical = 12.dp, horizontal = 16.dp) +private fun ConversationListItem( + conversation: Conversation, + onClick: () -> Unit, + onLongPressed: () -> Unit +) { + Surface( + modifier = Modifier + .fillMaxWidth() + .combinedClickable( + onClick = onClick, + onLongClick = onLongPressed + ), + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 8.dp, vertical = 6.dp), + verticalAlignment = Alignment.CenterVertically ) { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.Center, - modifier = Modifier - .fillMaxWidth(0.8f) - .height(BUTTON_HEIGHT) - ) { - Text( - text = stringResource(R.string.conversation_screen_create_button_title), - style = MaterialTheme.typography.bodyLarge - ) - Spacer(modifier = Modifier.width(8.dp)) - Icon( - imageVector = Icons.Default.Add, - contentDescription = "Add conversation" - ) - } + Text( + text = conversation.titleRepresentation(), + style = MaterialTheme.typography.bodyLarge.copy(fontSize = 16.sp), + maxLines = 1, + overflow = TextOverflow.Ellipsis, + color = colorResource(R.color.text_color), + modifier = Modifier.weight(1f) + ) } } } @@ -238,7 +236,5 @@ private fun ConversationListPreview() { }) Spacer(modifier = Modifier.height(8.dp)) - - CreateConversationButton { } } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 16890d084880..3e561b81c2d1 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -79,7 +79,8 @@ No conversations found Failed to create conversation Failed to delete conversation - New conversation + Delete conversation + Output shown here is generated by AI. Make sure to always double-check. From db5fd624a4ec252f6b29f7da03a4940f3238475d Mon Sep 17 00:00:00 2001 From: alperozturk Date: Thu, 13 Nov 2025 14:50:17 +0100 Subject: [PATCH 18/29] fix delete Signed-off-by: alperozturk --- .../client/assistant/conversation/ConversationScreen.kt | 7 ++++--- .../client/assistant/conversation/ConversationViewModel.kt | 2 ++ 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/nextcloud/client/assistant/conversation/ConversationScreen.kt b/app/src/main/java/com/nextcloud/client/assistant/conversation/ConversationScreen.kt index 3d9bd03cd230..280e5ef180ed 100644 --- a/app/src/main/java/com/nextcloud/client/assistant/conversation/ConversationScreen.kt +++ b/app/src/main/java/com/nextcloud/client/assistant/conversation/ConversationScreen.kt @@ -57,8 +57,6 @@ import com.nextcloud.ui.composeComponents.bottomSheet.MoreActionsBottomSheet import com.owncloud.android.R import com.owncloud.android.lib.resources.assistant.chat.model.Conversation -private val BUTTON_HEIGHT = 32.dp - @OptIn(ExperimentalMaterial3Api::class) @Suppress("LongMethod") @Composable @@ -172,12 +170,15 @@ private fun ConversationList( } if (selectedConversationId != -1L) { + val currentId = selectedConversationId + val bottomSheetAction = listOf( Triple( R.drawable.ic_delete, R.string.conversation_screen_delete_button_title ) { - viewModel.deleteConversation(selectedConversationId.toString()) + val sessionId: String = currentId.toString() + viewModel.deleteConversation(sessionId) selectedConversationId = -1L } ) diff --git a/app/src/main/java/com/nextcloud/client/assistant/conversation/ConversationViewModel.kt b/app/src/main/java/com/nextcloud/client/assistant/conversation/ConversationViewModel.kt index c2e290c8fde0..78891fcfc6c1 100644 --- a/app/src/main/java/com/nextcloud/client/assistant/conversation/ConversationViewModel.kt +++ b/app/src/main/java/com/nextcloud/client/assistant/conversation/ConversationViewModel.kt @@ -13,6 +13,7 @@ import com.nextcloud.client.assistant.conversation.model.ConversationScreenState import com.nextcloud.client.assistant.conversation.repository.ConversationRemoteRepository import com.nextcloud.utils.TimeConstants.MILLIS_PER_SECOND import com.owncloud.android.R +import com.owncloud.android.lib.common.utils.Log_OC import com.owncloud.android.lib.resources.assistant.chat.model.Conversation import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow @@ -79,6 +80,7 @@ class ConversationViewModel(private val remoteRepository: ConversationRemoteRepo } fun deleteConversation(sessionId: String) { + Log_OC.d("","BBBB: $sessionId") viewModelScope.launch(Dispatchers.IO) { val success = remoteRepository.deleteConversation(sessionId) if (success) { From 5ef90dbf70f1b0f8ac6c3c135bd0b6048d761b40 Mon Sep 17 00:00:00 2001 From: alperozturk Date: Thu, 13 Nov 2025 14:51:01 +0100 Subject: [PATCH 19/29] fix codacy Signed-off-by: alperozturk --- .../client/assistant/conversation/ConversationScreen.kt | 8 ++------ .../assistant/conversation/ConversationViewModel.kt | 2 +- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/com/nextcloud/client/assistant/conversation/ConversationScreen.kt b/app/src/main/java/com/nextcloud/client/assistant/conversation/ConversationScreen.kt index 280e5ef180ed..9001b940c063 100644 --- a/app/src/main/java/com/nextcloud/client/assistant/conversation/ConversationScreen.kt +++ b/app/src/main/java/com/nextcloud/client/assistant/conversation/ConversationScreen.kt @@ -191,18 +191,14 @@ private fun ConversationList( } @Composable -private fun ConversationListItem( - conversation: Conversation, - onClick: () -> Unit, - onLongPressed: () -> Unit -) { +private fun ConversationListItem(conversation: Conversation, onClick: () -> Unit, onLongPressed: () -> Unit) { Surface( modifier = Modifier .fillMaxWidth() .combinedClickable( onClick = onClick, onLongClick = onLongPressed - ), + ) ) { Row( modifier = Modifier diff --git a/app/src/main/java/com/nextcloud/client/assistant/conversation/ConversationViewModel.kt b/app/src/main/java/com/nextcloud/client/assistant/conversation/ConversationViewModel.kt index 78891fcfc6c1..7190d41e8cb1 100644 --- a/app/src/main/java/com/nextcloud/client/assistant/conversation/ConversationViewModel.kt +++ b/app/src/main/java/com/nextcloud/client/assistant/conversation/ConversationViewModel.kt @@ -80,7 +80,7 @@ class ConversationViewModel(private val remoteRepository: ConversationRemoteRepo } fun deleteConversation(sessionId: String) { - Log_OC.d("","BBBB: $sessionId") + Log_OC.d("", "BBBB: $sessionId") viewModelScope.launch(Dispatchers.IO) { val success = remoteRepository.deleteConversation(sessionId) if (success) { From f6cbd48dad8f748f8d5767a61defefb9c62d3004 Mon Sep 17 00:00:00 2001 From: alperozturk Date: Fri, 14 Nov 2025 12:00:31 +0100 Subject: [PATCH 20/29] update ui Signed-off-by: alperozturk --- .../nextcloud/client/assistant/AssistantScreen.kt | 2 +- .../client/assistant/chat/ChatContent.kt | 6 +++--- .../assistant/conversation/ConversationScreen.kt | 12 +++++++----- .../conversation/ConversationViewModel.kt | 3 ++- .../client/assistant/taskTypes/TaskTypesRow.kt | 2 +- .../main/res/drawable/ic_history_back_arrow.xml | 5 +++++ app/src/main/res/drawable/ic_menu_open.xml | 15 --------------- 7 files changed, 19 insertions(+), 26 deletions(-) create mode 100644 app/src/main/res/drawable/ic_history_back_arrow.xml delete mode 100644 app/src/main/res/drawable/ic_menu_open.xml diff --git a/app/src/main/java/com/nextcloud/client/assistant/AssistantScreen.kt b/app/src/main/java/com/nextcloud/client/assistant/AssistantScreen.kt index dcc2924184ba..d82f71d2e600 100644 --- a/app/src/main/java/com/nextcloud/client/assistant/AssistantScreen.kt +++ b/app/src/main/java/com/nextcloud/client/assistant/AssistantScreen.kt @@ -386,7 +386,7 @@ private fun EmptyContent(paddingValues: PaddingValues, iconId: Int?, description titleId?.let { Text( text = stringResource(titleId), - fontSize = MaterialTheme.typography.bodyMedium.fontSize, + fontSize = MaterialTheme.typography.headlineSmall.fontSize, textAlign = TextAlign.Center, color = colorResource(R.color.text_color) ) diff --git a/app/src/main/java/com/nextcloud/client/assistant/chat/ChatContent.kt b/app/src/main/java/com/nextcloud/client/assistant/chat/ChatContent.kt index 70c685d45a72..930334ae8d4f 100644 --- a/app/src/main/java/com/nextcloud/client/assistant/chat/ChatContent.kt +++ b/app/src/main/java/com/nextcloud/client/assistant/chat/ChatContent.kt @@ -123,7 +123,7 @@ private fun AssistantTypingIndicator() { bottomEnd = CHAT_BUBBLE_CORNER_RADIUS ) ) - .background(color = colorResource(R.color.task_container)) + .background(color = colorResource(R.color.white)) ) { TypingAnimation() } @@ -228,7 +228,7 @@ private fun AssistantMessageItem(message: ChatMessage) { ) ) .background( - color = colorResource(R.color.task_container) + color = colorResource(R.color.white) ) ) { MessageTextItem(message) @@ -259,7 +259,7 @@ private fun UserMessageItem(message: ChatMessage) { bottomStart = CHAT_BUBBLE_CORNER_RADIUS ) ) - .background(color = colorResource(R.color.task_container)) + .background(color = colorResource(R.color.white)) ) { MessageTextItem(message) } diff --git a/app/src/main/java/com/nextcloud/client/assistant/conversation/ConversationScreen.kt b/app/src/main/java/com/nextcloud/client/assistant/conversation/ConversationScreen.kt index 9001b940c063..d22c5c3e94cb 100644 --- a/app/src/main/java/com/nextcloud/client/assistant/conversation/ConversationScreen.kt +++ b/app/src/main/java/com/nextcloud/client/assistant/conversation/ConversationScreen.kt @@ -22,8 +22,8 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.outlined.ArrowBack import androidx.compose.material.icons.filled.Add -import androidx.compose.material.icons.filled.Close import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.FabPosition @@ -80,17 +80,17 @@ fun ConversationScreen(viewModel: ConversationViewModel, close: () -> Unit, open Scaffold( topBar = { Row(modifier = Modifier.fillMaxWidth()) { - Spacer(modifier = Modifier.weight(1f)) IconButton( onClick = { close() } ) { Icon( - imageVector = Icons.Default.Close, - contentDescription = "close conversations list" + imageVector = Icons.AutoMirrored.Outlined.ArrowBack, + contentDescription = "go back to assistant page" ) } + Spacer(modifier = Modifier.weight(1f)) } }, snackbarHost = { @@ -98,7 +98,9 @@ fun ConversationScreen(viewModel: ConversationViewModel, close: () -> Unit, open }, floatingActionButton = { FloatingActionButton(onClick = { - viewModel.createConversation(null) + viewModel.createConversation(null, onResult = { + openChat(it) + }) }) { Icon(Icons.Filled.Add, "Floating action button.") } diff --git a/app/src/main/java/com/nextcloud/client/assistant/conversation/ConversationViewModel.kt b/app/src/main/java/com/nextcloud/client/assistant/conversation/ConversationViewModel.kt index 7190d41e8cb1..68e4ff6358eb 100644 --- a/app/src/main/java/com/nextcloud/client/assistant/conversation/ConversationViewModel.kt +++ b/app/src/main/java/com/nextcloud/client/assistant/conversation/ConversationViewModel.kt @@ -63,7 +63,7 @@ class ConversationViewModel(private val remoteRepository: ConversationRemoteRepo } } - fun createConversation(title: String?) { + fun createConversation(title: String?, onResult: (Long) -> Unit) { viewModelScope.launch(Dispatchers.IO) { val timestamp = System.currentTimeMillis().div(MILLIS_PER_SECOND) val newConversation = remoteRepository.createConversation(title, timestamp) @@ -71,6 +71,7 @@ class ConversationViewModel(private val remoteRepository: ConversationRemoteRepo _conversations.update { listOf(newConversation.session) + it } + onResult(newConversation.session.id) } else { _errorMessageId.update { R.string.conversation_screen_create_error_title diff --git a/app/src/main/java/com/nextcloud/client/assistant/taskTypes/TaskTypesRow.kt b/app/src/main/java/com/nextcloud/client/assistant/taskTypes/TaskTypesRow.kt index a6bdac6033d3..0f5098120ea7 100644 --- a/app/src/main/java/com/nextcloud/client/assistant/taskTypes/TaskTypesRow.kt +++ b/app/src/main/java/com/nextcloud/client/assistant/taskTypes/TaskTypesRow.kt @@ -52,7 +52,7 @@ fun TaskTypesRow( onClick = { navigateToConversationList() } ) { Icon( - painter = painterResource(id = R.drawable.ic_menu_open), + painter = painterResource(id = R.drawable.ic_history_back_arrow), contentDescription = "open conversation list button", tint = colorResource(R.color.text_color) ) diff --git a/app/src/main/res/drawable/ic_history_back_arrow.xml b/app/src/main/res/drawable/ic_history_back_arrow.xml new file mode 100644 index 000000000000..2fd43ac792d4 --- /dev/null +++ b/app/src/main/res/drawable/ic_history_back_arrow.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_menu_open.xml b/app/src/main/res/drawable/ic_menu_open.xml deleted file mode 100644 index 9db746f5b785..000000000000 --- a/app/src/main/res/drawable/ic_menu_open.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - From c14c11b8fcea57884e41a27b6d2f993a137bab4d Mon Sep 17 00:00:00 2001 From: alperozturk Date: Fri, 14 Nov 2025 12:02:15 +0100 Subject: [PATCH 21/29] update ui Signed-off-by: alperozturk --- .../res/drawable/ic_history_back_arrow.xml | 21 +++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/app/src/main/res/drawable/ic_history_back_arrow.xml b/app/src/main/res/drawable/ic_history_back_arrow.xml index 2fd43ac792d4..5299d0ef2a4e 100644 --- a/app/src/main/res/drawable/ic_history_back_arrow.xml +++ b/app/src/main/res/drawable/ic_history_back_arrow.xml @@ -1,5 +1,18 @@ - - - - + + + + + From 407fc053a76caafae372e131d8c776a9fa812503 Mon Sep 17 00:00:00 2001 From: alperozturk Date: Fri, 14 Nov 2025 12:03:14 +0100 Subject: [PATCH 22/29] update ui Signed-off-by: alperozturk --- .../com/nextcloud/utils/extensions/ChatMessageExtensions.kt | 2 +- gradle/verification-metadata.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/nextcloud/utils/extensions/ChatMessageExtensions.kt b/app/src/main/java/com/nextcloud/utils/extensions/ChatMessageExtensions.kt index 4fe2719ead97..fae3a9d6f7a0 100644 --- a/app/src/main/java/com/nextcloud/utils/extensions/ChatMessageExtensions.kt +++ b/app/src/main/java/com/nextcloud/utils/extensions/ChatMessageExtensions.kt @@ -1,7 +1,7 @@ /* * Nextcloud - Android Client * - * SPDX-FileCopyrightText: 2025 Your Name + * SPDX-FileCopyrightText: 2025 Alper Ozturk * SPDX-License-Identifier: AGPL-3.0-or-later */ diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index 9ff56127c9ef..99f358691c3e 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -2,7 +2,7 @@ From 3c7c60b6c8868d69c915af893122a48f9b715e9e Mon Sep 17 00:00:00 2001 From: alperozturk Date: Fri, 14 Nov 2025 12:03:49 +0100 Subject: [PATCH 23/29] update ui Signed-off-by: alperozturk --- gradle/verification-metadata.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index 99f358691c3e..8fc7d5c961e4 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -2,7 +2,7 @@ From c1bf049abd3f45e10f28c6deb133e2e15cf4ff3f Mon Sep 17 00:00:00 2001 From: alperozturk Date: Fri, 14 Nov 2025 14:42:10 +0100 Subject: [PATCH 24/29] dont allow user doesnt have chat ui feature to open conversation list Signed-off-by: alperozturk --- .../client/assistant/AssistantScreen.kt | 10 ++++++---- .../assistant/conversation/ConversationScreen.kt | 16 ++++++++++++++-- .../utils/extensions/TaskTypeExtensions.kt | 12 ++++++++++++ app/src/main/res/values/strings.xml | 1 + 4 files changed, 33 insertions(+), 6 deletions(-) create mode 100644 app/src/main/java/com/nextcloud/utils/extensions/TaskTypeExtensions.kt diff --git a/app/src/main/java/com/nextcloud/client/assistant/AssistantScreen.kt b/app/src/main/java/com/nextcloud/client/assistant/AssistantScreen.kt index d82f71d2e600..b8b5d43532cb 100644 --- a/app/src/main/java/com/nextcloud/client/assistant/AssistantScreen.kt +++ b/app/src/main/java/com/nextcloud/client/assistant/AssistantScreen.kt @@ -69,6 +69,7 @@ import com.nextcloud.client.assistant.taskTypes.TaskTypesRow import com.nextcloud.ui.composeActivity.ComposeActivity import com.nextcloud.ui.composeComponents.alertDialog.SimpleAlertDialog import com.nextcloud.ui.composeComponents.bottomSheet.MoreActionsBottomSheet +import com.nextcloud.utils.extensions.getChat import com.owncloud.android.R import com.owncloud.android.lib.resources.assistant.v2.model.Task import com.owncloud.android.lib.resources.assistant.v2.model.TaskTypeData @@ -122,7 +123,8 @@ fun AssistantScreen( } HorizontalPager( - state = pagerState + state = pagerState, + userScrollEnabled = taskTypes.getChat() != null ) { page -> when (page) { 0 -> { @@ -132,7 +134,7 @@ fun AssistantScreen( } }, openChat = { newSessionId -> viewModel.initSessionId(newSessionId) - taskTypes?.find { it.isChat() }?.let { chatTaskType -> + taskTypes.getChat()?.let { chatTaskType -> viewModel.selectTaskType(chatTaskType) } scope.launch { @@ -386,7 +388,7 @@ private fun EmptyContent(paddingValues: PaddingValues, iconId: Int?, description titleId?.let { Text( text = stringResource(titleId), - fontSize = MaterialTheme.typography.headlineSmall.fontSize, + style = MaterialTheme.typography.headlineSmall, textAlign = TextAlign.Center, color = colorResource(R.color.text_color) ) @@ -396,7 +398,7 @@ private fun EmptyContent(paddingValues: PaddingValues, iconId: Int?, description descriptionId?.let { Text( text = stringResource(descriptionId), - fontSize = MaterialTheme.typography.bodyMedium.fontSize, + style = MaterialTheme.typography.bodyMedium, textAlign = TextAlign.Center, color = colorResource(R.color.text_color) ) diff --git a/app/src/main/java/com/nextcloud/client/assistant/conversation/ConversationScreen.kt b/app/src/main/java/com/nextcloud/client/assistant/conversation/ConversationScreen.kt index d22c5c3e94cb..0a9a3550b548 100644 --- a/app/src/main/java/com/nextcloud/client/assistant/conversation/ConversationScreen.kt +++ b/app/src/main/java/com/nextcloud/client/assistant/conversation/ConversationScreen.kt @@ -19,6 +19,7 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.material.icons.Icons @@ -48,6 +49,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.colorResource import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @@ -79,7 +81,7 @@ fun ConversationScreen(viewModel: ConversationViewModel, close: () -> Unit, open Scaffold( topBar = { - Row(modifier = Modifier.fillMaxWidth()) { + Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) { IconButton( onClick = { close() @@ -90,6 +92,12 @@ fun ConversationScreen(viewModel: ConversationViewModel, close: () -> Unit, open contentDescription = "go back to assistant page" ) } + Spacer(modifier = Modifier.width(8.dp)) + Text( + text = stringResource(R.string.conversation_screen_title), + textAlign = TextAlign.Center, + style = MaterialTheme.typography.headlineSmall + ) Spacer(modifier = Modifier.weight(1f)) } }, @@ -126,7 +134,11 @@ fun ConversationScreen(viewModel: ConversationViewModel, close: () -> Unit, open .padding(innerPadding), contentAlignment = Alignment.Center ) { - Text(stringResource(R.string.conversation_screen_empty_content_title)) + Text( + stringResource(R.string.conversation_screen_empty_content_title), + textAlign = TextAlign.Center, + style = MaterialTheme.typography.headlineSmall + ) } } diff --git a/app/src/main/java/com/nextcloud/utils/extensions/TaskTypeExtensions.kt b/app/src/main/java/com/nextcloud/utils/extensions/TaskTypeExtensions.kt new file mode 100644 index 000000000000..5fca6d0ab98e --- /dev/null +++ b/app/src/main/java/com/nextcloud/utils/extensions/TaskTypeExtensions.kt @@ -0,0 +1,12 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2025 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nextcloud.utils.extensions + +import com.owncloud.android.lib.resources.assistant.v2.model.TaskTypeData + +fun List?.getChat(): TaskTypeData? = this?.find { it.isChat() } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 3e561b81c2d1..13dfcaf51613 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -74,6 +74,7 @@ Output + Conversations No conversations yet Failed to fetch conversation list No conversations found From 3127afa17e06ed4375bb9b48cab8d1a5bc5861c5 Mon Sep 17 00:00:00 2001 From: alperozturk Date: Wed, 19 Nov 2025 08:32:53 +0100 Subject: [PATCH 25/29] hide history button is chat task type does not exists Signed-off-by: alperozturk --- .../assistant/taskTypes/TaskTypesRow.kt | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/com/nextcloud/client/assistant/taskTypes/TaskTypesRow.kt b/app/src/main/java/com/nextcloud/client/assistant/taskTypes/TaskTypesRow.kt index 0f5098120ea7..f1af94510c3e 100644 --- a/app/src/main/java/com/nextcloud/client/assistant/taskTypes/TaskTypesRow.kt +++ b/app/src/main/java/com/nextcloud/client/assistant/taskTypes/TaskTypesRow.kt @@ -46,16 +46,18 @@ fun TaskTypesRow( modifier = Modifier.background(color = colorResource(R.color.actionbar_color)), horizontalArrangement = Arrangement.Center ) { - Spacer(modifier = Modifier.width(11.dp)) + if (data.any { it.isChat() }) { + Spacer(modifier = Modifier.width(11.dp)) - IconButton( - onClick = { navigateToConversationList() } - ) { - Icon( - painter = painterResource(id = R.drawable.ic_history_back_arrow), - contentDescription = "open conversation list button", - tint = colorResource(R.color.text_color) - ) + IconButton( + onClick = { navigateToConversationList() } + ) { + Icon( + painter = painterResource(id = R.drawable.ic_history_back_arrow), + contentDescription = "open conversation list button", + tint = colorResource(R.color.text_color) + ) + } } PrimaryScrollableTabRow( From cbcc2d672f065e874b0549ee82d69829adc8d23a Mon Sep 17 00:00:00 2001 From: alperozturk Date: Fri, 28 Nov 2025 09:15:35 +0100 Subject: [PATCH 26/29] upgrade lib version Signed-off-by: alperozturk --- app/src/main/res/values/strings.xml | 1 - gradle/libs.versions.toml | 2 +- gradle/verification-metadata.xml | 18 ++++++++++++++++++ 3 files changed, 19 insertions(+), 2 deletions(-) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 13dfcaf51613..34c2d4f5ecc6 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -59,7 +59,6 @@ The task output is not ready yet. Failed to send a message Failed to fetch chat messages - Unable to fetch task types, please check your internet connection. Unable to fetch task list, please check your internet connection. Task list is empty. Check assistant app configuration. Delete task diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index bca609fb220b..4f9f40608808 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -5,7 +5,7 @@ androidCommonLibraryVersion = "0.29.0" androidGifDrawableVersion = "1.2.29" androidImageCropperVersion = "4.6.0" -androidLibraryVersion = "d030d521b4" +androidLibraryVersion = "b8f77935157e44c1d7a71f81271b412b0dbe8c76" androidPluginVersion = '8.13.1' androidsvgVersion = "1.4" androidxMediaVersion = "1.5.1" diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index 8fc7d5c961e4..30ef7812b678 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -19,6 +19,7 @@ + @@ -411,6 +412,7 @@ + @@ -18117,6 +18119,14 @@ + + + + + + + + @@ -22501,6 +22511,14 @@ + + + + + + + + From c6818310c44536935b1e5281843af160bb71afc0 Mon Sep 17 00:00:00 2001 From: alperozturk Date: Fri, 28 Nov 2025 09:20:49 +0100 Subject: [PATCH 27/29] fix codacy Signed-off-by: alperozturk --- app/src/main/java/com/owncloud/android/utils/PermissionUtil.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/java/com/owncloud/android/utils/PermissionUtil.kt b/app/src/main/java/com/owncloud/android/utils/PermissionUtil.kt index b69e94d5a1ff..a6e35ee36019 100644 --- a/app/src/main/java/com/owncloud/android/utils/PermissionUtil.kt +++ b/app/src/main/java/com/owncloud/android/utils/PermissionUtil.kt @@ -161,6 +161,7 @@ object PermissionUtil { } // region Storage permission checks + /** * Checks if the application has storage/media access permissions. * From 8265b7737aa0c3197e80e5480ba21e13afdcccfa Mon Sep 17 00:00:00 2001 From: alperozturk Date: Fri, 28 Nov 2025 10:25:13 +0100 Subject: [PATCH 28/29] fix codacy Signed-off-by: alperozturk --- app/src/main/res/values/strings.xml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 34c2d4f5ecc6..9281e6f74d74 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -68,7 +68,6 @@ An error occurred while creating the task Task successfully deleted An error occurred while deleting the task - Type some text Input Output @@ -493,7 +492,6 @@ Firstname Lastname Filetype Download - Internal share link only works for users with access to this folder Internal share link only works for users with access to this folder All 12 words together make a very strong password, letting only you view and make use of your encrypted files. Please write it down and keep it somewhere safe. No internet connection @@ -717,7 +715,6 @@ Share link Send link Set password - Share with… Unset Name, Federated Cloud ID or email address… @@ -736,7 +733,6 @@ Settings, database and server certificates from %1$s\'s data will be deleted permanently. \n\nDownloaded files will be kept untouched.\n\nThis process can take a while. Clear data - Additional permissions required to upload and download files. File not found in local file system Do you really want to delete the selected items? Do you really want to delete the selected items and their contents? @@ -1166,7 +1162,6 @@ Failed to start editor Add folder description Adds folder description - Retry to upload failed local files We couldnt locate the file on server. Another user may have deleted the file File not found. Are you sure that this file exists or has a previous conflict not been resolved? File upload conflict From a37a94a2eb7007160335b99015eadb5639739972 Mon Sep 17 00:00:00 2001 From: alperozturk Date: Fri, 28 Nov 2025 10:32:14 +0100 Subject: [PATCH 29/29] fix lint errors Signed-off-by: alperozturk --- .../activity_list_item_header_shimmer.xml | 14 ----- .../res/layout/file_details_share_group.xml | 55 ------------------- .../main/res/layout/fragment_compose_view.xml | 18 ------ app/src/main/res/layout/note_dialog.xml | 36 ------------ .../res/layout/search_users_groups_layout.xml | 44 --------------- app/src/main/res/values/dims.xml | 2 - 6 files changed, 169 deletions(-) delete mode 100644 app/src/main/res/layout/activity_list_item_header_shimmer.xml delete mode 100644 app/src/main/res/layout/file_details_share_group.xml delete mode 100644 app/src/main/res/layout/fragment_compose_view.xml delete mode 100644 app/src/main/res/layout/note_dialog.xml delete mode 100644 app/src/main/res/layout/search_users_groups_layout.xml diff --git a/app/src/main/res/layout/activity_list_item_header_shimmer.xml b/app/src/main/res/layout/activity_list_item_header_shimmer.xml deleted file mode 100644 index fdadb983ad0b..000000000000 --- a/app/src/main/res/layout/activity_list_item_header_shimmer.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - diff --git a/app/src/main/res/layout/file_details_share_group.xml b/app/src/main/res/layout/file_details_share_group.xml deleted file mode 100644 index 075e5ba65cab..000000000000 --- a/app/src/main/res/layout/file_details_share_group.xml +++ /dev/null @@ -1,55 +0,0 @@ - - - - - - - - - - - - - - diff --git a/app/src/main/res/layout/fragment_compose_view.xml b/app/src/main/res/layout/fragment_compose_view.xml deleted file mode 100644 index 0e8d25ea1819..000000000000 --- a/app/src/main/res/layout/fragment_compose_view.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/note_dialog.xml b/app/src/main/res/layout/note_dialog.xml deleted file mode 100644 index 6fac84886bf6..000000000000 --- a/app/src/main/res/layout/note_dialog.xml +++ /dev/null @@ -1,36 +0,0 @@ - - - - - - - - - - - diff --git a/app/src/main/res/layout/search_users_groups_layout.xml b/app/src/main/res/layout/search_users_groups_layout.xml deleted file mode 100644 index f0b30be55872..000000000000 --- a/app/src/main/res/layout/search_users_groups_layout.xml +++ /dev/null @@ -1,44 +0,0 @@ - - - - - - - - - - - diff --git a/app/src/main/res/values/dims.xml b/app/src/main/res/values/dims.xml index 1394d2559fda..b4f8d33b8ebb 100644 --- a/app/src/main/res/values/dims.xml +++ b/app/src/main/res/values/dims.xml @@ -91,8 +91,6 @@ 40dp 240dp 16sp - 200dp - 20dp 12sp 20dp 60dp